Skip to content

Commit 360e67f

Browse files
committed
IDP Permissions feature
Manage permissions for identity providers (IDPs) at the bucket and object level. This includes creating, updating, deleting, and searching for permissions associated with IDPs. The implementation involves changes to the database models, controllers, services, routes, and validators to support IDP permissions functionality.
1 parent 90af209 commit 360e67f

19 files changed

+1145
-10
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
const errorToProblem = require('../components/errorToProblem');
2+
const utils = require('../db/models/utils');
3+
const {
4+
addDashesToUuid,
5+
mixedQueryToArray,
6+
getCurrentIdentity,
7+
groupByObject,
8+
isTruthy
9+
} = require('../components/utils');
10+
const { NIL: SYSTEM_USER } = require('uuid');
11+
const { bucketIdpPermissionService, userService, bucketService } = require('../services');
12+
13+
const SERVICE = 'BucketIdpPermissionService';
14+
15+
/**
16+
* The Permission Controller
17+
*/
18+
const controller = {
19+
20+
/**
21+
* @function searchPermissions
22+
* Searches for bucket permissions granted to idps
23+
* @param {object} req Express request object
24+
* @param {object} res Express response object
25+
* @param {function} next The next callback function
26+
* @returns {function} Express middleware function
27+
*/
28+
async searchPermissions(req, res, next) {
29+
try {
30+
const bucketIds = mixedQueryToArray(req.query.bucketId);
31+
const idps = mixedQueryToArray(req.query.idp);
32+
const bucketPermissions = await bucketIdpPermissionService.searchPermissions({
33+
bucketId: bucketIds ? bucketIds.map(id => addDashesToUuid(id)) : bucketIds,
34+
idp: idps,
35+
permCode: mixedQueryToArray(req.query.permCode)
36+
});
37+
const response = groupByObject('bucketId', 'permissions', bucketPermissions);
38+
39+
// if also returning buckets with implicit object permissions
40+
if (isTruthy(req.query.objectPerms)) {
41+
42+
const buckets = await bucketIdpPermissionService.listInheritedBucketIds(idps);
43+
// merge list of bucket permissions
44+
buckets.forEach(bucketId => {
45+
if (!response.map(r => r.bucketId).includes(bucketId) &&
46+
// limit to to bucketId(s) request query parameter if given
47+
(!bucketIds?.length || bucketIds?.includes(bucketId))) {
48+
response.push({
49+
bucketId: bucketId,
50+
permissions: []
51+
});
52+
}
53+
});
54+
}
55+
56+
res.status(200).json(response);
57+
} catch (e) {
58+
next(errorToProblem(SERVICE, e));
59+
}
60+
},
61+
62+
/**
63+
* @function listPermissions
64+
* Returns the bucket permissions
65+
* @param {object} req Express request object
66+
* @param {object} res Express response object
67+
* @param {function} next The next callback function
68+
* @returns {function} Express middleware function
69+
*/
70+
async listPermissions(req, res, next) {
71+
try {
72+
const idps = mixedQueryToArray(req.query.idp);
73+
const response = await bucketIdpPermissionService.searchPermissions({
74+
bucketId: addDashesToUuid(req.params.bucketId),
75+
idp: idps,
76+
permCode: mixedQueryToArray(req.query.permCode)
77+
});
78+
res.status(200).json(response);
79+
} catch (e) {
80+
next(errorToProblem(SERVICE, e));
81+
}
82+
},
83+
84+
/**
85+
* @function addPermissions
86+
* Grants bucket permissions to idps
87+
* @param {object} req Express request object
88+
* @param {object} res Express response object
89+
* @param {function} next The next callback function
90+
* @returns {function} Express middleware function
91+
*/
92+
async addPermissions(req, res, next) {
93+
try {
94+
const currUserId = await userService.getCurrentUserId(
95+
getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER);
96+
const currBucketId = addDashesToUuid(req.params.bucketId);
97+
98+
if (isTruthy(req.query.recursive)) {
99+
100+
const parentBucket = await bucketService.read(currBucketId);
101+
102+
// Only apply permissions to child buckets that currentUser can MANAGE
103+
// If the current user is SYSTEM_USER, apply permissions to all child buckets
104+
const childBuckets = currUserId !== SYSTEM_USER ?
105+
await bucketService.getChildrenWithManagePermissions(currBucketId, currUserId) :
106+
await bucketService.searchChildBuckets(parentBucket, true, currUserId);
107+
108+
const allBuckets = [parentBucket, ...childBuckets];
109+
110+
const responses = await utils.trxWrapper(async (trx) => {
111+
return await Promise.all(
112+
allBuckets.map(b =>
113+
bucketIdpPermissionService.addPermissions(b.bucketId, req.body, currUserId, trx)
114+
)
115+
);
116+
});
117+
res.status(201).json(responses.flat());
118+
}
119+
else {
120+
const response = await bucketIdpPermissionService.addPermissions(
121+
currBucketId, req.body, currUserId);
122+
res.status(201).json(response);
123+
}
124+
} catch (e) {
125+
next(errorToProblem(SERVICE, e));
126+
}
127+
},
128+
129+
/**
130+
* @function removePermissions
131+
* Deletes bucket permissions for a idp
132+
* @param {object} req Express request object
133+
* @param {object} res Express response object
134+
* @param {function} next The next callback function
135+
* @returns {function} Express middleware function
136+
*/
137+
async removePermissions(req, res, next) {
138+
try {
139+
const idps = mixedQueryToArray(req.query.idp);
140+
const permissions = mixedQueryToArray(req.query.permCode);
141+
142+
const currUserId = await userService.getCurrentUserId(
143+
getCurrentIdentity(req.currentUser, SYSTEM_USER), SYSTEM_USER);
144+
const currBucketId = addDashesToUuid(req.params.bucketId);
145+
146+
if (isTruthy(req.query.recursive)) {
147+
148+
const parentBucket = await bucketService.read(currBucketId);
149+
// Only apply permissions to child buckets that currentUser can MANAGE
150+
// If the current user is SYSTEM_USER, apply permissions to all child buckets
151+
const childBuckets = currUserId !== SYSTEM_USER ?
152+
await bucketService.getChildrenWithManagePermissions(currBucketId, currUserId) :
153+
await bucketService.searchChildBuckets(parentBucket, true, currUserId);
154+
155+
const allBuckets = [parentBucket, ...childBuckets];
156+
157+
const responses = await utils.trxWrapper(async (trx) => {
158+
return await Promise.all(
159+
allBuckets.map(b =>
160+
bucketIdpPermissionService.removePermissions(b.bucketId, idps, permissions, trx)
161+
)
162+
);
163+
});
164+
res.status(200).json(responses.flat());
165+
}
166+
else {
167+
const response = await bucketIdpPermissionService.removePermissions(currBucketId, idps, permissions);
168+
res.status(200).json(response);
169+
}
170+
171+
} catch (e) {
172+
next(errorToProblem(SERVICE, e));
173+
}
174+
},
175+
176+
};
177+
178+
module.exports = controller;

