Skip to content

Commit 2e0f2f1

Browse files
[UI] Ember Data Migration - LDAP Library (hashicorp#11254) (hashicorp#11260)
* removes withConfig decorator and moves check to application route * updates backendModel references in ldap engine to secretsEngine * adds ldap config form class * updates ldap config type in application route * updates ldap configure and configuration routes to use api service * adds capabilities service to ldap engine * updates ldap mirage handler and scenario * adds ldap capabilities constants and helper for fetching capabilities for roles * updates ldap roles view to use api service * updates ldap role details view to use api service * updates ldap role create/edit views to use api service and form classes * updates ldap role subdirectory view to use api service * updates ldap role credentials view to use api service * updates ldap libraries list views to use api service * updates ldap library details view to use api service * updates ldap library details accounts view to use api service * updates ldap library details accounts check out view to use api service * updates ldap library details configuration view to use api service * updates ldap library create/edit workflows to use api service and form class * fixes lint errors * removes errant log Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
1 parent 0013387 commit 2e0f2f1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+697
-458
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2025
3+
* SPDX-License-Identifier: BUSL-1.1
4+
*/
5+
6+
import Form from 'vault/forms/form';
7+
import FormField from 'vault/utils/forms/field';
8+
9+
import type { Validations } from 'vault/app-types';
10+
import type { LdapLibraryConfigureRequest } from '@hashicorp/vault-client-typescript';
11+
12+
// omit 'disable_check_in_enforcement' here to transform it from boolean to string for display
13+
type LdapLibraryFormData = Omit<LdapLibraryConfigureRequest, 'disable_check_in_enforcement'> & {
14+
disable_check_in_enforcement: string;
15+
name: string;
16+
};
17+
18+
export default class LdapLibraryForm extends Form<LdapLibraryFormData> {
19+
formFields = [
20+
new FormField('name', 'string', {
21+
label: 'Library name',
22+
editDisabled: true,
23+
}),
24+
new FormField('service_account_names', 'string', {
25+
editType: 'stringArray',
26+
label: 'Accounts',
27+
subText:
28+
'The names of all the accounts that can be checked out from this set. These accounts must only be used by Vault, and may only be in one set.',
29+
}),
30+
new FormField('ttl', 'string', {
31+
editType: 'ttl',
32+
label: 'Default lease TTL',
33+
helperTextDisabled: 'Vault will use the default lease duration.',
34+
defaultValue: '24h',
35+
defaultShown: 'Engine default',
36+
}),
37+
new FormField('max_ttl', 'string', {
38+
editType: 'ttl',
39+
label: 'Max lease TTL',
40+
helperTextDisabled: 'Vault will use the default lease duration.',
41+
defaultValue: '24h',
42+
defaultShown: 'Engine default',
43+
}),
44+
// this is a boolean from the server but is transformed in the serializer to display as Disabled or Enabled
45+
new FormField('disable_check_in_enforcement', 'string', {
46+
editType: 'radio',
47+
label: 'Check-in enforcement',
48+
subText:
49+
'When enabled, accounts must be checked in by the entity or client token that checked them out. If disabled, anyone with the right permission can check the account back in.',
50+
possibleValues: ['Disabled', 'Enabled'],
51+
}),
52+
];
53+
54+
validations: Validations = {
55+
name: [{ type: 'presence', message: 'Library name is required.' }],
56+
service_account_names: [{ type: 'presence', message: 'At least one service account is required.' }],
57+
};
58+
}

ui/app/utils/constants/capabilities.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,7 @@ export const PATH_MAP = {
5353
ldapRotateStaticRole: apiPath`${'backend'}/rotate-role/${'name'}`,
5454
ldapStaticRoleCreds: apiPath`${'backend'}/static-cred/${'name'}`,
5555
ldapDynamicRoleCreds: apiPath`${'backend'}/creds/${'name'}`,
56+
ldapLibrary: apiPath`${'backend'}/library/${'name'}`,
57+
ldapLibraryCheckOut: apiPath`${'backend'}/library/${'name'}/check-out`,
58+
ldapLibraryCheckIn: apiPath`${'backend'}/library/${'name'}/check-in`,
5659
};

ui/lib/ldap/addon/components/accounts-checked-out.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ import { tracked } from '@glimmer/tracking';
88
import { service } from '@ember/service';
99
import { task } from 'ember-concurrency';
1010
import { waitFor } from '@ember/test-waiters';
11-
import errorMessage from 'vault/utils/error-message';
1211

1312
import type FlashMessageService from 'vault/services/flash-messages';
1413
import type AuthService from 'vault/services/auth';
15-
import type LdapLibraryModel from 'vault/models/ldap/library';
14+
import type { LdapLibrary } from 'vault/secrets/ldap';
1615
import type { LdapLibraryAccountStatus } from 'vault/adapters/ldap/library';
16+
import type { CapabilitiesMap } from 'vault/app-types';
17+
import type CapabilitiesService from 'vault/services/capabilities';
18+
import type ApiService from 'vault/services/api';
19+
import type SecretMountPath from 'vault/services/secret-mount-path';
1720

1821
interface Args {
19-
libraries: Array<LdapLibraryModel>;
22+
libraries: Array<LdapLibrary>;
23+
capabilities: CapabilitiesMap;
2024
statuses: Array<LdapLibraryAccountStatus>;
2125
showLibraryColumn: boolean;
2226
onCheckInSuccess: CallableFunction;
@@ -26,6 +30,9 @@ interface Args {
2630
export default class LdapAccountsCheckedOutComponent extends Component<Args> {
2731
@service declare readonly flashMessages: FlashMessageService;
2832
@service declare readonly auth: AuthService;
33+
@service declare readonly capabilities: CapabilitiesService;
34+
@service declare readonly api: ApiService;
35+
@service declare readonly secretMountPath: SecretMountPath;
2936

3037
@tracked selectedStatus: LdapLibraryAccountStatus | undefined;
3138

@@ -39,38 +46,47 @@ export default class LdapAccountsCheckedOutComponent extends Component<Args> {
3946

4047
get filteredAccounts() {
4148
// filter status to only show checked out accounts associated to the current user
42-
// if disable_check_in_enforcement is true on the library set then all checked out accounts are displayed
49+
// if disable_check_in_enforcement is true on the library then all checked out accounts are displayed
4350
return this.args.statuses.filter((status) => {
4451
const authEntityId = this.auth.authData?.entityId;
4552
const isRoot = !status.borrower_entity_id && !authEntityId; // root user will not have an entity id and it won't be populated on status
4653
const isEntity = status.borrower_entity_id === authEntityId;
4754
const library = this.findLibrary(status.library);
48-
const enforcementDisabled = library.disable_check_in_enforcement === 'Disabled';
4955

50-
return !status.available && (enforcementDisabled || isEntity || isRoot);
56+
return !status.available && (library.disable_check_in_enforcement || isEntity || isRoot);
5157
});
5258
}
5359

54-
disableCheckIn = (name: string) => {
55-
return !this.findLibrary(name).canCheckIn;
60+
disableCheckIn = (libraryName: string) => {
61+
const { completeLibraryName: name } = this.findLibrary(libraryName);
62+
const { currentPath: backend } = this.secretMountPath;
63+
const path = this.capabilities.pathFor('ldapLibraryCheckIn', { backend, name });
64+
const { canUpdate } = this.args.capabilities[path] || {};
65+
return !canUpdate;
5666
};
5767

58-
findLibrary(name: string): LdapLibraryModel {
59-
return this.args.libraries.find((library) => library.completeLibraryName === name) as LdapLibraryModel;
68+
findLibrary(name: string): LdapLibrary {
69+
return this.args.libraries.find((library) => library.completeLibraryName === name) as LdapLibrary;
6070
}
6171

62-
@task
63-
@waitFor
64-
*checkIn() {
65-
const { library, account } = this.selectedStatus as LdapLibraryAccountStatus;
66-
try {
67-
const libraryModel = this.findLibrary(library);
68-
yield libraryModel.checkInAccount(account);
69-
this.flashMessages.success(`Successfully checked in the account ${account}.`);
70-
this.args.onCheckInSuccess();
71-
} catch (error) {
72-
this.selectedStatus = undefined;
73-
this.flashMessages.danger(`Error checking in the account ${account}. \n ${errorMessage(error)}`);
74-
}
75-
}
72+
checkIn = task(
73+
waitFor(async () => {
74+
const { library, account } = this.selectedStatus as LdapLibraryAccountStatus;
75+
try {
76+
const { completeLibraryName } = this.findLibrary(library);
77+
const payload = { service_account_names: [account] };
78+
await this.api.secrets.ldapLibraryForceCheckIn(
79+
completeLibraryName,
80+
this.secretMountPath.currentPath,
81+
payload
82+
);
83+
this.flashMessages.success(`Successfully checked in the account ${account}.`);
84+
this.args.onCheckInSuccess();
85+
} catch (error) {
86+
const { message } = await this.api.parseError(error);
87+
this.selectedStatus = undefined;
88+
this.flashMessages.danger(`Error checking in the account ${account}. \n ${message}`);
89+
}
90+
})
91+
);
7692
}

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

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -44,45 +44,47 @@
4444
<span data-test-library={{library.name}}>{{library.name}}</span>
4545
</Item.content>
4646
<Item.menu>
47-
{{#if (or library.canRead library.canEdit library.canDelete)}}
48-
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
49-
<dd.ToggleIcon
50-
@icon="more-horizontal"
51-
@text="More options"
52-
@hasChevron={{false}}
53-
data-test-popup-menu-trigger={{library.name}}
54-
/>
55-
{{#if (this.isHierarchical library.name)}}
56-
<dd.Interactive
57-
data-test-subdirectory
58-
@route="libraries.subdirectory"
59-
@model={{library.completeLibraryName}}
60-
>Content</dd.Interactive>
61-
{{else}}
62-
{{#if library.canEdit}}
47+
{{#let (hash name=library.completeLibraryName backend=this.secretMountPath.currentPath) as |capabilityParams|}}
48+
{{#if (has-capability @capabilities "read" "update" "delete" pathKey="ldapLibrary" params=capabilityParams)}}
49+
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
50+
<dd.ToggleIcon
51+
@icon="more-horizontal"
52+
@text="More options"
53+
@hasChevron={{false}}
54+
data-test-popup-menu-trigger={{library.name}}
55+
/>
56+
{{#if (this.isHierarchical library.name)}}
6357
<dd.Interactive
64-
data-test-edit
65-
@route="libraries.library.edit"
66-
@model={{this.getEncodedLibraryName library}}
67-
>Edit</dd.Interactive>
58+
data-test-subdirectory
59+
@route="libraries.subdirectory"
60+
@model={{library.completeLibraryName}}
61+
>Content</dd.Interactive>
62+
{{else}}
63+
{{#if (has-capability @capabilities "update" pathKey="ldapLibrary" params=capabilityParams)}}
64+
<dd.Interactive
65+
data-test-edit
66+
@route="libraries.library.edit"
67+
@model={{this.getEncodedLibraryName library}}
68+
>Edit</dd.Interactive>
69+
{{/if}}
70+
{{#if (has-capability @capabilities "read" pathKey="ldapLibrary" params=capabilityParams)}}
71+
<dd.Interactive
72+
data-test-details
73+
@route="libraries.library.details"
74+
@model={{this.getEncodedLibraryName library}}
75+
>Details</dd.Interactive>
76+
{{/if}}
77+
{{#if (has-capability @capabilities "delete" pathKey="ldapLibrary" params=capabilityParams)}}
78+
<dd.Interactive
79+
data-test-delete
80+
@color="critical"
81+
{{on "click" (fn (mut this.libraryToDelete) library)}}
82+
>Delete</dd.Interactive>
83+
{{/if}}
6884
{{/if}}
69-
{{#if library.canRead}}
70-
<dd.Interactive
71-
data-test-details
72-
@route="libraries.library.details"
73-
@model={{this.getEncodedLibraryName library}}
74-
>Details</dd.Interactive>
75-
{{/if}}
76-
{{#if library.canDelete}}
77-
<dd.Interactive
78-
data-test-delete
79-
@color="critical"
80-
{{on "click" (fn (mut this.libraryToDelete) library)}}
81-
>Delete</dd.Interactive>
82-
{{/if}}
83-
{{/if}}
84-
</Hds::Dropdown>
85-
{{/if}}
85+
</Hds::Dropdown>
86+
{{/if}}
87+
{{/let}}
8688
</Item.menu>
8789
</ListItem>
8890
{{/each}}

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import { tracked } from '@glimmer/tracking';
88
import { service } from '@ember/service';
99
import { action } from '@ember/object';
1010
import { getOwner } from '@ember/owner';
11-
import errorMessage from 'vault/utils/error-message';
1211

13-
import type LdapLibraryModel from 'vault/models/ldap/library';
12+
import type { LdapLibrary } from 'vault/secrets/ldap';
1413
import type FlashMessageService from 'vault/services/flash-messages';
15-
import type { Breadcrumb, EngineOwner } from 'vault/vault/app-types';
14+
import type { Breadcrumb, CapabilitiesMap, EngineOwner } from 'vault/vault/app-types';
1615
import type RouterService from '@ember/routing/router-service';
1716
import type SecretsEngineResource from 'vault/resources/secrets/engine';
17+
import type SecretMountPath from 'vault/services/secret-mount-path';
18+
import type ApiService from 'vault/services/api';
1819

1920
interface Args {
20-
libraries: Array<LdapLibraryModel>;
21+
libraries: Array<LdapLibrary>;
22+
capabilities: CapabilitiesMap;
2123
promptConfig: boolean;
2224
secretsEngine: SecretsEngineResource;
2325
breadcrumbs: Array<Breadcrumb>;
@@ -26,18 +28,20 @@ interface Args {
2628
export default class LdapLibrariesPageComponent extends Component<Args> {
2729
@service declare readonly flashMessages: FlashMessageService;
2830
@service('app-router') declare readonly router: RouterService;
31+
@service declare readonly secretMountPath: SecretMountPath;
32+
@service declare readonly api: ApiService;
2933

3034
@tracked filterValue = '';
31-
@tracked libraryToDelete: LdapLibraryModel | null = null;
35+
@tracked libraryToDelete: LdapLibrary | null = null;
3236

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

35-
linkParams = (library: LdapLibraryModel) => {
39+
linkParams = (library: LdapLibrary) => {
3640
const route = this.isHierarchical(library.name) ? 'libraries.subdirectory' : 'libraries.library.details';
3741
return [route, library.completeLibraryName];
3842
};
3943

40-
getEncodedLibraryName = (library: LdapLibraryModel) => {
44+
getEncodedLibraryName = (library: LdapLibrary) => {
4145
return library.completeLibraryName;
4246
};
4347

@@ -54,14 +58,15 @@ export default class LdapLibrariesPageComponent extends Component<Args> {
5458
}
5559

5660
@action
57-
async onDelete(model: LdapLibraryModel) {
61+
async onDelete(library: LdapLibrary) {
5862
try {
59-
const message = `Successfully deleted library ${model.completeLibraryName}.`;
60-
await model.destroyRecord();
63+
const { completeLibraryName } = library;
64+
await this.api.secrets.ldapLibraryDelete(completeLibraryName, this.secretMountPath.currentPath);
6165
this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries');
62-
this.flashMessages.success(message);
66+
this.flashMessages.success(`Successfully deleted library ${completeLibraryName}.`);
6367
} catch (error) {
64-
this.flashMessages.danger(`Error deleting library \n ${errorMessage(error)}`);
68+
const { message } = await this.api.parseError(error);
69+
this.flashMessages.danger(`Error deleting library \n ${message}`);
6570
}
6671
}
6772
}

ui/lib/ldap/addon/components/page/library/check-out.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</Hds::Alert>
1919

2020
<div class="has-top-margin-m">
21-
<InfoTableRow @label="Account name" @value={{@credentials.account}} />
21+
<InfoTableRow @label="Account name" @value={{@credentials.service_account_name}} />
2222
<InfoTableRow @label="Password">
2323
<MaskedInput @value={{@credentials.password}} @displayOnly={{true}} @allowCopy={{true}} />
2424
</InfoTableRow>

ui/lib/ldap/addon/components/page/library/create-and-edit.hbs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Copyright IBM Corp. 2016, 2025
33
SPDX-License-Identifier: BUSL-1.1
44
}}
5-
<Page::Header @title={{if @model.isNew "Create Library" "Edit Library"}}>
5+
<Page::Header @title={{if @form.isNew "Create Library" "Edit Library"}}>
66
<:breadcrumbs>
77
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
88
</:breadcrumbs>
@@ -13,15 +13,15 @@
1313
<form {{on "submit" (perform this.save)}} class="has-top-margin-m">
1414
<MessageError @errorMessage={{this.error}} />
1515

16-
{{#each @model.formFields as |field|}}
17-
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
16+
{{#each @form.formFields as |field|}}
17+
<FormField @attr={{field}} @model={{@form}} @modelValidations={{this.modelValidations}} />
1818
{{/each}}
1919

2020
<hr class="has-background-gray-200 has-top-margin-l" />
2121

2222
<div class="has-top-margin-l has-bottom-margin-l is-flex">
2323
<Hds::Button
24-
@text={{if @model.isNew "Create library" "Save"}}
24+
@text={{if @form.isNew "Create library" "Save"}}
2525
data-test-submit
2626
type="submit"
2727
disabled={{this.save.isRunning}}

0 commit comments

Comments
 (0)