diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js index f4f25df61..dd7e8bb2b 100644 --- a/addons/api/addon/models/role.js +++ b/addons/api/addon/models/role.js @@ -36,4 +36,64 @@ export default class RoleModel extends GeneratedRoleModel { }); } + /** + * Adds principals via the `add-principals` method. + * See serializer and adapter for more information. + * @param {[string]} principalIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + addPrincipals(principalIDs, options={ adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'add-principals', + principalIDs + }; + // There is no "deep merge" in ES. + // All of this nonsense is here to ensure we get + // a decent merge of `adapterOptions`. + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions + } + }); + } + + /** + * Delete principals via the `remove-principals` method. + * See serializer and adapter for more information. + * @param {[string]} principalIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + removePrincipals(principalIDs, options={ adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'remove-principals', + principalIDs + }; + // There is no "deep merge" in ES. + // All of this nonsense is here to ensure we get + // a decent merge of `adapterOptions`. + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions + } + }); + } + + /** + * Delete a single principal via the `remove-principals` method. + * @param {number} principalIDs + * @param {object} options + * @return {Promise} + */ + removePrincipal(principalID, options) { + return this.removePrincipals([principalID], options); + } + } diff --git a/addons/api/addon/serializers/role.js b/addons/api/addon/serializers/role.js index 9cf12da58..5479d051e 100644 --- a/addons/api/addon/serializers/role.js +++ b/addons/api/addon/serializers/role.js @@ -9,6 +9,8 @@ export default class RoleSerializer extends ApplicationSerializer { * If `adapterOptions.serializeGrants` is true, the serialization should * include **only grants** and the version. Normally, grants are not * serialized. + * If `adapterOptions.principalIDs` is set (to an array of user and + * group IDs), then the payload is serialized via `serializeWithPrincipals`. * @override * @param {Snapshot} snapshot * @return {object} @@ -17,6 +19,9 @@ export default class RoleSerializer extends ApplicationSerializer { const serializeGrants = get(snapshot, 'adapterOptions.serializeGrants'); let serialized = super.serialize(...arguments); if (serializeGrants) serialized = this.serializeWithGrants(snapshot); + const principalIDs = snapshot?.adapterOptions?.principalIDs; + if (principalIDs) serialized = + this.serializeWithPrincipals(snapshot, principalIDs); return serialized; } @@ -32,6 +37,20 @@ export default class RoleSerializer extends ApplicationSerializer { }; } + /** + * Returns a payload containing only the principal_ids array using IDs + * passed into the function (rather than existing principals on the model). + * @param {Snapshot} snapshot + * @param {[string]} principalIDs + * @return {object} + */ + serializeWithPrincipals(snapshot, principalIDs) { + return { + version: snapshot.attr('version'), + principal_ids: principalIDs + }; + } + /** * Converts string arrays to object arrays for use with `fragment-string`. * E.g. `"foo"` -> `{value: "foo"}` diff --git a/addons/api/tests/unit/models/role-test.js b/addons/api/tests/unit/models/role-test.js index fe1b8224b..50a0fdb2d 100644 --- a/addons/api/tests/unit/models/role-test.js +++ b/addons/api/tests/unit/models/role-test.js @@ -41,4 +41,103 @@ module('Unit | Model | role', function (hooks) { const model = store.peekRecord('role', '123abc'); await model.saveGrants(); }); + + test('it has an `addPrincipals` method that targets a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/roles/123abc:add-principals', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + principal_ids: ['123_abc', 'foobar'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'role', + attributes: { + name: 'Role', + description: 'Description', + principals: [ + { id: '1', type: 'user' }, + { id: '3', type: 'group' } + ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('role', '123abc'); + await model.addPrincipals(['123_abc', 'foobar']); + }); + + test('it has a `removePrincipals` method that targets a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/roles/123abc:remove-principals', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + principal_ids: ['1', '3'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'role', + attributes: { + name: 'Role', + description: 'Description', + principals: [ + { id: '1', type: 'user' }, + { id: '3', type: 'group' } + ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('role', '123abc'); + await model.removePrincipals(['1', '3']); + }); + + test('it has a `removePrincipal` method that deletes a single principal using `removePrincipals` method', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/roles/123abc:remove-principals', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + principal_ids: ['3'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'role', + attributes: { + name: 'Role', + description: 'Description', + principals: [ + { id: '1', type: 'user' }, + { id: '3', type: 'group' } + ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('role', '123abc'); + await model.removePrincipal('3'); + }); });