feat: add auth service with local bcrypt password authentication
This commit is contained in:
parent
e72e4ec466
commit
f93793a1d8
18
app/modules/auth/schemas.py
Normal file
18
app/modules/auth/schemas.py
Normal file
@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
full_name: str
|
||||
email: str | None
|
||||
department: str | None
|
||||
role: str | None
|
||||
is_active: bool
|
||||
is_admin: bool
|
||||
last_login: datetime | None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
45
app/modules/auth/service.py
Normal file
45
app/modules/auth/service.py
Normal file
@ -0,0 +1,45 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from passlib.context import CryptContext
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.auth.models import User
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def hash_password(plain: str) -> str:
|
||||
return pwd_context.hash(plain)
|
||||
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
return pwd_context.verify(plain, hashed)
|
||||
|
||||
|
||||
def get_user(db: Session, username: str) -> Optional[User]:
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
|
||||
def authenticate_user(
|
||||
db: Session, username: str, password: str, ldap_enabled: bool
|
||||
) -> Optional[User]:
|
||||
user = get_user(db, username)
|
||||
if user is None or not user.is_active:
|
||||
return None
|
||||
|
||||
local_ok = user.pw_hash is not None and verify_password(password, user.pw_hash)
|
||||
if local_ok:
|
||||
_touch_last_login(db, user)
|
||||
return user
|
||||
|
||||
if ldap_enabled:
|
||||
# LDAP auth implemented in Part 2
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _touch_last_login(db: Session, user: User) -> None:
|
||||
user.last_login = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
69
tests/test_auth_service.py
Normal file
69
tests/test_auth_service.py
Normal file
@ -0,0 +1,69 @@
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.database import Base
|
||||
from app.modules.auth.models import User
|
||||
from app.modules.auth.service import authenticate_user, get_user, hash_password, verify_password
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db():
|
||||
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||
Base.metadata.create_all(bind=engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def alice(db):
|
||||
user = User(username="alice", full_name="Alice Smith", pw_hash=hash_password("secret123"))
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
def test_hash_and_verify_password():
|
||||
hashed = hash_password("mypassword")
|
||||
assert verify_password("mypassword", hashed) is True
|
||||
assert verify_password("wrong", hashed) is False
|
||||
|
||||
|
||||
def test_get_user_found(db, alice):
|
||||
assert get_user(db, "alice") is not None
|
||||
|
||||
|
||||
def test_get_user_not_found(db):
|
||||
assert get_user(db, "nobody") is None
|
||||
|
||||
|
||||
def test_authenticate_correct_password(db, alice):
|
||||
user = authenticate_user(db, "alice", "secret123", ldap_enabled=False)
|
||||
assert user is not None
|
||||
assert user.last_login is not None
|
||||
|
||||
|
||||
def test_authenticate_wrong_password(db, alice):
|
||||
assert authenticate_user(db, "alice", "wrong", ldap_enabled=False) is None
|
||||
|
||||
|
||||
def test_authenticate_unknown_user(db):
|
||||
assert authenticate_user(db, "ghost", "any", ldap_enabled=False) is None
|
||||
|
||||
|
||||
def test_authenticate_inactive_user(db):
|
||||
user = User(username="inactive", full_name="X", pw_hash=hash_password("pw"), is_active=False)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
assert authenticate_user(db, "inactive", "pw", ldap_enabled=False) is None
|
||||
|
||||
|
||||
def test_authenticate_no_pw_hash_no_ldap(db):
|
||||
user = User(username="ldaponly", full_name="Y", pw_hash=None)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
assert authenticate_user(db, "ldaponly", "any", ldap_enabled=False) is None
|
||||
Loading…
x
Reference in New Issue
Block a user