UI: OIDC Config for Vault as a provider (#17071)
* OIDC Config Routing (#16028) * adds oidc config routes * renames oidc applications route to clients * UI/vault 6646/landing page (#16069) * add to sidebar * add landing image and text * add permissions * add permissions to permissions service * remove comment * fix. * UI/OIDC models (#16091) * add models and fix routing * add ClientsCreate route * remove form functions from client model * update comment * address comments, cleanup models * add comment * OIDC Adapters and Serializers (#16120) * adds named-path base adapter * adds oidc adapters with tests * adds oidc serializers * fixes issue with supported_scopes relationship in oidc provider model * make radio card size flex (#16125) * OIDC config details routes (#16126) * adds details routes for oidc config resources * adds details templates for oidc config resources * OIDC parent route and index redirection (#16139) * adds parent oidc route with header and adds redirection if clients have been created * updates learn link * adds findRecord override to named-path adapter (#16145) * OIDC Scope Create/Edit View (#16174) * adds oidc scope-form to create and edit views * moves oidc header set logic from route to controller * OIDC Scope Details View (#16191) * adds oidc scope details view * removes disabled arg from scope delete confirm action * updates oidc scope template params link to use DocLink and adds success message on scope create success * updates oidc scope delete confirm action copy * adds oidc scopes list (#16196) * UI/vault 6655/OIDC create view (#16331) * setup header * wip * wip * wip * validations * error validations * cleanup * wip * fix error * clean up * handle modelValidations * add documentation on the decorator * remove spread attrs * first test and some fixes * halfway with test * fix error where the data object was sending param entiyIds and not entity_ids * validations or situation * fix test * small nit: * test if this fixes the test * fix * cleanup * nit * Assignments Update/Edit View (#16412) * wip * fix * render search-select after promise is fulfilled * add test coverage Co-authored-by: clairebontempo@gmail.com <cbontempo@hashicorp.com> * Added list view for keys (#16454) * Added list view for providers (#16442) * Added list view for providers * Removed check for model data length * Added new line at end of file * Fixed linting issues causing ui tests to fail * Added list view for application (#16469) * UI/remove has many relationship (#16470) * remove hasMany from models * remove relationships from assignments create form * update tests * Assignment list view (#16340) * inital setup * handle default allow all * add learn more link * Fixed the default allow_all for assignment list view to match Figma design * Fixed linting * Fixed hbs file syntax Co-authored-by: linda9379 <linda.jiang@hashicorp.com> * configure mirage and helper (#16482) * UI/OIDC client form (#16131) * WIP client form * wip * still WIP * fix form!; * remove computeds, cache form attrs instead * update scope form component name * add white space validation * add validations, cleanup * add edit form * fix link to in edit form * disable edit form * fix linkto * wip/ search select filter * WIP/search-select bug * fix assignment save * delete old modal js file * glimmerize/create new search select modal component * component cleanup * fix bugginess * fix search select and radio select action * add tests * revert some test changes * oops, removed test tag * add key list to response * fix test * move search select component to separate PR, revert changes * one more revert * remove oidc helper from this pr * remove hasMany relationship * minor cleanup * update assignment form to use fallback * fix allow_all appearing in dropdown on edit (#16508) * UI/ OIDC Application (client) details view (#16507) * fix test * finish details page * finish details view * clean u[ * fix typo * configure oidc mirage handler for tests * remove params, add new route instead * fix headers * remove console.log * remove controller/template reliance on tracked variable * rename variable * UI/Client route acceptance tests - fixed branch (#16654) * WIP client route tests * refactor client form so clientType is not edit-able * fix ttl in client form * wip// more acceptance tests and tags for hbs files * fix typo * fix syntax error * finish tests * fix client form test * resolve commits * update form test * OIDC Assignments Details view. (#16511) * setup * cleanup * view all fix * wip setting up tabs * wip * revert to no queryParam or tabs * add the read more component and styling * rename folder * cleanup * fix * UI/OIDC providers create/edit route (#16612) * update to use DocLink component * provider create form * cleaup * add formt est * revert label text * update doclink test * disallow new scopes from ss * fix test typo * fix provider form flash message * add period * test new form field attr * refactor form input * fix edit portion of issuer field * add test selector to new input field * add comment * Cleanup OIDC Config Mirage handler (#16674) * cleaup mirage * change to .then * pull out into config file * Scope acceptance tests (#16707) * Started writing acceptance tests * Added some more acceptance tests * Added tags for hbs and more tests * Modified variable names in scope form test * Fixed tests and linting * UI/OIDC Provider read view (#16632) * add providers/provider/client route * provider details view * add disabled button and tooltip for default * add toolbar separators * revert unrelated change * query all client records and filter by allowed client id" * refactor adapter to filter for clientId * cleanup adapter method * update test * refactor test * fix tests to accommodate for serializer change * update empty state message * fix linting * metadata for client list view (#16725) * Added metadata for list view in clients * Fixed linting * Fixed failing ui test * fix scopes and clients tests (#16768) * Initial fix of tests * Fixed failing scopes and clients acceptance tests * Fixed linting * UI: Key create/edit form (#16729) * add route models * add forms * add test * remove helperText attr * metadata for provider list view (#16738) * Added meta-data for provider list view * Added comment for serializer * Fixed import path for scopes and clients acceptance test files * UI/Add client ids to search select (#16744) * WIP use clientID instead of name * add client ids to search select * remove provider form component changes * fix search select on edit * cleanup comments and method * fix adapter query method * clean up comments * add test * remove destructuring so linting passes * fix tests * add accidentally deleted param * add clarifying comments * cleanup * change how shouldRenderName is set * cleanup tests * address comments * OIDC Assignment Acceptance tests (#16741) * test and fixes * merge stuff * fix * fixes * add waituntil * inconsistent nav issue * fixes * blah * UI/Key details view (#16776) * add details view * reformat model file * todo for when listing applications * add comment * update key form with refactored search select * add applications list * update test * update test * add names to flash messages * add rollbackAttributes to delete catch (#16796) * UI: Checks if records exists before creating record when URL contains :name (#16823) * check for record existing in createRecord * use error banner instead of flash messages for forms * add inline form message for validations * add error count message to inlinealert * add test for adapter * add tests * remove unused vars * UI: Disable limiting clients when creating key, filter clients when editing (#16926) * add tooltip to disabled radio button * pass query object to search select * update copy * add comment * cleanup console log and comment * fix tests * revert change because addressed in other pr * fix diff * fix test * UI: Add redirect when last client is deleted (#16927) * afterModel redirect if no models exist * fix test * change space * fix incorrect text * UI: Add InfoTooltip to selected 'ghost' client_ids (#16942) * return option if undefined * add info tooltip to search select * change word * add test * UI: OIDC config keys acceptance tests (#16968) * add keys test * update other oidc tests * remove-search select comment * UI: Filter Client providers list view (#17027) * pass param to adapter * add test * UI: OIDC Config Acceptance Tests (#17050) * WIP/provider acceptance tests" * WIP/this commit breaks lots of things * fix tests * update test selectors * combine key and client tests * cleanup clients and keys test * finish tests * small tidying * UI: Remove trailing comma from scopes, provider details page (#17069) * use info table row to cleanup scope logic * infotableitemarray cleanup * tidying * add changelog * teeny little empty state * fix wildcard string helper not working Co-authored-by: Jordan Reimer <zofskeez@gmail.com> Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com> Co-authored-by: Angel Garbarino <argarbarino@gmail.com> Co-authored-by: linda9379 <57650314+linda9379@users.noreply.github.com> Co-authored-by: linda9379 <linda.jiang@hashicorp.com>
This commit is contained in:
parent
2c11121c19
commit
83fc61c16b
|
@ -0,0 +1,2 @@
|
|||
```release-note:feature
|
||||
**UI OIDC Provider Config**: Adds configuration of Vault as an OIDC identity provider, and offer Vault’s various authentication methods and source of identity to any client applications.
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* base adapter for resources that are saved to a path whose unique identifier is name
|
||||
* save requests are made to the same endpoint and the resource is either created if not found or updated
|
||||
* */
|
||||
import ApplicationAdapter from './application';
|
||||
import { assert } from '@ember/debug';
|
||||
export default class NamedPathAdapter extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
saveMethod = 'POST'; // override when extending if PUT is used rather than POST
|
||||
|
||||
_saveRecord(store, { modelName }, snapshot) {
|
||||
// since the response is empty return the serialized data rather than nothing
|
||||
const data = store.serializerFor(modelName).serialize(snapshot);
|
||||
return this.ajax(this.urlForUpdateRecord(snapshot.attr('name'), modelName, snapshot), this.saveMethod, {
|
||||
data,
|
||||
}).then(() => data);
|
||||
}
|
||||
|
||||
// create does not return response similar to PUT request
|
||||
createRecord() {
|
||||
let [store, { modelName }, snapshot] = arguments;
|
||||
let name = snapshot.attr('name');
|
||||
// throw error if user attempts to create a record with same name, otherwise POST request silently overrides (updates) the existing model
|
||||
if (store.hasRecordForId(modelName, name)) {
|
||||
throw new Error(`A record already exists with the name: ${name}`);
|
||||
} else {
|
||||
return this._saveRecord(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
// update uses same endpoint and method as create
|
||||
updateRecord() {
|
||||
return this._saveRecord(...arguments);
|
||||
}
|
||||
|
||||
// if backend does not return name in response Ember Data will throw an error for pushing a record with no id
|
||||
// use the id (name) supplied to findRecord to set property on response data
|
||||
findRecord(store, type, name) {
|
||||
return super.findRecord(...arguments).then((resp) => {
|
||||
if (!resp.data.name) {
|
||||
resp.data.name = name;
|
||||
}
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
|
||||
// GET request with list=true as query param
|
||||
async query(store, type, query) {
|
||||
const url = this.urlForQuery(query, type.modelName);
|
||||
const { paramKey, filterFor, allowed_client_id } = query;
|
||||
// * 'paramKey' is a string of the param name (model attr) we're filtering for, e.g. 'client_id'
|
||||
// * 'filterFor' is an array of values to filter for (value type must match the attr type), e.g. array of ID strings
|
||||
// * 'allowed_client_id' is a valid query param to the /provider endpoint
|
||||
let queryParams = { list: true, ...(allowed_client_id && { allowed_client_id }) };
|
||||
const response = await this.ajax(url, 'GET', { data: queryParams });
|
||||
|
||||
// filter LIST response only if key_info exists and query includes both 'paramKey' & 'filterFor'
|
||||
if (filterFor) assert('filterFor must be an array', Array.isArray(filterFor));
|
||||
if (response.data.key_info && filterFor && paramKey && !filterFor.includes('*')) {
|
||||
const data = this.filterListResponse(paramKey, filterFor, response.data.key_info);
|
||||
return { ...response, data };
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
filterListResponse(paramKey, matchValues, key_info) {
|
||||
const keyInfoAsArray = Object.entries(key_info);
|
||||
const filtered = keyInfoAsArray.filter((key) => {
|
||||
const value = key[1]; // value is an object of model attributes
|
||||
return matchValues.includes(value[paramKey]);
|
||||
});
|
||||
const filteredKeyInfo = Object.fromEntries(filtered);
|
||||
const filteredKeys = Object.keys(filteredKeyInfo);
|
||||
return { keys: filteredKeys, key_info: filteredKeyInfo };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import NamedPathAdapter from '../named-path';
|
||||
|
||||
export default class OidcAssignmentAdapter extends NamedPathAdapter {
|
||||
pathForType() {
|
||||
return 'identity/oidc/assignment';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import NamedPathAdapter from '../named-path';
|
||||
|
||||
export default class OidcClientAdapter extends NamedPathAdapter {
|
||||
pathForType() {
|
||||
return 'identity/oidc/client';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import NamedPathAdapter from '../named-path';
|
||||
|
||||
export default class OidcKeyAdapter extends NamedPathAdapter {
|
||||
pathForType() {
|
||||
return 'identity/oidc/key';
|
||||
}
|
||||
rotate(name, verification_ttl) {
|
||||
const data = verification_ttl ? { verification_ttl } : {};
|
||||
return this.ajax(`${this.urlForUpdateRecord(name, 'oidc/key')}/rotate`, 'POST', { data });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import NamedPathAdapter from '../named-path';
|
||||
|
||||
export default class OidcProviderAdapter extends NamedPathAdapter {
|
||||
pathForType() {
|
||||
return 'identity/oidc/provider';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import NamedPathAdapter from '../named-path';
|
||||
|
||||
export default class OidcScopeAdapter extends NamedPathAdapter {
|
||||
pathForType() {
|
||||
return 'identity/oidc/scope';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
/**
|
||||
* @module Oidc::AssignmentForm
|
||||
* Oidc::AssignmentForm components are used to display the create view for OIDC providers assignments.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <Oidc::AssignmentForm @model={this.model}
|
||||
* @onCancel={transition-to "vault.cluster.access.oidc.assignment"} @param1={{param1}}
|
||||
* @onSave={transition-to "vault.cluster.access.oidc.assignments.assignment.details" this.model.name}
|
||||
* />
|
||||
* ```
|
||||
* @callback onCancel
|
||||
* @callback onSave
|
||||
* @param {object} model - The parent's model
|
||||
* @param {string} onCancel - callback triggered when cancel button is clicked
|
||||
* @param {string} onSave - callback triggered when save button is clicked
|
||||
*/
|
||||
|
||||
export default class OidcAssignmentFormComponent extends Component {
|
||||
@service store;
|
||||
@service flashMessages;
|
||||
@tracked modelValidations;
|
||||
@tracked errorBanner;
|
||||
|
||||
@task
|
||||
*save(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const { isValid, state } = this.args.model.validate();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
if (isValid) {
|
||||
const { isNew, name } = this.args.model;
|
||||
yield this.args.model.save();
|
||||
this.flashMessages.success(`Successfully ${isNew ? 'created' : 'updated'} the assignment ${name}.`);
|
||||
// this form is sometimes used in modal, passing the model notifies
|
||||
// the parent if the save was successful
|
||||
this.args.onSave(this.args.model);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.errorBanner = message;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
|
||||
this.args.model[method]();
|
||||
this.args.onCancel();
|
||||
}
|
||||
|
||||
@action
|
||||
handleOperation({ target }) {
|
||||
this.args.model.name = target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
onEntitiesSelect(selectedIds) {
|
||||
this.args.model.entityIds = selectedIds;
|
||||
}
|
||||
|
||||
@action
|
||||
onGroupsSelect(selectedIds) {
|
||||
this.args.model.groupIds = selectedIds;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
/**
|
||||
* @module OidcClientForm
|
||||
* OidcClientForm components are used to create and update OIDC clients (a.k.a. applications)
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <OidcClientForm @model={{this.model}} />
|
||||
* ```
|
||||
* @callback onCancel
|
||||
* @callback onSave
|
||||
* @param {Object} model - oidc client model
|
||||
* @param {onCancel} onCancel - callback triggered when cancel button is clicked
|
||||
* @param {onSave} onSave - callback triggered on save success
|
||||
*/
|
||||
|
||||
export default class OidcClientForm extends Component {
|
||||
@service store;
|
||||
@service flashMessages;
|
||||
@tracked modelValidations;
|
||||
@tracked errorBanner;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked radioCardGroupValue =
|
||||
!this.args.model.assignments || this.args.model.assignments.includes('allow_all')
|
||||
? 'allow_all'
|
||||
: 'limited';
|
||||
|
||||
get modelAssignments() {
|
||||
const { assignments } = this.args.model;
|
||||
if (assignments.includes('allow_all') && assignments.length === 1) {
|
||||
return [];
|
||||
} else {
|
||||
return assignments;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleAssignmentSelection(selection) {
|
||||
// if array then coming from search-select component, set selection as model assignments
|
||||
if (Array.isArray(selection)) {
|
||||
this.args.model.assignments = selection;
|
||||
} else {
|
||||
// otherwise update radio button value and reset assignments so
|
||||
// UI always reflects a user's selection (including when no assignments are selected)
|
||||
this.radioCardGroupValue = selection;
|
||||
this.args.model.assignments = [];
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
|
||||
this.args.model[method]();
|
||||
this.args.onCancel();
|
||||
}
|
||||
|
||||
@task
|
||||
*save(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const { isValid, state, invalidFormMessage } = this.args.model.validate();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = invalidFormMessage;
|
||||
if (isValid) {
|
||||
if (this.radioCardGroupValue === 'allow_all') {
|
||||
// the backend permits 'allow_all' AND other assignments, though 'allow_all' will take precedence
|
||||
// the UI limits the config by allowing either 'allow_all' OR a list of other assignments
|
||||
// note: when editing the UI removes any additional assignments previously configured via CLI
|
||||
this.args.model.assignments = ['allow_all'];
|
||||
}
|
||||
// if TTL components are toggled off, set to default lease duration
|
||||
const { idTokenTtl, accessTokenTtl } = this.args.model;
|
||||
// value returned from API is a number, and string when from form action
|
||||
if (Number(idTokenTtl) === 0) this.args.model.idTokenTtl = '24h';
|
||||
if (Number(accessTokenTtl) === 0) this.args.model.accessTokenTtl = '24h';
|
||||
const { isNew, name } = this.args.model;
|
||||
yield this.args.model.save();
|
||||
this.flashMessages.success(`Successfully ${isNew ? 'created' : 'updated'} the application ${name}.`);
|
||||
this.args.onSave();
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.errorBanner = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
/**
|
||||
* @module OidcKeyForm
|
||||
* OidcKeyForm components are used to create and update OIDC providers
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <OidcKeyForm @model={{this.model}} />
|
||||
* ```
|
||||
* @callback onCancel
|
||||
* @callback onSave
|
||||
* @param {Object} model - oidc client model
|
||||
* @param {onCancel} onCancel - callback triggered when cancel button is clicked
|
||||
* @param {onSave} onSave - callback triggered on save success
|
||||
*/
|
||||
|
||||
export default class OidcKeyForm extends Component {
|
||||
@service store;
|
||||
@service flashMessages;
|
||||
@tracked errorBanner;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked modelValidations;
|
||||
@tracked radioCardGroupValue =
|
||||
// If "*" is provided, all clients are allowed: https://www.vaultproject.io/api-docs/secret/identity/oidc-provider#parameters
|
||||
!this.args.model.allowedClientIds || this.args.model.allowedClientIds.includes('*')
|
||||
? 'allow_all'
|
||||
: 'limited';
|
||||
|
||||
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] };
|
||||
}
|
||||
|
||||
@action
|
||||
handleClientSelection(selection) {
|
||||
// if array then coming from search-select component, set selection as model clients
|
||||
if (Array.isArray(selection)) {
|
||||
this.args.model.allowedClientIds = selection.map((client) => client.clientId);
|
||||
} 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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
|
||||
this.args.model[method]();
|
||||
this.args.onCancel();
|
||||
}
|
||||
|
||||
@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 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.args.onSave();
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.errorBanner = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import parseURL from 'core/utils/parse-url';
|
||||
/**
|
||||
* @module OidcProviderForm
|
||||
* OidcProviderForm components are used to create and update OIDC providers
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <OidcProviderForm @model={{this.model}} />
|
||||
* ```
|
||||
* @callback onCancel
|
||||
* @callback onSave
|
||||
* @param {Object} model - oidc client model
|
||||
* @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 flashMessages;
|
||||
@tracked modelValidations;
|
||||
@tracked errorBanner;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked radioCardGroupValue =
|
||||
// If "*" is provided, all clients are allowed: https://www.vaultproject.io/api-docs/secret/identity/oidc-provider#parameters
|
||||
!this.args.model.allowedClientIds || this.args.model.allowedClientIds.includes('*')
|
||||
? 'allow_all'
|
||||
: 'limited';
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
const { model } = this.args;
|
||||
model.issuer = model.isNew ? '' : parseURL(model.issuer).origin;
|
||||
}
|
||||
|
||||
@action
|
||||
handleClientSelection(selection) {
|
||||
// if array then coming from search-select component, set selection as model clients
|
||||
if (Array.isArray(selection)) {
|
||||
this.args.model.allowedClientIds = selection.map((client) => client.clientId);
|
||||
} 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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
|
||||
this.args.model[method]();
|
||||
this.args.onCancel();
|
||||
}
|
||||
|
||||
@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 = ['*'];
|
||||
}
|
||||
yield this.args.model.save();
|
||||
this.flashMessages.success(
|
||||
`Successfully ${isNew ? 'created' : 'updated'} the OIDC provider
|
||||
${name}.`
|
||||
);
|
||||
this.args.onSave();
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.errorBanner = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* @module OidcScopeForm
|
||||
* Oidc scope form components are used to create and edit oidc scopes
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <Oidc::ScopeForm @model={{this.model}} />
|
||||
* ```
|
||||
* @callback onCancel
|
||||
* @callback onSave
|
||||
* @param {Object} model - oidc scope model
|
||||
* @param {onCancel} onCancel - callback triggered when cancel button is clicked
|
||||
* @param {onSave} onSave - callback triggered on save success
|
||||
*/
|
||||
|
||||
export default class OidcScopeFormComponent extends Component {
|
||||
@service flashMessages;
|
||||
@tracked errorBanner;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked modelValidations;
|
||||
// formatting here is purposeful so that whitespace renders correctly in JsonEditor
|
||||
exampleTemplate = `{
|
||||
"username": {{identity.entity.aliases.$MOUNT_ACCESSOR.name}},
|
||||
"contact": {
|
||||
"email": {{identity.entity.metadata.email}},
|
||||
"phone_number": {{identity.entity.metadata.phone_number}}
|
||||
},
|
||||
"groups": {{identity.entity.groups.names}}
|
||||
}`;
|
||||
|
||||
@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;
|
||||
yield this.args.model.save();
|
||||
this.flashMessages.success(`Successfully ${isNew ? 'created' : 'updated'} the scope ${name}.`);
|
||||
this.args.onSave();
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.errorBanner = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
@action
|
||||
cancel() {
|
||||
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
|
||||
this.args.model[method]();
|
||||
this.args.onCancel();
|
||||
}
|
||||
}
|
|
@ -16,9 +16,9 @@
|
|||
<p class="sub-text">
|
||||
{{@attr.options.subText}}
|
||||
{{#if @attr.options.docLink}}
|
||||
<a href={{@attr.options.docLink}} target="_blank" rel="noopener noreferrer">
|
||||
<DocLink @path={{@attr.options.docLink}}>
|
||||
See our documentation
|
||||
</a>
|
||||
</DocLink>
|
||||
for help.
|
||||
{{/if}}
|
||||
</p>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class OidcConfigureController extends Controller {
|
||||
@service router;
|
||||
|
||||
@tracked header = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.router.on('routeDidChange', (transition) => this.setHeader(transition));
|
||||
}
|
||||
|
||||
setHeader(transition) {
|
||||
// set correct header state based on child route
|
||||
// when no clients have been created, display create button as call to action
|
||||
// list views share the same header with tabs as resource links
|
||||
// the remaining routes are responsible for their own header
|
||||
const routeName = transition.to.name;
|
||||
if (routeName.includes('oidc.index')) {
|
||||
this.header = 'cta';
|
||||
} else {
|
||||
const isList = ['clients', 'assignments', 'keys', 'scopes', 'providers'].find((resource) => {
|
||||
return routeName.includes(`${resource}.index`);
|
||||
});
|
||||
this.header = isList ? 'list' : null;
|
||||
}
|
||||
}
|
||||
|
||||
get isCta() {
|
||||
return this.header === 'cta';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OidcAssignmentDetailsController extends Controller {
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
|
||||
@action
|
||||
async delete() {
|
||||
try {
|
||||
await this.model.destroyRecord();
|
||||
this.flashMessages.success('Assignment deleted successfully');
|
||||
this.router.transitionTo('vault.cluster.access.oidc.assignments');
|
||||
} catch (error) {
|
||||
this.model.rollbackAttributes();
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class OidcClientController extends Controller {
|
||||
@service router;
|
||||
@tracked isEditRoute;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.router.on(
|
||||
'routeDidChange',
|
||||
({ targetName }) => (this.isEditRoute = targetName.includes('edit') ? true : false)
|
||||
);
|
||||
}
|
||||
|
||||
get showHeader() {
|
||||
// hide header when rendering the edit form
|
||||
return !this.isEditRoute;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OidcClientDetailsController extends Controller {
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
|
||||
@action
|
||||
async delete() {
|
||||
try {
|
||||
await this.model.destroyRecord();
|
||||
this.flashMessages.success('Application deleted successfully');
|
||||
this.router.transitionTo('vault.cluster.access.oidc.clients');
|
||||
} catch (error) {
|
||||
this.model.rollbackAttributes();
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class OidcKeyController extends Controller {
|
||||
@service router;
|
||||
@tracked isEditRoute;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.router.on('routeDidChange', ({ targetName }) => {
|
||||
return (this.isEditRoute = targetName.includes('edit') ? true : false);
|
||||
});
|
||||
}
|
||||
|
||||
get showHeader() {
|
||||
// hide header when rendering the edit form
|
||||
return !this.isEditRoute;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
|
||||
export default class OidcKeyDetailsController extends Controller {
|
||||
@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);
|
||||
});
|
||||
}
|
||||
@action
|
||||
async delete() {
|
||||
try {
|
||||
await this.model.destroyRecord();
|
||||
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;
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class OidcProviderController extends Controller {
|
||||
@service router;
|
||||
@tracked isEditRoute;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.router.on('routeDidChange', ({ targetName }) => {
|
||||
return (this.isEditRoute = targetName.includes('edit') ? true : false);
|
||||
});
|
||||
}
|
||||
|
||||
get showHeader() {
|
||||
// hide header when rendering the edit form
|
||||
return !this.isEditRoute;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OidcProviderDetailsController extends Controller {
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
|
||||
@action
|
||||
async delete() {
|
||||
try {
|
||||
await this.model.destroyRecord();
|
||||
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;
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OidcScopeDetailsController extends Controller {
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
|
||||
@action
|
||||
async delete() {
|
||||
try {
|
||||
await this.model.destroyRecord();
|
||||
this.flashMessages.success('Scope deleted successfully');
|
||||
this.router.transitionTo('vault.cluster.access.oidc.scopes');
|
||||
} catch (error) {
|
||||
this.model.rollbackAttributes();
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ import { get } from '@ember/object';
|
|||
* state represents the error state of the properties defined in the validations object
|
||||
* const { isValid, errors } = state[propertyKeyName];
|
||||
* isValid represents the validity of the property
|
||||
* errors will be populated with messages defined in the validations object when validations fail
|
||||
* errors will be populated with messages defined in the validations object when validations fail. message must be a complete sentence (and include punctuation)
|
||||
* since a property can have multiple validations, errors is always returned as an array
|
||||
*
|
||||
*** basic example
|
||||
|
@ -30,7 +30,8 @@ import { get } from '@ember/object';
|
|||
* import Model from '@ember-data/model';
|
||||
* import withModelValidations from 'vault/decorators/model-validations';
|
||||
*
|
||||
* const validations = { foo: [{ type: 'presence', message: 'foo is a required field' }] };
|
||||
* Notes: all messages need to have a period at the end of them.
|
||||
* const validations = { foo: [{ type: 'presence', message: 'foo is a required field.' }] };
|
||||
* @withModelValidations(validations)
|
||||
* class SomeModel extends Model { foo = null; }
|
||||
*
|
||||
|
@ -42,7 +43,7 @@ import { get } from '@ember/object';
|
|||
*
|
||||
*** example using custom validator
|
||||
*
|
||||
* const validations = { foo: [{ validator: (model) => model.bar.includes('test') ? model.foo : false, message: 'foo is required if bar includes test' }] };
|
||||
* const validations = { foo: [{ validator: (model) => model.bar.includes('test') ? model.foo : false, message: 'foo is required if bar includes test.' }] };
|
||||
* @withModelValidations(validations)
|
||||
* class SomeModel extends Model { foo = false; bar = ['foo', 'baz']; }
|
||||
*
|
||||
|
@ -50,7 +51,11 @@ import { get } from '@ember/object';
|
|||
* const { isValid, state } = model.validate();
|
||||
* -> isValid = false;
|
||||
* -> state.foo.isValid = false;
|
||||
* -> state.foo.errors = ['foo is required if bar includes test'];
|
||||
* -> state.foo.errors = ['foo is required if bar includes test.'];
|
||||
*
|
||||
* *** example adding class in hbs file
|
||||
* all form-validations need to have a red border around them. Add this by adding a conditional class 'has-error-border'
|
||||
* class="input field {{if this.errors.name.errors 'has-error-border'}}"
|
||||
*/
|
||||
|
||||
export function withModelValidations(validations) {
|
||||
|
|
|
@ -54,7 +54,7 @@ export default Model.extend({
|
|||
defaultSubText:
|
||||
'Unless a custom policy is specified, Vault will use a default: 20 characters with at least 1 uppercase, 1 lowercase, 1 number, and 1 dash character.',
|
||||
defaultShown: 'Default',
|
||||
docLink: 'https://www.vaultproject.io/docs/concepts/password-policies',
|
||||
docLink: '/docs/concepts/password-policies',
|
||||
}),
|
||||
|
||||
// common fields
|
||||
|
@ -106,7 +106,7 @@ export default Model.extend({
|
|||
subText: 'Enter the custom username template to use.',
|
||||
defaultSubText:
|
||||
'Template describing how dynamic usernames are generated. Vault will use the default for this plugin.',
|
||||
docLink: 'https://www.vaultproject.io/docs/concepts/username-templating',
|
||||
docLink: '/docs/concepts/username-templating',
|
||||
defaultShown: 'Default',
|
||||
}),
|
||||
max_open_connections: attr('number', {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
import { isPresent } from '@ember/utils';
|
||||
|
||||
const validations = {
|
||||
name: [
|
||||
{ type: 'presence', message: 'Name is required.' },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message: 'Name cannot contain whitespace.',
|
||||
},
|
||||
],
|
||||
targets: [
|
||||
{
|
||||
validator(model) {
|
||||
return isPresent(model.entityIds) || isPresent(model.groupIds);
|
||||
},
|
||||
message: 'At least one entity or group is required.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@withModelValidations(validations)
|
||||
export default class OidcAssignmentModel extends Model {
|
||||
@attr('string') name;
|
||||
@attr('array') entityIds;
|
||||
@attr('array') groupIds;
|
||||
|
||||
// CAPABILITIES
|
||||
@lazyCapabilities(apiPath`identity/oidc/assignment/${'name'}`, 'name') assignmentPath;
|
||||
@lazyCapabilities(apiPath`identity/oidc/assignment`) assignmentsPath;
|
||||
|
||||
get canCreate() {
|
||||
return this.assignmentPath.get('canCreate');
|
||||
}
|
||||
get canRead() {
|
||||
return this.assignmentPath.get('canRead');
|
||||
}
|
||||
get canEdit() {
|
||||
return this.assignmentPath.get('canUpdate');
|
||||
}
|
||||
get canDelete() {
|
||||
return this.assignmentPath.get('canDelete');
|
||||
}
|
||||
get canList() {
|
||||
return this.assignmentsPath.get('canList');
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`identity/entity`) entitiesPath;
|
||||
get canListEntities() {
|
||||
return this.entitiesPath.get('canList');
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`identity/group`) groupsPath;
|
||||
get canListGroups() {
|
||||
return this.groupsPath.get('canList');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import fieldToAttrs from 'vault/utils/field-to-attrs';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
|
||||
const validations = {
|
||||
name: [
|
||||
{ type: 'presence', message: 'Name is required.' },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message: 'Name cannot contain whitespace.',
|
||||
},
|
||||
],
|
||||
key: [{ type: 'presence', message: 'Key is required.' }],
|
||||
};
|
||||
|
||||
@withModelValidations(validations)
|
||||
export default class OidcClientModel extends Model {
|
||||
@attr('string', { label: 'Application name', editDisabled: true }) name;
|
||||
@attr('string', {
|
||||
label: 'Type',
|
||||
subText:
|
||||
'Specify whether the application type is confidential or public. The public type must use PKCE. This cannot be edited later.',
|
||||
editType: 'radio',
|
||||
editDisabled: true,
|
||||
defaultValue: 'confidential',
|
||||
possibleValues: ['confidential', 'public'],
|
||||
})
|
||||
clientType;
|
||||
|
||||
@attr('array', {
|
||||
label: 'Redirect URIs',
|
||||
subText:
|
||||
'One of these values must exactly match the redirect_uri parameter value used in each authentication request.',
|
||||
editType: 'stringArray',
|
||||
})
|
||||
redirectUris;
|
||||
|
||||
// >> MORE OPTIONS TOGGLE <<
|
||||
|
||||
@attr('string', {
|
||||
label: 'Signing key',
|
||||
subText: 'Add a key to sign and verify the JSON web tokens (JWT). This cannot be edited later.',
|
||||
editType: 'searchSelect',
|
||||
editDisabled: true,
|
||||
onlyAllowExisting: true,
|
||||
defaultValue() {
|
||||
return ['default'];
|
||||
},
|
||||
fallbackComponent: 'input-search',
|
||||
selectLimit: 1,
|
||||
models: ['oidc/key'],
|
||||
})
|
||||
key;
|
||||
@attr({
|
||||
label: 'Access Token TTL',
|
||||
editType: 'ttl',
|
||||
defaultValue: '24h',
|
||||
})
|
||||
accessTokenTtl;
|
||||
|
||||
@attr({
|
||||
label: 'ID Token TTL',
|
||||
editType: 'ttl',
|
||||
defaultValue: '24h',
|
||||
})
|
||||
idTokenTtl;
|
||||
|
||||
// >> END MORE OPTIONS TOGGLE <<
|
||||
|
||||
@attr('array', { label: 'Assign access' }) assignments; // no editType because does not use form-field component
|
||||
@attr('string', { label: 'Client ID' }) clientId;
|
||||
@attr('string') clientSecret;
|
||||
|
||||
// CAPABILITIES //
|
||||
@lazyCapabilities(apiPath`identity/oidc/client/${'name'}`, 'name') clientPath;
|
||||
@lazyCapabilities(apiPath`identity/oidc/client`) clientsPath;
|
||||
get canCreate() {
|
||||
return this.clientPath.get('canCreate');
|
||||
}
|
||||
get canRead() {
|
||||
return this.clientPath.get('canRead');
|
||||
}
|
||||
get canEdit() {
|
||||
return this.clientPath.get('canUpdate');
|
||||
}
|
||||
get canDelete() {
|
||||
return this.clientPath.get('canDelete');
|
||||
}
|
||||
get canList() {
|
||||
return this.clientsPath.get('canList');
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`identity/oidc/key`) keysPath;
|
||||
get canListKeys() {
|
||||
return this.keysPath.get('canList');
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`identity/oidc/assignment/${'name'}`, 'name') assignmentPath;
|
||||
@lazyCapabilities(apiPath`identity/oidc/assignment`) assignmentsPath;
|
||||
get canCreateAssignments() {
|
||||
return this.assignmentPath.get('canCreate');
|
||||
}
|
||||
get canListAssignments() {
|
||||
return this.assignmentsPath.get('canList');
|
||||
}
|
||||
|
||||
// API WIP
|
||||
@lazyCapabilities(apiPath`identity/oidc/${'name'}/provider`, 'backend', 'name') clientProvidersPath;
|
||||
get canListProviders() {
|
||||
return this.clientProvidersPath.get('canList');
|
||||
}
|
||||
|
||||
// TODO refactor when field-to-attrs util is refactored as decorator
|
||||
_attributeMeta = null; // cache initial result of expandAttributeMeta in getter and return
|
||||
get formFields() {
|
||||
if (!this._attributeMeta) {
|
||||
this._attributeMeta = expandAttributeMeta(this, ['name', 'clientType', 'redirectUris']);
|
||||
}
|
||||
return this._attributeMeta;
|
||||
}
|
||||
|
||||
_fieldToAttrsGroups = null;
|
||||
// more options fields
|
||||
get fieldGroups() {
|
||||
if (!this._fieldToAttrsGroups) {
|
||||
this._fieldToAttrsGroups = fieldToAttrs(this, [
|
||||
{ 'More options': ['key', 'idTokenTtl', 'accessTokenTtl'] },
|
||||
]);
|
||||
}
|
||||
return this._fieldToAttrsGroups;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
|
||||
const validations = {
|
||||
name: [
|
||||
{ type: 'presence', message: 'Name is required.' },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message: 'Name cannot contain whitespace.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@withModelValidations(validations)
|
||||
export default class OidcKeyModel extends Model {
|
||||
@attr('string', { editDisabled: true }) name;
|
||||
@attr('string', {
|
||||
defaultValue: 'RS256',
|
||||
possibleValues: ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA'],
|
||||
})
|
||||
algorithm;
|
||||
|
||||
@attr({ editType: 'ttl', defaultValue: '24h' }) rotationPeriod;
|
||||
@attr({ label: 'Verification TTL', editType: 'ttl', defaultValue: '24h' }) verificationTtl;
|
||||
@attr('array', { label: 'Allowed applications' }) allowedClientIds; // no editType because does not use form-field component
|
||||
|
||||
// TODO refactor when field-to-attrs is refactored as decorator
|
||||
_attributeMeta = null; // cache initial result of expandAttributeMeta in getter and return
|
||||
get formFields() {
|
||||
if (!this._attributeMeta) {
|
||||
this._attributeMeta = expandAttributeMeta(this, [
|
||||
'name',
|
||||
'algorithm',
|
||||
'rotationPeriod',
|
||||
'verificationTtl',
|
||||
]);
|
||||
}
|
||||
return this._attributeMeta;
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`identity/oidc/key/${'name'}`, 'name') keyPath;
|
||||
@lazyCapabilities(apiPath`identity/oidc/key/${'name'}/rotate`, 'name') rotatePath;
|
||||
@lazyCapabilities(apiPath`identity/oidc/key`) keysPath;
|
||||
get canCreate() {
|
||||
return this.keyPath.get('canCreate');
|
||||
}
|
||||
get canRead() {
|
||||
return this.keyPath.get('canRead');
|
||||
}
|
||||
get canEdit() {
|
||||
return this.keyPath.get('canUpdate');
|
||||
}
|
||||
get canRotate() {
|
||||
return this.rotatePath.get('canUpdate');
|
||||
}
|
||||
get canDelete() {
|
||||
return this.keyPath.get('canDelete');
|
||||
}
|
||||
get canList() {
|
||||
return this.keysPath.get('canList');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
|
||||
const validations = {
|
||||
name: [
|
||||
{ type: 'presence', message: 'Name is required.' },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message: 'Name cannot contain whitespace.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@withModelValidations(validations)
|
||||
export default class OidcProviderModel extends Model {
|
||||
@attr('string', { editDisabled: true }) name;
|
||||
@attr('string', {
|
||||
subText:
|
||||
'The scheme, host, and optional port for your issuer. This will be used to build the URL that validates ID tokens.',
|
||||
placeholderText: 'e.g. https://example.com:8200',
|
||||
docLink: '/api-docs/secret/identity/oidc-provider#create-or-update-a-provider',
|
||||
helpText: `Optional. This defaults to a URL with Vault's api_addr`,
|
||||
})
|
||||
issuer;
|
||||
|
||||
@attr('array', {
|
||||
label: 'Supported scopes',
|
||||
subText: 'Scopes define information about a user and the OIDC service. Optional.',
|
||||
editType: 'searchSelect',
|
||||
models: ['oidc/scope'],
|
||||
fallbackComponent: 'string-list',
|
||||
onlyAllowExisting: true,
|
||||
})
|
||||
scopesSupported;
|
||||
|
||||
@attr('array', { label: 'Allowed applications' }) allowedClientIds; // no editType because does not use form-field component
|
||||
|
||||
// TODO refactor when field-to-attrs is refactored as decorator
|
||||
_attributeMeta = null; // cache initial result of expandAttributeMeta in getter and return
|
||||
get formFields() {
|
||||
if (!this._attributeMeta) {
|
||||
this._attributeMeta = expandAttributeMeta(this, ['name', 'issuer', 'scopesSupported']);
|
||||
}
|
||||
return this._attributeMeta;
|
||||
}
|
||||
@lazyCapabilities(apiPath`identity/oidc/provider/${'name'}`, 'name') providerPath;
|
||||
@lazyCapabilities(apiPath`identity/oidc/provider`) providersPath;
|
||||
get canCreate() {
|
||||
return this.providerPath.get('canCreate');
|
||||
}
|
||||
get canRead() {
|
||||
return this.providerPath.get('canRead');
|
||||
}
|
||||
get canEdit() {
|
||||
return this.providerPath.get('canUpdate');
|
||||
}
|
||||
get canDelete() {
|
||||
return this.providerPath.get('canDelete');
|
||||
}
|
||||
get canList() {
|
||||
return this.providersPath.get('canList');
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`identity/oidc/client`) clientsPath;
|
||||
get canListClients() {
|
||||
return this.clientsPath.get('canList');
|
||||
}
|
||||
@lazyCapabilities(apiPath`identity/oidc/scope`) scopesPath;
|
||||
get canListScopes() {
|
||||
return this.scopesPath.get('canList');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
|
||||
const validations = {
|
||||
name: [{ type: 'presence', message: 'Name is required.' }],
|
||||
};
|
||||
|
||||
@withModelValidations(validations)
|
||||
export default class OidcScopeModel extends Model {
|
||||
@attr('string', { editDisabled: true }) name;
|
||||
@attr('string', { editType: 'textarea' }) description;
|
||||
@attr('string', { label: 'JSON Template', editType: 'json', mode: 'ruby' }) template;
|
||||
|
||||
// TODO refactor when field-to-attrs is refactored as decorator
|
||||
_attributeMeta = null; // cache initial result of expandAttributeMeta in getter and return
|
||||
get formFields() {
|
||||
if (!this._attributeMeta) {
|
||||
this._attributeMeta = expandAttributeMeta(this, ['name', 'description', 'template']);
|
||||
}
|
||||
return this._attributeMeta;
|
||||
}
|
||||
|
||||
@lazyCapabilities(apiPath`identity/oidc/scope/${'name'}`, 'name') scopePath;
|
||||
@lazyCapabilities(apiPath`identity/oidc/scope`) scopesPath;
|
||||
get canCreate() {
|
||||
return this.scopePath.get('canCreate');
|
||||
}
|
||||
get canRead() {
|
||||
return this.scopePath.get('canRead');
|
||||
}
|
||||
get canEdit() {
|
||||
return this.scopePath.get('canUpdate');
|
||||
}
|
||||
get canDelete() {
|
||||
return this.scopePath.get('canDelete');
|
||||
}
|
||||
get canList() {
|
||||
return this.scopesPath.get('canList');
|
||||
}
|
||||
}
|
|
@ -109,6 +109,46 @@ Router.map(function () {
|
|||
this.route('index', { path: '/' });
|
||||
this.route('create');
|
||||
});
|
||||
this.route('oidc', function () {
|
||||
this.route('clients', function () {
|
||||
this.route('create');
|
||||
this.route('client', { path: '/:name' }, function () {
|
||||
this.route('details');
|
||||
this.route('providers');
|
||||
this.route('edit');
|
||||
});
|
||||
});
|
||||
this.route('keys', function () {
|
||||
this.route('create');
|
||||
this.route('key', { path: '/:name' }, function () {
|
||||
this.route('details');
|
||||
this.route('clients');
|
||||
this.route('edit');
|
||||
});
|
||||
});
|
||||
this.route('assignments', function () {
|
||||
this.route('create');
|
||||
this.route('assignment', { path: '/:name' }, function () {
|
||||
this.route('details');
|
||||
this.route('edit');
|
||||
});
|
||||
});
|
||||
this.route('providers', function () {
|
||||
this.route('create');
|
||||
this.route('provider', { path: '/:name' }, function () {
|
||||
this.route('details');
|
||||
this.route('clients');
|
||||
this.route('edit');
|
||||
});
|
||||
});
|
||||
this.route('scopes', function () {
|
||||
this.route('create');
|
||||
this.route('scope', { path: '/:name' }, function () {
|
||||
this.route('details');
|
||||
this.route('edit');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('secrets', function () {
|
||||
this.route('backends', { path: '/' });
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcConfigureRoute extends Route {}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcAssignmentRoute extends Route {
|
||||
model({ name }) {
|
||||
return this.store.findRecord('oidc/assignment', name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcAssignmentDetailsRoute extends Route {}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcAssignmentEditRoute extends Route {}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcAssignmentsCreateRoute extends Route {
|
||||
model() {
|
||||
return this.store.createRecord('oidc/assignment');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcAssignmentsRoute extends Route {
|
||||
model() {
|
||||
return this.store.query('oidc/assignment', {}).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcClientRoute extends Route {
|
||||
model({ name }) {
|
||||
return this.store.findRecord('oidc/client', name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import Route from '@ember/routing/route';
|
||||
export default class OidcClientDetailsRoute extends Route {}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcClientEditRoute extends Route {}
|
|
@ -0,0 +1,18 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcClientProvidersRoute extends Route {
|
||||
model() {
|
||||
const model = this.modelFor('vault.cluster.access.oidc.clients.client');
|
||||
return this.store
|
||||
.query('oidc/provider', {
|
||||
allowed_client_id: model.clientId,
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcClientsCreateRoute extends Route {
|
||||
model() {
|
||||
return this.store.createRecord('oidc/client');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
export default class OidcClientsRoute extends Route {
|
||||
@service router;
|
||||
|
||||
model() {
|
||||
return this.store.query('oidc/client', {}).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
afterModel(model) {
|
||||
if (model.length === 0) {
|
||||
this.router.transitionTo('vault.cluster.access.oidc');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OidcConfigureRoute extends Route {
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
return this.store
|
||||
.query('oidc/client', {})
|
||||
.then(() => {
|
||||
// transition to client list view if clients have been created
|
||||
this.router.transitionTo('vault.cluster.access.oidc.clients');
|
||||
})
|
||||
.catch(() => {
|
||||
// adapter throws error for 404 - swallow and remain on index route to show call to action
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcKeysCreateRoute extends Route {
|
||||
model() {
|
||||
return this.store.createRecord('oidc/key');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import Route from '@ember/routing/route';
|
||||
export default class OidcKeysRoute extends Route {
|
||||
model() {
|
||||
return this.store.query('oidc/key', {}).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcKeyRoute extends Route {
|
||||
model({ name }) {
|
||||
return this.store.findRecord('oidc/key', name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcKeyClientsRoute extends Route {
|
||||
async model() {
|
||||
const { allowedClientIds } = this.modelFor('vault.cluster.access.oidc.keys.key');
|
||||
return await this.store.query('oidc/client', { paramKey: 'client_id', filterFor: allowedClientIds });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcKeyDetailsRoute extends Route {}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcKeyEditRoute extends Route {}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcProvidersCreateRoute extends Route {
|
||||
model() {
|
||||
return this.store.createRecord('oidc/provider');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcProvidersRoute extends Route {
|
||||
model() {
|
||||
return this.store.query('oidc/provider', {}).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcProviderRoute extends Route {
|
||||
model({ name }) {
|
||||
return this.store.findRecord('oidc/provider', name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcProviderClientsRoute extends Route {
|
||||
async model() {
|
||||
const { allowedClientIds } = this.modelFor('vault.cluster.access.oidc.providers.provider');
|
||||
return await this.store.query('oidc/client', { paramKey: 'client_id', filterFor: allowedClientIds });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcProviderDetailsRoute extends Route {}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcProviderEditRoute extends Route {}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcScopesCreateRoute extends Route {
|
||||
model() {
|
||||
return this.store.createRecord('oidc/scope');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcScopesRoute extends Route {
|
||||
model() {
|
||||
return this.store.query('oidc/scope', {}).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcScopeRoute extends Route {
|
||||
model({ name }) {
|
||||
return this.store.findRecord('oidc/scope', name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcScopeDetailsRoute extends Route {}
|
|
@ -0,0 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class OidcScopeEditRoute extends Route {}
|
|
@ -0,0 +1,5 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class OidcAssignmentSerializer extends ApplicationSerializer {
|
||||
primaryKey = 'name';
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class OidcClientSerializer extends ApplicationSerializer {
|
||||
primaryKey = 'name';
|
||||
|
||||
// rehydrate each client model so all model attributes are accessible from the LIST response
|
||||
normalizeItems(payload) {
|
||||
if (payload.data) {
|
||||
if (payload.data?.keys && Array.isArray(payload.data.keys)) {
|
||||
return payload.data.keys.map((key) => ({ name: key, ...payload.data.key_info[key] }));
|
||||
}
|
||||
Object.assign(payload, payload.data);
|
||||
delete payload.data;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class OidcKeySerializer extends ApplicationSerializer {
|
||||
primaryKey = 'name';
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class OidcProviderSerializer extends ApplicationSerializer {
|
||||
primaryKey = 'name';
|
||||
|
||||
// need to normalize to get issuer metadata for provider's list view
|
||||
normalizeItems(payload) {
|
||||
if (payload.data) {
|
||||
if (payload.data?.keys && Array.isArray(payload.data.keys)) {
|
||||
return payload.data.keys.map((key) => ({ name: key, ...payload.data.key_info[key] }));
|
||||
}
|
||||
Object.assign(payload, payload.data);
|
||||
delete payload.data;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class OidcScopeSerializer extends ApplicationSerializer {
|
||||
primaryKey = 'name';
|
||||
}
|
|
@ -5,6 +5,7 @@ const API_PATHS = {
|
|||
access: {
|
||||
methods: 'sys/auth',
|
||||
mfa: 'identity/mfa/method',
|
||||
oidc: 'identity/oidc/client',
|
||||
entities: 'identity/entity/id',
|
||||
groups: 'identity/group/id',
|
||||
leases: 'sys/leases/lookup',
|
||||
|
@ -44,6 +45,7 @@ const API_PATHS_TO_ROUTE_PARAMS = {
|
|||
'sys/namespaces': { route: 'vault.cluster.access.namespaces', models: [] },
|
||||
'sys/control-group/': { route: 'vault.cluster.access.control-groups', models: [] },
|
||||
'identity/mfa/method': { route: 'vault.cluster.access.mfa', models: [] },
|
||||
'identity/oidc/client': { route: 'vault.cluster.access.oidc', models: [] },
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
@ -22,6 +22,10 @@
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
a.list-item-row,
|
||||
|
|
|
@ -3,16 +3,15 @@
|
|||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
.radio-card {
|
||||
width: 19rem;
|
||||
box-shadow: $box-shadow-low;
|
||||
display: flex;
|
||||
flex: 1 1 25%;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin: $spacing-xs $spacing-m;
|
||||
border: $base-border;
|
||||
border-radius: $radius;
|
||||
transition: all ease-in-out $speed;
|
||||
|
||||
max-width: 60%;
|
||||
input[type='radio'] {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
|
|
@ -272,3 +272,13 @@ a.button.disabled {
|
|||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.text-button {
|
||||
padding: unset;
|
||||
border: none;
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
cursor: pointer;
|
||||
color: $link;
|
||||
}
|
||||
|
|
|
@ -331,6 +331,11 @@ select.has-error-border {
|
|||
border: 1px solid $red-500;
|
||||
}
|
||||
|
||||
.dropdown-has-error-border > div.ember-basic-dropdown-trigger {
|
||||
border: 1px solid $red-500;
|
||||
}
|
||||
|
||||
|
||||
.autocomplete-input {
|
||||
background: $white !important;
|
||||
border: 1px solid $grey-light;
|
||||
|
|
|
@ -174,6 +174,9 @@
|
|||
.has-top-padding-l {
|
||||
padding-top: $spacing-l;
|
||||
}
|
||||
.has-top-padding-xxl {
|
||||
padding-top: $spacing-xxl;
|
||||
}
|
||||
.has-bottom-margin-xs {
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
|
|
@ -116,7 +116,6 @@
|
|||
@value={{get this.model attr.name}}
|
||||
@type={{attr.type}}
|
||||
@isLink={{eq attr.name "transformations"}}
|
||||
@viewAll="transformations"
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<p class="sub-text">
|
||||
{{attr.options.helpText}}
|
||||
{{#if attr.options.docLink}}
|
||||
<a href={{attr.options.docLink}} target="_blank" rel="noopener noreferrer">
|
||||
<DocLink @path={{attr.options.docLink}}>
|
||||
See our documentation
|
||||
</a>
|
||||
</DocLink>
|
||||
for help.
|
||||
{{/if}}
|
||||
</p>
|
||||
|
@ -43,9 +43,9 @@
|
|||
<p class="sub-text">
|
||||
{{attr.options.subText}}
|
||||
{{#if attr.options.docLink}}
|
||||
<a href={{attr.options.docLink}} target="_blank" rel="noopener noreferrer">
|
||||
<DocLink @path={{attr.options.docLink}}>
|
||||
See our documentation
|
||||
</a>
|
||||
</DocLink>
|
||||
for help.
|
||||
{{/if}}
|
||||
</p>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
spellcheck="false"
|
||||
value={{@model.name}}
|
||||
disabled={{not @model.isNew}}
|
||||
class="input field"
|
||||
class="input field {{if this.errors.name.errors 'has-error-border'}}"
|
||||
data-test-mlef-input="name"
|
||||
{{on "input" (pipe (pick "target.value") (fn (mut @model.name)))}}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
{{#unless @isInline}}
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
{{#if @model.isNew}}
|
||||
<LinkTo @route={{"vault.cluster.access.oidc.assignments"}}>
|
||||
Assignments
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{! You're editing in this view }}
|
||||
<LinkTo @route={{"vault.cluster.access.oidc.assignments.assignment.details"}} @model={{@model.name}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-oidc-assignment-title>
|
||||
{{if @model.isNew "Create" "Edit"}}
|
||||
assignment
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
{{/unless}}
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
<FormFieldLabel for="name" @label="Name" />
|
||||
<input
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
value={{@model.name}}
|
||||
disabled={{not @model.isNew}}
|
||||
class="input field {{if this.modelValidations.name.errors 'has-error-border'}}"
|
||||
{{on "input" this.handleOperation}}
|
||||
data-test-input="name"
|
||||
/>
|
||||
{{#if this.modelValidations.name.errors}}
|
||||
<AlertInline @type="danger" @message={{join ", " this.modelValidations.name.errors}} />
|
||||
{{/if}}
|
||||
<SearchSelect
|
||||
@id="entities"
|
||||
@label="Entities"
|
||||
@labelClass="is-label"
|
||||
@placeholder="Search"
|
||||
@models={{array "identity/entity"}}
|
||||
@inputValue={{@model.entityIds}}
|
||||
@shouldRenderName={{true}}
|
||||
@onChange={{this.onEntitiesSelect}}
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="string-list"
|
||||
data-test-search-select="entities"
|
||||
/>
|
||||
<SearchSelect
|
||||
@id="groups"
|
||||
@label="Groups"
|
||||
@labelClass="is-label"
|
||||
@placeholder="Search"
|
||||
@models={{array "identity/group"}}
|
||||
@inputValue={{@model.groupIds}}
|
||||
@shouldRenderName={{true}}
|
||||
@onChange={{this.onGroupsSelect}}
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="string-list"
|
||||
data-test-search-select="groups"
|
||||
/>
|
||||
</div>
|
||||
<div class="has-top-padding-s">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-oidc-assignment-save
|
||||
>
|
||||
{{if @model.isNew "Create" "Update"}}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-oidc-assignment-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{{#if this.modelValidations.targets.errors}}
|
||||
<AlertInline @type="danger" @message={{join ", " this.modelValidations.targets.errors}} @paddingTop={{true}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,99 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
{{#if @model.isNew}}
|
||||
<LinkTo @route="vault.cluster.access.oidc.clients">
|
||||
Applications
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo @route="vault.cluster.access.oidc.clients.client.details" @model={{@model.name}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-oidc-client-title>
|
||||
{{if @model.isNew "Create" "Edit"}}
|
||||
application
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-bottomless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
{{#each @model.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
|
||||
{{! MORE OPTIONS TOGGLE }}
|
||||
<FormFieldGroups @renderGroup="More options" @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
</div>
|
||||
{{! RADIO CARD + SEARCH SELECT }}
|
||||
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-xxl">
|
||||
<h4 class="title is-4">Assign access</h4>
|
||||
<div class="is-flex-row">
|
||||
<RadioCard
|
||||
@title="Allow everyone to access existing"
|
||||
@description="All Vault entities can authenticate through this application."
|
||||
@icon="org"
|
||||
@value="allow_all"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleAssignmentSelection}}
|
||||
/>
|
||||
<RadioCard
|
||||
@title="Limit access to selected users"
|
||||
@description="Choose or create an assignment to give access to selected entities."
|
||||
@icon="users"
|
||||
@value="limited"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleAssignmentSelection}}
|
||||
/>
|
||||
</div>
|
||||
{{#if (eq this.radioCardGroupValue "limited")}}
|
||||
<SearchSelectWithModal
|
||||
@id="assignments"
|
||||
@label="Assignment name"
|
||||
@subText="Search for an existing assignment, or type a new name to create it."
|
||||
@model="oidc/assignment"
|
||||
@inputValue={{this.modelAssignments}}
|
||||
@onChange={{this.handleAssignmentSelection}}
|
||||
@excludeOptions={{array "allow_all"}}
|
||||
@fallbackComponent="string-list"
|
||||
@modalFormComponent="oidc/assignment-form"
|
||||
@modalSubtext="Use assignment to specify which Vault entities and groups are allowed to authenticate."
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-oidc-client-save
|
||||
>
|
||||
{{if @model.isNew "Create" "Update"}}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-oidc-client-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<div class="control">
|
||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,52 @@
|
|||
{{#each @model as |client|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row"
|
||||
@params={{array "vault.cluster.access.oidc.clients.client.details" client.name}}
|
||||
data-test-oidc-client-linked-block={{client.name}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<Icon @name="code" class="has-text-grey-light" />
|
||||
<span class="has-text-weight-semibold is-underline">
|
||||
{{client.name}}
|
||||
</span>
|
||||
<div class="has-text-grey is-size-8">
|
||||
Client ID:
|
||||
{{client.clientId}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
<PopupMenu>
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.clients.client.details"
|
||||
@model={{client.name}}
|
||||
@disabled={{eq client.canRead false}}
|
||||
data-test-oidc-client-menu-link="details"
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.clients.client.edit"
|
||||
@model={{client.name}}
|
||||
@disabled={{eq client.canEdit false}}
|
||||
data-test-oidc-client-menu-link="edit"
|
||||
>
|
||||
Edit
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{/each}}
|
|
@ -0,0 +1,100 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
{{#if @model.isNew}}
|
||||
<LinkTo @route="vault.cluster.access.oidc.keys">
|
||||
Keys
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo @route="vault.cluster.access.oidc.keys.key.details" @model={{@model.name}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-oidc-key-title>
|
||||
{{if @model.isNew "Create" "Edit"}}
|
||||
key
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-bottomless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
{{#each @model.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
{{! RADIO CARD + SEARCH SELECT }}
|
||||
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-xxl">
|
||||
<h4 class="title is-4">Allowed applications</h4>
|
||||
<div class="is-flex-row">
|
||||
<RadioCard
|
||||
@title="Allow every application to use"
|
||||
@description="All applications can use this key for authentication requests."
|
||||
@icon="globe"
|
||||
@value="allow_all"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
/>
|
||||
<RadioCard
|
||||
@title="Limit access to selected application"
|
||||
@description="Only selected applications can use this key for authentication requests."
|
||||
@icon="globe-private"
|
||||
@value="limited"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disabled={{@model.isNew}}
|
||||
@disabledTooltipMessage="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")}}
|
||||
<SearchSelect
|
||||
@id="allowedClientIds"
|
||||
@subLabel="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}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="string-list"
|
||||
@passObject={{true}}
|
||||
@objectKeys={{array "clientId"}}
|
||||
@queryObject={{this.filterDropdownOptions}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-oidc-key-save
|
||||
>
|
||||
{{if @model.isNew "Create" "Update"}}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-oidc-key-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<div class="control">
|
||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,127 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
{{#if @model.isNew}}
|
||||
<LinkTo @route="vault.cluster.access.oidc.providers">
|
||||
Providers
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo @route="vault.cluster.access.oidc.providers.provider.details" @model={{@model.name}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-oidc-provider-title>
|
||||
{{if @model.isNew "Create" "Edit"}}
|
||||
provider
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-bottomless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
{{! 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}}
|
||||
/>
|
||||
<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}}
|
||||
@labelClass="is-label"
|
||||
/>
|
||||
</div>
|
||||
{{! RADIO CARD + SEARCH SELECT }}
|
||||
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-xxl">
|
||||
<h4 class="title is-4">Allowed applications</h4>
|
||||
<div class="is-flex-row">
|
||||
<RadioCard
|
||||
@title="Allow every application to use"
|
||||
@description="All applications can use this provider for authentication requests."
|
||||
@icon="globe"
|
||||
@value="allow_all"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
/>
|
||||
<RadioCard
|
||||
@title="Limit access to selected application"
|
||||
@description="Only selected applications can use this provider for authentication requests."
|
||||
@icon="globe-private"
|
||||
@value="limited"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
/>
|
||||
</div>
|
||||
{{#if (eq this.radioCardGroupValue "limited")}}
|
||||
<SearchSelect
|
||||
@id="allowedClientIds"
|
||||
@subLabel="Application name"
|
||||
@models={{array "oidc/client"}}
|
||||
@inputValue={{@model.allowedClientIds}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="string-list"
|
||||
@passObject={{true}}
|
||||
@objectKeys={{array "clientId"}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-oidc-provider-save
|
||||
>
|
||||
{{if @model.isNew "Create" "Update"}}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-oidc-provider-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<div class="control">
|
||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,52 @@
|
|||
{{#each @model as |provider|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row"
|
||||
@params={{array "vault.cluster.access.oidc.providers.provider.details" provider.name}}
|
||||
data-test-oidc-provider-linked-block={{provider.name}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<Icon @name="provider" class="has-text-grey-light" />
|
||||
<span class="has-text-weight-semibold is-underline">
|
||||
{{provider.name}}
|
||||
</span>
|
||||
<div class="has-text-grey is-size-8">
|
||||
Issuer:
|
||||
{{provider.issuer}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
<PopupMenu>
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.providers.provider.details"
|
||||
@model={{provider.name}}
|
||||
@disabled={{not provider.canRead}}
|
||||
data-test-oidc-provider-menu-link="details"
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.providers.provider.edit"
|
||||
@model={{provider.name}}
|
||||
@disabled={{not provider.canEdit}}
|
||||
data-test-oidc-provider-menu-link="edit"
|
||||
>
|
||||
Edit
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{/each}}
|
|
@ -0,0 +1,113 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
{{#if @model.isNew}}
|
||||
<LinkTo @route={{"vault.cluster.access.oidc.scopes"}}>
|
||||
Scopes
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{! You're editing in this view }}
|
||||
<LinkTo @route={{"vault.cluster.access.oidc.scopes.scope.details"}} @model={{@model.name}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-oidc-scope-title>
|
||||
{{if @model.isNew "Create" "Edit"}}
|
||||
scope
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<p class="has-bottom-margin-l">
|
||||
Providers may reference a set of scopes to make specific identity information available as claims
|
||||
</p>
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
{{#each @model.formFields as |field|}}
|
||||
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
<p class="is-size-9 has-text-grey has-bottom-margin-l">
|
||||
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field. See
|
||||
<button
|
||||
type="button"
|
||||
class="text-button"
|
||||
{{on "click" (fn (mut this.showTemplateModal))}}
|
||||
data-test-oidc-scope-example
|
||||
>
|
||||
example template
|
||||
</button>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="has-top-margin-l has-bottom-margin-l">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-oidc-scope-save
|
||||
>
|
||||
{{if @model.isNew "Create" "Update"}}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-oidc-scope-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<div class="control">
|
||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
|
||||
<Modal
|
||||
@title="Scope template"
|
||||
@onClose={{fn (mut this.showTemplateModal) false}}
|
||||
@isActive={{this.showTemplateModal}}
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section class="modal-card-body">
|
||||
<div class="is-flex-between is-flex-center has-bottom-margin-s">
|
||||
<p data-test-modal-copy>
|
||||
Example of a JSON template for scopes:
|
||||
</p>
|
||||
<CopyButton
|
||||
class="button is-transparent"
|
||||
@clipboardText={{this.exampleTemplate}}
|
||||
@buttonType="button"
|
||||
@success={{fn (set-flash-message "Example template copied!")}}
|
||||
>
|
||||
<Icon @name="clipboard-copy" aria-label="Copy" />
|
||||
</CopyButton>
|
||||
</div>
|
||||
{{! code-mirror modifier does not render value initially in wormhole until focus event fires }}
|
||||
{{! wait until the Modal is rendered and then show the JsonEditor }}
|
||||
{{#if this.showTemplateModal}}
|
||||
<JsonEditor @value={{this.exampleTemplate}} @mode="ruby" @readOnly={{true}} @showToolbar={{false}} />
|
||||
{{/if}}
|
||||
<p class="has-top-margin-m">
|
||||
The full list of template parameters can be found
|
||||
<DocLink @path="/docs/concepts/oidc-provider#scopes">
|
||||
here.
|
||||
</DocLink>
|
||||
</p>
|
||||
</section>
|
||||
<div class="modal-card-head has-border-top-light">
|
||||
<button type="button" class="button" {{on "click" (fn (mut this.showTemplateModal) false)}} data-test-close-modal>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
|
@ -3,6 +3,8 @@
|
|||
class="radio-card {{if (eq @value @groupValue) 'is-selected'}} {{if @disabled 'is-disabled'}}"
|
||||
...attributes
|
||||
>
|
||||
<ToolTip @verticalPosition="above" @horizontalPosition="center" as |T|>
|
||||
<T.Trigger tabindex="-1">
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
|
@ -25,11 +27,20 @@
|
|||
id={{dasherize @value}}
|
||||
name="config-mode"
|
||||
class="radio"
|
||||
disabled={{@disabled}}
|
||||
@disabled={{@disabled}}
|
||||
@value={{@value}}
|
||||
@groupValue={{@groupValue}}
|
||||
@onChange={{@onChange}}
|
||||
/>
|
||||
<span class="dot"></span>
|
||||
</div>
|
||||
</T.Trigger>
|
||||
{{#if (and @disabled @disabledTooltipMessage)}}
|
||||
<T.Content @defaultClass="tool-tip smaller-font">
|
||||
<div class="box">
|
||||
{{@disabledTooltipMessage}}
|
||||
</div>
|
||||
</T.Content>
|
||||
{{/if}}
|
||||
</ToolTip>
|
||||
</label>
|
|
@ -99,7 +99,6 @@
|
|||
@value={{get this.model attr.name}}
|
||||
@type={{attr.type}}
|
||||
@isLink={{eq attr.name "transformations"}}
|
||||
@viewAll="transformations"
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
@queryParam="role"
|
||||
@modelType="transform/role"
|
||||
@wildcardLabel={{attr.options.wildcardLabel}}
|
||||
@viewAll="roles"
|
||||
@backend={{this.model.backend}}
|
||||
/>
|
||||
{{else}}
|
||||
|
|
|
@ -61,6 +61,13 @@
|
|||
</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if (has-permission "access" routeParams="oidc")}}
|
||||
<li>
|
||||
<LinkTo @route="vault.cluster.access.oidc" data-test-link="oidc">
|
||||
OIDC Provider
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
</MenuSidebar>
|
||||
<div class="column is-9">
|
||||
{{outlet}}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
{{#if this.header}}
|
||||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
OIDC Provider
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<div class="box is-fullwidth is-sideless is-flex-between is-box-shadowless is-marginless" data-test-oidc-header>
|
||||
<p>
|
||||
Configure Vault to act as an OIDC identity provider, and offer
|
||||
{{"Vault’s"}}
|
||||
various authentication
|
||||
{{#if this.isCta}}
|
||||
<br />
|
||||
{{/if}}
|
||||
methods and source of identity to any client applications.
|
||||
<LearnLink @path="/tutorials/vault/oidc-identity-provider">
|
||||
Learn more
|
||||
</LearnLink>
|
||||
</p>
|
||||
{{#if this.isCta}}
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary"
|
||||
{{on "click" (transition-to "vault.cluster.access.oidc.clients.create")}}
|
||||
data-test-oidc-configure
|
||||
>
|
||||
Create your first app
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#unless this.isCta}}
|
||||
{{! show tab links in list routes }}
|
||||
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless" data-test-oidc-tabs>
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
<LinkTo @route="vault.cluster.access.oidc.clients" data-test-tab="clients">
|
||||
Applications
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.access.oidc.keys" data-test-tab="keys">
|
||||
Keys
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.access.oidc.assignments" data-test-tab="assignments">
|
||||
Assignments
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.access.oidc.providers" data-test-tab="providers">
|
||||
Providers
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.access.oidc.scopes" data-test-tab="scopes">
|
||||
Scopes
|
||||
</LinkTo>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
|
@ -0,0 +1,85 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
<LinkTo @route="vault.cluster.access.oidc.assignments">
|
||||
Assignments
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-oidc-assignment-title>
|
||||
{{@model.name}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
|
||||
<nav class="tabs" aria-label="tabs">
|
||||
<ul>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.assignments.assignment.details"
|
||||
@model={{@model}}
|
||||
data-test-oidc-assignment-details
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if @model.canDelete}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="toolbar-link"
|
||||
@onConfirmAction={{this.delete}}
|
||||
@confirmTitle="Delete assignment?"
|
||||
@confirmMessage="This assignment will be permanently deleted. You will not be able to recover it."
|
||||
@confirmButtonText="Delete"
|
||||
data-test-oidc-assignment-delete
|
||||
>
|
||||
Delete assignment
|
||||
</ConfirmAction>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if @model.canEdit}}
|
||||
<ToolbarLink
|
||||
@params={{array "vault.cluster.access.oidc.assignments.assignment.edit" @model.name}}
|
||||
data-test-oidc-assignment-edit
|
||||
>
|
||||
Edit assignment
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<InfoTableRow @label="Name" @value={{@model.name}} />
|
||||
<InfoTableRow
|
||||
@label="Entities"
|
||||
@type="array"
|
||||
@value={{@model.entityIds}}
|
||||
@model={{@model}}
|
||||
@isLink={{true}}
|
||||
@modelType="oidc/assignment"
|
||||
@itemRoute={{(array "vault.cluster.access.identity.show" "entities" "details")}}
|
||||
@alwaysRender={{true}}
|
||||
@toggleViewAll={{true}}
|
||||
/>
|
||||
<InfoTableRow
|
||||
@label="Groups"
|
||||
@type="array"
|
||||
@value={{@model.groupIds}}
|
||||
@model={{@model}}
|
||||
@isLink={{true}}
|
||||
@modelType="oidc/assignment"
|
||||
@itemRoute={{(array "vault.cluster.access.identity.show" "groups" "details")}}
|
||||
@alwaysRender={{true}}
|
||||
@doNotTruncate={{true}}
|
||||
/>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
<Oidc::AssignmentForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.assignments.assignment.details" this.model.name}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.assignments.assignment.details" this.model.name}}
|
||||
/>
|
|
@ -0,0 +1,5 @@
|
|||
<Oidc::AssignmentForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.assignments"}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.assignments.assignment.details" this.model.name}}
|
||||
/>
|
|
@ -0,0 +1,71 @@
|
|||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<ToolbarLink
|
||||
@type="add"
|
||||
@params={{array "vault.cluster.access.oidc.assignments.create"}}
|
||||
data-test-oidc-assignment-create
|
||||
>
|
||||
Create assignment
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#each this.model as |model|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row {{if (eq model.name 'allow_all') 'is-disabled'}}"
|
||||
@params={{array "vault.cluster.access.oidc.assignments.assignment.details" model.name}}
|
||||
@disabled={{eq model.name "allow_all"}}
|
||||
data-test-oidc-assignment-linked-block={{model.name}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<Icon @name="users" class="has-text-grey-light" />
|
||||
<span class="has-text-weight-semibold {{if (not-eq model.name 'allow_all') 'is-underline'}}">
|
||||
{{model.name}}
|
||||
</span>
|
||||
{{#if (eq model.name "allow_all")}}
|
||||
<div class="is-size-8">
|
||||
This is a built-in assignment that cannot be modified or deleted.
|
||||
<DocLink @path="/docs/concepts/oidc-provider#assignments">
|
||||
Learn more
|
||||
</DocLink>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if (not-eq model.name "allow_all")}}
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
<PopupMenu>
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.assignments.assignment.details"
|
||||
@model={{model.name}}
|
||||
@disabled={{eq model.canRead false}}
|
||||
data-test-oidc-assignment-menu-link="details"
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.assignments.assignment.edit"
|
||||
@model={{model.name}}
|
||||
@disabled={{eq model.canEdit false}}
|
||||
data-test-oidc-assignment-menu-link="edit"
|
||||
>
|
||||
Edit
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{/each}}
|
|
@ -0,0 +1,44 @@
|
|||
{{#if this.showHeader}}
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
<LinkTo @route="vault.cluster.access.oidc.clients">
|
||||
Applications
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-oidc-client-header>
|
||||
{{this.model.name}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
|
||||
<nav class="tabs" aria-label="tabs">
|
||||
<ul>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.clients.client.details"
|
||||
@model={{this.model}}
|
||||
data-test-oidc-client-details
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.clients.client.providers"
|
||||
@model={{this.model}}
|
||||
data-test-oidc-client-providers
|
||||
>
|
||||
Available providers
|
||||
</LinkTo>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
|
@ -0,0 +1,43 @@
|
|||
<Toolbar data-test-oidc-client-toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if this.model.canDelete}}
|
||||
<ConfirmAction
|
||||
data-test-oidc-client-delete
|
||||
@buttonClasses="toolbar-link"
|
||||
@onConfirmAction={{this.delete}}
|
||||
@confirmTitle="Delete application?"
|
||||
@confirmMessage="This application will be permanently deleted. You will need to re-create it to use it again."
|
||||
@confirmButtonText="Delete"
|
||||
>
|
||||
Delete application
|
||||
</ConfirmAction>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if this.model.canEdit}}
|
||||
<ToolbarLink
|
||||
data-test-oidc-client-edit
|
||||
@params={{array "vault.cluster.access.oidc.clients.client.edit" this.model.name}}
|
||||
>
|
||||
Edit application
|
||||
</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="Type" @value={{this.model.clientType}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Redirect URI" @value={{this.model.redirectUris}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Assignment" @value={{this.model.assignments}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Key" @alwaysRender={{true}}>
|
||||
<LinkTo @route="vault.cluster.access.oidc.keys.key.details" @model={{this.model.key}}>
|
||||
{{this.model.key}}
|
||||
</LinkTo>
|
||||
</InfoTableRow>
|
||||
<InfoTableRow @label="Client ID" @value={{this.model.clientId}} @addCopyButton={{true}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Client Secret">
|
||||
<MaskedInput @value={{this.model.clientSecret}} @displayOnly={{true}} @allowCopy={{true}} @alwaysRender={{true}} />
|
||||
</InfoTableRow>
|
||||
<InfoTableRow @label="ID Token TTL" @value={{format-duration this.model.idTokenTtl}} @alwaysRender={{true}} />
|
||||
<InfoTableRow @label="Access Token TTL" @value={{format-duration this.model.accessTokenTtl}} @alwaysRender={{true}} />
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
<Oidc::ClientForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.clients.client.details" this.model.name}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.clients.client.details" this.model.name}}
|
||||
/>
|
|
@ -0,0 +1,13 @@
|
|||
<Toolbar />
|
||||
{{#if (gt this.model.length 0)}}
|
||||
<Oidc::ProviderList @model={{this.model}} />
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No available providers"
|
||||
@message="Edit an existing provider or create a new one to allow this application access for authentication requests."
|
||||
>
|
||||
<LinkTo @route="vault.cluster.access.oidc.providers">
|
||||
View providers
|
||||
</LinkTo>
|
||||
</EmptyState>
|
||||
{{/if}}
|
|
@ -0,0 +1,5 @@
|
|||
<Oidc::ClientForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.clients"}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.clients.client.details" this.model.name}}
|
||||
/>
|
|
@ -0,0 +1,9 @@
|
|||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<ToolbarLink data-test-oidc-client-create @type="add" @params={{array "vault.cluster.access.oidc.clients.create"}}>
|
||||
Create application
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<Oidc::ClientList @model={{this.model}} />
|
|
@ -0,0 +1,14 @@
|
|||
<div class="box is-fullwidth is-shadowless" data-test-oidc-landing>
|
||||
<p>
|
||||
<b>Step 1:</b>
|
||||
Create an application, and obtain the client ID, client secret and issuer URL.
|
||||
</p>
|
||||
<p>
|
||||
<b>Step 2:</b>
|
||||
Set up a new auth method for Vault with the client application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="box is-fullwidth is-shadowless">
|
||||
<img data-test-oidc-img src={{img-path "~/oidc-landing.png"}} alt="OIDC configure diagram" />
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
<Oidc::KeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.access.oidc.keys"}}
|
||||
@onSave={{transition-to "vault.cluster.access.oidc.keys.key.details" this.model.name}}
|
||||
/>
|
|
@ -0,0 +1,56 @@
|
|||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<ToolbarLink data-test-oidc-key-create @type="add" @params={{array "vault.cluster.access.oidc.keys.create"}}>
|
||||
Create key
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#each this.model as |model|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row"
|
||||
@params={{array "vault.cluster.access.oidc.keys.key.details" model.name}}
|
||||
data-test-oidc-key-linked-block={{model.name}}
|
||||
>
|
||||
<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">
|
||||
{{model.name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
<PopupMenu>
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.keys.key.details"
|
||||
@model={{model.name}}
|
||||
@disabled={{eq model.canRead false}}
|
||||
data-test-oidc-key-menu-link="details"
|
||||
>
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="vault.cluster.access.oidc.keys.key.edit"
|
||||
@model={{model.name}}
|
||||
@disabled={{eq model.canEdit false}}
|
||||
data-test-oidc-key-menu-link="edit"
|
||||
>
|
||||
Edit
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{/each}}
|
|
@ -0,0 +1,36 @@
|
|||
{{#if this.showHeader}}
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="sep">/</span>
|
||||
<LinkTo @route="vault.cluster.access.oidc.keys" data-test-breadcrumb-link="oidc-keys">
|
||||
Keys
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
{{this.model.name}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<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}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
<LinkTo data-test-oidc-key-clients @route="vault.cluster.access.oidc.keys.key.clients" @model={{this.model}}>
|
||||
Applications
|
||||
</LinkTo>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue