package hostops import ( "context" "errors" "io" "os" "strings" "github.com/carlosframework/platform/internal/controlname" "github.com/carlosframework/platform/internal/release" ) type MutationTarget struct { Surface string Method string Key string ETag string Before []byte After []byte } // MutationCheck must resolve the complete affected scope set and validate // its live permits before returning nil. type MutationCheck func(context.Context, MutationTarget) error // MutationEvent is what the boundary reports to a fixture recorder // immediately before the raw effect (or its refusal). type MutationEvent struct { Target MutationTarget Verdict error } // mutationBoundary is the enabled data store. Named private fields only; it // never embeds release.Store, so no optional method of the raw store is // promoted past the checker. type mutationBoundary struct { localAppID func(context.Context, string, string) (string, error) control *ControlWriter owner RegistrationOwner raw release.Store check MutationCheck observe func(MutationEvent) } var ( _ release.Store = (*mutationBoundary)(nil) _ release.ConditionalDeleter = (*mutationBoundary)(nil) _ release.Opener = (*mutationBoundary)(nil) _ release.ModLister = (*mutationBoundary)(nil) _ release.SizeLister = (*mutationBoundary)(nil) _ release.PageLister = (*mutationBoundary)(nil) _ release.ConditionalGetter = (*mutationBoundary)(nil) _ release.ETagLister = (*mutationBoundary)(nil) _ release.BucketListDirs = (*mutationBoundary)(nil) ) func NewMutationBoundary(enabled bool, raw release.Store, check MutationCheck) (release.Store, error) { return NewObservedMutationBoundary(enabled, raw, check, nil) } // NewObservedMutationBoundary is the fixture constructor: observe sees every // mutation verdict. Production passes nil. func NewObservedMutationBoundary(enabled bool, raw release.Store, check MutationCheck, observe func(MutationEvent)) (release.Store, error) { if !enabled { return raw, nil } if raw == nil || check == nil { return nil, ErrContract } return &mutationBoundary{raw: raw, check: check, observe: observe}, nil } // structuralRefusal runs before the checker and cannot be bypassed by any // context value: canonical control objects and malformed app shapes. func structuralRefusal(key string) error { // The console never admits writers and has no origin-candidate write grant. // Refuse before Put's read so a planted candidate cannot trigger buffering. if strings.HasPrefix(key, "control/host-origins/") { return ErrUnclassified } if IsControlOnlyKey(key) { return ErrControlKey } for _, root := range []string{"apps/", "ledgers/"} { if rest, ok := strings.CutPrefix(key, root); ok { p := strings.SplitN(rest, "/", 3) if len(p) < 3 || !controlname.ValidAccount(p[0]) || !controlname.ValidApp(p[1]) || p[2] == "" { return ErrIdentity } } } return nil } func (s *mutationBoundary) decide(ctx context.Context, t MutationTarget) error { err := structuralRefusal(t.Key) if err == nil { err = s.check(ctx, t) } if s.observe != nil { s.observe(MutationEvent{Target: t, Verdict: err}) } return err } // ---- reads: original values and errors ---- func (s *mutationBoundary) Get(ctx context.Context, key string) ([]byte, string, error) { return s.raw.Get(ctx, key) } func (s *mutationBoundary) List(ctx context.Context, prefix string) ([]string, error) { return s.raw.List(ctx, prefix) } func (s *mutationBoundary) ListDirs(ctx context.Context, prefix string) ([]string, error) { return s.raw.ListDirs(ctx, prefix) } func (s *mutationBoundary) BucketShapedListDirs() bool { return release.HasBucketListDirs(s.raw) } func (s *mutationBoundary) Open(ctx context.Context, key string) (io.ReadCloser, string, error) { return release.OpenObject(ctx, s.raw, key) } func (s *mutationBoundary) ListMod(ctx context.Context, prefix string) ([]release.KeyMod, error) { if l, ok := s.raw.(release.ModLister); ok { return l.ListMod(ctx, prefix) } return nil, ErrContract } func (s *mutationBoundary) ListSizes(ctx context.Context, prefix string) ([]release.KeySize, error) { if l, ok := s.raw.(release.SizeLister); ok { return l.ListSizes(ctx, prefix) } return nil, ErrContract } func (s *mutationBoundary) ListPage(ctx context.Context, prefix, startAfter string, limit int) ([]string, error) { if l, ok := s.raw.(release.PageLister); ok { return l.ListPage(ctx, prefix, startAfter, limit) } return nil, ErrContract } func (s *mutationBoundary) GetIfChanged(ctx context.Context, key, etag string) ([]byte, string, bool, error) { if g, ok := s.raw.(release.ConditionalGetter); ok { return g.GetIfChanged(ctx, key, etag) } return nil, "", false, ErrContract } func (s *mutationBoundary) ListETags(ctx context.Context, prefix string) ([]release.KeyETag, error) { if l, ok := s.raw.(release.ETagLister); ok { return l.ListETags(ctx, prefix) } return nil, ErrContract } // ---- mutations: classified, then exact ---- func (s *mutationBoundary) Put(ctx context.Context, key string, body []byte, tag string) (string, error) { if err := structuralRefusal(key); err != nil { s.report(MutationTarget{Surface: "bucket", Method: "Put", Key: key, ETag: tag}, err) return "", err } old, seen, err := s.raw.Get(ctx, key) if errors.Is(err, os.ErrNotExist) { old, seen = nil, "" } else if err != nil { return "", err } if seen != tag { return "", release.ErrCASConflict } // Copies: classification cannot alter the bytes that are issued. t := MutationTarget{Surface: "bucket", Method: "Put", Key: key, ETag: tag, Before: append([]byte(nil), old...), After: append([]byte(nil), body...)} if err = s.decide(ctx, t); err != nil { return "", err } MarkAppMutationIssued(ctx) return s.raw.Put(ctx, key, body, tag) } // Delete never sends a raw unconditional delete: it acquires the exact // revision, authorizes that revision's owners, and deletes only it. func (s *mutationBoundary) Delete(ctx context.Context, key string) error { if err := structuralRefusal(key); err != nil { s.report(MutationTarget{Surface: "bucket", Method: "Delete", Key: key}, err) return err } body, tag, err := s.raw.Get(ctx, key) if errors.Is(err, os.ErrNotExist) { // Absence still passes the checker, so a missing key cannot hide an // unregistered route from the coverage fixture. return s.decide(ctx, MutationTarget{Surface: "bucket", Method: "Delete", Key: key}) } if err != nil { return err } if tag == "" { return ErrIdentity } if err = s.decide(ctx, MutationTarget{Surface: "bucket", Method: "Delete", Key: key, ETag: tag, Before: append([]byte(nil), body...)}); err != nil { return err } MarkAppMutationIssued(ctx) return release.DeleteIfMatch(ctx, s.raw, key, tag) } func (s *mutationBoundary) DeleteIfMatch(ctx context.Context, key, tag string) error { if err := structuralRefusal(key); err != nil { s.report(MutationTarget{Surface: "bucket", Method: "DeleteIfMatch", Key: key, ETag: tag}, err) return err } if tag == "" { return ErrIdentity } body, seen, err := s.raw.Get(ctx, key) if errors.Is(err, os.ErrNotExist) { if err = s.decide(ctx, MutationTarget{Surface: "bucket", Method: "DeleteIfMatch", Key: key, ETag: tag}); err != nil { return err } return nil } if err != nil { return err } if seen != tag { return release.ErrCASConflict } if err = s.decide(ctx, MutationTarget{Surface: "bucket", Method: "DeleteIfMatch", Key: key, ETag: tag, Before: append([]byte(nil), body...)}); err != nil { return err } MarkAppMutationIssued(ctx) return release.DeleteIfMatch(ctx, s.raw, key, tag) } // RefusedBeforeEffect reports an error that a boundary or a conditional // store returned before any effect was issued. Anything else may have acted. func RefusedBeforeEffect(err error) bool { for _, known := range []error{ErrPending, ErrIdentity, ErrContract, ErrUnclassified, release.ErrCASConflict, release.ErrInvalidETag, release.ErrConditionalDeleteUnsupported} { if errors.Is(err, known) { return true } } return false } func (s *mutationBoundary) report(t MutationTarget, err error) { if s.observe != nil { s.observe(MutationEvent{Target: t, Verdict: err}) } }