Add SSH CA engine with host/user cert signing, profiles, and KRL
Implement the complete SSH CA engine following the CA engine pattern: - Engine core (initialize, unseal, seal, HandleRequest) with ed25519/ecdsa key support - Host and user certificate signing with TTL enforcement and policy checks - Signing profiles with extensions, critical options, and principal restrictions - Certificate CRUD (list, get, revoke, delete) with proper auth enforcement - OpenSSH KRL generation rebuilt on revoke/delete operations - gRPC service (SSHCAService) with all RPCs and interceptor registration - REST routes for public endpoints (CA pubkey, KRL) and authenticated operations - Comprehensive test suite (15 tests covering lifecycle, signing, profiles, KRL, auth) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/crypto"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine/ca"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/engine/sshca"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
||||
"git.wntrmute.dev/kyle/metacrypt/internal/seal"
|
||||
)
|
||||
@@ -40,6 +41,23 @@ func (s *Server) registerRoutes(r chi.Router) {
|
||||
r.Post("/v1/ca/{mount}/cert/{serial}/revoke", s.requireAdmin(s.handleRevokeCert))
|
||||
r.Delete("/v1/ca/{mount}/cert/{serial}", s.requireAdmin(s.handleDeleteCert))
|
||||
|
||||
// Public SSH CA routes (no auth required, but must be unsealed).
|
||||
r.Get("/v1/sshca/{mount}/ca", s.requireUnseal(s.handleSSHCAPubkey))
|
||||
r.Get("/v1/sshca/{mount}/krl", s.requireUnseal(s.handleSSHCAKRL))
|
||||
|
||||
// SSH CA auth-required routes.
|
||||
r.Post("/v1/sshca/{mount}/sign-host", s.requireAuth(s.handleSSHCASignHost))
|
||||
r.Post("/v1/sshca/{mount}/sign-user", s.requireAuth(s.handleSSHCASignUser))
|
||||
r.Post("/v1/sshca/{mount}/profiles", s.requireAdmin(s.handleSSHCACreateProfile))
|
||||
r.Put("/v1/sshca/{mount}/profiles/{name}", s.requireAdmin(s.handleSSHCAUpdateProfile))
|
||||
r.Get("/v1/sshca/{mount}/profiles/{name}", s.requireAuth(s.handleSSHCAGetProfile))
|
||||
r.Get("/v1/sshca/{mount}/profiles", s.requireAuth(s.handleSSHCAListProfiles))
|
||||
r.Delete("/v1/sshca/{mount}/profiles/{name}", s.requireAdmin(s.handleSSHCADeleteProfile))
|
||||
r.Get("/v1/sshca/{mount}/certs/{serial}", s.requireAuth(s.handleSSHCAGetCert))
|
||||
r.Get("/v1/sshca/{mount}/certs", s.requireAuth(s.handleSSHCAListCerts))
|
||||
r.Post("/v1/sshca/{mount}/certs/{serial}/revoke", s.requireAdmin(s.handleSSHCARevokeCert))
|
||||
r.Delete("/v1/sshca/{mount}/certs/{serial}", s.requireAdmin(s.handleSSHCADeleteCert))
|
||||
|
||||
// Public PKI routes (no auth required, but must be unsealed).
|
||||
r.Get("/v1/pki/{mount}/ca", s.requireUnseal(s.handlePKIRoot))
|
||||
r.Get("/v1/pki/{mount}/ca/chain", s.requireUnseal(s.handlePKIChain))
|
||||
@@ -260,28 +278,30 @@ func (s *Server) handleEngineUnmount(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// adminOnlyOperations lists engine operations that require admin role.
|
||||
// This enforces the same gates as the typed REST routes, ensuring the
|
||||
// generic endpoint cannot bypass admin requirements.
|
||||
// Keys are "engineType:operation" to avoid name collisions across engines
|
||||
// (e.g. transit "rotate-key" is admin-only but user "rotate-key" is user-self).
|
||||
var adminOnlyOperations = map[string]bool{
|
||||
// CA engine.
|
||||
"import-root": true,
|
||||
"create-issuer": true,
|
||||
"delete-issuer": true,
|
||||
"revoke-cert": true,
|
||||
"delete-cert": true,
|
||||
"ca:import-root": true,
|
||||
"ca:create-issuer": true,
|
||||
"ca:delete-issuer": true,
|
||||
"ca:revoke-cert": true,
|
||||
"ca:delete-cert": true,
|
||||
// Transit engine.
|
||||
"create-key": true,
|
||||
"delete-key": true,
|
||||
"rotate-key": true,
|
||||
"update-key-config": true,
|
||||
"trim-key": true,
|
||||
"transit:create-key": true,
|
||||
"transit:delete-key": true,
|
||||
"transit:rotate-key": true,
|
||||
"transit:update-key-config": true,
|
||||
"transit:trim-key": true,
|
||||
// SSH CA engine.
|
||||
"create-profile": true,
|
||||
"update-profile": true,
|
||||
"delete-profile": true,
|
||||
"sshca:create-profile": true,
|
||||
"sshca:update-profile": true,
|
||||
"sshca:delete-profile": true,
|
||||
"sshca:revoke-cert": true,
|
||||
"sshca:delete-cert": true,
|
||||
// User engine.
|
||||
"provision": true,
|
||||
"delete-user": true,
|
||||
"user:provision": true,
|
||||
"user:delete-user": true,
|
||||
}
|
||||
|
||||
func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -302,8 +322,16 @@ func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
|
||||
// Resolve engine type from mount to qualify the admin-only lookup.
|
||||
mount, err := s.engines.GetMount(req.Mount)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"mount not found"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Enforce admin requirement for operations that have admin-only typed routes.
|
||||
if adminOnlyOperations[req.Operation] && !info.IsAdmin {
|
||||
// Key is "engineType:operation" to avoid cross-engine name collisions.
|
||||
if adminOnlyOperations[string(mount.Type)+":"+req.Operation] && !info.IsAdmin {
|
||||
http.Error(w, `{"error":"forbidden: admin required"}`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -734,13 +762,14 @@ func operationAction(op string) string {
|
||||
switch op {
|
||||
// Read operations.
|
||||
case "list-issuers", "list-certs", "get-cert", "get-root", "get-chain", "get-issuer",
|
||||
"list-keys", "get-key", "get-public-key", "list-users", "get-profile", "list-profiles":
|
||||
"list-keys", "get-key", "get-public-key", "list-users", "get-profile", "list-profiles",
|
||||
"get-ca-pubkey", "get-krl":
|
||||
return policy.ActionRead
|
||||
|
||||
// Granular cryptographic operations (including batch variants).
|
||||
case "encrypt", "batch-encrypt":
|
||||
return policy.ActionEncrypt
|
||||
case "decrypt", "batch-decrypt":
|
||||
case "decrypt", "batch-decrypt", "rewrap", "batch-rewrap":
|
||||
return policy.ActionDecrypt
|
||||
case "sign", "sign-host", "sign-user":
|
||||
return policy.ActionSign
|
||||
@@ -769,3 +798,417 @@ func readJSON(r *http.Request, v interface{}) error {
|
||||
}
|
||||
return json.Unmarshal(body, v)
|
||||
}
|
||||
|
||||
// --- SSH CA Handlers ---
|
||||
|
||||
func (s *Server) getSSHCAEngine(mountName string) (*sshca.SSHCAEngine, error) {
|
||||
mount, err := s.engines.GetMount(mountName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mount.Type != engine.EngineTypeSSHCA {
|
||||
return nil, errors.New("mount is not an SSH CA engine")
|
||||
}
|
||||
eng, ok := mount.Engine.(*sshca.SSHCAEngine)
|
||||
if !ok {
|
||||
return nil, errors.New("mount is not an SSH CA engine")
|
||||
}
|
||||
return eng, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCAPubkey(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
eng, err := s.getSSHCAEngine(mountName)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
pubKey, err := eng.GetCAPubkey(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write(pubKey) //nolint:gosec
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCAKRL(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
eng, err := s.getSSHCAEngine(mountName)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
krlData, err := eng.GetKRL()
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"sealed"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(krlData) //nolint:gosec
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCASignHost(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
var req struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
Hostname string `json:"hostname"`
|
||||
TTL string `json:"ttl"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
data := map[string]interface{}{
|
||||
"public_key": req.PublicKey,
|
||||
"hostname": req.Hostname,
|
||||
}
|
||||
if req.TTL != "" {
|
||||
data["ttl"] = req.TTL
|
||||
}
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "sign-host",
|
||||
Data: data,
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCASignUser(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
var req struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
Principals []string `json:"principals"`
|
||||
Profile string `json:"profile"`
|
||||
TTL string `json:"ttl"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
data := map[string]interface{}{
|
||||
"public_key": req.PublicKey,
|
||||
}
|
||||
if len(req.Principals) > 0 {
|
||||
principals := make([]interface{}, len(req.Principals))
|
||||
for i, p := range req.Principals {
|
||||
principals[i] = p
|
||||
}
|
||||
data["principals"] = principals
|
||||
}
|
||||
if req.Profile != "" {
|
||||
data["profile"] = req.Profile
|
||||
}
|
||||
if req.TTL != "" {
|
||||
data["ttl"] = req.TTL
|
||||
}
|
||||
|
||||
policyChecker := func(resource, action string) (string, bool) {
|
||||
pReq := &policy.Request{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
Resource: resource,
|
||||
Action: action,
|
||||
}
|
||||
eff, matched, pErr := s.policy.Match(r.Context(), pReq)
|
||||
if pErr != nil {
|
||||
return string(policy.EffectDeny), false
|
||||
}
|
||||
return string(eff), matched
|
||||
}
|
||||
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "sign-user",
|
||||
Data: data,
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
CheckPolicy: policyChecker,
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCACreateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
CriticalOptions map[string]string `json:"critical_options"`
|
||||
Extensions map[string]string `json:"extensions"`
|
||||
MaxTTL string `json:"max_ttl"`
|
||||
AllowedPrincipals []string `json:"allowed_principals"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
data := map[string]interface{}{"name": req.Name}
|
||||
if req.CriticalOptions != nil {
|
||||
opts := make(map[string]interface{}, len(req.CriticalOptions))
|
||||
for k, v := range req.CriticalOptions {
|
||||
opts[k] = v
|
||||
}
|
||||
data["critical_options"] = opts
|
||||
}
|
||||
if req.Extensions != nil {
|
||||
exts := make(map[string]interface{}, len(req.Extensions))
|
||||
for k, v := range req.Extensions {
|
||||
exts[k] = v
|
||||
}
|
||||
data["extensions"] = exts
|
||||
}
|
||||
if req.MaxTTL != "" {
|
||||
data["max_ttl"] = req.MaxTTL
|
||||
}
|
||||
if len(req.AllowedPrincipals) > 0 {
|
||||
principals := make([]interface{}, len(req.AllowedPrincipals))
|
||||
for i, p := range req.AllowedPrincipals {
|
||||
principals[i] = p
|
||||
}
|
||||
data["allowed_principals"] = principals
|
||||
}
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "create-profile",
|
||||
Data: data,
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCAUpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
var req struct {
|
||||
CriticalOptions map[string]string `json:"critical_options"`
|
||||
Extensions map[string]string `json:"extensions"`
|
||||
MaxTTL string `json:"max_ttl"`
|
||||
AllowedPrincipals []string `json:"allowed_principals"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
data := map[string]interface{}{"name": name}
|
||||
if req.CriticalOptions != nil {
|
||||
opts := make(map[string]interface{}, len(req.CriticalOptions))
|
||||
for k, v := range req.CriticalOptions {
|
||||
opts[k] = v
|
||||
}
|
||||
data["critical_options"] = opts
|
||||
}
|
||||
if req.Extensions != nil {
|
||||
exts := make(map[string]interface{}, len(req.Extensions))
|
||||
for k, v := range req.Extensions {
|
||||
exts[k] = v
|
||||
}
|
||||
data["extensions"] = exts
|
||||
}
|
||||
if req.MaxTTL != "" {
|
||||
data["max_ttl"] = req.MaxTTL
|
||||
}
|
||||
if len(req.AllowedPrincipals) > 0 {
|
||||
principals := make([]interface{}, len(req.AllowedPrincipals))
|
||||
for i, p := range req.AllowedPrincipals {
|
||||
principals[i] = p
|
||||
}
|
||||
data["allowed_principals"] = principals
|
||||
}
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "update-profile",
|
||||
Data: data,
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCAGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "get-profile",
|
||||
Data: map[string]interface{}{"name": name},
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCAListProfiles(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "list-profiles",
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCADeleteProfile(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
name := chi.URLParam(r, "name")
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
_, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "delete-profile",
|
||||
Data: map[string]interface{}{"name": name},
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCAGetCert(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
serial := chi.URLParam(r, "serial")
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "get-cert",
|
||||
Data: map[string]interface{}{"serial": serial},
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCAListCerts(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "list-certs",
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCARevokeCert(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
serial := chi.URLParam(r, "serial")
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
resp, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "revoke-cert",
|
||||
Data: map[string]interface{}{"serial": serial},
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp.Data)
|
||||
}
|
||||
|
||||
func (s *Server) handleSSHCADeleteCert(w http.ResponseWriter, r *http.Request) {
|
||||
mountName := chi.URLParam(r, "mount")
|
||||
serial := chi.URLParam(r, "serial")
|
||||
info := TokenInfoFromContext(r.Context())
|
||||
_, err := s.engines.HandleRequest(r.Context(), mountName, &engine.Request{
|
||||
Operation: "delete-cert",
|
||||
Data: map[string]interface{}{"serial": serial},
|
||||
CallerInfo: &engine.CallerInfo{
|
||||
Username: info.Username,
|
||||
Roles: info.Roles,
|
||||
IsAdmin: info.IsAdmin,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.writeEngineError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (s *Server) writeEngineError(w http.ResponseWriter, err error) {
|
||||
status := http.StatusInternalServerError
|
||||
switch {
|
||||
case errors.Is(err, engine.ErrMountNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, sshca.ErrCertNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, sshca.ErrProfileNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, sshca.ErrProfileExists):
|
||||
status = http.StatusConflict
|
||||
case errors.Is(err, sshca.ErrForbidden):
|
||||
status = http.StatusForbidden
|
||||
case errors.Is(err, sshca.ErrUnauthorized):
|
||||
status = http.StatusUnauthorized
|
||||
case strings.Contains(err.Error(), "forbidden"):
|
||||
status = http.StatusForbidden
|
||||
case strings.Contains(err.Error(), "not found"):
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
http.Error(w, `{"error":"`+err.Error()+`"}`, status)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user