Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI/managed namespace changes #10588

Merged
merged 21 commits into from
Jan 7, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
588acff
Redirect to url with namespace param if user logged into root namespa…
chelshaw Dec 10, 2020
e75a591
Managed toolbar (always on) handles input with regards to managed nam…
chelshaw Dec 14, 2020
3448f0b
Update mirage with vault-config mock route
chelshaw Dec 15, 2020
54d1254
Fix tests by skipping storage read when testing
chelshaw Dec 15, 2020
3dc42c4
New config service which gets updated on beforeModel hook of applicat…
chelshaw Dec 16, 2020
03b5f08
Redirect with namespace query param if no current namespace param AND…
chelshaw Dec 16, 2020
4007b14
Update config path and return value to array of string feature flags
chelshaw Dec 21, 2020
08f90ff
Update mirage response and config service to match mini-RFC
chelshaw Dec 22, 2020
3f92b6f
Rename config service to feature-flag service
chelshaw Dec 22, 2020
bc58e70
Add changelog
chelshaw Dec 22, 2020
a5d593a
Update mirage for feature namespace testing
chelshaw Dec 22, 2020
ebc2c65
Test coverage for managed namespace changes
chelshaw Dec 23, 2020
dc4ae06
Remove mirage:true in dev
chelshaw Dec 23, 2020
382e0fa
Handle null body case on feature-flag response, add pretender route f…
chelshaw Jan 5, 2021
2634e65
Merge branch 'master' into ui/hcp-namespace-changes
chelshaw Jan 5, 2021
8ba0d10
disable mirage in test env, Update managed namespace test to use pret…
chelshaw Jan 6, 2021
3170e39
Merge branch 'master' into ui/hcp-namespace-changes
chelshaw Jan 6, 2021
39e75cd
Feature flag service tests
chelshaw Jan 6, 2021
d73252e
Merge branch 'master' into ui/hcp-namespace-changes
chelshaw Jan 6, 2021
6d86ffb
Merge branch 'master' into ui/hcp-namespace-changes
chelshaw Jan 7, 2021
2e5f739
remove logs
chelshaw Jan 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/10588.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
ui: Adds check for feature flag on application, and updates namespace toolbar on login if present
```
21 changes: 21 additions & 0 deletions ui/app/controllers/vault/cluster/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
chelshaw marked this conversation as resolved.
Show resolved Hide resolved
// 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
Expand Down
12 changes: 12 additions & 0 deletions ui/app/routes/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default Route.extend({
routing: service('router'),
wizard: service(),
namespaceService: service('namespace'),
featureFlagService: service('featureFlag'),

actions: {
willTransition() {
Expand Down Expand Up @@ -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);
}
},
});
18 changes: 17 additions & 1 deletion ui/app/routes/vault/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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'];
Expand All @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions ui/app/services/feature-flag.js
Original file line number Diff line number Diff line change
@@ -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;
},
});
1 change: 1 addition & 0 deletions ui/app/styles/components/auth-form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

.field-label {
margin-right: $spacing-s;
align-self: center;
}

.is-label {
Expand Down
36 changes: 34 additions & 2 deletions ui/app/templates/vault/cluster/auth.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
3 changes: 3 additions & 0 deletions ui/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default function() {
this.namespace = 'v1';

this.get('sys/internal/counters/activity', function(db) {
console.log('getting sys/internal/counters/activity');
chelshaw marked this conversation as resolved.
Show resolved Hide resolved
let data = {};
const firstRecord = db['metrics/activities'].first();
if (firstRecord) {
Expand All @@ -14,10 +15,21 @@ export default function() {
});

this.get('sys/internal/counters/config', function(db) {
console.log('getting sys/internal/counters/config');
return {
request_id: '00001',
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();
}
7 changes: 7 additions & 0 deletions ui/mirage/factories/feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Factory } from 'ember-cli-mirage';

export default Factory.extend({
feature_flags() {
return []; // VAULT_CLOUD_ADMIN_NAMESPACE
},
});
9 changes: 0 additions & 9 deletions ui/mirage/factories/user.js

This file was deleted.

5 changes: 5 additions & 0 deletions ui/mirage/models/feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Model } from 'ember-cli-mirage';

export default Model.extend({
feature_flags: null,
});
1 change: 1 addition & 0 deletions ui/mirage/scenarios/default.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export default function(server) {
server.create('metrics/config');
server.create('feature', { feature_flags: ['SOME_FLAG', 'VAULT_CLOUD_ADMIN_NAMESPACE'] });
}
20 changes: 19 additions & 1 deletion ui/tests/acceptance/enterprise-namespaces-test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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'
);
});
});
1 change: 1 addition & 0 deletions ui/tests/acceptance/init-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
51 changes: 51 additions & 0 deletions ui/tests/acceptance/managed-namespace-test.js
Original file line number Diff line number Diff line change
@@ -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'
);
});
});
24 changes: 24 additions & 0 deletions ui/tests/unit/services/feature-flag-test.js
Original file line number Diff line number Diff line change
@@ -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'
);
});
});