Skip to the lesson
CivOps AI Academy · F06The Role and Exposure Matrix: Who Sees What and Who Does What, Written Once
0%

F06 · Session 6 · Chapter 1

Who sees what, written once

Broken access control is the most common serious flaw in web applications. It happens when the rule about who may see and do what is written in many places and they stop agreeing. This chapter explains the cure: one matrix, written once, that everything else is generated from.

25 min3 chapters≈ 85 minutes2 exercises12-question assessment · 80% passes

By the end of this chapter you can

  • Explain why access rules written in several places drift apart, and what that costs.
  • Read a Role and Exposure Matrix: roles, data, exposures and scope.
  • Explain least privilege and default deny in plant terms.

The number one flaw

OWASP, the open community that tracks web application security, ranks broken access control first in its Top 10: a signed-in user reaching data or actions their role should not [1]. It tops the list not because it is clever but because it is easy to get wrong. Most applications write the same rule in three places: the screen hides a button, the server checks a role, the database limits rows. Each copy is written by hand, at a different time, often by a different person or agent.

Permission driftA rule written separately in screens, server routes and the database agrees in month 1, but by month 9 the screens and routes are out of step. Month 1Month 4Month 9Screenshide the Approve button from operatorsagreesagreesout of stepServer routescheck the role on /api/approveagreesout of stepout of stepDatabaserow rules on downtime_eventsagreesagreesagreesThree hand-written copies of one rule drift apart. One matrix, generated into all three, cannot.
Permission drift. Three hand-written copies of one rule agree on day one. A few changes later, the screen and the server no longer match the database, and nobody notices until someone finds the gap.

On a plant platform the gaps have names. A line 2 operator can read line 3's quality holds. A contractor who left still has a login that can export work orders. A supervisor approves their own downtime entry. An AI agent given an administrator key can change records it was only meant to read. Each is a rule that existed somewhere, but not everywhere.

Knowledge check

A screen hides the Approve button from operators, but the server route behind it does not check the role. What can an operator do?

Reading the matrix

A Role and Exposure Matrix is a table. Each row is a role: a job, not a person. Each column is a set of data (a table) or a screen. Each cell is the exposure: what that role may do with that data. A blank cell means no access at all.

A Role and Exposure MatrixFive roles by five data sets; for example operators may insert and read downtime events, supervisors may approve them, the connector may only insert readings, and nobody but HR sees people. readingsdowntime_eventswork_ordersquality_checkspeopleOperatorreadinsert readnoneinsert readnoneSupervisorreadread approveinsert read updateread approvenoneManagerreadreadreadreadnoneMaintenancereadreadread updatenonenoneConnectorinsertnonenonenonenoneRows are roles, columns are data, each cell says what that role may do. A blank is a no.
A matrix for the bottling plant. Operators record downtime and quality checks for their line; supervisors approve them; the connector may only insert readings; nobody here sees personnel records.
ExposureMeaningExample
readSee the rows in scopeOperator reads readings for their line
insertAdd new rowsConnector inserts readings
updateChange rows that are still openSupervisor updates a work order's priority
approveMove a record from pending to approvedSupervisor approves a downtime entry
(blank)Nothing: not even that the data existsOperator and people records

There is no delete exposure in this matrix. Plant records are append-only: a mistake is corrected by a new entry that reverses or replaces it, so the history can be trusted and audited. If a role ever needs to remove data, that is a decision for the matrix's owner, written down with a reason.

Scope: own line, own area, whole site

Roles alone are not enough. Two operators share a role but work on different lines. Scope says which rows a role's exposure applies to, using the same hierarchy as the naming standard from Session 5: site, area, line.

Scope: own line, own area, whole siteNested boxes: the site contains the bottling and packaging areas; bottling contains lines 2 and 3. An operator on line 2 sees only line 2; the bottling supervisor sees bottling; the manager sees the site. Site: Cleveland (manager sees all of it)Area: bottling (supervisor sees this area)Line 2operator assigned heresees and recordsline 2 rows onlyLine 3the line 2 operatorsees nothing hereArea: packagingthe bottling supervisorsees nothing here;the manager sees it all
Scope. The same exposure, limited to the part of the plant each person is assigned to. The database enforces it on every query (Session 7).

Knowledge check

Two operators have the same role. One works on line 2, the other on line 3. What makes them see different rows?

Least privilege and default deny

Two principles shape every cell. Least privilege: each role gets the minimum access its job needs, and no more. NIST defines it as restricting each user or process to the privileges needed for its tasks [2]. Default deny: anything not explicitly granted is refused, so a forgotten cell fails closed, not open. OWASP's authorization guidance recommends both [3].

1matrix, the only place access is written
0permissions edited by hand elsewhere
blankmeans no: the default is deny

Roles are jobs, not people

Write supervisor, not Dana. People are assigned to roles and scopes; when someone changes job, their assignment changes and the matrix stays the same. A person with two jobs holds two roles, and gets the union of both. Avoid a role per person and avoid a catch-all admin role for daily work: administration is a separate, rarely used role with its own sign-in.

Knowledge check

Someone suggests giving every supervisor the admin role to save time. What does least privilege say?

References

  1. OWASP Top 10:2025 (A01 Broken Access Control). https://owasp.org/Top10/2025/
  2. NIST Computer Security Resource Center glossary: least privilege. https://csrc.nist.gov/glossary/term/least_privilege
  3. OWASP Authorization Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html

F06 · Chapter 2 · From intent to matrix

Writing your matrix

The matrix comes from the intent you wrote in Session 2 and the schema from Session 3: the decision says who needs to see what, and the tables say what there is to see. This chapter writes a complete matrix for one plant problem, as a file an agent can read.

30 min5 steps1 file: access/matrix.json≈ 35 minutes hands-on

By the end of this chapter you can

  • Derive the roles, data and exposures from your intent and schema.
  • Add scope, separation of duties and machine accounts.
  • Write the matrix as a file in the repository that both people and generators can read.

Five steps from intent to matrix

  1. List the roles that touch the decision. Start from the intent's who decides, then add who supplies the data and who reviews it.
  2. List the data: every table in your Session 3 schema, plus each screen.
  3. Fill each cell with the smallest exposure the job needs: read, insert, update, approve, or blank.
  4. Add scope to each role: line, area or site.
  5. Add the rules a cell cannot express: separation of duties, and the machine accounts.

Role-based access control, where rights attach to roles and people are assigned to roles, is a long-established model with a NIST standard behind it [1]. The matrix is that model written as one readable table.

A worked example

The intent from Session 2: the shift supervisor decides, every hour, which line gets the next maintenance technician, from downtime by reason and open work orders. The schema has four tables: readings, downtime_events, work_orders and quality_checks, plus a people table for assignments.

RoleScopereadingsdowntime_eventswork_ordersquality_checkspeople
Operatorlinereadinsert, readinsert, read
Supervisorareareadread, approveinsert, read, updateread, approve
Managersitereadreadreadread
Maintenanceareareadreadread, update
Connector (machine)siteinsert
Access adminsiteinsert, read, update

Read down a column to see who can reach a table; read across a row to see everything one job can do. Both views matter. IT will read columns ("who can see work orders?"); a supervisor will read their row ("what can I do?").

Knowledge check

Following the intent, should operators be able to read work orders?

Separation of duties

Some rules involve two people. The supervisor may approve downtime entries, but not one they recorded themselves, even if they also hold the operator role. NIST lists separation of duties as a standard access control (AC-5 in SP 800-53) [2]. A cell cannot express it, so the matrix carries it as a named rule that the generator turns into a database condition in Session 7.

Separation of dutiesAn operator records a downtime event; a different person with the supervisor role may approve it; the same person who recorded it is refused. Operator (person A)records downtime: 42 mindowntime_eventsstatus: awaiting reviewSupervisor (person B)approves: allowedPerson A, againapproves own entry: refusedThe matrix grants approve to supervisors; a rule adds: never on a record you created yourself.
Separation of duties. The same person cannot both record and approve. The rule lives in the matrix next to the cells it qualifies.

Machine accounts are roles too

The connector from Session 5B, an AI agent, a nightly report: each acts on the platform and so needs a role in the matrix, with the narrowest rights for its one job. Never let a machine borrow a person's login, and never give it the database's master key. A service key that bypasses all row rules belongs only in server code that a person reviews, never in a connector, a browser or an agent [3].

People and machine accountsFour person roles with personal logins beside two machine accounts, the connector and an AI agent, each with the narrowest rights. People: personal logins, two-step sign-inOperatorown lineSupervisorown areaManagerwhole siteMaintenanceown areaMachines: one account per job, narrowest rightsConnectorinsert readings · nothing elseAI agent (through MCP)read named summaries · no writesMachine accounts are roles in the matrix too. They never share a person's login.
People and machines. People sign in personally with two-step sign-in. Each machine gets its own account for one job.

