secrets-engine/src/secrets_engine/rotate.py
tegwick 72d3327c28
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Add native rotate and persistent lane overlay states
rotate replaces one declared KV field through the merge-safe patch path
and never prints the value. Overlay states active/suspended/deactivated/
compromised live under the evidence directory. compromise/reactivate and
successful suspend/deactivate/revoke update that overlay; exec/wrap/
handoff/provision refuse non-active lanes. Provider-side rotation stays
with rotation.owner. Production remains fail-closed.

Assistant: grok
Assistant-Session: 01a05f07-ae72-7781-9fcb-19efd61add00
2026-09-02 13:09:10 +02:00

43 lines
1.6 KiB
Python

"""Native KV rotation: replace a declared field without touching siblings.
This updates OpenBao custody only. Catalog ``rotation.owner`` remains the
provider/workload owner; this engine does not roll consumers or revoke a
provider credential. Auth-capability lanes rotate via wrap/handoff, not here.
"""
from __future__ import annotations
from pathlib import Path
from secrets_engine.catalog import CatalogEntry
from secrets_engine.errors import ProvisioningError
from secrets_engine.openbao import OpenBaoClient
from secrets_engine.provision import provision_from_file
def render_rotate_plan(entry: CatalogEntry, field: str) -> str:
owner = str((entry.rotation or {}).get("owner") or "")
lines = [
f"Rotate plan for lane '{entry.id}'",
f" field: {field}",
f" target: {entry.mount}/{entry.path}",
f" owner: {owner or '-'}",
" siblings: preserved (CAS patch)",
" workload delivery: not mutated",
]
return "\n".join(lines)
def rotate_from_file(
client: OpenBaoClient, entry: CatalogEntry, field: str, file_path: Path
) -> str:
"""Replace one declared KV field. Returns the field name only."""
if not entry.stores_kv_value():
raise ProvisioningError(
f"lane '{entry.id}' is {entry.kind}; rotate native KV via this "
"command, or wrap/handoff for auth-capability material"
)
if field not in entry.fields:
raise ProvisioningError(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
return provision_from_file(client, entry, field, file_path)