Files
loogle-scripts/services/loogle-mcp/app/auth.py
T

147 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
"""Autenticazione utenti famiglia — pattern Loogle Casa."""
import base64
import datetime
import hashlib
import hmac
import os
import secrets
import time
from typing import Optional
from fastapi import HTTPException, Request
from .db import get_conn
SESSION_DAYS = 30
PBKDF2_ITER = 240_000
MAX_ATTEMPTS = 8
WINDOW_S = 600
_attempts: dict = {}
FAMILY_USERS = (
("daniele", True),
("lucia", False),
("davide", False),
("luca", False),
)
DEFAULT_SCOPES = (
"context:read context:write knowledge:read knowledge:write gitea:read gitea:write"
)
def hash_password(password: str) -> str:
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, PBKDF2_ITER)
return "pbkdf2$%d$%s$%s" % (
PBKDF2_ITER,
base64.b64encode(salt).decode(),
base64.b64encode(dk).decode(),
)
def verify_password(password: str, stored: str) -> bool:
try:
_, iters, salt_b64, dk_b64 = stored.split("$")
salt = base64.b64decode(salt_b64)
expected = base64.b64decode(dk_b64)
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, int(iters))
return hmac.compare_digest(dk, expected)
except Exception:
return False
def ensure_family_users() -> None:
conn = get_conn()
for username, is_admin in FAMILY_USERS:
row = conn.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone()
if row:
continue
conn.execute(
"INSERT INTO users(username,password_hash,is_admin,must_change_password)"
" VALUES (?,?,?,1)",
(username, hash_password(username), 1 if is_admin else 0),
)
conn.commit()
def throttle(ip: str) -> None:
now = time.time()
hist = [t for t in _attempts.get(ip, []) if now - t < WINDOW_S]
_attempts[ip] = hist
if len(hist) >= MAX_ATTEMPTS:
raise HTTPException(429, "Troppi tentativi: riprova tra qualche minuto")
def record_attempt(ip: str) -> None:
_attempts.setdefault(ip, []).append(time.time())
def authenticate(username: str, password: str) -> Optional[dict]:
conn = get_conn()
row = conn.execute("SELECT * FROM users WHERE username=?", (username.strip(),)).fetchone()
if not row or not verify_password(password, row["password_hash"]):
return None
return dict(row)
def get_user_by_id(user_id: int) -> Optional[dict]:
row = get_conn().execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone()
return dict(row) if row else None
def get_user_by_username(username: str) -> Optional[dict]:
row = get_conn().execute("SELECT * FROM users WHERE username=?", (username,)).fetchone()
return dict(row) if row else None
def change_password(user_id: int, old_password: str, new_password: str) -> bool:
row = get_conn().execute("SELECT password_hash FROM users WHERE id=?", (user_id,)).fetchone()
if not row or not verify_password(old_password, row["password_hash"]):
return False
conn = get_conn()
conn.execute(
"UPDATE users SET password_hash=?, must_change_password=0 WHERE id=?",
(hash_password(new_password), user_id),
)
conn.commit()
return True
def current_user_from_cookie(request: Request) -> dict:
token = request.cookies.get("mcp_session", "")
if not token:
raise HTTPException(401, "Non autenticato")
row = get_conn().execute(
"SELECT u.id,u.username,u.is_admin,u.must_change_password"
" FROM sessions s JOIN users u ON u.id=s.user_id"
" WHERE s.token=? AND s.expires_at > datetime('now')",
(token,),
).fetchone()
if not row:
raise HTTPException(401, "Sessione scaduta")
return dict(row)
def require_admin(request: Request) -> dict:
user = current_user_from_cookie(request)
if not user["is_admin"]:
raise HTTPException(403, "Riservato all'amministratore")
return user
def ensure_sessions_table() -> None:
get_conn().execute(
"""
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT NOT NULL
)
"""
)
get_conn().commit()