mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +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 { | ||||
|         data: { | ||||
|           id, | ||||
|           name: id, | ||||
|           ...data, | ||||
|         }, | ||||
|       }; | ||||
|   | ||||
| @@ -144,7 +144,7 @@ export default ApplicationAdapter.extend({ | ||||
|  | ||||
|   async _updateAllowedRoles(store, { role, backend, db, type = 'add' }) { | ||||
|     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]); | ||||
|     connection.allowed_roles = allowedRoles; | ||||
|     return connection.save(); | ||||
|   | ||||
| @@ -59,8 +59,6 @@ export default class DatabaseConnectionEdit extends Component { | ||||
|   async handleCreateConnection(evt) { | ||||
|     evt.preventDefault(); | ||||
|     const secret = this.args.model; | ||||
|     const secretId = secret.name; | ||||
|     secret.set('id', secretId); | ||||
|     secret | ||||
|       .save() | ||||
|       .then(() => { | ||||
|   | ||||
| @@ -49,9 +49,7 @@ export default Model.extend({ | ||||
|     label: 'Connection will be verified', | ||||
|     defaultValue: true, | ||||
|   }), | ||||
|   allowed_roles: attr('array', { | ||||
|     readOnly: true, | ||||
|   }), | ||||
|   allowed_roles: attr('array'), | ||||
|   password_policy: attr('string', { | ||||
|     label: 'Use custom password policy', | ||||
|     editType: 'optionalText', | ||||
|   | ||||
| @@ -38,7 +38,7 @@ export default RESTSerializer.extend({ | ||||
|   normalizeResponse(store, primaryModelClass, payload, id, requestType) { | ||||
|     const nullResponses = ['updateRecord', 'createRecord', 'deleteRecord']; | ||||
|     const connections = nullResponses.includes(requestType) | ||||
|       ? { name: id, backend: payload.backend } | ||||
|       ? { name: payload.data.name, backend: payload.data.backend } | ||||
|       : this.normalizeSecrets(payload); | ||||
|     const { modelName } = primaryModelClass; | ||||
|     let transformedPayload = { [modelName]: connections }; | ||||
| @@ -63,7 +63,8 @@ export default RESTSerializer.extend({ | ||||
|     // filter data to only allow plugin specific attrs | ||||
|     const allowedAttributes = Object.keys(data).filter((dataAttrs) => pluginAttributes.includes(dataAttrs)); | ||||
|     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]; | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -40,7 +40,7 @@ | ||||
|     </EmptyState> | ||||
|   {{/unless}} | ||||
|   {{#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.Description> | ||||
|         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 customMessages from './custom-messages'; | ||||
| import clients from './clients'; | ||||
| import db from './db'; | ||||
| import database from './database'; | ||||
| import hcpLink from './hcp-link'; | ||||
| import kms from './kms'; | ||||
| import kubernetes from './kubernetes'; | ||||
| @@ -24,7 +24,7 @@ export { | ||||
|   base, | ||||
|   chrootNamespace, | ||||
|   clients, | ||||
|   db, | ||||
|   database, | ||||
|   hcpLink, | ||||
|   kms, | ||||
|   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