@pcg/auth 1.0.0-alpha.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/.turbo/turbo-build.log +15 -0
- package/README.md +84 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +349 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -0
- package/src/index.ts +4 -0
- package/src/permissions/action-scopes.ts +141 -0
- package/src/permissions/helpers.ts +304 -0
- package/src/permissions/index.ts +4 -0
- package/src/permissions/scopes-bulder.ts +99 -0
- package/src/types/permissions.ts +24 -0
- package/src/types/user.ts +33 -0
- package/tests/permissions/__snapshots__/helpers.test.ts.snap +43 -0
- package/tests/permissions/action-scopes.test.ts +111 -0
- package/tests/permissions/helpers.test.ts +169 -0
- package/tests/permissions/helpers.ts +10 -0
- package/tests/permissions/permissions.test.ts +218 -0
- package/tests/permissions/scopes-bulder.test.ts +80 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +11 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @pcg/auth@1.0.0-alpha.0 build /Users/serg_sadovyi/prj/deepvisionsoftware.github/pcg/packages/auth
|
|
4
|
+
> tsdown
|
|
5
|
+
|
|
6
|
+
[34mℹ[39m tsdown [2mv0.15.12[22m powered by rolldown [2mv1.0.0-beta.45[22m
|
|
7
|
+
[34mℹ[39m Using tsdown config: [4m/Users/serg_sadovyi/prj/deepvisionsoftware.github/pcg/packages/auth/tsdown.config.ts[24m
|
|
8
|
+
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
9
|
+
[34mℹ[39m tsconfig: [34m../../tsconfig.build.json[39m
|
|
10
|
+
[34mℹ[39m Build start
|
|
11
|
+
[34mℹ[39m [2mdist/[22m[1mindex.js[22m [2m11.71 kB[22m [2m│ gzip: 3.11 kB[22m
|
|
12
|
+
[34mℹ[39m [2mdist/[22mindex.js.map [2m20.80 kB[22m [2m│ gzip: 5.58 kB[22m
|
|
13
|
+
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.ts[22m[39m [2m 8.62 kB[22m [2m│ gzip: 2.00 kB[22m
|
|
14
|
+
[34mℹ[39m 3 files, total: 41.13 kB
|
|
15
|
+
[32m✔[39m Build complete in [32m577ms[39m
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @deep/auth
|
|
2
|
+
|
|
3
|
+
### Quick Start
|
|
4
|
+
|
|
5
|
+
Install npm package
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
npm i @deep/auth --save
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Use `isGranted` helper to check access rights.
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
import { isGranted } from '@deep/auth'
|
|
15
|
+
isGranted(user, 'js:episodes:create'); // boolean
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Resolved Permissions
|
|
19
|
+
|
|
20
|
+
User object `user` must contain `resolvedPemissions` array with array of objects:
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
user.resolvedPermission = [
|
|
24
|
+
{
|
|
25
|
+
id: "js:episodes:get",
|
|
26
|
+
scopes: []
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "js:episodes:list",
|
|
30
|
+
scopes: ["published"]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
But, is real life we store permissions in database as array of string, like this:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
[
|
|
40
|
+
"js:episodes:get",
|
|
41
|
+
"js:episodes[@published]:list"
|
|
42
|
+
]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Library has special helper, to resolve permissions:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { resolvePermissions } from '@deep/auth';
|
|
49
|
+
|
|
50
|
+
const permissions = [
|
|
51
|
+
"js:episodes:get",
|
|
52
|
+
"js:episodes[@published]:list"
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
user.resolvedPermission = resolvePermissions(permissions)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Permission Context
|
|
59
|
+
|
|
60
|
+
Some users don't have full access to action, but have access in some execution context. For example, user dont have permission to all episodes, but have permission only to published episodes.
|
|
61
|
+
|
|
62
|
+
In this case we determinate execution context. If user fetch only published episodes, we set scope to `published`. If user has special permission `js:episodes[@published]:list` he will obtain access to this action.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { isGranted, PermissionContext } from '@deep/auth';
|
|
66
|
+
const ctx = new PermissionContext();
|
|
67
|
+
|
|
68
|
+
if (filter.published) {
|
|
69
|
+
ctx.addScope('published');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isGranted(user, 'js:episodes:list'); // boolean
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Otherwise, we can build query based on user rights. In this case, we add system roles if user have special permission.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { isGranted } from '@deep/auth'
|
|
79
|
+
if(isGranted(user, 'js:roles:get', { scopes: ['system'] })) {
|
|
80
|
+
where.or.push({
|
|
81
|
+
type: 'system'
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
//#region src/permissions/action-scopes.d.ts
|
|
2
|
+
type ActionScopesArray = (string | string[])[];
|
|
3
|
+
/**
|
|
4
|
+
* Action scopes are used to describe
|
|
5
|
+
* what scopes are required to execute an action.
|
|
6
|
+
*/
|
|
7
|
+
declare class ActionScopes {
|
|
8
|
+
array: ActionScopesArray;
|
|
9
|
+
anyScope: boolean;
|
|
10
|
+
private idsMap;
|
|
11
|
+
constructor(scopes?: ActionScopesArray);
|
|
12
|
+
/**
|
|
13
|
+
* Set id scope (e.g. 'id#jsu:123')
|
|
14
|
+
* @param id - entity id
|
|
15
|
+
* @returns - this
|
|
16
|
+
* @example
|
|
17
|
+
* scopes.setEntityId('jsu:123')
|
|
18
|
+
*
|
|
19
|
+
* // scopes.getArray() = ['id#jsu:123']
|
|
20
|
+
*/
|
|
21
|
+
setEntityId(id: string): this;
|
|
22
|
+
/**
|
|
23
|
+
* Set scope (e.g. 'org', 'org+review')
|
|
24
|
+
* @param scope - scope name (e.g. 'org', 'org+review')
|
|
25
|
+
* @param id - entity id (e.g. 'jsorg:hci')
|
|
26
|
+
* @returns - this
|
|
27
|
+
* @example
|
|
28
|
+
* scopes.set('my')
|
|
29
|
+
*
|
|
30
|
+
* // scopes.getArray() = ['my']
|
|
31
|
+
*
|
|
32
|
+
* scopes.set('org', 'jsorg:hci')
|
|
33
|
+
*
|
|
34
|
+
* // scopes.getArray() = ['org#jsorg:hci']
|
|
35
|
+
*/
|
|
36
|
+
set(scope: string, id?: string): this;
|
|
37
|
+
has(scope: string): boolean;
|
|
38
|
+
getArray(): ActionScopesArray;
|
|
39
|
+
/**
|
|
40
|
+
* Permission scopes must cover auth scopes determined in auth context.
|
|
41
|
+
* @param permissionScopes
|
|
42
|
+
*/
|
|
43
|
+
canBeExecutedInScopes(permissionScopes: readonly (string | string[])[]): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve presaved scopes
|
|
46
|
+
* @example
|
|
47
|
+
* authCtx.setScope('org', 'jsorg:hci')
|
|
48
|
+
* [org#hci]
|
|
49
|
+
*
|
|
50
|
+
* authCtx.setScope('org+review');
|
|
51
|
+
* [org#hci, [org#hci, review]]
|
|
52
|
+
**/
|
|
53
|
+
private resolveScopeId;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/types/permissions.d.ts
|
|
57
|
+
/**
|
|
58
|
+
* Resolved permission string to object with scopes
|
|
59
|
+
* @example
|
|
60
|
+
* 'js:core:episodes[org]:get' => { id: 'js:core:episodes:get', scopes: ['org'] }
|
|
61
|
+
*/
|
|
62
|
+
interface ResolvedPermission {
|
|
63
|
+
id: string;
|
|
64
|
+
scopes: (string | string[])[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolved permission string to object with scopes
|
|
68
|
+
* @example
|
|
69
|
+
* 'g:js:core:episodes[org]:read' => { id: 'g:js:core:episodes:read', scopes: ['org'] }
|
|
70
|
+
*/
|
|
71
|
+
interface ResolvedPermissionGroup {
|
|
72
|
+
id: string;
|
|
73
|
+
scopes: (string | string[])[];
|
|
74
|
+
}
|
|
75
|
+
interface ReadonlyResolvedPermission {
|
|
76
|
+
readonly id: string;
|
|
77
|
+
readonly scopes: readonly (string | string[])[];
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/types/user.d.ts
|
|
81
|
+
interface IUser {
|
|
82
|
+
/**
|
|
83
|
+
* User ID
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* 'hcu:2x6g7l8n9op'
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
id: string;
|
|
90
|
+
/**
|
|
91
|
+
* User's permissions
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* ['js:core:episodes[org]:get', 'js:core:episodes[org]:create']
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
permissions: readonly string[];
|
|
98
|
+
/**
|
|
99
|
+
* User's resolved permissions
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* [
|
|
103
|
+
* { id: 'js:core:episodes:get', scopes: ['org'] },
|
|
104
|
+
* { id: 'js:core:episodes:create', scopes: ['org'] },
|
|
105
|
+
* ]
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
resolvedPermissions: ReadonlyResolvedPermission[];
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/permissions/helpers.d.ts
|
|
112
|
+
/**
|
|
113
|
+
* Check if user has access to permission
|
|
114
|
+
* @example
|
|
115
|
+
*
|
|
116
|
+
* // Check episode access
|
|
117
|
+
* const scopes = new ActionScopes();
|
|
118
|
+
* scopes.setEntityId(episode.id); // id#js:core:episodes:xxxxx
|
|
119
|
+
* scopes.set('org', episode.organizationId); // org#hcorg:hci
|
|
120
|
+
*
|
|
121
|
+
* isGranted(user, 'js:core:episodes:get', scopes)
|
|
122
|
+
* @param user - user object with resolvedPermissions
|
|
123
|
+
* @param permission - permission string (e.g. 'js:core:episodes:get')
|
|
124
|
+
* @param actionScopes - action scopes (e.g. ['org#hcorg:hci'] or ActionScopes instance)
|
|
125
|
+
* @returns - true if user has access to permission
|
|
126
|
+
*/
|
|
127
|
+
declare const isGranted: (user: IUser, permission: string, actionScopes?: ActionScopesArray | ActionScopes) => boolean;
|
|
128
|
+
declare const encodeScopes: (scopes: (string | string[])[]) => string;
|
|
129
|
+
/**
|
|
130
|
+
* Inject scopes into permission
|
|
131
|
+
* @param permission - permission string (e.g. 'js:core:episodes:get')
|
|
132
|
+
* @param scopesToInject - scopes to inject (e.g. ['org', 'published'])
|
|
133
|
+
* @returns - permission string with injected scopes (e.g. 'js:core:episodes[org,published]:get')
|
|
134
|
+
* @example
|
|
135
|
+
* injectScopesIntoPermission('js:core:episodes:get', ['org', 'published'])
|
|
136
|
+
* // js:core:episodes[org,published]:get
|
|
137
|
+
*/
|
|
138
|
+
declare const injectScopesIntoPermission: (permission: string, scopesToInject: (string | string[])[]) => string;
|
|
139
|
+
/**
|
|
140
|
+
* Concat scopes
|
|
141
|
+
* @param set - set of scopes
|
|
142
|
+
* @param items - items to concat
|
|
143
|
+
* @example
|
|
144
|
+
* concatScopes(['org#hci'], ['org#dv'])
|
|
145
|
+
* // ['org#hci', 'org#dv']
|
|
146
|
+
*/
|
|
147
|
+
declare const concatScopes: (set: (string | string[])[], items: (string | string[])[]) => void;
|
|
148
|
+
/**
|
|
149
|
+
* Resolve permission string to object with scopes
|
|
150
|
+
* @param permission - permission string
|
|
151
|
+
* @returns - resolved permission object
|
|
152
|
+
* @example
|
|
153
|
+
* resolvePermission('js:core:episodes[org,published]:get')
|
|
154
|
+
* // { id: 'js:core:episodes:get', scopes: ['org', 'published'] }
|
|
155
|
+
*/
|
|
156
|
+
declare const resolvePermission: (permission: string) => ResolvedPermission;
|
|
157
|
+
/**
|
|
158
|
+
* Resolve permissions to array of resolved permissions
|
|
159
|
+
* @param permissions - array of permissions
|
|
160
|
+
* @returns - array of resolved permissions
|
|
161
|
+
* @example
|
|
162
|
+
* resolvePermissions(['js:core:episodes[org]:get', 'js:core:episodes[published]:get'])
|
|
163
|
+
* // [{ id: 'js:core:episodes:get', scopes: ['org', 'published'] }]
|
|
164
|
+
*/
|
|
165
|
+
declare const resolvePermissions: (permissions: string[]) => ResolvedPermission[];
|
|
166
|
+
/**
|
|
167
|
+
* Resolve permission group string to object with scopes
|
|
168
|
+
* @param permissionGroup - permission group string
|
|
169
|
+
* @returns - resolved permission group object
|
|
170
|
+
* @example
|
|
171
|
+
* resolvePermissionGroup('g:js:core:episodes[org]:read')
|
|
172
|
+
* // { id: 'g:js:core:episodes:read', scopes: ['org'] }
|
|
173
|
+
*/
|
|
174
|
+
declare const resolvePermissionGroup: (permissionGroup: string) => ResolvedPermission;
|
|
175
|
+
/**
|
|
176
|
+
* Resolve permission groups to array of resolved permission groups
|
|
177
|
+
* @param permissionGroups - array of permission groups
|
|
178
|
+
* @returns - array of resolved permission groups
|
|
179
|
+
* @example
|
|
180
|
+
* resolvePermissionGroups(['g:js:core:episodes[org]:read', 'g:js:core:episodes[published]:read'])
|
|
181
|
+
* // [{ id: 'g:js:core:episodes:read', scopes: ['org', 'published'] }]
|
|
182
|
+
*/
|
|
183
|
+
declare const resolvePermissionGroups: (permissionGroups: string[]) => ResolvedPermissionGroup[];
|
|
184
|
+
declare const mergeResolvedPermissions: (array1: ResolvedPermission[], array2: ResolvedPermission[]) => ResolvedPermission[];
|
|
185
|
+
/**
|
|
186
|
+
* Replace scope in array of scopes
|
|
187
|
+
* @param scopes - array of scopes
|
|
188
|
+
* @param from - scope to replace
|
|
189
|
+
* @param to - scope to replace with
|
|
190
|
+
* @returns - array of scopes with replaced scopes
|
|
191
|
+
* @example
|
|
192
|
+
* replaceScope(['org#jsorg:xxx', ['org#jsorg:xxx', 'published']], 'org#jsorg:xxx', 'org#jsorg:hci')
|
|
193
|
+
* // ['org#jsorg:hci', ['org#jsorg:hci', 'published']]
|
|
194
|
+
*/
|
|
195
|
+
declare const replaceScope: (scopes: (string | string[])[],
|
|
196
|
+
// [org#jsorg:xxx, [org#jsorg:xxx, published]]
|
|
197
|
+
from: string, to: string | (string | string[])[]) => (string | string[])[];
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/permissions/scopes-bulder.d.ts
|
|
200
|
+
declare class ScopesBulder {
|
|
201
|
+
private scopes;
|
|
202
|
+
constructor(scopes?: (string | string[])[]);
|
|
203
|
+
static fromBulder(builder: ScopesBulder): ScopesBulder;
|
|
204
|
+
build(): (string | string[])[];
|
|
205
|
+
clone(): ScopesBulder;
|
|
206
|
+
/**
|
|
207
|
+
* Append scope or scopes to the current scopes
|
|
208
|
+
* @param scope - scope or array of scopes to append
|
|
209
|
+
* @example
|
|
210
|
+
* append('org#jsorg:hci')
|
|
211
|
+
* // ['org#xxx'] => ['org#xxx', 'org#jsorg:hci']
|
|
212
|
+
* append(['org#hci', 'lang#en])
|
|
213
|
+
* // ['org#xxx'] => ['org#xxx', ['org#hci', 'lang#en']]
|
|
214
|
+
*/
|
|
215
|
+
append(scope: string | string[]): void;
|
|
216
|
+
/**
|
|
217
|
+
* Extend current scopes with the given scopes
|
|
218
|
+
* @param scopes - scope or array of scopes to extend with
|
|
219
|
+
* @example
|
|
220
|
+
* extend(['lang#en', 'lang#de'])
|
|
221
|
+
* // ['published', 'draft'] => ['published', 'draft', 'lang#en', 'lang#de']
|
|
222
|
+
*/
|
|
223
|
+
extend(scopes: (string | string[])[] | ScopesBulder): void;
|
|
224
|
+
/**
|
|
225
|
+
* Join all scopes with the given scope
|
|
226
|
+
* @param scope - scope to join with
|
|
227
|
+
* @example
|
|
228
|
+
* join('org#jsorg:hci', 'before')
|
|
229
|
+
* // ['published', 'draft'] => [['org#jsorg:hci', 'published'], ['org#jsorg:hci', 'draft']]
|
|
230
|
+
* join(['lang#en', 'lang#de'])
|
|
231
|
+
* // ['published', 'draft'] => [['published', 'lang#en'], ['published', 'lang#de'], ['draft', 'lang#en'], ['draft', 'lang#de']]
|
|
232
|
+
*/
|
|
233
|
+
join(scopeOrScopes: string | (string | string[])[], pos?: 'before' | 'after'): void;
|
|
234
|
+
/**
|
|
235
|
+
* Replace prefix in all scopes
|
|
236
|
+
* @param from - prefix to replace
|
|
237
|
+
* @param to - prefix to replace with
|
|
238
|
+
* @example
|
|
239
|
+
* replacePrefix('org', 'id')
|
|
240
|
+
* ['org#jsorg:hci'] => ['id#jsorg:hci']
|
|
241
|
+
*/
|
|
242
|
+
replacePrefix(from: string, to: string): void;
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
export { ActionScopes, ActionScopesArray, IUser, ReadonlyResolvedPermission, ResolvedPermission, ResolvedPermissionGroup, ScopesBulder, concatScopes, encodeScopes, injectScopesIntoPermission, isGranted, mergeResolvedPermissions, replaceScope, resolvePermission, resolvePermissionGroup, resolvePermissionGroups, resolvePermissions };
|
|
246
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
//#region src/permissions/action-scopes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Action scopes are used to describe
|
|
4
|
+
* what scopes are required to execute an action.
|
|
5
|
+
*/
|
|
6
|
+
var ActionScopes = class {
|
|
7
|
+
array = [];
|
|
8
|
+
anyScope = false;
|
|
9
|
+
idsMap = /* @__PURE__ */ new Map();
|
|
10
|
+
constructor(scopes) {
|
|
11
|
+
if (scopes) this.array = scopes ?? [];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Set id scope (e.g. 'id#jsu:123')
|
|
15
|
+
* @param id - entity id
|
|
16
|
+
* @returns - this
|
|
17
|
+
* @example
|
|
18
|
+
* scopes.setEntityId('jsu:123')
|
|
19
|
+
*
|
|
20
|
+
* // scopes.getArray() = ['id#jsu:123']
|
|
21
|
+
*/
|
|
22
|
+
setEntityId(id) {
|
|
23
|
+
this.set(`id#${id}`);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Set scope (e.g. 'org', 'org+review')
|
|
28
|
+
* @param scope - scope name (e.g. 'org', 'org+review')
|
|
29
|
+
* @param id - entity id (e.g. 'jsorg:hci')
|
|
30
|
+
* @returns - this
|
|
31
|
+
* @example
|
|
32
|
+
* scopes.set('my')
|
|
33
|
+
*
|
|
34
|
+
* // scopes.getArray() = ['my']
|
|
35
|
+
*
|
|
36
|
+
* scopes.set('org', 'jsorg:hci')
|
|
37
|
+
*
|
|
38
|
+
* // scopes.getArray() = ['org#jsorg:hci']
|
|
39
|
+
*/
|
|
40
|
+
set(scope, id) {
|
|
41
|
+
if (id && /^[A-Za-z_]+$/.test(scope)) this.idsMap.set(scope, id);
|
|
42
|
+
if (scope.includes("+")) {
|
|
43
|
+
const subscopes = scope.split("+");
|
|
44
|
+
this.array.push(subscopes.map((subscope) => this.resolveScopeId(subscope)));
|
|
45
|
+
} else this.array.push(this.resolveScopeId(scope));
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
has(scope) {
|
|
49
|
+
return this.array.includes(scope);
|
|
50
|
+
}
|
|
51
|
+
getArray() {
|
|
52
|
+
return [...this.array];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Permission scopes must cover auth scopes determined in auth context.
|
|
56
|
+
* @param permissionScopes
|
|
57
|
+
*/
|
|
58
|
+
canBeExecutedInScopes(permissionScopes) {
|
|
59
|
+
if (this.array.includes("*")) return true;
|
|
60
|
+
if (permissionScopes.length === 0) return true;
|
|
61
|
+
/**
|
|
62
|
+
* Empty auth scopes but user has permission scopes = no access to action
|
|
63
|
+
*
|
|
64
|
+
* action scopes = []
|
|
65
|
+
* permission scopes = ['org#hci']
|
|
66
|
+
*
|
|
67
|
+
* Example:
|
|
68
|
+
* User try to get list of all entities
|
|
69
|
+
* But user only has permission to entity from org#hci
|
|
70
|
+
*/
|
|
71
|
+
if (this.array.length === 0) return false;
|
|
72
|
+
return this.array.some((authScope) => {
|
|
73
|
+
if (Array.isArray(authScope)) return permissionScopes.some((permissionScope) => {
|
|
74
|
+
if (Array.isArray(permissionScope)) return permissionScope.every((sub) => authScope.includes(sub));
|
|
75
|
+
return authScope.includes(permissionScope);
|
|
76
|
+
});
|
|
77
|
+
return permissionScopes.includes(authScope);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve presaved scopes
|
|
82
|
+
* @example
|
|
83
|
+
* authCtx.setScope('org', 'jsorg:hci')
|
|
84
|
+
* [org#hci]
|
|
85
|
+
*
|
|
86
|
+
* authCtx.setScope('org+review');
|
|
87
|
+
* [org#hci, [org#hci, review]]
|
|
88
|
+
**/
|
|
89
|
+
resolveScopeId(scope) {
|
|
90
|
+
return this.idsMap.has(scope) ? `${scope}#${this.idsMap.get(scope)}` : scope;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/permissions/helpers.ts
|
|
96
|
+
/**
|
|
97
|
+
* Normilize scopes to ActionScopes
|
|
98
|
+
* @example
|
|
99
|
+
* const scopes = normalizeScopes(['org#hci', 'user#hcu:xxxxx') // ActionScopes
|
|
100
|
+
*
|
|
101
|
+
* scopes.set('my')
|
|
102
|
+
*/
|
|
103
|
+
const normalizeScopes = (scopes) => {
|
|
104
|
+
if (!(scopes instanceof ActionScopes)) return new ActionScopes(scopes);
|
|
105
|
+
return scopes;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Check if user has access to permission
|
|
109
|
+
* @example
|
|
110
|
+
*
|
|
111
|
+
* // Check episode access
|
|
112
|
+
* const scopes = new ActionScopes();
|
|
113
|
+
* scopes.setEntityId(episode.id); // id#js:core:episodes:xxxxx
|
|
114
|
+
* scopes.set('org', episode.organizationId); // org#hcorg:hci
|
|
115
|
+
*
|
|
116
|
+
* isGranted(user, 'js:core:episodes:get', scopes)
|
|
117
|
+
* @param user - user object with resolvedPermissions
|
|
118
|
+
* @param permission - permission string (e.g. 'js:core:episodes:get')
|
|
119
|
+
* @param actionScopes - action scopes (e.g. ['org#hcorg:hci'] or ActionScopes instance)
|
|
120
|
+
* @returns - true if user has access to permission
|
|
121
|
+
*/
|
|
122
|
+
const isGranted = (user, permission, actionScopes = []) => {
|
|
123
|
+
const [service, module, resource, action] = permission.split(":");
|
|
124
|
+
if (!user?.resolvedPermissions || user.resolvedPermissions.length === 0) return false;
|
|
125
|
+
const userPermissions = user.resolvedPermissions;
|
|
126
|
+
const scopes = normalizeScopes(actionScopes);
|
|
127
|
+
const permissionCanBeExecutedInScopes = (perm, scopes$1) => !perm.scopes || perm.scopes.length === 0 || scopes$1.canBeExecutedInScopes(perm.scopes);
|
|
128
|
+
const regexp = /* @__PURE__ */ new RegExp(`^(${service}|\\*):(${module}|\\*):(${resource}|\\*):(${action}|\\*)$`);
|
|
129
|
+
if (userPermissions.some((perm) => regexp.test(perm.id) && permissionCanBeExecutedInScopes(perm, scopes))) return true;
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
const encodeScopes = (scopes) => {
|
|
133
|
+
const scopesString = scopes.map((s) => Array.isArray(s) ? s.join("+") : s).join(",");
|
|
134
|
+
return scopesString.length > 0 ? `[${scopesString}]` : "";
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Inject scopes into permission
|
|
138
|
+
* @param permission - permission string (e.g. 'js:core:episodes:get')
|
|
139
|
+
* @param scopesToInject - scopes to inject (e.g. ['org', 'published'])
|
|
140
|
+
* @returns - permission string with injected scopes (e.g. 'js:core:episodes[org,published]:get')
|
|
141
|
+
* @example
|
|
142
|
+
* injectScopesIntoPermission('js:core:episodes:get', ['org', 'published'])
|
|
143
|
+
* // js:core:episodes[org,published]:get
|
|
144
|
+
*/
|
|
145
|
+
const injectScopesIntoPermission = (permission, scopesToInject) => {
|
|
146
|
+
const { id, scopes = [] } = resolvePermission(permission);
|
|
147
|
+
const [service, module, resource, action] = id.split(":");
|
|
148
|
+
concatScopes(scopes, scopesToInject);
|
|
149
|
+
return `${service}:${module}:${resource}${encodeScopes(scopes)}:${action}`;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Concat scopes
|
|
153
|
+
* @param set - set of scopes
|
|
154
|
+
* @param items - items to concat
|
|
155
|
+
* @example
|
|
156
|
+
* concatScopes(['org#hci'], ['org#dv'])
|
|
157
|
+
* // ['org#hci', 'org#dv']
|
|
158
|
+
*/
|
|
159
|
+
const concatScopes = (set, items) => {
|
|
160
|
+
for (const item of items) if (Array.isArray(item)) {
|
|
161
|
+
if (!set.some((scope) => Array.isArray(scope) && scope.length === item.length && scope.every((s) => item.includes(s)))) set.push(item);
|
|
162
|
+
} else if (!set.includes(item)) set.push(item);
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Resolve permission string to object with scopes
|
|
166
|
+
* @param permission - permission string
|
|
167
|
+
* @returns - resolved permission object
|
|
168
|
+
* @example
|
|
169
|
+
* resolvePermission('js:core:episodes[org,published]:get')
|
|
170
|
+
* // { id: 'js:core:episodes:get', scopes: ['org', 'published'] }
|
|
171
|
+
*/
|
|
172
|
+
const resolvePermission = (permission) => {
|
|
173
|
+
const matches = /\[(?<scopes>.*?)\]/u.exec(permission);
|
|
174
|
+
if (matches?.[0] && matches.groups?.scopes) {
|
|
175
|
+
const scopes = String(matches.groups.scopes).split(",").map((s) => {
|
|
176
|
+
if (s.includes("+")) return s.split("+");
|
|
177
|
+
return s;
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
id: permission.replace(matches[0], ""),
|
|
181
|
+
scopes
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
id: permission,
|
|
186
|
+
scopes: []
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Resolve permissions to array of resolved permissions
|
|
191
|
+
* @param permissions - array of permissions
|
|
192
|
+
* @returns - array of resolved permissions
|
|
193
|
+
* @example
|
|
194
|
+
* resolvePermissions(['js:core:episodes[org]:get', 'js:core:episodes[published]:get'])
|
|
195
|
+
* // [{ id: 'js:core:episodes:get', scopes: ['org', 'published'] }]
|
|
196
|
+
*/
|
|
197
|
+
const resolvePermissions = (permissions) => {
|
|
198
|
+
const resolvedPermissions = [];
|
|
199
|
+
for (const permission of permissions) {
|
|
200
|
+
const { id, scopes } = resolvePermission(permission);
|
|
201
|
+
const currentPermission = resolvedPermissions.find((p) => p.id === id);
|
|
202
|
+
if (currentPermission) concatScopes(currentPermission.scopes, scopes);
|
|
203
|
+
else resolvedPermissions.push({
|
|
204
|
+
id,
|
|
205
|
+
scopes
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return resolvedPermissions;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Resolve permission group string to object with scopes
|
|
212
|
+
* @param permissionGroup - permission group string
|
|
213
|
+
* @returns - resolved permission group object
|
|
214
|
+
* @example
|
|
215
|
+
* resolvePermissionGroup('g:js:core:episodes[org]:read')
|
|
216
|
+
* // { id: 'g:js:core:episodes:read', scopes: ['org'] }
|
|
217
|
+
*/
|
|
218
|
+
const resolvePermissionGroup = (permissionGroup) => {
|
|
219
|
+
return resolvePermission(permissionGroup);
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Resolve permission groups to array of resolved permission groups
|
|
223
|
+
* @param permissionGroups - array of permission groups
|
|
224
|
+
* @returns - array of resolved permission groups
|
|
225
|
+
* @example
|
|
226
|
+
* resolvePermissionGroups(['g:js:core:episodes[org]:read', 'g:js:core:episodes[published]:read'])
|
|
227
|
+
* // [{ id: 'g:js:core:episodes:read', scopes: ['org', 'published'] }]
|
|
228
|
+
*/
|
|
229
|
+
const resolvePermissionGroups = (permissionGroups) => {
|
|
230
|
+
return resolvePermissions(permissionGroups);
|
|
231
|
+
};
|
|
232
|
+
const mergeResolvedPermissions = (array1, array2) => {
|
|
233
|
+
const result = [];
|
|
234
|
+
for (const rp1 of array1) {
|
|
235
|
+
const rps2 = array2.filter((rp2) => rp2.id === rp1.id);
|
|
236
|
+
if (rp1.scopes.length === 0 || rps2.some((rp2) => rp2.id === rp1.id && rp2.scopes.length === 0)) {
|
|
237
|
+
result.push({
|
|
238
|
+
id: rp1.id,
|
|
239
|
+
scopes: []
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const scopes = [...rp1.scopes];
|
|
244
|
+
for (const rp2 of rps2) concatScopes(scopes, rp2.scopes);
|
|
245
|
+
result.push({
|
|
246
|
+
id: rp1.id,
|
|
247
|
+
scopes
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
for (const rp2 of array2) if (!result.some((rp) => rp.id === rp2.id)) result.push(rp2);
|
|
251
|
+
return result;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Replace scope in array of scopes
|
|
255
|
+
* @param scopes - array of scopes
|
|
256
|
+
* @param from - scope to replace
|
|
257
|
+
* @param to - scope to replace with
|
|
258
|
+
* @returns - array of scopes with replaced scopes
|
|
259
|
+
* @example
|
|
260
|
+
* replaceScope(['org#jsorg:xxx', ['org#jsorg:xxx', 'published']], 'org#jsorg:xxx', 'org#jsorg:hci')
|
|
261
|
+
* // ['org#jsorg:hci', ['org#jsorg:hci', 'published']]
|
|
262
|
+
*/
|
|
263
|
+
const replaceScope = (scopes, from, to) => {
|
|
264
|
+
const result = [];
|
|
265
|
+
for (const scope of scopes) if (Array.isArray(scope)) result.push(replaceScope(scope, from, to));
|
|
266
|
+
else if (scope === from) if (Array.isArray(to)) result.push(...to);
|
|
267
|
+
else result.push(to);
|
|
268
|
+
else result.push(scope);
|
|
269
|
+
return result;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/permissions/scopes-bulder.ts
|
|
274
|
+
var ScopesBulder = class ScopesBulder {
|
|
275
|
+
constructor(scopes = []) {
|
|
276
|
+
this.scopes = scopes;
|
|
277
|
+
}
|
|
278
|
+
static fromBulder(builder) {
|
|
279
|
+
return new ScopesBulder(builder.scopes.slice());
|
|
280
|
+
}
|
|
281
|
+
build() {
|
|
282
|
+
return JSON.parse(JSON.stringify(this.scopes));
|
|
283
|
+
}
|
|
284
|
+
clone() {
|
|
285
|
+
return ScopesBulder.fromBulder(this);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Append scope or scopes to the current scopes
|
|
289
|
+
* @param scope - scope or array of scopes to append
|
|
290
|
+
* @example
|
|
291
|
+
* append('org#jsorg:hci')
|
|
292
|
+
* // ['org#xxx'] => ['org#xxx', 'org#jsorg:hci']
|
|
293
|
+
* append(['org#hci', 'lang#en])
|
|
294
|
+
* // ['org#xxx'] => ['org#xxx', ['org#hci', 'lang#en']]
|
|
295
|
+
*/
|
|
296
|
+
append(scope) {
|
|
297
|
+
this.scopes.push(scope);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Extend current scopes with the given scopes
|
|
301
|
+
* @param scopes - scope or array of scopes to extend with
|
|
302
|
+
* @example
|
|
303
|
+
* extend(['lang#en', 'lang#de'])
|
|
304
|
+
* // ['published', 'draft'] => ['published', 'draft', 'lang#en', 'lang#de']
|
|
305
|
+
*/
|
|
306
|
+
extend(scopes) {
|
|
307
|
+
if (Array.isArray(scopes)) concatScopes(this.scopes, scopes);
|
|
308
|
+
else if (scopes instanceof ScopesBulder) concatScopes(this.scopes, scopes.scopes);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Join all scopes with the given scope
|
|
312
|
+
* @param scope - scope to join with
|
|
313
|
+
* @example
|
|
314
|
+
* join('org#jsorg:hci', 'before')
|
|
315
|
+
* // ['published', 'draft'] => [['org#jsorg:hci', 'published'], ['org#jsorg:hci', 'draft']]
|
|
316
|
+
* join(['lang#en', 'lang#de'])
|
|
317
|
+
* // ['published', 'draft'] => [['published', 'lang#en'], ['published', 'lang#de'], ['draft', 'lang#en'], ['draft', 'lang#de']]
|
|
318
|
+
*/
|
|
319
|
+
join(scopeOrScopes, pos = "after") {
|
|
320
|
+
const result = [];
|
|
321
|
+
const scopesToJoin = Array.isArray(scopeOrScopes) ? scopeOrScopes : [scopeOrScopes];
|
|
322
|
+
if (scopesToJoin.length === 0) return;
|
|
323
|
+
const currentScopesCopy = this.build();
|
|
324
|
+
for (const scope of scopesToJoin) {
|
|
325
|
+
const scopeToJoin = Array.isArray(scope) ? scope : [scope];
|
|
326
|
+
for (const s of currentScopesCopy) if (Array.isArray(s)) result.push(pos === "before" ? [...scopeToJoin, ...s] : [...s, ...scopeToJoin]);
|
|
327
|
+
else result.push(pos === "before" ? [...scopeToJoin, s] : [s, ...scopeToJoin]);
|
|
328
|
+
}
|
|
329
|
+
this.scopes = result;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Replace prefix in all scopes
|
|
333
|
+
* @param from - prefix to replace
|
|
334
|
+
* @param to - prefix to replace with
|
|
335
|
+
* @example
|
|
336
|
+
* replacePrefix('org', 'id')
|
|
337
|
+
* ['org#jsorg:hci'] => ['id#jsorg:hci']
|
|
338
|
+
*/
|
|
339
|
+
replacePrefix(from, to) {
|
|
340
|
+
const result = [];
|
|
341
|
+
for (const scope of this.scopes) if (Array.isArray(scope)) result.push(scope.map((s) => s.replace(`${from}#`, `${to}#`)));
|
|
342
|
+
else result.push(scope.replace(`${from}#`, `${to}#`));
|
|
343
|
+
this.scopes = result;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
//#endregion
|
|
348
|
+
export { ActionScopes, ScopesBulder, concatScopes, encodeScopes, injectScopesIntoPermission, isGranted, mergeResolvedPermissions, replaceScope, resolvePermission, resolvePermissionGroup, resolvePermissionGroups, resolvePermissions };
|
|
349
|
+
//# sourceMappingURL=index.js.map
|