internal/mcpgateway/secrets.go
Rama main · réplica de lectura del repositorio original.
package mcpgateway
import (
"context"
"log/slog"
"maps"
"slices"
"github.com/enterpilot/gomodel/config"
"github.com/enterpilot/gomodel/internal/core"
)
// ServerSecretEntity is the SecretKey.Entity of admin-managed MCP servers and
// the first segment of their secret field paths, for example
// "mcp_servers.github.headers.Authorization" (ADR-0014 §4). Header values
// are the secret fields: admin-managed servers have no stdio environment.
const ServerSecretEntity = "mcp_servers"
func serverSecretEntity(name string) string {
return ServerSecretEntity + "." + name
}
func headerSecretField(name string) string {
return "headers." + name
}
// serverDestinations lists the fields of server that decide where it sends
// its headers, against previous, the row it replaces, for
// config.CheckSecretDestinations: only the URL. A new server has none.
func serverDestinations(previous, server *ManagedServer) []config.SecretDestination {
if previous == nil {
return nil
}
return []config.SecretDestination{{Field: "url", Stored: previous.URL, Saved: server.URL}}
}
// resolveServer converts a stored row into the spec the upstream runs with,
// header references resolved, and returns the resolution to record once the
// spec is applied. Errors are *config.SecretError values naming the header.
func (s *Service) resolveServer(ctx context.Context, row ManagedServer) (ServerSpec, *config.ResolvedEntity, error) {
spec := row.Spec()
entity := serverSecretEntity(row.Name)
fields := make(map[string]string, len(row.Headers))
for name, value := range row.Headers {
fields[entity+"."+headerSecretField(name)] = value
}
resolved, err := s.secrets.ResolveEntity(ctx, entity, fields)
if err != nil {
return ServerSpec{}, nil, err
}
for name, value := range row.Headers {
spec.Headers[name] = resolved.Value(entity + "." + headerSecretField(name))
if config.HasSecretReference(value) {
if spec.HeaderReferences == nil {
spec.HeaderReferences = make(map[string]string)
}
spec.HeaderReferences[name] = value
}
}
return spec, resolved, nil
}
// storeServerSecrets writes every literal header value of server through the
// generation's SecretWriter, when one is registered, replacing it with the
// returned reference. It returns the references it created, which the caller
// releases if the save then fails. keep holds the values of the row still
// stored: a writer-owned reference it does not hold is rejected, and if a
// write fails the references already created are released, except those in
// keep, which a writer that reuses a reference may have returned again.
func (s *Service) storeServerSecrets(ctx context.Context, server *ManagedServer, keep []string) ([]string, error) {
headers := maps.Clone(server.Headers)
var written []string
for _, name := range slices.Sorted(maps.Keys(headers)) {
key := config.SecretKey{Entity: ServerSecretEntity, ID: server.Name, Field: headerSecretField(name)}
stored, err := s.secrets.StoreSecret(ctx, key, headers[name], keep)
if err != nil {
s.releaseSecrets(ctx, server.Name, written, keep)
return nil, err
}
if stored != headers[name] {
written = append(written, stored)
headers[name] = stored
}
}
server.Headers = headers
return written, nil
}
// releaseSecrets deletes the writer-owned references of previous that current
// no longer holds. It is best effort: the change it follows is committed.
func (s *Service) releaseSecrets(ctx context.Context, name string, previous, current []string) {
if err := s.secrets.ReleaseSecrets(ctx, previous, current); err != nil {
slog.Warn("failed to delete secrets of an mcp server from the secret store", "server", core.LogText(name), "error", err)
}
}
func headerValues(server *ManagedServer) []string {
if server == nil {
return nil
}
return slices.Collect(maps.Values(server.Headers))
}
// RotateSecrets re-resolves the admin-managed servers owning fields, as
// reported by a secret recheck, and redials each one whose resolved headers
// changed (ADR-0014 §5). Every other server keeps its session untouched. A
// server whose references cannot be re-resolved keeps its current headers.
func (s *Service) RotateSecrets(ctx context.Context, fields []string) error {
unresolved, err := s.reload(ctx, func(name string) bool {
return s.secrets.EntityOwnsAny(serverSecretEntity(name), fields)
})
if err != nil {
return err
}
return unresolved
}