Provide first system administrator authority
This commit is contained in:
@@ -5,11 +5,21 @@ 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, SystemRoleAssignment, User, UserRoleAssignment
|
||||
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, TenantAccessProvisioner, TenantOwnerCandidateRef, UserRef
|
||||
from govoplan_core.core.access import (
|
||||
CreatedApiKeyRef,
|
||||
DevelopmentBootstrapRef,
|
||||
FirstAdminProvisioner,
|
||||
FirstAdminProvisioningError,
|
||||
FirstSystemAdministratorRef,
|
||||
TenantAccessProvisioner,
|
||||
TenantOwnerCandidateRef,
|
||||
UserRef,
|
||||
)
|
||||
|
||||
|
||||
class LegacyTenantAccessProvisioner(TenantAccessProvisioner):
|
||||
@@ -169,6 +179,108 @@ class LegacyTenantAccessProvisioner(TenantAccessProvisioner):
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user