app/src/controllers/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
module.exports = {
22
bucketController: require('./bucket'),
33
bucketPermissionController: require('./bucketPermission'),
4+
bucketIdpPermissionController: require('./bucketIdpPermission'),
45
inviteController: require('./invite'),
56
metadataController: require('./metadata'),
67
objectController: require('./object'),
78
objectPermissionController: require('./objectPermission'),
9+
objectIdpPermissionController: require('./objectIdpPermission'),
810
syncController: require('./sync'),
911
userController: require('./user'),
1012
tagController: require('./tag'),
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
const { NIL: SYSTEM_USER } = require('uuid');
2+
const errorToProblem = require('../components/errorToProblem');
3+
const utils = require('../components/utils');
4+
5+
const { objectIdpPermissionService, userService } = require('../services');
6+
7+
const SERVICE = 'ObjectIdpPermissionService';
8+
9+
/**
10+
* The Object IDP Permission Controller
11+
*/
12+
const controller = {
13+
/**
14+
* @function searchPermissions
15+
* Searches for object permissions granted to idps
16+
* @param {object} req Express request object
17+
* @param {object} res Express response object
18+
* @param {function} next The next callback function
19+
* @returns {function} Express middleware function
20+
*/
21+
async searchPermissions(req, res, next) {
22+
try {
23+
const bucketIds = utils.mixedQueryToArray(req.query.bucketId);
24+
const objIds = utils.mixedQueryToArray(req.query.objectId);
25+
const permCodes = utils.mixedQueryToArray(req.query.permCode);
26+
const idps = utils.mixedQueryToArray(req.query.idp);
27+
const result = await objectIdpPermissionService.searchPermissions({
28+
bucketId: bucketIds ? bucketIds.map(id => utils.addDashesToUuid(id)) : bucketIds,
29+
objId: objIds ? objIds.map(id => utils.addDashesToUuid(id)) : objIds,
30+
idp: idps,
31+
permCode: permCodes
32+
});
33+
const response = utils.groupByObject('objectId', 'permissions', result);
34+
35+
// if also returning inheritied permissions
36+
if (utils.isTruthy(req.query.bucketPerms)) {
37+
const objectIds = await objectIdpPermissionService.listInheritedObjectIds(idps, bucketIds, permCodes);
38+
39+
// merge list of object permissions
40+
objectIds.forEach(objectId => {
41+
if (!response.map(r => r.objectId).includes(objectId) &&
42+
// limit to objectId request query parameter if given
43+
(!objIds?.length || objIds?.includes(objectId))) {
44+
response.push({
45+
objectId: objectId,
46+
permissions: []
47+
});
48+
}
49+
});
50+
}
51+
52+
res.status(200).json(response);
53+
} catch (e) {
54+
next(errorToProblem(SERVICE, e));
55+
}
56+
},
57+
58+
/**
59+
* @function listPermissions
60+
* Returns the object permissions
61+
* @param {object} req Express request object
62+
* @param {object} res Express response object
63+
* @param {function} next The next callback function
64+
* @returns {function} Express middleware function
65+
*/
66+
async listPermissions(req, res, next) {
67+
try {
68+
const idps = utils.mixedQueryToArray(req.query.idp);
69+
const response = await objectIdpPermissionService.searchPermissions({
70+
objId: utils.addDashesToUuid(req.params.objectId),
71+
idp: idps,
72+
permCode: utils.mixedQueryToArray(req.query.permCode)
73+
});
74+
res.status(200).json(response);
75+
} catch (e) {
76+
next(errorToProblem(SERVICE, e));
77+
}
78+
},
79+
80+
/**
81+
* @function addPermissions
82+
* Grants object permissions to idps
83+
* @param {object} req Express request object
84+
* @param {object} res Express response object
85+
* @param {function} next The next callback function
86+
* @returns {function} Express middleware function
87+
*/
88+
async addPermissions(req, res, next) {
89+
try {
90+
const userId = await userService.getCurrentUserId(utils.getCurrentIdentity(req.currentUser, SYSTEM_USER));
91+
const response = await objectIdpPermissionService.addPermissions(
92+
utils.addDashesToUuid(req.params.objectId), req.body, userId
93+
);
94+
res.status(201).json(response);
95+
} catch (e) {
96+
next(errorToProblem(SERVICE, e));
97+
}
98+
},
99+
100+
/**
101+
* @function removePermissions
102+
* Deletes object permissions for a idp
103+
* @param {object} req Express request object
104+
* @param {object} res Express response object
105+
* @param {function} next The next callback function
106+
* @returns {function} Express middleware function
107+
*/
108+
async removePermissions(req, res, next) {
109+
try {
110+
const idps = utils.mixedQueryToArray(req.query.idp);
111+
const permissions = utils.mixedQueryToArray(req.query.permCode);
112+
const response = await objectIdpPermissionService.removePermissions(req.params.objectId, idps, permissions);
113+
res.status(200).json(response);
114+
} catch (e) {
115+
next(errorToProblem(SERVICE, e));
116+
}
117+
},
118+
119+
};
120+
121+
module.exports = controller;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const stamps = require('../stamps');
2+
3+
exports.up = function (knex) {
4+
return Promise.resolve()
5+
6+
// create tables
7+
.then(() => knex.schema.createTable('object_idp_permission', table => {
8+
table.uuid('id').primary();
9+
table.uuid('objectId').references('id').inTable('object').notNullable().onUpdate('CASCADE')
10+
.onDelete('CASCADE');
11+
table.string('idp').references('idp').inTable('identity_provider').notNullable().onUpdate('CASCADE')
12+
.onDelete('CASCADE');
13+
table.string('permCode').references('permCode').inTable('permission').notNullable().onUpdate('CASCADE')
14+
.onDelete('CASCADE');
15+
stamps(knex, table);
16+
}))
17+
.then(() => knex.schema.createTable('bucket_idp_permission', table => {
18+
table.uuid('id').primary();
19+
table.uuid('bucketId').references('bucketId').inTable('bucket').notNullable().onUpdate('CASCADE')
20+
.onDelete('CASCADE');
21+
table.string('idp').references('idp').inTable('identity_provider').notNullable().onUpdate('CASCADE')
22+
.onDelete('CASCADE');
23+
table.string('permCode').references('permCode').inTable('permission').notNullable().onUpdate('CASCADE')
24+
.onDelete('CASCADE');
25+
stamps(knex, table);
26+
}))
27+
28+
// Create audit triggers
29+
.then(() => knex.schema.raw(`CREATE TRIGGER audit_object_idp_permission_trigger
30+
AFTER UPDATE OR DELETE ON object_idp_permission
31+
FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`))
32+
33+
.then(() => knex.schema.raw(`CREATE TRIGGER audit_bucket_idp_permission_trigger
34+
AFTER UPDATE OR DELETE ON bucket_idp_permission
35+
FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`));
36+
};
37+
38+
exports.down = function (knex) {
39+
return Promise.resolve()
40+
41+
// Drop audit triggers
42+
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_bucket_idp_permission_trigger ON bucket_idp_permission'))
43+
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_object_idp_permission_trigger ON object_idp_permission'))
44+
45+
// Drop table
46+
.then(() => knex.schema.dropTableIfExists('bucket_idp_permission'))
47+
.then(() => knex.schema.dropTableIfExists('object_idp_permission'));
48+
};

app/src/db/models/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ const models = {
22
// Tables
33
Bucket: require('./tables/bucket'),
44
BucketPermission: require('./tables/bucketPermission'),
5+
BucketIdpPermission: require('./tables/bucketIdpPermission'),
56
IdentityProvider: require('./tables/identityProvider'),
67
Invite: require('./tables/invite'),
78
Metadata: require('./tables/metadata'),
89
ObjectModel: require('./tables/objectModel'),
910
ObjectPermission: require('./tables/objectPermission'),
11+
ObjectIdpPermission: require('./tables/objectIdpPermission'),
1012
ObjectQueue: require('./tables/objectQueue'),
1113
Permission: require('./tables/permission'),
1214
Tag: require('./tables/tag'),

0 commit comments

Comments
 (0)