From ee6999251d4ce4206fa652f459ad60675cdb5c77 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 7 Aug 2020 15:42:40 -0400 Subject: [PATCH 01/10] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20support=20f?= =?UTF-8?q?or=20grants=20on=20role=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/models/role.js | 29 +++++++++++- addons/api/addon/serializers/role.js | 29 ++++++++++++ addons/api/app/serializers/role.js | 1 + .../api/tests/unit/serializers/role-test.js | 44 +++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 addons/api/addon/serializers/role.js create mode 100644 addons/api/app/serializers/role.js create mode 100644 addons/api/tests/unit/serializers/role-test.js diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js index dbb69dce8..09ff5d256 100644 --- a/addons/api/addon/models/role.js +++ b/addons/api/addon/models/role.js @@ -1,3 +1,30 @@ import GeneratedRoleModel from '../generated/models/role'; +import { array } from 'ember-data-model-fragments/attributes'; -export default class RoleModel extends GeneratedRoleModel {} +export default class RoleModel extends GeneratedRoleModel { + + // =attributes + + /** + * Grants is read-only _most_ under normal circumstances. But grants can + * be persisted via a dedicated call to `saveGrants()`. + */ + @array({readOnly: true}) grants; + + // =methods + + /** + * Saves the `grants` array on the role via the `set-grants` method. + * See serializer and adapter for more information. + * @return {Promise} + */ + saveGrants() { + return this.save({ + adapterOptions: { + method: 'set-grants', + serializeGrants: true + } + }); + } + +} diff --git a/addons/api/addon/serializers/role.js b/addons/api/addon/serializers/role.js new file mode 100644 index 000000000..69e8fe568 --- /dev/null +++ b/addons/api/addon/serializers/role.js @@ -0,0 +1,29 @@ +import ApplicationSerializer from './application'; +import { get } from '@ember/object'; + +export default class RoleSerializer extends ApplicationSerializer { + + // =methods + + /** + * If `adapterOptions.serializeGrants` is true, the serialization should + * include **only grants** and nothing else. Normally, grants are not + * serialized. + * @override + * @param {Snapshot} snapshot + * @return {object} + */ + serialize(snapshot) { + const serializeGrants = get(snapshot, 'adapterOptions.serializeGrants'); + let serialized = super.serialize(...arguments); + if (serializeGrants) serialized = this.serializeWithGrants(snapshot); + return serialized; + } + + serializeWithGrants(snapshot) { + return { + grants: snapshot.attr('grants') + }; + } + +} diff --git a/addons/api/app/serializers/role.js b/addons/api/app/serializers/role.js new file mode 100644 index 000000000..58c592caf --- /dev/null +++ b/addons/api/app/serializers/role.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/role'; diff --git a/addons/api/tests/unit/serializers/role-test.js b/addons/api/tests/unit/serializers/role-test.js new file mode 100644 index 000000000..7c97e2db5 --- /dev/null +++ b/addons/api/tests/unit/serializers/role-test.js @@ -0,0 +1,44 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Serializer | role', function(hooks) { + setupTest(hooks); + + test('it serializes roles normally, without grants', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('role'); + const record = store.createRecord('role', { + name: 'User', + description: 'Description', + grants: ['foo', 'bar'] + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = {}; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + name: 'User', + description: 'Description' + }); + }); + + test('it serializes only grants when `adapterOptions.serializeGrants` is true', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('role'); + const record = store.createRecord('role', { + name: 'User', + description: 'Description', + grants: ['foo', 'bar'] + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = { + serializeGrants: true + }; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + grants: ['foo', 'bar'] + }); + }); + +}); From 3b09ec80cdbb78187b940e82a37d0857046d2874 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 7 Aug 2020 16:29:56 -0400 Subject: [PATCH 02/10] =?UTF-8?q?chore:=20=F0=9F=A4=96=20change=20custom?= =?UTF-8?q?=20method=20requests=20to=20use=20POST=20on=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/adapters/application.js | 21 +++++++++++++ addons/api/addon/serializers/role.js | 5 ++++ .../tests/unit/adapters/application-test.js | 17 +++++++++++ addons/api/tests/unit/models/role-test.js | 30 +++++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/addons/api/addon/adapters/application.js b/addons/api/addon/adapters/application.js index 09b772cb7..187c94ad1 100644 --- a/addons/api/addon/adapters/application.js +++ b/addons/api/addon/adapters/application.js @@ -1,4 +1,5 @@ import RESTAdapter from '@ember-data/adapter/rest'; +import { serializeIntoHash } from '@ember-data/adapter/-private'; import AdapterBuildURLMixin from '../mixins/adapter-build-url'; import config from 'ember-get-config'; import { get, getWithDefault } from '@ember/object'; @@ -143,6 +144,26 @@ export default class ApplicationAdapter extends RESTAdapter.extend( return super.query(...arguments).then(prenormalizeArrayResponse); } + /** + * Our API expects update calls with a custom method suffix + * (e.g. `:set-grants`) to be made with POST instead of PUT/PATCH. + * If this method is called with a custom method via `adapterOptions.method`, + * the API request will be made with POST. All other update requests are + * made as normal. + * @override + * @param {Store} store + * @param {Model} type + * @param {Snapshot} snapshot + * @return {Promise} promise + */ + updateRecord(store, type, snapshot) { + const method = get(snapshot, 'adapterOptions.method'); + const data = serializeIntoHash(store, type, snapshot, {}); + const id = snapshot.id; + const url = this.buildURL(type.modelName, id, snapshot, 'updateRecord'); + return this.ajax(url, method ? 'POST' : 'PUT', { data }); + } + /** * Overrides default ajax method by rewritting PUT requests as PATCH. * PATCH is the request method used by our API for updates. diff --git a/addons/api/addon/serializers/role.js b/addons/api/addon/serializers/role.js index 69e8fe568..d22f01ef0 100644 --- a/addons/api/addon/serializers/role.js +++ b/addons/api/addon/serializers/role.js @@ -20,6 +20,11 @@ export default class RoleSerializer extends ApplicationSerializer { return serialized; } + /** + * Returns a payload containing only the grants array. + * @param {Snapshot} snapshot + * @return {object} + */ serializeWithGrants(snapshot) { return { grants: snapshot.attr('grants') diff --git a/addons/api/tests/unit/adapters/application-test.js b/addons/api/tests/unit/adapters/application-test.js index c3d72d469..dc984bba2 100644 --- a/addons/api/tests/unit/adapters/application-test.js +++ b/addons/api/tests/unit/adapters/application-test.js @@ -112,6 +112,23 @@ module('Unit | Adapter | application', function (hooks) { adapter.ajax('/', 'PUT'); }); + test('it rewrites PUT/PATCH to POST when `adapterOptions.method` is passed to `updateRecord`', function (assert) { + assert.expect(1); + const adapter = this.owner.lookup('adapter:application'); + const store = this.owner.lookup('service:store'); + const snapshot = store.createRecord('role', {})._createSnapshot(); + snapshot.adapterOptions = { + method: 'custom-method' + } + // TODO this is icky, should be changed to a spy or stub + const originalAjax = RESTAdapter.prototype.ajax; + RESTAdapter.prototype.ajax = (url, type) => { + assert.equal(type, 'POST'); + RESTAdapter.prototype.ajax = originalAjax; + }; + adapter.updateRecord(store, {modelName: 'user'}, snapshot); + }); + test('it prenormalizes "empty" responses into a form that the fetch-manager will not reject', async function (assert) { assert.expect(1); const adapter = this.owner.lookup('adapter:application'); diff --git a/addons/api/tests/unit/models/role-test.js b/addons/api/tests/unit/models/role-test.js index b47f31343..68160dec1 100644 --- a/addons/api/tests/unit/models/role-test.js +++ b/addons/api/tests/unit/models/role-test.js @@ -1,12 +1,42 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; module('Unit | Model | role', function (hooks) { setupTest(hooks); + setupMirage(hooks); test('it exists', function(assert) { let store = this.owner.lookup('service:store'); let model = store.createRecord('role', {}); assert.ok(model); }); + + test('it has a `saveGrants` 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:set-grants', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + grants: ['grant1', 'grant2'] + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'role', + attributes: { + name: 'Role', + description: 'Description', + grants: ['grant1', 'grant2'], + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('role', '123abc'); + await model.saveGrants(); + }); }); From e142e99fdbbb95a3fbb8be577940017e82d9a344 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 7 Aug 2020 18:19:58 -0400 Subject: [PATCH 03/10] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20model=20grants?= =?UTF-8?q?=20as=20a=20fragment-array=20for=20statefulness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/generated/models/role.js | 6 +++ addons/api/addon/models/fragment-string.js | 16 ++++++++ addons/api/addon/models/role.js | 4 +- addons/api/addon/serializers/role.js | 21 +++++++++- addons/api/app/models/fragment-string.js | 1 + .../tests/unit/models/fragment-string-test.js | 13 ++++++ addons/api/tests/unit/models/role-test.js | 6 ++- .../api/tests/unit/serializers/role-test.js | 41 +++++++++++++++++-- 8 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 addons/api/addon/models/fragment-string.js create mode 100644 addons/api/app/models/fragment-string.js create mode 100644 addons/api/tests/unit/models/fragment-string-test.js diff --git a/addons/api/addon/generated/models/role.js b/addons/api/addon/generated/models/role.js index 254005ed5..0177972a0 100644 --- a/addons/api/addon/generated/models/role.js +++ b/addons/api/addon/generated/models/role.js @@ -33,4 +33,10 @@ export default class GeneratedRoleModel extends BaseModel { description: 'Whether the resource is disabled', }) disabled; + + @attr('number', { + description: 'Current version number of this resource.', + }) + version; + } diff --git a/addons/api/addon/models/fragment-string.js b/addons/api/addon/models/fragment-string.js new file mode 100644 index 000000000..78ddb80b5 --- /dev/null +++ b/addons/api/addon/models/fragment-string.js @@ -0,0 +1,16 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import { attr } from '@ember-data/model'; + +/** + * Represents an item in an array of strings. + */ +export default class FragmentStringModel extends Fragment { + + // =attributes + + @attr('string', { + description: 'The value of the string.' + }) + value; + +} diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js index 09ff5d256..ccd4d1048 100644 --- a/addons/api/addon/models/role.js +++ b/addons/api/addon/models/role.js @@ -1,5 +1,5 @@ import GeneratedRoleModel from '../generated/models/role'; -import { array } from 'ember-data-model-fragments/attributes'; +import { fragmentArray } from 'ember-data-model-fragments/attributes'; export default class RoleModel extends GeneratedRoleModel { @@ -9,7 +9,7 @@ export default class RoleModel extends GeneratedRoleModel { * Grants is read-only _most_ under normal circumstances. But grants can * be persisted via a dedicated call to `saveGrants()`. */ - @array({readOnly: true}) grants; + @fragmentArray('fragment-string', {readOnly: true}) grants; // =methods diff --git a/addons/api/addon/serializers/role.js b/addons/api/addon/serializers/role.js index d22f01ef0..c4fb5ebb2 100644 --- a/addons/api/addon/serializers/role.js +++ b/addons/api/addon/serializers/role.js @@ -7,7 +7,7 @@ export default class RoleSerializer extends ApplicationSerializer { /** * If `adapterOptions.serializeGrants` is true, the serialization should - * include **only grants** and nothing else. Normally, grants are not + * include **only grants** and the version. Normally, grants are not * serialized. * @override * @param {Snapshot} snapshot @@ -27,8 +27,25 @@ export default class RoleSerializer extends ApplicationSerializer { */ serializeWithGrants(snapshot) { return { - grants: snapshot.attr('grants') + version: snapshot.attr('version'), + grants: snapshot.attr('grants').map(grant => grant.attr('value')) }; } + /** + * Converts string arrays to object arrays for use with `fragment-string`. + * E.g. `"foo"` -> `{value: "foo"}` + * @override + * @param {Model} typeClass + * @param {Object} hash + * @return {Object} + */ + normalize(modelClass, resourceHash) { + // TODO: can fragment string normalization be handled generically? + if (resourceHash.grants) { + resourceHash.grants = resourceHash.grants.map(value => ({ value })); + } + return super.normalize(modelClass, resourceHash); + } + } diff --git a/addons/api/app/models/fragment-string.js b/addons/api/app/models/fragment-string.js new file mode 100644 index 000000000..90c1321af --- /dev/null +++ b/addons/api/app/models/fragment-string.js @@ -0,0 +1 @@ +export { default } from 'api/models/fragment-string'; diff --git a/addons/api/tests/unit/models/fragment-string-test.js b/addons/api/tests/unit/models/fragment-string-test.js new file mode 100644 index 000000000..8bfd2c018 --- /dev/null +++ b/addons/api/tests/unit/models/fragment-string-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | fragment string', 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-string', {}); + assert.ok(model); + }); +}); diff --git a/addons/api/tests/unit/models/role-test.js b/addons/api/tests/unit/models/role-test.js index 68160dec1..7cf7ba8fa 100644 --- a/addons/api/tests/unit/models/role-test.js +++ b/addons/api/tests/unit/models/role-test.js @@ -17,7 +17,8 @@ module('Unit | Model | role', function (hooks) { this.server.post('/v1/scopes/o_1/roles/123abc:set-grants', (schema, request) => { const body = JSON.parse(request.requestBody); assert.deepEqual(body, { - grants: ['grant1', 'grant2'] + grants: ['grant1', 'grant2'], + version: 1 }); }); const store = this.owner.lookup('service:store'); @@ -28,7 +29,8 @@ module('Unit | Model | role', function (hooks) { attributes: { name: 'Role', description: 'Description', - grants: ['grant1', 'grant2'], + grants: [{value: 'grant1'}, {value: 'grant2'}], + version: 1, scope: { scope_id: 'o_1', type: 'scope' diff --git a/addons/api/tests/unit/serializers/role-test.js b/addons/api/tests/unit/serializers/role-test.js index 7c97e2db5..5724cfb89 100644 --- a/addons/api/tests/unit/serializers/role-test.js +++ b/addons/api/tests/unit/serializers/role-test.js @@ -11,14 +11,16 @@ module('Unit | Serializer | role', function(hooks) { const record = store.createRecord('role', { name: 'User', description: 'Description', - grants: ['foo', 'bar'] + grants: [{value: 'foo'}, {value: 'bar'}], + version: 1 }); const snapshot = record._createSnapshot(); snapshot.adapterOptions = {}; const serializedRecord = serializer.serialize(snapshot); assert.deepEqual(serializedRecord, { name: 'User', - description: 'Description' + description: 'Description', + version: 1 }); }); @@ -29,7 +31,8 @@ module('Unit | Serializer | role', function(hooks) { const record = store.createRecord('role', { name: 'User', description: 'Description', - grants: ['foo', 'bar'] + grants: [{value: 'foo'}, {value: 'bar'}], + version: 1 }); const snapshot = record._createSnapshot(); snapshot.adapterOptions = { @@ -37,7 +40,37 @@ module('Unit | Serializer | role', function(hooks) { }; const serializedRecord = serializer.serialize(snapshot); assert.deepEqual(serializedRecord, { - grants: ['foo', 'bar'] + grants: ['foo', 'bar'], + version: 1 + }); + }); + + test('it normalizes records with array fields', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('role'); + const roleModelClass = store.createRecord('role').constructor; + const payload = { + id: '1', + name: 'Role 1', + grants: ['*', '*'] + }; + const normalized = serializer.normalizeSingleResponse( + store, + roleModelClass, + payload + ); + assert.deepEqual(normalized, { + included: [], + data: { + id: '1', + type: 'role', + attributes: { + name: 'Role 1', + grants: [{value: '*'}, {value: '*'}], + }, + relationships: {}, + }, }); }); From 480b55513fd1c449d9f4ed832d57df1ba3ca2333 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 7 Aug 2020 18:56:21 -0400 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20grants=20rou?= =?UTF-8?q?te=20and=20update=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/form/role/grants/index.hbs | 45 +++++++++++++++++++ ui/core/app/router.js | 4 +- .../routes/scopes/scope/roles/role/grants.js | 31 +++++++++++++ .../app/templates/scopes/scope/roles/role.hbs | 27 ++++++++++- .../scopes/scope/roles/role/grants.hbs | 7 +++ .../scopes/scope/roles/role/index.hbs | 40 ++++++----------- ui/core/mirage/config.js | 7 +++ ui/core/mirage/factories/role.js | 10 ++++- .../scopes/scope/roles/role/grants-test.js | 11 +++++ 9 files changed, 152 insertions(+), 30 deletions(-) create mode 100644 ui/core/app/components/form/role/grants/index.hbs create mode 100644 ui/core/app/routes/scopes/scope/roles/role/grants.js create mode 100644 ui/core/app/templates/scopes/scope/roles/role/grants.hbs create mode 100644 ui/core/tests/unit/routes/scopes/scope/roles/role/grants-test.js diff --git a/ui/core/app/components/form/role/grants/index.hbs b/ui/core/app/components/form/role/grants/index.hbs new file mode 100644 index 000000000..ec731c2fe --- /dev/null +++ b/ui/core/app/components/form/role/grants/index.hbs @@ -0,0 +1,45 @@ + + + {{!-- + {{#if @model.errors.grants}} + + {{#each @model.errors.grants as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + --}} + + + {{#each @model.grants as |grant|}} + + + + {{t "form.label.grant"}} + + + + + + + {{/each}} + + + + + diff --git a/ui/core/app/router.js b/ui/core/app/router.js index a4f121c19..bba93d1ad 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -21,7 +21,9 @@ Router.map(function () { this.route('new'); }); this.route('roles', function () { - this.route('role', { path: ':role_id' }, function () {}); + this.route('role', { path: ':role_id' }, function () { + this.route('grants'); + }); this.route('new'); }); this.route('projects', function() { diff --git a/ui/core/app/routes/scopes/scope/roles/role/grants.js b/ui/core/app/routes/scopes/scope/roles/role/grants.js new file mode 100644 index 000000000..098de9656 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/roles/role/grants.js @@ -0,0 +1,31 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class ScopesScopeRolesRoleGrantsRoute extends Route { + + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Save an role in current scope. + * @param {RoleModel} role + */ + @action + async save(role) { + try { + await role.saveGrants(); + this.notify.success( + this.intl.t('notify.save-success') + ); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } + +} diff --git a/ui/core/app/templates/scopes/scope/roles/role.hbs b/ui/core/app/templates/scopes/scope/roles/role.hbs index d66e83390..552717330 100644 --- a/ui/core/app/templates/scopes/scope/roles/role.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role.hbs @@ -1,3 +1,28 @@ {{title @model.displayName}} -{{outlet}} + + + + + + + +

{{t "resources.role"}}

+
+ + + + + {{t 'titles.settings'}} + + + {{t 'titles.grants'}} + + + + + + {{outlet}} + + +
diff --git a/ui/core/app/templates/scopes/scope/roles/role/grants.hbs b/ui/core/app/templates/scopes/scope/roles/role/grants.hbs new file mode 100644 index 000000000..81827f673 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/roles/role/grants.hbs @@ -0,0 +1,7 @@ +{{title (t 'titles.grants')}} + + diff --git a/ui/core/app/templates/scopes/scope/roles/role/index.hbs b/ui/core/app/templates/scopes/scope/roles/role/index.hbs index 1b35c1929..bbb671abe 100644 --- a/ui/core/app/templates/scopes/scope/roles/role/index.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role/index.hbs @@ -1,27 +1,13 @@ - - - - - - - -

{{t "resources.role"}}

-
- - - - - - {{t 'actions.delete'}} - - - -
+ + + + {{t 'actions.delete'}} + diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 5d54a9320..73342c6b0 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -64,6 +64,13 @@ export default function() { this.get('/scopes/:scope_id/roles/:id'); this.patch('/scopes/:scope_id/roles/:id'); this.del('/scopes/:scope_id/roles/:id'); + this.post('/scopes/:scope_id/roles/:id', function ({ roles }, request) { + const attrs = this.normalizedRequestAttrs(); + const id = request.params.id.split(':')[0]; + const role = roles.find(id); + attrs.id = id; + return role.update(attrs); + }); // IAM: Groups this.get('/scopes/:scope_id/groups'); diff --git a/ui/core/mirage/factories/role.js b/ui/core/mirage/factories/role.js index abcb8de67..e597c3433 100644 --- a/ui/core/mirage/factories/role.js +++ b/ui/core/mirage/factories/role.js @@ -1,3 +1,11 @@ import factory from '../generated/factories/role'; -export default factory.extend(); +export default factory.extend({ + + // eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects + grants: [ + 'id=*;action=*', + 'id=*;type=host-catalog;actions=create,read' + ], + +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/roles/role/grants-test.js b/ui/core/tests/unit/routes/scopes/scope/roles/role/grants-test.js new file mode 100644 index 000000000..87b0c03c7 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/roles/role/grants-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/roles/role/grants', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/roles/role/grants'); + assert.ok(route); + }); +}); From 083a913249ad46042159883d5e69f772d3c37193 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 7 Aug 2020 18:58:00 -0400 Subject: [PATCH 05/10] chore: add missing translations --- ui/core/translations/en-us.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index d2b11942c..c40894096 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -44,6 +44,7 @@ titles: users-welcome: Welcome to Users roles-welcome: Welcome to Roles groups-welcome: Welcome to Groups + grants: Permissions 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. @@ -65,6 +66,7 @@ form: type: Type identification: Username password: Password + grant: Grant errors: titles: unauthenticated-error: You are not signed in From f539224afe9c4c2c004f59bb2da72be03e538f90 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Mon, 10 Aug 2020 10:59:09 -0400 Subject: [PATCH 06/10] style: add bottom margin to key/value list --- addons/rose/addon/styles/rose/components/list/_key-value.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/rose/addon/styles/rose/components/list/_key-value.scss b/addons/rose/addon/styles/rose/components/list/_key-value.scss index a6b54eb24..fcce12059 100644 --- a/addons/rose/addon/styles/rose/components/list/_key-value.scss +++ b/addons/rose/addon/styles/rose/components/list/_key-value.scss @@ -2,6 +2,10 @@ @use '../../variables/typography'; @use '../../utilities/type'; +.rose-list-key-value { + margin-bottom: sizing.rems(m); +} + .rose-list-key-value-item { display: flex; border-bottom: 1px solid var(--ui-gray-subtler-3); From 674882d7bdda930f8dedca31c4aeeb6651d2a0c8 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Mon, 10 Aug 2020 16:40:03 -0400 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20finish=20up=20gran?= =?UTF-8?q?ts=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/form/role/grants/index.hbs | 46 +++++++++++++++++++ .../app/components/form/role/grants/index.js | 37 +++++++++++++++ .../routes/scopes/scope/roles/role/grants.js | 25 +++++++++- .../scopes/scope/roles/role/grants.hbs | 2 + ui/core/tests/acceptance/roles-test.js | 12 +++++ ui/core/translations/en-us.yaml | 2 + 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 ui/core/app/components/form/role/grants/index.js diff --git a/ui/core/app/components/form/role/grants/index.hbs b/ui/core/app/components/form/role/grants/index.hbs index ec731c2fe..098621b45 100644 --- a/ui/core/app/components/form/role/grants/index.hbs +++ b/ui/core/app/components/form/role/grants/index.hbs @@ -1,3 +1,39 @@ + + + {{!-- Add grant field (only one per form) --}} + + + + {{t "titles.new-grant"}} + + + + + + + {{t "actions.add"}} + + + + + + + + + + {{t "actions.remove"}} + + {{/each}} diff --git a/ui/core/app/components/form/role/grants/index.js b/ui/core/app/components/form/role/grants/index.js new file mode 100644 index 000000000..509d4356f --- /dev/null +++ b/ui/core/app/components/form/role/grants/index.js @@ -0,0 +1,37 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { computed, action } from '@ember/object'; + +export default class FormRoleGrantsComponent extends Component { + + // =attributes + + /** + * @type {string} + */ + @tracked newGrantString = ''; + + /** + * True if the grant string field is empty, false otherwise. This is used + * to disable the submit button. + * @return {boolean} + */ + @computed('newGrantString') + get cannotSave() { + return !this.newGrantString; + } + + // =actions + + /** + * Calls the passed function with the grant string as an argument and then + * clears the value of the grant string field. + * `@addGrant` should be passed by the context calling this component. + * @param {Function} addGrantFn + */ + @action + createGrant(addGrantFn) { + addGrantFn(this.newGrantString); + this.newGrantString = ''; + } +} diff --git a/ui/core/app/routes/scopes/scope/roles/role/grants.js b/ui/core/app/routes/scopes/scope/roles/role/grants.js index 098de9656..a46277957 100644 --- a/ui/core/app/routes/scopes/scope/roles/role/grants.js +++ b/ui/core/app/routes/scopes/scope/roles/role/grants.js @@ -10,7 +10,30 @@ export default class ScopesScopeRolesRoleGrantsRoute extends Route { @service notify; // =methods - + + /** + * Adds a new grant to the role at the beginning of the grants list. + * Grant creation is not immediately permanent; users may rollback the change + * via "cancel" or commit it via "save". + * @param {RoleModel} role + * @param {string} grantString + */ + @action + addGrant(role, grantString) { + role.grants.unshiftObject({ value: grantString }); + } + + /** + * Removes a grant from the role. Grant removal is not immediately permanent; + * users may rollback the change via "cancel" or commit it via "save". + * @param {RoleModel} role + * @param {FragmentString} grant + */ + @action + removeGrant(role, grant) { + role.grants.removeFragment(grant); + } + /** * Save an role in current scope. * @param {RoleModel} role diff --git a/ui/core/app/templates/scopes/scope/roles/role/grants.hbs b/ui/core/app/templates/scopes/scope/roles/role/grants.hbs index 81827f673..edb0faf13 100644 --- a/ui/core/app/templates/scopes/scope/roles/role/grants.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role/grants.hbs @@ -4,4 +4,6 @@ @model={{@model}} @submit={{route-action "save" @model}} @cancel={{route-action "cancel" @model}} + @addGrant={{route-action "addGrant" @model}} + @removeGrant={{route-action "removeGrant" @model}} /> diff --git a/ui/core/tests/acceptance/roles-test.js b/ui/core/tests/acceptance/roles-test.js index dd8483574..7db363fbe 100644 --- a/ui/core/tests/acceptance/roles-test.js +++ b/ui/core/tests/acceptance/roles-test.js @@ -217,4 +217,16 @@ module('Acceptance | roles', function (hooks) { 'Displays primary error message.' ); }); + + test('grants can be updated and saved or canceled', async function (assert) { + assert.expect(0); + }); + + test('grants can be removed and saved or canceled', async function (assert) { + assert.expect(0); + }); + + test('new grants can be added and saved or canceled', async function (assert) { + assert.expect(0); + }); }); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index c40894096..6e025b4b4 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -1,4 +1,5 @@ actions: + add: Add cancel: Cancel delete: Delete dismiss: Dismiss @@ -45,6 +46,7 @@ titles: roles-welcome: Welcome to Roles groups-welcome: Welcome to Groups grants: Permissions + new-grant: New Grant 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. From 96a771167b4853ce1cd3c865bcc6b27e22292a2d Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 11 Aug 2020 12:00:09 -0400 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=F0=9F=90=9B=20field=20is=20`grant?= =?UTF-8?q?=5Fstrings`=20not=20`grants`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/serializers/role.js | 7 ++++--- addons/api/tests/unit/models/role-test.js | 2 +- addons/api/tests/unit/serializers/role-test.js | 4 ++-- ui/core/mirage/factories/role.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/addons/api/addon/serializers/role.js b/addons/api/addon/serializers/role.js index c4fb5ebb2..9cf12da58 100644 --- a/addons/api/addon/serializers/role.js +++ b/addons/api/addon/serializers/role.js @@ -28,7 +28,7 @@ export default class RoleSerializer extends ApplicationSerializer { serializeWithGrants(snapshot) { return { version: snapshot.attr('version'), - grants: snapshot.attr('grants').map(grant => grant.attr('value')) + grant_strings: snapshot.attr('grants').map(grant => grant.attr('value')) }; } @@ -42,8 +42,9 @@ export default class RoleSerializer extends ApplicationSerializer { */ normalize(modelClass, resourceHash) { // TODO: can fragment string normalization be handled generically? - if (resourceHash.grants) { - resourceHash.grants = resourceHash.grants.map(value => ({ value })); + if (resourceHash.grant_strings) { + resourceHash.grants = + resourceHash.grant_strings.map(value => ({ value })); } return super.normalize(modelClass, resourceHash); } diff --git a/addons/api/tests/unit/models/role-test.js b/addons/api/tests/unit/models/role-test.js index 7cf7ba8fa..fe1b8224b 100644 --- a/addons/api/tests/unit/models/role-test.js +++ b/addons/api/tests/unit/models/role-test.js @@ -17,7 +17,7 @@ module('Unit | Model | role', function (hooks) { this.server.post('/v1/scopes/o_1/roles/123abc:set-grants', (schema, request) => { const body = JSON.parse(request.requestBody); assert.deepEqual(body, { - grants: ['grant1', 'grant2'], + grant_strings: ['grant1', 'grant2'], version: 1 }); }); diff --git a/addons/api/tests/unit/serializers/role-test.js b/addons/api/tests/unit/serializers/role-test.js index 5724cfb89..1293d28b4 100644 --- a/addons/api/tests/unit/serializers/role-test.js +++ b/addons/api/tests/unit/serializers/role-test.js @@ -40,7 +40,7 @@ module('Unit | Serializer | role', function(hooks) { }; const serializedRecord = serializer.serialize(snapshot); assert.deepEqual(serializedRecord, { - grants: ['foo', 'bar'], + grant_strings: ['foo', 'bar'], version: 1 }); }); @@ -53,7 +53,7 @@ module('Unit | Serializer | role', function(hooks) { const payload = { id: '1', name: 'Role 1', - grants: ['*', '*'] + grant_strings: ['*', '*'] }; const normalized = serializer.normalizeSingleResponse( store, diff --git a/ui/core/mirage/factories/role.js b/ui/core/mirage/factories/role.js index e597c3433..739b273e6 100644 --- a/ui/core/mirage/factories/role.js +++ b/ui/core/mirage/factories/role.js @@ -3,7 +3,7 @@ import factory from '../generated/factories/role'; export default factory.extend({ // eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects - grants: [ + grant_strings: [ 'id=*;action=*', 'id=*;type=host-catalog;actions=create,read' ], From 17048eca6b8b3abe298151882da644728c044d3f Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 11 Aug 2020 13:39:11 -0400 Subject: [PATCH 09/10] =?UTF-8?q?chore:=20=F0=9F=A4=96=20udpate=20auth=20p?= =?UTF-8?q?ayload=20from=20`name`=20to=20`login=5Fname`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/auth/addon/authenticators/password.js | 6 +++--- addons/auth/tests/unit/authenticators/password-test.js | 2 +- ui/core/mirage/route-handlers/auth.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/auth/addon/authenticators/password.js b/addons/auth/addon/authenticators/password.js index 7c6d79a4e..67c8ce422 100644 --- a/addons/auth/addon/authenticators/password.js +++ b/addons/auth/addon/authenticators/password.js @@ -49,19 +49,19 @@ export default class PasswordAuthenticator extends BaseAuthenticator { * * @override * @param {object} creds - * @param {string} creds.identification is sent to server as `name` + * @param {string} creds.identification is sent to server as `login_name` * @param {string} creds.password * @param {boolean} requestCookies request cookie tokens (default `true`) * @return {Promise} */ async authenticate( - { identification: name, password }, + { identification: login_name, password }, requestCookies = true, options ) { const body = JSON.stringify({ token_type: requestCookies ? 'cookie' : null, - credentials: { name, password }, + credentials: { login_name, password }, }); const authEndpointURL = this.buildAuthEndpointURL(options); const response = await fetch(authEndpointURL, { method: 'post', body }); diff --git a/addons/auth/tests/unit/authenticators/password-test.js b/addons/auth/tests/unit/authenticators/password-test.js index aebdfee56..372d57a0c 100644 --- a/addons/auth/tests/unit/authenticators/password-test.js +++ b/addons/auth/tests/unit/authenticators/password-test.js @@ -41,7 +41,7 @@ module('Unit | Authenticator | password', function (hooks) { assert.deepEqual(json, { token_type: 'cookie', credentials: { - name: 'foo', + login_name: 'foo', password: 'bar', }, }); diff --git a/ui/core/mirage/route-handlers/auth.js b/ui/core/mirage/route-handlers/auth.js index 596500f46..ed2942c2f 100644 --- a/ui/core/mirage/route-handlers/auth.js +++ b/ui/core/mirage/route-handlers/auth.js @@ -12,7 +12,7 @@ import { Response } from 'miragejs'; // */ // export default function authenticateHandler(schema, request) { // const payload = JSON.parse(request.requestBody); -// if (payload.credentials.name === 'error') { +// if (payload.credentials.login_name === 'error') { // return new Response(400); // } else { // const cookieName = config.auth.passwordCookieName; @@ -25,7 +25,7 @@ import { Response } from 'miragejs'; export function authHandler({ scopes }, request) { const payload = JSON.parse(request.requestBody); - if (payload.credentials.name === 'error') { + if (payload.credentials.login_name === 'error') { return new Response(400); } else { const scopeAttrs = From e944a9b26c8a8120386ad23964667d98ba1381ad Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 11 Aug 2020 14:45:53 -0400 Subject: [PATCH 10/10] =?UTF-8?q?chore:=20=F0=9F=A4=96=20disable=20autocom?= =?UTF-8?q?plete=20on=20forms=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this may be re-enabled as needed --- addons/rose/addon/templates/components/rose/form.hbs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/rose/addon/templates/components/rose/form.hbs b/addons/rose/addon/templates/components/rose/form.hbs index 6c74ccae4..f37f36d09 100644 --- a/addons/rose/addon/templates/components/rose/form.hbs +++ b/addons/rose/addon/templates/components/rose/form.hbs @@ -3,8 +3,9 @@ novalidate method="post" class="rose-form" + autocomplete="off" {{on "submit" this.handleSubmit}} -> +> {{yield (hash radioGroup=(component 'rose/form/radio/group' disabled=@disabled) checkbox=(component 'rose/form/checkbox' disabled=@disabled)