Writing it as a file

The matrix lives in the repository as access/matrix.json, so it is versioned and reviewed, and so generators can read it. JSON is fussy about commas but every tool reads it; your agent can keep it tidy. Anything not listed is denied.

access/matrix.json
{
  "version": 1,
  "owner": "operations manager",
  "roles": {
    "operator":    { "scope": "line" },
    "supervisor":  { "scope": "area" },
    "manager":     { "scope": "site" },
    "maintenance": { "scope": "area" },
    "connector":   { "scope": "site", "machine": true },
    "access_admin":{ "scope": "site" }
  },
  "data": {
    "readings":        { "operator": "read", "supervisor": "read", "manager": "read", "maintenance": "read", "connector": "insert" },
    "downtime_events": { "operator": "insert read", "supervisor": "read approve", "manager": "read", "maintenance": "read" },
    "work_orders":     { "supervisor": "insert read update", "manager": "read", "maintenance": "read update" },
    "quality_checks":  { "operator": "insert read", "supervisor": "read approve", "manager": "read" },
    "people":          { "access_admin": "insert read update" }
  },
  "rules": [
    { "name": "no_self_approval", "applies_to": ["downtime_events", "quality_checks"],
      "says": "approve is refused when the approver created the record" }
  ],
  "screens": {
    "/operator":  ["operator"],
    "/review":    ["supervisor"],
    "/dashboard": ["supervisor", "manager"],
    "/work":      ["supervisor", "maintenance"],
    "/people":    ["access_admin"]
  }
}
Exercise · Write your plant's matrix35 minutes

You need: Your intent statement (Session 2), your schema (Session 3), your repository and AI coding agent

Write the matrix for your first problem. Start on paper if it helps, then commit it as a file.

Outcome: access/matrix.json in your repository: every role, table and screen for your intent, nothing granted that the intent does not need.

Knowledge check

The connector needs to add readings. Which account should it use?

Knowledge check

Where does a rule such as 'no one approves their own entry' live?

References

  1. NIST: Role Based Access Control (RBAC) project. https://csrc.nist.gov/projects/role-based-access-control
  2. NIST SP 800-53 Rev. 5, Security and Privacy Controls (AC-5 Separation of Duties, AC-6 Least Privilege). https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final
  3. Supabase Docs: API keys (publishable and secret keys). https://supabase.com/docs/guides/api/api-keys

F06 · Chapter 3 · One source

Everything else is generated

A matrix that people read but code ignores is just another copy that drifts. This chapter connects it: a generator turns the matrix into database rules, route guards, menus, tests and a review sheet, and a completeness test makes sure nothing exists outside it.

20 min5 generated outputs1 completeness testevery change by pull request

By the end of this chapter you can

  • Name the five things generated from the matrix and which session builds each.
  • Write a completeness test: every table and every screen appears in the matrix.
  • Run an access change as a reviewed pull request, with its history.

One source, five outputs

Once the matrix is a file, a small script (the generator) reads it and writes everything that depends on it. Your AI coding agent writes the generator; you review what it produces. The outputs are never edited by hand: to change one, change the matrix and run the generator again.

One matrix, generated everywhereA single matrix file feeds a generator that produces database row rules, route guards, navigation, tests and a review sheet. access/matrix.jsonone file, in the repositoryreviewed like codeGeneratora script youragent writesDatabase row rulesSession 7: row-level security SQLRoute guardsSession 8: closed until openedNavigationeach role sees only its menuTestsone allowed and one refused per cellReview sheeta readable table for the owner and IT
One matrix, generated everywhere. The next two sessions build the first two outputs: database row rules (Session 7) and route guards (Session 8).
OutputWhat it doesBuilt in
Row-level security SQLThe database refuses any row a role or scope should not reachSession 7
Route guardsEvery page and API route is closed until the matrix opens itSession 8
NavigationEach role's menu shows only the screens it may openSessions 10 and 11
TestsFor every cell, one request that must succeed and one that must be refusedSessions 7, 8 and 17
Review sheetA readable table of the matrix for the owner and ITThis chapter

Nothing outside the matrix

Default deny protects you when a cell is blank. A completeness test protects you when a whole table or route is missing from the matrix. It lists every table in the database and every route in the application, and fails if any is not in the matrix. A new table added by an agent without a matrix entry fails CI on the same pull request.

