mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
* updates oidc key list route to use api service * updates oidc key details route to use api service * updates oidc key edit and create routes to use api service * updates oidc key clients route to use api service * fixes oidc key tests * removes console in oidc key-form Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
parent
7d474e2d8c
commit
a70ede72cd
18 changed files with 341 additions and 267 deletions
|
|
@ -6,9 +6,7 @@
|
|||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-bottomless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} />
|
||||
{{#each @model.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
<FormFieldGroups @model={{@form}} @groupName="formFieldGroups" @modelValidations={{this.modelValidations}} />
|
||||
</div>
|
||||
{{#unless @isModalForm}}
|
||||
{{! RADIO CARD + SEARCH SELECT }}
|
||||
|
|
@ -32,26 +30,28 @@
|
|||
@value="limited"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disabled={{@model.isNew}}
|
||||
@disabled={{@form.isNew}}
|
||||
@tooltipMessage={{if
|
||||
@model.isNew
|
||||
@form.isNew
|
||||
"This option has been disabled for now. To limit access, you must first create an application that references this key."
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{#if (eq this.radioCardGroupValue "limited")}}
|
||||
{{! clients are fetched in route and passed into select component }}
|
||||
<SearchSelect
|
||||
@id="allowedClientIds"
|
||||
@id="oidc-key-form-client-select"
|
||||
@label="Application name"
|
||||
@subText="Select which applications are allowed to use this key. Only applications that currently reference this key will appear in the dropdown."
|
||||
@models={{array "oidc/client"}}
|
||||
@inputValue={{@model.allowedClientIds}}
|
||||
@options={{@clients}}
|
||||
@parentManageSelected={{this.selectedClients}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="string-list"
|
||||
@passObject={{true}}
|
||||
@objectKeys={{array "clientId"}}
|
||||
@queryObject={{this.filterDropdownOptions}}
|
||||
@objectKeys={{array "client_id"}}
|
||||
@shouldRenderName={{true}}
|
||||
@renderTooltip={{this.renderTooltip}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
<div class="field box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<Hds::Button
|
||||
@text={{if @model.isNew "Create" "Update"}}
|
||||
@text={{if @form.isNew "Create" "Update"}}
|
||||
@icon={{if this.save.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.save.isRunning}}
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
@color="secondary"
|
||||
class="has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
{{on "click" @onCancel}}
|
||||
data-test-oidc-key-cancel
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { action } from '@ember/object';
|
|||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
* @module OidcKeyForm
|
||||
|
|
@ -26,72 +27,87 @@ import { task } from 'ember-concurrency';
|
|||
*/
|
||||
|
||||
export default class OidcKeyForm extends Component {
|
||||
@service store;
|
||||
@service api;
|
||||
@service flashMessages;
|
||||
|
||||
@tracked errorBanner;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked modelValidations;
|
||||
@tracked radioCardGroupValue =
|
||||
// If "*" is provided, all clients are allowed: https://developer.hashicorp.com/vault/api-docs/secret/identity/oidc-provider#parameters
|
||||
!this.args.model.allowedClientIds || this.args.model.allowedClientIds.includes('*')
|
||||
? 'allow_all'
|
||||
: 'limited';
|
||||
@tracked radioCardGroupValue = 'limited';
|
||||
@tracked selectedClients = [];
|
||||
|
||||
get filterDropdownOptions() {
|
||||
// query object sent to search-select so only clients that reference this key appear in dropdown
|
||||
return { paramKey: 'key', filterFor: [this.args.model.name] };
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
// If "*" is provided, all clients are allowed: https://developer.hashicorp.com/vault/api-docs/secret/identity/oidc-provider#parameters
|
||||
const { allowed_client_ids } = this.args.form.data;
|
||||
if (!allowed_client_ids || allowed_client_ids.includes('*')) {
|
||||
this.radioCardGroupValue = 'allow_all';
|
||||
}
|
||||
// initialize selectedClients for SearchSelect component with allowed_client_ids from form data
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
// function passed to search select
|
||||
renderTooltip(selection, dropdownOptions) {
|
||||
// if a client has been deleted it will not exist in dropdownOptions (response from search select's query)
|
||||
const clientExists = !!dropdownOptions.find((opt) => opt.client_id === selection);
|
||||
return !clientExists ? 'The application associated with this client_id no longer exists' : false;
|
||||
}
|
||||
|
||||
updateSelectedClients() {
|
||||
const { data } = this.args.form;
|
||||
this.selectedClients = data.allowed_client_ids?.map((clientId) =>
|
||||
this.args.clients.find((client) => client.client_id === clientId)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
handleClientSelection(selection) {
|
||||
// if array then coming from search-select component, set selection as model clients
|
||||
const { data } = this.args.form;
|
||||
// when triggered from search-select component an array is passed
|
||||
// set selection as clients
|
||||
if (Array.isArray(selection)) {
|
||||
this.args.model.allowedClientIds = selection.map((client) => client.clientId);
|
||||
data.allowed_client_ids = selection.map((client) => client.client_id);
|
||||
} else {
|
||||
// otherwise update radio button value and reset clients so
|
||||
// UI always reflects a user's selection (including when no clients are selected)
|
||||
this.radioCardGroupValue = selection;
|
||||
this.args.model.allowedClientIds = [];
|
||||
data.allowed_client_ids = [];
|
||||
}
|
||||
// update selectedClients which appear in SearchSelect
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
|
||||
this.args.model[method]();
|
||||
this.args.onCancel();
|
||||
}
|
||||
save = task(
|
||||
waitFor(async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const { isNew } = this.args.form;
|
||||
const { isValid, state, invalidFormMessage, data } = this.args.form.toJSON();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = invalidFormMessage;
|
||||
|
||||
@task
|
||||
*save(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const { isValid, state, invalidFormMessage } = this.args.model.validate();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = invalidFormMessage;
|
||||
if (isValid) {
|
||||
const { isNew, name } = this.args.model;
|
||||
if (this.radioCardGroupValue === 'allow_all') {
|
||||
this.args.model.allowedClientIds = ['*'];
|
||||
if (isValid) {
|
||||
if (this.radioCardGroupValue === 'allow_all') {
|
||||
data.allowed_client_ids = ['*'];
|
||||
}
|
||||
// if TTL components are toggled off, set to default lease duration
|
||||
const { rotation_period, verification_ttl } = data;
|
||||
// value returned from API is a number, and string when from form action
|
||||
if (Number(rotation_period) === 0) data.rotation_period = '24h';
|
||||
if (Number(verification_ttl) === 0) data.verification_ttl = '24h';
|
||||
|
||||
const { name, ...payload } = data;
|
||||
await this.api.identity.oidcWriteKey(name, payload);
|
||||
this.flashMessages.success(`Successfully ${isNew ? 'created' : 'updated'} the key ${name}.`);
|
||||
// this form is sometimes used in a modal, passing the form notifies the parent the save was successful
|
||||
this.args.onSave(this.args.form);
|
||||
}
|
||||
// if TTL components are toggled off, set to default lease duration
|
||||
const { rotationPeriod, verificationTtl } = this.args.model;
|
||||
// value returned from API is a number, and string when from form action
|
||||
if (Number(rotationPeriod) === 0) this.args.model.rotationPeriod = '24h';
|
||||
if (Number(verificationTtl) === 0) this.args.model.verificationTtl = '24h';
|
||||
yield this.args.model.save();
|
||||
this.flashMessages.success(
|
||||
`Successfully ${isNew ? 'created' : 'updated'} the key
|
||||
${name}.`
|
||||
);
|
||||
// this form is sometimes used in a modal, passing the model notifies
|
||||
// the parent if the save was successful
|
||||
this.args.onSave(this.args.model);
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.errorBanner = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.errorBanner = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default class OidcProviderForm extends Component {
|
|||
@action
|
||||
handleClientSelection(selection) {
|
||||
const { data } = this.args.form;
|
||||
// when trigger from search-select component an array is passed
|
||||
// when triggered from search-select component an array is passed
|
||||
// set selection as clients
|
||||
if (Array.isArray(selection)) {
|
||||
data.allowed_client_ids = selection.map((client) => client.client_id);
|
||||
|
|
|
|||
|
|
@ -10,32 +10,31 @@ import { task } from 'ember-concurrency';
|
|||
import { waitFor } from '@ember/test-waiters';
|
||||
|
||||
export default class OidcKeyDetailsController extends Controller {
|
||||
@service store;
|
||||
@service api;
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*rotateKey() {
|
||||
const adapter = this.store.adapterFor('oidc/key');
|
||||
yield adapter
|
||||
.rotate(this.model.name, this.model.verificationTtl)
|
||||
.then(() => {
|
||||
this.flashMessages.success(`Success: ${this.model.name} connection was rotated.`);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.flashMessages.danger(e.errors);
|
||||
});
|
||||
}
|
||||
rotateKey = task(
|
||||
waitFor(async () => {
|
||||
try {
|
||||
const { name, verification_ttl } = this.model.key;
|
||||
await this.api.identity.oidcRotateKey(name, { verification_ttl });
|
||||
this.flashMessages.success(`Success: ${name} connection was rotated.`);
|
||||
} catch (e) {
|
||||
const { message } = await this.api.parseError(e);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@action
|
||||
async delete() {
|
||||
try {
|
||||
await this.model.destroyRecord();
|
||||
await this.api.identity.oidcDeleteKey(this.model.key.name);
|
||||
this.flashMessages.success('Key deleted successfully');
|
||||
this.router.transitionTo('vault.cluster.access.oidc.keys');
|
||||
} catch (error) {
|
||||
this.model.rollbackAttributes();
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
ui/app/forms/oidc/key.ts
Normal file
43
ui/app/forms/oidc/key.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
|
||||
import type { Validations } from 'vault/app-types';
|
||||
import type { OidcWriteKeyRequest } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
type OidcKeyFormData = OidcWriteKeyRequest & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default class OidcKeyForm extends Form<OidcKeyFormData> {
|
||||
formFieldGroups = [
|
||||
new FormFieldGroup('default', [
|
||||
new FormField('name', 'string', { editDisabled: true }),
|
||||
new FormField('algorithm', 'string', {
|
||||
possibleValues: ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA'],
|
||||
}),
|
||||
new FormField('rotation_period', undefined, {
|
||||
editType: 'ttl',
|
||||
}),
|
||||
new FormField('verification_ttl', undefined, {
|
||||
label: 'Verification TTL',
|
||||
editType: 'ttl',
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
||||
validations: Validations = {
|
||||
name: [
|
||||
{ type: 'presence', message: 'Name is required.' },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message: 'Name cannot contain whitespace.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -5,11 +5,17 @@
|
|||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import OidcKeyForm from 'vault/forms/oidc/key';
|
||||
|
||||
export default class OidcKeysCreateRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
|
||||
model() {
|
||||
return this.store.createRecord('oidc/key');
|
||||
const defaultValues = {
|
||||
algorithm: 'RS256',
|
||||
rotation_period: '24h',
|
||||
verification_ttl: '24h',
|
||||
};
|
||||
return new OidcKeyForm(defaultValues, { isNew: true });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,32 @@
|
|||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { IdentityApiOidcListKeysListEnum } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
export default class OidcKeysRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service capabilities;
|
||||
|
||||
model() {
|
||||
return this.store.query('oidc/key', {}).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
async model() {
|
||||
try {
|
||||
const { keys } = await this.api.identity.oidcListKeys(IdentityApiOidcListKeysListEnum.TRUE);
|
||||
const paths = keys.map((name) => this.capabilities.pathFor('oidcKey', { name }));
|
||||
const capabilities = paths ? await this.capabilities.fetch(paths) : {};
|
||||
|
||||
return {
|
||||
keys,
|
||||
capabilities,
|
||||
};
|
||||
} catch (error) {
|
||||
const { status } = await this.api.parseError(error);
|
||||
if (status === 404) {
|
||||
return {
|
||||
keys: [],
|
||||
capabilities: {},
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,20 @@ import Route from '@ember/routing/route';
|
|||
import { service } from '@ember/service';
|
||||
|
||||
export default class OidcKeyRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service capabilities;
|
||||
|
||||
model({ name }) {
|
||||
return this.store.findRecord('oidc/key', name);
|
||||
async model({ name }) {
|
||||
const { data } = await this.api.identity.oidcReadKey(name);
|
||||
const { pathFor } = this.capabilities;
|
||||
const paths = {
|
||||
key: pathFor('oidcKey', { name }),
|
||||
rotate: pathFor('oidcKeyRotate', { name }),
|
||||
};
|
||||
const capabilities = await this.capabilities.fetch(Object.values(paths));
|
||||
return {
|
||||
key: { ...data, name },
|
||||
capabilities: { ...capabilities[paths.key], canRotate: capabilities[paths.rotate].canUpdate },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ export default class OidcKeyClientsRoute extends Route {
|
|||
@service capabilities;
|
||||
|
||||
async model() {
|
||||
const { allowedClientIds } = this.modelFor('vault.cluster.access.oidc.keys.key');
|
||||
const { key } = this.modelFor('vault.cluster.access.oidc.keys.key');
|
||||
const response = await this.api.identity.oidcListClients(IdentityApiOidcListClientsListEnum.TRUE);
|
||||
const clients = this.api.keyInfoToArray(response, 'name');
|
||||
// filter clients based on allowed_client_ids of provider
|
||||
const filteredClients = allowedClientIds.includes('*')
|
||||
const filteredClients = key.allowed_client_ids.includes('*')
|
||||
? clients
|
||||
: clients.filter((client) => allowedClientIds.includes(client.client_id));
|
||||
: clients.filter((client) => key.allowed_client_ids.includes(client.client_id));
|
||||
// fetch capabilities for filtered clients
|
||||
const paths = filteredClients.map(({ name }) => this.capabilities.pathFor('oidcClient', { name }));
|
||||
const capabilities = paths ? await this.capabilities.fetch(paths) : {};
|
||||
|
|
|
|||
|
|
@ -4,5 +4,24 @@
|
|||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { IdentityApiOidcListClientsListEnum } from '@hashicorp/vault-client-typescript';
|
||||
import OidcKeyForm from 'vault/forms/oidc/key';
|
||||
|
||||
export default class OidcKeyEditRoute extends Route {}
|
||||
export default class OidcKeyEditRoute extends Route {
|
||||
@service api;
|
||||
|
||||
async model() {
|
||||
const { key } = this.modelFor('vault.cluster.access.oidc.keys.key');
|
||||
// fetch clients to populate dropdown in form
|
||||
const response = await this.api.identity.oidcListClients(IdentityApiOidcListClientsListEnum.TRUE);
|
||||
const clients = this.api.keyInfoToArray(response, 'name');
|
||||
// filter clients that are associated with this key
|
||||
const filteredClients = clients.filter((client) => client.key === key.name);
|
||||
|
||||
return {
|
||||
clients: filteredClients,
|
||||
form: new OidcKeyForm(key),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
</Page::Header>
|
||||
|
||||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@form={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.keys"}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.keys.key.details" this.model.name}}
|
||||
/>
|
||||
|
|
@ -11,18 +11,18 @@
|
|||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#each this.model as |model|}}
|
||||
{{#each this.model.keys as |key|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row"
|
||||
@params={{array "vault.cluster.access.oidc.keys.key.details" model.name}}
|
||||
data-test-oidc-key-linked-block={{model.name}}
|
||||
@params={{array "vault.cluster.access.oidc.keys.key.details" key}}
|
||||
data-test-oidc-key-linked-block={{key}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<Icon @name="key" class="has-text-grey-light" />
|
||||
<span class="has-text-weight-semibold is-underline" data-test-item>
|
||||
{{model.name}}
|
||||
{{key}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -37,16 +37,20 @@
|
|||
/>
|
||||
<dd.Interactive
|
||||
@route="vault.cluster.access.oidc.keys.key.details"
|
||||
@model={{model.name}}
|
||||
@disabled={{eq model.canRead false}}
|
||||
@model={{key}}
|
||||
@disabled={{not (has-capability @model.capabilities "read" pathKey="oidcKey" params=key)}}
|
||||
data-test-oidc-key-menu-link="details"
|
||||
>Details</dd.Interactive>
|
||||
>
|
||||
Details
|
||||
</dd.Interactive>
|
||||
<dd.Interactive
|
||||
@route="vault.cluster.access.oidc.keys.key.edit"
|
||||
@model={{model.name}}
|
||||
@disabled={{eq model.canEdit false}}
|
||||
@model={{key}}
|
||||
@disabled={{not (has-capability @model.capabilities "update" pathKey="oidcKey" params=key)}}
|
||||
data-test-oidc-key-menu-link="edit"
|
||||
>Edit</dd.Interactive>
|
||||
>
|
||||
Edit
|
||||
</dd.Interactive>
|
||||
</Hds::Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
}}
|
||||
|
||||
{{#if (not-eq this.router.currentRoute.localName "edit")}}
|
||||
<Page::Header @title={{this.model.name}}>
|
||||
<Page::Header @title={{this.model.key.name}}>
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array
|
||||
(hash label="Vault" route="vault.cluster.dashboard" icon="vault")
|
||||
(hash label="OIDC provider: Keys" route="vault.cluster.access.oidc.keys")
|
||||
(hash label=this.model.name)
|
||||
(hash label=this.model.key.name)
|
||||
}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
|
|
@ -19,10 +19,18 @@
|
|||
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
|
||||
<nav class="tabs" aria-label="tabs">
|
||||
<ul>
|
||||
<LinkTo data-test-oidc-key-details @route="vault.cluster.access.oidc.keys.key.details" @model={{this.model}}>
|
||||
<LinkTo
|
||||
data-test-oidc-key-details
|
||||
@route="vault.cluster.access.oidc.keys.key.details"
|
||||
@model={{this.model.key.name}}
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
<LinkTo data-test-oidc-key-clients @route="vault.cluster.access.oidc.keys.key.clients" @model={{this.model}}>
|
||||
<LinkTo
|
||||
data-test-oidc-key-clients
|
||||
@route="vault.cluster.access.oidc.keys.key.clients"
|
||||
@model={{this.model.key.name}}
|
||||
>
|
||||
Applications
|
||||
</LinkTo>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -2,45 +2,46 @@
|
|||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
{{#let this.model.key this.model.capabilities as |key capabilities|}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if (and (not-eq key.name "default") capabilities.canDelete)}}
|
||||
<ConfirmAction
|
||||
@buttonText="Delete key"
|
||||
data-test-oidc-key-delete
|
||||
class="toolbar-button"
|
||||
@buttonColor="secondary"
|
||||
@onConfirmAction={{this.delete}}
|
||||
@confirmTitle="Delete key?"
|
||||
@confirmMessage="This key will be permanently deleted. You will not be able to recover it."
|
||||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if capabilities.canRotate}}
|
||||
<ConfirmAction
|
||||
@buttonText="Rotate key"
|
||||
data-test-oidc-key-rotate
|
||||
class="toolbar-button"
|
||||
@buttonColor="secondary"
|
||||
@onConfirmAction={{perform this.rotateKey}}
|
||||
@confirmTitle="Rotate this key?"
|
||||
@confirmMessage="After rotation, a new public/private key pair will be generated."
|
||||
@modalColor="warning"
|
||||
@isRunning={{this.rotateKey.isRunning}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if capabilities.canUpdate}}
|
||||
<ToolbarLink @route="vault.cluster.access.oidc.keys.key.edit" @model={{key.name}} data-test-oidc-key-edit>
|
||||
Edit key
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if (and (not-eq this.model.name "default") this.model.canDelete)}}
|
||||
<ConfirmAction
|
||||
@buttonText="Delete key"
|
||||
data-test-oidc-key-delete
|
||||
class="toolbar-button"
|
||||
@buttonColor="secondary"
|
||||
@onConfirmAction={{this.delete}}
|
||||
@confirmTitle="Delete key?"
|
||||
@confirmMessage="This key will be permanently deleted. You will not be able to recover it."
|
||||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if this.model.canRotate}}
|
||||
<ConfirmAction
|
||||
@buttonText="Rotate key"
|
||||
data-test-oidc-key-rotate
|
||||
class="toolbar-button"
|
||||
@buttonColor="secondary"
|
||||
@onConfirmAction={{perform this.rotateKey}}
|
||||
@confirmTitle="Rotate this key?"
|
||||
@confirmMessage="After rotation, a new public/private key pair will be generated."
|
||||
@modalColor="warning"
|
||||
@isRunning={{this.rotateKey.isRunning}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.model.canEdit}}
|
||||
<ToolbarLink @route="vault.cluster.access.oidc.keys.key.edit" @model={{this.model.name}} data-test-oidc-key-edit>
|
||||
Edit key
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<InfoTableRow @label="Name" @value={{this.model.name}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Algorithm" @value={{this.model.algorithm}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Rotation period" @value={{format-duration this.model.rotationPeriod}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Verification TTL" @value={{format-duration this.model.verificationTtl}} @alwaysRender={{true}} />
|
||||
</div>
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<InfoTableRow @label="Name" @value={{key.name}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Algorithm" @value={{key.algorithm}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Rotation period" @value={{format-duration key.rotation_period}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Verification TTL" @value={{format-duration key.verification_ttl}} @alwaysRender={{true}} />
|
||||
</div>
|
||||
{{/let}}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
@breadcrumbs={{array
|
||||
(hash label="Vault" route="vault.cluster.dashboard" icon="vault")
|
||||
(hash label="OIDC provider: Keys" route="vault.cluster.access.oidc.keys")
|
||||
(hash label="Details" route="vault.cluster.access.oidc.keys.key.details" model=this.model.name)
|
||||
(hash label="Details" route="vault.cluster.access.oidc.keys.key.details" model=this.model.form.data.name)
|
||||
(hash label="Edit key")
|
||||
}}
|
||||
/>
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
</Page::Header>
|
||||
|
||||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.keys.key.details" this.model.name}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.keys.key.details" this.model.name}}
|
||||
@form={{this.model.form}}
|
||||
@clients={{this.model.clients}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.keys.key.details" this.model.form.data.name}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.keys.key.details" this.model.form.data.name}}
|
||||
/>
|
||||
|
|
@ -39,6 +39,8 @@ export const PATH_MAP = {
|
|||
ldapStaticRole: apiPath`${'backend'}/static-role/${'name'}`,
|
||||
ldapStaticRoleCreds: apiPath`${'backend'}/static-cred/${'name'}`,
|
||||
oidcClient: apiPath`identity/oidc/client/${'name'}`,
|
||||
oidcKey: apiPath`identity/oidc/key/${'name'}`,
|
||||
oidcKeyRotate: apiPath`identity/oidc/key/${'name'}/rotate`,
|
||||
oidcProvider: apiPath`identity/oidc/provider/${'name'}`,
|
||||
oidcScope: apiPath`identity/oidc/scope/${'name'}`,
|
||||
pkiCertificates: apiPath`${'backend'}/certificates`,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
|||
await Promise.allSettled([
|
||||
this.api.identity.oidcDeleteClient('client-with-test-key'),
|
||||
this.api.identity.oidcDeleteClient('client-with-default-key'),
|
||||
clearRecord(this.store, 'oidc/key', 'test-key'),
|
||||
this.api.identity.oidcDeleteKey('test-key'),
|
||||
]);
|
||||
|
||||
// create client with default key
|
||||
|
|
@ -186,7 +186,7 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
|||
await Promise.allSettled([
|
||||
this.api.identity.oidcDeleteClient('client-with-test-key'),
|
||||
this.api.identity.oidcDeleteClient('client-with-default-key'),
|
||||
clearRecord(this.store, 'oidc/key', 'test-key'),
|
||||
this.api.identity.oidcDeleteKey('test-key'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -204,14 +204,14 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
|||
});
|
||||
|
||||
//* clear out test state
|
||||
await clearRecord(this.store, 'oidc/key', 'test-key');
|
||||
await this.api.identity.oidcDeleteKey('test-key');
|
||||
|
||||
// create a new key
|
||||
await visit(OIDC_BASE_URL + '/keys/create');
|
||||
await fillIn('[data-test-input="name"]', 'test-key');
|
||||
// toggle ttls to false, testing it sets correct default duration
|
||||
await click('[data-test-input="rotationPeriod"]');
|
||||
await click('[data-test-input="verificationTtl"]');
|
||||
await click('[data-test-input="rotation_period"]');
|
||||
await click('[data-test-input="verification_ttl"]');
|
||||
assert
|
||||
.dom('[data-test-oidc-radio="limited"] input')
|
||||
.isDisabled('limiting access radio button is disabled on create');
|
||||
|
|
@ -311,7 +311,7 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
|||
|
||||
test('it hides delete and edit key when no permission', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.server.get('/identity/oidc/keys', () => overrideResponse(null, { data: { keys: ['test-key'] } }));
|
||||
this.server.get('/identity/oidc/key', () => overrideResponse(null, { data: { keys: ['test-key'] } }));
|
||||
this.server.get('/identity/oidc/key/test-key', () =>
|
||||
overrideResponse(null, {
|
||||
data: {
|
||||
|
|
@ -323,7 +323,7 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
|||
})
|
||||
);
|
||||
this.server.post('/sys/capabilities-self', () =>
|
||||
capabilitiesStub('/identity/oidc/key/test-key', ['read'])
|
||||
capabilitiesStub('identity/oidc/key/test-key', ['read'])
|
||||
);
|
||||
|
||||
await visit(OIDC_BASE_URL + '/keys');
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ import { render, fillIn, click, findAll } from '@ember/test-helpers';
|
|||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import oidcConfigHandlers from 'vault/mirage/handlers/oidc-config';
|
||||
import { OIDC_BASE_URL, CLIENT_LIST_RESPONSE, SELECTORS } from 'vault/tests/helpers/oidc-config';
|
||||
import { CLIENT_LIST_RESPONSE, SELECTORS } from 'vault/tests/helpers/oidc-config';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { capabilitiesStub, overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import OidcKeyForm from 'vault/forms/oidc/key';
|
||||
import sinon from 'sinon';
|
||||
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
|
||||
module('Integration | Component | oidc/key-form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
|
@ -20,8 +23,33 @@ module('Integration | Component | oidc/key-form', function (hooks) {
|
|||
|
||||
hooks.beforeEach(function () {
|
||||
oidcConfigHandlers(this.server);
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.server.get('/identity/oidc/client', () => overrideResponse(null, { data: CLIENT_LIST_RESPONSE }));
|
||||
|
||||
const api = this.owner.lookup('service:api');
|
||||
sinon.stub(api.identity, 'oidcReadClient').resolves({ data: CLIENT_LIST_RESPONSE });
|
||||
this.writeStub = sinon.stub(api.identity, 'oidcWriteKey').resolves();
|
||||
|
||||
this.onSave = sinon.spy();
|
||||
this.onCancel = sinon.spy();
|
||||
this.clients = [{ name: 'app-1' }];
|
||||
|
||||
this.renderComponent = (client = {}) => {
|
||||
const defaultValues = {
|
||||
algorithm: 'RS256',
|
||||
rotation_period: '24h',
|
||||
verification_ttl: '24h',
|
||||
};
|
||||
const data = { ...defaultValues, ...client };
|
||||
this.form = new OidcKeyForm(data, { isNew: !Object.keys(client).length });
|
||||
return render(hbs`
|
||||
<Oidc::KeyForm
|
||||
@form={{this.form}}
|
||||
@clients={{this.clients}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
};
|
||||
|
||||
setRunOptions({
|
||||
rules: {
|
||||
// TODO: fix RadioCard component (replace with HDS)
|
||||
|
|
@ -36,20 +64,8 @@ module('Integration | Component | oidc/key-form', function (hooks) {
|
|||
|
||||
test('it should save new key', async function (assert) {
|
||||
assert.expect(8);
|
||||
this.server.post('/identity/oidc/key/test-key', (schema, req) => {
|
||||
assert.ok(true, 'Request made to save key');
|
||||
return JSON.parse(req.requestBody);
|
||||
});
|
||||
this.model = this.store.createRecord('oidc/key');
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
|
||||
await render(hbs`
|
||||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(SELECTORS.keySaveButton).hasText('Create', 'Save button has correct text');
|
||||
assert.dom('[data-test-input="algorithm"]').hasValue('RS256', 'default algorithm is correct');
|
||||
assert.strictEqual(findAll('[data-test-field]').length, 4, 'renders all input fields');
|
||||
|
|
@ -67,34 +83,16 @@ module('Integration | Component | oidc/key-form', function (hooks) {
|
|||
|
||||
assert.dom('[data-test-oidc-radio="limited"] input').isDisabled('limit radio button disabled on create');
|
||||
await fillIn('[data-test-input="name"]', 'test-key');
|
||||
|
||||
await click(SELECTORS.keySaveButton);
|
||||
assert.true(this.onSave.calledOnce, 'onSave callback fires on save success');
|
||||
assert.true(this.writeStub.calledWith('test-key'), 'API called to save key with correct parameters');
|
||||
});
|
||||
|
||||
test('it should update key and limit access to selected applications', async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
this.server.post('/identity/oidc/key/test-key', (schema, req) => {
|
||||
assert.ok(true, 'Request made to update key');
|
||||
return JSON.parse(req.requestBody);
|
||||
});
|
||||
|
||||
this.store.pushPayload('oidc/key', {
|
||||
modelName: 'oidc/key',
|
||||
name: 'test-key',
|
||||
allowed_client_ids: ['*'],
|
||||
});
|
||||
|
||||
this.model = this.store.peekRecord('oidc/key', 'test-key');
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on update success');
|
||||
|
||||
await render(hbs`
|
||||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await this.renderComponent({ name: 'test-key', allowed_client_ids: ['*'] });
|
||||
assert.dom(SELECTORS.keySaveButton).hasText('Update', 'Save button has correct text');
|
||||
assert.dom('[data-test-input="name"]').isDisabled('Name input is disabled when editing');
|
||||
assert.dom('[data-test-input="name"]').hasValue('test-key', 'Name input is populated with model value');
|
||||
|
|
@ -102,9 +100,9 @@ module('Integration | Component | oidc/key-form', function (hooks) {
|
|||
|
||||
await click('[data-test-oidc-radio="limited"]');
|
||||
assert
|
||||
.dom('[data-test-component="search-select"]#allowedClientIds')
|
||||
.dom('[data-test-component="search-select"]#oidc-key-form-client-select')
|
||||
.exists('Limited radio button shows clients search select');
|
||||
await click('[data-test-component="search-select"]#allowedClientIds .ember-basic-dropdown-trigger');
|
||||
await clickTrigger();
|
||||
assert.strictEqual(findAll('li.ember-power-select-option').length, 1, 'dropdown only renders one option');
|
||||
assert
|
||||
.dom('li.ember-power-select-option')
|
||||
|
|
@ -113,91 +111,42 @@ module('Integration | Component | oidc/key-form', function (hooks) {
|
|||
|
||||
await click('[data-test-oidc-radio="allow-all"]');
|
||||
assert
|
||||
.dom('[data-test-component="search-select"]#allowedClientIds')
|
||||
.dom('[data-test-component="search-select"]#oidc-key-form-client-select')
|
||||
.doesNotExist('Allow all radio button hides search select');
|
||||
|
||||
await click(SELECTORS.keySaveButton);
|
||||
assert.true(this.onSave.calledOnce, 'onSave callback fires on save success');
|
||||
assert.true(this.writeStub.calledWith('test-key'), 'API called to save key with correct parameters');
|
||||
});
|
||||
|
||||
test('it should rollback attributes or unload record on cancel', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.model = this.store.createRecord('oidc/key');
|
||||
this.onCancel = () => assert.ok(true, 'onCancel callback fires');
|
||||
|
||||
await render(hbs`
|
||||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
test('it should fire callback on cancel', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await this.renderComponent({ name: 'test-key' });
|
||||
await click(SELECTORS.keyCancelButton);
|
||||
assert.true(this.model.isDestroyed, 'New model is unloaded on cancel');
|
||||
|
||||
this.store.pushPayload('oidc/key', {
|
||||
modelName: 'oidc/key',
|
||||
name: 'test-key',
|
||||
allowed_client_ids: ['*'],
|
||||
});
|
||||
|
||||
this.model = this.store.peekRecord('oidc/key', 'test-key');
|
||||
|
||||
await render(hbs`
|
||||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await click('[data-test-oidc-radio="limited"]');
|
||||
await click(SELECTORS.keyCancelButton);
|
||||
assert.strictEqual(this.model.allowed_client_ids, undefined, 'Model attributes rolled back on cancel');
|
||||
assert.true(this.onCancel.calledOnce, 'onCancel callback fires on cancel');
|
||||
});
|
||||
|
||||
test('it should render fallback for search select', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/identity/oidc/key/test-key', (schema, req) => {
|
||||
assert.ok(true, 'Request made to update key');
|
||||
return JSON.parse(req.requestBody);
|
||||
});
|
||||
|
||||
this.store.pushPayload('oidc/key', {
|
||||
modelName: 'oidc/key',
|
||||
name: 'test-key',
|
||||
allowed_client_ids: ['*'],
|
||||
});
|
||||
|
||||
this.model = this.store.peekRecord('oidc/key', 'test-key');
|
||||
|
||||
this.server.get('/identity/oidc/client', () => overrideResponse(403));
|
||||
await render(hbs`
|
||||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
this.clients = [];
|
||||
await this.renderComponent({ name: 'test-key', allowed_client_ids: ['*'] });
|
||||
|
||||
await click('[data-test-oidc-radio="limited"]');
|
||||
assert
|
||||
.dom('[data-test-component="search-select"]#allowedClientIds [data-test-component="string-list"]')
|
||||
.dom(
|
||||
'[data-test-component="search-select"]#oidc-key-form-client-select [data-test-component="string-list"]'
|
||||
)
|
||||
.exists('Radio toggle shows client string-list input');
|
||||
});
|
||||
|
||||
test('it should render error alerts when API returns an error', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.model = this.store.createRecord('oidc/key');
|
||||
this.server.post('/sys/capabilities-self', () => capabilitiesStub(OIDC_BASE_URL + '/keys'));
|
||||
await render(hbs`
|
||||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
|
||||
this.writeStub.rejects(getErrorResponse());
|
||||
|
||||
await this.renderComponent();
|
||||
await fillIn('[data-test-input="name"]', 'test-app');
|
||||
await click(SELECTORS.keySaveButton);
|
||||
assert
|
||||
|
|
|
|||
Loading…
Reference in a new issue