Skip to content

Commit 8c92cd1

Browse files
norrisng-bcTimCsaky
authored andcommitted
Feature: create invites that (optionally) cascade perms to children
1 parent c8f45b2 commit 8c92cd1

File tree

7 files changed

+82
-34
lines changed

7 files changed

+82
-34
lines changed

app/src/controllers/bucketPermission.js

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
const { Permissions } = require('../components/constants');
21
const errorToProblem = require('../components/errorToProblem');
32
const utils = require('../db/models/utils');
43
const {
@@ -18,26 +17,6 @@ const SERVICE = 'BucketPermissionService';
1817
*/
1918
const controller = {
2019

21-
/**
22-
* Gets all child bucket records for a given bucket, where the specified user
23-
* has MANAGE permission on said child buckets.
24-
* @param {string} parentBucketId bucket id of the parent bucket
25-
* @param {string} userId user id
26-
* @returns {Promise<object[]>} An array of bucket records that are children of the parent,
27-
* where the user has MANAGE permissions.
28-
*/
29-
async _getChildrenWithManagePerms(parentBucketId, userId) {
30-
31-
const parentBucket = await bucketService.read(parentBucketId);
32-
const allChildren = await bucketService.searchChildBuckets(parentBucket, true, userId);
33-
34-
const filteredChildren = allChildren.filter(bucket =>
35-
bucket.bucketPermission?.some(perm => perm.userId === userId && perm.permCode === Permissions.MANAGE)
36-
);
37-
38-
return filteredChildren;
39-
},
40-
4120
/**
4221
* @function searchPermissions
4322
* Searches for bucket permissions
@@ -123,7 +102,7 @@ const controller = {
123102
// Only apply permissions to child buckets that currentUser can MANAGE
124103
// If the current user is SYSTEM_USER, apply permissions to all child buckets
125104
const childBuckets = currUserId !== SYSTEM_USER ?
126-
await this._getChildrenWithManagePerms(currBucketId, currUserId) :
105+
await bucketService.getChildrenWithManagePermissions(currBucketId, currUserId) :
127106
await bucketService.searchChildBuckets(parentBucket, true, currUserId);
128107

129108
const allBuckets = [parentBucket, ...childBuckets];
@@ -171,7 +150,7 @@ const controller = {
171150
// Only apply permissions to child buckets that currentUser can MANAGE
172151
// If the current user is SYSTEM_USER, apply permissions to all child buckets
173152
const childBuckets = currUserId !== SYSTEM_USER ?
174-
await this._getChildrenWithManagePerms(currBucketId, currUserId) :
153+
await bucketService.getChildrenWithManagePermissions(currBucketId, currUserId) :
175154
await bucketService.searchChildBuckets(parentBucket, true, currUserId);
176155

177156
const allBuckets = [parentBucket, ...childBuckets];

app/src/controllers/invite.js

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid');
33

44
const { AuthType, Permissions, ResourceType } = require('../components/constants');
55
const errorToProblem = require('../components/errorToProblem');
6-
const { addDashesToUuid, getCurrentIdentity } = require('../components/utils');
6+
const { addDashesToUuid, getCurrentIdentity, isTruthy } = require('../components/utils');
7+
const utils = require('../db/models/utils');
78
const {
89
bucketPermissionService,
910
bucketService,
@@ -29,7 +30,6 @@ const controller = {
2930
*/
3031
async createInvite(req, res, next) {
3132
let resource, type;
32-
3333
try {
3434
// Reject if expiresAt is more than 30 days away
3535
const maxExpiresAt = Math.floor(Date.now() / 1000) + 2592000;
@@ -120,12 +120,13 @@ const controller = {
120120
type: type,
121121
expiresAt: req.body.expiresAt ? new Date(req.body.expiresAt * 1000).toISOString() : undefined,
122122
userId: userId,
123-
permCodes: req.body.permCodes
123+
permCodes: req.body.permCodes,
124+
recursive: (req.body.bucketId && isTruthy(req.body.recursive)) ?? false
124125
});
125126
res.status(201).json(response.token);
126127
} catch (e) {
127128
if (e.statusCode === 404) {
128-
next(errorToProblem(SERVICE, new Problem(409, {
129+
next(errorToProblem(SERVICE, new Problem(404, {
129130
detail: `Resource type '${type}' not found`,
130131
instance: req.originalUrl,
131132
resource: resource
@@ -186,7 +187,7 @@ const controller = {
186187
await objectPermissionService.addPermissions(invite.resource,
187188
permCodes.map(permCode => ({ userId, permCode })), invite.createdBy);
188189
} else if (invite.type === ResourceType.BUCKET) {
189-
// Check for object existence
190+
// Check for bucket existence
190191
await bucketService.read(invite.resource).catch(() => {
191192
inviteService.delete(token);
192193
throw new Problem(409, {
@@ -196,14 +197,37 @@ const controller = {
196197
});
197198
});
198199

199-
// Grant invitation permission and cleanup
200-
await bucketPermissionService.addPermissions(invite.resource,
201-
permCodes.map(permCode => ({ userId, permCode })), invite.createdBy);
200+
// Grant invitation permission
201+
// if invite was for specified folder AND all subfolders
202+
if (invite.recursive) {
203+
const parentBucket = await bucketService.read(invite.resource);
204+
// Only apply permissions to child buckets that inviter can MANAGE
205+
// If the invite was created via basic auth, apply permissions to all child buckets
206+
const childBuckets = invite.createdBy !== SYSTEM_USER ?
207+
await bucketService.getChildrenWithManagePermissions(invite.resource, invite.createdBy) :
208+
await bucketService.searchChildBuckets(parentBucket, true, invite.createdBy);
209+
210+
const allBuckets = [parentBucket, ...childBuckets];
211+
212+
await utils.trxWrapper(async (trx) => {
213+
return await Promise.all(
214+
allBuckets.map(b =>
215+
bucketPermissionService.addPermissions(b.bucketId, permCodes.map(
216+
permCode => ({ userId, permCode })), invite.createdBy, trx)
217+
)
218+
);
219+
});
220+
}
221+
// else not recursive
222+
else {
223+
await bucketPermissionService.addPermissions(invite.resource,
224+
permCodes.map(permCode => ({ userId, permCode })), invite.createdBy);
225+
}
202226
}
203227

204228
// Cleanup invite on success
205229
inviteService.delete(token);
206-
res.status(200).json({ resource: invite.resource, type: invite.type });
230+
res.status(200).json({ resource: invite.resource, type: invite.type, recursive: invite.recursive });
207231
} catch (e) {
208232
if (e.statusCode === 404) {
209233
next(errorToProblem(SERVICE, new Problem(404, {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = function (knex) {
6+
return Promise.resolve()
7+
// add recursive column to invite table
8+
.then(() => knex.schema.alterTable('invite', table => {
9+
table.boolean('recursive').notNullable().defaultTo(false);
10+
}));
11+
};
12+
13+
/**
14+
* @param { import("knex").Knex } knex
15+
* @returns { Promise<void> }
16+
*/
17+
exports.down = function (knex) {
18+
return Promise.resolve()
19+
// drop recursive column from invite table
20+
.then(() => knex.schema.alterTable('invite', table => {
21+
table.dropColumn('recursive');
22+
}));
23+
};

app/src/db/models/tables/invite.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class ObjectModel extends Timestamps(Model) {
2323
type: { type: 'string', enum: ['bucketId', 'objectId'] },
2424
expiresAt: { type: 'string', format: 'date-time' },
2525
permCodes: { type: 'array', items: { type: 'string' } },
26+
recursive: { type: 'boolean' },
2627
...stamps
2728
},
2829
additionalProperties: false

app/src/services/bucket.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid');
22

33
const bucketPermissionService = require('./bucketPermission');
44
const { Bucket } = require('../db/models');
5+
const { Permissions } = require('../components/constants');
56

67
/**
78
* The Bucket DB Service
@@ -173,6 +174,25 @@ const service = {
173174
}
174175
},
175176

177+
/**
178+
* Gets all child bucket records for a given bucket, where the specified user
179+
* has MANAGE permission on said child buckets.
180+
* @param {string} parentBucketId bucket id of the parent bucket
181+
* @param {string} userId user id
182+
* @param {object} [etrx=undefined] An optional Objection Transaction object
183+
* @returns {Promise<object[]>} An array of bucket records that are children of the parent,
184+
* where the user has MANAGE permissions.
185+
*/
186+
getChildrenWithManagePermissions: async (parentBucketId, userId, etrx = undefined) => {
187+
const parentBucket = await service.read(parentBucketId);
188+
const allChildren = await service.searchChildBuckets(parentBucket, true, userId, etrx);
189+
190+
const filteredChildren = allChildren.filter(bucket =>
191+
bucket.bucketPermission?.some(perm => perm.userId === userId && perm.permCode === Permissions.MANAGE)
192+
);
193+
return filteredChildren;
194+
},
195+
176196
/**
177197
* @function searchBuckets
178198
* Search and filter for specific bucket records

app/src/services/invite.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const service = {
3333
// if permCodes provided set as unique permCodes otherwise just ['READ']
3434
permCodes: data.permCodes ? Array.from(new Set(data.permCodes)) : ['READ'],
3535
expiresAt: data.expiresAt,
36-
createdBy: data.userId ?? SYSTEM_USER
36+
createdBy: data.userId ?? SYSTEM_USER,
37+
recursive: data.recursive ?? false
3738
});
3839

3940
if (!etrx) await trx.commit();

app/src/validators/invite.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const schema = {
1717
then: Joi.array().items(...Object.values(InviteBucketAllowedPermissions)).min(1),
1818
otherwise: Joi.array().items(...Object.values(InviteObjectAllowedPermissions)).min(1)
1919
}),
20-
20+
recursive: type.truthy.optional() // TODO: make forbidden if body.objectId exists
2121
}).xor('bucketId', 'objectId'),
2222
query: Joi.object({
2323
})

0 commit comments

Comments
 (0)