mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	UI: Database fixes (#24947)
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/24947.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/24947.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:bug | ||||||
|  | ui: Fixed minor bugs with database secrets engine | ||||||
|  | ``` | ||||||
| @@ -56,6 +56,7 @@ export default ApplicationAdapter.extend({ | |||||||
|       return { |       return { | ||||||
|         data: { |         data: { | ||||||
|           id, |           id, | ||||||
|  |           name: id, | ||||||
|           ...data, |           ...data, | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|   | |||||||
| @@ -144,7 +144,7 @@ export default ApplicationAdapter.extend({ | |||||||
|  |  | ||||||
|   async _updateAllowedRoles(store, { role, backend, db, type = 'add' }) { |   async _updateAllowedRoles(store, { role, backend, db, type = 'add' }) { | ||||||
|     const connection = await store.queryRecord('database/connection', { backend, id: db }); |     const connection = await store.queryRecord('database/connection', { backend, id: db }); | ||||||
|     const roles = [...connection.allowed_roles]; |     const roles = [...(connection.allowed_roles || [])]; | ||||||
|     const allowedRoles = type === 'add' ? addToArray([roles, role]) : removeFromArray([roles, role]); |     const allowedRoles = type === 'add' ? addToArray([roles, role]) : removeFromArray([roles, role]); | ||||||
|     connection.allowed_roles = allowedRoles; |     connection.allowed_roles = allowedRoles; | ||||||
|     return connection.save(); |     return connection.save(); | ||||||
|   | |||||||
| @@ -59,8 +59,6 @@ export default class DatabaseConnectionEdit extends Component { | |||||||
|   async handleCreateConnection(evt) { |   async handleCreateConnection(evt) { | ||||||
|     evt.preventDefault(); |     evt.preventDefault(); | ||||||
|     const secret = this.args.model; |     const secret = this.args.model; | ||||||
|     const secretId = secret.name; |  | ||||||
|     secret.set('id', secretId); |  | ||||||
|     secret |     secret | ||||||
|       .save() |       .save() | ||||||
|       .then(() => { |       .then(() => { | ||||||
|   | |||||||
| @@ -49,9 +49,7 @@ export default Model.extend({ | |||||||
|     label: 'Connection will be verified', |     label: 'Connection will be verified', | ||||||
|     defaultValue: true, |     defaultValue: true, | ||||||
|   }), |   }), | ||||||
|   allowed_roles: attr('array', { |   allowed_roles: attr('array'), | ||||||
|     readOnly: true, |  | ||||||
|   }), |  | ||||||
|   password_policy: attr('string', { |   password_policy: attr('string', { | ||||||
|     label: 'Use custom password policy', |     label: 'Use custom password policy', | ||||||
|     editType: 'optionalText', |     editType: 'optionalText', | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ export default RESTSerializer.extend({ | |||||||
|   normalizeResponse(store, primaryModelClass, payload, id, requestType) { |   normalizeResponse(store, primaryModelClass, payload, id, requestType) { | ||||||
|     const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord']; |     const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord']; | ||||||
|     const connections = nullResponses.includes(requestType) |     const connections = nullResponses.includes(requestType) | ||||||
|       ? { name: id, backend: payload.backend } |       ? { name: payload.data.name, backend: payload.data.backend } | ||||||
|       : this.normalizeSecrets(payload); |       : this.normalizeSecrets(payload); | ||||||
|     const { modelName } = primaryModelClass; |     const { modelName } = primaryModelClass; | ||||||
|     let transformedPayload = { [modelName]: connections }; |     let transformedPayload = { [modelName]: connections }; | ||||||
| @@ -63,7 +63,8 @@ export default RESTSerializer.extend({ | |||||||
|     // filter data to only allow plugin specific attrs |     // filter data to only allow plugin specific attrs | ||||||
|     const allowedAttributes = Object.keys(data).filter((dataAttrs) => pluginAttributes.includes(dataAttrs)); |     const allowedAttributes = Object.keys(data).filter((dataAttrs) => pluginAttributes.includes(dataAttrs)); | ||||||
|     for (const key in data) { |     for (const key in data) { | ||||||
|       if (!allowedAttributes.includes(key)) { |       // All connections allow allowed_roles but it's not shown on the form | ||||||
|  |       if (key !== 'allowed_roles' && !allowedAttributes.includes(key)) { | ||||||
|         delete data[key]; |         delete data[key]; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ | |||||||
|     </EmptyState> |     </EmptyState> | ||||||
|   {{/unless}} |   {{/unless}} | ||||||
|   {{#if (and (not @model.errorMessage) (eq @roleType "dynamic"))}} |   {{#if (and (not @model.errorMessage) (eq @roleType "dynamic"))}} | ||||||
|     <Hds::Alert @type="inline" @color="warning" class="has-top-bottom-margin" as |A|> |     <Hds::Alert @type="inline" @color="warning" class="has-top-bottom-margin" data-test-credentials-warning as |A|> | ||||||
|       <A.Title>Warning</A.Title> |       <A.Title>Warning</A.Title> | ||||||
|       <A.Description> |       <A.Description> | ||||||
|         You will not be able to access these credentials later, so please copy them now. |         You will not be able to access these credentials later, so please copy them now. | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								ui/mirage/factories/database-connection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								ui/mirage/factories/database-connection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: BUSL-1.1 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import { Factory } from 'ember-cli-mirage'; | ||||||
|  |  | ||||||
|  | // For the purposes of testing, we only use a subset of fields relevant to mysql | ||||||
|  | export default Factory.extend({ | ||||||
|  |   backend: 'database', | ||||||
|  |   name: 'connection', | ||||||
|  |   plugin_name: 'mysql-database-plugin', | ||||||
|  |   verify_connection: true, | ||||||
|  |   connection_url: '{{username}}:{{password}}@tcp(127.0.0.1:33060)/', | ||||||
|  |   username: 'admin', | ||||||
|  |   max_open_connections: 4, | ||||||
|  |   max_idle_connections: 0, | ||||||
|  |   max_connection_lifetime: '0s', | ||||||
|  |   allowed_roles: () => [], | ||||||
|  |   root_rotation_statements: () => [ | ||||||
|  |     'SELECT user from mysql.user', | ||||||
|  |     "GRANT ALL PRIVILEGES ON *.* to 'sudo'@'%'", | ||||||
|  |   ], | ||||||
|  | }); | ||||||
							
								
								
									
										124
									
								
								ui/mirage/handlers/database.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								ui/mirage/handlers/database.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: BUSL-1.1 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import { Response } from 'miragejs'; | ||||||
|  |  | ||||||
|  | export default function (server) { | ||||||
|  |   const getRecord = (schema, req, dbKey) => { | ||||||
|  |     const { backend, name } = req.params; | ||||||
|  |     const record = schema.db[dbKey].findBy({ name, backend }); | ||||||
|  |     if (record) { | ||||||
|  |       delete record.backend; | ||||||
|  |       delete record.id; | ||||||
|  |     } | ||||||
|  |     return record ? { data: record } : new Response(404, {}, { errors: [] }); | ||||||
|  |   }; | ||||||
|  |   const createOrUpdateRecord = (schema, req, key) => { | ||||||
|  |     const { backend, name } = req.params; | ||||||
|  |     const payload = JSON.parse(req.requestBody); | ||||||
|  |     const record = schema[key].findOrCreateBy({ name, backend }); | ||||||
|  |     record.update(payload); | ||||||
|  |     return new Response(204); | ||||||
|  |   }; | ||||||
|  |   const deleteRecord = (schema, req, dbKey) => { | ||||||
|  |     const { name } = req.params; | ||||||
|  |     const record = schema.db[dbKey].findBy({ name }); | ||||||
|  |     if (record) { | ||||||
|  |       schema.db[dbKey].remove(record.id); | ||||||
|  |     } | ||||||
|  |     return new Response(204); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // Connection mgmt | ||||||
|  |   server.get('/:backend/config/:name', (schema, req) => { | ||||||
|  |     return getRecord(schema, req, 'database/connections'); | ||||||
|  |   }); | ||||||
|  |   server.get('/:backend/config', (schema) => { | ||||||
|  |     const keys = schema.db['databaseConnections'].map((record) => record.name); | ||||||
|  |     if (!keys.length) { | ||||||
|  |       return new Response(404, {}, { errors: [] }); | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       data: { | ||||||
|  |         keys, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  |   server.post('/:backend/config/:name', (schema, req) => { | ||||||
|  |     const { name } = req.params; | ||||||
|  |     const { username } = JSON.parse(req.requestBody); | ||||||
|  |     if (name === 'bad-connection') { | ||||||
|  |       return new Response( | ||||||
|  |         500, | ||||||
|  |         {}, | ||||||
|  |         { | ||||||
|  |           errors: [ | ||||||
|  |             `error creating database object: error verifying - ping: Error 1045 (28000): Access denied for user '${username}'@'192.168.65.1' (using password: YES)`, | ||||||
|  |           ], | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return createOrUpdateRecord(schema, req, 'database/connections'); | ||||||
|  |   }); | ||||||
|  |   server.delete('/:backend/config/:name', (schema, req) => { | ||||||
|  |     return deleteRecord(schema, req, 'database-connection'); | ||||||
|  |   }); | ||||||
|  |   // Rotate root | ||||||
|  |   server.post('/:backend/rotate-root/:name', (schema, req) => { | ||||||
|  |     const { name } = req.params; | ||||||
|  |     if (name === 'fail-rotate') { | ||||||
|  |       return new Response( | ||||||
|  |         500, | ||||||
|  |         {}, | ||||||
|  |         { | ||||||
|  |           errors: [ | ||||||
|  |             "1 error occurred:\n\t* failed to update user: failed to change password: Error 1045 (28000): Access denied for user 'admin'@'%' (using password: YES)\n\n", | ||||||
|  |           ], | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return new Response(204); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Generate credentials | ||||||
|  |   server.get('/:backend/creds/:role', (schema, req) => { | ||||||
|  |     const { role } = req.params; | ||||||
|  |     if (role === 'static-role') { | ||||||
|  |       // static creds | ||||||
|  |       return { | ||||||
|  |         request_id: 'static-1234', | ||||||
|  |         lease_id: '', | ||||||
|  |         renewable: false, | ||||||
|  |         lease_duration: 0, | ||||||
|  |         data: { | ||||||
|  |           last_vault_rotation: '2024-01-18T10:45:47.227193-06:00', | ||||||
|  |           password: 'generated-password', | ||||||
|  |           rotation_period: 86400, | ||||||
|  |           ttl: 3600, | ||||||
|  |           username: 'static-username', | ||||||
|  |         }, | ||||||
|  |         wrap_info: null, | ||||||
|  |         warnings: null, | ||||||
|  |         auth: null, | ||||||
|  |         mount_type: 'database', | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     // dynamic creds | ||||||
|  |     return { | ||||||
|  |       request_id: 'dynamic-1234', | ||||||
|  |       lease_id: `database/creds/${role}/abcd`, | ||||||
|  |       renewable: true, | ||||||
|  |       lease_duration: 3600, | ||||||
|  |       data: { | ||||||
|  |         password: 'generated-password', | ||||||
|  |         username: 'generated-username', | ||||||
|  |       }, | ||||||
|  |       wrap_info: null, | ||||||
|  |       warnings: null, | ||||||
|  |       auth: null, | ||||||
|  |       mount_type: 'database', | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Copyright (c) HashiCorp, Inc. |  | ||||||
|  * SPDX-License-Identifier: BUSL-1.1 |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| export default function (server) { |  | ||||||
|   server.get('/database/static-roles', function () { |  | ||||||
|     return { |  | ||||||
|       data: { keys: ['dev-static', 'prod-static'] }, |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   server.get('/database/static-roles/:rolename', function (db, req) { |  | ||||||
|     if (req.params.rolename.includes('tester')) { |  | ||||||
|       return new Response(400); |  | ||||||
|     } |  | ||||||
|     return { |  | ||||||
|       data: { |  | ||||||
|         rotation_statements: [ |  | ||||||
|           '{ "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] }', |  | ||||||
|         ], |  | ||||||
|         db_name: 'connection', |  | ||||||
|         username: 'alice', |  | ||||||
|         rotation_period: '1h', |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   server.post('/database/rotate-role/:rolename', function () { |  | ||||||
|     return new Response(204); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| @@ -9,7 +9,7 @@ import base from './base'; | |||||||
| import chrootNamespace from './chroot-namespace'; | import chrootNamespace from './chroot-namespace'; | ||||||
| import customMessages from './custom-messages'; | import customMessages from './custom-messages'; | ||||||
| import clients from './clients'; | import clients from './clients'; | ||||||
| import db from './db'; | import database from './database'; | ||||||
| import hcpLink from './hcp-link'; | import hcpLink from './hcp-link'; | ||||||
| import kms from './kms'; | import kms from './kms'; | ||||||
| import kubernetes from './kubernetes'; | import kubernetes from './kubernetes'; | ||||||
| @@ -24,7 +24,7 @@ export { | |||||||
|   base, |   base, | ||||||
|   chrootNamespace, |   chrootNamespace, | ||||||
|   clients, |   clients, | ||||||
|   db, |   database, | ||||||
|   hcpLink, |   hcpLink, | ||||||
|   kms, |   kms, | ||||||
|   kubernetes, |   kubernetes, | ||||||
|   | |||||||
							
								
								
									
										336
									
								
								ui/tests/acceptance/secrets/backend/database/workflow-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								ui/tests/acceptance/secrets/backend/database/workflow-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,336 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: BUSL-1.1 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import { module, test } from 'qunit'; | ||||||
|  | import { v4 as uuidv4 } from 'uuid'; | ||||||
|  | import { Response } from 'miragejs'; | ||||||
|  | import { click, currentURL, fillIn, visit } from '@ember/test-helpers'; | ||||||
|  | import { setupMirage } from 'ember-cli-mirage/test-support'; | ||||||
|  | import { create } from 'ember-cli-page-object'; | ||||||
|  |  | ||||||
|  | import ENV from 'vault/config/environment'; | ||||||
|  | import { setupApplicationTest } from 'vault/tests/helpers'; | ||||||
|  | import authPage from 'vault/tests/pages/auth'; | ||||||
|  | import flashMessage from 'vault/tests/pages/components/flash-message'; | ||||||
|  | import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; | ||||||
|  |  | ||||||
|  | const flash = create(flashMessage); | ||||||
|  |  | ||||||
|  | const PAGE = { | ||||||
|  |   // GENERIC | ||||||
|  |   emptyStateTitle: '[data-test-empty-state-title]', | ||||||
|  |   emptyStateAction: '[data-test-secret-create="connections"]', | ||||||
|  |   infoRow: '[data-test-component="info-table-row"]', | ||||||
|  |   infoRowLabel: (label) => `[data-test-row-label="${label}"]`, | ||||||
|  |   infoRowValue: (label) => `[data-test-row-value="${label}"]`, | ||||||
|  |   infoRowValueDiv: (label) => `[data-test-value-div="${label}"]`, | ||||||
|  |   // CONNECTIONS | ||||||
|  |   rotateModal: '[data-test-db-connection-modal-title]', | ||||||
|  |   confirmRotate: '[data-test-enable-rotate-connection]', | ||||||
|  |   skipRotate: '[data-test-enable-connection]', | ||||||
|  |   // ROLES | ||||||
|  |   addRole: '[data-test-secret-create]', | ||||||
|  |   roleSettingsSection: '[data-test-role-settings-section]', | ||||||
|  |   statementsSection: '[data-test-statements-section]', | ||||||
|  |   editRole: '[data-test-edit-link]', | ||||||
|  |   generateCredentials: (type = 'dynamic') => `[data-test-database-role-creds="${type}"]`, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const FORM = { | ||||||
|  |   inputByAttr: (attr) => `[data-test-input="${attr}"]`, | ||||||
|  |   creationStatement: (idx = 0) => | ||||||
|  |     `[data-test-input="creation_statements"] [data-test-string-list-input="${idx}"]`, | ||||||
|  |   saveBtn: '[data-test-secret-save]', | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | async function fillOutConnection(name) { | ||||||
|  |   await fillIn(FORM.inputByAttr('name'), name); | ||||||
|  |   await fillIn(FORM.inputByAttr('plugin_name'), 'mysql-database-plugin'); | ||||||
|  |   await fillIn(FORM.inputByAttr('connection_url'), '{{username}}:{{password}}@tcp(127.0.0.1:33060)/'); | ||||||
|  |   await fillIn(FORM.inputByAttr('username'), 'admin'); | ||||||
|  |   await fillIn(FORM.inputByAttr('password'), 'very-secure'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This test set is for testing the flow for database secrets engine. | ||||||
|  |  */ | ||||||
|  | module('Acceptance | database workflow', function (hooks) { | ||||||
|  |   setupApplicationTest(hooks); | ||||||
|  |   setupMirage(hooks); | ||||||
|  |  | ||||||
|  |   hooks.before(function () { | ||||||
|  |     ENV['ember-cli-mirage'].handler = 'database'; | ||||||
|  |   }); | ||||||
|  |   hooks.after(function () { | ||||||
|  |     ENV['ember-cli-mirage'].handler = null; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   hooks.beforeEach(async function () { | ||||||
|  |     this.backend = `db-workflow-${uuidv4()}`; | ||||||
|  |     this.store = this.owner.lookup('service:store'); | ||||||
|  |     await authPage.login(); | ||||||
|  |     await runCmd(mountEngineCmd('database', this.backend), false); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   hooks.afterEach(async function () { | ||||||
|  |     await authPage.login(); | ||||||
|  |     return runCmd(deleteEngineCmd(this.backend)); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   module('connections', function (hooks) { | ||||||
|  |     hooks.beforeEach(function () { | ||||||
|  |       this.expectedRows = [ | ||||||
|  |         { label: 'Database plugin', value: 'mysql-database-plugin' }, | ||||||
|  |         { label: 'Connection name', value: `connect-${this.backend}` }, | ||||||
|  |         { label: 'Use custom password policy', value: 'Default' }, | ||||||
|  |         { label: 'Connection URL', value: '{{username}}:{{password}}@tcp(127.0.0.1:33060)/' }, | ||||||
|  |         { label: 'Max open connections', value: '4' }, | ||||||
|  |         { label: 'Max idle connections', value: '0' }, | ||||||
|  |         { label: 'Max connection lifetime', value: '0s' }, | ||||||
|  |         { label: 'Username template', value: 'Default' }, | ||||||
|  |         { | ||||||
|  |           label: 'Root rotation statements', | ||||||
|  |           value: `Default`, | ||||||
|  |         }, | ||||||
|  |       ]; | ||||||
|  |     }); | ||||||
|  |     test('create with rotate', async function (assert) { | ||||||
|  |       assert.expect(24); | ||||||
|  |       this.server.post('/:backend/rotate-root/:name', () => { | ||||||
|  |         assert.ok(true, 'rotate root called'); | ||||||
|  |         new Response(204); | ||||||
|  |       }); | ||||||
|  |       await visit(`/vault/secrets/${this.backend}/overview`); | ||||||
|  |       assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct'); | ||||||
|  |       await click(PAGE.emptyStateAction); | ||||||
|  |       assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page'); | ||||||
|  |  | ||||||
|  |       // fill in connection details | ||||||
|  |       await fillOutConnection(`connect-${this.backend}`); | ||||||
|  |       await click(FORM.saveBtn); | ||||||
|  |  | ||||||
|  |       assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown'); | ||||||
|  |       await click(PAGE.confirmRotate); | ||||||
|  |  | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/show/connect-${this.backend}`, | ||||||
|  |         'Takes you to details page for connection' | ||||||
|  |       ); | ||||||
|  |       assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows'); | ||||||
|  |       this.expectedRows.forEach(({ label, value }) => { | ||||||
|  |         assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`); | ||||||
|  |         assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     test('create without rotate', async function (assert) { | ||||||
|  |       assert.expect(23); | ||||||
|  |       this.server.post('/:backend/rotate-root/:name', () => { | ||||||
|  |         assert.notOk(true, 'rotate root called when it should not have been'); | ||||||
|  |         new Response(204); | ||||||
|  |       }); | ||||||
|  |       await visit(`/vault/secrets/${this.backend}/overview`); | ||||||
|  |       assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct'); | ||||||
|  |       await click(PAGE.emptyStateAction); | ||||||
|  |       assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page'); | ||||||
|  |  | ||||||
|  |       // fill in connection details | ||||||
|  |       await fillOutConnection(`connect-${this.backend}`); | ||||||
|  |       await click(FORM.saveBtn); | ||||||
|  |  | ||||||
|  |       assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown'); | ||||||
|  |       await click(PAGE.skipRotate); | ||||||
|  |  | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/show/connect-${this.backend}`, | ||||||
|  |         'Takes you to details page for connection' | ||||||
|  |       ); | ||||||
|  |       assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows'); | ||||||
|  |       this.expectedRows.forEach(({ label, value }) => { | ||||||
|  |         assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`); | ||||||
|  |         assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     test('create failure', async function (assert) { | ||||||
|  |       assert.expect(25); | ||||||
|  |       this.server.post('/:backend/rotate-root/:name', (schema, req) => { | ||||||
|  |         const okay = req.params.name !== 'bad-connection'; | ||||||
|  |         assert.ok(okay, 'rotate root called but not for bad-connection'); | ||||||
|  |         new Response(204); | ||||||
|  |       }); | ||||||
|  |       await visit(`/vault/secrets/${this.backend}/overview`); | ||||||
|  |       assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct'); | ||||||
|  |       await click(PAGE.emptyStateAction); | ||||||
|  |       assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page'); | ||||||
|  |  | ||||||
|  |       // fill in connection details | ||||||
|  |       await fillOutConnection(`bad-connection`); | ||||||
|  |       await click(FORM.saveBtn); | ||||||
|  |       assert.strictEqual( | ||||||
|  |         flash.latestMessage, | ||||||
|  |         `error creating database object: error verifying - ping: Error 1045 (28000): Access denied for user 'admin'@'192.168.65.1' (using password: YES)`, | ||||||
|  |         'shows the error message from API' | ||||||
|  |       ); | ||||||
|  |       await fillIn(FORM.inputByAttr('name'), `connect-${this.backend}`); | ||||||
|  |       await click(FORM.saveBtn); | ||||||
|  |       assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown'); | ||||||
|  |       await click(PAGE.confirmRotate); | ||||||
|  |  | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/show/connect-${this.backend}`, | ||||||
|  |         'Takes you to details page for connection' | ||||||
|  |       ); | ||||||
|  |       assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows'); | ||||||
|  |       this.expectedRows.forEach(({ label, value }) => { | ||||||
|  |         assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`); | ||||||
|  |         assert.dom(PAGE.infoRowValue(label)).hasText(value, `Value for ${label} is correct`); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     test('create connection with rotate failure', async function (assert) { | ||||||
|  |       await visit(`/vault/secrets/${this.backend}/overview`); | ||||||
|  |       assert.dom(PAGE.emptyStateTitle).hasText('Connect a database', 'empty state title is correct'); | ||||||
|  |       await click(PAGE.emptyStateAction); | ||||||
|  |       assert.strictEqual(currentURL(), `/vault/secrets/${this.backend}/create`, 'Takes you to create page'); | ||||||
|  |  | ||||||
|  |       // fill in connection details | ||||||
|  |       await fillOutConnection(`fail-rotate`); | ||||||
|  |       await click(FORM.saveBtn); | ||||||
|  |       assert.dom(PAGE.rotateModal).hasText('Rotate your root credentials?', 'rotate modal is shown'); | ||||||
|  |       await click(PAGE.confirmRotate); | ||||||
|  |  | ||||||
|  |       assert.strictEqual( | ||||||
|  |         flash.latestMessage, | ||||||
|  |         `Error rotating root credentials: 1 error occurred: * failed to update user: failed to change password: Error 1045 (28000): Access denied for user 'admin'@'%' (using password: YES)`, | ||||||
|  |         'shows the error message from API' | ||||||
|  |       ); | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/show/fail-rotate`, | ||||||
|  |         'Takes you to details page for connection' | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   module('roles', function (hooks) { | ||||||
|  |     hooks.beforeEach(async function () { | ||||||
|  |       this.connection = `connect-${this.backend}`; | ||||||
|  |       await visit(`/vault/secrets/${this.backend}/create`); | ||||||
|  |       await fillOutConnection(this.connection); | ||||||
|  |       await click(FORM.saveBtn); | ||||||
|  |       await visit(`/vault/secrets/${this.backend}/show/${this.connection}`); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     test('it creates a dynamic role attached to the current connection', async function (assert) { | ||||||
|  |       const roleName = 'dynamic-role'; | ||||||
|  |       await click(PAGE.addRole); | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/create?initialKey=${this.connection}&itemType=role`, | ||||||
|  |         'Takes you to create role page' | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       assert | ||||||
|  |         .dom(`${PAGE.roleSettingsSection} ${PAGE.emptyStateTitle}`) | ||||||
|  |         .hasText('No role type selected', 'roles section shows empty state before selecting role type'); | ||||||
|  |       assert | ||||||
|  |         .dom(`${PAGE.statementsSection} ${PAGE.emptyStateTitle}`) | ||||||
|  |         .hasText('No role type selected', 'statements section shows empty state before selecting role type'); | ||||||
|  |  | ||||||
|  |       await fillIn(FORM.inputByAttr('name'), roleName); | ||||||
|  |       assert.dom('[data-test-selected-option]').hasText(this.connection, 'Connection is selected by default'); | ||||||
|  |  | ||||||
|  |       await fillIn(FORM.inputByAttr('type'), 'dynamic'); | ||||||
|  |       assert | ||||||
|  |         .dom(`${PAGE.roleSettingsSection} ${PAGE.emptyStateTitle}`) | ||||||
|  |         .doesNotExist('roles section no longer has empty state'); | ||||||
|  |       assert | ||||||
|  |         .dom(`${PAGE.statementsSection} ${PAGE.emptyStateTitle}`) | ||||||
|  |         .doesNotExist('statements section no longer has empty state'); | ||||||
|  |  | ||||||
|  |       // Fill in multiple creation statements | ||||||
|  |       await fillIn(FORM.creationStatement(), `GRANT SELECT ON *.* TO '{{name}}'@'%'`); | ||||||
|  |       await click(`[data-test-string-list-row="0"] [data-test-string-list-button="add"]`); | ||||||
|  |       await fillIn(FORM.creationStatement(1), `GRANT CREATE ON *.* TO '{{name}}'@'%'`); | ||||||
|  |       await click(FORM.saveBtn); | ||||||
|  |       // DETAILS | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/show/role/${roleName}`, | ||||||
|  |         'Takes you to details page for role after save' | ||||||
|  |       ); | ||||||
|  |       assert.dom(PAGE.infoRow).exists({ count: 7 }, 'correct number of info rows displayed'); | ||||||
|  |       [ | ||||||
|  |         { label: 'Role name', value: roleName }, | ||||||
|  |         { label: 'Connection name', value: this.connection }, | ||||||
|  |         { label: 'Type of role', value: 'dynamic' }, | ||||||
|  |         { label: 'Generated credentials’s Time-to-Live (TTL)', value: '1 hour' }, | ||||||
|  |         { label: 'Generated credentials’s maximum Time-to-Live (Max TTL)', value: '1 day' }, | ||||||
|  |         { | ||||||
|  |           label: 'Creation statements', | ||||||
|  |           value: `GRANT SELECT ON *.* TO '{{name}}'@'%',GRANT CREATE ON *.* TO '{{name}}'@'%'`, | ||||||
|  |         }, | ||||||
|  |         { label: 'Revocation statements', value: 'Default' }, | ||||||
|  |       ].forEach(({ label, value }) => { | ||||||
|  |         const valueSelector = | ||||||
|  |           label === 'Creation statements' ? PAGE.infoRowValueDiv(label) : PAGE.infoRowValue(label); | ||||||
|  |         assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`); | ||||||
|  |         assert.dom(valueSelector).hasText(value, `Value for ${label} is correct`); | ||||||
|  |       }); | ||||||
|  |       // EDIT | ||||||
|  |       await click(PAGE.editRole); | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/edit/role/${roleName}?itemType=role`, | ||||||
|  |         'Takes you to edit page for role' | ||||||
|  |       ); | ||||||
|  |       // TODO: these should be readonly not disabled | ||||||
|  |       assert.dom(FORM.inputByAttr('name')).isDisabled('Name is read-only'); | ||||||
|  |       assert.dom(FORM.inputByAttr('database')).isDisabled('Database is read-only'); | ||||||
|  |       assert.dom(FORM.inputByAttr('type')).isDisabled('Type is read-only'); | ||||||
|  |       await fillIn('[data-test-ttl-value="Generated credentials’s Time-to-Live (TTL)"]', '2'); | ||||||
|  |       await click(FORM.saveBtn); | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/show/role/${roleName}`, | ||||||
|  |         'Takes you to details page for role after save' | ||||||
|  |       ); | ||||||
|  |       assert | ||||||
|  |         .dom(PAGE.infoRowValue('Generated credentials’s Time-to-Live (TTL)')) | ||||||
|  |         .hasText('2 hours', 'Shows updated TTL'); | ||||||
|  |  | ||||||
|  |       // CREDENTIALS | ||||||
|  |       await click(PAGE.generateCredentials()); | ||||||
|  |       assert.strictEqual( | ||||||
|  |         currentURL(), | ||||||
|  |         `/vault/secrets/${this.backend}/credentials/${roleName}?roleType=dynamic`, | ||||||
|  |         'Takes you to credentials page for role' | ||||||
|  |       ); | ||||||
|  |       assert | ||||||
|  |         .dom('[data-test-credentials-warning]') | ||||||
|  |         .exists('shows warning about credentials only being available once'); | ||||||
|  |       assert | ||||||
|  |         .dom(`[data-test-value-div="Username"] [data-test-masked-input]`) | ||||||
|  |         .hasText('***********', 'Username is masked'); | ||||||
|  |       await click(`[data-test-value-div="Username"] [data-test-button="toggle-masked"]`); | ||||||
|  |       assert | ||||||
|  |         .dom(`[data-test-value-div="Username"] [data-test-masked-input]`) | ||||||
|  |         .hasText('generated-username', 'Username is generated'); | ||||||
|  |  | ||||||
|  |       assert | ||||||
|  |         .dom(`[data-test-value-div="Password"] [data-test-masked-input]`) | ||||||
|  |         .hasText('***********', 'Password is masked'); | ||||||
|  |       await click(`[data-test-value-div="Password"] [data-test-button="toggle-masked"]`); | ||||||
|  |       assert | ||||||
|  |         .dom(`[data-test-value-div="Password"] [data-test-masked-input]`) | ||||||
|  |         .hasText('generated-password', 'Password is generated'); | ||||||
|  |       assert.dom(PAGE.infoRowValue('Lease Duration')).hasText('3600', 'shows lease duration from response'); | ||||||
|  |       assert | ||||||
|  |         .dom(PAGE.infoRowValue('Lease ID')) | ||||||
|  |         .hasText(`database/creds/${roleName}/abcd`, 'shows lease ID from response'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										101
									
								
								ui/tests/unit/serializers/database/connection-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								ui/tests/unit/serializers/database/connection-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: BUSL-1.1 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import { module, test } from 'qunit'; | ||||||
|  | import { setupTest } from 'ember-qunit'; | ||||||
|  | import { v4 as uuidv4 } from 'uuid'; | ||||||
|  |  | ||||||
|  | module('Unit | Serializer | database/connection', function (hooks) { | ||||||
|  |   setupTest(hooks); | ||||||
|  |  | ||||||
|  |   hooks.beforeEach(function () { | ||||||
|  |     this.uid = uuidv4(); | ||||||
|  |     this.store = this.owner.lookup('service:store'); | ||||||
|  |   }); | ||||||
|  |   test('it should serialize only keys that are valid for the database type (elasticsearch)', function (assert) { | ||||||
|  |     const backend = `db-serializer-test-${this.uid}`; | ||||||
|  |     const name = `elastic-test-${this.uid}`; | ||||||
|  |     const record = this.store.createRecord('database/connection', { | ||||||
|  |       plugin_name: 'elasticsearch-database-plugin', | ||||||
|  |       backend, | ||||||
|  |       name, | ||||||
|  |       allowed_roles: ['readonly'], | ||||||
|  |       connection_url: 'http://localhost:9200', | ||||||
|  |       url: 'http://localhost:9200', | ||||||
|  |       username: 'elastic', | ||||||
|  |       password: 'changeme', | ||||||
|  |       tls_ca: 'some-value', | ||||||
|  |       ca_cert: undefined, // does not send undefined values | ||||||
|  |     }); | ||||||
|  |     const expectedResult = { | ||||||
|  |       plugin_name: 'elasticsearch-database-plugin', | ||||||
|  |       backend, | ||||||
|  |       name, | ||||||
|  |       verify_connection: true, | ||||||
|  |       allowed_roles: ['readonly'], | ||||||
|  |       url: 'http://localhost:9200', | ||||||
|  |       username: 'elastic', | ||||||
|  |       password: 'changeme', | ||||||
|  |       insecure: false, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const serializedRecord = record.serialize(); | ||||||
|  |     assert.deepEqual( | ||||||
|  |       serializedRecord, | ||||||
|  |       expectedResult, | ||||||
|  |       'invalid elasticsearch options were not added to the payload' | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   test('it should normalize values for the database type (elasticsearch)', function (assert) { | ||||||
|  |     const serializer = this.owner.lookup('serializer:database/connection'); | ||||||
|  |     const normalized = serializer.normalizeSecrets({ | ||||||
|  |       request_id: 'request-id', | ||||||
|  |       lease_id: '', | ||||||
|  |       renewable: false, | ||||||
|  |       lease_duration: 0, | ||||||
|  |       data: { | ||||||
|  |         allowed_roles: ['readonly'], | ||||||
|  |         connection_details: { | ||||||
|  |           backend: 'database', | ||||||
|  |           insecure: false, | ||||||
|  |           url: 'https://localhost:9200', | ||||||
|  |           username: 'root', | ||||||
|  |         }, | ||||||
|  |         password_policy: '', | ||||||
|  |         plugin_name: 'elasticsearch-database-plugin', | ||||||
|  |         plugin_version: '', | ||||||
|  |         root_credentials_rotate_statements: [], | ||||||
|  |       }, | ||||||
|  |       wrap_info: null, | ||||||
|  |       warnings: null, | ||||||
|  |       auth: null, | ||||||
|  |       mount_type: 'database', | ||||||
|  |       backend: 'database', | ||||||
|  |       id: 'elastic-test', | ||||||
|  |     }); | ||||||
|  |     const expectedResult = { | ||||||
|  |       allowed_roles: ['readonly'], | ||||||
|  |       backend: 'database', | ||||||
|  |       connection_details: { | ||||||
|  |         backend: 'database', | ||||||
|  |         insecure: false, | ||||||
|  |         url: 'https://localhost:9200', | ||||||
|  |         username: 'root', | ||||||
|  |       }, | ||||||
|  |       id: 'elastic-test', | ||||||
|  |       insecure: false, | ||||||
|  |       name: 'elastic-test', | ||||||
|  |       password_policy: '', | ||||||
|  |       plugin_name: 'elasticsearch-database-plugin', | ||||||
|  |       plugin_version: '', | ||||||
|  |       root_credentials_rotate_statements: [], | ||||||
|  |       root_rotation_statements: [], | ||||||
|  |       url: 'https://localhost:9200', | ||||||
|  |       username: 'root', | ||||||
|  |     }; | ||||||
|  |     assert.deepEqual(normalized, expectedResult, `Normalizes and flattens database response`); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw