diff --git a/changelog/23942.txt b/changelog/23942.txt new file mode 100644 index 000000000000..a4d43d48f091 --- /dev/null +++ b/changelog/23942.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: fix broken GUI when accessing from listener with chroot_namespace defined +``` diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js index de173feb8897..7469e062cdff 100644 --- a/ui/app/adapters/cluster.js +++ b/ui/app/adapters/cluster.js @@ -80,6 +80,11 @@ export default ApplicationAdapter.extend({ performancestandbycode: 200, }, unauthenticated: true, + }).catch(() => { + // sys/health will only fail when chroot set + // because it's allowed in root namespace only and + // configured to return a 200 response in other fail scenarios + return { has_chroot_namespace: true }; }); }, diff --git a/ui/app/models/cluster.js b/ui/app/models/cluster.js index 543ed7bf0a60..6ac7f90af367 100644 --- a/ui/app/models/cluster.js +++ b/ui/app/models/cluster.js @@ -16,6 +16,8 @@ export default class ClusterModel extends Model { @attr('boolean') standby; @attr('string') type; @attr('object') license; + // manually set on response when sys/health failure + @attr('boolean') hasChrootNamespace; /* Licensing concerns */ get licenseExpiry() { diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 520368c72d01..003d3f7ab99c 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -9,17 +9,16 @@ import { alias, and, equal } from '@ember/object/computed'; export default Model.extend({ name: attr('string'), // https://developer.hashicorp.com/vault/api-docs/system/health - initialized: attr('boolean'), - sealed: attr('boolean'), - isSealed: alias('sealed'), standby: attr('boolean'), isActive: equal('standby', false), - clusterName: attr('string'), clusterId: attr('string'), isLeader: and('initialized', 'isActive'), // https://developer.hashicorp.com/vault/api-docs/system/seal-status + initialized: attr('boolean'), + sealed: attr('boolean'), + isSealed: alias('sealed'), // The "t" parameter is the threshold, and "n" is the number of shares. t: attr('number'), n: attr('number'), diff --git a/ui/app/routes/vault/cluster/dashboard.js b/ui/app/routes/vault/cluster/dashboard.js index b50a68c1dbef..37b2cb88b7de 100644 --- a/ui/app/routes/vault/cluster/dashboard.js +++ b/ui/app/routes/vault/cluster/dashboard.js @@ -29,18 +29,20 @@ export default class VaultClusterDashboardRoute extends Route.extend(ClusterRout model() { const clusterModel = this.modelFor('vault.cluster'); - const replication = { - dr: clusterModel.dr, - performance: clusterModel.performance, - }; - + const hasChroot = clusterModel?.hasChrootNamespace; + const replication = hasChroot + ? null + : { + dr: clusterModel.dr, + performance: clusterModel.performance, + }; return hash({ replication, secretsEngines: this.store.query('secret-engine', {}), license: this.store.queryRecord('license', {}).catch(() => null), - isRootNamespace: this.namespace.inRootNamespace, + isRootNamespace: this.namespace.inRootNamespace && !hasChroot, version: this.version, - vaultConfiguration: this.getVaultConfiguration(), + vaultConfiguration: hasChroot ? null : this.getVaultConfiguration(), }); } diff --git a/ui/app/services/version.js b/ui/app/services/version.js index fe1ec0aae891..cf99b759c7a8 100644 --- a/ui/app/services/version.js +++ b/ui/app/services/version.js @@ -44,7 +44,7 @@ export default class VersionService extends Service { @task *getVersion() { if (this.version) return; - const response = yield this.store.adapterFor('cluster').health(); + const response = yield this.store.adapterFor('cluster').sealStatus(); this.version = response.version; return; } diff --git a/ui/app/templates/error.hbs b/ui/app/templates/error.hbs new file mode 100644 index 000000000000..9448637df46b --- /dev/null +++ b/ui/app/templates/error.hbs @@ -0,0 +1,46 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +
+
+
+
+ +
+
+
+
+ +
+
+

+ {{#if (eq this.model.httpStatus 403)}} + Not authorized + {{else if (eq this.model.httpStatus 404)}} + Page not found + {{else}} + Error + {{/if}} +

+

Error {{this.model.httpStatus}}

+
+
+ +

+ {{this.model.message}} + {{join ". " this.model.errors}} +

+ +
+ + + Go home + + + Learn more + +
+
+
\ No newline at end of file diff --git a/ui/app/templates/vault/cluster/dashboard.hbs b/ui/app/templates/vault/cluster/dashboard.hbs index fad7cd54d79e..b7f212674a0d 100644 --- a/ui/app/templates/vault/cluster/dashboard.hbs +++ b/ui/app/templates/vault/cluster/dashboard.hbs @@ -4,12 +4,12 @@ ~}} \ No newline at end of file diff --git a/ui/mirage/handlers/chroot-namespace.js b/ui/mirage/handlers/chroot-namespace.js new file mode 100644 index 000000000000..97a17e10074a --- /dev/null +++ b/ui/mirage/handlers/chroot-namespace.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { Response } from 'miragejs'; + +/* + These are mocked responses to mimic what we get from the server + when within a chrooted listener (assuming the namespace exists) + */ +export default function (server) { + server.get('sys/health', () => new Response(400, {}, { errors: ['unsupported path'] })); + server.get('sys/replication/status', () => new Response(400, {}, { errors: ['unsupported path'] })); +} diff --git a/ui/mirage/handlers/index.js b/ui/mirage/handlers/index.js index 7ddae8563173..574b2e0405cc 100644 --- a/ui/mirage/handlers/index.js +++ b/ui/mirage/handlers/index.js @@ -6,6 +6,7 @@ // add all handlers here // individual lookup done in mirage config import base from './base'; +import chrootNamespace from './chroot-namespace'; import clients from './clients'; import db from './db'; import hcpLink from './hcp-link'; @@ -19,6 +20,7 @@ import reducedDisclosure from './reduced-disclosure'; export { base, + chrootNamespace, clients, db, hcpLink, diff --git a/ui/package.json b/ui/package.json index e872b1147edf..7e199ccd3df3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,6 +26,7 @@ "fmt:styles": "prettier --write app/styles/**/*.*", "start": "VAULT_ADDR=http://localhost:8200; ember server --proxy=$VAULT_ADDR", "start2": "ember server --proxy=http://localhost:8202 --port=4202", + "start:chroot": "ember server --proxy=http://localhost:8300 --port=4300", "start:mirage": "start () { MIRAGE_DEV_HANDLER=$1 yarn run start; }; start", "test": "npm-run-all --print-name lint:js:quiet lint:hbs:quiet && node scripts/start-vault.js", "test:enos": "npm-run-all lint:js:quiet lint:hbs:quiet && node scripts/enos-test-ember.js", diff --git a/ui/tests/acceptance/chroot-namespace-test.js b/ui/tests/acceptance/chroot-namespace-test.js new file mode 100644 index 000000000000..5bca573f31c1 --- /dev/null +++ b/ui/tests/acceptance/chroot-namespace-test.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { currentRouteName } from '@ember/test-helpers'; +import authPage from 'vault/tests/pages/auth'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import ENV from 'vault/config/environment'; + +module('Acceptance | chroot-namespace enterprise ui', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.before(function () { + ENV['ember-cli-mirage'].handler = 'chrootNamespace'; + }); + hooks.after(function () { + ENV['ember-cli-mirage'].handler = null; + }); + + test('it should render normally when chroot namespace exists', async function (assert) { + await authPage.login(); + assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'goes to dashboard page'); + assert.dom('[data-test-badge-namespace]').includesText('root', 'Shows root namespace badge'); + }); +}); diff --git a/ui/tests/acceptance/dashboard-test.js b/ui/tests/acceptance/dashboard-test.js index e34cfb8fccb3..cdf95e18208f 100644 --- a/ui/tests/acceptance/dashboard-test.js +++ b/ui/tests/acceptance/dashboard-test.js @@ -49,10 +49,11 @@ module('Acceptance | landing page dashboard', function (hooks) { await visit('/vault/dashboard'); const version = this.owner.lookup('service:version'); const versionName = version.version; - const versionNameEnd = version.isEnterprise ? versionName.indexOf('+') : versionName.length; - assert - .dom(SELECTORS.cardHeader('Vault version')) - .hasText(`Vault v${versionName.slice(0, versionNameEnd)} root`); + const versionText = version.isEnterprise + ? `Vault v${versionName.slice(0, versionName.indexOf('+'))} root` + : `Vault v${versionName}`; + + assert.dom(SELECTORS.cardHeader('Vault version')).hasText(versionText); }); module('secrets engines card', function (hooks) {