Skip to content

Commit 8dd5dab

Browse files
authored
feat(decisioning): create_tenant_store — opinionated multi-tenant AccountStore with fail-closed isolation (#473)
* feat(decisioning): create_tenant_store — opinionated multi-tenant AccountStore with fail-closed isolation Ports JS @adcp/sdk@6.7's createTenantStore. The headline 6.7 helper: an AccountStore with a per-entry tenant gate baked in so cross-tenant entries never reach adopter code on upsert/sync_governance. Fail-closed: when resolve_from_auth(ctx) returns None (unauthenticated or unregistered principal), every entry rejects with PERMISSION_DENIED and list() returns []. resolve() also enforces the gate on Path-1 (operator-routed) calls — cross-tenant refs return None, hiding the existence of accounts the caller can't see. Immutability: gate methods are class-level and the store class uses __slots__, so adopter code that tries store.upsert = custom_handler gets AttributeError instead of silently bypassing isolation. Closes #457. Part of #452. * fix(tenant-store): Protocol conformance on resolve + per-entry callback isolation Address code-review BLOCKER and security-review SHOULD-FIX items: - resolve(ref, ctx) → resolve(ref, auth_info=None) matching the AccountStore Protocol. Adopter resolve_by_ref(ref, ctx) callback unchanged — ResolveContext is synthesized inside the public method. - Per-entry try/except inside upsert and sync_governance: a single callback exception no longer poisons the rest of the batch. Failed entries surface as PERMISSION_DENIED on the wire (no exception text leaked) and are logged server-side via logger.warning(exc_info=True). - list now catches tenant_to_account exceptions and returns [] — honoring the docstring's MUST-NOT-RAISE contract. - resolve Path 1 catches resolve_by_ref exceptions, returns None + logs WARNING (so a single adopter raise doesn't 500 the request). - Added tests for: AccountStore isinstance check, dispatcher kwarg call shape, four callback-raises cases. - Class-level immutability docstring note added (instance immutability is enforced via __slots__; class-level monkey-patching is possible but the leading-underscore + non-export keep _TenantStore out of adopter code paths).
1 parent defa022 commit 8dd5dab

3 files changed

Lines changed: 1235 additions & 0 deletions

File tree

src/adcp/decisioning/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ def create_media_buy(
167167
TaskRegistry,
168168
TaskState,
169169
)
170+
from adcp.decisioning.tenant_store import create_tenant_store
170171
from adcp.decisioning.translation import (
171172
TranslationMap,
172173
create_translation_map,
@@ -318,6 +319,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
318319
"create_adcp_server_from_platform",
319320
"create_oauth_passthrough_resolver",
320321
"create_roster_account_store",
322+
"create_tenant_store",
321323
"create_translation_map",
322324
"create_upstream_http_client",
323325
"require_account_match",

0 commit comments

Comments
 (0)