tangled
alpha
login
or
join now
bnewbold.net
/
cobalt
13
fork
atom
go scratch code for atproto
13
fork
atom
overview
issues
pulls
pipelines
initial auth permission parsing/serialization
bnewbold.net
8 months ago
0ca140bb
e040070a
+341
5 changed files
expand all
collapse all
unified
split
atproto
auth
permission.go
permission_test.go
testdata
permission_scopes_invalid.txt
permission_scopes_other.txt
permission_scopes_valid.txt
+212
atproto/auth/permission.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package auth
2
+
3
+
import (
4
+
"errors"
5
+
"fmt"
6
+
"net/url"
7
+
"strconv"
8
+
"strings"
9
+
)
10
+
11
+
var (
12
+
ErrInvalidPermissionSyntax = errors.New("invalid permission syntax")
13
+
ErrUnknownScope = errors.New("unknown scope type")
14
+
)
15
+
16
+
type Permission struct {
17
+
Type string `json:"type,omitempty"`
18
+
Resource string `json:"resource"`
19
+
20
+
// repo
21
+
Collections []string `json:"collection,omitempty"`
22
+
Action string `json:"action,omitempty"`
23
+
24
+
// rpc
25
+
Endpoints []string `json:"lxm,omitempty"`
26
+
Audience string `json:"aud,omitempty"`
27
+
28
+
// blob
29
+
MaxSize *uint64 `json:"maxSize,omitempty"`
30
+
Accept []string `json:"accept,omitempty"`
31
+
32
+
// account
33
+
Read []string `json:"read,omitempty"`
34
+
Manage []string `json:"manage,omitempty"`
35
+
36
+
// identity
37
+
DID []string `json:"did,omitempty"`
38
+
PLC []string `json:"plc,omitempty"`
39
+
40
+
// include
41
+
PermissionSet string `json:"permissionSet,omitempty"`
42
+
}
43
+
44
+
func (p *Permission) Scope() string {
45
+
46
+
positional := ""
47
+
params := make(url.Values)
48
+
49
+
switch p.Resource {
50
+
case "repo":
51
+
if len(p.Collections) == 1 {
52
+
positional = p.Collections[0]
53
+
} else if len(p.Collections) > 1 {
54
+
params["collection"] = p.Collections
55
+
}
56
+
if p.Action != "" {
57
+
params.Set("action", p.Action)
58
+
}
59
+
case "rpc":
60
+
if len(p.Endpoints) == 1 {
61
+
positional = p.Endpoints[0]
62
+
} else if len(p.Endpoints) > 1 {
63
+
params["lxm"] = p.Endpoints
64
+
}
65
+
if p.Audience != "" {
66
+
params.Set("aud", p.Audience)
67
+
}
68
+
case "blob":
69
+
if p.MaxSize != nil {
70
+
params.Set("maxSize", strconv.Itoa(int(*p.MaxSize)))
71
+
}
72
+
if len(p.Accept) == 1 {
73
+
positional = p.Accept[0]
74
+
} else if len(p.Accept) > 1 {
75
+
params["accept"] = p.Accept
76
+
}
77
+
case "account":
78
+
if len(p.Read) == 1 {
79
+
positional = p.Read[0]
80
+
} else if len(p.Read) > 1 {
81
+
params["read"] = p.Read
82
+
}
83
+
if len(p.Manage) > 0 {
84
+
params["manage"] = p.Manage
85
+
}
86
+
case "identity":
87
+
if len(p.DID) == 1 {
88
+
positional = p.DID[0]
89
+
} else if len(p.DID) > 1 {
90
+
params["did"] = p.DID
91
+
}
92
+
if len(p.PLC) > 0 {
93
+
params["plc"] = p.PLC
94
+
}
95
+
case "include":
96
+
if p.PermissionSet != "" {
97
+
positional = p.PermissionSet
98
+
}
99
+
// TODO: other params...
100
+
if p.Audience != "" {
101
+
params.Set("aud", p.Audience)
102
+
}
103
+
default:
104
+
return ""
105
+
}
106
+
107
+
scope := p.Resource
108
+
if positional != "" {
109
+
scope = scope + ":" + positional
110
+
}
111
+
if len(params) > 0 {
112
+
scope = scope + "?" + params.Encode()
113
+
}
114
+
return scope
115
+
}
116
+
117
+
func ParseScope(scope string) (*Permission, error) {
118
+
119
+
front, query, _ := strings.Cut(scope, "?")
120
+
resource, positional, _ := strings.Cut(front, ":")
121
+
122
+
params, err := url.ParseQuery(query)
123
+
if err != nil {
124
+
return nil, fmt.Errorf("%w: %w", ErrInvalidPermissionSyntax, err)
125
+
}
126
+
127
+
p := Permission{
128
+
Type: "permission",
129
+
Resource: resource,
130
+
}
131
+
132
+
// TODO: should unknown fields be an error?
133
+
// TODO: could pre-parse in all the various fields? and then just positional per type
134
+
switch resource {
135
+
case "repo":
136
+
if params.Has("collection") {
137
+
if positional != "" {
138
+
return nil, ErrInvalidPermissionSyntax
139
+
}
140
+
p.Collections = params["collection"]
141
+
}
142
+
if positional != "" {
143
+
p.Collections = []string{positional}
144
+
}
145
+
p.Action = params.Get("action")
146
+
case "rpc":
147
+
if params.Has("lxm") {
148
+
if positional != "" {
149
+
return nil, ErrInvalidPermissionSyntax
150
+
}
151
+
p.Endpoints = params["lxm"]
152
+
}
153
+
if positional != "" {
154
+
p.Endpoints = []string{positional}
155
+
}
156
+
p.Audience = params.Get("aud")
157
+
case "blob":
158
+
if params.Has("accept") {
159
+
if positional != "" {
160
+
return nil, ErrInvalidPermissionSyntax
161
+
}
162
+
p.Accept = params["accept"]
163
+
}
164
+
if positional != "" {
165
+
p.Accept = []string{positional}
166
+
}
167
+
if params.Has("maxSize") {
168
+
v, err := strconv.ParseUint(params.Get("maxSize"), 10, 64)
169
+
if err != nil {
170
+
return nil, fmt.Errorf("%w: %w", ErrInvalidPermissionSyntax, err)
171
+
}
172
+
p.MaxSize = &v
173
+
}
174
+
case "account":
175
+
if params.Has("read") {
176
+
if positional != "" {
177
+
return nil, ErrInvalidPermissionSyntax
178
+
}
179
+
p.Read = params["read"]
180
+
}
181
+
if positional != "" {
182
+
p.Read = []string{positional}
183
+
}
184
+
p.Manage = params["manage"]
185
+
case "identity":
186
+
if params.Has("did") {
187
+
if positional != "" {
188
+
return nil, ErrInvalidPermissionSyntax
189
+
}
190
+
p.DID = params["did"]
191
+
}
192
+
if positional != "" {
193
+
p.DID = []string{positional}
194
+
}
195
+
p.PLC = params["plc"]
196
+
case "include":
197
+
if params.Has("permissionSet") {
198
+
if positional != "" {
199
+
return nil, ErrInvalidPermissionSyntax
200
+
}
201
+
p.PermissionSet = params.Get("permissionSet")
202
+
}
203
+
if positional != "" {
204
+
p.PermissionSet = positional
205
+
}
206
+
// TODO: also parse most other params...
207
+
p.Audience = params.Get("aud")
208
+
default:
209
+
return nil, ErrUnknownScope
210
+
}
211
+
return &p, nil
212
+
}
+99
atproto/auth/permission_test.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package auth
2
+
3
+
import (
4
+
"bufio"
5
+
"fmt"
6
+
"os"
7
+
"testing"
8
+
9
+
"github.com/stretchr/testify/assert"
10
+
)
11
+
12
+
func TestRoundTrip(t *testing.T) {
13
+
assert := assert.New(t)
14
+
15
+
// NOTE: this escapes colons and slashes, which aren't strictly necessary
16
+
testScopes := []string{
17
+
"repo:com.example.record?action=all",
18
+
"repo?action=all&collection=com.example.record&collection=com.example.other",
19
+
"rpc:com.example.query?aud=did%3Aweb%3Aapi.example.com%23frag",
20
+
"rpc?aud=did%3Aweb%3Aapi.example.com%23frag&lxm=com.example.query&lxm=com.example.procedure",
21
+
"blob:image/*",
22
+
"blob?accept=image%2Fpng&accept=image%2Fjpeg&maxSize=123",
23
+
"account:email?manage=deactivate",
24
+
"identity:handle?plc=rotation",
25
+
"include:app.example.authBasics",
26
+
}
27
+
28
+
for _, scope := range testScopes {
29
+
p, err := ParseScope(scope)
30
+
assert.NoError(err)
31
+
if err != nil {
32
+
continue
33
+
}
34
+
assert.Equal(scope, p.Scope())
35
+
}
36
+
}
37
+
38
+
func TestInteropPermissionValid(t *testing.T) {
39
+
assert := assert.New(t)
40
+
file, err := os.Open("testdata/permission_scopes_valid.txt")
41
+
assert.NoError(err)
42
+
defer file.Close()
43
+
scanner := bufio.NewScanner(file)
44
+
for scanner.Scan() {
45
+
line := scanner.Text()
46
+
if len(line) == 0 || line[0] == '#' {
47
+
continue
48
+
}
49
+
p, err := ParseScope(line)
50
+
if err != nil {
51
+
fmt.Println("BAD: " + line)
52
+
}
53
+
assert.NoError(err)
54
+
if p != nil {
55
+
assert.False(p.Scope() == "")
56
+
}
57
+
}
58
+
assert.NoError(scanner.Err())
59
+
}
60
+
61
+
func TestInteropPermissionInvalid(t *testing.T) {
62
+
assert := assert.New(t)
63
+
file, err := os.Open("testdata/permission_scopes_invalid.txt")
64
+
assert.NoError(err)
65
+
defer file.Close()
66
+
scanner := bufio.NewScanner(file)
67
+
for scanner.Scan() {
68
+
line := scanner.Text()
69
+
if len(line) == 0 || line[0] == '#' {
70
+
continue
71
+
}
72
+
_, err := ParseScope(line)
73
+
if err == nil {
74
+
fmt.Println("BAD: " + line)
75
+
}
76
+
assert.Error(err)
77
+
}
78
+
assert.NoError(scanner.Err())
79
+
}
80
+
81
+
func TestInteropPermissionOther(t *testing.T) {
82
+
assert := assert.New(t)
83
+
file, err := os.Open("testdata/permission_scopes_other.txt")
84
+
assert.NoError(err)
85
+
defer file.Close()
86
+
scanner := bufio.NewScanner(file)
87
+
for scanner.Scan() {
88
+
line := scanner.Text()
89
+
if len(line) == 0 || line[0] == '#' {
90
+
continue
91
+
}
92
+
_, err := ParseScope(line)
93
+
if err == nil {
94
+
fmt.Println("BAD: " + line)
95
+
}
96
+
assert.Error(err)
97
+
}
98
+
assert.NoError(scanner.Err())
99
+
}
+13
atproto/auth/testdata/permission_scopes_invalid.txt
···
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
2
+
blob:image/png?maxSize=-123
3
+
blob:image/png?maxSize=blah
4
+
blob:image/png?maxSize
5
+
blob:image/png?maxSize=123?maxSize=123
6
+
7
+
# TODO: these partial strings
8
+
#repo:123
9
+
#repo
10
+
#repo:
11
+
#rpc:123
12
+
#rpc
13
+
#rpc:com.example.query?aud=api.example.com
+3
atproto/auth/testdata/permission_scopes_other.txt
···
0
0
0
···
1
+
atproto
2
+
blah
3
+
unknown:resource?type=true
+14
atproto/auth/testdata/permission_scopes_valid.txt
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
repo:com.example.record
2
+
repo:com.example.record?action=*
3
+
repo:*
4
+
repo?action=all&collection=com.example.record&collection=com.example.other
5
+
6
+
rpc:com.example.query?aud=did:web:api.example.com%23api_example
7
+
rpc?aud=did%3Aweb%3Aapi.example.com%23frag&lxm=com.example.query&lxm=com.example.procedure
8
+
9
+
blob:image/*?maxSize=2000
10
+
blob?accept=image%2Fpng&accept=image%2Fjpeg&maxSize=123
11
+
12
+
account:email?manage=deactivate
13
+
identity:handle?plc=rotation
14
+
include:app.example.authBasics