288 lines
11 KiB
Python
288 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping, Sequence
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_access.backend.admin.service import ensure_default_roles, get_or_create_account
|
|
from govoplan_access.backend.db.models import Account, Role, SystemRoleAssignment, User, UserRoleAssignment
|
|
from govoplan_access.backend.permissions.catalog import normalize_email, scopes_grant
|
|
from govoplan_access.backend.security.api_keys import create_api_key
|
|
from govoplan_access.backend.security.passwords import hash_password
|
|
from govoplan_core.admin.common import AdminValidationError
|
|
from govoplan_core.core.access import (
|
|
CreatedApiKeyRef,
|
|
DevelopmentBootstrapRef,
|
|
FirstAdminProvisioner,
|
|
FirstAdminProvisioningError,
|
|
FirstSystemAdministratorRef,
|
|
TenantAccessProvisioner,
|
|
TenantOwnerCandidateRef,
|
|
UserRef,
|
|
)
|
|
|
|
|
|
class LegacyTenantAccessProvisioner(TenantAccessProvisioner):
|
|
def ensure_default_roles(self, session: object, tenant: object | None = None) -> Mapping[str, object]:
|
|
return ensure_default_roles(session, tenant) # type: ignore[arg-type]
|
|
|
|
def tenant_owner_candidates(self, session: object) -> tuple[TenantOwnerCandidateRef, ...]:
|
|
db = _session(session)
|
|
accounts = db.query(Account).filter(Account.is_active.is_(True)).order_by(Account.email.asc()).all()
|
|
return tuple(
|
|
TenantOwnerCandidateRef(account_id=account.id, email=account.email, display_name=account.display_name)
|
|
for account in accounts
|
|
)
|
|
|
|
def ensure_tenant_owner_membership(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant: object,
|
|
owner_account_id: str,
|
|
) -> str:
|
|
db = _session(session)
|
|
tenant_id = getattr(tenant, "id", None)
|
|
if not tenant_id:
|
|
raise AdminValidationError("Tenant owner membership requires a persisted tenant.")
|
|
|
|
roles = ensure_default_roles(db, tenant) # type: ignore[arg-type]
|
|
owner_role = roles.get("owner")
|
|
if owner_role is None:
|
|
raise AdminValidationError("Tenant owner role is not available.")
|
|
|
|
owner_account = db.get(Account, owner_account_id)
|
|
if owner_account is None or not owner_account.is_active:
|
|
raise AdminValidationError("The selected tenant owner account is missing or inactive.")
|
|
|
|
membership = (
|
|
db.query(User)
|
|
.filter(User.tenant_id == tenant_id, User.account_id == owner_account.id)
|
|
.one_or_none()
|
|
)
|
|
if membership is None:
|
|
membership = User(
|
|
tenant_id=tenant_id,
|
|
account_id=owner_account.id,
|
|
email=owner_account.email,
|
|
display_name=owner_account.display_name,
|
|
is_active=True,
|
|
auth_provider=owner_account.auth_provider,
|
|
password_hash=owner_account.password_hash,
|
|
)
|
|
db.add(membership)
|
|
db.flush()
|
|
|
|
existing_assignment = (
|
|
db.query(UserRoleAssignment)
|
|
.filter(
|
|
UserRoleAssignment.tenant_id == tenant_id,
|
|
UserRoleAssignment.user_id == membership.id,
|
|
UserRoleAssignment.role_id == owner_role.id,
|
|
)
|
|
.one_or_none()
|
|
)
|
|
if existing_assignment is None:
|
|
db.add(UserRoleAssignment(tenant_id=tenant_id, user_id=membership.id, role_id=owner_role.id))
|
|
db.flush()
|
|
return membership.id
|
|
|
|
def ensure_development_admin(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant: object,
|
|
user_email: str,
|
|
user_password: str,
|
|
api_key_secret: str | None,
|
|
scopes: Sequence[str],
|
|
) -> DevelopmentBootstrapRef:
|
|
db = _session(session)
|
|
tenant_id = getattr(tenant, "id", None)
|
|
if not tenant_id:
|
|
raise AdminValidationError("Development bootstrap requires a persisted tenant.")
|
|
|
|
tenant_roles = ensure_default_roles(db, tenant) # type: ignore[arg-type]
|
|
system_roles = ensure_default_roles(db, None)
|
|
account, _, _ = get_or_create_account(
|
|
db,
|
|
email=user_email,
|
|
display_name="Development Admin",
|
|
password=user_password,
|
|
password_reset_required=False,
|
|
)
|
|
if not account.password_hash:
|
|
account.password_hash = hash_password(user_password)
|
|
account.is_active = True
|
|
db.add(account)
|
|
|
|
user = db.query(User).filter(User.tenant_id == tenant_id, User.account_id == account.id).one_or_none()
|
|
if user is None:
|
|
user = User(
|
|
tenant_id=tenant_id,
|
|
account_id=account.id,
|
|
email=account.email,
|
|
display_name="Development Admin",
|
|
is_tenant_admin=True,
|
|
auth_provider=account.auth_provider,
|
|
password_hash=account.password_hash,
|
|
)
|
|
db.add(user)
|
|
db.flush()
|
|
else:
|
|
user.email = account.email
|
|
user.password_hash = account.password_hash
|
|
user.is_active = True
|
|
db.add(user)
|
|
|
|
owner_role = tenant_roles["owner"]
|
|
existing_assignment = db.query(UserRoleAssignment).filter(
|
|
UserRoleAssignment.tenant_id == tenant_id,
|
|
UserRoleAssignment.user_id == user.id,
|
|
UserRoleAssignment.role_id == owner_role.id,
|
|
).one_or_none()
|
|
if existing_assignment is None:
|
|
db.add(UserRoleAssignment(tenant_id=tenant_id, user_id=user.id, role_id=owner_role.id))
|
|
|
|
system_owner = system_roles["system_owner"]
|
|
existing_system_assignment = db.query(SystemRoleAssignment).filter(
|
|
SystemRoleAssignment.account_id == account.id,
|
|
SystemRoleAssignment.role_id == system_owner.id,
|
|
).one_or_none()
|
|
if existing_system_assignment is None:
|
|
db.add(SystemRoleAssignment(account_id=account.id, role_id=system_owner.id))
|
|
|
|
created_api_key = None
|
|
if api_key_secret:
|
|
existing = [key for key in user.api_keys if key.name == "Development API key" and key.revoked_at is None]
|
|
if not existing:
|
|
api_key = create_api_key(
|
|
db,
|
|
user=user,
|
|
name="Development API key",
|
|
scopes=list(scopes),
|
|
secret=api_key_secret,
|
|
)
|
|
created_api_key = CreatedApiKeyRef(id=api_key.model.id, secret=api_key.secret)
|
|
|
|
db.flush()
|
|
return DevelopmentBootstrapRef(
|
|
user=UserRef(
|
|
id=user.id,
|
|
account_id=account.id,
|
|
tenant_id=tenant_id,
|
|
email=user.email,
|
|
display_name=user.display_name,
|
|
status="active" if user.is_active else "inactive",
|
|
),
|
|
created_api_key=created_api_key,
|
|
)
|
|
|
|
|
|
class LegacyFirstAdminProvisioner(FirstAdminProvisioner):
|
|
def has_durable_system_administrator(self, session: object) -> bool:
|
|
db = _session(session)
|
|
roles = (
|
|
db.query(Role)
|
|
.join(SystemRoleAssignment, SystemRoleAssignment.role_id == Role.id)
|
|
.join(Account, Account.id == SystemRoleAssignment.account_id)
|
|
.filter(Role.tenant_id.is_(None), Account.is_active.is_(True))
|
|
.all()
|
|
)
|
|
return any(
|
|
role.slug in {"system_owner", "system_admin"}
|
|
or scopes_grant(role.permissions or (), "access:system_setting:write")
|
|
for role in roles
|
|
)
|
|
|
|
def create_first_system_administrator(
|
|
self,
|
|
session: object,
|
|
*,
|
|
tenant: object,
|
|
email: str,
|
|
display_name: str | None,
|
|
password: str,
|
|
) -> FirstSystemAdministratorRef:
|
|
db = _session(session)
|
|
tenant_id = getattr(tenant, "id", None)
|
|
if not tenant_id:
|
|
raise FirstAdminProvisioningError(
|
|
"First-administrator enrollment requires a persisted initial tenant."
|
|
)
|
|
if len(password) < 12:
|
|
raise FirstAdminProvisioningError(
|
|
"The administrator password must contain at least 12 characters."
|
|
)
|
|
if self.has_durable_system_administrator(db):
|
|
raise FirstAdminProvisioningError(
|
|
"A durable system administrator already exists."
|
|
)
|
|
|
|
normalized_email = normalize_email(email)
|
|
if not normalized_email or "@" not in normalized_email:
|
|
raise FirstAdminProvisioningError("Enter a valid administrator email address.")
|
|
existing = (
|
|
db.query(Account)
|
|
.filter(Account.normalized_email == normalized_email)
|
|
.with_for_update()
|
|
.one_or_none()
|
|
)
|
|
if existing is not None:
|
|
raise FirstAdminProvisioningError(
|
|
"The enrollment email already belongs to an account. Use a new address for the first system owner."
|
|
)
|
|
|
|
tenant_roles = ensure_default_roles(db, tenant) # type: ignore[arg-type]
|
|
system_roles = ensure_default_roles(db, None)
|
|
account, created, _temporary_password = get_or_create_account(
|
|
db,
|
|
email=email,
|
|
display_name=display_name,
|
|
password=password,
|
|
password_reset_required=False,
|
|
)
|
|
if not created:
|
|
raise FirstAdminProvisioningError(
|
|
"The enrollment email already belongs to an account."
|
|
)
|
|
membership = User(
|
|
tenant_id=tenant_id,
|
|
account_id=account.id,
|
|
email=account.email,
|
|
display_name=display_name or account.display_name,
|
|
is_active=True,
|
|
is_tenant_admin=True,
|
|
auth_provider=account.auth_provider,
|
|
password_hash=account.password_hash,
|
|
)
|
|
db.add(membership)
|
|
db.flush()
|
|
db.add(
|
|
UserRoleAssignment(
|
|
tenant_id=tenant_id,
|
|
user_id=membership.id,
|
|
role_id=tenant_roles["owner"].id,
|
|
)
|
|
)
|
|
db.add(
|
|
SystemRoleAssignment(
|
|
account_id=account.id,
|
|
role_id=system_roles["system_owner"].id,
|
|
)
|
|
)
|
|
db.flush()
|
|
return FirstSystemAdministratorRef(
|
|
account_id=account.id,
|
|
email=account.email,
|
|
display_name=account.display_name,
|
|
membership_id=membership.id,
|
|
tenant_id=tenant_id,
|
|
)
|
|
|
|
|
|
def _session(session: object) -> Session:
|
|
if not isinstance(session, Session):
|
|
raise TypeError("Tenant access provisioner requires a SQLAlchemy Session")
|
|
return session
|