Skip to content

feat(server): createTenantStore — opinionated multi-tenant AccountStore with fail-closed isolation #457

@bokelley

Description

@bokelley

Parent: #452
Depends on: #456 (AccountStore.sync_governance typed surface)

JS source

.changeset/create-tenant-store.md (6.7) — the headline 6.7 helper.

Problem solved

Resolves the class of bug "adopters routing by wire-supplied operator without cross-checking the auth principal could write across tenants." This is solved by baking the per-entry tenant gate INTO the framework — cross-tenant entries never reach adopter code.

Proposed API

from adcp.decisioning import create_tenant_store

store = create_tenant_store(
    resolve_by_ref=lambda ref: ...,                    # AccountStore.resolve
    resolve_from_auth=lambda ctx: tenant_id_or_none,   # tenant from auth principal
    tenant_id=lambda account: account.tenant_id,       # extract tenant from Account
    tenant_to_account=lambda tenant_id: account_or_none,
    upsert_row=lambda row, ctx: ...,                   # optional adapter hook
    sync_governance_row=lambda row, ctx: ...,          # optional adapter hook
)

Fail-closed semantics

  • If resolve_from_auth(ctx) returns None, every entry fails PERMISSION_DENIED regardless of operator.
  • accounts.upsert and accounts.sync_governance on the returned store are immutable (Python equivalent of JS's writable: false) so adopters can't override and bypass the gate.
  • Adopters with genuine custom needs use composition or write a plain AccountStore.

Acceptance criteria

  • create_tenant_store exported from adcp.decisioning.
  • Fail-closed test: resolve_from_auth returns None → every entry rejected with PERMISSION_DENIED.
  • Cross-tenant test: principal's tenant != entry's tenant → rejected before reaching upsert_row/sync_governance_row.
  • Immutability test: attempting to override accounts.upsert on the returned store raises.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions