mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 13:51:36 +00:00
a53a4b845b
C-001 — CreateCertificate was server-accepted with null owner_id,
team_id, renewal_policy_id because the GUI neither collected the fields
nor enforced them, even though the backend's ManagedCertificate schema
and handler contract treat them as required. Fix the contract at all
four layers:
- web/src/pages/CertificatesPage.tsx: replace owner_id/team_id free-
text inputs with <select> elements fed by getOwners/getTeams/
getPolicies queries; mark all three required; gate the Create
button on owner_id + team_id + renewal_policy_id being set.
- internal/api/handler/certificates.go: ValidateRequired for
owner_id, team_id, renewal_policy_id on CreateCertificate so the
handler returns HTTP 400 with the offending field name before the
service layer is reached.
- internal/mcp/types.go: drop ',omitempty' from
CreateCertificateInput.RenewalPolicyID so the MCP schema reflects
the required contract; Update inputs keep partial-update semantics.
- api/openapi.yaml: 'required: [name, common_name, renewal_policy_id,
issuer_id, owner_id, team_id]' was already present on the Create
schema; clarified DeploymentTarget.agent_id description to note the
FK contract.
C-002 — CreateTargetWizard accepted an empty or bogus agent_id and the
service inserted directly, producing a Postgres 23503 FK-violation that
bubbled out as a generic HTTP 500. The FK itself (migration 000001 line
104: agent_id TEXT NOT NULL REFERENCES agents(id)) is correct; we keep
the schema strict and add validation at three layers:
- internal/service/target.go: introduce
ErrAgentNotFound sentinel and pre-validate agent_id in
TargetService.CreateTarget — empty string returns
'agent_id is required'; a nonexistent id returns the full
'referenced agent does not exist: <id>' error. Both wrap
ErrAgentNotFound via fmt.Errorf %w so callers can use errors.Is.
- internal/api/handler/targets.go: ValidateRequired on agent_id; map
errors.Is(err, service.ErrAgentNotFound) to HTTP 400 instead of
letting it fall through to the generic 500 branch.
- internal/mcp/types.go: drop ',omitempty' from
CreateTargetInput.AgentID to match the required contract.
- web/src/pages/TargetsPage.tsx: replace the free-text Agent ID input
with a <select> populated from getAgents(); include agent in the
canProceedToReview gate so Next is disabled until an agent is
chosen.
Regression coverage (21 new subtests total):
- TestCreateCertificate_MissingRequiredField_Returns400 — 6 subtests,
one per required field, each proves the handler guard fires before
the mock service is called.
- TestCreateTarget_MissingAgentID_Returns400 — handler guard.
- TestCreateTarget_NonexistentAgent_Returns400 — pins the
ErrAgentNotFound -> 400 translation.
- TestTargetService_CreateTarget_MissingAgentID — errors.Is sentinel.
- TestTargetService_CreateTarget_NonexistentAgentID — errors.Is.
- The existing TestTargetService_CreateTarget_Success, along with
TestCreateTarget_{MissingName,MissingType,NameTooLong}_* handler
tests, were updated to seed a real agent or include agent_id in
the request body so the happy paths still run cleanly.
Gates (Phase 4):
- go build/vet/test/race: green
- go test -cover: internal/service 68.7% (gate 55%),
internal/api/handler 78.9% (gate 60%)
- golangci-lint on service+handler+mcp: 0 issues
- govulncheck: no reachable vulns
- tsc --noEmit: clean
- vitest: 223/223 passing
See cowork/certctl-coverage-gap-audit.md entries C-001 and C-002.
282 lines
16 KiB
Go
282 lines
16 KiB
Go
package mcp
|
|
|
|
// Input types for MCP tool arguments.
|
|
// The jsonschema struct tags provide descriptions for LLM tool discovery.
|
|
|
|
// ── Pagination ──────────────────────────────────────────────────────
|
|
|
|
type ListParams struct {
|
|
Page int `json:"page,omitempty" jsonschema:"Page number (default 1)"`
|
|
PerPage int `json:"per_page,omitempty" jsonschema:"Results per page (default 50, max 500)"`
|
|
}
|
|
|
|
// ── Certificates ────────────────────────────────────────────────────
|
|
|
|
type ListCertificatesInput struct {
|
|
ListParams
|
|
Status string `json:"status,omitempty" jsonschema:"Filter by status: Pending, Active, Expiring, Expired, RenewalInProgress, Failed, Revoked, Archived"`
|
|
Environment string `json:"environment,omitempty" jsonschema:"Filter by environment"`
|
|
OwnerID string `json:"owner_id,omitempty" jsonschema:"Filter by owner ID"`
|
|
TeamID string `json:"team_id,omitempty" jsonschema:"Filter by team ID"`
|
|
IssuerID string `json:"issuer_id,omitempty" jsonschema:"Filter by issuer ID"`
|
|
}
|
|
|
|
type GetByIDInput struct {
|
|
ID string `json:"id" jsonschema:"Resource ID (e.g. mc-api-prod, t-platform)"`
|
|
}
|
|
|
|
type CreateCertificateInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Certificate ID (auto-generated if empty)"`
|
|
Name string `json:"name" jsonschema:"Display name"`
|
|
CommonName string `json:"common_name" jsonschema:"Certificate common name (e.g. api.example.com)"`
|
|
SANs []string `json:"sans,omitempty" jsonschema:"Subject Alternative Names"`
|
|
Environment string `json:"environment,omitempty" jsonschema:"Environment (e.g. production, staging)"`
|
|
OwnerID string `json:"owner_id" jsonschema:"Owner ID (required)"`
|
|
TeamID string `json:"team_id" jsonschema:"Team ID (required)"`
|
|
IssuerID string `json:"issuer_id" jsonschema:"Issuer connector ID"`
|
|
TargetIDs []string `json:"target_ids,omitempty" jsonschema:"Deployment target IDs"`
|
|
RenewalPolicyID string `json:"renewal_policy_id" jsonschema:"Renewal policy ID (required)"`
|
|
ProfileID string `json:"certificate_profile_id,omitempty" jsonschema:"Certificate profile ID"`
|
|
Tags map[string]string `json:"tags,omitempty" jsonschema:"Key-value tags"`
|
|
}
|
|
|
|
type UpdateCertificateInput struct {
|
|
ID string `json:"id" jsonschema:"Certificate ID to update"`
|
|
Name string `json:"name,omitempty" jsonschema:"Display name"`
|
|
Environment string `json:"environment,omitempty" jsonschema:"Environment"`
|
|
OwnerID string `json:"owner_id,omitempty" jsonschema:"Owner ID"`
|
|
TeamID string `json:"team_id,omitempty" jsonschema:"Team ID"`
|
|
TargetIDs []string `json:"target_ids,omitempty" jsonschema:"Deployment target IDs"`
|
|
RenewalPolicyID string `json:"renewal_policy_id,omitempty" jsonschema:"Renewal policy ID"`
|
|
ProfileID string `json:"certificate_profile_id,omitempty" jsonschema:"Certificate profile ID"`
|
|
Tags map[string]string `json:"tags,omitempty" jsonschema:"Key-value tags"`
|
|
}
|
|
|
|
type TriggerDeploymentInput struct {
|
|
ID string `json:"id" jsonschema:"Certificate ID"`
|
|
TargetID string `json:"target_id,omitempty" jsonschema:"Optional specific target ID"`
|
|
}
|
|
|
|
type RevokeCertificateInput struct {
|
|
ID string `json:"id" jsonschema:"Certificate ID to revoke"`
|
|
Reason string `json:"reason,omitempty" jsonschema:"RFC 5280 reason: unspecified, keyCompromise, caCompromise, affiliationChanged, superseded, cessationOfOperation, certificateHold, privilegeWithdrawn"`
|
|
}
|
|
|
|
type BulkRevokeCertificatesInput struct {
|
|
Reason string `json:"reason" jsonschema:"RFC 5280 reason: unspecified, keyCompromise, caCompromise, affiliationChanged, superseded, cessationOfOperation, certificateHold, privilegeWithdrawn"`
|
|
ProfileID string `json:"profile_id,omitempty" jsonschema:"Revoke all certs matching this profile ID"`
|
|
OwnerID string `json:"owner_id,omitempty" jsonschema:"Revoke all certs owned by this owner"`
|
|
AgentID string `json:"agent_id,omitempty" jsonschema:"Revoke all certs deployed via this agent"`
|
|
IssuerID string `json:"issuer_id,omitempty" jsonschema:"Revoke all certs issued by this issuer"`
|
|
TeamID string `json:"team_id,omitempty" jsonschema:"Revoke all certs owned by members of this team"`
|
|
CertificateIDs []string `json:"certificate_ids,omitempty" jsonschema:"Explicit list of certificate IDs to revoke"`
|
|
}
|
|
|
|
type ListVersionsInput struct {
|
|
ID string `json:"id" jsonschema:"Certificate ID"`
|
|
ListParams
|
|
}
|
|
|
|
// ── CRL & OCSP ──────────────────────────────────────────────────────
|
|
|
|
type GetDERCRLInput struct {
|
|
IssuerID string `json:"issuer_id" jsonschema:"Issuer ID for DER-encoded CRL"`
|
|
}
|
|
|
|
type OCSPInput struct {
|
|
IssuerID string `json:"issuer_id" jsonschema:"Issuer ID"`
|
|
Serial string `json:"serial" jsonschema:"Hex-encoded certificate serial number"`
|
|
}
|
|
|
|
// ── Issuers ─────────────────────────────────────────────────────────
|
|
|
|
type CreateIssuerInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Issuer ID"`
|
|
Name string `json:"name" jsonschema:"Issuer display name"`
|
|
Type string `json:"type" jsonschema:"Issuer type: ACME, GenericCA, StepCA"`
|
|
Config interface{} `json:"config,omitempty" jsonschema:"Issuer-specific configuration"`
|
|
Enabled bool `json:"enabled,omitempty" jsonschema:"Whether the issuer is enabled"`
|
|
}
|
|
|
|
type UpdateIssuerInput struct {
|
|
ID string `json:"id" jsonschema:"Issuer ID to update"`
|
|
Name string `json:"name,omitempty" jsonschema:"Issuer display name"`
|
|
Type string `json:"type,omitempty" jsonschema:"Issuer type"`
|
|
Config interface{} `json:"config,omitempty" jsonschema:"Issuer-specific configuration"`
|
|
Enabled *bool `json:"enabled,omitempty" jsonschema:"Whether the issuer is enabled"`
|
|
}
|
|
|
|
// ── Targets ─────────────────────────────────────────────────────────
|
|
|
|
type CreateTargetInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Target ID"`
|
|
Name string `json:"name" jsonschema:"Target display name"`
|
|
Type string `json:"type" jsonschema:"Target type: NGINX, Apache, HAProxy, F5, IIS"`
|
|
AgentID string `json:"agent_id" jsonschema:"Agent ID that manages this target (required)"`
|
|
Config interface{} `json:"config,omitempty" jsonschema:"Target-specific configuration"`
|
|
Enabled bool `json:"enabled,omitempty" jsonschema:"Whether the target is enabled"`
|
|
}
|
|
|
|
type UpdateTargetInput struct {
|
|
ID string `json:"id" jsonschema:"Target ID to update"`
|
|
Name string `json:"name,omitempty" jsonschema:"Target display name"`
|
|
Type string `json:"type,omitempty" jsonschema:"Target type"`
|
|
AgentID string `json:"agent_id,omitempty" jsonschema:"Agent ID"`
|
|
Config interface{} `json:"config,omitempty" jsonschema:"Target-specific configuration"`
|
|
Enabled *bool `json:"enabled,omitempty" jsonschema:"Whether the target is enabled"`
|
|
}
|
|
|
|
// ── Agents ──────────────────────────────────────────────────────────
|
|
|
|
type RegisterAgentInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Agent ID"`
|
|
Name string `json:"name" jsonschema:"Agent display name"`
|
|
Hostname string `json:"hostname" jsonschema:"Agent hostname"`
|
|
}
|
|
|
|
type AgentCSRInput struct {
|
|
AgentID string `json:"agent_id" jsonschema:"Agent ID"`
|
|
CSRPEM string `json:"csr_pem" jsonschema:"PEM-encoded certificate signing request"`
|
|
CertificateID string `json:"certificate_id,omitempty" jsonschema:"Certificate ID for the CSR"`
|
|
}
|
|
|
|
type AgentPickupInput struct {
|
|
AgentID string `json:"agent_id" jsonschema:"Agent ID"`
|
|
CertID string `json:"cert_id" jsonschema:"Certificate ID to pick up"`
|
|
}
|
|
|
|
type AgentJobStatusInput struct {
|
|
AgentID string `json:"agent_id" jsonschema:"Agent ID"`
|
|
JobID string `json:"job_id" jsonschema:"Job ID"`
|
|
Status string `json:"status" jsonschema:"Job status to report"`
|
|
Error string `json:"error,omitempty" jsonschema:"Error message if job failed"`
|
|
}
|
|
|
|
// ── Jobs ────────────────────────────────────────────────────────────
|
|
|
|
type ListJobsInput struct {
|
|
ListParams
|
|
Status string `json:"status,omitempty" jsonschema:"Filter by status: Pending, AwaitingCSR, AwaitingApproval, Running, Completed, Failed, Cancelled"`
|
|
Type string `json:"type,omitempty" jsonschema:"Filter by type: Issuance, Renewal, Deployment, Validation"`
|
|
}
|
|
|
|
type RejectJobInput struct {
|
|
ID string `json:"id" jsonschema:"Job ID to reject"`
|
|
Reason string `json:"reason,omitempty" jsonschema:"Reason for rejection"`
|
|
}
|
|
|
|
// ── Policies ────────────────────────────────────────────────────────
|
|
|
|
type CreatePolicyInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Policy ID"`
|
|
Name string `json:"name" jsonschema:"Policy display name"`
|
|
Type string `json:"type" jsonschema:"Policy type: AllowedIssuers, AllowedDomains, RequiredMetadata, AllowedEnvironments, RenewalLeadTime"`
|
|
Config interface{} `json:"config,omitempty" jsonschema:"Policy-specific configuration"`
|
|
Enabled bool `json:"enabled,omitempty" jsonschema:"Whether the policy is enabled"`
|
|
Severity string `json:"severity,omitempty" jsonschema:"Violation severity: Warning, Error, or Critical (default: Warning)"`
|
|
}
|
|
|
|
type UpdatePolicyInput struct {
|
|
ID string `json:"id" jsonschema:"Policy ID to update"`
|
|
Name string `json:"name,omitempty" jsonschema:"Policy display name"`
|
|
Type string `json:"type,omitempty" jsonschema:"Policy type"`
|
|
Config interface{} `json:"config,omitempty" jsonschema:"Policy-specific configuration"`
|
|
Enabled *bool `json:"enabled,omitempty" jsonschema:"Whether the policy is enabled"`
|
|
Severity string `json:"severity,omitempty" jsonschema:"Violation severity: Warning, Error, or Critical"`
|
|
}
|
|
|
|
type ListViolationsInput struct {
|
|
ID string `json:"id" jsonschema:"Policy ID"`
|
|
ListParams
|
|
}
|
|
|
|
// ── Profiles ────────────────────────────────────────────────────────
|
|
|
|
type CreateProfileInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Profile ID"`
|
|
Name string `json:"name" jsonschema:"Profile display name"`
|
|
Description string `json:"description,omitempty" jsonschema:"Profile description"`
|
|
AllowedKeyAlgorithms interface{} `json:"allowed_key_algorithms,omitempty" jsonschema:"Allowed key algorithms and minimum sizes"`
|
|
MaxTTLSeconds int `json:"max_ttl_seconds,omitempty" jsonschema:"Maximum certificate TTL in seconds"`
|
|
AllowedEKUs []string `json:"allowed_ekus,omitempty" jsonschema:"Allowed Extended Key Usages"`
|
|
RequiredSANPatterns []string `json:"required_san_patterns,omitempty" jsonschema:"Required SAN patterns"`
|
|
AllowShortLived bool `json:"allow_short_lived,omitempty" jsonschema:"Allow short-lived certificates (TTL < 1 hour)"`
|
|
Enabled bool `json:"enabled,omitempty" jsonschema:"Whether the profile is enabled"`
|
|
}
|
|
|
|
type UpdateProfileInput struct {
|
|
ID string `json:"id" jsonschema:"Profile ID to update"`
|
|
Name string `json:"name,omitempty" jsonschema:"Profile display name"`
|
|
Description string `json:"description,omitempty" jsonschema:"Profile description"`
|
|
AllowedKeyAlgorithms interface{} `json:"allowed_key_algorithms,omitempty" jsonschema:"Allowed key algorithms and minimum sizes"`
|
|
MaxTTLSeconds *int `json:"max_ttl_seconds,omitempty" jsonschema:"Maximum certificate TTL in seconds"`
|
|
AllowedEKUs []string `json:"allowed_ekus,omitempty" jsonschema:"Allowed Extended Key Usages"`
|
|
RequiredSANPatterns []string `json:"required_san_patterns,omitempty" jsonschema:"Required SAN patterns"`
|
|
AllowShortLived *bool `json:"allow_short_lived,omitempty" jsonschema:"Allow short-lived certificates"`
|
|
Enabled *bool `json:"enabled,omitempty" jsonschema:"Whether the profile is enabled"`
|
|
}
|
|
|
|
// ── Teams ───────────────────────────────────────────────────────────
|
|
|
|
type CreateTeamInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Team ID"`
|
|
Name string `json:"name" jsonschema:"Team name"`
|
|
Description string `json:"description,omitempty" jsonschema:"Team description"`
|
|
}
|
|
|
|
type UpdateTeamInput struct {
|
|
ID string `json:"id" jsonschema:"Team ID to update"`
|
|
Name string `json:"name,omitempty" jsonschema:"Team name"`
|
|
Description string `json:"description,omitempty" jsonschema:"Team description"`
|
|
}
|
|
|
|
// ── Owners ──────────────────────────────────────────────────────────
|
|
|
|
type CreateOwnerInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Owner ID"`
|
|
Name string `json:"name" jsonschema:"Owner display name"`
|
|
Email string `json:"email,omitempty" jsonschema:"Owner email for notifications"`
|
|
TeamID string `json:"team_id,omitempty" jsonschema:"Team ID the owner belongs to"`
|
|
}
|
|
|
|
type UpdateOwnerInput struct {
|
|
ID string `json:"id" jsonschema:"Owner ID to update"`
|
|
Name string `json:"name,omitempty" jsonschema:"Owner display name"`
|
|
Email string `json:"email,omitempty" jsonschema:"Owner email"`
|
|
TeamID string `json:"team_id,omitempty" jsonschema:"Team ID"`
|
|
}
|
|
|
|
// ── Agent Groups ────────────────────────────────────────────────────
|
|
|
|
type CreateAgentGroupInput struct {
|
|
ID string `json:"id,omitempty" jsonschema:"Agent group ID"`
|
|
Name string `json:"name" jsonschema:"Group display name"`
|
|
Description string `json:"description,omitempty" jsonschema:"Group description"`
|
|
MatchOS string `json:"match_os,omitempty" jsonschema:"Match agents by OS (e.g. linux, darwin, windows)"`
|
|
MatchArchitecture string `json:"match_architecture,omitempty" jsonschema:"Match agents by architecture (e.g. amd64, arm64)"`
|
|
MatchIPCIDR string `json:"match_ip_cidr,omitempty" jsonschema:"Match agents by IP CIDR range"`
|
|
MatchVersion string `json:"match_version,omitempty" jsonschema:"Match agents by version"`
|
|
Enabled bool `json:"enabled,omitempty" jsonschema:"Whether the group is enabled"`
|
|
}
|
|
|
|
type UpdateAgentGroupInput struct {
|
|
ID string `json:"id" jsonschema:"Agent group ID to update"`
|
|
Name string `json:"name,omitempty" jsonschema:"Group display name"`
|
|
Description string `json:"description,omitempty" jsonschema:"Group description"`
|
|
MatchOS string `json:"match_os,omitempty" jsonschema:"Match agents by OS"`
|
|
MatchArchitecture string `json:"match_architecture,omitempty" jsonschema:"Match agents by architecture"`
|
|
MatchIPCIDR string `json:"match_ip_cidr,omitempty" jsonschema:"Match agents by IP CIDR range"`
|
|
MatchVersion string `json:"match_version,omitempty" jsonschema:"Match agents by version"`
|
|
Enabled *bool `json:"enabled,omitempty" jsonschema:"Whether the group is enabled"`
|
|
}
|
|
|
|
// ── Stats ───────────────────────────────────────────────────────────
|
|
|
|
type TimelineInput struct {
|
|
Days int `json:"days,omitempty" jsonschema:"Number of days to look back (default 30, max 365)"`
|
|
}
|
|
|
|
// ── Empty ───────────────────────────────────────────────────────────
|
|
|
|
type EmptyInput struct{}
|