A completeness test your agent can write
// test/matrix-complete.test.mjs — nothing exists outside the matrix.
import { readFileSync, readdirSync } from "node:fs";
import assert from "node:assert/strict";

const matrix = JSON.parse(readFileSync("access/matrix.json", "utf8"));
const roles = Object.keys(matrix.roles);

// Every exposure names a known role and a known action.
for (const [table, cells] of Object.entries(matrix.data))
  for (const [role, actions] of Object.entries(cells)) {
    assert.ok(roles.includes(role), `${table}: unknown role ${role}`);
    for (const a of actions.split(" ")) assert.ok(["read", "insert", "update", "approve"].includes(a), `${table}.${role}: unknown action ${a}`);
  }

// Every table created by a migration is in the matrix.
const sql = readdirSync("supabase/migrations").map((f) => readFileSync(`supabase/migrations/${f}`, "utf8")).join("\n");
for (const [, table] of sql.matchAll(/create table (?:if not exists )?(?:public\.)?(\w+)/gi))
  assert.ok(matrix.data[table], `table ${table} is not in access/matrix.json`);
console.log("every table is in the matrix");

Folder names such as supabase/migrations follow the Supabase command-line tool's layout [2]; your agent will adjust them to your repository. Session 8 adds the same check for routes.

Knowledge check

An agent adds a new table, shift_notes, and forgets the matrix. What happens?

The review sheet

The owner of the matrix (usually the operations manager) and IT both need to read it without reading JSON. The generator writes a review sheet: one table per role, listing each data set and exposure in plain words, plus the rules. It is regenerated on every change, so the sheet the owner signs is always the one that runs.

Changing access

Access changes are ordinary pull requests. The change to access/matrix.json is one line; the generated SQL and menu change with it; the tests prove the new cell; the owner approves. Months later, "who gave maintenance the right to update work orders, and why?" is answered by the repository's history in seconds.

A permission change, end to endFive steps: a pull request edits the matrix, the generated SQL and menu change, tests prove it, the owner approves, and the merge deploys it with its history. Pull requestmatrix.json:maintenance may updatework_ordersGenerated diffone new policy in theSQL; one menu itemTestsmaintenance updateallowed; operatorupdate refusedOwner reviewthe plant's accessowner approvesMergedeploys; the changeand its reason are inhistoryEvery access change is a reviewed pull request, never a click in an admin screenWho changed access, when, why and who approved it: all answered by the repository's history.
An access change, end to end. No admin screen, no hand-edited policy: one line in the matrix, reviewed and tested.

People join and leave

Assigning a person to a role and a scope is data, not a change to the matrix: the access admin adds the assignment in the people table. When someone leaves, removing the assignment removes every right at once, and their personal login is disabled the same day. Review the assignments quarterly with each area owner [3].

Exercise · Generate the review sheet and the completeness test25 minutes

You need: Your repository with access/matrix.json, your AI coding agent, GitHub

Your agent writes the code; you check that the output matches what you meant.

Outcome: A generated access review sheet that is always current, and a CI check that refuses any table outside the matrix.

Knowledge check

A manager asks IT for an export of who can read work orders. Where does the answer come from?

References

  1. OWASP Authorization Cheat Sheet (enforce on the server, deny by default). https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
  2. Supabase Docs: Local development with the Supabase CLI (migrations). https://supabase.com/docs/guides/local-development
  3. NIST SP 800-53 Rev. 5 (AC-2 Account Management). https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final

Chapter 4 · 12 questions · 80% passes

Final assessment

Twelve questions across the element. Score 80% (10 of 12) to pass. Your LMS records your score and each answer; you can review the chapters and try again.

15 min12 questions≈ 15 minutesRetake allowed

Choose one answer for each question, then submit. You will see the right answer and why for every question.

1. Which flaw does OWASP rank first in its Top 10 for web applications?
2. Why do access rules written separately in the screen, the server and the database become dangerous?
3. In a Role and Exposure Matrix, what is a row?
4. What does a blank cell in the matrix mean?
5. Two operators share a role but work on different lines. What limits each to their own line's rows?
6. Why is there no delete exposure for plant records?
7. A supervisor also holds the operator role. May they approve a downtime entry they recorded?
8. Which account should the DMZ connector use?
9. Where is the matrix kept?
10. What does the completeness test check?
11. Hiding a screen from a role in the navigation is:
12. How should maintenance be given the right to update work orders?