mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 11:38:02 +00:00
UI: LDAP Hierarchical roles (#28824)
* remove named path adapter extension, add subdirectory query logic to adapter * add subdirectory route and logic to page::roles component * fix overview page search select * breadcrumbs * update tests and mirage * revert ss changes * oops * cleanup adapter, add _ for private methods * add acceptance test * remove type * add changelog * add ldap breadcrumb test * VAULT-31905 link jira * update breadcrumbs in Edit route * rename type interfaces
This commit is contained in:
@@ -53,7 +53,10 @@
|
||||
class="is-flex-half"
|
||||
/>
|
||||
<div>
|
||||
<OverviewCard @cardTitle="Generate credentials" @subText="Quickly generate credentials by typing the role name.">
|
||||
<OverviewCard
|
||||
@cardTitle="Generate credentials"
|
||||
@subText="Quickly generate credentials by typing the role name. Only the engine's top-level roles are listed here."
|
||||
>
|
||||
<:content>
|
||||
<div class="has-top-margin-m is-flex">
|
||||
<SearchSelect
|
||||
@@ -61,11 +64,14 @@
|
||||
@ariaLabel="Role"
|
||||
@placeholder="Select a role"
|
||||
@disallowNewItems={{true}}
|
||||
@options={{@roles}}
|
||||
@options={{this.roleOptions}}
|
||||
@selectLimit="1"
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.selectRole}}
|
||||
@renderInPlace={{true}}
|
||||
@passObject={{true}}
|
||||
@objectKeys={{array "id" "name" "type"}}
|
||||
@shouldRenderName={{true}}
|
||||
/>
|
||||
<div>
|
||||
<Hds::Button
|
||||
|
||||
@@ -24,15 +24,35 @@ interface Args {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default class LdapLibrariesPageComponent extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
|
||||
@tracked selectedRole: LdapRoleModel | undefined;
|
||||
|
||||
get roleOptions() {
|
||||
const options = this.args.roles
|
||||
// hierarchical roles are not selectable
|
||||
.filter((r: LdapRoleModel) => !r.name.endsWith('/'))
|
||||
// *hack alert* - type is set as id so it renders beside name in search select
|
||||
// this is to avoid more changes to search select and is okay here because
|
||||
// we use the type and name to select the item below, not the id
|
||||
.map((r: LdapRoleModel) => ({ id: r.type, name: r.name, type: r.type }));
|
||||
return options;
|
||||
}
|
||||
|
||||
@action
|
||||
selectRole([roleName]: Array<string>) {
|
||||
const model = this.args.roles.find((role) => role.name === roleName);
|
||||
this.selectedRole = model;
|
||||
async selectRole([option]: Array<Option>) {
|
||||
if (option) {
|
||||
const { name, type } = option;
|
||||
const model = this.args.roles.find((role) => role.name === name && role.type === type);
|
||||
this.selectedRole = model;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -43,46 +43,59 @@
|
||||
{{else}}
|
||||
<div class="has-bottom-margin-s">
|
||||
{{#each @roles as |role|}}
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "roles.role.details" role.type role.name}} as |Item|>
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{this.linkParams role}} as |Item|>
|
||||
<Item.content>
|
||||
<Icon @name="user" />
|
||||
<span data-test-role={{role.name}}>{{role.name}}</span>
|
||||
<span data-test-role="{{role.type}} {{role.name}}">{{role.name}}</span>
|
||||
<Hds::Badge @text={{role.type}} data-test-role-type-badge={{role.name}} />
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon @icon="more-horizontal" @text="More options" @hasChevron={{false}} data-test-popup-menu-trigger />
|
||||
{{#if role.canEdit}}
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="More options"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger="{{role.type}} {{role.name}}"
|
||||
/>
|
||||
{{#if (this.isHierarchical role.name)}}
|
||||
<dd.Interactive
|
||||
data-test-edit
|
||||
@route="roles.role.edit"
|
||||
data-test-subdirectory
|
||||
@route="roles.subdirectory"
|
||||
@models={{array role.type (concat role.path_to_role role.name)}}
|
||||
>Content</dd.Interactive>
|
||||
{{else}}
|
||||
{{#if role.canEdit}}
|
||||
<dd.Interactive
|
||||
data-test-edit
|
||||
@route="roles.role.edit"
|
||||
@models={{array role.type role.name}}
|
||||
>Edit</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canReadCreds}}
|
||||
<dd.Interactive data-test-get-creds @route="roles.role.credentials" @models={{array role.type role.name}}>
|
||||
Get credentials
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canRotateStaticCreds}}
|
||||
<dd.Interactive
|
||||
data-test-rotate-creds
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.credsToRotate) role)}}
|
||||
>Rotate credentials</dd.Interactive>
|
||||
{{/if}}
|
||||
<dd.Interactive
|
||||
data-test-details
|
||||
@route="roles.role.details"
|
||||
{{! this will force the roles.role model hook to fire since we may only have a partial model loaded in the list view }}
|
||||
@models={{array role.type role.name}}
|
||||
>Edit</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canReadCreds}}
|
||||
<dd.Interactive data-test-get-creds @route="roles.role.credentials" @models={{array role.type role.name}}>
|
||||
Get credentials
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if role.canRotateStaticCreds}}
|
||||
<dd.Interactive
|
||||
data-test-rotate-creds
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.credsToRotate) role)}}
|
||||
>Rotate credentials</dd.Interactive>
|
||||
{{/if}}
|
||||
<dd.Interactive
|
||||
data-test-details
|
||||
@route="roles.role.details"
|
||||
{{! this will force the roles.role model hook to fire since we may only have a partial model loaded in the list view }}
|
||||
@models={{array role.type role.name}}
|
||||
>Details</dd.Interactive>
|
||||
{{#if role.canDelete}}
|
||||
<dd.Interactive
|
||||
data-test-delete
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.roleToDelete) role)}}
|
||||
>Delete</dd.Interactive>
|
||||
>Details</dd.Interactive>
|
||||
{{#if role.canDelete}}
|
||||
<dd.Interactive
|
||||
data-test-delete
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.roleToDelete) role)}}
|
||||
>Delete</dd.Interactive>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
</Item.menu>
|
||||
@@ -108,7 +121,9 @@
|
||||
<Hds::Pagination::Numbered
|
||||
@currentPage={{@roles.meta.currentPage}}
|
||||
@currentPageSize={{@roles.meta.pageSize}}
|
||||
@route="roles"
|
||||
{{! localName will be either "index" or "subdirectory" }}
|
||||
@route="roles.{{this.router.currentRoute.localName}}"
|
||||
@models={{@currentRouteParams}}
|
||||
@showSizeSelector={{false}}
|
||||
@totalItems={{@roles.meta.filteredTotal}}
|
||||
@queryFunction={{this.paginationQueryParams}}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { getOwner } from '@ember/owner';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
@@ -15,7 +16,6 @@ import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type PaginationService from 'vault/services/pagination';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
interface Args {
|
||||
roles: Array<LdapRoleModel>;
|
||||
@@ -29,9 +29,20 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly pagination: PaginationService;
|
||||
|
||||
@tracked credsToRotate: LdapRoleModel | null = null;
|
||||
@tracked roleToDelete: LdapRoleModel | null = null;
|
||||
|
||||
isHierarchical = (name: string) => name.endsWith('/');
|
||||
|
||||
linkParams = (role: LdapRoleModel) => {
|
||||
const route = this.isHierarchical(role.name) ? 'roles.subdirectory' : 'roles.role.details';
|
||||
// if there is a path_to_role we're in a subdirectory
|
||||
// and must concat the ancestors with the leaf name to get the full role path
|
||||
const roleName = role.path_to_role ? role.path_to_role + role.name : role.name;
|
||||
return [route, role.type, roleName];
|
||||
};
|
||||
|
||||
get mountPoint(): string {
|
||||
const owner = getOwner(this) as EngineOwner;
|
||||
return owner.mountPoint;
|
||||
@@ -43,7 +54,8 @@ export default class LdapRolesPageComponent extends Component<Args> {
|
||||
|
||||
@action
|
||||
onFilterChange(pageFilter: string) {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles', { queryParams: { pageFilter } });
|
||||
// refresh route, which fires off lazyPaginatedQuery to re-request and filter response
|
||||
this.router.transitionTo(this.router?.currentRoute?.name, { queryParams: { pageFilter } });
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
10
ui/lib/ldap/addon/controllers/roles/subdirectory.ts
Normal file
10
ui/lib/ldap/addon/controllers/roles/subdirectory.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class LdapRolesSubdirectoryController extends Controller {
|
||||
queryParams = ['pageFilter', 'page'];
|
||||
}
|
||||
@@ -9,6 +9,8 @@ export default buildRoutes(function () {
|
||||
this.route('overview');
|
||||
this.route('roles', function () {
|
||||
this.route('create');
|
||||
// wildcard route so we can traverse hierarchical roles i.e. prod/admin/my-role
|
||||
this.route('subdirectory', { path: '/:type/subdirectory/*path_to_role' });
|
||||
this.route('role', { path: '/:type/:name' }, function () {
|
||||
this.route('details');
|
||||
this.route('edit');
|
||||
|
||||
@@ -16,14 +16,14 @@ import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
|
||||
|
||||
interface LdapConfigurationRouteModel {
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
configModel: LdapConfigModel;
|
||||
configError: AdapterError;
|
||||
}
|
||||
interface LdapConfigurationController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapConfigurationRouteModel;
|
||||
model: RouteModel;
|
||||
}
|
||||
|
||||
@withConfig('ldap/config')
|
||||
@@ -42,11 +42,7 @@ export default class LdapConfigurationRoute extends Route {
|
||||
};
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapConfigurationController,
|
||||
resolvedModel: LdapConfigurationRouteModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
|
||||
@@ -14,7 +14,7 @@ import type LdapConfigModel from 'vault/models/ldap/config';
|
||||
import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface LdapConfigureController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
}
|
||||
|
||||
@@ -30,11 +30,7 @@ export default class LdapConfigureRoute extends Route {
|
||||
return this.configModel || this.store.createRecord('ldap/config', { backend });
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapConfigureController,
|
||||
resolvedModel: LdapConfigModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: LdapConfigModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
|
||||
@@ -18,10 +18,10 @@ import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library';
|
||||
|
||||
interface LdapOverviewController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
}
|
||||
interface LdapOverviewRouteModel {
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
promptConfig: boolean;
|
||||
roles: Array<LdapRoleModel>;
|
||||
@@ -66,11 +66,7 @@ export default class LdapOverviewRoute extends Route {
|
||||
});
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapOverviewController,
|
||||
resolvedModel: LdapOverviewRouteModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
|
||||
31
ui/lib/ldap/addon/routes/roles.ts
Normal file
31
ui/lib/ldap/addon/routes/roles.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import type PaginationService from 'vault/services/pagination';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
|
||||
// Base class for roles/index and roles/subdirectory routes
|
||||
export default class LdapRolesRoute extends Route {
|
||||
@service declare readonly pagination: PaginationService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
lazyQuery(backendId: string, params: { page?: string; pageFilter: string }, adapterOptions: object) {
|
||||
const page = Number(params.page) || 1;
|
||||
return this.pagination.lazyPaginatedQuery(
|
||||
'ldap/role',
|
||||
{
|
||||
backend: backendId,
|
||||
page,
|
||||
pageFilter: params.pageFilter,
|
||||
responsePath: 'data.keys',
|
||||
skipCache: page === 1,
|
||||
},
|
||||
{ adapterOptions }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import type Controller from '@ember/controller';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface LdapRolesCreateController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRoleModel;
|
||||
}
|
||||
@@ -27,11 +27,7 @@ export default class LdapRolesCreateRoute extends Route {
|
||||
return this.store.createRecord('ldap/role', { backend });
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapRolesCreateController,
|
||||
resolvedModel: LdapRoleModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
|
||||
@@ -3,42 +3,32 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import LdapRolesRoute from '../roles';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfig } from 'core/decorators/fetch-secrets-engine-config';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
import type StoreService from 'vault/services/store';
|
||||
import type PaginationService from 'vault/services/pagination';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
import type Controller from '@ember/controller';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface LdapRolesRouteModel {
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
promptConfig: boolean;
|
||||
roles: Array<LdapRoleModel>;
|
||||
}
|
||||
interface LdapRolesController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRolesRouteModel;
|
||||
pageFilter: string | undefined;
|
||||
page: number | undefined;
|
||||
}
|
||||
|
||||
interface LdapRolesRouteParams {
|
||||
page?: string;
|
||||
pageFilter: string;
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: RouteModel;
|
||||
}
|
||||
|
||||
@withConfig('ldap/config')
|
||||
export default class LdapRolesRoute extends Route {
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly pagination: PaginationService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
export default class LdapRolesIndexRoute extends LdapRolesRoute {
|
||||
@service declare readonly store: StoreService; // necessary for @withConfig decorator
|
||||
|
||||
declare promptConfig: boolean;
|
||||
|
||||
@@ -51,39 +41,26 @@ export default class LdapRolesRoute extends Route {
|
||||
},
|
||||
};
|
||||
|
||||
model(params: LdapRolesRouteParams) {
|
||||
model(params: { page?: string; pageFilter: string }) {
|
||||
const backendModel = this.modelFor('application') as SecretEngineModel;
|
||||
return hash({
|
||||
backendModel,
|
||||
promptConfig: this.promptConfig,
|
||||
roles: this.pagination.lazyPaginatedQuery(
|
||||
'ldap/role',
|
||||
{
|
||||
backend: backendModel.id,
|
||||
page: Number(params.page) || 1,
|
||||
pageFilter: params.pageFilter,
|
||||
responsePath: 'data.keys',
|
||||
},
|
||||
{ adapterOptions: { showPartialError: true } }
|
||||
),
|
||||
roles: this.lazyQuery(backendModel.id, params, { showPartialError: true }),
|
||||
});
|
||||
}
|
||||
|
||||
setupController(
|
||||
controller: LdapRolesController,
|
||||
resolvedModel: LdapRolesRouteModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backendModel.id, route: 'overview', model: resolvedModel.backend },
|
||||
{ label: resolvedModel.backendModel.id, route: 'overview' },
|
||||
{ label: 'Roles' },
|
||||
];
|
||||
}
|
||||
|
||||
resetController(controller: LdapRolesController, isExiting: boolean) {
|
||||
resetController(controller: RouteController, isExiting: boolean) {
|
||||
if (isExiting) {
|
||||
controller.set('pageFilter', undefined);
|
||||
controller.set('page', undefined);
|
||||
|
||||
@@ -6,19 +6,17 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import { ModelFrom } from 'vault/vault/route';
|
||||
import type Store from '@ember-data/store';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
|
||||
interface LdapRoleRouteParams {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
export type LdapRolesRoleRouteModel = ModelFrom<LdapRolesRoleRoute>;
|
||||
|
||||
export default class LdapRoleRoute extends Route {
|
||||
export default class LdapRolesRoleRoute extends Route {
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
model(params: LdapRoleRouteParams) {
|
||||
model(params: { name: string; type: string }) {
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
const { name, type } = params;
|
||||
return this.store.queryRecord('ldap/role', { backend, name, type });
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
@@ -13,7 +14,7 @@ import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
|
||||
|
||||
export interface LdapStaticRoleCredentials {
|
||||
export interface StaticCredentials {
|
||||
dn: string;
|
||||
last_vault_rotation: string;
|
||||
password: string;
|
||||
@@ -23,7 +24,7 @@ export interface LdapStaticRoleCredentials {
|
||||
username: string;
|
||||
type: string;
|
||||
}
|
||||
export interface LdapDynamicRoleCredentials {
|
||||
export interface DynamicCredentials {
|
||||
distinguished_names: Array<string>;
|
||||
password: string;
|
||||
username: string;
|
||||
@@ -32,13 +33,13 @@ export interface LdapDynamicRoleCredentials {
|
||||
renewable: boolean;
|
||||
type: string;
|
||||
}
|
||||
interface LdapRoleCredentialsRouteModel {
|
||||
credentials: undefined | LdapStaticRoleCredentials | LdapDynamicRoleCredentials;
|
||||
interface RouteModel {
|
||||
credentials: undefined | StaticCredentials | DynamicCredentials;
|
||||
error: undefined | AdapterError;
|
||||
}
|
||||
interface LdapRoleCredentialsController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRoleCredentialsRouteModel;
|
||||
model: RouteModel;
|
||||
}
|
||||
|
||||
export default class LdapRoleCredentialsRoute extends Route {
|
||||
@@ -53,19 +54,16 @@ export default class LdapRoleCredentialsRoute extends Route {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
setupController(
|
||||
controller: LdapRoleCredentialsController,
|
||||
resolvedModel: LdapRoleCredentialsRouteModel,
|
||||
transition: Transition
|
||||
) {
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
const role = this.modelFor('roles.role') as LdapRoleModel;
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: role.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: role.name, route: 'roles.role' },
|
||||
{ label: 'credentials' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(role.name, role.type, role.backend),
|
||||
{ label: 'Credentials' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,31 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type Controller from '@ember/controller';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type Controller from '@ember/controller';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
|
||||
interface LdapRoleDetailsController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRoleModel;
|
||||
}
|
||||
|
||||
export default class LdapRoleEditRoute extends Route {
|
||||
setupController(
|
||||
controller: LdapRoleDetailsController,
|
||||
resolvedModel: LdapRoleModel,
|
||||
transition: Transition
|
||||
) {
|
||||
export default class LdapRolesRoleDetailsRoute extends Route {
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: resolvedModel.name },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, this.secretMountPath.currentPath, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,28 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type Controller from '@ember/controller';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
interface LdapRoleEditController extends Controller {
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: LdapRoleModel;
|
||||
}
|
||||
|
||||
export default class LdapRoleEditRoute extends Route {
|
||||
setupController(controller: LdapRoleEditController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
setupController(controller: RouteController, resolvedModel: LdapRoleModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: resolvedModel.backend, route: 'overview' },
|
||||
{ label: 'roles', route: 'roles' },
|
||||
{ label: resolvedModel.name, route: 'roles.role' },
|
||||
{ label: 'edit' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(resolvedModel.name, resolvedModel.type, resolvedModel.backend),
|
||||
{ label: 'Edit' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
75
ui/lib/ldap/addon/routes/roles/subdirectory.ts
Normal file
75
ui/lib/ldap/addon/routes/roles/subdirectory.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import LdapRolesRoute from '../roles';
|
||||
import { hash } from 'rsvp';
|
||||
import { ldapBreadcrumbs } from 'ldap/utils/ldap-breadcrumbs';
|
||||
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
import type Controller from '@ember/controller';
|
||||
import type LdapRoleModel from 'vault/models/ldap/role';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
|
||||
interface RouteModel {
|
||||
backendModel: SecretEngineModel;
|
||||
roleAncestry: { path_to_role: string; type: string };
|
||||
roles: Array<LdapRoleModel>;
|
||||
}
|
||||
|
||||
interface RouteController extends Controller {
|
||||
breadcrumbs: Array<Breadcrumb>;
|
||||
model: RouteModel;
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
page?: string;
|
||||
pageFilter: string;
|
||||
path_to_role: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default class LdapRolesSubdirectoryRoute extends LdapRolesRoute {
|
||||
queryParams = {
|
||||
pageFilter: {
|
||||
refreshModel: true,
|
||||
},
|
||||
page: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model(params: RouteParams) {
|
||||
const backendModel = this.modelFor('application') as SecretEngineModel;
|
||||
const { path_to_role, type } = params;
|
||||
const roleAncestry = { path_to_role, type };
|
||||
return hash({
|
||||
backendModel,
|
||||
roleAncestry,
|
||||
roles: this.lazyQuery(backendModel.id, params, { roleAncestry }),
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller: RouteController, resolvedModel: RouteModel, transition: Transition) {
|
||||
super.setupController(controller, resolvedModel, transition);
|
||||
const { backendModel, roleAncestry } = resolvedModel;
|
||||
const crumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: backendModel.id, route: 'overview' },
|
||||
{ label: 'Roles', route: 'roles' },
|
||||
...ldapBreadcrumbs(roleAncestry.path_to_role, roleAncestry.type, backendModel.id, true),
|
||||
];
|
||||
|
||||
// must call 'set' so breadcrumbs update as we navigate through directories
|
||||
controller.set('breadcrumbs', crumbs);
|
||||
}
|
||||
|
||||
resetController(controller: RouteController, isExiting: boolean) {
|
||||
if (isExiting) {
|
||||
controller.set('pageFilter', undefined);
|
||||
controller.set('page', undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
ui/lib/ldap/addon/templates/roles/subdirectory.hbs
Normal file
15
ui/lib/ldap/addon/templates/roles/subdirectory.hbs
Normal file
@@ -0,0 +1,15 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
{{#let this.model.roleAncestry.type this.model.roleAncestry.path_to_role as |roleType path_to_role|}}
|
||||
<Page::Roles
|
||||
@roles={{this.model.roles}}
|
||||
@promptConfig={{false}}
|
||||
@backendModel={{this.model.backendModel}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@pageFilter={{this.pageFilter}}
|
||||
@currentRouteParams={{array this.model.backendModel.id roleType path_to_role}}
|
||||
/>
|
||||
{{/let}}
|
||||
36
ui/lib/ldap/addon/utils/ldap-breadcrumbs.ts
Normal file
36
ui/lib/ldap/addon/utils/ldap-breadcrumbs.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
import type { Breadcrumb } from 'vault/vault/app-types';
|
||||
|
||||
export const ldapBreadcrumbs = (
|
||||
fullPath: string | undefined, // i.e. path/to/item
|
||||
roleType: string,
|
||||
mountPath: string,
|
||||
lastItemCurrent = false // this array of objects can be spread anywhere within the crumbs array
|
||||
): Breadcrumb[] => {
|
||||
if (!fullPath) return [];
|
||||
const ancestry = fullPath.split('/').filter((path) => path !== '');
|
||||
const isDirectory = fullPath.endsWith('/');
|
||||
|
||||
return ancestry.map((name: string, idx: number) => {
|
||||
const isLast = ancestry.length === idx + 1;
|
||||
// if the end of the path is the current route, don't return a route link
|
||||
if (isLast && lastItemCurrent) return { label: name };
|
||||
|
||||
// each segment is a continued concatenation of ancestral paths.
|
||||
// for example, if the full path to an item is "prod/admin/west"
|
||||
// the segments will be: prod/, prod/admin/, prod/admin/west.
|
||||
// LIST or GET requests can then be made for each crumb accordingly.
|
||||
const segment = ancestry.slice(0, idx + 1).join('/');
|
||||
|
||||
const itemPath = isLast && !isDirectory ? segment : `${segment}/`;
|
||||
const routeParams = [mountPath, roleType, itemPath];
|
||||
return {
|
||||
label: name,
|
||||
route: isLast && !isDirectory ? 'roles.role.details' : 'roles.subdirectory',
|
||||
models: routeParams,
|
||||
};
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user