diff --git a/src/govoplan_access/backend/api/v1/auth.py b/src/govoplan_access/backend/api/v1/auth.py index d2b6353..7013b20 100644 --- a/src/govoplan_access/backend/api/v1/auth.py +++ b/src/govoplan_access/backend/api/v1/auth.py @@ -1013,7 +1013,8 @@ def update_profile( 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 + stored_preferences = _user_ui_preferences(context.user.settings) + stored_palette = stored_preferences.palette if ( appearance.locked and payload.ui_preferences.palette is not None @@ -1023,6 +1024,17 @@ def update_profile( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="The effective appearance policy locks the palette.", ) + requested_overrides = payload.ui_preferences.appearance_overrides + if ( + "appearance_overrides" in payload.ui_preferences.model_fields_set + and not appearance.custom_overrides_allowed + and requested_overrides is not None + and requested_overrides != stored_preferences.appearance_overrides + ): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, + detail="The effective appearance policy does not allow personal custom overrides.", + ) if ( payload.ui_preferences.navigation is not None and payload.ui_preferences.navigation.locked diff --git a/src/govoplan_access/backend/manifest.py b/src/govoplan_access/backend/manifest.py index 187a561..5fe6c30 100644 --- a/src/govoplan_access/backend/manifest.py +++ b/src/govoplan_access/backend/manifest.py @@ -304,13 +304,18 @@ 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.", + summary="The authenticated profile explains the effective palette, policy lock, and governed personal token overrides.", 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." + "the effective palette, source, inherited value, and lock state on full profile responses. " + "Advanced accent, surface, and status overrides are accepted only when the system opts in, " + "the tenant does not block them, and neither palette scope is locked. Both light and dark " + "documents are versioned and validated atomically for hexadecimal values, WCAG AA paired " + "contrast, and distinct status colors. Invalid or disallowed documents are never partially " + "applied; users may still remove an inactive stored override to return to inheritance." ), layer="always", documentation_types=("admin", "user"),