mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +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';
|
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||||
|
|
||||||
export default class LdapLibraryAdapter extends NamedPathAdapter {
|
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`;
|
const base = `${this.buildURL()}/${encodePath(backend)}/library`;
|
||||||
return name ? `${base}/${name}` : base;
|
return path ? `${base}/${path}` : base;
|
||||||
}
|
}
|
||||||
|
|
||||||
urlForUpdateRecord(name, modelName, snapshot) {
|
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) {
|
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) {
|
query(store, type, query) {
|
||||||
const { backend } = query;
|
const { backend, path_to_library } = query;
|
||||||
return this.ajax(this.getURL(backend), 'GET', { data: { list: true } })
|
// 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) => {
|
.then((resp) => {
|
||||||
return resp.data.keys.map((name) => ({ name, backend }));
|
return resp.data.keys.map((name) => ({ name, backend, path_to_library }));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error.httpStatus === 404) {
|
if (error.httpStatus === 404) {
|
||||||
@@ -34,11 +39,11 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
|
|||||||
}
|
}
|
||||||
queryRecord(store, type, query) {
|
queryRecord(store, type, query) {
|
||||||
const { backend, name } = 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) {
|
fetchStatus(backend, name) {
|
||||||
const url = `${this.getURL(backend, name)}/status`;
|
const url = `${this._getURL(backend, name)}/status`;
|
||||||
return this.ajax(url, 'GET').then((resp) => {
|
return this.ajax(url, 'GET').then((resp) => {
|
||||||
const statuses = [];
|
const statuses = [];
|
||||||
for (const key in resp.data) {
|
for (const key in resp.data) {
|
||||||
@@ -53,7 +58,7 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
checkOutAccount(backend, name, ttl) {
|
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) => {
|
return this.ajax(url, 'POST', { data: { ttl } }).then((resp) => {
|
||||||
const { lease_id, lease_duration, renewable } = resp;
|
const { lease_id, lease_duration, renewable } = resp;
|
||||||
const { service_account_name: account, password } = resp.data;
|
const { service_account_name: account, password } = resp.data;
|
||||||
@@ -61,7 +66,7 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
checkInAccount(backend, name, service_account_names) {
|
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);
|
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) {
|
urlForDeleteRecord(id, modelName, snapshot) {
|
||||||
const { backend, type, name } = snapshot.record;
|
const { backend, type, completeRoleName } = snapshot.record;
|
||||||
return this._getURL(backend, this._pathForRoleType(type), name);
|
return this._getURL(backend, this._pathForRoleType(type), completeRoleName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const formFields = ['name', 'service_account_names', 'ttl', 'max_ttl', 'disable_
|
|||||||
@withFormFields(formFields)
|
@withFormFields(formFields)
|
||||||
export default class LdapLibraryModel extends Model {
|
export default class LdapLibraryModel extends Model {
|
||||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
@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', {
|
@attr('string', {
|
||||||
label: 'Library name',
|
label: 'Library name',
|
||||||
@@ -64,6 +65,12 @@ export default class LdapLibraryModel extends Model {
|
|||||||
})
|
})
|
||||||
disable_check_in_enforcement;
|
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() {
|
get displayFields() {
|
||||||
return this.formFields.filter((field) => field.name !== 'service_account_names');
|
return this.formFields.filter((field) => field.name !== 'service_account_names');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,12 @@ export default class LdapRoleModel extends Model {
|
|||||||
})
|
})
|
||||||
rollback_ldif;
|
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() {
|
get isStatic() {
|
||||||
return this.type === 'static';
|
return this.type === 'static';
|
||||||
}
|
}
|
||||||
@@ -224,9 +230,11 @@ export default class LdapRoleModel extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchCredentials() {
|
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() {
|
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}}
|
{{else}}
|
||||||
<div class="has-bottom-margin-s">
|
<div class="has-bottom-margin-s">
|
||||||
{{#each this.filteredLibraries as |library|}}
|
{{#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>
|
<Item.content>
|
||||||
<Icon @name="folder" />
|
<Icon @name="folder" />
|
||||||
<span data-test-library={{library.name}}>{{library.name}}</span>
|
<span data-test-library={{library.completeLibraryName}}>{{library.name}}</span>
|
||||||
</Item.content>
|
</Item.content>
|
||||||
<Item.menu>
|
<Item.menu>
|
||||||
{{#if (or library.canRead library.canEdit library.canDelete)}}
|
{{#if (or library.canRead library.canEdit library.canDelete)}}
|
||||||
@@ -55,24 +55,36 @@
|
|||||||
@icon="more-horizontal"
|
@icon="more-horizontal"
|
||||||
@text="More options"
|
@text="More options"
|
||||||
@hasChevron={{false}}
|
@hasChevron={{false}}
|
||||||
data-test-popup-menu-trigger
|
data-test-popup-menu-trigger={{library.completeLibraryName}}
|
||||||
/>
|
/>
|
||||||
{{#if library.canEdit}}
|
{{#if (this.isHierarchical library.name)}}
|
||||||
<dd.Interactive data-test-edit @route="libraries.library.edit" @model={{library}}>Edit</dd.Interactive>
|
|
||||||
{{/if}}
|
|
||||||
{{#if library.canRead}}
|
|
||||||
<dd.Interactive
|
<dd.Interactive
|
||||||
data-test-details
|
data-test-subdirectory
|
||||||
@route="libraries.library.details"
|
@route="libraries.subdirectory"
|
||||||
@model={{library}}
|
@model={{library.completeLibraryName}}
|
||||||
>Details</dd.Interactive>
|
>Content</dd.Interactive>
|
||||||
{{/if}}
|
{{else}}
|
||||||
{{#if library.canDelete}}
|
{{#if library.canEdit}}
|
||||||
<dd.Interactive
|
<dd.Interactive
|
||||||
data-test-delete
|
data-test-edit
|
||||||
@color="critical"
|
@route="libraries.library.edit"
|
||||||
{{on "click" (fn (mut this.libraryToDelete) library)}}
|
@model={{library.completeLibraryName}}
|
||||||
>Delete</dd.Interactive>
|
>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}}
|
{{/if}}
|
||||||
</Hds::Dropdown>
|
</Hds::Dropdown>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
|
|||||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||||
import type FlashMessageService from 'vault/services/flash-messages';
|
import type FlashMessageService from 'vault/services/flash-messages';
|
||||||
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
|
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
libraries: Array<LdapLibraryModel>;
|
libraries: Array<LdapLibraryModel>;
|
||||||
@@ -24,10 +25,18 @@ interface Args {
|
|||||||
|
|
||||||
export default class LdapLibrariesPageComponent extends Component<Args> {
|
export default class LdapLibrariesPageComponent extends Component<Args> {
|
||||||
@service declare readonly flashMessages: FlashMessageService;
|
@service declare readonly flashMessages: FlashMessageService;
|
||||||
|
@service('app-router') declare readonly router: RouterService;
|
||||||
|
|
||||||
@tracked filterValue = '';
|
@tracked filterValue = '';
|
||||||
@tracked libraryToDelete: LdapLibraryModel | null = null;
|
@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 {
|
get mountPoint(): string {
|
||||||
const owner = getOwner(this) as EngineOwner;
|
const owner = getOwner(this) as EngineOwner;
|
||||||
return owner.mountPoint;
|
return owner.mountPoint;
|
||||||
@@ -43,8 +52,9 @@ export default class LdapLibrariesPageComponent extends Component<Args> {
|
|||||||
@action
|
@action
|
||||||
async onDelete(model: LdapLibraryModel) {
|
async onDelete(model: LdapLibraryModel) {
|
||||||
try {
|
try {
|
||||||
const message = `Successfully deleted library ${model.name}.`;
|
const message = `Successfully deleted library ${model.completeLibraryName}.`;
|
||||||
await model.destroyRecord();
|
await model.destroyRecord();
|
||||||
|
this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries');
|
||||||
this.flashMessages.success(message);
|
this.flashMessages.success(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.flashMessages.danger(`Error deleting library \n ${errorMessage(error)}`);
|
this.flashMessages.danger(`Error deleting library \n ${errorMessage(error)}`);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<dd.Interactive
|
<dd.Interactive
|
||||||
data-test-subdirectory
|
data-test-subdirectory
|
||||||
@route="roles.subdirectory"
|
@route="roles.subdirectory"
|
||||||
@models={{array role.type (concat role.path_to_role role.name)}}
|
@models={{array role.type role.completeRoleName}}
|
||||||
>Content</dd.Interactive>
|
>Content</dd.Interactive>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if role.canEdit}}
|
{{#if role.canEdit}}
|
||||||
@@ -72,7 +72,11 @@
|
|||||||
>Edit</dd.Interactive>
|
>Edit</dd.Interactive>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if role.canReadCreds}}
|
{{#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
|
Get credentials
|
||||||
</dd.Interactive>
|
</dd.Interactive>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -87,7 +91,7 @@
|
|||||||
data-test-details
|
data-test-details
|
||||||
@route="roles.role.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 }}
|
{{! 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>
|
>Details</dd.Interactive>
|
||||||
{{#if role.canDelete}}
|
{{#if role.canDelete}}
|
||||||
<dd.Interactive
|
<dd.Interactive
|
||||||
|
|||||||
@@ -37,10 +37,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
|||||||
|
|
||||||
linkParams = (role: LdapRoleModel) => {
|
linkParams = (role: LdapRoleModel) => {
|
||||||
const route = this.isHierarchical(role.name) ? 'roles.subdirectory' : 'roles.role.details';
|
const route = this.isHierarchical(role.name) ? 'roles.subdirectory' : 'roles.role.details';
|
||||||
// if there is a path_to_role we're in a subdirectory
|
return [route, role.type, role.completeRoleName];
|
||||||
// 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 {
|
get mountPoint(): string {
|
||||||
@@ -61,7 +58,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
|||||||
@action
|
@action
|
||||||
async onRotate(model: LdapRoleModel) {
|
async onRotate(model: LdapRoleModel) {
|
||||||
try {
|
try {
|
||||||
const message = `Successfully rotated credentials for ${model.name}.`;
|
const message = `Successfully rotated credentials for ${model.completeRoleName}.`;
|
||||||
await model.rotateStaticPassword();
|
await model.rotateStaticPassword();
|
||||||
this.flashMessages.success(message);
|
this.flashMessages.success(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -74,7 +71,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
|||||||
@action
|
@action
|
||||||
async onDelete(model: LdapRoleModel) {
|
async onDelete(model: LdapRoleModel) {
|
||||||
try {
|
try {
|
||||||
const message = `Successfully deleted role ${model.name}.`;
|
const message = `Successfully deleted role ${model.completeRoleName}.`;
|
||||||
await model.destroyRecord();
|
await model.destroyRecord();
|
||||||
this.pagination.clearDataset('ldap/role');
|
this.pagination.clearDataset('ldap/role');
|
||||||
this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles');
|
this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles');
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export default buildRoutes(function () {
|
|||||||
});
|
});
|
||||||
this.route('libraries', function () {
|
this.route('libraries', function () {
|
||||||
this.route('create');
|
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('library', { path: '/:name' }, function () {
|
||||||
this.route('details', function () {
|
this.route('details', function () {
|
||||||
this.route('accounts');
|
this.route('accounts');
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type Transition from '@ember/routing/transition';
|
|||||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||||
import { LdapLibraryCheckOutCredentials } from 'vault/vault/adapters/ldap/library';
|
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 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 {
|
interface LdapLibraryCheckOutController extends Controller {
|
||||||
breadcrumbs: Array<Breadcrumb>;
|
breadcrumbs: Array<Breadcrumb>;
|
||||||
@@ -45,12 +46,14 @@ export default class LdapLibraryCheckOutRoute extends Route {
|
|||||||
transition: Transition
|
transition: Transition
|
||||||
) {
|
) {
|
||||||
super.setupController(controller, resolvedModel, transition);
|
super.setupController(controller, resolvedModel, transition);
|
||||||
|
|
||||||
const library = this.modelFor('libraries.library') as LdapLibraryModel;
|
const library = this.modelFor('libraries.library') as LdapLibraryModel;
|
||||||
|
const routeParams = (childResource: string) => {
|
||||||
|
return [library.backend, childResource];
|
||||||
|
};
|
||||||
controller.breadcrumbs = [
|
controller.breadcrumbs = [
|
||||||
{ label: library.backend, route: 'overview' },
|
{ label: library.backend, route: 'overview' },
|
||||||
{ label: 'Libraries', route: 'libraries' },
|
{ label: 'Libraries', route: 'libraries' },
|
||||||
{ label: library.name, route: 'libraries.library' },
|
...ldapBreadcrumbs(library.name, routeParams, libraryRoutes),
|
||||||
{ label: 'Check-Out' },
|
{ label: 'Check-Out' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
|
|||||||
import type Controller from '@ember/controller';
|
import type Controller from '@ember/controller';
|
||||||
import type Transition from '@ember/routing/transition';
|
import type Transition from '@ember/routing/transition';
|
||||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||||
|
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||||
|
|
||||||
interface LdapLibraryDetailsController extends Controller {
|
interface LdapLibraryDetailsController extends Controller {
|
||||||
breadcrumbs: Array<Breadcrumb>;
|
breadcrumbs: Array<Breadcrumb>;
|
||||||
@@ -23,10 +24,14 @@ export default class LdapLibraryDetailsRoute extends Route {
|
|||||||
) {
|
) {
|
||||||
super.setupController(controller, resolvedModel, transition);
|
super.setupController(controller, resolvedModel, transition);
|
||||||
|
|
||||||
|
const routeParams = (childResource: string) => {
|
||||||
|
return [resolvedModel.backend, childResource];
|
||||||
|
};
|
||||||
|
|
||||||
controller.breadcrumbs = [
|
controller.breadcrumbs = [
|
||||||
{ label: resolvedModel.backend, route: 'overview' },
|
{ label: resolvedModel.backend, route: 'overview' },
|
||||||
{ label: 'Libraries', route: 'libraries' },
|
{ 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 Controller from '@ember/controller';
|
||||||
import type Transition from '@ember/routing/transition';
|
import type Transition from '@ember/routing/transition';
|
||||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||||
|
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';
|
||||||
|
|
||||||
interface LdapLibraryEditController extends Controller {
|
interface LdapLibraryEditController extends Controller {
|
||||||
breadcrumbs: Array<Breadcrumb>;
|
breadcrumbs: Array<Breadcrumb>;
|
||||||
@@ -23,10 +24,13 @@ export default class LdapLibraryEditRoute extends Route {
|
|||||||
) {
|
) {
|
||||||
super.setupController(controller, resolvedModel, transition);
|
super.setupController(controller, resolvedModel, transition);
|
||||||
|
|
||||||
|
const routeParams = (childResource: string) => {
|
||||||
|
return [resolvedModel.backend, childResource];
|
||||||
|
};
|
||||||
controller.breadcrumbs = [
|
controller.breadcrumbs = [
|
||||||
{ label: resolvedModel.backend, route: 'overview' },
|
{ label: resolvedModel.backend, route: 'overview' },
|
||||||
{ label: 'Libraries', route: 'libraries' },
|
{ label: 'Libraries', route: 'libraries' },
|
||||||
{ label: resolvedModel.name, route: 'libraries.library.details' },
|
...ldapBreadcrumbs(resolvedModel.name, routeParams, libraryRoutes),
|
||||||
{ label: 'Edit' },
|
{ 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 Route from '@ember/routing/route';
|
||||||
import { service } from '@ember/service';
|
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 Store from '@ember-data/store';
|
||||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||||
@@ -58,11 +58,14 @@ export default class LdapRoleCredentialsRoute extends Route {
|
|||||||
super.setupController(controller, resolvedModel, transition);
|
super.setupController(controller, resolvedModel, transition);
|
||||||
|
|
||||||
const role = this.modelFor('roles.role') as LdapRoleModel;
|
const role = this.modelFor('roles.role') as LdapRoleModel;
|
||||||
|
const routeParams = (childResource: string) => {
|
||||||
|
return [role.backend, role.type, childResource];
|
||||||
|
};
|
||||||
controller.breadcrumbs = [
|
controller.breadcrumbs = [
|
||||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||||
{ label: role.backend, route: 'overview' },
|
{ label: role.backend, route: 'overview' },
|
||||||
{ label: 'Roles', route: 'roles' },
|
{ label: 'Roles', route: 'roles' },
|
||||||
...ldapBreadcrumbs(role.name, role.type, role.backend),
|
...ldapBreadcrumbs(role.name, routeParams, roleRoutes),
|
||||||
{ label: 'Credentials' },
|
{ label: 'Credentials' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
import { service } from '@ember/service';
|
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 { Breadcrumb } from 'vault/vault/app-types';
|
||||||
import type Controller from '@ember/controller';
|
import type Controller from '@ember/controller';
|
||||||
@@ -24,11 +24,15 @@ export default class LdapRolesRoleDetailsRoute extends Route {
|
|||||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||||
super.setupController(controller, resolvedModel, transition);
|
super.setupController(controller, resolvedModel, transition);
|
||||||
|
|
||||||
|
const routeParams = (childResource: string) => {
|
||||||
|
return [this.secretMountPath.currentPath, resolvedModel.type, childResource];
|
||||||
|
};
|
||||||
|
|
||||||
controller.breadcrumbs = [
|
controller.breadcrumbs = [
|
||||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||||
{ label: resolvedModel.backend, route: 'overview' },
|
{ label: resolvedModel.backend, route: 'overview' },
|
||||||
{ label: 'Roles', route: 'roles' },
|
{ 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 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 LdapRoleModel from 'vault/models/ldap/role';
|
||||||
import type Controller from '@ember/controller';
|
import type Controller from '@ember/controller';
|
||||||
@@ -20,11 +20,15 @@ export default class LdapRoleEditRoute extends Route {
|
|||||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||||
super.setupController(controller, resolvedModel, transition);
|
super.setupController(controller, resolvedModel, transition);
|
||||||
|
|
||||||
|
const routeParams = (childResource: string) => {
|
||||||
|
return [resolvedModel.backend, resolvedModel.type, childResource];
|
||||||
|
};
|
||||||
|
|
||||||
controller.breadcrumbs = [
|
controller.breadcrumbs = [
|
||||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||||
{ label: resolvedModel.backend, route: 'overview' },
|
{ label: resolvedModel.backend, route: 'overview' },
|
||||||
{ label: 'Roles', route: 'roles' },
|
{ label: 'Roles', route: 'roles' },
|
||||||
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, resolvedModel.backend),
|
...ldapBreadcrumbs(resolvedModel.name, routeParams, roleRoutes),
|
||||||
{ label: 'Edit' },
|
{ label: 'Edit' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import LdapRolesRoute from '../roles';
|
import LdapRolesRoute from '../roles';
|
||||||
import { hash } from 'rsvp';
|
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 { Breadcrumb } from 'vault/vault/app-types';
|
||||||
import type Controller from '@ember/controller';
|
import type Controller from '@ember/controller';
|
||||||
@@ -55,11 +55,16 @@ export default class LdapRolesSubdirectoryRoute extends LdapRolesRoute {
|
|||||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||||
super.setupController(controller, resolvedModel, transition);
|
super.setupController(controller, resolvedModel, transition);
|
||||||
const { backendModel, roleAncestry } = resolvedModel;
|
const { backendModel, roleAncestry } = resolvedModel;
|
||||||
|
|
||||||
|
const routeParams = (childResource: string) => {
|
||||||
|
return [backendModel.id, roleAncestry.type, childResource];
|
||||||
|
};
|
||||||
|
|
||||||
const crumbs = [
|
const crumbs = [
|
||||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||||
{ label: backendModel.id, route: 'overview' },
|
{ label: backendModel.id, route: 'overview' },
|
||||||
{ label: 'Roles', route: 'roles' },
|
{ 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
|
// 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';
|
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 = (
|
export const ldapBreadcrumbs = (
|
||||||
fullPath: string | undefined, // i.e. path/to/item
|
fullPath: string | undefined, // i.e. path/to/item
|
||||||
roleType: string,
|
routeParams: (childResource: string) => string[], // array of route param strings
|
||||||
mountPath: string,
|
routes: { details: string; subdirectory: string },
|
||||||
lastItemCurrent = false // this array of objects can be spread anywhere within the crumbs array
|
lastItemCurrent = false // this array of objects can be spread anywhere within the crumbs array
|
||||||
): Breadcrumb[] => {
|
): Breadcrumb[] => {
|
||||||
if (!fullPath) return [];
|
if (!fullPath) return [];
|
||||||
@@ -26,11 +32,10 @@ export const ldapBreadcrumbs = (
|
|||||||
const segment = ancestry.slice(0, idx + 1).join('/');
|
const segment = ancestry.slice(0, idx + 1).join('/');
|
||||||
|
|
||||||
const itemPath = isLast && !isDirectory ? segment : `${segment}/`;
|
const itemPath = isLast && !isDirectory ? segment : `${segment}/`;
|
||||||
const routeParams = [mountPath, roleType, itemPath];
|
|
||||||
return {
|
return {
|
||||||
label: name,
|
label: name,
|
||||||
route: isLast && !isDirectory ? 'roles.role.details' : 'roles.subdirectory',
|
route: isLast && !isDirectory ? routes.details : routes.subdirectory,
|
||||||
models: routeParams,
|
models: routeParams(itemPath),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,14 +46,16 @@ export default function (server) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listOrGetRecord = (schema, req, type) => {
|
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) {
|
if (req.queryParams.list) {
|
||||||
// passing a query with specific name is not flexible
|
// the mirage database has setup all hierarchical names to be prefixed with "admin/"
|
||||||
// but we only seeded the mirage db with one hierarchical role for each type
|
// while passing a query with specific name is not flexible, for simplicity
|
||||||
return listRecords(schema, 'ldapRoles', { type, name: `admin/child-${type}-role` });
|
// 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
|
// otherwise we want to view details for a specific resource
|
||||||
return getRecord(schema, req, 'ldapRoles', type);
|
return getRecord(schema, req, dbKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
// config
|
// config
|
||||||
@@ -77,9 +79,9 @@ export default function (server) {
|
|||||||
}));
|
}));
|
||||||
// libraries
|
// libraries
|
||||||
server.post('/:backend/library/:name', (schema, req) => createOrUpdateRecord(schema, req, 'ldapLibraries'));
|
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', (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) => {
|
const data = schema.db['ldapAccountStatuses'].reduce((prev, curr) => {
|
||||||
prev[curr.account] = {
|
prev[curr.account] = {
|
||||||
available: curr.available,
|
available: curr.available,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export default function (server) {
|
|||||||
server.create('ldap-role', 'static', { name: 'my-role' });
|
server.create('ldap-role', 'static', { name: 'my-role' });
|
||||||
server.create('ldap-role', 'dynamic', { name: 'my-role' });
|
server.create('ldap-role', 'dynamic', { name: 'my-role' });
|
||||||
server.create('ldap-library', { name: 'test-library' });
|
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', {
|
server.create('ldap-account-status', {
|
||||||
id: 'bob.johnson',
|
id: 'bob.johnson',
|
||||||
account: 'bob.johnson',
|
account: 'bob.johnson',
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import ldapMirageScenario from 'vault/mirage/scenarios/ldap';
|
import ldapMirageScenario from 'vault/mirage/scenarios/ldap';
|
||||||
import ldapHandlers from 'vault/mirage/handlers/ldap';
|
import ldapHandlers from 'vault/mirage/handlers/ldap';
|
||||||
import authPage from 'vault/tests/pages/auth';
|
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 { isURL, visitURL } from 'vault/tests/helpers/ldap/ldap-helpers';
|
||||||
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
|
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) {
|
module('Acceptance | ldap | libraries', function (hooks) {
|
||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
@@ -37,7 +38,7 @@ module('Acceptance | ldap | libraries', function (hooks) {
|
|||||||
|
|
||||||
test('it should show libraries on overview page', async function (assert) {
|
test('it should show libraries on overview page', async function (assert) {
|
||||||
await visitURL('overview', this.backend);
|
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) {
|
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) {
|
test('it should transition to library details route on list item click', async function (assert) {
|
||||||
await click('[data-test-list-item-link] a');
|
await click(LDAP_SELECTORS.libraryItem('test-library'));
|
||||||
assert.true(
|
assert.strictEqual(
|
||||||
isURL('libraries/test-library/details/accounts', this.backend),
|
currentURL(),
|
||||||
|
`/vault/secrets/${this.backend}/ldap/libraries/test-library/details/accounts`,
|
||||||
'Transitions to library details accounts route on list item click'
|
'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-account-name]').exists({ count: 2 }, 'lists the accounts');
|
||||||
assert.dom('[data-test-checked-out-account]').exists({ count: 1 }, 'lists the checked out 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) {
|
test('it should transition to routes from list item action menu', async function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
export const LDAP_SELECTORS = {
|
export const LDAP_SELECTORS = {
|
||||||
roleItem: (type: string, name: string) => `[data-test-role="${type} ${name}"]`,
|
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}"]`,
|
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}]`,
|
action: (action: string) => `[data-test-${action}]`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import hbs from 'htmlbars-inline-precompile';
|
|||||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||||
import { createSecretsEngine, generateBreadcrumbs } from 'vault/tests/helpers/ldap/ldap-helpers';
|
import { createSecretsEngine, generateBreadcrumbs } from 'vault/tests/helpers/ldap/ldap-helpers';
|
||||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
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) {
|
module('Integration | Component | ldap | Page::Libraries', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
@@ -25,7 +26,7 @@ module('Integration | Component | ldap | Page::Libraries', function (hooks) {
|
|||||||
this.backend = createSecretsEngine(this.store);
|
this.backend = createSecretsEngine(this.store);
|
||||||
this.breadcrumbs = generateBreadcrumbs(this.backend.id);
|
this.breadcrumbs = generateBreadcrumbs(this.backend.id);
|
||||||
|
|
||||||
for (const name of ['foo', 'bar']) {
|
for (const name of ['foo', 'bar', 'foo/']) {
|
||||||
this.store.pushPayload('ldap/library', {
|
this.store.pushPayload('ldap/library', {
|
||||||
modelName: 'ldap/library',
|
modelName: 'ldap/library',
|
||||||
backend: 'ldap-test',
|
backend: 'ldap-test',
|
||||||
@@ -93,12 +94,19 @@ module('Integration | Component | ldap | Page::Libraries', function (hooks) {
|
|||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
|
|
||||||
assert.dom('[data-test-list-item-content] svg').hasClass('hds-icon-folder', 'List item icon renders');
|
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-edit]').hasText('Edit', 'Edit link renders in menu');
|
||||||
assert.dom('[data-test-details]').hasText('Details', 'Details 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');
|
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) {
|
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');
|
.hasText('There are no libraries matching "baz"', 'Filter message renders');
|
||||||
|
|
||||||
await fillIn('[data-test-filter-input]', 'foo');
|
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]', '');
|
await fillIn('[data-test-filter-input]', '');
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-list-item-content]')
|
.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.adapter = this.store.adapterFor('ldap/role');
|
||||||
this.path = 'role';
|
this.path = 'role';
|
||||||
|
|
||||||
this.getModel = (type) => {
|
this.getModel = (type, roleName) => {
|
||||||
const name = 'test-role';
|
const name = roleName || 'test-role';
|
||||||
this.store.pushPayload('ldap/role', {
|
this.store.pushPayload('ldap/role', {
|
||||||
modelName: 'ldap/role',
|
modelName: 'ldap/role',
|
||||||
backend: 'ldap-test',
|
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) {
|
module('happy paths', function () {
|
||||||
assert.expect(6);
|
test('it should make request to correct endpoints when listing records', async function (assert) {
|
||||||
|
assert.expect(6);
|
||||||
|
|
||||||
const assertRequest = (schema, req) => {
|
const assertRequest = (schema, req) => {
|
||||||
assert.ok(req.queryParams.list, 'list query param sent when listing roles');
|
assert.ok(req.queryParams.list, 'list query param sent when listing roles');
|
||||||
const name = req.url.includes('static-role') ? 'static-test' : 'dynamic-test';
|
const name = req.url.includes('static-role') ? 'static-test' : 'dynamic-test';
|
||||||
return { data: { keys: [name] } };
|
return { data: { keys: [name] } };
|
||||||
};
|
};
|
||||||
|
|
||||||
this.server.get('/ldap-test/static-role', assertRequest);
|
this.server.get('/ldap-test/static-role', assertRequest);
|
||||||
this.server.get('/ldap-test/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];
|
const model = this.models[0];
|
||||||
assert.strictEqual(this.models.length, 2, 'Returns responses from both endpoints');
|
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');
|
assert.strictEqual(model.backend, 'ldap-test', 'Backend value is set on records returned from query');
|
||||||
// sorted alphabetically by name so dynamic should be first
|
// 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.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');
|
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 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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' });
|
await this.store.query('ldap/role', { backend: 'ldap-test' });
|
||||||
} catch (error) {
|
await this.store.query(
|
||||||
assert.deepEqual(
|
'ldap/role',
|
||||||
error.errors,
|
{ backend: 'ldap-test' },
|
||||||
['/v1/ldap-test/static-role: permission denied', '/v1/ldap-test/role: server error'],
|
{ adapterOptions: { showPartialError: true } }
|
||||||
'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.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should make request to correct endpoints when querying record', async function (assert) {
|
assert.true(
|
||||||
assert.expect(5);
|
flashSpy.calledOnceWith('Error fetching roles from /v1/ldap-test/static-role: permission denied'),
|
||||||
|
'Partial error info only displays when adapter option is passed'
|
||||||
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'
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const type of ['dynamic', 'static']) {
|
test('it should throw error for query when requests to both endpoints fail', async function (assert) {
|
||||||
this.model = await this.store.queryRecord('ldap/role', {
|
assert.expect(2);
|
||||||
backend: 'ldap-test',
|
|
||||||
type,
|
this.server.get('/ldap-test/:path', (schema, req) => {
|
||||||
name: 'test-role',
|
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(
|
try {
|
||||||
this.model.backend,
|
await this.store.query('ldap/role', { backend: 'ldap-test' });
|
||||||
'ldap-test',
|
} catch (error) {
|
||||||
'Backend value is set on records returned from query'
|
assert.deepEqual(
|
||||||
);
|
error.errors,
|
||||||
assert.strictEqual(this.model.type, 'static', 'Type value is set on records returned from query');
|
['/v1/ldap-test/static-role: permission denied', '/v1/ldap-test/role: server error'],
|
||||||
assert.strictEqual(this.model.name, 'test-role', 'Name value is set on records returned from query');
|
'Error messages is thrown with correct payload from query.'
|
||||||
});
|
);
|
||||||
|
assert.strictEqual(
|
||||||
test('it should make request to correct endpoints when creating new dynamic role record', async function (assert) {
|
error.message,
|
||||||
assert.expect(1);
|
'Error fetching roles:',
|
||||||
|
'Error message is thrown with correct payload from query.'
|
||||||
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 getModel = (type, name) => {
|
test('it should make request to correct endpoints when querying record', async function (assert) {
|
||||||
return this.store.createRecord('ldap/role', {
|
assert.expect(5);
|
||||||
backend: 'ldap-test',
|
|
||||||
name,
|
this.server.get('/ldap-test/:path/test-role', (schema, req) => {
|
||||||
type,
|
assert.strictEqual(
|
||||||
|
req.params.path,
|
||||||
|
this.path,
|
||||||
|
'GET request made to correct endpoint when querying record'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const model = getModel('dynamic-role', 'dynamic-role-name');
|
for (const type of ['dynamic', 'static']) {
|
||||||
await model.save();
|
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(
|
assert.strictEqual(
|
||||||
req.params.path,
|
this.model.backend,
|
||||||
this.path,
|
'ldap-test',
|
||||||
'POST request made to correct endpoint when creating new record for a static role'
|
'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) => {
|
test('it should make request to correct endpoints when creating new dynamic role record', async function (assert) {
|
||||||
return this.store.createRecord('ldap/role', {
|
assert.expect(1);
|
||||||
backend: 'ldap-test',
|
|
||||||
name,
|
this.server.post('/ldap-test/:path/:name', (schema, req) => {
|
||||||
type,
|
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');
|
const getModel = (type, name) => {
|
||||||
await model.save();
|
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) {
|
module('hierarchical paths', function () {
|
||||||
assert.expect(2);
|
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) => {
|
const staticAncestry = { path_to_role: 'static-admin/', type: 'static' };
|
||||||
assert.strictEqual(
|
const dynamicAncestry = { path_to_role: 'dynamic-admin/', type: 'dynamic' };
|
||||||
req.params.path,
|
|
||||||
this.path,
|
this.server.get(`/ldap-test/static-role/${staticAncestry.path_to_role}`, (schema, req) => {
|
||||||
'POST request made to correct endpoint when updating record'
|
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']) {
|
for (const type of ['dynamic', 'static']) {
|
||||||
const record = this.getModel(type);
|
test(`it should make request to correct endpoint when deleting a role for type: ${type}`, async function (assert) {
|
||||||
await record.save();
|
assert.expect(1);
|
||||||
this.path = 'static-role';
|
|
||||||
|
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) {
|
test('it should make request to correct endpoint when rotating static role password', async function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(1);
|
||||||
|
|
||||||
this.server.delete('/ldap-test/:path/test-role', (schema, req) => {
|
this.server.post('/ldap-test/rotate-role/admin/test-role', () => {
|
||||||
assert.strictEqual(
|
assert.ok('GET request made to correct endpoint when rotating static role password');
|
||||||
req.params.path,
|
});
|
||||||
this.path,
|
|
||||||
'DELETE request made to correct endpoint when deleting record'
|
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
|
* 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';
|
import { module, test } from 'qunit';
|
||||||
|
|
||||||
module('Unit | Utility | ldap breadcrumbs', function (hooks) {
|
module('Unit | Utility | ldap breadcrumbs', function (hooks) {
|
||||||
hooks.beforeEach(async function () {
|
hooks.beforeEach(async function () {
|
||||||
this.mountPath = 'my-engine';
|
this.mountPath = 'my-engine';
|
||||||
this.roleType = 'static';
|
this.roleType = 'static';
|
||||||
|
const routeParams = (childResource) => {
|
||||||
|
return [this.mountPath, this.roleType, childResource];
|
||||||
|
};
|
||||||
this.testCrumbs = (path, { lastItemCurrent }) => {
|
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 {
|
export default interface LdapLibraryModel extends WithFormFieldsAndValidationsModel {
|
||||||
backend: string;
|
backend: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
path_to_library: string;
|
||||||
service_account_names: string;
|
service_account_names: string;
|
||||||
default_ttl: number;
|
default_ttl: number;
|
||||||
max_ttl: number;
|
max_ttl: number;
|
||||||
disable_check_in_enforcement: string;
|
disable_check_in_enforcement: string;
|
||||||
|
get completeLibraryName(): string;
|
||||||
get displayFields(): Array<FormField>;
|
get displayFields(): Array<FormField>;
|
||||||
libraryPath: CapabilitiesModel;
|
libraryPath: CapabilitiesModel;
|
||||||
statusPath: 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;
|
username_template: string;
|
||||||
creation_ldif: string;
|
creation_ldif: string;
|
||||||
rollback_ldif: string;
|
rollback_ldif: string;
|
||||||
|
get completeRoleName(): string;
|
||||||
get isStatic(): string;
|
get isStatic(): string;
|
||||||
get isDynamic(): string;
|
get isDynamic(): string;
|
||||||
get fieldsForType(): Array<string>;
|
get fieldsForType(): Array<string>;
|
||||||
|
|||||||
Reference in New Issue
Block a user