skip to content

How would you build a roles-by-endpoints permission matrix that a newly added route cannot quietly slip past?

level: principalimportance: should knowfreq 34%

answer

  1. a hand-kept matrix is stale immediately
  2. enumerate the server's own route table
  3. roles, routes, and object relationship
  4. an undeclared cell fails the run
  5. a rule change is a matrix diff

basics

~20 s

Generate the matrix from the server's own route table at test time and cross it with fixture principals. Any route without a declared expectation fails the suite, so a new route is red until a human classifies it, and the declaration file becomes the reviewable statement of policy.

solid answer

~50 s

A matrix written by hand goes stale the day someone adds a route, so it has to be **generated from the route table the server already builds**, crossed with a small set of fixture principals and, for routes taking an object identifier, an object-relationship axis: my own, another site's, absent. Every cell needs a declared expectation in a checked-in file; a cell with no declaration is a **suite failure**, not a skip. That single rule is what a new route cannot slip past — it is red until somebody writes its row. The declaration file then does the second job: it is the artefact a reviewer reads to see that this route is now open to that role, without reading the handler, and a rule change becomes a diff of the matrix whose failing cells are the change's blast radius.

code

pseudocode · 14 lines
pseudocode
routes     = server.routeTable()                 # every method + path template served
cases      = load("permission-matrix.yaml")      # declared expectations, default DENY
principals = [coordinator_204, coordinator_311, monitor, administrator, anonymous]

for route in routes:
    for p in principals:
        for rel in relationshipsFor(route):      # OWN_SITE, OTHER_SITE, ABSENT - or [NONE]
            expected = cases.lookup(route, p.role, rel)
            if expected is MISSING:
                fail("unclassified: " + route.method + " " + route.path +
                     " for " + p.role + "/" + rel)
                continue                          # never call with no expectation
            actual = call(route, as = p, object = fixtureFor(route, p, rel))
            assertStatus(actual, expected)

go deeper

for a junior

Recall the shape: a table of who may call what, checked by tests, with the route list taken from the server itself so it cannot go stale.

for a middle

Explain the three axes and why the missing-declaration failure is the mechanism rather than a nicety, and how default-deny keeps the declaration file short.

for a senior

Show how a rule change turns into a diff whose failing cells are the blast radius, and name what the suite is blind to: fixtures, non-route entry points, unusual object shapes.

for a principal

Decide where the control actually lives. The suite enforces consistency; correctness is enforced by reviewing the declaration, so argue for the matrix as the reviewed artefact and staff the review accordingly.

## The matrix is the specification, the suite is only the check Teams usually build a permission matrix as a spreadsheet during a security review, and it is obsolete within a sprint because nothing forces it to move when the code does. Invert that. Make the matrix a checked-in file that the test suite **must** satisfy for every route the server actually serves, and it stops being documentation and becomes the specification — the only place where the intended answer to *who may call what* is written in one readable page. The key mechanical decision is where the row list comes from. It comes from the server's own route table, enumerated at test time. Not a hand-kept list, not a scan of directories: the same structure the server consults to dispatch a request. ## Three axes, not two - **Principal.** A small set of fixture principals, one per role that exists, plus an unauthenticated caller. Small is the point: one coordinator, one coordinator at a *different* site, one monitor, one administrator, one anonymous. - **Route.** Method and path template, from the route table. - **Object relationship.** Only for routes that take an object identifier, and this is the axis teams forget: *an object my scope owns*, *an object another site owns*, *an object that does not exist*. The second column is where the interesting failures live; without it the matrix proves only that a role may call a route, which is a much weaker statement than that a role may reach an object. ## Generation, and the rule that does the work 1. Enumerate the route table. 2. Cross it with the fixture principals, and for object-bearing routes with the relationship values. 3. Look up each cell in the declaration file. 4. **A missing declaration fails the run, naming the route.** This is the whole mechanism. A skip, a warning or a default-allow here and the property is gone. 5. For declared cells, call the route as that principal against that object and assert the declared outcome. A new route therefore arrives red. The author must add a row saying what each role gets, and that row is what review examines. ## Keeping it from exploding Fifty routes by five principals by three relationships is seven hundred and fifty cells, which nobody maintains by hand. Three things keep it tractable: - **Default deny, declare exceptions.** The file lists what is allowed; everything else is expected to be refused. The declaration is short because most cells are the same. - **Declare by resource group, override by route.** Rows attach to a resource family with per-route exceptions, so a new route on an existing resource inherits a sane default — but still fails until someone confirms the inheritance is right, because inheritance is opt-in per route. - **The relationship axis only where it applies.** Collection routes and routes with no object identifier carry two cells, not six. ## A rule change is a diff of the matrix This is what the artefact buys beyond coverage. Widen a rule and the failing cells *are* the blast radius: the run prints every route and principal whose outcome changed. The author's job becomes updating the declaration to match, and the reviewer reads that diff — `route X is now allowed for the monitor role` — and judges the intent without reading a line of the rule. A change to the rules that produces no matrix diff is either a no-op or a change whose effect is invisible to the suite, and both are worth knowing. ## What a green matrix still does not prove | gap | why the suite is blind to it | |---|---| | a cell declared wrong | the suite proves the code matches the declaration, and the declaration is human | | a role absent from the fixtures | it has no cells, so nothing is asserted about it | | entry points that are not routes | scheduled work and message consumers never appear in the route table | | object shapes the fixtures lack | one participant at one site is not a participant enrolled at two | The first row is the important one and it decides where review effort goes. The suite guarantees consistency between the declaration and the behaviour; it cannot guarantee the declaration is right. So the control is the declaration's diff in code review, and the suite exists to make sure the declaration cannot drift away from what the server actually does. Teams that understand this review the matrix carefully and the handler casually. Teams that do not review the handler carefully and never read the matrix at all, which is how a wrongly declared cell stays green for years.

  • How does a widening rule change become reviewable through the matrix?
    The run reports every cell whose outcome no longer matches its declaration, and that set is the change's blast radius. The author updates the declaration to match and the reviewer reads that diff as a sentence — this route is now open to that role — rather than inferring it from the rule. A rule change producing no matrix diff is either a no-op or invisible to the suite.
  • What is the risk of letting a new route inherit its resource group's declaration automatically?
    Silent classification, which is the exact failure the mechanism exists to prevent. Inheritance is fine as a default value but must still be opted into per route, so the author writes one line confirming it. Otherwise a route that reads a different object than its siblings picks up their permissions and never turns the suite red.
  • A cell was declared allowed for a role that should not have it. What catches that?
    Not the suite — it only proves the behaviour matches the declaration. The control is human review of the declaration diff, which is why the file is worth keeping small, readable and default-deny. That is also the argument for reviewing the matrix carefully and the handler casually, rather than the other way round.

A pre-flight checklist generated from the aircraft's own equipment list rather than typed once and photocopied. Fit a new instrument and it appears with no line against it, and the checklist is invalid until somebody writes one.

saying these in an interview costs you the question

  • Keeps the matrix as a spreadsheet updated during security reviews.
  • Lets an undeclared route be skipped or default to allowed.
  • Builds roles by routes and omits the object-relationship axis.
  • Thinks a green matrix proves the declared permissions are correct.
  • Assumes the matrix covers work that never arrives through a route.
  • Reviews the handler diff and never reads the declaration diff.