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/gate wizard #6094

Merged
merged 6 commits into from
Jan 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 48 additions & 5 deletions ui/app/components/wizard/features-selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,37 @@ import { FEATURE_MACHINE_TIME } from 'vault/helpers/wizard-constants';
export default Component.extend({
wizard: service(),
version: service(),
permissions: service(),

init() {
this._super(...arguments);
this.maybeHideFeatures();
},

maybeHideFeatures() {
let features = this.get('allFeatures');
features.forEach(feat => {
feat.disabled = this.doesNotHavePermission(feat.requiredPermissions);
});

if (this.get('showReplication') === false) {
let feature = this.get('allFeatures').findBy('key', 'replication');
feature.show = false;
}
},

doesNotHavePermission(requiredPermissions) {
andaley marked this conversation as resolved.
Show resolved Hide resolved
// requiredPermissions is an object of paths and capabilities defined within allFeatures.
// the expected shape is:
// {
// 'example/path': ['capability'],
// 'second/example/path': ['update', 'sudo'],
// }
return !Object.keys(requiredPermissions).every(path => {
return this.permissions.hasPermission(path, requiredPermissions[path]);
});
},

andaley marked this conversation as resolved.
Show resolved Hide resolved
estimatedTime: computed('selectedFeatures', function() {
let time = 0;
for (let feature of Object.keys(FEATURE_MACHINE_TIME)) {
Expand All @@ -44,15 +63,22 @@ export default Component.extend({
steps: ['Enabling a secrets engine', 'Adding a secret'],
selected: false,
show: true,
permission: 'secrets',
disabled: false,
requiredPermissions: {
'sys/mounts/example': ['update'],
},
},
{
key: 'authentication',
name: 'Authentication',
steps: ['Enabling an auth method', 'Managing your auth method'],
selected: false,
show: true,
permission: 'access',
disabled: false,
requiredPermissions: {
'sys/auth': ['read'],
'sys/auth/foo': ['update', 'sudo'],
},
},
{
key: 'policies',
Expand All @@ -65,23 +91,36 @@ export default Component.extend({
],
selected: false,
show: true,
permission: 'policies',
disabled: false,
requiredPermissions: {
'sys/policies/acl': ['list'],
},
},
{
key: 'replication',
name: 'Replication',
steps: ['Setting up replication', 'Your cluster information'],
selected: false,
show: true,
permission: 'status',
disabled: false,
requiredPermissions: {
'sys/replication/performance/primary/enable': ['update'],
'sys/replication/dr/primary/enable': ['update'],
},
},
{
key: 'tools',
name: 'Tools',
steps: ['Wrapping data', 'Lookup wrapped data', 'Rewrapping your data', 'Unwrapping your data'],
selected: false,
show: true,
permission: 'tools',
disabled: false,
requiredPermissions: {
'sys/wrapping/wrap': ['update'],
'sys/wrapping/lookup': ['update'],
'sys/wrapping/unwrap': ['update'],
'sys/wrapping/rewrap': ['update'],
},
},
];
}),
Expand All @@ -96,6 +135,10 @@ export default Component.extend({
.mapBy('key');
}),

cannotStartWizard: computed('selectedFeatures', function() {
return !this.get('selectedFeatures').length;
}),

actions: {
saveFeatures() {
let wizard = this.get('wizard');
Expand Down
4 changes: 2 additions & 2 deletions ui/app/helpers/has-permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { inject as service } from '@ember/service';

export default Helper.extend({
permissions: service(),
compute([navItem], { routeParams }) {
compute([route], { routeParams, capability }) {
let permissions = this.permissions;
return permissions.hasNavPermission(navItem, routeParams);
return permissions.hasNavPermission(route, routeParams, capability);
},
});
34 changes: 27 additions & 7 deletions ui/app/services/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,33 +108,53 @@ export default Service.extend({
}
},

hasPermission(pathName) {
hasPermission(pathName, capabilities = [null]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why [null] instead of []?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting capabilities as [] caused the tests to fail & incorrectly show items because calling .every on an empty array returned true. javascript! 🤷‍♀️

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh guess I haven't used every/some enough to realize that - thanks!

const path = this.pathNameWithNamespace(pathName);

if (this.canViewAll || this.hasMatchingExactPath(path) || this.hasMatchingGlobPath(path)) {
if (this.canViewAll) {
return true;
}
return false;

return capabilities.every(
capability => this.hasMatchingExactPath(path, capability) || this.hasMatchingGlobPath(path, capability)
);
},

hasMatchingExactPath(pathName) {
hasMatchingExactPath(pathName, capability) {
const exactPaths = this.get('exactPaths');
if (exactPaths) {
const prefix = Object.keys(exactPaths).find(path => path.startsWith(pathName));
return prefix && !this.isDenied(exactPaths[prefix]);
const hasMatchingPath = prefix && !this.isDenied(exactPaths[prefix]);

if (prefix && capability) {
return this.hasCapability(exactPaths[prefix], capability) && hasMatchingPath;
}

return hasMatchingPath;
}
return false;
},

hasMatchingGlobPath(pathName) {
hasMatchingGlobPath(pathName, capability) {
const globPaths = this.get('globPaths');
if (globPaths) {
const matchingPath = Object.keys(globPaths).find(k => pathName.includes(k));
return (matchingPath && !this.isDenied(globPaths[matchingPath])) || globPaths.hasOwnProperty('');
const hasMatchingPath =
(matchingPath && !this.isDenied(globPaths[matchingPath])) || globPaths.hasOwnProperty('');

if (matchingPath && capability) {
return this.hasCapability(globPaths[matchingPath], capability) && hasMatchingPath;
}

return hasMatchingPath;
}
return false;
},

hasCapability(path, capability) {
return path.capabilities.includes(capability);
},

isDenied(path) {
return path.capabilities.includes('deny');
},
Expand Down
5 changes: 5 additions & 0 deletions ui/app/styles/components/features-selection.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
&.is-active {
box-shadow: 0 0 0 1px $grey-light;
}

&.is-disabled {
background-color: $ui-gray-010;
color: $ui-gray-300;
}
}

.feature-box label {
Expand Down
21 changes: 18 additions & 3 deletions ui/app/templates/components/wizard/features-selection.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@
<h3 class="feature-header">Walk me through setting up:</h3>
<form id="features-form" class="feature-selection" {{action "saveFeatures" on="submit"}}>
{{#each allFeatures as |feature|}}
{{#if (and feature.show (has-permission feature.permission))}}
<div class="feature-box {{if feature.selected 'is-active'}}">
{{#if feature.show}}
<div class="feature-box {{if feature.selected 'is-active'}} {{if feature.disabled 'is-disabled'}}"
data-test-select-input={{true}}>
<div class="b-checkbox">
<input
id="feature-{{feature.key}}"
type="checkbox"
class="styled"
checked={{feature.selected}}
onchange={{action (mut feature.selected) value="target.checked"}}
disabled={{feature.disabled}}
data-test-checkbox={{feature.name}}
/>
<label for="feature-{{feature.key}}">{{feature.name}}</label>
<button type="button" class="button is-ghost icon is-pulled-right" onclick={{action (toggle (concat feature.key "-isOpen") this)}}>
Expand All @@ -29,6 +32,11 @@
@class="has-text-grey auto-width is-paddingless is-flex-column"
/>
</button>
{{#if feature.disabled}}
<Info-Tooltip data-test-tooltip>
You do not have permissions to tour some parts of this feature
</Info-Tooltip>
{{/if}}
</div>
{{#if (get this (concat feature.key "-isOpen"))}}
<ul class="feature-steps">
Expand All @@ -41,7 +49,14 @@
{{/if}}
{{/each}}
<span class="selection-summary">
<button type="submit" class="button is-primary">Start</button>
<button
type="submit"
class="button is-primary"
disabled={{cannotStartWizard}}
data-test-start-button
>
Start
</button>
{{#if selectedFeatures}}
<span class="time-estimate"><ICon @glyph="stopwatch" @class="has-text-grey auto-width is-paddingless is-flex-column"/>About {{estimatedTime}} minutes</span>
{{/if}}
Expand Down
53 changes: 53 additions & 0 deletions ui/tests/integration/components/features-selection-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { create } from 'ember-cli-page-object';
import featuresSelection from 'vault/tests/pages/components/wizard/features-selection';
import hbs from 'htmlbars-inline-precompile';
import Service from '@ember/service';

const component = create(featuresSelection);

const permissionsService = Service.extend({
hasPermission(path) {
// This enables the Secrets and Authentication wizard items and disables the others.
const allowedPaths = ['sys/mounts/example', 'sys/auth', 'sys/auth/foo', 'sys/wrapping/wrap'];
if (allowedPaths.includes(path)) {
return true;
}
return false;
},
});

module('Integration | Component | features-selection', function(hooks) {
setupRenderingTest(hooks);

hooks.beforeEach(function() {
this.owner.register('service:permissions', permissionsService);
});

test('it disables and enables wizard items according to user permissions', async function(assert) {
const enabled = { Secrets: true, Authentication: true, Policies: false, Tools: false };
await render(hbs`{{wizard/features-selection}}`);
andaley marked this conversation as resolved.
Show resolved Hide resolved

component.wizardItems.forEach(i => {
assert.equal(
i.hasDisabledTooltip,
!enabled[i.text],
'shows a tooltip only when the wizard item is not enabled'
);
});
});

test('it disables the start button if no wizard items are checked', async function(assert) {
await render(hbs`{{wizard/features-selection}}`);
assert.equal(component.hasDisabledStartButton, true);
});

test('it enables the start button when user has permission and wizard items are checked', async function(assert) {
await render(hbs`{{wizard/features-selection}}`);
await component.selectSecrets();

assert.equal(component.hasDisabledStartButton, false);
});
});
9 changes: 9 additions & 0 deletions ui/tests/pages/components/wizard/features-selection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { collection, isPresent, property, clickable } from 'ember-cli-page-object';

export default {
wizardItems: collection('[data-test-select-input]', {
hasDisabledTooltip: isPresent('[data-test-tooltip]'),
}),
hasDisabledStartButton: property('disabled', '[data-test-start-button]'),
selectSecrets: clickable('[data-test-checkbox=Secrets]'),
};
18 changes: 17 additions & 1 deletion ui/tests/unit/services/permissions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const PERMISSIONS_RESPONSE = {
capabilities: ['read'],
},
'bar/bee': {
capabilities: ['create'],
capabilities: ['create', 'list'],
},
boo: {
capabilities: ['deny'],
Expand Down Expand Up @@ -93,6 +93,22 @@ module('Unit | Service | permissions', function(hooks) {
assert.equal(service.hasPermission('hi'), true);
});

test('it returns true if a policy has the specified capabilities on a path', function(assert) {
let service = this.owner.lookup('service:permissions');
service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
assert.equal(service.hasPermission('bar/bee', ['create', 'list']), true);
assert.equal(service.hasPermission('baz/biz', ['read']), true);
});

test('it returns false if a policy does not have the specified capabilities on a path', function(assert) {
let service = this.owner.lookup('service:permissions');
service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
assert.equal(service.hasPermission('bar/bee', ['create', 'delete']), false);
assert.equal(service.hasPermission('foo', ['create']), false);
});

test('defaults to show all items when policy cannot be found', async function(assert) {
let service = this.owner.lookup('service:permissions');
this.server.get('/v1/sys/internal/ui/resultant-acl', () => {
Expand Down