Implement offline catalog lane scaffold generation
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a10391-6355-7b41-b0fa-3e14e874f091
This commit is contained in:
parent
6b9b95bc6e
commit
f5b7ff5118
9 changed files with 869 additions and 1 deletions
63
docs/catalog-lane-scaffold.md
Normal file
63
docs/catalog-lane-scaffold.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Offline exact KV-v2 workload-read lane scaffold
|
||||||
|
|
||||||
|
Owner: railiance-platform, RPF-WP-0050-T04. Warden pointer seam:
|
||||||
|
`ops-warden/wiki/CatalogLaneScaffoldContract.md`, WARDEN-WP-0041.
|
||||||
|
|
||||||
|
Run `python3 scripts/credential-change.py scaffold-lane --input <platform-request.yaml> --catalog-manifest <warden-request.yaml> --output-dir <new-external-staging-dir>`.
|
||||||
|
The catalog manifest is optional for ordinary local preparation and required for
|
||||||
|
cross-owner fixture acceptance. Both inputs are metadata only; this command has
|
||||||
|
no execute, credential, network, approval or apply option. jsonschema and PyYAML
|
||||||
|
are required, as used by the local validation environment.
|
||||||
|
|
||||||
|
`schemas/catalog-lane-scaffold.schema.json` is the closed platform request v1.
|
||||||
|
Explicit owner/consumer/lane, prospective Kubernetes SA/namespace/role/audience,
|
||||||
|
exact KV mount/path/field names, namespace-restricted ESO destination, risk for the
|
||||||
|
entire readable path and required review roles must be supplied. Missing claims
|
||||||
|
and risk are not inferred. The first shape supports one prospective ESO reader,
|
||||||
|
audience openbao and a maximum fifteen-minute token TTL. Tenant/dynamic secret,
|
||||||
|
OIDC/ceremony, wildcard, metadata listing, self-lifecycle and executable fetching
|
||||||
|
need separate reviewed extensions. No credential values or protected URLs belong
|
||||||
|
in this input. Unknown fields, malformed/duplicate YAML keys, control characters,
|
||||||
|
secret markers, invalid calendar dates and mismatched mappings are refused with
|
||||||
|
safe errors. Warden's unknown workload identity remains unknown and does not
|
||||||
|
confirm the prospective platform auth binding.
|
||||||
|
|
||||||
|
The generator reuses `credential-change.py`'s `generated_policy_hcl` and
|
||||||
|
`auth_payload`, preserving one exact data-read path and explicit policies/claims.
|
||||||
|
It emits six draft review artifacts: a CCR under `credential-change-requests/`,
|
||||||
|
its policy under `openbao/policies/`, auth payload under `openbao/auth/`, ESO review
|
||||||
|
manifest under `manifests/`, a non-executed evidence checklist under
|
||||||
|
`docs/evidence/`, and README. The policy path in the CCR is the eventual owner
|
||||||
|
repository destination, relative to the staged bundle. Auth payloads are wrapped
|
||||||
|
in draft metadata; no command executes them. No Secret object or value is emitted.
|
||||||
|
|
||||||
|
CCR status is proposed, review comments empty, bound claims unconfirmed and
|
||||||
|
frontdoor pending-review/unresolvable. Store/ExternalSecret annotations also say
|
||||||
|
pending-review. The ExternalSecret target has explicit annotation metadata to
|
||||||
|
contain source last-applied annotation copying. Evidence is not-executed and
|
||||||
|
verified=false; it lists metadata/capability/negative/cleanup receipts the owner
|
||||||
|
must produce later using its reviewed output-suppressing tools. It is a scaffold,
|
||||||
|
not a successful verification receipt. Nothing is apply-ready authority.
|
||||||
|
|
||||||
|
Generation requires a new output directory outside the platform or other owner
|
||||||
|
repository, refuses existing paths including symlinks, and never overwrites any
|
||||||
|
file. Caller-selected staging beneath the system temporary root is supported.
|
||||||
|
Inputs remain immutable and artifacts are deterministic; explicit dates and
|
||||||
|
request/catalog SHA256 bind provenance without clock/network reads.
|
||||||
|
|
||||||
|
## Cross-owner fixture
|
||||||
|
|
||||||
|
`examples/catalog-lane-scaffold/platform-request.yaml` and `warden-request.yaml`
|
||||||
|
use synthetic lane example-workload-kv, consumer example-consumer, prospective
|
||||||
|
reader example-consumer-eso and CCR-2099-9001. No published workload identity,
|
||||||
|
existing client or credential is claimed. Warden manifest references the platform
|
||||||
|
request and eventual CCR destination, includes explicit reviewed date and whole
|
||||||
|
path risk. Platform checks shared schema/lane/title/consumer/owner/risk and exact
|
||||||
|
CCR destination; Warden validates its own complete pointer schema.
|
||||||
|
|
||||||
|
Run both owner scaffold commands into separate new temporary directories and
|
||||||
|
compare the Warden draft catalog with the proposed CCR. Same lane and pointers,
|
||||||
|
risk/owner/consumer parity, retained unknown identity, unconfirmed platform claims,
|
||||||
|
unresolvable frontdoor and absence of executable fetch/approval state are required.
|
||||||
|
The synthetic fixture does not admit an actual lane. Source parity remains distinct
|
||||||
|
from any founder custody choice, attended dated window or production promotion.
|
||||||
33
docs/evidence/2026-10-04-catalog-lane-scaffold-offline.json
Normal file
33
docs/evidence/2026-10-04-catalog-lane-scaffold-offline.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"scope": "actual offline owner CLIs + independently verified deterministic cross-owner fixture; no admission or runtime act",
|
||||||
|
"platform_stage": "/tmp/p4-cross-platform-tj7_84lv/platform",
|
||||||
|
"warden_stage": "/tmp/warden-cross-owner-catalog",
|
||||||
|
"fixture_sha256": "38647d436ccfa8893974c4b88c93d3bd94a9c0a21a2603b54bd84c0195954e32",
|
||||||
|
"artifact_sha256": {
|
||||||
|
"README.md": "2f48aa6ef925f62722ca32d38283d746c0c386da751dec3f9a76fba125d24245",
|
||||||
|
"credential-change-requests/CCR-2099-9001.yaml": "e9db54694b966f186433aaf1f3de6685d8f34be166af9d966950bcca448f151f",
|
||||||
|
"docs/evidence/example-workload-kv.checklist.json": "7d9a58b160c958d8d46f7029398eef8e70f87f0d9423198fb789bdf8fe3a9f9a",
|
||||||
|
"manifests/example-workload-kv.draft.yaml": "2d23bee7451735e2a0aa74435c65a0968bd05e35c35625a4087e2f286a2408b5",
|
||||||
|
"openbao/auth/example-consumer-eso.draft.json": "545a03c95c50b68b0eb127ae5e72d904faf9f68ecfabe26deac27730905b476d",
|
||||||
|
"openbao/policies/workload-kv-read-example-consumer.hcl": "c2821cd6fe5bb16564366fb12fd37c559048841a41c6f2778ad2dad5901e4df4"
|
||||||
|
},
|
||||||
|
"new_scaffold_tests": 14,
|
||||||
|
"prior_window_tests": 7,
|
||||||
|
"unknown_workload_preserved": true,
|
||||||
|
"prospective_auth_claims_unconfirmed": true,
|
||||||
|
"frontdoor_pending_unresolvable": true,
|
||||||
|
"evidence_not_executed": true,
|
||||||
|
"source_sha256": {
|
||||||
|
"scripts/catalog_lane_scaffold.py": "96d60cc47aa78728f5ba96ce1fd63033adf23c05641de93ebbdda3902082d2d1",
|
||||||
|
"scripts/credential-change.py": "d78c493e7c75f3ad0e013745bc13e9f79b9fffc560e5c33ece62e982d800b391",
|
||||||
|
"schemas/catalog-lane-scaffold.schema.json": "8c2100052a86ebef021398bed662ed8a0fe6970506b785b22cec2398e75adaff",
|
||||||
|
"examples/catalog-lane-scaffold/platform-request.yaml": "2070fa0f937ee03add01d1e78b9dbdebf72472b3d8fcc8fae9a51436ec60ab8e",
|
||||||
|
"examples/catalog-lane-scaffold/warden-request.yaml": "38647d436ccfa8893974c4b88c93d3bd94a9c0a21a2603b54bd84c0195954e32",
|
||||||
|
"tests/test_catalog_lane_scaffold.py": "a4abc80b0a29362296208f7d233b9a206a299d0c47f244929f9851fb543911c6",
|
||||||
|
"docs/catalog-lane-scaffold.md": "f6b46acf6515fc13a7ce64b74306c9225c9384f73e63bc7b9723232b08ad3c3c"
|
||||||
|
},
|
||||||
|
"task_id": "RPF-WP-0050-T04",
|
||||||
|
"task_uuid": "6475d924-92c6-504a-b9a1-4f035273f0e1",
|
||||||
|
"recorded_date": "2026-10-04"
|
||||||
|
}
|
||||||
49
examples/catalog-lane-scaffold/platform-request.yaml
Normal file
49
examples/catalog-lane-scaffold/platform-request.yaml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
schema_version: 1
|
||||||
|
lane_id: example-workload-kv
|
||||||
|
ccr_id: CCR-2099-9001
|
||||||
|
title: Synthetic exact workload KV read fixture
|
||||||
|
date: '2026-10-04'
|
||||||
|
consumer_repo: example-consumer
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
requester:
|
||||||
|
agent: example-consumer
|
||||||
|
reason: Offline fixture only; no deployed consumer or credential exists
|
||||||
|
review_approvers:
|
||||||
|
- platform-owner
|
||||||
|
- consumer-owner
|
||||||
|
target:
|
||||||
|
domain: infotech
|
||||||
|
tenant: platform
|
||||||
|
workload: example-consumer
|
||||||
|
environment: build
|
||||||
|
purpose: Offline fixture of proposed exact-path metadata
|
||||||
|
openbao:
|
||||||
|
mount: platform
|
||||||
|
kv_path: platform/workloads/example-consumer/config
|
||||||
|
fields:
|
||||||
|
- CLIENT_SECRET
|
||||||
|
policy_name: workload-kv-read-example-consumer
|
||||||
|
auth:
|
||||||
|
mount: kubernetes
|
||||||
|
role: example-consumer-eso
|
||||||
|
audience: openbao
|
||||||
|
service_account_names:
|
||||||
|
- example-consumer-eso
|
||||||
|
service_account_namespaces:
|
||||||
|
- example-consumer
|
||||||
|
ttl: 15m
|
||||||
|
eso:
|
||||||
|
store_name: openbao-example-consumer
|
||||||
|
server: http://openbao.openbao.svc:8200
|
||||||
|
namespace: example-consumer
|
||||||
|
external_secret_name: example-consumer-config
|
||||||
|
secret_name: example-consumer-config
|
||||||
|
refresh_interval: 5m
|
||||||
|
data:
|
||||||
|
- secret_key: client-secret
|
||||||
|
property: CLIENT_SECRET
|
||||||
|
risk:
|
||||||
|
classification: standard
|
||||||
|
notes:
|
||||||
|
- Synthetic prospective lane risk applies to the entire readable path; no actual
|
||||||
|
credentials.
|
||||||
15
examples/catalog-lane-scaffold/warden-request.yaml
Normal file
15
examples/catalog-lane-scaffold/warden-request.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
schema_version: 1
|
||||||
|
lane_id: example-workload-kv
|
||||||
|
title: Synthetic exact workload KV read fixture
|
||||||
|
need_keywords:
|
||||||
|
- synthetic example workload config
|
||||||
|
consumer_repo: example-consumer
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
workload_ref:
|
||||||
|
applicability: applicable
|
||||||
|
unknown_reason: Synthetic fixture has no published deployment identity
|
||||||
|
risk: standard
|
||||||
|
wiki_ref: railiance-platform:docs/catalog-lane-scaffold.md
|
||||||
|
canon_ref: railiance-platform:credential-change-requests/CCR-2099-9001.yaml
|
||||||
|
reviewed: '2026-10-04'
|
||||||
|
platform_request_ref: railiance-platform:examples/catalog-lane-scaffold/platform-request.yaml
|
||||||
344
schemas/catalog-lane-scaffold.schema.json
Normal file
344
schemas/catalog-lane-scaffold.schema.json
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"lane_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"ccr_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^CCR-[0-9]{4}-[0-9]{4}$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
||||||
|
},
|
||||||
|
"consumer_repo": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"owner_repo": {
|
||||||
|
"const": "railiance-platform"
|
||||||
|
},
|
||||||
|
"requester": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"agent": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"agent",
|
||||||
|
"reason"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"review_approvers": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 32,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"tenant": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"workload": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"enum": [
|
||||||
|
"build",
|
||||||
|
"development",
|
||||||
|
"test",
|
||||||
|
"staging",
|
||||||
|
"production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"domain",
|
||||||
|
"tenant",
|
||||||
|
"workload",
|
||||||
|
"environment",
|
||||||
|
"purpose"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"openbao": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"mount": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"kv_path": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9][a-z0-9_-]*(?:/[a-z0-9][a-z0-9_-]*)+$"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 32,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[A-Z][A-Z0-9_]*$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policy_name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^workload-kv-read-[a-z0-9-]+$"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"mount": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"audience": {
|
||||||
|
"const": "openbao"
|
||||||
|
},
|
||||||
|
"service_account_names": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 32,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"service_account_namespaces": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 32,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ttl": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[1-9][0-9]*[smh]$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"mount",
|
||||||
|
"role",
|
||||||
|
"audience",
|
||||||
|
"service_account_names",
|
||||||
|
"service_account_namespaces",
|
||||||
|
"ttl"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"mount",
|
||||||
|
"kv_path",
|
||||||
|
"fields",
|
||||||
|
"policy_name",
|
||||||
|
"auth"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"eso": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"store_name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^https?://[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"external_secret_name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"secret_name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
|
||||||
|
},
|
||||||
|
"refresh_interval": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[1-9][0-9]*[smh]$"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 32,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"secret_key": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[a-zA-Z0-9._-]+$"
|
||||||
|
},
|
||||||
|
"property": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512,
|
||||||
|
"pattern": "^[A-Z][A-Z0-9_]*$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"secret_key",
|
||||||
|
"property"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"store_name",
|
||||||
|
"server",
|
||||||
|
"namespace",
|
||||||
|
"external_secret_name",
|
||||||
|
"secret_name",
|
||||||
|
"refresh_interval",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"risk": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"classification": {
|
||||||
|
"enum": [
|
||||||
|
"standard",
|
||||||
|
"high"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 32,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"classification",
|
||||||
|
"notes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"lane_id",
|
||||||
|
"ccr_id",
|
||||||
|
"title",
|
||||||
|
"date",
|
||||||
|
"consumer_repo",
|
||||||
|
"owner_repo",
|
||||||
|
"requester",
|
||||||
|
"review_approvers",
|
||||||
|
"target",
|
||||||
|
"openbao",
|
||||||
|
"eso",
|
||||||
|
"risk"
|
||||||
|
],
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "Draft exact KV-v2 workload-read lane input v1"
|
||||||
|
}
|
||||||
203
scripts/catalog_lane_scaffold.py
Normal file
203
scripts/catalog_lane_scaffold.py
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
"""Draft-only offline exact-path lane scaffolding; no execution surface."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
from datetime import date
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
MAX_INPUT_BYTES = 128 * 1024
|
||||||
|
SECRET_MARKERS = ('AGE-SECRET-KEY-1', '-----BEGIN', 'hvs.', 'hvb.', 'hvc.', 'ghp_', 'github_pat_', 'npm_', 'BAO_TOKEN=', 'VAULT_TOKEN=', 'OPENBAO_ROOT_TOKEN=')
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueLoader(yaml.SafeLoader):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def unique_mapping(loader, node):
|
||||||
|
result = {}
|
||||||
|
for key_node, value_node in node.value:
|
||||||
|
key = loader.construct_object(key_node)
|
||||||
|
if not isinstance(key, str) or key in result:
|
||||||
|
raise ValueError('Invalid or duplicate metadata key')
|
||||||
|
result[key] = loader.construct_object(value_node)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
UniqueLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, unique_mapping)
|
||||||
|
|
||||||
|
|
||||||
|
def read_metadata(path):
|
||||||
|
with Path(path).open('rb') as stream:
|
||||||
|
raw = stream.read(MAX_INPUT_BYTES + 1)
|
||||||
|
if len(raw) > MAX_INPUT_BYTES:
|
||||||
|
raise ValueError('Metadata input exceeds size limit')
|
||||||
|
text = raw.decode('utf-8')
|
||||||
|
if any(marker in text for marker in SECRET_MARKERS) or re.search(r'(?<![A-Za-z])sk-[A-Za-z0-9]', text):
|
||||||
|
raise ValueError('Metadata input contains rejected secret material')
|
||||||
|
try:
|
||||||
|
data = yaml.load(text, Loader=UniqueLoader)
|
||||||
|
canonical = json.dumps(data, allow_nan=False)
|
||||||
|
if any(marker in canonical for marker in SECRET_MARKERS) or re.search(r"(?<![A-Za-z])sk-[A-Za-z0-9]", canonical):
|
||||||
|
raise ValueError("Rejected parsed secret material")
|
||||||
|
except (yaml.YAMLError, TypeError, RecursionError, ValueError) as exc:
|
||||||
|
raise ValueError('Invalid metadata document') from None
|
||||||
|
return data, hashlib.sha256(raw).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_request(data):
|
||||||
|
schema = json.loads((REPO_DIR / 'schemas/catalog-lane-scaffold.schema.json').read_text())
|
||||||
|
if not jsonschema.Draft202012Validator(schema).is_valid(data):
|
||||||
|
raise ValueError('Request does not match closed platform scaffold schema v1')
|
||||||
|
def text_values(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return [item for nested in value.values() for item in text_values(nested)]
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [item for nested in value for item in text_values(nested)]
|
||||||
|
return [value] if isinstance(value, str) else []
|
||||||
|
if any(any(ord(char) < 32 for char in value) for value in text_values(data)):
|
||||||
|
raise ValueError('Control characters are not supported in scaffold metadata')
|
||||||
|
date.fromisoformat(data['date'])
|
||||||
|
bao = data['openbao']
|
||||||
|
if not bao['kv_path'].startswith(bao['mount'] + '/'):
|
||||||
|
raise ValueError('KV path must use the explicitly declared mount')
|
||||||
|
auth = bao['auth']
|
||||||
|
units = {'s': 1, 'm': 60, 'h': 3600}
|
||||||
|
ttl = auth['ttl']
|
||||||
|
if int(ttl[:-1]) * units[ttl[-1]] > 900:
|
||||||
|
raise ValueError('Prospective Kubernetes token TTL exceeds fifteen minutes')
|
||||||
|
eso = data['eso']
|
||||||
|
if len(auth['service_account_names']) != 1 or len(auth['service_account_namespaces']) != 1:
|
||||||
|
raise ValueError('One explicit ESO auth identity is supported')
|
||||||
|
keys = [item['secret_key'] for item in eso['data']]
|
||||||
|
properties = [item['property'] for item in eso['data']]
|
||||||
|
if len(set(keys)) != len(keys) or len(set(properties)) != len(properties) or set(properties) != set(bao['fields']):
|
||||||
|
raise ValueError('ESO mappings must cover the explicit field set exactly once')
|
||||||
|
|
||||||
|
|
||||||
|
def verify_catalog_join(request, catalog):
|
||||||
|
# Warden validates its own full input; platform checks only the shared seam.
|
||||||
|
if not isinstance(catalog, dict):
|
||||||
|
raise ValueError('Catalog pointer manifest must be an object')
|
||||||
|
for field in ('schema_version', 'lane_id', 'title', 'consumer_repo', 'owner_repo'):
|
||||||
|
if catalog.get(field) != request[field]:
|
||||||
|
raise ValueError('Cross-owner fixture identity mismatch')
|
||||||
|
if catalog.get('risk') != request['risk']['classification']:
|
||||||
|
raise ValueError('Cross-owner risk mismatch')
|
||||||
|
if catalog.get('canon_ref') != 'railiance-platform:credential-change-requests/' + request['ccr_id'] + '.yaml':
|
||||||
|
raise ValueError('Catalog CCR destination pointer mismatch')
|
||||||
|
ref = catalog.get('platform_request_ref')
|
||||||
|
if not isinstance(ref, str) or not re.fullmatch(r'railiance-platform:[a-zA-Z0-9_./-]+', ref) or '..' in ref:
|
||||||
|
raise ValueError('Catalog needs a non-secret owner request pointer')
|
||||||
|
|
||||||
|
|
||||||
|
def render_artifacts(data, policy_renderer, auth_renderer, request_digest, catalog_digest=None):
|
||||||
|
validate_request(data)
|
||||||
|
data = deepcopy(data)
|
||||||
|
bao = data['openbao']
|
||||||
|
auth = bao['auth']
|
||||||
|
lane = data['lane_id']
|
||||||
|
policy_file = 'openbao/policies/' + bao['policy_name'] + '.hcl'
|
||||||
|
ccr = {'id': data['ccr_id'], 'kind': 'credential-change-request', 'schema_version': 1,
|
||||||
|
'request_type': 'workload-kv-read', 'title': data['title'], 'status': 'proposed',
|
||||||
|
'created': data['date'], 'updated': data['date'], 'requester': data['requester'],
|
||||||
|
'review': {'required': True, 'required_approvers': data['review_approvers'], 'comments': []},
|
||||||
|
'target': data['target'],
|
||||||
|
'openbao': {'mount': bao['mount'], 'kv_path': bao['kv_path'], 'fields': bao['fields'],
|
||||||
|
'policy_name': bao['policy_name'], 'policy_file': policy_file, 'metadata_read': False,
|
||||||
|
'auth': {'method': 'kubernetes', 'mount': auth['mount'], 'role': auth['role'],
|
||||||
|
'audience': auth['audience'], 'ttl': auth['ttl'],
|
||||||
|
'token_max_ttl': auth['ttl'], 'token_explicit_max_ttl': auth['ttl'],
|
||||||
|
'token_no_default_policy': True, 'policies': [bao['policy_name']],
|
||||||
|
'bound_claims': {'service_account_names': auth['service_account_names'],
|
||||||
|
'service_account_namespaces': auth['service_account_namespaces']},
|
||||||
|
'bound_claims_confirmed': False}},
|
||||||
|
'access_frontdoor': {'type': 'external-secrets', 'catalog_id': lane,
|
||||||
|
'readiness': 'pending-review', 'resolvable': False},
|
||||||
|
'risk': data['risk'],
|
||||||
|
'verification': {'positive': ['Owner-reviewed native exact identity and exact path proof required'],
|
||||||
|
'negative': ['Owner-reviewed wrong identity, sibling GET and parent LIST denials required'],
|
||||||
|
'activation_conditions': ['Exact CCR approval, confirmed identity, attended custody and native receipt acceptance required']},
|
||||||
|
'lifecycle': {'deactivate': 'Owner-reviewed delivery withdrawal and token revocation; exact rollback receipt required',
|
||||||
|
'rotate': 'Separate version-guarded owner approval and contained value delivery required',
|
||||||
|
'compromised': 'Owner incident process; no automatic read, reseed or cleanup'},
|
||||||
|
'scaffold': {'draft_only': True, 'consumer_repo': data['consumer_repo'],
|
||||||
|
'owner_repo': data['owner_repo'], 'request_sha256': request_digest,
|
||||||
|
'catalog_request_sha256': catalog_digest}}
|
||||||
|
annotation = {'railiance.io/credential-change': data['ccr_id'], 'railiance.io/admission': 'pending-review'}
|
||||||
|
eso = data['eso']
|
||||||
|
store = {'apiVersion': 'external-secrets.io/v1', 'kind': 'ClusterSecretStore',
|
||||||
|
'metadata': {'name': eso['store_name'], 'annotations': deepcopy(annotation)},
|
||||||
|
'spec': {'provider': {'vault': {'server': eso['server'], 'path': bao['mount'], 'version': 'v2',
|
||||||
|
'auth': {'kubernetes': {'mountPath': auth['mount'], 'role': auth['role'],
|
||||||
|
'audiences': [auth['audience']],
|
||||||
|
'serviceAccountRef': {'name': auth['service_account_names'][0],
|
||||||
|
'namespace': auth['service_account_namespaces'][0]}}}}},
|
||||||
|
'conditions': [{'namespaces': [eso['namespace']]}]}}
|
||||||
|
secret = {'apiVersion': 'external-secrets.io/v1', 'kind': 'ExternalSecret',
|
||||||
|
'metadata': {'name': eso['external_secret_name'], 'namespace': eso['namespace'], 'annotations': deepcopy(annotation)},
|
||||||
|
'spec': {'refreshInterval': eso['refresh_interval'],
|
||||||
|
'secretStoreRef': {'kind': 'ClusterSecretStore', 'name': eso['store_name']},
|
||||||
|
'target': {'name': eso['secret_name'], 'creationPolicy': 'Owner', 'deletionPolicy': 'Retain',
|
||||||
|
'template': {'metadata': {'annotations': deepcopy(annotation)}}},
|
||||||
|
'data': [{'secretKey': item['secret_key'],
|
||||||
|
'remoteRef': {'key': bao['kv_path'][len(bao['mount']) + 1:], 'property': item['property']}}
|
||||||
|
for item in eso['data']]}}
|
||||||
|
evidence = {'schema_version': 1, 'lane_id': lane, 'ccr_id': data['ccr_id'], 'status': 'not-executed',
|
||||||
|
'draft_only': True, 'verified': False, 'request_sha256': request_digest,
|
||||||
|
'required_owner_checks': ['Exact revision/digest and required owner reviews',
|
||||||
|
'Identity/auth claims confirmed separately; no inferred workload identity',
|
||||||
|
'Metadata/capability readback through reviewed output-suppressing owner tool',
|
||||||
|
'No Secret bodies, values or raw subprocess errors in evidence',
|
||||||
|
'Native scoped positive/negative, cleanup/revocation and rollback receipts after approval']}
|
||||||
|
artifacts = {'credential-change-requests/' + data['ccr_id'] + '.yaml': yaml.safe_dump(ccr, sort_keys=False),
|
||||||
|
policy_file: '# DRAFT ONLY: exact CCR review and confirmed identity required.\n' + policy_renderer(ccr),
|
||||||
|
'openbao/auth/' + auth['role'] + '.draft.json': json.dumps({'draft_only': True, 'status': 'proposed',
|
||||||
|
'bound_claims_confirmed': False,
|
||||||
|
'auth_payload': auth_renderer(ccr)}, indent=2) + '\n',
|
||||||
|
'manifests/' + lane + '.draft.yaml': '# DRAFT REVIEW ARTIFACT. Do not apply. No custody/admission supplied.\n' + yaml.safe_dump_all([store, secret], sort_keys=False),
|
||||||
|
'docs/evidence/' + lane + '.checklist.json': json.dumps(evidence, indent=2) + '\n',
|
||||||
|
'README.md': 'Draft-only review bundle. No admission, approval, confirmed identity, credential value or executed receipt.\n'
|
||||||
|
'Review the exact request digest and owner destinations before promoting any file.\n'
|
||||||
|
'ESO target metadata is explicit to contain source last-applied annotations.\n'}
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
|
def scaffold(input_path, output_dir, policy_renderer, auth_renderer, catalog_manifest=None):
|
||||||
|
data, digest = read_metadata(input_path)
|
||||||
|
catalog_digest = None
|
||||||
|
if catalog_manifest is not None:
|
||||||
|
catalog, catalog_digest = read_metadata(catalog_manifest)
|
||||||
|
validate_request(data)
|
||||||
|
verify_catalog_join(data, catalog)
|
||||||
|
artifacts = render_artifacts(data, policy_renderer, auth_renderer, digest, catalog_digest)
|
||||||
|
output = Path(output_dir).absolute()
|
||||||
|
if output.is_symlink() or output.exists() or output.resolve().is_relative_to(REPO_DIR.resolve()):
|
||||||
|
raise ValueError('Output must be a new staging directory outside the owner repository')
|
||||||
|
if any((p / '.git').exists() for p in output.resolve().parents if p != Path(tempfile.gettempdir()).resolve()):
|
||||||
|
raise ValueError('Output staging must not be inside a repository')
|
||||||
|
output.mkdir(mode=0o700, parents=False)
|
||||||
|
for relative, content in artifacts.items():
|
||||||
|
path = output / relative
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with path.open('x', encoding='utf-8') as stream:
|
||||||
|
stream.write(content)
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
|
def command_scaffold(args, policy_renderer, auth_renderer):
|
||||||
|
try:
|
||||||
|
artifacts = scaffold(args.input, args.output_dir, policy_renderer, auth_renderer, args.catalog_manifest)
|
||||||
|
except (ValueError, OSError, UnicodeError, RecursionError):
|
||||||
|
print('Scaffold refused: invalid metadata, mismatched owner pointers, or unsafe staging output.', file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
print('Prepared ' + str(len(artifacts)) + ' draft review artifacts; nothing admitted or executed.')
|
||||||
|
return 0
|
||||||
|
|
@ -2240,6 +2240,13 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
)
|
)
|
||||||
applier_apply.set_defaults(func=command_applier_apply)
|
applier_apply.set_defaults(func=command_applier_apply)
|
||||||
|
|
||||||
|
from catalog_lane_scaffold import command_scaffold
|
||||||
|
scaffold = sub.add_parser("scaffold-lane", help="Generate proposed offline CCR/policy/ESO/evidence drafts in a new staging directory")
|
||||||
|
scaffold.add_argument("--input", required=True)
|
||||||
|
scaffold.add_argument("--output-dir", required=True)
|
||||||
|
scaffold.add_argument("--catalog-manifest")
|
||||||
|
scaffold.set_defaults(func=lambda args: command_scaffold(args, generated_policy_hcl, auth_payload))
|
||||||
|
|
||||||
from provisioning_window import command_window
|
from provisioning_window import command_window
|
||||||
window = sub.add_parser("window-checklist", help="Render an offline attended-window preparation checklist; never execute")
|
window = sub.add_parser("window-checklist", help="Render an offline attended-window preparation checklist; never execute")
|
||||||
window.add_argument("--window-file", required=True)
|
window.add_argument("--window-file", required=True)
|
||||||
|
|
|
||||||
150
tests/test_catalog_lane_scaffold.py
Normal file
150
tests/test_catalog_lane_scaffold.py
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import argparse
|
||||||
|
from copy import deepcopy
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPTS = REPO_DIR / 'scripts'
|
||||||
|
sys.path.insert(0, str(SCRIPTS))
|
||||||
|
import catalog_lane_scaffold as scaffold
|
||||||
|
SPEC = importlib.util.spec_from_file_location('ccr_scaffold_helpers', SCRIPTS / 'credential-change.py')
|
||||||
|
ccr = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(ccr)
|
||||||
|
|
||||||
|
|
||||||
|
class ScaffoldTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
self.root = Path(self.tmp.name)
|
||||||
|
self.request = REPO_DIR / 'examples/catalog-lane-scaffold/platform-request.yaml'
|
||||||
|
self.catalog = REPO_DIR / 'examples/catalog-lane-scaffold/warden-request.yaml'
|
||||||
|
self.data, self.digest = scaffold.read_metadata(self.request)
|
||||||
|
|
||||||
|
def render(self, data=None):
|
||||||
|
return scaffold.render_artifacts(data or self.data, ccr.generated_policy_hcl, ccr.auth_payload, self.digest)
|
||||||
|
|
||||||
|
def write_request(self, data):
|
||||||
|
path = self.root / 'request.yaml'
|
||||||
|
path.write_text(yaml.safe_dump(data))
|
||||||
|
return path
|
||||||
|
|
||||||
|
def test_existing_ccr_validator_accepts_only_proposed_unconfirmed_draft(self):
|
||||||
|
output = self.root / 'bundle'
|
||||||
|
scaffold.scaffold(self.request, output, ccr.generated_policy_hcl, ccr.auth_payload, self.catalog)
|
||||||
|
path = output / 'credential-change-requests/CCR-2099-9001.yaml'
|
||||||
|
with patch.object(ccr, 'REPO_DIR', output):
|
||||||
|
data, errors, warnings = ccr.validate_ccr(path)
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
self.assertTrue(any('not confirmed' in w for w in warnings))
|
||||||
|
self.assertEqual(data['status'], 'proposed')
|
||||||
|
self.assertFalse(data['access_frontdoor']['resolvable'])
|
||||||
|
self.assertFalse(data['openbao']['auth']['bound_claims_confirmed'])
|
||||||
|
self.assertEqual(data['review']['comments'], [])
|
||||||
|
self.assertTrue(ccr.applier_readiness_blockers(data))
|
||||||
|
self.assertEqual(set(ccr.auth_payload(data)['bound_service_account_names']), {'example-consumer-eso'})
|
||||||
|
|
||||||
|
def test_policy_and_eso_exact_scope_annotation_containment_and_no_values(self):
|
||||||
|
artifacts = self.render()
|
||||||
|
policy = artifacts['openbao/policies/workload-kv-read-example-consumer.hcl']
|
||||||
|
self.assertIn('path "platform/data/workloads/example-consumer/config"', policy)
|
||||||
|
self.assertEqual(policy.count('path "'), 1)
|
||||||
|
self.assertNotIn('metadata/', policy)
|
||||||
|
store, secret = list(yaml.safe_load_all(artifacts['manifests/example-workload-kv.draft.yaml']))
|
||||||
|
self.assertEqual(store['spec']['conditions'], [{'namespaces': ['example-consumer']}])
|
||||||
|
self.assertEqual(store['spec']['provider']['vault']['auth']['kubernetes']['audiences'], ['openbao'])
|
||||||
|
target = secret['spec']['target']
|
||||||
|
self.assertEqual(target['template']['metadata']['annotations']['railiance.io/admission'], 'pending-review')
|
||||||
|
self.assertNotIn('kubectl.kubernetes.io/last-applied-configuration', json.dumps(target))
|
||||||
|
self.assertEqual(secret['spec']['data'], [{'secretKey': 'client-secret', 'remoteRef': {'key': 'workloads/example-consumer/config', 'property': 'CLIENT_SECRET'}}])
|
||||||
|
self.assertNotIn('dataFrom', secret['spec'])
|
||||||
|
self.assertNotIn('Secret', [store['kind'], secret['kind']])
|
||||||
|
evidence = json.loads(artifacts['docs/evidence/example-workload-kv.checklist.json'])
|
||||||
|
self.assertFalse(evidence['verified'])
|
||||||
|
self.assertEqual(evidence['status'], 'not-executed')
|
||||||
|
|
||||||
|
def test_closed_schema_and_authority_injection_refused(self):
|
||||||
|
for location, key, value in [('top','status','approved'),('auth','bound_claims_confirmed',True),('top','secret_values',{'CLIENT_SECRET':'not-a-value'})]:
|
||||||
|
data = deepcopy(self.data)
|
||||||
|
target = data if location == 'top' else data['openbao']['auth']
|
||||||
|
target[key] = value
|
||||||
|
with self.assertRaises(ValueError): self.render(data)
|
||||||
|
|
||||||
|
def test_missing_identity_and_risk_are_not_inferred(self):
|
||||||
|
for location, key in [('auth','service_account_names'),('top','risk')]:
|
||||||
|
data = deepcopy(self.data)
|
||||||
|
target = data if location == 'top' else data['openbao']['auth']
|
||||||
|
del target[key]
|
||||||
|
with self.assertRaises(ValueError): self.render(data)
|
||||||
|
|
||||||
|
def test_wildcards_traversal_and_hcl_injection_refused(self):
|
||||||
|
for path in ['platform/workloads/*','platform/../other','platform/workloads/config\n','platform/a/" { capabilities = ["sudo"] }']:
|
||||||
|
data = deepcopy(self.data);data['openbao']['kv_path'] = path
|
||||||
|
with self.assertRaises(ValueError): self.render(data)
|
||||||
|
|
||||||
|
def test_wrong_mount_and_overlong_ttl_refused(self):
|
||||||
|
data = deepcopy(self.data);data['openbao']['mount'] = 'other'
|
||||||
|
with self.assertRaises(ValueError): self.render(data)
|
||||||
|
data = deepcopy(self.data);data['openbao']['auth']['ttl'] = '1h'
|
||||||
|
with self.assertRaises(ValueError): self.render(data)
|
||||||
|
|
||||||
|
def test_field_mapping_omissions_duplicates_and_extra_fields_refused(self):
|
||||||
|
for mapping in [[], [{'secret_key':'one','property':'OTHER'}], [{'secret_key':'a','property':'CLIENT_SECRET'},{'secret_key':'b','property':'CLIENT_SECRET'}]]:
|
||||||
|
data = deepcopy(self.data);data['eso']['data'] = mapping
|
||||||
|
with self.assertRaises(ValueError): self.render(data)
|
||||||
|
|
||||||
|
def test_cross_owner_pointer_fixture_and_unknown_identity_preserved(self):
|
||||||
|
catalog, _ = scaffold.read_metadata(self.catalog)
|
||||||
|
scaffold.verify_catalog_join(self.data, catalog)
|
||||||
|
self.assertIn('unknown_reason', catalog['workload_ref'])
|
||||||
|
for key,value in [('lane_id','other'),('risk','high'),('canon_ref','railiance-platform:credential-change-requests/OTHER.yaml'),('platform_request_ref','other-owner:request.yaml')]:
|
||||||
|
bad=deepcopy(catalog);bad[key]=value
|
||||||
|
with self.assertRaises(ValueError): scaffold.verify_catalog_join(self.data,bad)
|
||||||
|
|
||||||
|
def test_canonical_outputs_deterministic_and_input_immutable(self):
|
||||||
|
before = self.request.read_bytes();data_before=deepcopy(self.data)
|
||||||
|
self.assertEqual(self.render(), self.render())
|
||||||
|
self.assertEqual(self.data,data_before)
|
||||||
|
self.assertEqual(self.request.read_bytes(),before)
|
||||||
|
|
||||||
|
def test_collision_and_repo_output_refused_without_overwrite(self):
|
||||||
|
output=self.root/'occupied';output.mkdir();sentinel=output/'sentinel';sentinel.write_text('keep')
|
||||||
|
with self.assertRaises(ValueError): scaffold.scaffold(self.request,output,ccr.generated_policy_hcl,ccr.auth_payload)
|
||||||
|
self.assertEqual(sentinel.read_text(),'keep')
|
||||||
|
with self.assertRaises(ValueError): scaffold.scaffold(self.request,REPO_DIR/'live-output',ccr.generated_policy_hcl,ccr.auth_payload)
|
||||||
|
|
||||||
|
def test_no_network_or_subprocess_in_generation(self):
|
||||||
|
with patch.object(urllib.request,'urlopen',side_effect=AssertionError('network forbidden')),patch.object(subprocess,'run',side_effect=AssertionError('execution forbidden')):
|
||||||
|
artifacts=scaffold.scaffold(self.request,self.root/'isolated',ccr.generated_policy_hcl,ccr.auth_payload,self.catalog)
|
||||||
|
self.assertEqual(len(artifacts),6)
|
||||||
|
|
||||||
|
def test_duplicate_yaml_and_secret_material_refused_without_echo(self):
|
||||||
|
for text in ['lane_id: a\nlane_id: b\n', 'title: hvs.CANARY_NEVER_ECHO\n', 'title: "\\x68vs.CANARY_NEVER_ECHO"\n']:
|
||||||
|
path=self.root/'bad.yaml';path.write_text(text)
|
||||||
|
args=argparse.Namespace(input=path,output_dir=self.root/'bad-output',catalog_manifest=None)
|
||||||
|
stream=io.StringIO()
|
||||||
|
with patch('sys.stderr',stream): self.assertEqual(scaffold.command_scaffold(args,ccr.generated_policy_hcl,ccr.auth_payload),2)
|
||||||
|
self.assertNotIn('CANARY',stream.getvalue())
|
||||||
|
self.assertFalse((self.root/'bad-output').exists())
|
||||||
|
|
||||||
|
def test_oversized_input_refused(self):
|
||||||
|
path=self.root/'big.yaml';path.write_text('a'*(scaffold.MAX_INPUT_BYTES+1))
|
||||||
|
with self.assertRaises(ValueError): scaffold.read_metadata(path)
|
||||||
|
|
||||||
|
def test_schema_rejects_boolean_version_and_nonexistent_calendar_day(self):
|
||||||
|
for version,day in [(True,'2026-10-04'),(1,'2026-02-31')]:
|
||||||
|
data=deepcopy(self.data);data['schema_version']=version;data['date']=day
|
||||||
|
with self.assertRaises(ValueError): self.render(data)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': unittest.main()
|
||||||
|
|
@ -57,7 +57,7 @@ Generate a checklist from all named authoritative roots, verified Hub decision j
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RPF-WP-0050-T04
|
id: RPF-WP-0050-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "6475d924-92c6-504a-b9a1-4f035273f0e1"
|
state_hub_task_id: "6475d924-92c6-504a-b9a1-4f035273f0e1"
|
||||||
```
|
```
|
||||||
|
|
@ -65,3 +65,7 @@ state_hub_task_id: "6475d924-92c6-504a-b9a1-4f035273f0e1"
|
||||||
P4 accepted for platform-side source review and bounded scaffolding after Warden returns its contract. Catalog entry/scaffold never supplies credentials, reviews, admission or live authority. Keep owner acceptance explicit before implementation.
|
P4 accepted for platform-side source review and bounded scaffolding after Warden returns its contract. Catalog entry/scaffold never supplies credentials, reviews, admission or live authority. Keep owner acceptance explicit before implementation.
|
||||||
|
|
||||||
2026-10-03: Platform source review accepts the delivered Warden seam at `ops-warden/wiki/CatalogLaneScaffoldContract.md` (`ac112d8`). T04 is now a ready source implementation task for the platform-owned rendering and schema fixture. Warden T02 may progress independently; no mutual owner-return wait remains. This is source contract acceptance, not credential admission or production authorization.
|
2026-10-03: Platform source review accepts the delivered Warden seam at `ops-warden/wiki/CatalogLaneScaffoldContract.md` (`ac112d8`). T04 is now a ready source implementation task for the platform-owned rendering and schema fixture. Warden T02 may progress independently; no mutual owner-return wait remains. This is source contract acceptance, not credential admission or production authorization.
|
||||||
|
|
||||||
|
2026-10-04 T04 implementation: closed platform metadata schema and `credential-change.py scaffold-lane` authored with deterministic proposed CCR, existing exact-path policy/auth rendering, namespace-restricted draft ESO with annotation containment, and non-executed owner evidence checklist. Shared synthetic platform/Warden pointer fixtures retain unknown workload identity and unconfirmed auth claims. Fourteen focused offline tests pass in isolated prepared source; cross-owner staging fixture acceptance remains before marking T04 done. No admission, credentials, human answer or production execution.
|
||||||
|
|
||||||
|
2026-10-04 T04 acceptance: actual applied-source scaffold tests 14/14 and existing window tests 7/7 pass, with CLI help and diff checks. Both actual owner CLIs produced matching cross-owner synthetic fixture drafts; platform independently verified Warden catalog/model loading, exact lane/owner/consumer/risk/CCR pointers and fixture digest, preserved unknown workload, unconfirmed prospective auth, proposed/unresolvable frontdoor, explicit ESO target annotations and non-executed evidence. Six platform artifacts match the independently generated second platform stage byte-for-byte. Receipt: `docs/evidence/2026-10-04-catalog-lane-scaffold-offline.json`. T04 is done for offline source/scaffold/fixture acceptance only. T02 remains the founder custody-choice wait; T03 remains the Custodian dated-window handoff. No admission, actual credentials, schedule or production execution was performed.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue