Files
certctl/internal/domain/approval.go
T
shankar0123 19706e56b3 chore: drop 'Infisical' label from internal references
Strategic naming cleanup. Earlier doc-comments + commit messages framed Rank
4 / Rank 5 / Rank 7 work as 'Rank N of the 2026-05-03 Infisical deep-research
deliverable' — the 'Infisical' qualifier was a holdover from the original
deep-research framing where Infisical (a competing secrets-management
platform) was the comparator. Keeping the comparator's name in our source
adds noise without value; an external reader sees 'Infisical' and assumes a
dependency or shared lineage rather than reading it as the competitive
context it was.

Mechanical sed across 34 files (32 source / docs + 2 follow-up Python passes
to collapse 'deep-research deep-research' duplicates that emerged where the
original phrase wrapped across lines):

  s|Infisical deep-research|deep-research|g
  s|infisical-deep-research-results|deep-research-results-2026-05-03|g
  s|infisical-deep-research-prompt|deep-research-prompt-2026-05-03|g
  s|infisical-deep-research|deep-research|g
  s|Infisical|deep-research|g
  s|deep-research deep-research|deep-research|g  # collapse-pass

Net diff: 63 insertions / 64 deletions across cmd/, docs/, internal/,
migrations/. Pure text substitution; zero behavior change. Code path
unchanged — go vet clean, tests for TestApproval pass on both
internal/service and internal/api/handler packages.

Workspace docs (cowork/) carry the same references and will be swept
separately — they're not under certctl/ git control. The two filename
references (cowork/infisical-deep-research-results.md +
cowork/infisical-deep-research-prompt.md) get renamed alongside that sweep
to deep-research-results-2026-05-03.md /
deep-research-prompt-2026-05-03.md so cross-references in the certctl
repo doc-comments resolve cleanly.
2026-05-04 01:15:01 +00:00

103 lines
4.5 KiB
Go

package domain
import "time"
// ApprovalRequest represents a pending issuance / renewal that requires
// human approval before the issuer connector is dispatched. One row per
// (CertificateID, JobID) pair; the JobID points at the blocked Job whose
// Status is JobStatusAwaitingApproval.
//
// Lifecycle:
//
// pending → approved (Approve called by a non-requester)
// pending → rejected (Reject called)
// pending → expired (scheduler reaper at approvalCutoff)
//
// Once terminal, the row is immutable; the audit_events table is the
// durable record of who approved + why.
//
// Rank 7 of the 2026-05-03 deep-research deliverable
// (cowork/deep-research-results-2026-05-03.md Part 5). Closes the
// "two-person integrity / four-eyes principle" procurement gap for
// PCI-DSS Level 1, FedRAMP Moderate / High, and SOC 2 Type II
// customers.
type ApprovalRequest struct {
ID string `json:"id"` // ar-<slug>
CertificateID string `json:"certificate_id"` // FK managed_certificates.id
JobID string `json:"job_id"` // FK jobs.id (the blocked Job)
ProfileID string `json:"profile_id"` // CertificateProfile that triggered the gate
RequestedBy string `json:"requested_by"` // actor that triggered the renewal
State ApprovalState `json:"state"` // pending / approved / rejected / expired
DecidedBy *string `json:"decided_by,omitempty"` // null while state=pending
DecidedAt *time.Time `json:"decided_at,omitempty"` // null while state=pending
DecisionNote *string `json:"decision_note,omitempty"` // operator's reason text
Metadata map[string]string `json:"metadata,omitempty"` // common_name, sans, issuer_id, severity_tier
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ApprovalState is the closed enum of approval lifecycle states.
type ApprovalState string
const (
// ApprovalStatePending is the initial state — created by RequestApproval,
// blocking the linked Job at JobStatusAwaitingApproval. The scheduler does
// NOT dispatch the job until the approval transitions to approved.
ApprovalStatePending ApprovalState = "pending"
// ApprovalStateApproved is the success terminal state. Approve sets
// DecidedBy / DecidedAt / DecisionNote and transitions the linked Job
// from AwaitingApproval to Pending so the job processor picks it up.
ApprovalStateApproved ApprovalState = "approved"
// ApprovalStateRejected is the human-rejected terminal state. The
// linked Job transitions from AwaitingApproval to Cancelled.
ApprovalStateRejected ApprovalState = "rejected"
// ApprovalStateExpired is the timeout terminal state. The scheduler's
// reaper transitions stale pending requests to expired after the
// CERTCTL_JOB_AWAITING_APPROVAL_TIMEOUT cutoff (default 168h = 7 days).
ApprovalStateExpired ApprovalState = "expired"
)
// IsValidApprovalState reports whether s is a closed-enum value. Used by
// repository validation + handler request-body parsing to defend against
// off-enum typos at write time.
func IsValidApprovalState(s ApprovalState) bool {
switch s {
case ApprovalStatePending, ApprovalStateApproved,
ApprovalStateRejected, ApprovalStateExpired:
return true
}
return false
}
// IsTerminal reports whether s is one of the immutable terminal states
// (approved / rejected / expired). Once terminal, an ApprovalRequest's
// row cannot be mutated; subsequent Approve / Reject calls return
// ErrApprovalAlreadyDecided.
func (s ApprovalState) IsTerminal() bool {
switch s {
case ApprovalStateApproved, ApprovalStateRejected, ApprovalStateExpired:
return true
}
return false
}
// Approval-decision outcome strings used by the metrics counter
// (certctl_approval_decisions_total{outcome,profile_id}). Matches the
// Prometheus convention: lower-case, snake_case, bounded cardinality.
const (
ApprovalOutcomeApproved = "approved"
ApprovalOutcomeRejected = "rejected"
ApprovalOutcomeExpired = "expired"
ApprovalOutcomeBypassed = "bypassed"
)
// ApprovalActorSystemBypass is the synthetic actor identity stamped on
// audit rows + DecidedBy when CERTCTL_APPROVAL_BYPASS=true short-circuits
// the workflow for dev/CI. Production deploys MUST leave the bypass
// unset; compliance auditors run `SELECT FROM audit_events WHERE
// actor='system-bypass'` to confirm zero rows.
const ApprovalActorSystemBypass = "system-bypass"