mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	UI: chroot namespace listener (#23942)
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/23942.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/23942.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:bug | ||||||
|  | ui: fix broken GUI when accessing from listener with chroot_namespace defined | ||||||
|  | ``` | ||||||
| @@ -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 }; | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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() { | ||||||
|   | |||||||
| @@ -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'), | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |     const replication = hasChroot | ||||||
|  |       ? null | ||||||
|  |       : { | ||||||
|           dr: clusterModel.dr, |           dr: clusterModel.dr, | ||||||
|           performance: clusterModel.performance, |           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(), | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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; | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								ui/app/templates/error.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								ui/app/templates/error.hbs
									
									
									
									
									
										Normal 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> | ||||||
| @@ -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}} | ||||||
| /> | /> | ||||||
							
								
								
									
										15
									
								
								ui/mirage/handlers/chroot-namespace.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								ui/mirage/handlers/chroot-namespace.js
									
									
									
									
									
										Normal 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'] })); | ||||||
|  | } | ||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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", | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								ui/tests/acceptance/chroot-namespace-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ui/tests/acceptance/chroot-namespace-test.js
									
									
									
									
									
										Normal 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'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -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) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw