Pointsman separates who can administer a resource from which models and limits apply to an application request. Both use scopes, but they have different decisions and different defaults. Authentication establishes identity first; a valid login or virtual key does not bypass authorization.
Defaults: scoped grants decide administrative authorization. Application requests use the fixed organization → application → user → key policy path. The stored scope-tree request path is separately opt-in; creating a team, adding an admin grant or viewing a preview does not enable it.
Architecture and responsibilities#
| Component | Question it answers | Authority |
|---|---|---|
| Authentication | Who is calling, and is their credential valid? | Admin login/session or virtual-key identity resolution |
| Scoped authorization | May this administrator perform this action at this target? | Active principal, applicable grants, target ancestry and required role |
| Request policy composition | Which models may this credential use? | Effective allow-list from the selected request path, then registry lifecycle/capability checks |
| Limit evaluation | May this request consume more requests, tokens, spend or concurrency? | Matching limit rules and the counter/reservation machinery |
| Python policy service | What routing parameters have been learned from outcomes? | Background learner; it does not decide scope grants or administrator access |
The authorization core and policy composer run inside the Java gateway. PostgreSQL stores principals, scopes, grants and policy values. Request policy snapshots and cached identity bundles reduce database work; administrative writes trigger the appropriate snapshot refresh/invalidation. There is no remote authorization-service call to the Python learner.
What is enabled by default#
These are the application defaults when the deployment does not override them. Inspect the effective process configuration before relying on an environment-file comment.
| Setting | Default | Effect |
|---|---|---|
POINTSMAN_AUTHZ_CORE_DECIDES / pointsman.authz.core.decides |
true |
Existing administrative gates use the scoped core when their principal and target can be resolved; compatibility fallback is described below |
POINTSMAN_SHADOW_AUTHZ_ENABLED / pointsman.shadow.authz.enabled |
true |
When core deciding is disabled, compare its answer with the old gate without changing that gate's result |
POINTSMAN_DATAPATH_TREE_ENABLED / pointsman.datapath.tree.enabled |
false |
Application requests use the fixed chain; setting it true selects the credential's stored scope path and tree model composition, subject to fallback |
POINTSMAN_SHADOW_ALLOW_ENABLED / pointsman.shadow.allow.enabled |
false |
Optional comparison of fixed-chain and tree allow sets; comparison alone never enables enforcement |
The native /admin/scopes endpoints authorize directly against principals and grants. Turning off AUTHZ_CORE_DECIDES changes the compatibility seam for existing administrative gates; it does not turn native scope endpoints into legacy role checks. Turning off shadow comparison does not disable authorization.
The model: identities live outside the scope tree#
A principal is an actor with an ID, tenant association and status. Current administrative accounts project into principals using the same account ID. A credential is an API key, with its own identity, expiry and scope home. Application user IDs and administrative principal IDs are not interchangeable.
A scope is a place to attach authority and controls. Its kinds are structural:
PLATFORM
└── TENANT: Acme
├── GROUP: ML team
│ └── GROUP: Search project
└── GROUP: Support team
The platform root is unique; a tenant sits directly below it. Below a tenant, authored nodes are groups. Labels such as team, project or environment do not introduce new authorization roles. Principals can hold grants at several scopes; they are not extra containment nodes. Credentials point to a home, rather than adding a fourth tree level.
The scope creation API infers kind from the parent. It requires OWNER at that parent, rejects duplicate sibling slugs and unknown parents, and defaults to at most two levels below a tenant through pointsman.scopes.max-depth-below-tenant. The limit is configurable; responses warn beyond three levels. Current scope ancestry is immutable: there is no supported reparent operation.
Organizations and applications retain their existing APIs and mirror into tenant and group scopes. Account roles and workspace memberships project into grants. For example, a platform ADMIN account projects to OWNER at the root; a TENANT_ADMIN projects to ADMIN at its tenant; workspace administrator/developer membership projects at the application's scope. These legacy account labels are distinct from the four core grant roles.
Grants: authority flows down and combines by strength#
A grant associates principal_id, scope_id, role and optional expires_at. Stored records also retain their author, creation time and revocation. Roles have this explicit order:
VIEWER < DEVELOPER < ADMIN < OWNER
Each administrative operation declares its required role. For native scope APIs, reading a path, grants, policy or preview requires VIEWER; writing policies or limit rules requires ADMIN; creating child structure requires OWNER. Grant delegation has its own rules below. These roles do not grant inference models by themselves.
For an ordinary scoped permission check, the core:
- Denies a principal whose status is not
ACTIVE. - Enforces the tenant boundary. Crossing it requires a live grant at the platform root; the required-role check still applies.
- Considers only this principal's unrevoked, unexpired grants at the target or an ancestor. A grant expiring exactly at the decision time is expired.
- Takes the strongest applicable role. No applicable grant means denial.
- Allows only if that role meets the operation's required role.
A grant at ML team reaches Search project, but not Support team, Acme itself or another tenant. An ADMIN grant at ML team plus a VIEWER grant at Search project still yields ADMIN at Search project. A weaker child grant cannot remove inherited power; there is no negative or deny grant. Revoke or reduce the relevant ancestor authority to remove it. Removing one grant does not remove other applicable grants, including projected memberships.
Delegation and revocation#
Delegation depends on the actor's strongest applicable role at the target. The actor must also be active and have authority on that target's ancestry.
| Actor role | Roles they may grant |
|---|---|
VIEWER |
None |
DEVELOPER |
VIEWER |
ADMIN |
DEVELOPER, VIEWER |
OWNER |
Any role, including OWNER |
Revocation normally requires the standing to grant that role. An actor may also revoke a grant they authored if their current applicable role is at least that grant's role. Authorship alone is insufficient after losing the necessary authority. The API checks tenant association when creating grants: tenant principals receive grants within their tenant; platform staff act through root grants. Revocation marks the stored grant rather than deleting its audit history.
Policies: model permissions only narrow#
The generic policy-value vocabulary currently contains one key, allowed_models, with the INTERSECT operator. It is not an arbitrary expression language. Budgets, rate limits and guardrails have their own rule/configuration machinery; they are not additional implemented keys in this table.
A policy value has an attachment scope, subject kind, optional subject ID, key and non-empty value list:
| Subject kind | Stored ID | Meaning and current boundary |
|---|---|---|
SCOPE |
Omit subject_id |
Applies at this scope and descendants through composition |
PRINCIPAL |
principals.id |
Stored and previewable for that administrative principal; not consumed as a general principal override by the live request composer |
CREDENTIAL |
api_keys.id |
Credential-specific restriction; the live tree composer takes the first matching credential row on the root-to-home path |
The write API validates subject IDs, key vocabulary and non-empty, non-blank values. A missing row means no additional restriction, not deny-all. An explicit empty policy list is rejected. However, the result of intersecting restrictions can be empty, in which case no model is permitted. Existing explicit-empty legacy user/key contributions also remain deny-all, rather than becoming unrestricted during composition.
For example, model restrictions narrow at each level:
| Scope | Authored policy | Effective models |
|---|---|---|
| Tenant | {model-a, model-b, model-c} |
{model-a, model-b, model-c} |
| Team | {model-a, model-b} |
{model-a, model-b} |
| Project | {model-b} |
{model-b} |
Omitting the team row leaves tenant/project restrictions intact. A child cannot add model-c back. Creation rejects entries excluded by the relevant scope composition; a later ancestor change can still make descendants incompatible. Review descendants when tightening an ancestor.
All levels silent means unrestricted by this allow-list, not unrestricted access to every provider. Registry lifecycle, successor rules, capabilities, credential validity, budgets and guardrails still constrain execution. Both request paths use the same registry post-processing after obtaining the raw allow set.
Preview versus live composition#
GET /admin/scopes/{id}/policy/preview composes scope rows root-first and optionally all rows for the requested principal or credential along that path. It returns unrestricted, value, conflict_empty and per-level provenance: the contributed value, entries removed and whether that level removed entries (binding). The first restriction can constrain the result without removing entries from a prior finite set, so binding=false does not necessarily mean irrelevant.
enforced_on_request_path reports only the tree-path feature flag. It is not a per-request receipt, a fallback check or a guarantee that every subject preview matches live traffic. In the current live composer:
- Scope rows along the selected path participate.
- Only the first matching credential row in root-first order participates; the subject preview can compose several. Do not rely on stacked credential rows at multiple ancestors as cumulative live restrictions.
- The application user's legacy allow-list is folded in from the identity bundle. Arbitrary stored
PRINCIPALrows are not loaded into live request composition. Do not use that preview as proof of an enforced user restriction.
These are implementation boundaries even with the tree flag enabled. For current per-user enforcement, use the supported user allow-list workflow. For inherited team restrictions on the optional tree path, use scope policies and verify actual key behavior as well as previews.
Fixed four-level path versus stored scope path#
| Aspect | Fixed path — default | Stored tree path — opt-in |
|---|---|---|
| Identity | Authenticated organization/application/user/key bundle | Same authenticated bundle, plus the credential's stored scope ancestry |
| Ancestry used by limit rules | Derived platform/organization/application scopes; user/key rules use subject conditions | Stored home to root, so authored team/project ancestors can participate |
| Raw model permissions | Intersect explicit organization, application, user and key allow-lists; missing levels inherit | Scope policy rows, first matching credential policy and computed legacy user contribution |
| A key homed in a new team | Stored home is retained, but that team's inherited controls do not become live merely by creating it | The home/ancestors participate when resolved and the tree composition succeeds |
| Administrative grants | Scoped core remains enabled independently | Same administrative authorization; the data-path switch does not change grant strength |
A service key can omit a user or application; the fixed four-level model is not a requirement that every request have all four objects. Omitting scope_id when issuing a key chooses its application/organization mirror. An explicit authored home is validated against the issuing tenant/application boundary. Key-owned restrictions remain distinct from restrictions inherited at a deeper home; a dormant team policy must not be confused with the key's own limits.
The optional tree path resolves ancestry when building the identity bundle and carries it with cached identity state. It does not query the scope database at every admission step. Policy values are read from an in-memory snapshot. This keeps request work bounded, but requires correct projection, refresh and identity invalidation; it is not an instantaneous globally consistent policy engine.
Administrative API workflow#
Use team hierarchy for organization/account/key setup and admin API reference for surrounding endpoints. These examples show request bodies; replace model names and IDs with values from your deployment. Admin authentication and CSRF/origin requirements still apply.
1. Create a group#
Use POST /admin/scopes (requires OWNER at its parent):
{"parent_id":"<tenant-scope-id>","name":"ML team","slug":"ml-team"}
2. Delegate access#
Use POST /admin/scopes/{teamScopeId}/grants:
{"principal_id":"<admin-account-id>","role":"ADMIN"}
Omit expires_at for no expiry, or supply an ISO-8601 instant. An existing unrevoked grant for the same principal and scope is a conflict, even if expired; changing that grant requires revoke then grant. List grants with GET /admin/scopes/{id}/grants; revoke through DELETE /admin/scopes/grants/{grantId}.
3. Restrict models#
Use POST /admin/scopes/{teamScopeId}/policy (requires ADMIN):
{"subject_kind":"SCOPE","key":"allowed_models","value":["model-a","model-b"]}
The field is value, not entries. Omit subject_id for SCOPE. A credential row uses subject_kind: "CREDENTIAL" and its API-key ID; a principal row uses "PRINCIPAL" and its principal ID, subject to the live limitations above.
4. Preview and verify#
Read scope policy with GET /admin/scopes/{id}/policy. Preview scope composition at GET /admin/scopes/{id}/policy/preview?key=allowed_models; optionally add both subject_kind=CREDENTIAL and subject_id=<key-id> (or PRINCIPAL and its ID). There is no policy PUT: a duplicate scope/subject/key returns 409; replacement requires delete then create. Those two calls are not atomic, so removing a restriction can temporarily broaden access. Plan changes accordingly rather than treating replacement as a single transaction.
Failure boundaries and rollout checks#
| Condition | Current behavior |
|---|---|
| Inactive principal, expired grants, insufficient role or wrong tenant | Core authorization denies |
| Native scope endpoint cannot find a projected principal | Denies; no legacy fallback on that surface |
| Existing admin gate cannot compare because a principal/scope is missing, or its core evaluation errors | Compatibility seam uses the old gate's answer and records fallback; do not describe this as universal fail-closed core authorization |
| Tree path has no resolved home | Admission falls back to the derived fixed path and records no_resolved_path |
| Tree allow composition finds a projection gap or throws | Admission falls back to the fixed allow calculator and records policy_gap or error; authored-only restrictions are not guaranteed during that fallback |
| Effective model set is empty | Admission denies; it does not widen to the catalog |
Before opting into the tree request path, inspect the credential's actual ancestry, projected rows and refresh state. Check scope and subject previews, then validate allowed and denied requests for representative keys in an isolated environment. Include cross-tenant access, ancestor changes, expiry, revoke/delegation, key-owned limits and fallback behavior. Monitor the pointsman.authz.core.fallback, pointsman.datapath.tree.fallback and comparison counters; a healthy process alone does not prove the intended policy decided.
The qualified release scope does not turn every optional tree/subject configuration into a qualified deployment. This guide documents current behavior and its limits; it changes no runtime flags or acceptance thresholds.
Implementation anchors: gateway core/authz/Authz, core/grant/Role, core/scope/ScopePath, core/store/CoreProjector, core/policy/PolicyComposer, core/shadow/ShadowAuthz, identity/AllowSetShadow, pipeline/RequestAdmission and api/admin/ScopeAdminController. Continue with authentication, rate limiting, security and the separate routing policy service.