diff --git a/changelog/27405.txt b/changelog/27405.txt new file mode 100644 index 0000000000..ac5162e801 --- /dev/null +++ b/changelog/27405.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: AWS credentials form sets credential_type from backing role +``` diff --git a/ui/app/components/configure-aws-secret.hbs b/ui/app/components/configure-aws-secret.hbs index fafec4906b..0bd38b378e 100644 --- a/ui/app/components/configure-aws-secret.hbs +++ b/ui/app/components/configure-aws-secret.hbs @@ -40,7 +40,7 @@ autocomplete="off" spellcheck="false" @value={{@accessKey}} - data-test-aws-input="accessKey" + data-test-input="accessKey" /> @@ -56,7 +56,7 @@ name="secret" class="input" @value={{@secretKey}} - data-test-aws-input="secretKey" + data-test-input="secretKey" /> @@ -104,7 +104,7 @@ {{/if}}
- +
@@ -134,7 +134,7 @@ @onChange={{fn this.handleTtlChange "leaseMax"}} />
- +
diff --git a/ui/app/templates/components/generate-credentials.hbs b/ui/app/components/generate-credentials.hbs similarity index 57% rename from ui/app/templates/components/generate-credentials.hbs rename to ui/app/components/generate-credentials.hbs index 7b515e41f3..8481a662c9 100644 --- a/ui/app/templates/components/generate-credentials.hbs +++ b/ui/app/components/generate-credentials.hbs @@ -7,13 +7,13 @@ - - + + @@ -24,7 +24,7 @@ -{{#if this.model.hasGenerated}} +{{#if this.hasGenerated}}
{{#unless this.model.isError}} @@ -35,48 +35,42 @@ {{/unless}} - {{#each this.model.attrs as |attr|}} - {{#if (eq attr.type "object")}} - - {{else}} - {{#if - (or - (eq attr.name "key") - (eq attr.name "secretKey") - (eq attr.name "securityToken") - (eq attr.name "privateKey") - attr.options.masked - ) - }} - {{#if (get this.model attr.name)}} + {{#each this.displayFields as |key|}} + {{#let (get this.model.allByKey key) as |attr|}} + {{#if (eq attr.type "object")}} + + {{else}} + {{#if attr.options.masked}} + {{#if (get this.model attr.name)}} + + + + {{/if}} + {{else if (and (get this.model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}} + + {{else}} - - + /> {{/if}} - {{else if (and (get this.model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}} - - {{else}} - {{/if}} - {{/if}} + {{/let}} {{/each}}
@@ -106,34 +100,25 @@ @text="Back" @color="secondary" @route="vault.cluster.secrets.backend.list-root" - @model={{this.backendPath}} - data-test-secret-generate-back={{true}} + @model={{@backendPath}} + data-test-back-button /> {{else}} - + {{/if}}
{{else}} -
+
- {{#if this.model.helpText}} -

{{this.model.helpText}}

- {{/if}} - {{#if this.model.fieldGroups}} - - {{else}} - {{#each this.model.attrs as |attr|}} - - {{/each}} + {{#if this.helpText}} +

{{this.helpText}}

{{/if}} + {{#each this.formFields as |key|}} + + {{/each}}
@@ -142,14 +127,14 @@ @icon={{if this.loading "loading"}} type="submit" disabled={{this.loading}} - data-test-secret-generate={{true}} + data-test-save />
diff --git a/ui/app/components/generate-credentials.js b/ui/app/components/generate-credentials.js index 7c871cd95b..4cb4fc282f 100644 --- a/ui/app/components/generate-credentials.js +++ b/ui/app/components/generate-credentials.js @@ -4,56 +4,49 @@ */ import { service } from '@ember/service'; -import { computed, set } from '@ember/object'; -import Component from '@ember/component'; +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; -const MODEL_TYPES = { - 'ssh-sign': { - model: 'ssh-sign', - }, - 'ssh-creds': { +const CREDENTIAL_TYPES = { + ssh: { model: 'ssh-otp-credential', title: 'Generate SSH Credentials', + formFields: ['username', 'ip'], + displayFields: ['username', 'ip', 'key', 'keyType', 'port'], }, - 'aws-creds': { + aws: { model: 'aws-credential', title: 'Generate AWS Credentials', backIsListLink: true, + displayFields: ['accessKey', 'secretKey', 'securityToken', 'leaseId', 'renewable', 'leaseDuration'], + // aws form fields are dynamic + formFields: (model) => { + return { + iam_user: ['credentialType'], + assumed_role: ['credentialType', 'ttl', 'roleArn'], + federation_token: ['credentialType', 'ttl'], + session_token: ['credentialType', 'ttl'], + }[model.credentialType]; + }, }, }; -export default Component.extend({ - controlGroup: service(), - store: service(), - router: service(), - // set on the component - backendType: null, - backendPath: null, - roleName: null, - action: null, +export default class GenerateCredentials extends Component { + @service controlGroup; + @service store; + @service router; - model: null, - loading: false, - emptyData: '{\n}', + @tracked model; + @tracked loading = false; + @tracked hasGenerated = false; + emptyData = '{\n}'; - modelForType() { - const type = this.options; - if (type) { - return type.model; - } - // if we don't have a mode for that type then redirect them back to the backend list - this.router.transitionTo('vault.cluster.secrets.backend.list-root', this.backendPath); - }, - - options: computed('action', 'backendType', function () { - const action = this.action || 'creds'; - return MODEL_TYPES[`${this.backendType}-${action}`]; - }), - - init() { - this._super(...arguments); - this.createOrReplaceModel(); - }, + constructor() { + super(...arguments); + const modelType = this.modelForType(); + this.model = this.generateNewModel(modelType); + } willDestroy() { // components are torn down after store is unloaded and will cause an error if attempt to unload record @@ -61,20 +54,46 @@ export default Component.extend({ if (noTeardown && !this.model.isDestroyed && !this.model.isDestroying) { this.model.unloadRecord(); } - this._super(...arguments); - }, + super.willDestroy(); + } - createOrReplaceModel() { - const modelType = this.modelForType(); - const model = this.model; - const roleName = this.roleName; - const backendPath = this.backendPath; + modelForType() { + const type = this.options; + if (type) { + return type.model; + } + // if we don't have a mode for that type then redirect them back to the backend list + this.router.transitionTo('vault.cluster.secrets.backend.list-root', this.args.backendPath); + } + + get helpText() { + if (this.options?.model === 'aws-credential') { + return 'For Vault roles of credential type iam_user, there are no inputs, just submit the form. Choose a type to change the input options.'; + } + return ''; + } + + get options() { + return CREDENTIAL_TYPES[this.args.backendType]; + } + + get formFields() { + const typeOpts = this.options; + if (typeof typeOpts.formFields === 'function') { + return typeOpts.formFields(this.model); + } + return typeOpts.formFields; + } + + get displayFields() { + return this.options.displayFields; + } + + generateNewModel(modelType) { if (!modelType) { return; } - if (model) { - model.unloadRecord(); - } + const { roleName, backendPath, awsRoleType } = this.args; const attrs = { role: { backend: backendPath, @@ -82,44 +101,60 @@ export default Component.extend({ }, id: `${backendPath}-${roleName}`, }; - const newModel = this.store.createRecord(modelType, attrs); - this.set('model', newModel); - }, + if (awsRoleType) { + // this is only set from route if backendType = aws + attrs.credentialType = awsRoleType; + } + return this.store.createRecord(modelType, attrs); + } - actions: { - create() { - const model = this.model; - this.set('loading', true); - this.model - .save() - .then(() => { - model.set('hasGenerated', true); - }) - .catch((error) => { - // Handle control group AdapterError - if (error.message === 'Control Group encountered') { - this.controlGroup.saveTokenFromError(error); - const err = this.controlGroup.logFromError(error); - error.errors = [err.content]; - } - throw error; - }) - .finally(() => { - this.set('loading', false); - }); - }, + replaceModel() { + const modelType = this.modelForType(); + if (!modelType) { + return; + } + if (this.model) { + this.model.unloadRecord(); + } + this.model = this.generateNewModel(modelType); + } - codemirrorUpdated(attr, val, codemirror) { - codemirror.performLint(); - const hasErrors = codemirror.state.lint.marked.length > 0; + @action + create(evt) { + evt.preventDefault(); + this.loading = true; + this.model + .save() + .then(() => { + this.hasGenerated = true; + }) + .catch((error) => { + // Handle control group AdapterError + if (error.message === 'Control Group encountered') { + this.controlGroup.saveTokenFromError(error); + const err = this.controlGroup.logFromError(error); + error.errors = [err.content]; + } + throw error; + }) + .finally(() => { + this.loading = false; + }); + } - if (!hasErrors) { - set(this.model, attr, JSON.parse(val)); - } - }, + @action + codemirrorUpdated(attr, val, codemirror) { + codemirror.performLint(); + const hasErrors = codemirror.state.lint.marked.length > 0; - newModel() { - this.createOrReplaceModel(); - }, - }, -}); + if (!hasErrors) { + this.model[attr] = JSON.parse(val); + } + } + + @action + reset() { + this.hasGenerated = false; + this.replaceModel(); + } +} diff --git a/ui/app/controllers/vault/cluster/secrets/backend/credentials.js b/ui/app/controllers/vault/cluster/secrets/backend/credentials.js index a4b9d93ea8..9d6c74e99e 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/credentials.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/credentials.js @@ -6,11 +6,10 @@ import Controller from '@ember/controller'; export default Controller.extend({ - queryParams: ['action', 'roleType'], - action: '', + queryParams: ['roleType'], + // used for database credentials roleType: '', reset() { - this.set('action', ''); this.set('roleType', ''); }, }); diff --git a/ui/app/models/aws-credential.js b/ui/app/models/aws-credential.js index 7199ad75bf..7f5d6f057c 100644 --- a/ui/app/models/aws-credential.js +++ b/ui/app/models/aws-credential.js @@ -4,8 +4,8 @@ */ import Model, { attr } from '@ember-data/model'; -import { computed } from '@ember/object'; -import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; +import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; + const CREDENTIAL_TYPES = [ { value: 'iam_user', @@ -25,27 +25,28 @@ const CREDENTIAL_TYPES = [ }, ]; -const DISPLAY_FIELDS = ['accessKey', 'secretKey', 'securityToken', 'leaseId', 'renewable', 'leaseDuration']; -export default Model.extend({ - helpText: - 'For Vault roles of credential type iam_user, there are no inputs, just submit the form. Choose a type to change the input options.', - role: attr('object', { +@withExpandedAttributes() +export default class AwsCredential extends Model { + @attr('object', { readOnly: true, - }), + }) + role; - credentialType: attr('string', { + @attr('string', { defaultValue: 'iam_user', possibleValues: CREDENTIAL_TYPES, readOnly: true, - }), + }) + credentialType; - roleArn: attr('string', { + @attr('string', { label: 'Role ARN', helpText: 'The ARN of the role to assume if credential_type on the Vault role is assumed_role. Optional if the role has a single role ARN; required otherwise.', - }), + }) + roleArn; - ttl: attr({ + @attr({ editType: 'ttl', defaultValue: '3600s', setDefault: true, @@ -53,29 +54,17 @@ export default Model.extend({ label: 'TTL', helpText: 'Specifies the TTL for the use of the STS token. Valid only when credential_type is assumed_role, federation_token, or session_token.', - }), - leaseId: attr('string'), - renewable: attr('boolean'), - leaseDuration: attr('number'), - accessKey: attr('string'), - secretKey: attr('string'), - securityToken: attr('string'), + }) + ttl; - attrs: computed('credentialType', 'accessKey', 'securityToken', function () { - const type = this.credentialType; - const fieldsForType = { - iam_user: ['credentialType'], - assumed_role: ['credentialType', 'ttl', 'roleArn'], - federation_token: ['credentialType', 'ttl'], - session_token: ['credentialType', 'ttl'], - }; - if (this.accessKey || this.securityToken) { - return expandAttributeMeta(this, DISPLAY_FIELDS.slice(0)); - } - return expandAttributeMeta(this, fieldsForType[type].slice(0)); - }), + @attr('string') leaseId; + @attr('boolean') renewable; + @attr('number') leaseDuration; + @attr('string') accessKey; + @attr('string', { masked: true }) secretKey; + @attr('string', { masked: true }) securityToken; - toCreds: computed('accessKey', 'secretKey', 'securityToken', 'leaseId', function () { + get toCreds() { const props = { accessKey: this.accessKey, secretKey: this.secretKey, @@ -90,5 +79,5 @@ export default Model.extend({ return ret; }, {}); return JSON.stringify(propsWithVals, null, 2); - }), -}); + } +} diff --git a/ui/app/models/ssh-otp-credential.js b/ui/app/models/ssh-otp-credential.js index 13b715589d..a0576e8acf 100644 --- a/ui/app/models/ssh-otp-credential.js +++ b/ui/app/models/ssh-otp-credential.js @@ -3,27 +3,25 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { reads } from '@ember/object/computed'; import Model, { attr } from '@ember-data/model'; -import { computed } from '@ember/object'; -import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; -const CREATE_FIELDS = ['username', 'ip']; +import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; -const DISPLAY_FIELDS = ['username', 'ip', 'key', 'keyType', 'port']; -export default Model.extend({ - role: attr('object', { +@withExpandedAttributes() +export default class SshOtpCredential extends Model { + @attr('object', { readOnly: true, - }), - ip: attr('string', { + }) + role; + @attr('string', { label: 'IP Address', - }), - username: attr('string'), - key: attr('string'), - keyType: attr('string'), - port: attr('number'), - attrs: computed('key', function () { - const keys = this.key ? DISPLAY_FIELDS.slice(0) : CREATE_FIELDS.slice(0); - return expandAttributeMeta(this, keys); - }), - toCreds: reads('key'), -}); + }) + ip; + @attr('string') username; + @attr('string', { masked: true }) key; + @attr('string') keyType; + @attr('number') port; + + get toCreds() { + return this.key; + } +} diff --git a/ui/app/routes/vault/cluster/secrets/backend/credentials.js b/ui/app/routes/vault/cluster/secrets/backend/credentials.js index 8607d0c1b1..21f0dbff65 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/credentials.js +++ b/ui/app/routes/vault/cluster/secrets/backend/credentials.js @@ -17,12 +17,15 @@ export default Route.extend({ store: service(), beforeModel() { - const { backend } = this.paramsFor('vault.cluster.secrets.backend'); - if (backend != 'ssh') { - return; + const { id: backendPath, type: backendType } = this.modelFor('vault.cluster.secrets.backend'); + // redirect if the backend type does not support credentials + if (!SUPPORTED_DYNAMIC_BACKENDS.includes(backendType)) { + return this.router.transitionTo('vault.cluster.secrets.backend.list-root', backendPath); + } + // hydrate model if backend type is ssh + if (backendType === 'ssh') { + this.pathHelp.getNewModel('ssh-otp-credential', backendPath); } - const modelType = 'ssh-otp-credential'; - return this.pathHelp.getNewModel(modelType, backend); }, getDatabaseCredential(backend, secret, roleType = '') { @@ -51,23 +54,34 @@ export default Route.extend({ }); }, + async getAwsRole(backend, id) { + try { + const role = await this.store.queryRecord('role-aws', { backend, id }); + return role; + } catch (e) { + // swallow error, non-essential data + return; + } + }, + async model(params) { const role = params.secret; const { id: backendPath, type: backendType } = this.modelFor('vault.cluster.secrets.backend'); const roleType = params.roleType; - let dbCred; + let dbCred, awsRole; if (backendType === 'database') { dbCred = await this.getDatabaseCredential(backendPath, role, roleType); + } else if (backendType === 'aws') { + awsRole = await this.getAwsRole(backendPath, role); } - if (!SUPPORTED_DYNAMIC_BACKENDS.includes(backendType)) { - return this.router.transitionTo('vault.cluster.secrets.backend.list-root', backendPath); - } + return resolve({ backendPath, backendType, roleName: role, roleType, dbCred, + awsRoleType: awsRole?.credentialType, }); }, diff --git a/ui/app/templates/components/role-aws-edit.hbs b/ui/app/templates/components/role-aws-edit.hbs index 732b09fbd8..cd9fd2b11f 100644 --- a/ui/app/templates/components/role-aws-edit.hbs +++ b/ui/app/templates/components/role-aws-edit.hbs @@ -76,7 +76,7 @@
- + {{#if (eq this.mode "create")}} {{else}} - {{! TODO smells a little to have action off of query param requiring a conditional }} {{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/secrets/backend/sign.hbs b/ui/app/templates/vault/cluster/secrets/backend/sign.hbs index 91f19600b6..5a332483d7 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/sign.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/sign.hbs @@ -67,12 +67,7 @@
{{/if}}
- +
{{else}} @@ -113,14 +108,14 @@ @icon={{if this.loading "loading"}} type="submit" disabled={{this.loading}} - data-test-secret-generate + data-test-save /> diff --git a/ui/tests/acceptance/aws-test.js b/ui/tests/acceptance/aws-test.js index b9ac6669f5..2f79bc358e 100644 --- a/ui/tests/acceptance/aws-test.js +++ b/ui/tests/acceptance/aws-test.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click, fillIn, currentURL, find, settled, waitUntil } from '@ember/test-helpers'; +import { click, fillIn, currentURL, find, settled, waitUntil, visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { v4 as uuidv4 } from 'uuid'; @@ -13,7 +13,63 @@ import { GENERAL } from '../helpers/general-selectors'; import authPage from 'vault/tests/pages/auth'; import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; import { setupMirage } from 'ember-cli-mirage/test-support'; +import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; +import { overrideResponse } from 'vault/tests/helpers/stubs'; +const AWS_CREDS = { + configTab: '[data-test-configuration-tab]', + configure: '[data-test-secret-backend-configure]', + awsForm: '[data-test-aws-root-creds-form]', + viewBackend: '[data-test-backend-view-link]', + createSecret: '[data-test-secret-create]', + secretHeader: '[data-test-secret-header]', + secretLink: (name) => (name ? `[data-test-secret-link="${name}"]` : '[data-test-secret-link]'), + crumb: (path) => `[data-test-secret-breadcrumb="${path}"] a`, + ttlToggle: '[data-test-ttl-toggle="TTL"]', + warning: '[data-test-warning]', + delete: (role) => `[data-test-aws-role-delete="${role}"]`, + backButton: '[data-test-back-button]', + generateLink: '[data-test-backend-credentials]', +}; +const ROLE_TYPES = [ + { + credentialType: 'iam_user', + async fillOutForm(assert) { + // nothing to fill out + assert.dom('[data-test-field]').exists({ count: 1 }); + }, + expectedPayload: {}, + }, + { + credentialType: 'assumed_role', + async fillOutForm(assert) { + await click(GENERAL.toggleInput('TTL')); + assert.dom(GENERAL.toggleInput('TTL')).isNotChecked(); + await fillIn(GENERAL.inputByAttr('roleArn'), 'foobar'); + }, + expectedPayload: { + role_arn: 'foobar', + }, + }, + { + credentialType: 'federation_token', + async fillOutForm(assert) { + assert.dom(GENERAL.toggleInput('TTL')).isChecked(); + await fillIn(GENERAL.ttl.input('TTL'), '3'); + }, + expectedPayload: { + ttl: '10800s', + }, + }, + { + credentialType: 'session_token', + async fillOutForm(assert) { + await click(GENERAL.toggleInput('TTL')); + assert.dom(GENERAL.toggleInput('TTL')).isNotChecked(); + }, + expectedPayload: null, + }, +]; module('Acceptance | aws secret backend', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); @@ -30,28 +86,23 @@ module('Acceptance | aws secret backend', function (hooks) { test('aws backend', async function (assert) { const path = `aws-${this.uid}`; const roleName = 'awsrole'; - this.server.post(`/${path}/creds/${roleName}`, (_, req) => { - const payload = JSON.parse(req.requestBody); - assert.deepEqual(payload, { role_arn: 'foobar' }, 'does not send TTL when unchecked'); - return {}; - }); await enablePage.enable('aws', path); await settled(); - await click('[data-test-configuration-tab]'); + await click(AWS_CREDS.configTab); - await click('[data-test-secret-backend-configure]'); + await click(AWS_CREDS.configure); assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${path}`); - assert.dom('[data-test-aws-root-creds-form]').exists(); + assert.dom(AWS_CREDS.awsForm).exists(); assert.dom(GENERAL.tab('access-to-aws')).exists('renders the root creds tab'); assert.dom(GENERAL.tab('lease')).exists('renders the leases config tab'); - await fillIn('[data-test-aws-input="accessKey"]', 'foo'); - await fillIn('[data-test-aws-input="secretKey"]', 'bar'); + await fillIn(GENERAL.inputByAttr('accessKey'), 'foo'); + await fillIn(GENERAL.inputByAttr('secretKey'), 'bar'); - await click('[data-test-aws-input="root-save"]'); + await click(GENERAL.saveButton); assert.true( this.flashSuccessSpy.calledWith('The backend configuration saved successfully!'), @@ -60,53 +111,133 @@ module('Acceptance | aws secret backend', function (hooks) { await click(GENERAL.tab('lease')); - await click('[data-test-aws-input="lease-save"]'); + await click(GENERAL.saveButton); assert.true( this.flashSuccessSpy.calledTwice, 'a new success flash message is rendered upon saving lease' ); - await click('[data-test-backend-view-link]'); + await click(AWS_CREDS.viewBackend); assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`, 'navigates to the roles list'); - await click('[data-test-secret-create]'); + await click(AWS_CREDS.createSecret); - assert.dom('[data-test-secret-header]').hasText('Create an AWS Role', 'aws: renders the create page'); + assert.dom(AWS_CREDS.secretHeader).hasText('Create an AWS Role', 'aws: renders the create page'); - await fillIn('[data-test-input="name"]', roleName); + await fillIn(GENERAL.inputByAttr('name'), roleName); // save the role - await click('[data-test-role-aws-create]'); + await click(GENERAL.saveButton); await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this assert.strictEqual( currentURL(), `/vault/secrets/${path}/show/${roleName}`, 'aws: navigates to the show page on creation' ); - await click(`[data-test-secret-breadcrumb="${path}"] a`); + await click(AWS_CREDS.crumb(path)); assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`); - assert.dom(`[data-test-secret-link="${roleName}"]`).exists(); - - // check that generates credentials flow is correct - await click(`[data-test-secret-link="${roleName}"]`); - assert.dom('h1').hasText('Generate AWS Credentials'); - assert.dom('[data-test-input="credentialType"]').hasValue('iam_user'); - await fillIn('[data-test-input="credentialType"]', 'assumed_role'); - await click('[data-test-ttl-toggle="TTL"]'); - assert.dom('[data-test-ttl-toggle="TTL"]').isNotChecked(); - await fillIn('[data-test-input="roleArn"]', 'foobar'); - await click('[data-test-secret-generate]'); - assert.dom('[data-test-warning]').exists('Shows access warning after generation'); - await click('[data-test-secret-generate-back]'); + assert.dom(AWS_CREDS.secretLink(roleName)).exists(); //and delete - await click(`[data-test-secret-link="${roleName}"] [data-test-popup-menu-trigger]`); - await waitUntil(() => find(`[data-test-aws-role-delete="${roleName}"]`)); // flaky without - await click(`[data-test-aws-role-delete="${roleName}"]`); + await click(`${AWS_CREDS.secretLink(roleName)} [data-test-popup-menu-trigger]`); + await waitUntil(() => find(AWS_CREDS.delete(roleName))); // flaky without + await click(AWS_CREDS.delete(roleName)); await click(GENERAL.confirmButton); - assert.dom(`[data-test-secret-link="${roleName}"]`).doesNotExist('aws: role is no longer in the list'); + assert.dom(AWS_CREDS.secretLink(roleName)).doesNotExist('aws: role is no longer in the list'); + }); + + ROLE_TYPES.forEach((scenario) => { + test(`aws credentials - type ${scenario.credentialType}`, async function (assert) { + const path = `aws-cred-${this.uid}`; + const roleName = `awsrole-${scenario.credentialType}`; + this.server.post(`/${path}/creds/${roleName}`, (_, req) => { + const payload = JSON.parse(req.requestBody); + assert.deepEqual(payload, scenario.expectedPayload); + return { + data: { + access_key: 'AKIA...', + secret_key: 'xlCs...', + security_token: 'some-token', + arn: 'arn:aws:sts::123456789012:assumed-role/DeveloperRole/some-user-supplied-role-session-name', + }, + }; + }); + this.server.get(`/${path}/creds/${roleName}`, () => { + return { + data: { + access_key: 'AKIA...', + secret_key: 'xlCs...', + security_token: 'some-token', + arn: 'arn:aws:sts::123456789012:assumed-role/DeveloperRole/some-user-supplied-role-session-name', + }, + }; + }); + await runCmd(mountEngineCmd('aws', path)); + + await visit(`/vault/secrets/${path}/create`); + assert.dom('h1').hasText('Create an AWS Role'); + await fillIn(GENERAL.inputByAttr('name'), roleName); + await fillIn(GENERAL.inputByAttr('credentialType'), scenario.credentialType); + await click(GENERAL.saveButton); + await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this + assert.strictEqual(currentURL(), `/vault/secrets/${path}/show/${roleName}`); + await click(AWS_CREDS.generateLink); + assert + .dom(GENERAL.inputByAttr('credentialType')) + .hasValue(scenario.credentialType, 'credentialType matches backing role'); + + // based on credentialType, fill out form + await scenario.fillOutForm(assert); + + await click(GENERAL.saveButton); + assert.dom(AWS_CREDS.warning).exists('Shows access warning after generation'); + assert.dom(GENERAL.infoRowValue('Access key')).exists(); + assert.dom(GENERAL.infoRowValue('Secret key')).exists(); + assert.dom(GENERAL.infoRowValue('Security token')).exists(); + await visit('/vault/dashboard'); + + await runCmd(deleteEngineCmd(path)); + }); + }); + + test(`aws credentials without role read access`, async function (assert) { + const path = `aws-cred-${this.uid}`; + const roleName = `awsrole-noread`; + this.server.post(`/${path}/creds/${roleName}`, () => { + return { + data: { + access_key: 'AKIA...', + secret_key: 'xlCs...', + security_token: 'some-token', + arn: 'arn:aws:sts::123456789012:assumed-role/DeveloperRole/some-user-supplied-role-session-name', + }, + }; + }); + this.server.get(`/${path}/roles/${roleName}`, () => overrideResponse(403)); + await runCmd(mountEngineCmd('aws', path)); + await runCmd(`write ${path}/roles/${roleName} credential_type=assumed_role`); + + await visit(`/vault/secrets/${path}/list`); + assert.dom(AWS_CREDS.secretLink(roleName)).exists(); + await click(AWS_CREDS.secretLink(roleName)); + + assert.strictEqual(currentURL(), `/vault/secrets/${path}/credentials/${roleName}`); + assert + .dom(GENERAL.inputByAttr('credentialType')) + .hasValue('iam_user', 'credentialType defaults to first in list due to no role read permissions'); + + await fillIn(GENERAL.inputByAttr('credentialType'), 'assumed_role'); + + await click(GENERAL.saveButton); + assert.dom(AWS_CREDS.warning).exists('Shows access warning after generation'); + assert.dom(GENERAL.infoRowValue('Access key')).exists(); + assert.dom(GENERAL.infoRowValue('Secret key')).exists(); + assert.dom(GENERAL.infoRowValue('Security token')).exists(); + await visit('/vault/dashboard'); + + await runCmd(deleteEngineCmd(path)); }); }); diff --git a/ui/tests/acceptance/ssh-test.js b/ui/tests/acceptance/ssh-test.js index 72e1428009..1a7f9dc76a 100644 --- a/ui/tests/acceptance/ssh-test.js +++ b/ui/tests/acceptance/ssh-test.js @@ -3,14 +3,23 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click, fillIn, findAll, currentURL, find, settled, waitUntil } from '@ember/test-helpers'; +import { + click, + fillIn, + currentURL, + find, + settled, + waitUntil, + currentRouteName, + waitFor, +} from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { v4 as uuidv4 } from 'uuid'; import authPage from 'vault/tests/pages/auth'; import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { setRunOptions } from 'ember-a11y-testing/test-support'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; module('Acceptance | ssh secret backend', function (hooks) { setupApplicationTest(hooks); @@ -26,6 +35,7 @@ module('Acceptance | ssh secret backend', function (hooks) { { type: 'ca', name: 'carole', + credsRoute: 'vault.cluster.secrets.backend.sign', async fillInCreate() { await click('[data-test-input="allowUserCertificates"]'); }, @@ -61,6 +71,7 @@ module('Acceptance | ssh secret backend', function (hooks) { { type: 'otp', name: 'otprole', + credsRoute: 'vault.cluster.secrets.backend.credentials', async fillInCreate() { await fillIn('[data-test-input="defaultUser"]', 'admin'); await click('[data-test-toggle-group="Options"]'); @@ -84,13 +95,7 @@ module('Acceptance | ssh secret backend', function (hooks) { }, ]; test('ssh backend', async function (assert) { - // Popup menu causes flakiness - setRunOptions({ - rules: { - 'color-contrast': { enabled: false }, - }, - }); - assert.expect(28); + assert.expect(30); const sshPath = `ssh-${this.uid}`; await enablePage.enable('ssh', sshPath); @@ -100,27 +105,24 @@ module('Acceptance | ssh secret backend', function (hooks) { await click('[data-test-secret-backend-configure]'); assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${sshPath}`); - assert.ok(findAll('[data-test-ssh-configure-form]').length, 'renders the empty configuration form'); + assert.dom('[data-test-ssh-configure-form]').exists('renders the empty configuration form'); // default has generate CA checked so we just submit the form await click('[data-test-ssh-input="configure-submit"]'); - assert.ok( - await waitUntil(() => findAll('[data-test-ssh-input="public-key"]').length), - 'a public key is fetched' - ); + await waitFor('[data-test-ssh-input="public-key"]'); + assert.dom('[data-test-ssh-input="public-key"]').exists(); await click('[data-test-backend-view-link]'); assert.strictEqual(currentURL(), `/vault/secrets/${sshPath}/list`, `redirects to ssh index`); for (const role of ROLES) { // create a role - await click('[ data-test-secret-create]'); + await click('[data-test-secret-create]'); - assert.ok( - find('[data-test-secret-header]').textContent.includes('SSH Role'), - `${role.type}: renders the create page` - ); + assert + .dom('[data-test-secret-header]') + .includesText('SSH Role', `${role.type}: renders the create page`); await fillIn('[data-test-input="name"]', role.name); await fillIn('[data-test-input="keyType"]', role.type); @@ -138,7 +140,7 @@ module('Acceptance | ssh secret backend', function (hooks) { // sign a key with this role await click('[data-test-backend-credentials]'); - + assert.strictEqual(currentRouteName(), role.credsRoute); await role.fillInGenerate(); if (role.type === 'ca') { await settled(); @@ -146,29 +148,23 @@ module('Acceptance | ssh secret backend', function (hooks) { } // generate creds - await click('[data-test-secret-generate]'); + await click(GENERAL.saveButton); await settled(); // eslint-disable-line role.assertAfterGenerate(assert, sshPath); // click the "Back" button - await click('[data-test-secret-generate-back]'); + await click('[data-test-back-button]'); - assert.ok( - findAll('[data-test-secret-generate-form]').length, - `${role.type}: back takes you back to the form` - ); + assert.dom('[data-test-secret-generate-form]').exists(`${role.type}: back takes you back to the form`); - await click('[data-test-secret-generate-cancel]'); + await click(GENERAL.cancelButton); assert.strictEqual( currentURL(), `/vault/secrets/${sshPath}/list`, `${role.type}: cancel takes you to ssh index` ); - assert.ok( - findAll(`[data-test-secret-link="${role.name}"]`).length, - `${role.type}: role shows in the list` - ); + assert.dom(`[data-test-secret-link="${role.name}"]`).exists(`${role.type}: role shows in the list`); //and delete await click(`[data-test-secret-link="${role.name}"] [data-test-popup-menu-trigger]`); diff --git a/ui/tests/pages/secrets/backend/pki/edit-role.js b/ui/tests/pages/secrets/backend/pki/edit-role.js deleted file mode 100644 index 9da0f8d2da..0000000000 --- a/ui/tests/pages/secrets/backend/pki/edit-role.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Base } from '../create'; -import { settled } from '@ember/test-helpers'; -import { clickable, visitable, create, fillable } from 'ember-cli-page-object'; - -export default create({ - ...Base, - visitEdit: visitable('/vault/secrets/:backend/edit/:id'), - visitEditRoot: visitable('/vault/secrets/:backend/edit'), - toggleDomain: clickable('[data-test-toggle-group="Domain Handling"]'), - toggleOptions: clickable('[data-test-toggle-group="Options"]'), - name: fillable('[data-test-input="name"]'), - allowAnyName: clickable('[data-test-input="allowAnyName"]'), - allowedDomains: fillable('[data-test-input="allowedDomains"] .input'), - save: clickable('[data-test-role-create]'), - - async createRole(name, allowedDomains) { - await this.toggleDomain(); - await settled(); - await this.toggleOptions(); - await settled(); - await this.name(name); - await settled(); - await this.allowAnyName(); - await settled(); - await this.allowedDomains(allowedDomains); - await settled(); - await this.save(); - await settled(); - }, -}); diff --git a/ui/tests/pages/secrets/backend/pki/generate-cert.js b/ui/tests/pages/secrets/backend/pki/generate-cert.js deleted file mode 100644 index 77268e3479..0000000000 --- a/ui/tests/pages/secrets/backend/pki/generate-cert.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Base } from '../credentials'; -import { clickable, text, value, create, fillable, isPresent } from 'ember-cli-page-object'; - -export default create({ - ...Base, - title: text('[data-test-title]'), - commonName: fillable('[data-test-input="commonName"]'), - commonNameValue: value('[data-test-input="commonName"]'), - csr: fillable('[data-test-input="csr"]'), - submit: clickable('[data-test-secret-generate]'), - back: clickable('[data-test-secret-generate-back]'), - certificate: text('[data-test-row-value="Certificate"]'), - toggleOptions: clickable('[data-test-toggle-group]'), - enableTtl: clickable('[data-test-toggle-input]'), - hasCert: isPresent('[data-test-row-value="Certificate"]'), - fillInTime: fillable('[data-test-ttl-value]'), - fillInField: fillable('[data-test-select="ttl-unit"]'), - issueCert: async function (commonName) { - await this.commonName(commonName).toggleOptions().enableTtl().fillInField('h').fillInTime('30').submit(); - }, - - sign: async function (commonName, csr) { - return this.csr(csr) - .commonName(commonName) - .toggleOptions() - .enableTtl() - .fillInField('h') - .fillInTime('30') - .submit(); - }, -}); diff --git a/ui/tests/pages/secrets/backend/pki/show.js b/ui/tests/pages/secrets/backend/pki/show.js deleted file mode 100644 index f1a1a09225..0000000000 --- a/ui/tests/pages/secrets/backend/pki/show.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { Base } from '../show'; -import { settled } from '@ember/test-helpers'; -import { create, clickable, collection, text, isPresent } from 'ember-cli-page-object'; - -export default create({ - ...Base, - rows: collection('data-test-row-label'), - certificate: text('[data-test-row-value="Certificate"]'), - hasCert: isPresent('[data-test-row-value="Certificate"]'), - edit: clickable('[data-test-edit-link]'), - generateCert: clickable('[data-test-credentials-link]'), - deleteBtn: clickable('[data-test-role-delete] button'), - confirmBtn: clickable('[data-test-confirm-button]'), - async deleteRole() { - await this.deleteBtn(); - await settled(); - await this.confirmBtn(); - await settled(); - }, -}); diff --git a/ui/tests/pages/secrets/backend/ssh/generate-otp.js b/ui/tests/pages/secrets/backend/ssh/generate-otp.js index d4ed260a43..2e67e466e2 100644 --- a/ui/tests/pages/secrets/backend/ssh/generate-otp.js +++ b/ui/tests/pages/secrets/backend/ssh/generate-otp.js @@ -14,8 +14,8 @@ export default create({ ip: fillable('[data-test-input="ip"]'), warningIsPresent: isPresent('[data-test-warning]'), commonNameValue: value('[data-test-input="commonName"]'), - submit: clickable('[data-test-secret-generate]'), - back: clickable('[data-test-secret-generate-back]'), + submit: clickable('[data-test-save]'), + back: clickable('[data-test-back-button]'), generateOTP: async function () { await this.user('admin').ip('192.168.1.1').submit(); },