skip to content

In a GraphQL schema many teams extend, how do you guarantee every new field is authorized before it ships?

level: principalimportance: should knowfreq 42%

answer

  1. Change which state fails
  2. Review does not scale past a few teams
  3. The exemption list is the audit artefact
  4. Test the denial, not the annotation
  5. Turn rules on in log-only first

basics

~20 s

Make the default deny rather than allow, so an unmarked field fails a build check instead of shipping open; require an explicit, reviewed exception list for genuinely public fields; and prove enforcement with denial tests and denial metrics rather than with schema review.

solid answer

~50 s

Review does not scale: a four-person platform team cannot read every field that thirty-odd product teams add. The only durable answer is to make *unauthorized* the state that fails. Three parts. **Default deny** — a field with no declared rule and no reviewed public exemption fails the schema build, so the open field never reaches production. **Visibility** — the rule lives in the schema, so coverage is a query over an artefact rather than an audit of code. **Proof** — tests asserting a real denial per rule, since an annotation nobody wired up reads exactly like protection. Then split ownership: the platform team owns the mechanism, the default and the exemption process; the product team owns the policy for its own fields, being the only party that knows them. Roll rule *changes* out in log-only mode first — a tightened rule that silently empties a field looks like a broken feature.

code

pseudocode · 10 lines
pseudocode
# build-time closure check over the schema
fail = []
for type in schema.objectTypes:
    for field in type.fields:
        hasRule    = field.directives.any(isAuthRule) or type.directives.any(isAuthRule)
        exempt     = publicFieldList.contains(type.name + "." + field.name)
        baselined  = acceptedDebt.contains(type.name + "." + field.name)
        if not (hasRule or exempt or baselined): fail.add(field)

if fail.notEmpty(): error("unauthorized-by-default fields added: " + fail)

go deeper

for a junior

Take away the core idea: a new field with no rule should fail a build check, because relying on someone noticing it in review is how open fields reach production.

for a middle

Explain how a rule declared in the schema makes coverage measurable, and why a test must assert an actual denial rather than the presence of an annotation.

for a senior

Show the rollout discipline: log-only evaluation first, would-be denials read over a full business cycle, and denial rates monitored afterwards so a broken rule surfaces before a customer reports it.

for a principal

Own the split between mechanism and policy, the baselining strategy for an existing schema, and the honest limits — a build gate proves a decision was made, not that it was the right one.

## Why this is a systems question, not a code question Every individual authorization rule in a large graph is easy. The hard property is *closure*: that no field anywhere returns protected data without a decision having been made about it. In a schema of a few thousand fields extended by dozens of teams, closure cannot be maintained by attention. Someone adds a convenience edge, the reviewer is a peer on the same team who shares the same mental model, and the field ships open. Nothing in the process was negligent; the process simply had allow as its default. So the principal-level answer starts by changing which state is the failing one. ## Default deny, enforced at build time The mechanism to argue for: a field on a non-public type that carries no declared rule and no entry in a reviewed exemption list **fails the schema build**. Not a warning, not a dashboard — a red build on the pull request that introduced it. Three properties make this work in practice: 1. **It is local.** The failing check names the field and the team that added it, in the change that added it, when context is still in someone's head. 2. **It is cheap to satisfy.** Adding a rule is one annotation; declaring a field public is one reviewed line. If compliance is expensive, teams route around it. 3. **It has an escape hatch with a name on it.** The exemption file is the audit artefact. "Which fields are public and who decided?" becomes a diff, and the security review has somewhere to focus. Baselining is what makes this adoptable on an existing schema: snapshot today's unannotated fields as accepted debt, block anything new, and burn the baseline down by type. Trying to annotate everything first stalls, and the interesting risk is in the fields being added this quarter anyway. ## Coverage as a published number Because the rule is declared in the schema, coverage is countable: per type, per team, what fraction of fields carry a rule versus an exemption versus nothing. Publishing that per owning team does more than any policy document — it turns an invisible obligation into a number a team can move. It also gives the platform team a way to prioritise: types carrying financial or personal data first, not the schema alphabetically. ## Proving enforcement, not annotation The subtle failure of declarative schemes is decoration. An annotation whose wiring was never registered, or that the server silently ignores on a location it does not support, reads exactly like a protected field. So the test that matters asserts an actual denial: given a viewer without the right, selecting that field returns the denial shape. Generating one such test per declared rule from the schema itself is cheap and catches the entire class. The second test to insist on is the *unexpected path* test. Pick a protected object and assert denial along every edge that returns its type, generated by walking the schema rather than written by hand — because hand-written tests cover the paths the author already thought of, which are exactly the paths that were already protected. ## Rolling out rule changes safely Tightening a rule is a deployment risk, not just a security improvement. A worked example: a platform team narrows the rule on `Leg.driver` in a freight-tracking graph so only the shipper's own account may read it. Reads degrade politely. A tracking subscription, whose payload selects that field, does not — for a whole class of dispatch users the stream simply stopped delivering anything useful, and because a payload that resolves to nothing looks like quiet, nobody filed a bug for two days. The lesson generalises: any long-lived or background consumer turns an authorization change into a silent failure rather than a loud one. So run new and tightened rules in **log-only mode** first: evaluate the rule, allow the request, record what *would* have been denied with the field, the viewer class and the operation name. Read that for a period long enough to include the weekly and monthly jobs, fix the legitimate callers, then enforce. Keep the denial rate per field as a standing metric afterwards, with an alert on a step change — a spike means a rule is wrong or a client broke; a drop to zero on a field that used to deny means enforcement itself stopped. ## Ownership and the tradeoffs to name Split it: the platform team owns the mechanism, the default, the build check, the exemption process and the metrics; the product team owns the policy for its own fields, because only it knows whether a field is sensitive. Centralising the *policy* in a small team makes that team the bottleneck and, worse, makes them accountable for decisions they cannot evaluate. The tradeoffs worth conceding out loud: default-deny slows down teams shipping genuinely public fields, and generates friction that has to be kept small or it will be routed around. A declarative scheme cannot express instance-level policy, so it guarantees a *decision was made*, not that the decision was right. And no build check helps a field whose value comes from somewhere the enforcement layer does not sit on. The claim to make is narrow and honest: this converts an unbounded per-field human obligation into a bounded one, and makes what remains visible.

  • How do you apply default-deny to a schema that already has thousands of unannotated fields?
    Baseline it. Snapshot today's unannotated fields as accepted debt that the check tolerates, and fail the build for anything new or moved. Then burn the baseline down by data sensitivity — financial and personal types first — rather than alphabetically. Attempting full annotation before switching the check on is how these programmes stall; the marginal risk lives in the fields being added now.
  • Why insist on a denial test rather than a check that the rule is declared?
    Because a declared rule the server never wired up looks identical to a protected field from outside, and that is the failure mode that survives review. A test that asserts an unauthorized viewer actually gets the denial shape catches unwired directives, unsupported locations, and rules silently dropped during a schema build. Generating one per declared rule from the schema itself keeps the cost near zero.
  • Should the platform team own the policy for every field, or only the mechanism?
    Only the mechanism, the default and the audit. Policy needs domain knowledge the platform team does not have, and centralising it makes a small team both a release bottleneck and accountable for decisions they cannot evaluate. Give teams the mechanism, make the unmarked state fail, and keep the exemption list reviewable so central oversight applies where it adds value.
  • How do you know enforcement is still working six months later?
    Instrument it. Track denials per field and per rule as a standing metric, alert on a step change in either direction, and keep the generated denial tests in the pipeline. A field whose denial count falls to zero after a refactor has usually lost its check rather than gained obedient users, and that signal is invisible to any static check over the schema.

It is the difference between inspecting every shipment leaving the yard and building a gate that will not open without a manifest: the second one still needs exceptions, but they are written down.

saying these in an interview costs you the question

  • Relies on code review to catch unauthorized new fields
  • Centralises every field's policy in the platform team
  • Ships a tightened rule straight to enforcement
  • Counts declared annotations as evidence of enforcement
  • Treats coverage as a one-off audit rather than a build gate
  • Assumes long-lived consumers fail loudly when denied

context