mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +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 | ||||
|  */ | ||||
|  | ||||
| import ApplicationAdapter from 'vault/adapters/application'; | ||||
| import { encodePath } from 'vault/utils/path-encoding-helpers'; | ||||
| import SecretsEnginePathAdapter from 'vault/adapters/secrets-engine-path'; | ||||
|  | ||||
| export default class KubernetesConfigAdapter extends ApplicationAdapter { | ||||
|   namespace = 'v1'; | ||||
| export default class KubernetesConfigAdapter extends SecretsEnginePathAdapter { | ||||
|   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) { | ||||
|     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: { | ||||
|       dependencies: { | ||||
|         services: ['download', 'namespace', 'router', 'store', 'secret-mount-path', 'flash-messages'], | ||||
|   | ||||
| @@ -30,11 +30,6 @@ const ENTERPRISE_SECRET_ENGINES = [ | ||||
| ]; | ||||
|  | ||||
| const MOUNTABLE_SECRET_ENGINES = [ | ||||
|   { | ||||
|     displayName: 'Active Directory', | ||||
|     type: 'ad', | ||||
|     category: 'cloud', | ||||
|   }, | ||||
|   { | ||||
|     displayName: 'AliCloud', | ||||
|     type: 'alicloud', | ||||
| @@ -110,9 +105,15 @@ const MOUNTABLE_SECRET_ENGINES = [ | ||||
|     type: 'totp', | ||||
|     category: 'generic', | ||||
|   }, | ||||
|   { | ||||
|     displayName: 'LDAP', | ||||
|     type: 'ldap', | ||||
|     engineRoute: 'ldap.overview', | ||||
|     category: 'generic', | ||||
|     glyph: 'folder-users', | ||||
|   }, | ||||
|   { | ||||
|     displayName: 'Kubernetes', | ||||
|     value: 'kubernetes', | ||||
|     type: 'kubernetes', | ||||
|     engineRoute: 'kubernetes.overview', | ||||
|     category: 'generic', | ||||
|   | ||||
| @@ -18,6 +18,7 @@ const SUPPORTED_SECRET_BACKENDS = [ | ||||
|   'transform', | ||||
|   'keymgmt', | ||||
|   'kubernetes', | ||||
|   'ldap', | ||||
| ]; | ||||
|  | ||||
| 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() { | ||||
|     if (!this.engineType || this.engineType === 'kmip') { | ||||
|       return 'secrets'; | ||||
|     } | ||||
|     if (this.engineType === 'keymgmt') { | ||||
|       return 'key'; | ||||
|     } | ||||
|     return this.engineType; | ||||
|     const defaultIcon = this.engineType || 'secrets'; | ||||
|     return ( | ||||
|       { | ||||
|         keymgmt: 'key', | ||||
|         kmip: 'secrets', | ||||
|         ldap: 'folder-users', | ||||
|       }[this.engineType] || defaultIcon | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   get engineType() { | ||||
|   | ||||
| @@ -161,6 +161,7 @@ Router.map(function () { | ||||
|           this.mount('kmip'); | ||||
|           this.mount('kubernetes'); | ||||
|           this.mount('kv'); | ||||
|           this.mount('ldap'); | ||||
|           this.mount('pki'); | ||||
|           this.route('index', { path: '/' }); | ||||
|           this.route('configuration'); | ||||
|   | ||||
| @@ -5,13 +5,10 @@ | ||||
|  | ||||
| import ApplicationSerializer from '../application'; | ||||
|  | ||||
| export default class KubernetesConfigSerializer extends ApplicationSerializer { | ||||
| export default class KubernetesRoleSerializer extends ApplicationSerializer { | ||||
|   primaryKey = 'name'; | ||||
|  | ||||
|   serialize() { | ||||
|     const json = super.serialize(...arguments); | ||||
|     // remove backend value from payload | ||||
|     delete json.backend; | ||||
|     return json; | ||||
|   } | ||||
|   attrs = { | ||||
|     backend: { serialize: false }, | ||||
|   }; | ||||
| } | ||||
|   | ||||
							
								
								
									
										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
 | ||||
| export default class SecretMountPath extends Service { | ||||
|   currentPath = ''; | ||||
|   update(path) { | ||||
| 
 | ||||
|   update(path: string) { | ||||
|     this.currentPath = path; | ||||
|   } | ||||
|   get() { | ||||
|     return this.currentPath; | ||||
|   } | ||||
| } | ||||
| @@ -5,7 +5,6 @@ | ||||
|  | ||||
| .overview-card { | ||||
|   padding: $spacing-l; | ||||
|   display: initial; | ||||
|   line-height: initial; | ||||
|  | ||||
|   .title-number { | ||||
|   | ||||
| @@ -326,4 +326,9 @@ a.button.disabled { | ||||
|   font-size: inherit; | ||||
|   font-weight: inherit; | ||||
|   cursor: pointer; | ||||
|  | ||||
|   &:disabled { | ||||
|     color: $grey-light; | ||||
|     cursor: not-allowed; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -18,6 +18,10 @@ | ||||
|   background-color: $ui-gray-200; | ||||
| } | ||||
|  | ||||
| .has-background-gray-900 { | ||||
|   background-color: $ui-gray-900; | ||||
| } | ||||
|  | ||||
| .background-color-black { | ||||
|   background-color: black; | ||||
| } | ||||
| @@ -34,7 +38,9 @@ | ||||
| } | ||||
|  | ||||
| .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; | ||||
| } | ||||
|  | ||||
| @@ -75,6 +81,10 @@ select.has-error-border { | ||||
| .has-text-info { | ||||
|   color: $blue-500 !important; | ||||
| } | ||||
| // same without the !important | ||||
| .has-text-primary { | ||||
|   color: $blue-500; | ||||
| } | ||||
|  | ||||
| .has-text-success { | ||||
|   color: $green-500 !important; | ||||
| @@ -87,3 +97,7 @@ select.has-error-border { | ||||
| .has-text-danger { | ||||
|   color: $red-500 !important; | ||||
| } | ||||
|  | ||||
| .has-text-primary { | ||||
|   color: $blue; | ||||
| } | ||||
|   | ||||
| @@ -41,6 +41,11 @@ | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .is-flex-align-start { | ||||
|   display: flex; | ||||
|   align-items: flex-start; | ||||
| } | ||||
|  | ||||
| .is-flex-align-baseline { | ||||
|   display: flex; | ||||
|   align-items: baseline; | ||||
|   | ||||
| @@ -116,6 +116,10 @@ | ||||
|   border-radius: $radius; | ||||
| } | ||||
|  | ||||
| .border-radius-4 { | ||||
|   border-radius: $radius-large; | ||||
| } | ||||
|  | ||||
| // border-spacing | ||||
| .is-border-spacing-revert { | ||||
|   border-spacing: revert; | ||||
|   | ||||
| @@ -26,6 +26,10 @@ | ||||
|   padding-right: $spacing-s; | ||||
| } | ||||
|  | ||||
| .has-padding-s { | ||||
|   padding: $spacing-s; | ||||
| } | ||||
|  | ||||
| .has-padding-xxs { | ||||
|   padding: $spacing-xxs; | ||||
| } | ||||
| @@ -37,6 +41,10 @@ | ||||
|   padding: $spacing-l; | ||||
| } | ||||
|  | ||||
| .has-padding-l { | ||||
|   padding: $spacing-l; | ||||
| } | ||||
|  | ||||
| .has-bottom-padding-s { | ||||
|   padding-bottom: $spacing-s; | ||||
| } | ||||
| @@ -95,15 +103,22 @@ | ||||
|   margin-bottom: -$spacing-m; | ||||
| } | ||||
|  | ||||
| .has-top-margin-negative-xxl { | ||||
|   margin-top: -$spacing-xxl; | ||||
| } | ||||
|  | ||||
| .has-top-margin-xxs { | ||||
|   margin: $spacing-xxs 0; | ||||
| } | ||||
|  | ||||
| .has-right-margin-xxs { | ||||
|   margin-right: $spacing-xxs; | ||||
| } | ||||
|  | ||||
| .has-left-margin-xxs { | ||||
|   margin-left: $spacing-xxs; | ||||
| } | ||||
|  | ||||
| .has-bottom-margin-xxs { | ||||
|   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 | ||||
|     @params={{array backend.backendLink backend.id}} | ||||
|     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}} | ||||
|   > | ||||
|     <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}} | ||||
|           </select> | ||||
|         </div> | ||||
|         {{#if this.validationError}} | ||||
|           <AlertInline | ||||
|             @type="danger" | ||||
|             @message={{this.validationError}} | ||||
|             @paddingTop={{true}} | ||||
|             data-test-field-validation={{@attr.name}} | ||||
|           /> | ||||
|         {{/if}} | ||||
|       </div> | ||||
|     {{/if}} | ||||
|   {{else if (eq @attr.options.editType "searchSelect")}} | ||||
| @@ -86,9 +78,6 @@ | ||||
|         class={{if this.validationError "dropdown-has-error-border"}} | ||||
|       /> | ||||
|     </div> | ||||
|     {{#if this.validationError}} | ||||
|       <AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} /> | ||||
|     {{/if}} | ||||
|   {{else if (eq @attr.options.editType "mountAccessor")}} | ||||
|     <MountAccessorSelect | ||||
|       @name={{@attr.name}} | ||||
| @@ -126,6 +115,7 @@ | ||||
|       {{#let (or (get @model this.valuePath) @attr.options.setDefault) as |initialValue|}} | ||||
|         <TtlPicker | ||||
|           data-test-input={{@attr.name}} | ||||
|           class={{if this.validationError "ttl-picker-form-field-error"}} | ||||
|           @onChange={{this.setAndBroadcastTtl}} | ||||
|           @label={{this.labelString}} | ||||
|           @helperTextDisabled={{or @attr.options.helperTextDisabled "Vault will use the default lease duration."}} | ||||
| @@ -194,6 +184,7 @@ | ||||
|     </Toggle> | ||||
|   {{else if (eq @attr.options.editType "stringArray")}} | ||||
|     <StringList | ||||
|       class={{if this.validationError "string-list-form-field-error"}} | ||||
|       data-test-input={{@attr.name}} | ||||
|       @label={{this.labelString}} | ||||
|       @helpText={{if this.showHelpText @attr.options.helpText}} | ||||
| @@ -210,9 +201,6 @@ | ||||
|       @onChange={{this.setAndBroadcast}} | ||||
|       @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"))}} | ||||
|     <div class="control"> | ||||
|       {{#if (eq @attr.options.editType "textarea")}} | ||||
| @@ -224,9 +212,6 @@ | ||||
|           oninput={{this.onChangeWithEvent}} | ||||
|           class="textarea {{if this.validationError 'has-error-border'}}" | ||||
|         ></textarea> | ||||
|         {{#if this.validationError}} | ||||
|           <AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} /> | ||||
|         {{/if}} | ||||
|       {{else if (eq @attr.options.editType "password")}} | ||||
|         <Input | ||||
|           data-test-input={{@attr.name}} | ||||
| @@ -253,6 +238,7 @@ | ||||
|             @theme={{or @attr.options.theme "hashi"}} | ||||
|             @helpText={{@attr.options.helpText}} | ||||
|             @mode={{@attr.options.mode}} | ||||
|             @example={{@attr.options.example}} | ||||
|           > | ||||
|             {{#if @attr.options.allowReset}} | ||||
|               <button | ||||
| @@ -295,22 +281,12 @@ | ||||
|           class="input {{if this.validationError 'has-error-border'}}" | ||||
|           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 (and (get @model this.valuePath) (not (get @model @attr.options.validationAttr)))}} | ||||
|             <AlertInline @type="danger" @message={{@attr.options.invalidMessage}} /> | ||||
|           {{/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}} | ||||
|     </div> | ||||
|   {{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}} | ||||
|       @valueUpdated={{fn this.codemirrorUpdated false}} | ||||
|       @helpText={{@attr.options.helpText}} | ||||
|       @example={{@attr.options.example}} | ||||
|     /> | ||||
|   {{else if (eq @attr.options.editType "yield")}} | ||||
|     {{yield}} | ||||
|   {{/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> | ||||
| @@ -15,6 +15,18 @@ | ||||
|         </label> | ||||
|         <ToolbarActions> | ||||
|           {{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> | ||||
|           <CopyButton | ||||
|             class="button is-transparent" | ||||
| @@ -30,7 +42,7 @@ | ||||
|   {{/if}} | ||||
|   <div | ||||
|     {{code-mirror | ||||
|       content=@value | ||||
|       content=(or @value @example) | ||||
|       extraKeys=@extraKeys | ||||
|       gutters=@gutters | ||||
|       lineNumbers=(if @readOnly false true) | ||||
| @@ -38,6 +50,7 @@ | ||||
|       readOnly=@readOnly | ||||
|       theme=@theme | ||||
|       viewportMarg=@viewportMargin | ||||
|       onSetup=this.onSetup | ||||
|       onUpdate=this.onUpdate | ||||
|       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} [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} [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 { | ||||
| @@ -33,6 +34,12 @@ export default class JsonEditorComponent extends Component { | ||||
|     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 | ||||
|   onUpdate(...args) { | ||||
|     if (!this.args.readOnly) { | ||||
| @@ -47,4 +54,10 @@ export default class JsonEditorComponent extends Component { | ||||
|       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}} | ||||
|   class="overview-card border-radius-2" | ||||
|   data-test-overview-card-container={{@cardTitle}} | ||||
|   ...attributes | ||||
| > | ||||
|   <div class="is-flex-between" data-test-overview-card={{@cardTitle}}> | ||||
|     <h3 class="title is-5">{{@cardTitle}}</h3> | ||||
|   | ||||
| @@ -9,14 +9,16 @@ | ||||
|       <li data-test-crumb="{{idx}}"> | ||||
|         <span class="sep">/</span> | ||||
|         {{#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}} | ||||
|           {{#if breadcrumb.model}} | ||||
|             <LinkTo @route={{breadcrumb.route}} @model={{breadcrumb.model}}> | ||||
|             <LinkTo @route={{breadcrumb.route}} @model={{breadcrumb.model}} data-test-breadcrumb={{breadcrumb.label}}> | ||||
|               {{breadcrumb.label}} | ||||
|             </LinkTo> | ||||
|           {{else}} | ||||
|             <LinkTo @route={{breadcrumb.route}}> | ||||
|             <LinkTo @route={{breadcrumb.route}} data-test-breadcrumb={{breadcrumb.label}}> | ||||
|               {{breadcrumb.label}} | ||||
|             </LinkTo> | ||||
|           {{/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 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 | ||||
|  * 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() { | ||||
|   return function decorator(SuperClass) { | ||||
| interface BaseRoute extends Route { | ||||
|   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)) { | ||||
|       // eslint-disable-next-line
 | ||||
|       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 class FetchConfig extends SuperClass { | ||||
|       configModel = null; | ||||
|       configError = null; | ||||
| 
 | ||||
|     return class FetchSecretsEngineConfig extends SuperClass { | ||||
|       configModel: Model | null = null; | ||||
|       configError: AdapterError | null = null; | ||||
|       promptConfig = false; | ||||
| 
 | ||||
|       async beforeModel() { | ||||
|         super.beforeModel(...arguments); | ||||
|       async beforeModel(transition: Transition) { | ||||
|         super.beforeModel(transition); | ||||
| 
 | ||||
|         const backend = this.secretMountPath.get(); | ||||
|         const backend = this.secretMountPath.currentPath; | ||||
|         // check the store for record first
 | ||||
|         this.configModel = this.store.peekRecord('kubernetes/config', backend); | ||||
|         this.configModel = this.store.peekRecord(modelName, backend); | ||||
|         if (!this.configModel) { | ||||
|           return this.store | ||||
|             .queryRecord('kubernetes/config', { backend }) | ||||
|             .queryRecord(modelName, { backend }) | ||||
|             .then((record) => { | ||||
|               this.configModel = record; | ||||
|               this.promptConfig = false; | ||||
| @@ -68,5 +68,9 @@ export default class CodeMirrorModifier extends Modifier { | ||||
|     editor.on('focus', bind(this, this._onFocus, namedArgs)); | ||||
|  | ||||
|     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 | ||||
|  */ | ||||
|  | ||||
| export { default } from 'core/helpers/stringify'; | ||||
| export { default, stringify } from 'core/helpers/stringify'; | ||||
|   | ||||
| @@ -5,9 +5,9 @@ | ||||
|  | ||||
| import Route from '@ember/routing/route'; | ||||
| 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 { | ||||
|   @service store; | ||||
|   @service secretMountPath; | ||||
|   | ||||
| @@ -5,15 +5,15 @@ | ||||
|  | ||||
| import Route from '@ember/routing/route'; | ||||
| 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 { | ||||
|   @service store; | ||||
|   @service secretMountPath; | ||||
|  | ||||
|   async model() { | ||||
|     const backend = this.secretMountPath.get(); | ||||
|     const backend = this.secretMountPath.currentPath; | ||||
|     return this.configModel || this.store.createRecord('kubernetes/config', { backend }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -5,16 +5,16 @@ | ||||
|  | ||||
| import Route from '@ember/routing/route'; | ||||
| 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'; | ||||
|  | ||||
| @withConfig() | ||||
| @withConfig('kubernetes/config') | ||||
| export default class KubernetesOverviewRoute extends Route { | ||||
|   @service store; | ||||
|   @service secretMountPath; | ||||
|  | ||||
|   async model() { | ||||
|     const backend = this.secretMountPath.get(); | ||||
|     const backend = this.secretMountPath.currentPath; | ||||
|     return hash({ | ||||
|       promptConfig: this.promptConfig, | ||||
|       backend: this.modelFor('application'), | ||||
|   | ||||
| @@ -11,7 +11,7 @@ export default class KubernetesRolesCreateRoute extends Route { | ||||
|   @service secretMountPath; | ||||
|  | ||||
|   model() { | ||||
|     const backend = this.secretMountPath.get(); | ||||
|     const backend = this.secretMountPath.currentPath; | ||||
|     return this.store.createRecord('kubernetes/role', { backend }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -5,10 +5,10 @@ | ||||
|  | ||||
| import Route from '@ember/routing/route'; | ||||
| 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'; | ||||
|  | ||||
| @withConfig() | ||||
| @withConfig('kubernetes/config') | ||||
| export default class KubernetesRolesRoute extends Route { | ||||
|   @service store; | ||||
|   @service secretMountPath; | ||||
| @@ -17,7 +17,7 @@ export default class KubernetesRolesRoute extends Route { | ||||
|     // filter roles based on pageFilter value | ||||
|     const { pageFilter } = transition.to.queryParams; | ||||
|     const roles = this.store | ||||
|       .query('kubernetes/role', { backend: this.secretMountPath.get() }) | ||||
|       .query('kubernetes/role', { backend: this.secretMountPath.currentPath }) | ||||
|       .then((models) => | ||||
|         pageFilter | ||||
|           ? models.filter((model) => model.name.toLowerCase().includes(pageFilter.toLowerCase())) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ export default class KubernetesRoleCredentialsRoute extends Route { | ||||
|   model() { | ||||
|     return { | ||||
|       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; | ||||
|  | ||||
|   model() { | ||||
|     const backend = this.secretMountPath.get(); | ||||
|     const backend = this.secretMountPath.currentPath; | ||||
|     const { name } = this.paramsFor('roles.role'); | ||||
|     return this.store.queryRecord('kubernetes/role', { backend, name }); | ||||
|   } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ export default class KubernetesRoleEditRoute extends Route { | ||||
|   @service secretMountPath; | ||||
|  | ||||
|   model() { | ||||
|     const backend = this.secretMountPath.get(); | ||||
|     const backend = this.secretMountPath.currentPath; | ||||
|     const { name } = this.paramsFor('roles.role'); | ||||
|     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