Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport 1.18.x - UI: LDAP Hierarchical Library names #29384

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/29293.txt
Original file line number Diff line number Diff line change
@@ -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
```
3 changes: 3 additions & 0 deletions changelog/29376.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
activity: Include activity records from clients created by deleted or disabled auth mounts in Export API response.
```
27 changes: 16 additions & 11 deletions ui/app/adapters/ldap/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,28 @@ import NamedPathAdapter from 'vault/adapters/named-path';
import { encodePath } from 'vault/utils/path-encoding-helpers';

export default class LdapLibraryAdapter extends NamedPathAdapter {
getURL(backend, name) {
// path could be the library name (full path) or just part of the path i.e. west-account/
_getURL(backend, path) {
const base = `${this.buildURL()}/${encodePath(backend)}/library`;
return name ? `${base}/${name}` : base;
return path ? `${base}/${path}` : base;
}

urlForUpdateRecord(name, modelName, snapshot) {
return this.getURL(snapshot.attr('backend'), name);
// when editing the name IS the full path so we can use "name" instead of "completeLibraryName" here
return this._getURL(snapshot.attr('backend'), name);
}
urlForDeleteRecord(name, modelName, snapshot) {
return this.getURL(snapshot.attr('backend'), name);
const { backend, completeLibraryName } = snapshot.record;
return this._getURL(backend, completeLibraryName);
}

query(store, type, query) {
const { backend } = query;
return this.ajax(this.getURL(backend), 'GET', { data: { list: true } })
const { backend, path_to_library } = query;
// if we have a path_to_library then we're listing subdirectories at a hierarchical library path (i.e west-account/my-library)
const url = this._getURL(backend, path_to_library);
return this.ajax(url, 'GET', { data: { list: true } })
.then((resp) => {
return resp.data.keys.map((name) => ({ name, backend }));
return resp.data.keys.map((name) => ({ name, backend, path_to_library }));
})
.catch((error) => {
if (error.httpStatus === 404) {
Expand All @@ -34,11 +39,11 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
}
queryRecord(store, type, query) {
const { backend, name } = query;
return this.ajax(this.getURL(backend, name), 'GET').then((resp) => ({ ...resp.data, backend, name }));
return this.ajax(this._getURL(backend, name), 'GET').then((resp) => ({ ...resp.data, backend, name }));
}

fetchStatus(backend, name) {
const url = `${this.getURL(backend, name)}/status`;
const url = `${this._getURL(backend, name)}/status`;
return this.ajax(url, 'GET').then((resp) => {
const statuses = [];
for (const key in resp.data) {
Expand All @@ -53,15 +58,15 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
});
}
checkOutAccount(backend, name, ttl) {
const url = `${this.getURL(backend, name)}/check-out`;
const url = `${this._getURL(backend, name)}/check-out`;
return this.ajax(url, 'POST', { data: { ttl } }).then((resp) => {
const { lease_id, lease_duration, renewable } = resp;
const { service_account_name: account, password } = resp.data;
return { account, password, lease_id, lease_duration, renewable };
});
}
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);
}
}
4 changes: 2 additions & 2 deletions ui/app/adapters/ldap/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export default class LdapRoleAdapter extends ApplicationAdapter {
}

urlForDeleteRecord(id, modelName, snapshot) {
const { backend, type, name } = snapshot.record;
return this._getURL(backend, this._pathForRoleType(type), name);
const { backend, type, completeRoleName } = snapshot.record;
return this._getURL(backend, this._pathForRoleType(type), completeRoleName);
}

/*
Expand Down
7 changes: 7 additions & 0 deletions ui/app/models/ldap/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const formFields = ['name', 'service_account_names', 'ttl', 'max_ttl', 'disable_
@withFormFields(formFields)
export default class LdapLibraryModel extends Model {
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
@attr('string') path_to_library; // ancestral path to the library added in the adapter (only exists for nested libraries)

@attr('string', {
label: 'Library name',
Expand Down Expand Up @@ -64,6 +65,12 @@ export default class LdapLibraryModel extends Model {
})
disable_check_in_enforcement;

get completeLibraryName() {
// if there is a path_to_library then the name is hierarchical
// and we must concat the ancestors with the leaf name to get the full library path
return this.path_to_library ? this.path_to_library + this.name : this.name;
}

get displayFields() {
return this.formFields.filter((field) => field.name !== 'service_account_names');
}
Expand Down
12 changes: 10 additions & 2 deletions ui/app/models/ldap/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ export default class LdapRoleModel extends Model {
})
rollback_ldif;

get completeRoleName() {
// if there is a path_to_role then the name is hierarchical
// and we must concat the ancestors with the leaf name to get the full role path
return this.path_to_role ? this.path_to_role + this.name : this.name;
}

get isStatic() {
return this.type === 'static';
}
Expand Down Expand Up @@ -224,9 +230,11 @@ export default class LdapRoleModel extends Model {
}

fetchCredentials() {
return this.store.adapterFor('ldap/role').fetchCredentials(this.backend, this.type, this.name);
return this.store
.adapterFor('ldap/role')
.fetchCredentials(this.backend, this.type, this.completeRoleName);
}
rotateStaticPassword() {
return this.store.adapterFor('ldap/role').rotateStaticPassword(this.backend, this.name);
return this.store.adapterFor('ldap/role').rotateStaticPassword(this.backend, this.completeRoleName);
}
}
47 changes: 33 additions & 14 deletions ui/lib/ldap/addon/components/page/libraries.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
{{else}}
<div class="has-bottom-margin-s">
{{#each this.filteredLibraries as |library|}}
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "libraries.library.details" library.name}} as |Item|>
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{this.linkParams library}} as |Item|>
<Item.content>
<Icon @name="folder" />
<span data-test-library={{library.name}}>{{library.name}}</span>
<span data-test-library={{library.completeLibraryName}}>{{library.name}}</span>
</Item.content>
<Item.menu>
{{#if (or library.canRead library.canEdit library.canDelete)}}
Expand All @@ -55,21 +55,40 @@
@icon="more-horizontal"
@text="More options"
@hasChevron={{false}}
data-test-popup-menu-trigger
data-test-popup-menu-trigger={{library.completeLibraryName}}
/>
{{#if library.canEdit}}
<dd.Interactive @text="Edit" data-test-edit @route="libraries.library.edit" @model={{library}} />
{{/if}}
{{#if library.canRead}}
<dd.Interactive @text="Details" data-test-details @route="libraries.library.details" @model={{library}} />
{{/if}}
{{#if library.canDelete}}
{{#if (this.isHierarchical library.name)}}
<dd.Interactive
@text="Delete"
data-test-delete
@color="critical"
{{on "click" (fn (mut this.libraryToDelete) library)}}
@text="Content"
data-test-subdirectory
@route="libraries.subdirectory"
@model={{library.completeLibraryName}}
/>
{{else}}
{{#if library.canEdit}}
<dd.Interactive
@text="Edit"
data-test-edit
@route="libraries.library.edit"
@model={{library.completeLibraryName}}
/>
{{/if}}
{{#if library.canRead}}
<dd.Interactive
@text="Details"
data-test-details
@route="libraries.library.details"
@model={{library.completeLibraryName}}
/>
{{/if}}
{{#if library.canDelete}}
<dd.Interactive
@text="Delete"
data-test-delete
@color="critical"
{{on "click" (fn (mut this.libraryToDelete) library)}}
/>
{{/if}}
{{/if}}
</Hds::Dropdown>
{{/if}}
Expand Down
12 changes: 11 additions & 1 deletion ui/lib/ldap/addon/components/page/libraries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
import type SecretEngineModel from 'vault/models/secret-engine';
import type FlashMessageService from 'vault/services/flash-messages';
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
import type RouterService from '@ember/routing/router-service';

interface Args {
libraries: Array<LdapLibraryModel>;
Expand All @@ -24,10 +25,18 @@ interface Args {

export default class LdapLibrariesPageComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@service('app-router') declare readonly router: RouterService;

@tracked filterValue = '';
@tracked libraryToDelete: LdapLibraryModel | null = null;

isHierarchical = (name: string) => name.endsWith('/');

linkParams = (library: LdapLibraryModel) => {
const route = this.isHierarchical(library.name) ? 'libraries.subdirectory' : 'libraries.library.details';
return [route, library.completeLibraryName];
};

get mountPoint(): string {
const owner = getOwner(this) as EngineOwner;
return owner.mountPoint;
Expand All @@ -43,8 +52,9 @@ export default class LdapLibrariesPageComponent extends Component<Args> {
@action
async onDelete(model: LdapLibraryModel) {
try {
const message = `Successfully deleted library ${model.name}.`;
const message = `Successfully deleted library ${model.completeLibraryName}.`;
await model.destroyRecord();
this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries');
this.flashMessages.success(message);
} catch (error) {
this.flashMessages.danger(`Error deleting library \n ${errorMessage(error)}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default class LdapLibraryDetailsAccountsPageComponent extends Component<A
@tracked checkOutTtl: string | null = null;

get cliCommand() {
return `vault lease renew ad/library/${this.args.library.name}/check-out/:lease_id`;
return `vault lease renew ${this.args.library.backend}/library/${this.args.library.name}/check-out/:lease_id`;
}
@action
setTtl(data: TtlEvent) {
Expand Down
6 changes: 3 additions & 3 deletions ui/lib/ldap/addon/components/page/roles.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
@text="Content"
data-test-subdirectory
@route="roles.subdirectory"
@models={{array role.type (concat role.path_to_role role.name)}}
@models={{array role.type role.completeRoleName}}
/>
{{else}}
{{#if role.canEdit}}
Expand All @@ -73,7 +73,7 @@
@text="Get credentials"
data-test-get-creds
@route="roles.role.credentials"
@models={{array role.type role.name}}
@models={{array role.type role.completeRoleName}}
/>
{{/if}}
{{#if role.canRotateStaticCreds}}
Expand All @@ -89,7 +89,7 @@
data-test-details
@route="roles.role.details"
{{! this will force the roles.role model hook to fire since we may only have a partial model loaded in the list view }}
@models={{array role.type role.name}}
@models={{array role.type role.completeRoleName}}
/>
{{#if role.canDelete}}
<dd.Interactive
Expand Down
9 changes: 3 additions & 6 deletions ui/lib/ldap/addon/components/page/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ export default class LdapRolesPageComponent extends Component<Args> {

linkParams = (role: LdapRoleModel) => {
const route = this.isHierarchical(role.name) ? 'roles.subdirectory' : 'roles.role.details';
// if there is a path_to_role we're in a subdirectory
// and must concat the ancestors with the leaf name to get the full role path
const roleName = role.path_to_role ? role.path_to_role + role.name : role.name;
return [route, role.type, roleName];
return [route, role.type, role.completeRoleName];
};

get mountPoint(): string {
Expand All @@ -60,7 +57,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
@action
async onRotate(model: LdapRoleModel) {
try {
const message = `Successfully rotated credentials for ${model.name}.`;
const message = `Successfully rotated credentials for ${model.completeRoleName}.`;
await model.rotateStaticPassword();
this.flashMessages.success(message);
} catch (error) {
Expand All @@ -73,7 +70,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
@action
async onDelete(model: LdapRoleModel) {
try {
const message = `Successfully deleted role ${model.name}.`;
const message = `Successfully deleted role ${model.completeRoleName}.`;
await model.destroyRecord();
this.store.clearDataset('ldap/role');
this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles');
Expand Down
2 changes: 2 additions & 0 deletions ui/lib/ldap/addon/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default buildRoutes(function () {
});
this.route('libraries', function () {
this.route('create');
// wildcard route so we can traverse hierarchical libraries i.e. prod/admin/my-library
this.route('subdirectory', { path: '/subdirectory/*path_to_library' });
this.route('library', { path: '/:name' }, function () {
this.route('details', function () {
this.route('accounts');
Expand Down
11 changes: 7 additions & 4 deletions ui/lib/ldap/addon/routes/libraries/library/check-out.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type Transition from '@ember/routing/transition';
import type { Breadcrumb } from 'vault/vault/app-types';
import { LdapLibraryCheckOutCredentials } from 'vault/vault/adapters/ldap/library';
import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';

interface LdapLibraryCheckOutController extends Controller {
breadcrumbs: Array<Breadcrumb>;
Expand Down Expand Up @@ -45,13 +46,15 @@ export default class LdapLibraryCheckOutRoute extends Route {
transition: Transition
) {
super.setupController(controller, resolvedModel, transition);

const library = this.modelFor('libraries.library') as LdapLibraryModel;
const routeParams = (childResource: string) => {
return [library.backend, childResource];
};
controller.breadcrumbs = [
{ label: library.backend, route: 'overview' },
{ label: 'libraries', route: 'libraries' },
{ label: library.name, route: 'libraries.library' },
{ label: 'check-out' },
{ label: 'Libraries', route: 'libraries' },
...ldapBreadcrumbs(library.name, routeParams, libraryRoutes),
{ label: 'Check-Out' },
];
}

Expand Down
9 changes: 7 additions & 2 deletions ui/lib/ldap/addon/routes/libraries/library/details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
import type Controller from '@ember/controller';
import type Transition from '@ember/routing/transition';
import type { Breadcrumb } from 'vault/vault/app-types';
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';

interface LdapLibraryDetailsController extends Controller {
breadcrumbs: Array<Breadcrumb>;
Expand All @@ -23,10 +24,14 @@ export default class LdapLibraryDetailsRoute extends Route {
) {
super.setupController(controller, resolvedModel, transition);

const routeParams = (childResource: string) => {
return [resolvedModel.backend, childResource];
};

controller.breadcrumbs = [
{ label: resolvedModel.backend, route: 'overview' },
{ label: 'libraries', route: 'libraries' },
{ label: resolvedModel.name },
{ label: 'Libraries', route: 'libraries' },
...ldapBreadcrumbs(resolvedModel.name, routeParams, libraryRoutes, true),
];
}
}
10 changes: 7 additions & 3 deletions ui/lib/ldap/addon/routes/libraries/library/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
import type Controller from '@ember/controller';
import type Transition from '@ember/routing/transition';
import type { Breadcrumb } from 'vault/vault/app-types';
import { ldapBreadcrumbs, libraryRoutes } from 'ldap/utils/ldap-breadcrumbs';

interface LdapLibraryEditController extends Controller {
breadcrumbs: Array<Breadcrumb>;
Expand All @@ -23,11 +24,14 @@ export default class LdapLibraryEditRoute extends Route {
) {
super.setupController(controller, resolvedModel, transition);

const routeParams = (childResource: string) => {
return [resolvedModel.backend, childResource];
};
controller.breadcrumbs = [
{ label: resolvedModel.backend, route: 'overview' },
{ label: 'libraries', route: 'libraries' },
{ label: resolvedModel.name, route: 'libraries.library.details' },
{ label: 'edit' },
{ label: 'Libraries', route: 'libraries' },
...ldapBreadcrumbs(resolvedModel.name, routeParams, libraryRoutes),
{ label: 'Edit' },
];
}
}
Loading
Loading