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

Backport 1.2.4: allow viewing kv-v2 secrets gated by control groups #7573

Merged
merged 3 commits into from
Oct 4, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 3 additions & 2 deletions ui/app/adapters/secret-v2-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { get } from '@ember/object';
import ApplicationAdapter from './application';
import DS from 'ember-data';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ControlGroupError from 'vault/lib/control-group-error';

export default ApplicationAdapter.extend({
namespace: 'v1',
Expand All @@ -27,8 +28,8 @@ export default ApplicationAdapter.extend({

findRecord() {
return this._super(...arguments).catch(errorOrModel => {
// if it's a real 404, this will be an error, if not
// it will be the body of a deleted / destroyed version
// if the response is a real 404 or if the secret is gated by a control group this will be an error,
// otherwise the response will be the body of a deleted / destroyed version
if (errorOrModel instanceof DS.AdapterError) {
throw errorOrModel;
}
Expand Down
4 changes: 2 additions & 2 deletions ui/app/lib/control-group-error.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import EmberError from '@ember/error';
import DS from 'ember-data';

export default class ControlGroupError extends EmberError {
export default class ControlGroupError extends DS.AdapterError {
constructor(wrapInfo) {
let { accessor, creation_path, creation_time, token, ttl } = wrapInfo;
super();
Expand Down
15 changes: 8 additions & 7 deletions ui/app/models/identity/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import IdentityModel from './_base';
import DS from 'ember-data';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import identityCapabilities from 'vault/macros/identity-capabilities';
import apiPath from 'vault/utils/api-path';
import attachCapabilities from 'vault/lib/attach-capabilities';

const { attr, hasMany } = DS;

export default IdentityModel.extend({
let Model = IdentityModel.extend({
formFields: computed(function() {
return ['name', 'disabled', 'policies', 'metadata'];
}),
Expand Down Expand Up @@ -43,12 +43,13 @@ export default IdentityModel.extend({
inheritedGroupIds: attr({
readOnly: true,
}),

updatePath: identityCapabilities(),
canDelete: alias('updatePath.canDelete'),
canEdit: alias('updatePath.canUpdate'),
canRead: alias('updatePath.canRead'),

aliasPath: lazyCapabilities(apiPath`identity/entity-alias`),
canAddAlias: alias('aliasPath.canCreate'),
});

export default attachCapabilities(Model, {
updatePath: apiPath`identity/${'identityType'}/id/${'id'}`,
aliasPath: apiPath`identity/entity-alias`,
});
3 changes: 1 addition & 2 deletions ui/app/routes/vault/cluster/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ export default Route.extend(ModelBoundaryRoute, {
this.namespaceService.reset();
this.console.set('isOpen', false);
this.console.clearLog(true);
this.clearModelCache();
this.replaceWith('vault.cluster.auth', { queryParams: { redirect_to: '' } });
this.flashMessages.clearMessages();
this.permissions.reset();
this.replaceWith('vault.cluster.auth');
},
});
4 changes: 3 additions & 1 deletion ui/app/services/control-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Service, { inject as service } from '@ember/service';
import RSVP from 'rsvp';
import ControlGroupError from 'vault/lib/control-group-error';
import getStorage from 'vault/lib/token-storage';
import parseURL from 'core/utils/parse-url';

const CONTROL_GROUP_PREFIX = 'vault:cg-';
const TOKEN_SEPARATOR = '☃';
Expand Down Expand Up @@ -71,7 +72,8 @@ export default Service.extend({
if (this.get('version.isOSS')) {
return null;
}
let pathForUrl = url.replace('/v1/', '');
let pathForUrl = parseURL(url).pathname;
pathForUrl = pathForUrl.replace('/v1/', '');
let tokenInfo = this.get('tokenToUnwrap');
if (tokenInfo && tokenInfo.creation_path === pathForUrl) {
let { token, accessor, creation_time } = tokenInfo;
Expand Down
18 changes: 8 additions & 10 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@
"gen-story-md": "node scripts/gen-story-md.js"
},
"lint-staged": {
"linters": {
"*.js": [
"prettier-eslint --single-quote --no-use-tabs --trailing-comma es5 --print-width 110 --write",
"git add"
],
"*.scss": [
"prettier --write",
"git add"
]
}
"*.js": [
"prettier-eslint --single-quote --no-use-tabs --trailing-comma es5 --print-width 110 --write",
"git add"
],
"*.scss": [
"prettier --write",
"git add"
]
},
"devDependencies": {
"@ember/optional-features": "^0.7.0",
Expand Down
50 changes: 39 additions & 11 deletions ui/tests/acceptance/enterprise-control-groups-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import authForm from 'vault/tests/pages/components/auth-form';
import controlGroup from 'vault/tests/pages/components/control-group';
import controlGroupSuccess from 'vault/tests/pages/components/control-group-success';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret';
import listPage from 'vault/tests/pages/secrets/backend/list';

const consoleComponent = create(consoleClass);
const authFormComponent = create(authForm);
Expand All @@ -23,10 +24,6 @@ module('Acceptance | Enterprise | control groups', function(hooks) {
return authPage.login();
});

hooks.afterEach(function() {
return logout.visit();
});

const POLICY = `
path "kv/foo" {
capabilities = ["create", "read", "update", "delete", "list"]
Expand All @@ -40,6 +37,23 @@ module('Acceptance | Enterprise | control groups', function(hooks) {
}
}
}

path "kv-v2-mount/data/foo" {
capabilities = ["create", "read", "update", "list"]
control_group = {
max_ttl = "24h"
factor "ops_manager" {
identity {
group_names = ["managers"]
approvals = 1
}
}
}
}

path "kv-v2-mount/*" {
capabilities = ["list"]
}
`;

const AUTHORIZER_POLICY = `
Expand All @@ -59,9 +73,10 @@ module('Acceptance | Enterprise | control groups', function(hooks) {
await visit('/vault/secrets');
await consoleComponent.toggle();
await consoleComponent.runCommands([
//enable kv mount and write some data
//enable kv-v1 mount and write a secret
'write sys/mounts/kv type=kv',
'write kv/foo bar=baz',

//enable userpass, create user and associated entity
'write sys/auth/userpass type=userpass',
`write auth/userpass/users/${ADMIN_USER} password=${ADMIN_PASSWORD} policies=default`,
Expand All @@ -72,7 +87,9 @@ module('Acceptance | Enterprise | control groups', function(hooks) {
// read out mount to get the accessor
'read -field=accessor sys/internal/ui/mounts/auth/userpass',
]);

userpassAccessor = consoleComponent.lastTextOutput;

await consoleComponent.runCommands([
// lookup entity id for our authorizer
`write -field=id identity/lookup/entity name=${ADMIN_USER}`,
Expand All @@ -86,14 +103,25 @@ module('Acceptance | Enterprise | control groups', function(hooks) {
'write -field=client_token auth/token/create policies=kv-control-group',
]);
context.userToken = consoleComponent.lastLogOutput;
await logout.visit();

await authPage.login(context.userToken);
return this;
};

test('it redirects you if you try to navigate to a Control Group restricted path', async function(assert) {
const writeSecret = async function(backend, path, key, val) {
await listPage.visitRoot({ backend });
await listPage.create();
await editPage.createSecret(path, key, val);
};

test('for v2 secrets it redirects you if you try to navigate to a Control Group restricted path', async function(assert) {
await consoleComponent.runCommands([
'write sys/mounts/kv-v2-mount type=kv-v2',
'delete kv-v2-mount/metadata/foo',
]);
await writeSecret('kv-v2-mount', 'foo', 'bar', 'baz');
await setupControlGroup(this);
await visit('/vault/secrets/kv/show/foo');
await visit('/vault/secrets/kv-v2-mount/show/foo');
assert.equal(
currentRouteName(),
'vault.cluster.access.control-group-accessor',
Expand All @@ -112,7 +140,7 @@ module('Acceptance | Enterprise | control groups', function(hooks) {
await visit(url);
accessor = controlGroupComponent.accessor;
controlGroupToken = controlGroupComponent.token;
await logout.visit();
await authPage.logout();

// log in as the admin, navigate to the accessor page,
// and authorize the control group request
Expand All @@ -123,7 +151,7 @@ module('Acceptance | Enterprise | control groups', function(hooks) {
await visit(`/vault/access/control-groups/${accessor}`);
await controlGroupComponent.authorize();
assert.equal(controlGroupComponent.bannerPrefix, 'Thanks!', 'text display changes');
await logout.visit();
await authPage.logout();

await authPage.login(context.userToken);

Expand Down
62 changes: 54 additions & 8 deletions ui/tests/acceptance/redirect-to-test.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,85 @@
import { currentURL, visit } from '@ember/test-helpers';
import { currentURL, visit as _visit, settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import { create } from 'ember-cli-page-object';
import auth from 'vault/tests/pages/auth';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';

module('Acceptance | redirect_to functionality', function(hooks) {
const visit = async url => {
try {
await _visit(url);
} catch (e) {
if (e.message !== 'TransitionAborted') {
throw e;
}
}

await settled();
};

const consoleComponent = create(consoleClass);

const wrappedAuth = async () => {
await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=3m`);
return consoleComponent.lastLogOutput;
};

const setupWrapping = async () => {
await auth.logout();
await auth.visit();
await auth.tokenInput('root').submit();
let wrappedToken = await wrappedAuth();
return wrappedToken;
};
module('Acceptance | redirect_to query param functionality', function(hooks) {
setupApplicationTest(hooks);

hooks.beforeEach(function() {
// normally we'd use the auth.logout helper to visit the route and reset the app, but in this case that
// also routes us to the auth page, and then all of the transitions from the auth page get redirected back
// to the auth page resulting in no redirect_to query param being set
localStorage.clear();
});
test('redirect to a route after authentication', async function(assert) {
let url = '/vault/secrets/secret/create';
await visit(url);
assert.equal(
currentURL(),
`/vault/auth?redirect_to=${encodeURIComponent(url)}&with=token`,
`/vault/auth?redirect_to=${encodeURIComponent(url)}`,
'encodes url for the query param'
);
// the login method on this page does another visit call that we don't want here
await authPage.tokenInput('root').submit();
await auth.tokenInput('root').submit();
assert.equal(currentURL(), url, 'navigates to the redirect_to url after auth');
});

test('redirect from root does not include redirect_to', async function(assert) {
let url = '/';
await visit(url);
assert.equal(currentURL(), `/vault/auth?with=token`, 'there is no redirect_to query param');
assert.equal(currentURL(), `/vault/auth`, 'there is no redirect_to query param');
});

test('redirect to a route after authentication with a query param', async function(assert) {
let url = '/vault/secrets/secret/create?initialKey=hello';
await visit(url);
assert.equal(
currentURL(),
`/vault/auth?redirect_to=${encodeURIComponent(url)}&with=token`,
`/vault/auth?redirect_to=${encodeURIComponent(url)}`,
'encodes url for the query param'
);
await authPage.tokenInput('root').submit();
await auth.tokenInput('root').submit();
assert.equal(currentURL(), url, 'navigates to the redirect_to with the query param after auth');
});

test('redirect to logout with wrapped token authenticates you', async function(assert) {
let wrappedToken = await setupWrapping();
let url = '/vault/secrets/cubbyhole/create';

await auth.logout({
redirect_to: url,
wrapped_token: wrappedToken,
});

assert.equal(currentURL(), url, 'authenticates then navigates to the redirect_to url after auth');
});
});
3 changes: 3 additions & 0 deletions ui/tests/pages/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { create, visitable, fillable, clickable } from 'ember-cli-page-object';

export default create({
visit: visitable('/vault/auth'),
logout: visitable('/vault/logout'),
submit: clickable('[data-test-auth-submit]'),
tokenInput: fillable('[data-test-token]'),
login: async function(token) {
// make sure we're always logged out and logged back in
await this.logout();
await this.visit({ with: 'token' });
if (token) {
return this.tokenInput(token).submit();
Expand Down