mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:21:30 +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.
67 lines
3.0 KiB
SQL
67 lines
3.0 KiB
SQL
-- 000027_approval_workflow.up.sql
|
|
-- Rank 7 of the 2026-05-03 Infisical deep-research deliverable
|
|
-- (cowork/infisical-deep-research-results.md Part 5). Two-person
|
|
-- integrity / four-eyes principle for compliance-tier certificate
|
|
-- issuance. CertificateProfile.RequiresApproval gates the renewal-
|
|
-- loop entry; issuance_approval_requests captures the per-job
|
|
-- decision with full audit trail.
|
|
--
|
|
-- All operations use IF NOT EXISTS / IF EXISTS so the migration is
|
|
-- idempotent — safe to re-run on every certctl-server boot per the
|
|
-- "Idempotent migrations" architecture decision in CLAUDE.md.
|
|
--
|
|
-- Existing scaffolding REUSED (not redefined here):
|
|
-- - JobStatusAwaitingApproval enum value (internal/domain/job.go).
|
|
-- - JobRepository.ListTimedOutAwaitingJobs (postgres reaper query).
|
|
-- - Config.Scheduler.AwaitingApprovalTimeout (env-mapped, default
|
|
-- 168h via CERTCTL_JOB_AWAITING_APPROVAL_TIMEOUT).
|
|
--
|
|
-- The lifecycle states are pinned at the schema level via a CHECK
|
|
-- constraint matching internal/domain/approval.go::ApprovalState.
|
|
|
|
ALTER TABLE certificate_profiles
|
|
ADD COLUMN IF NOT EXISTS requires_approval BOOLEAN NOT NULL DEFAULT false;
|
|
|
|
CREATE TABLE IF NOT EXISTS issuance_approval_requests (
|
|
id TEXT PRIMARY KEY,
|
|
certificate_id TEXT NOT NULL REFERENCES managed_certificates(id) ON DELETE CASCADE,
|
|
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
profile_id TEXT NOT NULL REFERENCES certificate_profiles(id) ON DELETE RESTRICT,
|
|
requested_by TEXT NOT NULL,
|
|
state VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
decided_by TEXT,
|
|
decided_at TIMESTAMPTZ,
|
|
decision_note TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT approval_state_check CHECK (
|
|
state IN ('pending', 'approved', 'rejected', 'expired')
|
|
),
|
|
CONSTRAINT approval_decision_consistency CHECK (
|
|
(state = 'pending' AND decided_by IS NULL AND decided_at IS NULL)
|
|
OR (state IN ('approved', 'rejected', 'expired') AND decided_at IS NOT NULL)
|
|
)
|
|
);
|
|
|
|
-- Partial-unique index: at most one PENDING approval request per job
|
|
-- ID. Creates / re-creates idempotently. Terminal-state rows
|
|
-- (approved / rejected / expired) are not constrained — operators
|
|
-- can audit-trail multiple decisions over a job's lifetime, though
|
|
-- in practice each job creates exactly one ApprovalRequest at
|
|
-- AwaitingApproval entry and never recreates it.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_approval_pending_per_job
|
|
ON issuance_approval_requests(job_id)
|
|
WHERE state = 'pending';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_approval_state
|
|
ON issuance_approval_requests(state);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_approval_certificate
|
|
ON issuance_approval_requests(certificate_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_approval_pending_age
|
|
ON issuance_approval_requests(created_at)
|
|
WHERE state = 'pending';
|