Why must a field-level permission name an action, not just a field, for a role that may read what it cannot change?
answer
- one field, two separate rights
- seeing it is not saving it
- authorize the field and the action
- check the fields sent, not a delta
basics
~20 sRead and write rights over the same field are independent. A pastoral lead may read a pupil's predicted grade while only the class teacher may set it, so the unit of a field-level rule is the field and the action together.
solid answer
~40 sA field-level rule is a rule about three things at once: the principal, the field, and the action. Drop the action and you can only express "this role has something to do with this field", which is never what the requirement says. In a school's pupil records a class teacher writes `predictedGrade` and only reads `guardianPhone`; a pastoral lead reads `predictedGrade` and writes `safeguardingFlag`. The write check then has one job: take the set of fields the request asks the server to set, ask the rule set about each one, and refuse the whole update before anything is persisted. The set to authorize comes from the request, not from a comparison against the stored record — a decision computed from current data gives two byte-identical requests two different verdicts.
code
pseudocode · 15 lines# The fields to authorize come from the REQUEST, never from a diff
# against stored values.
requested = keys_present_in(request.body) # e.g. {predictedGrade, safeguardingFlag}
denied = []
for field in requested:
if not rules.permits(principal, action = "write", field = field, record = stored):
denied.append(field)
if denied is not empty:
# Nothing has been persisted yet: refuse the whole update.
decision_record.write(principal, stored.id, "write", denied, verdict = "deny")
return Forbidden(403) # body does NOT carry `denied`
persist(stored, values_from(request.body, requested))go deeper
Remember the shape of the rule: a field-level permission names a field and an action together. Being able to see a pupil's predicted grade on screen tells you nothing about whether you may change it.
Be ready to walk the check end to end: take the fields the request asks to set, ask the rule set about each one, and refuse the whole update before anything is written.
Show that you have met the delta trap in production. An answer computed by comparing the request against stored values moves as the data moves, so two identical requests get two verdicts.
The angle to own is where the field-and-action pair is defined and who may add to it. If a team can ship a new writable field without an access decision being made, the model is already leaking.
## What a field-level rule is a rule about A record-level rule answers one question: may this principal touch this record at all? A **field-level rule** answers a narrower one, and its unit is a triple — **the principal, the field, and the action**. Dropping the action from that triple is the most common way the model goes wrong, because in any real staff hierarchy read rights and write rights over the same field diverge, and they diverge in both directions. Take a school's pupil records, split between academic and pastoral staff. Four roles in the application's own role table, four fields: | Field | Class teacher | Pastoral lead | Office administrator | Head | |---|---|---|---|---| | `formGroup` | read | read | write | read | | `predictedGrade` | write | read | none | read | | `guardianPhone` | read | read | write | read | | `safeguardingFlag` | none | write | none | write | No column is uniform and no row is uniform. If the model held one permission per role-and-field pair, most of these cells would be unrepresentable: `guardianPhone` for the class teacher is readable and not writable, `predictedGrade` for the pastoral lead is the same shape, and `safeguardingFlag` for the office administrator is neither. ## Read and write diverge in both directions - **Read without write** is the common case: a teacher needs the guardian's number to make a call, and the office owns corrections to it. - **Write without read** is rarer but real: staff may add a pastoral concern to a pupil without being able to read the concerns already recorded by colleagues. The field accepts a contribution and returns nothing. - **Write conditioned on the record**: a class teacher writes `predictedGrade` only for pupils in their own teaching group. The condition rides on the rule; it does not become a different field. - **Neither**: `safeguardingFlag` for the office administrator, where even knowing whether it is set is a disclosure. ## Where the check gets its input The write check is three steps and the first one is the one people get wrong: 1. **Take the set of fields the request asks the server to set.** On a `PATCH` this is the keys the body actually carries. On a full replacement it is every field the client sent, whether the value changed or not. 2. **Ask the rule set, per field, whether this principal may write it on this record.** Conditions that depend on the record — the teaching group, the pupil's enrolment state — are evaluated here, against the record being written. 3. **Decide before anything is persisted.** The trap is computing step 1 as a **delta**: comparing the submitted values against the stored ones and authorizing only the fields whose values differ. It fails two ways. First, the verdict stops being a function of the request. The same bytes are allowed at 09:00 and refused at 09:05 because someone else edited the record in between, and an authorization answer you cannot reproduce from the request alone is one you cannot explain afterwards. Second, it quietly permits a caller to assert a value over a field they may not write whenever the assertion happens to match what is already there — harmless the day you ship it, and a live write path the day someone changes the rule or the default. Note the narrow form of the rule: the **set of fields to authorize** must not be computed from stored values. A condition inside a rule may of course read the record, and usually must. ## Deciding before you write Authorization over a multi-field update is **all or nothing**. Applying the permitted fields and skipping the rest leaves the record in a state no single authorized request could have produced, and the caller believes their whole submission landed. Refuse the request, persist nothing, and record which field caused it in the decision record rather than in the response body — what the response is allowed to say about a denied field is a separate decision with its own disclosure cost. The status code is `403`: the server knows who the caller is and is refusing them. A `401` with `WWW-Authenticate` would say something different and untrue — that the caller has not identified themselves. ## What it costs to get wrong Two bugs account for most field-level write failures in practice, and both come from reading the rule off the user interface instead of off the model: - **The field was on the form, so saving it must be allowed.** Rendering a value for reading is a read decision. The server that sent the field to the screen made no promise about writes. - **The field was hidden, so it cannot be submitted.** Hiding a control is presentation. The request body is whatever the caller chooses to send, and the check has to hold whether or not any screen ever showed the field.
- A client resubmits the whole pupil record with one value changed. Which fields does your write check authorize?Every field the body carries, because a full replacement asks the server to set all of them. A class teacher echoing back the record will therefore be refused on `safeguardingFlag` even though they changed nothing. The fix is the request shape — send only the fields being changed — not a delta computed against stored values, which would make the verdict depend on data that moves under you.
- Which status code should the response carry when a caller sends a field they may not write?`403`. The server has identified the caller and is refusing them, which is exactly what `403` means; `401` with `WWW-Authenticate` would falsely claim the caller has not identified themselves. `422` is also wrong: the body is not semantically malformed, it is authorized against and denied. What the response says beyond the code is a separate disclosure decision.
saying these in an interview costs you the question
- If the field renders on the form, saving it must be permitted.
- One permission per field is enough; reads and writes match anyway.
- Authorize only the fields whose submitted values differ from stored ones.
- A field hidden by the client cannot be submitted by that client.
- Apply the fields that are permitted and drop the rest.
- The database will reject any write the caller is not allowed to make.