diff --git a/ui/app/components/dashboard/replication-card.hbs b/ui/app/components/dashboard/replication-card.hbs index 4f954dcddf..b7c38246f3 100644 --- a/ui/app/components/dashboard/replication-card.hbs +++ b/ui/app/components/dashboard/replication-card.hbs @@ -26,13 +26,16 @@ data-test-type="dr-perf" >
diff --git a/ui/lib/config-ui/addon/components/login-settings/page/list.js b/ui/lib/config-ui/addon/components/login-settings/page/list.js index bef39a0fca..7e1ff592e2 100644 --- a/ui/lib/config-ui/addon/components/login-settings/page/list.js +++ b/ui/lib/config-ui/addon/components/login-settings/page/list.js @@ -31,8 +31,7 @@ export default class LoginSettingsList extends Component { try { await this.api.sys.uiLoginDefaultAuthDeleteConfiguration(this.ruleToDelete.id); this.flashMessages.success(`Successfully deleted rule ${this.ruleToDelete.id}.`); - - this.router.transitionTo('vault.cluster.config-ui.login-settings'); + this.router.refresh('vault.cluster.config-ui.login-settings'); } catch (error) { const message = errorMessage(error, 'Error deleting rule. Please try again.'); this.flashMessages.danger(message); diff --git a/ui/mirage/factories/login-rule.js b/ui/mirage/factories/login-rule.js index dda7875dbe..2290af46ac 100644 --- a/ui/mirage/factories/login-rule.js +++ b/ui/mirage/factories/login-rule.js @@ -6,8 +6,8 @@ import { Factory } from 'miragejs'; export default Factory.extend({ - name: (i) => `Login rule ${i}`, - namespace_path: (i) => `namespace-${i}`, + name: (i) => `Login-rule-${i}`, + namespace_path: (i) => `namespace-${i}/`, default_auth_type: 'okta', backup_auth_types: () => ['oidc', 'token'], disable_inheritance: false, diff --git a/ui/mirage/handlers/custom-login.js b/ui/mirage/handlers/custom-login.js index c8090f6bb1..9784694c72 100644 --- a/ui/mirage/handlers/custom-login.js +++ b/ui/mirage/handlers/custom-login.js @@ -3,6 +3,8 @@ * SPDX-License-Identifier: BUSL-1.1 */ +import { sanitizePath } from 'core/utils/sanitize-path'; + export default function (server) { // LIST, READ and DELETE requests for default-auth (login customizations) server.get('sys/config/ui/login/default-auth', (schema, req) => { @@ -12,9 +14,8 @@ export default function (server) { if (records) { const keys = records.map(({ name }) => name); const key_info = records.reduce((obj, record) => { - const { name, namespace, disable_inheritance } = record; - // TBD, but likely only limited information will be returned about the record from the LIST request - obj[name] = { namespace, disable_inheritance }; + const { name, namespace_path, disable_inheritance } = record; + obj[name] = { namespace_path, disable_inheritance, name }; return obj; }, {}); return { @@ -47,20 +48,21 @@ export default function (server) { // UNAUTHENTICATED READ ONLY for login form display logic server.get('sys/internal/ui/default-auth-methods', (schema, req) => { const nsHeader = req.requestHeaders['X-Vault-Namespace']; - // if no namespace is passed, assume root - const findRule = (ns = '') => schema.db['loginRules'].findBy({ namespace_path: ns }); - - let rule = findRule(nsHeader || ''); + const findRule = (ns) => schema.db['loginRules'].findBy({ namespace_path: ns }); + // the namespace header shouldn't have a trailing slash, but sanitize just in case + // if no namespace is passed then it's the root namespace (which does not have a trailing slash) + const nsPath = sanitizePath(nsHeader) ? `${nsHeader}/` : 'root'; + let rule = findRule(nsPath); if (!rule && nsHeader?.includes('/')) { - // for simplicity, tests only nest namespaces one level, e.g. "test-ns/child" + // for simplicity, tests only support namespaces nested one level, e.g. "test-ns/child" const [parent] = nsHeader.split('/'); - const parentRule = findRule(parent); + const parentRule = findRule(`${parent}/`); rule = parentRule?.disable_inheritance ? null : parentRule; } // Fallback to root namespace settings to simulate inheritance if no rule exists or parent has disabled inheritance - rule = rule || findRule(); + rule = rule || findRule('root'); const { default_auth_type, backup_auth_types, disable_inheritance } = rule || {}; return { data: { default_auth_type, backup_auth_types, disable_inheritance } }; diff --git a/ui/mirage/scenarios/custom-login.js b/ui/mirage/scenarios/custom-login.js index bafb297e19..1d29085ddf 100644 --- a/ui/mirage/scenarios/custom-login.js +++ b/ui/mirage/scenarios/custom-login.js @@ -6,19 +6,19 @@ export default function (server) { server.create('login-rule', { name: 'root-rule', - namespace_path: '', - default_auth_type: 'okta', - backup_auth_types: ['token'], + namespace_path: 'root', + default_auth_type: 'token', + backup_auth_types: ['userpass', 'ldap'], disable_inheritance: false, }); server.create('login-rule', { - namespace_path: 'admin', + namespace_path: 'admin/', default_auth_type: 'oidc', backup_auth_types: ['token'], }); server.create('login-rule', { name: 'ns-rule', - namespace_path: 'test-ns', + namespace_path: 'test-ns/', default_auth_type: 'ldap', backup_auth_types: [], disable_inheritance: true, diff --git a/ui/package.json b/ui/package.json index 40a7007491..c2d847385f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,12 +30,12 @@ "start": "VAULT_ADDR=http://127.0.0.1:8200; ember server --proxy=$VAULT_ADDR", "start2": "ember server --proxy=http://127.0.0.1:8202 --port=4202", "start:chroot": "ember server --proxy=http://127.0.0.1:8300 --port=4300", - "test": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,lint:types,vault \"yarn:lint:js:quiet\" \"yarn:lint:hbs:quiet\" \"yarn:lint:types\" \"node scripts/start-vault.js {@}\" --", + "lint": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,lint:types \"yarn:lint:js:quiet\" \"yarn:lint:hbs:quiet\" \"yarn:lint:types\"", + "test": "yarn lint && node scripts/start-vault.js", "test:enos": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,lint:types,enos \"yarn:lint:js:quiet\" \"yarn:lint:hbs:quiet\" \"yarn:lint:types\" \"node scripts/enos-test-ember.js {@}\" --", - "test:oss": "yarn run test -f='!enterprise'", + "test:oss": "yarn test -f='!enterprise'", "test:ent": "node scripts/start-vault.js -f='enterprise'", "test:quick": "node scripts/start-vault.js --split=8 --preserve-test-name --parallel=1", - "test:quick-oss": "node scripts/start-vault.js -f='!enterprise' --split=8 --preserve-test-name --parallel=1", "test:filter": "node scripts/start-vault.js --server -f='!enterprise'", "test:server": "node scripts/start-vault.js --server", "test:dev": "node scripts/start-vault.js", diff --git a/ui/tests/acceptance/access/identity/entities/index-test.js b/ui/tests/acceptance/access/identity/entities/index-test.js index 4742c20215..9d3d38797f 100644 --- a/ui/tests/acceptance/access/identity/entities/index-test.js +++ b/ui/tests/acceptance/access/identity/entities/index-test.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { fillIn, click, currentRouteName, currentURL, visit } from '@ember/test-helpers'; +import { click, currentRouteName, currentURL, visit } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import page from 'vault/tests/pages/access/identity/index'; @@ -11,14 +11,15 @@ import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { runCmd } from 'vault/tests/helpers/commands'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { v4 as uuidv4 } from 'uuid'; +import { setupMirage } from 'ember-cli-mirage/test-support'; const SELECTORS = { listItem: (name) => `[data-test-identity-row="${name}"]`, - menu: `[data-test-popup-menu-trigger]`, - menuItem: (element) => `[data-test-popup-menu="${element}"]`, }; + module('Acceptance | /access/identity/entities', function (hooks) { setupApplicationTest(hooks); + setupMirage(hooks); hooks.beforeEach(function () { return login(); @@ -42,35 +43,17 @@ module('Acceptance | /access/identity/entities', function (hooks) { ); }); - test('it navigates away from the entities page', async function (assert) { - const name = `entity-${uuidv4()}`; - await runCmd(`vault write identity/entity name="${name}" policies="default"`); - await page.visit({ item_type: 'entities' }); - await click(GENERAL.navLink('Back to main navigation')); - assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'navigates back to dashboard'); - await runCmd(`vault delete identity/entity/name/${name}`); - }); - - test('it navigates away from the groups page', async function (assert) { - const name = `entity-${uuidv4()}`; - await runCmd(`vault write identity/group name="${name}" policies="default" type="external"`); - await page.visit({ item_type: 'groups' }); - await click(GENERAL.navLink('Back to main navigation')); - assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'navigates back to dashboard'); - await runCmd(`vault delete identity/group/name/${name}`); - }); - test('it renders popup menu for entities', async function (assert) { const name = `entity-${uuidv4()}`; await runCmd(`vault write identity/entity name="${name}" policies="default"`); await visit('/vault/access/identity/entities'); assert.strictEqual(currentURL(), '/vault/access/identity/entities', 'navigates to entities tab'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`); + await click(`${SELECTORS.listItem(name)} ${GENERAL.menuTrigger}`); assert .dom('.hds-dropdown ul') .hasText('Details Create alias Edit Disable Delete', 'all actions render for entities'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`); + await click(`${SELECTORS.listItem(name)} ${GENERAL.menuItem('delete')}`); await click(GENERAL.confirmButton); }); @@ -80,41 +63,52 @@ module('Acceptance | /access/identity/entities', function (hooks) { await visit('/vault/access/identity/groups'); assert.strictEqual(currentURL(), '/vault/access/identity/groups', 'navigates to the groups tab'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`); + await click(`${SELECTORS.listItem(name)} ${GENERAL.menuTrigger}`); assert .dom('.hds-dropdown ul') .hasText('Details Create alias Edit Delete', 'all actions render for external groups'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`); + await click(`${SELECTORS.listItem(name)} ${GENERAL.menuItem('delete')}`); await click(GENERAL.confirmButton); }); test('it renders popup menu for external groups with alias', async function (assert) { - const name = `external-hasalias-${uuidv4()}`; - await runCmd(`vault write identity/group name="${name}" policies="default" type="external"`); - await visit('/vault/access/identity/groups'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`); - await click(SELECTORS.menuItem('create alias')); - await fillIn(GENERAL.inputByAttr('name'), 'alias-test'); - await click(GENERAL.submitButton); + const groupId = '44b2f1d1-699a-4a79-3a7b-37e53e17e7b2'; + const groupName = 'external-hasalias'; + // only relevant response keys are stubbed to simplify testing (more data is actually returned by both endpoints) + this.server.get('/identity/group/id', () => { + return { + data: { + key_info: { [groupId]: { name: groupName } }, + keys: [groupId], + }, + }; + }); + + this.server.get(`/identity/group/id/${groupId}`, () => { + return { + data: { + alias: { id: '15bac764-d690-b72a-9cbc-b1fdeac1af9e', name: 'alias-test' }, + type: 'external', + }, + }; + }); await visit('/vault/access/identity/groups'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`); + await click(`${SELECTORS.listItem(groupName)} ${GENERAL.menuTrigger}`); assert .dom('.hds-dropdown ul') .hasText('Details Edit Delete', 'no "Create alias" option for external groups with an alias'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`); - await click(GENERAL.confirmButton); }); test('it renders popup menu for internal groups', async function (assert) { const name = `internal-${uuidv4()}`; await runCmd(`vault write identity/group name="${name}" policies="default" type="internal"`); await visit('/vault/access/identity/groups'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`); + await click(`${SELECTORS.listItem(name)} ${GENERAL.menuTrigger}`); assert .dom('.hds-dropdown ul') .hasText('Details Edit Delete', 'no "Create alias" option for internal groups'); - await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`); + await click(`${SELECTORS.listItem(name)} ${GENERAL.menuItem('delete')}`); await click(GENERAL.confirmButton); }); }); diff --git a/ui/tests/acceptance/access/identity/shared-identity-test.js b/ui/tests/acceptance/access/identity/shared-identity-test.js index eb6eb33fc9..451c78a9eb 100644 --- a/ui/tests/acceptance/access/identity/shared-identity-test.js +++ b/ui/tests/acceptance/access/identity/shared-identity-test.js @@ -12,6 +12,7 @@ import { click, fillIn, visit, currentURL } from '@ember/test-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { capitalize } from '@ember/string'; import { singularize } from 'ember-inflector'; +import { setupMirage } from 'ember-cli-mirage/test-support'; // Helper to create an entity or group async function createEntityOrGroup(itemType, name) { @@ -36,6 +37,7 @@ async function createAlias(itemType, itemGeneratedId, name) { // Creation of an Entity or Group is inherently tested as part of the alias flow, so no separate test is needed. module('Acceptance | Create groups and entities alias test', function (hooks) { setupApplicationTest(hooks); + setupMirage(hooks); hooks.beforeEach(async function () { this.flashMessages = this.owner.lookup('service:flash-messages'); @@ -86,30 +88,48 @@ module('Acceptance | Create groups and entities alias test', function (hooks) { }); test(`${itemType}: it allows delete from the edit form`, async function (assert) { - const name = `${itemType}-${uuidv4()}`; - const itemGeneratedId = await createEntityOrGroup(itemType, name); - const aliasGeneratedId = await createAlias(itemType, itemGeneratedId, name); - - await click('[data-test-alias-edit-link]'); - assert.strictEqual( - currentURL(), - `/vault/access/identity/${itemType}/aliases/edit/${aliasGeneratedId}`, - `${itemType}: correctly navigates to edit` - ); - + assert.expect(3); + const itemId = uuidv4(); + const name = `${itemType}-${itemId}`; + this.server.get(`/identity/${singularize(itemType)}/id/${itemId}`, () => { + return { data: { id: itemId, name } }; + }); + this.server.delete(`/identity/${singularize(itemType)}/id/${itemId}`, () => { + assert.true(true, `request made to delete ${name}`); + }); + await visit(`/vault/access/identity/${itemType}/edit/${itemId}`); await click(GENERAL.confirmTrigger); // click the Delete entity-alias trigger button await click(GENERAL.confirmButton); assert.dom(GENERAL.latestFlashContent).includesText('Successfully deleted'); assert.strictEqual( currentURL(), - `/vault/access/identity/${itemType}/aliases`, + `/vault/access/identity/${itemType}`, `${itemType}: navigates to the list page after deletion` ); }); test(`${itemType}: it allows you to delete the ${itemType} from the list view`, async function (assert) { - const name = `${itemType}-${uuidv4()}`; - await createEntityOrGroup(itemType, name); + assert.expect(3); + const itemId = uuidv4(); + const name = `${itemType}-${itemId}`; + + this.server.get(`/identity/${singularize(itemType)}/id`, () => { + return { + data: { + key_info: { [itemId]: { name } }, + keys: [itemId], + }, + }; + }); + + this.server.get(`/identity/${singularize(itemType)}/id/${itemId}`, () => { + return { data: { id: itemId, name } }; + }); + + this.server.delete(`/identity/${singularize(itemType)}/id/${itemId}`, () => { + assert.true(true, `request made to delete ${name}`); + }); + await visit(`/vault/access/identity/${itemType}`); const rowSelector = `[data-test-identity-row="${name}"]`; @@ -120,8 +140,7 @@ module('Acceptance | Create groups and entities alias test', function (hooks) { await click(menuTriggerSelector); await click(GENERAL.menuItem('delete')); await click(GENERAL.confirmButton); - - assert.dom(rowSelector).doesNotExist(`${itemType}: is NOT in the list view`); + assert.dom(GENERAL.latestFlashContent).includesText('Successfully deleted'); }); } }); diff --git a/ui/tests/acceptance/auth/auth-login-test.js b/ui/tests/acceptance/auth/auth-login-test.js index e77ee25023..d1829eaa97 100644 --- a/ui/tests/acceptance/auth/auth-login-test.js +++ b/ui/tests/acceptance/auth/auth-login-test.js @@ -21,7 +21,8 @@ import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors'; import { v4 as uuidv4 } from 'uuid'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import sinon from 'sinon'; -import { supportedTypes } from 'vault/utils/auth-form-helpers'; +import { ERROR_JWT_LOGIN, supportedTypes } from 'vault/utils/auth-form-helpers'; +import { overrideResponse } from 'vault/tests/helpers/stubs'; const { rootToken } = VAULT_KEYS; @@ -197,7 +198,7 @@ module('Acceptance | auth login', function (hooks) { // Assertion count is one for the URL and one for each payload key module('it sends the right payload when authenticating', function (hooks) { hooks.beforeEach(function () { - this.assertAuthRequest = (assert, req, expectedPayload) => { + this.assertAuthPayload = (assert, req, expectedPayload) => { const body = JSON.parse(req.requestBody); assert.true(true, `it calls the correct URL: ${req.url}`); @@ -242,7 +243,7 @@ module('Acceptance | auth login', function (hooks) { this.authType = 'github'; this.expectedPayload = { token: 'mysupersecuretoken' }; this.server.post('/auth/custom-github/login', (schema, req) => { - this.assertAuthRequest(assert, req, this.expectedPayload); + this.assertAuthPayload(assert, req, this.expectedPayload); req.passthrough(); }); @@ -254,24 +255,30 @@ module('Acceptance | auth login', function (hooks) { this.authType = 'ldap'; this.expectedPayload = { password: 'some-password' }; this.server.post('/auth/custom-ldap/login/matilda', (schema, req) => { - this.assertAuthRequest(assert, req, this.expectedPayload); + this.assertAuthPayload(assert, req, this.expectedPayload); req.passthrough(); }); await this.fillAndLogIn(); }); + // JWT and OIDC use the same request URLs and how the login happens depends on the configuration. + // For simplicity, mocking each to be configured as their namesake. test('jwt', async function (assert) { - // auth_url is hit twice (once when inputs are filled and again on submit) - // so the assertion count is doubled - assert.expect(6); + // assertion for each payload key and request made + assert.expect(3); this.authType = 'jwt'; this.expectedPayload = { - redirect_uri: 'http://localhost:7357/ui/vault/auth/custom-jwt/oidc/callback', + jwt: 'some-jwt-token', role: 'some-dev', }; - this.server.post('/auth/custom-jwt/oidc/auth_url', (schema, req) => { - this.assertAuthRequest(assert, req, this.expectedPayload); + // Additional setup so that JWT method is configured as jwt (not OIDC) + this.server.post(`/auth/:path/oidc/auth_url`, () => + overrideResponse(400, { errors: [ERROR_JWT_LOGIN] }) + ); + + this.server.post('/auth/custom-jwt/login', (schema, req) => { + this.assertAuthPayload(assert, req, this.expectedPayload); // 1 assertion + 1 for each payload key req.passthrough(); }); @@ -288,7 +295,7 @@ module('Acceptance | auth login', function (hooks) { role: 'some-dev', }; this.server.post('/auth/custom-oidc/oidc/auth_url', (schema, req) => { - this.assertAuthRequest(assert, req, this.expectedPayload); + this.assertAuthPayload(assert, req, this.expectedPayload); req.passthrough(); }); @@ -300,7 +307,7 @@ module('Acceptance | auth login', function (hooks) { this.authType = 'okta'; this.expectedPayload = { password: 'some-password' }; this.server.post('/auth/custom-okta/login/matilda', (schema, req) => { - this.assertAuthRequest(assert, req, this.expectedPayload); + this.assertAuthPayload(assert, req, this.expectedPayload); req.passthrough(); }); @@ -312,7 +319,7 @@ module('Acceptance | auth login', function (hooks) { this.authType = 'radius'; this.expectedPayload = { password: 'some-password' }; this.server.post('/auth/custom-radius/login/matilda', (schema, req) => { - this.assertAuthRequest(assert, req, this.expectedPayload); + this.assertAuthPayload(assert, req, this.expectedPayload); req.passthrough(); }); @@ -324,7 +331,7 @@ module('Acceptance | auth login', function (hooks) { this.authType = 'userpass'; this.expectedPayload = { password: 'some-password' }; this.server.post('/auth/custom-userpass/login/matilda', (schema, req) => { - this.assertAuthRequest(assert, req, this.expectedPayload); + this.assertAuthPayload(assert, req, this.expectedPayload); req.passthrough(); }); @@ -336,7 +343,7 @@ module('Acceptance | auth login', function (hooks) { this.authType = 'saml'; this.expectedPayload = { role: 'some-dev' }; this.server.post('/auth/custom-saml/sso_service_url', (schema, req) => { - this.assertAuthRequest(assert, req, this.expectedPayload); + this.assertAuthPayload(assert, req, this.expectedPayload); req.passthrough(); }); diff --git a/ui/tests/acceptance/auth/login-settings-test.js b/ui/tests/acceptance/auth/login-settings-test.js index 215054c7bc..e454f04a43 100644 --- a/ui/tests/acceptance/auth/login-settings-test.js +++ b/ui/tests/acceptance/auth/login-settings-test.js @@ -23,19 +23,29 @@ module('Acceptance | Enterprise | auth form custom login settings', function (ho customLoginHandler(this.server); customLoginScenario(this.server); // mirage scenario sets: - // root namespace with 'okta' as default and 'token' as backup + // root namespace with 'token' as default backups are 'userpass' and 'ldap' // 'test-ns' with 'ldap' as default and no backups }); test('it renders login settings for root namespace', async function (assert) { await visit('/vault/auth'); - await waitFor(AUTH_FORM.tabBtn('okta')); - assert.dom(AUTH_FORM.tabBtn('okta')).hasAttribute('aria-selected', 'true'); - assert.dom(AUTH_FORM.authForm('okta')).exists('it renders default method'); - assert.dom(AUTH_FORM.advancedSettings).exists(); - + await waitFor(AUTH_FORM.tabBtn('token')); + assert.dom(AUTH_FORM.tabBtn('token')).hasAttribute('aria-selected', 'true'); + assert.dom(AUTH_FORM.authForm('token')).exists('it renders default method'); + assert + .dom(AUTH_FORM.advancedSettings) + .doesNotExist('it does not render advanced settings for token auth method'); await click(GENERAL.button('Sign in with other methods')); - assert.dom(AUTH_FORM.authForm('token')).exists('it renders backup method'); + assert + .dom(AUTH_FORM.tabBtn('userpass')) + .exists('it renders backup "Userpass" method') + .hasAttribute('aria-selected', 'true'); + assert.dom(AUTH_FORM.authForm('userpass')).exists('it renders "Userpass" form when method is selected'); + assert.dom(AUTH_FORM.advancedSettings).exists('it renders advanced settings for "Userpass"'); + assert.dom(AUTH_FORM.tabBtn('ldap')).exists('it renders backup "LDAP" method'); + await click(AUTH_FORM.tabBtn('ldap')); + assert.dom(AUTH_FORM.authForm('ldap')).exists('it renders "LDAP" form when method is selected'); + assert.dom(AUTH_FORM.advancedSettings).exists('it renders advanced settings for "LDAP"'); }); test('it renders login settings for namespaces', async function (assert) { @@ -48,10 +58,13 @@ module('Acceptance | Enterprise | auth form custom login settings', function (ho .dom(GENERAL.button('Sign in with other methods')) .doesNotExist('it does not render alternate view'); - // type in so that the namespace is "test-ns/child" + // All we're testing here is that the form settings update for nested namespaces. + // (We're not concerned with what the settings are since the mirage handler is stubbing the API logic) + // typeIn so that the text appends to the existing namespace input: "test-ns/child" await typeIn(GENERAL.inputByAttr('namespace'), '/child'); - await waitFor(AUTH_FORM.authForm('okta')); - assert.dom(AUTH_FORM.authForm('okta')).exists('it updates to render child namespace settings'); + await waitFor(AUTH_FORM.authForm('token')); + assert.dom(AUTH_FORM.authForm('token')).exists('it updates to render child namespace settings'); + assert.dom(AUTH_FORM.authForm('ldap')).doesNotExist('it does not render default view for parent'); }); module('listing visibility', function (hooks) { diff --git a/ui/tests/acceptance/config-ui/login-settings-test.js b/ui/tests/acceptance/config-ui/login-settings-test.js index 87a55cb80e..38bdac3ee5 100644 --- a/ui/tests/acceptance/config-ui/login-settings-test.js +++ b/ui/tests/acceptance/config-ui/login-settings-test.js @@ -5,37 +5,27 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import { click, visit, currentRouteName } from '@ember/test-helpers'; -import { login } from 'vault/tests/helpers/auth/auth-helpers'; +import { click, visit, currentRouteName, fillIn, waitUntil } from '@ember/test-helpers'; +import { login, logout, rootToken } from 'vault/tests/helpers/auth/auth-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { runCmd } from 'vault/tests/helpers/commands'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { overrideResponse } from 'vault/tests/helpers/stubs'; -import { v4 as uuidv4 } from 'uuid'; -const SELECTORS = { - rule: (name) => (name ? `[data-test-rule="${name}"]` : '[data-test-rule]'), - popupMenu: (name) => `[data-test-rule="${name}"] ${GENERAL.menuTrigger}`, -}; +import customLoginScenario from 'vault/mirage/scenarios/custom-login'; +import customLoginHandler from 'vault/mirage/handlers/custom-login'; +import Sinon from 'sinon'; + // read view for custom login settings module('Acceptance | Enterprise | config-ui/login-settings', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); hooks.beforeEach(async function () { - // a login rule cannot be created for a namespace that already has a rule applied. - // rules are deleted in the afterEach hook, but when debugging tests locally they may - // re-run before cleanup happens. - // using a uuid ensures the runCmd that creates new rules is always successful - // by using a different namespace_path each run. - this.ns1 = `ns1-${uuidv4()}`; - this.ns2 = `ns2-${uuidv4()}`; - return await login(); + await login(); }); test('it renders empty state if no login settings exist', async function (assert) { await visit('vault/config-ui/login-settings'); - assert.dom(GENERAL.emptyStateTitle).hasText('No UI login settings yet'); assert .dom(GENERAL.emptyStateMessage) @@ -44,7 +34,7 @@ module('Acceptance | Enterprise | config-ui/login-settings', function (hooks) { ); }); - test('it falls back error template if no permission', async function (assert) { + test('it renders error template when permission is denied', async function (assert) { this.server.get('/sys/config/ui/login/default-auth', () => overrideResponse(403)); await visit('vault/config-ui/login-settings'); assert.dom(GENERAL.pageError.error).hasText('Error permission denied'); @@ -52,90 +42,75 @@ module('Acceptance | Enterprise | config-ui/login-settings', function (hooks) { module('list, read and delete', function (hooks) { hooks.beforeEach(async function () { - await login(); + customLoginScenario(this.server); + customLoginHandler(this.server); + this.loginRules = this.server.db.loginRules; - // create login rules - await runCmd([ - `write sys/config/ui/login/default-auth/testRule1 backup_auth_types=userpass default_auth_type=okta disable_inheritance=false namespace_path=${this.ns1}`, - `write sys/config/ui/login/default-auth/testRule2 backup_auth_types=oidc default_auth_type=ldap disable_inheritance=true namespace_path=${this.ns2}`, - ]); + // Cannot use the login() helper because customLoginHandler returns "token" as the default auth method + await logout(); + await fillIn(GENERAL.inputByAttr('token'), rootToken); + await click(GENERAL.submitButton); + await waitUntil(() => currentRouteName() === 'vault.cluster.dashboard'); + await click(GENERAL.navLink('UI Login Settings')); }); - hooks.afterEach(async function () { - // since the test login rules are created for namespaces that do not exist - // they do not affect the login view for the "root" namespace - await login(); - - // cleanup login rules - await runCmd( - [ - 'delete sys/config/ui/login/default-auth/testRule1', - 'delete sys/config/ui/login/default-auth/testRule2', - ], - true - ); + test('it renders login rules', async function (assert) { + assert + .dom(GENERAL.listItem()) + .exists({ count: this.loginRules.length }, `${this.loginRules.length} rules render`); + this.loginRules.forEach(({ name, disable_inheritance, namespace_path }) => { + const inheritance = disable_inheritance ? 'Inheritance disabled' : 'Inheritance enabled'; + assert.dom(GENERAL.listItem(name)).hasText(`${name} ${namespace_path} ${inheritance}`); + }); }); - test('fetched login rule list renders', async function (assert) { - // Visit the login settings list index page - await visit('vault/config-ui/login-settings'); - - // verify fetched rules are rendered in list - assert.dom(SELECTORS.rule()).exists({ count: 2 }); - assert.dom(SELECTORS.rule('testRule1')).hasText(`testRule1 ${this.ns1}/ Inheritance enabled`); - assert.dom(SELECTORS.rule('testRule2')).hasText(`testRule2 ${this.ns2}/ Inheritance disabled`); - }); - - test('delete rule from list view', async function (assert) { - // Visit the login settings list index page - await visit('vault/config-ui/login-settings'); - - assert.dom(SELECTORS.rule()).exists({ count: 2 }); - await click(SELECTORS.popupMenu('testRule1')); + test('it deletes rule from list view', async function (assert) { + const successFlashSpy = Sinon.spy(this.owner.lookup('service:flash-messages'), 'success'); + const ruleToDelete = this.loginRules[0].name; + const initialCount = this.loginRules.length; // cache record length so we can confirm delete + await click(`${GENERAL.listItem(ruleToDelete)} ${GENERAL.menuTrigger}`); await click(GENERAL.menuItem('delete-rule')); - - assert.dom(GENERAL.confirmationModal).exists(); await click(GENERAL.confirmButton); - - // verify success message from deletion - assert.dom(GENERAL.latestFlashContent).includesText('Successfully deleted rule testRule1.'); - assert.dom(SELECTORS.rule('testRule1')).doesNotExist(); - assert.dom(SELECTORS.rule()).exists({ count: 1 }); + const [success] = successFlashSpy.lastCall.args; + assert.strictEqual( + success, + `Successfully deleted rule ${ruleToDelete}.`, + 'it calls flash success with expected message' + ); + assert.dom(GENERAL.listItem(ruleToDelete)).doesNotExist('the deleted rule does not exist'); + assert.dom(GENERAL.listItem()).exists({ count: initialCount - 1 }, `${initialCount - 1} rules render`); }); - test('navigate to rule details page and renders rule data', async function (assert) { + test('it navigates to rule details page and renders rule data', async function (assert) { + const rule = this.server.db.loginRules[0]; // visit individual rule page - await visit('vault/config-ui/login-settings'); - - await click(SELECTORS.popupMenu('testRule1')); + await click(`${GENERAL.listItem(rule.name)} ${GENERAL.menuTrigger}`); await click(GENERAL.menuItem('view-rule')); - // verify that user is redirected to the rule details page assert.strictEqual( currentRouteName(), 'vault.cluster.config-ui.login-settings.rule.details', 'goes to rule details page' ); - // verify fetched rule data is rendered - assert.dom(GENERAL.infoRowValue('Default method')).hasText('okta'); - assert.dom(GENERAL.infoRowValue('Namespace the rule applies to')).hasText(`${this.ns1}/`); - assert.dom(GENERAL.infoRowValue('Backup methods')).hasText('userpass'); + assert.dom(GENERAL.infoRowValue('Default method')).hasText(rule.default_auth_type); + assert.dom(GENERAL.infoRowValue('Namespace the rule applies to')).hasText(rule.namespace_path); + assert.dom(GENERAL.infoRowValue('Backup methods')).hasText(rule.backup_auth_types.join(',')); assert.dom(GENERAL.infoRowValue('Inheritance enabled')).hasText('Yes'); }); test('it navigates to rule details from linked block', async function (assert) { + const rule = this.server.db.loginRules[2]; await visit('vault/config-ui/login-settings'); - await click(SELECTORS.rule('testRule2')); + await click(GENERAL.listItem(rule.name)); assert.strictEqual( currentRouteName(), 'vault.cluster.config-ui.login-settings.rule.details', 'goes to rule details page' ); - - assert.dom(GENERAL.infoRowValue('Default method')).hasText('ldap'); - assert.dom(GENERAL.infoRowValue('Namespace the rule applies to')).hasText(`${this.ns2}/`); - assert.dom(GENERAL.infoRowValue('Backup methods')).hasText('oidc'); + assert.dom(GENERAL.infoRowValue('Default method')).hasText(rule.default_auth_type); + assert.dom(GENERAL.infoRowValue('Namespace the rule applies to')).hasText(rule.namespace_path); + assert.dom(GENERAL.infoRowValue('Backup methods')).hasText(rule.backup_auth_types.join(', ')); assert.dom(GENERAL.infoRowValue('Inheritance enabled')).hasText('No'); }); }); diff --git a/ui/tests/acceptance/custom-messages-test.js b/ui/tests/acceptance/custom-messages-test.js new file mode 100644 index 0000000000..7be96669db --- /dev/null +++ b/ui/tests/acceptance/custom-messages-test.js @@ -0,0 +1,112 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { visit } from '@ember/test-helpers'; +import { setupApplicationTest } from 'vault/tests/helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { CUSTOM_MESSAGES } from 'vault/tests/helpers/config-ui/message-selectors'; + +const authenticatedMessageResponse = { + request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b', + lease_id: '', + renewable: false, + lease_duration: 0, + data: { + key_info: { + 'some-awesome-id-2': { + authenticated: true, + end_time: null, + link: { + 'some link title': 'www.link.com', + }, + message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=', + options: null, + start_time: '2024-01-04T08:00:00Z', + title: 'Banner title', + type: 'banner', + }, + 'some-awesome-id-1': { + authenticated: true, + end_time: null, + message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==', + options: null, + start_time: '2024-01-01T08:00:00Z', + title: 'Modal title', + type: 'modal', + }, + }, + keys: ['some-awesome-id-2', 'some-awesome-id-1'], + }, + wrap_info: null, + warnings: null, + auth: null, + mount_type: '', +}; + +module('Acceptance | custom messages', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + test('it shows the alert banner and modal message', async function (assert) { + this.server.get('/sys/internal/ui/unauthenticated-messages', function () { + return authenticatedMessageResponse; + }); + await visit('/vault/dashboard'); + + const modalId = 'some-awesome-id-1'; + const alertId = 'some-awesome-id-2'; + assert.dom(CUSTOM_MESSAGES.modal(modalId)).exists(); + assert.dom(CUSTOM_MESSAGES.modalTitle(modalId)).hasText('Modal title'); + assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).exists(); + assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).hasText('here is a cool message'); + assert.dom(CUSTOM_MESSAGES.alertTitle(alertId)).hasText('Banner title'); + assert.dom(CUSTOM_MESSAGES.alertDescription(alertId)).hasText('hello world hello wolrd'); + assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title'); + }); + + test('it shows the multiple modal messages', async function (assert) { + const modalIdOne = 'some-awesome-id-2'; + const modalIdTwo = 'some-awesome-id-1'; + + this.server.get('/sys/internal/ui/unauthenticated-messages', function () { + authenticatedMessageResponse.data.key_info[modalIdOne].type = 'modal'; + authenticatedMessageResponse.data.key_info[modalIdOne].title = 'Modal title 1'; + authenticatedMessageResponse.data.key_info[modalIdTwo].type = 'modal'; + authenticatedMessageResponse.data.key_info[modalIdTwo].title = 'Modal title 2'; + return authenticatedMessageResponse; + }); + await visit('/vault/dashboard'); + + assert.dom(CUSTOM_MESSAGES.modal(modalIdOne)).exists(); + assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdOne)).hasText('Modal title 1'); + assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).exists(); + assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).hasText('hello world hello wolrd some link title'); + assert.dom(CUSTOM_MESSAGES.modal(modalIdTwo)).exists(); + assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdTwo)).hasText('Modal title 2'); + assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).exists(); + assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).hasText('here is a cool message'); + }); + + test('it shows the multiple banner messages', async function (assert) { + const bannerIdOne = 'some-awesome-id-2'; + const bannerIdTwo = 'some-awesome-id-1'; + + this.server.get('/sys/internal/ui/unauthenticated-messages', function () { + authenticatedMessageResponse.data.key_info[bannerIdOne].type = 'banner'; + authenticatedMessageResponse.data.key_info[bannerIdOne].title = 'Banner title 1'; + authenticatedMessageResponse.data.key_info[bannerIdTwo].type = 'banner'; + authenticatedMessageResponse.data.key_info[bannerIdTwo].title = 'Banner title 2'; + return authenticatedMessageResponse; + }); + await visit('/vault/dashboard'); + + assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdOne)).hasText('Banner title 1'); + assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdOne)).hasText('hello world hello wolrd'); + assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title'); + assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdTwo)).hasText('Banner title 2'); + assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdTwo)).hasText('here is a cool message'); + }); +}); diff --git a/ui/tests/acceptance/dashboard-test.js b/ui/tests/acceptance/dashboard-test.js index 953f24818b..e2239832ab 100644 --- a/ui/tests/acceptance/dashboard-test.js +++ b/ui/tests/acceptance/dashboard-test.js @@ -4,538 +4,41 @@ */ import { module, test } from 'qunit'; -import { visit, currentURL, settled, fillIn, click, waitFor, currentRouteName } from '@ember/test-helpers'; +import { visit, currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'vault/tests/helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { selectChoose } from 'ember-power-select/test-support'; -import { clickTrigger } from 'ember-power-select/test-support/helpers'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import clientsHandlers from 'vault/mirage/handlers/clients'; -import { formatNumber } from 'core/helpers/format-number'; -import { pollCluster } from 'vault/tests/helpers/poll-cluster'; -import { disableReplication } from 'vault/tests/helpers/replication'; -import connectionPage from 'vault/tests/pages/secrets/backend/database/connection'; -import { v4 as uuidv4 } from 'uuid'; -import { runCmd, deleteEngineCmd, createNS, deleteNS } from 'vault/tests/helpers/commands'; - import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors'; -import { CUSTOM_MESSAGES } from 'vault/tests/helpers/config-ui/message-selectors'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; - -const authenticatedMessageResponse = { - request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b', - lease_id: '', - renewable: false, - lease_duration: 0, - data: { - key_info: { - 'some-awesome-id-2': { - authenticated: true, - end_time: null, - link: { - 'some link title': 'www.link.com', - }, - message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=', - options: null, - start_time: '2024-01-04T08:00:00Z', - title: 'Banner title', - type: 'banner', - }, - 'some-awesome-id-1': { - authenticated: true, - end_time: null, - message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==', - options: null, - start_time: '2024-01-01T08:00:00Z', - title: 'Modal title', - type: 'modal', - }, - }, - keys: ['some-awesome-id-2', 'some-awesome-id-1'], - }, - wrap_info: null, - warnings: null, - auth: null, - mount_type: '', -}; +import Sinon from 'sinon'; module('Acceptance | landing page dashboard', function (hooks) { setupApplicationTest(hooks); - setupMirage(hooks); + + hooks.beforeEach(async function () { + this.version = this.owner.lookup('service:version'); + this.namespace = this.owner.lookup('service:namespace'); + }); test('navigate to dashboard on login', async function (assert) { - assert.expect(1); await login(); assert.strictEqual(currentURL(), '/vault/dashboard'); }); test('display the version number for the title', async function (assert) { - assert.expect(1); await login(); - await visit('/vault/dashboard'); - const version = this.owner.lookup('service:version'); // Since we're using mirage, version is mocked static value - const versionText = version.isEnterprise - ? `Vault ${version.versionDisplay} root` - : `Vault ${version.versionDisplay}`; + const versionText = this.version.isEnterprise + ? `Vault ${this.version.versionDisplay} root` + : `Vault ${this.version.versionDisplay}`; assert.dom(DASHBOARD.cardHeader('Vault version')).hasText(versionText); }); - module('secrets engines card', function (hooks) { - hooks.beforeEach(async function () { - await login(); - }); - - test('shows a secrets engine card', async function (assert) { - assert.expect(1); - await mountSecrets.enable('pki', 'pki'); - await settled(); - await visit('/vault/dashboard'); - assert.dom(DASHBOARD.cardHeader('Secrets engines')).hasText('Secrets engines'); - // cleanup engine mount - await runCmd(deleteEngineCmd('pki')); - }); - - test('it adds disabled css styling to unsupported secret engines', async function (assert) { - assert.expect(1); - await mountSecrets.enable('nomad', 'nomad'); - await settled(); - await visit('/vault/dashboard'); - assert.dom('[data-test-secrets-engines-row="nomad"] [data-test-view]').doesNotExist(); - // cleanup engine mount - await runCmd(deleteEngineCmd('nomad')); - }); - }); - - module('configuration details card', function (hooks) { - hooks.beforeEach(async function () { - this.data = { - api_addr: 'http://127.0.0.1:8200', - cache_size: 0, - cluster_addr: 'https://127.0.0.1:8201', - cluster_cipher_suites: '', - cluster_name: '', - default_lease_ttl: 0, - default_max_request_duration: 0, - detect_deadlocks: '', - disable_cache: false, - disable_clustering: false, - disable_indexing: false, - disable_mlock: true, - disable_performance_standby: false, - disable_printable_check: false, - disable_sealwrap: false, - disable_sentinel_trace: false, - enable_response_header_hostname: false, - enable_response_header_raft_node_id: false, - enable_ui: true, - experiments: null, - introspection_endpoint: false, - listeners: [ - { - config: { - address: '0.0.0.0:8200', - cluster_address: '0.0.0.0:8201', - tls_disable: true, - }, - type: 'tcp', - }, - ], - log_format: '', - log_level: 'debug', - log_requests_level: '', - max_lease_ttl: '48h', - pid_file: '', - plugin_directory: '', - plugin_file_permissions: 0, - plugin_file_uid: 0, - raw_storage_endpoint: true, - seals: [ - { - disabled: false, - type: 'shamir', - }, - ], - storage: { - cluster_addr: 'https://127.0.0.1:8201', - disable_clustering: false, - raft: { - max_entry_size: '', - }, - redirect_addr: 'http://127.0.0.1:8200', - type: 'raft', - }, - telemetry: { - add_lease_metrics_namespace_labels: false, - circonus_api_app: '', - circonus_api_token: '', - circonus_api_url: '', - circonus_broker_id: '', - circonus_broker_select_tag: '', - circonus_check_display_name: '', - circonus_check_force_metric_activation: '', - circonus_check_id: '', - circonus_check_instance_id: '', - circonus_check_search_tag: '', - circonus_check_tags: '', - circonus_submission_interval: '', - circonus_submission_url: '', - disable_hostname: true, - dogstatsd_addr: '', - dogstatsd_tags: null, - lease_metrics_epsilon: 3600000000000, - maximum_gauge_cardinality: 500, - metrics_prefix: '', - num_lease_metrics_buckets: 168, - prometheus_retention_time: 86400000000000, - stackdriver_debug_logs: false, - stackdriver_location: '', - stackdriver_namespace: '', - stackdriver_project_id: '', - statsd_address: '', - statsite_address: '', - usage_gauge_period: 5000000000, - }, - }; - - this.server.get('sys/config/state/sanitized', () => ({ - data: this.data, - wrap_info: null, - warnings: null, - auth: null, - })); - }); - - test('hides the configuration details card on a non-root namespace enterprise version', async function (assert) { - assert.expect(3); - await login(); - await visit('/vault/dashboard'); - const version = this.owner.lookup('service:version'); - assert.true(version.isEnterprise, 'vault is enterprise'); - assert.dom(DASHBOARD.cardName('configuration-details')).exists(); - await runCmd(createNS('world'), false); - await visit('/vault/dashboard?namespace=world'); - assert.dom(DASHBOARD.cardName('configuration-details')).doesNotExist(); - - // navigate to "root" before deleting - await visit('vault/dashboard'); - // clean up namespace pollution - await runCmd(deleteNS('world')); - }); - - test('shows the configuration details card', async function (assert) { - assert.expect(8); - await login(); - await visit('/vault/dashboard'); - assert.dom(DASHBOARD.cardHeader('configuration')).hasText('Configuration details'); - assert - .dom(DASHBOARD.vaultConfigurationCard.configDetailsField('api_addr')) - .hasText('http://127.0.0.1:8200'); - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('default_lease_ttl')).hasText('0'); - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('max_lease_ttl')).hasText('2 days'); - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled'); // tls_disable=true - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_format')).hasText('None'); - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_level')).hasText('debug'); - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('type')).hasText('raft'); - }); - - test('it should show tls as enabled if tls_disable, tls_cert_file and tls_key_file are in the config', async function (assert) { - assert.expect(1); - this.data.listeners[0].config.tls_disable = false; - this.data.listeners[0].config.tls_cert_file = './cert.pem'; - this.data.listeners[0].config.tls_key_file = './key.pem'; - - await login(); - await visit('/vault/dashboard'); - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled'); - }); - - test('it should show tls as enabled if only cert and key exist in config', async function (assert) { - assert.expect(1); - delete this.data.listeners[0].config.tls_disable; - this.data.listeners[0].config.tls_cert_file = './cert.pem'; - this.data.listeners[0].config.tls_key_file = './key.pem'; - await login(); - await visit('/vault/dashboard'); - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled'); - }); - - test('it should show tls as disabled if there is no tls information in the config', async function (assert) { - assert.expect(1); - this.data.listeners = []; - await login(); - await visit('/vault/dashboard'); - assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled'); - }); - }); - - module('quick actions card', function (hooks) { - hooks.beforeEach(async function () { - await login(); - }); - - test('shows the default state of the quick actions card', async function (assert) { - assert.expect(1); - assert.dom(DASHBOARD.emptyState('no-mount-selected')).exists(); - }); - - test('shows the correct actions and links associated with pki', async function (assert) { - assert.expect(12); - const backend = 'pki-dashboard'; - await mountSecrets.enable('pki', backend); - await runCmd([ - `write ${backend}/roles/some-role \ - issuer_ref="default" \ - allowed_domains="example.com" \ - allow_subdomains=true \ - max_ttl="720h"`, - ]); - await runCmd([`write ${backend}/root/generate/internal issuer_name="Hashicorp" common_name="Hello"`]); - await settled(); - await visit('/vault/dashboard'); - await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend); - await fillIn(DASHBOARD.selectEl, 'Issue certificate'); - assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); - assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use'); - - await selectChoose(DASHBOARD.searchSelect('params'), 'some-role'); - assert.dom(DASHBOARD.actionButton('Issue leaf certificate')).exists({ count: 1 }); - await click(DASHBOARD.actionButton('Issue leaf certificate')); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.roles.role.generate'); - - await visit('/vault/dashboard'); - - await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend); - await fillIn(DASHBOARD.selectEl, 'View certificate'); - assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); - assert.dom(DASHBOARD.subtitle('param')).hasText('Certificate serial number'); - assert.dom(DASHBOARD.actionButton('View certificate')).exists({ count: 1 }); - await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0); - await click(DASHBOARD.actionButton('View certificate')); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backend.pki.certificates.certificate.details' - ); - - await visit('/vault/dashboard'); - - await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend); - await fillIn(DASHBOARD.selectEl, 'View issuer'); - assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); - assert.dom(DASHBOARD.subtitle('param')).hasText('Issuer'); - assert.dom(DASHBOARD.actionButton('View issuer')).exists({ count: 1 }); - await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0); - await click(DASHBOARD.actionButton('View issuer')); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.issuers.issuer.details'); - - // cleanup engine mount - await runCmd(deleteEngineCmd(backend)); - }); - - const newConnection = async (backend, plugin = 'mongodb-database-plugin') => { - const name = `connection-${Date.now()}`; - await connectionPage.visitCreate({ backend }); - await connectionPage.dbPlugin(plugin); - await connectionPage.name(name); - await connectionPage.connectionUrl(`mongodb://127.0.0.1:4321/${name}`); - await connectionPage.toggleVerify(); - await click(GENERAL.submitButton); - await connectionPage.enable(); - return name; - }; - - test('shows the correct actions and links associated with database', async function (assert) { - assert.expect(4); - const databaseBackend = `database-${uuidv4()}`; - await mountSecrets.enable('database', databaseBackend); - await newConnection(databaseBackend); - await runCmd([ - `write ${databaseBackend}/roles/my-role \ - db_name=mongodb-database-plugin \ - creation_statements='{ "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] }' \ - default_ttl="1h" \ - max_ttl="24h`, - ]); - await settled(); - await visit('/vault/dashboard'); - await selectChoose(DASHBOARD.searchSelect('secrets-engines'), databaseBackend); - await fillIn(DASHBOARD.selectEl, 'Generate credentials for database'); - assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); - assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use'); - assert.dom(DASHBOARD.actionButton('Generate credentials')).exists({ count: 1 }); - await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0); - await click(DASHBOARD.actionButton('Generate credentials')); - assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.credentials'); - await runCmd(deleteEngineCmd(databaseBackend)); - }); - - test('does not show kv1 mounts', async function (assert) { - assert.expect(1); - // delete before in case you are rerunning the test and it fails without deleting - await runCmd(deleteEngineCmd('kv1')); - await runCmd([`write sys/mounts/kv1 type=kv`]); - await settled(); - await visit('/vault/dashboard'); - await clickTrigger('#type-to-select-a-mount'); - assert - .dom('.ember-power-select-option') - .doesNotHaveTextContaining('kv1', 'dropdown does not show kv1 mount'); - await runCmd(deleteEngineCmd('kv1')); - }); - }); - - module('client counts card enterprise', function (hooks) { - hooks.beforeEach(async function () { - clientsHandlers(this.server); - this.store = this.owner.lookup('service:store'); - - await login(); - this.response = await this.store.findRecord('clients/activity', 'clients/activity'); - }); - - test('shows the client count card for enterprise', async function (assert) { - const version = this.owner.lookup('service:version'); - assert.true(version.isEnterprise, 'version is enterprise'); - assert.strictEqual(currentURL(), '/vault/dashboard'); - assert.dom(DASHBOARD.cardName('client-count')).exists(); - assert.dom('[data-test-client-count-title]').hasText('Client count'); - await waitFor('[data-test-stat-text="Total"] .stat-label'); - assert.dom('[data-test-stat-text="Total"] .stat-label').hasText('Total'); - assert - .dom('[data-test-stat-text="Total"] .stat-value') - .hasText(formatNumber([this.response.total.clients])); - assert.dom('[data-test-stat-text="New"] .stat-label').hasText('New'); - assert - .dom('[data-test-stat-text="New"] .stat-text') - .hasText('The number of clients new to Vault in the current month.'); - assert - .dom('[data-test-stat-text="New"] .stat-value') - .hasText(formatNumber([this.response.byMonth.lastObject.new_clients.clients])); - assert - .dom(`${GENERAL.flashMessage}.is-info`) - .doesNotExist('Does not show warning about client count estimations'); - }); - }); - - module('replication card enterprise', function (hooks) { - hooks.beforeEach(async function () { - await login(); - await settled(); - await disableReplication('dr'); - await settled(); - await disableReplication('performance'); - await settled(); - }); - - test('shows the replication card empty state in enterprise version', async function (assert) { - await visit('/vault/dashboard'); - const version = this.owner.lookup('service:version'); - assert.true(version.isEnterprise, 'vault is enterprise'); - assert.dom(DASHBOARD.emptyState('replication')).exists(); - assert.dom(DASHBOARD.emptyStateTitle('replication')).hasText('Replication not set up'); - assert - .dom(DASHBOARD.emptyStateMessage('replication')) - .hasText('Data will be listed here. Enable a primary replication cluster to get started.'); - assert.dom(DASHBOARD.emptyStateActions('replication')).hasText('Enable replication'); - }); - - test('hides the replication card on a non-root namespace enterprise version', async function (assert) { - await visit('/vault/dashboard'); - const version = this.owner.lookup('service:version'); - assert.true(version.isEnterprise, 'vault is enterprise'); - assert.dom(DASHBOARD.cardName('replication')).exists(); - await runCmd(createNS('blah'), false); - await visit('/vault/dashboard?namespace=blah'); - assert.dom(DASHBOARD.cardName('replication')).doesNotExist(); - - // navigate to "root" before deleting - await visit('vault/dashboard'); - // clean up namespace pollution - await runCmd(deleteNS('blah')); - }); - - test('it should show replication status if both dr and performance replication are enabled as features in enterprise', async function (assert) { - const version = this.owner.lookup('service:version'); - assert.true(version.isEnterprise, 'vault is enterprise'); - await visit('/vault/replication'); - assert.strictEqual(currentURL(), '/vault/replication'); - await click('[data-test-replication-type-select="performance"]'); - await fillIn('[data-test-replication-cluster-mode-select]', 'primary'); - await click(GENERAL.submitButton); - await pollCluster(this.owner); - await waitFor('[data-test-replication-dashboard]'); - await visit('/vault/dashboard'); - assert.dom(DASHBOARD.title('DR primary')).hasText('DR primary'); - assert.dom(DASHBOARD.tooltipTitle('DR primary')).hasText('not set up'); - assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'DR primary', 'x-circle')).exists(); - assert.dom(DASHBOARD.title('Performance primary')).hasText('Performance primary'); - assert.dom(DASHBOARD.tooltipTitle('Performance primary')).hasText('running'); - assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'check-circle')).exists(); - }); - }); - - module('custom messages auth tests', function (hooks) { - hooks.beforeEach(function () { - return this.server.get('/sys/internal/ui/mounts', () => ({})); - }); - - test('it shows the alert banner and modal message', async function (assert) { - this.server.get('/sys/internal/ui/unauthenticated-messages', function () { - return authenticatedMessageResponse; - }); - await visit('/vault/dashboard'); - const modalId = 'some-awesome-id-1'; - const alertId = 'some-awesome-id-2'; - assert.dom(CUSTOM_MESSAGES.modal(modalId)).exists(); - assert.dom(CUSTOM_MESSAGES.modalTitle(modalId)).hasText('Modal title'); - assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).exists(); - assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).hasText('here is a cool message'); - assert.dom(CUSTOM_MESSAGES.alertTitle(alertId)).hasText('Banner title'); - assert.dom(CUSTOM_MESSAGES.alertDescription(alertId)).hasText('hello world hello wolrd'); - assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title'); - }); - - test('it shows the multiple modal messages', async function (assert) { - const modalIdOne = 'some-awesome-id-2'; - const modalIdTwo = 'some-awesome-id-1'; - - this.server.get('/sys/internal/ui/unauthenticated-messages', function () { - authenticatedMessageResponse.data.key_info[modalIdOne].type = 'modal'; - authenticatedMessageResponse.data.key_info[modalIdOne].title = 'Modal title 1'; - authenticatedMessageResponse.data.key_info[modalIdTwo].type = 'modal'; - authenticatedMessageResponse.data.key_info[modalIdTwo].title = 'Modal title 2'; - return authenticatedMessageResponse; - }); - await visit('/vault/dashboard'); - assert.dom(CUSTOM_MESSAGES.modal(modalIdOne)).exists(); - assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdOne)).hasText('Modal title 1'); - assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).exists(); - assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).hasText('hello world hello wolrd some link title'); - assert.dom(CUSTOM_MESSAGES.modal(modalIdTwo)).exists(); - assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdTwo)).hasText('Modal title 2'); - assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).exists(); - assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).hasText('here is a cool message'); - }); - - test('it shows the multiple banner messages', async function (assert) { - const bannerIdOne = 'some-awesome-id-2'; - const bannerIdTwo = 'some-awesome-id-1'; - - this.server.get('/sys/internal/ui/unauthenticated-messages', function () { - authenticatedMessageResponse.data.key_info[bannerIdOne].type = 'banner'; - authenticatedMessageResponse.data.key_info[bannerIdOne].title = 'Banner title 1'; - authenticatedMessageResponse.data.key_info[bannerIdTwo].type = 'banner'; - authenticatedMessageResponse.data.key_info[bannerIdTwo].title = 'Banner title 2'; - return authenticatedMessageResponse; - }); - await visit('/vault/dashboard'); - assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdOne)).hasText('Banner title 1'); - assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdOne)).hasText('hello world hello wolrd'); - assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title'); - assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdTwo)).hasText('Banner title 2'); - assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdTwo)).hasText('here is a cool message'); - }); + test('hides the configuration details card on a non-root namespace enterprise version', async function (assert) { + // The route checks `inRootNamespace` so stub that return + const nsStub = Sinon.stub(this.namespace, 'inRootNamespace').get(() => false); + await login(); + await visit('/vault/dashboard'); + assert.dom(DASHBOARD.cardName('configuration-details')).doesNotExist(); + nsStub.restore(); }); }); diff --git a/ui/tests/acceptance/jwt-auth-method-test.js b/ui/tests/acceptance/jwt-auth-method-test.js deleted file mode 100644 index 9217f26563..0000000000 --- a/ui/tests/acceptance/jwt-auth-method-test.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, visit, fillIn, waitFor } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { Response } from 'miragejs'; -import { ERROR_JWT_LOGIN } from 'vault/utils/auth-form-helpers'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors'; -import { overrideResponse } from 'vault/tests/helpers/stubs'; - -module('Acceptance | jwt auth method', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - localStorage.clear(); // ensure that a token isn't stored otherwise visit('/vault/auth') will redirect to secrets - this.server.post( - '/auth/:path/oidc/auth_url', - () => - new Response( - 400, - { 'Content-Type': 'application/json' }, - JSON.stringify({ errors: [ERROR_JWT_LOGIN] }) - ) - ); - this.server.get('/auth/foo/oidc/callback', () => ({ - auth: { client_token: 'root' }, - })); - }); - - test('it works correctly with default name and no role', async function (assert) { - assert.expect(6); - this.server.post('/auth/jwt/login', (schema, req) => { - const { jwt, role } = JSON.parse(req.requestBody); - assert.true(true, 'request made to auth/jwt/login after submit'); - assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body'); - assert.strictEqual(role, undefined, 'role is not sent in body when not filled in'); - return overrideResponse(403); - }); - await visit('/vault/auth'); - await fillIn(AUTH_FORM.selectMethod, 'jwt'); - assert.dom(GENERAL.inputByAttr('role')).exists({ count: 1 }, 'Role input exists'); - assert.dom(GENERAL.inputByAttr('jwt')).exists({ count: 1 }, 'JWT input exists'); - await fillIn(GENERAL.inputByAttr('jwt'), 'my-test-jwt-token'); - await click(GENERAL.submitButton); - await waitFor(GENERAL.messageError); - assert.dom(GENERAL.messageError).hasText('Error Authentication failed: permission denied'); - }); - - test('it works correctly with default name and a role', async function (assert) { - assert.expect(7); - this.server.post('/auth/jwt/login', (schema, req) => { - const { jwt, role } = JSON.parse(req.requestBody); - assert.ok(true, 'request made to auth/jwt/login after login'); - assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body'); - assert.strictEqual(role, 'some-role', 'role is sent in the body when filled in'); - return overrideResponse(403); - }); - await visit('/vault/auth'); - await fillIn(AUTH_FORM.selectMethod, 'jwt'); - assert.dom(GENERAL.inputByAttr('role')).exists({ count: 1 }, 'Role input exists'); - assert.dom(GENERAL.inputByAttr('jwt')).exists({ count: 1 }, 'JWT input exists'); - await fillIn(GENERAL.inputByAttr('role'), 'some-role'); - await fillIn(GENERAL.inputByAttr('jwt'), 'my-test-jwt-token'); - assert.dom(GENERAL.inputByAttr('jwt')).exists({ count: 1 }, 'JWT input exists'); - await click(GENERAL.submitButton); - await waitFor(GENERAL.messageError); - assert.dom(GENERAL.messageError).hasText('Error Authentication failed: permission denied'); - }); - - test('it works correctly with custom endpoint and a role', async function (assert) { - assert.expect(6); - this.server.get('/sys/internal/ui/mounts', () => ({ - data: { - auth: { - 'test-jwt/': { description: '', options: {}, type: 'jwt' }, - }, - }, - })); - this.server.post('/auth/test-jwt/login', (schema, req) => { - const { jwt, role } = JSON.parse(req.requestBody); - assert.ok(true, 'request made to auth/custom-jwt-login after login'); - assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body'); - assert.strictEqual(role, 'some-role', 'role is sent in body when filled in'); - return overrideResponse(403); - }); - await visit('/vault/auth'); - await click(AUTH_FORM.tabBtn('jwt')); - assert.dom(GENERAL.inputByAttr('role')).exists({ count: 1 }, 'Role input exists'); - assert.dom(GENERAL.inputByAttr('jwt')).exists({ count: 1 }, 'JWT input exists'); - await fillIn(GENERAL.inputByAttr('role'), 'some-role'); - await fillIn(GENERAL.inputByAttr('jwt'), 'my-test-jwt-token'); - await click(GENERAL.submitButton); - await waitFor(GENERAL.messageError); - assert.dom(GENERAL.messageError).hasText('Error Authentication failed: permission denied'); - }); -}); diff --git a/ui/tests/acceptance/mfa-login-test.js b/ui/tests/acceptance/mfa-login-test.js index 124a7fb06d..4b213f3a01 100644 --- a/ui/tests/acceptance/mfa-login-test.js +++ b/ui/tests/acceptance/mfa-login-test.js @@ -75,6 +75,7 @@ module('Acceptance | mfa-login', function (hooks) { test('it should handle single constraint with passcode method', async function (assert) { assert.expect(4); await login('mfa-a'); + await waitFor(GENERAL.title); assert.dom(GENERAL.title).hasText('Verify your identity'); assert.dom(MFA_SELECTORS.select()).doesNotExist('Select is hidden for single method'); assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, 'Single passcode input renders'); @@ -117,6 +118,7 @@ module('Acceptance | mfa-login', function (hooks) { test('it should handle single constraint with 2 push methods', async function (assert) { assert.expect(4); await login('mfa-d'); + await waitFor(MFA_SELECTORS.mfaForm); assert.dom(GENERAL.title).hasText('Verify your identity'); assert.dom(GENERAL.button('Verify with Okta')).exists('It renders button for Okta'); assert.dom(GENERAL.button('Verify with Duo')).exists('It renders button for Duo'); @@ -127,6 +129,7 @@ module('Acceptance | mfa-login', function (hooks) { test('it should handle single constraint with 1 passcode and 1 push method', async function (assert) { assert.expect(3); await login('mfa-e'); + await waitFor(MFA_SELECTORS.mfaForm); assert.dom(GENERAL.button('Verify with Okta')).exists('It renders button for Okta'); await click(GENERAL.button('Verify with TOTP')); assert.dom(MFA_SELECTORS.passcode()).exists('Passcode input renders'); diff --git a/ui/tests/acceptance/oidc-auth-method-test.js b/ui/tests/acceptance/oidc-auth-method-test.js index 3a57c66425..51e27cae3f 100644 --- a/ui/tests/acceptance/oidc-auth-method-test.js +++ b/ui/tests/acceptance/oidc-auth-method-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import { click, fillIn, visit, waitFor } from '@ember/test-helpers'; +import { click, fillIn, waitFor } from '@ember/test-helpers'; import { logout } from 'vault/tests/helpers/auth/auth-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { @@ -13,12 +13,12 @@ import { triggerMessageEvent, windowStub, } from 'vault/tests/helpers/oidc-window-stub'; -import { Response } from 'miragejs'; import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { ERROR_MISSING_PARAMS, ERROR_POPUP_FAILED, ERROR_WINDOW_CLOSED } from 'vault/utils/auth-form-helpers'; import { getErrorResponse } from 'vault/tests/helpers/api/error-response'; import sinon from 'sinon'; +import { RESPONSE_STUBS } from '../helpers/auth/response-stubs'; module('Acceptance | oidc auth method', function (hooks) { setupApplicationTest(hooks); @@ -27,64 +27,20 @@ module('Acceptance | oidc auth method', function (hooks) { hooks.beforeEach(function () { this.openStub = windowStub(); - this.setupMocks = (assert) => { + this.setupMocks = () => { this.server.post('/auth/oidc/oidc/auth_url', () => ({ data: { auth_url: 'http://example.com' }, })); - // there was a bug that would result in the /auth/:path/login endpoint hit with an empty payload rather than lookup-self - // ensure that the correct endpoint is hit after the oidc callback - if (assert) { - this.server.get('/auth/token/lookup-self', (schema, req) => { - assert.ok(true, 'request made to auth/token/lookup-self after oidc callback'); - return req.passthrough(); - }); - } + this.server.get(`/auth/oidc/oidc/callback`, () => RESPONSE_STUBS.oidc['oidc/callback']); + this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.oidc['lookup-self']); }; - - this.server.get('/auth/oidc/oidc/callback', () => ({ - auth: { client_token: 'root' }, - })); - - // ensure clean state - // Cannot use logout() here because it will hit the internal mount request before the mocks can interrupt it - window.localStorage.clear(); }); hooks.afterEach(async function () { this.openStub.restore(); - }); - - test('it should login with oidc when selected from auth methods dropdown', async function (assert) { - assert.expect(1); - this.setupMocks(assert); - await visit('/vault/auth'); - await fillIn(AUTH_FORM.selectMethod, 'oidc'); - - triggerMessageEvent('oidc'); - - await click(GENERAL.submitButton); - }); - - test('it should login with oidc from listed auth mount tab', async function (assert) { - assert.expect(3); - this.setupMocks(assert); // assert count (1) - - this.server.get('/sys/internal/ui/mounts', () => ({ - data: { - auth: { - 'test-path/': { description: '', options: {}, type: 'oidc' }, - }, - }, - })); - - // this assertion is hit twice, once on the initial visit to the login form, then again on "Sign in" - this.server.post('/auth/test-path/oidc/auth_url', () => { - assert.true(true, 'auth_url request made to correct non-standard mount path'); - return { data: { auth_url: 'http://example.com' } }; - }); - await visit('/vault/auth'); - triggerMessageEvent('oidc'); - await click(GENERAL.submitButton); + // ensure clean state + // Cannot use logout() here because it will hit the internal mount request before the mocks can interrupt it + localStorage.clear(); }); // coverage for bug where token was selected as auth method for oidc and jwt @@ -105,44 +61,6 @@ module('Acceptance | oidc auth method', function (hooks) { assert.dom(AUTH_FORM.selectMethod).hasValue('oidc', 'Previous auth method selected on logout'); }); - test('it should fetch role when switching between oidc/jwt auth methods and changing the mount path', async function (assert) { - await logout(); - let reqCount = 0; - this.server.post('/auth/:method/oidc/auth_url', (schema, req) => { - reqCount++; - const errors = - req.params.method === 'jwt' ? ['OIDC login is not configured for this mount'] : ['missing role']; - return new Response(400, {}, { errors }); - }); - - await fillIn(AUTH_FORM.selectMethod, 'oidc'); - assert.dom(GENERAL.inputByAttr('jwt')).doesNotExist('JWT Token input hidden for OIDC'); - await fillIn(AUTH_FORM.selectMethod, 'jwt'); - assert.dom(GENERAL.inputByAttr('jwt')).exists('JWT Token input renders for JWT configured method'); - await click(AUTH_FORM.advancedSettings); - await fillIn(GENERAL.inputByAttr('path'), 'foo'); - assert.strictEqual(reqCount, 3, 'Role is fetched when dependant values are changed'); - }); - - test('it should display role fetch errors when signing in with OIDC', async function (assert) { - this.server.post('/auth/:method/oidc/auth_url', (schema, req) => { - const { role } = JSON.parse(req.requestBody); - const status = role ? 403 : 400; - const errors = role ? ['permission denied'] : ['missing role']; - return new Response(status, {}, { errors }); - }); - await logout(); - await fillIn(AUTH_FORM.selectMethod, 'oidc'); - await click(GENERAL.submitButton); - assert.dom(GENERAL.messageError).hasText('Error Authentication failed: Invalid role. Please try again.'); - - await fillIn(GENERAL.inputByAttr('role'), 'test'); - await click(GENERAL.submitButton); - assert - .dom(GENERAL.messageError) - .hasText('Error Authentication failed: Error fetching role: permission denied'); - }); - // test case for https://github.com/hashicorp/vault/issues/12436 test('it should ignore messages sent from outside the app while waiting for oidc callback', async function (assert) { assert.expect(3); // one for both message events (2) and one for callback request diff --git a/ui/tests/acceptance/secrets/mounts-test.js b/ui/tests/acceptance/secrets/mounts-test.js index 54b819f8ba..817968192c 100644 --- a/ui/tests/acceptance/secrets/mounts-test.js +++ b/ui/tests/acceptance/secrets/mounts-test.js @@ -143,62 +143,6 @@ module('Acceptance | secrets/mounts', function (hooks) { .exists({ count: 1 }, 'renders only one instance of the engine'); }); - test('version 2 with no update to config endpoint still allows mount of secret engine', async function (assert) { - const enginePath = `kv-noUpdate-${this.uid}`; - const V2_POLICY = ` - path "${enginePath}/*" { - capabilities = ["list","create","read","sudo","delete"] - } - path "sys/mounts/*" - { - capabilities = ["create", "read", "update", "delete", "list", "sudo"] - } - - # List existing secrets engines. - path "sys/mounts" - { - capabilities = ["read"] - } - # Allow page to load after mount - path "sys/internal/ui/mounts/${enginePath}" { - capabilities = ["read"] - } - `; - await consoleComponent.toggle(); - await consoleComponent.runCommands( - [ - // delete any previous mount with same name - `delete sys/mounts/${enginePath}`, - `write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`, - 'write -field=client_token auth/token/create policies=kv-v2-degrade', - ], - false - ); - await settled(); - const userToken = consoleComponent.lastLogOutput; - - await login(userToken); - // create the engine - await mountSecrets.visit(); - await click(GENERAL.cardContainer('kv')); - await fillIn(GENERAL.inputByAttr('path'), enginePath); - await mountSecrets.setMaxVersion(101); - await click(GENERAL.submitButton); - - assert - .dom('[data-test-flash-message]') - .containsText( - `You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.` - ); - assert.strictEqual( - currentURL(), - `/vault/secrets/${enginePath}/kv/list`, - 'After mounting, redirects to secrets list page' - ); - await configPage.visit({ backend: enginePath }); - await settled(); - }); - test('it should transition to mountable addon engine after mount success', async function (assert) { // test supported backends that ARE ember engines (enterprise only engines are tested individually) const addons = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter( diff --git a/ui/tests/helpers/auth/auth-helpers.ts b/ui/tests/helpers/auth/auth-helpers.ts index 8e4825e1e1..ee42ff1c55 100644 --- a/ui/tests/helpers/auth/auth-helpers.ts +++ b/ui/tests/helpers/auth/auth-helpers.ts @@ -78,6 +78,7 @@ const LOGIN_DATA = { token: { token: 'mysupersecuretoken' }, username: { username: 'matilda', password: 'some-password' }, role: { role: 'some-dev' }, + jwt: { role: 'some-dev', jwt: 'some-jwt-token' }, }; // maps auth type to login input data export const AUTH_METHOD_LOGIN_DATA = { @@ -91,7 +92,7 @@ export const AUTH_METHOD_LOGIN_DATA = { radius: LOGIN_DATA.username, // role oidc: LOGIN_DATA.role, - jwt: LOGIN_DATA.role, + jwt: LOGIN_DATA.jwt, saml: LOGIN_DATA.role, }; diff --git a/ui/tests/helpers/auth/response-stubs.ts b/ui/tests/helpers/auth/response-stubs.ts index 85c31b9545..4b2b5a0613 100644 --- a/ui/tests/helpers/auth/response-stubs.ts +++ b/ui/tests/helpers/auth/response-stubs.ts @@ -3,6 +3,9 @@ * SPDX-License-Identifier: BUSL-1.1 */ +import timestamp from 'core/utils/timestamp'; +import { addDays } from 'date-fns'; + /* Authentication requests return authentication information in either the "auth" or "data" key, depending on the authentication method. @@ -52,14 +55,14 @@ export const RESPONSE_STUBS = { ...BASE_REQUEST_DATA, data: { accessor: 'MkjSR78ducuarJ6ypCDbHhBp', - creation_time: 1749659696, + creation_time: timestamp.now().getTime(), creation_ttl: 2764800, display_name: 'jwt-ugaKjSEAKwQkiGh1rbnGkp39oCSe3LQ2@clients', entity_id: 'b6061dc8-a19e-195e-43a8-43d37f4625dd', - expire_time: '2025-07-13T12:34:56.345108-04:00', + expire_time: addDays(timestamp.now(), 1), explicit_max_ttl: 0, id: 'hvs.myvaultgeneratedjwttoken', - issue_time: '2025-06-11T12:34:56.345113-04:00', + issue_time: timestamp.now().toISOString(), meta: { role: 'reader', }, @@ -145,14 +148,14 @@ export const RESPONSE_STUBS = { ...BASE_REQUEST_DATA, data: { accessor: 'ew50HTqF2xgsmaKIsdKpJtTc', - creation_time: 1749584514, + creation_time: timestamp.now().getTime(), creation_ttl: 2764800, display_name: 'my-oidc-google-oauth2|105299854624506884705', entity_id: '18b57edf-acff-3e65-2ff2-6c772ce44924', - expire_time: '2025-07-12T15:41:54.961915-04:00', + expire_time: addDays(timestamp.now(), 1), explicit_max_ttl: 0, id: 'hvs.myvaultgeneratedoidctoken', - issue_time: '2025-06-10T15:41:54.961919-04:00', + issue_time: timestamp.now().toISOString(), meta: { role: 'reader', }, @@ -219,14 +222,14 @@ export const RESPONSE_STUBS = { lease_duration: 0, data: { accessor: '3tl0hAUwdDJVduSEnIca7Tr6', - creation_time: 1744649084, + creation_time: timestamp.now().getTime(), creation_ttl: 2764800, display_name: 'token', entity_id: '', - expire_time: '2025-05-16T09:44:44.837733-07:00', + expire_time: addDays(timestamp.now(), 1), explicit_max_ttl: 0, id: 'hvs.myvaultgeneratedtoken', - issue_time: '2025-04-14T09:44:44.837735-07:00', + issue_time: timestamp.now().toISOString(), meta: null, num_uses: 0, orphan: false, @@ -289,14 +292,14 @@ export const RESPONSE_STUBS = { ...BASE_REQUEST_DATA, data: { accessor: 'H4fWtQaYX3aaEg1JIPSWiK9v', - creation_time: 1749585309, + creation_time: timestamp.now().getTime(), creation_ttl: 1800, display_name: 'saml-vaultuser@hashicorp.com', entity_id: '81fc10e5-49a3-d0a2-9835-ac6b551ee266', - expire_time: '2025-06-10T16:25:09.246659-04:00', + expire_time: addDays(timestamp.now(), 1), explicit_max_ttl: 0, id: 'hvs.myvaultgeneratedsamltoken', - issue_time: '2025-06-10T15:55:09.246666-04:00', + issue_time: timestamp.now().toISOString(), meta: { role: 'dev', }, diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index 59ca51c80d..ff4d218d28 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -38,7 +38,7 @@ export const GENERAL = { /* ────── Menus & Lists ────── */ menuTrigger: '[data-test-popup-menu-trigger]', menuItem: (name: string) => `[data-test-popup-menu="${name}"]`, - listItem: (label: string) => `[data-test-list-item="${label}"]`, + listItem: (label: string) => (label ? `[data-test-list-item="${label}"]` : '[data-test-list-item]'), listItemLink: '[data-test-list-item-link]', linkedBlock: (item: string) => `[data-test-linked-block="${item}"]`, diff --git a/ui/tests/integration/components/auth/form-template-test.js b/ui/tests/integration/components/auth/form-template-test.js index 4a74b0d46e..8d6b53ae89 100644 --- a/ui/tests/integration/components/auth/form-template-test.js +++ b/ui/tests/integration/components/auth/form-template-test.js @@ -232,9 +232,12 @@ module('Integration | Component | auth | form template', function (hooks) { }); // in the ent module to test ALL supported login methods - // iterating in tests should generally be avoided, but purposefully wanted to test the component - // renders as expected as auth types change + // iterating in tests should generally be avoided, but the for loop below is intentional + // to test the component renders as expected when auth types change test('it selects each supported auth type and renders its form and relevant fields', async function (assert) { + const routerStub = sinon.stub(this.router, 'urlFor').returns('123-example.com'); + // Setup so jwt auth mount is configured for jwt login and not oidc (jwt/oidc are interchangeable) + this.server.post(`/auth/jwt/oidc/auth_url`, () => overrideResponse(400, { errors: [ERROR_JWT_LOGIN] })); const authMethodTypes = supportedTypes(true); const totalFields = Object.values(AUTH_METHOD_LOGIN_DATA).reduce( (sum, obj) => sum + Object.keys(obj).length, @@ -245,15 +248,8 @@ module('Integration | Component | auth | form template', function (hooks) { await this.renderComponent(); for (const authType of authMethodTypes) { - let stub; - if (['oidc', 'jwt'].includes(authType)) { - stub = sinon.stub(this.router, 'urlFor').returns('123-example.com'); - } const loginData = AUTH_METHOD_LOGIN_DATA[authType]; - - const fields = Object.keys(loginData); await fillIn(GENERAL.selectByAttr('auth type'), authType); - assert.dom(GENERAL.selectByAttr('auth type')).hasValue(authType), `${authType}: it selects type`; assert.dom(AUTH_FORM.authForm(authType)).exists(`${authType}: it renders form component`); @@ -269,14 +265,11 @@ module('Integration | Component | auth | form template', function (hooks) { const assertion = authType === 'token' ? 'doesNotExist' : 'exists'; assert.dom(GENERAL.inputByAttr('path'))[assertion](`${authType}: mount path input ${assertion}`); - fields.forEach((field) => { + Object.keys(loginData).forEach((field) => { assert.dom(GENERAL.inputByAttr(field)).exists(`${authType}: ${field} input renders`); }); - - if (stub) { - stub.restore(); - } } + routerStub.restore(); }); test('dropdown includes enterprise methods', async function (assert) { diff --git a/ui/tests/integration/components/auth/page/method-authentication-test.js b/ui/tests/integration/components/auth/page/method-authentication-test.js index 2ddf48f7fc..520ddec90c 100644 --- a/ui/tests/integration/components/auth/page/method-authentication-test.js +++ b/ui/tests/integration/components/auth/page/method-authentication-test.js @@ -19,9 +19,14 @@ import sinon from 'sinon'; const methodAuthenticationTests = (test) => { test('it sets token data on login for default path', async function (assert) { - assert.expect(5); + const count = this.assertTokenLookup ? 6 : 5; + assert.expect(count); // Setup this.stubRequests(); + if (this.assertTokenLookup) { + this.assertTokenLookup(assert); + } + // Render and log in await this.renderComponent(); await fillIn(AUTH_FORM.selectMethod, this.authType); @@ -117,7 +122,12 @@ module('Integration | Component | auth | page | method authentication', function overrideResponse(400, { errors: [ERROR_JWT_LOGIN] }) ); this.server.post(`/auth/${this.path}/login`, () => this.response); - this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.jwt['lookup-self']); + }; + this.assertTokenLookup = (assert) => { + this.server.get(`/auth/token/lookup-self`, () => { + assert.true(true, 'request made to auth/token/lookup-self after jwt login'); + return RESPONSE_STUBS.jwt['lookup-self']; + }); }; }); @@ -157,7 +167,14 @@ module('Integration | Component | auth | page | method authentication', function return { data: { auth_url: 'http://dev-foo-bar.com' } }; }); this.server.get(`/auth/${this.path}/oidc/callback`, () => this.response); - this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.oidc['lookup-self']); + }; + this.assertTokenLookup = (assert) => { + this.server.get(`/auth/token/lookup-self`, () => { + // there was a bug that would result in the /auth/:path/login endpoint hit with an empty payload rather than lookup-self + // ensure that the correct endpoint is hit after the oidc callback + assert.true(true, 'request made to auth/token/lookup-self after oidc callback'); + return RESPONSE_STUBS.oidc['lookup-self']; + }); }; // additional OIDC setup @@ -275,7 +292,12 @@ module('Integration | Component | auth | page | method authentication', function }, })); this.server.post(`/auth/${this.path}/token`, () => this.response); - this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.saml['lookup-self']); + }; + this.assertTokenLookup = (assert) => { + this.server.get(`/auth/token/lookup-self`, () => { + assert.true(true, 'request made to auth/token/lookup-self after saml token exchange and login'); + return RESPONSE_STUBS.saml['lookup-self']; + }); }; this.windowStub = windowStub(); }); diff --git a/ui/tests/integration/components/dashboard/overview-test.js b/ui/tests/integration/components/dashboard/overview-test.js index d689b90298..1f71b6fd69 100644 --- a/ui/tests/integration/components/dashboard/overview-test.js +++ b/ui/tests/integration/components/dashboard/overview-test.js @@ -9,6 +9,7 @@ import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors'; +import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; module('Integration | Component | dashboard/overview', function (hooks) { setupRenderingTest(hooks); @@ -20,19 +21,10 @@ module('Integration | Component | dashboard/overview', function (hooks) { this.permissions = this.owner.lookup('service:permissions'); this.store = this.owner.lookup('service:store'); this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1+ent'; - this.version.type = 'enterprise'; this.isRootNamespace = true; this.replication = { - dr: { - clusterId: '123', - state: 'running', - }, - performance: { - clusterId: 'abc-1', - state: 'running', - isPrimary: true, - }, + dr: { clusterId: '123', state: 'running' }, + performance: { clusterId: 'abc-1', state: 'running', isPrimary: true }, }; this.store.pushPayload('secret-engine', { modelName: 'secret-engine', @@ -67,7 +59,7 @@ module('Integration | Component | dashboard/overview', function (hooks) { }; this.refreshModel = () => {}; this.renderComponent = async () => { - return await render( + return render( hbs` { + store.pushPayload('secret-engine', { modelName: 'secret-engine', data: modelData }); + }); + this.secretsEngines = store.peekAll('secret-engine', {}); this.renderComponent = () => { return render(hbs``); }; @@ -103,6 +52,19 @@ module('Integration | Component | dashboard/quick-actions-card', function (hooks }); }); + hooks.afterEach(function () { + this.transitionStub.restore(); + }); + + test('it does not include kvv1 mounts', async function (assert) { + await this.renderComponent(); + await clickTrigger('#type-to-select-a-mount'); + + findAll('.ember-power-select-option').forEach((o) => { + assert.dom(o).doesNotHaveTextContaining('kv-v1-test'); + }); + }); + test('it should show quick action empty state if no engine is selected', async function (assert) { await this.renderComponent(); assert.dom('.title').hasText('Quick actions'); @@ -110,29 +72,96 @@ module('Integration | Component | dashboard/quick-actions-card', function (hooks assert.dom(DASHBOARD.emptyState('no-mount-selected')).exists({ count: 1 }); }); - test('it should show correct actions for pki', async function (assert) { + test('it selects a pki role and issues a leaf certificate', async function (assert) { + const backend = 'pki-0-test'; + this.server.get(`/${backend}/roles`, () => ({ data: { keys: ['some-role'] } })); await this.renderComponent(); - await selectChoose(DASHBOARD.searchSelect('secrets-engines'), 'pki-0-test'); + + await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend); await fillIn(DASHBOARD.selectEl, 'Issue certificate'); assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); - await fillIn(DASHBOARD.selectEl, 'Issue certificate'); - assert.dom(DASHBOARD.actionButton('Issue leaf certificate')).exists({ count: 1 }); assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use'); + await selectChoose(DASHBOARD.searchSelect('params'), 'some-role'); + assert.dom(DASHBOARD.actionButton('Issue leaf certificate')).exists({ count: 1 }); + await click(DASHBOARD.actionButton('Issue leaf certificate')); + const [route, backendParam, roleParam] = this.transitionStub.lastCall.args; + assert.strictEqual( + route, + 'vault.cluster.secrets.backend.pki.roles.role.generate', + 'transition is called with expected route' + ); + assert.strictEqual(backendParam, backend, 'transition has expected backend param'); + assert.strictEqual(roleParam, 'some-role', 'transition has expected role param'); + }); + + test('it views a pki certificate', async function (assert) { + const backend = 'pki-0-test'; + this.server.get(`/${backend}/certs`, () => ({ data: { keys: ['51:1c:39:42:ba'] } })); + await this.renderComponent(); + + await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend); await fillIn(DASHBOARD.selectEl, 'View certificate'); + assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); assert.dom(DASHBOARD.subtitle('param')).hasText('Certificate serial number'); assert.dom(DASHBOARD.actionButton('View certificate')).exists({ count: 1 }); + await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0); + await click(DASHBOARD.actionButton('View certificate')); + const [route, backendParam, certParam] = this.transitionStub.lastCall.args; + assert.strictEqual( + route, + 'vault.cluster.secrets.backend.pki.certificates.certificate.details', + 'transition is called with expected route' + ); + assert.strictEqual(backendParam, backend, 'transition has expected backend param'); + assert.strictEqual(certParam, '51:1c:39:42:ba', 'transition has expected cert param'); + }); + + test('it views a pki issuer', async function (assert) { + const backend = 'pki-0-test'; + this.server.get(`/${backend}/issuers`, () => { + return { data: { key_info: { '101709a1': { issuer_name: 'test' } }, keys: ['101709a1'] } }; + }); + await this.renderComponent(); + + await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend); await fillIn(DASHBOARD.selectEl, 'View issuer'); + assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); assert.dom(DASHBOARD.subtitle('param')).hasText('Issuer'); assert.dom(DASHBOARD.actionButton('View issuer')).exists({ count: 1 }); + await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0); + await click(DASHBOARD.actionButton('View issuer')); + const [route, backendParam, issuerParam] = this.transitionStub.lastCall.args; + assert.strictEqual( + route, + 'vault.cluster.secrets.backend.pki.issuers.issuer.details', + 'transition is called with expected route' + ); + assert.strictEqual(backendParam, backend, 'transition has expected backend param'); + assert.strictEqual(issuerParam, '101709a1', 'transition has expected issuer param'); }); - test('it should show correct actions for database', async function (assert) { + + test('it selects a role and generates credentials for a database', async function (assert) { + const backend = 'database-test'; + this.server.get(`/${backend}/roles`, () => ({ data: { keys: ['my-role'] } })); await this.renderComponent(); - await selectChoose(DASHBOARD.searchSelect('secrets-engines'), 'database-test'); - assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); + + await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend); await fillIn(DASHBOARD.selectEl, 'Generate credentials for database'); + assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist(); assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use'); assert.dom(DASHBOARD.actionButton('Generate credentials')).exists({ count: 1 }); + await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0); + await click(DASHBOARD.actionButton('Generate credentials')); + const [route, backendParam, issuerParam] = this.transitionStub.lastCall.args; + assert.strictEqual( + route, + 'vault.cluster.secrets.backend.credentials', + 'transition is called with expected route' + ); + assert.strictEqual(backendParam, backend, 'transition has expected backend param'); + assert.strictEqual(issuerParam, 'my-role', 'transition has expected role param'); }); + test('it should show correct actions for kv', async function (assert) { await this.renderComponent(); await clickTrigger('#type-to-select-a-mount'); diff --git a/ui/tests/integration/components/dashboard/replication-card-test.js b/ui/tests/integration/components/dashboard/replication-card-test.js index e8d9a154ba..acbdaa450f 100644 --- a/ui/tests/integration/components/dashboard/replication-card-test.js +++ b/ui/tests/integration/components/dashboard/replication-card-test.js @@ -20,11 +20,12 @@ module('Integration | Component | dashboard/replication-card', function (hooks) dr: { clusterId: '123', state: 'running', + mode: 'primary', }, performance: { clusterId: 'abc-1', state: 'running', - isPrimary: true, + mode: 'primary', }, }; this.version = { @@ -52,17 +53,9 @@ module('Integration | Component | dashboard/replication-card', function (hooks) assert.dom(DASHBOARD.tooltipTitle('Performance primary')).hasText('running'); assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'check-circle')).exists(); }); + test('it should display replication information if both dr and performance replication are enabled as features and only dr is setup', async function (assert) { - this.replication = { - dr: { - clusterId: '123', - state: 'running', - }, - performance: { - clusterId: '', - isPrimary: true, - }, - }; + this.replication.performance = { mode: 'disabled' }; await render( hbs` `); @@ -39,6 +38,22 @@ module('Integration | Component | dashboard/secrets-engines-card', function (hoo assert.dom('[data-test-secrets-engines-card-show-all]').doesNotExist(); }); + test('it disables unsupported secret engines', async function (assert) { + this.store.pushPayload('secret-engine', { + modelName: 'secret-engine', + data: { + accessor: 'nomad_f3400dee', + path: 'nomad-test/', + type: 'nomad', + }, + }); + this.secretsEngines = this.store.peekAll('secret-engine', {}); + + await render(hbs``); + assert.dom('[data-test-secrets-engines-row="nomad"] [data-test-view]').doesNotExist(); + assert.dom(SES.secretPath('nomad-test/')).hasClass('has-text-grey'); + }); + module('secrets engines with 5 or more enabled', function (hooks) { hooks.beforeEach(function () { this.store.pushPayload('secret-engine', { diff --git a/ui/tests/integration/components/dashboard/vault-configuration-details-card-test.js b/ui/tests/integration/components/dashboard/vault-configuration-details-card-test.js new file mode 100644 index 0000000000..5f588fe5bf --- /dev/null +++ b/ui/tests/integration/components/dashboard/vault-configuration-details-card-test.js @@ -0,0 +1,147 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors'; + +module('Integration | Component | dashboard/vault-configuration-details-card', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.data = { + api_addr: 'http://127.0.0.1:8200', + cache_size: 0, + cluster_addr: 'https://127.0.0.1:8201', + cluster_cipher_suites: '', + cluster_name: '', + default_lease_ttl: 0, + default_max_request_duration: 0, + detect_deadlocks: '', + disable_cache: false, + disable_clustering: false, + disable_indexing: false, + disable_mlock: true, + disable_performance_standby: false, + disable_printable_check: false, + disable_sealwrap: false, + disable_sentinel_trace: false, + enable_response_header_hostname: false, + enable_response_header_raft_node_id: false, + enable_ui: true, + experiments: null, + introspection_endpoint: false, + listeners: [ + { + config: { + address: '0.0.0.0:8200', + cluster_address: '0.0.0.0:8201', + tls_disable: true, + }, + type: 'tcp', + }, + ], + log_format: '', + log_level: 'debug', + log_requests_level: '', + max_lease_ttl: '48h', + pid_file: '', + plugin_directory: '', + plugin_file_permissions: 0, + plugin_file_uid: 0, + raw_storage_endpoint: true, + seals: [ + { + disabled: false, + type: 'shamir', + }, + ], + storage: { + cluster_addr: 'https://127.0.0.1:8201', + disable_clustering: false, + raft: { + max_entry_size: '', + }, + redirect_addr: 'http://127.0.0.1:8200', + type: 'raft', + }, + telemetry: { + add_lease_metrics_namespace_labels: false, + circonus_api_app: '', + circonus_api_token: '', + circonus_api_url: '', + circonus_broker_id: '', + circonus_broker_select_tag: '', + circonus_check_display_name: '', + circonus_check_force_metric_activation: '', + circonus_check_id: '', + circonus_check_instance_id: '', + circonus_check_search_tag: '', + circonus_check_tags: '', + circonus_submission_interval: '', + circonus_submission_url: '', + disable_hostname: true, + dogstatsd_addr: '', + dogstatsd_tags: null, + lease_metrics_epsilon: 3600000000000, + maximum_gauge_cardinality: 500, + metrics_prefix: '', + num_lease_metrics_buckets: 168, + prometheus_retention_time: 86400000000000, + stackdriver_debug_logs: false, + stackdriver_location: '', + stackdriver_namespace: '', + stackdriver_project_id: '', + statsd_address: '', + statsite_address: '', + usage_gauge_period: 5000000000, + }, + }; + + this.renderComponent = () => { + return render(hbs``); + }; + }); + + test('it renders configuration details', async function (assert) { + await this.renderComponent(); + assert.dom(DASHBOARD.cardHeader('configuration')).hasText('Configuration details'); + assert + .dom(DASHBOARD.vaultConfigurationCard.configDetailsField('api_addr')) + .hasText('http://127.0.0.1:8200'); + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('default_lease_ttl')).hasText('0'); + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('max_lease_ttl')).hasText('2 days'); + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled'); // tls_disable=true + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_format')).hasText('None'); + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_level')).hasText('debug'); + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('type')).hasText('raft'); + }); + + test('it should show tls as enabled if tls_disable, tls_cert_file and tls_key_file are in the config', async function (assert) { + this.data.listeners[0].config.tls_disable = false; + this.data.listeners[0].config.tls_cert_file = './cert.pem'; + this.data.listeners[0].config.tls_key_file = './key.pem'; + + await this.renderComponent(); + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled'); + }); + + test('it should show tls as enabled if only cert and key exist in config', async function (assert) { + delete this.data.listeners[0].config.tls_disable; + this.data.listeners[0].config.tls_cert_file = './cert.pem'; + this.data.listeners[0].config.tls_key_file = './key.pem'; + + await this.renderComponent(); + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled'); + }); + + test('it should show tls as disabled if there is no tls information in the config', async function (assert) { + this.data.listeners = []; + await this.renderComponent(); + assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled'); + }); +}); diff --git a/ui/tests/integration/components/mount/secrets-engine-form-test.js b/ui/tests/integration/components/mount/secrets-engine-form-test.js index 99102dcc89..0923e5e586 100644 --- a/ui/tests/integration/components/mount/secrets-engine-form-test.js +++ b/ui/tests/integration/components/mount/secrets-engine-form-test.js @@ -5,9 +5,14 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, click, typeIn } from '@ember/test-helpers'; +import { render, click, typeIn, fillIn } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs'; +import { + allowAllCapabilitiesStub, + capabilitiesStub, + noopStub, + overrideResponse, +} from 'vault/tests/helpers/stubs'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { ALL_ENGINES } from 'vault/utils/all-engines-metadata'; @@ -23,9 +28,8 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { hooks.beforeEach(function () { this.flashMessages = this.owner.lookup('service:flash-messages'); - this.flashMessages.registerTypes(['success', 'danger']); this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success'); - this.store = this.owner.lookup('service:store'); + this.flashWarningSpy = sinon.spy(this.flashMessages, 'warning'); this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub()); this.server.post('/sys/mounts/foo', noopStub()); this.onMountSuccess = sinon.spy(); @@ -95,9 +99,12 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { ); }); - module('KV engine', function () { - test('it shows KV specific fields when type is kv', async function (assert) { + module('KV engine', function (hooks) { + hooks.beforeEach(function () { this.model.type = 'kv'; + }); + + test('it shows KV specific fields when type is kv', async function (assert) { await render( hbs`` ); @@ -105,6 +112,39 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) { assert.dom(GENERAL.inputByAttr('kv_config.cas_required')).exists('shows CAS required field'); assert.dom(GENERAL.inputByAttr('kv_config.delete_version_after')).exists('shows delete after field'); }); + + test('version 2 with no update to config endpoint still allows mount of secret engine', async function (assert) { + assert.expect(6); + this.server.post('/sys/capabilities-self', () => capabilitiesStub('my-kv-engine/config', ['deny'])); + this.server.post('/sys/mounts/my-kv-engine', (schema, req) => { + assert.true(true, 'it makes request to mount engine'); + const payload = JSON.parse(req.requestBody); + const expected = { + config: { listing_visibility: 'hidden', force_no_cache: false }, + options: { version: 2 }, + type: 'kv', + }; + assert.propEqual(payload, expected, 'mount request has expected payload'); + return overrideResponse(204); + }); + + await render( + hbs`` + ); + await fillIn(GENERAL.inputByAttr('path'), 'my-kv-engine'); + await fillIn(GENERAL.inputByAttr('kv_config.max_versions'), '101'); + await click(GENERAL.submitButton); + const [message] = this.flashWarningSpy.lastCall.args; + assert.strictEqual( + message, + `You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.`, + 'it calls warning flash with expected message' + ); + const [type, enginePath, useEngineRoute] = this.onMountSuccess.lastCall.args; + assert.strictEqual(type, 'kv', 'onMountSuccess called with expected type'); + assert.strictEqual(enginePath, 'my-kv-engine', 'onMountSuccess called with expected engine path'); + assert.true(useEngineRoute, 'onMountSuccess called useEngineRoute: true'); + }); }); module('WIF secret engines', function () { diff --git a/ui/types/global.d.ts b/ui/types/global.d.ts index f2c2d26101..59e57c6764 100644 --- a/ui/types/global.d.ts +++ b/ui/types/global.d.ts @@ -15,3 +15,5 @@ declare module '@icholy/duration' { import Duration from '@icholy/duration'; export default Duration; } + +declare module 'vault/tests/helpers/vault-keys';