Namespace IAM Policy Flow
Overview
Authenticated callers use the OCC API or CLI to list, read, or create Namespace Roles, exact AccessBindings, and ServicePrincipals, and to delete Roles and AccessBindings. OCC checks authority and Namespace ownership before the selected IAM Driver writes policy. Policy and audit commit in the same State transaction. Phase 6 covers configured policy, Group membership, Owner bindings, and deploy-time grants.
Entry Points
- Trigger:
GET,POST, orDELETEunder/namespaces/:namespaceId/iam/* - Trigger: controller startup with an
iamconfiguration,POST /api/auth/accounts, each create in a Namespace, andPOST /namespaces/:namespaceId/agents/:agentId/deploy - Source:
packages/contracts/src/api/routes.ts:occApiRoutes - Source:
apps/controller/src/index.ts:requiredPermissions - Source:
apps/controller/src/http/iam.ts:iamHandlers - Assumptions: the caller is admitted to the Installation, the selected IAM Driver implements Namespace policy management, and the requested Namespace already exists.
Flow
graph TD
A["Caller invokes Namespace IAM route"] --> B["OCC admits identity and required permissions"]
B --> C{"Read or mutation?"}
C -->|read| D["Controller asks selected IAM Driver to read Namespace policy"]
C -->|create/delete| S{"Human-session caller?"}
S -->|yes| L{"Assigning session current?"}
L -->|yes, held through transaction| CFI["Hold shared IAM configuration lock"]
L -->|revoked or expired| X["Roll back and deny"]
S -->|no| CFI
CFI --> E["Controller validates Role, subject, and exact target"]
E --> T{"IAM Driver joins transaction?"}
T -->|yes| F["Selected IAM Driver mutates platform IAM policy"]
T -->|no| U["Reject with 503 before policy write"]
F --> G["Controller appends audit event in the same transaction"]
D --> H["API returns policy metadata"]
G -->|COMMIT validates held session| H
G -->|held session expired| X
M["Startup applies the iam configuration"] --> P["IAM Driver writes configured Roles, Groups, and Group bindings"]
N["Caller creates a resource in an existing Namespace or deploys an Agent"] --> AC["OCC authorizes the exact operation under the Namespace lock"]
AC --> O["OCC writes the Owner binding or missing Agent operate grants"]
P --> Q["Policy and audit commit in the same transaction"]
O --> QExecution Trace
1. Route admission and permission selection
apps/controller/src/index.ts:requiredPermissions
apps/controller/src/index.ts admits and resolves the caller before dispatch.
OCC uses the selected IAM Driver to check Installation administer and exact
Namespace read. Delegation requires both checks even when the caller can
access the target.
2. Role and AccessBinding commands reach the controller
apps/controller/src/http/iam.ts:iamHandlers
List and read call OpenClawController IAM methods and return policy metadata.
Create and delete use transactIAM and return after policy and attributable audit
commit. Authentication wraps controller.transact for human sessions. It holds the
session before the command and checks expiry after the command and audit append.
For guarded profiles,
packages/occ/src/state/human-authentication.ts:PostgresHumanAuthentication.holdSession
validates the bound account and sign-in method through the actor guard.
PostgreSQL locks the user in shared mode before locking the session. OCC then
takes the shared IAM configuration lock before Namespace and account locks.
Session holds preserve policy transaction timeouts. Account-management operations
keep exclusive locks, a five-second lock limit, and a ten-second statement limit.
Independent sessions can write different Namespaces concurrently. Logout and
account or method revocation wait for protected writes, and a revocation that
finishes first prevents the write. Expiry during the write or at server-side
COMMIT validation rolls back policy and its success audit. The memory adapter serializes writes
with local logout and rechecks the session inside the transaction. Non-session
credentials use their usual admission path.
Mutation events record Installation administer in authorization. Role
events name the Namespace and include roleId and permissions. AccessBinding
events name the bound target, including the Namespace for a Namespace binding,
and include bindingId, subjectKind,
subjectId, and roleId. Deletion reads the removed row in the same
transaction for audit. ServicePrincipal creation takes an empty body and
audits the Namespace and servicePrincipalId. The non-Agent identity belongs
to and appears only in its Namespace, starts without grants, and gains access
through an AccessBinding. The service key flow issues and
revokes its keys. The identity has no delete route.
3. OCC validates policy ownership
packages/occ/src/index.ts:holdIAMPolicyAuthority
packages/occ/src/index.ts:createIAMAccessBinding
Each Role and AccessBinding mutation calls holdIAMPolicyAuthority, which takes
the shared configuration fence before the Namespace lock and final checks.
A configured-read withdrawal therefore commits before authorization or after
the policy write. A denied write saves no policy change or success audit.
Configured Roles still cannot grant Installation permissions.
Role creation accepts only nonempty, duplicate-free permissions for Namespace
resource kinds; namespace permissions support only read. iamRolePermissions
also refuses action/kind pairs outside SUPPORTED_PERMISSION_ACTIONS (contracts),
because no operation checks them, and then any create Permission, because create
is checked on the Namespace and this API binds only exact resources
(NAMESPACE_POLICY_CREATE_REASON). AccessBinding creation accepts identity subjects and exact
targets in the same Namespace, including the Namespace itself when the target
ID matches the path Namespace. OCC verifies the target resource exists and that
the caller can read it before asking the IAM Driver to create the binding.
assertAccessBindingRoleApplies then refuses, with 400, a Role that has no
Permission for the target's kind, or a create Permission stored before Role
creation refused them, because evaluation would drop those grants.
4. The IAM Driver persists or reads policy
packages/iam/src/index.ts:NativeIAMDriver
The native IAM Driver uses the platform policy repository. Before calling a
mutation method, OCC returns 503 if the selected Driver lacks
namespacePolicyTransaction: "platform-unit-of-work". Reads need no declaration.
The native Driver rejects missing Roles, cross-Namespace targets, unsupported
subjects, duplicate IDs, referenced Role deletion, and unknown exact bindings.
HTTP maps IAMPolicyValidationError to 400 INVALID_REQUEST with the offending
field as the detail path for invalid Role or binding input. Deleting a referenced Role raises IAMRoleInUseError
(409 RESOURCE_CONFLICT). Human Principals can receive bindings directly.
ServicePrincipal subjects must belong to the exact Namespace.
PostgresPlatformState.loadNativeIAMState reuses the original transaction inside
a State callback, so authorization sees pending grants and removals. Outside a
callback it opens a read transaction. Escaped work fails on the closed
transaction after commit, rollback, or an unknown outcome, without obtaining a
new client. Phases 2 and 3 describe write ordering. Human HTTP writes use
holdAuthenticationSession in the original PostgreSQL unit.
readNativeIAMState caches a policy snapshot per transaction. A read-only
REPEATABLE READ unit reuses it for every decision and lookup, including every
item in a list. A writing unit reuses it until the next statement, so IAM
writes and locks precede the next read. Decisions sharing a snapshot miss
intervening commits. freezeSnapshot makes it immutable. The native Driver
validates and indexes it once by ID and issuer/subject for identities, by
subject for bindings, and by action and kind for Restrictions. The next unit
reads fresh policy.
packages/occ/src/index.ts:OpenClawController.updateAgent checks Agent update
again after locking the Namespace. Namespace IAM writes take the same lock.
If a binding withdrawal commits first and the caller has no other applicable
update grant, the Agent save returns 403 and leaves the draft unchanged.
5. Platform state commits policy and audit together
packages/occ/src/state/postgres-state.ts:PostgresPlatformState
PostgreSQL commits Role and AccessBinding writes with API audit. An unchanged-row
UPDATE locks a held session, preserving its lifetime and authentication metadata.
It queues the deferred occ.require_held_authentication_session constraint to
check expiry with the database clock at COMMIT, including when the client delays
COMMIT after its final authentication check. Expiry raises
held_authentication_session_is_current. State maps it to 403, rolls back
policy and success audit, and the API audits the denial separately.
On unknown commit, State discards the connection without another query. OCC
reports dependency failure. The caller must neither assume rollback nor replay
the mutation. Later requests read current policy through the IAM Driver.
The Namespace lock serializes grant creation with Namespace deletion. Exact
targets retain their deletion locks. Deleting a
target removes its bindings in the same transaction. Identity foreign keys
protect bindings without expanding application-role privileges.
packages/occ/src/iam-policy-cleanup.ts uses the caller's transaction for
deletion audit projections and Namespace policy removal.
Both adapters apply one subject rule on every AccessBinding write: a human
without a Namespace, a non-Agent ServicePrincipal of the exact Namespace, or the
ServicePrincipal of a live Agent there. PostgreSQL checks the owning Agent in
the same query because its Agent owner key is deferred to commit. The in-memory
adapter resolves subjects live through its resolveIAMIdentity lookup, so
humans enrolled after construction can be bound, then falls back to identities
provisioned at construction. Agent-owned ServicePrincipals resolve only through
its current Agent state.
State also provides an opt-in Installation authority and native-IAM barrier for an original transaction. Its SQL supplier is unregistered, and the Namespace routes above do not use it. It does not protect these routes until the selected account, session, and policy writers join the same protocol.
6. Default users, owners, and deploy-time grants
packages/occ/src/index.ts:OpenClawController.bindCreator
Default users defines the behavior.
- Configured policy. The compositions call
applyIAMConfigurationat startup, and the bootstrap route after bootstrap. It takes the IAM configuration lock exclusively.configuredPolicyturns theiamconfiguration intorole_config_,group_config_, andbinding_config_rows. The Driver'sapplyConfiguredPolicycompares them withlistConfiguredPolicy, writes missing rows, updates changed Roles, and deletes unlisted rows, but refuses to delete a Role that a binding uses.createNamespacepasses itsnamespaceIdto write only what it lacks.auditConfiguredPolicyrecords each change. - Membership. The
createAuthAccountroute addscontroller.defaultUserGroupIdsto the account seed and its audit event asgroupIds.insertNativeIAMPrincipaltakes the IAM configuration lock shared and inserts each membership with the account. - Authority fence.
performFencedruns each session request insidecontroller.withAuthorityFence. The four Namespace Role and AccessBinding mutations, the create paths below, anddeployAgentWithAuthorizationcallholdAuthorityfirst, which holds the session through COMMIT and takes the IAM configuration lock shared. Each create in an existing Namespace and the deploy authorize again afterlockNamespace, which AccessBinding deletes also take.createNamespaceinstead authorizescreateon the Namespace collection at the Installation before it creates the Namespace.fenceAgentProvisioningalso takes the IAM configuration lock beforelockNamespace. - Agent Backend selection.
createAgentresolves a supplied Backend ID and checks its type only after the secondagent:createauthorization. A caller without that grant receives403for both configured and unknown valid IDs. An authorized creator can select a compatible Backend without Installationadminister; that permission still protectsGET /backends. - Owner bindings. Each create path calls
bindCreatorafter it inserts the resource row, in the same unit:createNamespace,createAgent,createConfiguration,createSecret,createCredentialSource,createPreset,ensureNamespaceDefaultPresets,createServiceAccount, andprocessAgentProvisioningConfigurationfor the requester.bindCreatorskips the write withoutownerRoleor the Driver'screateOwnerBinding, or with theauthorizeoption. Otherwise the Driver writesbinding_owner_<resource-id>throughcreateOCCBinding, andauditBindingappends the event withreason: "creator". A Namespace's Owner binding comes before its default Presets, so the creator's own grant creates them. - Parent checks.
authorizationDecisionpassesparentAgentIdwith eachagent_revisionandsandbox_policycheck. Revision reads name the Agent they loaded, or the route's Agent ingetRevision. A sandbox policy's parent derives from its ID, which migration 0058 ties to the Agent ID. The native Driver'ssameResourcethen matches an exact binding on that Agent. - Deployment.
deployAgentWithAuthorizationcallsgrantDeployedAgentSources, which collects the Agent's Secret and credential source references. For each reference that the caller canoperateand the Agent ServicePrincipal cannot,ensureAgentOperateGrantswrites the exact binding, audited withreason: "agent_deploy". A failed deploy rolls back the grants.
Debugging and Verification
node --test tests/integration/occ-api.test.mjschecks the HTTP contract and API admission behavior for Namespace IAM policy, including refusal of all five writes without declared transaction participation. The Agent Backend case also checks equal create denials for configured and unknown valid Backend IDs, request-correlated authorization audit, unchanged process-local Agent, Configuration, IAM policy, ServicePrincipal, and operation State, and creation by a non-administrator with the required grants.node --test tests/integration/postgres-default-users.test.tschecks, with real PostgreSQL and HTTP admission, the configured rows, membership, Owner bindings indefault, refusal of a second member, deploy-time grants, a configuration change at restart, and the authority fences.node --test tests/conformance/occ-api-security.test.tschecks that Agent responses exposeservicePrincipalIdwithout accepting caller-supplied values.node --test tests/integration/postgres-namespace-iam-policy.test.tschecks PostgreSQL persistence, audit atomicity, and deletion behavior with real state.node --test tests/conformance/postgres-transaction-unknown-commit.test.tschecks that an unknown commit does not wait for a later rollback query.- The production bootstrap Sharing case in
tests/integration/postgres-production-wireup.test.tschecks revocation after real HTTP admission, both commit orders for configured Namespace and target-read withdrawal, cross-replica logout ordering, expiry during policy work, and expiry after the final client check with COMMIT paused at the transport. It uses real authentication, IAM and PostgreSQL; its passive Compute does not prove a native Gateway. - The guarded activation case in
tests/integration/postgres-password-default-activation.test.tschecks a Namespace wait beyond five seconds, an independent session's write, and subsequent account revocation. These cases emit credential-free execution diagnostics for review evidence. - A
403signals missing Installation administration, exact Namespace read, or target read for binding creation. It can also signal session revocation or expiry before the protected transaction completed. A400names the invalid field in its detail path. A409on Role deletion means a binding still references the Role.
