carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "errors"
6 "io"
7 "os"
8 "strings"
9
10 "github.com/carlosframework/platform/internal/controlname"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14type MutationTarget struct {
15 Surface string
16 Method string
17 Key string
18 ETag string
19 Before []byte
20 After []byte
21}
22
23// MutationCheck must resolve the complete affected scope set and validate
24// its live permits before returning nil.
25type MutationCheck func(context.Context, MutationTarget) error
26
27// MutationEvent is what the boundary reports to a fixture recorder
28// immediately before the raw effect (or its refusal).
29type MutationEvent struct {
30 Target MutationTarget
31 Verdict error
32}
33
34// mutationBoundary is the enabled data store. Named private fields only; it
35// never embeds release.Store, so no optional method of the raw store is
36// promoted past the checker.
37type mutationBoundary struct {
38 localAppID func(context.Context, string, string) (string, error)
39 control *ControlWriter
40 owner RegistrationOwner
41 raw release.Store
42 check MutationCheck
43 observe func(MutationEvent)
44}
45
46var (
47 _ release.Store = (*mutationBoundary)(nil)
48 _ release.ConditionalDeleter = (*mutationBoundary)(nil)
49 _ release.Opener = (*mutationBoundary)(nil)
50 _ release.ModLister = (*mutationBoundary)(nil)
51 _ release.SizeLister = (*mutationBoundary)(nil)
52 _ release.PageLister = (*mutationBoundary)(nil)
53 _ release.ConditionalGetter = (*mutationBoundary)(nil)
54 _ release.ETagLister = (*mutationBoundary)(nil)
55 _ release.BucketListDirs = (*mutationBoundary)(nil)
56)
57
58func NewMutationBoundary(enabled bool, raw release.Store, check MutationCheck) (release.Store, error) {
59 return NewObservedMutationBoundary(enabled, raw, check, nil)
60}
61
62// NewObservedMutationBoundary is the fixture constructor: observe sees every
63// mutation verdict. Production passes nil.
64func NewObservedMutationBoundary(enabled bool, raw release.Store, check MutationCheck, observe func(MutationEvent)) (release.Store, error) {
65 if !enabled {
66 return raw, nil
67 }
68 if raw == nil || check == nil {
69 return nil, ErrContract
70 }
71 return &mutationBoundary{raw: raw, check: check, observe: observe}, nil
72}
73
74// structuralRefusal runs before the checker and cannot be bypassed by any
75// context value: canonical control objects and malformed app shapes.
76func structuralRefusal(key string) error {
77 // The console never admits writers and has no origin-candidate write grant.
78 // Refuse before Put's read so a planted candidate cannot trigger buffering.
79 if strings.HasPrefix(key, "control/host-origins/") {
80 return ErrUnclassified
81 }
82
83 if IsControlOnlyKey(key) {
84 return ErrControlKey
85 }
86 for _, root := range []string{"apps/", "ledgers/"} {
87 if rest, ok := strings.CutPrefix(key, root); ok {
88 p := strings.SplitN(rest, "/", 3)
89 if len(p) < 3 || !controlname.ValidAccount(p[0]) || !controlname.ValidApp(p[1]) || p[2] == "" {
90 return ErrIdentity
91 }
92 }
93 }
94 return nil
95}
96
97func (s *mutationBoundary) decide(ctx context.Context, t MutationTarget) error {
98 err := structuralRefusal(t.Key)
99 if err == nil {
100 err = s.check(ctx, t)
101 }
102 if s.observe != nil {
103 s.observe(MutationEvent{Target: t, Verdict: err})
104 }
105 return err
106}
107
108// ---- reads: original values and errors ----
109
110func (s *mutationBoundary) Get(ctx context.Context, key string) ([]byte, string, error) {
111 return s.raw.Get(ctx, key)
112}
113func (s *mutationBoundary) List(ctx context.Context, prefix string) ([]string, error) {
114 return s.raw.List(ctx, prefix)
115}
116func (s *mutationBoundary) ListDirs(ctx context.Context, prefix string) ([]string, error) {
117 return s.raw.ListDirs(ctx, prefix)
118}
119func (s *mutationBoundary) BucketShapedListDirs() bool { return release.HasBucketListDirs(s.raw) }
120func (s *mutationBoundary) Open(ctx context.Context, key string) (io.ReadCloser, string, error) {
121 return release.OpenObject(ctx, s.raw, key)
122}
123func (s *mutationBoundary) ListMod(ctx context.Context, prefix string) ([]release.KeyMod, error) {
124 if l, ok := s.raw.(release.ModLister); ok {
125 return l.ListMod(ctx, prefix)
126 }
127 return nil, ErrContract
128}
129func (s *mutationBoundary) ListSizes(ctx context.Context, prefix string) ([]release.KeySize, error) {
130 if l, ok := s.raw.(release.SizeLister); ok {
131 return l.ListSizes(ctx, prefix)
132 }
133 return nil, ErrContract
134}
135func (s *mutationBoundary) ListPage(ctx context.Context, prefix, startAfter string, limit int) ([]string, error) {
136 if l, ok := s.raw.(release.PageLister); ok {
137 return l.ListPage(ctx, prefix, startAfter, limit)
138 }
139 return nil, ErrContract
140}
141
142func (s *mutationBoundary) GetIfChanged(ctx context.Context, key, etag string) ([]byte, string, bool, error) {
143 if g, ok := s.raw.(release.ConditionalGetter); ok {
144 return g.GetIfChanged(ctx, key, etag)
145 }
146 return nil, "", false, ErrContract
147}
148func (s *mutationBoundary) ListETags(ctx context.Context, prefix string) ([]release.KeyETag, error) {
149 if l, ok := s.raw.(release.ETagLister); ok {
150 return l.ListETags(ctx, prefix)
151 }
152 return nil, ErrContract
153}
154
155// ---- mutations: classified, then exact ----
156
157func (s *mutationBoundary) Put(ctx context.Context, key string, body []byte, tag string) (string, error) {
158 if err := structuralRefusal(key); err != nil {
159 s.report(MutationTarget{Surface: "bucket", Method: "Put", Key: key, ETag: tag}, err)
160 return "", err
161 }
162 old, seen, err := s.raw.Get(ctx, key)
163 if errors.Is(err, os.ErrNotExist) {
164 old, seen = nil, ""
165 } else if err != nil {
166 return "", err
167 }
168 if seen != tag {
169 return "", release.ErrCASConflict
170 }
171 // Copies: classification cannot alter the bytes that are issued.
172 t := MutationTarget{Surface: "bucket", Method: "Put", Key: key, ETag: tag, Before: append([]byte(nil), old...), After: append([]byte(nil), body...)}
173 if err = s.decide(ctx, t); err != nil {
174 return "", err
175 }
176 MarkAppMutationIssued(ctx)
177 return s.raw.Put(ctx, key, body, tag)
178}
179
180// Delete never sends a raw unconditional delete: it acquires the exact
181// revision, authorizes that revision's owners, and deletes only it.
182func (s *mutationBoundary) Delete(ctx context.Context, key string) error {
183 if err := structuralRefusal(key); err != nil {
184 s.report(MutationTarget{Surface: "bucket", Method: "Delete", Key: key}, err)
185 return err
186 }
187 body, tag, err := s.raw.Get(ctx, key)
188 if errors.Is(err, os.ErrNotExist) {
189 // Absence still passes the checker, so a missing key cannot hide an
190 // unregistered route from the coverage fixture.
191 return s.decide(ctx, MutationTarget{Surface: "bucket", Method: "Delete", Key: key})
192 }
193 if err != nil {
194 return err
195 }
196 if tag == "" {
197 return ErrIdentity
198 }
199 if err = s.decide(ctx, MutationTarget{Surface: "bucket", Method: "Delete", Key: key, ETag: tag, Before: append([]byte(nil), body...)}); err != nil {
200 return err
201 }
202 MarkAppMutationIssued(ctx)
203 return release.DeleteIfMatch(ctx, s.raw, key, tag)
204}
205
206func (s *mutationBoundary) DeleteIfMatch(ctx context.Context, key, tag string) error {
207 if err := structuralRefusal(key); err != nil {
208 s.report(MutationTarget{Surface: "bucket", Method: "DeleteIfMatch", Key: key, ETag: tag}, err)
209 return err
210 }
211 if tag == "" {
212 return ErrIdentity
213 }
214 body, seen, err := s.raw.Get(ctx, key)
215 if errors.Is(err, os.ErrNotExist) {
216 if err = s.decide(ctx, MutationTarget{Surface: "bucket", Method: "DeleteIfMatch", Key: key, ETag: tag}); err != nil {
217 return err
218 }
219 return nil
220 }
221 if err != nil {
222 return err
223 }
224 if seen != tag {
225 return release.ErrCASConflict
226 }
227 if err = s.decide(ctx, MutationTarget{Surface: "bucket", Method: "DeleteIfMatch", Key: key, ETag: tag, Before: append([]byte(nil), body...)}); err != nil {
228 return err
229 }
230 MarkAppMutationIssued(ctx)
231 return release.DeleteIfMatch(ctx, s.raw, key, tag)
232}
233
234// RefusedBeforeEffect reports an error that a boundary or a conditional
235// store returned before any effect was issued. Anything else may have acted.
236func RefusedBeforeEffect(err error) bool {
237 for _, known := range []error{ErrPending, ErrIdentity, ErrContract, ErrUnclassified, release.ErrCASConflict, release.ErrInvalidETag, release.ErrConditionalDeleteUnsupported} {
238 if errors.Is(err, known) {
239 return true
240 }
241 }
242 return false
243}
244
245func (s *mutationBoundary) report(t MutationTarget, err error) {
246 if s.observe != nil {
247 s.observe(MutationEvent{Target: t, Verdict: err})
248 }
249}
250