feat: M15a — certificate revocation API, CRL endpoint, and revocation notifications

Implements core revocation infrastructure: POST /api/v1/certificates/{id}/revoke
with all 8 RFC 5280 reason codes, JSON-formatted CRL at GET /api/v1/crl, webhook
and email revocation notifications, best-effort issuer notification, and immutable
revocation audit trail. Includes 48 new tests across service, handler, integration,
and domain layers (600+ total). Fixes 3 pre-existing test bugs (team_test error
matching, agent_group delete status code, team handler per_page validation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-22 10:59:18 -04:00
parent d881403d11
commit 5d98e373e3
27 changed files with 1710 additions and 37 deletions
+72
View File
@@ -103,6 +103,14 @@ func (m *mockCertRepo) GetExpiringCertificates(ctx context.Context, before time.
return expiring, nil
}
func (m *mockCertRepo) GetLatestVersion(ctx context.Context, certID string) (*domain.CertificateVersion, error) {
versions := m.Versions[certID]
if len(versions) == 0 {
return nil, errNotFound
}
return versions[len(versions)-1], nil
}
func (m *mockCertRepo) AddCert(cert *domain.ManagedCertificate) {
m.Certs[cert.ID] = cert
}
@@ -605,6 +613,13 @@ func (m *mockIssuerConnector) RenewCertificate(ctx context.Context, commonName s
return m.IssueCertificate(ctx, commonName, sans, csrPEM)
}
func (m *mockIssuerConnector) RevokeCertificate(ctx context.Context, serial string, reason string) error {
if m.Err != nil {
return m.Err
}
return nil
}
// Constructor functions for mocks
func newMockCertificateRepository() *mockCertRepo {
@@ -725,6 +740,63 @@ func (m *mockIssuerRepository) AddIssuer(issuer *domain.Issuer) {
m.issuers[issuer.ID] = issuer
}
// mockRevocationRepo is a test implementation of RevocationRepository
type mockRevocationRepo struct {
Revocations []*domain.CertificateRevocation
CreateErr error
ListErr error
}
func (m *mockRevocationRepo) Create(ctx context.Context, revocation *domain.CertificateRevocation) error {
if m.CreateErr != nil {
return m.CreateErr
}
m.Revocations = append(m.Revocations, revocation)
return nil
}
func (m *mockRevocationRepo) GetBySerial(ctx context.Context, serial string) (*domain.CertificateRevocation, error) {
for _, r := range m.Revocations {
if r.SerialNumber == serial {
return r, nil
}
}
return nil, errNotFound
}
func (m *mockRevocationRepo) ListAll(ctx context.Context) ([]*domain.CertificateRevocation, error) {
if m.ListErr != nil {
return nil, m.ListErr
}
return m.Revocations, nil
}
func (m *mockRevocationRepo) ListByCertificate(ctx context.Context, certID string) ([]*domain.CertificateRevocation, error) {
var result []*domain.CertificateRevocation
for _, r := range m.Revocations {
if r.CertificateID == certID {
result = append(result, r)
}
}
return result, nil
}
func (m *mockRevocationRepo) MarkIssuerNotified(ctx context.Context, id string) error {
for _, r := range m.Revocations {
if r.ID == id {
r.IssuerNotified = true
return nil
}
}
return errNotFound
}
func newMockRevocationRepository() *mockRevocationRepo {
return &mockRevocationRepo{
Revocations: make([]*domain.CertificateRevocation, 0),
}
}
// mockNotifier is a simple notifier for testing
type mockNotifier struct {
messages []*mockNotifierMessage