Dev.to Security 🔐 Cybersecurity 👁 0 📖 4 min read

Testing the Permission Matrix

Quick answer: A permission matrix is a table of roles, resources, and actions where every cell is either allow or deny, and it should be tested as a data-driven set rather than a handful of hand-written calls. Encode the

Quick answer: A permission matrix is a table of roles, resources, and actions where every cell is either allow or deny, and it should be tested as a data-driven set rather than a handful of hand-written calls. Encode the matrix as the expected result, then for each role send every action against every resource and assert the exact status code, the response body, and the side effect. The cases that expose real holes are the deny cells, vertical and horizontal escalation, and the default-deny rule for anything the matrix does not name.

The matrix is the oracle, not the happy path

Write the matrix down first: list the principals such as anonymous, viewer, editor, and owner, the resources they touch, and the actions each one performs. That table is the oracle your tests compare against, so a missing cell is a gap in the test before it is a bug in the code. Most teams only exercise the admin path, which is all allow, plus one obvious deny. That leaves the combinations where a lower role reaches a high-priv action or one user reaches another user's object. A matrix also fixes the default rule: any combination not explicitly allowed is denied, and that default needs its own assertion rather than being assumed.

Drive every cell instead of hand-writing cases

The number of cells grows quickly, so hand-writing each request is how cases get skipped. Build a parameterized list from the matrix and loop over it, selecting the token for the role, issuing the action against the resource, and comparing the result to the expected cell. Keep one token per role so the identity under test is never in doubt, and keep the expected allow or deny next to the inputs so a wrong result is easy to read. A desktop API workspace can store each role's token in a variable and run an ordered scenario that walks the matrix, asserting status, headers, and body at each step. Powerduck works this way, with a local mock returning the allow or deny conditionally so the full matrix runs without a live authorization server.

The privilege failures that matter

Failure class Definition Test setup Expected result
Vertical escalation Lower role calls a high-priv action Viewer token against an admin endpoint 403, no side effect
Horizontal (IDOR) Same role, another user's resource User A token with user B's resource id 403, no data leaked
Anonymous No identity at all Request without a token 401
Default deny Combination not in the matrix Action left unlisted 403
Owner versus member Ownership changes the result Owner and collaborator tokens Allow only where the matrix says

Assert beyond the status code

A correct 403 is not enough on its own. On a deny, confirm the response body does not include the protected fields and that no side effect occurred, because an endpoint can return 403 while still writing a row. On an allow, confirm the action truly executes rather than only that the status is 200. Watch the distinction between a missing resource and a forbidden one: returning 404 for a real object the caller cannot see can hide the resource, while 403 reveals it exists, so choose deliberately and test it. Sub-resources should inherit the parent's permission, and a batch call needs a rule for partial authorization rather than silently applying the forbidden items.

Honest limits

A local mock returns allow or deny from rules you wrote, so it proves the matrix is fully covered and the client handles each result, but it does not verify the real authorization engine, row-level database permissions, middleware order, or policy caching. The mock is not the source of truth for access, and a rule there can quietly differ from the live policy. A passing run only shows the implementation matches the matrix you wrote; it does not prove the matrix captured every business rule. Confirm genuine escalation and object-level access against a real environment with realistic data, and treat the matrix itself as something a reviewer checks.

Frequently asked questions

What is the difference between vertical and horizontal escalation? Vertical is a lower role reaching a higher-priv action. Horizontal is a user reaching another user's resource at the same level, often called IDOR.

Why use a data-driven test instead of writing each call? The matrix has many cells, and a loop generated from the table covers every one without skipping a combination or letting a case drift from the expected result.

Is a 403 enough to prove a deny? No. Also assert the body leaks no protected fields and no side effect occurred, since a write can happen before the status is returned.

What should happen to a combination not in the matrix? It should be denied by default. Add an explicit case for an unlisted action so the default-deny rule is verified rather than assumed.

The bottom line

Treat the permission matrix as the test oracle and drive every role, resource, and action cell with a parameterized run. Cover vertical and horizontal escalation, anonymous access, and default deny, then assert the body and side effect on each deny and verify the real authorization engine separately. For more on this approach, see the API testing guide hub.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.