Files
govoplan-access/src/govoplan_access/backend/tenancy/provisioning.py

176 lines
6.8 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, SystemRoleAssignment, User, UserRoleAssignment
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
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,
)
def _session(session: object) -> Session:
if not isinstance(session, Session):
raise TypeError("Tenant access provisioner requires a SQLAlchemy Session")
return session