From c14be070f759ba0c4dedc39a5e129d39198af050 Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Tue, 1 Jun 2021 22:29:19 -0400 Subject: [PATCH] feat: Add oidc status change dropdown to auth method actions --- addons/api/addon/models/auth-method.js | 4 +- addons/api/mirage/config.js | 29 +++- addons/api/mirage/route-handlers/auth.js | 2 +- addons/api/mirage/scenarios/default.js | 6 + addons/core/translations/resources/en-us.yaml | 1 + addons/core/translations/states/en-us.yaml | 6 + .../scopes/scope/auth-methods/auth-method.js | 16 +++ .../auth-methods/auth-method/-actions.hbs | 34 ++++- .../acceptance/auth-methods/oidc-test.js | 134 ++++++++++++++++++ 9 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 ui/admin/tests/acceptance/auth-methods/oidc-test.js diff --git a/addons/api/addon/models/auth-method.js b/addons/api/addon/models/auth-method.js index dc03abc0d..1ba8050a9 100644 --- a/addons/api/addon/models/auth-method.js +++ b/addons/api/addon/models/auth-method.js @@ -3,7 +3,7 @@ import { fragment } from 'ember-data-model-fragments/attributes'; import { equal } from '@ember/object/computed'; /** - * Enum ptions per auth method type and field. + * Enum options per auth method type and field. */ export const options = { oidc: { @@ -27,6 +27,8 @@ export const options = { }; export default class AuthMethodModel extends GeneratedAuthMethodModel { + STATES_ENUM = options.oidc.state; + // =attributes /** diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index 769e7e810..d34110910 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -1,6 +1,6 @@ import Ember from 'ember'; import config from '../config/environment'; -import { authHandler, deauthHandler } from './route-handlers/auth'; +import { deauthHandler } from './route-handlers/auth'; import { pickRandomStatusString } from './factories/session'; import { Response } from 'miragejs'; import initializeMockIPC from './scenarios/ipc'; @@ -71,6 +71,31 @@ export default function () { } return new Response(202); }); + this.post('/auth-methods/:idMethod', function (schema, request) { + const idMethod = request.params.idMethod; + const id = idMethod.split(':')[0]; + const method = idMethod.split(':')[1]; + + if (method === 'authenticate') { + schema.authHandler.call(this, schema, request); + return; + } + + const attrs = this.normalizedRequestAttrs(); + const authMethods = schema.authMethods; + const authMethod = authMethods.find(id); + let updatedAttrs = {}; + + if (method === 'change-state') { + updatedAttrs = { + attributes: { + state: attrs.attributes.state, + }, + }; + } + + return authMethod.update(updatedAttrs); + }); // Auth Method Accounts this.get( @@ -107,7 +132,7 @@ export default function () { ); // Authenticate/deauthenticate routes - this.post('/auth-methods/:id_method', authHandler); + // this.post('/auth-methods/:id_method', authHandler); this.post('/scopes/:id_method', deauthHandler); // IAM : Users diff --git a/addons/api/mirage/route-handlers/auth.js b/addons/api/mirage/route-handlers/auth.js index bd4fca7ac..522ac0b52 100644 --- a/addons/api/mirage/route-handlers/auth.js +++ b/addons/api/mirage/route-handlers/auth.js @@ -109,7 +109,7 @@ const commandHandlers = { export function authHandler({ scopes, authMethods }, request) { const payload = JSON.parse(request.requestBody); - const [, id] = request.params.id_method.match(/(?.[^:]*)/); + const [, id] = request.params.idMethod.match(/(?.[^:]*)/); const { command } = payload; const authMethod = authMethods.find(id); const scope = scopes.find(authMethod.scopeId); diff --git a/addons/api/mirage/scenarios/default.js b/addons/api/mirage/scenarios/default.js index 62018788c..1f2595bb8 100644 --- a/addons/api/mirage/scenarios/default.js +++ b/addons/api/mirage/scenarios/default.js @@ -1,3 +1,5 @@ +import { authHandler } from '../route-handlers/auth'; + export default function (server) { // Scope resources @@ -50,4 +52,8 @@ export default function (server) { server.createList('target', 2, { scope }, 'withRandomHostSets'); server.createList('session', 4, { scope }, 'withAssociations'); }); + + // Authenticate route handler + // TODO: Better way to pass-in auth handler + server.schema.authHandler = authHandler; } diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 7432ac466..aa42e8d53 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -38,6 +38,7 @@ auth-method: delete: Delete Auth Method make-primary: Make Primary remove-as-primary: Remove as Primary + change-state: State notifications: make-primary-success: Auth method successfully elected to primary. remove-as-primary-success: Auth method successfully removed as primary. diff --git a/addons/core/translations/states/en-us.yaml b/addons/core/translations/states/en-us.yaml index aa8e846bd..23a92c6cf 100644 --- a/addons/core/translations/states/en-us.yaml +++ b/addons/core/translations/states/en-us.yaml @@ -8,3 +8,9 @@ connection: success: description: "Local proxy address ({protocol}): {address}:{port}" primary: Primary +auth-method: + oidc: + states: + inactive: Inactive + active-private: Active Private + active-public: Active Public diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method.js b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method.js index 3c608f660..c492fed7b 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method.js @@ -1,4 +1,6 @@ import Route from '@ember/routing/route'; +import { action } from '@ember/object'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; export default class ScopesScopeAuthMethodsAuthMethodRoute extends Route { // =methods @@ -36,4 +38,18 @@ export default class ScopesScopeAuthMethodsAuthMethodRoute extends Route { outlet: 'actions', }); } + + // =actions + + /** + * Update state of OIDC auth method + * @param {string} state - "inactive", "active-private", "active-public" + */ + @action + @notifyError(({ message }) => message) + @notifySuccess('notifications.save-success') + async changeState(state) { + const model = this.modelFor('scopes.scope.auth-methods.auth-method'); + await model.changeState(state); + } } diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-actions.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-actions.hbs index c46e656f8..5b05d127a 100644 --- a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-actions.hbs +++ b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-actions.hbs @@ -1,3 +1,35 @@ +{{#if @model.isOIDC}} + + + + + {{#each @model.STATES_ENUM as |state|}} + + + + {{/each}} + + + + +{{/if}} + {{t 'resources.auth-method.actions.delete'}} - \ No newline at end of file + diff --git a/ui/admin/tests/acceptance/auth-methods/oidc-test.js b/ui/admin/tests/acceptance/auth-methods/oidc-test.js new file mode 100644 index 000000000..75b89edeb --- /dev/null +++ b/ui/admin/tests/acceptance/auth-methods/oidc-test.js @@ -0,0 +1,134 @@ +import { module, test } from 'qunit'; +import { + visit, + currentURL, + click, + // fillIn, + find, + // findAll, +} from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import { Response } from 'miragejs'; +import { + authenticateSession, + // These are left here intentionally for future reference. + //currentSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | auth methods | oidc', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + const instances = { + scopes: { + global: null, + org: null, + }, + authMethod: null, + }; + const urls = { + orgScope: null, + authMethods: null, + authMethod: null, + }; + + hooks.beforeEach(function () { + authenticateSession({}); + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.scopes.org = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + instances.authMethod = this.server.create('auth-method', { + scope: instances.scopes.org, + type: 'oidc', + }); + + urls.orgScope = `/scopes/${instances.scopes.org.id}`; + urls.authMethods = `${urls.orgScope}/auth-methods`; + urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`; + }); + + test('visiting oidc auth method', async function (assert) { + assert.expect(1); + await visit(urls.authMethod); + await a11yAudit(); + assert.equal(currentURL(), urls.authMethod); + }); + + test('can view oidc state', async function(assert) { + assert.expect(1); + await visit(urls.authMethod); + await click('.rose-layout-page-actions .rose-dropdown-trigger'); + assert.equal( + find('.rose-dropdown[open] input[aria-checked=true]').value, + instances.authMethod.attributes.state + ); + }); + + test('can update oidc state', async function(assert) { + assert.expect(1); + const updateValue = 'inactive'; + await visit(urls.authMethod); + await click('.rose-layout-page-actions .rose-dropdown-trigger'); + await click(`.rose-dropdown[open] input[value="${updateValue}"]`); + const authMethod = this.server.db.authMethods.find(instances.authMethod.id); + assert.equal(authMethod.attributes.state, updateValue); + }); + + test('can update oidc state to active-private', async function(assert) { + assert.expect(1); + const updateValue = 'active-private'; + await visit(urls.authMethod); + await click('.rose-layout-page-actions .rose-dropdown-trigger'); + await click(`.rose-dropdown[open] input[value="${updateValue}"]`); + const authMethod = this.server.db.authMethods.find(instances.authMethod.id); + assert.equal(authMethod.attributes.state, updateValue); + }); + + test('can update oidc state to active-public', async function(assert) { + assert.expect(1); + // Update default 'active-public' state to inactive + instances.authMethod.attributes.state = 'inactive'; + const updateValue = 'active-public'; + await visit(urls.authMethod); + await click('.rose-layout-page-actions .rose-dropdown-trigger'); + await click(`.rose-dropdown[open] input[value="${updateValue}"]`); + const authMethod = this.server.db.authMethods.find(instances.authMethod.id); + assert.equal(authMethod.attributes.state, updateValue); + }); + + test('errors are displayed when state update fails', async function (assert) { + assert.expect(2); + const newState = 'inactive'; + await visit(urls.authMethod); + await click('.rose-layout-page-actions .rose-dropdown-trigger'); + // Load page before mocking update + this.server.post('/auth-methods/:idMethod', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'error', + message: 'Sorry!', + } + ); + }); + await click(`.rose-dropdown[open] input[value="${newState}"]`); + const authMethod = this.server.db.authMethods.find(instances.authMethod.id); + assert.notEqual( + newState, + authMethod.attributes.state, + 'Auth method state is not be updated.' + ); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'Sorry!', + 'Displays error message.' + ); + }); +});