mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:12:04 +00:00
2025275b43
Rank 7 of the 2026-05-03 Infisical deep-research deliverable, commit 1 of 4
(cowork/rank-7-approval-workflow-primitive-prompt.md). The four-commit
chain ships the issuance approval-workflow primitive (request → human review
→ CA call) closing the two-person integrity / four-eyes principle
procurement gap for PCI-DSS Level 1, FedRAMP Moderate / High, SOC 2
Type II, and HIPAA-regulated PHI deployments.
This commit lands ONLY the foundation — schema, types, repository
interface, postgres implementation. No service / handler wiring yet.
The four-commit shape is bisectable: the schema can land in production
behind a flag (via the default RequiresApproval=false on every existing
profile) without any operator-visible behavior change until commits 2-4
wire the surrounding workflow.
Existing scaffolding REUSED (not redefined here):
- JobStatusAwaitingApproval enum value (internal/domain/job.go).
- JobRepository.ListTimedOutAwaitingJobs (postgres reaper query).
- Config.Scheduler.AwaitingApprovalTimeout (env-mapped via
CERTCTL_JOB_AWAITING_APPROVAL_TIMEOUT, default 168h = 7 days).
- Scheduler.SetAwaitingApprovalTimeout wiring.
Files added:
internal/domain/approval.go - ApprovalRequest type,
ApprovalState closed enum
(pending/approved/rejected/
expired), IsValidApprovalState +
IsTerminal helpers, outcome
const block + bypass-actor
sentinel.
internal/repository/postgres/approval.go - ApprovalRepository
implementation: Create
(ar-<slug> ID gen + JSONB
metadata round-trip + lib/pq
23505 → ErrAlreadyExists
translation), Get, GetByJobID,
List (paginated with state /
cert / requester filters),
UpdateState (pending→terminal
transitions only, with
already-terminal disambiguation),
ExpireStale (bulk reaper,
decided_by='system-reaper').
migrations/000027_approval_workflow.{up,down}.sql
- Idempotent IF NOT EXISTS /
IF EXISTS. Adds
certificate_profiles.requires_approval
BOOLEAN NOT NULL DEFAULT false,
issuance_approval_requests
table with FK to
managed_certificates / jobs /
certificate_profiles, four
indexes (state, certificate,
pending-age, partial-unique
pending-per-job), and the
approval_decision_consistency
CHECK constraint enforcing
decided_by/decided_at must be
non-null for terminal states.
Files modified:
internal/domain/profile.go - Adds CertificateProfile.RequiresApproval
bool field with full doc
comment + JSON tag. Defaults
to false (back-compat — every
existing profile keeps the
unattended renewal path).
internal/repository/interfaces.go - Adds ApprovalRepository
interface (6 methods) +
ApprovalFilter struct.
internal/repository/errors.go - Adds ErrAlreadyExists sentinel
for postgres SQLSTATE 23505
(unique-constraint violations
from the partial-unique
pending-per-job index, plus
the "already terminal" state-
transition signal). Mirrors
the existing ErrNotFound +
ErrForeignKeyConstraint shape.
Verified:
gofmt: clean.
go vet ./internal/domain/... ./internal/repository/...: exit 0.
go build ./internal/domain/... ./internal/repository/...: exit 0.
Out of scope for this commit (lands in commits 2-4):
- service/approval.go (RequestApproval / Approve / Reject / ListPending
/ ExpireStale + same-actor RBAC + bypass mode + audit + metrics).
- service/approval_metrics.go (decisions counter + pending-age histogram).
- 8 service-level table-driven tests including the load-bearing
TestApproval_Approve_RejectsSameActor two-person integrity pin.
- api/handler/approval.go (5 endpoints + RBAC integration).
- api/openapi.yaml (5 new operationIds).
- Integration into CertificateService.TriggerRenewal +
RenewalService.CheckExpiringCertificates + Scheduler.ReapTimedOutJobs.
- cmd/server/main.go wiring.
- Config.Approval.BypassEnabled + CERTCTL_APPROVAL_BYPASS env var.
- docs/connectors.md CertificateProfile config-table row.
- docs/approval-workflow.md operator playbook + compliance control mapping.
Reference: cowork/infisical-deep-research-results.md Part 5 Rank 7.
Acquisition prompt: cowork/rank-7-approval-workflow-primitive-prompt.md.
76 lines
3.5 KiB
Go
76 lines
3.5 KiB
Go
// Package repository defines the repository-layer error sentinels that
|
|
// handlers map to HTTP status codes via errors.Is.
|
|
//
|
|
// S-2 closure (cat-s6-efc7f6f6bd50): pre-S-2 every handler-side
|
|
// not-found dispatch was a `strings.Contains(err.Error(), "not found")`
|
|
// site (30+ across internal/api/handler/*.go), brittle to any
|
|
// repository-layer message change and untyped against the actual
|
|
// failure mode. Post-S-2 the dispatch is type-checked: repositories
|
|
// wrap sql.ErrNoRows via fmt.Errorf("...: %w", repository.ErrNotFound)
|
|
// and FK constraint violations via repository.ErrForeignKeyConstraint;
|
|
// handlers consume via errors.Is. The substring matching is preserved
|
|
// at the lib/pq boundary inside `errors.go::isFKError` because the
|
|
// PostgreSQL driver returns un-typed *pq.Error values whose codes are
|
|
// the canonical signal — but it's confined to one helper rather than
|
|
// scattered across every handler file. See unified-audit.md
|
|
// cat-s6-efc7f6f6bd50 for the closure rationale.
|
|
package repository
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// ErrNotFound is the canonical sentinel for repository methods that
|
|
// return after sql.ErrNoRows (or its wrapped form). Handlers that
|
|
// surface a 404 should `errors.Is(err, repository.ErrNotFound)`
|
|
// rather than substring-match.
|
|
var ErrNotFound = errors.New("repository: row not found")
|
|
|
|
// ErrAlreadyExists is the canonical sentinel for postgres unique-
|
|
// constraint (SQLSTATE 23505) violations bubbling up from an INSERT
|
|
// (or partial-unique INSERT, like Rank 7's idx_approval_pending_per_job
|
|
// which enforces "at most one pending approval per job"). Handlers
|
|
// that surface a 409 Conflict should
|
|
// `errors.Is(err, repository.ErrAlreadyExists)`.
|
|
//
|
|
// The repo also reuses ErrAlreadyExists for "row is already terminal"
|
|
// state-transition attempts (e.g., Approve called on an already-
|
|
// approved request) — semantically the same "you're trying to create
|
|
// a state that already exists" failure mode.
|
|
var ErrAlreadyExists = errors.New("repository: row already exists")
|
|
|
|
// ErrForeignKeyConstraint is the canonical sentinel for PostgreSQL
|
|
// FK / RESTRICT violations bubbling up from a DELETE or UPDATE.
|
|
// Handlers that surface a 409 Conflict should
|
|
// `errors.Is(err, repository.ErrForeignKeyConstraint)`.
|
|
//
|
|
// The B-1 closure introduced ErrRenewalPolicyInUse as the per-entity
|
|
// FK sentinel for renewal_policies; future per-entity FK sentinels
|
|
// (ErrIssuerInUse, ErrTeamInUse, ErrOwnerInUse) can wrap this generic
|
|
// one via fmt.Errorf("...: %w", ErrForeignKeyConstraint) so handlers
|
|
// can choose between generic-409 and entity-specific 409 dispatch.
|
|
var ErrForeignKeyConstraint = errors.New("repository: foreign key constraint violation")
|
|
|
|
// IsForeignKeyError detects PostgreSQL FK violation errors from the
|
|
// lib/pq driver via the canonical error-text patterns it emits. The
|
|
// substring matching is intentionally confined to this helper —
|
|
// callers should use this once at the repo layer to wrap into the
|
|
// typed ErrForeignKeyConstraint sentinel, then handlers consume via
|
|
// errors.Is.
|
|
//
|
|
// Patterns recognised:
|
|
// - "violates foreign key constraint" (the standard PG message)
|
|
// - "violates restrict" / "RESTRICT" (DELETE blocked by ON DELETE RESTRICT)
|
|
//
|
|
// Returns false for nil err so callers can defensively chain it.
|
|
func IsForeignKeyError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := err.Error()
|
|
return strings.Contains(msg, "violates foreign key") ||
|
|
strings.Contains(msg, "RESTRICT") ||
|
|
strings.Contains(msg, "violates restrict")
|
|
}
|