UI: chroot namespace listener (#23942)

This commit is contained in:
Chelsea Shaw
2023-11-02 12:55:09 -05:00
committed by GitHub
parent 750ab337ea
commit 3eb205a87d
13 changed files with 127 additions and 22 deletions

3
changelog/23942.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:bug
ui: fix broken GUI when accessing from listener with chroot_namespace defined
```

View File

@@ -80,6 +80,11 @@ export default ApplicationAdapter.extend({
performancestandbycode: 200, performancestandbycode: 200,
}, },
unauthenticated: true, 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 };
}); });
}, },

View File

@@ -16,6 +16,8 @@ export default class ClusterModel extends Model {
@attr('boolean') standby; @attr('boolean') standby;
@attr('string') type; @attr('string') type;
@attr('object') license; @attr('object') license;
// manually set on response when sys/health failure
@attr('boolean') hasChrootNamespace;
/* Licensing concerns */ /* Licensing concerns */
get licenseExpiry() { get licenseExpiry() {

View File

@@ -9,17 +9,16 @@ import { alias, and, equal } from '@ember/object/computed';
export default Model.extend({ export default Model.extend({
name: attr('string'), name: attr('string'),
// https://developer.hashicorp.com/vault/api-docs/system/health // https://developer.hashicorp.com/vault/api-docs/system/health
initialized: attr('boolean'),
sealed: attr('boolean'),
isSealed: alias('sealed'),
standby: attr('boolean'), standby: attr('boolean'),
isActive: equal('standby', false), isActive: equal('standby', false),
clusterName: attr('string'),
clusterId: attr('string'), clusterId: attr('string'),
isLeader: and('initialized', 'isActive'), isLeader: and('initialized', 'isActive'),
// https://developer.hashicorp.com/vault/api-docs/system/seal-status // 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. // The "t" parameter is the threshold, and "n" is the number of shares.
t: attr('number'), t: attr('number'),
n: attr('number'), n: attr('number'),

View File

@@ -29,18 +29,20 @@ export default class VaultClusterDashboardRoute extends Route.extend(ClusterRout
model() { model() {
const clusterModel = this.modelFor('vault.cluster'); const clusterModel = this.modelFor('vault.cluster');
const replication = { const hasChroot = clusterModel?.hasChrootNamespace;
dr: clusterModel.dr, const replication = hasChroot
performance: clusterModel.performance, ? null
}; : {
dr: clusterModel.dr,
performance: clusterModel.performance,
};
return hash({ return hash({
replication, replication,
secretsEngines: this.store.query('secret-engine', {}), secretsEngines: this.store.query('secret-engine', {}),
license: this.store.queryRecord('license', {}).catch(() => null), license: this.store.queryRecord('license', {}).catch(() => null),
isRootNamespace: this.namespace.inRootNamespace, isRootNamespace: this.namespace.inRootNamespace && !hasChroot,
version: this.version, version: this.version,
vaultConfiguration: this.getVaultConfiguration(), vaultConfiguration: hasChroot ? null : this.getVaultConfiguration(),
}); });
} }

View File

@@ -44,7 +44,7 @@ export default class VersionService extends Service {
@task @task
*getVersion() { *getVersion() {
if (this.version) return; if (this.version) return;
const response = yield this.store.adapterFor('cluster').health(); const response = yield this.store.adapterFor('cluster').sealStatus();
this.version = response.version; this.version = response.version;
return; return;
} }

View File

@@ -0,0 +1,46 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<div class="is-flex-grow-1 is-flex-v-centered">
<div class="empty-state-content">
<div class="is-flex-v-centered has-bottom-margin-xxl">
<div class="brand-icon-large">
<Icon @name="vault" @size="24" @stretched={{true}} />
</div>
</div>
<div class="is-flex-center">
<div class="error-icon">
<Icon @name="help" @size="24" class="has-text-grey" @stretched={{true}} />
</div>
<div class="has-left-margin-s">
<h1 class="is-size-4 has-text-semibold has-text-grey has-line-height-1">
{{#if (eq this.model.httpStatus 403)}}
Not authorized
{{else if (eq this.model.httpStatus 404)}}
Page not found
{{else}}
Error
{{/if}}
</h1>
<p class="has-text-grey is-size-8">Error {{this.model.httpStatus}}</p>
</div>
</div>
<p class="has-text-grey has-top-margin-m has-bottom-padding-l has-border-bottom-light" data-test-error-description>
{{this.model.message}}
{{join ". " this.model.errors}}
</p>
<div class="is-flex-between has-top-margin-s">
<ExternalLink @href="/" @sameTab={{true}} class="is-no-underline is-flex-center has-text-semibold">
<Chevron @direction="left" />
Go home
</ExternalLink>
<DocLink @path="/vault/api-docs#http-status-codes">
Learn more
</DocLink>
</div>
</div>
</div>

View File

@@ -4,12 +4,12 @@
~}} ~}}
<Dashboard::Overview <Dashboard::Overview
@replication={{@model.replication}} @replication={{this.model.replication}}
@secretsEngines={{@model.secretsEngines}} @secretsEngines={{this.model.secretsEngines}}
@license={{@model.license}} @license={{this.model.license}}
@isRootNamespace={{@model.isRootNamespace}} @isRootNamespace={{this.model.isRootNamespace}}
@version={{@model.version}} @version={{this.model.version}}
@vaultConfiguration={{@model.vaultConfiguration}} @vaultConfiguration={{this.model.vaultConfiguration}}
@refreshModel={{this.refreshModel}} @refreshModel={{this.refreshModel}}
@replicationUpdatedAt={{this.replicationUpdatedAt}} @replicationUpdatedAt={{this.replicationUpdatedAt}}
/> />

View File

@@ -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'] }));
}

View File

@@ -6,6 +6,7 @@
// add all handlers here // add all handlers here
// individual lookup done in mirage config // individual lookup done in mirage config
import base from './base'; import base from './base';
import chrootNamespace from './chroot-namespace';
import clients from './clients'; import clients from './clients';
import db from './db'; import db from './db';
import hcpLink from './hcp-link'; import hcpLink from './hcp-link';
@@ -19,6 +20,7 @@ import reducedDisclosure from './reduced-disclosure';
export { export {
base, base,
chrootNamespace,
clients, clients,
db, db,
hcpLink, hcpLink,

View File

@@ -26,6 +26,7 @@
"fmt:styles": "prettier --write app/styles/**/*.*", "fmt:styles": "prettier --write app/styles/**/*.*",
"start": "VAULT_ADDR=http://localhost:8200; ember server --proxy=$VAULT_ADDR", "start": "VAULT_ADDR=http://localhost:8200; ember server --proxy=$VAULT_ADDR",
"start2": "ember server --proxy=http://localhost:8202 --port=4202", "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", "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": "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", "test:enos": "npm-run-all lint:js:quiet lint:hbs:quiet && node scripts/enos-test-ember.js",

View File

@@ -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');
});
});

View File

@@ -49,10 +49,11 @@ module('Acceptance | landing page dashboard', function (hooks) {
await visit('/vault/dashboard'); await visit('/vault/dashboard');
const version = this.owner.lookup('service:version'); const version = this.owner.lookup('service:version');
const versionName = version.version; const versionName = version.version;
const versionNameEnd = version.isEnterprise ? versionName.indexOf('+') : versionName.length; const versionText = version.isEnterprise
assert ? `Vault v${versionName.slice(0, versionName.indexOf('+'))} root`
.dom(SELECTORS.cardHeader('Vault version')) : `Vault v${versionName}`;
.hasText(`Vault v${versionName.slice(0, versionNameEnd)} root`);
assert.dom(SELECTORS.cardHeader('Vault version')).hasText(versionText);
}); });
module('secrets engines card', function (hooks) { module('secrets engines card', function (hooks) {