UI/managed namespace changes (#10588)
* Redirect to url with namespace param if user logged into root namespace without permission * Feature flag service for managing flags * Redirect with namespace query param if no current namespace param AND managed root namespace set * Test coverage for managed namespace changes * Handle null body case on feature-flag response, add pretender route for feature-flags on shamir test
This commit is contained in:
parent
6ed50b5df9
commit
70d3185d3a
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
ui: Adds check for feature flag on application, and updates namespace toolbar on login if present
|
||||
```
|
|
@ -7,11 +7,32 @@ export default Controller.extend({
|
|||
vaultController: controller('vault'),
|
||||
clusterController: controller('vault.cluster'),
|
||||
namespaceService: service('namespace'),
|
||||
featureFlagService: service('featureFlag'),
|
||||
namespaceQueryParam: alias('clusterController.namespaceQueryParam'),
|
||||
queryParams: [{ authMethod: 'with' }],
|
||||
wrappedToken: alias('vaultController.wrappedToken'),
|
||||
authMethod: '',
|
||||
redirectTo: alias('vaultController.redirectTo'),
|
||||
managedNamespaceRoot: alias('featureFlagService.managedNamespaceRoot'),
|
||||
|
||||
get managedNamespaceChild() {
|
||||
let fullParam = this.namespaceQueryParam;
|
||||
let split = fullParam.split('/');
|
||||
if (split.length > 1) {
|
||||
split.shift();
|
||||
return `/${split.join('/')}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
updateManagedNamespace: task(function*(value) {
|
||||
// debounce
|
||||
yield timeout(500);
|
||||
// TODO: Move this to shared fn
|
||||
const newNamespace = `${this.managedNamespaceRoot}${value}`;
|
||||
this.namespaceService.setNamespace(newNamespace, true);
|
||||
this.set('namespaceQueryParam', newNamespace);
|
||||
}).restartable(),
|
||||
|
||||
updateNamespace: task(function*(value) {
|
||||
// debounce
|
||||
|
|
|
@ -8,6 +8,7 @@ export default Route.extend({
|
|||
routing: service('router'),
|
||||
wizard: service(),
|
||||
namespaceService: service('namespace'),
|
||||
featureFlagService: service('featureFlag'),
|
||||
|
||||
actions: {
|
||||
willTransition() {
|
||||
|
@ -81,4 +82,15 @@ export default Route.extend({
|
|||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
async beforeModel() {
|
||||
const result = await fetch('/v1/sys/internal/ui/feature-flags', {
|
||||
method: 'GET',
|
||||
});
|
||||
if (result.status === 200) {
|
||||
const body = await result.json();
|
||||
const flags = body.data?.feature_flags || [];
|
||||
this.featureFlagService.setFeatureFlags(flags);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { reject } from 'rsvp';
|
|||
import Route from '@ember/routing/route';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import Ember from 'ember';
|
||||
import getStorage from '../../lib/token-storage';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
|
||||
|
||||
|
@ -15,6 +16,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
permissions: service(),
|
||||
store: service(),
|
||||
auth: service(),
|
||||
featureFlagService: service('featureFlag'),
|
||||
currentCluster: service(),
|
||||
modelTypes: computed(function() {
|
||||
return ['node', 'secret', 'secret-engine'];
|
||||
|
@ -34,7 +36,21 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
|||
|
||||
async beforeModel() {
|
||||
const params = this.paramsFor(this.routeName);
|
||||
this.namespaceService.setNamespace(params.namespaceQueryParam);
|
||||
let namespace = params.namespaceQueryParam;
|
||||
const currentTokenName = this.auth.get('currentTokenName');
|
||||
// if no namespace queryParam and user authenticated,
|
||||
// use user's root namespace to redirect to properly param'd url
|
||||
if (!namespace && currentTokenName && !Ember.testing) {
|
||||
const storage = getStorage().getItem(currentTokenName);
|
||||
namespace = storage.userRootNamespace;
|
||||
// only redirect if something other than nothing
|
||||
if (namespace) {
|
||||
this.transitionTo({ queryParams: { namespace } });
|
||||
}
|
||||
} else if (!namespace && !!this.featureFlagService.managedNamespaceRoot) {
|
||||
this.transitionTo({ queryParams: { namespace: this.featureFlagService.managedNamespaceRoot } });
|
||||
}
|
||||
this.namespaceService.setNamespace(namespace);
|
||||
const id = this.getClusterId(params);
|
||||
if (id) {
|
||||
this.auth.setCluster(id);
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import Service from '@ember/service';
|
||||
|
||||
const FLAGS = {
|
||||
vaultCloudNamespace: 'VAULT_CLOUD_ADMIN_NAMESPACE',
|
||||
};
|
||||
|
||||
export default Service.extend({
|
||||
featureFlags: null,
|
||||
setFeatureFlags(flags) {
|
||||
this.set('featureFlags', flags);
|
||||
},
|
||||
|
||||
get managedNamespaceRoot() {
|
||||
if (this.featureFlags && this.featureFlags.includes(FLAGS.vaultCloudNamespace)) {
|
||||
return 'admin';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
.field-label {
|
||||
margin-right: $spacing-s;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.is-label {
|
||||
|
|
|
@ -4,10 +4,42 @@
|
|||
Sign in to Vault
|
||||
</h1>
|
||||
</Page.header>
|
||||
{{#if (has-feature "Namespaces")}}
|
||||
{{#if managedNamespaceRoot}}
|
||||
<Page.sub-header>
|
||||
<Toolbar>
|
||||
<div class="toolbar-namespace-picker" data-test-managed-namespace-toolbar>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
<label class="is-label" for="namespace">Namespace</label>
|
||||
</div>
|
||||
<div class="field-label">
|
||||
<span class="has-text-grey" data-test-managed-namespace-root>/{{managedNamespaceRoot}}</span>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input
|
||||
value={{managedNamespaceChild}}
|
||||
placeholder="/ (Default)"
|
||||
oninput={{perform updateManagedNamespace value="target.value"}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
name="namespace"
|
||||
id="namespace"
|
||||
class="input"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</Page.sub-header>
|
||||
{{else if (has-feature "Namespaces")}}
|
||||
<Page.sub-header>
|
||||
<Toolbar class="toolbar-namespace-picker">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field is-horizontal" data-test-namespace-toolbar>
|
||||
<div class="field-label is-normal">
|
||||
<label class="is-label" for="namespace">Namespace</label>
|
||||
</div>
|
||||
|
|
|
@ -56,6 +56,9 @@ module.exports = function(environment) {
|
|||
ENV.APP.rootElement = '#ember-testing';
|
||||
ENV.APP.autoboot = false;
|
||||
ENV.flashMessageDefaults.timeout = 50;
|
||||
ENV['ember-cli-mirage'] = {
|
||||
enabled: false,
|
||||
};
|
||||
}
|
||||
if (environment !== 'production') {
|
||||
ENV.APP.DEFAULT_PAGE_SIZE = 15;
|
||||
|
|
|
@ -19,5 +19,15 @@ export default function() {
|
|||
data: db['metrics/configs'].first(),
|
||||
};
|
||||
});
|
||||
|
||||
this.get('/sys/internal/ui/feature-flags', db => {
|
||||
const featuresResponse = db.features.first();
|
||||
return {
|
||||
data: {
|
||||
feature_flags: featuresResponse ? featuresResponse.feature_flags : null,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.passthrough();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { Factory } from 'ember-cli-mirage';
|
||||
|
||||
export default Factory.extend({
|
||||
feature_flags() {
|
||||
return []; // VAULT_CLOUD_ADMIN_NAMESPACE
|
||||
},
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
import Mirage from 'ember-cli-mirage';
|
||||
|
||||
export default Mirage.Factory.extend({
|
||||
name(i) {
|
||||
return `Person ${i}`;
|
||||
},
|
||||
age: 28,
|
||||
admin: false,
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import { Model } from 'ember-cli-mirage';
|
||||
|
||||
export default Model.extend({
|
||||
feature_flags: null,
|
||||
});
|
|
@ -1,3 +1,4 @@
|
|||
export default function(server) {
|
||||
server.create('metrics/config');
|
||||
server.create('feature', { feature_flags: ['SOME_FLAG', 'VAULT_CLOUD_ADMIN_NAMESPACE'] });
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { click, settled, visit } from '@ember/test-helpers';
|
||||
import { click, settled, visit, fillIn, currentURL } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
|
@ -71,4 +71,22 @@ module('Acceptance | Enterprise | namespaces', function(hooks) {
|
|||
.dom('[data-test-namespace-link="beep/boop/bop"]')
|
||||
.exists('renders the link to the nested namespace');
|
||||
});
|
||||
|
||||
test('it shows the regular namespace toolbar when not managed', async function(assert) {
|
||||
// This test is the opposite of the test in managed-namespace-test
|
||||
await logout.visit();
|
||||
assert.equal(currentURL(), '/vault/auth?with=token', 'Does not redirect');
|
||||
assert.dom('[data-test-namespace-toolbar]').exists('Normal namespace toolbar exists');
|
||||
assert
|
||||
.dom('[data-test-managed-namespace-toolbar]')
|
||||
.doesNotExist('Managed namespace toolbar does not exist');
|
||||
assert.dom('input#namespace').hasAttribute('placeholder', '/ (Root)');
|
||||
await fillIn('input#namespace', '/foo');
|
||||
let encodedNamespace = encodeURIComponent('/foo');
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/auth?namespace=${encodedNamespace}&with=token`,
|
||||
'Does not prepend root to namespace'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -77,6 +77,7 @@ module('Acceptance | init', function(hooks) {
|
|||
this.server.get('/v1/sys/health', () => {
|
||||
return [200, { 'Content-Type': 'application/json' }, JSON.stringify(HEALTH_RESPONSE)];
|
||||
});
|
||||
this.server.get('/v1/sys/internal/ui/feature-flags', this.server.passthrough);
|
||||
});
|
||||
|
||||
hooks.afterEach(function() {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { currentURL, visit, fillIn } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import Pretender from 'pretender';
|
||||
|
||||
const FEATURE_FLAGS_RESPONSE = {
|
||||
data: {
|
||||
feature_flags: ['VAULT_CLOUD_ADMIN_NAMESPACE'],
|
||||
},
|
||||
};
|
||||
|
||||
module('Acceptance | Enterprise | Managed namespace root', function(hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
/**
|
||||
* Since the features are fetched on the application load,
|
||||
* we have to populate them on the beforeEach hook because
|
||||
* the fetch won't trigger again within the tests
|
||||
*/
|
||||
this.server = new Pretender(function() {
|
||||
this.get('/v1/sys/internal/ui/feature-flags', () => {
|
||||
return [200, { 'Content-Type': 'application/json' }, JSON.stringify(FEATURE_FLAGS_RESPONSE)];
|
||||
});
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.get('/v1/sys/license/features', this.passthrough);
|
||||
});
|
||||
});
|
||||
|
||||
hooks.afterEach(function() {
|
||||
this.server.shutdown();
|
||||
});
|
||||
|
||||
test('it shows the managed namespace toolbar when feature flag exists', async function(assert) {
|
||||
await visit('/vault/auth');
|
||||
assert.equal(currentURL(), '/vault/auth?namespace=admin&with=token', 'Redirected to base namespace');
|
||||
|
||||
assert.dom('[data-test-namespace-toolbar]').doesNotExist('Normal namespace toolbar does not exist');
|
||||
assert.dom('[data-test-managed-namespace-toolbar]').exists('Managed namespace toolbar exists');
|
||||
assert.dom('[data-test-managed-namespace-root]').hasText('/admin', 'Shows /admin namespace prefix');
|
||||
assert.dom('input#namespace').hasAttribute('placeholder', '/ (Default)');
|
||||
await fillIn('input#namespace', '/foo');
|
||||
let encodedNamespace = encodeURIComponent('admin/foo');
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/auth?namespace=${encodedNamespace}&with=token`,
|
||||
'Correctly prepends root to namespace'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Service | feature-flag', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let service = this.owner.lookup('service:feature-flag');
|
||||
assert.ok(service);
|
||||
});
|
||||
|
||||
test('it returns the namespace root when flag is present', function(assert) {
|
||||
let service = this.owner.lookup('service:feature-flag');
|
||||
assert.equal(service.managedNamespaceRoot, null, 'Managed namespace root is null by default');
|
||||
service.setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']);
|
||||
assert.equal(service.managedNamespaceRoot, 'admin', 'Managed namespace is admin when flag present');
|
||||
service.setFeatureFlags(['SOMETHING_ELSE']);
|
||||
assert.equal(
|
||||
service.managedNamespaceRoot,
|
||||
null,
|
||||
'Flags were overwritten and root namespace is null again'
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue