On Behalf Of
An application acting for a User, such as an AI agent, calls OBP with a Consent and authenticates as a consent user, an identity OBP creates for that Consent. user_id is the caller (the consent user); on_behalf_of_user_id is the User who granted the Consent. OBP derives the latter from the Consent, never from the request.
Rows stored against a consent user would end with its Consent, so each column that stores a user id has a policy for calls made by a consent user:
- the caller: store the consent user (audit records, and the Consent's own permissions).
- the User acted for: store the User who granted the Consent. Some tables keep both ids; see Note.
- refused: a consent user may not do this (create a Consent, a Consumer or a token).
A request naming a consent user as its target User gets 400 InvalidUserId. v7.0.0 responses that show both ids use user_id and on_behalf_of_user_id, e.g. Dynamic Entity record metadata.
Columns that store a user id
Today is what this instance stores now: 18 of the 75 columns whose policy is "the User acted for" or "refused" apply it. "Not written by a consent user" means no endpoint writes that column with a consent user's id.
| Record | Column | Policy | Today | Note |
|---|---|---|---|---|
| AccountAccess | user_fk |
the caller | the caller | views copied from the consent JWT each request; has lifecycle GC |
| Entitlement | mUserId |
the caller | the caller | only when createdByProcess == consent_user: the consent engine copying the consent's own scope |
| Entitlement | mGrantedByUserId |
the caller | the caller | audit: who granted |
| UserLocks | UserId |
the caller | the caller | lock the authenticated user |
| ExpectedChallengeAnswer | ExpectedUserId |
the caller | the caller | consent and signing-basket authorisation: the caller IS the person authorising, so the challenge is theirs |
| ExpectedChallengeAnswer | ExpectedUserId |
the User acted for | the User acted for | payment SCA: the challenge belongs to the human whose money moves, never to the agent that started the payment |
| ChatMessage | SenderUserId |
the caller | the caller | sender = the authenticated user is truthful |
| Metric | userId |
the caller | the caller | record both: on-behalf-of via consent_reference_id at read time |
| MetricArchive | userId |
the caller | the caller | as Metric_UserId |
| ConnectorTrace | userId |
the caller | the caller | as Metric_UserId |
| DynamicDataAccess | GrantedBy |
the caller | the caller | audit: who granted |
| AuthUser | user |
the caller | the caller | login row -> its own ResourceUser; not attribution |
| OpenIDConnectToken | AuthUserPrimaryKey |
the caller | the caller | token belongs to the login; not attribution |
| UserRefreshes | mUserId |
the caller | the caller | operational: refresh of the authenticated user's own account list |
| PlatformApp | MarkedByUserId |
the caller | the caller | audit: who marked the Consumer as a platform app |
| ConsumerAccountAccess | GrantedByUserId |
the caller | the caller | audit: who gave the Consumer access to the view, as Entitlement_GrantedByUserId |
| GroupMembership | CreatedByUserId |
the caller | the caller | audit: who added the member, as Entitlement_GrantedByUserId |
| AssetStatusHistory | ChangedByUserId |
the caller | the caller | audit: who changed the asset's status, the question a regulator asks |
| RetentionPolicy | CreatedByUserId |
the caller | the caller | audit: who created the retention policy |
| RetentionHold | PlacedByUserId, ReleasedByUserId |
the caller | the caller | audit: who placed, and who released, a legal hold |
| RetentionRun | RequestedByUserId, ApprovedByUserId |
the caller | the caller | audit: who requested a run and who approved it; the two must differ, which is only meaningful for the users who actually acted |
| RetentionArchive | OwnerUserId |
the caller | the caller | copied from the archived resource as it was stored there; never written from the caller |
| RetentionRunItem | OwnerUserId |
the caller | the caller | disposal register: copied from the disposed resource as it was stored there; never written from the caller |
| TransactionRequest | mUserId, mOnBehalfOfUserId |
the User acted for | the User acted for | record both: mUserId = userId, mOnBehalfOfUserId = onBehalfOfUserId |
| Entitlement | mUserId |
the User acted for | the User acted for | the role holder; the consent-engine case is Entitlement_UserId_ConsentScope |
| GroupMembership | UserId |
the User acted for | not written by a consent user | the member, who holds the Roles the Group grants; as Entitlement_UserId |
| AccountHolders | user |
the User acted for | the User acted for | the human holds the account; one held by a per-consent identity strands when the consent dies |
| UserCustomerLink | mUserId |
the User acted for | the User acted for | a Customer is linked to a human; a link on an agent identity dies with its Consent |
| AccountApplication | mUserId |
the User acted for | the caller | explicit target: user_id comes from the request and is guarded at the endpoint, so the provider redirect is unreachable -- see ON_BEHALF_OF_USER_ID_PLAN.md row 14 |
| AccountAccessRequest | RequestorUserId |
the User acted for | the caller | who asked for access; the request outlives the session it was made in |
| AccountAccessRequest | TargetUserId |
the User acted for | not written by a consent user | explicit target: a consent user named here is rejected at the endpoint |
| AccountAccessRequest | CheckerUserId |
the User acted for | the caller | who approved; maker/checker evidence has to name a human |
| DynamicChangeRequest | RequestorUserId |
the User acted for | the caller | maker of a dynamic-code change |
| DynamicChangeRequest | CheckerUserId |
the User acted for | the caller | checker; must differ from the requestor |
| EntitlementRequest | mUserId |
the User acted for | the caller | who asked for the role; the grant that follows lands on a human |
| UserScope | mUserId |
the User acted for | the caller | the scope holder |
| ApiCollection | UserId |
the User acted for | the caller | the user's own saved collection, created through POST /my/api-collections |
| UserAttribute | UserId |
the User acted for | the caller | the user's own attribute, created through the /my/ endpoints |
| UserAgreement | UserId |
the User acted for | the caller | the user's own acceptance of terms |
| UserInitAction | UserId |
the User acted for | the caller | the user's own onboarding action |
| UserAuthContext | mUserId |
the User acted for | the caller | consent copies the on-behalf-of user's contexts into ConsentAuthContext separately |
| UserAuthContextUpdate | mUserId |
the User acted for | the caller | as UserAuthContext_UserId |
| DynamicEntity | UserId |
the User acted for | the User acted for | the definition's creator; a definition outlives the Consent that created it |
| DynamicData | UserId |
the User acted for | the User acted for | personal rows, and the one reference where the redirect MUST be symmetric: MapppedDynamicDataProvider resolves on save/update/get/delete alike, because a row keyed by this column on both sides is otherwise written by an agent and then invisible to it |
| DynamicData | CreatedByUserId, CreatedByOnBehalfOfUserId |
the User acted for | the User acted for | record both: who made the call that created the record, agent included, and the person it was made for; published in the v7.0.0 record metadata |
| DynamicData | UpdatedByUserId, UpdatedByOnBehalfOfUserId |
the User acted for | the User acted for | record both: as DynamicData_CreatedByUserId, for the call that last saved the record |
| DynamicDataAccess | UserId |
the User acted for | the caller | row-level ACL. Deliberately still on the consent user today, because the bootstrap grant and the allows check have to agree with each other -- rows strand, nothing leaks. A later Phase 2 row; see the plan |
| DynamicEndpoint | UserId |
the User acted for | the caller | the dynamic endpoint's creator; outlives the Consent |
| DynamicResourceDoc | CreatedByUserId, UpdatedByUserId |
the User acted for | the caller | and UpdatedByUserId; a dynamic artefact outlives the Consent that created it |
| DynamicMessageDoc | CreatedByUserId, UpdatedByUserId |
the User acted for | the caller | and UpdatedByUserId; a dynamic artefact outlives the Consent that created it |
| ConnectorMethod | CreatedByUserId, UpdatedByUserId |
the User acted for | the caller | and UpdatedByUserId; a dynamic artefact outlives the Consent that created it |
| AbacRule | CreatedByUserId, UpdatedByUserId |
the User acted for | the caller | and UpdatedByUserId; an access rule outlives the Consent that created it |
| Counterparty | mCreatedByUserId, mCreatedByOnBehalfOfUserId |
the User acted for | the User acted for | record both: a counterparty controls where money may be sent, so the actor stays on mCreatedByUserId (and is published as created_by_user_id) while the human goes to mCreatedByOnBehalfOfUserId |
| CounterpartyWhereTag | user |
the User acted for | the caller | who tagged the counterparty's location |
| ApiProductSubscription | CreatedByUserId |
the User acted for | the caller | a subscription outlives the Consent that took it out |
| DynamicGlossaryItem | CreatedByUserId |
the User acted for | the caller | outlives the Consent that created it |
| DomainApi | CreatedByUserId |
the User acted for | the User acted for | the Domain API's creator; a published API outlives the Consent that created it |
| DynamicEntityWebhook | CreatedByUserId, CreatedByOnBehalfOfUserId |
the User acted for | the User acted for | record both, like Counterparty: a webhook sends data out of OBP and outlives the Consent that made it, so the agent stays on CreatedByUserId (published as created_by_user_id) and the person it acted for goes to CreatedByOnBehalfOfUserId |
| AccountCreation | CreatedByUserId, CreatedByOnBehalfOfUserId |
the User acted for | the User acted for | record both, like Counterparty: who made the call that created the account, agent included, and the person it was made for; published in the v7.0.0 Create Account metadata. The holder is AccountHolders_User |
| Bank | CreatedByUserId |
the User acted for | the User acted for | creator grant already resolved at the endpoint |
| SelfServiceBank | CreatedByUserId |
the User acted for | the caller | as Bank_CreatedByUserId; the Create My Bank quota counts these rows per creator |
| Tenant | CreatedByUserId |
the User acted for | the caller | outlives the Consent that created it |
| Asset | CreatedByUserId |
the User acted for | the User acted for | outlives the Consent that created it, as RoutingScheme_CreatedByUserId; seeded rows hold system:asset-seed |
| File | UserId, OnBehalfOfUserId |
the User acted for | the User acted for | record both: UserId = who uploaded (the agent), OnBehalfOfUserId = the human, who owns the file |
| File | ScanStatusUpdatedByUserId |
the caller | the caller | audit: who set the scan status, the scanner or the staff member who decided |
| FileAttachment | UserId, OnBehalfOfUserId |
the User acted for | the User acted for | record both: UserId = who attached (the agent), OnBehalfOfUserId = the human |
| FileAccess | GrantedByUserId, OnBehalfOfUserId |
the User acted for | the User acted for | record both: GrantedByUserId = who granted (the agent), OnBehalfOfUserId = the file's owner it acted for |
| FileAccess | GranteeUserId |
the User acted for | not written by a consent user | explicit target: the User given access is named in the request; access given to a consent user would die with its Consent |
| PayeeLookup | CreatedByUserId |
the User acted for | the caller | outlives the Consent that created it |
| RoutingScheme | CreatedByUserId |
the User acted for | the caller | outlives the Consent that created it |
| UtilityPaymentCallback | CreatedByUserId |
the User acted for | the caller | outlives the Consent that created it |
| StandingOrder | UserId |
the User acted for | the caller | the payer; a standing order keeps executing long after any Consent expires |
| DirectDebit | UserId |
the User acted for | the caller | the payer; a mandate keeps executing long after any Consent expires |
| Mandate | CreatedByUserId, UpdatedByUserId |
the User acted for | the caller | and UpdatedByUserId; a mandate authorises payment and outlives the Consent that created it |
| SignatoryPanel | UserIds |
the User acted for | the caller | list of user ids |
| AccountWebhook | mCreatedByUserId |
the User acted for | the caller | published as created_by_user_id on v3.1.0 and v4.0.0; nothing reads it as an ownership key, so the human is not locked out -- but a webhook outlives the Consent that made it and keeps sending account events, so the human behind it should be recorded. Record-both, like Counterparty. See todo/webhook_attribution.md |
| SystemAccountNotificationWebhook | CreatedByUserId |
the User acted for | the caller | as AccountWebhook_CreatedByUserId |
| BankAccountNotificationWebhook | CreatedByUserId |
the User acted for | the caller | as AccountWebhook_CreatedByUserId |
| ChatRoom | CreatedByUserId |
the User acted for | the caller | Portal chat: a human's room |
| Participant | UserId |
the User acted for | the caller | the person in the room |
| Reaction | UserId |
the User acted for | the caller | the person who reacted |
| ChatEmailDigestState | UserId |
the User acted for | the caller | the person the digest is for |
| ChatMessage | MentionedUserIds |
the User acted for | the caller | explicit targets, humans by construction |
| CrmEvent | mUserId |
the User acted for | the caller | the user the event concerns |
| KycCheck | user |
the User acted for | the caller | the customer's user |
| KycCheck | mStaffUserId |
the User acted for | the caller | staff = human operator |
| KycDocument | user |
the User acted for | the caller | the customer's user |
| KycStatus | user |
the User acted for | the caller | the customer's user |
| SocialMedia | user |
the User acted for | the caller | the customer's user |
| CustomerMessage | user |
the User acted for | the caller | the customer's user |
| Meeting | mCustomerUserId |
the User acted for | the caller | the customer side of the meeting |
| Meeting | mStaffUserId |
the User acted for | the caller | staff = human operator |
| Tag | user |
the User acted for | the caller | the author of the annotation; it outlives the Consent |
| WhereTag | user |
the User acted for | the caller | the author of the annotation; it outlives the Consent |
| TransactionImage | user |
the User acted for | the caller | the author of the annotation; it outlives the Consent |
| Consent | mUserId |
refused | the caller (not refused) | a consent user creating a consent = nested delegation |
| Consumer | createdByUserId |
refused | the caller (not refused) | credentials outlive the consent |
| Token | userForeignKey |
refused | the caller (not refused) | credentials outlive the consent |