mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:31:29 +00:00
api, handler: 4 admin-gated CA hierarchy endpoints + OpenAPI (Rank 8 commit 4)
Rank 8 commit 4 of 5. The API + RBAC layer that operators drive
the new hierarchy management surface from.
Endpoints (all admin-gated via middleware.IsAdmin; non-admin Bearer
callers get 403):
POST /api/v1/issuers/{id}/intermediates
Discriminator on body shape:
empty parent_ca_id + root_cert_pem + key_driver_id
→ CreateRoot (registers operator-supplied root CA).
parent_ca_id non-empty
→ CreateChild (signs new sub-CA cert under parent).
Service-layer error → HTTP code mapping:
ErrCANotSelfSigned → 400
ErrCAKeyMismatch → 400
ErrPathLenExceeded → 400
ErrNameConstraintExceeded → 400
ErrInvalidCertPEM → 400
ErrParentCANotActive → 409
ErrIntermediateCANotFound → 404
(other) → 500
GET /api/v1/issuers/{id}/intermediates
Returns flat list ordered by created_at; caller renders the
tree from each row's parent_ca_id (nil = root).
GET /api/v1/intermediates/{id}
Single-row detail.
POST /api/v1/intermediates/{id}/retire
Two-phase: confirm=false → active→retiring; confirm=true →
retiring→retired with active-children check (drain-first
semantics; ErrCAStillHasActiveChildren → 409).
Files changed:
internal/api/handler/intermediate_ca.go — 4 handlers
+ handler-defined
service interface
(dependency
inversion).
internal/api/handler/intermediate_ca_test.go — 8 test variants
(M-008 admin-
gate triplet
complete).
internal/api/handler/m008_admin_gate_test.go — register the
new admin-gated
handler in
AdminGatedHandlers
so the M-008
coherence
scanner stays
green.
internal/api/router/router.go — 4 r.Register
calls + new
IntermediateCAs
field on
HandlerRegistry.
cmd/server/main.go — wire the
postgres repo +
service +
handler. Reuses
the same
signer.FileDriver
instance the
OCSP responder
bootstrap path
feeds.
api/openapi.yaml — 4 new
operationIds,
full body
schema + status-
code dispatch.
Tests (8 in this commit):
TestIntermediateCA_Handler_NonAdmin_Returns403 (admin gate
— table-driven across all 4 endpoints)
TestIntermediateCA_Handler_AdminExplicitFalse_Returns403
(defensive: AdminKey present but false ≠ AdminKey absent)
TestIntermediateCA_Handler_AdminPermitted_ForwardsActor
(admin actor forwarded to service for audit attribution)
TestIntermediateCA_HandlerCreate_RootDispatch
(body discriminator: empty parent_ca_id → CreateRoot)
TestIntermediateCA_HandlerCreate_ChildDispatch
(body discriminator: parent_ca_id present → CreateChild)
TestIntermediateCA_HandlerCreate_BadRequestOnMissingRootBundle
(validation: no parent + no root bundle → 400)
TestIntermediateCA_HandlerCreate_ServiceErrorMappings
(table-driven: 7 service errors → expected HTTP codes)
TestIntermediateCA_HandlerRetire_TwoPhaseConfirm
(confirm=false then confirm=true forwarded correctly)
TestIntermediateCA_HandlerRetire_StillHasActiveChildren_Returns409
(drain-first contract — 409 not 500)
Verified locally:
gofmt: clean.
go vet ./...: exit 0.
go test -short -count=1 ./internal/api/handler/...: ok 4.498s.
bash scripts/ci-guards/openapi-handler-parity.sh: clean
(router routes: 182, openapi operations: 148; the +4 new routes
have +4 new operationIds — parity preserved).
bash scripts/ci-guards/* (all 24 guards): clean.
Out of scope of THIS commit (commit 5):
- web/src/pages/IssuerHierarchyPage.tsx (recursive tree render).
- docs/intermediate-ca-hierarchy.md sysadmin runbook (FedRAMP /
financial-services / internal-PKI patterns).
- docs/connectors.md hierarchy_mode row.
- WORKSPACE-ROADMAP entries (HSM-backed roots, automated
rotation, CRL chaining, NameConstraints templates, D3
dendrogram).
Reference: cowork/rank-8-intermediate-ca-hierarchy-prompt.md, commit 4.
This commit is contained in:
@@ -2910,6 +2910,151 @@ paths:
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/api/v1/issuers/{id}/intermediates:
|
||||
post:
|
||||
tags: [IntermediateCAs]
|
||||
summary: Create a root or child intermediate CA under the issuer
|
||||
description: |
|
||||
Admin-gated. Discriminator on body shape: when parent_ca_id is
|
||||
empty AND root_cert_pem + key_driver_id are present, the
|
||||
endpoint registers an operator-supplied root CA. Otherwise it
|
||||
signs a child sub-CA cert under the named parent (RFC 5280
|
||||
§4.2.1.9 path-length tightening + §4.2.1.10 NameConstraints
|
||||
subset semantics enforced at the service layer).
|
||||
operationId: createIntermediateCA
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/resourceId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string }
|
||||
parent_ca_id:
|
||||
type: string
|
||||
description: Empty for root registration; non-empty for child signing
|
||||
root_cert_pem:
|
||||
type: string
|
||||
description: Operator-supplied root cert PEM (root path only)
|
||||
key_driver_id:
|
||||
type: string
|
||||
description: signer.Driver reference for the root key (root path only)
|
||||
subject:
|
||||
type: object
|
||||
description: Distinguished name for child CA (child path only)
|
||||
algorithm:
|
||||
type: string
|
||||
description: Signing algorithm for child key (default ECDSA-P256)
|
||||
ttl_days:
|
||||
type: integer
|
||||
path_len_constraint:
|
||||
type: integer
|
||||
nullable: true
|
||||
name_constraints:
|
||||
type: array
|
||||
items: { type: object }
|
||||
ocsp_responder_url:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
responses:
|
||||
"201":
|
||||
description: IntermediateCA row created
|
||||
"400":
|
||||
description: Validation failed (RFC 5280 violations, malformed cert PEM, missing root bundle)
|
||||
"401":
|
||||
description: Authentication required
|
||||
"403":
|
||||
description: Admin role required
|
||||
"409":
|
||||
description: Parent CA not in active state
|
||||
"404":
|
||||
description: Parent CA not found
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
get:
|
||||
tags: [IntermediateCAs]
|
||||
summary: List the CA hierarchy for an issuer
|
||||
description: |
|
||||
Admin-gated. Returns the flat list of every IntermediateCA row
|
||||
for the issuer, ordered by created_at. The caller renders the
|
||||
tree from each row's parent_ca_id (nil = root).
|
||||
operationId: listIntermediateCAs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/resourceId"
|
||||
responses:
|
||||
"200":
|
||||
description: Flat list of CA rows
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items: { type: object }
|
||||
"401":
|
||||
description: Authentication required
|
||||
"403":
|
||||
description: Admin role required
|
||||
|
||||
/api/v1/intermediates/{id}:
|
||||
get:
|
||||
tags: [IntermediateCAs]
|
||||
summary: Get a single intermediate CA by ID
|
||||
operationId: getIntermediateCA
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/resourceId"
|
||||
responses:
|
||||
"200":
|
||||
description: IntermediateCA row
|
||||
"401":
|
||||
description: Authentication required
|
||||
"403":
|
||||
description: Admin role required
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
||||
/api/v1/intermediates/{id}/retire:
|
||||
post:
|
||||
tags: [IntermediateCAs]
|
||||
summary: Retire an intermediate CA (two-phase drain)
|
||||
description: |
|
||||
Admin-gated. Two-phase: first call (confirm=false) transitions
|
||||
active to retiring (the CA stops issuing new children but
|
||||
existing children continue). Second call (confirm=true)
|
||||
transitions retiring to retired (terminal). Refuses the
|
||||
terminal transition if the CA still has active children —
|
||||
drain-first semantics.
|
||||
operationId: retireIntermediateCA
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/resourceId"
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
note: { type: string }
|
||||
confirm: { type: boolean, default: false }
|
||||
responses:
|
||||
"200":
|
||||
description: Retire transition recorded
|
||||
"401":
|
||||
description: Authentication required
|
||||
"403":
|
||||
description: Admin role required
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"409":
|
||||
description: CA still has active children; drain them first
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/api/v1/notifications:
|
||||
get:
|
||||
tags: [Notifications]
|
||||
|
||||
Reference in New Issue
Block a user