- Tables forge_connections/users/workspaces (+ membres, projets, états, audit) ; migration des anciennes bases avec portées (workspace_id, connection_id) par défaut (1, 1). - Jetons chiffrés Fernet (KANBANTE_MASTER_KEY ou clé générée en base), jamais réaffichés. - ForgeRegistry : connexion héritée créée depuis l'environnement, espace Principal, instantanés préfixés par connexion. - GitHubClient complet (Bearer, pagination Link, étiquettes par nom, PATCH archived, branches via git/refs). - Paramètres surchargeables en base (résolveur config, P3).
69 lines
2.0 KiB
Python
69 lines
2.0 KiB
Python
"""User accounts: password hashing and bootstrap (0.24.0, P2).
|
|
|
|
Passwords are hashed with PBKDF2-HMAC-SHA256 (stdlib) with a
|
|
per-user salt; the stored string carries its parameters
|
|
("pbkdf2$<iterations>$<salt_b64>$<hash_b64>") so the cost can
|
|
be raised later without invalidating existing hashes.
|
|
"""
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import secrets
|
|
|
|
from app import models
|
|
|
|
_ITERATIONS = 260_000
|
|
|
|
|
|
def hash_password(password: str) -> str:
|
|
salt = secrets.token_bytes(16)
|
|
digest = hashlib.pbkdf2_hmac(
|
|
"sha256", password.encode("utf-8"), salt, _ITERATIONS
|
|
)
|
|
return "pbkdf2${}${}${}".format(
|
|
_ITERATIONS,
|
|
base64.b64encode(salt).decode("ascii"),
|
|
base64.b64encode(digest).decode("ascii"),
|
|
)
|
|
|
|
|
|
def verify_password(password: str, stored: str) -> bool:
|
|
try:
|
|
scheme, iterations, salt_b64, hash_b64 = stored.split("$")
|
|
if scheme != "pbkdf2":
|
|
return False
|
|
salt = base64.b64decode(salt_b64)
|
|
expected = base64.b64decode(hash_b64)
|
|
digest = hashlib.pbkdf2_hmac(
|
|
"sha256", password.encode("utf-8"), salt, int(iterations)
|
|
)
|
|
return hmac.compare_digest(digest, expected)
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def ensure_bootstrap_admin() -> dict | None:
|
|
"""Create the first admin from the env credentials.
|
|
|
|
Runs when the users table is empty and KANBANTE_AUTH_USER /
|
|
_PASSWORD are configured (decision D6): Bruno's existing
|
|
login becomes the initial database admin, with no lockout.
|
|
The env path keeps working as an emergency fallback either
|
|
way (see app.main auth).
|
|
"""
|
|
from app.config import get_settings
|
|
|
|
if models.count_users() > 0:
|
|
return None
|
|
settings = get_settings()
|
|
if not settings.auth_enabled:
|
|
return None
|
|
user_id = models.create_user(
|
|
settings.auth_user,
|
|
hash_password(settings.auth_password),
|
|
role="admin",
|
|
display_name=settings.auth_user,
|
|
)
|
|
return models.get_user(user_id)
|