feat: implement the PIP claim + validate authorization join
resolve_consume_binding was a `return None` stub, so protocol step 1 of
docs/approval-consumption.md (GET /v1/approvals/{id}/claim) and the
validation join never existed. validate_action_authorization had no caller
in src/ at all - it was reachable only from tests. Production fail-closed
was correct, but for an undocumented second reason, and WP-0007-T04's
"what remains is not local engine work" was wrong.
The join now reproduces the exact CheckRequest via build_action_request,
fetches the durable ActionAuthorization, and validates request binding,
digest, validity, authority, policy pin, and distinct-approver threshold
before offering a consume binding. _require_lane_approval threads the exact
field set for provision/rotate/verify/exec so the digest covers the real
proposed action.
Deliberate choices:
- The approval-engine object id is never inferred from a State Hub decision
UUID; flex-auth stated GET /decisions/{uuid} is not the durable object.
- No default policy pin. flex-auth stated secrets-engine.lifecycle/v1 is
example vocabulary, not a published package.
- A half-configured join raises rather than returning None, so a partial
deployment cannot be mistaken for an unconfigured one.
Behavior is unchanged today: every new input is absent by default, so
production still fails closed and plan/--dry-run still work. 234 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
This commit is contained in:
parent
ebcc36ecab
commit
627810b478
10 changed files with 456 additions and 25 deletions
|
|
@ -19,6 +19,15 @@ def repo_root() -> Path:
|
|||
return Path.cwd()
|
||||
|
||||
|
||||
def _positive_int(raw: str, default: int = 1) -> int:
|
||||
"""Parse a positive approval threshold. Anything malformed keeps the default."""
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return value if value >= 1 else default
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Config:
|
||||
catalog_dir: Path
|
||||
|
|
@ -33,6 +42,13 @@ class Config:
|
|||
keycape_issuer: str = ""
|
||||
keycape_client_secret_file: Path | None = None
|
||||
openbao_jwt_login_file: Path | None = None
|
||||
# PIP/PDP join (SECRETS-WP-0007-T04 / SECRETS-WP-0008-T02). All absent by
|
||||
# default: an unset value fails production closed exactly as before.
|
||||
authorization_subject_id: str = ""
|
||||
authorization_subject_type: str = ""
|
||||
authorization_policy_package: str = ""
|
||||
authorization_policy_version: str = ""
|
||||
authorization_min_approvals: int = 1
|
||||
|
||||
@classmethod
|
||||
def load(cls) -> "Config":
|
||||
|
|
@ -55,4 +71,19 @@ class Config:
|
|||
keycape_issuer=os.environ.get("SECRETS_ENGINE_KEYCAPE_ISSUER", ""),
|
||||
keycape_client_secret_file=Path(keycape_secret) if keycape_secret else None,
|
||||
openbao_jwt_login_file=Path(jwt_login) if jwt_login else None,
|
||||
authorization_subject_id=os.environ.get(
|
||||
"SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID", ""
|
||||
),
|
||||
authorization_subject_type=os.environ.get(
|
||||
"SECRETS_ENGINE_AUTHORIZATION_SUBJECT_TYPE", ""
|
||||
),
|
||||
authorization_policy_package=os.environ.get(
|
||||
"SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE", ""
|
||||
),
|
||||
authorization_policy_version=os.environ.get(
|
||||
"SECRETS_ENGINE_AUTHORIZATION_POLICY_VERSION", ""
|
||||
),
|
||||
authorization_min_approvals=_positive_int(
|
||||
os.environ.get("SECRETS_ENGINE_AUTHORIZATION_MIN_APPROVALS", "")
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue