feat: enforce effective appearance policy

This commit is contained in:
2026-08-20 07:03:28 +02:00
parent 566b3b83ad
commit 6052dde760
2 changed files with 53 additions and 2 deletions
+34 -2
View File
@@ -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
+19
View File
@@ -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",