Ctrl K
ComunidadGuías…

internal/mcpgateway/store_sealed.go

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

Archivo original ↗Preguntar a KiBot
package mcpgateway

import (
	"context"
	"errors"
	"strings"

	"github.com/enterpilot/gomodel/internal/encryption"
)

// mcpSecretKind names managed MCP servers in sealed values' AAD.
const mcpSecretKind = "mcp_server"

// sealedStore encrypts header values on the way into the store and decrypts
// them on the way out. Header names stay readable.
type sealedStore struct {
	Store
	box  *encryption.Box
	swap headerSwapper // nil when the store cannot swap; see confirmKey
}

func sealStore(store Store, box *encryption.Box) Store {
	if box == nil {
		return store
	}
	swap, _ := store.(headerSwapper)
	return &sealedStore{Store: store, box: box, swap: swap}
}

func headerSecretFields(headers map[string]string) []encryption.Field {
	fields := make([]encryption.Field, 0, len(headers))
	for name := range headers {
		value := headers[name]
		fields = append(fields, encryption.Field{Name: "headers." + name, Value: &value})
	}
	return fields
}

// transformHeaders replaces server's header map with a copy whose values went
// through fn.
func transformHeaders(server *ManagedServer, fn func(id string, fields ...encryption.Field) error) error {
	if len(server.Headers) == 0 {
		return nil
	}
	out := make(map[string]string, len(server.Headers))
	for name, value := range server.Headers {
		if err := fn(strings.TrimSpace(server.Name), encryption.Field{Name: "headers." + name, Value: &value}); err != nil {
			return err
		}
		out[name] = value
	}
	server.Headers = out
	return nil
}

func (s *sealedStore) open(server *ManagedServer) error {
	return transformHeaders(server, func(id string, fields ...encryption.Field) error {
		return s.box.OpenFields(mcpSecretKind, id, fields...)
	})
}

func (s *sealedStore) List(ctx context.Context) ([]ManagedServer, error) {
	servers, err := s.Store.List(ctx)
	if err != nil {
		return nil, err
	}
	for i := range servers {
		if err := s.open(&servers[i]); err != nil {
			return nil, err
		}
	}
	return servers, nil
}

func (s *sealedStore) Get(ctx context.Context, name string) (*ManagedServer, error) {
	server, err := s.Store.Get(ctx, name)
	if err != nil {
		return nil, err
	}
	if err := s.open(server); err != nil {
		return nil, err
	}
	return server, nil
}

// Upsert seals a copy of the headers; the caller's map is left untouched.
func (s *sealedStore) Upsert(ctx context.Context, server ManagedServer) error {
	sealed, err := s.seal(server)
	if err != nil {
		return err
	}
	if err := s.Store.Upsert(ctx, sealed); err != nil {
		return err
	}
	return s.confirmKey(ctx, server, sealed)
}

// Update seals like Upsert; it rewrites only an existing row.
func (s *sealedStore) Update(ctx context.Context, server ManagedServer) error {
	sealed, err := s.seal(server)
	if err != nil {
		return err
	}
	if err := s.Store.Update(ctx, sealed); err != nil {
		return err
	}
	return s.confirmKey(ctx, server, sealed)
}

// seal returns a copy of server with its header values sealed.
func (s *sealedStore) seal(server ManagedServer) (ManagedServer, error) {
	err := transformHeaders(&server, func(id string, fields ...encryption.Field) error {
		return s.box.SealFields(mcpSecretKind, id, fields...)
	})
	return server, err
}

// confirmKey re-seals a just-written row when a data key rotation activated a
// new key while the save was in flight (see Box.ConfirmSeal). The swap is
// conditional, so a newer save of the same row is never overwritten.
func (s *sealedStore) confirmKey(ctx context.Context, plain, written ManagedServer) error {
	if s.swap == nil {
		return nil
	}
	return s.box.ConfirmSeal(func() error {
		resealed, err := s.seal(plain)
		if err != nil {
			return err
		}
		_, err = s.swap.swapHeaders(ctx, written, resealed)
		return err
	}, headerSecretFields(written.Headers)...)
}

// headerSwapper replaces a server's headers only while they still hold the
// values read earlier. Both store backends implement it.
type headerSwapper interface {
	swapHeaders(ctx context.Context, current, next ManagedServer) (bool, error)
}

// reencrypt rewrites every server holding a plaintext header or one sealed
// with an older data key. Each write is conditional on the headers still being
// what was read, so a concurrent admin edit is never overwritten: the row is
// read again and retried.
func (s *sealedStore) reencrypt(ctx context.Context, swap headerSwapper) (encryption.Report, error) {
	listed, err := s.Store.List(ctx)
	if err != nil {
		return encryption.Report{Entity: "mcp_servers"}, err
	}
	names := make([]string, len(listed))
	for i, row := range listed {
		names[i] = row.Name
	}
	return encryption.ReencryptRows("mcp_servers", names, func(name string) (encryption.RowOutcome, error) {
		return s.reencryptRow(ctx, swap, name)
	})
}

func (s *sealedStore) reencryptRow(ctx context.Context, swap headerSwapper, name string) (encryption.RowOutcome, error) {
	current, err := s.Store.Get(ctx, name)
	if errors.Is(err, ErrNotFound) {
		return encryption.RowUnchanged, nil
	}
	if err != nil {
		return 0, err
	}
	if !s.box.NeedsReseal(headerSecretFields(current.Headers)...) {
		return encryption.RowUnchanged, nil
	}
	plain := *current
	if err := s.open(&plain); err != nil {
		return 0, err
	}
	next, err := s.seal(plain)
	if err != nil {
		return 0, err
	}
	swapped, err := swap.swapHeaders(ctx, *current, next)
	if err != nil {
		return 0, err
	}
	if !swapped {
		return encryption.RowConflict, nil
	}
	return encryption.RowRewritten, nil
}