@unchainedshop/roles 1.1.0-1
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/.npm/package/README +7 -0
- package/.npm/package/npm-shrinkwrap.json +10 -0
- package/.versions +18 -0
- package/CHANGELOG.md +6 -0
- package/README.md +5 -0
- package/index.ts +2 -0
- package/lib/roles-index.d.ts +3 -0
- package/lib/roles-index.js +4 -0
- package/lib/roles-index.js.map +1 -0
- package/lib/roles.d.ts +23 -0
- package/lib/roles.js +146 -0
- package/lib/roles.js.map +1 -0
- package/lib/utils/has.d.ts +3 -0
- package/lib/utils/has.js +8 -0
- package/lib/utils/has.js.map +1 -0
- package/lib/utils/isFunction.d.ts +1 -0
- package/lib/utils/isFunction.js +4 -0
- package/lib/utils/isFunction.js.map +1 -0
- package/package.js +25 -0
- package/package.json +38 -0
- package/roles.test.js +212 -0
- package/src/roles-index.ts +4 -0
- package/src/roles.ts +168 -0
- package/src/utils/has.ts +10 -0
- package/src/utils/isFunction.ts +3 -0
- package/tsconfig.build.json +26 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
This directory and the files immediately inside it are automatically generated
|
|
2
|
+
when you change this package's NPM dependencies. Commit the files in this
|
|
3
|
+
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
|
|
4
|
+
so that others run the same versions of sub-dependencies.
|
|
5
|
+
|
|
6
|
+
You should NOT check in the node_modules directory that Meteor automatically
|
|
7
|
+
creates; if you are using git, the .gitignore file tells git to ignore it.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"dependencies": {
|
|
4
|
+
"lodash.clone": {
|
|
5
|
+
"version": "4.5.0",
|
|
6
|
+
"resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz",
|
|
7
|
+
"integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg=="
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
package/.versions
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
babel-compiler@7.9.0
|
|
2
|
+
babel-runtime@1.5.1
|
|
3
|
+
dynamic-import@0.7.2
|
|
4
|
+
ecmascript@0.16.2
|
|
5
|
+
ecmascript-runtime@0.8.0
|
|
6
|
+
ecmascript-runtime-client@0.12.1
|
|
7
|
+
ecmascript-runtime-server@0.11.0
|
|
8
|
+
fetch@0.1.1
|
|
9
|
+
inter-process-messaging@0.1.1
|
|
10
|
+
local-test:unchained:roles@1.1.0
|
|
11
|
+
meteor@1.10.0
|
|
12
|
+
modern-browsers@0.1.8
|
|
13
|
+
modules@0.18.0
|
|
14
|
+
modules-runtime@0.13.0
|
|
15
|
+
promise@0.12.0
|
|
16
|
+
react-fast-refresh@0.2.3
|
|
17
|
+
typescript@4.5.4
|
|
18
|
+
unchained:roles@1.1.0
|
package/CHANGELOG.md
ADDED
package/README.md
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles-index.js","sourceRoot":"","sources":["../src/roles-index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEtC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
package/lib/roles.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RoleInterface, RolesInterface } from '@unchainedshop/types/roles';
|
|
2
|
+
export declare const Roles: RolesInterface;
|
|
3
|
+
/**
|
|
4
|
+
* Constructs a new role
|
|
5
|
+
*/
|
|
6
|
+
export declare class Role implements RoleInterface {
|
|
7
|
+
name: string;
|
|
8
|
+
allowRules: {
|
|
9
|
+
[name: string]: any;
|
|
10
|
+
};
|
|
11
|
+
helpers: {
|
|
12
|
+
[name: string]: any;
|
|
13
|
+
};
|
|
14
|
+
constructor(name: string);
|
|
15
|
+
/**
|
|
16
|
+
* Adds a helper to a role
|
|
17
|
+
*/
|
|
18
|
+
helper(helper: string, func: any): void;
|
|
19
|
+
/**
|
|
20
|
+
* Adds allow properties to a role
|
|
21
|
+
*/
|
|
22
|
+
allow(action: any, allow: any): void;
|
|
23
|
+
}
|
package/lib/roles.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import clone from 'lodash.clone';
|
|
2
|
+
import { has } from './utils/has';
|
|
3
|
+
import { isFunction } from './utils/isFunction';
|
|
4
|
+
export const Roles = {
|
|
5
|
+
roles: {},
|
|
6
|
+
actions: [],
|
|
7
|
+
helpers: [],
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new action
|
|
10
|
+
*/
|
|
11
|
+
registerAction(name) {
|
|
12
|
+
if (!this.actions.includes(name)) {
|
|
13
|
+
this.actions.push(name);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new helper
|
|
18
|
+
*/
|
|
19
|
+
registerHelper(name) {
|
|
20
|
+
if (!this.helpers.includes(name)) {
|
|
21
|
+
this.helpers.push(name);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
/**
|
|
25
|
+
* Get user roles
|
|
26
|
+
*/
|
|
27
|
+
getUserRoles(userId, roles, includeSpecial) {
|
|
28
|
+
const newRoles = [...(roles || [])];
|
|
29
|
+
if (includeSpecial) {
|
|
30
|
+
newRoles.push('__all__');
|
|
31
|
+
if (!userId) {
|
|
32
|
+
newRoles.push('__notLoggedIn__');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
newRoles.push('__loggedIn__');
|
|
36
|
+
if (!newRoles.includes('admin')) {
|
|
37
|
+
newRoles.push('__notAdmin__');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return newRoles;
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Returns true if the user passes the allow check
|
|
45
|
+
*/
|
|
46
|
+
async allow(context, roles, action, [obj, params]) {
|
|
47
|
+
const userRoles = Roles.getUserRoles(context.userId, roles, true);
|
|
48
|
+
return userRoles.reduce(async (roleIsAllowedPromise, role) => {
|
|
49
|
+
const roleIsAllowed = await roleIsAllowedPromise;
|
|
50
|
+
if (roleIsAllowed)
|
|
51
|
+
return true;
|
|
52
|
+
if (Roles.roles[role] && Roles.roles[role].allowRules && Roles.roles[role].allowRules[action]) {
|
|
53
|
+
return Roles.roles[role].allowRules[action].reduce(async (rulesIsAllowedPromise, allowFn) => {
|
|
54
|
+
const ruleIsAllowed = await rulesIsAllowedPromise;
|
|
55
|
+
if (ruleIsAllowed)
|
|
56
|
+
return true;
|
|
57
|
+
return allowFn(obj, params, context);
|
|
58
|
+
}, Promise.resolve(false));
|
|
59
|
+
}
|
|
60
|
+
return roleIsAllowed;
|
|
61
|
+
}, Promise.resolve(false));
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* To check if a user has permisisons to execute an action
|
|
65
|
+
*/
|
|
66
|
+
userHasPermission: async (context, action, args) => {
|
|
67
|
+
const roles = Array.isArray(context.user?.roles) ? context.user.roles : [];
|
|
68
|
+
const allows = await Roles.allow(context, roles, action, args);
|
|
69
|
+
return allows === true;
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Adds roles to a user
|
|
73
|
+
*/
|
|
74
|
+
async addUserToRoles(context, roles) {
|
|
75
|
+
let userRoles = roles;
|
|
76
|
+
if (!Array.isArray(userRoles)) {
|
|
77
|
+
userRoles = [userRoles];
|
|
78
|
+
}
|
|
79
|
+
return context.modules.users.addRoles(context.userId, userRoles);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Constructs a new role
|
|
84
|
+
*/
|
|
85
|
+
export class Role {
|
|
86
|
+
name;
|
|
87
|
+
allowRules;
|
|
88
|
+
helpers;
|
|
89
|
+
constructor(name) {
|
|
90
|
+
this.name = name;
|
|
91
|
+
if (has(Roles.roles, name))
|
|
92
|
+
throw new Error(`"${name}" role is already defined`);
|
|
93
|
+
this.allowRules = {};
|
|
94
|
+
this.helpers = {};
|
|
95
|
+
Roles.roles[name] = this;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Adds a helper to a role
|
|
99
|
+
*/
|
|
100
|
+
helper(helper, func) {
|
|
101
|
+
if (!Roles.helpers.includes(helper)) {
|
|
102
|
+
Roles.registerHelper(helper);
|
|
103
|
+
}
|
|
104
|
+
let helperFn = func;
|
|
105
|
+
if (!isFunction(helperFn)) {
|
|
106
|
+
const clonedValue = clone(helperFn);
|
|
107
|
+
helperFn = () => {
|
|
108
|
+
return clonedValue;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (!this.helpers[helper]) {
|
|
112
|
+
this.helpers[helper] = [];
|
|
113
|
+
}
|
|
114
|
+
this.helpers[helper].push(helperFn);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Adds allow properties to a role
|
|
118
|
+
*/
|
|
119
|
+
allow(action, allow) {
|
|
120
|
+
if (!Roles.actions.includes(action)) {
|
|
121
|
+
Roles.registerAction(action);
|
|
122
|
+
}
|
|
123
|
+
let allowFn = allow;
|
|
124
|
+
if (!isFunction(allowFn)) {
|
|
125
|
+
const clonedValue = clone(allowFn);
|
|
126
|
+
allowFn = () => {
|
|
127
|
+
return clonedValue;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
this.allowRules[action] = this.allowRules[action] || [];
|
|
131
|
+
this.allowRules[action].push(allowFn);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The admin role, who recives the default actions.
|
|
136
|
+
*/
|
|
137
|
+
Roles.adminRole = new Role('admin');
|
|
138
|
+
/**
|
|
139
|
+
* All the logged in users users
|
|
140
|
+
*/
|
|
141
|
+
Roles.loggedInRole = new Role('__loggedIn__');
|
|
142
|
+
/**
|
|
143
|
+
* Always, no exception
|
|
144
|
+
*/
|
|
145
|
+
Roles.allRole = new Role('__all__');
|
|
146
|
+
//# sourceMappingURL=roles.js.map
|
package/lib/roles.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.js","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"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"}
|
package/lib/utils/has.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"has.js","sourceRoot":"","sources":["../../src/utils/has.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,GAA2B,EAAE,GAAW,EAAW,EAAE;IACvE,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEhC,OAAO,CACL,CAAC,CAAC,GAAG;QACL,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YAClB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1D,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CACvC,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isFunction: (func: () => any) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isFunction.js","sourceRoot":"","sources":["../../src/utils/isFunction.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAe,EAAW,EAAE;IACrD,OAAO,IAAI,IAAI,OAAO,IAAI,KAAK,UAAU,CAAC;AAC5C,CAAC,CAAC"}
|
package/package.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Package.describe({
|
|
2
|
+
name: 'unchained:roles',
|
|
3
|
+
version: '1.1.0',
|
|
4
|
+
summary: 'Unchained Engine: Roles',
|
|
5
|
+
git: 'https://github.com/unchainedshop/unchained',
|
|
6
|
+
documentation: 'README.md',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
Npm.depends({
|
|
10
|
+
'lodash.clone': '4.5.0',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
Package.onUse((api) => {
|
|
14
|
+
api.versionsFrom('2.7.3');
|
|
15
|
+
|
|
16
|
+
api.use('ecmascript');
|
|
17
|
+
api.use('typescript');
|
|
18
|
+
|
|
19
|
+
api.mainModule('src/roles-index.ts');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
Package.onTest((api) => {
|
|
23
|
+
api.use('ecmascript');
|
|
24
|
+
api.use('unchained:roles');
|
|
25
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unchainedshop/roles",
|
|
3
|
+
"version": "1.1.0-1",
|
|
4
|
+
"description": "Roles package for unchained engine",
|
|
5
|
+
"main": "lib/roles-index.js",
|
|
6
|
+
"types": "lib/roles-index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"clean": "rm -rf lib",
|
|
10
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
11
|
+
"watch": "tsc --watch",
|
|
12
|
+
"link:core": "npm link @unchainedshop/types",
|
|
13
|
+
"test": "jest --watch"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/unchainedshop/unchained.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"unchained",
|
|
21
|
+
"ecommerce"
|
|
22
|
+
],
|
|
23
|
+
"author": "Joël Meiller",
|
|
24
|
+
"license": "EUPL-1.2",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/unchainedshop/unchained/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"lodash.clone": "4.5.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^16.11.41",
|
|
34
|
+
"@unchainedshop/types": "../@unchainedshop/types",
|
|
35
|
+
"chai": "^4.3.6",
|
|
36
|
+
"typescript": "^4.7.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/roles.test.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
test,
|
|
4
|
+
expect,
|
|
5
|
+
it,
|
|
6
|
+
beforeAll,
|
|
7
|
+
afterEach,
|
|
8
|
+
jest,
|
|
9
|
+
} from '@jest/globals';
|
|
10
|
+
import { setupDatabase } from '../../tests/helpers';
|
|
11
|
+
import { User } from '../../tests/seeds/users';
|
|
12
|
+
import { isFunction, has } from './helpers';
|
|
13
|
+
import { Role, Roles } from './index';
|
|
14
|
+
|
|
15
|
+
describe('Roles', () => {
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
// eslint-disable-next-line no-unused-vars
|
|
18
|
+
await setupDatabase();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
Roles.roles = {};
|
|
23
|
+
Roles.actions = [];
|
|
24
|
+
Roles.helpers = [];
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('Role utilities', () => {
|
|
28
|
+
const testRole = new Role('test_role');
|
|
29
|
+
const actionName = 'view_secret';
|
|
30
|
+
|
|
31
|
+
it('should register an action rule', () => {
|
|
32
|
+
const allowFn = () => true;
|
|
33
|
+
testRole.allow(actionName, allowFn);
|
|
34
|
+
expect(testRole.allowRules[actionName]).toEqual(
|
|
35
|
+
expect.arrayContaining([allowFn])
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should register a helper', () => {
|
|
40
|
+
const helperFn = () => true;
|
|
41
|
+
testRole.helper('test_helper', helperFn);
|
|
42
|
+
expect(testRole.helpers.test_helper).toEqual(
|
|
43
|
+
expect.arrayContaining([helperFn])
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return true if the user passes the allow check', () => {
|
|
48
|
+
const permissionTestRole = new Role('permission_test_role');
|
|
49
|
+
permissionTestRole.allow('view_data', () => true);
|
|
50
|
+
|
|
51
|
+
expect(Roles.allow('permission_user', 'view_data')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should return false if the user doesn't pass the allow check", () => {
|
|
55
|
+
const permissionTestRole = new Role('permission_test_role');
|
|
56
|
+
permissionTestRole.allow('view_data', () => false);
|
|
57
|
+
|
|
58
|
+
expect(Roles.allow('permission_user', 'view_data')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return false given a false user id', () => {
|
|
62
|
+
const permissionTestRole = new Role('permission_test_role');
|
|
63
|
+
permissionTestRole.allow('view_data', () => false);
|
|
64
|
+
|
|
65
|
+
expect(Roles.allow('not_found_user', 'view_data')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return false given a false role', () => {
|
|
69
|
+
expect(Roles.allow('permission_user', 'view_data')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Add allow to role', () => {
|
|
74
|
+
it('allow', async () => {
|
|
75
|
+
const permissionTestRole = new Role('permission_test_role');
|
|
76
|
+
permissionTestRole.allow('view_data', () => true);
|
|
77
|
+
|
|
78
|
+
expect(Roles.userHasPermission('permission_user', 'view_data')).toBe(
|
|
79
|
+
true
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('Retrieve Roles', () => {
|
|
85
|
+
it('should add roles to user document', async () => {
|
|
86
|
+
const result = Roles.addUserToRoles(User._id, 'test_role');
|
|
87
|
+
expect(result).toMatchObject({ ok: 1, nModified: 1, n: 1 });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should get roles attach to user', () => {
|
|
91
|
+
const roles = Roles.getUserRoles(User._id);
|
|
92
|
+
expect(roles).toEqual(expect.arrayContaining(['test_role']));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should get roles including special ones', () => {
|
|
96
|
+
const roles = Roles.getUserRoles(User._id, true);
|
|
97
|
+
expect(roles).toEqual(
|
|
98
|
+
expect.arrayContaining([
|
|
99
|
+
'test_role',
|
|
100
|
+
'__all__',
|
|
101
|
+
'__loggedIn__',
|
|
102
|
+
'__notAdmin__',
|
|
103
|
+
])
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should get admin user roles including special ones', () => {
|
|
108
|
+
const roles = Roles.getUserRoles('admin', true);
|
|
109
|
+
expect(roles).toEqual(
|
|
110
|
+
expect.arrayContaining([
|
|
111
|
+
'test_role',
|
|
112
|
+
'admin',
|
|
113
|
+
'__all__',
|
|
114
|
+
'__loggedIn__',
|
|
115
|
+
])
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should get roles including special ones as not logged in user', () => {
|
|
120
|
+
const roles = Roles.getUserRoles(null, true);
|
|
121
|
+
expect(roles).toEqual(
|
|
122
|
+
expect.arrayContaining(['__all__', '__notLoggedIn__'])
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Role Helper Registration', () => {
|
|
128
|
+
it('should add a helper', () => {
|
|
129
|
+
Roles.registerHelper('test_helper');
|
|
130
|
+
expect(Roles.helpers).toEqual(['test_helper']);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should add a helper attaching it to adminRole', () => {
|
|
134
|
+
Roles.registerHelper('test_admin_helper', jest.fn());
|
|
135
|
+
expect(Roles.helpers).toEqual(
|
|
136
|
+
expect.arrayContaining(['test_admin_helper'])
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should skip adding helper if it already exists', () => {
|
|
141
|
+
Roles.registerHelper('test_helper');
|
|
142
|
+
Roles.registerHelper('test_admin_helper');
|
|
143
|
+
|
|
144
|
+
expect(Roles.helpers).toEqual(
|
|
145
|
+
expect.arrayContaining(['test_helper', 'test_admin_helper'])
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Roles.registerAction
|
|
151
|
+
describe('Action registration', () => {
|
|
152
|
+
it('should add an action', () => {
|
|
153
|
+
Roles.registerAction('test_action');
|
|
154
|
+
expect(Roles.actions).toEqual(['test_action']);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should skip adding action if it already exists', () => {
|
|
158
|
+
Roles.registerAction('test_action');
|
|
159
|
+
expect(Roles.actions).toEqual(['test_action']);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Roles.Role
|
|
164
|
+
describe('Role contruction', () => {
|
|
165
|
+
it('should construct a new role', () => {
|
|
166
|
+
expect(new Role('test_role')).toMatchObject({
|
|
167
|
+
name: 'test_role',
|
|
168
|
+
allowRules: {},
|
|
169
|
+
helpers: {},
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw an error if given a role with similar name', () => {
|
|
174
|
+
// eslint-disable-next-line no-new
|
|
175
|
+
new Role('test_role');
|
|
176
|
+
expect(() => new Role('test_role')).toThrow();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('isFunction', () => {
|
|
181
|
+
test('it should return true give a function', () => {
|
|
182
|
+
expect(isFunction(() => {})).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('it should return false given an improper function', () => {
|
|
186
|
+
expect(isFunction('false')).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('has', () => {
|
|
191
|
+
test('it should return true for existent key', () => {
|
|
192
|
+
const obj = {
|
|
193
|
+
foo: 'bar',
|
|
194
|
+
};
|
|
195
|
+
expect(has(obj, 'foo')).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('it should return true for existent nested key', () => {
|
|
199
|
+
const obj = {
|
|
200
|
+
foo: { bar: 'baz' },
|
|
201
|
+
};
|
|
202
|
+
expect(has(obj, 'foo.bar')).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('it should return false for non existent', () => {
|
|
206
|
+
const obj = {
|
|
207
|
+
foo: 'bar',
|
|
208
|
+
};
|
|
209
|
+
expect(has(obj, 'baz')).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
package/src/roles.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import clone from 'lodash.clone';
|
|
2
|
+
import { RoleInterface, RolesInterface } from '@unchainedshop/types/roles';
|
|
3
|
+
import { has } from './utils/has';
|
|
4
|
+
import { isFunction } from './utils/isFunction';
|
|
5
|
+
|
|
6
|
+
export const Roles: RolesInterface = {
|
|
7
|
+
roles: {},
|
|
8
|
+
actions: [],
|
|
9
|
+
helpers: [],
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new action
|
|
12
|
+
*/
|
|
13
|
+
registerAction(name: string): void {
|
|
14
|
+
if (!this.actions.includes(name)) {
|
|
15
|
+
this.actions.push(name);
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new helper
|
|
21
|
+
*/
|
|
22
|
+
registerHelper(name: string): void {
|
|
23
|
+
if (!this.helpers.includes(name)) {
|
|
24
|
+
this.helpers.push(name);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get user roles
|
|
30
|
+
*/
|
|
31
|
+
getUserRoles(userId, roles, includeSpecial) {
|
|
32
|
+
const newRoles = [...(roles || [])];
|
|
33
|
+
if (includeSpecial) {
|
|
34
|
+
newRoles.push('__all__');
|
|
35
|
+
if (!userId) {
|
|
36
|
+
newRoles.push('__notLoggedIn__');
|
|
37
|
+
} else {
|
|
38
|
+
newRoles.push('__loggedIn__');
|
|
39
|
+
if (!newRoles.includes('admin')) {
|
|
40
|
+
newRoles.push('__notAdmin__');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return newRoles;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the user passes the allow check
|
|
50
|
+
*/
|
|
51
|
+
async allow(context, roles, action, [obj, params]) {
|
|
52
|
+
const userRoles = Roles.getUserRoles(context.userId, roles, true);
|
|
53
|
+
|
|
54
|
+
return userRoles.reduce(async (roleIsAllowedPromise: Promise<boolean>, role) => {
|
|
55
|
+
const roleIsAllowed = await roleIsAllowedPromise;
|
|
56
|
+
|
|
57
|
+
if (roleIsAllowed) return true;
|
|
58
|
+
|
|
59
|
+
if (Roles.roles[role] && Roles.roles[role].allowRules && Roles.roles[role].allowRules[action]) {
|
|
60
|
+
return Roles.roles[role].allowRules[action].reduce(
|
|
61
|
+
async (rulesIsAllowedPromise: Promise<boolean>, allowFn: any) => {
|
|
62
|
+
const ruleIsAllowed = await rulesIsAllowedPromise;
|
|
63
|
+
if (ruleIsAllowed) return true;
|
|
64
|
+
|
|
65
|
+
return allowFn(obj, params, context);
|
|
66
|
+
},
|
|
67
|
+
Promise.resolve(false),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return roleIsAllowed;
|
|
72
|
+
}, Promise.resolve(false));
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* To check if a user has permisisons to execute an action
|
|
77
|
+
*/
|
|
78
|
+
userHasPermission: async (context, action, args) => {
|
|
79
|
+
const roles = Array.isArray(context.user?.roles) ? context.user.roles : [];
|
|
80
|
+
const allows = await Roles.allow(context, roles, action, args);
|
|
81
|
+
return allows === true;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Adds roles to a user
|
|
86
|
+
*/
|
|
87
|
+
async addUserToRoles(context, roles) {
|
|
88
|
+
let userRoles = roles;
|
|
89
|
+
if (!Array.isArray(userRoles)) {
|
|
90
|
+
userRoles = [userRoles];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return context.modules.users.addRoles(context.userId, userRoles);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Constructs a new role
|
|
99
|
+
*/
|
|
100
|
+
export class Role implements RoleInterface {
|
|
101
|
+
allowRules: { [name: string]: any };
|
|
102
|
+
|
|
103
|
+
helpers: { [name: string]: any };
|
|
104
|
+
|
|
105
|
+
constructor(public name: string) {
|
|
106
|
+
if (has(Roles.roles, name)) throw new Error(`"${name}" role is already defined`);
|
|
107
|
+
|
|
108
|
+
this.allowRules = {};
|
|
109
|
+
this.helpers = {};
|
|
110
|
+
|
|
111
|
+
Roles.roles[name] = this as any as RoleInterface;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Adds a helper to a role
|
|
116
|
+
*/
|
|
117
|
+
helper(helper: string, func: any) {
|
|
118
|
+
if (!Roles.helpers.includes(helper)) {
|
|
119
|
+
Roles.registerHelper(helper);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let helperFn = func;
|
|
123
|
+
|
|
124
|
+
if (!isFunction(helperFn)) {
|
|
125
|
+
const clonedValue = clone(helperFn);
|
|
126
|
+
helperFn = () => {
|
|
127
|
+
return clonedValue;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!this.helpers[helper]) {
|
|
132
|
+
this.helpers[helper] = [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.helpers[helper].push(helperFn);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Adds allow properties to a role
|
|
140
|
+
*/
|
|
141
|
+
allow(action, allow) {
|
|
142
|
+
if (!Roles.actions.includes(action)) {
|
|
143
|
+
Roles.registerAction(action);
|
|
144
|
+
}
|
|
145
|
+
let allowFn = allow;
|
|
146
|
+
if (!isFunction(allowFn)) {
|
|
147
|
+
const clonedValue = clone(allowFn);
|
|
148
|
+
allowFn = () => {
|
|
149
|
+
return clonedValue;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
this.allowRules[action] = this.allowRules[action] || [];
|
|
153
|
+
this.allowRules[action].push(allowFn);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The admin role, who recives the default actions.
|
|
159
|
+
*/
|
|
160
|
+
Roles.adminRole = new Role('admin') as any as RoleInterface;
|
|
161
|
+
/**
|
|
162
|
+
* All the logged in users users
|
|
163
|
+
*/
|
|
164
|
+
Roles.loggedInRole = new Role('__loggedIn__') as any as RoleInterface;
|
|
165
|
+
/**
|
|
166
|
+
* Always, no exception
|
|
167
|
+
*/
|
|
168
|
+
Roles.allRole = new Role('__all__') as any as RoleInterface;
|
package/src/utils/has.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const has = (obj: { [key: string]: any }, key: string): boolean => {
|
|
2
|
+
const keyParts = key.split('.');
|
|
3
|
+
|
|
4
|
+
return (
|
|
5
|
+
!!obj &&
|
|
6
|
+
(keyParts.length > 1
|
|
7
|
+
? has(obj[key.split('.')[0]], keyParts.slice(1).join('.'))
|
|
8
|
+
: obj.hasOwnProperty.call(obj, key))
|
|
9
|
+
);
|
|
10
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": true,
|
|
4
|
+
"allowSyntheticDefaultImports": true,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"lib": ["esnext"],
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noUnusedLocals": false,
|
|
14
|
+
"outDir": "lib",
|
|
15
|
+
"preserveWatchOutput": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"sourceMap": true,
|
|
18
|
+
"target": "esnext",
|
|
19
|
+
"types": ["node"],
|
|
20
|
+
"baseUrl": ".", // This must be specified if "paths" is.
|
|
21
|
+
"paths": {
|
|
22
|
+
"meteor/unchained:*": ["node_modules/@unchainedshop/types/index.d.ts"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["src"]
|
|
26
|
+
}
|