mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
UI: LDAP Hierarchical Library names (#29293)
* refactor crumbs * add subdirectory library route and hierarchical nav * update library breadcrumbs; * fix role popup menus * add getter to library model for full path * cleanup model getters * add changelog * add bug fix note * add transition after deleting * fix function definition * update adapter test * add test coverage * fix crumb typo
This commit is contained in:
6
changelog/29293.txt
Normal file
6
changelog/29293.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
```release-note:improvement
|
||||
ui: Adds navigation for LDAP hierarchical libraries
|
||||
```
|
||||
```release-note:bug
|
||||
ui: Fixes navigation for quick actions in LDAP roles' popup menu
|
||||
```
|
||||
@@ -7,23 +7,28 @@ import NamedPathAdapter from 'vault/adapters/named-path';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default class LdapLibraryAdapter extends NamedPathAdapter {
|
||||
getURL(backend, name) {
|
||||
// path could be the library name (full path) or just part of the path i.e. west-account/
|
||||
_getURL(backend, path) {
|
||||
const base = `${this.buildURL()}/${encodePath(backend)}/library`;
|
||||
return name ? `${base}/${name}` : base;
|
||||
return path ? `${base}/${path}` : base;
|
||||
}
|
||||
|
||||
urlForUpdateRecord(name, modelName, snapshot) {
|
||||
return this.getURL(snapshot.attr('backend'), name);
|
||||
// when editing the name IS the full path so we can use "name" instead of "completeLibraryName" here
|
||||
return this._getURL(snapshot.attr('backend'), name);
|
||||
}
|
||||
urlForDeleteRecord(name, modelName, snapshot) {
|
||||
return this.getURL(snapshot.attr('backend'), name);
|
||||
const { backend, completeLibraryName } = snapshot.record;
|
||||
return this._getURL(backend, completeLibraryName);
|
||||
}
|
||||
|
||||
query(store, type, query) {
|
||||
const { backend } = query;
|
||||
return this.ajax(this.getURL(backend), 'GET', { data: { list: true } })
|
||||
const { backend, path_to_library } = query;
|
||||
// if we have a path_to_library then we're listing subdirectories at a hierarchical library path (i.e west-account/my-library)
|
||||
const url = this._getURL(backend, path_to_library);
|
||||
return this.ajax(url, 'GET', { data: { list: true } })
|
||||
.then((resp) => {
|
||||
return resp.data.keys.map((name) => ({ name, backend }));
|
||||
return resp.data.keys.map((name) => ({ name, backend, path_to_library }));
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.httpStatus === 404) {
|
||||
@@ -34,11 +39,11 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
|
||||
}
|
||||
queryRecord(store, type, query) {
|
||||
const { backend, name } = query;
|
||||
return this.ajax(this.getURL(backend, name), 'GET').then((resp) => ({ ...resp.data, backend, name }));
|
||||
return this.ajax(this._getURL(backend, name), 'GET').then((resp) => ({ ...resp.data, backend, name }));
|
||||
}
|
||||
|
||||
fetchStatus(backend, name) {
|
||||
const url = `${this.getURL(backend, name)}/status`;
|
||||
const url = `${this._getURL(backend, name)}/status`;
|
||||
return this.ajax(url, 'GET').then((resp) => {
|
||||
const statuses = [];
|
||||
for (const key in resp.data) {
|
||||
@@ -53,7 +58,7 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
|
||||
});
|
||||
}
|
||||
checkOutAccount(backend, name, ttl) {
|
||||
const url = `${this.getURL(backend, name)}/check-out`;
|
||||
const url = `${this._getURL(backend, name)}/check-out`;
|
||||
return this.ajax(url, 'POST', { data: { ttl } }).then((resp) => {
|
||||
const { lease_id, lease_duration, renewable } = resp;
|
||||
const { service_account_name: account, password } = resp.data;
|
||||
@@ -61,7 +66,7 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
|
||||
});
|
||||
}
|
||||
checkInAccount(backend, name, service_account_names) {
|
||||
const url = `${this.getURL(backend, name)}/check-in`;
|
||||
const url = `${this._getURL(backend, name)}/check-in`;
|
||||
return this.ajax(url, 'POST', { data: { service_account_names } }).then((resp) => resp.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ export default class LdapRoleAdapter extends ApplicationAdapter {
|
||||
}
|
||||
|
||||
urlForDeleteRecord(id, modelName, snapshot) {
|
||||
const { backend, type, name } = snapshot.record;
|
||||
return this._getURL(backend, this._pathForRoleType(type), name);
|
||||
const { backend, type, completeRoleName } = snapshot.record;
|
||||
return this._getURL(backend, this._pathForRoleType(type), completeRoleName);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -18,6 +18,7 @@ const formFields = ['name', 'service_account_names', 'ttl', 'max_ttl', 'disable_
|
||||
@withFormFields(formFields)
|
||||
export default class LdapLibraryModel extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
||||
@attr('string') path_to_library; // ancestral path to the library added in the adapter (only exists for nested libraries)
|
||||
|
||||
@attr('string', {
|
||||
label: 'Library name',
|
||||
@@ -64,6 +65,12 @@ export default class LdapLibraryModel extends Model {
|
||||
})
|
||||
disable_check_in_enforcement;
|
||||
|
||||
get completeLibraryName() {
|
||||
// if there is a path_to_library then the name is hierarchical
|
||||
// and we must concat the ancestors with the leaf name to get the full library path
|
||||
return this.path_to_library ? this.path_to_library + this.name : this.name;
|
||||
}
|
||||
|
||||
get displayFields() {
|
||||
return this.formFields.filter((field) => field.name !== 'service_account_names');
|
||||
}
|
||||
|
||||
@@ -163,6 +163,12 @@ export default class LdapRoleModel extends Model {
|
||||
})
|
||||
rollback_ldif;
|
||||
|
||||
get completeRoleName() {
|
||||
// if there is a path_to_role then the name is hierarchical
|
||||
// and we must concat the ancestors with the leaf name to get the full role path
|
||||
return this.path_to_role ? this.path_to_role + this.name : this.name;
|
||||
}
|
||||
|
||||
get isStatic() {
|
||||
return this.type === 'static';
|
||||
}
|
||||
@@ -224,9 +230,11 @@ export default class LdapRoleModel extends Model {
|
||||
}
|
||||
|
||||
fetchCredentials() {
|
||||
return this.store.adapterFor('ldap/role').fetchCredentials(this.backend, this.type, this.name);
|
||||
return this.store
|
||||
.adapterFor('ldap/role')
|
||||
.fetchCredentials(this.backend, this.type, this.completeRoleName);
|
||||
}
|
||||
rotateStaticPassword() {
|
||||
return this.store.adapterFor('ldap/role').rotateStaticPassword(this.backend, this.name);
|
||||
return this.store.adapterFor('ldap/role').rotateStaticPassword(this.backend, this.completeRoleName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@
|
||||
{{else}}
|
||||
<div class="has-bottom-margin-s">
|
||||
{{#each this.filteredLibraries as |library|}}
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "libraries.library.details" library.name}} as |Item|>
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{this.linkParams library}} as |Item|>
|
||||
<Item.content>
|
||||
<Icon @name="folder" />
|
||||
<span data-test-library={{library.name}}>{{library.name}}</span>
|
||||
<span data-test-library={{library.completeLibraryName}}>{{library.name}}</span>
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
{{#if (or library.canRead library.canEdit library.canDelete)}}
|
||||
@@ -55,24 +55,36 @@
|
||||
@icon="more-horizontal"
|
||||
@text="More options"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
data-test-popup-menu-trigger={{library.completeLibraryName}}
|
||||
/>
|
||||
{{#if library.canEdit}}
|
||||
<dd.Interactive data-test-edit @route="libraries.library.edit" @model={{library}}>Edit</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if library.canRead}}
|
||||
{{#if (this.isHierarchical library.name)}}
|
||||
<dd.Interactive
|
||||
data-test-details
|
||||
@route="libraries.library.details"
|
||||
@model={{library}}
|
||||
>Details</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if library.canDelete}}
|
||||
<dd.Interactive
|
||||
data-test-delete
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.libraryToDelete) library)}}
|
||||
>Delete</dd.Interactive>
|
||||
data-test-subdirectory
|
||||
@route="libraries.subdirectory"
|
||||
@model={{library.completeLibraryName}}
|
||||
>Content</dd.Interactive>
|
||||
{{else}}
|
||||
{{#if library.canEdit}}
|
||||
<dd.Interactive
|
||||
data-test-edit
|
||||
@route="libraries.library.edit"
|
||||
@model={{library.completeLibraryName}}
|
||||
>Edit</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if library.canRead}}
|
||||
<dd.Interactive
|
||||
data-test-details
|
||||
@route="libraries.library.details"
|
||||
@model={{library.completeLibraryName}}
|
||||
>Details</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if library.canDelete}}
|
||||
<dd.Interactive
|
||||
data-test-delete
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.libraryToDelete) library)}}
|
||||
>Delete</dd.Interactive>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/if}}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
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';
|
||||
|
||||
interface Args {
|
||||
libraries: Array<LdapLibraryModel>;
|
||||
@@ -24,10 +25,18 @@ interface Args {
|
||||
|
||||
export default class LdapLibrariesPageComponent extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
|
||||
@tracked filterValue = '';
|
||||
@tracked libraryToDelete: LdapLibraryModel | null = null;
|
||||
|
||||
isHierarchical = (name: string) => name.endsWith('/');
|
||||
|
||||
linkParams = (library: LdapLibraryModel) => {
|
||||
const route = this.isHierarchical(library.name) ? 'libraries.subdirectory' : 'libraries.library.details';
|
||||
return [route, library.completeLibraryName];
|
||||
};
|
||||
|
||||
get mountPoint(): string {
|
||||
const owner = getOwner(this) as EngineOwner;
|
||||
return owner.mountPoint;
|
||||
@@ -43,8 +52,9 @@ export default class LdapLibrariesPageComponent extends Component<Args> {
|
||||
@action
|
||||
async onDelete(model: LdapLibraryModel) {
|
||||
try {
|
||||
const message = `Successfully deleted library ${model.name}.`;
|
||||
const message = `Successfully deleted library ${model.completeLibraryName}.`;
|
||||
await model.destroyRecord();
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries');
|
||||
this.flashMessages.success(message);
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(`Error deleting library \n ${errorMessage(error)}`);
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<dd.Interactive
|
||||
data-test-subdirectory
|
||||
@route="roles.subdirectory"
|
||||
@models={{array role.type (concat role.path_to_role role.name)}}
|
||||
@models={{array role.type role.completeRoleName}}
|
||||
>Content</dd.Interactive>
|
||||
{{else}}
|
||||
{{#if role.canEdit}}
|
||||
@@ -72,7 +72,11 @@
|
||||
>Edit</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canReadCreds}}
|
||||
<dd.Interactive data-test-get-creds @route="roles.role.credentials" @models={{array role.type role.name}}>
|
||||
<dd.Interactive
|
||||
data-test-get-creds
|
||||
@route="roles.role.credentials"
|
||||
@models={{array role.type role.completeRoleName}}
|
||||
>
|
||||
Get credentials
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
@@ -87,7 +91,7 @@
|
||||
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}}
|
||||
@models={{array role.type role.completeRoleName}}
|
||||
>Details</dd.Interactive>
|
||||
{{#if role.canDelete}}
|
||||
<dd.Interactive
|
||||
|
||||
@@ -37,10 +37,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
||||
|
||||
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];
|
||||
return [route, role.type, role.completeRoleName];
|
||||
};
|
||||
|
||||
get mountPoint(): string {
|
||||
@@ -61,7 +58,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
||||
@action
|
||||
async onRotate(model: LdapRoleModel) {
|
||||
try {
|
||||
const message = `Successfully rotated credentials for ${model.name}.`;
|
||||
const message = `Successfully rotated credentials for ${model.completeRoleName}.`;
|
||||
await model.rotateStaticPassword();
|
||||
this.flashMessages.success(message);
|
||||
} catch (error) {
|
||||
@@ -74,7 +71,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
||||
@action
|
||||
async onDelete(model: LdapRoleModel) {
|
||||
try {
|
||||
const message = `Successfully deleted role ${model.name}.`;
|
||||
const message = `Successfully deleted role ${model.completeRoleName}.`;
|
||||
await model.destroyRecord();
|
||||
this.pagination.clearDataset('ldap/role');
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles');
|
||||
|
||||
@@ -19,6 +19,8 @@ export default buildRoutes(function () {
|
||||
});
|
||||
this.route('libraries', function () {
|
||||
this.route('create');
|
||||
// wildcard route so we can traverse hierarchical libraries i.e. prod/admin/my-library
|
||||
this.route('subdirectory', { path: '/subdirectory/*path_to_library' });
|
||||
this.route('library', { path: '/:name' }, function () {
|
||||
this.route('details', function () {
|
||||
this.route('accounts');
|
||||
|
||||
@@ -16,6 +16,7 @@ import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import { LdapLibraryCheckOutCredentials } from 'vault/vault/adapters/ldap/library';
|
||||
import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
|
||||
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
interface LdapLibraryCheckOutController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
@@ -45,12 +46,14 @@ export default class LdapLibraryCheckOutRoute extends Route {
|
||||
transition: Transition
|
||||
) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const library = this.modelFor('libraries.library') as LdapLibraryModel;
|
||||
const routeParams = (childResource: string) => {
|
||||
return [library.backend, childResource];
|
||||
};
|
||||
controller.breadcrumbs = [
|
||||
{ label: library.backend, route: 'overview' },
|
||||
{ label: 'Libraries', route: 'libraries' },
|
||||
{ label: library.name, route: 'libraries.library' },
|
||||
...ldapBreadcrumbs(library.name, routeParams, libraryRoutes),
|
||||
{ label: 'Check-Out' },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
|
||||
import type Controller from '@ember/controller';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
interface LdapLibraryDetailsController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
@@ -23,10 +24,14 @@ export default class LdapLibraryDetailsRoute extends Route {
|
||||
) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const routeParams = (childResource: string) => {
|
||||
return [resolvedModel.backend, childResource];
|
||||
};
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'Libraries', route: 'libraries' },
|
||||
{ label: resolvedModel.name },
|
||||
...ldapBreadcrumbs(resolvedModel.name, routeParams, libraryRoutes, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
|
||||
import type Controller from '@ember/controller';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
interface LdapLibraryEditController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
@@ -23,10 +24,13 @@ export default class LdapLibraryEditRoute extends Route {
|
||||
) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const routeParams = (childResource: string) => {
|
||||
return [resolvedModel.backend, childResource];
|
||||
};
|
||||
controller.breadcrumbs = [
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'Libraries', route: 'libraries' },
|
||||
{ label: resolvedModel.name, route: 'libraries.library.details' },
|
||||
...ldapBreadcrumbs(resolvedModel.name, routeParams, libraryRoutes),
|
||||
{ label: 'Edit' },
|
||||
];
|
||||
}
|
||||
|
||||
60
ui/lib/ldap/addon/routes/libraries/subdirectory.ts
Normal file
60
ui/lib/ldap/addon/routes/libraries/subdirectory.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type LdapLibraryModel from 'vault/models/ldap/library';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
path_to_library: string;
|
||||
libraries: Array<LdapLibraryModel>;
|
||||
}
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: RouteModel;
|
||||
}
|
||||
interface RouteParams {
|
||||
path_to_library?: string;
|
||||
}
|
||||
|
||||
export default class LdapLibrariesSubdirectoryRoute extends Route {
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
model(params: RouteParams) {
|
||||
const backendModel = this.modelFor('application') as SecretEngineModel;
|
||||
const { path_to_library } = params;
|
||||
return hash({
|
||||
backendModel,
|
||||
path_to_library,
|
||||
libraries: this.store.query('ldap/library', { backend: backendModel.id, path_to_library }),
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const routeParams = (childResource: string) => {
|
||||
return [resolvedModel.backendModel.id, childResource];
|
||||
};
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backendModel.id, route: 'overview' },
|
||||
{ label: 'Libraries', route: 'libraries' },
|
||||
...ldapBreadcrumbs(resolvedModel.path_to_library, routeParams, libraryRoutes, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
import { ldapBreadcrumbs, roleRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
@@ -58,11 +58,14 @@ export default class LdapRoleCredentialsRoute extends Route {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const role = this.modelFor('roles.role') as LdapRoleModel;
|
||||
const routeParams = (childResource: string) => {
|
||||
return [role.backend, role.type, childResource];
|
||||
};
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: role.backend, route: 'overview' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(role.name, role.type, role.backend),
|
||||
...ldapBreadcrumbs(role.name, routeParams, roleRoutes),
|
||||
{ label: 'Credentials' },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
import { ldapBreadcrumbs, roleRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type Controller from '@ember/controller';
|
||||
@@ -24,11 +24,15 @@ export default class LdapRolesRoleDetailsRoute extends Route {
|
||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const routeParams = (childResource: string) => {
|
||||
return [this.secretMountPath.currentPath, resolvedModel.type, childResource];
|
||||
};
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, this.secretMountPath.currentPath, true),
|
||||
...ldapBreadcrumbs(resolvedModel.name, routeParams, roleRoutes, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
import { ldapBreadcrumbs, roleRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type Controller from '@ember/controller';
|
||||
@@ -20,11 +20,15 @@ export default class LdapRoleEditRoute extends Route {
|
||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const routeParams = (childResource: string) => {
|
||||
return [resolvedModel.backend, resolvedModel.type, childResource];
|
||||
};
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, resolvedModel.backend),
|
||||
...ldapBreadcrumbs(resolvedModel.name, routeParams, roleRoutes),
|
||||
{ label: 'Edit' },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import LdapRolesRoute from '../roles';
|
||||
import { hash } from 'rsvp';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
import { ldapBreadcrumbs, roleRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type Controller from '@ember/controller';
|
||||
@@ -55,11 +55,16 @@ export default class LdapRolesSubdirectoryRoute extends LdapRolesRoute {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
const { backendModel, roleAncestry } = resolvedModel;
|
||||
|
||||
const routeParams = (childResource: string) => {
|
||||
return [backendModel.id, roleAncestry.type, childResource];
|
||||
};
|
||||
|
||||
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),
|
||||
...ldapBreadcrumbs(roleAncestry.path_to_role, routeParams, roleRoutes, true),
|
||||
];
|
||||
|
||||
// must call 'set' so breadcrumbs update as we navigate through directories
|
||||
|
||||
11
ui/lib/ldap/addon/templates/libraries/subdirectory.hbs
Normal file
11
ui/lib/ldap/addon/templates/libraries/subdirectory.hbs
Normal file
@@ -0,0 +1,11 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<Page::Libraries
|
||||
@libraries={{this.model.libraries}}
|
||||
@promptConfig={{false}}
|
||||
@backendModel={{this.model.backendModel}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
@@ -4,10 +4,16 @@
|
||||
*/
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
export const roleRoutes = { details: 'roles.role.details', subdirectory: 'roles.subdirectory' };
|
||||
export const libraryRoutes = {
|
||||
details: 'libraries.library.details',
|
||||
subdirectory: 'libraries.subdirectory',
|
||||
};
|
||||
|
||||
export const ldapBreadcrumbs = (
|
||||
fullPath: string | undefined, // i.e. path/to/item
|
||||
roleType: string,
|
||||
mountPath: string,
|
||||
routeParams: (childResource: string) => string[], // array of route param strings
|
||||
routes: { details: string; subdirectory: string },
|
||||
lastItemCurrent = false // this array of objects can be spread anywhere within the crumbs array
|
||||
): Breadcrumb[] => {
|
||||
if (!fullPath) return [];
|
||||
@@ -26,11 +32,10 @@ export const ldapBreadcrumbs = (
|
||||
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,
|
||||
route: isLast && !isDirectory ? routes.details : routes.subdirectory,
|
||||
models: routeParams(itemPath),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -46,14 +46,16 @@ export default function (server) {
|
||||
};
|
||||
|
||||
const listOrGetRecord = (schema, req, type) => {
|
||||
// if the param name is admin, we want to LIST admin/ roles
|
||||
const dbKey = type ? 'ldapRoles' : 'ldapLibraries';
|
||||
const query = type ? { type, name: `admin/child-${type}-role` } : { name: 'admin/test-library' };
|
||||
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` });
|
||||
// the mirage database has setup all hierarchical names to be prefixed with "admin/"
|
||||
// while passing a query with specific name is not flexible, for simplicity
|
||||
// we only seeded the mirage db with one hierarchical resource for each role and a library
|
||||
return listRecords(schema, dbKey, query);
|
||||
}
|
||||
// otherwise we want to view details for a specific role
|
||||
return getRecord(schema, req, 'ldapRoles', type);
|
||||
// otherwise we want to view details for a specific resource
|
||||
return getRecord(schema, req, dbKey);
|
||||
};
|
||||
|
||||
// config
|
||||
@@ -77,9 +79,9 @@ export default function (server) {
|
||||
}));
|
||||
// libraries
|
||||
server.post('/:backend/library/:name', (schema, req) => createOrUpdateRecord(schema, req, 'ldapLibraries'));
|
||||
server.get('/:backend/library/:name', (schema, req) => getRecord(schema, req, 'ldapLibraries'));
|
||||
server.get('/:backend/library/*name', (schema, req) => listOrGetRecord(schema, req));
|
||||
server.get('/:backend/library', (schema) => listRecords(schema, 'ldapLibraries'));
|
||||
server.get('/:backend/library/:name/status', (schema) => {
|
||||
server.get('/:backend/library/*name/status', (schema) => {
|
||||
const data = schema.db['ldapAccountStatuses'].reduce((prev, curr) => {
|
||||
prev[curr.account] = {
|
||||
available: curr.available,
|
||||
|
||||
@@ -14,6 +14,8 @@ export default function (server) {
|
||||
server.create('ldap-role', 'static', { name: 'my-role' });
|
||||
server.create('ldap-role', 'dynamic', { name: 'my-role' });
|
||||
server.create('ldap-library', { name: 'test-library' });
|
||||
// mirage handler is hardcoded to accommodate hierarchical paths starting with 'admin/'
|
||||
server.create('ldap-library', { name: 'admin/test-library' });
|
||||
server.create('ldap-account-status', {
|
||||
id: 'bob.johnson',
|
||||
account: 'bob.johnson',
|
||||
|
||||
@@ -10,9 +10,10 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import ldapMirageScenario from 'vault/mirage/scenarios/ldap';
|
||||
import ldapHandlers from 'vault/mirage/handlers/ldap';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { click } from '@ember/test-helpers';
|
||||
import { click, currentURL } from '@ember/test-helpers';
|
||||
import { isURL, visitURL } from 'vault/tests/helpers/ldap/ldap-helpers';
|
||||
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
|
||||
import { LDAP_SELECTORS } from 'vault/tests/helpers/ldap/ldap-selectors';
|
||||
|
||||
module('Acceptance | ldap | libraries', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -37,7 +38,7 @@ module('Acceptance | ldap | libraries', function (hooks) {
|
||||
|
||||
test('it should show libraries on overview page', async function (assert) {
|
||||
await visitURL('overview', this.backend);
|
||||
assert.dom('[data-test-libraries-count]').hasText('1');
|
||||
assert.dom('[data-test-libraries-count]').hasText('2');
|
||||
});
|
||||
|
||||
test('it should transition to create library route on toolbar link click', async function (assert) {
|
||||
@@ -49,15 +50,34 @@ module('Acceptance | ldap | libraries', function (hooks) {
|
||||
});
|
||||
|
||||
test('it should transition to library details route on list item click', async function (assert) {
|
||||
await click('[data-test-list-item-link] a');
|
||||
assert.true(
|
||||
isURL('libraries/test-library/details/accounts', this.backend),
|
||||
await click(LDAP_SELECTORS.libraryItem('test-library'));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${this.backend}/ldap/libraries/test-library/details/accounts`,
|
||||
'Transitions to library details accounts route on list item click'
|
||||
);
|
||||
assert.dom('[data-test-account-name]').exists({ count: 2 }, 'lists the accounts');
|
||||
assert.dom('[data-test-checked-out-account]').exists({ count: 1 }, 'lists the checked out accounts');
|
||||
});
|
||||
|
||||
test('it should transition to library details for hierarchical list items', async function (assert) {
|
||||
await click(LDAP_SELECTORS.libraryItem('admin/'));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${this.backend}/ldap/libraries/subdirectory/admin/`,
|
||||
'Transitions to subdirectory list view'
|
||||
);
|
||||
|
||||
await click(LDAP_SELECTORS.libraryItem('admin/test-library'));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${this.backend}/ldap/libraries/admin%2Ftest-library/details/accounts`,
|
||||
'Transitions to child library details accounts'
|
||||
);
|
||||
assert.dom('[data-test-account-name]').exists({ count: 2 }, 'lists the accounts');
|
||||
assert.dom('[data-test-checked-out-account]').exists({ count: 1 }, 'lists the checked out accounts');
|
||||
});
|
||||
|
||||
test('it should transition to routes from list item action menu', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
export const LDAP_SELECTORS = {
|
||||
roleItem: (type: string, name: string) => `[data-test-role="${type} ${name}"]`,
|
||||
libraryItem: (name: string) => `[data-test-library="${name}"]`,
|
||||
roleMenu: (type: string, name: string) => `[data-test-popup-menu-trigger="${type} ${name}"]`,
|
||||
libraryMenu: (name: string) => `[data-test-popup-menu-trigger="${name}"]`,
|
||||
action: (action: string) => `[data-test-${action}]`,
|
||||
};
|
||||
|
||||
@@ -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 { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { LDAP_SELECTORS } from 'vault/tests/helpers/ldap/ldap-selectors';
|
||||
|
||||
module('Integration | Component | ldap | Page::Libraries', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@@ -25,7 +26,7 @@ module('Integration | Component | ldap | Page::Libraries', function (hooks) {
|
||||
this.backend = createSecretsEngine(this.store);
|
||||
this.breadcrumbs = generateBreadcrumbs(this.backend.id);
|
||||
|
||||
for (const name of ['foo', 'bar']) {
|
||||
for (const name of ['foo', 'bar', 'foo/']) {
|
||||
this.store.pushPayload('ldap/library', {
|
||||
modelName: 'ldap/library',
|
||||
backend: 'ldap-test',
|
||||
@@ -93,12 +94,19 @@ module('Integration | Component | ldap | Page::Libraries', function (hooks) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom('[data-test-list-item-content] svg').hasClass('hds-icon-folder', 'List item icon renders');
|
||||
assert.dom('[data-test-library]').hasText(this.libraries[0].name, 'List item name renders');
|
||||
assert.dom('[data-test-library="foo"]').hasText('foo', 'List item name renders');
|
||||
|
||||
await click('[data-test-popup-menu-trigger]');
|
||||
await click(LDAP_SELECTORS.libraryMenu('foo'));
|
||||
assert.dom('[data-test-subdirectory]').doesNotExist();
|
||||
assert.dom('[data-test-edit]').hasText('Edit', 'Edit link renders in menu');
|
||||
assert.dom('[data-test-details]').hasText('Details', 'Details link renders in menu');
|
||||
assert.dom('[data-test-delete]').hasText('Delete', 'Details link renders in menu');
|
||||
|
||||
await click(LDAP_SELECTORS.libraryMenu('foo/'));
|
||||
assert.dom('[data-test-subdirectory]').hasText('Content', 'Content link renders in menu');
|
||||
assert.dom('[data-test-edit]').doesNotExist();
|
||||
assert.dom('[data-test-details]').doesNotExist();
|
||||
assert.dom('[data-test-delete]').doesNotExist();
|
||||
});
|
||||
|
||||
test('it should filter libraries', async function (assert) {
|
||||
@@ -110,11 +118,11 @@ module('Integration | Component | ldap | Page::Libraries', function (hooks) {
|
||||
.hasText('There are no libraries matching "baz"', 'Filter message renders');
|
||||
|
||||
await fillIn('[data-test-filter-input]', 'foo');
|
||||
assert.dom('[data-test-list-item-content]').exists({ count: 1 }, 'List is filtered with correct results');
|
||||
assert.dom('[data-test-list-item-content]').exists({ count: 2 }, 'List is filtered with correct results');
|
||||
|
||||
await fillIn('[data-test-filter-input]', '');
|
||||
assert
|
||||
.dom('[data-test-list-item-content]')
|
||||
.exists({ count: 2 }, 'All libraries are displayed when filter is cleared');
|
||||
.exists({ count: 3 }, 'All libraries are displayed when filter is cleared');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,8 +19,8 @@ module('Unit | Adapter | ldap/role', function (hooks) {
|
||||
this.adapter = this.store.adapterFor('ldap/role');
|
||||
this.path = 'role';
|
||||
|
||||
this.getModel = (type) => {
|
||||
const name = 'test-role';
|
||||
this.getModel = (type, roleName) => {
|
||||
const name = roleName || 'test-role';
|
||||
this.store.pushPayload('ldap/role', {
|
||||
modelName: 'ldap/role',
|
||||
backend: 'ldap-test',
|
||||
@@ -32,214 +32,296 @@ module('Unit | Adapter | ldap/role', function (hooks) {
|
||||
};
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when listing records', async function (assert) {
|
||||
assert.expect(6);
|
||||
module('happy paths', function () {
|
||||
test('it should make request to correct endpoints when listing records', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const assertRequest = (schema, req) => {
|
||||
assert.ok(req.queryParams.list, 'list query param sent when listing roles');
|
||||
const name = req.url.includes('static-role') ? 'static-test' : 'dynamic-test';
|
||||
return { data: { keys: [name] } };
|
||||
};
|
||||
const assertRequest = (schema, req) => {
|
||||
assert.ok(req.queryParams.list, 'list query param sent when listing roles');
|
||||
const name = req.url.includes('static-role') ? 'static-test' : 'dynamic-test';
|
||||
return { data: { keys: [name] } };
|
||||
};
|
||||
|
||||
this.server.get('/ldap-test/static-role', assertRequest);
|
||||
this.server.get('/ldap-test/role', assertRequest);
|
||||
this.server.get('/ldap-test/static-role', assertRequest);
|
||||
this.server.get('/ldap-test/role', assertRequest);
|
||||
|
||||
this.models = await this.store.query('ldap/role', { backend: 'ldap-test' });
|
||||
this.models = await this.store.query('ldap/role', { backend: 'ldap-test' });
|
||||
|
||||
const model = this.models[0];
|
||||
assert.strictEqual(this.models.length, 2, 'Returns responses from both endpoints');
|
||||
assert.strictEqual(model.backend, 'ldap-test', 'Backend value is set on records returned from query');
|
||||
// sorted alphabetically by name so dynamic should be first
|
||||
assert.strictEqual(model.type, 'dynamic', 'Type value is set on records returned from query');
|
||||
assert.strictEqual(model.name, 'dynamic-test', 'Name value is set on records returned from query');
|
||||
});
|
||||
|
||||
test('it should conditionally trigger info level flash message for single endpoint error from query', async function (assert) {
|
||||
const flashMessages = this.owner.lookup('service:flashMessages');
|
||||
const flashSpy = sinon.spy(flashMessages, 'info');
|
||||
|
||||
this.server.get('/ldap-test/static-role', () => {
|
||||
return new Response(403, {}, { errors: ['permission denied'] });
|
||||
});
|
||||
this.server.get('/ldap-test/role', () => ({ data: { keys: ['dynamic-test'] } }));
|
||||
|
||||
await this.store.query('ldap/role', { backend: 'ldap-test' });
|
||||
await this.store.query(
|
||||
'ldap/role',
|
||||
{ backend: 'ldap-test' },
|
||||
{ adapterOptions: { showPartialError: true } }
|
||||
);
|
||||
|
||||
assert.true(
|
||||
flashSpy.calledOnceWith('Error fetching roles from /v1/ldap-test/static-role: permission denied'),
|
||||
'Partial error info only displays when adapter option is passed'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw error for query when requests to both endpoints fail', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.get('/ldap-test/:path', (schema, req) => {
|
||||
const errors = {
|
||||
'static-role': ['permission denied'],
|
||||
role: ['server error'],
|
||||
}[req.params.path];
|
||||
return new Response(req.params.path === 'static-role' ? 403 : 500, {}, { errors });
|
||||
const model = this.models[0];
|
||||
assert.strictEqual(this.models.length, 2, 'Returns responses from both endpoints');
|
||||
assert.strictEqual(model.backend, 'ldap-test', 'Backend value is set on records returned from query');
|
||||
// sorted alphabetically by name so dynamic should be first
|
||||
assert.strictEqual(model.type, 'dynamic', 'Type value is set on records returned from query');
|
||||
assert.strictEqual(model.name, 'dynamic-test', 'Name value is set on records returned from query');
|
||||
});
|
||||
|
||||
try {
|
||||
test('it should conditionally trigger info level flash message for single endpoint error from query', async function (assert) {
|
||||
const flashMessages = this.owner.lookup('service:flashMessages');
|
||||
const flashSpy = sinon.spy(flashMessages, 'info');
|
||||
|
||||
this.server.get('/ldap-test/static-role', () => {
|
||||
return new Response(403, {}, { errors: ['permission denied'] });
|
||||
});
|
||||
this.server.get('/ldap-test/role', () => ({ data: { keys: ['dynamic-test'] } }));
|
||||
|
||||
await this.store.query('ldap/role', { backend: 'ldap-test' });
|
||||
} catch (error) {
|
||||
assert.deepEqual(
|
||||
error.errors,
|
||||
['/v1/ldap-test/static-role: permission denied', '/v1/ldap-test/role: server error'],
|
||||
'Error messages is thrown with correct payload from query.'
|
||||
await this.store.query(
|
||||
'ldap/role',
|
||||
{ backend: 'ldap-test' },
|
||||
{ adapterOptions: { showPartialError: true } }
|
||||
);
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'Error fetching roles:',
|
||||
'Error message is thrown with correct payload from query.'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when querying record', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.server.get('/ldap-test/:path/test-role', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'GET request made to correct endpoint when querying record'
|
||||
assert.true(
|
||||
flashSpy.calledOnceWith('Error fetching roles from /v1/ldap-test/static-role: permission denied'),
|
||||
'Partial error info only displays when adapter option is passed'
|
||||
);
|
||||
});
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
this.model = await this.store.queryRecord('ldap/role', {
|
||||
backend: 'ldap-test',
|
||||
type,
|
||||
name: 'test-role',
|
||||
test('it should throw error for query when requests to both endpoints fail', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.get('/ldap-test/:path', (schema, req) => {
|
||||
const errors = {
|
||||
'static-role': ['permission denied'],
|
||||
role: ['server error'],
|
||||
}[req.params.path];
|
||||
return new Response(req.params.path === 'static-role' ? 403 : 500, {}, { errors });
|
||||
});
|
||||
this.path = 'static-role';
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
this.model.backend,
|
||||
'ldap-test',
|
||||
'Backend value is set on records returned from query'
|
||||
);
|
||||
assert.strictEqual(this.model.type, 'static', 'Type value is set on records returned from query');
|
||||
assert.strictEqual(this.model.name, 'test-role', 'Name value is set on records returned from query');
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when creating new dynamic role record', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/ldap-test/:path/:name', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'POST request made to correct endpoint when creating new record for a dynamic role'
|
||||
);
|
||||
try {
|
||||
await this.store.query('ldap/role', { backend: 'ldap-test' });
|
||||
} catch (error) {
|
||||
assert.deepEqual(
|
||||
error.errors,
|
||||
['/v1/ldap-test/static-role: permission denied', '/v1/ldap-test/role: server error'],
|
||||
'Error messages is thrown with correct payload from query.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'Error fetching roles:',
|
||||
'Error message is thrown with correct payload from query.'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const getModel = (type, name) => {
|
||||
return this.store.createRecord('ldap/role', {
|
||||
backend: 'ldap-test',
|
||||
name,
|
||||
type,
|
||||
test('it should make request to correct endpoints when querying record', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.server.get('/ldap-test/:path/test-role', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'GET request made to correct endpoint when querying record'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const model = getModel('dynamic-role', 'dynamic-role-name');
|
||||
await model.save();
|
||||
});
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
this.model = await this.store.queryRecord('ldap/role', {
|
||||
backend: 'ldap-test',
|
||||
type,
|
||||
name: 'test-role',
|
||||
});
|
||||
this.path = 'static-role';
|
||||
}
|
||||
|
||||
test('it should make request to correct endpoints when creating new static role record', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/ldap-test/:path/:name', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'POST request made to correct endpoint when creating new record for a static role'
|
||||
this.model.backend,
|
||||
'ldap-test',
|
||||
'Backend value is set on records returned from query'
|
||||
);
|
||||
assert.strictEqual(this.model.type, 'static', 'Type value is set on records returned from query');
|
||||
assert.strictEqual(this.model.name, 'test-role', 'Name value is set on records returned from query');
|
||||
});
|
||||
|
||||
const getModel = (type, name) => {
|
||||
return this.store.createRecord('ldap/role', {
|
||||
backend: 'ldap-test',
|
||||
name,
|
||||
type,
|
||||
test('it should make request to correct endpoints when creating new dynamic role record', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/ldap-test/:path/:name', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'POST request made to correct endpoint when creating new record for a dynamic role'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const model = getModel('static-role', 'static-role-name');
|
||||
await model.save();
|
||||
const getModel = (type, name) => {
|
||||
return this.store.createRecord('ldap/role', {
|
||||
backend: 'ldap-test',
|
||||
name,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
const model = getModel('dynamic-role', 'dynamic-role-name');
|
||||
await model.save();
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when creating new static role record', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/ldap-test/:path/:name', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'POST request made to correct endpoint when creating new record for a static role'
|
||||
);
|
||||
});
|
||||
|
||||
const getModel = (type, name) => {
|
||||
return this.store.createRecord('ldap/role', {
|
||||
backend: 'ldap-test',
|
||||
name,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
const model = getModel('static-role', 'static-role-name');
|
||||
await model.save();
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when updating record', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.post('/ldap-test/:path/test-role', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'POST request made to correct endpoint when updating record'
|
||||
);
|
||||
});
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
const record = this.getModel(type);
|
||||
await record.save();
|
||||
this.path = 'static-role';
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when deleting record', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.delete('/ldap-test/:path/test-role', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'DELETE request made to correct endpoint when deleting record'
|
||||
);
|
||||
});
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
const record = this.getModel(type);
|
||||
await record.destroyRecord();
|
||||
this.path = 'static-role';
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when fetching credentials', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.path = 'creds';
|
||||
|
||||
this.server.get('/ldap-test/:path/test-role', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'GET request made to correct endpoint when fetching credentials'
|
||||
);
|
||||
});
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
await this.adapter.fetchCredentials('ldap-test', type, 'test-role');
|
||||
this.path = 'static-cred';
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when rotating static role password', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/ldap-test/rotate-role/test-role', () => {
|
||||
assert.ok('GET request made to correct endpoint when rotating static role password');
|
||||
});
|
||||
|
||||
await this.adapter.rotateStaticPassword('ldap-test', 'test-role');
|
||||
});
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when updating record', async function (assert) {
|
||||
assert.expect(2);
|
||||
module('hierarchical paths', function () {
|
||||
test('it should make request to correct endpoint when listing hierarchical records', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.server.post('/ldap-test/:path/test-role', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'POST request made to correct endpoint when updating record'
|
||||
const staticAncestry = { path_to_role: 'static-admin/', type: 'static' };
|
||||
const dynamicAncestry = { path_to_role: 'dynamic-admin/', type: 'dynamic' };
|
||||
|
||||
this.server.get(`/ldap-test/static-role/${staticAncestry.path_to_role}`, (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.queryParams.list,
|
||||
'true',
|
||||
`query request lists roles of type: ${staticAncestry.type}`
|
||||
);
|
||||
return { data: { keys: ['my-static-role'] } };
|
||||
});
|
||||
this.server.get(`/ldap-test/role/${dynamicAncestry.path_to_role}`, (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.queryParams.list,
|
||||
'true',
|
||||
`query request lists roles of type: ${dynamicAncestry.type}`
|
||||
);
|
||||
return { data: { keys: ['my-dynamic-role'] } };
|
||||
});
|
||||
|
||||
await this.store.query(
|
||||
'ldap/role',
|
||||
{ backend: 'ldap-test' },
|
||||
{ adapterOptions: { roleAncestry: staticAncestry } }
|
||||
);
|
||||
await this.store.query(
|
||||
'ldap/role',
|
||||
{ backend: 'ldap-test' },
|
||||
{ adapterOptions: { roleAncestry: dynamicAncestry } }
|
||||
);
|
||||
});
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
const record = this.getModel(type);
|
||||
await record.save();
|
||||
this.path = 'static-role';
|
||||
test(`it should make request to correct endpoint when deleting a role for type: ${type}`, async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const url =
|
||||
type === 'static'
|
||||
? '/ldap-test/static-role/admin/my-static-role'
|
||||
: '/ldap-test/role/admin/my-dynamic-role';
|
||||
|
||||
this.server.delete(url, () => {
|
||||
assert.true(true, `DELETE request made to delete hierarchical role of type: ${type}`);
|
||||
});
|
||||
|
||||
const record = this.getModel(type, `admin/my-${type}-role`);
|
||||
await record.destroyRecord();
|
||||
});
|
||||
|
||||
test(`it should make request to correct endpoints when fetching credentials for type: ${type}`, async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const url =
|
||||
type === 'static'
|
||||
? '/ldap-test/static-cred/admin/my-static-role'
|
||||
: '/ldap-test/creds/admin/my-dynamic-role';
|
||||
|
||||
this.server.get(url, () => {
|
||||
assert.true(true, `request made to fetch credentials for role type: ${type}`);
|
||||
});
|
||||
|
||||
await this.adapter.fetchCredentials('ldap-test', type, `admin/my-${type}-role`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when deleting record', async function (assert) {
|
||||
assert.expect(2);
|
||||
test('it should make request to correct endpoint when rotating static role password', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.delete('/ldap-test/:path/test-role', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'DELETE request made to correct endpoint when deleting record'
|
||||
);
|
||||
this.server.post('/ldap-test/rotate-role/admin/test-role', () => {
|
||||
assert.ok('GET request made to correct endpoint when rotating static role password');
|
||||
});
|
||||
|
||||
await this.adapter.rotateStaticPassword('ldap-test', 'admin/test-role');
|
||||
});
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
const record = this.getModel(type);
|
||||
await record.destroyRecord();
|
||||
this.path = 'static-role';
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoints when fetching credentials', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.path = 'creds';
|
||||
|
||||
this.server.get('/ldap-test/:path/test-role', (schema, req) => {
|
||||
assert.strictEqual(
|
||||
req.params.path,
|
||||
this.path,
|
||||
'GET request made to correct endpoint when fetching credentials'
|
||||
);
|
||||
});
|
||||
|
||||
for (const type of ['dynamic', 'static']) {
|
||||
await this.adapter.fetchCredentials('ldap-test', type, 'test-role');
|
||||
this.path = 'static-cred';
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when rotating static role password', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/ldap-test/rotate-role/test-role', () => {
|
||||
assert.ok('GET request made to correct endpoint when rotating static role password');
|
||||
});
|
||||
|
||||
await this.adapter.rotateStaticPassword('ldap-test', 'test-role');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
import { ldapBreadcrumbs, roleRoutes } 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';
|
||||
const routeParams = (childResource) => {
|
||||
return [this.mountPath, this.roleType, childResource];
|
||||
};
|
||||
this.testCrumbs = (path, { lastItemCurrent }) => {
|
||||
return ldapBreadcrumbs(path, this.roleType, this.mountPath, lastItemCurrent);
|
||||
return ldapBreadcrumbs(path, routeParams, roleRoutes, lastItemCurrent);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
2
ui/types/vault/models/ldap/library.d.ts
vendored
2
ui/types/vault/models/ldap/library.d.ts
vendored
@@ -13,10 +13,12 @@ import type {
|
||||
export default interface LdapLibraryModel extends WithFormFieldsAndValidationsModel {
|
||||
backend: string;
|
||||
name: string;
|
||||
path_to_library: string;
|
||||
service_account_names: string;
|
||||
default_ttl: number;
|
||||
max_ttl: number;
|
||||
disable_check_in_enforcement: string;
|
||||
get completeLibraryName(): string;
|
||||
get displayFields(): Array<FormField>;
|
||||
libraryPath: CapabilitiesModel;
|
||||
statusPath: CapabilitiesModel;
|
||||
|
||||
1
ui/types/vault/models/ldap/role.d.ts
vendored
1
ui/types/vault/models/ldap/role.d.ts
vendored
@@ -20,6 +20,7 @@ export default interface LdapRoleModel extends WithFormFieldsAndValidationsModel
|
||||
username_template: string;
|
||||
creation_ldif: string;
|
||||
rollback_ldif: string;
|
||||
get completeRoleName(): string;
|
||||
get isStatic(): string;
|
||||
get isDynamic(): string;
|
||||
get fieldsForType(): Array<string>;
|
||||
|
||||
Reference in New Issue
Block a user