diff --git a/ui/app/adapters/secret-v2-version.js b/ui/app/adapters/secret-v2-version.js index d981898da2f3..a6e13209114b 100644 --- a/ui/app/adapters/secret-v2-version.js +++ b/ui/app/adapters/secret-v2-version.js @@ -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', @@ -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; } diff --git a/ui/app/lib/control-group-error.js b/ui/app/lib/control-group-error.js index 525fe41f95f2..214ff5286fae 100644 --- a/ui/app/lib/control-group-error.js +++ b/ui/app/lib/control-group-error.js @@ -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(); diff --git a/ui/app/models/identity/entity.js b/ui/app/models/identity/entity.js index 6911f179ba51..2d87e3a55a1b 100644 --- a/ui/app/models/identity/entity.js +++ b/ui/app/models/identity/entity.js @@ -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']; }), @@ -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`, +}); diff --git a/ui/app/routes/vault/cluster/logout.js b/ui/app/routes/vault/cluster/logout.js index 2dd69193871c..2e6e22f3ffde 100644 --- a/ui/app/routes/vault/cluster/logout.js +++ b/ui/app/routes/vault/cluster/logout.js @@ -21,7 +21,6 @@ export default Route.extend(ModelBoundaryRoute, { this.namespaceService.reset(); this.console.set('isOpen', false); this.console.clearLog(true); - this.clearModelCache(); this.flashMessages.clearMessages(); this.permissions.reset(); this.replaceWith('vault.cluster.auth'); diff --git a/ui/app/services/control-group.js b/ui/app/services/control-group.js index 22fac9e5b386..536f1b3383f3 100644 --- a/ui/app/services/control-group.js +++ b/ui/app/services/control-group.js @@ -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 = '☃'; @@ -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; diff --git a/ui/tests/acceptance/enterprise-control-groups-test.js b/ui/tests/acceptance/enterprise-control-groups-test.js index 68118600274d..fdd0b6fcc69a 100644 --- a/ui/tests/acceptance/enterprise-control-groups-test.js +++ b/ui/tests/acceptance/enterprise-control-groups-test.js @@ -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); @@ -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"] @@ -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 = ` @@ -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`, @@ -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}`, @@ -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', @@ -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 @@ -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); diff --git a/ui/tests/acceptance/redirect-to-test.js b/ui/tests/acceptance/redirect-to-test.js index 2d89732875d3..b25e4b65c1b4 100644 --- a/ui/tests/acceptance/redirect-to-test.js +++ b/ui/tests/acceptance/redirect-to-test.js @@ -79,6 +79,7 @@ module('Acceptance | redirect_to query param functionality', function(hooks) { redirect_to: url, wrapped_token: wrappedToken, }); + assert.equal(currentURL(), url, 'authenticates then navigates to the redirect_to url after auth'); }); }); diff --git a/ui/tests/pages/auth.js b/ui/tests/pages/auth.js index ebbfe8c4595e..62dd30af1708 100644 --- a/ui/tests/pages/auth.js +++ b/ui/tests/pages/auth.js @@ -6,7 +6,7 @@ export default create({ 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 :w + // make sure we're always logged out and logged back in await this.logout(); await this.visit({ with: 'token' }); if (token) {