mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 13:51:36 +00:00
19706e56b3
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.
67 lines
3.0 KiB
SQL
67 lines
3.0 KiB
SQL
-- 000027_approval_workflow.up.sql
|
|
-- Rank 7 of the 2026-05-03 deep-research deliverable
|
|
-- (cowork/deep-research-results-2026-05-03.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';
|