@atproto/oauth-scopes 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/atproto-oauth-scope.d.ts +12 -0
- package/dist/atproto-oauth-scope.d.ts.map +1 -0
- package/dist/atproto-oauth-scope.js +32 -0
- package/dist/atproto-oauth-scope.js.map +1 -0
- package/dist/index.d.ts +9 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -13
- package/dist/index.js.map +1 -1
- package/dist/lib/lexicon.d.ts +2 -0
- package/dist/lib/lexicon.d.ts.map +1 -0
- package/dist/lib/lexicon.js +3 -0
- package/dist/lib/lexicon.js.map +1 -0
- package/dist/lib/mime.d.ts +1 -1
- package/dist/lib/mime.d.ts.map +1 -1
- package/dist/lib/mime.js +2 -0
- package/dist/lib/mime.js.map +1 -1
- package/dist/lib/nsid.d.ts +2 -2
- package/dist/lib/nsid.d.ts.map +1 -1
- package/dist/lib/nsid.js +4 -6
- package/dist/lib/nsid.js.map +1 -1
- package/dist/lib/parser.d.ts +29 -0
- package/dist/lib/parser.d.ts.map +1 -0
- package/dist/lib/parser.js +152 -0
- package/dist/lib/parser.js.map +1 -0
- package/dist/lib/resource-permission.d.ts +10 -0
- package/dist/lib/resource-permission.d.ts.map +1 -0
- package/dist/lib/resource-permission.js +3 -0
- package/dist/lib/resource-permission.js.map +1 -0
- package/dist/lib/syntax-lexicon.d.ts +26 -0
- package/dist/lib/syntax-lexicon.d.ts.map +1 -0
- package/dist/lib/syntax-lexicon.js +58 -0
- package/dist/lib/syntax-lexicon.js.map +1 -0
- package/dist/lib/syntax-string.d.ts +16 -0
- package/dist/lib/syntax-string.d.ts.map +1 -0
- package/dist/lib/syntax-string.js +121 -0
- package/dist/lib/syntax-string.js.map +1 -0
- package/dist/lib/syntax.d.ts +23 -0
- package/dist/lib/syntax.d.ts.map +1 -0
- package/dist/lib/syntax.js +22 -0
- package/dist/lib/syntax.js.map +1 -0
- package/dist/lib/util.d.ts +4 -1
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +4 -12
- package/dist/lib/util.js.map +1 -1
- package/dist/scope-permissions-transition.d.ts +15 -0
- package/dist/scope-permissions-transition.d.ts.map +1 -0
- package/dist/{permission-set-transition.js → scope-permissions-transition.js} +5 -5
- package/dist/scope-permissions-transition.js.map +1 -0
- package/dist/scope-permissions.d.ts +22 -0
- package/dist/scope-permissions.d.ts.map +1 -0
- package/dist/{permission-set.js → scope-permissions.js} +20 -16
- package/dist/scope-permissions.js.map +1 -0
- package/dist/scopes/account-permission.d.ts +35 -0
- package/dist/scopes/account-permission.d.ts.map +1 -0
- package/dist/scopes/account-permission.js +71 -0
- package/dist/scopes/account-permission.js.map +1 -0
- package/dist/scopes/blob-permission.d.ts +27 -0
- package/dist/scopes/blob-permission.d.ts.map +1 -0
- package/dist/scopes/blob-permission.js +86 -0
- package/dist/scopes/blob-permission.js.map +1 -0
- package/dist/scopes/identity-permission.d.ts +25 -0
- package/dist/scopes/identity-permission.d.ts.map +1 -0
- package/dist/scopes/identity-permission.js +53 -0
- package/dist/scopes/identity-permission.js.map +1 -0
- package/dist/scopes/include-scope.d.ts +54 -0
- package/dist/scopes/include-scope.d.ts.map +1 -0
- package/dist/scopes/include-scope.js +156 -0
- package/dist/scopes/include-scope.js.map +1 -0
- package/dist/scopes/repo-permission.d.ts +40 -0
- package/dist/scopes/repo-permission.d.ts.map +1 -0
- package/dist/scopes/repo-permission.js +101 -0
- package/dist/scopes/repo-permission.js.map +1 -0
- package/dist/scopes/rpc-permission.d.ts +38 -0
- package/dist/scopes/rpc-permission.d.ts.map +1 -0
- package/dist/scopes/rpc-permission.js +81 -0
- package/dist/scopes/rpc-permission.js.map +1 -0
- package/dist/scopes-set.d.ts +12 -1
- package/dist/scopes-set.d.ts.map +1 -1
- package/dist/scopes-set.js +49 -3
- package/dist/scopes-set.js.map +1 -1
- package/package.json +7 -3
- package/src/atproto-oauth-scope.ts +43 -0
- package/src/index.ts +10 -14
- package/src/lib/lexicon.ts +1 -0
- package/src/lib/mime.ts +2 -1
- package/src/lib/nsid.ts +5 -6
- package/src/lib/parser.ts +176 -0
- package/src/lib/resource-permission.ts +10 -0
- package/src/lib/syntax-lexicon.ts +55 -0
- package/src/lib/syntax-string.test.ts +130 -0
- package/src/lib/syntax-string.ts +132 -0
- package/src/lib/syntax.test.ts +43 -0
- package/src/lib/syntax.ts +47 -0
- package/src/lib/util.ts +7 -12
- package/src/{permission-set-transition.test.ts → scope-permissions-transition.test.ts} +33 -20
- package/src/{permission-set-transition.ts → scope-permissions-transition.ts} +11 -11
- package/src/{permission-set.test.ts → scope-permissions.test.ts} +77 -35
- package/src/scope-permissions.ts +91 -0
- package/src/{resources/account-scope.test.ts → scopes/account-permission.test.ts} +45 -33
- package/src/scopes/account-permission.ts +75 -0
- package/src/{resources/blob-scope.test.ts → scopes/blob-permission.test.ts} +31 -23
- package/src/scopes/blob-permission.ts +105 -0
- package/src/{resources/identity-scope.test.ts → scopes/identity-permission.test.ts} +13 -13
- package/src/scopes/identity-permission.ts +54 -0
- package/src/scopes/include-scope.test.ts +626 -0
- package/src/scopes/include-scope.ts +168 -0
- package/src/{resources/repo-scope.test.ts → scopes/repo-permission.test.ts} +77 -65
- package/src/scopes/repo-permission.ts +111 -0
- package/src/scopes/rpc-permission.test.ts +323 -0
- package/src/scopes/rpc-permission.ts +85 -0
- package/src/scopes-set.test.ts +5 -5
- package/src/scopes-set.ts +79 -5
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
- package/dist/lib/did.d.ts +0 -3
- package/dist/lib/did.d.ts.map +0 -1
- package/dist/lib/did.js +0 -6
- package/dist/lib/did.js.map +0 -1
- package/dist/parser.d.ts +0 -31
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -118
- package/dist/parser.js.map +0 -1
- package/dist/permission-set-transition.d.ts +0 -15
- package/dist/permission-set-transition.d.ts.map +0 -1
- package/dist/permission-set-transition.js.map +0 -1
- package/dist/permission-set.d.ts +0 -22
- package/dist/permission-set.d.ts.map +0 -1
- package/dist/permission-set.js.map +0 -1
- package/dist/resources/account-scope.d.ts +0 -35
- package/dist/resources/account-scope.d.ts.map +0 -1
- package/dist/resources/account-scope.js +0 -60
- package/dist/resources/account-scope.js.map +0 -1
- package/dist/resources/blob-scope.d.ts +0 -25
- package/dist/resources/blob-scope.d.ts.map +0 -1
- package/dist/resources/blob-scope.js +0 -74
- package/dist/resources/blob-scope.js.map +0 -1
- package/dist/resources/identity-scope.d.ts +0 -25
- package/dist/resources/identity-scope.d.ts.map +0 -1
- package/dist/resources/identity-scope.js +0 -46
- package/dist/resources/identity-scope.js.map +0 -1
- package/dist/resources/repo-scope.d.ts +0 -37
- package/dist/resources/repo-scope.d.ts.map +0 -1
- package/dist/resources/repo-scope.js +0 -92
- package/dist/resources/repo-scope.js.map +0 -1
- package/dist/resources/rpc-scope.d.ts +0 -31
- package/dist/resources/rpc-scope.d.ts.map +0 -1
- package/dist/resources/rpc-scope.js +0 -74
- package/dist/resources/rpc-scope.js.map +0 -1
- package/dist/syntax.d.ts +0 -76
- package/dist/syntax.d.ts.map +0 -1
- package/dist/syntax.js +0 -249
- package/dist/syntax.js.map +0 -1
- package/dist/utilities.d.ts +0 -17
- package/dist/utilities.d.ts.map +0 -1
- package/dist/utilities.js +0 -108
- package/dist/utilities.js.map +0 -1
- package/src/lib/did.ts +0 -3
- package/src/parser.ts +0 -150
- package/src/permission-set.ts +0 -78
- package/src/resources/account-scope.ts +0 -66
- package/src/resources/blob-scope.ts +0 -86
- package/src/resources/identity-scope.ts +0 -49
- package/src/resources/repo-scope.ts +0 -101
- package/src/resources/rpc-scope.test.ts +0 -280
- package/src/resources/rpc-scope.ts +0 -77
- package/src/syntax.test.ts +0 -203
- package/src/syntax.ts +0 -325
- package/src/utilities.ts +0 -109
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ScopePermissions } from './scope-permissions.js'
|
|
2
2
|
|
|
3
|
-
describe('
|
|
3
|
+
describe('ScopePermissions', () => {
|
|
4
4
|
describe('allowsAccount', () => {
|
|
5
5
|
it('should properly allow "account:email"', () => {
|
|
6
|
-
const set = new
|
|
6
|
+
const set = new ScopePermissions('account:email')
|
|
7
7
|
|
|
8
8
|
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)
|
|
9
9
|
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
|
|
@@ -18,7 +18,7 @@ describe('PermissionSet', () => {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
it('should ignore "transition:email"', () => {
|
|
21
|
-
const set = new
|
|
21
|
+
const set = new ScopePermissions('transition:email')
|
|
22
22
|
|
|
23
23
|
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(false)
|
|
24
24
|
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
|
|
@@ -27,31 +27,31 @@ describe('PermissionSet', () => {
|
|
|
27
27
|
|
|
28
28
|
describe('allowsBlob', () => {
|
|
29
29
|
it('should allow any mime with "blob:*/*"', () => {
|
|
30
|
-
const set = new
|
|
30
|
+
const set = new ScopePermissions('blob:*/*')
|
|
31
31
|
expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
|
|
32
32
|
expect(set.allowsBlob({ mime: 'application/json' })).toBe(true)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
it('should only allow images with "blob:image/*"', () => {
|
|
36
|
-
const set = new
|
|
36
|
+
const set = new ScopePermissions('blob:image/*')
|
|
37
37
|
expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
|
|
38
38
|
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
it('should ignore invalid scope "blob:*"', () => {
|
|
42
|
-
const set = new
|
|
42
|
+
const set = new ScopePermissions('blob:*')
|
|
43
43
|
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
44
44
|
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
it('should ignore invalid scope "blob:/image"', () => {
|
|
48
|
-
const set = new
|
|
48
|
+
const set = new ScopePermissions('blob:/image')
|
|
49
49
|
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
50
50
|
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
it('should ignore "transition:generic"', () => {
|
|
54
|
-
const set = new
|
|
54
|
+
const set = new ScopePermissions('transition:generic')
|
|
55
55
|
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
56
56
|
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
57
57
|
})
|
|
@@ -59,7 +59,7 @@ describe('PermissionSet', () => {
|
|
|
59
59
|
|
|
60
60
|
describe('allowsRepo', () => {
|
|
61
61
|
it('should allow any repo action with "repo:*"', () => {
|
|
62
|
-
const set = new
|
|
62
|
+
const set = new ScopePermissions('repo:*')
|
|
63
63
|
expect(
|
|
64
64
|
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
65
65
|
).toBe(true)
|
|
@@ -72,7 +72,7 @@ describe('PermissionSet', () => {
|
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
it('should allow specific repo actions', () => {
|
|
75
|
-
const set = new
|
|
75
|
+
const set = new ScopePermissions('repo:*?action=create')
|
|
76
76
|
expect(
|
|
77
77
|
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
78
78
|
).toBe(true)
|
|
@@ -91,7 +91,7 @@ describe('PermissionSet', () => {
|
|
|
91
91
|
})
|
|
92
92
|
|
|
93
93
|
it('should allow specific repo collection & actions', () => {
|
|
94
|
-
const set = new
|
|
94
|
+
const set = new ScopePermissions('repo:com.example.foo?action=create')
|
|
95
95
|
expect(
|
|
96
96
|
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
97
97
|
).toBe(true)
|
|
@@ -110,7 +110,7 @@ describe('PermissionSet', () => {
|
|
|
110
110
|
})
|
|
111
111
|
|
|
112
112
|
it('should ignore transition:generic', () => {
|
|
113
|
-
const set = new
|
|
113
|
+
const set = new ScopePermissions('transition:generic')
|
|
114
114
|
expect(
|
|
115
115
|
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
116
116
|
).toBe(false)
|
|
@@ -128,19 +128,28 @@ describe('PermissionSet', () => {
|
|
|
128
128
|
|
|
129
129
|
describe('allowsRpc', () => {
|
|
130
130
|
it('should ignore "rpc:*?lxm=*"', () => {
|
|
131
|
-
const set = new
|
|
131
|
+
const set = new ScopePermissions('rpc:*?lxm=*')
|
|
132
132
|
expect(
|
|
133
|
-
set.allowsRpc({
|
|
133
|
+
set.allowsRpc({
|
|
134
|
+
aud: 'did:web:example.com',
|
|
135
|
+
lxm: 'com.example.method',
|
|
136
|
+
}),
|
|
134
137
|
).toBe(false)
|
|
135
138
|
expect(
|
|
136
|
-
set.allowsRpc({
|
|
139
|
+
set.allowsRpc({
|
|
140
|
+
aud: 'did:web:example.com',
|
|
141
|
+
lxm: 'app.bsky.feed.getFeed',
|
|
142
|
+
}),
|
|
137
143
|
).toBe(false)
|
|
138
144
|
})
|
|
139
145
|
|
|
140
146
|
it('should allow constraining "lxm"', () => {
|
|
141
|
-
const set = new
|
|
147
|
+
const set = new ScopePermissions('rpc:app.bsky.feed.getFeed?aud=*')
|
|
142
148
|
expect(
|
|
143
|
-
set.allowsRpc({
|
|
149
|
+
set.allowsRpc({
|
|
150
|
+
aud: 'did:web:example.com',
|
|
151
|
+
lxm: 'app.bsky.feed.getFeed',
|
|
152
|
+
}),
|
|
144
153
|
).toBe(true)
|
|
145
154
|
expect(
|
|
146
155
|
set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
|
|
@@ -149,38 +158,62 @@ describe('PermissionSet', () => {
|
|
|
149
158
|
// Control
|
|
150
159
|
|
|
151
160
|
expect(
|
|
152
|
-
set.allowsRpc({
|
|
161
|
+
set.allowsRpc({
|
|
162
|
+
aud: 'did:web:example.com',
|
|
163
|
+
lxm: 'com.example.method',
|
|
164
|
+
}),
|
|
153
165
|
).toBe(false)
|
|
154
166
|
})
|
|
155
167
|
|
|
156
168
|
it('should allow constraining "aud"', () => {
|
|
157
|
-
const set = new
|
|
169
|
+
const set = new ScopePermissions('rpc:*?aud=did:web:example.com%23foo')
|
|
158
170
|
expect(
|
|
159
|
-
set.allowsRpc({
|
|
171
|
+
set.allowsRpc({
|
|
172
|
+
aud: 'did:web:example.com#foo',
|
|
173
|
+
lxm: 'com.example.method',
|
|
174
|
+
}),
|
|
160
175
|
).toBe(true)
|
|
161
176
|
expect(
|
|
162
|
-
set.allowsRpc({
|
|
177
|
+
set.allowsRpc({
|
|
178
|
+
aud: 'did:web:example.com#foo',
|
|
179
|
+
lxm: 'app.bsky.feed.getFeed',
|
|
180
|
+
}),
|
|
163
181
|
).toBe(true)
|
|
164
182
|
|
|
165
183
|
// Control
|
|
166
184
|
|
|
167
185
|
expect(
|
|
168
|
-
set.allowsRpc({
|
|
186
|
+
set.allowsRpc({
|
|
187
|
+
aud: 'did:web:bar.com#foo', // invalid aud (wrong service id)
|
|
188
|
+
lxm: 'com.example.method',
|
|
189
|
+
}),
|
|
190
|
+
).toBe(false)
|
|
191
|
+
expect(
|
|
192
|
+
set.allowsRpc({
|
|
193
|
+
aud: 'did:web:example.com', // invalid aud (no service id)
|
|
194
|
+
lxm: 'com.example.method',
|
|
195
|
+
}),
|
|
169
196
|
).toBe(false)
|
|
170
197
|
})
|
|
171
198
|
|
|
172
199
|
it('should allow constraining "lxm" and "aud"', () => {
|
|
173
|
-
const set = new
|
|
174
|
-
'rpc:app.bsky.feed.getFeed?aud=did:example
|
|
200
|
+
const set = new ScopePermissions(
|
|
201
|
+
'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23foo',
|
|
175
202
|
)
|
|
176
203
|
expect(
|
|
177
|
-
set.allowsRpc({
|
|
204
|
+
set.allowsRpc({
|
|
205
|
+
aud: 'did:web:example.com#foo',
|
|
206
|
+
lxm: 'app.bsky.feed.getFeed',
|
|
207
|
+
}),
|
|
178
208
|
).toBe(true)
|
|
179
209
|
|
|
180
210
|
// Control
|
|
181
211
|
|
|
182
212
|
expect(
|
|
183
|
-
set.allowsRpc({
|
|
213
|
+
set.allowsRpc({
|
|
214
|
+
aud: 'did:web:example.com',
|
|
215
|
+
lxm: 'com.example.method',
|
|
216
|
+
}),
|
|
184
217
|
).toBe(false)
|
|
185
218
|
expect(
|
|
186
219
|
set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
|
|
@@ -188,26 +221,32 @@ describe('PermissionSet', () => {
|
|
|
188
221
|
})
|
|
189
222
|
|
|
190
223
|
it('should ignore "transition:generic"', () => {
|
|
191
|
-
const set = new
|
|
224
|
+
const set = new ScopePermissions('transition:generic')
|
|
192
225
|
expect(
|
|
193
|
-
set.allowsRpc({
|
|
226
|
+
set.allowsRpc({
|
|
227
|
+
aud: 'did:web:example.com',
|
|
228
|
+
lxm: 'app.bsky.feed.getFeed',
|
|
229
|
+
}),
|
|
194
230
|
).toBe(false)
|
|
195
231
|
expect(
|
|
196
|
-
set.allowsRpc({
|
|
232
|
+
set.allowsRpc({
|
|
233
|
+
aud: 'did:web:example.com',
|
|
234
|
+
lxm: 'com.example.method',
|
|
235
|
+
}),
|
|
197
236
|
).toBe(false)
|
|
198
237
|
})
|
|
199
238
|
|
|
200
239
|
it('should ignore "transition:chat.bsky"', () => {
|
|
201
|
-
const set = new
|
|
240
|
+
const set = new ScopePermissions('transition:chat.bsky')
|
|
202
241
|
expect(
|
|
203
242
|
set.allowsRpc({
|
|
204
|
-
aud: 'did:example
|
|
243
|
+
aud: 'did:web:example.com',
|
|
205
244
|
lxm: 'chat.bsky.message.send',
|
|
206
245
|
}),
|
|
207
246
|
).toBe(false)
|
|
208
247
|
expect(
|
|
209
248
|
set.allowsRpc({
|
|
210
|
-
aud: 'did:example
|
|
249
|
+
aud: 'did:web:example.com',
|
|
211
250
|
lxm: 'chat.bsky.conversation.get',
|
|
212
251
|
}),
|
|
213
252
|
).toBe(false)
|
|
@@ -215,10 +254,13 @@ describe('PermissionSet', () => {
|
|
|
215
254
|
// Control
|
|
216
255
|
|
|
217
256
|
expect(
|
|
218
|
-
set.allowsRpc({
|
|
257
|
+
set.allowsRpc({
|
|
258
|
+
aud: 'did:web:example.com',
|
|
259
|
+
lxm: 'app.bsky.feed.post',
|
|
260
|
+
}),
|
|
219
261
|
).toBe(false)
|
|
220
262
|
expect(
|
|
221
|
-
set.allowsRpc({ aud: 'did:example
|
|
263
|
+
set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
|
|
222
264
|
).toBe(false)
|
|
223
265
|
})
|
|
224
266
|
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ScopeMissingError } from './scope-missing-error.js'
|
|
2
|
+
import {
|
|
3
|
+
AccountPermission,
|
|
4
|
+
AccountPermissionMatch,
|
|
5
|
+
} from './scopes/account-permission.js'
|
|
6
|
+
import {
|
|
7
|
+
BlobPermission,
|
|
8
|
+
BlobPermissionMatch,
|
|
9
|
+
} from './scopes/blob-permission.js'
|
|
10
|
+
import {
|
|
11
|
+
IdentityPermission,
|
|
12
|
+
IdentityPermissionMatch,
|
|
13
|
+
} from './scopes/identity-permission.js'
|
|
14
|
+
import {
|
|
15
|
+
RepoPermission,
|
|
16
|
+
RepoPermissionMatch,
|
|
17
|
+
} from './scopes/repo-permission.js'
|
|
18
|
+
import { RpcPermission, RpcPermissionMatch } from './scopes/rpc-permission.js'
|
|
19
|
+
import { ScopesSet } from './scopes-set.js'
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
AccountPermissionMatch,
|
|
23
|
+
BlobPermissionMatch,
|
|
24
|
+
IdentityPermissionMatch,
|
|
25
|
+
RepoPermissionMatch,
|
|
26
|
+
RpcPermissionMatch,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ScopePermissions {
|
|
30
|
+
public readonly scopes: ScopesSet
|
|
31
|
+
|
|
32
|
+
constructor(scope?: null | string | Iterable<string>) {
|
|
33
|
+
this.scopes = new ScopesSet(
|
|
34
|
+
!scope // "" | null | undefined
|
|
35
|
+
? undefined
|
|
36
|
+
: typeof scope === 'string'
|
|
37
|
+
? scope.split(' ')
|
|
38
|
+
: scope,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public allowsAccount(options: AccountPermissionMatch): boolean {
|
|
43
|
+
return this.scopes.matches('account', options)
|
|
44
|
+
}
|
|
45
|
+
public assertAccount(options: AccountPermissionMatch): void {
|
|
46
|
+
if (!this.allowsAccount(options)) {
|
|
47
|
+
const scope = AccountPermission.scopeNeededFor(options)
|
|
48
|
+
throw new ScopeMissingError(scope)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public allowsIdentity(options: IdentityPermissionMatch): boolean {
|
|
53
|
+
return this.scopes.matches('identity', options)
|
|
54
|
+
}
|
|
55
|
+
public assertIdentity(options: IdentityPermissionMatch): void {
|
|
56
|
+
if (!this.allowsIdentity(options)) {
|
|
57
|
+
const scope = IdentityPermission.scopeNeededFor(options)
|
|
58
|
+
throw new ScopeMissingError(scope)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public allowsBlob(options: BlobPermissionMatch): boolean {
|
|
63
|
+
return this.scopes.matches('blob', options)
|
|
64
|
+
}
|
|
65
|
+
public assertBlob(options: BlobPermissionMatch): void {
|
|
66
|
+
if (!this.allowsBlob(options)) {
|
|
67
|
+
const scope = BlobPermission.scopeNeededFor(options)
|
|
68
|
+
throw new ScopeMissingError(scope)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public allowsRepo(options: RepoPermissionMatch): boolean {
|
|
73
|
+
return this.scopes.matches('repo', options)
|
|
74
|
+
}
|
|
75
|
+
public assertRepo(options: RepoPermissionMatch): void {
|
|
76
|
+
if (!this.allowsRepo(options)) {
|
|
77
|
+
const scope = RepoPermission.scopeNeededFor(options)
|
|
78
|
+
throw new ScopeMissingError(scope)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public allowsRpc(options: RpcPermissionMatch): boolean {
|
|
83
|
+
return this.scopes.matches('rpc', options)
|
|
84
|
+
}
|
|
85
|
+
public assertRpc(options: RpcPermissionMatch): void {
|
|
86
|
+
if (!this.allowsRpc(options)) {
|
|
87
|
+
const scope = RpcPermission.scopeNeededFor(options)
|
|
88
|
+
throw new ScopeMissingError(scope)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1,67 +1,71 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AccountPermission } from './account-permission.js'
|
|
2
2
|
|
|
3
|
-
describe('
|
|
3
|
+
describe('AccountPermission', () => {
|
|
4
4
|
describe('static', () => {
|
|
5
5
|
describe('fromString', () => {
|
|
6
6
|
it('should parse valid scope strings', () => {
|
|
7
|
-
const scope1 =
|
|
7
|
+
const scope1 = AccountPermission.fromString('account:email?action=read')
|
|
8
8
|
expect(scope1).not.toBeNull()
|
|
9
9
|
expect(scope1!.attr).toBe('email')
|
|
10
10
|
expect(scope1!.action).toBe('read')
|
|
11
11
|
|
|
12
|
-
const scope2 =
|
|
12
|
+
const scope2 = AccountPermission.fromString(
|
|
13
|
+
'account:repo?action=manage',
|
|
14
|
+
)
|
|
13
15
|
expect(scope2).not.toBeNull()
|
|
14
16
|
expect(scope2!.attr).toBe('repo')
|
|
15
17
|
expect(scope2!.action).toBe('manage')
|
|
16
18
|
})
|
|
17
19
|
|
|
18
20
|
it('should parse scope without action (defaults to read)', () => {
|
|
19
|
-
const scope =
|
|
21
|
+
const scope = AccountPermission.fromString('account:status')
|
|
20
22
|
expect(scope).not.toBeNull()
|
|
21
23
|
expect(scope!.attr).toBe('status')
|
|
22
24
|
expect(scope!.action).toBe('read')
|
|
23
25
|
})
|
|
24
26
|
|
|
25
27
|
it('should reject invalid attribute names', () => {
|
|
26
|
-
const scope =
|
|
28
|
+
const scope = AccountPermission.fromString('account:invalid')
|
|
27
29
|
expect(scope).toBeNull()
|
|
28
30
|
})
|
|
29
31
|
|
|
30
32
|
it('should reject invalid action names', () => {
|
|
31
|
-
const scope =
|
|
33
|
+
const scope = AccountPermission.fromString(
|
|
34
|
+
'account:email?action=invalid',
|
|
35
|
+
)
|
|
32
36
|
expect(scope).toBeNull()
|
|
33
37
|
})
|
|
34
38
|
|
|
35
39
|
it('should reject malformed scope strings', () => {
|
|
36
|
-
expect(
|
|
37
|
-
expect(
|
|
38
|
-
expect(
|
|
39
|
-
expect(
|
|
40
|
+
expect(AccountPermission.fromString('invalid:email')).toBeNull()
|
|
41
|
+
expect(AccountPermission.fromString('account')).toBeNull()
|
|
42
|
+
expect(AccountPermission.fromString('')).toBeNull()
|
|
43
|
+
expect(AccountPermission.fromString('account:')).toBeNull()
|
|
40
44
|
})
|
|
41
45
|
})
|
|
42
46
|
|
|
43
47
|
describe('scopeNeededFor', () => {
|
|
44
48
|
it('should return correct scope string for read actions', () => {
|
|
45
49
|
expect(
|
|
46
|
-
|
|
50
|
+
AccountPermission.scopeNeededFor({ attr: 'email', action: 'read' }),
|
|
47
51
|
).toBe('account:email')
|
|
48
52
|
expect(
|
|
49
|
-
|
|
53
|
+
AccountPermission.scopeNeededFor({ attr: 'repo', action: 'read' }),
|
|
50
54
|
).toBe('account:repo')
|
|
51
55
|
expect(
|
|
52
|
-
|
|
56
|
+
AccountPermission.scopeNeededFor({ attr: 'status', action: 'read' }),
|
|
53
57
|
).toBe('account:status')
|
|
54
58
|
})
|
|
55
59
|
|
|
56
60
|
it('should return correct scope string for manage actions', () => {
|
|
57
61
|
expect(
|
|
58
|
-
|
|
62
|
+
AccountPermission.scopeNeededFor({ attr: 'email', action: 'manage' }),
|
|
59
63
|
).toBe('account:email?action=manage')
|
|
60
64
|
expect(
|
|
61
|
-
|
|
65
|
+
AccountPermission.scopeNeededFor({ attr: 'repo', action: 'manage' }),
|
|
62
66
|
).toBe('account:repo?action=manage')
|
|
63
67
|
expect(
|
|
64
|
-
|
|
68
|
+
AccountPermission.scopeNeededFor({
|
|
65
69
|
attr: 'status',
|
|
66
70
|
action: 'manage',
|
|
67
71
|
}),
|
|
@@ -73,40 +77,44 @@ describe('AccountScope', () => {
|
|
|
73
77
|
describe('instance', () => {
|
|
74
78
|
describe('matches', () => {
|
|
75
79
|
it('should match read action', () => {
|
|
76
|
-
const scope =
|
|
80
|
+
const scope = AccountPermission.fromString('account:email?action=read')
|
|
77
81
|
expect(scope).not.toBeNull()
|
|
78
82
|
expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
|
|
79
83
|
})
|
|
80
84
|
|
|
81
85
|
it('should match manage action', () => {
|
|
82
|
-
const scope =
|
|
86
|
+
const scope = AccountPermission.fromString('account:repo?action=manage')
|
|
83
87
|
expect(scope).not.toBeNull()
|
|
84
88
|
expect(scope!.matches({ attr: 'repo', action: 'manage' })).toBe(true)
|
|
85
89
|
})
|
|
86
90
|
|
|
87
91
|
it('should not match unspecified action', () => {
|
|
88
|
-
const scope =
|
|
92
|
+
const scope = AccountPermission.fromString('account:email?action=read')
|
|
89
93
|
expect(scope).not.toBeNull()
|
|
90
94
|
expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)
|
|
91
95
|
})
|
|
92
96
|
|
|
93
97
|
it('should not match different attribute', () => {
|
|
94
|
-
const scope =
|
|
98
|
+
const scope = AccountPermission.fromString('account:email?action=read')
|
|
95
99
|
expect(scope).not.toBeNull()
|
|
96
100
|
expect(scope!.matches({ attr: 'repo', action: 'read' })).toBe(false)
|
|
97
101
|
})
|
|
98
102
|
|
|
99
103
|
it('should default to "read" action', () => {
|
|
100
|
-
const scope =
|
|
104
|
+
const scope = AccountPermission.fromString('account:email')
|
|
101
105
|
expect(scope).not.toBeNull()
|
|
102
106
|
expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
|
|
103
107
|
expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)
|
|
104
108
|
})
|
|
105
109
|
|
|
106
110
|
it('should work with all valid attributes', () => {
|
|
107
|
-
const emailScope =
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
const emailScope = AccountPermission.fromString(
|
|
112
|
+
'account:email?action=read',
|
|
113
|
+
)
|
|
114
|
+
const repoScope = AccountPermission.fromString(
|
|
115
|
+
'account:repo?action=manage',
|
|
116
|
+
)
|
|
117
|
+
const statusScope = AccountPermission.fromString(
|
|
110
118
|
'account:status?action=read',
|
|
111
119
|
)
|
|
112
120
|
|
|
@@ -126,7 +134,9 @@ describe('AccountScope', () => {
|
|
|
126
134
|
})
|
|
127
135
|
|
|
128
136
|
it('should allow read when "manage" action is specified', () => {
|
|
129
|
-
const scope =
|
|
137
|
+
const scope = AccountPermission.fromString(
|
|
138
|
+
'account:email?action=manage',
|
|
139
|
+
)
|
|
130
140
|
expect(scope).not.toBeNull()
|
|
131
141
|
expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
|
|
132
142
|
})
|
|
@@ -134,24 +144,26 @@ describe('AccountScope', () => {
|
|
|
134
144
|
|
|
135
145
|
describe('toString', () => {
|
|
136
146
|
it('should format scope with explicit action', () => {
|
|
137
|
-
const scope = new
|
|
147
|
+
const scope = new AccountPermission('email', 'manage')
|
|
138
148
|
expect(scope.toString()).toBe('account:email?action=manage')
|
|
139
149
|
})
|
|
140
150
|
|
|
141
151
|
it('should format scope with default action', () => {
|
|
142
|
-
const scope = new
|
|
152
|
+
const scope = new AccountPermission('repo', 'read')
|
|
143
153
|
expect(scope.toString()).toBe('account:repo')
|
|
144
154
|
})
|
|
145
155
|
|
|
146
156
|
it('should format all attributes correctly', () => {
|
|
147
|
-
expect(new
|
|
157
|
+
expect(new AccountPermission('email', 'read').toString()).toBe(
|
|
148
158
|
'account:email',
|
|
149
159
|
)
|
|
150
|
-
expect(new
|
|
151
|
-
|
|
160
|
+
expect(new AccountPermission('repo', 'read').toString()).toBe(
|
|
161
|
+
'account:repo',
|
|
162
|
+
)
|
|
163
|
+
expect(new AccountPermission('status', 'read').toString()).toBe(
|
|
152
164
|
'account:status',
|
|
153
165
|
)
|
|
154
|
-
expect(new
|
|
166
|
+
expect(new AccountPermission('email', 'manage').toString()).toBe(
|
|
155
167
|
'account:email?action=manage',
|
|
156
168
|
)
|
|
157
169
|
})
|
|
@@ -169,7 +181,7 @@ describe('AccountScope', () => {
|
|
|
169
181
|
]
|
|
170
182
|
|
|
171
183
|
for (const scope of testCases) {
|
|
172
|
-
expect(
|
|
184
|
+
expect(AccountPermission.fromString(scope)?.toString()).toBe(scope)
|
|
173
185
|
}
|
|
174
186
|
})
|
|
175
187
|
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Parser } from '../lib/parser.js'
|
|
2
|
+
import { ResourcePermission } from '../lib/resource-permission.js'
|
|
3
|
+
import { ScopeStringSyntax } from '../lib/syntax-string.js'
|
|
4
|
+
import { ScopeSyntax, isScopeStringFor } from '../lib/syntax.js'
|
|
5
|
+
import { knownValuesValidator } from '../lib/util.js'
|
|
6
|
+
|
|
7
|
+
export const ACCOUNT_ATTRIBUTES = Object.freeze([
|
|
8
|
+
'email',
|
|
9
|
+
'repo',
|
|
10
|
+
'status',
|
|
11
|
+
] as const)
|
|
12
|
+
export type AccountAttribute = (typeof ACCOUNT_ATTRIBUTES)[number]
|
|
13
|
+
|
|
14
|
+
export const ACCOUNT_ACTIONS = Object.freeze(['read', 'manage'] as const)
|
|
15
|
+
export type AccountAction = (typeof ACCOUNT_ACTIONS)[number]
|
|
16
|
+
|
|
17
|
+
export type AccountPermissionMatch = {
|
|
18
|
+
attr: AccountAttribute
|
|
19
|
+
action: AccountAction
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class AccountPermission
|
|
23
|
+
implements ResourcePermission<'account', AccountPermissionMatch>
|
|
24
|
+
{
|
|
25
|
+
constructor(
|
|
26
|
+
public readonly attr: AccountAttribute,
|
|
27
|
+
public readonly action: AccountAction,
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
matches(options: AccountPermissionMatch) {
|
|
31
|
+
return (
|
|
32
|
+
this.attr === options.attr &&
|
|
33
|
+
(this.action === 'manage' || this.action === options.action)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toString() {
|
|
38
|
+
return AccountPermission.parser.format(this)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected static readonly parser = new Parser(
|
|
42
|
+
'account',
|
|
43
|
+
{
|
|
44
|
+
attr: {
|
|
45
|
+
multiple: false,
|
|
46
|
+
required: true,
|
|
47
|
+
validate: knownValuesValidator(ACCOUNT_ATTRIBUTES),
|
|
48
|
+
},
|
|
49
|
+
action: {
|
|
50
|
+
multiple: false,
|
|
51
|
+
required: false,
|
|
52
|
+
validate: knownValuesValidator(ACCOUNT_ACTIONS),
|
|
53
|
+
default: 'read' as const,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
'attr',
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
static fromString(scope: string) {
|
|
60
|
+
if (!isScopeStringFor(scope, 'account')) return null
|
|
61
|
+
const syntax = ScopeStringSyntax.fromString(scope)
|
|
62
|
+
return AccountPermission.fromSyntax(syntax)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static fromSyntax(syntax: ScopeSyntax<'account'>) {
|
|
66
|
+
const result = AccountPermission.parser.parse(syntax)
|
|
67
|
+
if (!result) return null
|
|
68
|
+
|
|
69
|
+
return new AccountPermission(result.attr, result.action)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static scopeNeededFor(options: AccountPermissionMatch) {
|
|
73
|
+
return AccountPermission.parser.format(options)
|
|
74
|
+
}
|
|
75
|
+
}
|