fix(settings): confine settings scopes to the caller's own on multi-tenant hosts (#368) - #379
antosubash wants to merge 2 commits into
Conversation
…enant hosts (#368) The settings routes checked settings.view/edit/delete only, so on a multi-tenant host any holder could write the host-wide system scope and any tenant's or user's scope by naming it in the URL. Add a settings.system permission and settings/scope_guard.py. With multi_tenant on, a caller reaches its own tenant scope and its own user scope; the system scope, other scopes, resolve for another tenant/user, id-based CRUD, the module-settings API and the /admin/settings screens need a platform settings admin. Without a tenant resolver, a user whose record carries a tenant_id is never a platform admin, even with the admin role. Single-tenant hosts are unchanged.
Deploying simple-module-python with
|
| Latest commit: |
69a381f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://43be1a41.simple-module-python.pages.dev |
| Branch Preview URL: | https://fix-368-settings-tenant-scop.simple-module-python.pages.dev |
|
🤖 Claimed existing pull request. I am creating an isolated worktree from |
|
🔀 Base-branch conflicts resolved and pushed. Verification follows. Resolved all three conflict files without staging or committing. Updated Verification: |
|
✅ Post-resolution verification passed. Independent QA passed. Local report: Conflict-resolution QA evidence (independent verifier). Attached QA evidenceOmitted evidence:
|
|
✅ CI checks passed for this pull request. |



Closes #368.
Why
The settings routes only checked
settings.view,settings.editandsettings.delete. On a multi-tenant host, anyone holding them could:The issue's repro: acme's admin turned on a records setting for the whole host, and wrote a setting into globex's scope.
What changed
settings.system, plussettings/scope_guard.py.multi_tenanton,settings.*lets a caller manage:settings.system. This covers:resolvefor another tenant or user;/api/settings/modules;/admin/settingsscreen and action.adminholds every permission, includingsettings.system. Beyond that, it depends on whether a tenant resolver is registered:tenantsmodule): a user whose record carries atenant_idis never a platform admin, even with theadminrole. On that setup the globaladminrole is the only admin role a tenant's operator can have, so the role alone can't tell the two apart.tenantsresolver: tenant roles arrive astenant:<role>andadminstays a platform role, even while that user is working inside an organisation.multi_tenantis off.Permission required: settings.system. A permission check that fails first still gives its usual 401 or 403.Deliberate limits
/resolvefalls back to system values. Resolving your own keys still ends in system-scope values, as before. That is the configuration the caller already runs under, and secrets stay masked. This is documented onrequire_resolvable.settings.view. Settings registration runs before the host'smulti_tenantvalue is available, and requiringsettings.systemeverywhere would hide the entry from custom single-tenant roles that can still use the screen. On a multi-tenant host, a tenant user who hassettings.viewsees the entry, and the screen answers with a 403 that namessettings.system.brandingwrites system-scope settings through its own API, which checks onlybranding.manage. That is the same class of bug, and branding: no table to add a mixin to — it's a single process-global settings singleton that must become per-tenant lookup #373 tracks it.Testing
modules/settings/tests/test_settings_tenant_scope.py, 26 tests:resolve, list, id-based routes, module-settings API and screens, including their write actions;tenantsresolver path, withtenant:ownermapped to the settings permissions.ruff,tyand the 300-line check are clean.resolvedocstring and the extra write-path tests, both added.