tangled
alpha
login
or
join now
bnewbold.net
/
cobalt
13
fork
atom
go scratch code for atproto
13
fork
atom
overview
issues
pulls
pipelines
updates to permisison string support
bnewbold.net
3 months ago
d2c62f8d
098f3ed5
+201
-83
6 changed files
expand all
collapse all
unified
split
permissions
permission.go
permission_test.go
testdata
generic_scopes.json
generic_scopes_SKIP.json
generic_scopes_invalid.txt
permission_scopes_valid.txt
+78
-67
permissions/permission.go
···
5
5
"fmt"
6
6
"net/url"
7
7
"strings"
8
8
+
"unicode"
8
9
9
10
"github.com/bluesky-social/indigo/atproto/syntax"
10
11
)
11
12
12
13
var (
13
14
ErrInvalidPermissionSyntax = errors.New("invalid permission syntax")
14
14
-
ErrUnknownScope = errors.New("unknown scope type")
15
15
+
ErrInvalidPermissionParams = errors.New("invalid permission parameters")
16
16
+
ErrUnknownResource = errors.New("unknown permission resource")
15
17
)
16
18
17
19
type GenericPermission struct {
···
25
27
Resource string `json:"resource"`
26
28
27
29
// common params (eg, identity, account)
28
28
-
Attribute string `json:"attr,omitempty"`
29
29
-
Action []string `json:"action,omitempty"`
30
30
-
Audience string `json:"aud,omitempty"`
31
31
-
32
32
-
// repo
33
33
-
Collections []string `json:"collection,omitempty"`
34
34
-
35
35
-
// rpc
36
36
-
Endpoints []string `json:"lxm,omitempty"`
37
37
-
38
38
-
// blob
39
39
-
Accept []string `json:"accept,omitempty"`
40
40
-
41
41
-
// include
42
42
-
NSID string `json:"nsid,omitempty"`
30
30
+
Accept []string `json:"accept,omitempty"`
31
31
+
Action []string `json:"action,omitempty"`
32
32
+
Attribute string `json:"attr,omitempty"`
33
33
+
Audience string `json:"aud,omitempty"`
34
34
+
InheritAud bool `json:"inheritAud,omitempty"`
35
35
+
Collection []string `json:"collection,omitempty"`
36
36
+
Endpoint []string `json:"lxm,omitempty"`
37
37
+
NSID string `json:"nsid,omitempty"`
43
38
}
44
39
45
45
-
func (p *Permission) Scope() string {
40
40
+
func (p *Permission) ScopeString() string {
46
41
47
42
positional := ""
48
43
params := make(url.Values)
···
73
68
params.Set("aud", p.Audience)
74
69
}
75
70
case "repo":
76
76
-
if len(p.Collections) == 1 {
77
77
-
positional = p.Collections[0]
78
78
-
} else if len(p.Collections) > 1 {
79
79
-
params["collection"] = p.Collections
71
71
+
if len(p.Collection) == 1 {
72
72
+
positional = p.Collection[0]
73
73
+
} else if len(p.Collection) > 1 {
74
74
+
params["collection"] = p.Collection
80
75
}
81
76
if len(p.Action) != 0 {
82
77
params["action"] = p.Action
83
78
}
84
79
case "rpc":
85
85
-
if len(p.Endpoints) == 1 {
86
86
-
positional = p.Endpoints[0]
87
87
-
} else if len(p.Endpoints) > 1 {
88
88
-
params["lxm"] = p.Endpoints
80
80
+
if len(p.Endpoint) == 1 {
81
81
+
positional = p.Endpoint[0]
82
82
+
} else if len(p.Endpoint) > 1 {
83
83
+
params["lxm"] = p.Endpoint
89
84
}
90
85
if p.Audience != "" {
91
86
params.Set("aud", p.Audience)
···
106
101
107
102
func ParseGenericScope(scope string) (*GenericPermission, error) {
108
103
104
104
+
if !isASCII(scope) {
105
105
+
return nil, ErrInvalidPermissionSyntax
106
106
+
}
107
107
+
109
108
front, query, _ := strings.Cut(scope, "?")
110
109
resource, positional, _ := strings.Cut(front, ":")
110
110
+
111
111
+
// XXX: more charset restrictions
111
112
112
113
params, err := url.ParseQuery(query)
113
114
if err != nil {
···
122
123
return &p, nil
123
124
}
124
125
125
125
-
// TODO: improve this function
126
126
+
// TODO: improve this function, add tests
126
127
func validBlobAccept(accept string) bool {
127
128
if accept == "*/*" {
128
129
return true
···
140
141
return true
141
142
}
142
143
143
143
-
// TODO: improve this function
144
144
+
// TODO: improve this function, add tests
144
145
func validServiceRef(accept string) bool {
145
146
parts := strings.SplitN(accept, "#", 3)
146
147
if len(parts) != 2 {
···
156
157
return true
157
158
}
158
159
159
159
-
func ParseScope(scope string) (*Permission, error) {
160
160
+
func ParsePermissionString(scope string) (*Permission, error) {
160
161
g, err := ParseGenericScope(scope)
161
162
if err != nil {
162
163
return nil, err
···
171
172
case "account":
172
173
for k, _ := range g.Params {
173
174
if !(k == "attr" || k == "action") {
174
174
-
return nil, fmt.Errorf("%w: unexpected param: %s", ErrInvalidPermissionSyntax, k)
175
175
+
return nil, fmt.Errorf("%w: unsupported 'account' param: %s", ErrInvalidPermissionParams, k)
175
176
}
176
177
}
177
178
if g.Params.Has("attr") {
178
179
if g.Positional != "" || len(g.Params["attr"]) != 1 {
179
179
-
return nil, ErrInvalidPermissionSyntax
180
180
+
return nil, ErrInvalidPermissionParams
180
181
}
181
182
p.Attribute = g.Params.Get("attr")
182
183
}
···
184
185
p.Attribute = g.Positional
185
186
}
186
187
if p.Attribute == "" {
187
187
-
return nil, ErrInvalidPermissionSyntax
188
188
+
return nil, ErrInvalidPermissionParams
188
189
}
189
190
if p.Attribute != "" && p.Attribute != "email" && p.Attribute != "repo" {
190
190
-
return nil, ErrInvalidPermissionSyntax
191
191
+
return nil, ErrInvalidPermissionParams
191
192
}
193
193
+
// TODO: maybe this should not be limited to a single "action" string?
192
194
if len(g.Params["action"]) > 1 {
193
193
-
return nil, ErrInvalidPermissionSyntax
195
195
+
return nil, ErrInvalidPermissionParams
194
196
}
195
197
p.Action = g.Params["action"]
196
198
for _, act := range p.Action {
197
199
if act != "read" && act != "manage" {
198
198
-
return nil, ErrInvalidPermissionSyntax
200
200
+
return nil, ErrInvalidPermissionParams
199
201
}
200
202
}
201
203
case "blob":
202
204
for k, _ := range g.Params {
203
205
if !(k == "accept") {
204
204
-
return nil, fmt.Errorf("%w: unexpected param: %s", ErrInvalidPermissionSyntax, k)
206
206
+
return nil, fmt.Errorf("%w: unsupported 'blob' param: %s", ErrInvalidPermissionParams, k)
205
207
}
206
208
}
207
209
if g.Params.Has("accept") {
208
210
if g.Positional != "" {
209
209
-
return nil, ErrInvalidPermissionSyntax
211
211
+
return nil, ErrInvalidPermissionParams
210
212
}
211
213
p.Accept = g.Params["accept"]
212
214
}
···
214
216
p.Accept = []string{g.Positional}
215
217
}
216
218
if len(p.Accept) == 0 {
217
217
-
return nil, ErrInvalidPermissionSyntax
219
219
+
return nil, ErrInvalidPermissionParams
218
220
}
219
221
for _, acc := range p.Accept {
220
222
if !validBlobAccept(acc) {
221
221
-
return nil, ErrInvalidPermissionSyntax
223
223
+
return nil, ErrInvalidPermissionParams
222
224
}
223
225
}
224
226
case "identity":
225
227
for k, _ := range g.Params {
226
228
if !(k == "attr") {
227
227
-
return nil, fmt.Errorf("%w: unexpected param: %s", ErrInvalidPermissionSyntax, k)
229
229
+
return nil, fmt.Errorf("%w: unsupported 'identity' param: %s", ErrInvalidPermissionParams, k)
228
230
}
229
231
}
230
232
if g.Params.Has("attr") {
231
233
if g.Positional != "" || len(g.Params["attr"]) != 1 {
232
232
-
return nil, ErrInvalidPermissionSyntax
234
234
+
return nil, ErrInvalidPermissionParams
233
235
}
234
236
p.Attribute = g.Params.Get("attr")
235
237
}
···
237
239
p.Attribute = g.Positional
238
240
}
239
241
if p.Attribute != "*" && p.Attribute != "handle" {
240
240
-
return nil, ErrInvalidPermissionSyntax
242
242
+
return nil, ErrInvalidPermissionParams
241
243
}
242
244
case "include":
243
245
for k, _ := range g.Params {
244
246
if !(k == "nsid" || k == "aud") {
245
245
-
return nil, fmt.Errorf("%w: unexpected param: %s", ErrInvalidPermissionSyntax, k)
247
247
+
return nil, fmt.Errorf("%w: unsupported 'include' param: %s", ErrInvalidPermissionParams, k)
246
248
}
247
249
}
248
250
if g.Params.Has("nsid") {
249
251
if g.Positional != "" || len(g.Params["nsid"]) != 1 {
250
250
-
return nil, ErrInvalidPermissionSyntax
252
252
+
return nil, ErrInvalidPermissionParams
251
253
}
252
254
p.NSID = g.Params.Get("nsid")
253
255
}
···
256
258
}
257
259
_, err := syntax.ParseNSID(p.NSID)
258
260
if err != nil {
259
259
-
return nil, fmt.Errorf("%w: %w", ErrInvalidPermissionSyntax, err)
261
261
+
return nil, fmt.Errorf("%w: %w", ErrInvalidPermissionParams, err)
260
262
}
261
263
if g.Params.Has("aud") && (len(g.Params["aud"]) != 1 || g.Params.Get("aud") == "") {
262
262
-
return nil, ErrInvalidPermissionSyntax
264
264
+
return nil, ErrInvalidPermissionParams
263
265
}
264
266
p.Audience = g.Params.Get("aud")
265
267
if p.Audience != "" && p.Audience != "*" && !validServiceRef(p.Audience) {
266
266
-
return nil, ErrInvalidPermissionSyntax
268
268
+
return nil, ErrInvalidPermissionParams
267
269
}
268
270
// possibly other params in the future...
269
271
case "repo":
270
272
for k, _ := range g.Params {
271
273
if !(k == "collection" || k == "action") {
272
272
-
return nil, fmt.Errorf("%w: unexpected param: %s", ErrInvalidPermissionSyntax, k)
274
274
+
return nil, fmt.Errorf("%w: unsupported 'repo' param: %s", ErrInvalidPermissionParams, k)
273
275
}
274
276
}
275
277
if g.Params.Has("collection") {
276
278
if g.Positional != "" {
277
277
-
return nil, ErrInvalidPermissionSyntax
279
279
+
return nil, ErrInvalidPermissionParams
278
280
}
279
279
-
p.Collections = g.Params["collection"]
281
281
+
p.Collection = g.Params["collection"]
280
282
}
281
283
if g.Positional != "" {
282
282
-
p.Collections = []string{g.Positional}
284
284
+
p.Collection = []string{g.Positional}
283
285
}
284
284
-
if len(p.Collections) == 0 {
285
285
-
return nil, ErrInvalidPermissionSyntax
286
286
+
if len(p.Collection) == 0 {
287
287
+
return nil, ErrInvalidPermissionParams
286
288
}
287
287
-
for _, coll := range p.Collections {
289
289
+
for _, coll := range p.Collection {
288
290
if coll == "*" {
289
291
continue
290
292
}
291
293
_, err := syntax.ParseNSID(coll)
292
294
if err != nil {
293
293
-
return nil, fmt.Errorf("%w: %w", ErrInvalidPermissionSyntax, err)
295
295
+
return nil, fmt.Errorf("%w: %w", ErrInvalidPermissionParams, err)
294
296
}
295
297
}
296
298
p.Action = g.Params["action"]
297
299
for _, act := range p.Action {
298
300
if act != "create" && act != "update" && act != "delete" {
299
299
-
return nil, ErrInvalidPermissionSyntax
301
301
+
return nil, ErrInvalidPermissionParams
300
302
}
301
303
}
302
304
case "rpc":
303
305
for k, _ := range g.Params {
304
306
if !(k == "lxm" || k == "aud") {
305
305
-
return nil, fmt.Errorf("%w: unexpected param: %s", ErrInvalidPermissionSyntax, k)
307
307
+
return nil, fmt.Errorf("%w: unsupported 'rpc' param: %s", ErrInvalidPermissionParams, k)
306
308
}
307
309
}
308
310
if g.Params.Has("lxm") {
309
311
if g.Positional != "" {
310
310
-
return nil, ErrInvalidPermissionSyntax
312
312
+
return nil, ErrInvalidPermissionParams
311
313
}
312
312
-
p.Endpoints = g.Params["lxm"]
314
314
+
p.Endpoint = g.Params["lxm"]
313
315
}
314
316
if g.Positional != "" {
315
315
-
p.Endpoints = []string{g.Positional}
317
317
+
p.Endpoint = []string{g.Positional}
316
318
}
317
317
-
if len(p.Endpoints) == 0 {
318
318
-
return nil, ErrInvalidPermissionSyntax
319
319
+
if len(p.Endpoint) == 0 {
320
320
+
return nil, ErrInvalidPermissionParams
319
321
}
320
322
if len(g.Params["aud"]) != 1 {
321
321
-
return nil, ErrInvalidPermissionSyntax
323
323
+
return nil, ErrInvalidPermissionParams
322
324
}
323
325
p.Audience = g.Params.Get("aud")
324
324
-
for _, nsid := range p.Endpoints {
326
326
+
for _, nsid := range p.Endpoint {
325
327
if nsid == "*" {
326
328
if p.Audience == "*" {
327
327
-
return nil, ErrInvalidPermissionSyntax
329
329
+
return nil, ErrInvalidPermissionParams
328
330
}
329
331
continue
330
332
}
331
333
_, err := syntax.ParseNSID(nsid)
332
334
if err != nil {
333
333
-
return nil, fmt.Errorf("%w: %w", ErrInvalidPermissionSyntax, err)
335
335
+
return nil, fmt.Errorf("%w: %w", ErrInvalidPermissionParams, err)
334
336
}
335
337
}
336
338
if p.Audience != "*" && !validServiceRef(p.Audience) {
337
337
-
return nil, ErrInvalidPermissionSyntax
339
339
+
return nil, ErrInvalidPermissionParams
338
340
}
339
341
default:
340
340
-
return nil, ErrUnknownScope
342
342
+
return nil, fmt.Errorf("%w: %s", ErrUnknownResource, g.Resource)
341
343
}
342
344
return &p, nil
343
345
}
346
346
+
347
347
+
func isASCII(s string) bool {
348
348
+
for i := 0; i < len(s); i++ {
349
349
+
if s[i] > unicode.MaxASCII {
350
350
+
return false
351
351
+
}
352
352
+
}
353
353
+
return true
354
354
+
}
+29
-6
permissions/permission_test.go
···
27
27
}
28
28
29
29
for _, scope := range testScopes {
30
30
-
p, err := ParseScope(scope)
30
30
+
p, err := ParsePermissionString(scope)
31
31
assert.NoError(err)
32
32
if err != nil {
33
33
fmt.Println("BAD: " + scope)
34
34
continue
35
35
}
36
36
-
assert.Equal(scope, p.Scope())
36
36
+
assert.Equal(scope, p.ScopeString())
37
37
}
38
38
}
39
39
···
42
42
Generic GenericPermission `json:"generic"`
43
43
}
44
44
45
45
-
func TestGenericPermissions(t *testing.T) {
45
45
+
func TestGenericGenericScopesValid(t *testing.T) {
46
46
assert := assert.New(t)
47
47
file, err := os.Open("testdata/generic_scopes.json")
48
48
if err != nil {
···
68
68
}
69
69
}
70
70
71
71
+
func TestGenericScopesInvalid(t *testing.T) {
72
72
+
assert := assert.New(t)
73
73
+
file, err := os.Open("testdata/generic_scopes_invalid.txt")
74
74
+
if err != nil {
75
75
+
assert.NoError(err)
76
76
+
t.Fail()
77
77
+
}
78
78
+
defer file.Close()
79
79
+
scanner := bufio.NewScanner(file)
80
80
+
for scanner.Scan() {
81
81
+
line := scanner.Text()
82
82
+
if len(line) == 0 || line[0] == '#' {
83
83
+
continue
84
84
+
}
85
85
+
_, err := ParseGenericScope(line)
86
86
+
if err != nil {
87
87
+
fmt.Println("BAD: " + line)
88
88
+
}
89
89
+
assert.Error(err)
90
90
+
}
91
91
+
assert.NoError(scanner.Err())
92
92
+
}
93
93
+
71
94
func TestInteropPermissionValid(t *testing.T) {
72
95
assert := assert.New(t)
73
96
file, err := os.Open("testdata/permission_scopes_valid.txt")
···
87
110
fmt.Println("BAD: " + line)
88
111
}
89
112
assert.NoError(err)
90
90
-
p, err := ParseScope(line)
113
113
+
p, err := ParsePermissionString(line)
91
114
if err != nil {
92
115
fmt.Println("BAD: " + line)
93
116
}
94
117
assert.NoError(err)
95
118
if p != nil {
96
96
-
assert.False(p.Scope() == "")
119
119
+
assert.False(p.ScopeString() == "")
97
120
}
98
121
}
99
122
assert.NoError(scanner.Err())
···
113
136
if len(line) == 0 || line[0] == '#' {
114
137
continue
115
138
}
116
116
-
_, err := ParseScope(line)
139
139
+
_, err := ParsePermissionString(line)
117
140
if err == nil {
118
141
fmt.Println("BAD: " + line)
119
142
}
+71
permissions/testdata/generic_scopes.json
···
1
1
[
2
2
{
3
3
+
"scope": "resource",
4
4
+
"generic": {
5
5
+
"resource": "resource",
6
6
+
"positional": "",
7
7
+
"params": {}
8
8
+
}
9
9
+
},
10
10
+
{
11
11
+
"scope": "resource:positional?key=val",
12
12
+
"generic": {
13
13
+
"resource": "resource",
14
14
+
"positional": "positional",
15
15
+
"params": {
16
16
+
"key": ["val"]
17
17
+
}
18
18
+
}
19
19
+
},
20
20
+
{
21
21
+
"scope": "resource:positional?thing&key=val",
22
22
+
"generic": {
23
23
+
"resource": "resource",
24
24
+
"positional": "positional",
25
25
+
"params": {
26
26
+
"thing": [""],
27
27
+
"key": ["val"]
28
28
+
}
29
29
+
}
30
30
+
},
31
31
+
{
32
32
+
"scope": "service:did:web:com.example#type?key=val",
33
33
+
"generic": {
34
34
+
"resource": "service",
35
35
+
"positional": "did:web:com.example#type",
36
36
+
"params": {
37
37
+
"key": ["val"]
38
38
+
}
39
39
+
}
40
40
+
},
41
41
+
{
42
42
+
"scope": "resource:",
43
43
+
"generic": {
44
44
+
"resource": "resource",
45
45
+
"positional": "",
46
46
+
"params": {}
47
47
+
}
48
48
+
},
49
49
+
{
50
50
+
"scope": "resource:?",
51
51
+
"generic": {
52
52
+
"resource": "resource",
53
53
+
"positional": "",
54
54
+
"params": {}
55
55
+
}
56
56
+
},
57
57
+
{
58
58
+
"scope": "resource:&",
59
59
+
"generic": {
60
60
+
"resource": "resource",
61
61
+
"positional": "&",
62
62
+
"params": {}
63
63
+
}
64
64
+
},
65
65
+
{
66
66
+
"scope": "resource?",
67
67
+
"generic": {
68
68
+
"resource": "resource",
69
69
+
"positional": "",
70
70
+
"params": {}
71
71
+
}
72
72
+
},
73
73
+
{
3
74
"scope": "res:pos?p=true",
4
75
"generic": {
5
76
"resource": "res",
-10
permissions/testdata/generic_scopes_SKIP.json
···
1
1
-
[
2
2
-
{
3
3
-
"scope": "my-res:my%20pos",
4
4
-
"generic": {
5
5
-
"resource": "my-res",
6
6
-
"positional": "my pos",
7
7
-
"params": {}
8
8
-
}
9
9
-
}
10
10
-
]
+2
permissions/testdata/generic_scopes_invalid.txt
···
1
1
+
resource:positional?key=québec
2
2
+
emoji:☺️
+21
permissions/testdata/permission_scopes_valid.txt
···
37
37
rpc?aud=*&lxm=com.example.method1&lxm=com.example.method2
38
38
rpc:com.example.query?aud=did:web:api.example.com%23api_example
39
39
rpc?aud=did%3Aweb%3Aapi.example.com%23frag&lxm=com.example.query&lxm=com.example.procedure
40
40
+
41
41
+
42
42
+
# examples from specification text
43
43
+
repo:app.example.profile
44
44
+
repo:app.example.profile?action=create&action=update&action=delete
45
45
+
repo?collection=app.example.profile&collection=app.example.post
46
46
+
repo:*
47
47
+
repo:*?action=delete
48
48
+
rpc:app.example.moderation.createReport?aud=*
49
49
+
rpc?lxm=*&aud=did:web:api.example.com%23svc_appview
50
50
+
blob:*/*
51
51
+
blob?accept=video/*&accept=text/html
52
52
+
account:email
53
53
+
account:repo?action=manage
54
54
+
identity:handle
55
55
+
identity:*
56
56
+
identity:*?
57
57
+
rpc?lxm=*&aud=did:web:api.example.com%23svc_appview
58
58
+
blob?accept=video/*&accept=text/html
59
59
+
repo:app.example.profile?action=create&action=update&action=delete
60
60
+
include:app.example.authFull?aud=did:web:api.example.com%23svc_chat