@atproto/oauth-scopes 0.5.1 → 0.5.3
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 +22 -0
- package/package.json +10 -6
- package/jest.config.cjs +0 -14
- package/src/atproto-oauth-scope.ts +0 -79
- package/src/index.ts +0 -13
- package/src/lib/lexicon.ts +0 -21
- package/src/lib/mime.test.ts +0 -98
- package/src/lib/mime.ts +0 -71
- package/src/lib/nsid.ts +0 -5
- package/src/lib/parser.ts +0 -176
- package/src/lib/resource-permission.ts +0 -10
- package/src/lib/syntax-lexicon.ts +0 -55
- package/src/lib/syntax-string.test.ts +0 -130
- package/src/lib/syntax-string.ts +0 -132
- package/src/lib/syntax.test.ts +0 -43
- package/src/lib/syntax.ts +0 -54
- package/src/lib/util.ts +0 -18
- package/src/scope-missing-error.ts +0 -15
- package/src/scope-permissions-transition.test.ts +0 -122
- package/src/scope-permissions-transition.ts +0 -71
- package/src/scope-permissions.test.ts +0 -303
- package/src/scope-permissions.ts +0 -91
- package/src/scopes/account-permission.test.ts +0 -187
- package/src/scopes/account-permission.ts +0 -78
- package/src/scopes/blob-permission.test.ts +0 -126
- package/src/scopes/blob-permission.ts +0 -105
- package/src/scopes/identity-permission.test.ts +0 -80
- package/src/scopes/identity-permission.ts +0 -54
- package/src/scopes/include-scope.test.ts +0 -637
- package/src/scopes/include-scope.ts +0 -208
- package/src/scopes/repo-permission.test.ts +0 -267
- package/src/scopes/repo-permission.ts +0 -111
- package/src/scopes/rpc-permission.test.ts +0 -323
- package/src/scopes/rpc-permission.ts +0 -90
- package/src/scopes-set.test.ts +0 -47
- package/src/scopes-set.ts +0 -134
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { ScopePermissions } from './scope-permissions.js'
|
|
2
|
-
|
|
3
|
-
describe('ScopePermissions', () => {
|
|
4
|
-
describe('allowsAccount', () => {
|
|
5
|
-
it('should properly allow "account:email"', () => {
|
|
6
|
-
const set = new ScopePermissions('account:email')
|
|
7
|
-
|
|
8
|
-
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)
|
|
9
|
-
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
|
|
10
|
-
|
|
11
|
-
expect(set.allowsAccount({ attr: 'repo', action: 'read' })).toBe(false)
|
|
12
|
-
expect(set.allowsAccount({ attr: 'repo', action: 'manage' })).toBe(false)
|
|
13
|
-
|
|
14
|
-
expect(set.allowsAccount({ attr: 'status', action: 'read' })).toBe(false)
|
|
15
|
-
expect(set.allowsAccount({ attr: 'status', action: 'manage' })).toBe(
|
|
16
|
-
false,
|
|
17
|
-
)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('should ignore "transition:email"', () => {
|
|
21
|
-
const set = new ScopePermissions('transition:email')
|
|
22
|
-
|
|
23
|
-
expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(false)
|
|
24
|
-
expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe('allowsBlob', () => {
|
|
29
|
-
it('should allow any mime with "blob:*/*"', () => {
|
|
30
|
-
const set = new ScopePermissions('blob:*/*')
|
|
31
|
-
expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
|
|
32
|
-
expect(set.allowsBlob({ mime: 'application/json' })).toBe(true)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('should only allow images with "blob:image/*"', () => {
|
|
36
|
-
const set = new ScopePermissions('blob:image/*')
|
|
37
|
-
expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)
|
|
38
|
-
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('should ignore invalid scope "blob:*"', () => {
|
|
42
|
-
const set = new ScopePermissions('blob:*')
|
|
43
|
-
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
44
|
-
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('should ignore invalid scope "blob:/image"', () => {
|
|
48
|
-
const set = new ScopePermissions('blob:/image')
|
|
49
|
-
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
50
|
-
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('should ignore "transition:generic"', () => {
|
|
54
|
-
const set = new ScopePermissions('transition:generic')
|
|
55
|
-
expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)
|
|
56
|
-
expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
describe('allowsRepo', () => {
|
|
61
|
-
it('should allow any repo action with "repo:*"', () => {
|
|
62
|
-
const set = new ScopePermissions('repo:*')
|
|
63
|
-
expect(
|
|
64
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
65
|
-
).toBe(true)
|
|
66
|
-
expect(
|
|
67
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
68
|
-
).toBe(true)
|
|
69
|
-
expect(
|
|
70
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
71
|
-
).toBe(true)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('should allow specific repo actions', () => {
|
|
75
|
-
const set = new ScopePermissions('repo:*?action=create')
|
|
76
|
-
expect(
|
|
77
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
78
|
-
).toBe(true)
|
|
79
|
-
expect(
|
|
80
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
81
|
-
).toBe(true)
|
|
82
|
-
|
|
83
|
-
// Control
|
|
84
|
-
|
|
85
|
-
expect(
|
|
86
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
87
|
-
).toBe(false)
|
|
88
|
-
expect(
|
|
89
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
90
|
-
).toBe(false)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('should allow specific repo collection & actions', () => {
|
|
94
|
-
const set = new ScopePermissions('repo:com.example.foo?action=create')
|
|
95
|
-
expect(
|
|
96
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
97
|
-
).toBe(true)
|
|
98
|
-
|
|
99
|
-
// Control
|
|
100
|
-
|
|
101
|
-
expect(
|
|
102
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
103
|
-
).toBe(false)
|
|
104
|
-
expect(
|
|
105
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
106
|
-
).toBe(false)
|
|
107
|
-
expect(
|
|
108
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
109
|
-
).toBe(false)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('should ignore transition:generic', () => {
|
|
113
|
-
const set = new ScopePermissions('transition:generic')
|
|
114
|
-
expect(
|
|
115
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),
|
|
116
|
-
).toBe(false)
|
|
117
|
-
expect(
|
|
118
|
-
set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),
|
|
119
|
-
).toBe(false)
|
|
120
|
-
expect(
|
|
121
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),
|
|
122
|
-
).toBe(false)
|
|
123
|
-
expect(
|
|
124
|
-
set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),
|
|
125
|
-
).toBe(false)
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
describe('allowsRpc', () => {
|
|
130
|
-
it('should ignore "rpc:*?lxm=*"', () => {
|
|
131
|
-
const set = new ScopePermissions('rpc:*?lxm=*')
|
|
132
|
-
expect(
|
|
133
|
-
set.allowsRpc({
|
|
134
|
-
aud: 'did:web:example.com',
|
|
135
|
-
lxm: 'com.example.method',
|
|
136
|
-
}),
|
|
137
|
-
).toBe(false)
|
|
138
|
-
expect(
|
|
139
|
-
set.allowsRpc({
|
|
140
|
-
aud: 'did:web:example.com',
|
|
141
|
-
lxm: 'app.bsky.feed.getFeed',
|
|
142
|
-
}),
|
|
143
|
-
).toBe(false)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('should allow constraining "lxm"', () => {
|
|
147
|
-
const set = new ScopePermissions('rpc:app.bsky.feed.getFeed?aud=*')
|
|
148
|
-
expect(
|
|
149
|
-
set.allowsRpc({
|
|
150
|
-
aud: 'did:web:example.com',
|
|
151
|
-
lxm: 'app.bsky.feed.getFeed',
|
|
152
|
-
}),
|
|
153
|
-
).toBe(true)
|
|
154
|
-
expect(
|
|
155
|
-
set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
|
|
156
|
-
).toBe(true)
|
|
157
|
-
|
|
158
|
-
// Control
|
|
159
|
-
|
|
160
|
-
expect(
|
|
161
|
-
set.allowsRpc({
|
|
162
|
-
aud: 'did:web:example.com',
|
|
163
|
-
lxm: 'com.example.method',
|
|
164
|
-
}),
|
|
165
|
-
).toBe(false)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('should allow constraining "aud"', () => {
|
|
169
|
-
const set = new ScopePermissions('rpc:*?aud=did:web:example.com%23foo')
|
|
170
|
-
expect(
|
|
171
|
-
set.allowsRpc({
|
|
172
|
-
aud: 'did:web:example.com#foo',
|
|
173
|
-
lxm: 'com.example.method',
|
|
174
|
-
}),
|
|
175
|
-
).toBe(true)
|
|
176
|
-
expect(
|
|
177
|
-
set.allowsRpc({
|
|
178
|
-
aud: 'did:web:example.com#foo',
|
|
179
|
-
lxm: 'app.bsky.feed.getFeed',
|
|
180
|
-
}),
|
|
181
|
-
).toBe(true)
|
|
182
|
-
|
|
183
|
-
// Control
|
|
184
|
-
|
|
185
|
-
expect(
|
|
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
|
-
}),
|
|
196
|
-
).toBe(false)
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('should allow constraining "lxm" and "aud"', () => {
|
|
200
|
-
const set = new ScopePermissions(
|
|
201
|
-
'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23foo',
|
|
202
|
-
)
|
|
203
|
-
expect(
|
|
204
|
-
set.allowsRpc({
|
|
205
|
-
aud: 'did:web:example.com#foo',
|
|
206
|
-
lxm: 'app.bsky.feed.getFeed',
|
|
207
|
-
}),
|
|
208
|
-
).toBe(true)
|
|
209
|
-
|
|
210
|
-
// Control
|
|
211
|
-
|
|
212
|
-
expect(
|
|
213
|
-
set.allowsRpc({
|
|
214
|
-
aud: 'did:web:example.com',
|
|
215
|
-
lxm: 'com.example.method',
|
|
216
|
-
}),
|
|
217
|
-
).toBe(false)
|
|
218
|
-
expect(
|
|
219
|
-
set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),
|
|
220
|
-
).toBe(false)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('should ignore "transition:generic"', () => {
|
|
224
|
-
const set = new ScopePermissions('transition:generic')
|
|
225
|
-
expect(
|
|
226
|
-
set.allowsRpc({
|
|
227
|
-
aud: 'did:web:example.com',
|
|
228
|
-
lxm: 'app.bsky.feed.getFeed',
|
|
229
|
-
}),
|
|
230
|
-
).toBe(false)
|
|
231
|
-
expect(
|
|
232
|
-
set.allowsRpc({
|
|
233
|
-
aud: 'did:web:example.com',
|
|
234
|
-
lxm: 'com.example.method',
|
|
235
|
-
}),
|
|
236
|
-
).toBe(false)
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
it('should ignore "transition:chat.bsky"', () => {
|
|
240
|
-
const set = new ScopePermissions('transition:chat.bsky')
|
|
241
|
-
expect(
|
|
242
|
-
set.allowsRpc({
|
|
243
|
-
aud: 'did:web:example.com',
|
|
244
|
-
lxm: 'chat.bsky.message.send',
|
|
245
|
-
}),
|
|
246
|
-
).toBe(false)
|
|
247
|
-
expect(
|
|
248
|
-
set.allowsRpc({
|
|
249
|
-
aud: 'did:web:example.com',
|
|
250
|
-
lxm: 'chat.bsky.conversation.get',
|
|
251
|
-
}),
|
|
252
|
-
).toBe(false)
|
|
253
|
-
|
|
254
|
-
// Control
|
|
255
|
-
|
|
256
|
-
expect(
|
|
257
|
-
set.allowsRpc({
|
|
258
|
-
aud: 'did:web:example.com',
|
|
259
|
-
lxm: 'app.bsky.feed.post',
|
|
260
|
-
}),
|
|
261
|
-
).toBe(false)
|
|
262
|
-
expect(
|
|
263
|
-
set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),
|
|
264
|
-
).toBe(false)
|
|
265
|
-
})
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
describe('assertRpc combined-aud', () => {
|
|
269
|
-
it('allows did#serviceId aud when scope grants the same combined form', () => {
|
|
270
|
-
const set = new ScopePermissions(
|
|
271
|
-
'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23bsky_appview',
|
|
272
|
-
)
|
|
273
|
-
expect(() =>
|
|
274
|
-
set.assertRpc({
|
|
275
|
-
aud: 'did:web:example.com#bsky_appview',
|
|
276
|
-
lxm: 'app.bsky.feed.getFeed',
|
|
277
|
-
}),
|
|
278
|
-
).not.toThrow()
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
it('rejects bare-DID aud when scope grants a combined form', () => {
|
|
282
|
-
const set = new ScopePermissions(
|
|
283
|
-
'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23bsky_appview',
|
|
284
|
-
)
|
|
285
|
-
expect(() =>
|
|
286
|
-
set.assertRpc({
|
|
287
|
-
aud: 'did:web:example.com',
|
|
288
|
-
lxm: 'app.bsky.feed.getFeed',
|
|
289
|
-
}),
|
|
290
|
-
).toThrow()
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('allows wildcard aud against a combined-form match', () => {
|
|
294
|
-
const set = new ScopePermissions('rpc:app.bsky.feed.getFeed?aud=*')
|
|
295
|
-
expect(() =>
|
|
296
|
-
set.assertRpc({
|
|
297
|
-
aud: 'did:web:example.com#bsky_appview',
|
|
298
|
-
lxm: 'app.bsky.feed.getFeed',
|
|
299
|
-
}),
|
|
300
|
-
).not.toThrow()
|
|
301
|
-
})
|
|
302
|
-
})
|
|
303
|
-
})
|
package/src/scope-permissions.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
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,187 +0,0 @@
|
|
|
1
|
-
import { AccountPermission } from './account-permission.js'
|
|
2
|
-
|
|
3
|
-
describe('AccountPermission', () => {
|
|
4
|
-
describe('static', () => {
|
|
5
|
-
describe('fromString', () => {
|
|
6
|
-
it('should parse valid scope strings', () => {
|
|
7
|
-
const scope1 = AccountPermission.fromString('account:email?action=read')
|
|
8
|
-
expect(scope1).not.toBeNull()
|
|
9
|
-
expect(scope1!.attr).toBe('email')
|
|
10
|
-
expect(scope1!.action).toEqual(['read'])
|
|
11
|
-
|
|
12
|
-
const scope2 = AccountPermission.fromString(
|
|
13
|
-
'account:repo?action=manage',
|
|
14
|
-
)
|
|
15
|
-
expect(scope2).not.toBeNull()
|
|
16
|
-
expect(scope2!.attr).toBe('repo')
|
|
17
|
-
expect(scope2!.action).toEqual(['manage'])
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('should parse scope without action (defaults to read)', () => {
|
|
21
|
-
const scope = AccountPermission.fromString('account:status')
|
|
22
|
-
expect(scope).not.toBeNull()
|
|
23
|
-
expect(scope!.attr).toBe('status')
|
|
24
|
-
expect(scope!.action).toEqual(['read'])
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('should reject invalid attribute names', () => {
|
|
28
|
-
const scope = AccountPermission.fromString('account:invalid')
|
|
29
|
-
expect(scope).toBeNull()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('should reject invalid action names', () => {
|
|
33
|
-
const scope = AccountPermission.fromString(
|
|
34
|
-
'account:email?action=invalid',
|
|
35
|
-
)
|
|
36
|
-
expect(scope).toBeNull()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('should reject malformed scope strings', () => {
|
|
40
|
-
expect(AccountPermission.fromString('invalid:email')).toBeNull()
|
|
41
|
-
expect(AccountPermission.fromString('account')).toBeNull()
|
|
42
|
-
expect(AccountPermission.fromString('')).toBeNull()
|
|
43
|
-
expect(AccountPermission.fromString('account:')).toBeNull()
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('scopeNeededFor', () => {
|
|
48
|
-
it('should return correct scope string for read actions', () => {
|
|
49
|
-
expect(
|
|
50
|
-
AccountPermission.scopeNeededFor({ attr: 'email', action: 'read' }),
|
|
51
|
-
).toBe('account:email')
|
|
52
|
-
expect(
|
|
53
|
-
AccountPermission.scopeNeededFor({ attr: 'repo', action: 'read' }),
|
|
54
|
-
).toBe('account:repo')
|
|
55
|
-
expect(
|
|
56
|
-
AccountPermission.scopeNeededFor({ attr: 'status', action: 'read' }),
|
|
57
|
-
).toBe('account:status')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('should return correct scope string for manage actions', () => {
|
|
61
|
-
expect(
|
|
62
|
-
AccountPermission.scopeNeededFor({ attr: 'email', action: 'manage' }),
|
|
63
|
-
).toBe('account:email?action=manage')
|
|
64
|
-
expect(
|
|
65
|
-
AccountPermission.scopeNeededFor({ attr: 'repo', action: 'manage' }),
|
|
66
|
-
).toBe('account:repo?action=manage')
|
|
67
|
-
expect(
|
|
68
|
-
AccountPermission.scopeNeededFor({
|
|
69
|
-
attr: 'status',
|
|
70
|
-
action: 'manage',
|
|
71
|
-
}),
|
|
72
|
-
).toBe('account:status?action=manage')
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('instance', () => {
|
|
78
|
-
describe('matches', () => {
|
|
79
|
-
it('should match read action', () => {
|
|
80
|
-
const scope = AccountPermission.fromString('account:email?action=read')
|
|
81
|
-
expect(scope).not.toBeNull()
|
|
82
|
-
expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('should match manage action', () => {
|
|
86
|
-
const scope = AccountPermission.fromString('account:repo?action=manage')
|
|
87
|
-
expect(scope).not.toBeNull()
|
|
88
|
-
expect(scope!.matches({ attr: 'repo', action: 'manage' })).toBe(true)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('should not match unspecified action', () => {
|
|
92
|
-
const scope = AccountPermission.fromString('account:email?action=read')
|
|
93
|
-
expect(scope).not.toBeNull()
|
|
94
|
-
expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('should not match different attribute', () => {
|
|
98
|
-
const scope = AccountPermission.fromString('account:email?action=read')
|
|
99
|
-
expect(scope).not.toBeNull()
|
|
100
|
-
expect(scope!.matches({ attr: 'repo', action: 'read' })).toBe(false)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('should default to "read" action', () => {
|
|
104
|
-
const scope = AccountPermission.fromString('account:email')
|
|
105
|
-
expect(scope).not.toBeNull()
|
|
106
|
-
expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
|
|
107
|
-
expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('should work with all valid attributes', () => {
|
|
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(
|
|
118
|
-
'account:status?action=read',
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
expect(emailScope).not.toBeNull()
|
|
122
|
-
expect(repoScope).not.toBeNull()
|
|
123
|
-
expect(statusScope).not.toBeNull()
|
|
124
|
-
|
|
125
|
-
expect(emailScope!.matches({ attr: 'email', action: 'read' })).toBe(
|
|
126
|
-
true,
|
|
127
|
-
)
|
|
128
|
-
expect(repoScope!.matches({ attr: 'repo', action: 'manage' })).toBe(
|
|
129
|
-
true,
|
|
130
|
-
)
|
|
131
|
-
expect(statusScope!.matches({ attr: 'status', action: 'read' })).toBe(
|
|
132
|
-
true,
|
|
133
|
-
)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('should allow read when "manage" action is specified', () => {
|
|
137
|
-
const scope = AccountPermission.fromString(
|
|
138
|
-
'account:email?action=manage',
|
|
139
|
-
)
|
|
140
|
-
expect(scope).not.toBeNull()
|
|
141
|
-
expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)
|
|
142
|
-
})
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
describe('toString', () => {
|
|
146
|
-
it('should format scope with explicit action', () => {
|
|
147
|
-
const scope = new AccountPermission('email', ['manage'])
|
|
148
|
-
expect(scope.toString()).toBe('account:email?action=manage')
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('should format scope with default action', () => {
|
|
152
|
-
const scope = new AccountPermission('repo', ['read'])
|
|
153
|
-
expect(scope.toString()).toBe('account:repo')
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('should format all attributes correctly', () => {
|
|
157
|
-
expect(new AccountPermission('email', ['read']).toString()).toBe(
|
|
158
|
-
'account:email',
|
|
159
|
-
)
|
|
160
|
-
expect(new AccountPermission('repo', ['read']).toString()).toBe(
|
|
161
|
-
'account:repo',
|
|
162
|
-
)
|
|
163
|
-
expect(new AccountPermission('status', ['read']).toString()).toBe(
|
|
164
|
-
'account:status',
|
|
165
|
-
)
|
|
166
|
-
expect(new AccountPermission('email', ['manage']).toString()).toBe(
|
|
167
|
-
'account:email?action=manage',
|
|
168
|
-
)
|
|
169
|
-
})
|
|
170
|
-
})
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('should maintain consistency between toString and fromString', () => {
|
|
174
|
-
const testCases = [
|
|
175
|
-
'account:email',
|
|
176
|
-
'account:email?action=manage',
|
|
177
|
-
'account:repo',
|
|
178
|
-
'account:repo?action=manage',
|
|
179
|
-
'account:status',
|
|
180
|
-
'account:status?action=manage',
|
|
181
|
-
]
|
|
182
|
-
|
|
183
|
-
for (const scope of testCases) {
|
|
184
|
-
expect(AccountPermission.fromString(scope)?.toString()).toBe(scope)
|
|
185
|
-
}
|
|
186
|
-
})
|
|
187
|
-
})
|
|
@@ -1,78 +0,0 @@
|
|
|
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 { NeRoArray, 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: NeRoArray<AccountAction>,
|
|
28
|
-
) {}
|
|
29
|
-
|
|
30
|
-
matches(options: AccountPermissionMatch) {
|
|
31
|
-
return (
|
|
32
|
-
this.attr === options.attr &&
|
|
33
|
-
(this.action.includes('manage') || this.action.includes(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: true,
|
|
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({
|
|
74
|
-
attr: options.attr,
|
|
75
|
-
action: [options.action],
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
}
|