Files
nmon/internal/api/secrets.go

55 lines
2.0 KiB
Go

package api
import (
"context"
"net/http"
"regexp"
"github.com/danielgtaylor/huma/v2"
)
// secretNameRE keeps names to what a config file can reference and a log line can show.
var secretNameRE = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$`)
// maxSecretLen bounds a stored secret; real API tokens are far shorter.
const maxSecretLen = 4096
// SecretBody is the body of PUT /secrets/{name}.
type SecretBody struct {
Value string `json:"value" minLength:"1" maxLength:"4096" doc:"The secret. It is encrypted at rest and can never be read back."`
}
func (s *server) registerSecrets(api huma.API) {
huma.Register(api, huma.Operation{
OperationID: "put-secret", Method: http.MethodPut, Path: "/api/v1/secrets/{name}", Tags: []string{"secrets"},
Summary: "Set a named secret (write-only)",
Description: "Creates or replaces the secret, encrypted with the inventory key. Config files refer to it by name " +
"(for example a Proxmox cluster's secret_name). There is no way to read a secret back.",
DefaultStatus: http.StatusNoContent,
Errors: []int{http.StatusUnprocessableEntity, http.StatusServiceUnavailable},
}, s.putSecret)
}
func (s *server) putSecret(ctx context.Context, in *struct {
Name string `path:"name" doc:"Secret name, letters, digits and . _ : -"`
Body SecretBody
}) (*struct{}, error) {
w := s.Secrets
if w == nil {
return nil, errNotConfigured("secret store")
}
if !secretNameRE.MatchString(in.Name) {
return nil, huma.Error422UnprocessableEntity("name: letters, digits and . _ : - only, at most 64 characters")
}
if in.Body.Value == "" || len(in.Body.Value) > maxSecretLen {
return nil, huma.Error422UnprocessableEntity("value must be 1 to 4096 bytes")
}
if err := w.PutSecret(ctx, in.Name, in.Body.Value); err != nil {
return nil, s.storeErr(err)
}
if err := s.Store.AppendAudit(ctx, actorOf(ctx), "secret.put", "secret:"+in.Name, nil, nil); err != nil { // the value is never audited
s.Logger.Warn("audit secret.put", "err", err)
}
return &struct{}{}, nil
}