feat: enforce effective appearance policy
This commit is contained in:
@@ -16,6 +16,7 @@ from govoplan_core.api.v1.schemas import (
|
||||
AuthShellResponse,
|
||||
AuthSessionResponse,
|
||||
AuthSessionUserInfo,
|
||||
EffectiveAppearanceInfo,
|
||||
GroupInfo,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
@@ -31,6 +32,7 @@ from govoplan_core.api.v1.schemas import (
|
||||
UserUiPreferences,
|
||||
)
|
||||
from govoplan_core.core.access import AuthMethod, PrincipalRef
|
||||
from govoplan_core.core.appearance import resolve_effective_appearance
|
||||
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.principal_cache import invalidate_auth_principals
|
||||
@@ -255,12 +257,23 @@ def _user_ui_preferences(settings_payload: object) -> UserUiPreferences:
|
||||
return UserUiPreferences()
|
||||
|
||||
|
||||
def _effective_appearance_info(session: Session, *, tenant: Tenant, user: User) -> EffectiveAppearanceInfo:
|
||||
system_item = get_system_settings(session)
|
||||
decision = resolve_effective_appearance(
|
||||
system_settings=system_item.settings,
|
||||
tenant_settings=tenant.settings,
|
||||
user_settings=user.settings,
|
||||
)
|
||||
return EffectiveAppearanceInfo.model_validate(decision.as_dict())
|
||||
|
||||
|
||||
def _user_info(
|
||||
user: User,
|
||||
account: Account,
|
||||
*,
|
||||
preferred_language: str | None = None,
|
||||
enabled_language_codes: list[str] | None = None,
|
||||
appearance: EffectiveAppearanceInfo | None = None,
|
||||
) -> UserInfo:
|
||||
return UserInfo(
|
||||
id=user.id,
|
||||
@@ -273,6 +286,7 @@ def _user_info(
|
||||
preferred_language=preferred_language,
|
||||
enabled_language_codes=enabled_language_codes or [],
|
||||
ui_preferences=_user_ui_preferences(user.settings),
|
||||
appearance=appearance or EffectiveAppearanceInfo(),
|
||||
)
|
||||
|
||||
|
||||
@@ -593,7 +607,7 @@ def _shell_response(
|
||||
]
|
||||
)
|
||||
return AuthShellResponse(
|
||||
user=_user_info(user, account),
|
||||
user=_user_info(user, account, appearance=_effective_appearance_info(session, tenant=tenant, user=user)),
|
||||
tenant=active_tenant,
|
||||
active_tenant=active_tenant,
|
||||
tenants=memberships,
|
||||
@@ -669,6 +683,7 @@ def _profile_response(session: Session, context: AuthContext) -> AuthProfileResp
|
||||
context.account,
|
||||
preferred_language=preferred_language,
|
||||
enabled_language_codes=user_enabled,
|
||||
appearance=_effective_appearance_info(session, tenant=context.tenant, user=context.user),
|
||||
),
|
||||
tenant=active_tenant,
|
||||
active_tenant=active_tenant,
|
||||
@@ -783,7 +798,13 @@ def _me_response(
|
||||
)
|
||||
]
|
||||
return MeResponse(
|
||||
user=_user_info(user, account, preferred_language=preferred_language, enabled_language_codes=user_enabled),
|
||||
user=_user_info(
|
||||
user,
|
||||
account,
|
||||
preferred_language=preferred_language,
|
||||
enabled_language_codes=user_enabled,
|
||||
appearance=_effective_appearance_info(session, tenant=tenant, user=user),
|
||||
),
|
||||
tenant=active_tenant,
|
||||
active_tenant=active_tenant,
|
||||
tenants=memberships,
|
||||
@@ -991,6 +1012,17 @@ def update_profile(
|
||||
if payload.ui_preferences is None:
|
||||
next_settings["ui"] = UserUiPreferences().model_dump()
|
||||
else:
|
||||
appearance = _effective_appearance_info(session, tenant=context.tenant, user=context.user)
|
||||
stored_palette = _user_ui_preferences(context.user.settings).palette
|
||||
if (
|
||||
appearance.locked
|
||||
and payload.ui_preferences.palette is not None
|
||||
and payload.ui_preferences.palette != stored_palette
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail="The effective appearance policy locks the palette.",
|
||||
)
|
||||
if (
|
||||
payload.ui_preferences.navigation is not None
|
||||
and payload.ui_preferences.navigation.locked
|
||||
|
||||
@@ -301,6 +301,25 @@ ADMIN_READ_SCOPES = (
|
||||
)
|
||||
|
||||
ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = (
|
||||
DocumentationTopic(
|
||||
id="access.reference.effective-appearance",
|
||||
title="Understand the effective appearance source",
|
||||
summary="The authenticated profile explains whether the active palette comes from the user, tenant, system, or a policy lock.",
|
||||
body=(
|
||||
"An explicit personal palette normally wins over tenant and system defaults. "
|
||||
"Resetting it stores inheritance, not a copy of the current default. A tenant "
|
||||
"policy lock suppresses personal choices; a system lock suppresses both tenant "
|
||||
"and personal choices. Access enforces the lock during profile writes and returns "
|
||||
"the effective palette, source, inherited value, and lock state on full profile responses."
|
||||
),
|
||||
layer="always",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("user", "tenant_admin", "system_admin"),
|
||||
order=8,
|
||||
links=(DocumentationLink(label="Profile API", href="/api/v1/auth/profile", kind="api"),),
|
||||
related_modules=("admin", "tenancy"),
|
||||
metadata={"kind": "reference", "help_contexts": ["core.settings", "admin.system-settings", "tenancy.admin.tenant-settings"]},
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="access.reference.resource-explanation-subjects",
|
||||
title="Select a user for resource-access diagnostics",
|
||||
|
||||
Reference in New Issue
Block a user