Commit Graph

3 Commits

Author SHA1 Message Date
shankar0123 21aeed4f4e legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):

addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:

  // Copyright 2026 certctl LLC. All rights reserved.
  // SPDX-License-Identifier: BUSL-1.1

Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).

Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.

Generated via:
  addlicense -c "certctl LLC" -y 2026 \
    -f cowork/legal/copyright-header.tpl \
    -ignore '**/testdata/**' -ignore '**/*_test.go' \
    cmd/ internal/

Verification:
  find cmd internal -name '*.go' -not -name '*_test.go' \
    -not -path '*/testdata/*' \
    -exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l

  Returns: 0

gofmt clean. Header additions are comments only, no compile impact.

Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
2026-05-13 21:23:35 +00:00
shankar0123 2025275b43 domain, migrations: ApprovalRequest type + issuance_approval_requests + RequiresApproval
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.
2026-05-04 00:55:17 +00:00
shankar0123 0e29c416b1 refactor(handler,repo): replace strings.Contains error dispatch with typed sentinels (S-2)
Closes one 2026-04-24 audit finding (P2):

  - cat-s6-efc7f6f6bd50: 30 strings.Contains(err.Error(), ...) sites
    in internal/api/handler/ — brittle to repository-layer message
    changes, untyped against the actual failure mode.

Approach (Option B from prompt design notes):
  - New typed sentinels in internal/repository/errors.go:
      ErrNotFound, ErrForeignKeyConstraint
      IsForeignKeyError(err) helper (the only place substring
      matching at the lib/pq boundary is allowed; isolates the
      DB-driver string knowledge to one function).
  - New typed sentinel in internal/domain/errors.go:
      ErrValidation (reserved for future per-entity validation
      wrappers; not yet used by all handlers).
  - 49 sites in internal/repository/postgres/*.go updated to wrap
    sql.ErrNoRows-derived errors via fmt.Errorf("...: %w",
    repository.ErrNotFound).
  - 18 not-found handler sites + 2 FK-constraint handler sites
    refactored to errors.Is(err, repository.ErrNotFound) /
    repository.IsForeignKeyError(err).
  - 23 inline `fmt.Errorf("X not found")` test fixtures across
    handler tests rewrapped to wrap repository.ErrNotFound.
  - test_utils.go::ErrMockNotFound rewrapped to wrap
    repository.ErrNotFound; renewal_policy.go closure docblock
    updated to reflect the new convention.
  - integration test mockJobRepository.Get wraps repository.ErrNotFound.

CI regression guardrail:
- .github/workflows/ci.yml::"Forbidden strings.Contains(err.Error())
  regression guard (S-2)" greps for the three patterns ("not found",
  "violates foreign key", "RESTRICT") under internal/api/handler/
  and fails the build on regression.

Verification:
- go build ./... — clean
- go vet ./... — clean
- go test ./... -short -count=1 — all packages pass (handler +
  repository + service + integration)
- golangci-lint v2.11.4 run ./... — 0 issues
- S-2 guardrail dry-run on post-fix tree → empty (good)
- All sibling guardrails (S-1, G-3, D-1+D-2, B-1, L-1, H-1, C-1, F-1, P-1) pass

Audit findings closed:
- cat-s6-efc7f6f6bd50 (P2)

Deferred follow-ups:
- 6 domain-specific substring patterns still inline in handlers
  ("cannot approve", "cannot reject", "cannot be parsed",
  "no certificates found", "challenge password", "invalid"/
  "required" validation chains in profiles + agent_groups). Each
  needs its own typed sentinel, scoped per service. Documented
  by the S-2 CI guardrail's allowlist for closure-comments only.
- Per-entity not-found sentinels (Option A — ErrCertificateNotFound,
  ErrAgentNotFound, etc.) deferred. Generic ErrNotFound covers the
  current dispatch needs; per-entity precision would let handlers
  return entity-aware error bodies without a domain.Type field,
  but not blocking.
2026-04-25 17:54:14 +00:00