Add Policy-based Navigation (#5967)

* add permissions service

* start template helper

* match prefixed paths

* gate sidebar links

* land on first page user has access to

* show nav when user first logs in

* clear paths when user logs out

* add tests

* implement feedback

* show all nav items if no policy is found

* update onboarding wizard

* fix some unrelated tests

* add support for namespaces

* gate wizard

* unstage package and lockfile
This commit is contained in:
Noelle Daley 2019-01-18 14:04:40 -08:00 committed by GitHub
parent b436df67e9
commit 20deed3a3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 725 additions and 239 deletions

View File

@ -0,0 +1,11 @@
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
query() {
return this.ajax(this.urlForQuery(), 'GET');
},
urlForQuery() {
return this.buildURL() + '/internal/ui/resultant-acl';
},
});

View File

@ -158,6 +158,9 @@ export default Component.extend(DEFAULTS, {
handleError(e) {
this.set('loading', false);
if (!e.errors) {
return e;
}
let errors = e.errors.map(error => {
if (error.detail) {
return error.detail;

View File

@ -1,5 +1,4 @@
import Component from '@ember/component';
export default Component.extend({
'data-test-navheader': true,
classNameBindings: 'consoleFullscreen:panel-fullscreen',

View File

@ -44,6 +44,7 @@ export default Component.extend({
steps: ['Enabling a secrets engine', 'Adding a secret'],
selected: false,
show: true,
permission: 'secrets',
},
{
key: 'authentication',
@ -51,6 +52,7 @@ export default Component.extend({
steps: ['Enabling an auth method', 'Managing your auth method'],
selected: false,
show: true,
permission: 'access',
},
{
key: 'policies',
@ -63,6 +65,7 @@ export default Component.extend({
],
selected: false,
show: true,
permission: 'policies',
},
{
key: 'replication',
@ -70,6 +73,7 @@ export default Component.extend({
steps: ['Setting up replication', 'Your cluster information'],
selected: false,
show: true,
permission: 'status',
},
{
key: 'tools',
@ -77,6 +81,7 @@ export default Component.extend({
steps: ['Wrapping data', 'Lookup wrapped data', 'Rewrapping your data', 'Unwrapping your data'],
selected: false,
show: true,
permission: 'tools',
},
];
}),

View File

@ -7,6 +7,7 @@ export default Controller.extend({
store: service(),
media: service(),
router: service(),
permissions: service(),
namespaceService: service('namespace'),
vaultVersion: service('version'),

View File

@ -0,0 +1,10 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
permissions: service(),
compute([navItem], { routeParams }) {
let permissions = this.permissions;
return permissions.hasNavPermission(navItem, routeParams);
},
});

View File

@ -0,0 +1,10 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
permissions: service(),
compute([navItem]) {
let permissions = this.permissions;
return permissions.navPathParams(navItem);
},
});

View File

@ -13,6 +13,7 @@ const POLL_INTERVAL_MS = 10000;
export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
namespaceService: service('namespace'),
version: service(),
permissions: service(),
store: service(),
auth: service(),
currentCluster: service(),
@ -58,6 +59,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
const id = this.getClusterId(params);
if (id) {
this.get('auth').setCluster(id);
this.get('permissions').getPaths.perform();
return this.get('version').fetchFeatures();
} else {
return reject({ httpStatus: 404, message: 'not found', path: params.cluster_name });

View File

@ -8,6 +8,7 @@ export default Route.extend(ModelBoundaryRoute, {
controlGroup: service(),
flashMessages: service(),
console: service(),
permissions: service(),
modelTypes: computed(function() {
return ['secret', 'secret-engine'];
@ -21,5 +22,6 @@ export default Route.extend(ModelBoundaryRoute, {
this.clearModelCache();
this.replaceWith('vault.cluster');
this.get('flashMessages').clearMessages();
this.get('permissions').reset();
},
});

View File

@ -18,6 +18,7 @@ const BACKENDS = supportedAuthBackends();
export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX };
export default Service.extend({
permissions: service(),
namespace: service(),
IDLE_TIMEOUT: 3 * 60e3,
expirationCalcTS: null,
@ -308,7 +309,15 @@ export default Service.extend({
const adapter = this.clusterAdapter();
return adapter.authenticate(options).then(resp => {
return this.persistAuthData(options, resp.auth || resp.data, this.get('namespace.path'));
return this.persistAuthData(options, resp.auth || resp.data, this.get('namespace.path')).then(
authData => {
return this.get('permissions')
.getPaths.perform()
.then(() => {
return authData;
});
}
);
});
},

View File

@ -0,0 +1,141 @@
import Service, { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
const API_PATHS = {
secrets: { engine: 'cubbyhole/' },
access: {
methods: 'sys/auth',
entities: 'identity/entities',
groups: 'identity/groups',
leases: 'sys/leases/lookup',
namespaces: 'sys/namespaces',
'control-groups': 'sys/control-group/',
},
policies: {
acl: 'sys/policies/acl',
rgp: 'sys/policies/rgp',
egp: 'sys/policies/egp',
},
tools: {
wrap: 'sys/wrapping/wrap',
lookup: 'sys/wrapping/lookup',
unwrap: 'sys/wrapping/unwrap',
rewrap: 'sys/wrapping/rewrap',
random: 'sys/tools/random',
hash: 'sys/tools/hash',
},
status: {
replication: 'sys/replication',
license: 'sys/license',
seal: 'sys/seal',
},
};
const API_PATHS_TO_ROUTE_PARAMS = {
'sys/auth': ['vault.cluster.access.methods'],
'identity/entities': ['vault.cluster.access.identity', 'entities'],
'identity/groups': ['vault.cluster.access.identity', 'groups'],
'sys/leases/lookup': ['vault.cluster.access.leases'],
'sys/namespaces': ['vault.cluster.access.namespaces'],
'sys/control-group/': ['vault.cluster.access.control-groups'],
};
/*
The Permissions service is used to gate top navigation and sidebar items. It fetches
a users' policy from the resultant-acl endpoint and stores their allowed exact and glob
paths as state. It also has methods for checking whether a user has permission for a given
path.
*/
export default Service.extend({
exactPaths: null,
globPaths: null,
canViewAll: null,
store: service(),
auth: service(),
namespace: service(),
getPaths: task(function*() {
if (this.paths) {
return;
}
try {
let resp = yield this.get('store')
.adapterFor('permissions')
.query();
this.setPaths(resp);
return;
} catch (err) {
// If no policy can be found, default to showing all nav items.
this.set('canViewAll', true);
}
}),
setPaths(resp) {
this.set('exactPaths', resp.data.exact_paths);
this.set('globPaths', resp.data.glob_paths);
this.set('canViewAll', resp.data.root);
},
reset() {
this.set('exactPaths', null);
this.set('globPaths', null);
this.set('canViewAll', null);
},
hasNavPermission(navItem, routeParams) {
if (routeParams) {
return this.hasPermission(API_PATHS[navItem][routeParams]);
}
return Object.values(API_PATHS[navItem]).some(path => this.hasPermission(path));
},
navPathParams(navItem) {
const path = Object.values(API_PATHS[navItem]).find(path => this.hasPermission(path));
if (['policies', 'tools'].includes(navItem)) {
return path.split('/').lastObject;
}
return API_PATHS_TO_ROUTE_PARAMS[path];
},
pathNameWithNamespace(pathName) {
const namespace = this.get('namespace').path;
if (namespace) {
return `${namespace}/${pathName}`;
} else {
return pathName;
}
},
hasPermission(pathName) {
const path = this.pathNameWithNamespace(pathName);
if (this.canViewAll || this.hasMatchingExactPath(path) || this.hasMatchingGlobPath(path)) {
return true;
}
return false;
},
hasMatchingExactPath(pathName) {
const exactPaths = this.get('exactPaths');
if (exactPaths) {
const prefix = Object.keys(exactPaths).find(path => path.startsWith(pathName));
return prefix && !this.isDenied(exactPaths[prefix]);
}
return false;
},
hasMatchingGlobPath(pathName) {
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('');
}
return false;
},
isDenied(path) {
return path.capabilities.includes('deny');
},
});

View File

@ -4,6 +4,12 @@
color: $grey;
}
.access-information {
display: flex;
padding: $size-8 0px;
font-size: $size-8;
}
.feature-box {
box-shadow: $box-shadow;
border-radius: $radius;

View File

@ -2,13 +2,17 @@
<h2 class="title is-6">
Choosing where to go
</h2>
<p>You did it! You now have access to your Vault and can start entering your data. We can help you get started with any of the options below</p>
<p>You did it! You now have access to your Vault and can start entering your data. We can help you get started with any of the options below.</p>
<div class="access-information">
<ICon @glyph="information-circled" class="has-text-info"/>
<p>Vault only shows links to pages that you have access to based on your policies. Contact your administrator if you need access changes.</p>
</div>
{{#if (or (has-feature "Performance Replication") (has-feature "DR Replication")) }}
{{/if}}
<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 feature.show}}
{{#if (and feature.show (has-permission feature.permission))}}
<div class="feature-box {{if feature.selected 'is-active'}}">
<div class="b-checkbox">
<input

View File

@ -1,117 +1,130 @@
<div class="popup-menu-content">
<div class="box">
{{#if (and activeCluster.unsealed auth.currentToken)}}
<nav class="menu">
<p class="menu-label">Replication</p>
<ul>
{{#if cluster.anyReplicationEnabled}}
<li>
{{#link-to
"vault.cluster.replication.mode.index"
"dr"
disabled=(not currentToken)
invokeAction=(action onLinkClick)
}}
{{replication-mode-summary
mode="dr"
display='menu'
cluster=cluster
}}
{{/link-to}}
</li>
<li>
{{#if (has-feature "Performance Replication")}}
{{#if (has-permission 'status' routeParams='replication')}}
<nav class="menu">
<p class="menu-label">Replication</p>
<ul>
{{#if cluster.anyReplicationEnabled}}
<li>
{{#link-to
"vault.cluster.replication.mode.index"
"performance"
"dr"
disabled=(not currentToken)
invokeAction=(action onLinkClick)
}}
{{replication-mode-summary
mode="dr"
display='menu'
cluster=cluster
}}
{{/link-to}}
</li>
<li>
{{#if (has-feature "Performance Replication")}}
{{#link-to
"vault.cluster.replication.mode.index"
"performance"
disabled=(not currentToken)
invokeAction=(action onLinkClick)
}}
{{replication-mode-summary
mode="performance"
display="menu"
cluster=cluster
tagName="span"
}}
{{/link-to}}
{{else}}
{{replication-mode-summary
mode="performance"
display="menu"
cluster=cluster
tagName="span"
class="menu-item"
}}
{{/if}}
</li>
{{else if version.isOSS}}
<li>
{{#link-to "vault.cluster.replication"}}
<div class="level is-mobile">
<span class="level-left">Learn More</span>
<ICon @glyph="edition-enterprise" @size="16" @class="level-right" />
</div>
{{/link-to}}
{{else}}
{{replication-mode-summary
mode="performance"
display="menu"
cluster=cluster
class="menu-item"
</li>
{{else}}
<li>
{{#link-to "vault.cluster.replication"
invokeAction=(action onLinkClick)
}}
{{/if}}
</li>
{{else if version.isOSS}}
<li>
{{#link-to "vault.cluster.replication"}}
<div class="level is-mobile">
<span class="level-left">Learn More</span>
<ICon @glyph="edition-enterprise" @size="16" @class="level-right" />
</div>
{{/link-to}}
</li>
{{else}}
<li>
{{#link-to "vault.cluster.replication"
invokeAction=(action onLinkClick)
}}
<div class="level is-mobile">
<span class="level-left">Enable</span>
<ICon @glyph="neutral-circled-outline" @size="16" @class="has-text-grey-light level-right" />
</div>
{{/link-to}}
</li>
{{/if}}
</ul>
</nav>
<hr/>
<div class="level is-mobile">
<span class="level-left">Enable</span>
<ICon @glyph="neutral-circled-outline" @size="16" @class="has-text-grey-light level-right" />
</div>
{{/link-to}}
</li>
{{/if}}
</ul>
</nav>
<hr/>
{{/if}}
{{/if}}
{{#unless version.isOSS}}
{{#if (has-permission 'status' routeParams='license')}}
<nav class="menu">
<div class="menu-label">
License
</div>
<ul class="menu-list">
<li class="action">
{{#link-to "vault.cluster.license" activeCluster.name invokeAction=onLinkClick}}
<div class="level is-mobile">
<span class="level-left">See Details</span>
<ICon @glyph="chevron-right" @size="12" @class="has-text-grey-light level-right" />
</div>
{{/link-to}}
</li>
</ul>
</nav>
<hr/>
{{/if}}
{{/unless}}
<nav class="menu">
<div class="menu-label">
License
Seal Status
</div>
<ul class="menu-list">
<li class="action">
{{#link-to "vault.cluster.license" activeCluster.name invokeAction=onLinkClick}}
<div class="level is-mobile">
<span class="level-left">See Details</span>
<ICon @glyph="chevron-right" @size="12" @class="has-text-grey-light level-right" />
</div>
{{/link-to}}
{{#if activeCluster.unsealed}}
{{#if (has-permission 'status' routeParams='seal')}}
{{#link-to 'vault.cluster.settings.seal' cluster.name
class="level is-mobile"
invokeAction=(action (queue (action onLinkClick) (action d.actions.close)))
}}
<div class="level is-mobile">
<span class="level-left">Unsealed</span>
<ICon @glyph="checkmark-circled-outline" @size="16" @class="has-text-success level-right" />
</div>
{{/link-to}}
{{else}}
<span class="menu-item">
<div class="level is-mobile">
<span class="level-left">Unsealed</span>
<ICon @glyph="checkmark-circled-outline" @size="16" @class="has-text-success level-right" />
</div>
</span>
{{/if}}
{{else}}
<span class="menu-item">
<div class="level is-mobile">
<span class="level-left has-text-danger">Sealed</span>
<ICon @glyph="close-circled-outline" @size="16" @class="has-text-danger level-right" />
</div>
</span>
{{/if}}
</li>
</ul>
</nav>
<hr/>
{{/unless}}
<nav class="menu">
<div class="menu-label">
Seal Status
</div>
<ul class="menu-list">
<li class="action">
{{#if activeCluster.unsealed}}
{{#link-to 'vault.cluster.settings.seal' cluster.name
class="level is-mobile"
invokeAction=(action (queue (action onLinkClick) (action d.actions.close)))
}}
<div class="level is-mobile">
<span class="level-left">Unsealed</span>
<ICon @glyph="checkmark-circled-outline" @size="16" @class="has-text-success level-right" />
</div>
{{/link-to}}
{{else}}
<span class="menu-item">
<div class="level is-mobile">
<span class="level-left has-text-danger">Sealed</span>
<ICon @glyph="close-circled-outline" @size="16" @class="has-text-danger level-right" />
</div>
</span>
{{/if}}
</li>
</ul>
</nav>
</div>
</div>

View File

@ -17,43 +17,54 @@
</NamespacePicker>
</li>
{{/if}}
<li class="{{if (is-active-route 'vault.cluster.secrets') 'is-active'}}">
{{#link-to
"vault.cluster.secrets"
current-when="vault.cluster.secrets vault.cluster.settings.mount-secret-backend vault.cluster.settings.configure-secret-backend"
invokeAction=(action Nav.closeDrawer)
}}
Secrets
{{/link-to}}
</li>
<li class="{{if (is-active-route 'vault.cluster.access') 'is-active'}}">
{{#link-to
"vault.cluster.access"
{{#if (has-permission 'secrets')}}
<li class="{{if (is-active-route 'vault.cluster.secrets') 'is-active'}}">
{{#link-to
"vault.cluster.secrets"
current-when="vault.cluster.secrets vault.cluster.settings.mount-secret-backend vault.cluster.settings.configure-secret-backend"
invokeAction=(action Nav.closeDrawer)
data-test-navbar-item='secrets'
}}
Secrets
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission 'access')}}
<li class="{{if (is-active-route 'vault.cluster.access') 'is-active'}}">
{{#link-to
params=(route-params-for 'access')
current-when="vault.cluster.access vault.cluster.settings.auth"
invokeAction=(action Nav.closeDrawer)
data-test-navbar-item='access'
}}
Access
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission 'policies')}}
<li class="{{if (is-active-route (array 'vault.cluster.policies' 'vault.cluster.policy')) 'is-active'}}">
{{#link-to
"vault.cluster.policies"
(route-params-for 'policies')
current-when="vault.cluster.policies vault.cluster.policy"
invokeAction=(action Nav.closeDrawer)
data-test-navbar-item='policies'
}}
Access
{{/link-to}}
</li>
<li class="{{if (is-active-route (array 'vault.cluster.policies' 'vault.cluster.policy')) 'is-active'}}">
{{#link-to
"vault.cluster.policies"
"acl"
current-when="vault.cluster.policies vault.cluster.policy"
invokeAction=(action Nav.closeDrawer)
}}
Policies
{{/link-to}}
</li>
<li class="{{if (is-active-route 'vault.cluster.tools') 'is-active'}}">
{{#link-to
"vault.cluster.tools.tool"
"wrap"
invokeAction=(action Nav.closeDrawer)
}}
Tools
{{/link-to}}
</li>
Policies
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission 'tools')}}
<li class="{{if (is-active-route 'vault.cluster.tools') 'is-active'}}">
{{#link-to
"vault.cluster.tools.tool"
(route-params-for 'tools')
invokeAction=(action Nav.closeDrawer)
}}
Tools
{{/link-to}}
</li>
{{/if}}
</ul>
</Nav.main>
<Nav.items>

View File

@ -1,47 +1,59 @@
<div class="columns">
{{#menu-sidebar title="Access" class="is-3" data-test-sidebar=true}}
<li>
{{#link-to "vault.cluster.access.methods" data-test-link=true current-when="vault.cluster.access.methods vault.cluster.access.method"}}
Auth Methods
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.access.identity" "entities" data-test-link=true }}
Entities
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.access.identity" "groups" data-test-link=true }}
Groups
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.access.leases" data-test-link=true}}
Leases
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.access.namespaces" data-test-link=true }}
Namespaces
{{#unless (has-feature "Namespaces")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.access.control-groups" data-test-link=true current-when="vault.cluster.access.control-groups vault.cluster.access.control-group-accessor vault.cluster.access.control-groups-configure"}}
Control Groups
{{#unless (has-feature "Control Groups")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{#if (has-permission "access" routeParams="methods")}}
<li>
{{#link-to "vault.cluster.access.methods" data-test-link=true current-when="vault.cluster.access.methods vault.cluster.access.method"}}
Auth Methods
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "access" routeParams="entities")}}
<li>
{{#link-to "vault.cluster.access.identity" "entities" data-test-link=true }}
Entities
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "access" routeParams="groups")}}
<li>
{{#link-to "vault.cluster.access.identity" "groups" data-test-link=true }}
Groups
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "access" routeParams="leases")}}
<li>
{{#link-to "vault.cluster.access.leases" data-test-link=true}}
Leases
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "access" routeParams="namespaces")}}
<li>
{{#link-to "vault.cluster.access.namespaces" data-test-link=true }}
Namespaces
{{#unless (has-feature "Namespaces")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "access" routeParams="control-groups")}}
<li>
{{#link-to "vault.cluster.access.control-groups" data-test-link=true current-when="vault.cluster.access.control-groups vault.cluster.access.control-group-accessor vault.cluster.access.control-groups-configure"}}
Control Groups
{{#unless (has-feature "Control Groups")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{/if}}
{{/menu-sidebar}}
<div class="column is-9">
{{outlet}}

View File

@ -1,36 +1,42 @@
<div class="columns">
{{#menu-sidebar title="Policies" class="is-3" data-test-sidebar=true}}
<li>
{{#link-to "vault.cluster.policies" "acl" data-test-link=true class=(if (is-active-route "vault.cluster.policies" "acl") "is-active")}}
ACL Policies
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.policies" "rgp" data-test-link=true class=(if (is-active-route "vault.cluster.policies" "rgp") "is-active")}}
Role Governing Policies
{{#if (has-permission "policies" routeParams="acl")}}
<li>
{{#link-to "vault.cluster.policies" "acl" data-test-link=true class=(if (is-active-route "vault.cluster.policies" "acl") "is-active")}}
ACL Policies
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "policies" routeParams="rgp")}}
<li>
{{#link-to "vault.cluster.policies" "rgp" data-test-link=true class=(if (is-active-route "vault.cluster.policies" "rgp") "is-active")}}
Role Governing Policies
{{#unless (has-feature "Sentinel")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.policies" "egp" data-test-link=true class=(if (is-active-route "vault.cluster.policies" "egp") "is-active")}}
Endpoint Governing Policies
{{#unless (has-feature "Sentinel")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "policies" routeParams="egp")}}
<li>
{{#link-to "vault.cluster.policies" "egp" data-test-link=true class=(if (is-active-route "vault.cluster.policies" "egp") "is-active")}}
Endpoint Governing Policies
{{#unless (has-feature "Sentinel")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{#unless (has-feature "Sentinel")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{/if}}
{{/menu-sidebar}}
<div class="column is-9">
{{outlet}}

View File

@ -1,34 +1,40 @@
<div class="columns">
{{#menu-sidebar title="Policies" class="is-3" data-test-sidebar=true}}
<li>
{{#link-to "vault.cluster.policies" "acl" data-test-link=true class=(if (is-active-route "vault.cluster.policy" "acl") "is-active")}}
ACL Policies
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.policies" "rgp" data-test-link=true class=(if (is-active-route "vault.cluster.policy" "rgp") "is-active")}}
Role Governing Policies
{{#unless (has-feature "Sentinel")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
<li>
{{#link-to "vault.cluster.policies" "egp" data-test-link=true class=(if (is-active-route "vault.cluster.policy" "egp") "is-active")}}
Endpoint Governing Policies
{{#unless (has-feature "Sentinel")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{#if (has-permission "policies" routeParams="acl")}}
<li>
{{#link-to "vault.cluster.policies" "acl" data-test-link=true class=(if (is-active-route "vault.cluster.policy" "acl") "is-active")}}
ACL Policies
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "policies" routeParams="rgp")}}
<li>
{{#link-to "vault.cluster.policies" "rgp" data-test-link=true class=(if (is-active-route "vault.cluster.policy" "rgp") "is-active")}}
Role Governing Policies
{{#unless (has-feature "Sentinel")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{/if}}
{{#if (has-permission "policies" routeParams="egp")}}
<li>
{{#link-to "vault.cluster.policies" "egp" data-test-link=true class=(if (is-active-route "vault.cluster.policy" "egp") "is-active")}}
Endpoint Governing Policies
{{#unless (has-feature "Sentinel")}}
{{#if (is-version "OSS")}}
{{edition-badge edition="Enterprise"}}
{{else}}
{{edition-badge edition="Premium"}}
{{/if}}
{{/unless}}
{{/link-to}}
</li>
{{/if}}
{{/menu-sidebar}}
<div class="column is-9">
{{outlet}}

View File

@ -1,14 +1,16 @@
<div class="columns">
{{#menu-sidebar title="Tools" class="is-3"}}
{{#each (tools-actions) as |supportedAction|}}
<li>
{{#link-to 'vault.cluster.tools.tool' supportedAction refreshModel=true
class="(if (eq supportedAction selectedAction) 'is-active')"
data-test-tools-action-link=supportedAction
}}
{{capitalize supportedAction}}
{{/link-to}}
</li>
{{#if (has-permission "tools" routeParams=supportedAction)}}
<li>
{{#link-to 'vault.cluster.tools.tool' supportedAction refreshModel=true
class="(if (eq supportedAction selectedAction) 'is-active')"
data-test-tools-action-link=supportedAction
}}
{{capitalize supportedAction}}
{{/link-to}}
</li>
{{/if}}
{{/each}}
{{/menu-sidebar}}
<div class="column is-9">

View File

@ -62,7 +62,7 @@ module.exports = function(environment) {
ENV.flashMessageDefaults.timeout = 50;
}
if (environment !== 'production') {
ENV.APP.DEFAULT_PAGE_SIZE = 5;
ENV.APP.DEFAULT_PAGE_SIZE = 15;
ENV.contentSecurityPolicyHeader = 'Content-Security-Policy';
ENV.contentSecurityPolicyMeta = true;
ENV.contentSecurityPolicy = {

View File

@ -0,0 +1,71 @@
import { create } from 'ember-cli-page-object';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
const consoleComponent = create(consoleClass);
const tokenWithPolicy = async function(name, policy) {
await consoleComponent.runCommands([
`write sys/policies/acl/${name} policy=${policy}`,
`write -field=client_token auth/token/create policies=${name}`,
]);
return consoleComponent.lastLogOutput;
};
module('Acceptance | cluster', function(hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function() {
await logout.visit();
return authPage.login();
});
test('hides nav item if user does not have permission', async function(assert) {
const deny_policies_policy = `'
path "sys/policies/*" {
capabilities = ["deny"]
},
'`;
const userToken = await tokenWithPolicy('hide-policies-nav', deny_policies_policy);
await logout.visit();
await authPage.login(userToken);
assert.dom('[data-test-navbar-item=policies]').doesNotExist();
await logout.visit();
});
test('shows nav item if user does have permission', async function(assert) {
const read_secrets_policy = `'
path "cubbyhole/" {
capabilities = ["read"]
},
'`;
const userToken = await tokenWithPolicy('show-secrets-nav', read_secrets_policy);
await logout.visit();
await authPage.login(userToken);
assert.dom('[data-test-navbar-item=secrets]').exists();
await logout.visit();
});
test('nav item links to first route that user has access to', async function(assert) {
const read_rgp_policy = `'
path "sys/policies/rgp" {
capabilities = ["read"]
},
'`;
const userToken = await tokenWithPolicy('show-policies-nav', read_rgp_policy);
await logout.visit();
await authPage.login(userToken);
assert.dom('[data-test-navbar-item=policies]').hasAttribute('href', '/ui/vault/policies/rgp');
await logout.visit();
});
});

View File

@ -48,18 +48,17 @@ module('Integration | Component | auth form', function(hooks) {
hooks.beforeEach(function() {
this.owner.lookup('service:csp-event').attach();
component.setContext(this);
this.owner.register('service:router', routerService);
this.router = this.owner.lookup('service:router');
});
hooks.afterEach(function() {
this.owner.lookup('service:csp-event').remove();
component.removeContext();
});
const CSP_ERR_TEXT = `Error This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`;
test('it renders error on CSP violation', async function(assert) {
this.owner.unregister('service:auth');
this.owner.register('service:auth', authService);
this.auth = this.owner.lookup('service:auth');
this.set('cluster', EmberObject.create({ standby: true }));
@ -156,6 +155,7 @@ module('Integration | Component | auth form', function(hooks) {
});
test('it calls authenticate with the correct path', async function(assert) {
this.owner.unregister('service:auth');
this.owner.register('service:auth', workingAuthService);
this.auth = this.owner.lookup('service:auth');
let authSpy = sinon.spy(this.get('auth'), 'authenticate');

View File

@ -0,0 +1,162 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import Pretender from 'pretender';
import Service from '@ember/service';
const PERMISSIONS_RESPONSE = {
data: {
exact_paths: {
foo: {
capabilities: ['read'],
},
'bar/bee': {
capabilities: ['create'],
},
boo: {
capabilities: ['deny'],
},
},
glob_paths: {
'baz/biz': {
capabilities: ['read'],
},
},
},
};
module('Unit | Service | permissions', function(hooks) {
setupTest(hooks);
hooks.beforeEach(function() {
this.server = new Pretender();
this.server.get('/v1/sys/internal/ui/resultant-acl', () => {
return [200, { 'Content-Type': 'application/json' }, JSON.stringify(PERMISSIONS_RESPONSE)];
});
});
hooks.afterEach(function() {
this.server.shutdown();
});
test('sets paths properly', async function(assert) {
let service = this.owner.lookup('service:permissions');
await service.getPaths.perform();
assert.deepEqual(service.get('exactPaths'), PERMISSIONS_RESPONSE.data.exact_paths);
assert.deepEqual(service.get('globPaths'), PERMISSIONS_RESPONSE.data.glob_paths);
});
test('returns true if a policy includes access to an exact path', function(assert) {
let service = this.owner.lookup('service:permissions');
service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
assert.equal(service.hasPermission('foo'), true);
});
test('returns true if a paths prefix is included in the policys exact paths', function(assert) {
let service = this.owner.lookup('service:permissions');
service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
assert.equal(service.hasPermission('bar'), true);
});
test('it returns true if a policy includes access to a glob path', function(assert) {
let service = this.owner.lookup('service:permissions');
service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
assert.equal(service.hasPermission('baz/biz/hi'), true);
});
test('it returns true if a policy includes access to the * glob path', function(assert) {
let service = this.owner.lookup('service:permissions');
const splatPath = { '': {} };
service.set('globPaths', splatPath);
assert.equal(service.hasPermission('hi'), true);
});
test('it returns false if the matched path includes the deny capability', function(assert) {
let service = this.owner.lookup('service:permissions');
service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
assert.equal(service.hasPermission('boo'), false);
});
test('it returns false if a policy does not includes access to a path', function(assert) {
let service = this.owner.lookup('service:permissions');
assert.equal(service.hasPermission('danger'), false);
});
test('sets the root token', function(assert) {
let service = this.owner.lookup('service:permissions');
service.setPaths({ data: { root: true } });
assert.equal(service.canViewAll, true);
});
test('returns true with the root token', function(assert) {
let service = this.owner.lookup('service:permissions');
service.set('canViewAll', true);
assert.equal(service.hasPermission('hi'), true);
});
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', () => {
return [403, { 'Content-Type': 'application/json' }];
});
await service.getPaths.perform();
assert.equal(service.canViewAll, true);
});
test('returns the first allowed nav route for policies', function(assert) {
let service = this.owner.lookup('service:permissions');
const policyPaths = {
'sys/policies/acl': {
capabilities: ['deny'],
},
'sys/policies/rgp': {
capabilities: ['read'],
},
};
service.set('exactPaths', policyPaths);
assert.equal(service.navPathParams('policies'), 'rgp');
});
test('returns the first allowed nav route for access', function(assert) {
let service = this.owner.lookup('service:permissions');
const accessPaths = {
'sys/auth': {
capabilities: ['deny'],
},
'identity/entities': {
capabilities: ['read'],
},
};
const expected = ['vault.cluster.access.identity', 'entities'];
service.set('exactPaths', accessPaths);
assert.deepEqual(service.navPathParams('access'), expected);
});
test('hasNavPermission returns true if a policy includes access to at least one path', function(assert) {
let service = this.owner.lookup('service:permissions');
const accessPaths = {
'sys/auth': {
capabilities: ['deny'],
},
'sys/leases/lookup': {
capabilities: ['read'],
},
};
service.set('exactPaths', accessPaths);
assert.equal(service.hasNavPermission('access', 'leases'), true);
});
test('hasNavPermission returns false if a policy does not include access to any paths', function(assert) {
let service = this.owner.lookup('service:permissions');
service.set('exactPaths', {});
assert.equal(service.hasNavPermission('access'), false);
});
test('appends the namespace to the path if there is one', function(assert) {
const namespaceService = Service.extend({
path: 'marketing',
});
this.owner.register('service:namespace', namespaceService);
let service = this.owner.lookup('service:permissions');
assert.equal(service.pathNameWithNamespace('sys/auth'), 'marketing/sys/auth');
});
});