feat: add access module boundary migrations
This commit is contained in:
@@ -8,21 +8,36 @@ from govoplan_core.api.v1.schemas import (
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
MeResponse,
|
||||
PrincipalContextInfo,
|
||||
ProfileUpdateRequest,
|
||||
RoleInfo,
|
||||
SwitchTenantRequest,
|
||||
TenantInfo,
|
||||
TenantMembershipInfo,
|
||||
UserInfo,
|
||||
UserUiPreferences,
|
||||
)
|
||||
from govoplan_core.core.access import AuthMethod, PrincipalRef
|
||||
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal
|
||||
from govoplan_core.admin.settings import get_system_settings
|
||||
from govoplan_core.audit.logging import audit_from_principal
|
||||
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, maintenance_response_detail, saved_maintenance_mode
|
||||
from govoplan_access.backend.db.models import Account, Tenant, User
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_core.i18n import (
|
||||
i18n_settings,
|
||||
normalize_enabled_language_codes,
|
||||
normalize_language_code,
|
||||
preferred_language_code,
|
||||
system_enabled_language_codes,
|
||||
system_i18n_payload,
|
||||
tenant_enabled_language_codes,
|
||||
user_enabled_language_codes,
|
||||
)
|
||||
from govoplan_core.security.permissions import normalize_email, scopes_grant
|
||||
from govoplan_core.security.time import utc_now
|
||||
from govoplan_core.settings import settings
|
||||
from govoplan_access.backend.semantic import collect_function_assignment_ids, collect_function_delegation_ids, identity_id_for_account
|
||||
from govoplan_access.backend.security.passwords import verify_password
|
||||
from govoplan_access.backend.security.sessions import (
|
||||
collect_system_roles,
|
||||
@@ -68,17 +83,32 @@ def _clear_auth_cookies(response: Response) -> None:
|
||||
response.delete_cookie(settings.auth_csrf_cookie_name, **kwargs)
|
||||
|
||||
|
||||
def _tenant_info(tenant: Tenant) -> TenantInfo:
|
||||
def _tenant_info(tenant: Tenant, *, enabled_language_codes: list[str] | None = None) -> TenantInfo:
|
||||
return TenantInfo(
|
||||
id=tenant.id,
|
||||
slug=tenant.slug,
|
||||
name=tenant.name,
|
||||
is_active=tenant.is_active,
|
||||
default_locale=tenant.default_locale,
|
||||
enabled_language_codes=enabled_language_codes or [],
|
||||
)
|
||||
|
||||
|
||||
def _user_info(user: User, account: Account) -> UserInfo:
|
||||
def _user_ui_preferences(settings_payload: object) -> UserUiPreferences:
|
||||
raw = settings_payload.get("ui") if isinstance(settings_payload, dict) else None
|
||||
try:
|
||||
return UserUiPreferences.model_validate(raw if isinstance(raw, dict) else {})
|
||||
except ValueError:
|
||||
return UserUiPreferences()
|
||||
|
||||
|
||||
def _user_info(
|
||||
user: User,
|
||||
account: Account,
|
||||
*,
|
||||
preferred_language: str | None = None,
|
||||
enabled_language_codes: list[str] | None = None,
|
||||
) -> UserInfo:
|
||||
return UserInfo(
|
||||
id=user.id,
|
||||
account_id=account.id,
|
||||
@@ -87,6 +117,9 @@ def _user_info(user: User, account: Account) -> UserInfo:
|
||||
tenant_display_name=user.display_name,
|
||||
is_tenant_admin=user.is_tenant_admin,
|
||||
password_reset_required=account.password_reset_required,
|
||||
preferred_language=preferred_language,
|
||||
enabled_language_codes=enabled_language_codes or [],
|
||||
ui_preferences=_user_ui_preferences(user.settings),
|
||||
)
|
||||
|
||||
|
||||
@@ -124,6 +157,25 @@ def _tenant_memberships(session: Session, account: Account) -> list[TenantMember
|
||||
return memberships
|
||||
|
||||
|
||||
def _language_context(session: Session, *, tenant: Tenant, user: User) -> dict[str, object]:
|
||||
system_settings = get_system_settings(session)
|
||||
system_payload = system_i18n_payload(system_settings)
|
||||
system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale)
|
||||
tenant_enabled = tenant_enabled_language_codes(tenant.settings, system_enabled, default_locale=tenant.default_locale)
|
||||
user_enabled = user_enabled_language_codes(user.settings, tenant_enabled)
|
||||
preferred = preferred_language_code(
|
||||
user.settings,
|
||||
user_enabled,
|
||||
default_locale=tenant.default_locale or system_payload.get("default_language"),
|
||||
)
|
||||
return {
|
||||
"available_languages": system_payload["available_languages"],
|
||||
"tenant_enabled_language_codes": tenant_enabled,
|
||||
"user_enabled_language_codes": user_enabled,
|
||||
"preferred_language": preferred,
|
||||
}
|
||||
|
||||
|
||||
def _resolve_login_user(session: Session, payload: LoginRequest) -> tuple[Account, User, Tenant]:
|
||||
account = (
|
||||
session.query(Account)
|
||||
@@ -157,13 +209,22 @@ def _me_response(
|
||||
user: User,
|
||||
tenant: Tenant,
|
||||
effective_scopes: list[str] | None = None,
|
||||
auth_method: AuthMethod = "session",
|
||||
api_key_id: str | None = None,
|
||||
session_id: str | None = None,
|
||||
service_account_id: str | None = None,
|
||||
include_system: bool = True,
|
||||
include_all_memberships: bool = True,
|
||||
) -> MeResponse:
|
||||
tenant_roles = collect_user_roles(session, user)
|
||||
system_roles = collect_system_roles(session, account) if include_system else []
|
||||
groups = collect_user_groups(session, user)
|
||||
active_tenant = _tenant_info(tenant)
|
||||
scopes = effective_scopes if effective_scopes is not None else collect_user_scopes(session, user, include_system=include_system)
|
||||
languages = _language_context(session, tenant=tenant, user=user)
|
||||
tenant_enabled = list(languages["tenant_enabled_language_codes"])
|
||||
user_enabled = list(languages["user_enabled_language_codes"])
|
||||
preferred_language = str(languages["preferred_language"])
|
||||
active_tenant = _tenant_info(tenant, enabled_language_codes=tenant_enabled)
|
||||
memberships = _tenant_memberships(session, account) if include_all_memberships else [
|
||||
TenantMembershipInfo(
|
||||
id=tenant.id,
|
||||
@@ -175,13 +236,35 @@ def _me_response(
|
||||
)
|
||||
]
|
||||
return MeResponse(
|
||||
user=_user_info(user, account),
|
||||
user=_user_info(user, account, preferred_language=preferred_language, enabled_language_codes=user_enabled),
|
||||
tenant=active_tenant,
|
||||
active_tenant=active_tenant,
|
||||
tenants=memberships,
|
||||
scopes=effective_scopes if effective_scopes is not None else collect_user_scopes(session, user, include_system=include_system),
|
||||
scopes=scopes,
|
||||
roles=_roles_info(tenant_roles) + _roles_info(system_roles, level="system"),
|
||||
groups=_groups_info(groups),
|
||||
principal=PrincipalContextInfo.model_validate(
|
||||
PrincipalRef(
|
||||
account_id=account.id,
|
||||
membership_id=user.id,
|
||||
tenant_id=tenant.id,
|
||||
identity_id=identity_id_for_account(session, account.id),
|
||||
scopes=frozenset(scopes),
|
||||
group_ids=frozenset(group.id for group in groups),
|
||||
role_ids=frozenset(role.id for role in tenant_roles + system_roles),
|
||||
function_assignment_ids=frozenset(collect_function_assignment_ids(session, user)),
|
||||
delegation_ids=frozenset(collect_function_delegation_ids(session, user)),
|
||||
auth_method=auth_method,
|
||||
api_key_id=api_key_id,
|
||||
session_id=session_id,
|
||||
service_account_id=service_account_id,
|
||||
email=account.email,
|
||||
display_name=account.display_name or user.display_name,
|
||||
).to_dict()
|
||||
),
|
||||
available_languages=languages["available_languages"],
|
||||
enabled_language_codes=tenant_enabled,
|
||||
default_language=preferred_language,
|
||||
)
|
||||
|
||||
|
||||
@@ -209,7 +292,15 @@ def login(payload: LoginRequest, request: Request, response: Response, session:
|
||||
return LoginResponse(
|
||||
access_token=created.token,
|
||||
expires_at=created.model.expires_at,
|
||||
**me_payload.model_dump(),
|
||||
**_me_response(
|
||||
session,
|
||||
account=account,
|
||||
user=user,
|
||||
tenant=tenant,
|
||||
effective_scopes=me_payload.scopes,
|
||||
auth_method="session",
|
||||
session_id=created.model.id,
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
|
||||
@@ -224,6 +315,10 @@ def me(principal: ApiPrincipal = Depends(get_api_principal), session: Session =
|
||||
user=principal.user,
|
||||
tenant=tenant,
|
||||
effective_scopes=principal.scopes,
|
||||
auth_method=principal.auth_method,
|
||||
api_key_id=principal.api_key_id,
|
||||
session_id=principal.session_id,
|
||||
service_account_id=principal.principal.service_account_id,
|
||||
include_system=principal.auth_session is not None,
|
||||
include_all_memberships=principal.auth_session is not None,
|
||||
)
|
||||
@@ -244,6 +339,46 @@ def update_profile(
|
||||
if "tenant_display_name" in payload.model_fields_set:
|
||||
principal.user.display_name = payload.tenant_display_name.strip() if payload.tenant_display_name else None
|
||||
session.add(principal.user)
|
||||
next_settings = dict(principal.user.settings or {})
|
||||
settings_changed = False
|
||||
if {"preferred_language", "enabled_language_codes"}.intersection(payload.model_fields_set):
|
||||
tenant = session.get(Tenant, principal.tenant_id)
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Active tenant not found")
|
||||
system_settings = get_system_settings(session)
|
||||
system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale)
|
||||
tenant_enabled = tenant_enabled_language_codes(tenant.settings, system_enabled, default_locale=tenant.default_locale)
|
||||
next_i18n = i18n_settings(next_settings)
|
||||
if "preferred_language" in payload.model_fields_set:
|
||||
preferred = normalize_language_code(payload.preferred_language)
|
||||
if preferred:
|
||||
normalized_preferred = normalize_enabled_language_codes(
|
||||
[preferred],
|
||||
[{"code": code} for code in tenant_enabled],
|
||||
fallback_codes=tenant_enabled,
|
||||
)[0]
|
||||
next_i18n["preferred_language"] = normalized_preferred
|
||||
else:
|
||||
next_i18n.pop("preferred_language", None)
|
||||
if "enabled_language_codes" in payload.model_fields_set:
|
||||
next_i18n["enabled_language_codes"] = normalize_enabled_language_codes(
|
||||
payload.enabled_language_codes,
|
||||
[{"code": code} for code in tenant_enabled],
|
||||
fallback_codes=tenant_enabled,
|
||||
)
|
||||
next_settings["i18n"] = next_i18n
|
||||
settings_changed = True
|
||||
if "ui_preferences" in payload.model_fields_set:
|
||||
if payload.ui_preferences is None:
|
||||
next_settings["ui"] = UserUiPreferences().model_dump()
|
||||
else:
|
||||
next_ui = _user_ui_preferences(next_settings).model_dump()
|
||||
next_ui.update(payload.ui_preferences.model_dump(exclude_unset=True))
|
||||
next_settings["ui"] = UserUiPreferences.model_validate(next_ui).model_dump()
|
||||
settings_changed = True
|
||||
if settings_changed:
|
||||
principal.user.settings = next_settings
|
||||
session.add(principal.user)
|
||||
|
||||
audit_from_principal(
|
||||
session,
|
||||
@@ -262,6 +397,8 @@ def update_profile(
|
||||
account=principal.account,
|
||||
user=principal.user,
|
||||
tenant=tenant,
|
||||
auth_method=principal.auth_method,
|
||||
session_id=principal.session_id,
|
||||
include_system=True,
|
||||
include_all_memberships=True,
|
||||
)
|
||||
@@ -283,7 +420,14 @@ def switch_tenant(
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
|
||||
session.commit()
|
||||
return _me_response(session, account=principal.account, user=membership, tenant=tenant)
|
||||
return _me_response(
|
||||
session,
|
||||
account=principal.account,
|
||||
user=membership,
|
||||
tenant=tenant,
|
||||
auth_method="session",
|
||||
session_id=principal.session_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
|
||||
Reference in New Issue
Block a user