mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 18:48:08 +00:00 
			
		
		
		
	LDAP/AD Secrets Engine (#20790)
* adds ldap ember engine (#20786) * adds ldap as mountable and supported secrets engine (#20793) * removes active directory as mountable secrets engine (#20798) * LDAP Config Ember Data Setup (#20863) * adds secret-engine-path adapter * adds model, adapater and serializer for ldap config * adds test for ldap config adapter * addresses PR feedback * updates remaining instances of getURL in secrets-engine-path adapter * adds underscore to getURL method in kubernetes/config adapter * adds check config vars test for kubernetes/config adapter * adds comment regarding primaryKey in secrets-engine-path adapter * adds tab-page-header component for ldap secrets engine (#20941) * LDAP Config Route (#21059) * converts secret-mount-path service to ts and moves kubernetes fetch-config decorator to core addon and converts to ts * adds ldap config route * fixes withConfig import path in kubernetes roles route * updates types in ldap config route * adds unit tests for fetch-secret-config decorator * updates comments in fetch-secret-config decorator * renames fetch-secret-config decorator * LDAP Configure Page Component (#21384) * adds ldap page configure component * removes pauseTest and updates radio card selector in ldap config test * LDAP Configuration (#21430) * adds ldap configuration route * adds secrets-engine-mount-config component to core addon * adds ldap config-cta component * adds display fields to ldap configuration page and test * fixes ldap config-cta test * adds yield to secrets-engine-mount-config component * fixes tests * LDAP Overview Route and Page Component (#21579) * adds ldap overview route and page component * changes toolbar link action type for create role on overview page * LDAP Role Model, Adapter and Serializer (#21655) * adds model, adapter and serializer for ldap roles * addresses review feedback * changes ldap role type from tracked prop to attr and sets in adapter for query methods * adds assertions to verify that frontend only props are returned from query methods in ldap role adapter * LDAP Library Model, Adapter and Serializer (#21728) * adds model, adapter and serializer for ldap library * updates capitalization and punction for ldap role and library form fields * LDAP Roles Create and Edit (#21818) * moves stringify and jsonify helpers to core addon * adds validation error for ttl picker in form field component * adds ldap roles create and edit routes and page component * adds ldap mirage handler and factory for roles * adds example workflow to json editor component * adds tests for ldap page create and edit component * addresses feedback * LDAP Role Details (#22036) * adds ldap role route to pass down model to child routes * adds ldap role details route and page component * updates ldap role model capabilities checks * adds periods to error messages * removes modelFor from ldap roles edit and details routes * adds flash message on ldap role delete success * LDAP Roles (#22070) * adds ldap roles route and page component * update ldap role adapter tests and adds adapter options to query for partialErrorInfo * updates ldap role adapter based on PR feedback * adds filter-input component to core addon * updates ldap roles page to use filter-input component * updates ldap role adapter tests * LDAP Role Credentials (#22142) * adds ldap roles route and page component * update ldap role adapter tests and adds adapter options to query for partialErrorInfo * adds credentials actions to ldap roles list menu and fixes rotate action in details view * adds ldap role credentials route and page component * adds tests for ldap role credentials * LDAP Library Create and Edit (#22171) * adds ldap library create/edit routes and page component * adds ldap library create-and-edit tests and library mirage factory * updates form-field component to display validation errors and warnings for all fields * updates ldap library edit route class name * updates ldap library model interface name * adds missing period in flash message * LDAP Libraries (#22184) * updates interface and class names in ldap roles route * adds ldap libraries route and page component * fixes lint error * LDAP Library Details (#22200) * updates interface and class names in ldap roles route * adds ldap libraries route and page component * fixes lint error * adds ldap library details route and page component * LDAP Library Details Configuration (#22201) * updates interface and class names in ldap roles route * adds ldap libraries route and page component * fixes lint error * adds ldap library details route and page component * adds ldap library details configuration route and page component * updates ldap library check-in enforcement value mapping * fixes issue in code mirror modifier after merging upgrade * fixes failing database secrets test * LDAP Library Account Details (#22287) * adds route and page component for ldap library accounts * adds ldap component for checked out accounts * updates ldap library adapter tests * LDAP Library Check-out (#22289) * adds route and page component for ldap library accounts * adds ldap component for checked out accounts * adds route and page component for ldap library checkout * addresses PR feedback * LDAP Overview Cards (#22325) * adds overview cards to ldap overview route * adds create library toolbar action to ldap overview route * adds acceptance tests for ldap workflows (#22375) * Fetch Secrets Engine Config Decorator Docs (#22416) * removes uneccesary asyncs from ldap route model hooks * updates ldap overview route class name * adds documentation for fetch-secrets-engine-config decorator * add changelog * adding back external links, missed due to merge. * changelog * fix test after merging in dashboard work * Update 20790.txt --------- Co-authored-by: Angel Garbarino <angel@hashicorp.com> Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/20790.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/20790.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:feature | ||||||
|  | **UI LDAP secrets engine**: Add LDAP secrets engine to the UI. | ||||||
|  | ``` | ||||||
| @@ -3,41 +3,12 @@ | |||||||
|  * SPDX-License-Identifier: BUSL-1.1 |  * SPDX-License-Identifier: BUSL-1.1 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| import ApplicationAdapter from 'vault/adapters/application'; | import SecretsEnginePathAdapter from 'vault/adapters/secrets-engine-path'; | ||||||
| import { encodePath } from 'vault/utils/path-encoding-helpers'; |  | ||||||
|  |  | ||||||
| export default class KubernetesConfigAdapter extends ApplicationAdapter { | export default class KubernetesConfigAdapter extends SecretsEnginePathAdapter { | ||||||
|   namespace = 'v1'; |   path = 'config'; | ||||||
|  |  | ||||||
|   getURL(backend, path = 'config') { |  | ||||||
|     return `${this.buildURL()}/${encodePath(backend)}/${path}`; |  | ||||||
|   } |  | ||||||
|   urlForUpdateRecord(name, modelName, snapshot) { |  | ||||||
|     return this.getURL(snapshot.attr('backend')); |  | ||||||
|   } |  | ||||||
|   urlForDeleteRecord(backend) { |  | ||||||
|     return this.getURL(backend); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   queryRecord(store, type, query) { |  | ||||||
|     const { backend } = query; |  | ||||||
|     return this.ajax(this.getURL(backend), 'GET').then((resp) => { |  | ||||||
|       resp.backend = backend; |  | ||||||
|       return resp; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   createRecord() { |  | ||||||
|     return this._saveRecord(...arguments); |  | ||||||
|   } |  | ||||||
|   updateRecord() { |  | ||||||
|     return this._saveRecord(...arguments); |  | ||||||
|   } |  | ||||||
|   _saveRecord(store, { modelName }, snapshot) { |  | ||||||
|     const data = store.serializerFor(modelName).serialize(snapshot); |  | ||||||
|     const url = this.getURL(snapshot.attr('backend')); |  | ||||||
|     return this.ajax(url, 'POST', { data }).then(() => data); |  | ||||||
|   } |  | ||||||
|   checkConfigVars(backend) { |   checkConfigVars(backend) { | ||||||
|     return this.ajax(`${this.getURL(backend, 'check')}`, 'GET'); |     return this.ajax(`${this._getURL(backend, 'check')}`, 'GET'); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								ui/app/adapters/ldap/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								ui/app/adapters/ldap/config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import SecretsEnginePathAdapter from 'vault/adapters/secrets-engine-path'; | ||||||
|  |  | ||||||
|  | export default class LdapConfigAdapter extends SecretsEnginePathAdapter { | ||||||
|  |   path = 'config'; | ||||||
|  |  | ||||||
|  |   async rotateRoot(backend) { | ||||||
|  |     return this.ajax(this._getURL(backend, 'rotate-root'), 'POST'); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								ui/app/adapters/ldap/library.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								ui/app/adapters/ldap/library.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import NamedPathAdapter from 'vault/adapters/named-path'; | ||||||
|  | import { encodePath } from 'vault/utils/path-encoding-helpers'; | ||||||
|  |  | ||||||
|  | export default class LdapLibraryAdapter extends NamedPathAdapter { | ||||||
|  |   getURL(backend, name) { | ||||||
|  |     const base = `${this.buildURL()}/${encodePath(backend)}/library`; | ||||||
|  |     return name ? `${base}/${name}` : base; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   urlForUpdateRecord(name, modelName, snapshot) { | ||||||
|  |     return this.getURL(snapshot.attr('backend'), name); | ||||||
|  |   } | ||||||
|  |   urlForDeleteRecord(name, modelName, snapshot) { | ||||||
|  |     return this.getURL(snapshot.attr('backend'), name); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   query(store, type, query) { | ||||||
|  |     const { backend } = query; | ||||||
|  |     return this.ajax(this.getURL(backend), 'GET', { data: { list: true } }) | ||||||
|  |       .then((resp) => { | ||||||
|  |         return resp.data.keys.map((name) => ({ name, backend })); | ||||||
|  |       }) | ||||||
|  |       .catch((error) => { | ||||||
|  |         if (error.httpStatus === 404) { | ||||||
|  |           return []; | ||||||
|  |         } | ||||||
|  |         throw error; | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |   queryRecord(store, type, query) { | ||||||
|  |     const { backend, name } = query; | ||||||
|  |     return this.ajax(this.getURL(backend, name), 'GET').then((resp) => ({ ...resp.data, backend, name })); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fetchStatus(backend, name) { | ||||||
|  |     const url = `${this.getURL(backend, name)}/status`; | ||||||
|  |     return this.ajax(url, 'GET').then((resp) => { | ||||||
|  |       const statuses = []; | ||||||
|  |       for (const key in resp.data) { | ||||||
|  |         const status = { | ||||||
|  |           ...resp.data[key], | ||||||
|  |           account: key, | ||||||
|  |           library: name, | ||||||
|  |         }; | ||||||
|  |         statuses.push(status); | ||||||
|  |       } | ||||||
|  |       return statuses; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   checkOutAccount(backend, name, ttl) { | ||||||
|  |     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`; | ||||||
|  |     return this.ajax(url, 'POST', { data: { service_account_names } }).then((resp) => resp.data); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										92
									
								
								ui/app/adapters/ldap/role.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								ui/app/adapters/ldap/role.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import NamedPathAdapter from 'vault/adapters/named-path'; | ||||||
|  | import { encodePath } from 'vault/utils/path-encoding-helpers'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | export default class LdapRoleAdapter extends NamedPathAdapter { | ||||||
|  |   @service flashMessages; | ||||||
|  |  | ||||||
|  |   getURL(backend, path, name) { | ||||||
|  |     const base = `${this.buildURL()}/${encodePath(backend)}/${path}`; | ||||||
|  |     return name ? `${base}/${name}` : base; | ||||||
|  |   } | ||||||
|  |   pathForRoleType(type, isCred) { | ||||||
|  |     const staticPath = isCred ? 'static-cred' : 'static-role'; | ||||||
|  |     const dynamicPath = isCred ? 'creds' : 'role'; | ||||||
|  |     return type === 'static' ? staticPath : dynamicPath; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   urlForUpdateRecord(name, modelName, snapshot) { | ||||||
|  |     const { backend, type } = snapshot.record; | ||||||
|  |     return this.getURL(backend, this.pathForRoleType(type), name); | ||||||
|  |   } | ||||||
|  |   urlForDeleteRecord(name, modelName, snapshot) { | ||||||
|  |     const { backend, type } = snapshot.record; | ||||||
|  |     return this.getURL(backend, this.pathForRoleType(type), name); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async query(store, type, query, recordArray, options) { | ||||||
|  |     const { showPartialError } = options.adapterOptions || {}; | ||||||
|  |     const { backend } = query; | ||||||
|  |     const roles = []; | ||||||
|  |     const errors = []; | ||||||
|  |  | ||||||
|  |     for (const roleType of ['static', 'dynamic']) { | ||||||
|  |       const url = this.getURL(backend, this.pathForRoleType(roleType)); | ||||||
|  |       try { | ||||||
|  |         const models = await this.ajax(url, 'GET', { data: { list: true } }).then((resp) => { | ||||||
|  |           return resp.data.keys.map((name) => ({ name, backend, type: roleType })); | ||||||
|  |         }); | ||||||
|  |         roles.addObjects(models); | ||||||
|  |       } catch (error) { | ||||||
|  |         if (error.httpStatus !== 404) { | ||||||
|  |           errors.push(error); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (errors.length) { | ||||||
|  |       const errorMessages = errors.reduce((errors, e) => { | ||||||
|  |         e.errors.forEach((error) => { | ||||||
|  |           errors.push(`${e.path}: ${error}`); | ||||||
|  |         }); | ||||||
|  |         return errors; | ||||||
|  |       }, []); | ||||||
|  |       if (errors.length === 2) { | ||||||
|  |         // throw error as normal if both requests fail | ||||||
|  |         // ignore status code and concat errors to be displayed in Page::Error component with generic message | ||||||
|  |         throw { message: 'Error fetching roles:', errors: errorMessages }; | ||||||
|  |       } else if (showPartialError) { | ||||||
|  |         // if only one request fails, surface the error to the user an info level flash message | ||||||
|  |         // this may help for permissions errors where a users policy may be incorrect | ||||||
|  |         this.flashMessages.info(`Error fetching roles from ${errorMessages.join(', ')}`); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return roles.sortBy('name'); | ||||||
|  |   } | ||||||
|  |   queryRecord(store, type, query) { | ||||||
|  |     const { backend, name, type: roleType } = query; | ||||||
|  |     const url = this.getURL(backend, this.pathForRoleType(roleType), name); | ||||||
|  |     return this.ajax(url, 'GET').then((resp) => ({ ...resp.data, backend, name, type: roleType })); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fetchCredentials(backend, type, name) { | ||||||
|  |     const url = this.getURL(backend, this.pathForRoleType(type, true), name); | ||||||
|  |     return this.ajax(url, 'GET').then((resp) => { | ||||||
|  |       if (type === 'dynamic') { | ||||||
|  |         const { lease_id, lease_duration, renewable } = resp; | ||||||
|  |         return { ...resp.data, lease_id, lease_duration, renewable, type }; | ||||||
|  |       } | ||||||
|  |       return { ...resp.data, type }; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   rotateStaticPassword(backend, name) { | ||||||
|  |     const url = this.getURL(backend, 'rotate-role', name); | ||||||
|  |     return this.ajax(url, 'POST'); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								ui/app/adapters/secrets-engine-path.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								ui/app/adapters/secrets-engine-path.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * General use adapter to access specified paths on secrets engines | ||||||
|  |  * For example /:backend/config is a typical use case for this adapter | ||||||
|  |  * These types of records do not have an id and use the backend value of the secrets engine as the primaryKey in the serializer | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import ApplicationAdapter from 'vault/adapters/application'; | ||||||
|  | import { encodePath } from 'vault/utils/path-encoding-helpers'; | ||||||
|  |  | ||||||
|  | export default class SecretsEnginePathAdapter extends ApplicationAdapter { | ||||||
|  |   namespace = 'v1'; | ||||||
|  |  | ||||||
|  |   // define path value in extending class or pass into method directly | ||||||
|  |   _getURL(backend, path) { | ||||||
|  |     return `${this.buildURL()}/${encodePath(backend)}/${path || this.path}`; | ||||||
|  |   } | ||||||
|  |   urlForUpdateRecord(name, modelName, snapshot) { | ||||||
|  |     return this._getURL(snapshot.attr('backend')); | ||||||
|  |   } | ||||||
|  |   // primaryKey must be set to backend in serializer | ||||||
|  |   urlForDeleteRecord(backend) { | ||||||
|  |     return this._getURL(backend); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   queryRecord(store, type, query) { | ||||||
|  |     const { backend } = query; | ||||||
|  |     return this.ajax(this._getURL(backend), 'GET').then((resp) => { | ||||||
|  |       resp.backend = backend; | ||||||
|  |       return resp; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   createRecord() { | ||||||
|  |     return this._saveRecord(...arguments); | ||||||
|  |   } | ||||||
|  |   updateRecord() { | ||||||
|  |     return this._saveRecord(...arguments); | ||||||
|  |   } | ||||||
|  |   _saveRecord(store, { modelName }, snapshot) { | ||||||
|  |     const data = store.serializerFor(modelName).serialize(snapshot); | ||||||
|  |     const url = this._getURL(snapshot.attr('backend')); | ||||||
|  |     return this.ajax(url, 'POST', { data }).then(() => data); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -52,6 +52,14 @@ export default class App extends Application { | |||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|  |     ldap: { | ||||||
|  |       dependencies: { | ||||||
|  |         services: ['router', 'store', 'secret-mount-path', 'flash-messages', 'auth'], | ||||||
|  |         externalRoutes: { | ||||||
|  |           secrets: 'vault.cluster.secrets.backends', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|     kv: { |     kv: { | ||||||
|       dependencies: { |       dependencies: { | ||||||
|         services: ['download', 'namespace', 'router', 'store', 'secret-mount-path', 'flash-messages'], |         services: ['download', 'namespace', 'router', 'store', 'secret-mount-path', 'flash-messages'], | ||||||
|   | |||||||
| @@ -30,11 +30,6 @@ const ENTERPRISE_SECRET_ENGINES = [ | |||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const MOUNTABLE_SECRET_ENGINES = [ | const MOUNTABLE_SECRET_ENGINES = [ | ||||||
|   { |  | ||||||
|     displayName: 'Active Directory', |  | ||||||
|     type: 'ad', |  | ||||||
|     category: 'cloud', |  | ||||||
|   }, |  | ||||||
|   { |   { | ||||||
|     displayName: 'AliCloud', |     displayName: 'AliCloud', | ||||||
|     type: 'alicloud', |     type: 'alicloud', | ||||||
| @@ -110,9 +105,15 @@ const MOUNTABLE_SECRET_ENGINES = [ | |||||||
|     type: 'totp', |     type: 'totp', | ||||||
|     category: 'generic', |     category: 'generic', | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     displayName: 'LDAP', | ||||||
|  |     type: 'ldap', | ||||||
|  |     engineRoute: 'ldap.overview', | ||||||
|  |     category: 'generic', | ||||||
|  |     glyph: 'folder-users', | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     displayName: 'Kubernetes', |     displayName: 'Kubernetes', | ||||||
|     value: 'kubernetes', |  | ||||||
|     type: 'kubernetes', |     type: 'kubernetes', | ||||||
|     engineRoute: 'kubernetes.overview', |     engineRoute: 'kubernetes.overview', | ||||||
|     category: 'generic', |     category: 'generic', | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ const SUPPORTED_SECRET_BACKENDS = [ | |||||||
|   'transform', |   'transform', | ||||||
|   'keymgmt', |   'keymgmt', | ||||||
|   'kubernetes', |   'kubernetes', | ||||||
|  |   'ldap', | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| export function supportedSecretBackends() { | export function supportedSecretBackends() { | ||||||
|   | |||||||
							
								
								
									
										129
									
								
								ui/app/models/ldap/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								ui/app/models/ldap/config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Model, { attr } from '@ember-data/model'; | ||||||
|  | import { withFormFields } from 'vault/decorators/model-form-fields'; | ||||||
|  | import { withModelValidations } from 'vault/decorators/model-validations'; | ||||||
|  |  | ||||||
|  | const validations = { | ||||||
|  |   binddn: [{ type: 'presence', message: 'Administrator distinguished name is required.' }], | ||||||
|  |   bindpass: [{ type: 'presence', message: 'Administrator password is required.' }], | ||||||
|  | }; | ||||||
|  | const formGroups = [ | ||||||
|  |   { default: ['binddn', 'bindpass', 'url', 'password_policy'] }, | ||||||
|  |   { 'TLS options': ['starttls', 'insecure_tls', 'certificate', 'client_tls_cert', 'client_tls_key'] }, | ||||||
|  |   { 'More options': ['userdn', 'userattr', 'upndomain', 'connection_timeout', 'request_timeout'] }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | @withModelValidations(validations) | ||||||
|  | @withFormFields(null, formGroups) | ||||||
|  | export default class LdapConfigModel extends Model { | ||||||
|  |   @attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'Administrator Distinguished Name', | ||||||
|  |     subText: | ||||||
|  |       'Distinguished name of the administrator to bind (Bind DN) when performing user and group search. Example: cn=vault,ou=Users,dc=example,dc=com.', | ||||||
|  |   }) | ||||||
|  |   binddn; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'Administrator Password', | ||||||
|  |     subText: 'Password to use along with Bind DN when performing user search.', | ||||||
|  |   }) | ||||||
|  |   bindpass; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'URL', | ||||||
|  |     subText: 'The directory server to connect to.', | ||||||
|  |   }) | ||||||
|  |   url; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'optionalText', | ||||||
|  |     label: 'Use custom password policy', | ||||||
|  |     subText: 'Specify the name of an existing password policy.', | ||||||
|  |     defaultSubText: 'Unless a custom policy is specified, Vault will use a default.', | ||||||
|  |     defaultShown: 'Default', | ||||||
|  |     docLink: '/vault/docs/concepts/password-policies', | ||||||
|  |   }) | ||||||
|  |   password_policy; | ||||||
|  |  | ||||||
|  |   @attr('string') schema; | ||||||
|  |  | ||||||
|  |   @attr('boolean', { | ||||||
|  |     label: 'Start TLS', | ||||||
|  |     subText: 'If checked, or address contains “ldaps://”, creates an encrypted connection with LDAP.', | ||||||
|  |   }) | ||||||
|  |   starttls; | ||||||
|  |  | ||||||
|  |   @attr('boolean', { | ||||||
|  |     label: 'Insecure TLS', | ||||||
|  |     subText: 'If checked, skips LDAP server SSL certificate verification - insecure, use with caution!', | ||||||
|  |   }) | ||||||
|  |   insecure_tls; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'file', | ||||||
|  |     label: 'CA Certificate', | ||||||
|  |     helpText: 'CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded.', | ||||||
|  |   }) | ||||||
|  |   certificate; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'file', | ||||||
|  |     label: 'Client TLS Certificate', | ||||||
|  |     helpText: 'Client certificate to provide to the LDAP server, must be x509 PEM encoded.', | ||||||
|  |   }) | ||||||
|  |   client_tls_cert; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'file', | ||||||
|  |     label: 'Client TLS Key', | ||||||
|  |     helpText: 'Client key to provide to the LDAP server, must be x509 PEM encoded.', | ||||||
|  |   }) | ||||||
|  |   client_tls_key; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'Userdn', | ||||||
|  |     helpText: 'The base DN under which to perform user search in library management and static roles.', | ||||||
|  |   }) | ||||||
|  |   userdn; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'Userattr', | ||||||
|  |     subText: 'The attribute field name used to perform user search in library management and static roles.', | ||||||
|  |   }) | ||||||
|  |   userattr; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'Upndomain', | ||||||
|  |     subText: 'The domain (userPrincipalDomain) used to construct a UPN string for authentication.', | ||||||
|  |   }) | ||||||
|  |   upndomain; | ||||||
|  |  | ||||||
|  |   @attr('number', { | ||||||
|  |     editType: 'optionalText', | ||||||
|  |     label: 'Connection Timeout', | ||||||
|  |     subText: 'Specify the connection timeout length in seconds.', | ||||||
|  |     defaultSubText: 'Vault will use the default of 30 seconds.', | ||||||
|  |     defaultShown: 'Default 30 seconds.', | ||||||
|  |   }) | ||||||
|  |   connection_timeout; | ||||||
|  |  | ||||||
|  |   @attr('number', { | ||||||
|  |     editType: 'optionalText', | ||||||
|  |     label: 'Request Timeout', | ||||||
|  |     subText: 'Specify the connection timeout length in seconds.', | ||||||
|  |     defaultSubText: 'Vault will use the default of 90 seconds.', | ||||||
|  |     defaultShown: 'Default 90 seconds.', | ||||||
|  |   }) | ||||||
|  |   request_timeout; | ||||||
|  |  | ||||||
|  |   async rotateRoot() { | ||||||
|  |     const adapter = this.store.adapterFor('ldap/config'); | ||||||
|  |     return adapter.rotateRoot(this.backend); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										106
									
								
								ui/app/models/ldap/library.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								ui/app/models/ldap/library.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Model, { attr } from '@ember-data/model'; | ||||||
|  | import { withFormFields } from 'vault/decorators/model-form-fields'; | ||||||
|  | import { withModelValidations } from 'vault/decorators/model-validations'; | ||||||
|  | import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; | ||||||
|  |  | ||||||
|  | const validations = { | ||||||
|  |   name: [{ type: 'presence', message: 'Library name is required.' }], | ||||||
|  |   service_account_names: [{ type: 'presence', message: 'At least one service account is required.' }], | ||||||
|  | }; | ||||||
|  | const formFields = ['name', 'service_account_names', 'ttl', 'max_ttl', 'disable_check_in_enforcement']; | ||||||
|  |  | ||||||
|  | @withModelValidations(validations) | ||||||
|  | @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', { label: 'Library name' }) name; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'stringArray', | ||||||
|  |     label: 'Accounts', | ||||||
|  |     subText: | ||||||
|  |       '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.', | ||||||
|  |   }) | ||||||
|  |   service_account_names; | ||||||
|  |  | ||||||
|  |   @attr({ | ||||||
|  |     editType: 'ttl', | ||||||
|  |     label: 'Default lease TTL', | ||||||
|  |     detailsLabel: 'TTL', | ||||||
|  |     helperTextDisabled: 'Vault will use the default lease duration.', | ||||||
|  |     defaultValue: '24h', | ||||||
|  |     defaultShown: 'Engine default', | ||||||
|  |   }) | ||||||
|  |   ttl; | ||||||
|  |  | ||||||
|  |   @attr({ | ||||||
|  |     editType: 'ttl', | ||||||
|  |     label: 'Max lease TTL', | ||||||
|  |     detailsLabel: 'Max TTL', | ||||||
|  |     helperTextDisabled: 'Vault will use the default lease duration.', | ||||||
|  |     defaultValue: '24h', | ||||||
|  |     defaultShown: 'Engine default', | ||||||
|  |   }) | ||||||
|  |   max_ttl; | ||||||
|  |  | ||||||
|  |   // this is a boolean from the server but is transformed in the serializer to display as Disabled or Enabled | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'radio', | ||||||
|  |     label: 'Check-in enforcement', | ||||||
|  |     subText: | ||||||
|  |       '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.', | ||||||
|  |     possibleValues: ['Disabled', 'Enabled'], | ||||||
|  |     defaultValue: 'Enabled', | ||||||
|  |   }) | ||||||
|  |   disable_check_in_enforcement; | ||||||
|  |  | ||||||
|  |   get displayFields() { | ||||||
|  |     return this.formFields.filter((field) => field.name !== 'service_account_names'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @lazyCapabilities(apiPath`${'backend'}/library/${'name'}`, 'backend', 'name') libraryPath; | ||||||
|  |   @lazyCapabilities(apiPath`${'backend'}/library/${'name'}/status`, 'backend', 'name') statusPath; | ||||||
|  |   @lazyCapabilities(apiPath`${'backend'}/library/${'name'}/check-out`, 'backend', 'name') checkOutPath; | ||||||
|  |   @lazyCapabilities(apiPath`${'backend'}/library/${'name'}/check-in`, 'backend', 'name') checkInPath; | ||||||
|  |  | ||||||
|  |   get canCreate() { | ||||||
|  |     return this.libraryPath.get('canCreate') !== false; | ||||||
|  |   } | ||||||
|  |   get canDelete() { | ||||||
|  |     return this.libraryPath.get('canDelete') !== false; | ||||||
|  |   } | ||||||
|  |   get canEdit() { | ||||||
|  |     return this.libraryPath.get('canUpdate') !== false; | ||||||
|  |   } | ||||||
|  |   get canRead() { | ||||||
|  |     return this.libraryPath.get('canRead') !== false; | ||||||
|  |   } | ||||||
|  |   get canList() { | ||||||
|  |     return this.libraryPath.get('canList') !== false; | ||||||
|  |   } | ||||||
|  |   get canReadStatus() { | ||||||
|  |     return this.statusPath.get('canRead') !== false; | ||||||
|  |   } | ||||||
|  |   get canCheckOut() { | ||||||
|  |     return this.checkOutPath.get('canUpdate') !== false; | ||||||
|  |   } | ||||||
|  |   get canCheckIn() { | ||||||
|  |     return this.checkInPath.get('canUpdate') !== false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fetchStatus() { | ||||||
|  |     return this.store.adapterFor('ldap/library').fetchStatus(this.backend, this.name); | ||||||
|  |   } | ||||||
|  |   checkOutAccount(ttl) { | ||||||
|  |     return this.store.adapterFor('ldap/library').checkOutAccount(this.backend, this.name, ttl); | ||||||
|  |   } | ||||||
|  |   checkInAccount(account) { | ||||||
|  |     return this.store.adapterFor('ldap/library').checkInAccount(this.backend, this.name, [account]); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										230
									
								
								ui/app/models/ldap/role.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								ui/app/models/ldap/role.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,230 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Model, { attr } from '@ember-data/model'; | ||||||
|  | import { withFormFields } from 'vault/decorators/model-form-fields'; | ||||||
|  | import { withModelValidations } from 'vault/decorators/model-validations'; | ||||||
|  | import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; | ||||||
|  |  | ||||||
|  | const creationLdifExample = `# The example below is treated as a comment and will not be submitted | ||||||
|  | # dn: cn={{.Username}},ou=users,dc=learn,dc=example | ||||||
|  | # objectClass: person | ||||||
|  | # objectClass: top | ||||||
|  | `; | ||||||
|  | const deletionLdifExample = `# The example below is treated as a comment and will not be submitted | ||||||
|  | # dn: cn={{.Username}},ou=users,dc=learn,dc=example | ||||||
|  | # changetype: delete | ||||||
|  | `; | ||||||
|  | const rollbackLdifExample = `# The example below is treated as a comment and will not be submitted | ||||||
|  | # dn: cn={{.Username}},ou=users,dc=learn,dc=example | ||||||
|  | # changetype: delete | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | const validations = { | ||||||
|  |   name: [{ type: 'presence', message: 'Name is required' }], | ||||||
|  |   username: [ | ||||||
|  |     { | ||||||
|  |       validator: (model) => (model.isStatic && !model.username ? false : true), | ||||||
|  |       message: 'Username is required.', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  |   rotation_period: [ | ||||||
|  |     { | ||||||
|  |       validator: (model) => (model.isStatic && !model.rotation_period ? false : true), | ||||||
|  |       message: 'Rotation Period is required.', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  |   creation_ldif: [ | ||||||
|  |     { | ||||||
|  |       validator: (model) => (model.isDynamic && !model.creation_ldif ? false : true), | ||||||
|  |       message: 'Creation LDIF is required.', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  |   deletion_ldif: [ | ||||||
|  |     { | ||||||
|  |       validator: (model) => (model.isDynamic && !model.creation_ldif ? false : true), | ||||||
|  |       message: 'Deletion LDIF is required.', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const staticRoleFields = ['username', 'dn', 'rotation_period']; | ||||||
|  | export const dynamicRoleFields = [ | ||||||
|  |   'default_ttl', | ||||||
|  |   'max_ttl', | ||||||
|  |   'username_template', | ||||||
|  |   'creation_ldif', | ||||||
|  |   'deletion_ldif', | ||||||
|  |   'rollback_ldif', | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | @withModelValidations(validations) | ||||||
|  | @withFormFields() | ||||||
|  | export default class LdapRoleModel extends Model { | ||||||
|  |   @attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     defaultValue: 'static', | ||||||
|  |   }) | ||||||
|  |   type; // this must be set to either static or dynamic in order for the adapter to build the correct url and for the correct form fields to display | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'Role name', | ||||||
|  |     subText: 'The name of the role that will be used in Vault.', | ||||||
|  |   }) | ||||||
|  |   name; | ||||||
|  |  | ||||||
|  |   // static role properties | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'Distinguished name', | ||||||
|  |     subText: 'Distinguished name (DN) of entry Vault should manage.', | ||||||
|  |   }) | ||||||
|  |   dn; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     label: 'Username', | ||||||
|  |     subText: | ||||||
|  |       "The name of the user to be used when logging in. This is useful when DN isn't used for login purposes.", | ||||||
|  |   }) | ||||||
|  |   username; | ||||||
|  |  | ||||||
|  |   @attr({ | ||||||
|  |     editType: 'ttl', | ||||||
|  |     label: 'Rotation period', | ||||||
|  |     helperTextEnabled: | ||||||
|  |       'Specifies the amount of time Vault should wait before rotating the password. The minimum is 5 seconds.', | ||||||
|  |     hideToggle: true, | ||||||
|  |   }) | ||||||
|  |   rotation_period; | ||||||
|  |  | ||||||
|  |   // dynamic role properties | ||||||
|  |   @attr({ | ||||||
|  |     editType: 'ttl', | ||||||
|  |     label: 'Generated credential’s time-to-live (TTL)', | ||||||
|  |     detailsLabel: 'TTL', | ||||||
|  |     helperTextDisabled: 'Vault will use the default of 1 hour.', | ||||||
|  |     defaultValue: '1h', | ||||||
|  |     defaultShown: 'Engine default', | ||||||
|  |   }) | ||||||
|  |   default_ttl; | ||||||
|  |  | ||||||
|  |   @attr({ | ||||||
|  |     editType: 'ttl', | ||||||
|  |     label: 'Generated credential’s maximum time-to-live (Max TTL)', | ||||||
|  |     detailsLabel: 'Max TTL', | ||||||
|  |     helperTextDisabled: 'Vault will use the engine default of 24 hours.', | ||||||
|  |     defaultValue: '24h', | ||||||
|  |     defaultShown: 'Engine default', | ||||||
|  |   }) | ||||||
|  |   max_ttl; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'optionalText', | ||||||
|  |     label: 'Username template', | ||||||
|  |     subText: 'Enter the custom username template to use.', | ||||||
|  |     defaultSubText: | ||||||
|  |       'Template describing how dynamic usernames are generated. Vault will use the default for this plugin.', | ||||||
|  |     docLink: '/vault/docs/concepts/username-templating', | ||||||
|  |     defaultShown: 'Default', | ||||||
|  |   }) | ||||||
|  |   username_template; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'json', | ||||||
|  |     label: 'Creation LDIF', | ||||||
|  |     helpText: 'Specifies the LDIF statements executed to create a user. May optionally be base64 encoded.', | ||||||
|  |     example: creationLdifExample, | ||||||
|  |     mode: 'ruby', | ||||||
|  |     sectionHeading: 'LDIF Statements', // render section heading before form field | ||||||
|  |   }) | ||||||
|  |   creation_ldif; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'json', | ||||||
|  |     label: 'Deletion LDIF', | ||||||
|  |     helpText: | ||||||
|  |       'Specifies the LDIF statements executed to delete a user once its TTL has expired. May optionally be base64 encoded.', | ||||||
|  |     example: deletionLdifExample, | ||||||
|  |     mode: 'ruby', | ||||||
|  |   }) | ||||||
|  |   deletion_ldif; | ||||||
|  |  | ||||||
|  |   @attr('string', { | ||||||
|  |     editType: 'json', | ||||||
|  |     label: 'Rollback LDIF', | ||||||
|  |     helpText: | ||||||
|  |       'Specifies the LDIF statement to attempt to rollback any changes if the creation results in an error. May optionally be base64 encoded.', | ||||||
|  |     example: rollbackLdifExample, | ||||||
|  |     mode: 'ruby', | ||||||
|  |   }) | ||||||
|  |   rollback_ldif; | ||||||
|  |  | ||||||
|  |   get isStatic() { | ||||||
|  |     return this.type === 'static'; | ||||||
|  |   } | ||||||
|  |   get isDynamic() { | ||||||
|  |     return this.type === 'dynamic'; | ||||||
|  |   } | ||||||
|  |   // this is used to build the form fields as well as serialize the correct payload based on type | ||||||
|  |   // if a new attr is added be sure to add it to the appropriate array | ||||||
|  |   get fieldsForType() { | ||||||
|  |     return this.isStatic | ||||||
|  |       ? ['username', 'dn', 'rotation_period'] | ||||||
|  |       : ['default_ttl', 'max_ttl', 'username_template', 'creation_ldif', 'deletion_ldif', 'rollback_ldif']; | ||||||
|  |   } | ||||||
|  |   get formFields() { | ||||||
|  |     // filter all fields and return only those relevant to type | ||||||
|  |     return this.allFields.filter((field) => { | ||||||
|  |       // name is the only common field | ||||||
|  |       return field.name === 'name' || this.fieldsForType.includes(field.name); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get displayFields() { | ||||||
|  |     // insert type after role name | ||||||
|  |     const [name, ...rest] = this.formFields; | ||||||
|  |     const typeField = { name: 'type', options: { label: 'Role type' } }; | ||||||
|  |     return [name, typeField, ...rest]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get roleUri() { | ||||||
|  |     return this.isStatic ? 'static-role' : 'role'; | ||||||
|  |   } | ||||||
|  |   get credsUri() { | ||||||
|  |     return this.isStatic ? 'static-cred' : 'creds'; | ||||||
|  |   } | ||||||
|  |   @lazyCapabilities(apiPath`${'backend'}/${'roleUri'}/${'name'}`, 'backend', 'roleUri', 'name') rolePath; | ||||||
|  |   @lazyCapabilities(apiPath`${'backend'}/${'credsUri'}/${'name'}`, 'backend', 'credsUri', 'name') credsPath; | ||||||
|  |   @lazyCapabilities(apiPath`${'backend'}/rotate-role/${'name'}`, 'backend', 'name') staticRotateCredsPath; | ||||||
|  |  | ||||||
|  |   get canCreate() { | ||||||
|  |     return this.rolePath.get('canCreate') !== false; | ||||||
|  |   } | ||||||
|  |   get canDelete() { | ||||||
|  |     return this.rolePath.get('canDelete') !== false; | ||||||
|  |   } | ||||||
|  |   get canEdit() { | ||||||
|  |     return this.rolePath.get('canUpdate') !== false; | ||||||
|  |   } | ||||||
|  |   get canRead() { | ||||||
|  |     return this.rolePath.get('canRead') !== false; | ||||||
|  |   } | ||||||
|  |   get canList() { | ||||||
|  |     return this.rolePath.get('canList') !== false; | ||||||
|  |   } | ||||||
|  |   get canReadCreds() { | ||||||
|  |     return this.credsPath.get('canRead') !== false; | ||||||
|  |   } | ||||||
|  |   get canRotateStaticCreds() { | ||||||
|  |     return this.isStatic && this.staticRotateCredsPath.get('canCreate') !== false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fetchCredentials() { | ||||||
|  |     return this.store.adapterFor('ldap/role').fetchCredentials(this.backend, this.type, this.name); | ||||||
|  |   } | ||||||
|  |   rotateStaticPassword() { | ||||||
|  |     return this.store.adapterFor('ldap/role').rotateStaticPassword(this.backend, this.name); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -129,13 +129,14 @@ export default class SecretEngineModel extends Model { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   get icon() { |   get icon() { | ||||||
|     if (!this.engineType || this.engineType === 'kmip') { |     const defaultIcon = this.engineType || 'secrets'; | ||||||
|       return 'secrets'; |     return ( | ||||||
|     } |       { | ||||||
|     if (this.engineType === 'keymgmt') { |         keymgmt: 'key', | ||||||
|       return 'key'; |         kmip: 'secrets', | ||||||
|     } |         ldap: 'folder-users', | ||||||
|     return this.engineType; |       }[this.engineType] || defaultIcon | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get engineType() { |   get engineType() { | ||||||
|   | |||||||
| @@ -161,6 +161,7 @@ Router.map(function () { | |||||||
|           this.mount('kmip'); |           this.mount('kmip'); | ||||||
|           this.mount('kubernetes'); |           this.mount('kubernetes'); | ||||||
|           this.mount('kv'); |           this.mount('kv'); | ||||||
|  |           this.mount('ldap'); | ||||||
|           this.mount('pki'); |           this.mount('pki'); | ||||||
|           this.route('index', { path: '/' }); |           this.route('index', { path: '/' }); | ||||||
|           this.route('configuration'); |           this.route('configuration'); | ||||||
|   | |||||||
| @@ -5,13 +5,10 @@ | |||||||
|  |  | ||||||
| import ApplicationSerializer from '../application'; | import ApplicationSerializer from '../application'; | ||||||
|  |  | ||||||
| export default class KubernetesConfigSerializer extends ApplicationSerializer { | export default class KubernetesRoleSerializer extends ApplicationSerializer { | ||||||
|   primaryKey = 'name'; |   primaryKey = 'name'; | ||||||
|  |  | ||||||
|   serialize() { |   attrs = { | ||||||
|     const json = super.serialize(...arguments); |     backend: { serialize: false }, | ||||||
|     // remove backend value from payload |   }; | ||||||
|     delete json.backend; |  | ||||||
|     return json; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								ui/app/serializers/ldap/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								ui/app/serializers/ldap/config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import ApplicationSerializer from '../application'; | ||||||
|  |  | ||||||
|  | export default class LdapConfigSerializer extends ApplicationSerializer { | ||||||
|  |   primaryKey = 'backend'; | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								ui/app/serializers/ldap/library.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								ui/app/serializers/ldap/library.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import ApplicationSerializer from '../application'; | ||||||
|  |  | ||||||
|  | export default class LdapLibrarySerializer extends ApplicationSerializer { | ||||||
|  |   primaryKey = 'name'; | ||||||
|  |  | ||||||
|  |   attrs = { | ||||||
|  |     backend: { serialize: false }, | ||||||
|  |     name: { serialize: false }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // disable_check_in_enforcement is a boolean but needs to be presented as Disabled or Enabled | ||||||
|  |   normalize(modelClass, data) { | ||||||
|  |     data.disable_check_in_enforcement = data.disable_check_in_enforcement ? 'Disabled' : 'Enabled'; | ||||||
|  |     return super.normalize(modelClass, data); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   serialize() { | ||||||
|  |     const json = super.serialize(...arguments); | ||||||
|  |     json.disable_check_in_enforcement = json.disable_check_in_enforcement === 'Enabled' ? false : true; | ||||||
|  |     return json; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								ui/app/serializers/ldap/role.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ui/app/serializers/ldap/role.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import ApplicationSerializer from '../application'; | ||||||
|  |  | ||||||
|  | export default class LdapRoleSerializer extends ApplicationSerializer { | ||||||
|  |   primaryKey = 'name'; | ||||||
|  |  | ||||||
|  |   serialize(snapshot) { | ||||||
|  |     // remove all fields that are not relevant to specified role type | ||||||
|  |     const { fieldsForType } = snapshot.record; | ||||||
|  |     const json = super.serialize(...arguments); | ||||||
|  |     Object.keys(json).forEach((key) => { | ||||||
|  |       if (!fieldsForType.includes(key)) { | ||||||
|  |         delete json[key]; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     return json; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -10,10 +10,8 @@ import Service from '@ember/service'; | |||||||
| // are not accessible
 | // are not accessible
 | ||||||
| export default class SecretMountPath extends Service { | export default class SecretMountPath extends Service { | ||||||
|   currentPath = ''; |   currentPath = ''; | ||||||
|   update(path) { | 
 | ||||||
|  |   update(path: string) { | ||||||
|     this.currentPath = path; |     this.currentPath = path; | ||||||
|   } |   } | ||||||
|   get() { |  | ||||||
|     return this.currentPath; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| @@ -5,7 +5,6 @@ | |||||||
|  |  | ||||||
| .overview-card { | .overview-card { | ||||||
|   padding: $spacing-l; |   padding: $spacing-l; | ||||||
|   display: initial; |  | ||||||
|   line-height: initial; |   line-height: initial; | ||||||
|  |  | ||||||
|   .title-number { |   .title-number { | ||||||
|   | |||||||
| @@ -326,4 +326,9 @@ a.button.disabled { | |||||||
|   font-size: inherit; |   font-size: inherit; | ||||||
|   font-weight: inherit; |   font-weight: inherit; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|  |  | ||||||
|  |   &:disabled { | ||||||
|  |     color: $grey-light; | ||||||
|  |     cursor: not-allowed; | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,6 +18,10 @@ | |||||||
|   background-color: $ui-gray-200; |   background-color: $ui-gray-200; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .has-background-gray-900 { | ||||||
|  |   background-color: $ui-gray-900; | ||||||
|  | } | ||||||
|  |  | ||||||
| .background-color-black { | .background-color-black { | ||||||
|   background-color: black; |   background-color: black; | ||||||
| } | } | ||||||
| @@ -34,7 +38,9 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .has-error-border, | .has-error-border, | ||||||
| select.has-error-border { | select.has-error-border, | ||||||
|  | .ttl-picker-form-field-error input, | ||||||
|  | .string-list-form-field-error .field:first-of-type textarea { | ||||||
|   border: 1px solid $red-500; |   border: 1px solid $red-500; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -75,6 +81,10 @@ select.has-error-border { | |||||||
| .has-text-info { | .has-text-info { | ||||||
|   color: $blue-500 !important; |   color: $blue-500 !important; | ||||||
| } | } | ||||||
|  | // same without the !important | ||||||
|  | .has-text-primary { | ||||||
|  |   color: $blue-500; | ||||||
|  | } | ||||||
|  |  | ||||||
| .has-text-success { | .has-text-success { | ||||||
|   color: $green-500 !important; |   color: $green-500 !important; | ||||||
| @@ -87,3 +97,7 @@ select.has-error-border { | |||||||
| .has-text-danger { | .has-text-danger { | ||||||
|   color: $red-500 !important; |   color: $red-500 !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .has-text-primary { | ||||||
|  |   color: $blue; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -41,6 +41,11 @@ | |||||||
|   align-items: center; |   align-items: center; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .is-flex-align-start { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: flex-start; | ||||||
|  | } | ||||||
|  |  | ||||||
| .is-flex-align-baseline { | .is-flex-align-baseline { | ||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: baseline; |   align-items: baseline; | ||||||
|   | |||||||
| @@ -116,6 +116,10 @@ | |||||||
|   border-radius: $radius; |   border-radius: $radius; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .border-radius-4 { | ||||||
|  |   border-radius: $radius-large; | ||||||
|  | } | ||||||
|  |  | ||||||
| // border-spacing | // border-spacing | ||||||
| .is-border-spacing-revert { | .is-border-spacing-revert { | ||||||
|   border-spacing: revert; |   border-spacing: revert; | ||||||
|   | |||||||
| @@ -26,6 +26,10 @@ | |||||||
|   padding-right: $spacing-s; |   padding-right: $spacing-s; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .has-padding-s { | ||||||
|  |   padding: $spacing-s; | ||||||
|  | } | ||||||
|  |  | ||||||
| .has-padding-xxs { | .has-padding-xxs { | ||||||
|   padding: $spacing-xxs; |   padding: $spacing-xxs; | ||||||
| } | } | ||||||
| @@ -37,6 +41,10 @@ | |||||||
|   padding: $spacing-l; |   padding: $spacing-l; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .has-padding-l { | ||||||
|  |   padding: $spacing-l; | ||||||
|  | } | ||||||
|  |  | ||||||
| .has-bottom-padding-s { | .has-bottom-padding-s { | ||||||
|   padding-bottom: $spacing-s; |   padding-bottom: $spacing-s; | ||||||
| } | } | ||||||
| @@ -95,15 +103,22 @@ | |||||||
|   margin-bottom: -$spacing-m; |   margin-bottom: -$spacing-m; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .has-top-margin-negative-xxl { | ||||||
|  |   margin-top: -$spacing-xxl; | ||||||
|  | } | ||||||
|  |  | ||||||
| .has-top-margin-xxs { | .has-top-margin-xxs { | ||||||
|   margin: $spacing-xxs 0; |   margin: $spacing-xxs 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| .has-right-margin-xxs { | .has-right-margin-xxs { | ||||||
|   margin-right: $spacing-xxs; |   margin-right: $spacing-xxs; | ||||||
| } | } | ||||||
|  |  | ||||||
| .has-left-margin-xxs { | .has-left-margin-xxs { | ||||||
|   margin-left: $spacing-xxs; |   margin-left: $spacing-xxs; | ||||||
| } | } | ||||||
|  |  | ||||||
| .has-bottom-margin-xxs { | .has-bottom-margin-xxs { | ||||||
|   margin-bottom: $spacing-xxs !important; |   margin-bottom: $spacing-xxs !important; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,16 +0,0 @@ | |||||||
| {{! |  | ||||||
|   Copyright (c) HashiCorp, Inc. |  | ||||||
|   SPDX-License-Identifier: BUSL-1.1 |  | ||||||
| ~}} |  | ||||||
|  |  | ||||||
| <WizardSection |  | ||||||
|   @headerText="Active Directory" |  | ||||||
|   @headerIcon="azure-color" |  | ||||||
|   @docText="Docs: Active Directory Secrets" |  | ||||||
|   @docPath="/docs/secrets/ad/index.html" |  | ||||||
| > |  | ||||||
|   <p> |  | ||||||
|     The AD Secrets Engine rotates AD passwords dynamically, and is designed for a high-load environment where many instances |  | ||||||
|     may be accessing a shared password simultaneously. |  | ||||||
|   </p> |  | ||||||
| </WizardSection> |  | ||||||
| @@ -49,7 +49,7 @@ | |||||||
|   <LinkedBlock |   <LinkedBlock | ||||||
|     @params={{array backend.backendLink backend.id}} |     @params={{array backend.backendLink backend.id}} | ||||||
|     class="list-item-row linked-block-item is-no-underline" |     class="list-item-row linked-block-item is-no-underline" | ||||||
|     data-test-auth-backend-link={{backend.id}} |     data-test-secrets-backend-link={{backend.id}} | ||||||
|     @disabled={{if backend.isSupportedBackend false true}} |     @disabled={{if backend.isSupportedBackend false true}} | ||||||
|   > |   > | ||||||
|     <div> |     <div> | ||||||
|   | |||||||
							
								
								
									
										92
									
								
								ui/docs/fetch-secrets-engine-config.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								ui/docs/fetch-secrets-engine-config.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | # Fetch Secrets Engine Configuration Decorator | ||||||
|  |  | ||||||
|  | The `fetch-secrets-engine-config` decorator is available in the core addon and can be used on a route that needs to be aware of the configuration details of a secrets engine prior to model hook execution. This is useful for conditionally displaying a call to action for the user to complete the configuration. | ||||||
|  |  | ||||||
|  | ## API | ||||||
|  |  | ||||||
|  | The decorator accepts a single argument with the name of the Ember Data model to be fetched. | ||||||
|  |  | ||||||
|  | - **modelName** [string] - name of the Ember Data model to fetch which is passed to the `queryRecord` method. | ||||||
|  |  | ||||||
|  | With the provided model name, the decorator fetches the record using the store `queryRecord` method in the `beforeModel` route hook. Several properties are set on the route class based on the status of the request: | ||||||
|  |  | ||||||
|  | - **configModel** [Model | null] - set on success with resolved Ember Data model. | ||||||
|  |  | ||||||
|  | - **configError** [AdapterError | null] - set if the request errors with any status other than 404. | ||||||
|  |  | ||||||
|  | - **promptConfig** [boolean] - set to `true` if the request returns a 404, otherwise set to `false`. This is for convenience since checking for `(!this.configModel && !this.configError)` would result in the same value. | ||||||
|  |  | ||||||
|  | ## Usage | ||||||
|  |  | ||||||
|  | ### Configure route | ||||||
|  |  | ||||||
|  | ```js | ||||||
|  | @withConfig('foo/config') | ||||||
|  | export default class FooConfigureRoute extends Route { | ||||||
|  |   @service store; | ||||||
|  |   @service secretMountPath; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     const backend = this.secretMountPath.currentPath; | ||||||
|  |     return this.configModel || this.store.createRecord('foo/config', { backend }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | In the scenario of creating/updating the configuration, the model is used to populate the form if available, otherwise the form is presented in an empty state. Fetch errors are not a concern, nor is prompting the user to configure so only the `configModel` property is used. | ||||||
|  |  | ||||||
|  | ### Configuration route | ||||||
|  |  | ||||||
|  | ```js | ||||||
|  | @withConfig('foo/config') | ||||||
|  | export default class FooConfigurationRoute extends Route { | ||||||
|  |   @service store; | ||||||
|  |   @service secretMountPath; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     // the error could also be thrown to display the error template | ||||||
|  |     // in this example a component is used to display the error | ||||||
|  |     return { | ||||||
|  |       configModel: this.configModel, | ||||||
|  |       configError: this.configError, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | For configuration routes, the model and error properties may be used to determine what should be displayed to the user: | ||||||
|  |  | ||||||
|  | `configuration.hbs` | ||||||
|  |  | ||||||
|  | ```hbs | ||||||
|  | {{#if @configModel}} | ||||||
|  |   {{#each @configModel.fields as |field|}} | ||||||
|  |     <InfoTableRow @label={{field.label}} @value={{field.value}} /> | ||||||
|  |   {{/each}} | ||||||
|  | {{else if @configError}} | ||||||
|  |   <Page::Error @error={{@configError}} /> | ||||||
|  | {{else}} | ||||||
|  |   <ConfigCta /> | ||||||
|  | {{/if}} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Other routes (overview etc.) | ||||||
|  |  | ||||||
|  | This is the most basic usage where a route only needs to be aware of whether or not to show the config prompt: | ||||||
|  |  | ||||||
|  | ```js | ||||||
|  | @withConfig('foo/config') | ||||||
|  | export default class FooOverviewRoute extends Route { | ||||||
|  |   @service store; | ||||||
|  |   @service secretMountPath; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     const backend = this.secretMountPath.currentPath; | ||||||
|  |     return hash({ | ||||||
|  |       promptConfig: this.promptConfig, | ||||||
|  |       roles: this.store.query('foo/role', { backend }).catch(() => []), | ||||||
|  |       libraries: this.store.query('foo/library', { backend }).catch(() => []), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
							
								
								
									
										12
									
								
								ui/lib/core/addon/components/filter-input.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								ui/lib/core/addon/components/filter-input.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <div class="field"> | ||||||
|  |   <p class="control has-icons-left"> | ||||||
|  |     <Input | ||||||
|  |       class="filter input" | ||||||
|  |       placeholder={{this.placeholder}} | ||||||
|  |       data-test-filter-input | ||||||
|  |       @value={{@value}} | ||||||
|  |       {{on "input" this.onInput}} | ||||||
|  |     /> | ||||||
|  |     <Icon @name="search" class="search-icon has-text-grey-light" /> | ||||||
|  |   </p> | ||||||
|  | </div> | ||||||
							
								
								
									
										31
									
								
								ui/lib/core/addon/components/filter-input.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ui/lib/core/addon/components/filter-input.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import { debounce } from '@ember/runloop'; | ||||||
|  |  | ||||||
|  | import type { HTMLElementEvent } from 'vault/forms'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   placeholder?: string; // defaults to Type to filter results | ||||||
|  |   wait?: number; // defaults to 200 | ||||||
|  |   onInput(value: string): void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class FilterInputComponent extends Component<Args> { | ||||||
|  |   get placeholder() { | ||||||
|  |     return this.args.placeholder || 'Type to filter results'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action onInput(event: HTMLElementEvent<HTMLInputElement>) { | ||||||
|  |     const callback = () => { | ||||||
|  |       this.args.onInput(event.target.value); | ||||||
|  |     }; | ||||||
|  |     const wait = this.args.wait || 200; | ||||||
|  |     // ts complains when trying to pass object of optional args to callback as 3rd arg to debounce | ||||||
|  |     debounce(this, callback, wait); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -57,14 +57,6 @@ | |||||||
|             {{/each}} |             {{/each}} | ||||||
|           </select> |           </select> | ||||||
|         </div> |         </div> | ||||||
|         {{#if this.validationError}} |  | ||||||
|           <AlertInline |  | ||||||
|             @type="danger" |  | ||||||
|             @message={{this.validationError}} |  | ||||||
|             @paddingTop={{true}} |  | ||||||
|             data-test-field-validation={{@attr.name}} |  | ||||||
|           /> |  | ||||||
|         {{/if}} |  | ||||||
|       </div> |       </div> | ||||||
|     {{/if}} |     {{/if}} | ||||||
|   {{else if (eq @attr.options.editType "searchSelect")}} |   {{else if (eq @attr.options.editType "searchSelect")}} | ||||||
| @@ -86,9 +78,6 @@ | |||||||
|         class={{if this.validationError "dropdown-has-error-border"}} |         class={{if this.validationError "dropdown-has-error-border"}} | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|     {{#if this.validationError}} |  | ||||||
|       <AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} /> |  | ||||||
|     {{/if}} |  | ||||||
|   {{else if (eq @attr.options.editType "mountAccessor")}} |   {{else if (eq @attr.options.editType "mountAccessor")}} | ||||||
|     <MountAccessorSelect |     <MountAccessorSelect | ||||||
|       @name={{@attr.name}} |       @name={{@attr.name}} | ||||||
| @@ -126,6 +115,7 @@ | |||||||
|       {{#let (or (get @model this.valuePath) @attr.options.setDefault) as |initialValue|}} |       {{#let (or (get @model this.valuePath) @attr.options.setDefault) as |initialValue|}} | ||||||
|         <TtlPicker |         <TtlPicker | ||||||
|           data-test-input={{@attr.name}} |           data-test-input={{@attr.name}} | ||||||
|  |           class={{if this.validationError "ttl-picker-form-field-error"}} | ||||||
|           @onChange={{this.setAndBroadcastTtl}} |           @onChange={{this.setAndBroadcastTtl}} | ||||||
|           @label={{this.labelString}} |           @label={{this.labelString}} | ||||||
|           @helperTextDisabled={{or @attr.options.helperTextDisabled "Vault will use the default lease duration."}} |           @helperTextDisabled={{or @attr.options.helperTextDisabled "Vault will use the default lease duration."}} | ||||||
| @@ -194,6 +184,7 @@ | |||||||
|     </Toggle> |     </Toggle> | ||||||
|   {{else if (eq @attr.options.editType "stringArray")}} |   {{else if (eq @attr.options.editType "stringArray")}} | ||||||
|     <StringList |     <StringList | ||||||
|  |       class={{if this.validationError "string-list-form-field-error"}} | ||||||
|       data-test-input={{@attr.name}} |       data-test-input={{@attr.name}} | ||||||
|       @label={{this.labelString}} |       @label={{this.labelString}} | ||||||
|       @helpText={{if this.showHelpText @attr.options.helpText}} |       @helpText={{if this.showHelpText @attr.options.helpText}} | ||||||
| @@ -210,9 +201,6 @@ | |||||||
|       @onChange={{this.setAndBroadcast}} |       @onChange={{this.setAndBroadcast}} | ||||||
|       @onKeyUp={{@onKeyUp}} |       @onKeyUp={{@onKeyUp}} | ||||||
|     /> |     /> | ||||||
|     {{#if this.validationError}} |  | ||||||
|       <AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} /> |  | ||||||
|     {{/if}} |  | ||||||
|   {{else if (or (eq @attr.type "number") (eq @attr.type "string"))}} |   {{else if (or (eq @attr.type "number") (eq @attr.type "string"))}} | ||||||
|     <div class="control"> |     <div class="control"> | ||||||
|       {{#if (eq @attr.options.editType "textarea")}} |       {{#if (eq @attr.options.editType "textarea")}} | ||||||
| @@ -224,9 +212,6 @@ | |||||||
|           oninput={{this.onChangeWithEvent}} |           oninput={{this.onChangeWithEvent}} | ||||||
|           class="textarea {{if this.validationError 'has-error-border'}}" |           class="textarea {{if this.validationError 'has-error-border'}}" | ||||||
|         ></textarea> |         ></textarea> | ||||||
|         {{#if this.validationError}} |  | ||||||
|           <AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} /> |  | ||||||
|         {{/if}} |  | ||||||
|       {{else if (eq @attr.options.editType "password")}} |       {{else if (eq @attr.options.editType "password")}} | ||||||
|         <Input |         <Input | ||||||
|           data-test-input={{@attr.name}} |           data-test-input={{@attr.name}} | ||||||
| @@ -253,6 +238,7 @@ | |||||||
|             @theme={{or @attr.options.theme "hashi"}} |             @theme={{or @attr.options.theme "hashi"}} | ||||||
|             @helpText={{@attr.options.helpText}} |             @helpText={{@attr.options.helpText}} | ||||||
|             @mode={{@attr.options.mode}} |             @mode={{@attr.options.mode}} | ||||||
|  |             @example={{@attr.options.example}} | ||||||
|           > |           > | ||||||
|             {{#if @attr.options.allowReset}} |             {{#if @attr.options.allowReset}} | ||||||
|               <button |               <button | ||||||
| @@ -295,22 +281,12 @@ | |||||||
|           class="input {{if this.validationError 'has-error-border'}}" |           class="input {{if this.validationError 'has-error-border'}}" | ||||||
|           maxLength={{@attr.options.characterLimit}} |           maxLength={{@attr.options.characterLimit}} | ||||||
|         /> |         /> | ||||||
|  |         {{! TODO: explore removing in favor of new model validations pattern since it is only used on the namespace model }} | ||||||
|         {{#if @attr.options.validationAttr}} |         {{#if @attr.options.validationAttr}} | ||||||
|           {{#if (and (get @model this.valuePath) (not (get @model @attr.options.validationAttr)))}} |           {{#if (and (get @model this.valuePath) (not (get @model @attr.options.validationAttr)))}} | ||||||
|             <AlertInline @type="danger" @message={{@attr.options.invalidMessage}} /> |             <AlertInline @type="danger" @message={{@attr.options.invalidMessage}} /> | ||||||
|           {{/if}} |           {{/if}} | ||||||
|         {{/if}} |         {{/if}} | ||||||
|         {{#if this.validationError}} |  | ||||||
|           <AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} /> |  | ||||||
|         {{/if}} |  | ||||||
|         {{#if this.validationWarning}} |  | ||||||
|           <AlertInline |  | ||||||
|             @type="warning" |  | ||||||
|             @message={{this.validationWarning}} |  | ||||||
|             @paddingTop={{true}} |  | ||||||
|             data-test-validation-warning |  | ||||||
|           /> |  | ||||||
|         {{/if}} |  | ||||||
|       {{/if}} |       {{/if}} | ||||||
|     </div> |     </div> | ||||||
|   {{else if (or (eq @attr.type "boolean") (eq @attr.options.editType "boolean"))}} |   {{else if (or (eq @attr.type "boolean") (eq @attr.options.editType "boolean"))}} | ||||||
| @@ -347,8 +323,27 @@ | |||||||
|       @value={{if (get @model this.valuePath) (stringify (get @model this.valuePath)) this.emptyData}} |       @value={{if (get @model this.valuePath) (stringify (get @model this.valuePath)) this.emptyData}} | ||||||
|       @valueUpdated={{fn this.codemirrorUpdated false}} |       @valueUpdated={{fn this.codemirrorUpdated false}} | ||||||
|       @helpText={{@attr.options.helpText}} |       @helpText={{@attr.options.helpText}} | ||||||
|  |       @example={{@attr.options.example}} | ||||||
|     /> |     /> | ||||||
|   {{else if (eq @attr.options.editType "yield")}} |   {{else if (eq @attr.options.editType "yield")}} | ||||||
|     {{yield}} |     {{yield}} | ||||||
|   {{/if}} |   {{/if}} | ||||||
|  |   {{#if this.validationError}} | ||||||
|  |     <AlertInline | ||||||
|  |       @type="danger" | ||||||
|  |       @message={{this.validationError}} | ||||||
|  |       @paddingTop={{not-eq @attr.options.editType "ttl"}} | ||||||
|  |       data-test-field-validation={{@attr.name}} | ||||||
|  |       class={{if (eq @attr.options.editType "stringArray") "has-top-margin-negative-xxl"}} | ||||||
|  |     /> | ||||||
|  |   {{/if}} | ||||||
|  |   {{#if this.validationWarning}} | ||||||
|  |     <AlertInline | ||||||
|  |       @type="warning" | ||||||
|  |       @message={{this.validationWarning}} | ||||||
|  |       @paddingTop={{if (and (not this.validationError) (eq @attr.options.editType "ttl")) false true}} | ||||||
|  |       data-test-validation-warning={{@attr.name}} | ||||||
|  |       class={{if (and (not this.validationError) (eq @attr.options.editType "stringArray")) "has-top-margin-negative-xxl"}} | ||||||
|  |     /> | ||||||
|  |   {{/if}} | ||||||
| </div> | </div> | ||||||
| @@ -15,6 +15,18 @@ | |||||||
|         </label> |         </label> | ||||||
|         <ToolbarActions> |         <ToolbarActions> | ||||||
|           {{yield}} |           {{yield}} | ||||||
|  |           {{#if @example}} | ||||||
|  |             <button | ||||||
|  |               type="button" | ||||||
|  |               class="toolbar-link" | ||||||
|  |               disabled={{not @value}} | ||||||
|  |               {{on "click" this.restoreExample}} | ||||||
|  |               data-test-restore-example | ||||||
|  |             > | ||||||
|  |               Restore example | ||||||
|  |               <Icon @name="reload" /> | ||||||
|  |             </button> | ||||||
|  |           {{/if}} | ||||||
|           <div class="toolbar-separator"></div> |           <div class="toolbar-separator"></div> | ||||||
|           <CopyButton |           <CopyButton | ||||||
|             class="button is-transparent" |             class="button is-transparent" | ||||||
| @@ -30,7 +42,7 @@ | |||||||
|   {{/if}} |   {{/if}} | ||||||
|   <div |   <div | ||||||
|     {{code-mirror |     {{code-mirror | ||||||
|       content=@value |       content=(or @value @example) | ||||||
|       extraKeys=@extraKeys |       extraKeys=@extraKeys | ||||||
|       gutters=@gutters |       gutters=@gutters | ||||||
|       lineNumbers=(if @readOnly false true) |       lineNumbers=(if @readOnly false true) | ||||||
| @@ -38,6 +50,7 @@ | |||||||
|       readOnly=@readOnly |       readOnly=@readOnly | ||||||
|       theme=@theme |       theme=@theme | ||||||
|       viewportMarg=@viewportMargin |       viewportMarg=@viewportMargin | ||||||
|  |       onSetup=this.onSetup | ||||||
|       onUpdate=this.onUpdate |       onUpdate=this.onUpdate | ||||||
|       onFocus=this.onFocus |       onFocus=this.onFocus | ||||||
|     }} |     }} | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ import { action } from '@ember/object'; | |||||||
|  * @param {String} [theme] - Specify or customize the look via a named "theme" class in scss. |  * @param {String} [theme] - Specify or customize the look via a named "theme" class in scss. | ||||||
|  * @param {String} [value] - Value within the display. Generally, a json string. |  * @param {String} [value] - Value within the display. Generally, a json string. | ||||||
|  * @param {String} [viewportMargin] - Size of viewport. Often set to "Infinity" to load/show all text regardless of length. |  * @param {String} [viewportMargin] - Size of viewport. Often set to "Infinity" to load/show all text regardless of length. | ||||||
|  |  * @param {string} [example] - Example to show when value is null -- when example is provided a restore action will render in the toolbar to clear the current value and show the example after input | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| export default class JsonEditorComponent extends Component { | export default class JsonEditorComponent extends Component { | ||||||
| @@ -33,6 +34,12 @@ export default class JsonEditorComponent extends Component { | |||||||
|     return this.args.showToolbar === false ? false : true; |     return this.args.showToolbar === false ? false : true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   onSetup(editor) { | ||||||
|  |     // store reference to codemirror editor so that it can be passed to valueUpdated when restoring example | ||||||
|  |     this._codemirrorEditor = editor; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @action |   @action | ||||||
|   onUpdate(...args) { |   onUpdate(...args) { | ||||||
|     if (!this.args.readOnly) { |     if (!this.args.readOnly) { | ||||||
| @@ -47,4 +54,10 @@ export default class JsonEditorComponent extends Component { | |||||||
|       this.args.onFocusOut(...args); |       this.args.onFocusOut(...args); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   restoreExample() { | ||||||
|  |     // set value to null which will cause the example value to be passed into the editor | ||||||
|  |     this.args.valueUpdated(null, this._codemirrorEditor); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
|   @hasBorder={{true}} |   @hasBorder={{true}} | ||||||
|   class="overview-card border-radius-2" |   class="overview-card border-radius-2" | ||||||
|   data-test-overview-card-container={{@cardTitle}} |   data-test-overview-card-container={{@cardTitle}} | ||||||
|  |   ...attributes | ||||||
| > | > | ||||||
|   <div class="is-flex-between" data-test-overview-card={{@cardTitle}}> |   <div class="is-flex-between" data-test-overview-card={{@cardTitle}}> | ||||||
|     <h3 class="title is-5">{{@cardTitle}}</h3> |     <h3 class="title is-5">{{@cardTitle}}</h3> | ||||||
|   | |||||||
| @@ -9,14 +9,16 @@ | |||||||
|       <li data-test-crumb="{{idx}}"> |       <li data-test-crumb="{{idx}}"> | ||||||
|         <span class="sep">/</span> |         <span class="sep">/</span> | ||||||
|         {{#if breadcrumb.linkExternal}} |         {{#if breadcrumb.linkExternal}} | ||||||
|           <LinkToExternal @route={{breadcrumb.route}}>{{breadcrumb.label}}</LinkToExternal> |           <LinkToExternal @route={{breadcrumb.route}} data-test-breadcrumb={{breadcrumb.label}}> | ||||||
|  |             {{breadcrumb.label}} | ||||||
|  |           </LinkToExternal> | ||||||
|         {{else if breadcrumb.route}} |         {{else if breadcrumb.route}} | ||||||
|           {{#if breadcrumb.model}} |           {{#if breadcrumb.model}} | ||||||
|             <LinkTo @route={{breadcrumb.route}} @model={{breadcrumb.model}}> |             <LinkTo @route={{breadcrumb.route}} @model={{breadcrumb.model}} data-test-breadcrumb={{breadcrumb.label}}> | ||||||
|               {{breadcrumb.label}} |               {{breadcrumb.label}} | ||||||
|             </LinkTo> |             </LinkTo> | ||||||
|           {{else}} |           {{else}} | ||||||
|             <LinkTo @route={{breadcrumb.route}}> |             <LinkTo @route={{breadcrumb.route}} data-test-breadcrumb={{breadcrumb.label}}> | ||||||
|               {{breadcrumb.label}} |               {{breadcrumb.label}} | ||||||
|             </LinkTo> |             </LinkTo> | ||||||
|           {{/if}} |           {{/if}} | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								ui/lib/core/addon/components/secrets-engine-mount-config.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								ui/lib/core/addon/components/secrets-engine-mount-config.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | <div ...attributes> | ||||||
|  |   <ToggleButton | ||||||
|  |     @isOpen={{this.showConfig}} | ||||||
|  |     @openLabel="Hide mount configuration" | ||||||
|  |     @closedLabel="Show mount configuration" | ||||||
|  |     @onClick={{fn (mut this.showConfig) (not this.showConfig)}} | ||||||
|  |     class="is-block" | ||||||
|  |     data-test-mount-config-toggle | ||||||
|  |   /> | ||||||
|  |   {{#if this.showConfig}} | ||||||
|  |     {{#each this.fields as |field|}} | ||||||
|  |       <InfoTableRow @label={{field.label}} @value={{field.value}} data-test-mount-config-field={{field.label}} /> | ||||||
|  |     {{/each}} | ||||||
|  |     {{! block for additional fields that may be engine specific }} | ||||||
|  |     {{yield}} | ||||||
|  |   {{/if}} | ||||||
|  | </div> | ||||||
							
								
								
									
										29
									
								
								ui/lib/core/addon/components/secrets-engine-mount-config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ui/lib/core/addon/components/secrets-engine-mount-config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  |  | ||||||
|  | import type SecretEngineModel from 'vault/models/secret-engine'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   model: SecretEngineModel; | ||||||
|  | } | ||||||
|  | interface Field { | ||||||
|  |   label: string; | ||||||
|  |   value: string | boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class SecretsEngineMountConfigComponent extends Component<Args> { | ||||||
|  |   @tracked showConfig = false; | ||||||
|  |  | ||||||
|  |   get fields(): Array<Field> { | ||||||
|  |     const { model } = this.args; | ||||||
|  |     return [ | ||||||
|  |       { label: 'Secret Engine Type', value: model.engineType }, | ||||||
|  |       { label: 'Path', value: model.path }, | ||||||
|  |       { label: 'Accessor', value: model.accessor }, | ||||||
|  |       { label: 'Local', value: model.local }, | ||||||
|  |       { label: 'Seal Wrap', value: model.sealWrap }, | ||||||
|  |       { label: 'Default Lease TTL', value: model.config.defaultLeaseTtl }, | ||||||
|  |       { label: 'Max Lease TTL', value: model.config.maxLeaseTtl }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,35 +5,48 @@ | |||||||
| 
 | 
 | ||||||
| import Route from '@ember/routing/route'; | import Route from '@ember/routing/route'; | ||||||
| 
 | 
 | ||||||
|  | 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 Model from '@ember-data/model'; | ||||||
|  | import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports
 | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * the overview, configure, configuration and roles routes all need to be aware of the config for the engine |  * for use in routes that need to be aware of the config for a secrets engine | ||||||
|  * if the user has not configured they are prompted to do so in each of the routes |  * if the user has not configured they are prompted to do so in each of the routes | ||||||
|  * decorate the necessary routes to perform the check in the beforeModel hook since that may change what is returned for the model |  * decorate the necessary routes to perform the check in the beforeModel hook since that may change what is returned for the model | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| export function withConfig() { | interface BaseRoute extends Route { | ||||||
|   return function decorator(SuperClass) { |   store: Store; | ||||||
|  |   secretMountPath: SecretMountPath; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function withConfig(modelName: string) { | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  |   return function <RouteClass extends new (...args: any[]) => BaseRoute>(SuperClass: RouteClass) { | ||||||
|     if (!Object.prototype.isPrototypeOf.call(Route, SuperClass)) { |     if (!Object.prototype.isPrototypeOf.call(Route, SuperClass)) { | ||||||
|       // eslint-disable-next-line
 |       // eslint-disable-next-line
 | ||||||
|       console.error( |       console.error( | ||||||
|         'withConfig decorator must be used on an instance of ember Route class. Decorator not applied to returned class' |         'withConfig decorator must be used on an instance of Ember Route class. Decorator not applied to returned class' | ||||||
|       ); |       ); | ||||||
|       return SuperClass; |       return SuperClass; | ||||||
|     } |     } | ||||||
|     return class FetchConfig extends SuperClass { | 
 | ||||||
|       configModel = null; |     return class FetchSecretsEngineConfig extends SuperClass { | ||||||
|       configError = null; |       configModel: Model | null = null; | ||||||
|  |       configError: AdapterError | null = null; | ||||||
|       promptConfig = false; |       promptConfig = false; | ||||||
| 
 | 
 | ||||||
|       async beforeModel() { |       async beforeModel(transition: Transition) { | ||||||
|         super.beforeModel(...arguments); |         super.beforeModel(transition); | ||||||
| 
 | 
 | ||||||
|         const backend = this.secretMountPath.get(); |         const backend = this.secretMountPath.currentPath; | ||||||
|         // check the store for record first
 |         // check the store for record first
 | ||||||
|         this.configModel = this.store.peekRecord('kubernetes/config', backend); |         this.configModel = this.store.peekRecord(modelName, backend); | ||||||
|         if (!this.configModel) { |         if (!this.configModel) { | ||||||
|           return this.store |           return this.store | ||||||
|             .queryRecord('kubernetes/config', { backend }) |             .queryRecord(modelName, { backend }) | ||||||
|             .then((record) => { |             .then((record) => { | ||||||
|               this.configModel = record; |               this.configModel = record; | ||||||
|               this.promptConfig = false; |               this.promptConfig = false; | ||||||
| @@ -68,5 +68,9 @@ export default class CodeMirrorModifier extends Modifier { | |||||||
|     editor.on('focus', bind(this, this._onFocus, namedArgs)); |     editor.on('focus', bind(this, this._onFocus, namedArgs)); | ||||||
|  |  | ||||||
|     this._editor = editor; |     this._editor = editor; | ||||||
|  |  | ||||||
|  |     if (namedArgs.onSetup) { | ||||||
|  |       namedArgs.onSetup(editor); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								ui/lib/core/app/components/filter-input.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ui/lib/core/app/components/filter-input.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | export { default } from 'core/components/filter-input'; | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | export { default } from 'core/components/secrets-engine-mount-config'; | ||||||
							
								
								
									
										6
									
								
								ui/lib/core/app/helpers/jsonify.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ui/lib/core/app/helpers/jsonify.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | export { default, jsonify } from 'core/helpers/jsonify'; | ||||||
| @@ -3,4 +3,4 @@ | |||||||
|  * SPDX-License-Identifier: MPL-2.0 |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| export { default } from 'core/helpers/stringify'; | export { default, stringify } from 'core/helpers/stringify'; | ||||||
|   | |||||||
| @@ -5,9 +5,9 @@ | |||||||
|  |  | ||||||
| import Route from '@ember/routing/route'; | import Route from '@ember/routing/route'; | ||||||
| import { inject as service } from '@ember/service'; | import { inject as service } from '@ember/service'; | ||||||
| import { withConfig } from '../decorators/fetch-config'; | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
|  |  | ||||||
| @withConfig() | @withConfig('kubernetes/config') | ||||||
| export default class KubernetesConfigureRoute extends Route { | export default class KubernetesConfigureRoute extends Route { | ||||||
|   @service store; |   @service store; | ||||||
|   @service secretMountPath; |   @service secretMountPath; | ||||||
|   | |||||||
| @@ -5,15 +5,15 @@ | |||||||
|  |  | ||||||
| import Route from '@ember/routing/route'; | import Route from '@ember/routing/route'; | ||||||
| import { inject as service } from '@ember/service'; | import { inject as service } from '@ember/service'; | ||||||
| import { withConfig } from '../decorators/fetch-config'; | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
|  |  | ||||||
| @withConfig() | @withConfig('kubernetes/config') | ||||||
| export default class KubernetesConfigureRoute extends Route { | export default class KubernetesConfigureRoute extends Route { | ||||||
|   @service store; |   @service store; | ||||||
|   @service secretMountPath; |   @service secretMountPath; | ||||||
|  |  | ||||||
|   async model() { |   async model() { | ||||||
|     const backend = this.secretMountPath.get(); |     const backend = this.secretMountPath.currentPath; | ||||||
|     return this.configModel || this.store.createRecord('kubernetes/config', { backend }); |     return this.configModel || this.store.createRecord('kubernetes/config', { backend }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,16 +5,16 @@ | |||||||
|  |  | ||||||
| import Route from '@ember/routing/route'; | import Route from '@ember/routing/route'; | ||||||
| import { inject as service } from '@ember/service'; | import { inject as service } from '@ember/service'; | ||||||
| import { withConfig } from 'kubernetes/decorators/fetch-config'; | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
| import { hash } from 'rsvp'; | import { hash } from 'rsvp'; | ||||||
|  |  | ||||||
| @withConfig() | @withConfig('kubernetes/config') | ||||||
| export default class KubernetesOverviewRoute extends Route { | export default class KubernetesOverviewRoute extends Route { | ||||||
|   @service store; |   @service store; | ||||||
|   @service secretMountPath; |   @service secretMountPath; | ||||||
|  |  | ||||||
|   async model() { |   async model() { | ||||||
|     const backend = this.secretMountPath.get(); |     const backend = this.secretMountPath.currentPath; | ||||||
|     return hash({ |     return hash({ | ||||||
|       promptConfig: this.promptConfig, |       promptConfig: this.promptConfig, | ||||||
|       backend: this.modelFor('application'), |       backend: this.modelFor('application'), | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ export default class KubernetesRolesCreateRoute extends Route { | |||||||
|   @service secretMountPath; |   @service secretMountPath; | ||||||
|  |  | ||||||
|   model() { |   model() { | ||||||
|     const backend = this.secretMountPath.get(); |     const backend = this.secretMountPath.currentPath; | ||||||
|     return this.store.createRecord('kubernetes/role', { backend }); |     return this.store.createRecord('kubernetes/role', { backend }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,10 +5,10 @@ | |||||||
|  |  | ||||||
| import Route from '@ember/routing/route'; | import Route from '@ember/routing/route'; | ||||||
| import { inject as service } from '@ember/service'; | import { inject as service } from '@ember/service'; | ||||||
| import { withConfig } from 'kubernetes/decorators/fetch-config'; | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
| import { hash } from 'rsvp'; | import { hash } from 'rsvp'; | ||||||
|  |  | ||||||
| @withConfig() | @withConfig('kubernetes/config') | ||||||
| export default class KubernetesRolesRoute extends Route { | export default class KubernetesRolesRoute extends Route { | ||||||
|   @service store; |   @service store; | ||||||
|   @service secretMountPath; |   @service secretMountPath; | ||||||
| @@ -17,7 +17,7 @@ export default class KubernetesRolesRoute extends Route { | |||||||
|     // filter roles based on pageFilter value |     // filter roles based on pageFilter value | ||||||
|     const { pageFilter } = transition.to.queryParams; |     const { pageFilter } = transition.to.queryParams; | ||||||
|     const roles = this.store |     const roles = this.store | ||||||
|       .query('kubernetes/role', { backend: this.secretMountPath.get() }) |       .query('kubernetes/role', { backend: this.secretMountPath.currentPath }) | ||||||
|       .then((models) => |       .then((models) => | ||||||
|         pageFilter |         pageFilter | ||||||
|           ? models.filter((model) => model.name.toLowerCase().includes(pageFilter.toLowerCase())) |           ? models.filter((model) => model.name.toLowerCase().includes(pageFilter.toLowerCase())) | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ export default class KubernetesRoleCredentialsRoute extends Route { | |||||||
|   model() { |   model() { | ||||||
|     return { |     return { | ||||||
|       roleName: this.paramsFor('roles.role').name, |       roleName: this.paramsFor('roles.role').name, | ||||||
|       backend: this.secretMountPath.get(), |       backend: this.secretMountPath.currentPath, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ export default class KubernetesRoleDetailsRoute extends Route { | |||||||
|   @service secretMountPath; |   @service secretMountPath; | ||||||
|  |  | ||||||
|   model() { |   model() { | ||||||
|     const backend = this.secretMountPath.get(); |     const backend = this.secretMountPath.currentPath; | ||||||
|     const { name } = this.paramsFor('roles.role'); |     const { name } = this.paramsFor('roles.role'); | ||||||
|     return this.store.queryRecord('kubernetes/role', { backend, name }); |     return this.store.queryRecord('kubernetes/role', { backend, name }); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ export default class KubernetesRoleEditRoute extends Route { | |||||||
|   @service secretMountPath; |   @service secretMountPath; | ||||||
|  |  | ||||||
|   model() { |   model() { | ||||||
|     const backend = this.secretMountPath.get(); |     const backend = this.secretMountPath.currentPath; | ||||||
|     const { name } = this.paramsFor('roles.role'); |     const { name } = this.paramsFor('roles.role'); | ||||||
|     return this.store.queryRecord('kubernetes/role', { backend, name }); |     return this.store.queryRecord('kubernetes/role', { backend, name }); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										75
									
								
								ui/lib/ldap/addon/components/accounts-checked-out.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								ui/lib/ldap/addon/components/accounts-checked-out.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | <OverviewCard | ||||||
|  |   @cardTitle="Accounts checked-out" | ||||||
|  |   @subText="The accounts that are currently on lease with this token or exist in a library set with check-in enforcement disabled." | ||||||
|  |   class="has-padding-l" | ||||||
|  |   ...attributes | ||||||
|  | > | ||||||
|  |   <hr class="has-background-gray-200" /> | ||||||
|  |  | ||||||
|  |   {{#if this.filteredAccounts}} | ||||||
|  |     <Hds::Table @model={{this.filteredAccounts}} @columns={{this.columns}}> | ||||||
|  |       <:body as |Body|> | ||||||
|  |         <Body.Tr> | ||||||
|  |           <Body.Td data-test-checked-out-account={{Body.data.account}}>{{Body.data.account}}</Body.Td> | ||||||
|  |           {{#if @showLibraryColumn}} | ||||||
|  |             <Body.Td data-test-checked-out-library={{Body.data.account}}>{{Body.data.library}}</Body.Td> | ||||||
|  |           {{/if}} | ||||||
|  |           <Body.Td> | ||||||
|  |             <button | ||||||
|  |               type="button" | ||||||
|  |               class="text-button has-text-primary has-text-weight-semibold" | ||||||
|  |               disabled={{this.disableCheckIn Body.data.library}} | ||||||
|  |               data-test-checked-out-account-action={{Body.data.account}} | ||||||
|  |               {{on "click" (fn (mut this.selectedStatus) Body.data)}} | ||||||
|  |             > | ||||||
|  |               <Icon @name="queue" /> | ||||||
|  |               Check-in | ||||||
|  |             </button> | ||||||
|  |           </Body.Td> | ||||||
|  |         </Body.Tr> | ||||||
|  |       </:body> | ||||||
|  |     </Hds::Table> | ||||||
|  |   {{else}} | ||||||
|  |     <EmptyState | ||||||
|  |       @title="No accounts checked out yet" | ||||||
|  |       @message="There is no account that is currently in use." | ||||||
|  |       class="is-shadowless" | ||||||
|  |     /> | ||||||
|  |   {{/if}} | ||||||
|  | </OverviewCard> | ||||||
|  |  | ||||||
|  | {{#if this.selectedStatus}} | ||||||
|  |   <Modal | ||||||
|  |     @title="Account Check-in" | ||||||
|  |     @isActive={{this.selectedStatus}} | ||||||
|  |     @showCloseButton={{true}} | ||||||
|  |     @onClose={{fn (mut this.selectedStatus) undefined}} | ||||||
|  |   > | ||||||
|  |     <section class="modal-card-body"> | ||||||
|  |       <p> | ||||||
|  |         This action will check-in account | ||||||
|  |         {{this.selectedStatus.account}} | ||||||
|  |         back to the library. Do you want to proceed? | ||||||
|  |       </p> | ||||||
|  |     </section> | ||||||
|  |     <footer class="modal-card-foot modal-card-foot-outlined"> | ||||||
|  |       <button | ||||||
|  |         type="button" | ||||||
|  |         class="button is-primary {{if this.save.isRunning 'is-loading'}}" | ||||||
|  |         disabled={{this.checkIn.isRunning}} | ||||||
|  |         data-test-check-in-confirm | ||||||
|  |         {{on "click" (perform this.checkIn)}} | ||||||
|  |       > | ||||||
|  |         Confirm | ||||||
|  |       </button> | ||||||
|  |       <button | ||||||
|  |         type="button" | ||||||
|  |         class="button" | ||||||
|  |         disabled={{this.checkIn.isRunning}} | ||||||
|  |         {{on "click" (fn (mut this.selectedStatus) "")}} | ||||||
|  |       > | ||||||
|  |         Cancel | ||||||
|  |       </button> | ||||||
|  |     </footer> | ||||||
|  |   </Modal> | ||||||
|  | {{/if}} | ||||||
							
								
								
									
										72
									
								
								ui/lib/ldap/addon/components/accounts-checked-out.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								ui/lib/ldap/addon/components/accounts-checked-out.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { task } from 'ember-concurrency'; | ||||||
|  | import { waitFor } from '@ember/test-waiters'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  | import type AuthService from 'vault/services/auth'; | ||||||
|  | import type LdapLibraryModel from 'vault/models/ldap/library'; | ||||||
|  | import type { LdapLibraryAccountStatus } from 'vault/adapters/ldap/library'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   libraries: Array<LdapLibraryModel>; | ||||||
|  |   statuses: Array<LdapLibraryAccountStatus>; | ||||||
|  |   showLibraryColumn: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapAccountsCheckedOutComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |   @service declare readonly auth: AuthService; | ||||||
|  |  | ||||||
|  |   @tracked selectedStatus: LdapLibraryAccountStatus | undefined; | ||||||
|  |  | ||||||
|  |   get columns() { | ||||||
|  |     const columns = [{ label: 'Account' }, { label: 'Action' }]; | ||||||
|  |     if (this.args.showLibraryColumn) { | ||||||
|  |       columns.splice(1, 0, { label: 'Library' }); | ||||||
|  |     } | ||||||
|  |     return columns; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get filteredAccounts() { | ||||||
|  |     // filter status to only show checked out accounts associated to the current user | ||||||
|  |     // if disable_check_in_enforcement is true on the library set then all checked out accounts are displayed | ||||||
|  |     return this.args.statuses.filter((status) => { | ||||||
|  |       const authEntityId = this.auth.authData?.entity_id; | ||||||
|  |       const isRoot = !status.borrower_entity_id && !authEntityId; // root user will not have an entity id and it won't be populated on status | ||||||
|  |       const isEntity = status.borrower_entity_id === authEntityId; | ||||||
|  |       const library = this.findLibrary(status.library); | ||||||
|  |       const enforcementDisabled = library.disable_check_in_enforcement === 'Disabled'; | ||||||
|  |  | ||||||
|  |       return !status.available && (enforcementDisabled || isEntity || isRoot); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   disableCheckIn = (name: string) => { | ||||||
|  |     return !this.findLibrary(name).canCheckIn; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   findLibrary(name: string): LdapLibraryModel { | ||||||
|  |     return this.args.libraries.find((library) => library.name === name) as LdapLibraryModel; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @task | ||||||
|  |   @waitFor | ||||||
|  |   *checkIn() { | ||||||
|  |     const { library, account } = this.selectedStatus as LdapLibraryAccountStatus; | ||||||
|  |     try { | ||||||
|  |       const libraryModel = this.findLibrary(library); | ||||||
|  |       yield libraryModel.checkInAccount(account); | ||||||
|  |       this.flashMessages.success(`Successfully checked in the account ${account}.`); | ||||||
|  |       // transitioning to the current route to trigger the model hook so we can fetch the updated status | ||||||
|  |       this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.details.accounts'); | ||||||
|  |     } catch (error) { | ||||||
|  |       this.selectedStatus = undefined; | ||||||
|  |       this.flashMessages.danger(`Error checking in the account ${account}. \n ${errorMessage(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								ui/lib/ldap/addon/components/config-cta.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ui/lib/ldap/addon/components/config-cta.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <EmptyState | ||||||
|  |   data-test-config-cta | ||||||
|  |   @title="LDAP not configured" | ||||||
|  |   @message="Get started by setting up the connection with your existing LDAP system." | ||||||
|  | > | ||||||
|  |   <LinkTo class="has-top-margin-xs" @route="configure"> | ||||||
|  |     Configure LDAP | ||||||
|  |   </LinkTo> | ||||||
|  | </EmptyState> | ||||||
							
								
								
									
										39
									
								
								ui/lib/ldap/addon/components/page/configuration.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								ui/lib/ldap/addon/components/page/configuration.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | <TabPageHeader @model={{@backendModel}} @breadcrumbs={{@breadcrumbs}}> | ||||||
|  |   <:toolbarActions> | ||||||
|  |     {{#if @configModel}} | ||||||
|  |       <ConfirmAction | ||||||
|  |         @buttonClasses="toolbar-link" | ||||||
|  |         @onConfirmAction={{perform this.rotateRoot}} | ||||||
|  |         @confirmTitle="Rotate root?" | ||||||
|  |         @confirmMessage="After rotation, Vault will generate a new root password in your directory server." | ||||||
|  |         @confirmButtonText="Rotate" | ||||||
|  |         @disabled={{this.rotateRoot.isRunning}} | ||||||
|  |         data-test-toolbar-rotate-action | ||||||
|  |       > | ||||||
|  |         Rotate root | ||||||
|  |       </ConfirmAction> | ||||||
|  |     {{/if}} | ||||||
|  |     <ToolbarLink @route="configure" data-test-toolbar-config-action> | ||||||
|  |       {{if @configModel "Edit configuration" "Configure LDAP"}} | ||||||
|  |     </ToolbarLink> | ||||||
|  |   </:toolbarActions> | ||||||
|  | </TabPageHeader> | ||||||
|  |  | ||||||
|  | {{#if @configModel}} | ||||||
|  |   {{#each this.defaultFields as |field|}} | ||||||
|  |     <InfoTableRow @label={{field.label}} @value={{field.value}} @formatTtl={{field.formatTtl}} @alwaysRender={{true}} /> | ||||||
|  |   {{/each}} | ||||||
|  |  | ||||||
|  |   <h2 class="title is-4 has-top-margin-xl">TLS Connection</h2> | ||||||
|  |   <hr class="is-marginless" /> | ||||||
|  |  | ||||||
|  |   {{#each this.connectionFields as |field|}} | ||||||
|  |     <InfoTableRow @label={{field.label}} @value={{field.value}} @alwaysRender={{true}} /> | ||||||
|  |   {{/each}} | ||||||
|  | {{else if @configError}} | ||||||
|  |   <Page::Error @error={{@configError}} /> | ||||||
|  | {{else}} | ||||||
|  |   <ConfigCta /> | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | <SecretsEngineMountConfig @model={{@backendModel}} class="has-top-margin-xl has-bottom-margin-xl" data-test-mount-config /> | ||||||
							
								
								
									
										82
									
								
								ui/lib/ldap/addon/components/page/configuration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								ui/lib/ldap/addon/components/page/configuration.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { task } from 'ember-concurrency'; | ||||||
|  | import { waitFor } from '@ember/test-waiters'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | import type LdapConfigModel from 'vault/models/ldap/config'; | ||||||
|  | import type SecretEngineModel from 'vault/models/secret-engine'; | ||||||
|  | import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports | ||||||
|  | import type { Breadcrumb } from 'vault/vault/app-types'; | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   configModel: LdapConfigModel; | ||||||
|  |   configError: AdapterError; | ||||||
|  |   backendModel: SecretEngineModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface Field { | ||||||
|  |   label: string; | ||||||
|  |   value: any; // eslint-disable-line @typescript-eslint/no-explicit-any | ||||||
|  |   formatTtl?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapConfigurationPageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |  | ||||||
|  |   get defaultFields(): Array<Field> { | ||||||
|  |     const model = this.args.configModel; | ||||||
|  |     const keys = [ | ||||||
|  |       'binddn', | ||||||
|  |       'url', | ||||||
|  |       'schema', | ||||||
|  |       'password_policy', | ||||||
|  |       'userdn', | ||||||
|  |       'userattr', | ||||||
|  |       'connection_timeout', | ||||||
|  |       'request_timeout', | ||||||
|  |     ]; | ||||||
|  |     return model.allFields.reduce<Array<Field>>((filtered, field) => { | ||||||
|  |       if (keys.includes(field.name)) { | ||||||
|  |         const label = | ||||||
|  |           { | ||||||
|  |             schema: 'Schema', | ||||||
|  |             password_policy: 'Password Policy', | ||||||
|  |           }[field.name] || field.options.label; | ||||||
|  |         filtered.splice(keys.indexOf(field.name), 0, { | ||||||
|  |           label, | ||||||
|  |           value: model[field.name as keyof typeof model], | ||||||
|  |           formatTtl: field.name.includes('timeout'), | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       return filtered; | ||||||
|  |     }, []); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get connectionFields(): Array<Field> { | ||||||
|  |     const model = this.args.configModel; | ||||||
|  |     const keys = ['certificate', 'starttls', 'insecure_tls', 'client_tls_cert', 'client_tls_key']; | ||||||
|  |     return model.allFields.reduce<Array<Field>>((filtered, field) => { | ||||||
|  |       if (keys.includes(field.name)) { | ||||||
|  |         filtered.splice(keys.indexOf(field.name), 0, { | ||||||
|  |           label: field.options.label, | ||||||
|  |           value: model[field.name as keyof typeof model], | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       return filtered; | ||||||
|  |     }, []); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @task | ||||||
|  |   @waitFor | ||||||
|  |   *rotateRoot() { | ||||||
|  |     try { | ||||||
|  |       yield this.args.configModel.rotateRoot(); | ||||||
|  |       this.flashMessages.success('Root password successfully rotated.'); | ||||||
|  |     } catch (error) { | ||||||
|  |       this.flashMessages.danger(`Error rotating root password \n ${errorMessage(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										113
									
								
								ui/lib/ldap/addon/components/page/configure.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								ui/lib/ldap/addon/components/page/configure.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | <PageHeader as |p|> | ||||||
|  |   <p.top> | ||||||
|  |     <Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} /> | ||||||
|  |   </p.top> | ||||||
|  |   <p.levelLeft> | ||||||
|  |     <h1 class="title is-3">Configure LDAP</h1> | ||||||
|  |   </p.levelLeft> | ||||||
|  | </PageHeader> | ||||||
|  |  | ||||||
|  | <hr class="is-marginless has-background-gray-200" /> | ||||||
|  |  | ||||||
|  | <form class="has-top-margin-l" {{on "submit" (perform this.save)}}> | ||||||
|  |   <Hds::Form::RadioCard::Group @name="schema options" as |RadioGroup|> | ||||||
|  |     {{#each this.schemaOptions as |option|}} | ||||||
|  |       <RadioGroup.RadioCard | ||||||
|  |         @checked={{eq option.value @model.schema}} | ||||||
|  |         {{on "change" (fn (mut @model.schema) option.value)}} | ||||||
|  |         data-test-radio-card={{option.title}} | ||||||
|  |         as |Card| | ||||||
|  |       > | ||||||
|  |         <Card.Icon @name={{option.icon}} /> | ||||||
|  |         <Card.Label>{{option.title}}</Card.Label> | ||||||
|  |         <Card.Description>{{option.description}}</Card.Description> | ||||||
|  |       </RadioGroup.RadioCard> | ||||||
|  |     {{/each}} | ||||||
|  |   </Hds::Form::RadioCard::Group> | ||||||
|  |  | ||||||
|  |   <div class="has-top-margin-xl"> | ||||||
|  |     <MessageError @errorMessage={{this.error}} /> | ||||||
|  |  | ||||||
|  |     <h2 class="title is-4">Schema Options</h2> | ||||||
|  |     <hr class="has-background-gray-200" /> | ||||||
|  |  | ||||||
|  |     {{#if @model.schema}} | ||||||
|  |       <div class="has-top-margin-l"> | ||||||
|  |         <FormFieldGroups @model={{@model}} @groupName="formFieldGroups" @modelValidations={{this.modelValidations}} /> | ||||||
|  |       </div> | ||||||
|  |     {{else}} | ||||||
|  |       <EmptyState | ||||||
|  |         class="is-shadowless has-top-margin-l" | ||||||
|  |         @title="Choose an option" | ||||||
|  |         @message="Pick an option above to see available configuration options" | ||||||
|  |       /> | ||||||
|  |     {{/if}} | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <hr class="has-background-gray-200 has-top-margin-l" /> | ||||||
|  |  | ||||||
|  |   <div class="has-top-margin-l has-bottom-margin-l is-flex"> | ||||||
|  |     <button | ||||||
|  |       data-test-config-save | ||||||
|  |       class="button is-primary" | ||||||
|  |       type="submit" | ||||||
|  |       disabled={{or this.save.isRunning (not @model.schema)}} | ||||||
|  |       {{on "click" (perform this.save)}} | ||||||
|  |     > | ||||||
|  |       Save | ||||||
|  |     </button> | ||||||
|  |     <button | ||||||
|  |       data-test-config-cancel | ||||||
|  |       class="button has-left-margin-xs" | ||||||
|  |       type="button" | ||||||
|  |       disabled={{or this.save.isRunning this.fetchInferred.isRunning}} | ||||||
|  |       {{on "click" this.cancel}} | ||||||
|  |     > | ||||||
|  |       Back | ||||||
|  |     </button> | ||||||
|  |     {{#if this.invalidFormMessage}} | ||||||
|  |       <AlertInline | ||||||
|  |         @type="danger" | ||||||
|  |         @paddingTop={{true}} | ||||||
|  |         @message={{this.invalidFormMessage}} | ||||||
|  |         @mimicRefresh={{true}} | ||||||
|  |         data-test-invalid-form-message | ||||||
|  |       /> | ||||||
|  |     {{/if}} | ||||||
|  |   </div> | ||||||
|  | </form> | ||||||
|  |  | ||||||
|  | {{#if this.showRotatePrompt}} | ||||||
|  |   <Modal | ||||||
|  |     @title="Rotate your root password?" | ||||||
|  |     @type="info" | ||||||
|  |     @isActive={{this.showRotatePrompt}} | ||||||
|  |     @showCloseButton={{true}} | ||||||
|  |     @onClose={{fn (mut this.showRotatePrompt) false}} | ||||||
|  |   > | ||||||
|  |     <section class="modal-card-body"> | ||||||
|  |       <p> | ||||||
|  |         It’s best practice to rotate the administrator (root) password immediately after the initial configuration of the | ||||||
|  |         LDAP engine. The rotation will update the password both in Vault and your directory server. Once rotated, | ||||||
|  |         <span class="has-text-weight-semibold">only Vault knows the new root password.</span> | ||||||
|  |       </p> | ||||||
|  |       <br /> | ||||||
|  |       <p> | ||||||
|  |         Would you like to rotate your new credentials? You can also do this later. | ||||||
|  |       </p> | ||||||
|  |     </section> | ||||||
|  |     <footer class="modal-card-foot modal-card-foot-outlined"> | ||||||
|  |       <button | ||||||
|  |         data-test-save-with-rotate | ||||||
|  |         type="button" | ||||||
|  |         class="button is-primary" | ||||||
|  |         {{on "click" (fn (perform this.save) null true)}} | ||||||
|  |       > | ||||||
|  |         Save and rotate | ||||||
|  |       </button> | ||||||
|  |       <button data-test-save-without-rotate type="button" class="button" {{on "click" (fn (perform this.save) null false)}}> | ||||||
|  |         Save without rotating | ||||||
|  |       </button> | ||||||
|  |     </footer> | ||||||
|  |   </Modal> | ||||||
|  | {{/if}} | ||||||
							
								
								
									
										113
									
								
								ui/lib/ldap/addon/components/page/configure.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								ui/lib/ldap/addon/components/page/configure.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { task } from 'ember-concurrency'; | ||||||
|  | import { waitFor } from '@ember/test-waiters'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | import type LdapConfigModel from 'vault/models/ldap/config'; | ||||||
|  | import { Breadcrumb, ValidationMap } from 'vault/vault/app-types'; | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   model: LdapConfigModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  | interface SchemaOption { | ||||||
|  |   title: string; | ||||||
|  |   icon: string; | ||||||
|  |   description: string; | ||||||
|  |   value: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapConfigurePageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   @tracked showRotatePrompt = false; | ||||||
|  |   @tracked modelValidations: ValidationMap | null = null; | ||||||
|  |   @tracked invalidFormMessage = ''; | ||||||
|  |   @tracked error = ''; | ||||||
|  |  | ||||||
|  |   get schemaOptions(): Array<SchemaOption> { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         title: 'OpenLDAP', | ||||||
|  |         icon: 'folder', | ||||||
|  |         description: | ||||||
|  |           'OpenLDAP is one of the most popular open source directory service developed by the OpenLDAP Project.', | ||||||
|  |         value: 'openldap', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'AD', | ||||||
|  |         icon: 'microsoft', | ||||||
|  |         description: | ||||||
|  |           'Active Directory is a directory service developed by Microsoft for Windows domain networks.', | ||||||
|  |         value: 'ad', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'RACF', | ||||||
|  |         icon: 'users', | ||||||
|  |         description: | ||||||
|  |           "For managing IBM's Resource Access Control Facility (RACF) security system, the generated passwords must be 8 characters or less.", | ||||||
|  |         value: 'racf', | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   leave(route: string) { | ||||||
|  |     this.router.transitionTo(`vault.cluster.secrets.backend.ldap.${route}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   validate() { | ||||||
|  |     const { isValid, state, invalidFormMessage } = this.args.model.validate(); | ||||||
|  |     this.modelValidations = isValid ? null : state; | ||||||
|  |     this.invalidFormMessage = isValid ? '' : invalidFormMessage; | ||||||
|  |     return isValid; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async rotateRoot() { | ||||||
|  |     try { | ||||||
|  |       await this.args.model.rotateRoot(); | ||||||
|  |     } catch (error) { | ||||||
|  |       // since config save was successful at this point we only want to show the error in a flash message | ||||||
|  |       this.flashMessages.danger(`Error rotating root password \n ${errorMessage(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @task | ||||||
|  |   @waitFor | ||||||
|  |   *save(event: Event | null, rotate: boolean) { | ||||||
|  |     if (event) { | ||||||
|  |       event.preventDefault(); | ||||||
|  |     } | ||||||
|  |     const isValid = this.validate(); | ||||||
|  |     // show rotate creds prompt for new models when form state is valid | ||||||
|  |     this.showRotatePrompt = isValid && this.args.model.isNew && !this.showRotatePrompt; | ||||||
|  |  | ||||||
|  |     if (isValid && !this.showRotatePrompt) { | ||||||
|  |       try { | ||||||
|  |         yield this.args.model.save(); | ||||||
|  |         // if save was triggered from confirm action in rotate password prompt we need to make an additional request | ||||||
|  |         if (rotate) { | ||||||
|  |           yield this.rotateRoot(); | ||||||
|  |         } | ||||||
|  |         this.flashMessages.success('Successfully configured LDAP engine'); | ||||||
|  |         this.leave('configuration'); | ||||||
|  |       } catch (error) { | ||||||
|  |         this.error = errorMessage(error, 'Error saving configuration. Please try again or contact support.'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   cancel() { | ||||||
|  |     const { model } = this.args; | ||||||
|  |     const transitionRoute = model.isNew ? 'overview' : 'configuration'; | ||||||
|  |     const cleanupMethod = model.isNew ? 'unloadRecord' : 'rollbackAttributes'; | ||||||
|  |     model[cleanupMethod](); | ||||||
|  |     this.leave(transitionRoute); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										91
									
								
								ui/lib/ldap/addon/components/page/libraries.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								ui/lib/ldap/addon/components/page/libraries.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | |||||||
|  | <TabPageHeader @model={{@backendModel}} @breadcrumbs={{@breadcrumbs}}> | ||||||
|  |   <:toolbarFilters> | ||||||
|  |     {{#if (and (not @promptConfig) @libraries)}} | ||||||
|  |       <FilterInput @placeholder="Filter libraries" @onInput={{fn (mut this.filterValue)}} /> | ||||||
|  |     {{/if}} | ||||||
|  |   </:toolbarFilters> | ||||||
|  |   <:toolbarActions> | ||||||
|  |     {{#if @promptConfig}} | ||||||
|  |       <ToolbarLink @route="configure" data-test-toolbar-action="config"> | ||||||
|  |         Configure LDAP | ||||||
|  |       </ToolbarLink> | ||||||
|  |     {{else}} | ||||||
|  |       <ToolbarLink @route="libraries.create" @type="add" data-test-toolbar-action="library"> | ||||||
|  |         Create library | ||||||
|  |       </ToolbarLink> | ||||||
|  |     {{/if}} | ||||||
|  |   </:toolbarActions> | ||||||
|  | </TabPageHeader> | ||||||
|  |  | ||||||
|  | {{#if @promptConfig}} | ||||||
|  |   <ConfigCta /> | ||||||
|  | {{else if (not this.filteredLibraries)}} | ||||||
|  |   {{#if this.filterValue}} | ||||||
|  |     <EmptyState @title="There are no libraries matching "{{this.filterValue}}"" /> | ||||||
|  |   {{else}} | ||||||
|  |     <EmptyState | ||||||
|  |       data-test-config-cta | ||||||
|  |       @title="No libraries created yet" | ||||||
|  |       @message="Use libraries to manage a set of highly privileged accounts that can be shared among a team." | ||||||
|  |     > | ||||||
|  |       <LinkTo class="has-top-margin-xs" @route="libraries.create"> | ||||||
|  |         Create library | ||||||
|  |       </LinkTo> | ||||||
|  |     </EmptyState> | ||||||
|  |   {{/if}} | ||||||
|  | {{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|> | ||||||
|  |         <Item.content> | ||||||
|  |           <Icon @name="folder" /> | ||||||
|  |           <span data-test-library={{library.name}}>{{library.name}}</span> | ||||||
|  |         </Item.content> | ||||||
|  |         <Item.menu as |Menu|> | ||||||
|  |           {{#if library.libraryPath.isLoading}} | ||||||
|  |             <li class="action"> | ||||||
|  |               <button disabled type="button" class="link button is-loading is-transparent"> | ||||||
|  |                 loading | ||||||
|  |               </button> | ||||||
|  |             </li> | ||||||
|  |           {{else}} | ||||||
|  |             <li class="action"> | ||||||
|  |               <LinkTo | ||||||
|  |                 class="has-text-black has-text-weight-semibold" | ||||||
|  |                 data-test-edit | ||||||
|  |                 @route="libraries.library.edit" | ||||||
|  |                 @model={{library}} | ||||||
|  |                 @disabled={{not library.canEdit}} | ||||||
|  |               > | ||||||
|  |                 Edit | ||||||
|  |               </LinkTo> | ||||||
|  |             </li> | ||||||
|  |             <li class="action"> | ||||||
|  |               <LinkTo | ||||||
|  |                 class="has-text-black has-text-weight-semibold" | ||||||
|  |                 data-test-details | ||||||
|  |                 @route="libraries.library.details" | ||||||
|  |                 @model={{library}} | ||||||
|  |                 @disabled={{not library.canRead}} | ||||||
|  |               > | ||||||
|  |                 Details | ||||||
|  |               </LinkTo> | ||||||
|  |             </li> | ||||||
|  |             {{#if library.canDelete}} | ||||||
|  |               <li class="action"> | ||||||
|  |                 <Menu.Message | ||||||
|  |                   data-test-delete | ||||||
|  |                   @id={{library.id}} | ||||||
|  |                   @triggerText="Delete" | ||||||
|  |                   @title="Are you sure?" | ||||||
|  |                   @message="This library and associated accounts will be permanently deleted. You will not be able to recover it." | ||||||
|  |                   @onConfirm={{fn this.onDelete library}} | ||||||
|  |                 /> | ||||||
|  |               </li> | ||||||
|  |             {{/if}} | ||||||
|  |           {{/if}} | ||||||
|  |         </Item.menu> | ||||||
|  |       </ListItem> | ||||||
|  |     {{/each}} | ||||||
|  |   </div> | ||||||
|  | {{/if}} | ||||||
							
								
								
									
										53
									
								
								ui/lib/ldap/addon/components/page/libraries.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								ui/lib/ldap/addon/components/page/libraries.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import { getOwner } from '@ember/application'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | 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'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   libraries: Array<LdapLibraryModel>; | ||||||
|  |   promptConfig: boolean; | ||||||
|  |   backendModel: SecretEngineModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibrariesPageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |  | ||||||
|  |   @tracked filterValue = ''; | ||||||
|  |  | ||||||
|  |   get mountPoint(): string { | ||||||
|  |     const owner = getOwner(this) as EngineOwner; | ||||||
|  |     return owner.mountPoint; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get filteredLibraries() { | ||||||
|  |     const { libraries } = this.args; | ||||||
|  |     return this.filterValue | ||||||
|  |       ? libraries.filter((library) => library.name.toLowerCase().includes(this.filterValue.toLowerCase())) | ||||||
|  |       : libraries; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   async onDelete(model: LdapLibraryModel) { | ||||||
|  |     try { | ||||||
|  |       const message = `Successfully deleted library ${model.name}.`; | ||||||
|  |       await model.destroyRecord(); | ||||||
|  |       this.args.libraries.removeObject(model); | ||||||
|  |       this.flashMessages.success(message); | ||||||
|  |     } catch (error) { | ||||||
|  |       this.flashMessages.danger(`Error deleting library \n ${errorMessage(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								ui/lib/ldap/addon/components/page/library/check-out.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								ui/lib/ldap/addon/components/page/library/check-out.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | <PageHeader as |p|> | ||||||
|  |   <p.top> | ||||||
|  |     <Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} /> | ||||||
|  |   </p.top> | ||||||
|  |   <p.levelLeft> | ||||||
|  |     <h1 class="title is-3" data-test-header-title> | ||||||
|  |       Check-out | ||||||
|  |     </h1> | ||||||
|  |   </p.levelLeft> | ||||||
|  | </PageHeader> | ||||||
|  |  | ||||||
|  | <hr class="is-marginless has-background-gray-200" /> | ||||||
|  |  | ||||||
|  | <Hds::Alert @type="inline" @color="warning" class="has-top-margin-m" as |Alert|> | ||||||
|  |   <Alert.Title>Warning</Alert.Title> | ||||||
|  |   <Alert.Description data-test-alert-description> | ||||||
|  |     You won’t be able to access these credentials later, so please copy them now. | ||||||
|  |   </Alert.Description> | ||||||
|  | </Hds::Alert> | ||||||
|  |  | ||||||
|  | <div class="has-top-margin-m"> | ||||||
|  |   <InfoTableRow @label="Account name" @value={{@credentials.account}} /> | ||||||
|  |   <InfoTableRow @label="Password"> | ||||||
|  |     <MaskedInput @value={{@credentials.password}} @displayOnly={{true}} @allowCopy={{true}} /> | ||||||
|  |   </InfoTableRow> | ||||||
|  |   <InfoTableRow @label="Lease ID" @value={{@credentials.lease_id}} /> | ||||||
|  |   <InfoTableRow @label="Lease duration" @value={{@credentials.lease_duration}} @formatTtl={{true}} /> | ||||||
|  |   <InfoTableRow @label="Lease renewable"> | ||||||
|  |     <div class="is-flex-v-centered"> | ||||||
|  |       <Icon | ||||||
|  |         @name={{if @credentials.renewable "check-circle" "x-circle"}} | ||||||
|  |         class="is-marginless {{if @credentials.renewable 'has-text-success' 'has-text-danger'}}" | ||||||
|  |       /> | ||||||
|  |       <span class="has-left-margin-xs"> | ||||||
|  |         {{if @credentials.renewable "True" "False"}} | ||||||
|  |       </span> | ||||||
|  |     </div> | ||||||
|  |   </InfoTableRow> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <div class="has-top-margin-xl has-bottom-margin-l"> | ||||||
|  |   <button | ||||||
|  |     data-test-done | ||||||
|  |     class="button is-primary" | ||||||
|  |     type="button" | ||||||
|  |     {{on "click" (transition-to "vault.cluster.secrets.backend.ldap.libraries.library.details.accounts")}} | ||||||
|  |   > | ||||||
|  |     Done | ||||||
|  |   </button> | ||||||
|  | </div> | ||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | <PageHeader as |p|> | ||||||
|  |   <p.top> | ||||||
|  |     <Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} /> | ||||||
|  |   </p.top> | ||||||
|  |   <p.levelLeft> | ||||||
|  |     <h1 class="title is-3"> | ||||||
|  |       {{if @model.isNew "Create Library" "Edit Library"}} | ||||||
|  |     </h1> | ||||||
|  |   </p.levelLeft> | ||||||
|  | </PageHeader> | ||||||
|  |  | ||||||
|  | <hr class="is-marginless has-background-gray-200" /> | ||||||
|  |  | ||||||
|  | <form {{on "submit" (perform this.save)}} class="has-top-margin-m"> | ||||||
|  |   <MessageError @errorMessage={{this.error}} /> | ||||||
|  |  | ||||||
|  |   {{#each @model.formFields as |field|}} | ||||||
|  |     <FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} /> | ||||||
|  |   {{/each}} | ||||||
|  |  | ||||||
|  |   <hr class="has-background-gray-200 has-top-margin-l" /> | ||||||
|  |  | ||||||
|  |   <div class="has-top-margin-l has-bottom-margin-l is-flex"> | ||||||
|  |     <button data-test-save class="button is-primary" type="submit" disabled={{this.save.isRunning}}> | ||||||
|  |       {{if @model.isNew "Create library" "Save"}} | ||||||
|  |     </button> | ||||||
|  |     <button | ||||||
|  |       data-test-cancel | ||||||
|  |       class="button has-left-margin-xs" | ||||||
|  |       type="button" | ||||||
|  |       disabled={{this.save.isRunning}} | ||||||
|  |       {{on "click" this.cancel}} | ||||||
|  |     > | ||||||
|  |       Cancel | ||||||
|  |     </button> | ||||||
|  |     {{#if this.invalidFormMessage}} | ||||||
|  |       <AlertInline | ||||||
|  |         @type="danger" | ||||||
|  |         @paddingTop={{true}} | ||||||
|  |         @message={{this.invalidFormMessage}} | ||||||
|  |         @mimicRefresh={{true}} | ||||||
|  |         data-test-invalid-form-message | ||||||
|  |       /> | ||||||
|  |     {{/if}} | ||||||
|  |   </div> | ||||||
|  | </form> | ||||||
							
								
								
									
										55
									
								
								ui/lib/ldap/addon/components/page/library/create-and-edit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								ui/lib/ldap/addon/components/page/library/create-and-edit.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { task } from 'ember-concurrency'; | ||||||
|  | import { waitFor } from '@ember/test-waiters'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | import type LdapLibraryModel from 'vault/models/ldap/library'; | ||||||
|  | import { Breadcrumb, ValidationMap } from 'vault/vault/app-types'; | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   model: LdapLibraryModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapCreateAndEditLibraryPageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   @tracked modelValidations: ValidationMap | null = null; | ||||||
|  |   @tracked invalidFormMessage = ''; | ||||||
|  |   @tracked error = ''; | ||||||
|  |  | ||||||
|  |   @task | ||||||
|  |   @waitFor | ||||||
|  |   *save(event: Event) { | ||||||
|  |     event.preventDefault(); | ||||||
|  |  | ||||||
|  |     const { model } = this.args; | ||||||
|  |     const { isValid, state, invalidFormMessage } = model.validate(); | ||||||
|  |  | ||||||
|  |     this.modelValidations = isValid ? null : state; | ||||||
|  |     this.invalidFormMessage = isValid ? '' : invalidFormMessage; | ||||||
|  |  | ||||||
|  |     if (isValid) { | ||||||
|  |       try { | ||||||
|  |         const action = model.isNew ? 'created' : 'updated'; | ||||||
|  |         yield model.save(); | ||||||
|  |         this.flashMessages.success(`Successfully ${action} the library ${model.name}.`); | ||||||
|  |         this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.details', model.name); | ||||||
|  |       } catch (error) { | ||||||
|  |         this.error = errorMessage(error, 'Error saving library. Please try again or contact support.'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   cancel() { | ||||||
|  |     this.args.model.rollbackAttributes(); | ||||||
|  |     this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries'); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								ui/lib/ldap/addon/components/page/library/details.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								ui/lib/ldap/addon/components/page/library/details.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | <PageHeader as |p|> | ||||||
|  |   <p.top> | ||||||
|  |     <Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} /> | ||||||
|  |   </p.top> | ||||||
|  |   <p.levelLeft> | ||||||
|  |     <h1 class="title is-3" data-test-header-title> | ||||||
|  |       {{@model.name}} | ||||||
|  |     </h1> | ||||||
|  |   </p.levelLeft> | ||||||
|  | </PageHeader> | ||||||
|  |  | ||||||
|  | <div class="tabs-container box is-bottomless is-marginless is-paddingless"> | ||||||
|  |   <nav class="tabs" aria-label="ldap tabs"> | ||||||
|  |     <ul> | ||||||
|  |       <LinkTo @route="libraries.library.details.accounts" data-test-tab="accounts">Accounts</LinkTo> | ||||||
|  |       <LinkTo @route="libraries.library.details.configuration" data-test-tab="config">Configuration</LinkTo> | ||||||
|  |     </ul> | ||||||
|  |   </nav> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <Toolbar> | ||||||
|  |   <ToolbarActions> | ||||||
|  |     {{#if @model.canDelete}} | ||||||
|  |       <ConfirmAction @buttonClasses="toolbar-link" @onConfirmAction={{this.delete}} data-test-delete> | ||||||
|  |         Delete library | ||||||
|  |       </ConfirmAction> | ||||||
|  |       {{#if @model.canEdit}} | ||||||
|  |         <div class="toolbar-separator"></div> | ||||||
|  |       {{/if}} | ||||||
|  |     {{/if}} | ||||||
|  |     {{#if @model.canEdit}} | ||||||
|  |       <ToolbarLink @route="libraries.library.edit" data-test-edit> | ||||||
|  |         Edit library | ||||||
|  |       </ToolbarLink> | ||||||
|  |     {{/if}} | ||||||
|  |   </ToolbarActions> | ||||||
|  | </Toolbar> | ||||||
							
								
								
									
										31
									
								
								ui/lib/ldap/addon/components/page/library/details.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ui/lib/ldap/addon/components/page/library/details.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | import type LdapLibraryModel from 'vault/models/ldap/library'; | ||||||
|  | import { Breadcrumb } from 'vault/vault/app-types'; | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   model: LdapLibraryModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibraryDetailsPageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   async delete() { | ||||||
|  |     try { | ||||||
|  |       await this.args.model.destroyRecord(); | ||||||
|  |       this.flashMessages.success('Library deleted successfully.'); | ||||||
|  |       this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries'); | ||||||
|  |     } catch (error) { | ||||||
|  |       const message = errorMessage(error, 'Unable to delete library. Please try again or contact support.'); | ||||||
|  |       this.flashMessages.danger(message); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,90 @@ | |||||||
|  | <div class="has-top-margin-l is-flex-align-start"> | ||||||
|  |   <Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l is-flex-half"> | ||||||
|  |     <div class="is-flex-between"> | ||||||
|  |       <h3 class="is-size-5 has-text-weight-semibold">All accounts</h3> | ||||||
|  |       {{#if @library.canCheckOut}} | ||||||
|  |         <button | ||||||
|  |           type="button" | ||||||
|  |           class="button is-link" | ||||||
|  |           data-test-check-out | ||||||
|  |           {{on "click" (fn (mut this.showCheckOutPrompt) true)}} | ||||||
|  |         > | ||||||
|  |           Check-out | ||||||
|  |         </button> | ||||||
|  |       {{/if}} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <p class="has-text-grey is-size-8">The accounts within this library</p> | ||||||
|  |     <hr class="has-background-gray-200" /> | ||||||
|  |  | ||||||
|  |     <Hds::Table @model={{@statuses}} @columns={{array (hash label="Accounts") (hash label="Status")}}> | ||||||
|  |       <:body as |Body|> | ||||||
|  |         <Body.Tr> | ||||||
|  |           <Body.Td data-test-account-name={{Body.data.account}}>{{Body.data.account}}</Body.Td> | ||||||
|  |           <Body.Td> | ||||||
|  |             <Hds::Badge | ||||||
|  |               @text={{if Body.data.available "Available" "Unavailable"}} | ||||||
|  |               @color={{if Body.data.available "success" "neutral"}} | ||||||
|  |               data-test-account-status={{Body.data.account}} | ||||||
|  |             /> | ||||||
|  |           </Body.Td> | ||||||
|  |         </Body.Tr> | ||||||
|  |       </:body> | ||||||
|  |     </Hds::Table> | ||||||
|  |   </Hds::Card::Container> | ||||||
|  |  | ||||||
|  |   <div class="has-left-margin-l is-flex-half"> | ||||||
|  |     <AccountsCheckedOut @libraries={{array @library}} @statuses={{@statuses}} data-test-checked-out-card /> | ||||||
|  |  | ||||||
|  |     <OverviewCard | ||||||
|  |       @cardTitle="To renew a checked-out account" | ||||||
|  |       @subText="Use the CLI command below:" | ||||||
|  |       class="has-padding-l has-top-margin-l" | ||||||
|  |     > | ||||||
|  |       <div class="has-padding-s has-background-gray-900 border-radius-4 is-flex-between has-top-margin-s"> | ||||||
|  |         <code class="has-text-white is-size-7" data-test-cli-command>{{this.cliCommand}}</code> | ||||||
|  |         <CopyButton | ||||||
|  |           class="button is-compact is-transparent has-text-grey-light" | ||||||
|  |           data-test-cli-command-copy | ||||||
|  |           @clipboardText={{this.cliCommand}} | ||||||
|  |           @buttonType="button" | ||||||
|  |           @success={{action (set-flash-message "Renew command copied!")}} | ||||||
|  |         > | ||||||
|  |           Copy | ||||||
|  |           <Icon @name="clipboard-copy" aria-label="Copy" /> | ||||||
|  |         </CopyButton> | ||||||
|  |       </div> | ||||||
|  |     </OverviewCard> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {{#if this.showCheckOutPrompt}} | ||||||
|  |   <Modal | ||||||
|  |     @title="Account Check-out" | ||||||
|  |     @isActive={{this.showCheckOutPrompt}} | ||||||
|  |     @showCloseButton={{true}} | ||||||
|  |     @onClose={{fn (mut this.showCheckOutPrompt) false}} | ||||||
|  |   > | ||||||
|  |     <section class="modal-card-body"> | ||||||
|  |       <p> | ||||||
|  |         Current generated credential’s time-to-live is set at | ||||||
|  |         {{format-duration @library.ttl}}. You can set a different limit if you’d like: | ||||||
|  |       </p> | ||||||
|  |       <br /> | ||||||
|  |       <TtlPicker @label="TTL" @hideToggle={{true}} @initialValue={{@library.ttl}} @onChange={{this.setTtl}} /> | ||||||
|  |     </section> | ||||||
|  |     <footer class="modal-card-foot modal-card-foot-outlined"> | ||||||
|  |       <button data-test-check-out="save" type="button" class="button is-primary" {{on "click" this.checkOut}}> | ||||||
|  |         Check-out | ||||||
|  |       </button> | ||||||
|  |       <button | ||||||
|  |         data-test-check-out="cancel" | ||||||
|  |         type="button" | ||||||
|  |         class="button" | ||||||
|  |         {{on "click" (fn (mut this.showCheckOutPrompt) false)}} | ||||||
|  |       > | ||||||
|  |         Cancel | ||||||
|  |       </button> | ||||||
|  |     </footer> | ||||||
|  |   </Modal> | ||||||
|  | {{/if}} | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  |  | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  | import type LdapLibraryModel from 'vault/models/ldap/library'; | ||||||
|  | import type { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library'; | ||||||
|  | import { TtlEvent } from 'vault/vault/app-types'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   library: LdapLibraryModel; | ||||||
|  |   statuses: Array<LdapLibraryAccountStatus>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibraryDetailsAccountsPageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   @tracked showCheckOutPrompt = false; | ||||||
|  |   @tracked checkOutTtl: string | null = null; | ||||||
|  |  | ||||||
|  |   get cliCommand() { | ||||||
|  |     return `vault lease renew ad/library/${this.args.library.name}/check-out/:lease_id`; | ||||||
|  |   } | ||||||
|  |   @action | ||||||
|  |   setTtl(data: TtlEvent) { | ||||||
|  |     this.checkOutTtl = data.timeString; | ||||||
|  |   } | ||||||
|  |   @action | ||||||
|  |   checkOut() { | ||||||
|  |     this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.check-out', { | ||||||
|  |       queryParams: { ttl: this.checkOutTtl }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | {{#each @model.displayFields as |field|}} | ||||||
|  |   {{#let (get @model field.name) as |value|}} | ||||||
|  |     {{#if (eq field.name "disable_check_in_enforcement")}} | ||||||
|  |       <InfoTableRow @label={{field.options.label}}> | ||||||
|  |         <Icon | ||||||
|  |           class="is-flex-v-centered {{if (eq value 'Enabled') 'icon-true' 'icon-false'}}" | ||||||
|  |           @name={{if (eq value "Enabled") "check-circle" "x-square"}} | ||||||
|  |           data-test-check-in-icon | ||||||
|  |         /> | ||||||
|  |         <span>{{value}}</span> | ||||||
|  |       </InfoTableRow> | ||||||
|  |     {{else}} | ||||||
|  |       <InfoTableRow | ||||||
|  |         data-test-filtered-field | ||||||
|  |         @label={{or field.options.detailsLabel field.options.label}} | ||||||
|  |         @value={{value}} | ||||||
|  |         @formatTtl={{eq field.options.editType "ttl"}} | ||||||
|  |       /> | ||||||
|  |     {{/if}} | ||||||
|  |   {{/let}} | ||||||
|  | {{/each}} | ||||||
							
								
								
									
										69
									
								
								ui/lib/ldap/addon/components/page/overview.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								ui/lib/ldap/addon/components/page/overview.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | <TabPageHeader @model={{@backendModel}} @breadcrumbs={{@breadcrumbs}}> | ||||||
|  |   <:toolbarActions> | ||||||
|  |     {{#if @promptConfig}} | ||||||
|  |       <ToolbarLink @route="configure" data-test-toolbar-action="config"> | ||||||
|  |         Configure LDAP | ||||||
|  |       </ToolbarLink> | ||||||
|  |     {{/if}} | ||||||
|  |   </:toolbarActions> | ||||||
|  | </TabPageHeader> | ||||||
|  |  | ||||||
|  | {{#if @promptConfig}} | ||||||
|  |   <ConfigCta /> | ||||||
|  | {{else}} | ||||||
|  |   <div class="is-grid has-top-margin-l grid-2-columns grid-gap-2"> | ||||||
|  |     <OverviewCard | ||||||
|  |       @cardTitle="Roles" | ||||||
|  |       @subText="The total number of roles that have been set up in this secret engine in order to generate credentials." | ||||||
|  |       @actionText="Create new" | ||||||
|  |       @actionTo="roles.create" | ||||||
|  |     > | ||||||
|  |       <h2 class="title is-2 has-font-weight-normal has-top-margin-m" data-test-roles-count> | ||||||
|  |         {{or @roles.length "None"}} | ||||||
|  |       </h2> | ||||||
|  |     </OverviewCard> | ||||||
|  |     <OverviewCard | ||||||
|  |       @cardTitle="Libraries" | ||||||
|  |       @subText="The total number of libraries that have been created for service account management." | ||||||
|  |       @actionText="Create new" | ||||||
|  |       @actionTo="libraries.create" | ||||||
|  |     > | ||||||
|  |       <h2 class="title is-2 has-font-weight-normal has-top-margin-m" data-test-libraries-count> | ||||||
|  |         {{or @libraries.length "None"}} | ||||||
|  |       </h2> | ||||||
|  |     </OverviewCard> | ||||||
|  |   </div> | ||||||
|  |   <div class="is-flex-align-start has-top-margin-l"> | ||||||
|  |     <AccountsCheckedOut | ||||||
|  |       @libraries={{@libraries}} | ||||||
|  |       @statuses={{@librariesStatus}} | ||||||
|  |       @showLibraryColumn={{true}} | ||||||
|  |       class="is-flex-half" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <div class="has-left-margin-l is-flex-half"> | ||||||
|  |       <OverviewCard @cardTitle="Generate credentials" @subText="Quickly generate credentials by typing the role name."> | ||||||
|  |         <div class="has-top-margin-m is-flex"> | ||||||
|  |           <SearchSelect | ||||||
|  |             class="is-flex-1" | ||||||
|  |             @placeholder="Select a role" | ||||||
|  |             @disallowNewItems={{true}} | ||||||
|  |             @options={{@roles}} | ||||||
|  |             @selectLimit="1" | ||||||
|  |             @fallbackComponent="input-search" | ||||||
|  |             @onChange={{this.selectRole}} | ||||||
|  |           /> | ||||||
|  |           <button | ||||||
|  |             class="button has-left-margin-s" | ||||||
|  |             type="button" | ||||||
|  |             disabled={{not this.selectedRole}} | ||||||
|  |             {{on "click" this.generateCredentials}} | ||||||
|  |             data-test-generate-credential-button | ||||||
|  |           > | ||||||
|  |             Get credentials | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |       </OverviewCard> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | {{/if}} | ||||||
							
								
								
									
										43
									
								
								ui/lib/ldap/addon/components/page/overview.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								ui/lib/ldap/addon/components/page/overview.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  |  | ||||||
|  | import type LdapLibraryModel from 'vault/models/ldap/library'; | ||||||
|  | import type SecretEngineModel from 'vault/models/secret-engine'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  | import type { Breadcrumb } from 'vault/vault/app-types'; | ||||||
|  | import LdapRoleModel from 'vault/models/ldap/role'; | ||||||
|  | import { LdapLibraryAccountStatus } from 'vault/vault/adapters/ldap/library'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   roles: Array<LdapRoleModel>; | ||||||
|  |   libraries: Array<LdapLibraryModel>; | ||||||
|  |   librariesStatus: Array<LdapLibraryAccountStatus>; | ||||||
|  |   promptConfig: boolean; | ||||||
|  |   backendModel: SecretEngineModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibrariesPageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   @tracked selectedRole: LdapRoleModel | undefined; | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   selectRole([roleName]: Array<string>) { | ||||||
|  |     const model = this.args.roles.find((role) => role.name === roleName); | ||||||
|  |     this.selectedRole = model; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   generateCredentials() { | ||||||
|  |     const { type, name } = this.selectedRole as LdapRoleModel; | ||||||
|  |     this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles.role.credentials', type, name); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										70
									
								
								ui/lib/ldap/addon/components/page/role/create-and-edit.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								ui/lib/ldap/addon/components/page/role/create-and-edit.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | <PageHeader as |p|> | ||||||
|  |   <p.top> | ||||||
|  |     <Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} /> | ||||||
|  |   </p.top> | ||||||
|  |   <p.levelLeft> | ||||||
|  |     <h1 class="title is-3"> | ||||||
|  |       {{if @model.isNew "Create Role" "Edit Role"}} | ||||||
|  |     </h1> | ||||||
|  |   </p.levelLeft> | ||||||
|  | </PageHeader> | ||||||
|  |  | ||||||
|  | <hr class="is-marginless has-background-gray-200" /> | ||||||
|  |  | ||||||
|  | <form {{on "submit" (perform this.save)}} class="has-top-margin-m"> | ||||||
|  |   <MessageError @errorMessage={{this.error}} /> | ||||||
|  |  | ||||||
|  |   <label class="is-label"> | ||||||
|  |     Role type | ||||||
|  |   </label> | ||||||
|  |   <Hds::Form::RadioCard::Group @name="role type options" class="has-bottom-margin-m" as |RadioGroup|> | ||||||
|  |     {{#each this.roleTypeOptions as |option|}} | ||||||
|  |       <RadioGroup.RadioCard | ||||||
|  |         @checked={{eq option.value @model.type}} | ||||||
|  |         @disabled={{not @model.isNew}} | ||||||
|  |         {{on "change" (fn (mut @model.type) option.value)}} | ||||||
|  |         data-test-radio-card={{option.value}} | ||||||
|  |         as |Card| | ||||||
|  |       > | ||||||
|  |         <Card.Icon @name={{option.icon}} /> | ||||||
|  |         <Card.Label>{{option.title}}</Card.Label> | ||||||
|  |         <Card.Description>{{option.description}}</Card.Description> | ||||||
|  |       </RadioGroup.RadioCard> | ||||||
|  |     {{/each}} | ||||||
|  |   </Hds::Form::RadioCard::Group> | ||||||
|  |  | ||||||
|  |   {{#each @model.formFields as |field|}} | ||||||
|  |     {{! display section heading ahead of ldif fields }} | ||||||
|  |     {{#if field.options.sectionHeading}} | ||||||
|  |       <hr class="has-background-gray-200" /> | ||||||
|  |       <h2 class="title is-4 has-top-margin-xl">{{field.options.sectionHeading}}</h2> | ||||||
|  |     {{/if}} | ||||||
|  |     <FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} /> | ||||||
|  |   {{/each}} | ||||||
|  |  | ||||||
|  |   <hr class="has-background-gray-200 has-top-margin-l" /> | ||||||
|  |  | ||||||
|  |   <div class="has-top-margin-l has-bottom-margin-l is-flex"> | ||||||
|  |     <button data-test-save class="button is-primary" type="submit" disabled={{this.save.isRunning}}> | ||||||
|  |       {{if @model.isNew "Create role" "Save"}} | ||||||
|  |     </button> | ||||||
|  |     <button | ||||||
|  |       data-test-cancel | ||||||
|  |       class="button has-left-margin-xs" | ||||||
|  |       type="button" | ||||||
|  |       disabled={{this.save.isRunning}} | ||||||
|  |       {{on "click" this.cancel}} | ||||||
|  |     > | ||||||
|  |       Cancel | ||||||
|  |     </button> | ||||||
|  |     {{#if this.invalidFormMessage}} | ||||||
|  |       <AlertInline | ||||||
|  |         @type="danger" | ||||||
|  |         @paddingTop={{true}} | ||||||
|  |         @message={{this.invalidFormMessage}} | ||||||
|  |         @mimicRefresh={{true}} | ||||||
|  |         data-test-invalid-form-message | ||||||
|  |       /> | ||||||
|  |     {{/if}} | ||||||
|  |   </div> | ||||||
|  | </form> | ||||||
							
								
								
									
										82
									
								
								ui/lib/ldap/addon/components/page/role/create-and-edit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								ui/lib/ldap/addon/components/page/role/create-and-edit.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { task } from 'ember-concurrency'; | ||||||
|  | import { waitFor } from '@ember/test-waiters'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | import type LdapRoleModel from 'vault/models/ldap/role'; | ||||||
|  | import { Breadcrumb, ValidationMap } from 'vault/vault/app-types'; | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   model: LdapRoleModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  | interface RoleTypeOption { | ||||||
|  |   title: string; | ||||||
|  |   icon: string; | ||||||
|  |   description: string; | ||||||
|  |   value: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapCreateAndEditRolePageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   @tracked modelValidations: ValidationMap | null = null; | ||||||
|  |   @tracked invalidFormMessage = ''; | ||||||
|  |   @tracked error = ''; | ||||||
|  |  | ||||||
|  |   get roleTypeOptions(): Array<RoleTypeOption> { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         title: 'Static role', | ||||||
|  |         icon: 'user', | ||||||
|  |         description: 'Static roles map to existing users in an LDAP system.', | ||||||
|  |         value: 'static', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Dynamic role', | ||||||
|  |         icon: 'folder-users', | ||||||
|  |         description: 'Dynamic roles allow Vault to create and delete a user in an LDAP system.', | ||||||
|  |         value: 'dynamic', | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @task | ||||||
|  |   @waitFor | ||||||
|  |   *save(event: Event) { | ||||||
|  |     event.preventDefault(); | ||||||
|  |  | ||||||
|  |     const { model } = this.args; | ||||||
|  |     const { isValid, state, invalidFormMessage } = model.validate(); | ||||||
|  |  | ||||||
|  |     this.modelValidations = isValid ? null : state; | ||||||
|  |     this.invalidFormMessage = isValid ? '' : invalidFormMessage; | ||||||
|  |  | ||||||
|  |     if (isValid) { | ||||||
|  |       try { | ||||||
|  |         const action = model.isNew ? 'created' : 'updated'; | ||||||
|  |         yield model.save(); | ||||||
|  |         this.flashMessages.success(`Successfully ${action} the role ${model.name}`); | ||||||
|  |         this.router.transitionTo( | ||||||
|  |           'vault.cluster.secrets.backend.ldap.roles.role.details', | ||||||
|  |           model.type, | ||||||
|  |           model.name | ||||||
|  |         ); | ||||||
|  |       } catch (error) { | ||||||
|  |         this.error = errorMessage(error, 'Error saving role. Please try again or contact support.'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   cancel() { | ||||||
|  |     this.args.model.rollbackAttributes(); | ||||||
|  |     this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles'); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								ui/lib/ldap/addon/components/page/role/credentials.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								ui/lib/ldap/addon/components/page/role/credentials.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | <PageHeader as |p|> | ||||||
|  |   <p.top> | ||||||
|  |     <Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} /> | ||||||
|  |   </p.top> | ||||||
|  |   <p.levelLeft> | ||||||
|  |     <h1 class="title is-3" data-test-header-title> | ||||||
|  |       Credentials | ||||||
|  |     </h1> | ||||||
|  |   </p.levelLeft> | ||||||
|  | </PageHeader> | ||||||
|  |  | ||||||
|  | <hr class="is-marginless has-background-gray-200" /> | ||||||
|  |  | ||||||
|  | {{#if (eq @credentials.type "dynamic")}} | ||||||
|  |   <Hds::Alert @type="inline" @color="warning" class="has-top-margin-m" as |Alert|> | ||||||
|  |     <Alert.Title>Warning</Alert.Title> | ||||||
|  |     <Alert.Description data-test-alert-description> | ||||||
|  |       You won’t be able to access these credentials later, so please copy them now. | ||||||
|  |     </Alert.Description> | ||||||
|  |   </Hds::Alert> | ||||||
|  | {{/if}} | ||||||
|  |  | ||||||
|  | <div class="has-top-margin-m"> | ||||||
|  |   {{#each this.fields as |field|}} | ||||||
|  |     {{#let (get @credentials field.key) as |value|}} | ||||||
|  |       {{#if field.hasBlock}} | ||||||
|  |         <InfoTableRow @label={{field.label}}> | ||||||
|  |           {{#if (eq field.hasBlock "masked")}} | ||||||
|  |             <MaskedInput @value={{value}} @displayOnly={{true}} @allowCopy={{true}} /> | ||||||
|  |           {{else if (eq field.hasBlock "check")}} | ||||||
|  |             <div class="is-flex-v-centered"> | ||||||
|  |               <Icon | ||||||
|  |                 @name={{if value "check-circle" "x-circle"}} | ||||||
|  |                 class="is-marginless {{if value 'has-text-success' 'has-text-danger'}}" | ||||||
|  |               /> | ||||||
|  |               <span class="has-left-margin-xs"> | ||||||
|  |                 {{if value "True" "False"}} | ||||||
|  |               </span> | ||||||
|  |             </div> | ||||||
|  |           {{/if}} | ||||||
|  |         </InfoTableRow> | ||||||
|  |       {{else}} | ||||||
|  |         <InfoTableRow | ||||||
|  |           @label={{field.label}} | ||||||
|  |           @value={{value}} | ||||||
|  |           @formatDate={{field.formatDate}} | ||||||
|  |           @formatTtl={{field.formatTtl}} | ||||||
|  |           @type={{field.type}} | ||||||
|  |         /> | ||||||
|  |       {{/if}} | ||||||
|  |     {{/let}} | ||||||
|  |   {{/each}} | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <div class="has-top-margin-xl has-bottom-margin-l"> | ||||||
|  |   <button | ||||||
|  |     data-test-done | ||||||
|  |     class="button is-primary" | ||||||
|  |     type="button" | ||||||
|  |     {{on "click" (transition-to "vault.cluster.secrets.backend.ldap.roles.role.details")}} | ||||||
|  |   > | ||||||
|  |     Done | ||||||
|  |   </button> | ||||||
|  | </div> | ||||||
							
								
								
									
										33
									
								
								ui/lib/ldap/addon/components/page/role/credentials.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								ui/lib/ldap/addon/components/page/role/credentials.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  |  | ||||||
|  | import type { | ||||||
|  |   LdapStaticRoleCredentials, | ||||||
|  |   LdapDynamicRoleCredentials, | ||||||
|  | } from 'ldap/routes/roles/role/credentials'; | ||||||
|  | import { Breadcrumb } from 'vault/vault/app-types'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   credentials: LdapStaticRoleCredentials | LdapDynamicRoleCredentials; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapRoleCredentialsPageComponent extends Component<Args> { | ||||||
|  |   staticFields = [ | ||||||
|  |     { label: 'Last Vault rotation', key: 'last_vault_rotation', formatDate: 'MMM d yyyy, h:mm:ss aaa' }, | ||||||
|  |     { label: 'Password', key: 'password', hasBlock: 'masked' }, | ||||||
|  |     { label: 'Username', key: 'username' }, | ||||||
|  |     { label: 'Rotation period', key: 'rotation_period', formatTtl: true }, | ||||||
|  |     { label: 'Time remaining', key: 'ttl', formatTtl: true }, | ||||||
|  |   ]; | ||||||
|  |   dynamicFields = [ | ||||||
|  |     { label: 'Distinguished Name', key: 'distinguished_names' }, | ||||||
|  |     { label: 'Username', key: 'username', hasBlock: 'masked' }, | ||||||
|  |     { label: 'Password', key: 'password', hasBlock: 'masked' }, | ||||||
|  |     { label: 'Lease ID', key: 'lease_id' }, | ||||||
|  |     { label: 'Lease duration', key: 'lease_duration', formatTtl: true }, | ||||||
|  |     { label: 'Lease renewable', key: 'renewable', hasBlock: 'check' }, | ||||||
|  |   ]; | ||||||
|  |   get fields() { | ||||||
|  |     return this.args.credentials.type === 'dynamic' ? this.dynamicFields : this.staticFields; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								ui/lib/ldap/addon/components/page/role/details.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								ui/lib/ldap/addon/components/page/role/details.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | <PageHeader as |p|> | ||||||
|  |   <p.top> | ||||||
|  |     <Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} /> | ||||||
|  |   </p.top> | ||||||
|  |   <p.levelLeft> | ||||||
|  |     <h1 class="title is-3" data-test-header-title> | ||||||
|  |       {{@model.name}} | ||||||
|  |     </h1> | ||||||
|  |   </p.levelLeft> | ||||||
|  | </PageHeader> | ||||||
|  |  | ||||||
|  | <Toolbar> | ||||||
|  |   <ToolbarActions> | ||||||
|  |     {{#if @model.canDelete}} | ||||||
|  |       <ConfirmAction @buttonClasses="toolbar-link" @onConfirmAction={{this.delete}} data-test-delete> | ||||||
|  |         Delete role | ||||||
|  |       </ConfirmAction> | ||||||
|  |       <div class="toolbar-separator"></div> | ||||||
|  |     {{/if}} | ||||||
|  |     {{#if @model.canReadCreds}} | ||||||
|  |       <ToolbarLink @route="roles.role.credentials" data-test-get-credentials> | ||||||
|  |         Get credentials | ||||||
|  |       </ToolbarLink> | ||||||
|  |     {{/if}} | ||||||
|  |     {{#if @model.canRotateStaticCreds}} | ||||||
|  |       <ConfirmAction | ||||||
|  |         @buttonClasses="toolbar-link" | ||||||
|  |         @confirmTitle="Rotate credentials?" | ||||||
|  |         @confirmMessage="When manually rotating credentials, the rotation period will start over." | ||||||
|  |         @confirmButtonText="Rotate" | ||||||
|  |         @disabled={{this.rotateCredentials.isRunning}} | ||||||
|  |         @onConfirmAction={{perform this.rotateCredentials}} | ||||||
|  |         data-test-rotate-credentials | ||||||
|  |       > | ||||||
|  |         Rotate credentials | ||||||
|  |       </ConfirmAction> | ||||||
|  |     {{/if}} | ||||||
|  |     {{#if @model.canEdit}} | ||||||
|  |       <ToolbarLink @route="roles.role.edit" data-test-edit> | ||||||
|  |         Edit role | ||||||
|  |       </ToolbarLink> | ||||||
|  |     {{/if}} | ||||||
|  |   </ToolbarActions> | ||||||
|  | </Toolbar> | ||||||
|  |  | ||||||
|  | {{#each @model.displayFields as |field|}} | ||||||
|  |   {{#let (get @model field.name) as |value|}} | ||||||
|  |     <InfoTableRow | ||||||
|  |       data-test-filtered-field | ||||||
|  |       @label={{or field.options.detailsLabel field.options.label}} | ||||||
|  |       @value={{if (eq field.options.editType "ttl") (format-duration value) value}} | ||||||
|  |     /> | ||||||
|  |   {{/let}} | ||||||
|  | {{/each}} | ||||||
							
								
								
									
										44
									
								
								ui/lib/ldap/addon/components/page/role/details.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								ui/lib/ldap/addon/components/page/role/details.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  | import { task } from 'ember-concurrency'; | ||||||
|  | import { waitFor } from '@ember/test-waiters'; | ||||||
|  |  | ||||||
|  | import type LdapRoleModel from 'vault/models/ldap/role'; | ||||||
|  | import { Breadcrumb } from 'vault/vault/app-types'; | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   model: LdapRoleModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapRoleDetailsPageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   async delete() { | ||||||
|  |     try { | ||||||
|  |       await this.args.model.destroyRecord(); | ||||||
|  |       this.flashMessages.success('Role deleted successfully.'); | ||||||
|  |       this.router.transitionTo('vault.cluster.secrets.backend.ldap.roles'); | ||||||
|  |     } catch (error) { | ||||||
|  |       const message = errorMessage(error, 'Unable to delete role. Please try again or contact support.'); | ||||||
|  |       this.flashMessages.danger(message); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @task | ||||||
|  |   @waitFor | ||||||
|  |   *rotateCredentials() { | ||||||
|  |     try { | ||||||
|  |       yield this.args.model.rotateStaticPassword(); | ||||||
|  |       this.flashMessages.success('Credentials successfully rotated.'); | ||||||
|  |     } catch (error) { | ||||||
|  |       this.flashMessages.danger(`Error rotating credentials \n ${errorMessage(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										117
									
								
								ui/lib/ldap/addon/components/page/roles.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								ui/lib/ldap/addon/components/page/roles.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | <TabPageHeader @model={{@backendModel}} @breadcrumbs={{@breadcrumbs}}> | ||||||
|  |   <:toolbarFilters> | ||||||
|  |     {{#if (and (not @promptConfig) @roles)}} | ||||||
|  |       <FilterInput @placeholder="Filter roles" @onInput={{fn (mut this.filterValue)}} /> | ||||||
|  |     {{/if}} | ||||||
|  |   </:toolbarFilters> | ||||||
|  |   <:toolbarActions> | ||||||
|  |     {{#if @promptConfig}} | ||||||
|  |       <ToolbarLink @route="configure" data-test-toolbar-action="config"> | ||||||
|  |         Configure LDAP | ||||||
|  |       </ToolbarLink> | ||||||
|  |     {{else}} | ||||||
|  |       <ToolbarLink @route="roles.create" @type="add" data-test-toolbar-action="role"> | ||||||
|  |         Create role | ||||||
|  |       </ToolbarLink> | ||||||
|  |     {{/if}} | ||||||
|  |   </:toolbarActions> | ||||||
|  | </TabPageHeader> | ||||||
|  |  | ||||||
|  | {{#if @promptConfig}} | ||||||
|  |   <ConfigCta /> | ||||||
|  | {{else if (not this.filteredRoles)}} | ||||||
|  |   {{#if this.filterValue}} | ||||||
|  |     <EmptyState @title="There are no roles matching "{{this.filterValue}}"" /> | ||||||
|  |   {{else}} | ||||||
|  |     <EmptyState | ||||||
|  |       data-test-config-cta | ||||||
|  |       @title="No roles created yet" | ||||||
|  |       @message="Roles in Vault will allow you to manage LDAP credentials. Create a role to get started." | ||||||
|  |     > | ||||||
|  |       <LinkTo class="has-top-margin-xs" @route="roles.create"> | ||||||
|  |         Create role | ||||||
|  |       </LinkTo> | ||||||
|  |     </EmptyState> | ||||||
|  |   {{/if}} | ||||||
|  | {{else}} | ||||||
|  |   <div class="has-bottom-margin-s"> | ||||||
|  |     {{#each this.filteredRoles as |role|}} | ||||||
|  |       <ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "roles.role.details" role.type role.name}} as |Item|> | ||||||
|  |         <Item.content> | ||||||
|  |           <Icon @name="user" /> | ||||||
|  |           <span data-test-role={{role.name}}>{{role.name}}</span> | ||||||
|  |           <Hds::Badge @text={{role.type}} data-test-role-type-badge={{role.name}} /> | ||||||
|  |         </Item.content> | ||||||
|  |         <Item.menu as |Menu|> | ||||||
|  |           {{#if role.rolePath.isLoading}} | ||||||
|  |             <li class="action"> | ||||||
|  |               <button disabled type="button" class="link button is-loading is-transparent"> | ||||||
|  |                 loading | ||||||
|  |               </button> | ||||||
|  |             </li> | ||||||
|  |           {{else}} | ||||||
|  |             <li class="action"> | ||||||
|  |               <LinkTo | ||||||
|  |                 class="has-text-black has-text-weight-semibold" | ||||||
|  |                 data-test-edit | ||||||
|  |                 @route="roles.role.edit" | ||||||
|  |                 @models={{array role.type role.name}} | ||||||
|  |                 @disabled={{not role.canEdit}} | ||||||
|  |               > | ||||||
|  |                 Edit | ||||||
|  |               </LinkTo> | ||||||
|  |             </li> | ||||||
|  |             <li class="action"> | ||||||
|  |               <LinkTo | ||||||
|  |                 class="has-text-black has-text-weight-semibold" | ||||||
|  |                 data-test-get-creds | ||||||
|  |                 @route="roles.role.credentials" | ||||||
|  |                 @models={{array role.type role.name}} | ||||||
|  |                 @disabled={{not role.canReadCreds}} | ||||||
|  |               > | ||||||
|  |                 Get credentials | ||||||
|  |               </LinkTo> | ||||||
|  |             </li> | ||||||
|  |             {{#if role.canRotateStaticCreds}} | ||||||
|  |               <li class="action"> | ||||||
|  |                 <Menu.Message | ||||||
|  |                   data-test-rotate-creds | ||||||
|  |                   @id={{concat "rotate-" role.id}} | ||||||
|  |                   @triggerText="Rotate credentials" | ||||||
|  |                   @title="Are you sure?" | ||||||
|  |                   @message="When manually rotating credentials, the rotation period will start over." | ||||||
|  |                   @confirmButtonText="Rotate" | ||||||
|  |                   @onConfirm={{fn this.onRotate role}} | ||||||
|  |                 /> | ||||||
|  |               </li> | ||||||
|  |             {{/if}} | ||||||
|  |             <li class="action"> | ||||||
|  |               <LinkTo | ||||||
|  |                 class="has-text-black has-text-weight-semibold" | ||||||
|  |                 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}} | ||||||
|  |                 @disabled={{not role.canRead}} | ||||||
|  |               > | ||||||
|  |                 Details | ||||||
|  |               </LinkTo> | ||||||
|  |             </li> | ||||||
|  |             {{#if role.canDelete}} | ||||||
|  |               <li class="action"> | ||||||
|  |                 <Menu.Message | ||||||
|  |                   data-test-delete | ||||||
|  |                   @id={{concat "delete-" role.id}} | ||||||
|  |                   @triggerText="Delete" | ||||||
|  |                   @title="Are you sure?" | ||||||
|  |                   @message="Deleting this role means that you’ll need to recreate it in order to generate credentials again." | ||||||
|  |                   @onConfirm={{fn this.onDelete role}} | ||||||
|  |                 /> | ||||||
|  |               </li> | ||||||
|  |             {{/if}} | ||||||
|  |           {{/if}} | ||||||
|  |         </Item.menu> | ||||||
|  |       </ListItem> | ||||||
|  |     {{/each}} | ||||||
|  |   </div> | ||||||
|  | {{/if}} | ||||||
							
								
								
									
										64
									
								
								ui/lib/ldap/addon/components/page/roles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								ui/lib/ldap/addon/components/page/roles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Component from '@glimmer/component'; | ||||||
|  | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import { getOwner } from '@ember/application'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | import type LdapRoleModel from 'vault/models/ldap/role'; | ||||||
|  | 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'; | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   roles: Array<LdapRoleModel>; | ||||||
|  |   promptConfig: boolean; | ||||||
|  |   backendModel: SecretEngineModel; | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapRolesPageComponent extends Component<Args> { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |  | ||||||
|  |   @tracked filterValue = ''; | ||||||
|  |  | ||||||
|  |   get mountPoint(): string { | ||||||
|  |     const owner = getOwner(this) as EngineOwner; | ||||||
|  |     return owner.mountPoint; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get filteredRoles() { | ||||||
|  |     const { roles } = this.args; | ||||||
|  |     return this.filterValue | ||||||
|  |       ? roles.filter((role) => role.name.toLowerCase().includes(this.filterValue.toLowerCase())) | ||||||
|  |       : roles; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   async onRotate(model: LdapRoleModel) { | ||||||
|  |     try { | ||||||
|  |       const message = `Successfully rotated credentials for ${model.name}.`; | ||||||
|  |       await model.rotateStaticPassword(); | ||||||
|  |       this.flashMessages.success(message); | ||||||
|  |     } catch (error) { | ||||||
|  |       this.flashMessages.danger(`Error rotating credentials \n ${errorMessage(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   async onDelete(model: LdapRoleModel) { | ||||||
|  |     try { | ||||||
|  |       const message = `Successfully deleted role ${model.name}.`; | ||||||
|  |       await model.destroyRecord(); | ||||||
|  |       this.args.roles.removeObject(model); | ||||||
|  |       this.flashMessages.success(message); | ||||||
|  |     } catch (error) { | ||||||
|  |       this.flashMessages.danger(`Error deleting role \n ${errorMessage(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								ui/lib/ldap/addon/components/tab-page-header.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ui/lib/ldap/addon/components/tab-page-header.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | <PageHeader as |p|> | ||||||
|  |   <p.top> | ||||||
|  |     <Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} /> | ||||||
|  |   </p.top> | ||||||
|  |   <p.levelLeft> | ||||||
|  |     <h1 class="title is-3" data-test-header-title> | ||||||
|  |       <Icon @name={{@model.icon}} @size="24" class="has-text-grey-light" /> | ||||||
|  |       {{@model.id}} | ||||||
|  |     </h1> | ||||||
|  |   </p.levelLeft> | ||||||
|  | </PageHeader> | ||||||
|  |  | ||||||
|  | <div class="tabs-container box is-bottomless is-marginless is-paddingless"> | ||||||
|  |   <nav class="tabs" aria-label="ldap tabs"> | ||||||
|  |     <ul> | ||||||
|  |       <LinkTo @route="overview" data-test-tab="overview">Overview</LinkTo> | ||||||
|  |       <LinkTo @route="roles" data-test-tab="roles">Roles</LinkTo> | ||||||
|  |       <LinkTo @route="libraries" data-test-tab="libraries">Libraries</LinkTo> | ||||||
|  |       <LinkTo @route="configuration" data-test-tab="config">Configuration</LinkTo> | ||||||
|  |     </ul> | ||||||
|  |   </nav> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <Toolbar> | ||||||
|  |   <ToolbarFilters> | ||||||
|  |     {{yield to="toolbarFilters"}} | ||||||
|  |   </ToolbarFilters> | ||||||
|  |   <ToolbarActions> | ||||||
|  |     {{yield to="toolbarActions"}} | ||||||
|  |   </ToolbarActions> | ||||||
|  | </Toolbar> | ||||||
							
								
								
									
										22
									
								
								ui/lib/ldap/addon/engine.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ui/lib/ldap/addon/engine.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Engine from 'ember-engines/engine'; | ||||||
|  | import loadInitializers from 'ember-load-initializers'; | ||||||
|  | import Resolver from 'ember-resolver'; | ||||||
|  | import config from './config/environment'; | ||||||
|  |  | ||||||
|  | const { modulePrefix } = config; | ||||||
|  |  | ||||||
|  | export default class LdapEngine extends Engine { | ||||||
|  |   modulePrefix = modulePrefix; | ||||||
|  |   Resolver = Resolver; | ||||||
|  |   dependencies = { | ||||||
|  |     services: ['router', 'store', 'secret-mount-path', 'flash-messages', 'auth'], | ||||||
|  |     externalRoutes: ['secrets'], | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | loadInitializers(LdapEngine, modulePrefix); | ||||||
							
								
								
									
										31
									
								
								ui/lib/ldap/addon/routes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ui/lib/ldap/addon/routes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import buildRoutes from 'ember-engines/routes'; | ||||||
|  |  | ||||||
|  | export default buildRoutes(function () { | ||||||
|  |   this.route('overview'); | ||||||
|  |   this.route('roles', function () { | ||||||
|  |     this.route('create'); | ||||||
|  |     this.route('role', { path: '/:type/:name' }, function () { | ||||||
|  |       this.route('details'); | ||||||
|  |       this.route('edit'); | ||||||
|  |       this.route('credentials'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   this.route('libraries', function () { | ||||||
|  |     this.route('create'); | ||||||
|  |     this.route('library', { path: '/:name' }, function () { | ||||||
|  |       this.route('details', function () { | ||||||
|  |         this.route('accounts'); | ||||||
|  |         this.route('configuration'); | ||||||
|  |       }); | ||||||
|  |       this.route('edit'); | ||||||
|  |       this.route('check-out'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   this.route('configure'); | ||||||
|  |   this.route('configuration'); | ||||||
|  | }); | ||||||
							
								
								
									
										57
									
								
								ui/lib/ldap/addon/routes/configuration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								ui/lib/ldap/addon/routes/configuration.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
|  |  | ||||||
|  | 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 LdapConfigModel from 'vault/models/ldap/config'; | ||||||
|  | import type SecretEngineModel from 'vault/models/secret-engine'; | ||||||
|  | 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 { | ||||||
|  |   backendModel: SecretEngineModel; | ||||||
|  |   configModel: LdapConfigModel; | ||||||
|  |   configError: AdapterError; | ||||||
|  | } | ||||||
|  | interface LdapConfigurationController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapConfigurationRouteModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @withConfig('ldap/config') | ||||||
|  | export default class LdapConfigurationRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   declare configModel: LdapConfigModel; | ||||||
|  |   declare configError: AdapterError; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     return { | ||||||
|  |       backendModel: this.modelFor('application'), | ||||||
|  |       configModel: this.configModel, | ||||||
|  |       configError: this.configError, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapConfigurationController, | ||||||
|  |     resolvedModel: LdapConfigurationRouteModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: 'secrets', route: 'secrets', linkExternal: true }, | ||||||
|  |       { label: resolvedModel.backendModel.id }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								ui/lib/ldap/addon/routes/configure.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								ui/lib/ldap/addon/routes/configure.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
|  |  | ||||||
|  | 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 LdapConfigModel from 'vault/models/ldap/config'; | ||||||
|  | import type Controller from '@ember/controller'; | ||||||
|  | import type { Breadcrumb } from 'vault/vault/app-types'; | ||||||
|  |  | ||||||
|  | interface LdapConfigureController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @withConfig('ldap/config') | ||||||
|  | export default class LdapConfigureRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   declare configModel: LdapConfigModel; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     const backend = this.secretMountPath.currentPath; | ||||||
|  |     return this.configModel || this.store.createRecord('ldap/config', { backend }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapConfigureController, | ||||||
|  |     resolvedModel: LdapConfigModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: 'Secrets', route: 'secrets', linkExternal: true }, | ||||||
|  |       { label: resolvedModel.backend, route: 'overview' }, | ||||||
|  |       { label: 'Configure' }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								ui/lib/ldap/addon/routes/error.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								ui/lib/ldap/addon/routes/error.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | import type Transition from '@ember/routing/transition'; | ||||||
|  | import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports | ||||||
|  | import type SecretEngineModel from 'vault/models/secret-engine'; | ||||||
|  | import type { Breadcrumb } from 'vault/vault/app-types'; | ||||||
|  | import type Controller from '@ember/controller'; | ||||||
|  | import type SecretMountPath from 'vault/services/secret-mount-path'; | ||||||
|  |  | ||||||
|  | interface LdapErrorController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   backend: SecretEngineModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapErrorRoute extends Route { | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   setupController(controller: LdapErrorController, resolvedModel: AdapterError, transition: Transition) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: 'secrets', route: 'secrets', linkExternal: true }, | ||||||
|  |       { label: this.secretMountPath.currentPath, route: 'overview' }, | ||||||
|  |     ]; | ||||||
|  |     controller.backend = this.modelFor('application') as SecretEngineModel; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								ui/lib/ldap/addon/routes/libraries/create.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								ui/lib/ldap/addon/routes/libraries/create.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | import type Store from '@ember-data/store'; | ||||||
|  | import type SecretMountPath from 'vault/services/secret-mount-path'; | ||||||
|  | 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'; | ||||||
|  |  | ||||||
|  | interface LdapLibrariesCreateController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapLibraryModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibrariesCreateRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     const backend = this.secretMountPath.currentPath; | ||||||
|  |     return this.store.createRecord('ldap/library', { backend }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapLibrariesCreateController, | ||||||
|  |     resolvedModel: LdapLibraryModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: resolvedModel.backend, route: 'overview' }, | ||||||
|  |       { label: 'libraries', route: 'libraries' }, | ||||||
|  |       { label: 'create' }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										57
									
								
								ui/lib/ldap/addon/routes/libraries/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								ui/lib/ldap/addon/routes/libraries/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
|  | 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'; | ||||||
|  |  | ||||||
|  | interface LdapLibrariesRouteModel { | ||||||
|  |   backendModel: SecretEngineModel; | ||||||
|  |   promptConfig: boolean; | ||||||
|  |   libraries: Array<LdapLibraryModel>; | ||||||
|  | } | ||||||
|  | interface LdapLibrariesController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapLibrariesRouteModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @withConfig('ldap/config') | ||||||
|  | export default class LdapLibrariesRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   declare promptConfig: boolean; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     const backendModel = this.modelFor('application') as SecretEngineModel; | ||||||
|  |     return hash({ | ||||||
|  |       backendModel, | ||||||
|  |       promptConfig: this.promptConfig, | ||||||
|  |       libraries: this.store.query('ldap/library', { backend: backendModel.id }), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapLibrariesController, | ||||||
|  |     resolvedModel: LdapLibrariesRouteModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: 'secrets', route: 'secrets', linkExternal: true }, | ||||||
|  |       { label: resolvedModel.backendModel.id }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								ui/lib/ldap/addon/routes/libraries/library.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								ui/lib/ldap/addon/routes/libraries/library.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | import type Store from '@ember-data/store'; | ||||||
|  | import type SecretMountPath from 'vault/services/secret-mount-path'; | ||||||
|  |  | ||||||
|  | interface LdapLibraryRouteParams { | ||||||
|  |   name: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibraryRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   model(params: LdapLibraryRouteParams) { | ||||||
|  |     const backend = this.secretMountPath.currentPath; | ||||||
|  |     const { name } = params; | ||||||
|  |     return this.store.queryRecord('ldap/library', { backend, name }); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								ui/lib/ldap/addon/routes/libraries/library/check-out.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								ui/lib/ldap/addon/routes/libraries/library/check-out.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { action } from '@ember/object'; | ||||||
|  | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
|  | import type FlashMessageService from 'vault/services/flash-messages'; | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  | 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 { LdapLibraryCheckOutCredentials } from 'vault/vault/adapters/ldap/library'; | ||||||
|  | import type AdapterError from 'ember-data/adapter'; // eslint-disable-line ember/use-ember-data-rfc-395-imports | ||||||
|  |  | ||||||
|  | interface LdapLibraryCheckOutController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapLibraryCheckOutCredentials; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibraryCheckOutRoute extends Route { | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   accountsRoute = 'vault.cluster.secrets.backend.ldap.libraries.library.details.accounts'; | ||||||
|  |  | ||||||
|  |   beforeModel(transition: Transition) { | ||||||
|  |     // transition must be from the details.accounts route to ensure it was initiated by the check-out action | ||||||
|  |     if (transition.from?.name !== this.accountsRoute) { | ||||||
|  |       this.router.replaceWith(this.accountsRoute); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   model(_params: object, transition: Transition) { | ||||||
|  |     const { ttl } = transition.to.queryParams; | ||||||
|  |     const library = this.modelFor('libraries.library') as LdapLibraryModel; | ||||||
|  |     return library.checkOutAccount(ttl); | ||||||
|  |   } | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapLibraryCheckOutController, | ||||||
|  |     resolvedModel: LdapLibraryCheckOutCredentials, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     const library = this.modelFor('libraries.library') as LdapLibraryModel; | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: library.backend, route: 'overview' }, | ||||||
|  |       { label: 'libraries', route: 'libraries' }, | ||||||
|  |       { label: library.name, route: 'libraries.library' }, | ||||||
|  |       { label: 'check-out' }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   error(error: AdapterError) { | ||||||
|  |     // if check-out fails, return to library details route | ||||||
|  |     const message = errorMessage(error, 'Error checking out account. Please try again or contact support.'); | ||||||
|  |     this.flashMessages.danger(message); | ||||||
|  |     this.router.replaceWith(this.accountsRoute); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								ui/lib/ldap/addon/routes/libraries/library/details.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								ui/lib/ldap/addon/routes/libraries/library/details.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  |  | ||||||
|  | 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'; | ||||||
|  |  | ||||||
|  | interface LdapLibraryDetailsController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapLibraryModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibraryDetailsRoute extends Route { | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapLibraryDetailsController, | ||||||
|  |     resolvedModel: LdapLibraryModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: resolvedModel.backend, route: 'overview' }, | ||||||
|  |       { label: 'libraries', route: 'libraries' }, | ||||||
|  |       { label: resolvedModel.name }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { hash } from 'rsvp'; | ||||||
|  |  | ||||||
|  | import type LdapLibraryModel from 'vault/models/ldap/library'; | ||||||
|  |  | ||||||
|  | export default class LdapLibraryRoute extends Route { | ||||||
|  |   model() { | ||||||
|  |     const model = this.modelFor('libraries.library') as LdapLibraryModel; | ||||||
|  |     return hash({ | ||||||
|  |       library: model, | ||||||
|  |       statuses: model.fetchStatus(), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								ui/lib/ldap/addon/routes/libraries/library/details/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								ui/lib/ldap/addon/routes/libraries/library/details/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  |  | ||||||
|  | export default class LdapLibraryRoute extends Route { | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   redirect() { | ||||||
|  |     this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.details.accounts'); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								ui/lib/ldap/addon/routes/libraries/library/edit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								ui/lib/ldap/addon/routes/libraries/library/edit.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  |  | ||||||
|  | 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'; | ||||||
|  |  | ||||||
|  | interface LdapLibraryEditController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapLibraryModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapLibraryEditRoute extends Route { | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapLibraryEditController, | ||||||
|  |     resolvedModel: LdapLibraryModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: resolvedModel.backend, route: 'overview' }, | ||||||
|  |       { label: 'libraries', route: 'libraries' }, | ||||||
|  |       { label: resolvedModel.name, route: 'libraries.library.details' }, | ||||||
|  |       { label: 'edit' }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								ui/lib/ldap/addon/routes/libraries/library/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								ui/lib/ldap/addon/routes/libraries/library/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | import type RouterService from '@ember/routing/router-service'; | ||||||
|  |  | ||||||
|  | export default class LdapLibraryRoute extends Route { | ||||||
|  |   @service declare readonly router: RouterService; | ||||||
|  |  | ||||||
|  |   redirect() { | ||||||
|  |     this.router.transitionTo('vault.cluster.secrets.backend.ldap.libraries.library.details'); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								ui/lib/ldap/addon/routes/overview.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								ui/lib/ldap/addon/routes/overview.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
|  | 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 SecretEngineModel from 'vault/models/secret-engine'; | ||||||
|  | import type LdapRoleModel from 'vault/models/ldap/role'; | ||||||
|  | import type LdapLibraryModel from 'vault/models/ldap/library'; | ||||||
|  | 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 { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  | } | ||||||
|  | interface LdapOverviewRouteModel { | ||||||
|  |   backendModel: SecretEngineModel; | ||||||
|  |   promptConfig: boolean; | ||||||
|  |   roles: Array<LdapRoleModel>; | ||||||
|  |   libraries: Array<LdapLibraryModel>; | ||||||
|  |   librariesStatus: Array<LdapLibraryAccountStatus>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @withConfig('ldap/config') | ||||||
|  | export default class LdapOverviewRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   declare promptConfig: boolean; | ||||||
|  |  | ||||||
|  |   async fetchLibrariesStatus(libraries: Array<LdapLibraryModel>): Promise<Array<LdapLibraryAccountStatus>> { | ||||||
|  |     const allStatuses: Array<LdapLibraryAccountStatus> = []; | ||||||
|  |  | ||||||
|  |     for (const library of libraries) { | ||||||
|  |       try { | ||||||
|  |         const statuses = await library.fetchStatus(); | ||||||
|  |         allStatuses.push(...statuses); | ||||||
|  |       } catch (error) { | ||||||
|  |         // suppressing error | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return allStatuses; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async fetchLibraries(backend: string) { | ||||||
|  |     return this.store.query('ldap/library', { backend }).catch(() => []); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async model() { | ||||||
|  |     const backend = this.secretMountPath.currentPath; | ||||||
|  |     const libraries = await this.fetchLibraries(backend); | ||||||
|  |     return hash({ | ||||||
|  |       promptConfig: this.promptConfig, | ||||||
|  |       backendModel: this.modelFor('application'), | ||||||
|  |       roles: this.store.query('ldap/role', { backend }).catch(() => []), | ||||||
|  |       libraries, | ||||||
|  |       librariesStatus: this.fetchLibrariesStatus(libraries as Array<LdapLibraryModel>), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapOverviewController, | ||||||
|  |     resolvedModel: LdapOverviewRouteModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: 'secrets', route: 'secrets', linkExternal: true }, | ||||||
|  |       { label: resolvedModel.backendModel.id }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								ui/lib/ldap/addon/routes/roles/create.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								ui/lib/ldap/addon/routes/roles/create.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | import type Store from '@ember-data/store'; | ||||||
|  | import type SecretMountPath from 'vault/services/secret-mount-path'; | ||||||
|  | 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 LdapRolesCreateController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapRoleModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapRolesCreateRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     const backend = this.secretMountPath.currentPath; | ||||||
|  |     return this.store.createRecord('ldap/role', { backend }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapRolesCreateController, | ||||||
|  |     resolvedModel: LdapRoleModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: resolvedModel.backend, route: 'overview' }, | ||||||
|  |       { label: 'roles', route: 'roles' }, | ||||||
|  |       { label: 'create' }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								ui/lib/ldap/addon/routes/roles/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								ui/lib/ldap/addon/routes/roles/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  | import { withConfig } from 'core/decorators/fetch-secrets-engine-config'; | ||||||
|  | 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 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 { | ||||||
|  |   backendModel: SecretEngineModel; | ||||||
|  |   promptConfig: boolean; | ||||||
|  |   roles: Array<LdapRoleModel>; | ||||||
|  | } | ||||||
|  | interface LdapRolesController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapRolesRouteModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @withConfig('ldap/config') | ||||||
|  | export default class LdapRolesRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   declare promptConfig: boolean; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     const backendModel = this.modelFor('application') as SecretEngineModel; | ||||||
|  |     return hash({ | ||||||
|  |       backendModel, | ||||||
|  |       promptConfig: this.promptConfig, | ||||||
|  |       roles: this.store.query( | ||||||
|  |         'ldap/role', | ||||||
|  |         { backend: backendModel.id }, | ||||||
|  |         { adapterOptions: { showPartialError: true } } | ||||||
|  |       ), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapRolesController, | ||||||
|  |     resolvedModel: LdapRolesRouteModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: 'secrets', route: 'secrets', linkExternal: true }, | ||||||
|  |       { label: resolvedModel.backendModel.id }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								ui/lib/ldap/addon/routes/roles/role.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								ui/lib/ldap/addon/routes/roles/role.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | import type Store from '@ember-data/store'; | ||||||
|  | import type SecretMountPath from 'vault/services/secret-mount-path'; | ||||||
|  |  | ||||||
|  | interface LdapRoleRouteParams { | ||||||
|  |   name: string; | ||||||
|  |   type: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapRoleRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |   @service declare readonly secretMountPath: SecretMountPath; | ||||||
|  |  | ||||||
|  |   model(params: LdapRoleRouteParams) { | ||||||
|  |     const backend = this.secretMountPath.currentPath; | ||||||
|  |     const { name, type } = params; | ||||||
|  |     return this.store.queryRecord('ldap/role', { backend, name, type }); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								ui/lib/ldap/addon/routes/roles/role/credentials.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								ui/lib/ldap/addon/routes/roles/role/credentials.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  | import { inject as service } from '@ember/service'; | ||||||
|  |  | ||||||
|  | import type Store from '@ember-data/store'; | ||||||
|  | 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 LdapRoleCredentialsController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapRoleModel; | ||||||
|  | } | ||||||
|  | export interface LdapStaticRoleCredentials { | ||||||
|  |   dn: string; | ||||||
|  |   last_vault_rotation: string; | ||||||
|  |   password: string; | ||||||
|  |   last_password: string; | ||||||
|  |   rotation_period: number; | ||||||
|  |   ttl: number; | ||||||
|  |   username: string; | ||||||
|  |   type: string; | ||||||
|  | } | ||||||
|  | export interface LdapDynamicRoleCredentials { | ||||||
|  |   distinguished_names: Array<string>; | ||||||
|  |   password: string; | ||||||
|  |   username: string; | ||||||
|  |   lease_id: string; | ||||||
|  |   lease_duration: string; | ||||||
|  |   renewable: boolean; | ||||||
|  |   type: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapRoleCredentialsRoute extends Route { | ||||||
|  |   @service declare readonly store: Store; | ||||||
|  |  | ||||||
|  |   model() { | ||||||
|  |     const role = this.modelFor('roles.role') as LdapRoleModel; | ||||||
|  |     return role.fetchCredentials(); | ||||||
|  |   } | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapRoleCredentialsController, | ||||||
|  |     resolvedModel: LdapStaticRoleCredentials | LdapDynamicRoleCredentials, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     const role = this.modelFor('roles.role') as LdapRoleModel; | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: role.backend, route: 'overview' }, | ||||||
|  |       { label: 'roles', route: 'roles' }, | ||||||
|  |       { label: role.name, route: 'roles.role' }, | ||||||
|  |       { label: 'credentials' }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								ui/lib/ldap/addon/routes/roles/role/details.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								ui/lib/ldap/addon/routes/roles/role/details.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import Route from '@ember/routing/route'; | ||||||
|  |  | ||||||
|  | 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 LdapRoleDetailsController extends Controller { | ||||||
|  |   breadcrumbs: Array<Breadcrumb>; | ||||||
|  |   model: LdapRoleModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class LdapRoleEditRoute extends Route { | ||||||
|  |   setupController( | ||||||
|  |     controller: LdapRoleDetailsController, | ||||||
|  |     resolvedModel: LdapRoleModel, | ||||||
|  |     transition: Transition | ||||||
|  |   ) { | ||||||
|  |     super.setupController(controller, resolvedModel, transition); | ||||||
|  |  | ||||||
|  |     controller.breadcrumbs = [ | ||||||
|  |       { label: resolvedModel.backend, route: 'overview' }, | ||||||
|  |       { label: 'roles', route: 'roles' }, | ||||||
|  |       { label: resolvedModel.name }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Jordan Reimer
					Jordan Reimer