feat: add access module boundary migrations

This commit is contained in:
2026-07-10 12:51:16 +02:00
parent 37828fe340
commit 04681f1d75
41 changed files with 7229 additions and 738 deletions

View File

@@ -25,6 +25,7 @@ from govoplan_access.backend.security.sessions import (
collect_user_groups,
collect_user_scopes,
)
from govoplan_access.backend.semantic import collect_function_assignment_ids, collect_function_delegation_ids
from govoplan_access.backend.auth.dependencies import ApiPrincipal, has_scope
from govoplan_access.backend.db.models import (
Account,
@@ -44,7 +45,7 @@ def _http_admin_error(exc: Exception) -> HTTPException:
if isinstance(exc, AdminConflictError):
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
if isinstance(exc, AdminValidationError):
return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc))
return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
@@ -150,6 +151,8 @@ def _user_item(session: Session, user: User, *, owner_ids: set[str] | None = Non
last_login_at=account.last_login_at,
groups=[_group_summary(session, group, include_members=False) for group in groups],
roles=[_role_summary(session, role) for role in roles],
function_assignment_ids=collect_function_assignment_ids(session, user),
function_delegation_ids=collect_function_delegation_ids(session, user),
effective_scopes=collect_user_scopes(session, user, include_system=False),
is_owner=user.id in effective_owner_ids,
is_last_active_owner=user.id in effective_owner_ids and len(effective_owner_ids) == 1,

View File

@@ -5,6 +5,8 @@ from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
from govoplan_core.api.v1.schemas import DeltaDeletedItem
RETENTION_DAY_KEYS = (
"raw_campaign_json_retention_days",
"generated_eml_retention_days",
@@ -128,6 +130,9 @@ class TenantSettingsItem(BaseModel):
slug: str
name: str
default_locale: str = Field(default="en", min_length=1, max_length=20)
available_languages: list[dict[str, Any]] = Field(default_factory=list)
system_enabled_language_codes: list[str] = Field(default_factory=list)
enabled_language_codes: list[str] = Field(default_factory=list)
settings: dict[str, Any] = Field(default_factory=dict)
@@ -135,6 +140,7 @@ class TenantSettingsUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
default_locale: str = Field(min_length=1, max_length=20)
enabled_language_codes: list[str] | None = None
class RoleSummary(BaseModel):
@@ -153,6 +159,227 @@ class RoleSummary(BaseModel):
system_required: bool = False
class IdentityAccountLinkItem(BaseModel):
id: str
account_id: str
email: str | None = None
display_name: str | None = None
is_primary: bool = False
source: str = "local"
class IdentityAdminItem(BaseModel):
id: str
display_name: str | None = None
external_subject: str | None = None
source: str = "local"
is_active: bool = True
accounts: list[IdentityAccountLinkItem] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
class IdentityListResponse(BaseModel):
identities: list[IdentityAdminItem]
class IdentityCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
display_name: str | None = Field(default=None, max_length=255)
external_subject: str | None = Field(default=None, max_length=255)
source: str = Field(default="local", max_length=50)
account_ids: list[str] = Field(default_factory=list)
primary_account_id: str | None = None
is_active: bool = True
class IdentityUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
display_name: str | None = Field(default=None, max_length=255)
external_subject: str | None = Field(default=None, max_length=255)
source: str | None = Field(default=None, max_length=50)
account_ids: list[str] | None = None
primary_account_id: str | None = None
is_active: bool | None = None
class OrganizationUnitItem(BaseModel):
id: str
tenant_id: str
parent_id: str | None = None
slug: str = Field(min_length=1, max_length=100)
name: str = Field(min_length=1, max_length=255)
description: str | None = None
is_active: bool = True
created_at: datetime
updated_at: datetime
class OrganizationUnitListResponse(BaseModel):
organization_units: list[OrganizationUnitItem]
class OrganizationUnitCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
slug: str
name: str = Field(min_length=1, max_length=255)
parent_id: str | None = None
description: str | None = None
is_active: bool = True
class OrganizationUnitUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
slug: str | None = None
name: str | None = Field(default=None, min_length=1, max_length=255)
parent_id: str | None = None
description: str | None = None
is_active: bool | None = None
class FunctionAdminItem(BaseModel):
id: str
tenant_id: str
organization_unit_id: str
slug: str = Field(min_length=1, max_length=100)
name: str = Field(min_length=1, max_length=255)
description: str | None = None
role_ids: list[str] = Field(default_factory=list)
delegable: bool = False
act_in_place_allowed: bool = False
is_active: bool = True
created_at: datetime
updated_at: datetime
class FunctionListResponse(BaseModel):
functions: list[FunctionAdminItem]
class FunctionCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
organization_unit_id: str
slug: str
name: str = Field(min_length=1, max_length=255)
description: str | None = None
role_ids: list[str] = Field(default_factory=list)
delegable: bool = False
act_in_place_allowed: bool = False
is_active: bool = True
class FunctionUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
organization_unit_id: str | None = None
slug: str | None = None
name: str | None = Field(default=None, min_length=1, max_length=255)
description: str | None = None
role_ids: list[str] | None = None
delegable: bool | None = None
act_in_place_allowed: bool | None = None
is_active: bool | None = None
class FunctionAssignmentAdminItem(BaseModel):
id: str
tenant_id: str
account_id: str
identity_id: str | None = None
function_id: str
organization_unit_id: str
applies_to_subunits: bool = False
source: str = "direct"
delegated_from_assignment_id: str | None = None
acting_for_account_id: str | None = None
valid_from: datetime | None = None
valid_until: datetime | None = None
is_active: bool = True
created_at: datetime
updated_at: datetime
class FunctionAssignmentListResponse(BaseModel):
assignments: list[FunctionAssignmentAdminItem]
class FunctionAssignmentCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
account_id: str
function_id: str
organization_unit_id: str | None = None
identity_id: str | None = None
applies_to_subunits: bool = False
source: str = Field(default="direct", max_length=50)
delegated_from_assignment_id: str | None = None
acting_for_account_id: str | None = None
valid_from: datetime | None = None
valid_until: datetime | None = None
is_active: bool = True
class FunctionAssignmentUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
identity_id: str | None = None
organization_unit_id: str | None = None
applies_to_subunits: bool | None = None
source: str | None = Field(default=None, max_length=50)
delegated_from_assignment_id: str | None = None
acting_for_account_id: str | None = None
valid_from: datetime | None = None
valid_until: datetime | None = None
is_active: bool | None = None
class FunctionDelegationAdminItem(BaseModel):
id: str
tenant_id: str
function_assignment_id: str
delegator_account_id: str
delegate_account_id: str
mode: Literal["delegate", "act_in_place"] = "delegate"
reason: str | None = None
valid_from: datetime | None = None
valid_until: datetime | None = None
revoked_at: datetime | None = None
is_active: bool = True
created_at: datetime
updated_at: datetime
class FunctionDelegationListResponse(BaseModel):
delegations: list[FunctionDelegationAdminItem]
class FunctionDelegationCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
function_assignment_id: str
delegate_account_id: str
mode: Literal["delegate", "act_in_place"] = "delegate"
reason: str | None = None
valid_from: datetime | None = None
valid_until: datetime | None = None
is_active: bool = True
class FunctionDelegationUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
reason: str | None = None
valid_from: datetime | None = None
valid_until: datetime | None = None
is_active: bool | None = None
revoked: bool | None = None
class GroupSummary(BaseModel):
id: str
slug: str = Field(min_length=1, max_length=100)
@@ -180,6 +407,8 @@ class UserAdminItem(BaseModel):
last_login_at: datetime | None = None
groups: list[GroupSummary] = Field(default_factory=list)
roles: list[RoleSummary] = Field(default_factory=list)
function_assignment_ids: list[str] = Field(default_factory=list)
function_delegation_ids: list[str] = Field(default_factory=list)
effective_scopes: list[str] = Field(default_factory=list)
is_owner: bool = False
is_last_active_owner: bool = False
@@ -191,6 +420,13 @@ class UserListResponse(BaseModel):
users: list[UserAdminItem]
class UserListDeltaResponse(UserListResponse):
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class UserCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -222,6 +458,13 @@ class GroupListResponse(BaseModel):
groups: list[GroupSummary]
class GroupListDeltaResponse(GroupListResponse):
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class GroupCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -247,6 +490,13 @@ class RoleListResponse(BaseModel):
roles: list[RoleSummary]
class RoleListDeltaResponse(RoleListResponse):
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class RoleCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -280,6 +530,13 @@ class SystemAccountListResponse(BaseModel):
roles: list[RoleSummary]
class SystemAccountListDeltaResponse(SystemAccountListResponse):
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class SystemAccountUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -396,12 +653,124 @@ class RetentionRunResponse(BaseModel):
result: dict[str, Any]
class ConfigurationSafetyFieldItem(BaseModel):
key: str
label: str
owner_module: str
scope: Literal["system", "tenant", "user", "group", "campaign"]
storage: str
ui_managed: bool
risk: Literal["low", "medium", "high", "destructive"]
secret_handling: Literal["none", "reference_only", "env_only"] = "none"
required_scopes: list[str] = Field(default_factory=list)
dry_run_required: bool = False
validation_required: bool = True
policy_explanation_required: bool = False
audit_event: str | None = None
maintenance_required: bool = False
two_person_approval_required: bool = False
rollback_history_required: bool = False
notes: str | None = None
class ConfigurationSafetyCatalogResponse(BaseModel):
fields: list[ConfigurationSafetyFieldItem]
class ConfigurationChangeSafetyPlanRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
key: str
value: Any = None
dry_run: bool = False
maintenance_mode: bool = False
approval_count: int = Field(default=0, ge=0)
class ConfigurationChangeSafetyPlanResponse(BaseModel):
plan: dict[str, Any]
class ConfigurationChangeRequestCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
key: str
value: Any = None
dry_run: bool = False
target: dict[str, Any] = Field(default_factory=dict)
reason: str | None = Field(default=None, max_length=1000)
class ConfigurationChangeRequestApproveRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
reason: str | None = Field(default=None, max_length=1000)
class ConfigurationChangeRequestResponse(BaseModel):
request: dict[str, Any]
class ConfigurationControlSnapshotResponse(BaseModel):
requests: list[dict[str, Any]] = Field(default_factory=list)
history: list[dict[str, Any]] = Field(default_factory=list)
class ConfigurationControlDeltaResponse(ConfigurationControlSnapshotResponse):
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class ConfigurationPackageCatalogResponse(BaseModel):
validation: dict[str, Any]
class ConfigurationPackageRunRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
package: dict[str, Any]
tenant_id: str | None = None
supplied_data: dict[str, Any] = Field(default_factory=dict)
change_request_id: str | None = None
class ConfigurationPackageDryRunResponse(BaseModel):
diagnostics: list[dict[str, Any]] = Field(default_factory=list)
required_data: list[dict[str, Any]] = Field(default_factory=list)
plan: list[dict[str, Any]] = Field(default_factory=list)
class ConfigurationPackageApplyResponse(BaseModel):
diagnostics: list[dict[str, Any]] = Field(default_factory=list)
created_refs: dict[str, str] = Field(default_factory=dict)
updated_refs: dict[str, str] = Field(default_factory=dict)
class ConfigurationPackageExportRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
tenant_id: str | None = None
scopes: list[str] = Field(default_factory=list)
module_ids: list[str] = Field(default_factory=list)
object_refs: list[str] = Field(default_factory=list)
class ConfigurationPackageExportResponse(BaseModel):
fragments: list[dict[str, Any]] = Field(default_factory=list)
data_requirements: list[dict[str, Any]] = Field(default_factory=list)
diagnostics: list[dict[str, Any]] = Field(default_factory=list)
class SystemSettingsItem(BaseModel):
default_locale: str = "en"
allow_tenant_custom_groups: bool = True
allow_tenant_custom_roles: bool = True
allow_tenant_api_keys: bool = True
privacy_retention_policy: PrivacyRetentionPolicyItem = Field(default_factory=PrivacyRetentionPolicyItem)
available_languages: list[dict[str, Any]] = Field(default_factory=list)
enabled_language_codes: list[str] = Field(default_factory=list)
settings: dict[str, Any] = Field(default_factory=dict)
@@ -413,6 +782,8 @@ class SystemSettingsUpdateRequest(BaseModel):
allow_tenant_custom_roles: bool
allow_tenant_api_keys: bool
privacy_retention_policy: PrivacyRetentionPolicyItem | None = None
available_languages: list[dict[str, Any]] | None = None
enabled_language_codes: list[str] | None = None
class ApiKeyAdminItem(BaseModel):
@@ -432,6 +803,13 @@ class ApiKeyListResponse(BaseModel):
api_keys: list[ApiKeyAdminItem]
class ApiKeyListDeltaResponse(ApiKeyListResponse):
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class AdminApiKeyCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -463,3 +841,5 @@ class AuditAdminListResponse(BaseModel):
page: int = 1
page_size: int = 100
pages: int = 1
cursor: str | None = None
next_cursor: str | None = None

View File

@@ -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")

File diff suppressed because it is too large Load Diff