---
title: 'Policy Structure'
source: 'https://academia.sh/en/courses/cloud-governance/policy-structure'
course: 'Cloud Governance, Security and Cost'
language: en
updated: '2026-09-13T05:07:35+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Writes a policy document evaluator built from effect, action, resource, and condition fields'
    - 'Compares four phrasings of the same intent by scope: covered pairs, excess scope, and destructive action'
    - 'Counts how an explicit deny beats an allow regardless of order, and how many legitimate requests a condition stops'
---

# Policy Structure

A policy document evaluator is written and the same intent is expressed four separate ways to count how many actions each phrasing actually covers: the scope a wildcard opens is pulled apart item by item, an explicit deny is shown to always win, and how far a condition narrows the scope is measured.

The previous lesson's connections said "the ingest team has access to the measurement store" without
saying which access. A connection actually stands for a set of actions, and until that set is written
the connection count measures nothing. A policy document writes that set with four fields: **effect**
(allow or deny), **action**, **resource**, and **condition**. This lesson's measure is what a phrasing
actually covers.

The gap sits between the writer's intent and what the evaluator reads. Intent can be narrow while the
phrasing stays broad, and the gap produces no warning anywhere, because unused extra permission is not
an error. It can only be seen by counting.

- **CI7.** The model carries the fictional regional measurement network's seven resource types,
  forty-four action types, and forty-two resource instances. No real service is run; what is measured
  is the evaluator built with `node`.
- **CI8.** A policy document carries four fields. The field set is defined within the lesson; richer
  phrasings add more fields, the measurement itself does not change shape.
- **CI9.** The wildcard is only `*`, and its matching rule is defined in the code. As the pattern
  language grows, so does the scope; the numbers here are the scope this one character opens.
- **CI10.** The destructive action set was marked by hand: delete verbs plus the verbs that change
  retention, access, or environment settings. The marking is debatable; the counted value depends on
  it.
- **CI11.** Context was reduced to four profiles — internal and external network, fresh and stale
  auth. The condition measurement is read against these four profiles.
- **CI12.** Document-order shuffling used a generator we wrote ourselves; the seed `31071923` is
  written into the code, and a hundred twenty orders were tried.

## The Evaluator

