From f2c0458fb5cb8d42dcd8a6a63e3b4b52259b9049 Mon Sep 17 00:00:00 2001 From: Dharini Jeeva Date: Thu, 22 Dec 2022 14:09:56 -0800 Subject: [PATCH] Add model support for managed groups (#1544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 streamline managed groups getter on role --- addons/api/addon/models/role.js | 119 ++++++++++++++++++ addons/api/tests/unit/models/role-test.js | 7 +- .../scopes/scope/roles/role/principals.js | 48 ++----- 3 files changed, 134 insertions(+), 40 deletions(-) diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js index 4f2efa720..226c27b2f 100644 --- a/addons/api/addon/models/role.js +++ b/addons/api/addon/models/role.js @@ -1,11 +1,13 @@ import GeneratedRoleModel from '../generated/models/role'; import { attr } from '@ember-data/model'; import { inject as service } from '@ember/service'; +import { all, resolve } from 'rsvp'; export default class RoleModel extends GeneratedRoleModel { // =services @service store; + @service resourceFilterStore; // =attributes @@ -37,6 +39,123 @@ export default class RoleModel extends GeneratedRoleModel { : null; } + /** + * A list of IDs for principals of type `user`. + * @type {string[]} + */ + get userIDs() { + return this.principals + .filter(({ type }) => type === 'user') + .map(({ principal_id }) => principal_id); + } + + /** + * A list of IDs for principals of type `group`. + * @type {string[]} + */ + get groupIDs() { + return this.principals + .filter(({ type }) => type === 'group') + .map(({ principal_id }) => principal_id); + } + + /** + * A list of IDs for principals of type `managed group`. + * @type {string[]} + */ + get managedGroupIDs() { + return this.principals + .filter(({ type }) => type === 'managed group') + .map(({ principal_id }) => principal_id); + } + + /** + * A promise that resolves to an array of User instances. + * When calling this getter, be sure to await resolution + * before interacting with the results. + * @type {Promise[UserModel]} + */ + get users() { + const ids = this.userIDs; + + // Role has prinicipal IDs, + // return a promise which resolves model instances for those IDs + if (ids?.length) { + return this.resourceFilterStore + .queryBy('user', { id: ids }, { scope_id: 'global', recursive: true }) + .then((models) => models.map((model) => model)); + } + + // No principal IDs, + // return a promise resolving to an empty array + return resolve([]); + } + + /** + * A promise that resolves to an array of Group instances. + * When calling this getter, be sure to await resolution + * before interacting with the results. + * @type {Promise[GroupModel]} + */ + get groups() { + const ids = this.groupIDs; + + // Role has prinicipal IDs, + // return a promise which resolves model instances for those IDs + if (ids?.length) { + return this.resourceFilterStore + .queryBy('group', { id: ids }, { scope_id: 'global', recursive: true }) + .then((models) => models.map((model) => model)); + } + + // No principal IDs, + // return a promise resolving to an empty array + return resolve([]); + } + + /** + * A promise that resolves to an array of ManagedGroup instances. + * When calling this getter, be sure to await resolution + * before interacting with the results. + * @type {Promise[ManagedGroupModel]} + */ + get managedGroups() { + const ids = this.managedGroupIDs; + + // Role has prinicipal IDs + // return a promise which resolves model instances for those IDs + if (ids?.length) { + // Collect all auth methods. + const authMethods = this.resourceFilterStore.queryBy( + 'auth-method', + { type: 'oidc' }, + { scope_id: 'global', recursive: true } + ); + + // For each auth method, query all managed groups with IDs + // that match the role's manage group principal IDs. + const managedGroups = authMethods + .then((methods) => + all( + methods.map(({ id: auth_method_id }) => + this.resourceFilterStore + .queryBy('managed-group', { id: ids }, { auth_method_id }) + .then((models) => models.map((model) => model)) + ) + ) + ) + // The result is an array of arrays of model instances (grouped by + // auth methods), so these must be flattened. + .then((grouped) => grouped.flat()); + + return managedGroups; + } + + // No principal IDs, + // return a promise resolving to an empty array + return resolve([]); + } + // =methods /** diff --git a/addons/api/tests/unit/models/role-test.js b/addons/api/tests/unit/models/role-test.js index 9d64e9ac2..fc0c5d8e8 100644 --- a/addons/api/tests/unit/models/role-test.js +++ b/addons/api/tests/unit/models/role-test.js @@ -64,6 +64,7 @@ module('Unit | Model | role', function (hooks) { principals: [ { id: '1', type: 'user' }, { id: '3', type: 'group' }, + { id: '5', type: 'managed group' }, ], version: 1, scope: { @@ -84,7 +85,7 @@ module('Unit | Model | role', function (hooks) { (schema, request) => { const body = JSON.parse(request.requestBody); assert.deepEqual(body, { - principal_ids: ['1', '3'], + principal_ids: ['1', '3', '5'], version: 1, }); return { id: '123abc' }; @@ -101,6 +102,7 @@ module('Unit | Model | role', function (hooks) { principals: [ { id: '1', type: 'user' }, { id: '3', type: 'group' }, + { id: '5', type: 'managed group' }, ], version: 1, scope: { @@ -111,7 +113,7 @@ module('Unit | Model | role', function (hooks) { }, }); const model = store.peekRecord('role', '123abc'); - await model.removePrincipals(['1', '3']); + await model.removePrincipals(['1', '3', '5']); }); test('it has a `removePrincipal` method that deletes a single principal using `removePrincipals` method', async function (assert) { @@ -138,6 +140,7 @@ module('Unit | Model | role', function (hooks) { principals: [ { id: '1', type: 'user' }, { id: '3', type: 'group' }, + { id: '5', type: 'managed group' }, ], version: 1, scope: { diff --git a/ui/admin/app/routes/scopes/scope/roles/role/principals.js b/ui/admin/app/routes/scopes/scope/roles/role/principals.js index 25b9cbb64..4977dac20 100644 --- a/ui/admin/app/routes/scopes/scope/roles/role/principals.js +++ b/ui/admin/app/routes/scopes/scope/roles/role/principals.js @@ -8,60 +8,32 @@ import { notifySuccess, notifyError } from 'core/decorators/notify'; export default class ScopesScopeRolesRolePrincipalsRoute extends Route { // =services @service intl; - @service resourceFilterStore; // =methods /** - * Returns users and groups associated with this role. + * Returns users, groups and managed-groups associated with this role. * @param {object} params - * @return {Promise{RoleModel}} + * @return {Promise{role, principals}} */ async model() { const role = this.modelFor('scopes.scope.roles.role'); + // Fetch user and group principals. + const users = await role.users; + const groups = await role.groups; + const managedGroups = await role.managedGroups; + // Merge polymorphic principals. + const principals = [...users, ...groups, ...managedGroups]; - // Gather user and group IDs as separate arrays, since these - // will be queried in separate API queries. - const userIDs = role.principals - .filter(({ type }) => type === 'user') - .map(({ principal_id }) => principal_id); - const groupIDs = role.principals - .filter(({ type }) => type === 'group') - .map(({ principal_id }) => principal_id); - - // Query for users. - const users = userIDs?.length - ? ( - await this.resourceFilterStore.queryBy( - 'user', - { id: userIDs }, - { scope_id: 'global', recursive: true } - ) - ).map((model) => model) - : []; - // Query for groups. - const groups = groupIDs?.length - ? ( - await this.resourceFilterStore.queryBy( - 'group', - { id: groupIDs }, - { scope_id: 'global', recursive: true } - ) - ).map((model) => model) - : []; - - return { - role, - principals: users.concat(groups), - }; + return { role, principals }; } // =actions /** * Remove a principal from the current role and redirect to principals index. - * @param {UserModel, GroupModel} principal + * @param {UserModel, GroupModel, ManagedGroupModel} principal */ @action @loading