feat(forge): fondation multi-forges — connexions chiffrées, registre, client GitHub (P0/P1)
- 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).
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user