mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
UI: LDAP Hierarchical roles (#28824)
* remove named path adapter extension, add subdirectory query logic to adapter * add subdirectory route and logic to page::roles component * fix overview page search select * breadcrumbs * update tests and mirage * revert ss changes * oops * cleanup adapter, add _ for private methods * add acceptance test * remove type * add changelog * add ldap breadcrumb test * VAULT-31905 link jira * update breadcrumbs in Edit route * rename type interfaces
This commit is contained in:
6
changelog/28824.txt
Normal file
6
changelog/28824.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
```release-note:improvement
|
||||
ui: Adds navigation for LDAP hierarchical roles
|
||||
```
|
||||
```release-note:bug
|
||||
ui: Fixes rendering issues of LDAP dynamic and static roles with the same name
|
||||
```
|
||||
@@ -3,46 +3,87 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import NamedPathAdapter from 'vault/adapters/named-path';
|
||||
import ApplicationAdapter from 'vault/adapters/application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { service } from '@ember/service';
|
||||
import AdapterError from '@ember-data/adapter/error';
|
||||
import { addManyToArray } from 'vault/helpers/add-to-array';
|
||||
import sortObjects from 'vault/utils/sort-objects';
|
||||
|
||||
export default class LdapRoleAdapter extends NamedPathAdapter {
|
||||
export const ldapRoleID = (type, name) => `type:${type}::name:${name}`;
|
||||
|
||||
export default class LdapRoleAdapter extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
|
||||
@service flashMessages;
|
||||
|
||||
getURL(backend, path, name) {
|
||||
// we do this in the adapter because query() requests separate endpoints to fetch static and dynamic roles.
|
||||
// it also handles some error logic and serializing (some of which is for lazyPaginatedQuery)
|
||||
// so for consistency formatting the response here
|
||||
_constructRecord({ backend, name, type }) {
|
||||
// ID cannot just be the 'name' because static and dynamic roles can have identical names
|
||||
return { id: ldapRoleID(type, name), backend, name, type };
|
||||
}
|
||||
|
||||
_getURL(backend, path, name) {
|
||||
const base = `${this.buildURL()}/${encodePath(backend)}/${path}`;
|
||||
return name ? `${base}/${name}` : base;
|
||||
}
|
||||
pathForRoleType(type, isCred) {
|
||||
|
||||
_pathForRoleType(type, isCred) {
|
||||
const staticPath = isCred ? 'static-cred' : 'static-role';
|
||||
const dynamicPath = isCred ? 'creds' : 'role';
|
||||
return type === 'static' ? staticPath : dynamicPath;
|
||||
}
|
||||
|
||||
urlForUpdateRecord(name, modelName, snapshot) {
|
||||
const { backend, type } = snapshot.record;
|
||||
return this.getURL(backend, this.pathForRoleType(type), name);
|
||||
}
|
||||
urlForDeleteRecord(name, modelName, snapshot) {
|
||||
const { backend, type } = snapshot.record;
|
||||
return this.getURL(backend, this.pathForRoleType(type), name);
|
||||
_createOrUpdate(store, modelSchema, snapshot) {
|
||||
const { backend, name, type } = snapshot.record;
|
||||
const data = snapshot.serialize();
|
||||
return this.ajax(this._getURL(backend, this._pathForRoleType(type), name), 'POST', {
|
||||
data,
|
||||
}).then(() => {
|
||||
// add ID to response because ember data dislikes 204s...
|
||||
return { data: this._constructRecord({ backend, name, type }) };
|
||||
});
|
||||
}
|
||||
|
||||
createRecord() {
|
||||
return this._createOrUpdate(...arguments);
|
||||
}
|
||||
|
||||
updateRecord() {
|
||||
return this._createOrUpdate(...arguments);
|
||||
}
|
||||
|
||||
urlForDeleteRecord(id, modelName, snapshot) {
|
||||
const { backend, type, name } = snapshot.record;
|
||||
return this._getURL(backend, this._pathForRoleType(type), name);
|
||||
}
|
||||
|
||||
/*
|
||||
roleAncestry: { path_to_role: string; type: string };
|
||||
*/
|
||||
async query(store, type, query, recordArray, options) {
|
||||
const { showPartialError } = options.adapterOptions || {};
|
||||
const { showPartialError, roleAncestry } = options.adapterOptions || {};
|
||||
const { backend } = query;
|
||||
|
||||
if (roleAncestry) {
|
||||
return this._querySubdirectory(backend, roleAncestry);
|
||||
}
|
||||
|
||||
return this._queryAll(backend, showPartialError);
|
||||
}
|
||||
|
||||
// LIST request for all roles (static and dynamic)
|
||||
async _queryAll(backend, showPartialError) {
|
||||
let roles = [];
|
||||
const errors = [];
|
||||
|
||||
for (const roleType of ['static', 'dynamic']) {
|
||||
const url = this.getURL(backend, this.pathForRoleType(roleType));
|
||||
const url = this._getURL(backend, this._pathForRoleType(roleType));
|
||||
try {
|
||||
const models = await this.ajax(url, 'GET', { data: { list: true } }).then((resp) => {
|
||||
return resp.data.keys.map((name) => ({ id: name, name, backend, type: roleType }));
|
||||
return resp.data.keys.map((name) => this._constructRecord({ backend, name, type: roleType }));
|
||||
});
|
||||
roles = addManyToArray(roles, models);
|
||||
} catch (error) {
|
||||
@@ -75,14 +116,32 @@ export default class LdapRoleAdapter extends NamedPathAdapter {
|
||||
// changing the responsePath or providing the extractLazyPaginatedData serializer method causes normalizeResponse to return data: [undefined]
|
||||
return { data: { keys: sortObjects(roles, 'name') } };
|
||||
}
|
||||
|
||||
// LIST request for children of a hierarchical role
|
||||
async _querySubdirectory(backend, roleAncestry) {
|
||||
// path_to_role is the ancestral path
|
||||
const { path_to_role, type: roleType } = roleAncestry;
|
||||
const url = `${this._getURL(backend, this._pathForRoleType(roleType))}/${path_to_role}`;
|
||||
const roles = await this.ajax(url, 'GET', { data: { list: true } }).then((resp) => {
|
||||
return resp.data.keys.map((name) => ({
|
||||
...this._constructRecord({ backend, name, type: roleType }),
|
||||
path_to_role, // adds path_to_role attr to ldap/role model
|
||||
}));
|
||||
});
|
||||
return { data: { keys: sortObjects(roles, 'name') } };
|
||||
}
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
const { backend, name, type: roleType } = query;
|
||||
const url = this.getURL(backend, this.pathForRoleType(roleType), name);
|
||||
return this.ajax(url, 'GET').then((resp) => ({ ...resp.data, backend, name, type: roleType }));
|
||||
const url = this._getURL(backend, this._pathForRoleType(roleType), name);
|
||||
return this.ajax(url, 'GET').then((resp) => ({
|
||||
...resp.data,
|
||||
...this._constructRecord({ backend, name, type: roleType }),
|
||||
}));
|
||||
}
|
||||
|
||||
fetchCredentials(backend, type, name) {
|
||||
const url = this.getURL(backend, this.pathForRoleType(type, true), name);
|
||||
const url = this._getURL(backend, this._pathForRoleType(type, true), name);
|
||||
return this.ajax(url, 'GET').then((resp) => {
|
||||
if (type === 'dynamic') {
|
||||
const { lease_id, lease_duration, renewable } = resp;
|
||||
@@ -92,7 +151,7 @@ export default class LdapRoleAdapter extends NamedPathAdapter {
|
||||
});
|
||||
}
|
||||
rotateStaticPassword(backend, name) {
|
||||
const url = this.getURL(backend, 'rotate-role', name);
|
||||
const url = this._getURL(backend, 'rotate-role', name);
|
||||
return this.ajax(url, 'POST');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ export const dynamicRoleFields = [
|
||||
@withModelValidations(validations)
|
||||
@withFormFields()
|
||||
export default class LdapRoleModel extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
||||
@attr('string') backend; // mount path of ldap engine -- set on response from value passed to queryRecord
|
||||
@attr('string') path_to_role; // ancestral path to the role added in the adapter (only exists for nested roles)
|
||||
|
||||
@attr('string', {
|
||||
defaultValue: 'static',
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class LdapRoleSerializer extends ApplicationSerializer {
|
||||
primaryKey = 'name';
|
||||
|
||||
serialize(snapshot) {
|
||||
// remove all fields that are not relevant to specified role type
|
||||
const { fieldsForType } = snapshot.record;
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { assert } from '@ember/debug';
|
||||
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface Args {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
}
|
||||
interface Breadcrumb {
|
||||
label: string;
|
||||
route?: string; // Do not provide for current route
|
||||
icon?: string;
|
||||
model?: string;
|
||||
models?: string[];
|
||||
linkToExternal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @module Page::Breadcrumbs
|
||||
|
||||
@@ -53,7 +53,10 @@
|
||||
class="is-flex-half"
|
||||
/>
|
||||
<div>
|
||||
<OverviewCard @cardTitle="Generate credentials" @subText="Quickly generate credentials by typing the role name.">
|
||||
<OverviewCard
|
||||
@cardTitle="Generate credentials"
|
||||
@subText="Quickly generate credentials by typing the role name. Only the engine's top-level roles are listed here."
|
||||
>
|
||||
<:content>
|
||||
<div class="has-top-margin-m is-flex">
|
||||
<SearchSelect
|
||||
@@ -61,11 +64,14 @@
|
||||
@ariaLabel="Role"
|
||||
@placeholder="Select a role"
|
||||
@disallowNewItems={{true}}
|
||||
@options={{@roles}}
|
||||
@options={{this.roleOptions}}
|
||||
@selectLimit="1"
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.selectRole}}
|
||||
@renderInPlace={{true}}
|
||||
@passObject={{true}}
|
||||
@objectKeys={{array "id" "name" "type"}}
|
||||
@shouldRenderName={{true}}
|
||||
/>
|
||||
<div>
|
||||
<Hds::Button
|
||||
|
||||
@@ -24,15 +24,35 @@ interface Args {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default class LdapLibrariesPageComponent extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
|
||||
@tracked selectedRole: LdapRoleModel | undefined;
|
||||
|
||||
get roleOptions() {
|
||||
const options = this.args.roles
|
||||
// hierarchical roles are not selectable
|
||||
.filter((r: LdapRoleModel) => !r.name.endsWith('/'))
|
||||
// *hack alert* - type is set as id so it renders beside name in search select
|
||||
// this is to avoid more changes to search select and is okay here because
|
||||
// we use the type and name to select the item below, not the id
|
||||
.map((r: LdapRoleModel) => ({ id: r.type, name: r.name, type: r.type }));
|
||||
return options;
|
||||
}
|
||||
|
||||
@action
|
||||
selectRole([roleName]: Array<string>) {
|
||||
const model = this.args.roles.find((role) => role.name === roleName);
|
||||
this.selectedRole = model;
|
||||
async selectRole([option]: Array<Option>) {
|
||||
if (option) {
|
||||
const { name, type } = option;
|
||||
const model = this.args.roles.find((role) => role.name === name && role.type === type);
|
||||
this.selectedRole = model;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -43,46 +43,59 @@
|
||||
{{else}}
|
||||
<div class="has-bottom-margin-s">
|
||||
{{#each @roles as |role|}}
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "roles.role.details" role.type role.name}} as |Item|>
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{this.linkParams role}} as |Item|>
|
||||
<Item.content>
|
||||
<Icon @name="user" />
|
||||
<span data-test-role={{role.name}}>{{role.name}}</span>
|
||||
<span data-test-role="{{role.type}} {{role.name}}">{{role.name}}</span>
|
||||
<Hds::Badge @text={{role.type}} data-test-role-type-badge={{role.name}} />
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon @icon="more-horizontal" @text="More options" @hasChevron={{false}} data-test-popup-menu-trigger />
|
||||
{{#if role.canEdit}}
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="More options"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger="{{role.type}} {{role.name}}"
|
||||
/>
|
||||
{{#if (this.isHierarchical role.name)}}
|
||||
<dd.Interactive
|
||||
data-test-edit
|
||||
@route="roles.role.edit"
|
||||
data-test-subdirectory
|
||||
@route="roles.subdirectory"
|
||||
@models={{array role.type (concat role.path_to_role role.name)}}
|
||||
>Content</dd.Interactive>
|
||||
{{else}}
|
||||
{{#if role.canEdit}}
|
||||
<dd.Interactive
|
||||
data-test-edit
|
||||
@route="roles.role.edit"
|
||||
@models={{array role.type role.name}}
|
||||
>Edit</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canReadCreds}}
|
||||
<dd.Interactive data-test-get-creds @route="roles.role.credentials" @models={{array role.type role.name}}>
|
||||
Get credentials
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canRotateStaticCreds}}
|
||||
<dd.Interactive
|
||||
data-test-rotate-creds
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.credsToRotate) role)}}
|
||||
>Rotate credentials</dd.Interactive>
|
||||
{{/if}}
|
||||
<dd.Interactive
|
||||
data-test-details
|
||||
@route="roles.role.details"
|
||||
{{! this will force the roles.role model hook to fire since we may only have a partial model loaded in the list view }}
|
||||
@models={{array role.type role.name}}
|
||||
>Edit</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canReadCreds}}
|
||||
<dd.Interactive data-test-get-creds @route="roles.role.credentials" @models={{array role.type role.name}}>
|
||||
Get credentials
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canRotateStaticCreds}}
|
||||
<dd.Interactive
|
||||
data-test-rotate-creds
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.credsToRotate) role)}}
|
||||
>Rotate credentials</dd.Interactive>
|
||||
{{/if}}
|
||||
<dd.Interactive
|
||||
data-test-details
|
||||
@route="roles.role.details"
|
||||
{{! this will force the roles.role model hook to fire since we may only have a partial model loaded in the list view }}
|
||||
@models={{array role.type role.name}}
|
||||
>Details</dd.Interactive>
|
||||
{{#if role.canDelete}}
|
||||
<dd.Interactive
|
||||
data-test-delete
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.roleToDelete) role)}}
|
||||
>Delete</dd.Interactive>
|
||||
>Details</dd.Interactive>
|
||||
{{#if role.canDelete}}
|
||||
<dd.Interactive
|
||||
data-test-delete
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.roleToDelete) role)}}
|
||||
>Delete</dd.Interactive>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
</Item.menu>
|
||||
@@ -108,7 +121,9 @@
|
||||
<Hds::Pagination::Numbered
|
||||
@currentPage={{@roles.meta.currentPage}}
|
||||
@currentPageSize={{@roles.meta.pageSize}}
|
||||
@route="roles"
|
||||
{{! localName will be either "index" or "subdirectory" }}
|
||||
@route="roles.{{this.router.currentRoute.localName}}"
|
||||
@models={{@currentRouteParams}}
|
||||
@showSizeSelector={{false}}
|
||||
@totalItems={{@roles.meta.filteredTotal}}
|
||||
@queryFunction={{this.paginationQueryParams}}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { getOwner } from '@ember/owner';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
@@ -15,7 +16,6 @@ import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type PaginationService from 'vault/services/pagination';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
interface Args {
|
||||
roles: Array<LdapRoleModel>;
|
||||
@@ -29,9 +29,20 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly pagination: PaginationService;
|
||||
|
||||
@tracked credsToRotate: LdapRoleModel | null = null;
|
||||
@tracked roleToDelete: LdapRoleModel | null = null;
|
||||
|
||||
isHierarchical = (name: string) => name.endsWith('/');
|
||||
|
||||
linkParams = (role: LdapRoleModel) => {
|
||||
const route = this.isHierarchical(role.name) ? 'roles.subdirectory' : 'roles.role.details';
|
||||
// if there is a path_to_role we're in a subdirectory
|
||||
// and must concat the ancestors with the leaf name to get the full role path
|
||||
const roleName = role.path_to_role ? role.path_to_role + role.name : role.name;
|
||||
return [route, role.type, roleName];
|
||||
};
|
||||
|
||||
get mountPoint(): string {
|
||||
const owner = getOwner(this) as EngineOwner;
|
||||
return owner.mountPoint;
|
||||
@@ -43,7 +54,8 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
||||
|
||||
@action
|
||||
onFilterChange(pageFilter: string) {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles', { queryParams: { pageFilter } });
|
||||
// refresh route, which fires off lazyPaginatedQuery to re-request and filter response
|
||||
this.router.transitionTo(this.router?.currentRoute?.name, { queryParams: { pageFilter } });
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
10
ui/lib/ldap/addon/controllers/roles/subdirectory.ts
Normal file
10
ui/lib/ldap/addon/controllers/roles/subdirectory.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class LdapRolesSubdirectoryController extends Controller {
|
||||
queryParams = ['pageFilter', 'page'];
|
||||
}
|
||||
@@ -9,6 +9,8 @@ export default buildRoutes(function () {
|
||||
this.route('overview');
|
||||
this.route('roles', function () {
|
||||
this.route('create');
|
||||
// wildcard route so we can traverse hierarchical roles i.e. prod/admin/my-role
|
||||
this.route('subdirectory', { path: '/:type/subdirectory/*path_to_role' });
|
||||
this.route('role', { path: '/:type/:name' }, function () {
|
||||
this.route('details');
|
||||
this.route('edit');
|
||||
|
||||
@@ -16,14 +16,14 @@ import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
|
||||
|
||||
interface LdapConfigurationRouteModel {
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
configModel: LdapConfigModel;
|
||||
configError: AdapterError;
|
||||
}
|
||||
interface LdapConfigurationController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapConfigurationRouteModel;
|
||||
model: RouteModel;
|
||||
}
|
||||
|
||||
@withConfig('ldap/config')
|
||||
@@ -42,11 +42,7 @@ export default class LdapConfigurationRoute extends Route {
|
||||
};
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapConfigurationController,
|
||||
resolvedModel: LdapConfigurationRouteModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
|
||||
@@ -14,7 +14,7 @@ import type LdapConfigModel from 'vault/models/ldap/config';
|
||||
import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface LdapConfigureController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
}
|
||||
|
||||
@@ -30,11 +30,7 @@ export default class LdapConfigureRoute extends Route {
|
||||
return this.configModel || this.store.createRecord('ldap/config', { backend });
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapConfigureController,
|
||||
resolvedModel: LdapConfigModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: LdapConfigModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
|
||||
@@ -18,10 +18,10 @@ import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library';
|
||||
|
||||
interface LdapOverviewController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
}
|
||||
interface LdapOverviewRouteModel {
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
promptConfig: boolean;
|
||||
roles: Array<LdapRoleModel>;
|
||||
@@ -66,11 +66,7 @@ export default class LdapOverviewRoute extends Route {
|
||||
});
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapOverviewController,
|
||||
resolvedModel: LdapOverviewRouteModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
|
||||
31
ui/lib/ldap/addon/routes/roles.ts
Normal file
31
ui/lib/ldap/addon/routes/roles.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import type PaginationService from 'vault/services/pagination';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
|
||||
// Base class for roles/index and roles/subdirectory routes
|
||||
export default class LdapRolesRoute extends Route {
|
||||
@service declare readonly pagination: PaginationService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
lazyQuery(backendId: string, params: { page?: string; pageFilter: string }, adapterOptions: object) {
|
||||
const page = Number(params.page) || 1;
|
||||
return this.pagination.lazyPaginatedQuery(
|
||||
'ldap/role',
|
||||
{
|
||||
backend: backendId,
|
||||
page,
|
||||
pageFilter: params.pageFilter,
|
||||
responsePath: 'data.keys',
|
||||
skipCache: page === 1,
|
||||
},
|
||||
{ adapterOptions }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import type Controller from '@ember/controller';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface LdapRolesCreateController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRoleModel;
|
||||
}
|
||||
@@ -27,11 +27,7 @@ export default class LdapRolesCreateRoute extends Route {
|
||||
return this.store.createRecord('ldap/role', { backend });
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapRolesCreateController,
|
||||
resolvedModel: LdapRoleModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
|
||||
@@ -3,42 +3,32 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import LdapRolesRoute from '../roles';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
import type StoreService from 'vault/services/store';
|
||||
import type PaginationService from 'vault/services/pagination';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface LdapRolesRouteModel {
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
promptConfig: boolean;
|
||||
roles: Array<LdapRoleModel>;
|
||||
}
|
||||
interface LdapRolesController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRolesRouteModel;
|
||||
pageFilter: string | undefined;
|
||||
page: number | undefined;
|
||||
}
|
||||
|
||||
interface LdapRolesRouteParams {
|
||||
page?: string;
|
||||
pageFilter: string;
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: RouteModel;
|
||||
}
|
||||
|
||||
@withConfig('ldap/config')
|
||||
export default class LdapRolesRoute extends Route {
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly pagination: PaginationService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
export default class LdapRolesIndexRoute extends LdapRolesRoute {
|
||||
@service declare readonly store: StoreService; // necessary for @withConfig decorator
|
||||
|
||||
declare promptConfig: boolean;
|
||||
|
||||
@@ -51,39 +41,26 @@ export default class LdapRolesRoute extends Route {
|
||||
},
|
||||
};
|
||||
|
||||
model(params: LdapRolesRouteParams) {
|
||||
model(params: { page?: string; pageFilter: string }) {
|
||||
const backendModel = this.modelFor('application') as SecretEngineModel;
|
||||
return hash({
|
||||
backendModel,
|
||||
promptConfig: this.promptConfig,
|
||||
roles: this.pagination.lazyPaginatedQuery(
|
||||
'ldap/role',
|
||||
{
|
||||
backend: backendModel.id,
|
||||
page: Number(params.page) || 1,
|
||||
pageFilter: params.pageFilter,
|
||||
responsePath: 'data.keys',
|
||||
},
|
||||
{ adapterOptions: { showPartialError: true } }
|
||||
),
|
||||
roles: this.lazyQuery(backendModel.id, params, { showPartialError: true }),
|
||||
});
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapRolesController,
|
||||
resolvedModel: LdapRolesRouteModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backendModel.id, route: 'overview', model: resolvedModel.backend },
|
||||
{ label: resolvedModel.backendModel.id, route: 'overview' },
|
||||
{ label: 'Roles' },
|
||||
];
|
||||
}
|
||||
|
||||
resetController(controller: LdapRolesController, isExiting: boolean) {
|
||||
resetController(controller: RouteController, isExiting: boolean) {
|
||||
if (isExiting) {
|
||||
controller.set('pageFilter', undefined);
|
||||
controller.set('page', undefined);
|
||||
|
||||
@@ -6,19 +6,17 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import { ModelFrom } from 'vault/vault/route';
|
||||
import type Store from '@ember-data/store';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
|
||||
interface LdapRoleRouteParams {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
export type LdapRolesRoleRouteModel = ModelFrom<LdapRolesRoleRoute>;
|
||||
|
||||
export default class LdapRoleRoute extends Route {
|
||||
export default class LdapRolesRoleRoute extends Route {
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
model(params: LdapRoleRouteParams) {
|
||||
model(params: { name: string; type: string }) {
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
const { name, type } = params;
|
||||
return this.store.queryRecord('ldap/role', { backend, name, type });
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
@@ -13,7 +14,7 @@ import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
|
||||
|
||||
export interface LdapStaticRoleCredentials {
|
||||
export interface StaticCredentials {
|
||||
dn: string;
|
||||
last_vault_rotation: string;
|
||||
password: string;
|
||||
@@ -23,7 +24,7 @@ export interface LdapStaticRoleCredentials {
|
||||
username: string;
|
||||
type: string;
|
||||
}
|
||||
export interface LdapDynamicRoleCredentials {
|
||||
export interface DynamicCredentials {
|
||||
distinguished_names: Array<string>;
|
||||
password: string;
|
||||
username: string;
|
||||
@@ -32,13 +33,13 @@ export interface LdapDynamicRoleCredentials {
|
||||
renewable: boolean;
|
||||
type: string;
|
||||
}
|
||||
interface LdapRoleCredentialsRouteModel {
|
||||
credentials: undefined | LdapStaticRoleCredentials | LdapDynamicRoleCredentials;
|
||||
interface RouteModel {
|
||||
credentials: undefined | StaticCredentials | DynamicCredentials;
|
||||
error: undefined | AdapterError;
|
||||
}
|
||||
interface LdapRoleCredentialsController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRoleCredentialsRouteModel;
|
||||
model: RouteModel;
|
||||
}
|
||||
|
||||
export default class LdapRoleCredentialsRoute extends Route {
|
||||
@@ -53,19 +54,16 @@ export default class LdapRoleCredentialsRoute extends Route {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
setupController(
|
||||
controller: LdapRoleCredentialsController,
|
||||
resolvedModel: LdapRoleCredentialsRouteModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const role = this.modelFor('roles.role') as LdapRoleModel;
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: role.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: role.name, route: 'roles.role' },
|
||||
{ label: 'credentials' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(role.name, role.type, role.backend),
|
||||
{ label: 'Credentials' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,31 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type Controller from '@ember/controller';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type Controller from '@ember/controller';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
|
||||
interface LdapRoleDetailsController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRoleModel;
|
||||
}
|
||||
|
||||
export default class LdapRoleEditRoute extends Route {
|
||||
setupController(
|
||||
controller: LdapRoleDetailsController,
|
||||
resolvedModel: LdapRoleModel,
|
||||
transition: Transition
|
||||
) {
|
||||
export default class LdapRolesRoleDetailsRoute extends Route {
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: resolvedModel.name },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, this.secretMountPath.currentPath, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,28 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type Controller from '@ember/controller';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface LdapRoleEditController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRoleModel;
|
||||
}
|
||||
|
||||
export default class LdapRoleEditRoute extends Route {
|
||||
setupController(controller: LdapRoleEditController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: resolvedModel.name, route: 'roles.role' },
|
||||
{ label: 'edit' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, resolvedModel.backend),
|
||||
{ label: 'Edit' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
75
ui/lib/ldap/addon/routes/roles/subdirectory.ts
Normal file
75
ui/lib/ldap/addon/routes/roles/subdirectory.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import LdapRolesRoute from '../roles';
|
||||
import { hash } from 'rsvp';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type Controller from '@ember/controller';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
roleAncestry: { path_to_role: string; type: string };
|
||||
roles: Array<LdapRoleModel>;
|
||||
}
|
||||
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: RouteModel;
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
page?: string;
|
||||
pageFilter: string;
|
||||
path_to_role: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default class LdapRolesSubdirectoryRoute extends LdapRolesRoute {
|
||||
queryParams = {
|
||||
pageFilter: {
|
||||
refreshModel: true,
|
||||
},
|
||||
page: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model(params: RouteParams) {
|
||||
const backendModel = this.modelFor('application') as SecretEngineModel;
|
||||
const { path_to_role, type } = params;
|
||||
const roleAncestry = { path_to_role, type };
|
||||
return hash({
|
||||
backendModel,
|
||||
roleAncestry,
|
||||
roles: this.lazyQuery(backendModel.id, params, { roleAncestry }),
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
const { backendModel, roleAncestry } = resolvedModel;
|
||||
const crumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: backendModel.id, route: 'overview' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(roleAncestry.path_to_role, roleAncestry.type, backendModel.id, true),
|
||||
];
|
||||
|
||||
// must call 'set' so breadcrumbs update as we navigate through directories
|
||||
controller.set('breadcrumbs', crumbs);
|
||||
}
|
||||
|
||||
resetController(controller: RouteController, isExiting: boolean) {
|
||||
if (isExiting) {
|
||||
controller.set('pageFilter', undefined);
|
||||
controller.set('page', undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
ui/lib/ldap/addon/templates/roles/subdirectory.hbs
Normal file
15
ui/lib/ldap/addon/templates/roles/subdirectory.hbs
Normal file
@@ -0,0 +1,15 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
{{#let this.model.roleAncestry.type this.model.roleAncestry.path_to_role as |roleType path_to_role|}}
|
||||
<Page::Roles
|
||||
@roles={{this.model.roles}}
|
||||
@promptConfig={{false}}
|
||||
@backendModel={{this.model.backendModel}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@pageFilter={{this.pageFilter}}
|
||||
@currentRouteParams={{array this.model.backendModel.id roleType path_to_role}}
|
||||
/>
|
||||
{{/let}}
|
||||
36
ui/lib/ldap/addon/utils/ldap-breadcrumbs.ts
Normal file
36
ui/lib/ldap/addon/utils/ldap-breadcrumbs.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
export const ldapBreadcrumbs = (
|
||||
fullPath: string | undefined, // i.e. path/to/item
|
||||
roleType: string,
|
||||
mountPath: string,
|
||||
lastItemCurrent = false // this array of objects can be spread anywhere within the crumbs array
|
||||
): Breadcrumb[] => {
|
||||
if (!fullPath) return [];
|
||||
const ancestry = fullPath.split('/').filter((path) => path !== '');
|
||||
const isDirectory = fullPath.endsWith('/');
|
||||
|
||||
return ancestry.map((name: string, idx: number) => {
|
||||
const isLast = ancestry.length === idx + 1;
|
||||
// if the end of the path is the current route, don't return a route link
|
||||
if (isLast && lastItemCurrent) return { label: name };
|
||||
|
||||
// each segment is a continued concatenation of ancestral paths.
|
||||
// for example, if the full path to an item is "prod/admin/west"
|
||||
// the segments will be: prod/, prod/admin/, prod/admin/west.
|
||||
// LIST or GET requests can then be made for each crumb accordingly.
|
||||
const segment = ancestry.slice(0, idx + 1).join('/');
|
||||
|
||||
const itemPath = isLast && !isDirectory ? segment : `${segment}/`;
|
||||
const routeParams = [mountPath, roleType, itemPath];
|
||||
return {
|
||||
label: name,
|
||||
route: isLast && !isDirectory ? 'roles.role.details' : 'roles.subdirectory',
|
||||
models: routeParams,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -30,19 +30,42 @@ export default function (server) {
|
||||
};
|
||||
const listRecords = (schema, dbKey, query = {}) => {
|
||||
const records = schema.db[dbKey].where(query);
|
||||
const keys = records.map(({ name }) => {
|
||||
if (name.includes('/')) {
|
||||
const [parent, child] = name.split('/');
|
||||
// query.name is only passed by listOrGetRecord and means we want to list children of admin/
|
||||
// otherwise this is the request for all roles in an engine so we return the top-level paths
|
||||
return query?.name ? child : `${parent}/`;
|
||||
}
|
||||
return name;
|
||||
});
|
||||
|
||||
return {
|
||||
data: { keys: records.map((record) => record.name) },
|
||||
data: { keys },
|
||||
};
|
||||
};
|
||||
|
||||
const listOrGetRecord = (schema, req, type) => {
|
||||
// if the param name is admin, we want to LIST admin/ roles
|
||||
if (req.queryParams.list) {
|
||||
// passing a query with specific name is not flexible
|
||||
// but we only seeded the mirage db with one hierarchical role for each type
|
||||
return listRecords(schema, 'ldapRoles', { type, name: `admin/child-${type}-role` });
|
||||
}
|
||||
// otherwise we want to view details for a specific role
|
||||
return getRecord(schema, req, 'ldapRoles', type);
|
||||
};
|
||||
|
||||
// config
|
||||
server.post('/:backend/config', (schema, req) => createOrUpdateRecord(schema, req, 'ldapConfigs'));
|
||||
server.get('/:backend/config', (schema, req) => getRecord(schema, req, 'ldapConfigs'));
|
||||
// roles
|
||||
server.post('/:backend/static-role/:name', (schema, req) => createOrUpdateRecord(schema, req, 'ldapRoles'));
|
||||
server.post('/:backend/role/:name', (schema, req) => createOrUpdateRecord(schema, req, 'ldapRoles'));
|
||||
server.get('/:backend/static-role/:name', (schema, req) => getRecord(schema, req, 'ldapRoles', 'static'));
|
||||
server.get('/:backend/role/:name', (schema, req) => getRecord(schema, req, 'ldapRoles', 'dynamic'));
|
||||
// if the role is hierarchical the name ends in a forward slash so we make a list request
|
||||
server.get('/:backend/static-role/*name', (schema, req) => listOrGetRecord(schema, req, 'static'));
|
||||
server.get('/:backend/role/*name', (schema, req) => listOrGetRecord(schema, req, 'dynamic'));
|
||||
|
||||
server.get('/:backend/static-role', (schema) => listRecords(schema, 'ldapRoles', { type: 'static' }));
|
||||
server.get('/:backend/role', (schema) => listRecords(schema, 'ldapRoles', { type: 'dynamic' }));
|
||||
// role credentials
|
||||
|
||||
@@ -7,6 +7,12 @@ export default function (server) {
|
||||
server.create('ldap-config', { path: 'kubernetes', backend: 'ldap-test' });
|
||||
server.create('ldap-role', 'static', { name: 'static-role' });
|
||||
server.create('ldap-role', 'dynamic', { name: 'dynamic-role' });
|
||||
// hierarchical roles
|
||||
server.create('ldap-role', 'static', { name: 'admin/child-static-role' });
|
||||
server.create('ldap-role', 'dynamic', { name: 'admin/child-dynamic-role' });
|
||||
// use same name for both role types to test edge cases
|
||||
server.create('ldap-role', 'static', { name: 'my-role' });
|
||||
server.create('ldap-role', 'dynamic', { name: 'my-role' });
|
||||
server.create('ldap-library', { name: 'test-library' });
|
||||
server.create('ldap-account-status', {
|
||||
id: 'bob.johnson',
|
||||
|
||||
@@ -11,9 +11,10 @@ import ldapMirageScenario from 'vault/mirage/scenarios/ldap';
|
||||
import ldapHandlers from 'vault/mirage/handlers/ldap';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { click, fillIn, waitFor } from '@ember/test-helpers';
|
||||
import { isURL, visitURL } from 'vault/tests/helpers/ldap/ldap-helpers';
|
||||
import { assertURL, isURL, visitURL } from 'vault/tests/helpers/ldap/ldap-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
|
||||
import { LDAP_SELECTORS } from 'vault/tests/helpers/ldap/ldap-selectors';
|
||||
|
||||
module('Acceptance | ldap | roles', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -45,37 +46,46 @@ module('Acceptance | ldap | roles', function (hooks) {
|
||||
});
|
||||
|
||||
test('it should transition to role details route on list item click', async function (assert) {
|
||||
await click('[data-test-list-item-link]:nth-of-type(1) a');
|
||||
assert.true(
|
||||
isURL('roles/dynamic/dynamic-role/details', this.backend),
|
||||
'Transitions to role details route on list item click'
|
||||
);
|
||||
let path;
|
||||
|
||||
await click('[data-test-breadcrumb="roles"] a');
|
||||
await click('[data-test-list-item-link]:nth-of-type(2) a');
|
||||
assert.true(
|
||||
isURL('roles/static/static-role/details', this.backend),
|
||||
'Transitions to role details route on list item click'
|
||||
);
|
||||
await click(LDAP_SELECTORS.roleItem('dynamic', 'dynamic-role'));
|
||||
path = 'roles/dynamic/dynamic-role/details';
|
||||
assertURL(assert, this.backend, path);
|
||||
await click(GENERAL.breadcrumbLink('Roles'));
|
||||
|
||||
await click(LDAP_SELECTORS.roleItem('static', 'static-role'));
|
||||
path = 'roles/static/static-role/details';
|
||||
assertURL(assert, this.backend, path);
|
||||
await click(GENERAL.breadcrumbLink('Roles'));
|
||||
|
||||
// edge case, roles of different type with same name
|
||||
await click(LDAP_SELECTORS.roleItem('dynamic', 'my-role'));
|
||||
path = 'roles/dynamic/my-role/details';
|
||||
assertURL(assert, this.backend, path);
|
||||
await click(GENERAL.breadcrumbLink('Roles'));
|
||||
|
||||
await click(LDAP_SELECTORS.roleItem('static', 'my-role'));
|
||||
path = 'roles/static/my-role/details';
|
||||
assertURL(assert, this.backend, path);
|
||||
});
|
||||
|
||||
test('it should transition to routes from list item action menu', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
for (const action of ['edit', 'get-creds', 'details']) {
|
||||
await click('[data-test-popup-menu-trigger]');
|
||||
await click(`[data-test-${action}]`);
|
||||
await click(LDAP_SELECTORS.roleMenu('dynamic', 'dynamic-role'));
|
||||
await click(LDAP_SELECTORS.action(action));
|
||||
const uri = action === 'get-creds' ? 'credentials' : action;
|
||||
assert.true(
|
||||
isURL(`roles/dynamic/dynamic-role/${uri}`, this.backend),
|
||||
`Transitions to ${uri} route on list item action menu click`
|
||||
);
|
||||
await click('[data-test-breadcrumb="roles"] a');
|
||||
await click(GENERAL.breadcrumbLink('Roles'));
|
||||
}
|
||||
});
|
||||
|
||||
test('it should transition to routes from role details toolbar links', async function (assert) {
|
||||
await click('[data-test-list-item-link]:nth-of-type(1) a');
|
||||
await click(LDAP_SELECTORS.roleItem('dynamic', 'dynamic-role'));
|
||||
await click('[data-test-get-credentials]');
|
||||
assert.true(
|
||||
isURL('roles/dynamic/dynamic-role/credentials', this.backend),
|
||||
@@ -100,4 +110,60 @@ module('Acceptance | ldap | roles', function (hooks) {
|
||||
await click('[data-test-tab="roles"]');
|
||||
assert.dom('[data-test-filter-input]').hasNoValue('Roles page filter value cleared on route exit');
|
||||
});
|
||||
|
||||
module('subdirectory', function () {
|
||||
test('it navigates to hierarchical roles', async function (assert) {
|
||||
let path;
|
||||
// hierarchical paths
|
||||
await click(LDAP_SELECTORS.roleItem('dynamic', 'admin/'));
|
||||
path = 'roles/dynamic/subdirectory/admin/';
|
||||
assertURL(assert, this.backend, path);
|
||||
|
||||
await click(LDAP_SELECTORS.roleItem('dynamic', 'child-dynamic-role'));
|
||||
path = 'roles/dynamic/admin%2Fchild-dynamic-role/details';
|
||||
assertURL(assert, this.backend, path);
|
||||
|
||||
// navigate out via breadcrumbs to test
|
||||
await click(GENERAL.breadcrumbLink('admin'));
|
||||
path = 'roles/dynamic/subdirectory/admin/';
|
||||
assertURL(assert, this.backend, path);
|
||||
|
||||
await click(GENERAL.breadcrumbLink('Roles'));
|
||||
|
||||
await click(LDAP_SELECTORS.roleItem('static', 'admin/'));
|
||||
path = 'roles/static/subdirectory/admin/';
|
||||
assertURL(assert, this.backend, path);
|
||||
await click(LDAP_SELECTORS.roleItem('static', 'child-static-role'));
|
||||
path = 'roles/static/admin%2Fchild-static-role/details';
|
||||
assertURL(assert, this.backend, path);
|
||||
|
||||
// navigate out via breadcrumbs to test
|
||||
await click(GENERAL.breadcrumbLink('admin'));
|
||||
path = 'roles/static/subdirectory/admin/';
|
||||
assertURL(assert, this.backend, path);
|
||||
});
|
||||
|
||||
test('it should transition to subdirectory from hierarchical role popup menu', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await click(LDAP_SELECTORS.roleMenu('dynamic', 'admin/'));
|
||||
for (const action of ['edit', 'get-creds', 'details']) {
|
||||
assert.dom(LDAP_SELECTORS.action(action)).doesNotExist(`${action} does not render in popup menu`);
|
||||
}
|
||||
await click(LDAP_SELECTORS.action('subdirectory'));
|
||||
assertURL(assert, this.backend, 'roles/dynamic/subdirectory/admin/');
|
||||
});
|
||||
|
||||
test('it should clear roles page filter value on route exit', async function (assert) {
|
||||
await visitURL('roles/static/subdirectory/admin/', this.backend);
|
||||
await fillIn('[data-test-filter-input]', 'foo');
|
||||
assert
|
||||
.dom('[data-test-filter-input]')
|
||||
.hasValue('foo', 'Roles page filter value set after model refresh and rerender');
|
||||
await waitFor(GENERAL.emptyStateTitle);
|
||||
await click('[data-test-tab="libraries"]');
|
||||
await click('[data-test-tab="roles"]');
|
||||
assert.dom('[data-test-filter-input]').hasNoValue('Roles page filter value cleared on route exit');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,10 @@ export const isURL = (uri, backend = 'ldap-test') => {
|
||||
return currentURL() === `${baseURL(backend)}${stripLeadingSlash(uri)}`;
|
||||
};
|
||||
|
||||
export const assertURL = (assert, backend, path) => {
|
||||
assert.strictEqual(currentURL(), baseURL(backend) + path, `url is ${path}`);
|
||||
};
|
||||
|
||||
export const visitURL = (uri, backend = 'ldap-test') => {
|
||||
return visit(`${baseURL(backend)}${stripLeadingSlash(uri)}`);
|
||||
};
|
||||
|
||||
10
ui/tests/helpers/ldap/ldap-selectors.ts
Normal file
10
ui/tests/helpers/ldap/ldap-selectors.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export const LDAP_SELECTORS = {
|
||||
roleItem: (type: string, name: string) => `[data-test-role="${type} ${name}"]`,
|
||||
roleMenu: (type: string, name: string) => `[data-test-popup-menu-trigger="${type} ${name}"]`,
|
||||
action: (action: string) => `[data-test-${action}]`,
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import { ldapRoleID } from 'vault/adapters/ldap/role';
|
||||
|
||||
module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@@ -29,7 +30,8 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h
|
||||
this.newModel = this.store.createRecord('ldap/role', { backend: 'ldap-test' });
|
||||
|
||||
['static', 'dynamic'].forEach((type) => {
|
||||
this[`${type}RoleData`] = this.server.create('ldap-role', type, { name: `${type}-role` });
|
||||
const name = `${type}-role`;
|
||||
this[`${type}RoleData`] = this.server.create('ldap-role', type, { name, id: ldapRoleID(type, name) });
|
||||
this.store.pushPayload('ldap/role', {
|
||||
modelName: 'ldap/role',
|
||||
backend: 'ldap-test',
|
||||
@@ -44,6 +46,8 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h
|
||||
{ label: 'Create' },
|
||||
];
|
||||
|
||||
this.fetchModel = (type, name) => this.store.peekRecord('ldap/role', ldapRoleID(type, name));
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(
|
||||
hbs`<Page::Role::CreateAndEdit @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
@@ -100,14 +104,14 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h
|
||||
});
|
||||
};
|
||||
|
||||
this.model = this.store.peekRecord('ldap/role', 'static-role');
|
||||
this.model = this.fetchModel('static', 'static-role');
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-radio-card="static"]').isDisabled('Type selection is disabled when editing');
|
||||
assert.dom('[data-test-input="name"]').isDisabled('Name field is disabled when editing');
|
||||
checkFields(['name', 'dn', 'username']);
|
||||
checkTtl(['rotation_period']);
|
||||
|
||||
this.model = this.store.peekRecord('ldap/role', 'dynamic-role');
|
||||
this.model = this.fetchModel('dynamic', 'dynamic-role');
|
||||
await this.renderComponent();
|
||||
checkFields(['name', 'username_template']);
|
||||
checkTtl(['default_ttl', 'max_ttl']);
|
||||
@@ -115,7 +119,7 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h
|
||||
});
|
||||
|
||||
test('it should go back to list route and clean up model on cancel', async function (assert) {
|
||||
this.model = this.store.peekRecord('ldap/role', 'static-role');
|
||||
this.model = this.fetchModel('static', 'static-role');
|
||||
const spy = sinon.spy(this.model, 'rollbackAttributes');
|
||||
|
||||
await this.renderComponent();
|
||||
@@ -181,7 +185,7 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h
|
||||
assert.deepEqual(expected, data, 'POST request made to save role with correct properties');
|
||||
});
|
||||
|
||||
this.model = this.store.peekRecord('ldap/role', 'static-role');
|
||||
this.model = this.fetchModel('static', 'static-role');
|
||||
await this.renderComponent();
|
||||
|
||||
await fillIn('[data-test-input="dn"]', 'foo');
|
||||
|
||||
@@ -11,6 +11,7 @@ import { render, click } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import { duration } from 'core/helpers/format-duration';
|
||||
import { ldapRoleID } from 'vault/adapters/ldap/role';
|
||||
|
||||
module('Integration | Component | ldap | Page::Role::Details', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@@ -25,6 +26,7 @@ module('Integration | Component | ldap | Page::Role::Details', function (hooks)
|
||||
}));
|
||||
this.renderComponent = (type) => {
|
||||
const data = this.server.create('ldap-role', type);
|
||||
data.id = ldapRoleID(type, data.name);
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.pushPayload('ldap/role', {
|
||||
modelName: 'ldap/role',
|
||||
@@ -32,7 +34,7 @@ module('Integration | Component | ldap | Page::Role::Details', function (hooks)
|
||||
type,
|
||||
...data,
|
||||
});
|
||||
this.model = store.peekRecord('ldap/role', data.name);
|
||||
this.model = store.peekRecord('ldap/role', ldapRoleID(type, data.name));
|
||||
this.breadcrumbs = [
|
||||
{ label: this.model.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
|
||||
@@ -12,6 +12,7 @@ import hbs from 'htmlbars-inline-precompile';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { createSecretsEngine, generateBreadcrumbs } from 'vault/tests/helpers/ldap/ldap-helpers';
|
||||
import sinon from 'sinon';
|
||||
import { LDAP_SELECTORS } from 'vault/tests/helpers/ldap/ldap-selectors';
|
||||
|
||||
module('Integration | Component | ldap | Page::Roles', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@@ -95,7 +96,9 @@ module('Integration | Component | ldap | Page::Roles', function (hooks) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom('[data-test-list-item-content] svg').hasClass('flight-icon-user', 'List item icon renders');
|
||||
assert.dom('[data-test-role="static-test"]').hasText(this.roles[0].name, 'List item name renders');
|
||||
assert
|
||||
.dom(LDAP_SELECTORS.roleItem('static', 'static-test'))
|
||||
.hasText(this.roles[0].name, 'List item name renders');
|
||||
assert
|
||||
.dom('[data-test-role-type-badge="static-test"]')
|
||||
.hasText(this.roles[0].type, 'List item type badge renders');
|
||||
@@ -116,8 +119,14 @@ module('Integration | Component | ldap | Page::Roles', function (hooks) {
|
||||
});
|
||||
|
||||
test('it should filter roles', async function (assert) {
|
||||
const transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
const currentRouteName = 'vault.cluster.secrets.backend.ldap.roles';
|
||||
this.router = this.owner.lookup('service:router');
|
||||
const transitionStub = sinon.stub(this.router, 'transitionTo');
|
||||
// stub because the component calls either "roles.subdirectory" or "roles.index"
|
||||
// depending on where it renders
|
||||
sinon.stub(this.router, 'currentRoute').value({
|
||||
name: currentRouteName,
|
||||
});
|
||||
this.roles.meta.filteredTotal = 0;
|
||||
this.pageFilter = 'foo';
|
||||
|
||||
@@ -129,10 +138,13 @@ module('Integration | Component | ldap | Page::Roles', function (hooks) {
|
||||
|
||||
await fillIn('[data-test-filter-input]', 'bar');
|
||||
|
||||
assert.true(
|
||||
transitionStub.calledWith('vault.cluster.secrets.backend.ldap.roles', {
|
||||
const [calledRoute, calledParams] = transitionStub.lastCall.args;
|
||||
assert.strictEqual(calledRoute, currentRouteName);
|
||||
assert.propEqual(
|
||||
calledParams,
|
||||
{
|
||||
queryParams: { pageFilter: 'bar' },
|
||||
}),
|
||||
},
|
||||
'Transition called with correct query params on filter change'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { Response } from 'miragejs';
|
||||
import sinon from 'sinon';
|
||||
import { ldapRoleID } from 'vault/adapters/ldap/role';
|
||||
|
||||
module('Unit | Adapter | ldap/role', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -17,6 +18,18 @@ module('Unit | Adapter | ldap/role', function (hooks) {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.adapter = this.store.adapterFor('ldap/role');
|
||||
this.path = 'role';
|
||||
|
||||
this.getModel = (type) => {
|
||||
const name = 'test-role';
|
||||
this.store.pushPayload('ldap/role', {
|
||||
modelName: 'ldap/role',
|
||||
backend: 'ldap-test',
|
||||
name,
|
||||
type,
|
||||
id: ldapRoleID(type, name),
|
||||
});
|
||||
return this.store.peekRecord('ldap/role', ldapRoleID(type, name));
|
||||
};
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when listing records', async function (assert) {
|
||||
@@ -24,7 +37,7 @@ module('Unit | Adapter | ldap/role', function (hooks) {
|
||||
|
||||
const assertRequest = (schema, req) => {
|
||||
assert.ok(req.queryParams.list, 'list query param sent when listing roles');
|
||||
const name = req.params.path === 'static-role' ? 'static-test' : 'dynamic-test';
|
||||
const name = req.url.includes('static-role') ? 'static-test' : 'dynamic-test';
|
||||
return { data: { keys: [name] } };
|
||||
};
|
||||
|
||||
@@ -176,15 +189,8 @@ module('Unit | Adapter | ldap/role', function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
this.store.pushPayload('ldap/role', {
|
||||
modelName: 'ldap/role',
|
||||
backend: 'ldap-test',
|
||||
name: 'test-role',
|
||||
});
|
||||
const record = this.store.peekRecord('ldap/role', 'test-role');
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
record.type = type;
|
||||
const record = this.getModel(type);
|
||||
await record.save();
|
||||
this.path = 'static-role';
|
||||
}
|
||||
@@ -201,18 +207,8 @@ module('Unit | Adapter | ldap/role', function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
const getModel = () => {
|
||||
this.store.pushPayload('ldap/role', {
|
||||
modelName: 'ldap/role',
|
||||
backend: 'ldap-test',
|
||||
name: 'test-role',
|
||||
});
|
||||
return this.store.peekRecord('ldap/role', 'test-role');
|
||||
};
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
const record = getModel();
|
||||
record.type = type;
|
||||
const record = this.getModel(type);
|
||||
await record.destroyRecord();
|
||||
this.path = 'static-role';
|
||||
}
|
||||
|
||||
76
ui/tests/unit/utils/ldap-breadcrumbs-test.js
Normal file
76
ui/tests/unit/utils/ldap-breadcrumbs-test.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | ldap breadcrumbs', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.mountPath = 'my-engine';
|
||||
this.roleType = 'static';
|
||||
this.testCrumbs = (path, { lastItemCurrent }) => {
|
||||
return ldapBreadcrumbs(path, this.roleType, this.mountPath, lastItemCurrent);
|
||||
};
|
||||
});
|
||||
|
||||
test('it generates crumbs when the path is a directory', function (assert) {
|
||||
const path = 'prod/org/';
|
||||
let actual = this.testCrumbs(path, { lastItemCurrent: true });
|
||||
let expected = [
|
||||
{ label: 'prod', route: 'roles.subdirectory', models: [this.mountPath, this.roleType, 'prod/'] },
|
||||
{ label: 'org' },
|
||||
];
|
||||
assert.propEqual(actual, expected, 'crumbs are correct when lastItemCurrent = true');
|
||||
|
||||
actual = this.testCrumbs(path, { lastItemCurrent: false });
|
||||
expected = [
|
||||
{ label: 'prod', route: 'roles.subdirectory', models: [this.mountPath, this.roleType, 'prod/'] },
|
||||
{ label: 'org', route: 'roles.subdirectory', models: [this.mountPath, this.roleType, 'prod/org/'] },
|
||||
];
|
||||
assert.propEqual(actual, expected, 'crumbs are correct when lastItemCurrent = false');
|
||||
});
|
||||
|
||||
test('it generates crumbs when the path is not a directory', function (assert) {
|
||||
const path = 'prod/org/admin';
|
||||
let actual = this.testCrumbs(path, { lastItemCurrent: true });
|
||||
let expected = [
|
||||
{ label: 'prod', route: 'roles.subdirectory', models: [this.mountPath, this.roleType, 'prod/'] },
|
||||
{ label: 'org', route: 'roles.subdirectory', models: [this.mountPath, this.roleType, 'prod/org/'] },
|
||||
{ label: 'admin' },
|
||||
];
|
||||
assert.propEqual(actual, expected, 'crumbs are correct when lastItemCurrent = true');
|
||||
|
||||
actual = this.testCrumbs(path, { lastItemCurrent: false });
|
||||
expected = [
|
||||
{ label: 'prod', route: 'roles.subdirectory', models: [this.mountPath, this.roleType, 'prod/'] },
|
||||
{ label: 'org', route: 'roles.subdirectory', models: [this.mountPath, this.roleType, 'prod/org/'] },
|
||||
{
|
||||
label: 'admin',
|
||||
route: 'roles.role.details',
|
||||
models: [this.mountPath, this.roleType, 'prod/org/admin'],
|
||||
},
|
||||
];
|
||||
assert.propEqual(actual, expected, 'crumbs are correct when lastItemCurrent = false');
|
||||
});
|
||||
|
||||
test('it generates crumbs when the path is the top-level', function (assert) {
|
||||
const path = 'prod/';
|
||||
let actual = this.testCrumbs(path, { lastItemCurrent: true });
|
||||
let expected = [{ label: 'prod' }];
|
||||
assert.propEqual(actual, expected, 'crumbs are correct when lastItemCurrent = true');
|
||||
|
||||
actual = this.testCrumbs(path, { lastItemCurrent: false });
|
||||
expected = [
|
||||
{ label: 'prod', route: 'roles.subdirectory', models: [this.mountPath, this.roleType, 'prod/'] },
|
||||
];
|
||||
assert.propEqual(actual, expected, 'crumbs are correct when lastItemCurrent = false');
|
||||
});
|
||||
|
||||
test('it fails gracefully when no path', function (assert) {
|
||||
const path = undefined;
|
||||
const actual = this.testCrumbs(path, { lastItemCurrent: false });
|
||||
assert.propEqual(actual, [], 'returns empty array when path is null');
|
||||
});
|
||||
});
|
||||
@@ -63,6 +63,9 @@ export interface WithFormFieldsAndValidationsModel extends WithFormFieldsModel,
|
||||
export interface Breadcrumb {
|
||||
label: string;
|
||||
route?: string;
|
||||
icon?: string;
|
||||
model?: string;
|
||||
models?: string[];
|
||||
linkExternal?: boolean;
|
||||
}
|
||||
|
||||
@@ -73,12 +76,6 @@ export interface TtlEvent {
|
||||
goSafeTimeString: string;
|
||||
}
|
||||
|
||||
export interface Breadcrumb {
|
||||
label: string;
|
||||
route?: string;
|
||||
linkExternal?: boolean;
|
||||
}
|
||||
|
||||
export interface EngineOwner extends Owner {
|
||||
mountPoint: string;
|
||||
}
|
||||
|
||||
2
ui/types/vault/models/ldap/role.d.ts
vendored
2
ui/types/vault/models/ldap/role.d.ts
vendored
@@ -7,8 +7,10 @@ import type { FormField } from 'vault/app-types';
|
||||
import CapabilitiesModel from '../capabilities';
|
||||
import { LdapDynamicRoleCredentials, LdapStaticRoleCredentials } from 'ldap/routes/roles/role/credentials';
|
||||
export default interface LdapRoleModel extends WithFormFieldsAndValidationsModel {
|
||||
id: string;
|
||||
type: string;
|
||||
backend: string;
|
||||
path_to_role: string;
|
||||
name: string;
|
||||
dn: string;
|
||||
username: string;
|
||||
|
||||
Reference in New Issue
Block a user