From cb217388d463c2514a96f48dc11d5587fd257e02 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:28:16 -0600 Subject: [PATCH] UI: handle reduced disclosure endpoints (#24262) * Create app-footer component with tests * glimmerize vault route + controller * Add dev mode badge to new footer * Fix version on dashboard * update app-footer tests * update version title component * Handle case for chroot namespace fail on health check * cleanup * fix ent tests * add missing headers * extra version fetch on login success, clear version on logout and seal * Add coverage for clearing version on seal * rename isOSS to isCommunity * remove is-version helper * test version in footer on unseal flow * fix enterprise test * VAULT-21399 test coverage * VAULT-21400 test coverage --- ui/app/adapters/cluster.js | 12 +- ui/app/components/app-footer.hbs | 31 ++++ ui/app/components/app-footer.js | 16 ++ .../dashboard/vault-version-title.js | 9 +- ui/app/components/logo-edition.hbs | 2 +- ui/app/controllers/vault.js | 17 +-- ui/app/controllers/vault/cluster/auth.js | 4 +- .../vault/cluster/settings/seal.js | 6 +- ui/app/controllers/vault/cluster/unseal.js | 4 +- ui/app/routes/vault.js | 27 ++-- ui/app/routes/vault/cluster.js | 3 +- ui/app/routes/vault/cluster/license.js | 5 +- ui/app/routes/vault/cluster/logout.js | 2 + ui/app/routes/vault/cluster/policies/index.js | 2 +- ui/app/services/control-group.js | 4 +- ui/app/services/version.js | 45 ++++-- ui/app/styles/components/env-banner.scss | 6 +- .../dashboard/vault-version-title.hbs | 3 +- ui/app/templates/vault.hbs | 26 +--- ui/lib/core/addon/helpers/is-version.js | 24 --- ui/lib/core/app/helpers/is-version.js | 6 - ui/mirage/handlers/base.js | 1 + .../enterprise-license-banner-test.js | 1 + .../enterprise-reduced-disclosure-test.js | 137 ++++++++++++++++++ .../acceptance/enterprise-sidebar-nav-test.js | 1 - .../integration/components/app-footer-test.js | 47 ++++++ .../components/dashboard/overview-test.js | 4 + .../components/license-banners-test.js | 1 + .../components/link-status-test.js | 5 +- .../mount-backend/type-form-test.js | 2 +- .../page/pki-configuration-details-test.js | 4 +- .../pki/page/pki-configuration-edit-test.js | 4 +- .../components/pki/pki-tidy-form-test.js | 6 +- .../components/sidebar/frame-test.js | 2 +- ui/tests/pages/auth.js | 4 +- ui/tests/unit/adapters/kv/data-test.js | 2 +- ui/tests/unit/services/control-group-test.js | 21 +-- ui/tests/unit/services/version-test.js | 19 +-- 38 files changed, 359 insertions(+), 156 deletions(-) create mode 100644 ui/app/components/app-footer.hbs create mode 100644 ui/app/components/app-footer.js delete mode 100644 ui/lib/core/addon/helpers/is-version.js delete mode 100644 ui/lib/core/app/helpers/is-version.js create mode 100644 ui/tests/acceptance/enterprise-reduced-disclosure-test.js create mode 100644 ui/tests/integration/components/app-footer-test.js diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js index 7469e062cd..131d143e84 100644 --- a/ui/app/adapters/cluster.js +++ b/ui/app/adapters/cluster.js @@ -5,7 +5,6 @@ import AdapterError from '@ember-data/adapter/error'; import { inject as service } from '@ember/service'; -import { assign } from '@ember/polyfills'; import { hash, resolve } from 'rsvp'; import { assert } from '@ember/debug'; import { pluralize } from 'ember-inflector'; @@ -22,6 +21,7 @@ const ENDPOINTS = [ 'init', 'capabilities-self', 'license', + 'internal/ui/version', ]; const REPLICATION_ENDPOINTS = { @@ -55,12 +55,12 @@ export default ApplicationAdapter.extend({ id, name: snapshot.attr('name'), }; - ret = assign(ret, health); + ret = Object.assign(ret, health); if (sealStatus instanceof AdapterError === false) { - ret = assign(ret, { nodes: [sealStatus] }); + ret = Object.assign(ret, { nodes: [sealStatus] }); } if (replicationStatus && replicationStatus instanceof AdapterError === false) { - ret = assign(ret, replicationStatus.data); + ret = Object.assign(ret, replicationStatus.data); } return resolve(ret); }); @@ -94,6 +94,10 @@ export default ApplicationAdapter.extend({ }); }, + fetchVersion() { + return this.ajax(`${this.urlFor('internal/ui/version')}`, 'GET').catch(() => ({})); + }, + sealStatus() { return this.ajax(this.urlFor('seal-status'), 'GET', { unauthenticated: true }); }, diff --git a/ui/app/components/app-footer.hbs b/ui/app/components/app-footer.hbs new file mode 100644 index 0000000000..616feb27d3 --- /dev/null +++ b/ui/app/components/app-footer.hbs @@ -0,0 +1,31 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + + + + {{#if this.isDevelopment}} + + + + Local development + + + + {{/if}} + + + Vault + {{this.version.version}} + + {{#if this.version.isCommunity}} + + Upgrade to Vault Enterprise + + {{/if}} + + Documentation + + + \ No newline at end of file diff --git a/ui/app/components/app-footer.js b/ui/app/components/app-footer.js new file mode 100644 index 0000000000..2a64928d1d --- /dev/null +++ b/ui/app/components/app-footer.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import ENV from 'vault/config/environment'; + +export default class AppFooterComponent extends Component { + @service version; + + get isDevelopment() { + return ENV.environment === 'development'; + } +} diff --git a/ui/app/components/dashboard/vault-version-title.js b/ui/app/components/dashboard/vault-version-title.js index 1462844740..5c5cd07556 100644 --- a/ui/app/components/dashboard/vault-version-title.js +++ b/ui/app/components/dashboard/vault-version-title.js @@ -12,17 +12,10 @@ import { inject as service } from '@ember/service'; * * @example * ```js - * + * * ``` */ export default class DashboardVaultVersionTitle extends Component { - @service version; @service namespace; - - get versionHeader() { - return this.version.isEnterprise - ? `Vault v${this.version.version.slice(0, this.version.version.indexOf('+'))}` - : `Vault v${this.version.version}`; - } } diff --git a/ui/app/components/logo-edition.hbs b/ui/app/components/logo-edition.hbs index 4c2651f6bf..b57e234519 100644 --- a/ui/app/components/logo-edition.hbs +++ b/ui/app/components/logo-edition.hbs @@ -15,7 +15,7 @@ d="M69.7218638,30.2482468 L63.2587814,8.45301543 L58,8.45301543 L65.9885305,34.6072931 L73.4551971,34.6072931 L81.4437276,8.45301543 L76.1849462,8.45301543 L69.7218638,30.2482468 Z M97.6329749,22.0014025 C97.6329749,17.2103787 95.8265233,15.0897616 89.6845878,15.0897616 C87.5168459,15.0897616 84.8272401,15.4431978 82.9806452,15.9929874 L83.5827957,19.6451613 C85.3089606,19.2917251 87.2358423,19.056101 89.0021505,19.056101 C92.1333333,19.056101 92.7354839,19.802244 92.7354839,21.9228612 L92.7354839,23.9256662 L88.0387097,23.9256662 C84.0645161,23.9256662 82.3383513,25.4179523 82.3383513,29.3057504 C82.3383513,32.6044881 83.8637993,35 87.4365591,35 C89.4035842,35 91.4910394,34.4502104 93.2573477,33.3113604 L93.618638,34.6072931 L97.6329749,34.6072931 L97.6329749,22.0014025 Z M92.7354839,30.2089762 C91.8121864,30.7194951 90.4874552,31.1907433 89.0422939,31.1907433 C87.5168459,31.1907433 87.0752688,30.601683 87.0752688,29.2664797 C87.0752688,27.8134642 87.5168459,27.3814867 89.1225806,27.3814867 L92.7354839,27.3814867 L92.7354839,30.2089762 Z M102.421505,15.4824684 L102.421505,29.345021 C102.421505,32.7615708 103.585663,35 106.837276,35 C109.125448,35 112.216487,34.1753156 114.665233,32.997195 L115.146953,34.6072931 L118.880287,34.6072931 L118.880287,15.4824684 L113.982796,15.4824684 L113.982796,28.7559607 C112.216487,29.6591865 110.088889,30.3660589 108.884588,30.3660589 C107.760573,30.3660589 107.318996,29.85554 107.318996,28.8345021 L107.318996,15.4824684 L102.421505,15.4824684 Z M129.168459,34.6072931 L129.168459,7 L124.270968,7.66760168 L124.270968,34.6072931 L129.168459,34.6072931 Z M144.394265,30.601683 C143.551254,30.8373072 142.6681,30.9943899 141.94552,30.9943899 C140.660932,30.9943899 140.179211,30.3267882 140.179211,29.3057504 L140.179211,19.2917251 L144.875986,19.2917251 L145.197133,15.4824684 L140.179211,15.4824684 L140.179211,10.0631136 L135.28172,10.7307153 L135.28172,15.4824684 L132.351254,15.4824684 L132.351254,19.2917251 L135.28172,19.2917251 L135.28172,29.9340813 C135.28172,33.3506311 137.088172,35 140.660932,35 C141.905376,35 143.912545,34.6858345 144.956272,34.2538569 L144.394265,30.601683 Z" > - {{#if (is-version "Enterprise")}} + {{#if this.version.isEnterprise}} { this.model.cluster.get('leaderNode').set('sealed', true); this.auth.deleteCurrentToken(); - return this.transitionToRoute('vault.cluster.unseal'); + // Reset version so it doesn't show on footer + this.version.version = null; + return this.router.transitionTo('vault.cluster.unseal'); }); }, }, diff --git a/ui/app/controllers/vault/cluster/unseal.js b/ui/app/controllers/vault/cluster/unseal.js index 4441493c89..c01f8ce006 100644 --- a/ui/app/controllers/vault/cluster/unseal.js +++ b/ui/app/controllers/vault/cluster/unseal.js @@ -4,14 +4,16 @@ */ import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; export default Controller.extend({ + router: service(), showLicenseError: false, actions: { transitionToCluster() { return this.model.reload().then(() => { - return this.transitionToRoute('vault.cluster', this.model.name); + return this.router.transitionTo('vault.cluster', this.model.name); }); }, diff --git a/ui/app/routes/vault.js b/ui/app/routes/vault.js index bc67466c3a..f71dc7c394 100644 --- a/ui/app/routes/vault.js +++ b/ui/app/routes/vault.js @@ -8,18 +8,21 @@ import { Promise } from 'rsvp'; import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; import Ember from 'ember'; -/* eslint-disable ember/no-ember-testing-in-module-scope */ -const SPLASH_DELAY = Ember.testing ? 0 : 300; -export default Route.extend({ - store: service(), - version: service(), +const SPLASH_DELAY = 300; + +export default class VaultRoute extends Route { + @service router; + @service store; + @service version; beforeModel() { - return this.version.fetchVersion(); - }, + // So we can know what type (Enterprise/Community) we're running + return this.version.fetchType(); + } model() { + const delay = Ember.testing ? 0 : SPLASH_DELAY; // hardcode single cluster const fixture = { data: { @@ -34,13 +37,13 @@ export default Route.extend({ return new Promise((resolve) => { later(() => { resolve(this.store.peekAll('cluster')); - }, SPLASH_DELAY); + }, delay); }); - }, + } redirect(model, transition) { if (model.get('length') === 1 && transition.targetName === 'vault.index') { - return this.transitionTo('vault.cluster', model.get('firstObject.name')); + return this.router.transitionTo('vault.cluster', model.get('firstObject.name')); } - }, -}); + } +} diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js index 2908592c21..7d55d33b90 100644 --- a/ui/app/routes/vault/cluster.js +++ b/ui/app/routes/vault/cluster.js @@ -58,7 +58,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, { const managedRoot = this.featureFlagService.managedNamespaceRoot; assert( 'Cannot use VAULT_CLOUD_ADMIN_NAMESPACE flag with non-enterprise Vault version', - !(managedRoot && this.version.isOSS) + !(managedRoot && this.version.isCommunity) ); if (!namespace && currentTokenName && !Ember.testing) { // if no namespace queryParam and user authenticated, @@ -80,6 +80,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, { if (id) { this.auth.setCluster(id); if (this.auth.currentToken) { + this.version.fetchVersion(); await this.permissions.getPaths.perform(); } return this.version.fetchFeatures(); diff --git a/ui/app/routes/vault/cluster/license.js b/ui/app/routes/vault/cluster/license.js index 33682c5334..66e219523c 100644 --- a/ui/app/routes/vault/cluster/license.js +++ b/ui/app/routes/vault/cluster/license.js @@ -10,10 +10,11 @@ import { inject as service } from '@ember/service'; export default Route.extend(ClusterRoute, { store: service(), version: service(), + router: service(), beforeModel() { - if (this.version.isOSS) { - this.transitionTo('vault.cluster'); + if (this.version.isCommunity) { + this.router.transitionTo('vault.cluster'); } }, diff --git a/ui/app/routes/vault/cluster/logout.js b/ui/app/routes/vault/cluster/logout.js index b7b7101608..8fa567ada4 100644 --- a/ui/app/routes/vault/cluster/logout.js +++ b/ui/app/routes/vault/cluster/logout.js @@ -17,6 +17,7 @@ export default Route.extend(ModelBoundaryRoute, { permissions: service(), namespaceService: service('namespace'), router: service(), + version: service(), modelTypes: computed(function () { return ['secret', 'secret-engine']; @@ -32,6 +33,7 @@ export default Route.extend(ModelBoundaryRoute, { this.console.clearLog(true); this.flashMessages.clearMessages(); this.permissions.reset(); + this.version.version = null; queryParams.with = authType; if (ns) { diff --git a/ui/app/routes/vault/cluster/policies/index.js b/ui/app/routes/vault/cluster/policies/index.js index a983115827..1a3a8b167b 100644 --- a/ui/app/routes/vault/cluster/policies/index.js +++ b/ui/app/routes/vault/cluster/policies/index.js @@ -13,7 +13,7 @@ export default Route.extend(ClusterRoute, ListRoute, { version: service(), shouldReturnEmptyModel(policyType, version) { - return policyType !== 'acl' && (version.get('isOSS') || !version.get('hasSentinel')); + return policyType !== 'acl' && (version.get('isCommunity') || !version.get('hasSentinel')); }, model(params) { diff --git a/ui/app/services/control-group.js b/ui/app/services/control-group.js index 5713e6b271..7ea218fbfb 100644 --- a/ui/app/services/control-group.js +++ b/ui/app/services/control-group.js @@ -73,7 +73,7 @@ export default Service.extend({ }, tokenForUrl(url) { - if (this.version.isOSS) { + if (this.version.isCommunity) { return null; } let pathForUrl = parseURL(url).pathname; @@ -89,7 +89,7 @@ export default Service.extend({ checkForControlGroup(callbackArgs, response, wasWrapTTLRequested) { const creationPath = response && response?.wrap_info?.creation_path; if ( - this.version.isOSS || + this.version.isCommunity || wasWrapTTLRequested || !response || (creationPath && WRAPPED_RESPONSE_PATHS.includes(creationPath)) || diff --git a/ui/app/services/version.js b/ui/app/services/version.js index cf99b759c7..7e3c7a2278 100644 --- a/ui/app/services/version.js +++ b/ui/app/services/version.js @@ -11,7 +11,17 @@ export default class VersionService extends Service { @service store; @tracked features = []; @tracked version = null; + @tracked type = null; + get isEnterprise() { + return this.type === 'enterprise'; + } + + get isCommunity() { + return !this.isEnterprise; + } + + /* Features */ get hasPerfReplication() { return this.features.includes('Performance Replication'); } @@ -32,26 +42,35 @@ export default class VersionService extends Service { return this.features.includes('Control Groups'); } - get isEnterprise() { - if (!this.version) return false; - return this.version.includes('+'); + get versionDisplay() { + if (!this.version) { + return ''; + } + return this.isEnterprise ? `v${this.version.slice(0, this.version.indexOf('+'))}` : `v${this.version}`; } - get isOSS() { - return !this.isEnterprise; + @task({ drop: true }) + *getVersion() { + if (this.version) return; + const response = yield this.store.adapterFor('cluster').fetchVersion(); + this.version = response.data?.version; } @task - *getVersion() { - if (this.version) return; - const response = yield this.store.adapterFor('cluster').sealStatus(); - this.version = response.version; - return; + *getType() { + if (this.type !== null) return; + const response = yield this.store.adapterFor('cluster').health(); + if (response.has_chroot_namespace) { + // chroot_namespace feature is only available in enterprise + this.type = 'enterprise'; + return; + } + this.type = response.enterprise ? 'enterprise' : 'community'; } @keepLatestTask *getFeatures() { - if (this.features?.length || this.isOSS) { + if (this.features?.length || this.isCommunity) { return; } try { @@ -67,6 +86,10 @@ export default class VersionService extends Service { return this.getVersion.perform(); } + fetchType() { + return this.getType.perform(); + } + fetchFeatures() { return this.getFeatures.perform(); } diff --git a/ui/app/styles/components/env-banner.scss b/ui/app/styles/components/env-banner.scss index 86895dc2e3..cda5d646cf 100644 --- a/ui/app/styles/components/env-banner.scss +++ b/ui/app/styles/components/env-banner.scss @@ -4,7 +4,7 @@ */ .env-banner { - align-self: center; + font-size: 0.8rem; border-radius: 3rem; background: linear-gradient( 135deg, @@ -13,8 +13,6 @@ ); // only use case for purple in the app. define here instead of utils/color_var animation: env-banner-color-rotate 8s infinite linear alternate; color: $white; - margin-top: -20px; - margin-bottom: 6px; .hs-icon { margin: 0; @@ -22,7 +20,7 @@ .notification { background-color: transparent; - line-height: 1.66; + line-height: 2; padding: 0 $spacing-12; } } diff --git a/ui/app/templates/components/dashboard/vault-version-title.hbs b/ui/app/templates/components/dashboard/vault-version-title.hbs index 194a1ae5f5..8509f23eb0 100644 --- a/ui/app/templates/components/dashboard/vault-version-title.hbs +++ b/ui/app/templates/components/dashboard/vault-version-title.hbs @@ -6,7 +6,8 @@ - {{this.versionHeader}} + Vault + {{@version.versionDisplay}} {{#if @version.isEnterprise}} {{/if}} diff --git a/ui/app/templates/vault.hbs b/ui/app/templates/vault.hbs index 3a72ea2d85..b26997c65a 100644 --- a/ui/app/templates/vault.hbs +++ b/ui/app/templates/vault.hbs @@ -5,28 +5,4 @@ {{outlet}} - - - Vault - {{this.auth.activeCluster.leaderNode.version}} - - {{#if (is-version "OSS")}} - - Upgrade to Vault Enterprise - - {{/if}} - - Documentation - - - - -{{#if (eq this.env "development")}} - - - - Local development - - - -{{/if}} \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/core/addon/helpers/is-version.js b/ui/lib/core/addon/helpers/is-version.js deleted file mode 100644 index 88c6da62c7..0000000000 --- a/ui/lib/core/addon/helpers/is-version.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -/* eslint-disable ember/no-observers */ -import { inject as service } from '@ember/service'; -import { assert } from '@ember/debug'; -import Helper from '@ember/component/helper'; -import { observer } from '@ember/object'; - -export default Helper.extend({ - version: service(), - onFeaturesChange: observer('version.version', function () { - this.recompute(); - }), - compute([sku]) { - if (sku !== 'OSS' && sku !== 'Enterprise') { - assert(`${sku} is not one of the available values for Vault versions.`, false); - return false; - } - return this.get(`version.is${sku}`); - }, -}); diff --git a/ui/lib/core/app/helpers/is-version.js b/ui/lib/core/app/helpers/is-version.js deleted file mode 100644 index 0386f00a8a..0000000000 --- a/ui/lib/core/app/helpers/is-version.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export { default } from 'core/helpers/is-version'; diff --git a/ui/mirage/handlers/base.js b/ui/mirage/handlers/base.js index 2693f4a759..52a7ff996a 100644 --- a/ui/mirage/handlers/base.js +++ b/ui/mirage/handlers/base.js @@ -18,6 +18,7 @@ export default function (server) { server.get('/sys/health', function () { return { + enterprise: true, initialized: true, sealed: false, standby: false, diff --git a/ui/tests/acceptance/enterprise-license-banner-test.js b/ui/tests/acceptance/enterprise-license-banner-test.js index d7c252c7e5..9d84c71e7d 100644 --- a/ui/tests/acceptance/enterprise-license-banner-test.js +++ b/ui/tests/acceptance/enterprise-license-banner-test.js @@ -26,6 +26,7 @@ const generateHealthResponse = (now, state) => { break; } return { + enterprise: true, initialized: true, sealed: false, standby: false, diff --git a/ui/tests/acceptance/enterprise-reduced-disclosure-test.js b/ui/tests/acceptance/enterprise-reduced-disclosure-test.js new file mode 100644 index 0000000000..ccce736995 --- /dev/null +++ b/ui/tests/acceptance/enterprise-reduced-disclosure-test.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { settled, visit } from '@ember/test-helpers'; +import authPage from 'vault/tests/pages/auth'; +import { createTokenCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands'; +import { pollCluster } from 'vault/tests/helpers/poll-cluster'; +import VAULT_KEYS from 'vault/tests/helpers/vault-keys'; +import ENV from 'vault/config/environment'; + +const { unsealKeys } = VAULT_KEYS; +const SELECTORS = { + footerVersion: `[data-test-footer-version]`, + dashboardTitle: `[data-test-dashboard-card-header="Vault version"]`, +}; + +module('Acceptance | Enterprise | reduced disclosure test', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.before(function () { + ENV['ember-cli-mirage'].handler = 'mfaConfig'; + }); + hooks.beforeEach(function () { + this.versionSvc = this.owner.lookup('service:version'); + return authPage.logout(); + }); + hooks.after(function () { + ENV['ember-cli-mirage'].handler = null; + }); + + test('it works when reduced disclosure enabled', async function (assert) { + const namespace = 'reduced-disclosure'; + assert.dom(SELECTORS.footerVersion).hasText(`Vault`, 'shows Vault without version when logged out'); + await authPage.login(); + + // Ensure it shows version on dashboard + assert.dom(SELECTORS.dashboardTitle).includesText(`Vault v1.`); + assert + .dom(SELECTORS.footerVersion) + .hasText(`Vault ${this.versionSvc.version}`, 'shows Vault version after login'); + + await runCmd(`write sys/namespaces/${namespace} -f`, false); + await authPage.loginNs(namespace); + + assert + .dom(SELECTORS.footerVersion) + .hasText(`Vault ${this.versionSvc.version}`, 'shows Vault version within namespace'); + + const token = await runCmd(createTokenCmd('default')); + + await authPage.logout(); + assert.dom(SELECTORS.footerVersion).hasText(`Vault`, 'no vault version after logout'); + + await authPage.loginNs(namespace, token); + assert + .dom(SELECTORS.footerVersion) + .hasText(`Vault ${this.versionSvc.version}`, 'shows Vault version for default policy in namespace'); + }); + + test('it works for user accessing child namespace', async function (assert) { + const namespace = 'reduced-disclosure'; + await authPage.login(); + + await runCmd(`write sys/namespaces/${namespace} -f`, false); + const token = await runCmd( + tokenWithPolicyCmd( + 'child-ns-access', + ` + path "${namespace}/sys/*" { + capabilities = ["read"] + } + ` + ) + ); + + await authPage.logout(); + await authPage.login(token); + assert + .dom(SELECTORS.footerVersion) + .hasText(`Vault ${this.versionSvc.version}`, 'shows Vault version for default policy in namespace'); + + // navigate to child namespace + await visit(`/vault/dashboard?namespace=${namespace}`); + assert + .dom(SELECTORS.footerVersion) + .hasText( + `Vault ${this.versionSvc.version}`, + 'shows Vault version for default policy in child namespace' + ); + assert.dom(SELECTORS.dashboardTitle).includesText('Vault v1.'); + }); + + test('shows correct version on unseal flow', async function (assert) { + await authPage.login(); + + const versionSvc = this.owner.lookup('service:version'); + await visit('/vault/settings/seal'); + assert + .dom('[data-test-footer-version]') + .hasText(`Vault ${versionSvc.version}`, 'shows version on seal page'); + assert.strictEqual(currentURL(), '/vault/settings/seal'); + + // seal + await click('[data-test-seal]'); + + await click('[data-test-confirm-button]'); + + await pollCluster(this.owner); + await settled(); + assert.strictEqual(currentURL(), '/vault/unseal', 'vault is on the unseal page'); + assert.dom('[data-test-footer-version]').hasText(`Vault`, 'Clears version on unseal'); + + // unseal + for (const key of unsealKeys) { + await fillIn('[data-test-shamir-key-input]', key); + + await click('button[type="submit"]'); + + await pollCluster(this.owner); + await settled(); + } + + assert.dom('[data-test-cluster-status]').doesNotExist('ui does not show sealed warning'); + assert.strictEqual(currentRouteName(), 'vault.cluster.auth', 'vault is ready to authenticate'); + assert.dom('[data-test-footer-version]').hasText(`Vault`, 'Version is still not shown before auth'); + await authPage.login(); + assert + .dom('[data-test-footer-version]') + .hasText(`Vault ${versionSvc.version}`, 'Version is shown after login'); + }); +}); diff --git a/ui/tests/acceptance/enterprise-sidebar-nav-test.js b/ui/tests/acceptance/enterprise-sidebar-nav-test.js index 73246bea82..de95fd704d 100644 --- a/ui/tests/acceptance/enterprise-sidebar-nav-test.js +++ b/ui/tests/acceptance/enterprise-sidebar-nav-test.js @@ -22,7 +22,6 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) { // common links are tested in the sidebar-nav test and will not be covered here test('it should render enterprise only navigation links', async function (assert) { - assert.expect(12); assert.dom(panel('Cluster')).exists('Cluster nav panel renders'); await click(link('Replication')); diff --git a/ui/tests/integration/components/app-footer-test.js b/ui/tests/integration/components/app-footer-test.js new file mode 100644 index 0000000000..cc9ee55551 --- /dev/null +++ b/ui/tests/integration/components/app-footer-test.js @@ -0,0 +1,47 @@ +/** + * 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'; + +const selectors = { + versionDisplay: '[data-test-footer-version]', + upgradeLink: '[data-test-footer-upgrade-link]', + docsLink: '[data-test-footer-documentation-link]', +}; + +module('Integration | Component | app-footer', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.versionSvc = this.owner.lookup('service:version'); + }); + + test('it renders a sane default', async function (assert) { + await render(hbs``); + assert.dom(selectors.versionDisplay).hasText('Vault', 'Vault without version by default'); + assert.dom(selectors.upgradeLink).hasText('Upgrade to Vault Enterprise', 'upgrade link shows'); + assert.dom(selectors.docsLink).hasText('Documentation', 'displays docs link'); + }); + + test('it renders for community version', async function (assert) { + this.versionSvc.version = '1.15.1'; + this.versionSvc.type = 'community'; + await render(hbs``); + assert.dom(selectors.versionDisplay).hasText('Vault 1.15.1', 'Vault shows version when available'); + assert.dom(selectors.upgradeLink).hasText('Upgrade to Vault Enterprise', 'upgrade link shows'); + assert.dom(selectors.docsLink).hasText('Documentation', 'displays docs link'); + }); + test('it renders for ent version', async function (assert) { + this.versionSvc.version = '1.15.1+hsm'; + this.versionSvc.type = 'enterprise'; + await render(hbs``); + assert.dom(selectors.versionDisplay).hasText('Vault 1.15.1+hsm', 'shows version when available'); + assert.dom(selectors.upgradeLink).doesNotExist('upgrade link not shown'); + assert.dom(selectors.docsLink).hasText('Documentation', 'displays docs link'); + }); +}); diff --git a/ui/tests/integration/components/dashboard/overview-test.js b/ui/tests/integration/components/dashboard/overview-test.js index 9e661595c5..50e9fa6873 100644 --- a/ui/tests/integration/components/dashboard/overview-test.js +++ b/ui/tests/integration/components/dashboard/overview-test.js @@ -114,6 +114,7 @@ module('Integration | Component | dashboard/overview', function (hooks) { test('it should show client count on enterprise w/ license', async function (assert) { this.version = this.owner.lookup('service:version'); this.version.version = '1.13.1+ent'; + this.version.type = 'enterprise'; this.license = { autoloaded: { license_id: '7adbf1f4-56ef-35cd-3a6c-50ef2627865d', @@ -142,6 +143,7 @@ module('Integration | Component | dashboard/overview', function (hooks) { test('it should hide client count on enterprise w/o license ', async function (assert) { this.version = this.owner.lookup('service:version'); this.version.version = '1.13.1+ent'; + this.version.type = 'enterprise'; this.isRootNamespace = true; await render( @@ -168,6 +170,7 @@ module('Integration | Component | dashboard/overview', function (hooks) { test('it should hide replication on enterprise not on root namespace', async function (assert) { this.version = this.owner.lookup('service:version'); this.version.version = '1.13.1+ent'; + this.version.type = 'enterprise'; this.isRootNamespace = false; this.license = { autoloaded: { @@ -217,6 +220,7 @@ module('Integration | Component | dashboard/overview', function (hooks) { test('shows the learn more card on enterprise', async function (assert) { this.version = this.owner.lookup('service:version'); this.version.version = '1.13.1+ent'; + this.version.type = 'enterprise'; this.version.features = [ 'Performance Replication', 'DR Replication', diff --git a/ui/tests/integration/components/license-banners-test.js b/ui/tests/integration/components/license-banners-test.js index 3a3d2caa2b..2df72bebf6 100644 --- a/ui/tests/integration/components/license-banners-test.js +++ b/ui/tests/integration/components/license-banners-test.js @@ -28,6 +28,7 @@ module('Integration | Component | license-banners', function (hooks) { this.tomorrow = addDays(mockNow, 1); this.version = this.owner.lookup('service:version'); this.version.version = '1.13.1+ent'; + this.version.type = 'enterprise'; }); hooks.after(function () { timestamp.now.restore(); diff --git a/ui/tests/integration/components/link-status-test.js b/ui/tests/integration/components/link-status-test.js index 04bd942b9b..815384a207 100644 --- a/ui/tests/integration/components/link-status-test.js +++ b/ui/tests/integration/components/link-status-test.js @@ -24,7 +24,7 @@ module('Integration | Component | link-status', function (hooks) { // this can be removed once feature is released for OSS hooks.beforeEach(function () { - this.owner.lookup('service:version').set('version', '1.13.0+ent'); + this.owner.lookup('service:version').set('type', 'enterprise'); this.statuses = statuses; }); @@ -37,7 +37,7 @@ module('Integration | Component | link-status', function (hooks) { }); test('it does not render banner in oss version', async function (assert) { - this.owner.lookup('service:version').set('version', '1.13.0'); + this.owner.lookup('service:version').set('type', 'community'); await render(hbs` @@ -50,7 +50,6 @@ module('Integration | Component | link-status', function (hooks) { await render(hbs` `); - assert.dom(SELECTORS.bannerConnected).exists('Success banner renders for connected state'); assert .dom('[data-test-link-status]') diff --git a/ui/tests/integration/components/mount-backend/type-form-test.js b/ui/tests/integration/components/mount-backend/type-form-test.js index 9ab5b06a12..d67841c371 100644 --- a/ui/tests/integration/components/mount-backend/type-form-test.js +++ b/ui/tests/integration/components/mount-backend/type-form-test.js @@ -50,7 +50,7 @@ module('Integration | Component | mount-backend/type-form', function (hooks) { module('Enterprise', function (hooks) { hooks.beforeEach(function () { this.version = this.owner.lookup('service:version'); - this.version.version = '1.12.1+ent'; + this.version.type = 'enterprise'; }); test('it renders correct items for enterprise secrets', async function (assert) { diff --git a/ui/tests/integration/components/pki/page/pki-configuration-details-test.js b/ui/tests/integration/components/pki/page/pki-configuration-details-test.js index 3ee97db4c5..9e5aaff448 100644 --- a/ui/tests/integration/components/pki/page/pki-configuration-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-configuration-details-test.js @@ -151,7 +151,7 @@ module('Integration | Component | Page::PkiConfigurationDetails', function (hook test('it renders enterprise params in crl section', async function (assert) { this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1+ent'; + this.version.type = 'enterprise'; await render( hbs`,`, { owner: this.engine } @@ -166,7 +166,7 @@ module('Integration | Component | Page::PkiConfigurationDetails', function (hook test('it does not render enterprise params in crl section', async function (assert) { this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1'; + this.version.type = 'community'; await render( hbs`,`, { owner: this.engine } diff --git a/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js b/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js index 30cf39d0ac..cc8644921e 100644 --- a/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js +++ b/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js @@ -276,7 +276,7 @@ module('Integration | Component | page/pki-configuration-edit', function (hooks) test('it renders enterprise only params', async function (assert) { assert.expect(6); this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1+ent'; + this.version.type = 'enterprise'; this.server.post(`/${this.backend}/config/acme`, () => {}); this.server.post(`/${this.backend}/config/cluster`, () => {}); this.server.post(`/${this.backend}/config/crl`, (schema, req) => { @@ -327,7 +327,7 @@ module('Integration | Component | page/pki-configuration-edit', function (hooks) test('it does not render enterprise only params for OSS', async function (assert) { assert.expect(9); this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1'; + this.version.type = 'community'; this.server.post(`/${this.backend}/config/acme`, () => {}); this.server.post(`/${this.backend}/config/cluster`, () => {}); this.server.post(`/${this.backend}/config/crl`, (schema, req) => { diff --git a/ui/tests/integration/components/pki/pki-tidy-form-test.js b/ui/tests/integration/components/pki/pki-tidy-form-test.js index f13ff9f6e8..a3acc91257 100644 --- a/ui/tests/integration/components/pki/pki-tidy-form-test.js +++ b/ui/tests/integration/components/pki/pki-tidy-form-test.js @@ -19,7 +19,7 @@ module('Integration | Component | pki tidy form', function (hooks) { hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); this.version = this.owner.lookup('service:version'); - this.version.version = '1.14.1+ent'; + this.version.type = 'enterprise'; this.server.post('/sys/capabilities-self', () => {}); this.onSave = () => {}; this.onCancel = () => {}; @@ -33,7 +33,6 @@ module('Integration | Component | pki tidy form', function (hooks) { test('it hides or shows fields depending on auto-tidy toggle', async function (assert) { assert.expect(37); - this.version.version = '1.14.1+ent'; const sectionHeaders = [ 'Universal operations', 'ACME operations', @@ -82,7 +81,6 @@ module('Integration | Component | pki tidy form', function (hooks) { test('it renders all attribute fields, including enterprise', async function (assert) { assert.expect(25); - this.version.version = '1.14.1+ent'; this.autoTidy.enabled = true; const skipFields = ['enabled', 'tidyAcme', 'intervalDuration']; // combined with duration ttl or asserted separately await render( @@ -123,7 +121,7 @@ module('Integration | Component | pki tidy form', function (hooks) { test('it hides enterprise fields for OSS', async function (assert) { assert.expect(7); - this.version.version = '1.14.1'; + this.version.type = 'community'; this.autoTidy.enabled = true; const enterpriseFields = [ diff --git a/ui/tests/integration/components/sidebar/frame-test.js b/ui/tests/integration/components/sidebar/frame-test.js index e310988a3f..4df34374b7 100644 --- a/ui/tests/integration/components/sidebar/frame-test.js +++ b/ui/tests/integration/components/sidebar/frame-test.js @@ -27,7 +27,7 @@ module('Integration | Component | sidebar-frame', function (hooks) { const currentCluster = this.owner.lookup('service:currentCluster'); currentCluster.setCluster({ hcpLinkStatus: 'connected' }); const version = this.owner.lookup('service:version'); - version.version = '1.13.0-dev1+ent'; + version.type = 'enterprise'; await render(hbs` diff --git a/ui/tests/pages/auth.js b/ui/tests/pages/auth.js index f1e2cad69e..66c53c47e7 100644 --- a/ui/tests/pages/auth.js +++ b/ui/tests/pages/auth.js @@ -45,7 +45,7 @@ export default create({ await this.usernameInput(username); return this.passwordInput(password).submit(); }, - loginNs: async function (ns) { + loginNs: async function (ns, token = rootToken) { // make sure we're always logged out and logged back in await this.logout(); await settled(); @@ -55,7 +55,7 @@ export default create({ await settled(); await this.namespaceInput(ns); await settled(); - await this.tokenInput(rootToken).submit(); + await this.tokenInput(token).submit(); return; }, clickLogout: async function (clearNamespace = false) { diff --git a/ui/tests/unit/adapters/kv/data-test.js b/ui/tests/unit/adapters/kv/data-test.js index ae67ccd307..01edae328f 100644 --- a/ui/tests/unit/adapters/kv/data-test.js +++ b/ui/tests/unit/adapters/kv/data-test.js @@ -79,7 +79,7 @@ module('Unit | Adapter | kv/data', function (hooks) { hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); this.version = this.owner.lookup('service:version'); - this.version.version = 'example+ent'; // Required for testing control-group flow + this.version.type = 'enterprise'; // Required for testing control-group flow this.secretMountPath = this.owner.lookup('service:secret-mount-path'); this.backend = 'my/kv-back&end'; this.secretMountPath.currentPath = this.backend; diff --git a/ui/tests/unit/services/control-group-test.js b/ui/tests/unit/services/control-group-test.js index 6a47784670..b0c9089681 100644 --- a/ui/tests/unit/services/control-group-test.js +++ b/ui/tests/unit/services/control-group-test.js @@ -51,8 +51,8 @@ module('Unit | Service | control group', function (hooks) { hooks.afterEach(function () {}); - const isOSS = (context) => set(context, 'version.isOSS', true); - const isEnt = (context) => set(context, 'version.isOSS', false); + const isCommunity = (context) => set(context, 'version.type', 'community'); + const isEnt = (context) => set(context, 'version.type', 'enterprise'); const resolvesArgs = (assert, result, expectedArgs) => { return result.then((...args) => { return assert.deepEqual(args, expectedArgs, 'resolves with the passed args'); @@ -61,31 +61,31 @@ module('Unit | Service | control group', function (hooks) { [ [ - 'it resolves isOSS:true, wrapTTL: true, response: has wrap_info', - isOSS, + 'it resolves isCommunity:true, wrapTTL: true, response: has wrap_info', + isCommunity, [[{ one: 'two', three: 'four' }], { wrap_info: { token: 'foo', accessor: 'bar' } }, true], (assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]), ], [ - 'it resolves isOSS:true, wrapTTL: false, response: has no wrap_info', - isOSS, + 'it resolves isCommunity:true, wrapTTL: false, response: has no wrap_info', + isCommunity, [[{ one: 'two', three: 'four' }], { wrap_info: null }, false], (assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]), ], [ - 'it resolves isOSS: false and wrapTTL:true response: has wrap_info', + 'it resolves isCommunity: false and wrapTTL:true response: has wrap_info', isEnt, [[{ one: 'two', three: 'four' }], { wrap_info: { token: 'foo', accessor: 'bar' } }, true], (assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]), ], [ - 'it resolves isOSS: false and wrapTTL:false response: has no wrap_info', + 'it resolves isCommunity: false and wrapTTL:false response: has no wrap_info', isEnt, [[{ one: 'two', three: 'four' }], { wrap_info: null }, false], (assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]), ], [ - 'it rejects isOSS: false, wrapTTL:false, response: has wrap_info', + 'it rejects isCommunity: false, wrapTTL:false, response: has wrap_info', isEnt, [ [{ one: 'two', three: 'four' }], @@ -107,7 +107,8 @@ module('Unit | Service | control group', function (hooks) { ], ].forEach(function ([name, setup, args, expectation]) { test(`checkForControlGroup: ${name}`, function (assert) { - const assertCount = name === 'it rejects isOSS: false, wrapTTL:false, response: has wrap_info' ? 2 : 1; + const assertCount = + name === 'it rejects isCommunity: false, wrapTTL:false, response: has wrap_info' ? 2 : 1; assert.expect(assertCount); if (setup) { setup(this); diff --git a/ui/tests/unit/services/version-test.js b/ui/tests/unit/services/version-test.js index 1bc1ca4d77..f7ebf7b2f2 100644 --- a/ui/tests/unit/services/version-test.js +++ b/ui/tests/unit/services/version-test.js @@ -9,24 +9,17 @@ import { setupTest } from 'ember-qunit'; module('Unit | Service | version', function (hooks) { setupTest(hooks); - test('setting version computes isOSS properly', function (assert) { + test('setting type computes isCommunity properly', function (assert) { const service = this.owner.lookup('service:version'); - service.version = '0.9.5'; - assert.true(service.isOSS); + service.type = 'community'; + assert.true(service.isCommunity); assert.false(service.isEnterprise); }); - test('setting version computes isEnterprise properly', function (assert) { + test('setting type computes isEnterprise properly', function (assert) { const service = this.owner.lookup('service:version'); - service.version = '0.9.5+ent'; - assert.false(service.isOSS); - assert.true(service.isEnterprise); - }); - - test('setting version with hsm ending computes isEnterprise properly', function (assert) { - const service = this.owner.lookup('service:version'); - service.version = '0.9.5+ent.hsm'; - assert.false(service.isOSS); + service.type = 'enterprise'; + assert.false(service.isCommunity); assert.true(service.isEnterprise); });