Skip to content

feat(authz): support 'unscoped' wildcard sentinel in /v1/authz/search#303

Open
smoreinis wants to merge 4 commits into
mainfrom
feat/authz-search-wildcard-sentinel
Open

feat(authz): support 'unscoped' wildcard sentinel in /v1/authz/search#303
smoreinis wants to merge 4 commits into
mainfrom
feat/authz-search-wildcard-sentinel

Conversation

@smoreinis

@smoreinis smoreinis commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lets an auth provider answer POST /v1/authz/search with the wildcard sentinel { "unscoped": true } to mean all resources of this type, which the Agentex authorization proxy maps onto the existing unscoped path (None → no id IN (...) filter) instead of an enumerated id list.

  • Proxy (adapter_agentex_authz_proxy.py): recognize unscoped: true and return None; otherwise pass items through unchanged.
  • Port (port.py): widen list_resources return type to Iterable[str] | None.
  • Contract doc (auth_provider_contract.md): formalize the unscoped sentinel in the search response schema, the inclusion-filter note, and the endpoint summary.
  • Tests: unit coverage for the four cases.

Motivation

A provider that grants broad access (e.g. a single-account "allow everything" authorization model) previously had to enumerate every accessible id, because items: [] hides everything and there was no "all resources" signal in the contract. Enumeration couples the provider to Agentex's storage and scales poorly on large accounts. The sentinel lets such a provider answer search statelessly.

Design notes

  • Explicit boolean chosen over overloading items: null: a provider must deliberately opt into "everything", and a malformed response missing both fields raises rather than silently failing open.
  • No downstream changes. AuthorizationService.list_resources already returns Iterable[str] | None, and every consumer (DAuthorizedResourceIds, the list routes, the Postgres list filter) already treats None as "no filter" — the path taken when authorization is disabled. The sentinel reuses that exact, already-tested path.
  • The sentinel is opt-in and backward compatible: existing providers returning an items array behave exactly as before.

Test plan

  • New unit tests (tests/unit/adapters/authorization/test_adapter_agentex_authz_proxy.py):
    • { "items": ["a","b"] }["a","b"]
    • { "unscoped": true }None
    • { "unscoped": true, "items": ["x"] }None (unscoped wins)
    • { "items": [] }[] (sentinel not triggered; still hides everything)
  • Existing authz unit suite passes (no regression).

Greptile Summary

Adds an unscoped: true wildcard sentinel to POST /v1/authz/search so auth providers can signal "all resources of this type" without enumerating IDs, mapping it onto the existing None (no-filter) path that authorization-bypass already uses.

  • Adapter (adapter_agentex_authz_proxy.py): a single strict is True identity check intercepts the sentinel before falling through to response["items"]; malformed responses missing both fields still raise KeyError (fail-closed), and the comment explains why .get is intentionally avoided in the normal path.
  • Port (port.py): list_resources return type widened to Iterable[str] | None; all existing consumers (authorization_service, list routes, Postgres filter) already treat None as "no filter" — no downstream changes required.
  • Tests: six cases covering normal list, sentinel, unscoped-wins-over-items, empty-items, truthy-non-boolean unscoped, and the fail-closed KeyError invariant.

Confidence Score: 5/5

Safe to merge — the change is additive and backward compatible, with strict identity checking to prevent unintentional sentinel activation and fail-closed behavior preserved for malformed responses.

The sentinel check uses is True (not a truthy test), so only an explicit JSON boolean true triggers the unscoped path. The fallback to response["items"] (direct key access) preserves the existing fail-closed contract. All downstream consumers already handle None as "no filter". The test suite covers every branch including the malformed-response KeyError invariant. No changes to consumers, migrations, or data models are needed.

No files require special attention.

Important Files Changed

Filename Overview
agentex/src/adapters/authorization/adapter_agentex_authz_proxy.py Adds strict is True sentinel check for unscoped before falling through to response["items"]; fail-closed behavior (KeyError on malformed response) is preserved and documented.
agentex/src/adapters/authorization/port.py Widens list_resources return type from Iterable[str] to `Iterable[str]
agentex/tests/unit/adapters/authorization/test_adapter_agentex_authz_proxy.py Six test cases cover all sentinel branches: normal list, unscoped: true, unscoped wins over items, empty items, truthy-but-not-boolean unscoped, and fail-closed KeyError on malformed response.
agentex/docs/docs/development_guides/auth_provider_contract.md Documents unscoped sentinel in the response schema, the inclusion-filter note, and the endpoint summary table; accurate and consistent with the implementation.
agentex/tests/unit/adapters/authorization/init.py Empty __init__.py to make the new test directory a Python package.

Sequence Diagram

sequenceDiagram
    participant Client
    participant AgentexProxy as AgentexAuthorizationProxy
    participant AuthProvider as Auth Provider (external)
    participant Consumer as list_resources consumer

    Client->>AgentexProxy: list_resources(principal, resource_type)
    AgentexProxy->>AuthProvider: POST /v1/authz/search
    AuthProvider-->>AgentexProxy: "200 {items: [a, b]}"
    AgentexProxy-->>Consumer: [a, b]  (inclusion filter)

    Client->>AgentexProxy: list_resources(principal, resource_type)
    AgentexProxy->>AuthProvider: POST /v1/authz/search
    AuthProvider-->>AgentexProxy: "200 {unscoped: true}"
    Note over AgentexProxy: response.get(unscoped) is True → return None
    AgentexProxy-->>Consumer: None  (no id filter — all resources)

    Client->>AgentexProxy: list_resources(principal, resource_type)
    AgentexProxy->>AuthProvider: POST /v1/authz/search
    AuthProvider-->>AgentexProxy: "200 {unexpected_key: val}"
    Note over AgentexProxy: response[items] raises KeyError (fail-closed)
    AgentexProxy--xConsumer: KeyError
Loading

Reviews (2): Last reviewed commit: "test(authz): address greptile review fin..." | Re-trigger Greptile

@smoreinis smoreinis marked this pull request as ready for review June 11, 2026 20:52
@smoreinis smoreinis requested a review from a team as a code owner June 11, 2026 20:52
- Add fail-closed test: response missing both 'unscoped' and 'items' raises KeyError
- Use an unambiguous truthy-non-boolean value (1) in the strict-sentinel test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant