secrets-engine/src/secrets_engine/config.py

98 lines
4.2 KiB
Python
Raw Normal View History

"""Runtime configuration resolved from environment and repo layout.
Nothing here is a secret. Backend auth (named providers in engine_auth) is
resolved lazily inside the backend adapter, never cached on disk by this module.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
def repo_root() -> Path:
"""Repo root = nearest ancestor containing pyproject.toml (fallback: cwd)."""
here = Path(__file__).resolve()
for parent in (here, *here.parents):
if (parent / "pyproject.toml").exists():
return parent
return Path.cwd()
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 _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
policy_dir: Path
evidence_dir: Path
hub_url: str
bao_addr: str
topic_id: str
approval_url: str = ""
approval_token_file: Path | None = None
approval_client_secret_file: Path | None = None
keycape_token_url: str = ""
keycape_issuer: str = ""
keycape_client_secret_file: Path | None = None
openbao_jwt_login_file: Path | None = None
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
# 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
feat: complete and prove the authorization chain end to end Implements step 2 (access-engine POST /v1/check) and wires the whole GH-DEC-2026-003 sequence together, then proves it against a live throwaway OpenBao rather than only unit-level fakes. - decision_check.check_decision performs the PDP call; an unreachable or non-200 PDP raises, since silence is never permission. - approval_consume.authorize_action coordinates steps 1 and 2 and returns an AuthorizedAction. Both steps build the same CheckRequest via a shared _expected_request, since two descriptions of the action cannot produce corresponding digests. - apply_unreachable_engine_stance takes authorized=. The published map defines fail_closed as no side effect WITHOUT a durable decision record, so holding a validated one means the residue does not apply. Not a bypass: both steps must have succeeded and CAS consume still precedes OpenBao. Unconfigured still returns None and fails closed. The end-to-end test caught one more instance of the cross-vocabulary bug: a leftover comparison of the claim's binding.action against ours. The claim says secrets.kv.destroy where we say destroy, so it would have failed against every real claim. Removed; the tie is pdp_digest. Integration coverage asserts PIP-then-PDP ordering, that consume is the last step before the backend, and that an unreachable PDP, denied decision, invalid claim, missing pdp_digest, consume conflict and action mismatch each stop before OpenBao. 284 tests pass; production still fails closed. 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 14:56:02 +02:00
pdp_url: str = ""
pdp_token_file: Path | None = None
@classmethod
def load(cls) -> "Config":
root = repo_root()
token_file = os.environ.get("SECRETS_ENGINE_APPROVAL_TOKEN_FILE", "")
approval_secret = os.environ.get("SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE", "")
keycape_secret = os.environ.get("SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE", "")
jwt_login = os.environ.get("SECRETS_ENGINE_OPENBAO_JWT_LOGIN", "")
feat: complete and prove the authorization chain end to end Implements step 2 (access-engine POST /v1/check) and wires the whole GH-DEC-2026-003 sequence together, then proves it against a live throwaway OpenBao rather than only unit-level fakes. - decision_check.check_decision performs the PDP call; an unreachable or non-200 PDP raises, since silence is never permission. - approval_consume.authorize_action coordinates steps 1 and 2 and returns an AuthorizedAction. Both steps build the same CheckRequest via a shared _expected_request, since two descriptions of the action cannot produce corresponding digests. - apply_unreachable_engine_stance takes authorized=. The published map defines fail_closed as no side effect WITHOUT a durable decision record, so holding a validated one means the residue does not apply. Not a bypass: both steps must have succeeded and CAS consume still precedes OpenBao. Unconfigured still returns None and fails closed. The end-to-end test caught one more instance of the cross-vocabulary bug: a leftover comparison of the claim's binding.action against ours. The claim says secrets.kv.destroy where we say destroy, so it would have failed against every real claim. Removed; the tie is pdp_digest. Integration coverage asserts PIP-then-PDP ordering, that consume is the last step before the backend, and that an unreachable PDP, denied decision, invalid claim, missing pdp_digest, consume conflict and action mismatch each stop before OpenBao. 284 tests pass; production still fails closed. 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 14:56:02 +02:00
pdp_token = os.environ.get("SECRETS_ENGINE_PDP_TOKEN_FILE", "")
return cls(
catalog_dir=Path(os.environ.get("SECRETS_ENGINE_CATALOG", root / "catalog")),
policy_dir=Path(os.environ.get("SECRETS_ENGINE_POLICIES", root / "policies")),
evidence_dir=Path(os.environ.get("SECRETS_ENGINE_EVIDENCE", root / ".evidence")),
hub_url=os.environ.get("SECRETS_ENGINE_HUB_URL", "http://127.0.0.1:8000"),
bao_addr=os.environ.get("BAO_ADDR", os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200")),
topic_id=os.environ.get(
"SECRETS_ENGINE_TOPIC_ID", "cee7bedf-2b48-46ef-8601-006474f2ad7a"
),
approval_url=os.environ.get("SECRETS_ENGINE_APPROVAL_URL", ""),
approval_token_file=Path(token_file) if token_file else None,
approval_client_secret_file=Path(approval_secret) if approval_secret else None,
keycape_token_url=os.environ.get("SECRETS_ENGINE_KEYCAPE_TOKEN_URL", ""),
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,
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
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", "")
),
feat: complete and prove the authorization chain end to end Implements step 2 (access-engine POST /v1/check) and wires the whole GH-DEC-2026-003 sequence together, then proves it against a live throwaway OpenBao rather than only unit-level fakes. - decision_check.check_decision performs the PDP call; an unreachable or non-200 PDP raises, since silence is never permission. - approval_consume.authorize_action coordinates steps 1 and 2 and returns an AuthorizedAction. Both steps build the same CheckRequest via a shared _expected_request, since two descriptions of the action cannot produce corresponding digests. - apply_unreachable_engine_stance takes authorized=. The published map defines fail_closed as no side effect WITHOUT a durable decision record, so holding a validated one means the residue does not apply. Not a bypass: both steps must have succeeded and CAS consume still precedes OpenBao. Unconfigured still returns None and fails closed. The end-to-end test caught one more instance of the cross-vocabulary bug: a leftover comparison of the claim's binding.action against ours. The claim says secrets.kv.destroy where we say destroy, so it would have failed against every real claim. Removed; the tie is pdp_digest. Integration coverage asserts PIP-then-PDP ordering, that consume is the last step before the backend, and that an unreachable PDP, denied decision, invalid claim, missing pdp_digest, consume conflict and action mismatch each stop before OpenBao. 284 tests pass; production still fails closed. 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 14:56:02 +02:00
pdp_url=os.environ.get("SECRETS_ENGINE_PDP_URL", ""),
pdp_token_file=Path(pdp_token) if pdp_token else None,
)