Chapter 27 — Control design for scoped intervention
Lesson map
Lesson 27 published. Map, example, EX053.
flowchart TD
N0["Learning outcomes and connection"]
N1["27.1 Anatomy of a scoped control"]
N2["27.2 Why selectors are dangerous"]
N3["27.4 Observability and reversibility"]
N4["27.5 Testing the control"]
N5["Chapter synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5
N5 --> EX["Worked example"]
EX --> Q["Exercise EX053"]
Logical drawing
flowchart LR
N0["Learning outcomes and connection"]
N1["27.1 Anatomy of a scoped control"]
N2["27.2 Why selectors are dangerous"]
N3["27.4 Observability and reversibility"]
N4["27.5 Testing the control"]
N5["Chapter synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX053"]
[Learning outcomes and connection]
|
v
[27.1 Anatomy of a scoped control]
|
v
[27.2 Why selectors are dangerous]
|
v
[27.4 Observability and reversibility]
|
v
[27.5 Testing the control]
|
v
[Chapter synthesis]
|
v
[Worked example] --> [Exercise EX053]
Learning outcomes and connection
After this chapter you can (1) design a containment control whose scope is checked before execution; (2) add approval gates, blast-radius limits and dry runs; (3) make every effect observable and reversible; and (4) test that the control cannot affect an unrelated tenant. It builds on Chapters 10, 25 and 26 and prepares Chapters 31, 34 and 38.
27.1 Anatomy of a scoped control
A containment control receives a request (case ID, action, target selectors, approval reference) and must: resolve selectors to concrete, tenant-qualified identifiers; check scope — every target within the case's tenant and within the approval; check limits — number of targets, criticality class; plan — produce the exact list of changes (dry run); execute — apply changes with idempotent operations; observe — record before/after state of each target; expose — publish the effect to the case and audit log; and reverse — provide a one-step undo using recorded prior state.
27.2 Why selectors are dangerous
Requests often name targets loosely ("disable alice"), and the same name exists in several tenants or directories. Resolution must be explicit and its result shown to the approver; approval refers to resolved identifiers, not to names. Complete mediation (Saltzer and Schroeder, 1975) means the check happens at execution time, against current state, not only when the request was written. Lab 27 below reproduces this: the selector alice matches both t1/alice and t2/alice. Approving t1/alice rather than alice is what makes the tenant check meaningful at execution time.
27.3 Worked example (synthetic): suspect account and a protected tenant
"""Lab 27 (synthetic): scoped containment control with pre-checks, dry-run and effect log."""
ACCOUNTS = {"t1/alice": "active", "t1/svc-sync": "active", "t2/alice": "active", "t2/admin": "active"}
def contain(targets, case_tenant, approved_ids, dry_run=True):
log, plan = [], []
for a in targets:
tenant = a.split("/")[0]
if tenant != case_tenant: log.append(("DENY", a, "outside case tenant")); continue
if a not in approved_ids: log.append(("DENY", a, "not in approval")); continue
if a not in ACCOUNTS: log.append(("DENY", a, "unknown account")); continue
plan.append(a)
if len(plan) > 3: return [("DENY", "*", "blast radius above limit 3")], {}
before = dict(ACCOUNTS)
if not dry_run:
for a in plan: ACCOUNTS[a] = "disabled"; log.append(("DONE", a, "disabled"))
else:
log += [("PLAN", a, "would disable") for a in plan]
changed = {k: (before[k], ACCOUNTS[k]) for k in ACCOUNTS if before[k] != ACCOUNTS[k]}
return log, changed
# The suspicious selector matched by user name, not tenant-qualified id
targets = ["t1/alice", "t2/alice"]
for dry in (True, False):
log, changed = contain(targets, "t1", approved_ids={"t1/alice"}, dry_run=dry)
print("dry-run" if dry else "execute", log, "changed:", changed)
print("unrelated tenant untouched:", ACCOUNTS["t2/alice"] == "active" and ACCOUNTS["t2/admin"] == "active")
print("reverse:", {a: "active" for a, s in ACCOUNTS.items() if s == "disabled"})
Expected output:
dry-run [('DENY', 't2/alice', 'outside case tenant'), ('PLAN', 't1/alice', 'would disable')] changed: {}
execute [('DENY', 't2/alice', 'outside case tenant'), ('DONE', 't1/alice', 'disabled')] changed: {'t1/alice': ('active', 'disabled')}
unrelated tenant untouched: True
reverse: {'t1/alice': 'active'}
The selector matched alice in two tenants. The control denied the tenant-2 account (outside case tenant) and would have denied it again as "not in approval". The dry run shows the plan; execution changes exactly one account; the effect log lists before/after state; the unrelated tenant is untouched; and the reverse operation is derived from recorded prior state. A blast-radius limit (three targets) stops a selector that unexpectedly expands to many accounts. If the approval had wrongly listed t2/alice as well, the tenant check would still have denied it first.
27.4 Observability and reversibility
Each action emits a structured record: request ID, approval ID, actor, resolved target, prior state, new state, timestamp, result. Reversal is itself an action requiring authority (often lower than the original). Some actions are not fully reversible — revoked tokens cannot be un-revoked; deleted mail rules must be recreated from recorded definitions — so the control must store enough prior state to reconstruct, and must state which actions are irreversible to the approver before approval.
In Lab 27, the recorded prior state ('active', 'disabled') is what lets the reverse step restore t1/alice without guessing. If the same control instead revoked t1/alice's sessions, the dry-run plan would have to say that this part cannot be undone.
27.5 Testing the control
Tests: positive (in-scope target disabled); negative (cross-tenant target denied even if named in approval; target not in approval denied); limit (four targets denied as a set, not partially executed); idempotence (executing twice changes nothing more); reversal (state restored exactly); failure (provider error midway → partial-execution report and no silent success).
The Lab 27 code already passes the positive and cross-tenant negative tests, with t2/alice denied and t1/alice disabled. Its limit check runs before any change, so four in-scope targets produce a single denial. The provider-error test needs a fake account store that fails after the first disable, because the lab's in-memory dictionary never fails.
27.6 Errors and limits
- Approving names rather than resolved identifiers.
- Checks performed at request time only.
- Partial execution reported as success.
- Irreversible actions not flagged before approval.
Lab 27 guards against the first two errors by approving t1/alice rather than the name alice, and by checking tenant and approval inside contain when it runs. The limit check returns before any account changes, so four targets are never half-disabled and reported as success. The lab disables accounts, which can be reversed, so an irreversible action such as token revocation would need its own warning.
Exercises — chapter 27
EX053 — Negative tests. Objective: prove scope. Inputs: Lab 27. Steps: write three additional calls: approval includes t2/alice but case tenant is t1; four t1 targets approved; unknown account; record outputs and state that the unrelated tenant is untouched. Expected evidence: calls and outputs. Rubric: 3 = all three denials shown with reasons and invariant checked; 2 = two; 1 = one; 0 = none.
EX054 — Partial failure. Objective: design for midway errors. Inputs: a three-target action where the provider fails on the second target. Steps: specify the control's behaviour, records and the approver's view. Expected evidence: specification. Rubric: 3 = no silent success, per-target result, option to roll back first target or retry second, case updated; 2 = partial; 1 = vague; 0 = none.
Chapter synthesis
A scoped control resolves targets explicitly, checks tenant, approval and limits at execution, shows a dry-run plan, records before/after state, exposes effects and supports reversal — and is tested especially for what it must refuse.
Continue studying — chapter 27 of 50
← Chapter 26 · Course chapter index · Chapter 28 →
Continue to the next authored lesson.