Replace ops-mason-build with read-only ops-mason-survey grant (MASON-IN-0004)

Founder decision 2026-10-07, option (c)+(a): ops-mason surveys and verifies
only. Policy/role writes, secret-id minting and token creation go through the
founder-attended railiance-platform delegated applier. ops-mason-build retired
to policies/retired/ (never installed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 132360@bnt-lap001
Assistant-Session: 769b1afd-4330-4c86-96b2-e510e26db42b
This commit is contained in:
tegwick 2026-10-07 10:44:24 +02:00
parent 47171385ce
commit 6b177270d3
10 changed files with 261 additions and 40 deletions

View file

@ -261,21 +261,47 @@ it as `BAO_TOKEN=$(cat ~/.claude-bao-token) bao <cmd>`.
### What the grant authorises ### What the grant authorises
Policy `ops-mason-build` (`policies/ops-mason-build.hcl`), 45 minutes, max one Policy `ops-mason-survey` (`policies/ops-mason-survey.hcl`), 45 minutes, max
hour, named `claude-<task>`: one hour, named `claude-<task>`. Read-only, by founder decision 2026-10-07
(MASON-IN-0004, option (c)+(a)); it replaces `ops-mason-build`, now in
`policies/retired/` and never to be installed.
| Allowed | Denied | | Allowed | Denied / not granted |
|---|---| |---|---|
| read `sys/mounts`, `sys/auth`, policy list — the phase-2 survey | **every read of `*/data/*`** on `platform`, `operators`, `secret` | | read/list `sys/mounts`, `sys/auth`; list `sys/policies/acl`, read `sys/policies/acl/*` — the phase-2 survey | **any policy write** (no create/update/delete on `sys/policies/acl/*`; explicit deny on its own policy) |
| create/update policies under `sys/policies/acl/*` | enabling or tuning mounts (`sys/mounts/*`) — a railiance-platform act | | read/list role definitions on `auth/kubernetes/role/*`, `auth/netkingdom/role/*`, `auth/jwt/role/*`; list `auth/approle/role`, read `auth/approle/role/<name>` | **any auth-role write**; AppRole `role-id`, `secret-id`, `secret-id/*`, `secret-id-accessor/*`, `custom-secret-id` (explicit deny) |
| create/update `auth/kubernetes/role/*`, `auth/approle/role/*` | anything not listed | | read/list KV **metadata** on `platform/`, `operators/` — enough to confirm a delivery landed | **every `*/data/*`** on `platform`, `operators`, `secret` (explicit deny); KV metadata writes |
| read KV **metadata** — versions and timestamps, enough to confirm a delivery landed | KV **values** | | `sys/capabilities`, `sys/capabilities-self` (query only), `auth/token/lookup-self`, `auth/token/revoke-self` | **token creation of any kind**; revoking other tokens; `sys/mounts/*` |
| mint and revoke short-lived test tokens, check capabilities | |
The denial is the point. `SCOPE.md` says ops-mason never touches secret values; The grant can show what exists and prove what a token can reach; it cannot
this makes OpenBao enforce it rather than leaving it to whoever is driving. An change anything. That removes the escalation MASON-IN-0004 described — write a
explicit `deny` outranks any grant, including one added to this policy later by policy without the denies, bind it to an AppRole, read the role-id, mint a
mistake. secret-id, log in — because none of those steps is granted.
### Installing the survey policy (founder act)
ops-mason cannot install its own grant. The founder runs, silently and with an
absolute path (the warden wrapper refuses output and runs with a temporary
`HOME`):
```bash
warden access openbao-platform-admin-login --exec -- sh -c \
'bao policy write ops-mason-survey /home/worsch/ops-mason/policies/ops-mason-survey.hcl >/dev/null 2>&1'
```
Silent check — the installed policy's hash into a 0600 receipt, compared with
the repo file's hash outside the wrapper:
```bash
warden access openbao-platform-admin-login --exec -- sh -c \
'umask 077; mkdir -p /home/worsch/.local/state/railiance/receipts; bao policy read ops-mason-survey 2>/dev/null | sha256sum > /home/worsch/.local/state/railiance/receipts/ops-mason-survey.sha256'
cat /home/worsch/.local/state/railiance/receipts/ops-mason-survey.sha256
sha256sum < /home/worsch/ops-mason/policies/ops-mason-survey.hcl
```
The two hashes match only if OpenBao returns the file byte-for-byte; if they
differ, compare the stored text against the file before concluding the install
is wrong (OpenBao may normalise a trailing newline).
### Choosing the task name ### Choosing the task name
@ -286,8 +312,13 @@ recognise later.
### When a broader grant is genuinely needed ### When a broader grant is genuinely needed
Enabling a mount, reading a value, or touching another subsystem's paths is out Writing a policy or auth role, minting a secret-id or a token, enabling a
of `ops-mason-build` on purpose. Do not widen the policy to get past a refusal. mount, reading a value, or touching another subsystem's paths is out of
Either the act belongs to another repo, or it needs its own named policy and its `ops-mason-survey` on purpose. Do not widen the policy to get past a refusal.
own decision — the way `operators/` did. Builds that need writes are applied by the founder through the
railiance-platform delegated applier (`scripts/credential-change.py
applier-apply` under `warden access openbao-platform-admin-login --exec`), as
for CCR-2026-0033/0034; ops-mason supplies the plan and verifies afterwards.
Anything else belongs to another repo or needs its own named policy and its
own founder decision.

View file

@ -78,8 +78,11 @@ tooling_contacts:
capability: "Create policies and auth roles for an approved construction plan (structure only, never secret values)" capability: "Create policies and auth roles for an approved construction plan (structure only, never secret values)"
intended_owner: secrets-engine intended_owner: secrets-engine
blocked_on: >- blocked_on: >-
No engine exposes a policy / auth-role construction surface; ops-mason No engine exposes a policy / auth-role construction surface. Since
runs the bao CLI under a scoped, time-boxed ops-mason-build token. MASON-IN-0004 (founder, 2026-10-07) ops-mason holds no build grant:
these writes are applied by the founder through the railiance-platform
delegated applier; ops-mason surveys and verifies under the read-only
ops-mason-survey token.
review: "2026-12-21" review: "2026-12-21"
gap_state: accepted gap_state: accepted
accepted_exception: *plan-approval-gate accepted_exception: *plan-approval-gate
@ -164,7 +167,7 @@ tooling_contacts:
tooling: OpenBao tooling: OpenBao
operation: "Operator OIDC login; mint a scoped, bounded token under the operator's own session" operation: "Operator OIDC login; mint a scoped, bounded token under the operator's own session"
write: true write: true
capability: "Hand an agent session a task-scoped ops-mason-build token instead of the operator's session" capability: "Hand an agent session a task-scoped, read-only ops-mason-survey token instead of the operator's session"
supplied_authority: none supplied_authority: none
note: >- note: >-
Runs under the operator's own authority and narrows it; ops-mason holds Runs under the operator's own authority and narrows it; ops-mason holds

View file

@ -64,7 +64,7 @@ Generate the section rather than gathering it by hand (MASON-WP-0008). The
founder mints the scoped grant; the agent then runs the survey with it: founder mints the scoped grant; the agent then runs the survey with it:
```bash ```bash
./scripts/bao-session.sh grant <task> # founder: ops-mason-build, 45m, claude-<task> ./scripts/bao-session.sh grant <task> # founder: ops-mason-survey (read-only), 45m, claude-<task>
./scripts/plan-survey.py \ ./scripts/plan-survey.py \
--kv-path platform/workloads/<consumer>/<name> \ --kv-path platform/workloads/<consumer>/<name> \
--policy workload-kv-read-<consumer>-<name> \ --policy workload-kv-read-<consumer>-<name> \
@ -83,10 +83,10 @@ matching ops-warden catalog entries, a UTC timestamp, `BAO_ADDR`, the token
draft answer into a decision; it is generated evidence, not a review. draft answer into a decision; it is generated evidence, not a review.
The survey refuses to run without a grant, refuses a token whose policies The survey refuses to run without a grant, refuses a token whose policies
are anything but `ops-mason-build` (plus `default`), and never prints the are anything but `ops-mason-survey` (plus `default`), and never prints the
token. Every bao call passes an allowlist that rejects `kv get` and any token. Every bao call passes an allowlist that rejects `kv get` and any
`*/data/*` path before a process starts. A query the grant does not permit `*/data/*` path before a process starts. A query the grant does not permit
is reported as `not permitted by ops-mason-build` and listed under is reported as `not permitted by ops-mason-survey` and listed under
"incomplete" rather than read as an empty answer. Policies with names that "incomplete" rather than read as an empty answer. Policies with names that
do not match the demand are not read, so a broadly named policy covering do not match the demand are not read, so a broadly named policy covering
the path is not detected; the output says so. the path is not detected; the output says so.

View file

@ -103,7 +103,8 @@ state_hub_intake_id: "01a06e68-8c29-7230-b0dc-88f1e374afa4"
id: MASON-IN-0004 id: MASON-IN-0004
kind: intake kind: intake
title: Close the policy-plus-AppRole escalation path in ops-mason-build title: Close the policy-plus-AppRole escalation path in ops-mason-build
status: open status: closed
outcome: resolved
origin: review origin: review
origin_ref: inbound-library ILIB-WP-0003 (finding raised 2026-10-05 while preparing the first grant) origin_ref: inbound-library ILIB-WP-0003 (finding raised 2026-10-05 while preparing the first grant)
priority: high priority: high
@ -152,5 +153,23 @@ notes: >-
A survey needs only (c)'s read-only grant. Today MASON-WP-0008 plan-survey runs A survey needs only (c)'s read-only grant. Today MASON-WP-0008 plan-survey runs
under the full build grant, so separating them also reduces what a survey under the full build grant, so separating them also reduces what a survey
session can do. session can do.
resolution: >-
Founder decision (Bernd Worsch, 2026-10-07, given in the key-cape session):
(c)+(a), a read-only grant only. ops-mason gets the read-only
ops-mason-survey policy (policies/ops-mason-survey.hcl: read/list on mounts,
auth methods, policies, auth role definitions and KV metadata; capability
queries; lookup-self and revoke-self; explicit denies on */data/*, its own
policy, and AppRole role-id / secret-id / secret-id-accessor /
custom-secret-id). No policy writes, no auth-role writes, no secret-id
minting, no token creation. Those acts go through the founder-attended
railiance-platform delegated applier (scripts/credential-change.py
applier-apply under warden access openbao-platform-admin-login --exec), as
for CCR-2026-0033/0034 on 2026-10-07. ops-mason-build moved to
policies/retired/ with a do-not-install header; it was not present in
OpenBao at the time. bao-session.sh, plan-survey.py, custody-inventory.py,
tests, AGENTS.md, INTENT.md and docs/construction-plan-format.md switched to
ops-mason-survey. Installing the policy is a founder act (AGENTS.md,
"Installing the survey policy"). Commit: COMMIT_PENDING.
updated: '2026-10-07T00:00:00Z'
state_hub_intake_id: "01a11118-1f35-7ad5-ad51-65dd785c932f" state_hub_intake_id: "01a11118-1f35-7ad5-ad51-65dd785c932f"
``` ```

View file

@ -0,0 +1,150 @@
# ops-mason-survey — read-only survey and verification. Nothing else.
#
# Founder decision 2026-10-07 under MASON-IN-0004, option (c)+(a): ops-mason
# designs, surveys and verifies; it does not build. Policy writes, auth-role
# writes, AppRole secret-id minting and token creation are applied by the
# founder through the railiance-platform delegated applier
# (scripts/credential-change.py applier-apply under
# `warden access openbao-platform-admin-login --exec`). This replaces
# ops-mason-build (policies/retired/), whose policy-write + AppRole + token
# authority could be chained into a token carrying none of its denies.
#
# There is no write capability anywhere in this file except the two
# query-shaped endpoints (sys/capabilities*, revoke-self) that grant nothing.
# Granted through scripts/bao-session.sh as a 45-minute, named token.
#
# Path priority note: OpenBao uses the single most specific matching path
# within a policy, and a path with a `+` segment loses to one without. A deny
# on `auth/approle/role/+/secret-id/*` would therefore LOSE to a broad
# `auth/approle/role/*` grant. So AppRole is granted narrowly (list the roles,
# read `role/<name>` only) and the sub-endpoints are never granted at all; the
# explicit denies below are a second line, not the only one.
# --- survey: mounts, auth methods, policies ---------------------------------
# Phase 2 of the construction process. `bao secrets list` / `bao auth list`.
path "sys/mounts" {
capabilities = ["read", "list"]
}
path "sys/auth" {
capabilities = ["read", "list"]
}
# `bao policy list`.
path "sys/policies/acl" {
capabilities = ["list"]
}
# `bao policy read <name>` — policy text, never a secret value. read only:
# no create/update/delete, so this grant cannot write or remove any policy.
path "sys/policies/acl/*" {
capabilities = ["read"]
}
# --- survey: auth role definitions (read only) ------------------------------
# Kubernetes roles hold bindings and policy names, no credential material.
path "auth/kubernetes/role/*" {
capabilities = ["read", "list"]
}
# AppRole: list role names, read a role's definition. Deliberately not
# `auth/approle/role/*` — see the priority note above.
path "auth/approle/role" {
capabilities = ["list"]
}
path "auth/approle/role/+" {
capabilities = ["read"]
}
# OIDC/JWT roles used in this estate (netkingdom OIDC mount, jwt mount):
# bound claims, redirect URIs and policy names, no credential material.
path "auth/netkingdom/role/*" {
capabilities = ["read", "list"]
}
path "auth/jwt/role/*" {
capabilities = ["read", "list"]
}
# --- survey: KV structure, never KV values ----------------------------------
# Metadata carries versions, timestamps and custom_metadata — enough to confirm
# a path exists and that a paste-once delivery landed. Read/list only: this
# grant cannot describe, create or delete a path.
path "platform/metadata/*" {
capabilities = ["read", "list"]
}
path "operators/metadata/*" {
capabilities = ["read", "list"]
}
# --- verification -----------------------------------------------------------
# `bao token capabilities <path>` asks what a token can reach. The endpoints
# take a POST, hence update; they answer a question and grant nothing.
path "sys/capabilities" {
capabilities = ["update"]
}
path "sys/capabilities-self" {
capabilities = ["update"]
}
# The survey checks its own token's policies before running.
path "auth/token/lookup-self" {
capabilities = ["read"]
}
# End the grant early when the task is done.
path "auth/token/revoke-self" {
capabilities = ["update"]
}
# Not granted, on purpose: auth/token/create (no token minting of any kind),
# auth/token/revoke (other tokens), any auth/*/role write, sys/mounts/*.
# --- the grant cannot read or rewrite its own scope -------------------------
# It has no policy write at all, so this is belt and braces: if a write on
# sys/policies/acl/* is ever added here by mistake, this exact path still
# outranks the glob and the grant cannot edit its own denials.
path "sys/policies/acl/ops-mason-survey" {
capabilities = ["deny"]
}
# --- the AppRole escalation, stated as denials ------------------------------
# Reading a role-id or minting a secret-id is exactly how a role becomes a
# login. None of these is granted above; deny makes that explicit and also
# overrides any other policy attached to the same token.
path "auth/approle/role/+/role-id" {
capabilities = ["deny"]
}
path "auth/approle/role/+/secret-id" {
capabilities = ["deny"]
}
path "auth/approle/role/+/secret-id/*" {
capabilities = ["deny"]
}
path "auth/approle/role/+/secret-id-accessor/*" {
capabilities = ["deny"]
}
path "auth/approle/role/+/custom-secret-id" {
capabilities = ["deny"]
}
# --- the line on secret values, stated as a denial --------------------------
# Explicit deny outranks any grant, including one added here later by mistake.
path "platform/data/*" {
capabilities = ["deny"]
}
path "operators/data/*" {
capabilities = ["deny"]
}
path "secret/data/*" {
capabilities = ["deny"]
}

View file

@ -1,3 +1,19 @@
# RETIRED — DO NOT INSTALL.
#
# Retired 2026-10-07 by founder decision (Bernd Worsch) under MASON-IN-0004,
# option (c)+(a): ops-mason holds only the read-only ops-mason-survey grant
# (policies/ops-mason-survey.hcl). This policy could write ACL policies and
# AppRole roles and mint tokens; together those let a holder create a policy
# without its denies, bind it to an AppRole, read the role-id, mint a secret-id
# and log in as a token that carries none of the denies below. Policy writes,
# auth-role writes, secret-id minting and token creation now go through the
# founder-attended railiance-platform delegated applier
# (scripts/credential-change.py applier-apply under
# `warden access openbao-platform-admin-login --exec`).
#
# Kept for the record only. It was never present in OpenBao at retirement
# (bao-session.sh grant reported "does not exist yet" on 2026-10-07).
#
# ops-mason-build — the authority ops-mason actually needs, and no more. # ops-mason-build — the authority ops-mason actually needs, and no more.
# #
# SCOPE.md says ops-mason "never touches secret values, even transiently". Until # SCOPE.md says ops-mason "never touches secret values, even transiently". Until

View file

@ -12,10 +12,12 @@
# for as long as you have it, indistinguishable from you in the audit log. The # for as long as you have it, indistinguishable from you in the audit log. The
# grant path instead mints a separate token that is: # grant path instead mints a separate token that is:
# #
# * scoped — carries the ops-mason-build policy, which can create policies, # * scoped — carries the read-only ops-mason-survey policy: it can list
# auth roles and KV *structure* but cannot read secret values. # and read mounts, auth methods, policies, auth role definitions
# SCOPE.md's "never touch secret values" stops being a promise in # and KV *metadata*, and query capabilities. It cannot write a
# a document and becomes something OpenBao enforces. # policy or role, mint a secret-id or a token, or read a secret
# value (MASON-IN-0004, founder decision 2026-10-07). Builds go
# through the founder-attended railiance-platform applier.
# * bounded — 45 minutes, no renewal past one hour. # * bounded — 45 minutes, no renewal past one hour.
# * named — display_name claude-<task>, so the audit log says which task an # * named — display_name claude-<task>, so the audit log says which task an
# action belonged to, not just that platform-root did it. # action belonged to, not just that platform-root did it.
@ -30,7 +32,7 @@ set -euo pipefail
export BAO_ADDR="${BAO_ADDR:-http://127.0.0.1:18200}" export BAO_ADDR="${BAO_ADDR:-http://127.0.0.1:18200}"
GRANT_FILE="${GRANT_FILE:-$HOME/.claude-bao-token}" GRANT_FILE="${GRANT_FILE:-$HOME/.claude-bao-token}"
GRANT_POLICY="${GRANT_POLICY:-ops-mason-build}" GRANT_POLICY="${GRANT_POLICY:-ops-mason-survey}"
GRANT_TTL="${GRANT_TTL:-45m}" GRANT_TTL="${GRANT_TTL:-45m}"
GRANT_MAX_TTL="${GRANT_MAX_TTL:-1h}" GRANT_MAX_TTL="${GRANT_MAX_TTL:-1h}"
OIDC_PATH="${OIDC_PATH:-netkingdom}" OIDC_PATH="${OIDC_PATH:-netkingdom}"

View file

@ -5,7 +5,7 @@
./scripts/custody-inventory.py operators one mount or prefix ./scripts/custody-inventory.py operators one mount or prefix
./scripts/custody-inventory.py --undescribed only paths missing metadata ./scripts/custody-inventory.py --undescribed only paths missing metadata
Reads metadata only — never a value — so it runs under ops-mason-build and can Reads metadata only — never a value — so it runs under ops-mason-survey and can
be handed to anyone orienting themselves without granting them a single secret. be handed to anyone orienting themselves without granting them a single secret.
A path with no description is a finding, not a formatting problem: it is a A path with no description is a finding, not a formatting problem: it is a

View file

@ -12,7 +12,7 @@ auth-role listings, KV metadata existence for the target path, matching
ops-warden routing catalog entries, and a draft answer to "does an existing lane ops-warden routing catalog entries, and a draft answer to "does an existing lane
already satisfy this demand?". already satisfy this demand?".
Metadata only. It runs under the ops-mason-build grant (~/.claude-bao-token), Metadata only. It runs under the ops-mason-survey grant (~/.claude-bao-token),
refuses any other token, and every bao invocation passes `allowed_bao_call`, an refuses any other token, and every bao invocation passes `allowed_bao_call`, an
allowlist that rejects `kv get` and every `*/data/*` path before a process starts. allowlist that rejects `kv get` and every `*/data/*` path before a process starts.
Policy bodies are read with `bao policy read` (policy text, never a secret value) Policy bodies are read with `bao policy read` (policy text, never a secret value)
@ -33,11 +33,11 @@ from pathlib import Path
from typing import Callable from typing import Callable
GRANT_FILE = "~/.claude-bao-token" GRANT_FILE = "~/.claude-bao-token"
GRANT_POLICY = "ops-mason-build" GRANT_POLICY = "ops-mason-survey"
DEFAULT_ADDR = "http://127.0.0.1:18200" DEFAULT_ADDR = "http://127.0.0.1:18200"
DEFAULT_CATALOG = "~/ops-warden/registry/routing/catalog.yaml" DEFAULT_CATALOG = "~/ops-warden/registry/routing/catalog.yaml"
MAX_POLICY_READS = 15 MAX_POLICY_READS = 15
NOT_PERMITTED = "not permitted by ops-mason-build" NOT_PERMITTED = "not permitted by ops-mason-survey"
# Path segments too generic to identify a consumer. # Path segments too generic to identify a consumer.
GENERIC = {"platform", "workloads", "operators", "secret", "data", "metadata", GENERIC = {"platform", "workloads", "operators", "secret", "data", "metadata",
"kv", "read", "workload", "role", "token", "auth", "policy"} "kv", "read", "workload", "role", "token", "auth", "policy"}
@ -133,7 +133,7 @@ def grant_env(environ: dict | None = None) -> dict:
with open(grant) as f: with open(grant) as f:
env["BAO_TOKEN"] = f.read().strip() env["BAO_TOKEN"] = f.read().strip()
if not env.get("BAO_TOKEN"): if not env.get("BAO_TOKEN"):
raise SurveyError("no ops-mason-build grant (~/.claude-bao-token absent and BAO_TOKEN unset); " raise SurveyError("no ops-mason-survey grant (~/.claude-bao-token absent and BAO_TOKEN unset); "
"ask the founder to run ./scripts/bao-session.sh grant <task>") "ask the founder to run ./scripts/bao-session.sh grant <task>")
return env return env

View file

@ -43,7 +43,7 @@ path "platform/metadata/workloads/inbound-library/imap" {
''' '''
def lookup(policies=("default", "ops-mason-build")): def lookup(policies=("default", "ops-mason-survey")):
return {"data": {"id": TOKEN, "accessor": "acc-SECRET", "display_name": "token-claude-mason-wp-0008", return {"data": {"id": TOKEN, "accessor": "acc-SECRET", "display_name": "token-claude-mason-wp-0008",
"policies": list(policies), "ttl": 2000}} "policies": list(policies), "ttl": 2000}}
@ -59,7 +59,7 @@ class FakeBao:
"sys/": {"type": "system"}}, "sys/": {"type": "system"}},
("auth", "list"): {"approle/": {"type": "approle"}, "kubernetes/": {"type": "kubernetes"}, ("auth", "list"): {"approle/": {"type": "approle"}, "kubernetes/": {"type": "kubernetes"},
"token/": {"type": "token"}}, "token/": {"type": "token"}},
("policy", "list"): ["default", "root", "ops-mason-build", "workload-kv-read-inbound-library-imap", ("policy", "list"): ["default", "root", "ops-mason-survey", "workload-kv-read-inbound-library-imap",
"workload-kv-read-other"], "workload-kv-read-other"],
("list", "auth/approle/role"): ["inbound-library", "rein-openweights"], ("list", "auth/approle/role"): ["inbound-library", "rein-openweights"],
("list", "auth/kubernetes/role"): ["external-secrets-activity-core"], ("list", "auth/kubernetes/role"): ["external-secrets-activity-core"],
@ -149,20 +149,20 @@ def test_missing_capability_degrades_and_is_reported(catalog, capsys):
("policy", "read", "workload-kv-read-inbound-library-imap")}) ("policy", "read", "workload-kv-read-inbound-library-imap")})
rc, out = run(fake, catalog, capsys) rc, out = run(fake, catalog, capsys)
assert rc == 0 assert rc == 0
assert "not permitted by ops-mason-build" in out assert "not permitted by ops-mason-survey" in out
assert "incomplete:" in out assert "incomplete:" in out
def test_no_grant_fails_loudly(monkeypatch, tmp_path): def test_no_grant_fails_loudly(monkeypatch, tmp_path):
monkeypatch.setattr(survey.os.path, "expanduser", lambda p: str(tmp_path / "absent")) monkeypatch.setattr(survey.os.path, "expanduser", lambda p: str(tmp_path / "absent"))
with pytest.raises(survey.SurveyError, match="no ops-mason-build grant"): with pytest.raises(survey.SurveyError, match="no ops-mason-survey grant"):
survey.main(ARGS, runner=FakeBao(), environ={}) survey.main(ARGS, runner=FakeBao(), environ={})
@pytest.mark.parametrize("policies", [("root",), ("default", "ops-mason-build", "platform-admin"), ("default",)]) @pytest.mark.parametrize("policies", [("root",), ("default", "ops-mason-survey", "platform-admin"), ("default",)])
def test_non_grant_token_is_refused(catalog, policies): def test_non_grant_token_is_refused(catalog, policies):
fake = FakeBao(table={("token", "lookup"): lookup(policies)}) fake = FakeBao(table={("token", "lookup"): lookup(policies)})
with pytest.raises(survey.SurveyError, match="not an ops-mason-build grant") as exc: with pytest.raises(survey.SurveyError, match="not an ops-mason-survey grant") as exc:
survey.main([*ARGS, "--catalog", str(catalog)], runner=fake, environ={"BAO_TOKEN": TOKEN}) survey.main([*ARGS, "--catalog", str(catalog)], runner=fake, environ={"BAO_TOKEN": TOKEN})
assert TOKEN not in str(exc.value) assert TOKEN not in str(exc.value)
assert len(fake.calls) == 1 assert len(fake.calls) == 1
@ -182,7 +182,7 @@ def test_script_exits_nonzero_without_grant(tmp_path):
p = subprocess.run([sys.executable, str(Path(__file__).parents[1] / "scripts/plan-survey.py"), *ARGS], p = subprocess.run([sys.executable, str(Path(__file__).parents[1] / "scripts/plan-survey.py"), *ARGS],
capture_output=True, text=True, env=env) capture_output=True, text=True, env=env)
assert p.returncode == 2 assert p.returncode == 2
assert "ERROR: no ops-mason-build grant" in p.stderr assert "ERROR: no ops-mason-survey grant" in p.stderr
@pytest.mark.parametrize("args", [ @pytest.mark.parametrize("args", [