Browse documentation

Tenant and object authorization

Carry trusted tenant context through reads, writes, jobs and events, then authorize the specific object at every boundary.

On this page
Before you start
  • Tenant membership rules
  • An action vocabulary
  • Resource ownership relationships

An authorized object request

A caller supplied tenant or object identifier is a lookup input, never the authority for access.

An authorized object requestStep 1: Principal to API boundary. Request object. Step 2: API boundary to Policy engine. Trusted identity context. Step 3: Policy engine to Tenant data. Constrained lookup. Step 4: Tenant data to Policy engine. Object attributes. Step 5: Policy engine to API boundary. Allow or deny. Step 6: API boundary to Principal. Permitted representation.PrincipalAPI boundaryPolicy engineTenant dataRequest object1Trusted identity context2Constrained lookup3Object attributes4Allow or deny5Permitted representation6

Scroll across the diagram to see all four systems.

Inspect each exchange

Request object

Present a credential and a target resource identifier.

Resolve tenant context from authority

Treat a tenant identifier in a route, header or request body as untrusted input. Resolve the caller’s allowed tenants from the authenticated identity and membership system. Compare the requested tenant to that authority before querying protected data.

A service account may act for several tenants, but the grant must define that delegation explicitly. Do not let a downstream worker select an arbitrary tenant merely because it knows the connection identifier.

Authorize the object as well as the endpoint

OWASP identifies missing object authorization as a major API risk. Every endpoint that takes an object identifier and performs an action needs to check that the principal may perform that action on that object. A valid session and a role check at login are insufficient.

Constrain database queries to the trusted tenant and then apply resource policy. Validate parent and child relationships together: a contact route nested under an account must prove that the contact actually belongs to the allowed account.

Constrained resource accesspython
def update_contact(principal, tenant_id, contact_id, change):
    membership = authorize_tenant(principal, tenant_id)
    contact = contacts.get(
        tenant_id=membership.tenant_id,
        contact_id=contact_id,
    )
    authorize_action(principal, "contacts.write", contact)
    permitted = validate_writable_fields(change, principal)
    return contacts.update(contact, permitted)

Define which fields can cross the boundary

A user who can update a contact’s phone number may not be allowed to set its owner, tenant identifier or compliance state. Allowlist writable fields and validate transitions rather than binding an entire request body directly to a persistence model.

Read permissions also need field policy. A reporting role may need aggregated revenue while a support role needs contact context. Avoid returning a complete object and relying on the frontend to hide sensitive fields.

BoundaryRequired contextFailure to prevent
API requestPrincipal, tenant, action and objectChanging the URL to access a different tenant.
Background jobDelegated authority and target tenantExecuting a stored job under a broader worker identity.
Webhook handlingVerified connection and mapped organizationTrusting tenant_id inside an unsigned payload.
Cache lookupTenant and relevant authorization dimensionsServing one tenant’s response to another.
File exportExport owner and fresh download permissionReusing a long lived link after access is removed.

Preserve authority through delayed work

An asynchronous job should carry the minimum delegation context and a reference to the initiating principal. Decide which permissions are rechecked at execution time, especially for destructive actions or exports. Authorization at submission does not automatically permit every later action.

When membership is revoked, stop future work according to the grant policy and make the job state observable. A worker’s technical permission to call an API does not establish the business permission of the original request.

Test neighboring records and indirect references

Use at least two tenants and multiple roles in contract tests. Try cross tenant object IDs, batch requests containing mixed tenants, parent and child mismatches, old export links and changed membership during an active job.

Record the policy decision with a safe principal reference and resource class. Do not log the full protected record merely to prove that an authorization denial occurred.

Evaluate an object permission

Change tenant membership, action scope or the writable field to inspect the authorization boundary.

PATCH/api/v1/tenants/tenant_a/contacts/contact_240

Contract inputs

Includes request values and the server validation context defined below.

Expected outcome

HTTP 200

Action permitted

The tenant, action and field are within the principal’s authority.

{
  "contact_id": "contact_240",
  "updated_fields": [
    "phone"
  ]
}

Decision path

  1. Resolve tenant membership
  2. Constrain object lookup
  3. Check contacts.write
  4. Allow the phone field
Input schema 4 fields
FieldTypeMeaning
requested_tenantRequiredstringTenant selected in the request.
authorized_tenantsRequiredarrayTenants established by trusted membership.
scopesRequiredarrayGranted action scopes.
fieldRequiredstringField the caller intends to update.
Download JSON Schema

Standards and documentation

Explore the protocol specifications and platform guidance for this topic.

Bring the contract into your operating environment.

Discuss an implementation

Image preview