Ctrl K
ComunidadGuías…

internal/mcpgateway/secrets.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}