Skip to content

feature_flags: tenant overrides already modeled, but no request-time dependency resolves the active tenant #375

Description

@antosubash

Why

FeatureFlagOverride (modules/feature_flags/feature_flags/models.py:32) already has a working tenant scope and per-tenant admin endpoints (endpoints/api.py:87-137), and unlike settings those endpoints are correctly gated behind PERM_FEATURE_FLAGS_MANAGE/VIEW as a platform-admin, cross-tenant screen — so the schema and admin surface need no change. What's missing is any FastAPI dependency that resolves is_enabled(name, tenant_id=<active tenant>) from a request, so as soon as any module wants to gate behavior on a per-tenant flag at request time, there is no supported way to do it correctly (a naive call would forget tenant_id and silently fall back to system-only).

Tables

  • FeatureFlagOverride (models.py:32) — adopt MultiTenantMixin: no, same reasoning as settings.Setting: it is a self-scoping (scope, scope_id, name) table by design (models.py:8-10 docstring), and scope_id already carries the tenant id when scope="tenant".
    • UniqueConstraint("scope", "scope_id", "name", name=UQ_OVERRIDE_SCOPE_NAME) (models.py:36) — already correct per-tenant; no change needed, not an SM024 case.

Code paths

  • FeatureFlagRegistry.is_enabled(name, tenant_id=None) (framework/core/simple_module_core/feature_flags.py:50-52, tenant_override at :72-74) is an in-memory, process-global registry keyed by (name, tenant_id) — correct data structure, but no call site in the codebase passes a request-resolved tenant_id (confirmed: is_enabled( only appears in this module's own tests and the framework's own unit test, framework/core/tests/test_feature_flags.py). There is no RequiresFeatureFlag-style FastAPI dependency analogous to RequiresPermission that reads app.state.tenant_resolver's result and calls is_enabled(name, tenant_id=<resolved>). Any module that starts gating a route on a flag today would have to thread the active tenant through itself, and would silently get system-only behavior if it forgot — a fail-open in the feature sense (not the tenancy-isolation sense) rather than a hard error, which is easy to miss in review.
  • Admin endpoints (list_flags_for_tenant, set_tenant_override, clear_tenant_override — endpoints/api.py:87-136) take tenant_id from the URL path and are gated by PERM_FEATURE_FLAGS_MANAGE/VIEW; confirm (out of this module) that those permissions are platform-admin-only, since a tenant-scoped admin holding a module-local "manage settings" style permission must not be able to toggle other tenants' flags through this path the way settings' bug currently allows for Setting.
  • hydrate_registry (service.py:180-189) loads every override (all scopes, all tenants) into the process-global registry at boot via list_overrides() (service.py:54-63) — this is correctly cross-tenant (registry must hold every tenant's overrides to serve is_enabled for any of them) and needs no all_tenants() wrapper change, but should be called out as an intentional platform-wide read so a future reviewer doesn't "fix" it into a tenant-scoped query and break every other tenant's flags.
  • No file/object storage paths, no caches keyed without tenant (the registry dict is already tenant-keyed), no public/anonymous routes.

Migration

  • None. No schema change; scope_id already holds tenant ids for scope="tenant" rows and the unique constraint is already per-tenant.

Tests

  • Add a RequiresFeatureFlag-equivalent dependency (or document the pattern) and test that a route gated on a tenant-scoped flag: (a) resolves the active tenant from the request, (b) returns the tenant's own override when set, (c) falls back to system value, then default, when no tenant override exists, matching list_flags's documented precedence (service.py:75-110).
  • Cross-tenant admin isolation: verify (independent of this module, but exercised through it) that a tenant-scoped caller cannot call PUT /tenant/{other_tenant_id}/{name} — this depends on how PERM_FEATURE_FLAGS_MANAGE is scoped elsewhere; add the test here regardless so a future permission-scoping regression is caught at the endpoint level.
  • Registry hydration at boot still loads every tenant's overrides correctly under multi_tenant=True with no request context (exercises all_tenants()-equivalent boot-time behavior).

Out of scope / open questions

  • Whether PERM_FEATURE_FLAGS_MANAGE should be split into a platform-admin variant and a tenant-admin variant (so a tenant admin could manage only their own tenant's flags through a tenant-facing screen) is a product question outside this module's current single-permission model.
  • No RequiresFeatureFlag dependency exists anywhere in the framework today — creating one is arguably a simple_module_hosting framework change, filed here because feature_flags owns the registry it would wrap.

Context: part of the SaaS tenancy work in #370 (fail-closed isolation, tenant_context()/all_tenants(), the tenants module). Design: docs/plans/2026-09-27-saas-tenancy-design.md and docs/framework/multi-tenancy.md on that branch.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions