Test object-level authorization by crossing actors, resources, and actions against an explicit access rule. Include another user in the same tenant, a user in a different tenant, and a privileged user whose authority has limits. Verify both the response and the absence of unauthorized state changes.
Login establishes an identity. It does not establish ownership of every object named in a request. An unpredictable identifier can make guessing harder, but the server still needs to decide whether this actor may perform this action on this object now.
Define the rule before testing the endpoint
Consider a fictional document service. Members may read their own documents within their tenant and edit them while they are drafts. A tenant administrator may read any document within that tenant and edit drafts. Nobody in this example may edit a final document. Cross-tenant access is denied even when a subject identifier happens to match.
Those are deliberately specific business rules, not universal recommendations for a document service. A real application might support explicit sharing, legal holds, support staff, or delegated administration. Add those relationships to its model instead of assuming that “owner” covers every legitimate case.
OWASP API1:2023 identifies broken object-level authorization as a failure to check access when an API acts on a supplied object identifier. The identifier can arrive in a path, query, or body. A route guarded by authentication middleware still needs the object-specific decision.
Use identities established by the trusted authentication layer. A request body containing tenant: north or role: tenant_admin cannot grant that authority. If a user can select an active tenant, verify their membership before building the authorization context.
Make allowed and denied cases equally concrete
For the example, draft-a belongs to Alice in tenant North. The “South Alice” account has the same local subject label in tenant South; it is a different security principal in this model.
| Actor | Object | Read | Edit | Reason |
|---|---|---|---|---|
| Alice, North member | Alice's North draft | Allow | Allow | Same tenant, owner, editable state |
| Bob, North member | Alice's North draft | Deny | Deny | Tenant membership does not imply ownership |
| South Alice, South member | Alice's North draft | Deny | Deny | Matching subject text does not cross tenants |
| North administrator | Alice's North draft | Allow | Allow | Administrative authority is scoped to North |
| South administrator | Alice's North draft | Deny | Deny | Administrator is not a global role |
| Alice, North member | Alice's North final document | Allow | Deny | Ownership does not override the state rule |
This matrix makes two common omissions visible. Same-tenant access can still be unauthorized, and administrative access still has a resource boundary. Testing only an anonymous caller and an owner leaves both questions unanswered.
OWASP's authorization regression guidance uses an actor-resource-action model and recommends persistent matrices for horizontal access, privilege changes, and tenant isolation. The matrix above is a concrete policy for our invented service; it is not an OWASP-prescribed permission model.
Put the decision on every path to the object
A detail endpoint may load a document by ID and then check its ownership. A list endpoint may instead constrain the query to the caller's permitted set. Either design needs a clear rule for every returned object. Checking only the first object in a list is insufficient.
Follow nested resources too. If a request identifies both a project and a document, establish that the document belongs to the authorized project. Permission to read project A cannot authorize document B merely because B appears under A in a client-supplied URL.
Mutations need particular care. Check the current resource and applicable state in a way that remains valid when the write occurs. Depending on the storage system, that may mean a transaction, a conditional update, or another concurrency control. A successful check followed by an unrelated write can leave a gap if ownership or state changes between them.
The OWASP Authorization Cheat Sheet recommends denying by default, checking permissions on every request, and testing authorization logic. In application review, trace those requirements through bulk operations, exports, downloads, background jobs, and alternate API versions. A protected UI button does not secure a second route to the same operation.
Assert effects, not just status codes
A denied read should not return the protected document in an error body. A denied edit should leave the document unchanged and avoid publishing a downstream event. A denied deletion should not detach related files before returning an error.
Choose error behavior according to the API's disclosure policy. Some services return 404 for inaccessible objects to avoid revealing existence; others use 403 where existence is already known to the caller. Keep that behavior consistent, and test any response details for leakage. The API error contract comes after the authorization decision.
For an asynchronous operation, inspect both enqueueing and execution. Test whether an unauthorized request can place a job on the queue, and whether a worker rechecks any authority or resource state that can change before execution. The precise rule depends on whether the job intentionally carries a durable, scoped grant or relies on current membership.
Caches also need the same model. A response keyed only by document ID may be reused for another caller unless authorization happens before reuse and the representation is appropriate. Include a two-user sequence in integration tests instead of checking each request only against an empty cache.
What the local fixture demonstrates
The accompanying Python fixture uses trusted, synthetic actor dictionaries and an in-memory document store. It exercises 16 actor-object-action cases, including the table's boundaries, a missing object, an unsupported action, an administrator editing a final document, and a legitimate edit by Bob to his own draft.
All 16 cases passed on Python 3.14.2. Each denied case checked that the response contained only the denial marker and that the entire store was unchanged. Successful reads also preserved the store; successful edits changed only the intended document.
That is evidence about this small policy function and its caller. It does not test authentication, an HTTP route, a database transaction, a cache, a job worker, or a deployed application's access controls. Passing the fixture would not prove that a production API calls the same logic at every boundary.
Use the matrix as a contract for the real service's integration suite. Add a new row when introducing sharing, a new role, a new state transition, or another route to the resource. The useful result is a reviewable rule plus evidence that each relevant path enforces it, including when the answer is no.
Primary guidance and the synthetic fixture checked October 28, 2026. No external API, real tenant, or customer data was accessed.