mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 04:16:31 +02:00
* cleanup from clients migrations * updates oidc provider list views to use api client * updates oidc provider details view to use api service * adds oidc provider form class * updates oidc provider create and edit routes to use api service and form * updates oidc provider-form component to support form class * updates oidc acceptance tests * updates oidc provider delete to use api service * test fixes * updates search-select fallback to check if fallback component is defined Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
parent
392a72652b
commit
18ef01267b
@ -3,48 +3,28 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Header @title="{{if @model.isNew 'Create' 'Edit'}} Provider">
|
||||
<Page::Header @title="{{if @form.isNew 'Create' 'Edit'}} Provider">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<form {{on "submit" (perform this.save)}} {{did-insert this.setIssuer @model}}>
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-bottomless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} />
|
||||
{{! name field }}
|
||||
<FormField
|
||||
data-test-field={{true}}
|
||||
@attr={{get @model.formFields "0"}}
|
||||
@model={{@model}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
/>
|
||||
{{#let (get @model.formFields "1") as |attr|}}
|
||||
<FormFieldLabel
|
||||
for={{attr.name}}
|
||||
@label="Issuer"
|
||||
@helpText={{attr.options.helpText}}
|
||||
@subText={{attr.options.subText}}
|
||||
@docLink={{attr.options.docLink}}
|
||||
<FormFieldGroups @model={{@form}} @groupName="formFieldGroups" @modelValidations={{this.modelValidations}}>
|
||||
{{! scopes are fetched in route and passed into select component }}
|
||||
<SearchSelect
|
||||
@id="oidc-provider-form-scope-select"
|
||||
@options={{@scopes}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="string-list"
|
||||
@onChange={{fn (mut @form.data.scopes_supported)}}
|
||||
@inputValue={{if @form.data.scopes_supported (array @form.data.scopes_supported)}}
|
||||
class="is-marginless"
|
||||
/>
|
||||
<Input
|
||||
data-test-field={{true}}
|
||||
data-test-input={{attr.name}}
|
||||
id={{attr.name}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
@value={{@model.issuer}}
|
||||
class="input {{if this.validationError 'has-error-border'}}"
|
||||
placeholder={{attr.options.placeholderText}}
|
||||
/>
|
||||
{{/let}}
|
||||
{{! scopesSupported field }}
|
||||
<FormField
|
||||
data-test-field={{true}}
|
||||
@attr={{get @model.formFields "2"}}
|
||||
@model={{@model}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
/>
|
||||
</FormFieldGroups>
|
||||
</div>
|
||||
{{! RADIO CARD + SEARCH SELECT }}
|
||||
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-xxl">
|
||||
@ -70,16 +50,18 @@
|
||||
/>
|
||||
</div>
|
||||
{{#if (eq this.radioCardGroupValue "limited")}}
|
||||
{{! clients are fetched in route and passed into select component }}
|
||||
<SearchSelect
|
||||
@id="allowedClientIds"
|
||||
@id="oidc-provider-form-client-select"
|
||||
@label="Application name"
|
||||
@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"}}
|
||||
@objectKeys={{array "client_id"}}
|
||||
@shouldRenderName={{true}}
|
||||
@renderTooltip={{this.renderTooltip}}
|
||||
/>
|
||||
{{/if}}
|
||||
@ -87,7 +69,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}}
|
||||
@ -98,7 +80,7 @@
|
||||
@color="secondary"
|
||||
class="has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
{{on "click" @onCancel}}
|
||||
data-test-oidc-provider-cancel
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -8,33 +8,45 @@ import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import parseURL from 'core/utils/parse-url';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
* @module OidcProviderForm
|
||||
* OidcProviderForm components are used to create and update OIDC providers
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <OidcProviderForm @model={{this.model}} />
|
||||
* <OidcProviderForm @form={{this.model.form}} @scopes={{this.model.scopes}} />
|
||||
* ```
|
||||
* @callback onCancel
|
||||
* @callback onSave
|
||||
* @param {Object} model - oidc client model
|
||||
* @param {Object} form - oidc provider form
|
||||
* @param {Array} scopes - array of scopes to populate dropdown in form
|
||||
* @param {Array} clients - array of clients to populate dropdown in form
|
||||
* @param {onCancel} onCancel - callback triggered when cancel button is clicked
|
||||
* @param {onSave} onSave - callback triggered on save success
|
||||
*/
|
||||
|
||||
export default class OidcProviderForm extends Component {
|
||||
@service store;
|
||||
@service api;
|
||||
@service flashMessages;
|
||||
|
||||
@tracked modelValidations;
|
||||
@tracked errorBanner;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked radioCardGroupValue =
|
||||
@tracked radioCardGroupValue = 'limited';
|
||||
@tracked selectedClients = [];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
// 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';
|
||||
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();
|
||||
}
|
||||
|
||||
get breadcrumbs() {
|
||||
const crumbs = [
|
||||
@ -42,74 +54,73 @@ export default class OidcProviderForm extends Component {
|
||||
{ label: 'OIDC provider: Providers', route: 'vault.cluster.access.oidc.providers' },
|
||||
];
|
||||
|
||||
if (!this.args.model.isNew) {
|
||||
if (!this.args.form.isNew) {
|
||||
crumbs.push({
|
||||
label: this.args.model.name,
|
||||
label: this.args.form.data.name,
|
||||
route: 'vault.cluster.access.oidc.providers.provider.details',
|
||||
model: this.args.model.name,
|
||||
model: this.args.form.data.name,
|
||||
});
|
||||
}
|
||||
|
||||
crumbs.push({ label: this.args.model.isNew ? 'Create provider' : 'Edit provider' });
|
||||
crumbs.push({ label: this.args.form.isNew ? 'Create provider' : 'Edit provider' });
|
||||
return crumbs;
|
||||
}
|
||||
|
||||
// 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.clientId === selection);
|
||||
const clientExists = !!dropdownOptions.find((opt) => opt.client_id === selection);
|
||||
return !clientExists ? 'The application associated with this client_id no longer exists' : false;
|
||||
}
|
||||
|
||||
// fired on did-insert from render modifier
|
||||
@action
|
||||
setIssuer(elem, [model]) {
|
||||
model.issuer = model.isNew ? '' : parseURL(model.issuer).origin;
|
||||
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 trigger 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 { 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 = ['*'];
|
||||
}
|
||||
const { name, ...payload } = data;
|
||||
await this.api.identity.oidcWriteProvider(name, payload);
|
||||
this.flashMessages.success(
|
||||
`Successfully ${this.args.form.isNew ? 'created' : 'updated'} the OIDC provider ${name}.`
|
||||
);
|
||||
this.args.onSave();
|
||||
}
|
||||
yield this.args.model.save();
|
||||
this.flashMessages.success(
|
||||
`Successfully ${isNew ? 'created' : 'updated'} the OIDC provider
|
||||
${name}.`
|
||||
);
|
||||
this.args.onSave();
|
||||
} 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.';
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#each @model as |provider|}}
|
||||
{{#each @model.providers as |provider|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row"
|
||||
@params={{array "vault.cluster.access.oidc.providers.provider.details" provider.name}}
|
||||
@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
{{#if (or provider.canRead provider.canEdit)}}
|
||||
{{#if (has-capability @model.capabilities "read" "update" pathKey="oidcProvider" params=provider)}}
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@ -32,19 +32,23 @@
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
{{#if provider.canRead}}
|
||||
{{#if (has-capability @model.capabilities "read" pathKey="oidcProvider" params=provider)}}
|
||||
<dd.Interactive
|
||||
@route="vault.cluster.access.oidc.providers.provider.details"
|
||||
@model={{provider.name}}
|
||||
data-test-oidc-provider-menu-link="details"
|
||||
>Details</dd.Interactive>
|
||||
>
|
||||
Details
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if provider.canEdit}}
|
||||
{{#if (has-capability @model.capabilities "update" pathKey="oidcProvider" params=provider)}}
|
||||
<dd.Interactive
|
||||
@route="vault.cluster.access.oidc.providers.provider.edit"
|
||||
@model={{provider.name}}
|
||||
data-test-oidc-provider-menu-link="edit"
|
||||
>Edit</dd.Interactive>
|
||||
>
|
||||
Edit
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/if}}
|
||||
|
||||
@ -8,18 +8,18 @@ import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class OidcProviderDetailsController extends Controller {
|
||||
@service api;
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
|
||||
@action
|
||||
async delete() {
|
||||
try {
|
||||
await this.model.destroyRecord();
|
||||
await this.api.identity.oidcDeleteProvider(this.model.provider.name);
|
||||
this.flashMessages.success('Provider deleted successfully');
|
||||
this.router.transitionTo('vault.cluster.access.oidc.providers');
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
47
ui/app/forms/oidc/provider.ts
Normal file
47
ui/app/forms/oidc/provider.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 { OidcWriteProviderRequest } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
type OidcProviderFormData = OidcWriteProviderRequest & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default class OidcProviderForm extends Form<OidcProviderFormData> {
|
||||
formFieldGroups = [
|
||||
new FormFieldGroup('default', [
|
||||
new FormField('name', 'string', { editDisabled: true }),
|
||||
new FormField('issuer', 'string', {
|
||||
subText:
|
||||
"The scheme, host, and optional port for your issuer. This will be used to build the URL that validates ID tokens. Defaults to a URL with Vault's api_addr.",
|
||||
placeholder: 'e.g. https://example.com:8200',
|
||||
docLink: '/vault/api-docs/secret/identity/oidc-provider#create-or-update-a-provider',
|
||||
}),
|
||||
// SearchSelect within the FormField component works in conjunction with Ember Data Models
|
||||
// we can still use the component since it supports passing in an array of objects as options for the select
|
||||
// yield out the field so scopes can be fetched in the route and passed directly to SearchSelect
|
||||
new FormField('supported_scopes', undefined, {
|
||||
label: 'Supported scopes',
|
||||
subText: 'Scopes define information about a user and the OIDC service. Optional.',
|
||||
editType: 'yield',
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
||||
validations: Validations = {
|
||||
name: [
|
||||
{ type: 'presence', message: 'Name is required.' },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message: 'Name cannot contain whitespace.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@ -5,22 +5,36 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { IdentityApiOidcListProvidersListEnum } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
export default class OidcClientProvidersRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service capabilities;
|
||||
|
||||
model() {
|
||||
const { client } = this.modelFor('vault.cluster.access.oidc.clients.client');
|
||||
return this.store
|
||||
.query('oidc/provider', {
|
||||
allowed_client_id: client.client_id,
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
async model() {
|
||||
try {
|
||||
const { client } = this.modelFor('vault.cluster.access.oidc.clients.client');
|
||||
const response = await this.api.identity.oidcListProviders(
|
||||
IdentityApiOidcListProvidersListEnum.TRUE,
|
||||
client.client_id // use allowed_client_id query param to filter providers for this client
|
||||
);
|
||||
const paths = response.keys.map((name) => this.capabilities.pathFor('oidcProvider', { name }));
|
||||
const capabilities = paths ? await this.capabilities.fetch(paths) : {};
|
||||
|
||||
return {
|
||||
providers: this.api.keyInfoToArray(response, 'name'),
|
||||
capabilities,
|
||||
};
|
||||
} catch (error) {
|
||||
const { status } = await this.api.parseError(error);
|
||||
if (status === 404) {
|
||||
return {
|
||||
providers: [],
|
||||
capabilities: {},
|
||||
};
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,10 @@ export default class OidcClientsRoute extends Route {
|
||||
} catch (error) {
|
||||
const { status } = await this.api.parseError(error);
|
||||
if (status === 404) {
|
||||
return [];
|
||||
return {
|
||||
clients: [],
|
||||
capabilities: {},
|
||||
};
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@ -33,7 +36,7 @@ export default class OidcClientsRoute extends Route {
|
||||
}
|
||||
|
||||
afterModel(model) {
|
||||
if (model.length === 0) {
|
||||
if (model.clients.length === 0) {
|
||||
this.router.transitionTo('vault.cluster.access.oidc');
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,11 +5,30 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import OidcProviderForm from 'vault/forms/oidc/provider';
|
||||
import {
|
||||
IdentityApiOidcListScopesListEnum,
|
||||
IdentityApiOidcListClientsListEnum,
|
||||
} from '@hashicorp/vault-client-typescript';
|
||||
|
||||
export default class OidcProvidersCreateRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
|
||||
model() {
|
||||
return this.store.createRecord('oidc/provider');
|
||||
async model() {
|
||||
// fetch scopes and clients to populate dropdowns in form
|
||||
const [scopesResult, clientsResult] = await Promise.allSettled([
|
||||
this.api.identity.oidcListScopes(IdentityApiOidcListScopesListEnum.TRUE),
|
||||
this.api.identity.oidcListClients(IdentityApiOidcListClientsListEnum.TRUE),
|
||||
]);
|
||||
|
||||
// SearchSelect requires options to be objects.
|
||||
const scopes = scopesResult.value?.keys?.map((key) => ({ id: key })) ?? [];
|
||||
const clients = this.api.keyInfoToArray(clientsResult.value, 'name');
|
||||
|
||||
return {
|
||||
form: new OidcProviderForm({}, { isNew: true }),
|
||||
scopes,
|
||||
clients,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,17 +5,32 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { IdentityApiOidcListProvidersListEnum } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
export default class OidcProvidersRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service capabilities;
|
||||
|
||||
model() {
|
||||
return this.store.query('oidc/provider', {}).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
async model() {
|
||||
try {
|
||||
const response = await this.api.identity.oidcListProviders(IdentityApiOidcListProvidersListEnum.TRUE);
|
||||
const paths = response.keys.map((name) => this.capabilities.pathFor('oidcProvider', { name }));
|
||||
const capabilities = paths ? await this.capabilities.fetch(paths) : {};
|
||||
|
||||
return {
|
||||
providers: this.api.keyInfoToArray(response, 'name'),
|
||||
capabilities,
|
||||
};
|
||||
} catch (error) {
|
||||
const { status } = await this.api.parseError(error);
|
||||
if (status === 404) {
|
||||
return {
|
||||
providers: [],
|
||||
capabilities: {},
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,9 +7,12 @@ import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class OidcProviderRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service capabilities;
|
||||
|
||||
model({ name }) {
|
||||
return this.store.findRecord('oidc/provider', name);
|
||||
async model({ name }) {
|
||||
const { data } = await this.api.identity.oidcReadProvider(name);
|
||||
const capabilities = await this.capabilities.for('oidcProvider', { name });
|
||||
return { provider: { ...data, name }, capabilities };
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,13 +12,13 @@ export default class OidcProviderClientsRoute extends Route {
|
||||
@service capabilities;
|
||||
|
||||
async model() {
|
||||
const { allowedClientIds } = this.modelFor('vault.cluster.access.oidc.providers.provider');
|
||||
const { provider } = this.modelFor('vault.cluster.access.oidc.providers.provider');
|
||||
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 = provider.allowed_client_ids.includes('*')
|
||||
? clients
|
||||
: clients.filter((client) => allowedClientIds.includes(client.client_id));
|
||||
: clients.filter((client) => provider.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,38 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import OidcProviderForm from 'vault/forms/oidc/provider';
|
||||
import {
|
||||
IdentityApiOidcListScopesListEnum,
|
||||
IdentityApiOidcListClientsListEnum,
|
||||
} from '@hashicorp/vault-client-typescript';
|
||||
import parseURL from 'core/utils/parse-url';
|
||||
|
||||
export default class OidcProviderEditRoute extends Route {}
|
||||
export default class OidcProviderEditRoute extends Route {
|
||||
@service api;
|
||||
|
||||
async model() {
|
||||
// fetch scopes and clients to populate dropdowns in form
|
||||
const [scopesResult, clientsResult] = await Promise.allSettled([
|
||||
this.api.identity.oidcListScopes(IdentityApiOidcListScopesListEnum.TRUE),
|
||||
this.api.identity.oidcListClients(IdentityApiOidcListClientsListEnum.TRUE),
|
||||
]);
|
||||
|
||||
// SearchSelect requires options to be objects.
|
||||
const scopes = scopesResult.value?.keys?.map((key) => ({ id: key })) ?? [];
|
||||
const clients = this.api.keyInfoToArray(clientsResult.value, 'name');
|
||||
const { provider } = this.modelFor('vault.cluster.access.oidc.providers.provider');
|
||||
// parse issuer to only include scheme, host, and port for form field
|
||||
const formData = {
|
||||
...provider,
|
||||
issuer: provider.issuer ? parseURL(provider.issuer).origin : '',
|
||||
};
|
||||
|
||||
return {
|
||||
form: new OidcProviderForm(formData),
|
||||
scopes,
|
||||
clients,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
}}
|
||||
|
||||
<Toolbar />
|
||||
{{#if (gt this.model.length 0)}}
|
||||
{{#if (gt this.model.providers.length 0)}}
|
||||
<Oidc::ProviderList @model={{this.model}} />
|
||||
{{else}}
|
||||
<Hds::ApplicationState class="top-padding-32 is-marginless" as |A|>
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
}}
|
||||
|
||||
<Oidc::ProviderForm
|
||||
@model={{this.model}}
|
||||
@form={{this.model.form}}
|
||||
@scopes={{this.model.scopes}}
|
||||
@clients={{this.model.clients}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.providers"}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.providers.provider.details" this.model.name}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.providers.provider.details" this.model.form.data.name}}
|
||||
/>
|
||||
@ -4,13 +4,13 @@
|
||||
}}
|
||||
|
||||
{{#if (not-eq this.router.currentRoute.localName "edit")}}
|
||||
<Page::Header @title={{this.model.name}}>
|
||||
<Page::Header @title={{this.model.provider.name}}>
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array
|
||||
(hash label="Vault" route="vault.cluster.dashboard" icon="vault")
|
||||
(hash label="OIDC provider: Providers" route="vault.cluster.access.oidc.providers")
|
||||
(hash label=this.model.name)
|
||||
(hash label=this.model.provider.name)
|
||||
}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
@ -22,14 +22,14 @@
|
||||
<LinkTo
|
||||
data-test-oidc-provider-details
|
||||
@route="vault.cluster.access.oidc.providers.provider.details"
|
||||
@model={{this.model}}
|
||||
@model={{this.model.provider.name}}
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
<LinkTo
|
||||
data-test-oidc-provider-clients
|
||||
@route="vault.cluster.access.oidc.providers.provider.clients"
|
||||
@model={{this.model}}
|
||||
@model={{this.model.provider.name}}
|
||||
>
|
||||
Applications
|
||||
</LinkTo>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if (and (not-eq this.model.name "default") this.model.canDelete)}}
|
||||
{{#if (and (not-eq this.model.provider.name "default") this.model.capabilities.canDelete)}}
|
||||
<ConfirmAction
|
||||
data-test-oidc-provider-delete
|
||||
@buttonText="Delete provider"
|
||||
@ -17,10 +17,10 @@
|
||||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if this.model.canEdit}}
|
||||
{{#if this.model.capabilities.canUpdate}}
|
||||
<ToolbarLink
|
||||
@route="vault.cluster.access.oidc.providers.provider.edit"
|
||||
@model={{this.model.name}}
|
||||
@model={{this.model.provider.name}}
|
||||
data-test-oidc-provider-edit
|
||||
>
|
||||
Edit provider
|
||||
@ -29,15 +29,13 @@
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<InfoTableRow @label="Name" @value={{this.model.name}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Issuer URL" @value={{this.model.issuer}} @addCopyButton={{true}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Name" @value={{this.model.provider.name}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Issuer URL" @value={{this.model.provider.issuer}} @addCopyButton={{true}} @alwaysRender={{true}} />
|
||||
<InfoTableRow
|
||||
@label="Scopes"
|
||||
@type="array"
|
||||
@value={{@model.scopesSupported}}
|
||||
@model={{@model}}
|
||||
@value={{this.model.provider.scopes_supported}}
|
||||
@isLink={{true}}
|
||||
@modelType="oidc/scope"
|
||||
@itemRoute={{"vault.cluster.access.oidc.scopes.scope.details"}}
|
||||
@alwaysRender={{true}}
|
||||
@doNotTruncate={{true}}
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
}}
|
||||
|
||||
<Oidc::ProviderForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.providers.provider.details" this.model.name}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.providers.provider.details" this.model.name}}
|
||||
@form={{this.model.form}}
|
||||
@scopes={{this.model.scopes}}
|
||||
@clients={{this.model.clients}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.providers.provider.details" this.model.form.data.name}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.providers.provider.details" this.model.form.data.name}}
|
||||
/>
|
||||
@ -39,6 +39,7 @@ export const PATH_MAP = {
|
||||
ldapStaticRole: apiPath`${'backend'}/static-role/${'name'}`,
|
||||
ldapStaticRoleCreds: apiPath`${'backend'}/static-cred/${'name'}`,
|
||||
oidcClient: apiPath`identity/oidc/client/${'name'}`,
|
||||
oidcProvider: apiPath`identity/oidc/provider/${'name'}`,
|
||||
pkiCertificates: apiPath`${'backend'}/certificates`,
|
||||
pkiConfigAcme: apiPath`${'backend'}/config/acme`,
|
||||
pkiConfigAutoTidy: apiPath`${'backend'}/config/auto-tidy`,
|
||||
|
||||
@ -148,13 +148,13 @@ export default class SearchSelect extends Component {
|
||||
? options
|
||||
: [...this.addSearchText(options)];
|
||||
|
||||
if (!this.args.parentManageSelected) {
|
||||
// set selectedOptions and remove matches from dropdown list
|
||||
this.selectedOptions = this.args.inputValue
|
||||
? this.formatInputAndUpdateDropdown(this.args.inputValue)
|
||||
: [];
|
||||
}
|
||||
// set selectedOptions and remove matches from dropdown list
|
||||
const values = this.args.parentManageSelected
|
||||
? this.args.parentManageSelected.map((opt) => opt[this.idKey])
|
||||
: this.args.inputValue;
|
||||
this.selectedOptions = values ? this.formatInputAndUpdateDropdown(values) : [];
|
||||
}
|
||||
this.shouldUseFallback = this.args.fallbackComponent && !this.args.options?.length;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
oidcConfigHandlers(this.server);
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.api = this.owner.lookup('service:api');
|
||||
return login();
|
||||
});
|
||||
|
||||
@ -47,9 +48,11 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
||||
assert.expect(21);
|
||||
|
||||
//* start with clean test state
|
||||
await clearRecord(this.store, 'oidc/client', 'client-with-test-key');
|
||||
await clearRecord(this.store, 'oidc/client', 'client-with-default-key');
|
||||
await clearRecord(this.store, 'oidc/key', 'test-key');
|
||||
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'),
|
||||
]);
|
||||
|
||||
// create client with default key
|
||||
await visit(OIDC_BASE_URL + '/clients/create');
|
||||
@ -180,9 +183,11 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
||||
);
|
||||
|
||||
//* clean up test state
|
||||
await clearRecord(this.store, 'oidc/client', 'client-with-test-key');
|
||||
await clearRecord(this.store, 'oidc/client', 'client-with-default-key');
|
||||
await clearRecord(this.store, 'oidc/key', 'test-key');
|
||||
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'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('it creates, rotates and deletes a key', async function (assert) {
|
||||
@ -366,9 +371,11 @@ module('Acceptance | oidc-config clients', function (hooks) {
|
||||
assert.expect(21);
|
||||
|
||||
//* clear out test state
|
||||
await clearRecord(this.store, 'oidc/client', 'test-app');
|
||||
await clearRecord(this.store, 'oidc/client', 'my-webapp'); // created by oidc-provider-test
|
||||
await clearRecord(this.store, 'oidc/assignment', 'assignment-inline');
|
||||
await Promise.allSettled([
|
||||
this.api.identity.oidcDeleteClient('test-app'),
|
||||
this.api.identity.oidcDeleteClient('my-webapp'),
|
||||
clearRecord(this.store, 'oidc/assignment', 'assignment-inline'),
|
||||
]);
|
||||
|
||||
// create a client with allow all access
|
||||
await visit(OIDC_BASE_URL + '/clients/create');
|
||||
|
||||
@ -26,6 +26,8 @@ import {
|
||||
clearRecord,
|
||||
} from 'vault/tests/helpers/oidc-config';
|
||||
import { capabilitiesStub, overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const searchSelect = create(ss);
|
||||
const flashMessage = create(fm);
|
||||
|
||||
@ -38,6 +40,7 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
oidcConfigHandlers(this.server);
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.api = this.owner.lookup('service:api');
|
||||
// mock client list so OIDC BASE URL does not redirect to landing call-to-action image
|
||||
this.server.get('/identity/oidc/client', () => overrideResponse(null, { data: CLIENT_LIST_RESPONSE }));
|
||||
return login();
|
||||
@ -165,9 +168,13 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) {
|
||||
test('it creates a scope, and creates a provider with that scope', async function (assert) {
|
||||
assert.expect(28);
|
||||
|
||||
const apiSpy = sinon.spy(this.owner.lookup('service:api').identity, 'oidcWriteProvider');
|
||||
|
||||
//* clear out test state
|
||||
await clearRecord(this.store, 'oidc/scope', 'test-scope');
|
||||
await clearRecord(this.store, 'oidc/provider', 'test-provider');
|
||||
await Promise.allSettled([
|
||||
clearRecord(this.store, 'oidc/scope', 'test-scope'),
|
||||
this.api.identity.oidcDeleteProvider('test-provider'),
|
||||
]);
|
||||
|
||||
// create a new scope
|
||||
await visit(OIDC_BASE_URL + '/scopes/create');
|
||||
@ -242,8 +249,8 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) {
|
||||
'navigates to provider create form'
|
||||
);
|
||||
await fillIn(GENERAL.inputByAttr('name'), 'test-provider');
|
||||
await clickTrigger('#scopesSupported');
|
||||
await selectChoose('#scopesSupported', 'test-scope');
|
||||
await clickTrigger('#oidc-provider-form-scope-select');
|
||||
await selectChoose('#oidc-provider-form-scope-select', 'test-scope');
|
||||
await click(SELECTORS.providerSaveButton);
|
||||
assert.strictEqual(
|
||||
flashMessage.latestMessage,
|
||||
@ -282,7 +289,9 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) {
|
||||
'navigates to provider edit page from details'
|
||||
);
|
||||
await click('[data-test-oidc-radio="limited"]');
|
||||
await click('[data-test-component="search-select"]#allowedClientIds .ember-basic-dropdown-trigger');
|
||||
await click(
|
||||
'[data-test-component="search-select"]#oidc-provider-form-client-select .ember-basic-dropdown-trigger'
|
||||
);
|
||||
await fillIn(GENERAL.searchSelect.searchInput, 'test-app');
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await click(SELECTORS.providerSaveButton);
|
||||
@ -296,9 +305,9 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) {
|
||||
'vault.cluster.access.oidc.providers.provider.details',
|
||||
'navigates back to provider details after updating'
|
||||
);
|
||||
const providerModel = this.store.peekRecord('oidc/provider', 'test-provider');
|
||||
const { allowed_client_ids } = apiSpy.lastCall.args[1];
|
||||
assert.propEqual(
|
||||
providerModel.allowedClientIds,
|
||||
allowed_client_ids,
|
||||
['whaT7KB0C3iBH1l3rXhd5HPf0n6vXU0s'],
|
||||
'provider saves client_id (not id or name) in allowed_client_ids param'
|
||||
);
|
||||
@ -390,20 +399,22 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) {
|
||||
// PROVIDER DELETE + EDIT PERMISSIONS
|
||||
test('it hides delete and edit for a provider when no permission', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.get('/identity/oidc/providers', () =>
|
||||
overrideResponse(null, { data: { providers: ['test-provider'] } })
|
||||
);
|
||||
|
||||
const provider = {
|
||||
allowed_client_ids: ['*'],
|
||||
issuer: 'http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider',
|
||||
scopes_supported: ['test-scope'],
|
||||
};
|
||||
const data = {
|
||||
keys: ['test-provider'],
|
||||
key_info: { 'test-provider': provider },
|
||||
};
|
||||
this.server.get('/identity/oidc/provider', () => overrideResponse(null, { data }));
|
||||
this.server.get('/identity/oidc/provider/test-provider', () =>
|
||||
overrideResponse(null, {
|
||||
data: {
|
||||
allowed_client_ids: ['*'],
|
||||
issuer: 'http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider',
|
||||
scopes_supported: ['test-scope'],
|
||||
},
|
||||
})
|
||||
overrideResponse(null, { data: provider })
|
||||
);
|
||||
this.server.post('/sys/capabilities-self', () =>
|
||||
capabilitiesStub(OIDC_BASE_URL + '/provider/test-provider', ['read'])
|
||||
capabilitiesStub('identity/oidc/provider/test-provider', ['read'])
|
||||
);
|
||||
|
||||
await visit(OIDC_BASE_URL + '/providers');
|
||||
|
||||
@ -11,7 +11,6 @@ import sinon from 'sinon';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import enablePage from 'vault/tests/pages/settings/auth/enable';
|
||||
import { visit, settled, currentURL, waitFor, currentRouteName, fillIn, click } from '@ember/test-helpers';
|
||||
import { clearRecord } from 'vault/tests/helpers/oidc-config';
|
||||
import { runCmd } from 'vault/tests/helpers/commands';
|
||||
import queryParamString from 'vault/utils/query-param-string';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
@ -121,7 +120,6 @@ module('Acceptance | oidc provider', function (hooks) {
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.uid = uuidv4();
|
||||
this.store = this.owner.lookup('service:store');
|
||||
await login();
|
||||
await settled();
|
||||
this.oidcSetupInformation = await setupOidc(this.uid);
|
||||
@ -129,6 +127,11 @@ module('Acceptance | oidc provider', function (hooks) {
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
const api = this.owner.lookup('service:api');
|
||||
await Promise.allSettled([
|
||||
api.identity.oidcDeleteProvider(WEB_APP_NAME),
|
||||
api.identity.oidcDeleteProvider(PROVIDER_NAME),
|
||||
]);
|
||||
await login();
|
||||
});
|
||||
|
||||
@ -169,10 +172,6 @@ module('Acceptance | oidc provider', function (hooks) {
|
||||
.hasTextContaining(`click here to go back to app`, 'Shows link back to app');
|
||||
const link = document.querySelector('[data-test-oidc-redirect]').getAttribute('href');
|
||||
assert.ok(link.includes('/callback?code='), 'Redirects to correct url');
|
||||
|
||||
//* clean up test state
|
||||
await clearRecord(this.store, 'oidc/client', WEB_APP_NAME);
|
||||
await clearRecord(this.store, 'oidc/provider', PROVIDER_NAME);
|
||||
});
|
||||
|
||||
test('OIDC Provider redirects to auth if current token and prompt = login', async function (assert) {
|
||||
@ -220,10 +219,6 @@ module('Acceptance | oidc provider', function (hooks) {
|
||||
);
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.oidc-provider');
|
||||
assert.dom('[data-test-consent-form]').exists('Consent form exists');
|
||||
|
||||
//* clean up test state
|
||||
await clearRecord(this.store, 'oidc/client', WEB_APP_NAME);
|
||||
await clearRecord(this.store, 'oidc/provider', PROVIDER_NAME);
|
||||
});
|
||||
|
||||
// Error handling test coverage, see issue for more context https://github.com/hashicorp/vault/issues/27772
|
||||
@ -271,7 +266,5 @@ module('Acceptance | oidc provider', function (hooks) {
|
||||
//* clean up test state
|
||||
authStub.restore();
|
||||
await login();
|
||||
await clearRecord(this.store, 'oidc/client', WEB_APP_NAME);
|
||||
await clearRecord(this.store, 'oidc/provider', PROVIDER_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,14 +5,12 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
module('Integration | Component | oidc/client-list', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.renderComponent = (isLimited) => {
|
||||
|
||||
@ -9,11 +9,12 @@ 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 { SELECTORS, OIDC_BASE_URL, CLIENT_LIST_RESPONSE } from 'vault/tests/helpers/oidc-config';
|
||||
import parseURL from 'core/utils/parse-url';
|
||||
import { SELECTORS, CLIENT_LIST_RESPONSE } 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 OidcProviderForm from 'vault/forms/oidc/provider';
|
||||
import sinon from 'sinon';
|
||||
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';
|
||||
|
||||
const ISSUER_URL = 'http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider';
|
||||
|
||||
@ -23,22 +24,27 @@ module('Integration | Component | oidc/provider-form', function (hooks) {
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
oidcConfigHandlers(this.server);
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.server.get('/identity/oidc/scope', () => {
|
||||
return {
|
||||
request_id: 'scope-list-id',
|
||||
lease_id: '',
|
||||
renewable: false,
|
||||
lease_duration: 0,
|
||||
data: {
|
||||
keys: ['test-scope'],
|
||||
},
|
||||
wrap_info: null,
|
||||
warnings: null,
|
||||
auth: null,
|
||||
};
|
||||
});
|
||||
this.server.get('/identity/oidc/client', () => overrideResponse(null, { data: CLIENT_LIST_RESPONSE }));
|
||||
this.api = this.owner.lookup('service:api');
|
||||
this.apiStub = sinon.stub(this.api.identity, 'oidcWriteProvider').resolves();
|
||||
|
||||
this.scopes = [{ id: 'test-scope' }];
|
||||
this.clients = this.api.keyInfoToArray(CLIENT_LIST_RESPONSE, 'name');
|
||||
this.onCancel = sinon.spy();
|
||||
this.onSave = sinon.spy();
|
||||
|
||||
this.renderComponent = (data) => {
|
||||
this.form = new OidcProviderForm(data || {}, { isNew: !data });
|
||||
return render(hbs`
|
||||
<Oidc::ProviderForm
|
||||
@form={{this.form}}
|
||||
@scopes={{this.scopes}}
|
||||
@clients={{this.clients}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
};
|
||||
|
||||
setRunOptions({
|
||||
rules: {
|
||||
// TODO: Fix SearchSelect component
|
||||
@ -53,19 +59,8 @@ module('Integration | Component | oidc/provider-form', function (hooks) {
|
||||
|
||||
test('it should save new provider', async function (assert) {
|
||||
assert.expect(13);
|
||||
this.server.post('/identity/oidc/provider/test-provider', (schema, req) => {
|
||||
assert.ok(true, 'Request made to save provider');
|
||||
return JSON.parse(req.requestBody);
|
||||
});
|
||||
this.model = this.store.createRecord('oidc/provider');
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
|
||||
await render(hbs`
|
||||
<Oidc::ProviderForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Create Provider', 'Form title renders correct text');
|
||||
assert.dom(SELECTORS.providerSaveButton).hasText('Create', 'Save button has correct text');
|
||||
@ -73,7 +68,9 @@ module('Integration | Component | oidc/provider-form', function (hooks) {
|
||||
.dom('[data-test-input="issuer"]')
|
||||
.hasAttribute('placeholder', 'e.g. https://example.com:8200', 'issuer placeholder text is correct');
|
||||
assert.strictEqual(findAll('[data-test-field]').length, 3, 'renders all input fields');
|
||||
await click('[data-test-component="search-select"]#scopesSupported .ember-basic-dropdown-trigger');
|
||||
await click(
|
||||
'[data-test-component="search-select"]#oidc-provider-form-scope-select .ember-basic-dropdown-trigger'
|
||||
);
|
||||
assert.dom('li.ember-power-select-option').hasText('test-scope', 'dropdown renders scopes');
|
||||
|
||||
// check validation errors
|
||||
@ -89,47 +86,37 @@ module('Integration | Component | oidc/provider-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-provider-form-client-select')
|
||||
.exists('Limited radio button shows clients search select');
|
||||
await click('[data-test-component="search-select"]#allowedClientIds .ember-basic-dropdown-trigger');
|
||||
await click(
|
||||
'[data-test-component="search-select"]#oidc-provider-form-client-select .ember-basic-dropdown-trigger'
|
||||
);
|
||||
assert.dom('li.ember-power-select-option').hasTextContaining('test-app', 'dropdown renders client name');
|
||||
assert.dom('[data-test-smaller-id]').exists('renders smaller client id in dropdown');
|
||||
|
||||
await click('[data-test-oidc-radio="allow-all"]');
|
||||
assert
|
||||
.dom('[data-test-component="search-select"]#allowedClientIds')
|
||||
.dom('[data-test-component="search-select"]#oidc-provider-form-client-select')
|
||||
.doesNotExist('Allow all radio button hides search select');
|
||||
|
||||
await fillIn('[data-test-input="name"]', 'test-provider');
|
||||
await click(SELECTORS.providerSaveButton);
|
||||
|
||||
assert.true(this.onSave.called, 'onSave callback fires on save success');
|
||||
assert.true(this.apiStub.calledWith('test-provider'), 'Request made to save provider');
|
||||
});
|
||||
|
||||
test('it should update provider', async function (assert) {
|
||||
assert.expect(9);
|
||||
assert.expect(8);
|
||||
|
||||
this.server.post('/identity/oidc/provider/test-provider', (schema, req) => {
|
||||
assert.ok(true, 'Request made to save provider');
|
||||
return JSON.parse(req.requestBody);
|
||||
});
|
||||
|
||||
this.store.pushPayload('oidc/provider', {
|
||||
modelName: 'oidc/provider',
|
||||
const provider = {
|
||||
name: 'test-provider',
|
||||
allowed_client_ids: ['*'],
|
||||
issuer: ISSUER_URL,
|
||||
scopes_supported: ['test-scope'],
|
||||
});
|
||||
};
|
||||
|
||||
this.model = this.store.peekRecord('oidc/provider', 'test-provider');
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
|
||||
|
||||
await render(hbs`
|
||||
<Oidc::ProviderForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
await this.renderComponent(provider);
|
||||
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Edit Provider', 'Title renders correct text');
|
||||
assert.dom(SELECTORS.providerSaveButton).hasText('Update', 'Save button has correct text');
|
||||
@ -137,87 +124,49 @@ module('Integration | Component | oidc/provider-form', function (hooks) {
|
||||
assert
|
||||
.dom('[data-test-input="name"]')
|
||||
.hasValue('test-provider', 'Name input is populated with model value');
|
||||
assert
|
||||
.dom('[data-test-input="issuer"]')
|
||||
.hasValue(parseURL(ISSUER_URL).origin, 'issuer value is just scheme://host:port portion of full URL');
|
||||
|
||||
assert.dom('[data-test-selected-option]').hasText('test-scope', 'model scope is selected');
|
||||
assert.dom('[data-test-oidc-radio="allow-all"] input').isChecked('Allow all radio button is selected');
|
||||
await click(SELECTORS.providerSaveButton);
|
||||
|
||||
assert.true(this.onSave.called, 'onSave callback fires on save success');
|
||||
const { name, ...payload } = provider;
|
||||
assert.true(this.apiStub.calledWith(name, payload), 'Request made to save provider');
|
||||
});
|
||||
|
||||
test('it should rollback attributes or unload record on cancel', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.model = this.store.createRecord('oidc/provider');
|
||||
this.onCancel = () => assert.ok(true, 'onCancel callback fires');
|
||||
|
||||
await render(hbs`
|
||||
<Oidc::ProviderForm
|
||||
@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();
|
||||
await click(SELECTORS.providerCancelButton);
|
||||
assert.true(this.model.isDestroyed, 'New model is unloaded on cancel');
|
||||
|
||||
this.store.pushPayload('oidc/provider', {
|
||||
modelName: 'oidc/provider',
|
||||
name: 'test-provider',
|
||||
allowed_client_ids: ['*'],
|
||||
issuer: ISSUER_URL,
|
||||
scopes_supported: ['test-scope'],
|
||||
});
|
||||
|
||||
this.model = this.store.peekRecord('oidc/provider', 'test-provider');
|
||||
|
||||
await render(hbs`
|
||||
<Oidc::ProviderForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await click('[data-test-oidc-radio="limited"]');
|
||||
await click(SELECTORS.providerCancelButton);
|
||||
assert.strictEqual(this.model.allowed_client_ids, undefined, 'Model attributes rolled back on cancel');
|
||||
assert.true(this.onCancel.called, 'onCancel callback fires on cancel');
|
||||
});
|
||||
|
||||
test('it should render fallback for search select', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.model = this.store.createRecord('oidc/provider');
|
||||
this.server.get('/identity/oidc/scope', () => overrideResponse(403));
|
||||
this.server.get('/identity/oidc/client', () => overrideResponse(403));
|
||||
await render(hbs`
|
||||
<Oidc::ProviderForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
|
||||
this.scopes = [];
|
||||
this.clients = [];
|
||||
await this.renderComponent();
|
||||
|
||||
assert
|
||||
.dom('[data-test-component="search-select"]#scopesSupported [data-test-component="string-list"]')
|
||||
.dom(
|
||||
'[data-test-component="search-select"]#oidc-provider-form-scope-select [data-test-component="string-list"]'
|
||||
)
|
||||
.exists('renders fall back for scopes search select');
|
||||
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-provider-form-client-select [data-test-component="string-list"]'
|
||||
)
|
||||
.exists('Radio toggle shows assignments 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/provider');
|
||||
this.server.post('/sys/capabilities-self', () => capabilitiesStub(OIDC_BASE_URL + '/providers'));
|
||||
await render(hbs`
|
||||
<Oidc::ProviderForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`);
|
||||
|
||||
this.apiStub.rejects(getErrorResponse());
|
||||
await this.renderComponent();
|
||||
|
||||
await fillIn('[data-test-input="name"]', 'some-provider');
|
||||
await click(SELECTORS.providerSaveButton);
|
||||
assert
|
||||
|
||||
@ -5,32 +5,47 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { allowAllCapabilitiesStub, capabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
module('Integration | Component | oidc/provider-list', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.createRecord('oidc/provider', { name: 'first-provider', issuer: 'foobar' });
|
||||
this.store.createRecord('oidc/provider', { name: 'second-provider', issuer: 'foobar' });
|
||||
this.model = this.store.peekAll('oidc/provider');
|
||||
this.renderComponent = (isLimited) => {
|
||||
const providers = [
|
||||
{ name: 'first-provider', issuer: 'foobar' },
|
||||
{ name: 'second-provider', issuer: 'foobar' },
|
||||
];
|
||||
|
||||
const capabilities = providers.reduce((capabilities, provider) => {
|
||||
const path = this.owner.lookup('service:capabilities').pathFor('oidcProvider', provider);
|
||||
const isFirstProvider = provider.name === 'first-provider';
|
||||
const canRead = isLimited ? isFirstProvider : true;
|
||||
const canUpdate = !isLimited;
|
||||
capabilities[path] = { canRead, canUpdate };
|
||||
return capabilities;
|
||||
}, {});
|
||||
|
||||
this.model = {
|
||||
providers,
|
||||
capabilities,
|
||||
};
|
||||
return render(hbs`<Oidc::ProviderList @model={{this.model}} />`);
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders list of providers', async function (assert) {
|
||||
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub(['read', 'update']));
|
||||
await render(hbs`<Oidc::ProviderList @model={{this.model}} />`);
|
||||
await this.renderComponent(false);
|
||||
|
||||
assert.dom('[data-test-oidc-provider-linked-block]').exists({ count: 2 }, 'Two clients are rendered');
|
||||
assert.dom('[data-test-oidc-provider-linked-block="first-provider"]').exists('First client is rendered');
|
||||
assert.dom('[data-test-oidc-provider-linked-block]').exists({ count: 2 }, 'Two providers are rendered');
|
||||
assert
|
||||
.dom('[data-test-oidc-provider-linked-block="first-provider"]')
|
||||
.exists('First provider is rendered');
|
||||
assert
|
||||
.dom('[data-test-oidc-provider-linked-block="second-provider"]')
|
||||
.exists('Second client is rendered');
|
||||
.exists('Second provider is rendered');
|
||||
|
||||
await click('[data-test-oidc-provider-linked-block="first-provider"] [data-test-popup-menu-trigger]');
|
||||
assert.dom('[data-test-oidc-provider-menu-link="details"]').exists('Details link is rendered');
|
||||
@ -38,15 +53,7 @@ module('Integration | Component | oidc/provider-list', function (hooks) {
|
||||
});
|
||||
|
||||
test('it renders popup menu based on permissions', async function (assert) {
|
||||
this.server.post('/sys/capabilities-self', (schema, req) => {
|
||||
const { paths } = JSON.parse(req.requestBody);
|
||||
if (paths[0] === 'identity/oidc/provider/first-provider') {
|
||||
return capabilitiesStub('identity/oidc/provider/first-provider', ['read']);
|
||||
} else {
|
||||
return capabilitiesStub('identity/oidc/provider/second-provider', ['deny']);
|
||||
}
|
||||
});
|
||||
await render(hbs`<Oidc::ProviderList @model={{this.model}} />`);
|
||||
await this.renderComponent(true);
|
||||
assert.dom('[data-test-popup-menu-trigger]').exists({ count: 1 }, 'Only one popup menu is rendered');
|
||||
await click(GENERAL.menuTrigger);
|
||||
assert.dom('[data-test-oidc-provider-menu-link="details"]').exists('Details link is rendered');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user