-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathscopes.go
More file actions
337 lines (296 loc) · 10.2 KB
/
scopes.go
File metadata and controls
337 lines (296 loc) · 10.2 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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
package scopes
import (
"regexp"
"slices"
"strings"
"github.com/sourcegraph/sourcegraph-accounts-sdk-go/services"
)
// The list of concrete scopes that can be requested by a client.
const (
OpenID Scope = "openid"
Profile Scope = "profile"
Email Scope = "email"
OfflineAccess Scope = "offline_access"
// The list of scopes for governing access of a client to a service. For
// example, "client.ssc" should only be granted to clients that can retrieve SSC
// data, etc.
ClientSSC Scope = "client.ssc"
ClientDotcom Scope = "client.dotcom"
)
// Ths list of regular expressions to make sure each part of a scope is
// spec-compliant.
var (
ServiceRegex = regexp.MustCompile(`^[a-z_]{1,30}$`)
PermissionRegex = regexp.MustCompile(`^[a-z_.]{1,215}$`)
ActionRegex = regexp.MustCompile(`^(read|write|delete)$`)
)
// Action is a type for the action part of a scope.
type Action string
const (
ActionRead Action = "read"
ActionWrite Action = "write"
ActionDelete Action = "delete"
)
// Scope is the string literal of a scope.
type Scope string
// ToStrings converts a list of scopes to a list of strings.
func ToStrings(scopes []Scope) []string {
ss := make([]string, len(scopes))
for i, scope := range scopes {
ss[i] = string(scope)
}
return ss
}
// ToScopes converts a list of strings to a list of scopes.
func ToScopes(strings []string) Scopes {
scopes := make([]Scope, len(strings))
for i, s := range strings {
scopes[i] = Scope(s)
}
return scopes
}
// AllowedScopes is a concrete list of allowed scopes that can be registered by
// a client.
type AllowedScopes []Scope
// Contains returns true if the scope is in the list of allowed scopes. It DOES
// NOT do prefix matching like Strategy to prevent clients registering free-form
// and nonsense scopes.
func (s AllowedScopes) Contains(scope Scope) bool {
return slices.Contains(s, scope)
}
// ToScope returns a scope string in the format of
// "service::permission::action".
func ToScope(service services.Service, permission Permission, action Action) Scope {
return Scope(string(service) + "::" + string(permission) + "::" + string(action))
}
// Permission is a type for the permission part of a scope.
type Permission string
var (
codyGatewayPermissions = []Permission{
"flaggedprompts",
"workspaces",
}
samsPermissions = []Permission{
"user",
"user.profile",
"user.roles",
// Grants access to all scopes - use sparingly.
"user.metadata",
// Read-only SAMS-internal metadata
"user.metadata.internal",
// Cody Pro and SSC metadata
"user.metadata.cody",
// Legacy Sourcegraph.com metadata
"user.metadata.dotcom",
// Metadata owned by the 'Cody Gatekeeper' service.
"user.metadata.cody_gatekeeper",
// Metadata owned by PLG efforts for supporting community members.
"user.metadata.plg",
"session",
"roles",
"roles.resources",
"service_access_tokens",
"service_access_tokens.analytics",
}
telemetryGatewayPermissions = []Permission{
"events",
}
enterprisePortalPermissions = []Permission{
PermissionEnterprisePortalSubscription,
PermissionEnterprisePortalSubscriptionPermission,
PermissionEnterprisePortalCodyAccess,
PermissionEnterprisePortalMetering,
}
workspacesPermissions = []Permission{
"workspace",
"instance",
"permission.workspace",
"metering",
}
mailGatekeeperPermissions = []Permission{
"emails",
}
ampPermissions = []Permission{
"user",
}
analyticsPermissions = []Permission{
"analytics",
}
cloudAPIPermissions = []Permission{
"instance",
}
)
const (
// Permissions for Enterprise Portal gRPC service:
// enterpriseportal.subscriptions.v1.SubscriptionsService
// PermissionEnterprisePortalSubscription designates permissions for
// Enteprrise subscriptions.
PermissionEnterprisePortalSubscription Permission = "subscription"
// PermissionEnterprisePortalSubscriptionPermission designates permissions
// for managing permissions on Enterprise subscriptions.
PermissionEnterprisePortalSubscriptionPermission Permission = "permission.subscription"
// Permissions for Enterprise Portal gRPC service:
// enterpriseportal.codyaccess.v1.CodyAccessService
// PermissionEnterprisePortalCodyAccess designates permissions for Enterprise
// Cody Access for managed Cody features.
PermissionEnterprisePortalCodyAccess Permission = "codyaccess"
// PermissionEnterprisePortalMetering designates permissions for Deep Search
// quota management and metering functionality.
PermissionEnterprisePortalMetering Permission = "metering"
)
// Allowed returns all allowed scopes for a client. The caller should use
// AllowedScopes.Contains for matching requested scopes.
func Allowed() AllowedScopes {
// Start with the scopes that are defined in the OAuth and OIDC spec.
allowed := []Scope{
OpenID, Profile, Email, OfflineAccess,
// Legacy scopes that will be replaced in future iterations, e.g.
// - "client.ssc" -> "ssc::<permission>::<action>"
// - "client.dotcom" -> "dotcom::<permission>::<action>"
ClientSSC, ClientDotcom,
}
// Add full { read, write, delete } actions for all permissions for the given service.
appendScopes := func(service services.Service, permissions []Permission) {
for _, permission := range permissions {
// Special case: read-only for SAMS-internal user metadata.
if permission == "user.metadata.internal" {
allowed = append(allowed, ToScope(service, permission, ActionRead))
continue
}
allowed = append(
allowed,
[]Scope{
ToScope(service, permission, ActionRead),
ToScope(service, permission, ActionWrite),
ToScope(service, permission, ActionDelete),
}...,
)
}
}
appendScopes(services.Amp, ampPermissions)
appendScopes(services.CodyGateway, codyGatewayPermissions)
appendScopes(services.SAMS, samsPermissions)
appendScopes(services.TelemetryGateway, telemetryGatewayPermissions)
appendScopes(services.EnterprisePortal, enterprisePortalPermissions)
appendScopes(services.MailGatekeeper, mailGatekeeperPermissions)
appendScopes(services.Workspaces, workspacesPermissions)
appendScopes(services.Analytics, analyticsPermissions)
appendScopes(services.CloudAPI, cloudAPIPermissions)
// 👉 ADD YOUR SCOPES HERE
return allowed
}
type ParsedScope struct {
Service services.Service
Permission Permission
Action Action
}
// ParseScope parses a scope into its parts. It returns the service, permission,
// action, and a boolean indicating if the scope is valid.
//
// Not using strings.Split and returning a non-pointer type to achieve "0 allocs/op" based on benchmarks:
//
// go test -bench=. -benchmem -cpu=4
//
// BenchmarkStrategy_Match-4 6745492 156.6 ns/op 0 B/op 0 allocs/op
// BenchmarkStrategy_NoMatch-4 7670725 155.6 ns/op 0 B/op 0 allocs/op
func ParseScope(scope Scope) (_ ParsedScope, valid bool) {
// Special case for builtin OAuth and OIDC scopes that has no alias.
if scope == OpenID || scope == Email || scope == OfflineAccess {
return ParsedScope{
Service: "",
Permission: Permission(scope),
Action: "",
}, true
}
// Backward compatibility for legacy scopes.
if scope == ClientSSC || scope == ClientDotcom {
return ParsedScope{
Service: "",
Permission: Permission(scope),
Action: "",
}, true
}
i := strings.Index(string(scope), "::")
if i == -1 {
return ParsedScope{}, false
}
service := scope[:i]
i += 2
j := strings.Index(string(scope[i:]), "::")
if j == -1 {
return ParsedScope{}, false
}
permission := scope[i : i+j]
action := scope[i+j+2:]
if service == "" || permission == "" || action == "" {
return ParsedScope{}, false // Any of the parts of the scope can't be empty.
}
return ParsedScope{
Service: services.Service(service),
Permission: Permission(permission),
Action: Action(action),
}, true
}
var aliases = map[Scope]Scope{
Profile: ToScope(services.SAMS, "user.profile", ActionRead),
}
// Strategy is a custom scope strategy that matches scopes based on the following rules:
// - Builtin scopes ("openid", "email", "offline_access") without alias are
// matched by their exact name.
// - Any matcher or needle that must have the desired the format,
// "service::permission::action". Otherwise consider not match (returns false).
// - A overall match is considered when all "service", "permission", and
// "action" match (returns true).
// - The "permission" part of the scope is (conceptually) prefix matching, i.e.
// "user" matches "user" as well as "user.roles" and "user.metadata".
//
// Full specification of the token scope is available at
// https://handbook.sourcegraph.com/departments/engineering/teams/core-services/sams/token_scope_specification/
//
// NOTE: This function must accept strings to have the type of
// `fosite.ScopeStrategy`.
func Strategy(matcherLiterals []string, needleLiteral string) bool {
needle := Scope(needleLiteral)
// Canonicalize some scopes that are being searched for with an alias, e.g. only
// search for "sams::user.profile::read" instead of worrying about both
// "sams::user.profile::read" AND "profile".
if alias, ok := aliases[needle]; ok {
needle = alias
}
needleScope, valid := ParseScope(needle)
if !valid {
return false
}
for _, matcherLiteral := range matcherLiterals {
matcher := Scope(matcherLiteral)
if alias, ok := aliases[matcher]; ok {
matcher = alias
}
// If the matcher is longer than the needle, it is impossible to have a match
// because the permission is prefixing matching, i.e. the needle needs to be at
// least the same length as the matcher.
if len(matcher) > len(needle) {
continue
}
scope, valid := ParseScope(matcher)
if !valid {
continue
}
// If the service or action do not match, it is pointless to check the
// permission.
if scope.Service != needleScope.Service || scope.Action != needleScope.Action {
continue
}
if scope.Permission == needleScope.Permission || strings.HasPrefix(string(needleScope.Permission), string(scope.Permission)+".") {
return true
}
}
return false
}
// Scopes is a list of scopes.
type Scopes []Scope
// Match returns true if any of the scope in the list matches the target scope
// using Strategy.
func (s Scopes) Match(target Scope) bool {
return Strategy(ToStrings(s), string(target))
}