RPF-WP-0069-T07: approve sets approved only once every required role approves.
credential-change.py approve set status approved after the first reviewer; for role-gated request types validation then refused the second approve before it was recorded. append_decision now records each approval and keeps the prior status while the lane validator still reports a missing role approval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 509845@bnt-lap001 Assistant-Session: ed7a7d95-d8eb-455c-b594-963fb1830f76
This commit is contained in:
parent
371f787493
commit
aeb21434bd
5 changed files with 108 additions and 5 deletions
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import difflib
|
||||
import json
|
||||
import os
|
||||
|
|
@ -480,9 +481,15 @@ def validate_kv_metadata(value: Any, errors: list[str]) -> None:
|
|||
|
||||
def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
scan_for_secrets(path, errors)
|
||||
ccr = load_yaml(path)
|
||||
data_errors, warnings = validate_ccr_data(ccr)
|
||||
return ccr, errors + data_errors, warnings
|
||||
|
||||
|
||||
def validate_ccr_data(ccr: dict[str, Any]) -> tuple[list[str], list[str]]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
for field in (
|
||||
"id",
|
||||
"kind",
|
||||
|
|
@ -526,7 +533,7 @@ def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]:
|
|||
errors.extend(lane_module(ccr).validate(ccr, REPO_DIR))
|
||||
else:
|
||||
errors.append("unsupported request_type")
|
||||
return ccr, errors, warnings
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str:
|
||||
|
|
@ -1857,6 +1864,21 @@ def render_status(payload: dict[str, Any]) -> str:
|
|||
return "\n".join(lines)
|
||||
|
||||
|
||||
ROLE_APPROVAL_MISSING = "explicit role approval missing: "
|
||||
|
||||
|
||||
def pending_role_approvals(ccr: dict[str, Any]) -> list[str]:
|
||||
"""Roles the request type still requires before the CCR may be `approved`.
|
||||
|
||||
Only lane validators that enforce role approvals report these; for other
|
||||
request types the list is always empty.
|
||||
"""
|
||||
candidate = copy.deepcopy(ccr)
|
||||
candidate["status"] = "approved"
|
||||
errors, _warnings = validate_ccr_data(candidate)
|
||||
return [e[len(ROLE_APPROVAL_MISSING):] for e in errors if e.startswith(ROLE_APPROVAL_MISSING)]
|
||||
|
||||
|
||||
def append_decision(path: Path, status: str, reviewer: str, comment: str) -> dict[str, Any]:
|
||||
reject_secret_text(comment, "review comment")
|
||||
ccr, _warnings = validate_or_exit(path)
|
||||
|
|
@ -1872,6 +1894,10 @@ def append_decision(path: Path, status: str, reviewer: str, comment: str) -> dic
|
|||
"comment": comment,
|
||||
}
|
||||
)
|
||||
# An approval is recorded per reviewer, but the CCR only becomes `approved`
|
||||
# once every role its request type requires has approved (RPF-WP-0069-T07).
|
||||
if status == "approved" and pending_role_approvals(ccr):
|
||||
status = ccr["status"]
|
||||
ccr["status"] = status
|
||||
ccr["updated"] = datetime.now(timezone.utc).date().isoformat()
|
||||
dump_yaml(path, ccr)
|
||||
|
|
@ -2261,7 +2287,12 @@ def command_record_evidence(args: argparse.Namespace) -> int:
|
|||
def command_decision(args: argparse.Namespace, status: str) -> int:
|
||||
path = resolve_ccr(args.ref)
|
||||
ccr = append_decision(path, status, args.reviewer, args.comment)
|
||||
print(f"[OK] {path.name} -> {status}")
|
||||
print(f"[OK] {path.name} -> {ccr['status']}")
|
||||
if status == "approved" and ccr["status"] != "approved":
|
||||
print(
|
||||
f"[INFO] {args.reviewer} approval recorded; still awaiting: "
|
||||
+ ", ".join(pending_role_approvals(ccr))
|
||||
)
|
||||
if args.record_state_hub:
|
||||
openbao = ccr["openbao"]
|
||||
auth = openbao["auth"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue