first commit
This commit is contained in:
107
backend/auth.py
Normal file
107
backend/auth.py
Normal file
@ -0,0 +1,107 @@
|
||||
import datetime as dt
|
||||
import grp
|
||||
import pwd
|
||||
from typing import List, Set
|
||||
|
||||
import jwt
|
||||
import pam
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
|
||||
from backend.settings import Settings, get_settings
|
||||
|
||||
|
||||
def _is_member_of(username: str, groups: Set[str]) -> bool:
|
||||
if not groups:
|
||||
return False
|
||||
try:
|
||||
user_entry = pwd.getpwnam(username)
|
||||
except KeyError:
|
||||
return False
|
||||
user_groups = {g.gr_name for g in grp.getgrall() if username in g.gr_mem}
|
||||
primary_group = grp.getgrgid(user_entry.pw_gid).gr_name
|
||||
user_groups.add(primary_group)
|
||||
return bool(user_groups & groups)
|
||||
|
||||
|
||||
def is_authorized_admin(username: str, settings: Settings) -> bool:
|
||||
# UID 0 always allowed
|
||||
try:
|
||||
entry = pwd.getpwnam(username)
|
||||
except KeyError:
|
||||
return False
|
||||
if entry.pw_uid == 0:
|
||||
return True
|
||||
allowed_users = set(settings.auth_allowed_users)
|
||||
allowed_groups = set(settings.auth_allowed_groups)
|
||||
if username in allowed_users:
|
||||
return True
|
||||
if _is_member_of(username, allowed_groups):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def authenticate_admin_user(username: str, password: str, settings: Settings) -> None:
|
||||
if not is_authorized_admin(username, settings):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User not authorized to log in",
|
||||
)
|
||||
pam_client = pam.pam()
|
||||
if not pam_client.authenticate(username, password, service=settings.auth_pam_service):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid credentials",
|
||||
)
|
||||
|
||||
|
||||
def issue_token(username: str, settings: Settings) -> str:
|
||||
payload = {
|
||||
"sub": username,
|
||||
"exp": dt.datetime.utcnow() + dt.timedelta(seconds=settings.token_ttl_seconds),
|
||||
"iat": dt.datetime.utcnow(),
|
||||
}
|
||||
return jwt.encode(payload, settings.auth_secret, algorithm="HS256")
|
||||
|
||||
|
||||
def decode_token(token: str, settings: Settings) -> str:
|
||||
try:
|
||||
payload = jwt.decode(token, settings.auth_secret, algorithms=["HS256"])
|
||||
except jwt.ExpiredSignatureError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired") from exc
|
||||
except jwt.PyJWTError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from exc
|
||||
|
||||
username = payload.get("sub")
|
||||
if not username or not is_authorized_admin(username, settings):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized user")
|
||||
return username
|
||||
|
||||
|
||||
def get_current_admin(
|
||||
request: Request,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> str:
|
||||
auth_header = request.headers.get("authorization")
|
||||
if not auth_header or not auth_header.lower().startswith("bearer "):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing token")
|
||||
token = auth_header.split(" ", 1)[1].strip()
|
||||
return decode_token(token, settings)
|
||||
|
||||
|
||||
def list_manageable_users(settings: Settings) -> List[str]:
|
||||
"""List system users we are allowed to manage (uid >= 1000, real shells, optional allowlist)."""
|
||||
allowed = set(settings.allowed_users) if settings.allowed_users else None
|
||||
candidates: List[str] = []
|
||||
for entry in pwd.getpwall():
|
||||
if entry.pw_uid == 0:
|
||||
# Never operate on root accounts
|
||||
continue
|
||||
if entry.pw_uid < 1000:
|
||||
continue
|
||||
if entry.pw_shell in ("/usr/sbin/nologin", "/bin/false"):
|
||||
continue
|
||||
if allowed is not None and entry.pw_name not in allowed:
|
||||
continue
|
||||
candidates.append(entry.pw_name)
|
||||
candidates.sort()
|
||||
return candidates
|
||||
Reference in New Issue
Block a user