Initial import — snapshot from admin host /srv/gosec/gsc-ops-api
This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.
The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.
Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.
Future changes go through PRs against this repo — no more bin-only
deploys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
161
internal/service/certificate.go
Normal file
161
internal/service/certificate.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/gosec/gsc-ops-api/internal/client"
|
||||
"github.com/gosec/gsc-ops-api/pkg/types"
|
||||
)
|
||||
|
||||
// CertificateService handles EJBCA certificate operations
|
||||
type CertificateService struct {
|
||||
client *client.EJBCAClient
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// NewCertificateService creates a new certificate service
|
||||
func NewCertificateService(ejbcaClient *client.EJBCAClient, logger zerolog.Logger) *CertificateService {
|
||||
return &CertificateService{
|
||||
client: ejbcaClient,
|
||||
logger: logger.With().Str("service", "certificate").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// ListCertificates searches for certificates
|
||||
func (s *CertificateService) ListCertificates(search string, limit int) ([]types.Certificate, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
criteria := []client.CertSearchCriterion{}
|
||||
if search != "" {
|
||||
criteria = append(criteria, client.CertSearchCriterion{
|
||||
Property: "QUERY",
|
||||
Value: search,
|
||||
Operation: "LIKE",
|
||||
})
|
||||
}
|
||||
|
||||
certs, err := s.client.SearchCertificates(&client.CertSearchRequest{
|
||||
MaxResults: limit,
|
||||
Criteria: criteria,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]types.Certificate, 0, len(certs))
|
||||
for _, c := range certs {
|
||||
cert := types.Certificate{
|
||||
SerialNumber: c.SerialNumber,
|
||||
SubjectDN: c.SubjectDN,
|
||||
IssuerDN: c.IssuerDN,
|
||||
Status: c.Status,
|
||||
CAName: c.CAName,
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, c.NotBefore); err == nil {
|
||||
cert.NotBefore = t
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, c.NotAfter); err == nil {
|
||||
cert.NotAfter = t
|
||||
}
|
||||
result = append(result, cert)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCertificate gets a certificate by serial number
|
||||
func (s *CertificateService) GetCertificate(serialNumber, issuerDN string) (*types.Certificate, error) {
|
||||
c, err := s.client.GetCertificate(issuerDN, serialNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert := &types.Certificate{
|
||||
SerialNumber: c.SerialNumber,
|
||||
SubjectDN: c.SubjectDN,
|
||||
IssuerDN: c.IssuerDN,
|
||||
Status: c.Status,
|
||||
CAName: c.CAName,
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, c.NotBefore); err == nil {
|
||||
cert.NotBefore = t
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, c.NotAfter); err == nil {
|
||||
cert.NotAfter = t
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// RequestCertificate requests a new certificate from EJBCA
|
||||
func (s *CertificateService) RequestCertificate(req *types.CertRequest) (*types.Certificate, error) {
|
||||
san := buildSANString(req.SubjectDN, req.SANs)
|
||||
|
||||
enrollReq := &client.CertEnrollRequest{
|
||||
CertificateProfileName: req.CertProfileName,
|
||||
EndEntityProfileName: req.EndEntityName,
|
||||
CAName: req.CAName,
|
||||
Username: req.EndEntityName,
|
||||
Password: "internal",
|
||||
IncludeChain: true,
|
||||
SubjectAltName: san,
|
||||
}
|
||||
|
||||
c, err := s.client.EnrollCertificate(enrollReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert := &types.Certificate{
|
||||
SerialNumber: c.SerialNumber,
|
||||
SubjectDN: c.SubjectDN,
|
||||
IssuerDN: c.IssuerDN,
|
||||
Status: c.Status,
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// buildSANString builds an EJBCA-format SAN string (e.g. "dNSName=foo,dNSName=bar").
|
||||
// If sans is empty, extracts CN from subjectDN as a fallback DNS SAN.
|
||||
func buildSANString(subjectDN string, sans []string) string {
|
||||
if len(sans) > 0 {
|
||||
parts := make([]string, 0, len(sans))
|
||||
for _, s := range sans {
|
||||
if s != "" {
|
||||
parts = append(parts, fmt.Sprintf("dNSName=%s", s))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
// Fallback: extract CN from SubjectDN and use as DNS SAN
|
||||
cn := extractCN(subjectDN)
|
||||
if cn != "" {
|
||||
return fmt.Sprintf("dNSName=%s", cn)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractCN extracts the CN value from a SubjectDN string like "CN=foo.bar,O=Org"
|
||||
func extractCN(subjectDN string) string {
|
||||
for _, part := range strings.Split(subjectDN, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(part, "CN=") {
|
||||
return strings.TrimPrefix(part, "CN=")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RevokeCertificate revokes a certificate
|
||||
func (s *CertificateService) RevokeCertificate(serialNumber string, req *types.CertRevoke) error {
|
||||
reason := req.Reason
|
||||
if reason == "" {
|
||||
reason = "UNSPECIFIED"
|
||||
}
|
||||
return s.client.RevokeCertificate(req.IssuerDN, serialNumber, reason)
|
||||
}
|
||||
Reference in New Issue
Block a user