```js
// policy.mjs — policy document evaluator; not a real service, only a model
export const TYPE_ACTION = {                             // resource type -> actions defined on it
  'machine-cluster': ['list', 'read', 'create', 'start', 'stop', 'resize', 'delete', 'tag-write'],
  'network-config': ['list', 'read', 'rule-add', 'rule-delete', 'route-write', 'endpoint-open'],
  'object-store': ['list', 'read', 'write', 'delete', 'version-read', 'lifecycle-write', 'access-write'],
  'measurement-store': ['list', 'read', 'write', 'delete', 'query', 'retention-write'],
  'function-package': ['list', 'read', 'publish', 'invoke', 'version-delete', 'env-var-write'],
  'edge-node-group': ['list', 'read', 'record-add', 'record-delete', 'firmware-update'],
  'record-stream': ['list', 'read', 'write', 'consume', 'retention-write', 'delete'],
};
export const ENV = ['development', 'staging', 'production'];
const INSTANCE = ['region-1', 'region-2'];                // two instances per type and environment
export const DESTRUCTIVE = new Set(['delete', 'rule-delete', 'record-delete', 'version-delete', 'access-write',
  'env-var-write', 'retention-write', 'lifecycle-write', 'stop']);

export const ACTION = Object.entries(TYPE_ACTION).flatMap(([t, fs]) => fs.map((f) => `${t}:${f}`));
export const RESOURCE = Object.keys(TYPE_ACTION).flatMap((t) =>
  ENV.flatMap((e) => INSTANCE.map((i) => ({ name: `${t}/${e}/${i}`, type: t, env: e }))));
export const PAIR = ACTION.flatMap((a) =>                // (action, resource) universe
  RESOURCE.filter((r) => r.type === a.split(':')[0]).map((r) => ({ action: a, resource: r })));
export const CONTEXT = [                                 // request context; written by hand
  { name: 'internal-fresh', net: 'internal', authAge: 5 }, { name: 'internal-stale', net: 'internal', authAge: 240 },
  { name: 'external-fresh', net: 'external', authAge: 5 }, { name: 'external-stale', net: 'external', authAge: 240 },
];
export const REQUEST = PAIR.flatMap((p) => CONTEXT.map((c) => ({ ...p, context: c })));

export const pattern = (p, s) =>                          // the wildcard is only *
  new RegExp(`^${p.split('*').map((x) => x.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`).test(s);
const conditionHolds = (c, r) => (c.net === undefined || c.net === r.context.net) &&
  (c.authAgeAtMost === undefined || r.context.authAge <= c.authAgeAtMost) &&
  (c.env === undefined || c.env === r.resource.env);
export const matches = (s, r) => s.action.some((p) => pattern(p, r.action)) &&
  s.resource.some((p) => pattern(p, r.resource.name)) && (!s.condition || conditionHolds(s.condition, r));

// Rule: explicit deny always wins; document order does not change the outcome
export function evaluate(statements, request) {
  const matched = statements.filter((s) => matches(s, request));
  if (matched.some((s) => s.effect === 'deny')) return 'explicit-deny';
  return matched.some((s) => s.effect === 'allow') ? 'allow' : 'implicit-deny';
}
// For comparison: the first match wins
export function firstMatch(statements, request) {
  for (const s of statements) if (matches(s, request)) return s.effect === 'deny' ? 'explicit-deny' : 'allow';
  return 'implicit-deny';
}
export function* generator(seed) {                        // our own generator; the seed is visible
  let x = seed % 2147483647;
  for (;;) { x = (x * 48271) % 2147483647; yield x; }
}
```

The evaluator produces three outcomes. **Allow** means a matching allow statement exists and no
matching deny statement does. **Explicit deny** means a deny statement matches, and it wins no matter
how many allow statements also match. **Implicit deny** means no statement matches at all — what is
not written is forbidden. The second evaluator stands for comparison: a rule where the first matching
statement wins.

## The Same Intent, Four Phrasings

```js
// run1.mjs — the same intent is phrased four ways and the actions it actually covers are counted
import { ACTION, RESOURCE, PAIR, DESTRUCTIVE, matches, CONTEXT } from './policy.mjs';

const intent = (p) => ['read', 'list', 'query'].includes(p.action.split(':')[1]) &&
  p.resource.type === 'measurement-store' && p.resource.env === 'production';
const PHRASING = [
  ['A  action *, resource *', { effect: 'allow', action: ['*'], resource: ['*'] }],
  ['B  action measurement-store:*', { effect: 'allow', action: ['measurement-store:*'], resource: ['*'] }],
  ['C  action *:read *:list *:query', { effect: 'allow', action: ['*:read', '*:list', '*:query'],
    resource: ['measurement-store/*'] }],
  ['D  action and resource written in full', { effect: 'allow',
    action: ['measurement-store:read', 'measurement-store:list', 'measurement-store:query'],
    resource: ['measurement-store/production/*'] }],
];
console.log(`action types ${ACTION.length}, resources ${RESOURCE.length}, (action, resource) universe ${PAIR.length}`);
console.log(`contexts ${CONTEXT.length}; pairs the intent requires ${PAIR.filter(intent).length}\n`);
console.log('phrasing                                 covered   excess  destructive action   types touched');
for (const [label, s] of PHRASING) {
  const covered = PAIR.filter((p) => matches(s, { ...p, context: CONTEXT[0] }));
  const excess = covered.filter((p) => !intent(p)).length;
  const destructive = covered.filter((p) => DESTRUCTIVE.has(p.action.split(':')[1])).length;
  const types = new Set(covered.map((p) => p.resource.type)).size;
  console.log(`${label.padEnd(40)} ${String(covered.length).padEnd(9)} ${String(excess).padEnd(7)} ` +
    `${String(destructive).padEnd(20)} ${types}`);
}
```

```
action types 44, resources 42, (action, resource) universe 264
contexts 4; pairs the intent requires 6

phrasing                                 covered   excess  destructive action   types touched
A  action *, resource *                  264       258     78                   7
B  action measurement-store:*            36        30      12                   1
C  action *:read *:list *:query          18        12      0                    1
D  action and resource written in full   6         0       0                    1
```

All four cover the same intent: the ingest team should be able to read the measurement stores in
production. The intent requires exactly six pairs. Phrasing A opens two hundred sixty-four of them —
the entire universe — and seventy-eight of the two hundred fifty-eight extra pairs land on a
destructive action. The writer never wanted that; they left fields unfilled without meaning to.

The gap between B and C shows which field was loosened. B leaves the action field open and covers
thirty-six pairs; twelve are destructive, because two of the measurement store's six actions are
delete and write-retention. C writes the action field and loosens the resource field instead:
eighteen pairs, none destructive, but all three environments are inside. The only difference between
C and D is one segment of the resource pattern, and scope changes threefold. The number says: loosening
the action field brings in destructive actions, loosening the resource field removes the environment
boundary. Two separate risks, written two separate ways.

## The Scope a Wildcard Opens

```js
// run2.mjs — the scope a wildcard opens is pulled apart item by item
import { ACTION, RESOURCE, PAIR, DESTRUCTIVE, pattern } from './policy.mjs';

const pairCount = (ap, rp) => PAIR.filter((p) => pattern(ap, p.action) && pattern(rp, p.resource.name)).length;
console.log('action pattern           matching action types   destructive        pairs with every resource');
for (const p of ['*', 'measurement-store:*', '*:read', '*:delete', '*:*-write', 'measurement-store:read']) {
  const e = ACTION.filter((a) => pattern(p, a));
  console.log(`${p.padEnd(24)} ${String(e.length).padEnd(23)} ` +
    `${String(e.filter((a) => DESTRUCTIVE.has(a.split(':')[1])).length).padEnd(18)} ${pairCount(p, '*')}`);
}
console.log('\nresource pattern                 matching resources   envs   pairs with measurement-store:*');
for (const p of ['*', '*/production/*', 'measurement-store/*', 'measurement-store/production/*']) {
  const e = RESOURCE.filter((r) => pattern(p, r.name));
  console.log(`${p.padEnd(32)} ${String(e.length).padEnd(20)} ${String(new Set(e.map((r) => r.env)).size).padEnd(6)} ` +
    `${pairCount('measurement-store:*', p)}`);
}
console.log('\n"measurement-store:*" item by item:');
console.log('  ' + ACTION.filter((a) => pattern('measurement-store:*', a))
  .map((a) => `${a.split(':')[1]}${DESTRUCTIVE.has(a.split(':')[1]) ? ' (destructive)' : ''}`).join(', '));
const full = pairCount('measurement-store:read', 'measurement-store/production/*');
console.log('\nmultiple of the fully written form:');
for (const [ap, rp] of [['measurement-store:read', 'measurement-store/production/*'], ['measurement-store:read', 'measurement-store/*'],
  ['measurement-store:*', 'measurement-store/production/*'], ['measurement-store:*', 'measurement-store/*'], ['*', '*']]) {
  console.log(`  ${ap.padEnd(24)} ${rp.padEnd(32)} pairs ${String(pairCount(ap, rp)).padEnd(4)} multiple ${pairCount(ap, rp) / full}`);
}
```

```
action pattern           matching action types   destructive        pairs with every resource
*                        44                      13                 264
measurement-store:*      6                       2                  36
*:read                   7                       0                  42
*:delete                 4                       4                  24
*:*-write                7                       5                  42
measurement-store:read   1                       0                  6

resource pattern                 matching resources   envs   pairs with measurement-store:*
*                                42                   3      36
*/production/*                   14                   1      12
measurement-store/*              6                    3      36
measurement-store/production/*   2                    1      12

"measurement-store:*" item by item:
  list, read, write, delete (destructive), query, retention-write (destructive)

multiple of the fully written form:
  measurement-store:read   measurement-store/production/*   pairs 2    multiple 1
  measurement-store:read   measurement-store/*              pairs 6    multiple 3
  measurement-store:*      measurement-store/production/*   pairs 12   multiple 6
  measurement-store:*      measurement-store/*              pairs 36   multiple 18
  *                        *                                pairs 264  multiple 132
```

A single wildcard written alone matches forty-four action types, thirteen of them destructive.
`measurement-store:*` means six verbs, and opening it item by item shows two of them are delete and
write-retention — both verbs someone granting "read permission" never had in mind. The pattern text
is five characters long; the scope it opens is sixfold.

The last table shows the two fields behaving like a product. A fully written permission covers two
pairs. Turning the environment segment of the resource pattern into a star multiplies that by three,
turning the verb segment of the action pattern into a star multiplies it by six, and both together by
eighteen. Reducing both fields to a single star multiplies it by a hundred thirty-two. This is why
counting how many stars appear in a policy document is not enough on its own: which segment the star
sits in matters more than how many there are.

## Explicit Deny and Condition

```js
// run3.mjs — explicit deny always wins; how much a condition narrows the scope
import { REQUEST, evaluate, firstMatch, matches, generator } from './policy.mjs';

const DOCUMENT = [
  { name: 'p1-read', effect: 'allow', action: ['*:read', '*:list', '*:query'], resource: ['*'] },
  { name: 'p2-write', effect: 'allow', action: ['measurement-store:write', 'record-stream:write'],
    resource: ['*/staging/*', '*/development/*'] },
  { name: 'p3-maintenance', effect: 'allow', action: ['*'], resource: ['machine-cluster/*'] },
  { name: 'p4-delete-ban', effect: 'deny', action: ['*:delete', '*:rule-delete', '*:record-delete', '*:version-delete'],
    resource: ['*/production/*'] },
  { name: 'p5-external-net-ban', effect: 'deny', action: ['*'], resource: ['*/production/*'], condition: { net: 'external' } },
];
const count = (ds, f = evaluate) => REQUEST.reduce((a, r) => (a[f(ds, r)]++, a),
  { allow: 0, 'explicit-deny': 0, 'implicit-deny': 0 });
console.log(`request universe ${REQUEST.length} = 264 (action, resource) x 4 contexts\n`);
console.log('document set                 allow  explicit deny   implicit deny');
const base = count(DOCUMENT);
console.log(`${'five documents (full set)'.padEnd(28)} ${String(base.allow).padEnd(6)} ` +
  `${String(base['explicit-deny']).padEnd(15)} ${base['implicit-deny']}`);
const overlapping = REQUEST.filter((r) => DOCUMENT.some((d) => d.effect === 'allow' && matches(d, r)) &&
  DOCUMENT.some((d) => d.effect === 'deny' && matches(d, r))).length;
console.log(`requests matching both an allow and a deny document: ${overlapping} — deny wins in all of them`);

console.log('\ndocument removed                 allow  explicit deny   implicit deny');
for (const name of ['p4-delete-ban', 'p5-external-net-ban']) {
  const s = count(DOCUMENT.filter((d) => d.name !== name));
  console.log(`${(name + ' removed').padEnd(32)} ${String(s.allow).padEnd(6)} ${String(s['explicit-deny']).padEnd(15)} ${s['implicit-deny']}`);
}
const narrowed = DOCUMENT.map((d) => (d.name === 'p1-read' ? { ...d, condition: { authAgeAtMost: 60 } } : d));
const stalled = REQUEST.filter((r) => evaluate(DOCUMENT, r) === 'allow' && evaluate(narrowed, r) !== 'allow');
console.log(`once p1 gets an "auth age at most 60 min" condition, allow becomes ${count(narrowed).allow}; ${stalled.length} requests stall, ` +
  `actions: ${[...new Set(stalled.map((r) => r.action.split(':')[1]))].join(', ')}`);

const gen = generator(31071923);                          // document order is shuffled with our own generator
const shuffle = (a) => { const c = [...a];
  for (let i = c.length - 1; i > 0; i--) { const j = gen.next().value % (i + 1); [c[i], c[j]] = [c[j], c[i]]; }
  return c; };
const baseFirst = REQUEST.map((r) => firstMatch(DOCUMENT, r));
let diffA = 0, diffB = 0;
for (let n = 0; n < 120; n++) {
  const s = shuffle(DOCUMENT);
  REQUEST.forEach((r, x) => {
    if (evaluate(s, r) !== evaluate(DOCUMENT, r)) diffA++;
    if (firstMatch(s, r) !== baseFirst[x]) diffB++;
  });
}
console.log(`\nacross 120 shuffled orders, ${REQUEST.length} requests:`);
console.log(`  explicit-deny-wins rule  outcome changed ${diffA}`);
console.log(`  first-match-wins rule    outcome changed ${diffB}`);
```

```
request universe 1056 = 264 (action, resource) x 4 contexts

document set                 allow  explicit deny   implicit deny
five documents (full set)    448    204             404
requests matching both an allow and a deny document: 88 — deny wins in all of them

document removed                 allow  explicit deny   implicit deny
p4-delete-ban removed            452    176             428
p5-external-net-ban removed      528    56              472
once p1 gets an "auth age at most 60 min" condition, allow becomes 318; 130 requests stall, actions: list, read, query

across 120 shuffled orders, 1056 requests:
  explicit-deny-wins rule  outcome changed 0
  first-match-wins rule    outcome changed 5588
```

Four hundred forty-eight of one thousand fifty-six requests are allowed, two hundred four are
explicitly denied, four hundred four are implicitly denied because they match no document at all.
Eighty-eight requests match both an allow document and a deny document, and deny wins on all of them.
That is what explicit deny means: no matter how many allow documents match, no matter how many stars
they carry, a single deny document overrides them.

The document-removal rows separate how much of that explicit deny is actually doing work. Removing
`p4` drops explicit deny from two hundred four to a hundred seventy-six — twenty-eight requests — but
allow only rises by four. The remaining twenty-four requests were already falling into implicit deny,
because no allow document opened them. `p4` looks like it denies twenty-eight requests but is actually
blocking only four. The table for `p5` tells a different story: eighty of its hundred forty-eight
explicit denials turn into allows once it is removed. Two deny documents that look alike carry a
fivefold difference in weight.

The condition row gives the cost of narrowing. Once `p1` gets an auth-age condition, allowed requests
drop from four hundred forty-eight to three hundred eighteen; the hundred thirty stalled requests use
read, list, and query — none of them destructive. The condition did not stop a destructive action; it
stopped legitimate reads that arrived with stale auth. The condition's payoff has to be sought in a
different column, because this column shows only its cost.

The last two lines measure how much order matters. Across a hundred twenty shuffled orders, one
thousand fifty-six requests were re-evaluated each time: zero outcomes changed under the explicit-deny
rule. Under the first-match rule, five thousand five hundred eighty-eight changed — roughly four
percent of the hundred twenty-six thousand seven hundred twenty evaluations. This is one of explicit
deny's advantages: the outcome does not depend on the order documents were written in, so when two
teams write documents without knowing about each other, the result is one both of them could have
predicted.

## Summary

- 44 action types and 42 resources build 264 (action, resource) pairs; a six-pair intent is covered
  264, 36, 18, and 6 ways by four different phrasings.
- Loosening the action field brings in destructive actions (12 pairs in phrasing B), loosening the
  resource field removes the environment boundary (threefold between C and D); two separate risks.
- Which segment a star sits in matters more than the count: the environment segment is threefold, the
  verb segment sixfold, both together eighteenfold, a single star a hundred thirty-twofold.
- 88 requests match both an allow and a deny document, and deny wins on all of them; across 120
  shuffled orders, the outcome changed for 0 under explicit-deny-wins, 5,588 under first-match-wins.
- Two deny documents look alike but carry different weight: `p4` denies 28 requests but blocks only
  4, while 80 of `p5`'s 148 denials are a real block.
- The condition added to `p1` drops allowed requests from 448 to 318; all 130 stalled requests are
  legitimate reads, none of them destructive.

## Next Step

This lesson's five documents all sat in one place, and all of them were attached to a single subject.
A resource can carry a policy too: a document written onto the store says who can reach it, from the
resource's own side. A request then passes through two separate document sets, and the outcome is
their intersection. The next lesson evaluates both directions together: how many requests are denied
for being allowed in only one direction, why a resource-based policy becomes mandatory once the
account boundary is crossed, and how many documents it takes to find out why an access was denied.
