Skip to content

Commit fd73ec9

Browse files
committed
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
1 parent 730d998 commit fd73ec9

File tree

29 files changed

+523
-243
lines changed

29 files changed

+523
-243
lines changed

changelog/29293.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
```release-note:improvement
2+
ui: Adds navigation for LDAP hierarchical libraries
3+
```
4+
```release-note:bug
5+
ui: Fixes navigation for quick actions in LDAP roles' popup menu
6+
```

ui/app/adapters/ldap/library.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,28 @@ import NamedPathAdapter from 'vault/adapters/named-path';
77
import { encodePath } from 'vault/utils/path-encoding-helpers';
88

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

1516
urlForUpdateRecord(name, modelName, snapshot) {
16-
return this.getURL(snapshot.attr('backend'), name);
17+
// when editing the name IS the full path so we can use "name" instead of "completeLibraryName" here
18+
return this._getURL(snapshot.attr('backend'), name);
1719
}
1820
urlForDeleteRecord(name, modelName, snapshot) {
19-
return this.getURL(snapshot.attr('backend'), name);
21+
const { backend, completeLibraryName } = snapshot.record;
22+
return this._getURL(backend, completeLibraryName);
2023
}
2124

2225
query(store, type, query) {
23-
const { backend } = query;
24-
return this.ajax(this.getURL(backend), 'GET', { data: { list: true } })
26+
const { backend, path_to_library } = query;
27+
// if we have a path_to_library then we're listing subdirectories at a hierarchical library path (i.e west-account/my-library)
28+
const url = this._getURL(backend, path_to_library);
29+
return this.ajax(url, 'GET', { data: { list: true } })
2530
.then((resp) => {
26-
return resp.data.keys.map((name) => ({ name, backend }));
31+
return resp.data.keys.map((name) => ({ name, backend, path_to_library }));
2732
})
2833
.catch((error) => {
2934
if (error.httpStatus === 404) {
@@ -34,11 +39,11 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
3439
}
3540
queryRecord(store, type, query) {
3641
const { backend, name } = query;
37-
return this.ajax(this.getURL(backend, name), 'GET').then((resp) => ({ ...resp.data, backend, name }));
42+
return this.ajax(this._getURL(backend, name), 'GET').then((resp) => ({ ...resp.data, backend, name }));
3843
}
3944

4045
fetchStatus(backend, name) {
41-
const url = `${this.getURL(backend, name)}/status`;
46+
const url = `${this._getURL(backend, name)}/status`;
4247
return this.ajax(url, 'GET').then((resp) => {
4348
const statuses = [];
4449
for (const key in resp.data) {
@@ -53,15 +58,15 @@ export default class LdapLibraryAdapter extends NamedPathAdapter {
5358
});
5459
}
5560
checkOutAccount(backend, name, ttl) {
56-
const url = `${this.getURL(backend, name)}/check-out`;
61+
const url = `${this._getURL(backend, name)}/check-out`;
5762
return this.ajax(url, 'POST', { data: { ttl } }).then((resp) => {
5863
const { lease_id, lease_duration, renewable } = resp;
5964
const { service_account_name: account, password } = resp.data;
6065
return { account, password, lease_id, lease_duration, renewable };
6166
});
6267
}
6368
checkInAccount(backend, name, service_account_names) {
64-
const url = `${this.getURL(backend, name)}/check-in`;
69+
const url = `${this._getURL(backend, name)}/check-in`;
6570
return this.ajax(url, 'POST', { data: { service_account_names } }).then((resp) => resp.data);
6671
}
6772
}

ui/app/adapters/ldap/role.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ export default class LdapRoleAdapter extends ApplicationAdapter {
5656
}
5757

5858
urlForDeleteRecord(id, modelName, snapshot) {
59-
const { backend, type, name } = snapshot.record;
60-
return this._getURL(backend, this._pathForRoleType(type), name);
59+
const { backend, type, completeRoleName } = snapshot.record;
60+
return this._getURL(backend, this._pathForRoleType(type), completeRoleName);
6161
}
6262

6363
/*

ui/app/models/ldap/library.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const formFields = ['name', 'service_account_names', 'ttl', 'max_ttl', 'disable_
1818
@withFormFields(formFields)
1919
export default class LdapLibraryModel extends Model {
2020
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
21+
@attr('string') path_to_library; // ancestral path to the library added in the adapter (only exists for nested libraries)
2122

2223
@attr('string', {
2324
label: 'Library name',
@@ -64,6 +65,12 @@ export default class LdapLibraryModel extends Model {
6465
})
6566
disable_check_in_enforcement;
6667

68+
get completeLibraryName() {
69+
// if there is a path_to_library then the name is hierarchical
70+
// and we must concat the ancestors with the leaf name to get the full library path
71+
return this.path_to_library ? this.path_to_library + this.name : this.name;
72+
}
73+
6774
get displayFields() {
6875
return this.formFields.filter((field) => field.name !== 'service_account_names');
6976
}

ui/app/models/ldap/role.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ export default class LdapRoleModel extends Model {
163163
})
164164
rollback_ldif;
165165

166+
get completeRoleName() {
167+
// if there is a path_to_role then the name is hierarchical
168+
// and we must concat the ancestors with the leaf name to get the full role path
169+
return this.path_to_role ? this.path_to_role + this.name : this.name;
170+
}
171+
166172
get isStatic() {
167173
return this.type === 'static';
168174
}
@@ -224,9 +230,11 @@ export default class LdapRoleModel extends Model {
224230
}
225231

226232
fetchCredentials() {
227-
return this.store.adapterFor('ldap/role').fetchCredentials(this.backend, this.type, this.name);
233+
return this.store
234+
.adapterFor('ldap/role')
235+
.fetchCredentials(this.backend, this.type, this.completeRoleName);
228236
}
229237
rotateStaticPassword() {
230-
return this.store.adapterFor('ldap/role').rotateStaticPassword(this.backend, this.name);
238+
return this.store.adapterFor('ldap/role').rotateStaticPassword(this.backend, this.completeRoleName);
231239
}
232240
}

ui/lib/ldap/addon/components/page/libraries.hbs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
{{else}}
4444
<div class="has-bottom-margin-s">
4545
{{#each this.filteredLibraries as |library|}}
46-
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "libraries.library.details" library.name}} as |Item|>
46+
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{this.linkParams library}} as |Item|>
4747
<Item.content>
4848
<Icon @name="folder" />
49-
<span data-test-library={{library.name}}>{{library.name}}</span>
49+
<span data-test-library={{library.completeLibraryName}}>{{library.name}}</span>
5050
</Item.content>
5151
<Item.menu>
5252
{{#if (or library.canRead library.canEdit library.canDelete)}}
@@ -55,21 +55,40 @@
5555
@icon="more-horizontal"
5656
@text="More options"
5757
@hasChevron={{false}}
58-
data-test-popup-menu-trigger
58+
data-test-popup-menu-trigger={{library.completeLibraryName}}
5959
/>
60-
{{#if library.canEdit}}
61-
<dd.Interactive @text="Edit" data-test-edit @route="libraries.library.edit" @model={{library}} />
62-
{{/if}}
63-
{{#if library.canRead}}
64-
<dd.Interactive @text="Details" data-test-details @route="libraries.library.details" @model={{library}} />
65-
{{/if}}
66-
{{#if library.canDelete}}
60+
{{#if (this.isHierarchical library.name)}}
6761
<dd.Interactive
68-
@text="Delete"
69-
data-test-delete
70-
@color="critical"
71-
{{on "click" (fn (mut this.libraryToDelete) library)}}
62+
@text="Content"
63+
data-test-subdirectory
64+
@route="libraries.subdirectory"
65+
@model={{library.completeLibraryName}}
7266
/>
67+
{{else}}
68+
{{#if library.canEdit}}
69+
<dd.Interactive
70+
@text="Edit"
71+
data-test-edit
72+
@route="libraries.library.edit"
73+
@model={{library.completeLibraryName}}
74+
/>
75+
{{/if}}
76+
{{#if library.canRead}}
77+
<dd.Interactive
78+
@text="Details"
79+
data-test-details
80+
@route="libraries.library.details"
81+
@model={{library.completeLibraryName}}
82+
/>
83+
{{/if}}
84+
{{#if library.canDelete}}
85+
<dd.Interactive
86+
@text="Delete"
87+
data-test-delete
88+
@color="critical"
89+
{{on "click" (fn (mut this.libraryToDelete) library)}}
90+
/>
91+
{{/if}}
7392
{{/if}}
7493
</Hds::Dropdown>
7594
{{/if}}

ui/lib/ldap/addon/components/page/libraries.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type LdapLibraryModel from 'vault/models/ldap/library';
1414
import type SecretEngineModel from 'vault/models/secret-engine';
1515
import type FlashMessageService from 'vault/services/flash-messages';
1616
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
17+
import type RouterService from '@ember/routing/router-service';
1718

1819
interface Args {
1920
libraries: Array<LdapLibraryModel>;
@@ -24,10 +25,18 @@ interface Args {
2425

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

2830
@tracked filterValue = '';
2931
@tracked libraryToDelete: LdapLibraryModel | null = null;
3032

33+
isHierarchical = (name: string) => name.endsWith('/');
34+
35+
linkParams = (library: LdapLibraryModel) => {
36+
const route = this.isHierarchical(library.name) ? 'libraries.subdirectory' : 'libraries.library.details';
37+
return [route, library.completeLibraryName];
38+
};
39+
3140
get mountPoint(): string {
3241
const owner = getOwner(this) as EngineOwner;
3342
return owner.mountPoint;
@@ -43,8 +52,9 @@ export default class LdapLibrariesPageComponent extends Component<Args> {
4352
@action
4453
async onDelete(model: LdapLibraryModel) {
4554
try {
46-
const message = `Successfully deleted library ${model.name}.`;
55+
const message = `Successfully deleted library ${model.completeLibraryName}.`;
4756
await model.destroyRecord();
57+
this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries');
4858
this.flashMessages.success(message);
4959
} catch (error) {
5060
this.flashMessages.danger(`Error deleting library \n ${errorMessage(error)}`);

ui/lib/ldap/addon/components/page/roles.hbs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
@text="Content"
6363
data-test-subdirectory
6464
@route="roles.subdirectory"
65-
@models={{array role.type (concat role.path_to_role role.name)}}
65+
@models={{array role.type role.completeRoleName}}
6666
/>
6767
{{else}}
6868
{{#if role.canEdit}}
@@ -73,7 +73,7 @@
7373
@text="Get credentials"
7474
data-test-get-creds
7575
@route="roles.role.credentials"
76-
@models={{array role.type role.name}}
76+
@models={{array role.type role.completeRoleName}}
7777
/>
7878
{{/if}}
7979
{{#if role.canRotateStaticCreds}}
@@ -89,7 +89,7 @@
8989
data-test-details
9090
@route="roles.role.details"
9191
{{! this will force the roles.role model hook to fire since we may only have a partial model loaded in the list view }}
92-
@models={{array role.type role.name}}
92+
@models={{array role.type role.completeRoleName}}
9393
/>
9494
{{#if role.canDelete}}
9595
<dd.Interactive

ui/lib/ldap/addon/components/page/roles.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
3636

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

4542
get mountPoint(): string {
@@ -60,7 +57,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
6057
@action
6158
async onRotate(model: LdapRoleModel) {
6259
try {
63-
const message = `Successfully rotated credentials for ${model.name}.`;
60+
const message = `Successfully rotated credentials for ${model.completeRoleName}.`;
6461
await model.rotateStaticPassword();
6562
this.flashMessages.success(message);
6663
} catch (error) {
@@ -73,7 +70,7 @@ export default class LdapRolesPageComponent extends Component<Args> {
7370
@action
7471
async onDelete(model: LdapRoleModel) {
7572
try {
76-
const message = `Successfully deleted role ${model.name}.`;
73+
const message = `Successfully deleted role ${model.completeRoleName}.`;
7774
await model.destroyRecord();
7875
this.store.clearDataset('ldap/role');
7976
this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles');

ui/lib/ldap/addon/routes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export default buildRoutes(function () {
1919
});
2020
this.route('libraries', function () {
2121
this.route('create');
22+
// wildcard route so we can traverse hierarchical libraries i.e. prod/admin/my-library
23+
this.route('subdirectory', { path: '/subdirectory/*path_to_library' });
2224
this.route('library', { path: '/:name' }, function () {
2325
this.route('details', function () {
2426
this.route('accounts');

0 commit comments

Comments
 (0)