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:
22 files changed
+268
-30
No files matched your search
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
export { default, appName } from 'core/helpers/app-name';
|
||||
export { default } from 'core/helpers/app-name';
|
||||
@@ -1 +1 @@
|
||||
export { default, companyCopyright } from 'core/helpers/company-copyright';
|
||||
export { default } from 'core/helpers/company-copyright';
|
||||
@@ -1 +1 @@
|
||||
export { default, companyName } from 'core/helpers/company-name';
|
||||
export { default } from 'core/helpers/company-name';
|
||||
@@ -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 @@
|
||||
export { default, formatDateIso } from 'core/helpers/format-date-iso';
|
||||
export { default } from 'core/helpers/format-date-iso';
|
||||
@@ -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 +0,0 @@
|
||||
export { default } from 'rose/components/rose/tab';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.'
|
||||
// );
|
||||
// });
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user