2026-09-02 01:06:50 +02:00
|
|
|
"""PEP consume-before-side-effect client (GH-DEC-2026-003).
|
|
|
|
|
|
|
|
|
|
The PEP that is about to cause a protected OpenBao write MUST obtain a
|
|
|
|
|
successful approval-engine CAS consume first. Holding a claim or an ALLOW is
|
|
|
|
|
not authority to act. Conflict, unavailability, or a missing binding means
|
|
|
|
|
do not call OpenBao.
|
|
|
|
|
|
|
|
|
|
This module does not render an authorization decision. The consume response
|
|
|
|
|
is mutation evidence, never a permission.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import re
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any, Callable
|
|
|
|
|
from urllib.error import HTTPError, URLError
|
|
|
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
from secrets_engine.approval_claim import (
|
|
|
|
|
binding_from_check_request,
|
|
|
|
|
claim_binding_digest,
|
|
|
|
|
validate_approval_claim,
|
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
2026-09-06 01:01:55 +02:00
|
|
|
)
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
from secrets_engine.authorization import build_action_request, request_digest
|
2026-09-02 01:06:50 +02:00
|
|
|
from secrets_engine.errors import DecisionError
|
|
|
|
|
from secrets_engine.openbao import read_strict_token_file
|
|
|
|
|
from secrets_engine.pep_stance import demo_exception_enabled
|
|
|
|
|
|
|
|
|
|
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
|
|
|
_MAX_BODY = 256 * 1024
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ConsumeBinding:
|
|
|
|
|
"""Inputs the PEP presents to approval-engine consume."""
|
|
|
|
|
|
|
|
|
|
approval_id: str
|
|
|
|
|
request_digest: str
|
|
|
|
|
decision_id: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ConsumedApproval:
|
|
|
|
|
"""Non-secret confirmation that consume succeeded for this request."""
|
|
|
|
|
|
|
|
|
|
approval_id: str
|
|
|
|
|
request_digest: str
|
|
|
|
|
decision_id: str = ""
|
|
|
|
|
idempotent: bool = False
|
|
|
|
|
consumed_at: str = ""
|
|
|
|
|
|
|
|
|
|
def as_evidence(self) -> dict[str, object]:
|
|
|
|
|
payload: dict[str, object] = {
|
|
|
|
|
"approval_consumed": True,
|
|
|
|
|
"approval_id": self.approval_id,
|
|
|
|
|
"request_digest": self.request_digest,
|
|
|
|
|
"approval_consume_idempotent": self.idempotent,
|
|
|
|
|
}
|
|
|
|
|
if self.decision_id:
|
|
|
|
|
payload["decision_id"] = self.decision_id
|
|
|
|
|
if self.consumed_at:
|
|
|
|
|
payload["approval_consumed_at"] = self.consumed_at
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
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
2026-09-06 01:01:55 +02:00
|
|
|
def _authorization_id(entry: Any, decision: Any) -> str:
|
|
|
|
|
"""Non-secret approval-engine object id for this lane, or "" if unbound.
|
|
|
|
|
|
|
|
|
|
flex-auth: ``ActionAuthorization.id`` is the approval-engine object UUID.
|
|
|
|
|
It is never the State Hub decision UUID, so it is not inferred from one.
|
|
|
|
|
"""
|
|
|
|
|
approval = getattr(entry, "approval", None) or {}
|
|
|
|
|
if isinstance(approval, dict):
|
|
|
|
|
declared = str(approval.get("authorization_id", "") or "").strip()
|
|
|
|
|
if declared:
|
|
|
|
|
return declared
|
|
|
|
|
for attr in ("authorization_id", "action_authorization_id"):
|
|
|
|
|
served = str(getattr(decision, attr, "") or "").strip()
|
|
|
|
|
if served:
|
|
|
|
|
return served
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _request_purpose(entry: Any) -> str:
|
|
|
|
|
"""Declared purpose for the request context. Never invented at call time."""
|
|
|
|
|
approval = getattr(entry, "approval", None) or {}
|
|
|
|
|
if isinstance(approval, dict):
|
|
|
|
|
declared = str(approval.get("purpose", "") or "").strip()
|
|
|
|
|
if declared:
|
|
|
|
|
return declared
|
|
|
|
|
for consumer in getattr(entry, "consumers", None) or []:
|
|
|
|
|
if isinstance(consumer, dict):
|
|
|
|
|
declared = str(consumer.get("purpose", "") or "").strip()
|
|
|
|
|
if declared:
|
|
|
|
|
return declared
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
def fetch_approval_claim(
|
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
2026-09-06 01:01:55 +02:00
|
|
|
*,
|
|
|
|
|
base_url: str,
|
|
|
|
|
token_file: Path,
|
|
|
|
|
authorization_id: str,
|
|
|
|
|
timeout_seconds: float = 3,
|
|
|
|
|
opener: Callable[..., Any] = urlopen,
|
|
|
|
|
) -> dict[str, Any]:
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
"""GET /v1/approvals/{id}/claim (PIP). Any non-200 fails closed.
|
|
|
|
|
|
|
|
|
|
The body is approval-engine's approval-claim, not a flex-auth
|
|
|
|
|
ActionAuthorization -- that object is deferred and was never ratified
|
|
|
|
|
(GH-DEC-2026-005 / FLEX-DEC-2026-006).
|
|
|
|
|
"""
|
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
2026-09-06 01:01:55 +02:00
|
|
|
if not base_url or not base_url.startswith(("http://", "https://")):
|
|
|
|
|
raise DecisionError("approval-engine claim URL is missing or invalid")
|
|
|
|
|
ident = authorization_id.strip()
|
|
|
|
|
if not ident or "/" in ident or any(ch.isspace() for ch in ident):
|
|
|
|
|
raise DecisionError("approval claim requires a concrete authorization id")
|
|
|
|
|
token = read_strict_token_file(Path(token_file), purpose="approval claim credential")
|
|
|
|
|
request = Request(
|
|
|
|
|
base_url.rstrip("/") + f"/v1/approvals/{ident}/claim",
|
|
|
|
|
method="GET",
|
|
|
|
|
)
|
|
|
|
|
request.add_header("Authorization", f"Bearer {token}")
|
|
|
|
|
request.add_header("Accept", "application/json")
|
|
|
|
|
try:
|
|
|
|
|
with opener(request, timeout=timeout_seconds) as response:
|
|
|
|
|
if getattr(response, "status", 200) != 200:
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
raise DecisionError("approval claim did not return a claim")
|
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
2026-09-06 01:01:55 +02:00
|
|
|
payload = json.loads(response.read(_MAX_BODY).decode("utf-8"))
|
|
|
|
|
except HTTPError as e:
|
|
|
|
|
raise DecisionError(f"approval claim refused: {_status_message(e.code)}") from e
|
|
|
|
|
except URLError as e:
|
|
|
|
|
raise DecisionError("approval-engine is unreachable for claim") from e
|
|
|
|
|
except json.JSONDecodeError as e:
|
|
|
|
|
raise DecisionError("approval claim returned a non-JSON body") from e
|
|
|
|
|
if not isinstance(payload, dict):
|
|
|
|
|
raise DecisionError("approval claim returned a non-object body")
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
2026-09-02 01:06:50 +02:00
|
|
|
def resolve_consume_binding(
|
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
2026-09-06 01:01:55 +02:00
|
|
|
cfg: Any,
|
|
|
|
|
entry: Any,
|
|
|
|
|
action: str,
|
|
|
|
|
decision: Any,
|
|
|
|
|
*,
|
|
|
|
|
fields: tuple[str, ...] = (),
|
|
|
|
|
policy_targets: tuple[str, ...] = (),
|
|
|
|
|
auth_targets: tuple[str, ...] = (),
|
|
|
|
|
opener: Callable[..., Any] | None = None,
|
2026-09-02 01:06:50 +02:00
|
|
|
) -> ConsumeBinding | None:
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
"""Join the proposed action to a served approval-claim (step 1 of GH-DEC-2026-003).
|
|
|
|
|
|
|
|
|
|
Returns None only when no serving path is configured at all, keeping
|
|
|
|
|
production fail-closed exactly as it was before the join existed. Anything
|
|
|
|
|
configured-but-wrong raises: a half-configured PEP must not look like an
|
|
|
|
|
unconfigured one.
|
|
|
|
|
|
|
|
|
|
Step 2 (the flex-auth DecisionEnvelope from POST /v1/check) is validated by
|
|
|
|
|
``authorization.validate_decision_envelope``. No PDP is reachable for this
|
|
|
|
|
consumer yet -- flex-auth runs per-consumer cluster-local pins and
|
|
|
|
|
``flex-auth-secrets-engine`` has not been created -- so that call is not
|
|
|
|
|
wired here and production stays closed at the stance gate regardless.
|
2026-09-02 01:06:50 +02:00
|
|
|
"""
|
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
2026-09-06 01:01:55 +02:00
|
|
|
base_url = str(getattr(cfg, "approval_url", "") or "")
|
|
|
|
|
token_file = getattr(cfg, "approval_token_file", None)
|
|
|
|
|
authorization_id = _authorization_id(entry, decision)
|
|
|
|
|
if not base_url or not token_file or not authorization_id:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
subject_id = str(getattr(cfg, "authorization_subject_id", "") or "")
|
|
|
|
|
subject_type = str(getattr(cfg, "authorization_subject_type", "") or "")
|
|
|
|
|
if not subject_id or not subject_type:
|
|
|
|
|
raise DecisionError(
|
|
|
|
|
"authorization join requires SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID "
|
|
|
|
|
"and _SUBJECT_TYPE; the PEP must not assert an unnamed subject"
|
|
|
|
|
)
|
|
|
|
|
purpose = _request_purpose(entry)
|
|
|
|
|
if not purpose:
|
|
|
|
|
raise DecisionError(
|
|
|
|
|
"authorization join requires a declared approval/consumer purpose"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expected_request = build_action_request(
|
|
|
|
|
entry,
|
|
|
|
|
action,
|
|
|
|
|
subject_id=subject_id,
|
|
|
|
|
subject_type=subject_type,
|
|
|
|
|
purpose=purpose,
|
|
|
|
|
fields=fields,
|
|
|
|
|
policy_targets=policy_targets,
|
|
|
|
|
auth_targets=auth_targets,
|
|
|
|
|
)
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
# Two different digests over the same proposed action, by contract: the
|
|
|
|
|
# approval-engine native binding digest, and the flex-auth CheckRequest
|
|
|
|
|
# digest. They are not interchangeable and are never compared to each other.
|
|
|
|
|
native_digest = claim_binding_digest(**binding_from_check_request(expected_request))
|
|
|
|
|
pdp_digest = request_digest(expected_request)
|
|
|
|
|
|
|
|
|
|
claim = fetch_approval_claim(
|
|
|
|
|
base_url=base_url,
|
|
|
|
|
token_file=Path(token_file),
|
|
|
|
|
authorization_id=authorization_id,
|
|
|
|
|
opener=opener or urlopen,
|
|
|
|
|
)
|
|
|
|
|
validate_approval_claim(
|
|
|
|
|
claim,
|
|
|
|
|
approval_id=authorization_id,
|
|
|
|
|
expected_binding_digest=native_digest,
|
|
|
|
|
expected_pdp_digest=pdp_digest,
|
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
2026-09-06 01:01:55 +02:00
|
|
|
)
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
binding = claim.get("binding") or {}
|
|
|
|
|
if isinstance(binding, dict) and binding.get("action") not in (None, action):
|
|
|
|
|
raise DecisionError("approval claim does not bind this action")
|
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
2026-09-06 01:01:55 +02:00
|
|
|
return ConsumeBinding(
|
feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
2026-09-06 08:02:01 +02:00
|
|
|
approval_id=authorization_id,
|
|
|
|
|
request_digest=pdp_digest,
|
|
|
|
|
decision_id="",
|
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
2026-09-06 01:01:55 +02:00
|
|
|
)
|
2026-09-02 01:06:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def consume_approval(
|
|
|
|
|
*,
|
|
|
|
|
base_url: str,
|
|
|
|
|
token_file: Path,
|
|
|
|
|
binding: ConsumeBinding,
|
|
|
|
|
timeout_seconds: float = 3,
|
|
|
|
|
opener: Callable[..., Any] = urlopen,
|
|
|
|
|
) -> ConsumedApproval:
|
|
|
|
|
"""POST /v1/approvals/{id}/consume. Fail closed on anything but confirmed use."""
|
|
|
|
|
if not base_url or not base_url.startswith(("http://", "https://")):
|
|
|
|
|
raise DecisionError("approval-engine consume URL is missing or invalid")
|
|
|
|
|
approval_id = binding.approval_id.strip()
|
|
|
|
|
if not approval_id or "/" in approval_id or any(ch.isspace() for ch in approval_id):
|
|
|
|
|
raise DecisionError("approval consume requires a concrete approval id")
|
|
|
|
|
if not DIGEST_RE.fullmatch(binding.request_digest):
|
|
|
|
|
raise DecisionError("approval consume requires the canonical request digest")
|
|
|
|
|
|
|
|
|
|
token = read_strict_token_file(Path(token_file), purpose="approval consume credential")
|
|
|
|
|
body: dict[str, str] = {"request_digest": binding.request_digest}
|
|
|
|
|
if binding.decision_id:
|
|
|
|
|
body["decision_id"] = binding.decision_id
|
|
|
|
|
encoded = json.dumps(body).encode("utf-8")
|
|
|
|
|
request = Request(
|
|
|
|
|
base_url.rstrip("/") + f"/v1/approvals/{approval_id}/consume",
|
|
|
|
|
data=encoded,
|
|
|
|
|
method="POST",
|
|
|
|
|
headers={
|
|
|
|
|
"Authorization": f"Bearer {token}",
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
"Accept": "application/json",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
response = opener(request, timeout=timeout_seconds)
|
|
|
|
|
try:
|
|
|
|
|
status = int(response.getcode())
|
|
|
|
|
raw = response.read(_MAX_BODY + 1)
|
|
|
|
|
finally:
|
|
|
|
|
response.close()
|
|
|
|
|
except HTTPError as exc:
|
|
|
|
|
status = int(getattr(exc, "code", 0) or 0)
|
|
|
|
|
try:
|
|
|
|
|
exc.read(_MAX_BODY)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
raise DecisionError(_status_message(status)) from None
|
|
|
|
|
except (URLError, TimeoutError, OSError):
|
|
|
|
|
raise DecisionError(
|
|
|
|
|
"approval-engine unreachable; OpenBao must not be called"
|
|
|
|
|
) from None
|
|
|
|
|
|
|
|
|
|
if status != 200 or len(raw) > _MAX_BODY:
|
|
|
|
|
raise DecisionError(_status_message(status if status != 200 else 502))
|
|
|
|
|
try:
|
|
|
|
|
payload = json.loads(raw.decode("utf-8"))
|
|
|
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
|
|
|
raise DecisionError("approval consume returned invalid JSON") from exc
|
|
|
|
|
if not isinstance(payload, dict):
|
|
|
|
|
raise DecisionError("approval consume returned invalid payload")
|
|
|
|
|
if payload.get("status") != "consumed":
|
|
|
|
|
raise DecisionError("approval consumption was not confirmed")
|
|
|
|
|
if payload.get("request_digest") != binding.request_digest:
|
|
|
|
|
raise DecisionError("approval consume digest does not match the request")
|
|
|
|
|
decision_id = payload.get("decision_id")
|
|
|
|
|
if decision_id is not None and (
|
|
|
|
|
not isinstance(decision_id, str) or not decision_id
|
|
|
|
|
):
|
|
|
|
|
raise DecisionError("approval consume returned an invalid decision id")
|
|
|
|
|
consumed_at = payload.get("consumed_at")
|
|
|
|
|
if consumed_at is not None and not isinstance(consumed_at, str):
|
|
|
|
|
raise DecisionError("approval consume returned an invalid consumed_at")
|
|
|
|
|
return ConsumedApproval(
|
|
|
|
|
approval_id=str(payload.get("approval_id") or approval_id),
|
|
|
|
|
request_digest=binding.request_digest,
|
|
|
|
|
decision_id=decision_id or binding.decision_id,
|
|
|
|
|
idempotent=bool(payload.get("idempotent")),
|
|
|
|
|
consumed_at=consumed_at or "",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_production_consume(
|
|
|
|
|
cfg: Any,
|
|
|
|
|
entry: Any,
|
|
|
|
|
*,
|
|
|
|
|
binding: ConsumeBinding | None,
|
|
|
|
|
evidence: Any = None,
|
|
|
|
|
opener: Callable[..., Any] | None = None,
|
|
|
|
|
) -> ConsumedApproval | None:
|
|
|
|
|
"""CAS-consume before a production OpenBao call. No-op off the prod path.
|
|
|
|
|
|
|
|
|
|
Build/test remain fail-open relative to approval-engine. The three-factor
|
|
|
|
|
unsafe-demo exception is not a consume path. Missing binding, URL, or
|
|
|
|
|
credential fail closed so a stance bypass cannot reach OpenBao.
|
|
|
|
|
"""
|
|
|
|
|
if getattr(entry, "stage", "") != "prod":
|
|
|
|
|
return None
|
|
|
|
|
if demo_exception_enabled(cfg):
|
|
|
|
|
return None
|
|
|
|
|
if binding is None:
|
|
|
|
|
raise DecisionError(
|
|
|
|
|
"production OpenBao call requires CAS consume of an approval "
|
|
|
|
|
"after an access-engine ALLOW; no durable consume binding is served"
|
|
|
|
|
)
|
|
|
|
|
base_url = str(getattr(cfg, "approval_url", "") or "")
|
|
|
|
|
token_file = getattr(cfg, "approval_token_file", None)
|
|
|
|
|
if not base_url:
|
|
|
|
|
raise DecisionError(
|
|
|
|
|
"production OpenBao call requires approval-engine consume; "
|
|
|
|
|
"SECRETS_ENGINE_APPROVAL_URL is unset"
|
|
|
|
|
)
|
|
|
|
|
if not token_file:
|
|
|
|
|
raise DecisionError(
|
|
|
|
|
"production OpenBao call requires approval-engine consume; "
|
|
|
|
|
"SECRETS_ENGINE_APPROVAL_TOKEN_FILE is unset"
|
|
|
|
|
)
|
|
|
|
|
consumed = consume_approval(
|
|
|
|
|
base_url=base_url,
|
|
|
|
|
token_file=Path(token_file),
|
|
|
|
|
binding=binding,
|
|
|
|
|
opener=opener or urlopen,
|
|
|
|
|
)
|
|
|
|
|
if evidence is not None and hasattr(evidence, "mark_consumed"):
|
|
|
|
|
evidence.mark_consumed(consumed)
|
|
|
|
|
return consumed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _status_message(status: int) -> str:
|
|
|
|
|
if status == 409:
|
|
|
|
|
return "approval consume conflict; OpenBao must not be called"
|
|
|
|
|
if status == 404:
|
|
|
|
|
return "approval not found; OpenBao must not be called"
|
|
|
|
|
if status in {401, 403}:
|
|
|
|
|
return "approval consume unauthorized; OpenBao must not be called"
|
|
|
|
|
if status == 503:
|
|
|
|
|
return "approval-engine unavailable; OpenBao must not be called"
|
|
|
|
|
if status == 0:
|
|
|
|
|
return "approval-engine unreachable; OpenBao must not be called"
|
|
|
|
|
return "approval consume failed; OpenBao must not be called"
|