From 971e483d019b478d43007baa74f3f6dc77cc0e5e Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Wed, 12 Aug 2020 13:41:23 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20principals=20list?= =?UTF-8?q?=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/models/fragment-principal.js | 20 ++++++++++ addons/api/addon/models/role.js | 9 +++++ .../addon/serializers/fragment-principal.js | 18 +++++++++ addons/api/app/models/fragment-principal.js | 1 + .../api/app/serializers/fragment-principal.js | 1 + .../unit/models/fragment-principal-test.js | 13 +++++++ .../serializers/fragment-principal-test.js | 30 +++++++++++++++ ui/core/app/router.js | 1 + .../scopes/scope/roles/role/principals.js | 37 +++++++++++++++++++ .../app/templates/scopes/scope/roles/role.hbs | 3 ++ .../scopes/scope/roles/role/principals.hbs | 22 +++++++++++ ui/core/mirage/factories/role.js | 26 +++++++++++++ ui/core/mirage/scenarios/default.js | 7 ++-- ui/core/tests/acceptance/roles-test.js | 4 ++ .../scope/roles/role/principals-test.js | 11 ++++++ ui/core/translations/en-us.yaml | 2 + 16 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 addons/api/addon/models/fragment-principal.js create mode 100644 addons/api/addon/serializers/fragment-principal.js create mode 100644 addons/api/app/models/fragment-principal.js create mode 100644 addons/api/app/serializers/fragment-principal.js create mode 100644 addons/api/tests/unit/models/fragment-principal-test.js create mode 100644 addons/api/tests/unit/serializers/fragment-principal-test.js create mode 100644 ui/core/app/routes/scopes/scope/roles/role/principals.js create mode 100644 ui/core/app/templates/scopes/scope/roles/role/principals.hbs create mode 100644 ui/core/tests/unit/routes/scopes/scope/roles/role/principals-test.js diff --git a/addons/api/addon/models/fragment-principal.js b/addons/api/addon/models/fragment-principal.js new file mode 100644 index 000000000..4c6bcb9c0 --- /dev/null +++ b/addons/api/addon/models/fragment-principal.js @@ -0,0 +1,20 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import { attr } from '@ember-data/model'; + +export default class FragmentPrincipal extends Fragment { + + // =attributes + + @attr('string', { + description: 'ID of the related scope' + }) scope_id; + + @attr('string', { + description: 'ID of the related principal (user or group)' + }) principal_id; + + @attr('string', { + description: 'Type of the related principal (user or group)' + }) type; + +} diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js index ccd4d1048..f4f25df61 100644 --- a/addons/api/addon/models/role.js +++ b/addons/api/addon/models/role.js @@ -5,6 +5,15 @@ export default class RoleModel extends GeneratedRoleModel { // =attributes + /** + * Principals are users and groups assigned to a role. They are represented + * as references to User and Group instances. Since Ember Data relationships + * are wanting, we do not model these as a polymorphic relationship as might + * see obvious. Instead, the application layer is expected to load referenced + * users and groups as needed. + */ + @fragmentArray('fragment-principal', {readOnly: true}) principals; + /** * Grants is read-only _most_ under normal circumstances. But grants can * be persisted via a dedicated call to `saveGrants()`. diff --git a/addons/api/addon/serializers/fragment-principal.js b/addons/api/addon/serializers/fragment-principal.js new file mode 100644 index 000000000..8d8896594 --- /dev/null +++ b/addons/api/addon/serializers/fragment-principal.js @@ -0,0 +1,18 @@ +import JSONSerializer from '@ember-data/serializer/json'; + +export default class FragmentPrincipalSerializer extends JSONSerializer { + + // =methods + + /** + * Renames `id` to `principal_id` because fragments may not have IDs. + * Otherwise normalizes as normal. + * @param {ModelClass} typeClass + * @param {object} resourceHash + * @return {object} + */ + normalize(typeClass, { id: principal_id, ...obj }) { + return super.normalize(typeClass, { principal_id, ...obj }); + } + +} diff --git a/addons/api/app/models/fragment-principal.js b/addons/api/app/models/fragment-principal.js new file mode 100644 index 000000000..52ab0dd03 --- /dev/null +++ b/addons/api/app/models/fragment-principal.js @@ -0,0 +1 @@ +export { default } from 'api/models/fragment-principal'; diff --git a/addons/api/app/serializers/fragment-principal.js b/addons/api/app/serializers/fragment-principal.js new file mode 100644 index 000000000..ef051478b --- /dev/null +++ b/addons/api/app/serializers/fragment-principal.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/fragment-principal'; diff --git a/addons/api/tests/unit/models/fragment-principal-test.js b/addons/api/tests/unit/models/fragment-principal-test.js new file mode 100644 index 000000000..055f52da7 --- /dev/null +++ b/addons/api/tests/unit/models/fragment-principal-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | fragment principal', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let store = this.owner.lookup('service:store'); + let model = store.createRecord('fragment-principal', {}); + assert.ok(model); + }); +}); diff --git a/addons/api/tests/unit/serializers/fragment-principal-test.js b/addons/api/tests/unit/serializers/fragment-principal-test.js new file mode 100644 index 000000000..bda2d46c5 --- /dev/null +++ b/addons/api/tests/unit/serializers/fragment-principal-test.js @@ -0,0 +1,30 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import FragmentPrincipal from 'api/models/fragment-principal'; + +module('Unit | Serializer | fragment principal', function(hooks) { + setupTest(hooks); + + test('it normalizes `id` into `principal_id`', function(assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('fragment-principal'); + const normalized = serializer.normalize(FragmentPrincipal, { + scope_id: '1', + principal_id: '2', + type: 'user' + }); + assert.deepEqual(normalized, { + data: { + attributes: { + scope_id: '1', + principal_id: '2', + type: 'user' + }, + id: null, + relationships: {}, + type: 'fragment-principal' + } + }); + }); +}); diff --git a/ui/core/app/router.js b/ui/core/app/router.js index bba93d1ad..b26d1d098 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -23,6 +23,7 @@ Router.map(function () { this.route('roles', function () { this.route('role', { path: ':role_id' }, function () { this.route('grants'); + this.route('principals'); }); this.route('new'); }); diff --git a/ui/core/app/routes/scopes/scope/roles/role/principals.js b/ui/core/app/routes/scopes/scope/roles/role/principals.js new file mode 100644 index 000000000..db40834d2 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/roles/role/principals.js @@ -0,0 +1,37 @@ +import Route from '@ember/routing/route'; +import { all } from 'rsvp'; + +export default class ScopesScopeRolesRolePrincipalsRoute extends Route { + + // =methods + + /** + * Returns users and groups associated with this role. + * @param {object} params + * @return {Promise{RoleModel}} + */ + model() { + const adapterOptions = { scopeID: this.modelFor('scopes.scope').id }; + const role = this.modelFor('scopes.scope.roles.role'); + const users = role.principals + .filterBy('type', 'user') + .map(({ principal_id }) => + this.store.findRecord('user', principal_id, { adapterOptions }) + .then(model => ({ + type: 'user', + model + })) + ); + const groups = role.principals + .filterBy('type', 'group') + .map(({ principal_id }) => + this.store.findRecord('group', principal_id, { adapterOptions }) + .then(model => ({ + type: 'group', + model + })) + ); + return all(users.concat(groups)); + } + +} diff --git a/ui/core/app/templates/scopes/scope/roles/role.hbs b/ui/core/app/templates/scopes/scope/roles/role.hbs index 552717330..0cd189b0a 100644 --- a/ui/core/app/templates/scopes/scope/roles/role.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role.hbs @@ -15,6 +15,9 @@ {{t 'titles.settings'}} + + {{t 'titles.principals'}} + {{t 'titles.grants'}} diff --git a/ui/core/app/templates/scopes/scope/roles/role/principals.hbs b/ui/core/app/templates/scopes/scope/roles/role/principals.hbs new file mode 100644 index 000000000..28c387e47 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/roles/role/principals.hbs @@ -0,0 +1,22 @@ +{{title (t 'titles.principals')}} + + + + + {{t 'form.label.name'}} + {{t 'form.label.type'}} + + + + {{#each @model as |principal|}} + + + {{principal.model.displayName}} + + + {{t (concat 'resources.' principal.type)}} + + + {{/each}} + + diff --git a/ui/core/mirage/factories/role.js b/ui/core/mirage/factories/role.js index 739b273e6..0dab3b584 100644 --- a/ui/core/mirage/factories/role.js +++ b/ui/core/mirage/factories/role.js @@ -1,4 +1,5 @@ import factory from '../generated/factories/role'; +import { trait } from 'ember-cli-mirage'; export default factory.extend({ @@ -8,4 +9,29 @@ export default factory.extend({ 'id=*;type=host-catalog;actions=create,read' ], + /** + * Create roles with associated "principals": related users and groups. + */ + withPrincipals: trait({ + afterCreate(role, server) { + const users = server.createList('user', 2); + const groups = server.createList('group', 2); + const userPrincipalFragments = users.map(user => ({ + scope_id: role.scope.id, + id: user.id, + type: 'user' + })); + const groupPrincipalFragments = groups.map(group => ({ + scope_id: role.scope.id, + id: group.id, + type: 'group' + })); + const principals = [ + ...userPrincipalFragments, + ...groupPrincipalFragments + ]; + role.update({ principals }); + } + }) + }); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index 01b55037b..6f8e29775 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -18,11 +18,10 @@ export default function(server) { // User server.createList('user', 5, { scope }); - - // Role - server.createList('role', 5, { scope }); - // Groups + // Group server.createList('group', 5, { scope }); + // Role + server.createList('role', 5, { scope }, 'withPrincipals'); // Other resources diff --git a/ui/core/tests/acceptance/roles-test.js b/ui/core/tests/acceptance/roles-test.js index 7db363fbe..2aa364f1b 100644 --- a/ui/core/tests/acceptance/roles-test.js +++ b/ui/core/tests/acceptance/roles-test.js @@ -218,6 +218,10 @@ module('Acceptance | roles', function (hooks) { ); }); + test('principals may be viewed in principals tab', async function (assert) { + assert.expect(0); + }); + test('grants can be updated and saved or canceled', async function (assert) { assert.expect(0); }); diff --git a/ui/core/tests/unit/routes/scopes/scope/roles/role/principals-test.js b/ui/core/tests/unit/routes/scopes/scope/roles/role/principals-test.js new file mode 100644 index 000000000..ce601db65 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/roles/role/principals-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/roles/role/principals', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/roles/role/principals'); + assert.ok(route); + }); +}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index 5252c7e6f..a1a339daf 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -47,6 +47,7 @@ titles: groups-welcome: Welcome to Groups grants: Permissions new-grant: New Grant + principals: Principals descriptions: host-catalogs: Overview of collections of hosts and host sets. empty-set: There are no items to display yet. You may be able to add items or try back later. @@ -69,6 +70,7 @@ form: identification: Username password: Password grant: Grant + principal: Principal errors: titles: unauthenticated-error: You are not signed in