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