Catalyst / admin/Syanpse-Vanguard 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Syanpse-Vanguard

public
Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Syanpse-Vanguard / synapse-vanguard-v3 / sv / auth / users.py 3212 B · main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
"""Local user store + password hashing. Federation (OIDC/SAML) will map external
identities onto these same rows/roles in a later milestone."""
from __future__ import annotations

import uuid
from datetime import datetime, timezone

import bcrypt

from sv.auth.rbac import Role
from sv.config import DEFAULT_TENANT_ID, settings
from sv.storage.base import Query, StorageEngine


def hash_password(plaintext: str) -> str:
    return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt()).decode()


def verify_password(plaintext: str, hashed: str) -> bool:
    try:
        return bcrypt.checkpw(plaintext.encode(), hashed.encode())
    except ValueError:
        return False


async def get_user_by_username(engine: StorageEngine, username: str) -> dict | None:
    return await engine.fetchone(Query(
        "SELECT id, tenant_id, username, password_hash, role, disabled "
        "FROM users WHERE username = ?", [username],
    ))


async def create_user(engine: StorageEngine, username: str, password: str,
                      role: Role, tenant_id=DEFAULT_TENANT_ID) -> str:
    uid = str(uuid.uuid4())
    await engine.run(Query(
        "INSERT INTO users (id, tenant_id, username, password_hash, role, "
        "disabled, created_ts) VALUES (?, ?, ?, ?, ?, FALSE, ?)",
        [uid, str(tenant_id), username, hash_password(password), role.value,
         datetime.now(timezone.utc).replace(tzinfo=None)],
    ))
    return uid


async def list_users(engine: StorageEngine, tenant_id=DEFAULT_TENANT_ID) -> list[dict]:
    return await engine.fetchall(Query(
        "SELECT id, username, role, disabled, created_ts FROM users "
        "WHERE tenant_id = ? ORDER BY username", [str(tenant_id)],
    ))


async def get_user_by_id(engine: StorageEngine, uid: str) -> dict | None:
    return await engine.fetchone(Query(
        "SELECT id, username, role, disabled FROM users WHERE id = ?", [uid],
    ))


async def set_disabled(engine: StorageEngine, uid: str, disabled: bool) -> None:
    await engine.run(Query(
        "UPDATE users SET disabled = ? WHERE id = ?", [disabled, uid],
    ))


async def set_role(engine: StorageEngine, uid: str, role: Role) -> None:
    await engine.run(Query(
        "UPDATE users SET role = ? WHERE id = ?", [role.value, uid],
    ))


async def set_password(engine: StorageEngine, uid: str, password: str) -> None:
    await engine.run(Query(
        "UPDATE users SET password_hash = ? WHERE id = ?",
        [hash_password(password), uid],
    ))


async def count_admins(engine: StorageEngine, tenant_id=DEFAULT_TENANT_ID) -> int:
    row = await engine.fetchone(Query(
        "SELECT COUNT(*) AS n FROM users WHERE tenant_id = ? AND role = ? "
        "AND disabled = FALSE", [str(tenant_id), Role.TENANT_ADMIN.value],
    ))
    return row["n"] if row else 0


async def seed_bootstrap_admin(engine: StorageEngine) -> None:
    """Create the initial TENANT_ADMIN from env if the users table is empty."""
    row = await engine.fetchone(Query("SELECT COUNT(*) AS n FROM users"))
    if row and row["n"] > 0:
        return
    await create_user(engine, settings.bootstrap_admin_user,
                      settings.bootstrap_admin_password, Role.TENANT_ADMIN)