@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,208 +0,0 @@
|
|
|
1
|
-
import { LexiconPermission, LexiconPermissionSet } from '../lib/lexicon.js'
|
|
2
|
-
import { Nsid, isNsid } from '../lib/nsid.js'
|
|
3
|
-
import { Parser } from '../lib/parser.js'
|
|
4
|
-
import { LexPermissionSyntax } from '../lib/syntax-lexicon.js'
|
|
5
|
-
import { ScopeStringSyntax } from '../lib/syntax-string.js'
|
|
6
|
-
import {
|
|
7
|
-
ScopeStringFor,
|
|
8
|
-
ScopeSyntax,
|
|
9
|
-
isScopeStringFor,
|
|
10
|
-
isScopeSyntaxFor,
|
|
11
|
-
} from '../lib/syntax.js'
|
|
12
|
-
import { RepoPermission } from './repo-permission.js'
|
|
13
|
-
import {
|
|
14
|
-
AtprotoDidRefAbsolute,
|
|
15
|
-
RpcPermission,
|
|
16
|
-
isAtprotoDidRefAbsolute,
|
|
17
|
-
} from './rpc-permission.js'
|
|
18
|
-
|
|
19
|
-
export { type LexiconPermission, type LexiconPermissionSet, type Nsid, isNsid }
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* This is used to handle "include:" oauth scope values, used to include
|
|
23
|
-
* permissions from a lexicon defined permission set. Not being a resource
|
|
24
|
-
* permission, it does not implement `Matchable`.
|
|
25
|
-
*/
|
|
26
|
-
export class IncludeScope {
|
|
27
|
-
constructor(
|
|
28
|
-
public readonly nsid: Nsid,
|
|
29
|
-
public readonly aud: undefined | AtprotoDidRefAbsolute = undefined,
|
|
30
|
-
) {}
|
|
31
|
-
|
|
32
|
-
toString() {
|
|
33
|
-
return IncludeScope.parser.format(this)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
toPermissions(
|
|
37
|
-
permissionSet: LexiconPermissionSet,
|
|
38
|
-
): Array<RepoPermission | RpcPermission> {
|
|
39
|
-
return Array.from(this.buildPermissions(permissionSet))
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
toScopes(
|
|
43
|
-
permissionSet: LexiconPermissionSet,
|
|
44
|
-
): Array<ScopeStringFor<'repo' | 'rpc'>> {
|
|
45
|
-
return Array.from(this.buildPermissions(permissionSet), (p) => p.toString())
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Converts an "include:" to the list of permissions it includes, based on the
|
|
50
|
-
* lexicon defined permission set.
|
|
51
|
-
*/
|
|
52
|
-
*buildPermissions(
|
|
53
|
-
permissionSet: LexiconPermissionSet,
|
|
54
|
-
): Generator<RepoPermission | RpcPermission, void, unknown> {
|
|
55
|
-
for (const lexPermission of permissionSet.permissions) {
|
|
56
|
-
const syntax = this.parseLexPermission(lexPermission)
|
|
57
|
-
if (!syntax) continue
|
|
58
|
-
|
|
59
|
-
const resourcePermission = toResourcePermission(syntax)
|
|
60
|
-
if (!resourcePermission) continue
|
|
61
|
-
|
|
62
|
-
if (this.isAllowedPermission(resourcePermission)) {
|
|
63
|
-
yield resourcePermission
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
protected parseLexPermission(
|
|
69
|
-
permission: LexiconPermission,
|
|
70
|
-
): ScopeSyntax<'repo' | 'rpc'> | null {
|
|
71
|
-
// This function converts permissions listed in the permission set into
|
|
72
|
-
// their respective ScopeSyntax representations, handling special cases as
|
|
73
|
-
// needed.
|
|
74
|
-
|
|
75
|
-
if (isLexPermissionForResource(permission, 'repo')) {
|
|
76
|
-
return new LexPermissionSyntax(permission)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (isLexPermissionForResource(permission, 'rpc')) {
|
|
80
|
-
// "rpc" permissions with a defined audience are not allowed in permission
|
|
81
|
-
// sets
|
|
82
|
-
if (permission.aud !== undefined && permission.aud !== '*') {
|
|
83
|
-
return null
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// "rpc" permissions can "inherit" their audience from "aud" param defined
|
|
87
|
-
// in the "include:<nsid>?aud=<audience>" scope the permission set was
|
|
88
|
-
// loaded from.
|
|
89
|
-
if (
|
|
90
|
-
permission.inheritAud === true &&
|
|
91
|
-
permission.aud === undefined &&
|
|
92
|
-
this.aud !== undefined
|
|
93
|
-
) {
|
|
94
|
-
const { inheritAud, ...rest } = permission
|
|
95
|
-
return new LexPermissionSyntax({ aud: this.aud, ...rest })
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return new LexPermissionSyntax(permission)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return null
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Verifies that a permission included through a lexicon permission set is
|
|
106
|
-
* allowed in the context of the `include:` scope. This basically checks that
|
|
107
|
-
* the permission is "under" the namespace authority of the `include:` scope,
|
|
108
|
-
* and that it only contains "repo:", "rpc:", or "blob:" permissions.
|
|
109
|
-
*/
|
|
110
|
-
protected isAllowedPermission(
|
|
111
|
-
permission: RpcPermission | RepoPermission,
|
|
112
|
-
): boolean {
|
|
113
|
-
if (permission instanceof RpcPermission) {
|
|
114
|
-
return permission.lxm.every(this.isParentAuthorityOf, this)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (permission instanceof RepoPermission) {
|
|
118
|
-
return permission.collection.every(this.isParentAuthorityOf, this)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
throw new TypeError(`Unexpected permission ${permission}`)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Verifies that a permission item's nsid is under the same authority as the
|
|
126
|
-
* nsid of the lexicon itself (which is the same as the nsid of the `include:`
|
|
127
|
-
* scope).
|
|
128
|
-
*/
|
|
129
|
-
public isParentAuthorityOf(otherNsid: '*' | Nsid) {
|
|
130
|
-
if (otherNsid === '*') {
|
|
131
|
-
return false
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const lexiconNsid = this.nsid
|
|
135
|
-
|
|
136
|
-
const groupPrefixEnd = lexiconNsid.lastIndexOf('.')
|
|
137
|
-
|
|
138
|
-
// There should always be a dot, but since this is a security feature, let's
|
|
139
|
-
// be strict about it.
|
|
140
|
-
if (groupPrefixEnd === -1) {
|
|
141
|
-
throw new TypeError('Dot character (".") missing from lexicon NSID')
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Make sure that otherNsid is at least as long as the "group prefix"
|
|
145
|
-
if (groupPrefixEnd >= otherNsid.length - 1) {
|
|
146
|
-
return false
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Make sure that the "otherNsid" starts with the group of the lexiconNsid,
|
|
150
|
-
// up to the dot itself. We check in reverse order as nsids tend to have
|
|
151
|
-
// long common prefixes.
|
|
152
|
-
for (let i = groupPrefixEnd; i >= 0; i--) {
|
|
153
|
-
if (lexiconNsid.charCodeAt(i) !== otherNsid.charCodeAt(i)) {
|
|
154
|
-
return false
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return true
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
protected static readonly parser = new Parser(
|
|
162
|
-
'include',
|
|
163
|
-
{
|
|
164
|
-
nsid: {
|
|
165
|
-
multiple: false,
|
|
166
|
-
required: true,
|
|
167
|
-
validate: isNsid,
|
|
168
|
-
},
|
|
169
|
-
aud: {
|
|
170
|
-
multiple: false,
|
|
171
|
-
required: false,
|
|
172
|
-
validate: isAtprotoDidRefAbsolute,
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
'nsid',
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
static fromString(scope: string) {
|
|
179
|
-
if (!isScopeStringFor(scope, 'include')) return null
|
|
180
|
-
const syntax = ScopeStringSyntax.fromString(scope)
|
|
181
|
-
return IncludeScope.fromSyntax(syntax)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
static fromSyntax(syntax: ScopeSyntax<'include'>) {
|
|
185
|
-
const result = IncludeScope.parser.parse(syntax)
|
|
186
|
-
if (!result) return null
|
|
187
|
-
return new IncludeScope(result.nsid, result.aud)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function toResourcePermission(
|
|
192
|
-
syntax: ScopeSyntax<'repo' | 'rpc'>,
|
|
193
|
-
): RepoPermission | RpcPermission | null {
|
|
194
|
-
if (isScopeSyntaxFor(syntax, 'repo')) {
|
|
195
|
-
return RepoPermission.fromSyntax(syntax)
|
|
196
|
-
}
|
|
197
|
-
if (isScopeSyntaxFor(syntax, 'rpc')) {
|
|
198
|
-
return RpcPermission.fromSyntax(syntax)
|
|
199
|
-
}
|
|
200
|
-
return null
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function isLexPermissionForResource<
|
|
204
|
-
P extends { resource: unknown },
|
|
205
|
-
T extends string,
|
|
206
|
-
>(permission: P, type: T): permission is P & { resource: T } {
|
|
207
|
-
return permission.resource === type
|
|
208
|
-
}
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { RepoPermission } from './repo-permission.js'
|
|
2
|
-
|
|
3
|
-
describe('RepoPermission', () => {
|
|
4
|
-
describe('static', () => {
|
|
5
|
-
describe('fromString', () => {
|
|
6
|
-
it('should parse positional scope', () => {
|
|
7
|
-
const scope = RepoPermission.fromString('repo:com.example.foo')
|
|
8
|
-
expect(scope).not.toBeNull()
|
|
9
|
-
expect(scope!.collection).toEqual(['com.example.foo'])
|
|
10
|
-
expect(scope!.action).toEqual(['create', 'update', 'delete'])
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('should parse valid repo scope with multiple actions', () => {
|
|
14
|
-
const scope = RepoPermission.fromString(
|
|
15
|
-
'repo:com.example.foo?action=create&action=update',
|
|
16
|
-
)
|
|
17
|
-
expect(scope).not.toBeNull()
|
|
18
|
-
expect(scope!.collection).toEqual(['com.example.foo'])
|
|
19
|
-
expect(scope!.action).toEqual(['create', 'update'])
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('should parse valid repo scope without actions (defaults to create, update, delete)', () => {
|
|
23
|
-
const scope = RepoPermission.fromString('repo:com.example.foo')
|
|
24
|
-
expect(scope).not.toBeNull()
|
|
25
|
-
expect(scope!.collection).toEqual(['com.example.foo'])
|
|
26
|
-
expect(scope!.action).toEqual(['create', 'update', 'delete'])
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('should allow wildcard collection with specific action', () => {
|
|
30
|
-
const scope = RepoPermission.fromString('repo:*?action=create')
|
|
31
|
-
expect(scope).not.toBeNull()
|
|
32
|
-
expect(scope!.collection).toEqual(['*'])
|
|
33
|
-
expect(scope!.action).toEqual(['create'])
|
|
34
|
-
expect(
|
|
35
|
-
scope!.matches({ action: 'create', collection: 'any.collection' }),
|
|
36
|
-
).toBe(true)
|
|
37
|
-
expect(
|
|
38
|
-
scope!.matches({ action: 'update', collection: 'any.collection' }),
|
|
39
|
-
).toBe(false)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('should allow wildcard collection without actions', () => {
|
|
43
|
-
const scope = RepoPermission.fromString('repo:*')
|
|
44
|
-
expect(scope).not.toBeNull()
|
|
45
|
-
expect(scope!.collection).toEqual(['*'])
|
|
46
|
-
expect(scope!.action).toEqual(['create', 'update', 'delete'])
|
|
47
|
-
expect(
|
|
48
|
-
scope!.matches({ action: 'create', collection: 'any.collection' }),
|
|
49
|
-
).toBe(true)
|
|
50
|
-
expect(
|
|
51
|
-
scope!.matches({ action: 'update', collection: 'any.collection' }),
|
|
52
|
-
).toBe(true)
|
|
53
|
-
expect(
|
|
54
|
-
scope!.matches({ action: 'delete', collection: 'any.collection' }),
|
|
55
|
-
).toBe(true)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('should ignore scopes with invalid collection names', () => {
|
|
59
|
-
expect(RepoPermission.fromString('repo:foo bar')).toBeNull()
|
|
60
|
-
expect(RepoPermission.fromString('repo:.foo')).toBeNull()
|
|
61
|
-
expect(RepoPermission.fromString('repo:bar.')).toBeNull()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('should reject invalid action names', () => {
|
|
65
|
-
const scope = RepoPermission.fromString(
|
|
66
|
-
'repo:com.example.foo?action=invalid',
|
|
67
|
-
)
|
|
68
|
-
expect(scope).toBeNull()
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('should return null for invalid repo scope', () => {
|
|
72
|
-
expect(RepoPermission.fromString('invalid')).toBeNull()
|
|
73
|
-
expect(RepoPermission.fromString('scope')).toBeNull()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
for (const invalid of [
|
|
77
|
-
'repo:*?action=*',
|
|
78
|
-
'invalid',
|
|
79
|
-
'repo:invalid',
|
|
80
|
-
'repo:com.example.foo?action=invalid',
|
|
81
|
-
'repo?collection=invalid&action=invalid',
|
|
82
|
-
]) {
|
|
83
|
-
it(`should return null for invalid rpc scope: ${invalid}`, () => {
|
|
84
|
-
expect(RepoPermission.fromString(invalid)).toBeNull()
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
describe('scopeNeededFor', () => {
|
|
90
|
-
it('should return correct scope string for specific collection and action', () => {
|
|
91
|
-
const scope = RepoPermission.scopeNeededFor({
|
|
92
|
-
collection: 'com.example.foo',
|
|
93
|
-
action: 'create',
|
|
94
|
-
})
|
|
95
|
-
expect(scope).toBe('repo:com.example.foo?action=create')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('should return scope that accepts all collections with specific action', () => {
|
|
99
|
-
const scope = RepoPermission.scopeNeededFor({
|
|
100
|
-
collection: '*',
|
|
101
|
-
action: 'create',
|
|
102
|
-
})
|
|
103
|
-
expect(scope).toBe('repo:*?action=create')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('ignores invalid options', () => {
|
|
107
|
-
// @NOTE the scopeNeededFor assumes valid input, so it does not validate
|
|
108
|
-
// collection or action.
|
|
109
|
-
|
|
110
|
-
expect(
|
|
111
|
-
RepoPermission.scopeNeededFor({
|
|
112
|
-
collection: 'invalid',
|
|
113
|
-
action: 'create',
|
|
114
|
-
}),
|
|
115
|
-
).toBe('repo:invalid?action=create')
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
describe('instance', () => {
|
|
121
|
-
describe('matches', () => {
|
|
122
|
-
it('should match create action', () => {
|
|
123
|
-
const scope = RepoPermission.fromString(
|
|
124
|
-
'repo:com.example.foo?action=create',
|
|
125
|
-
)
|
|
126
|
-
expect(scope).not.toBeNull()
|
|
127
|
-
expect(
|
|
128
|
-
scope!.matches({ action: 'create', collection: 'com.example.foo' }),
|
|
129
|
-
).toBe(true)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('should not match unspecified action', () => {
|
|
133
|
-
const scope = RepoPermission.fromString(
|
|
134
|
-
'repo:com.example.foo?action=create',
|
|
135
|
-
)
|
|
136
|
-
expect(scope).not.toBeNull()
|
|
137
|
-
expect(
|
|
138
|
-
scope!.matches({ action: 'update', collection: 'com.example.foo' }),
|
|
139
|
-
).toBe(false)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('should match wildcard collection', () => {
|
|
143
|
-
const scope = RepoPermission.fromString('repo:*?action=create')
|
|
144
|
-
expect(scope).not.toBeNull()
|
|
145
|
-
expect(
|
|
146
|
-
scope!.matches({ action: 'create', collection: 'com.example.bar' }),
|
|
147
|
-
).toBe(true)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('should not match different action with wildcard collection', () => {
|
|
151
|
-
const scope = RepoPermission.fromString('repo:*?action=create')
|
|
152
|
-
expect(scope).not.toBeNull()
|
|
153
|
-
expect(
|
|
154
|
-
scope!.matches({ action: 'delete', collection: 'com.example.bar' }),
|
|
155
|
-
).toBe(false)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('should match multiple actions', () => {
|
|
159
|
-
const scope = RepoPermission.fromString(
|
|
160
|
-
'repo:com.example.foo?action=create&action=update',
|
|
161
|
-
)
|
|
162
|
-
expect(scope).not.toBeNull()
|
|
163
|
-
expect(
|
|
164
|
-
scope!.matches({ action: 'create', collection: 'com.example.foo' }),
|
|
165
|
-
).toBe(true)
|
|
166
|
-
expect(
|
|
167
|
-
scope!.matches({ action: 'update', collection: 'com.example.foo' }),
|
|
168
|
-
).toBe(true)
|
|
169
|
-
expect(
|
|
170
|
-
scope!.matches({ action: 'delete', collection: 'com.example.foo' }),
|
|
171
|
-
).toBe(false)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('should default to "create", "update", and "delete" actions', () => {
|
|
175
|
-
const scope = RepoPermission.fromString('repo:com.example.foo')
|
|
176
|
-
expect(scope).not.toBeNull()
|
|
177
|
-
expect(
|
|
178
|
-
scope!.matches({ action: 'create', collection: 'com.example.foo' }),
|
|
179
|
-
).toBe(true)
|
|
180
|
-
expect(
|
|
181
|
-
scope!.matches({ action: 'update', collection: 'com.example.foo' }),
|
|
182
|
-
).toBe(true)
|
|
183
|
-
expect(
|
|
184
|
-
scope!.matches({ action: 'delete', collection: 'com.example.foo' }),
|
|
185
|
-
).toBe(true)
|
|
186
|
-
})
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
describe('toString', () => {
|
|
190
|
-
it('should format repo scope correctly', () => {
|
|
191
|
-
const scope = new RepoPermission(
|
|
192
|
-
['com.example.foo'],
|
|
193
|
-
['create', 'update'],
|
|
194
|
-
)
|
|
195
|
-
expect(scope).not.toBeNull()
|
|
196
|
-
expect(scope!.toString()).toBe(
|
|
197
|
-
'repo:com.example.foo?action=create&action=update',
|
|
198
|
-
)
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
describe('consistency', () => {
|
|
204
|
-
const testCases: { input: string; expected: string }[] = [
|
|
205
|
-
{ input: 'repo:com.example.foo', expected: 'repo:com.example.foo' },
|
|
206
|
-
{
|
|
207
|
-
input: 'repo:com.example.foo?action=create',
|
|
208
|
-
expected: 'repo:com.example.foo?action=create',
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
input: 'repo:com.example.foo?action=create&action=update',
|
|
212
|
-
expected: 'repo:com.example.foo?action=create&action=update',
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
input: 'repo:*?action=create&action=update&action=delete',
|
|
216
|
-
expected: 'repo:*',
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
input: 'repo:com.example.foo?action=create&action=update&action=delete',
|
|
220
|
-
expected: 'repo:com.example.foo',
|
|
221
|
-
},
|
|
222
|
-
{ input: 'repo:*?action=create', expected: 'repo:*?action=create' },
|
|
223
|
-
{ input: 'repo:*?action=update', expected: 'repo:*?action=update' },
|
|
224
|
-
{
|
|
225
|
-
input: 'repo?collection=*&action=update',
|
|
226
|
-
expected: 'repo:*?action=update',
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
input: 'repo?collection=*&collection=com.example.foo&action=update',
|
|
230
|
-
expected: 'repo:*?action=update',
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
input: 'repo?collection=*',
|
|
234
|
-
expected: 'repo:*',
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
input: 'repo?collection=*&action=create&action=update&action=delete',
|
|
238
|
-
expected: 'repo:*',
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
input: 'repo?collection=*&collection=com.example.foo',
|
|
242
|
-
expected: 'repo:*',
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
input: 'repo?action=create&collection=com.example.foo',
|
|
246
|
-
expected: 'repo:com.example.foo?action=create',
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
input:
|
|
250
|
-
'repo?collection=com.example.foo&action=create&action=update&action=delete',
|
|
251
|
-
expected: 'repo:com.example.foo',
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
input:
|
|
255
|
-
'repo?action=create&collection=com.example.foo&collection=com.example.bar',
|
|
256
|
-
expected:
|
|
257
|
-
'repo?collection=com.example.bar&collection=com.example.foo&action=create',
|
|
258
|
-
},
|
|
259
|
-
]
|
|
260
|
-
|
|
261
|
-
for (const { input, expected } of testCases) {
|
|
262
|
-
it(`should properly re-format ${input}`, () => {
|
|
263
|
-
expect(RepoPermission.fromString(input)?.toString()).toBe(expected)
|
|
264
|
-
})
|
|
265
|
-
}
|
|
266
|
-
})
|
|
267
|
-
})
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { Nsid, isNsid } from '../lib/nsid.js'
|
|
2
|
-
import { Parser } from '../lib/parser.js'
|
|
3
|
-
import { ResourcePermission } from '../lib/resource-permission.js'
|
|
4
|
-
import { ScopeStringSyntax } from '../lib/syntax-string.js'
|
|
5
|
-
import {
|
|
6
|
-
NeArray,
|
|
7
|
-
NeRoArray,
|
|
8
|
-
ScopeSyntax,
|
|
9
|
-
isScopeStringFor,
|
|
10
|
-
} from '../lib/syntax.js'
|
|
11
|
-
import { knownValuesValidator } from '../lib/util.js'
|
|
12
|
-
|
|
13
|
-
export { type Nsid, isNsid }
|
|
14
|
-
|
|
15
|
-
export const REPO_ACTIONS = Object.freeze([
|
|
16
|
-
'create',
|
|
17
|
-
'update',
|
|
18
|
-
'delete',
|
|
19
|
-
] as const)
|
|
20
|
-
export type RepoAction = (typeof REPO_ACTIONS)[number]
|
|
21
|
-
export const isRepoAction = knownValuesValidator(REPO_ACTIONS)
|
|
22
|
-
|
|
23
|
-
export type CollectionParam = '*' | Nsid
|
|
24
|
-
export const isCollectionParam = (value: unknown): value is CollectionParam =>
|
|
25
|
-
value === '*' || isNsid(value)
|
|
26
|
-
|
|
27
|
-
export type RepoPermissionMatch = {
|
|
28
|
-
collection: string
|
|
29
|
-
action: RepoAction
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class RepoPermission
|
|
33
|
-
implements ResourcePermission<'repo', RepoPermissionMatch>
|
|
34
|
-
{
|
|
35
|
-
constructor(
|
|
36
|
-
public readonly collection: NeRoArray<'*' | Nsid>,
|
|
37
|
-
public readonly action: NeRoArray<RepoAction>,
|
|
38
|
-
) {}
|
|
39
|
-
|
|
40
|
-
matches({ action, collection }: RepoPermissionMatch) {
|
|
41
|
-
return (
|
|
42
|
-
this.action.includes(action) &&
|
|
43
|
-
(this.collection.includes('*') ||
|
|
44
|
-
(this.collection as readonly string[]).includes(collection))
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
toString() {
|
|
49
|
-
return RepoPermission.parser.format(this)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
protected static readonly parser = new Parser(
|
|
53
|
-
'repo',
|
|
54
|
-
{
|
|
55
|
-
collection: {
|
|
56
|
-
multiple: true,
|
|
57
|
-
required: true,
|
|
58
|
-
validate: isCollectionParam,
|
|
59
|
-
normalize: (value) => {
|
|
60
|
-
if (value.length > 1) {
|
|
61
|
-
if (value.includes('*')) return ['*'] as const
|
|
62
|
-
return [...new Set(value)].sort() as NeArray<Nsid>
|
|
63
|
-
}
|
|
64
|
-
return value as ['*' | Nsid]
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
action: {
|
|
68
|
-
multiple: true,
|
|
69
|
-
required: false,
|
|
70
|
-
validate: isRepoAction,
|
|
71
|
-
default: REPO_ACTIONS,
|
|
72
|
-
normalize: (value) => {
|
|
73
|
-
return value === REPO_ACTIONS
|
|
74
|
-
? REPO_ACTIONS // No need to filter if the default was used
|
|
75
|
-
: (REPO_ACTIONS.filter(includedIn, value) as NeArray<RepoAction>)
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
'collection',
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
static fromString(scope: string): RepoPermission | null {
|
|
83
|
-
if (!isScopeStringFor(scope, 'repo')) return null
|
|
84
|
-
const syntax = ScopeStringSyntax.fromString(scope)
|
|
85
|
-
return RepoPermission.fromSyntax(syntax)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
static fromSyntax(syntax: ScopeSyntax<'repo'>): RepoPermission | null {
|
|
89
|
-
const result = RepoPermission.parser.parse(syntax)
|
|
90
|
-
if (!result) return null
|
|
91
|
-
|
|
92
|
-
return new RepoPermission(result.collection, result.action)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
static scopeNeededFor(options: RepoPermissionMatch): string {
|
|
96
|
-
return RepoPermission.parser.format({
|
|
97
|
-
collection: [options.collection as '*' | Nsid],
|
|
98
|
-
action: [options.action],
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Special utility function to be used as predicate for array methods like
|
|
105
|
-
* `Array.prototype.includes`, etc. When used as predicate, it expects that
|
|
106
|
-
* the array method is called with a `thisArg` that is a readonly array of
|
|
107
|
-
* the same type as the `value` parameter.
|
|
108
|
-
*/
|
|
109
|
-
function includedIn<T>(this: readonly T[], value: T): boolean {
|
|
110
|
-
return this.includes(value)
|
|
111
|
-
}
|