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/hostclaim"
11 "github.com/carlosframework/platform/internal/lease"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// versionedBoundary guards the console's exact-version handle (DBVersions).
16// Reads keep their values and errors. CopyVersion, DeleteVersion and
17// PutIfAbsent are classified: the control bucket by the bucket classifier,
18// data buckets by the claim holder of instances/<host>/ or moves/<host>/.
19// Named private fields; never embeds lease.VersionedStore.
20type versionedBoundary struct {
21 raw lease.VersionedStore
22 control release.Store // raw control store, read-only use
23 writer *ControlWriter
24 bucket string // the control (deployment) bucket
25 data map[string]bool
26 observe func(MutationEvent)
27}
28
29var (
30 _ lease.VersionedStore = (*versionedBoundary)(nil)
31 _ lease.VersionRangeOpener = (*versionedBoundary)(nil)
32)
33
34func NewVersionedBoundary(enabled bool, raw lease.VersionedStore, control release.Store, writer *ControlWriter, controlBucket string, dataBuckets []string, observe func(MutationEvent)) (lease.VersionedStore, error) {
35 if !enabled {
36 return raw, nil
37 }
38 if raw == nil || control == nil || writer == nil || controlBucket == "" {
39 return nil, ErrContract
40 }
41 data := map[string]bool{}
42 for _, b := range dataBuckets {
43 if b == "" || b == controlBucket {
44 return nil, ErrContract
45 }
46 data[b] = true
47 }
48 return &versionedBoundary{raw: raw, control: control, writer: writer, bucket: controlBucket, data: data, observe: observe}, nil
49}
50
51func (v *versionedBoundary) ListVersions(ctx context.Context, bucket, prefix string) ([]lease.ObjectVersion, error) {
52 return v.raw.ListVersions(ctx, bucket, prefix)
53}
54func (v *versionedBoundary) OpenVersion(ctx context.Context, bucket, key, versionID string) (io.ReadCloser, error) {
55 return v.raw.OpenVersion(ctx, bucket, key, versionID)
56}
57func (v *versionedBoundary) GetVersion(ctx context.Context, bucket, key, versionID string) ([]byte, error) {
58 return v.raw.GetVersion(ctx, bucket, key, versionID)
59}
60func (v *versionedBoundary) OpenVersionRange(ctx context.Context, bucket, key, versionID string, offset, length int64) (io.ReadCloser, error) {
61 if r, ok := v.raw.(lease.VersionRangeOpener); ok {
62 return r.OpenVersionRange(ctx, bucket, key, versionID, offset, length)
63 }
64 return nil, ErrContract
65}
66
67func (v *versionedBoundary) authorize(ctx context.Context, method, bucket, key string, before, after []byte) error {
68 t := MutationTarget{Surface: "versioned:" + bucket, Method: method, Key: key, Before: before, After: after}
69 err := v.classify(ctx, bucket, t)
70 if v.observe != nil {
71 v.observe(MutationEvent{Target: t, Verdict: err})
72 }
73 return err
74}
75
76func (v *versionedBoundary) classify(ctx context.Context, bucket string, t MutationTarget) error {
77 switch {
78 case bucket == v.bucket:
79 if provisioningKey(t.Key) && t.Method != "CopyVersion:source" {
80 // Version replacement/deletion can resurrect a lower counter.
81 // Only the typed retired-record sweep may remove old versions.
82 key, _ := ctx.Value(retiredRecordSweepKey{}).(string)
83 if t.Method != "DeleteVersion" || key != t.Key {
84 return ErrContract
85 }
86 if _, _, err := v.control.Get(ctx, t.Key); !errors.Is(err, os.ErrNotExist) {
87 if err != nil {
88 return err
89 }
90 return ErrPending
91 }
92 }
93 if err := structuralRefusal(t.Key); err != nil {
94 return err
95 }
96 c, err := ClassifyMutation(ctx, v.control, v.writer, t)
97 if err != nil {
98 return err
99 }
100 if c.NonApp != "" && len(c.Scopes) == 0 && strings.HasPrefix(c.Family, "lifecycle") {
101 // Lifecycle roots are never rewritten by exact-version calls.
102 return ErrIdentity
103 }
104 return AuthorizeScopes(ctx, v.control, v.writer, c)
105 case v.data[bucket]:
106 host := ""
107 for _, root := range []string{"instances/", "moves/"} {
108 if rest, ok := strings.CutPrefix(t.Key, root); ok {
109 host, _, _ = strings.Cut(rest, "/")
110 }
111 }
112 if host == "" {
113 return ErrUnclassified
114 }
115 obs, err := hostclaim.Read(ctx, v.control, host)
116 if err != nil {
117 return err
118 }
119 if obs.Absent || obs.Claim.Holder == nil {
120 return ErrPending
121 }
122 h := obs.Claim.Holder
123 return AuthorizeScopes(ctx, v.control, v.writer, Classification{Family: "data-bucket", Scopes: []AppScope{{h.Account, h.App}}})
124 }
125 return ErrUnclassified
126}
127
128func (v *versionedBoundary) versionBody(ctx context.Context, bucket, key, versionID string) []byte {
129 if bucket != v.bucket {
130 return nil
131 }
132 b, err := v.raw.GetVersion(ctx, bucket, key, versionID)
133 if err != nil {
134 return nil
135 }
136 return b
137}
138
139// CopyVersion authorizes both the source (its bytes move into another
140// scope) and the destination.
141func (v *versionedBoundary) CopyVersion(ctx context.Context, srcBucket, srcKey, srcVersionID, dstBucket, dstKey string, createOnly bool) (string, string, error) {
142 body := v.versionBody(ctx, srcBucket, srcKey, srcVersionID)
143 if err := v.authorize(ctx, "CopyVersion:source", srcBucket, srcKey, body, nil); err != nil {
144 return "", "", err
145 }
146 if err := v.authorize(ctx, "CopyVersion", dstBucket, dstKey, nil, body); err != nil {
147 return "", "", err
148 }
149 MarkAppMutationIssued(ctx)
150 return v.raw.CopyVersion(ctx, srcBucket, srcKey, srcVersionID, dstBucket, dstKey, createOnly)
151}
152
153func (v *versionedBoundary) DeleteVersion(ctx context.Context, bucket, key, versionID string) error {
154 if versionID == "" {
155 return lease.ErrNoVersionID
156 }
157 if err := v.authorize(ctx, "DeleteVersion", bucket, key, v.versionBody(ctx, bucket, key, versionID), nil); err != nil {
158 return err
159 }
160 if bucket == v.bucket && provisioningKey(key) {
161 if err := retiredRecordVersion(ctx, v.control, v.raw, bucket, key, versionID); err != nil {
162 return err
163 }
164 }
165 MarkAppMutationIssued(ctx)
166 return v.raw.DeleteVersion(ctx, bucket, key, versionID)
167}
168
169func (v *versionedBoundary) PutIfAbsent(ctx context.Context, bucket, key string, body []byte) (string, error) {
170 if err := v.authorize(ctx, "Put", bucket, key, nil, body); err != nil {
171 return "", err
172 }
173 MarkAppMutationIssued(ctx)
174 return v.raw.PutIfAbsent(ctx, bucket, key, body)
175}
176