Skip to content

Commit 85da9e7

Browse files
committed
Adding support for actions!
See readme for details.
1 parent 44ab836 commit 85da9e7

File tree

7 files changed

+104
-6
lines changed

7 files changed

+104
-6
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ userSchema.permissions = {
5050
admin: {
5151
read: ['status'],
5252
write: ['status'],
53-
create: true
53+
create: true,
54+
actions: ['merge'],
5455
},
5556
owner: {
5657
read: ['status'],
@@ -71,6 +72,7 @@ The permissions object consists of properties that represent your authorization
7172
* `remove` - Boolean
7273
* `write` - [array of fields] *NOTE: if `upsert: true`, the group will need to have `create` permissions too*
7374
* `read` - [array of fields]
75+
* `actions` - An array or arbitrary strings that lets you define more complicated actions (usually involving multiple fields or documents) that are allowed. Your own application code will need to handle the updates (likely disabling field level checking for those updates).
7476

7577
You can also specify a `defaults` group, which represents permissions that are available to all groups.
7678

@@ -145,7 +147,8 @@ console.log(user.permissions);
145147
// {
146148
// read: [...],
147149
// write: [...],
148-
// remove: [boolean]
150+
// remove: [boolean],
151+
// actions: [...],
149152
// }
150153

151154
// OR
@@ -156,7 +159,8 @@ console.log(user.foo);
156159
// {
157160
// read: [...],
158161
// write: [...],
159-
// remove: [boolean]
162+
// remove: [boolean],
163+
// actions: [...],
160164
// }
161165
```
162166

__tests__/embedPermissions.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ test.before((t) => {
1111
write: ['friend'],
1212
create: true,
1313
remove: true,
14+
actions: ['like'],
1415
},
1516
};
1617
t.context.schema = schema;
@@ -54,6 +55,7 @@ test('Permissions embedded under default key', (t) => {
5455
read: ['friend'],
5556
write: ['friend'],
5657
remove: true,
58+
actions: ['like'],
5759
},
5860
'Incorrect permissions embedded',
5961
);
@@ -66,6 +68,7 @@ test('Permissions embedded under default key', (t) => {
6668
read: [],
6769
write: [],
6870
remove: false,
71+
actions: [],
6972
},
7073
'Incorrect permissions embedded',
7174
);
@@ -79,6 +82,7 @@ test('Permissions embedded under custom key', (t) => {
7982
read: ['friend'],
8083
write: ['friend'],
8184
remove: true,
85+
actions: ['like'],
8286
},
8387
'Incorrect permissions embedded',
8488
);
@@ -91,6 +95,7 @@ test('Permissions embedded under custom key', (t) => {
9195
read: [],
9296
write: [],
9397
remove: false,
98+
actions: [],
9499
},
95100
'Incorrect permissions embedded',
96101
);
@@ -135,11 +140,36 @@ test('Verify that the permissions data cannot be changed', (t) => {
135140
'The permissions object shouldn\'t be writable [remove]',
136141
);
137142

143+
t.throws(
144+
() => { doc.permissions.actions = []; },
145+
Error,
146+
'The permissions object shouldn\'t be writable [actions]',
147+
);
148+
138149
t.throws(
139150
() => { doc.permissions = {}; },
140151
Error,
141152
'The permissions field should not be writable',
142153
);
154+
155+
// Try to directly edit the fields. These won't throw, but also shouldn't
156+
// affect the permissions on the object.
157+
doc.permissions.read.push('foo');
158+
doc.permissions.write.push('foo');
159+
doc.permissions.actions.push('foo');
160+
doc.permissions.read.shift();
161+
doc.permissions.write.shift();
162+
doc.permissions.actions.shift();
163+
t.deepEqual(
164+
doc.permissions,
165+
{
166+
read: ['friend'],
167+
write: ['friend'],
168+
remove: true,
169+
actions: ['like'],
170+
},
171+
'Permissions able to be mutated when they should not be.',
172+
);
143173
});
144174

145175
test.todo('Make sure enumerable is set to true for the embedded permissions');

__tests__/exampleSchemas/bareBonesSchema.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ bareBonesSchema.permissions = {
77
write: ['address', 'phone', 'birthday', 'not_here_either'],
88
create: true,
99
remove: true,
10+
actions: ['sayHello', 'jumpRealHigh'],
1011
},
1112
};
1213

__tests__/exampleSchemas/goodSchema.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,19 @@ goodSchema.permissions = {
1212
read: ['_id', 'name'],
1313
write: [],
1414
create: false,
15+
actions: ['wave'],
1516
},
1617
admin: {
1718
read: ['address', 'phone', 'birthday'],
1819
write: ['address', 'phone', 'birthday'],
1920
create: true,
2021
remove: true,
22+
actions: ['combine'],
2123
},
2224
self: {
2325
read: ['address', 'phone', 'birthday'],
2426
write: ['address', 'phone'],
27+
actions: ['combine'],
2528
},
2629
stranger: {},
2730
hasVirtuals: {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const test = require('ava');
2+
const goodSchema = require('./exampleSchemas/goodSchema');
3+
const bareBonesSchema = require('./exampleSchemas/bareBonesSchema');
4+
const getAuthorizedActions = require('../src/getAuthorizedActions');
5+
6+
test('No matching authLevels', (t) => {
7+
t.deepEqual(
8+
getAuthorizedActions(bareBonesSchema, 'foobar'),
9+
[],
10+
);
11+
});
12+
13+
test('Handles non-existent authLevels', (t) => {
14+
t.deepEqual(
15+
getAuthorizedActions(goodSchema, ['defaults', 'foobar']),
16+
['wave'],
17+
);
18+
});
19+
20+
test('Combines basic authLevel actions', (t) => {
21+
t.deepEqual(
22+
getAuthorizedActions(goodSchema, ['defaults', 'admin']).sort(),
23+
['combine', 'wave'].sort(),
24+
);
25+
});
26+
27+
test('Handles authLevels that have no actions specified', (t) => {
28+
t.deepEqual(
29+
getAuthorizedActions(goodSchema, ['defaults', 'stranger']),
30+
['wave'],
31+
);
32+
});
33+
34+
test('Correctly deduping actions that are mentioned in multiple authLevels', (t) => {
35+
t.deepEqual(
36+
getAuthorizedActions(goodSchema, ['defaults', 'self', 'admin']).sort(),
37+
['combine', 'wave'].sort(),
38+
);
39+
});

src/embedPermissions.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const _ = require('lodash');
22
const getAuthorizedFields = require('./getAuthorizedFields');
3+
const getAuthorizedActions = require('./getAuthorizedActions');
34
const hasPermission = require('./hasPermission');
45

56
const embedPermissionsSymbol = Symbol('Embedded Permissions');
@@ -24,10 +25,16 @@ function embedPermissions(schema, options, authLevels, doc) {
2425
});
2526
}
2627

28+
const read = getAuthorizedFields(schema, authLevels, 'read');
29+
const write = getAuthorizedFields(schema, authLevels, 'write');
30+
const hasRemovePermission = hasPermission(schema, authLevels, 'remove');
31+
const actions = getAuthorizedActions(schema, authLevels);
32+
2733
doc[embedPermissionsSymbol] = Object.freeze({
28-
read: getAuthorizedFields(schema, authLevels, 'read'),
29-
write: getAuthorizedFields(schema, authLevels, 'write'),
30-
remove: hasPermission(schema, authLevels, 'remove'),
34+
get read() { return [...read]; },
35+
get write() { return [...write]; },
36+
remove: hasRemovePermission,
37+
get actions() { return [...actions]; },
3138
});
3239
}
3340

src/getAuthorizedActions.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const _ = require('lodash');
2+
const cleanAuthLevels = require('./cleanAuthLevels');
3+
4+
function getAuthorizedActions(schema, authLevels) {
5+
const cleanedLevels = cleanAuthLevels(schema, authLevels);
6+
7+
return _.chain(cleanedLevels)
8+
.flatMap(level => schema.permissions[level].actions)
9+
.filter()
10+
.uniq() // dropping duplicates
11+
.value();
12+
}
13+
14+
module.exports = getAuthorizedActions;

0 commit comments

Comments
 (0)