Add an offline attribute-admission branch beside exact-record review.
Compact-sitting v8 can admit a new memo from carried class, submitter, and approval attributes, but shipped sets are empty and exact-record decisions stay on their pins. The sitting image pin is unchanged. Assistant: grok Assistant-Session: 01a10329-31cb-78d3-b7a3-788c2f91d6d4
This commit is contained in:
parent
d014453a1a
commit
e85f511c4f
8 changed files with 1237 additions and 10 deletions
|
|
@ -92,8 +92,12 @@ with tempfile.TemporaryDirectory() as temp:
|
|||
# every act stays scoped to the act records, with the 900 s window.
|
||||
list_only = json.loads((r / 'list_only_records.json').read_text())
|
||||
listable = {k: dict(v, memo_version=1) for k, v in records.items()} | list_only
|
||||
now = int(time.time())
|
||||
for memo, record in listable.items():
|
||||
# Sample the clock per check. A single `now` goes stale while each
|
||||
# check reloads the package, and the 12h case has only 60s of slack.
|
||||
def stamp():
|
||||
return int(time.time())
|
||||
|
||||
request = {
|
||||
'id': 'local-regression',
|
||||
'tenant': 'tenant:platform',
|
||||
|
|
@ -104,7 +108,7 @@ with tempfile.TemporaryDirectory() as temp:
|
|||
'tenant_source': 'registration-supplied',
|
||||
'principal_type_source': 'authentication-derived',
|
||||
'assurance': {'level': 'aal2', 'mfa': True, 'methods': ['pwd', 'otp'],
|
||||
'source': 'key-cape', 'at': now},
|
||||
'source': 'key-cape', 'at': stamp()},
|
||||
},
|
||||
},
|
||||
'resource': {'id': memo, 'type': 'decision-memo',
|
||||
|
|
@ -125,14 +129,14 @@ with tempfile.TemporaryDirectory() as temp:
|
|||
return c
|
||||
|
||||
at = ['subject', 'attributes', 'assurance', 'at']
|
||||
check(label + ':fresh', request, 'allow')
|
||||
check(label + ':fresh', variant(at, stamp()), 'allow')
|
||||
# The observed production refusal: 1744 s, allowed for list...
|
||||
check(label + ':observed-1744s', variant(at, now - 1744), 'allow')
|
||||
check(label + ':inside-12h', variant(at, now - 43200 + 60), 'allow')
|
||||
check(label + ':observed-1744s', variant(at, stamp() - 1744), 'allow')
|
||||
check(label + ':inside-12h', variant(at, stamp() - 43200 + 60), 'allow')
|
||||
# ...but the 12 h bound is real, and read does not follow list.
|
||||
check(label + ':past-12h', variant(at, now - 43200 - 60), 'deny')
|
||||
check(label + ':past-12h', variant(at, stamp() - 43200 - 60), 'deny')
|
||||
is_act_record = memo in records
|
||||
read_stale = variant(at, now - 1744) | {'action': 'read'}
|
||||
read_stale = variant(at, stamp() - 1744) | {'action': 'read'}
|
||||
check(label + ':read-stale-refused', read_stale, 'deny')
|
||||
if not is_act_record:
|
||||
# T03 records are listable only; no act widens to them, even fresh.
|
||||
|
|
@ -143,7 +147,7 @@ with tempfile.TemporaryDirectory() as temp:
|
|||
('no-group', ['subject', 'attributes', 'groups'], []),
|
||||
('service', ['subject', 'type'], 'service'),
|
||||
('agent', ['subject', 'type'], 'agent'),
|
||||
('future-mfa', at, now + 300),
|
||||
('future-mfa', at, stamp() + 300),
|
||||
('no-mfa', ['subject', 'attributes', 'assurance', 'mfa'], False),
|
||||
('aal1', ['subject', 'attributes', 'assurance', 'level'], 'aal1'),
|
||||
('forged-human-route', ['subject', 'attributes', 'principal_type_source'], 'registration-supplied'),
|
||||
|
|
|
|||
119
tools/gen_informed_decision_admission.py
Normal file
119
tools/gen_informed_decision_admission.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate attribute-admission sets from informed-decision's submitter registry.
|
||||
|
||||
The compact-sitting attribute branch does not author who may pose. It copies
|
||||
submitter ids and the union of their memo classes from
|
||||
``deploy/intake-submitters.json``. An empty registry generates empty sets, so
|
||||
the branch admits nobody. A malformed registry refuses to write.
|
||||
|
||||
Usage:
|
||||
python3 tools/gen_informed_decision_admission.py [--source PATH]
|
||||
python3 tools/gen_informed_decision_admission.py [--source PATH] --check
|
||||
|
||||
``--check`` reports drift and writes nothing. ``--policy`` is for tests.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
POLICY = ROOT / "examples" / "informed-decision-sitting" / "policy.md"
|
||||
DEFAULT_SOURCE = Path(os.path.expanduser("~/informed-decision/deploy/intake-submitters.json"))
|
||||
|
||||
BEGIN = "# BEGIN GENERATED admitted_sets (tools/gen_informed_decision_admission.py; do not edit by hand)"
|
||||
END = "# END GENERATED admitted_sets"
|
||||
SA = re.compile(r"^system:serviceaccount:[a-z0-9-]+:[a-z0-9-]+$")
|
||||
SLUG = re.compile(r"^[a-z0-9][a-z0-9-]{1,62}$")
|
||||
ROW_KEYS = {"submitter_id", "memo_classes", "review_groups", "daily_limit"}
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(message)
|
||||
|
||||
|
||||
def load_sets(path: Path) -> tuple[list[str], list[str]]:
|
||||
try:
|
||||
document = json.loads(path.read_text())
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
fail(f"cannot read submitter registry {path}: {exc}")
|
||||
if not isinstance(document, dict):
|
||||
fail("intake submitters must be an object; refusing to generate")
|
||||
submitters: list[str] = []
|
||||
classes: set[str] = set()
|
||||
for username, row in document.items():
|
||||
if not isinstance(username, str) or not SA.fullmatch(username) or not isinstance(row, dict):
|
||||
fail(f"refusing submitter row {username!r}")
|
||||
if set(row) != ROW_KEYS:
|
||||
fail(f"refusing submitter row {username!r}: keys must be {sorted(ROW_KEYS)}")
|
||||
submitter_id = row["submitter_id"]
|
||||
if not isinstance(submitter_id, str) or not SLUG.fullmatch(submitter_id):
|
||||
fail(f"refusing submitter id {submitter_id!r}")
|
||||
limit = row["daily_limit"]
|
||||
if type(limit) is not int or limit < 1:
|
||||
fail(f"refusing daily_limit for {username}")
|
||||
for field in ("memo_classes", "review_groups"):
|
||||
values = row[field]
|
||||
if not isinstance(values, list) or any(not isinstance(item, str) or not SLUG.fullmatch(item) for item in values):
|
||||
fail(f"refusing {field} for {username}")
|
||||
submitters.append(submitter_id)
|
||||
classes.update(row["memo_classes"])
|
||||
if len(set(submitters)) != len(submitters):
|
||||
fail("duplicate submitter_id; refusing to generate")
|
||||
return sorted(set(submitters)), sorted(classes)
|
||||
|
||||
|
||||
def render_set(name: str, values: list[str]) -> str:
|
||||
if not values:
|
||||
return f"{name} := set()"
|
||||
body = "\n".join(f' "{value}",' for value in values)
|
||||
return f"{name} := {{\n{body}\n}}"
|
||||
|
||||
|
||||
def render_block(submitters: list[str], classes: list[str]) -> str:
|
||||
return (
|
||||
f"{BEGIN}\n"
|
||||
f"{render_set('admitted_submitters', submitters)}\n"
|
||||
"\n"
|
||||
f"{render_set('admitted_classes', classes)}\n"
|
||||
f"{END}"
|
||||
)
|
||||
|
||||
|
||||
def replace_block(policy: str, block: str, path: Path) -> str:
|
||||
pattern = re.compile(re.escape(BEGIN) + r".*?" + re.escape(END), re.S)
|
||||
if not pattern.search(policy):
|
||||
fail(f"{path}: generated markers not found")
|
||||
return pattern.sub(lambda _: block, policy, count=1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
|
||||
parser.add_argument("--policy", type=Path, default=POLICY)
|
||||
parser.add_argument("--check", action="store_true", help="report drift, write nothing")
|
||||
args = parser.parse_args()
|
||||
|
||||
submitters, classes = load_sets(args.source)
|
||||
current = args.policy.read_text()
|
||||
updated = replace_block(current, render_block(submitters, classes), args.policy)
|
||||
if args.check:
|
||||
if updated != current:
|
||||
print(f"drift: {args.policy}", file=sys.stderr)
|
||||
print(f"{len(submitters)} submitters, {len(classes)} classes; drift")
|
||||
return 1
|
||||
print(f"{len(submitters)} submitters, {len(classes)} classes; in sync")
|
||||
return 0
|
||||
if updated != current:
|
||||
args.policy.write_text(updated)
|
||||
print(f"wrote {args.policy}")
|
||||
print(f"{len(submitters)} submitters, {len(classes)} classes")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue