-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathscope.go
More file actions
190 lines (174 loc) · 7.06 KB
/
Copy pathscope.go
File metadata and controls
190 lines (174 loc) · 7.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package cortex
import (
"context"
"errors"
"fmt"
"strings"
)
type contextKey int
const (
scopeKey contextKey = iota
principalKey
)
// ErrNoScope is returned when an operation that requires a scope receives
// a zero one. A zero scope means the thread broke somewhere upstream, so
// failing here is preferable to querying across every host-defined level.
var ErrNoScope = errors.New("cortex: no scope on context")
// Level is one rung of a host-defined scope hierarchy. Cortex never
// interprets Key; it only matches on it.
type Level struct {
Key string `json:"key"`
Value string `json:"value"`
}
// Scope is an ordered hierarchy the host defines. TwinOS uses
// workspace then project; another host might use org, team, environment.
// Order is significant: it determines which indexed column each level
// lands in, so a host must keep its ordering stable across releases.
type Scope struct {
Levels []Level `json:"levels"`
}
func (s Scope) IsZero() bool { return len(s.Levels) == 0 }
// Covers reports whether s is other, or an ancestor of it: every level s
// carries appears in other at the same position, with the same key and
// value. A scope covers itself.
//
// This asks "is s somewhere in other's own ancestry", which is a
// different question from the prefix matching the stores do on a list.
// Prefix matching widens DOWNWARD, so a workspace filter reaches every
// project inside it. Covers reads UPWARD, which is what a project-level
// run needs when it wants to know what its workspace configured.
func (s Scope) Covers(other Scope) bool {
if len(s.Levels) > len(other.Levels) {
return false
}
for i, lvl := range s.Levels {
if other.Levels[i] != lvl {
return false
}
}
return true
}
// Get returns the value for key, and whether the level was present.
func (s Scope) Get(key string) (string, bool) {
for _, l := range s.Levels {
if l.Key == key {
return l.Value, true
}
}
return "", false
}
// Canonical renders the scope as "key=value/key=value", preserving order.
// It is stored alongside the indexed columns so an exact-match lookup does
// not need to compare three columns separately.
func (s Scope) Canonical() string {
if len(s.Levels) == 0 {
return ""
}
parts := make([]string, 0, len(s.Levels))
for _, l := range s.Levels {
parts = append(parts, l.Key+"="+l.Value)
}
return strings.Join(parts, "/")
}
// ParseCanonical parses a Canonical() string back into a Scope, preserving
// level order. It is the inverse of Canonical: stores that persist only
// the canonical string (or the flattened scope_l0/l1/l2 columns plus a
// scope_canon column) use this to reconstruct Scope on read. An empty
// string parses to the zero Scope.
//
// A segment that doesn't contain "=" is an error rather than something to
// skip: silently dropping it would reconstruct a scope NARROWER than what
// was actually stored, which a caller comparing it against the row's own
// scope_l0/l1/l2 columns (or using it to authorize a write) would never
// notice. Callers should treat a non-nil error here as a corrupt row —
// the same as any other scan failure — rather than falling back to
// whatever levels did parse.
func ParseCanonical(canon string) (Scope, error) {
if canon == "" {
return Scope{}, nil
}
parts := strings.Split(canon, "/")
levels := make([]Level, 0, len(parts))
for _, p := range parts {
key, value, found := strings.Cut(p, "=")
if !found {
return Scope{}, fmt.Errorf("cortex: malformed scope_canon segment %q in %q", p, canon)
}
levels = append(levels, Level{Key: key, Value: value})
}
return Scope{Levels: levels}, nil
}
// maxScopeLevels is how many levels a Scope may carry. It mirrors
// indexedLevels in store/{postgres,sqlite,mongo}: levels beyond this land
// in the scope_extra overflow and are never matched as a predicate, even
// in exact mode, so a deeper scope would have its trailing levels
// silently accepted and then ignored by every store.
//
// This is why store/storetest's ScopeExtraNeverNull test can only ever
// exercise an empty overflow map: WithScope refuses anything deeper than
// this before a Scope carrying real overflow could ever reach a store.
// If this constant ever rises, postgres and sqlite's scopePredicates
// still only match scope_l0/l1/l2 (extra is stored but never queried,
// same as today), while mongo's scopeFilter ignores extra entirely on
// both read and write today — raising this constant without also
// teaching mongo's scopeFilter to match on extra would silently diverge
// mongo's isolation guarantee from the other two backends for any level
// beyond the third, with nothing here catching it.
const maxScopeLevels = 3
// WithScope attaches a scope to ctx. Two shapes are refused rather than
// stored:
//
// - A Level with an empty Key or empty Value. Either flattens to a
// "key=" predicate that matches every row sharing that partial key —
// a shared bucket, the exact cross-tenant hazard this phase exists to
// close.
// - A scope deeper than maxScopeLevels. Levels past the indexed columns
// are written to scope_extra but never read back as a predicate, so a
// value the caller supplied would be accepted and then silently
// ignored on every query.
//
// Both cases return ctx unchanged rather than panicking or erroring:
// ScopeFromContext then yields the zero scope, and every store guard
// already refuses that with ErrNoScope. This mirrors the deleted
// scopeFromTenant bridge, which returned ctx unchanged for an absent
// tenant instead of manufacturing a scope for it.
func WithScope(ctx context.Context, s Scope) context.Context {
if len(s.Levels) > maxScopeLevels {
return ctx
}
for _, l := range s.Levels {
if l.Key == "" || l.Value == "" {
return ctx
}
}
return context.WithValue(ctx, scopeKey, s)
}
// ScopeFromContext extracts the scope. A missing scope returns the zero
// value, which callers must treat as an error rather than as "match all".
func ScopeFromContext(ctx context.Context) Scope {
v, ok := ctx.Value(scopeKey).(Scope)
if !ok {
return Scope{}
}
return v
}
// WithPrincipal attaches the host's caller identity to ctx. Cortex never
// interprets this value — it only carries it from wherever a host
// authenticates a request through to Subject.Principal, for a
// ToolAuthorizer to interpret however its host defines identity.
//
// It travels on the context, alongside Scope, rather than through
// RunOverrides: both are ambient request identity with the same
// lifetime, and RunOverrides mirrors a JSON wire type that either can't
// carry an uninterpreted any or would force cortex to start interpreting
// it. Dispatch also has no overrides parameter, so a struct field would
// leave direct dispatch permanently principal-less.
func WithPrincipal(ctx context.Context, principal any) context.Context {
return context.WithValue(ctx, principalKey, principal)
}
// PrincipalFromContext extracts the caller identity WithPrincipal
// attached. A missing principal returns nil, mirroring ScopeFromContext's
// zero-value-on-absence shape rather than panicking.
func PrincipalFromContext(ctx context.Context) any {
return ctx.Value(principalKey)
}