Merge pull request #583 from hashicorp/ICU-1369-oidc-status-change

[draft] feat: Add oidc status change dropdown to auth method actions
This commit is contained in:
Randall Morey authored and GitHub committed 2021-06-04 11:48:16 -04:00
commit f02f488edd
22 files changed
+268 -30

No files matched your search

+22 -1
View File
@@ -4,7 +4,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: {
@@ -75,6 +75,27 @@ export default class AuthMethodModel extends GeneratedAuthMethodModel {
return this.type === 'oidc';
}
/**
* @type {boolean}
*/
get isInactive() {
return this.attributes.state === 'inactive';
}
/**
* @type {boolean}
*/
get isPrivate() {
return this.attributes.state === 'active-private';
}
/**
* @type {boolean}
*/
get isPublic() {
return this.attributes.state === 'active-public';
}
// =methods
/**
+2 -1
View File
@@ -106,8 +106,9 @@ export default function () {
}
);
// Authenticate/deauthenticate routes
// Authenticate + auth methods custom routes
this.post('/auth-methods/:id_method', authHandler);
// Deauthenticate route
this.post('/scopes/:id_method', deauthHandler);
// IAM : Users
+26 -6
View File
@@ -107,14 +107,34 @@ const commandHandlers = {
},
};
// Handles all custom methods on /auth-methods/:id route
export function authHandler({ scopes, authMethods }, request) {
const payload = JSON.parse(request.requestBody);
const [, id] = request.params.id_method.match(/(?<id>.[^:]*)/);
const { command } = payload;
const [, id, method] = request.params.id_method.match(
/(?<id>.[^:]*):(?<method>(.*))/
);
const authMethod = authMethods.find(id);
const scope = scopes.find(authMethod.scopeId);
const scopeAttrs = this.serialize(scopes.find(scope.id));
return commandHandlers[authMethod.type][command](payload, scopeAttrs);
if (method === 'authenticate') {
const payload = JSON.parse(request.requestBody);
const { command } = payload;
const scope = scopes.find(authMethod.scopeId);
const scopeAttrs = this.serialize(scopes.find(scope.id));
return commandHandlers[authMethod.type][command](payload, scopeAttrs);
}
// TODO: this handler doesn't really belong here, but we already route
// POST requests on existing auth methods to this route handler file under the
// assumption it would be for authentication. While authentication should
// still occur here, we should handle other custom methods in the
// mirage config and route here only for auth.
if (method === 'change-state') {
const attrs = this.normalizedRequestAttrs();
return authMethod.update({
attributes: {
state: attrs.attributes.state,
},
});
}
}
export function deauthHandler() {
+1 -1
View File
@@ -1 +1 @@
export { default, appName } from 'core/helpers/app-name';
export { default } from 'core/helpers/app-name';
+1 -1
View File
@@ -1 +1 @@
export { default, companyCopyright } from 'core/helpers/company-copyright';
export { default } from 'core/helpers/company-copyright';
+1 -1
View File
@@ -1 +1 @@
export { default, companyName } from 'core/helpers/company-name';
export { default } from 'core/helpers/company-name';
+1 -1
View File
@@ -1 +1 @@
export { default, docUrl } from 'core/helpers/doc-url';
export { default } from 'core/helpers/doc-url';
@@ -1,4 +1 @@
export {
default,
formatDateIsoHuman,
} from 'core/helpers/format-date-iso-human';
export { default } from 'core/helpers/format-date-iso-human';
+1 -1
View File
@@ -1 +1 @@
export { default, formatDateIso } from 'core/helpers/format-date-iso';
export { default } from 'core/helpers/format-date-iso';
+1 -1
View File
@@ -1 +1 @@
export { default, isLoading } from 'core/helpers/is-loading';
export { default } from 'core/helpers/is-loading';
@@ -31,3 +31,4 @@ set-password: Set Password
change-password: Change Password
edit-form: Edit Form
change-origin: Change Origin URL
change-state: Change State
@@ -8,3 +8,6 @@ connection:
success:
description: "Local proxy address ({protocol}): {address}:{port}"
primary: Primary
inactive: Inactive
active-private: Private
active-public: Public
@@ -1 +0,0 @@
export { default } from 'rose/components/rose/layout/tabs';
-1
View File
@@ -1 +0,0 @@
export { default } from 'rose/components/rose/tab';
+1 -1
View File
@@ -43,7 +43,7 @@
"@ember/test-helpers": "^2.1.4",
"@glimmer/component": "^1.0.3",
"@glimmer/tracking": "^1.0.3",
"@hashicorp/structure-icons": "^1.8.1",
"@hashicorp/structure-icons": "^1.9.0-0",
"@storybook/addon-actions": "^6.0.26",
"@storybook/addon-docs": "^6.0.26",
"@storybook/addon-knobs": "^6.0.26",
@@ -1,4 +1,5 @@
import Controller from '@ember/controller';
import { options } from 'api/models/auth-method';
export default class ScopesScopeAuthMethodsAuthMethodController extends Controller {
// =attributes
@@ -10,4 +11,12 @@ export default class ScopesScopeAuthMethodsAuthMethodController extends Controll
get breadCrumb() {
return this.model.displayName;
}
/**
* OIDC auth-method states
* @type {[string]}
*/
get oidcStates() {
return options.oidc.state;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Admin</title>
<meta name="description" content="">
<meta name="description" content="Simple and secure remote access with HashiCorp Boundary.">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for "head"}}
@@ -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
*/
@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);
}
}
+1 -1
View File
@@ -83,9 +83,9 @@
.rose-form-fieldset {
.rose-table {
.rose-form-label {
left: -9999px;
position: absolute;
top: -9999px;
left: -9999px;
}
}
}
@@ -1,3 +1,36 @@
{{#if @model.isOIDC}}
<Rose::Dropdown
@text={{t (concat 'states.' @model.attributes.state)}}
@icon={{if
@model.isInactive
'disabled'
(if @model.isPrivate 'lock-closed-outline' 'check-circle-outline')
}}
@dropdownRight={{true}}
as |dropdown|
>
<dropdown.section @title={{t 'actions.change-state'}}>
<Rose::Form as |form|>
<form.radioGroup
@name='theme'
@selectedValue={{@model.attributes.state}}
@changed={{route-action 'changeState'}}
as |radioGroup|
>
{{#each this.oidcStates as |state|}}
<dropdown.item>
<radioGroup.radio
@label={{t (concat 'states.' state)}}
@value={{state}}
/>
</dropdown.item>
{{/each}}
</form.radioGroup>
</Rose::Form>
</dropdown.section>
</Rose::Dropdown>
{{/if}}
<Rose::Dropdown
@text='{{t 'actions.manage'}}'
@dropdownRight={{true}}
@@ -0,0 +1,139 @@
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);
});
// FIXME: How to mock just one request routed to /auth-methods and let everything else passthrough?
// test('errors are displayed when state update fails', async function (assert) {
// assert.expect(2);
// this.server.post('/auth-methods/:idMethod', (_, request) => {
// // Only respond to state update with error
// if (request.params.idMethod.match(/(change-state)/i)) {
// return new Response(
// 400,
// {},
// {
// status: 400,
// code: 'error',
// message: 'Sorry!',
// }
// );
// }
// return request;
// });
// const newState = 'inactive';
// await visit(urls.authMethod);
// await click('.rose-layout-page-actions .rose-dropdown-trigger');
// await click(`.rose-dropdown[open] input[value="${newState}"]`);
// // debugger;
// 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.'
// );
// });
});
+7 -7
View File
@@ -2023,10 +2023,10 @@
resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-1.1.0.tgz#d6dbc7574774b238114582410e8fee0dc3532bdf"
integrity sha512-rR7tJoSwJ2eooOpYGxGGW95sLq6GXUaS1UtWvN7pei6n2/okYvCGld9vsUTvkl2migxbkszsycwtMf/GEc1k1A==
"@hashicorp/structure-icons@^1.8.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@hashicorp/structure-icons/-/structure-icons-1.8.1.tgz#d29945df2b41dcb317b141e51a26bd4be796a164"
integrity sha512-XFYdCIshmaR3Igc8eWpOZ2Gr3IR/0TogXZ4PQ9bz1E9cLzF3njBcs3tCpJUOwRwe/wMI5YTlL/sOGvcZ77AB/Q==
"@hashicorp/structure-icons@^1.9.0-0":
version "1.9.0-0"
resolved "https://registry.yarnpkg.com/@hashicorp/structure-icons/-/structure-icons-1.9.0-0.tgz#7bd2f4a61d2dd81247090d2cfb86f1f89d4ea22a"
integrity sha512-khvUS/zThfR6pLW4hGVRpcGY2Ta5azeS/Vzb9iCfr3A8OhSZFdnrlkqjYknpBg4RwrQDBpWTzZhe/Z35Ne4Wxg==
"@iarna/cli@^1.2.0":
version "1.2.0"
@@ -21816,9 +21816,9 @@ xmldom@^0.5.0:
integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==
xmlhttprequest-ssl@^1.6.2, xmlhttprequest-ssl@~1.5.4:
version "1.6.2"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz#dd6899bfbcf684b554e393c30b13b9f3b001a7ee"
integrity sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==
version "1.6.3"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.2"