Ui kv preflight endpoints (#4439)

* remove unused response-wrapping route and controller

* move to using the internal mounts endpoint for the secrets list and individual engine lookup

* remove errors about sys/mounts access because we don't need it anymore 🎉

* use modelFor instead of peekRecord for looking up the secret-engine

* remove test because we removed that error page - in the worst case scenario, a user will only have access to cubbyhole, but will see that in the secrets engines list

* make the dev CSP the same as the Go CSP

* update serializer to handle SSH responses as well as new engine fetches

* back out some changes to ttl-picker and field test object so that tests pass

* get rid of trailing space in the secret engine link

* add secrets-engine  adapater tests for new query behavior
This commit is contained in:
Matthew Irish 2018-04-24 16:30:44 -05:00 committed by GitHub
parent 639dc005ee
commit 7bf3476be9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 153 additions and 135 deletions

View File

@ -1,6 +1,5 @@
import Ember from 'ember';
import ApplicationAdapter from './application';
import DS from 'ember-data';
export default ApplicationAdapter.extend({
url(path) {
@ -8,30 +7,16 @@ export default ApplicationAdapter.extend({
return path ? url + '/' + path : url;
},
pathForType(type) {
let path;
switch (type) {
case 'cluster':
path = 'clusters';
break;
case 'secret-engine':
path = 'mounts';
break;
default:
path = Ember.String.pluralize(type);
break;
}
return path;
pathForType() {
return 'mounts';
},
query() {
return this.ajax(this.url(), 'GET').catch(e => {
if (e instanceof DS.AdapterError) {
Ember.set(e, 'policyPath', 'sys/mounts');
}
throw e;
});
query(store, type, query) {
let url = `/${this.urlPrefix()}/internal/ui/mounts`;
if (query.path) {
url = `${url}/${query.path}`;
}
return this.ajax(url, 'GET');
},
createRecord(store, type, snapshot) {

View File

@ -1,9 +0,0 @@
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: {
selectedAction: 'action',
},
selectedAction: 'wrap',
});

View File

@ -116,7 +116,6 @@ Router.map(function() {
});
});
this.route('response-wrapping');
this.route('not-found', { path: '/*path' });
});
this.route('not-found', { path: '/*path' });

View File

@ -1,8 +1,4 @@
import Ember from 'ember';
import ClusterRoute from 'vault/mixins/cluster-route';
export default Ember.Route.extend(ClusterRoute, {
model() {
return this.store.query('secret-engine', {});
},
});
export default Ember.Route.extend(ClusterRoute);

View File

@ -2,19 +2,31 @@ import Ember from 'ember';
const { inject } = Ember;
export default Ember.Route.extend({
flashMessages: inject.service(),
beforeModel(transition) {
const target = transition.targetName;
const { backend } = this.paramsFor(this.routeName);
const backendModel = this.store.peekRecord('secret-engine', backend);
const type = backendModel && backendModel.get('type');
if (type === 'kv' && backendModel.get('options.version') === 2) {
model(params) {
let { backend } = params;
return this.store
.query('secret-engine', {
path: backend,
})
.then(model => {
if (model) {
return model.get('firstObject');
}
});
},
afterModel(model, transition) {
let target = transition.targetName;
let path = model && model.get('path');
let type = model && model.get('type');
if (type === 'kv' && model.get('options.version') === 2) {
this.get('flashMessages').stickyInfo(
`"${backend}" is a newer version of the KV backend. The Vault UI does not currently support the additional versioning features. All actions taken through the UI in this engine will operate on the most recent version of a secret.`
`"${path}" is a newer version of the KV backend. The Vault UI does not currently support the additional versioning features. All actions taken through the UI in this engine will operate on the most recent version of a secret.`
);
}
if (target === this.routeName) {
return this.replaceWith('vault.cluster.secrets.backend.list-root', backend);
return this.replaceWith('vault.cluster.secrets.backend.list-root', path);
}
},
});

View File

@ -7,8 +7,7 @@ export default Ember.Route.extend(UnloadModel, {
templateName: 'vault/cluster/secrets/backend/credentials',
backendModel() {
const backend = this.paramsFor('vault.cluster.secrets.backend').backend;
return this.store.peekRecord('secret-engine', backend);
return this.modelFor('vault.cluster.secrets.backend');
},
pathQuery(role, backend) {

View File

@ -45,29 +45,29 @@ export default Ember.Route.extend({
model(params) {
const secret = params.secret ? params.secret : '';
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const backends = this.modelFor('vault.cluster.secrets').mapBy('id');
const backendModel = this.modelFor('vault.cluster.secrets.backend');
return Ember.RSVP.hash({
secret,
secrets: this.store
.lazyPaginatedQuery(this.getModelType(backend, params.tab), {
id: secret,
backend,
responsePath: 'data.keys',
page: params.page,
pageFilter: params.pageFilter,
size: 100,
})
.then(model => {
this.set('has404', false);
return model;
})
.catch(err => {
if (backends.includes(backend) && err.httpStatus === 404 && secret === '') {
return [];
} else {
throw err;
}
}),
.lazyPaginatedQuery(this.getModelType(backend, params.tab), {
id: secret,
backend,
responsePath: 'data.keys',
page: params.page,
pageFilter: params.pageFilter,
size: 100,
})
.then(model => {
this.set('has404', false);
return model;
})
.catch(err => {
if (backendModel && err.httpStatus === 404 && secret === '') {
return [];
} else {
throw err;
}
})
});
},
@ -138,11 +138,9 @@ export default Ember.Route.extend({
error(error, transition) {
const { secret } = this.paramsFor(this.routeName);
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const backends = this.modelFor('vault.cluster.secrets').mapBy('id');
Ember.set(error, 'secret', secret);
Ember.set(error, 'isRoot', true);
Ember.set(error, 'hasBackend', backends.includes(backend));
Ember.set(error, 'backend', backend);
const hasModel = this.controllerFor(this.routeName).get('hasModel');
// only swallow the error if we have a previous model

View File

@ -5,7 +5,7 @@ import UnloadModelRoute from 'vault/mixins/unload-model-route';
export default Ember.Route.extend(UnloadModelRoute, {
capabilities(secret) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
let backendModel = this.store.peekRecord('secret-engine', backend);
let backendModel = this.modelFor('vault.cluster.secrets.backend');
let backendType = backendModel.get('type');
let version = backendModel.get('options.version');
let path;
@ -21,8 +21,8 @@ export default Ember.Route.extend(UnloadModelRoute, {
return this.store.findRecord('capabilities', path);
},
backendType(path) {
return this.store.peekRecord('secret-engine', path).get('type');
backendType() {
return this.modelFor('vault.cluster.secrets.backend').get('type');
},
templateName: 'vault/cluster/secrets/backend/secretEditLayout',
@ -50,7 +50,7 @@ export default Ember.Route.extend(UnloadModelRoute, {
aws: 'role-aws',
pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki',
};
let backendModel = this.store.peekRecord('secret-engine', backend);
let backendModel = this.modelFor('vault.cluster.secrets.backend', backend);
let defaultType = 'secret';
if (backendModel.get('type') === 'kv' && backendModel.get('options.version') === 2) {
defaultType = 'secret-v2';
@ -81,7 +81,7 @@ export default Ember.Route.extend(UnloadModelRoute, {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const preferAdvancedEdit =
this.controllerFor('vault.cluster.secrets.backend').get('preferAdvancedEdit') || false;
const backendType = this.backendType(backend);
const backendType = this.backendType();
model.secret.setProperties({ backend });
controller.setProperties({
model: model.secret,
@ -105,10 +105,8 @@ export default Ember.Route.extend(UnloadModelRoute, {
error(error) {
const { secret } = this.paramsFor(this.routeName);
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const backends = this.modelFor('vault.cluster.secrets').mapBy('id');
Ember.set(error, 'keyId', backend + '/' + secret);
Ember.set(error, 'backend', backend);
Ember.set(error, 'hasBackend', backends.includes(backend));
return true;
},

View File

@ -5,8 +5,7 @@ export default Ember.Route.extend(UnloadModel, {
templateName: 'vault/cluster/secrets/backend/sign',
backendModel() {
const backend = this.paramsFor('vault.cluster.secrets.backend').backend;
return this.store.peekRecord('secret-engine', backend);
return this.modelFor('vault.cluster.secrets.backend');
},
pathQuery(role, backend) {

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.query('secret-engine', {});
},
});

View File

@ -6,8 +6,8 @@ const CONFIGURABLE_BACKEND_TYPES = ['aws', 'ssh', 'pki'];
export default Ember.Route.extend({
model() {
const { backend } = this.paramsFor(this.routeName);
return this.store.query('secret-engine', {}).then(() => {
const model = this.store.peekRecord('secret-engine', backend);
return this.store.query('secret-engine', { path: backend }).then(modelList => {
let model = modelList && modelList.get('firstObject');
if (!model || !CONFIGURABLE_BACKEND_TYPES.includes(model.get('type'))) {
const error = new DS.AdapterError();
Ember.set(error, 'httpStatus', 404);

View File

@ -38,7 +38,17 @@ export default DS.RESTSerializer.extend({
} else if (isQueryRecord) {
backends = this.normalizeBackend(null, payload);
} else {
backends = Object.keys(payload.data).map(id => this.normalizeBackend(id, payload[id]));
// this is terrible, I'm sorry
// TODO extract AWS and SSH config saving from the secret-engine model to simplify this
if (payload.data.secret) {
backends = Object.keys(payload.data.secret).map(id =>
this.normalizeBackend(id, payload.data.secret[id])
);
} else if (!payload.data.path) {
backends = Object.keys(payload.data).map(id => this.normalizeBackend(id, payload[id]));
} else {
backends = [this.normalizeBackend(payload.data.path, payload.data)];
}
}
const transformedPayload = { [primaryModelClass.modelName]: backends };

View File

@ -6,7 +6,7 @@
value={{time}}
id="time-{{elementId}}"
type="text"
name="time-{{elementId}}"
name="time"
class="input"
oninput={{action 'changedValue' 'time'}}
/>
@ -15,8 +15,8 @@
<div class="select is-fullwidth">
<select
data-test-ttl-unit
name="unit-{{elementId}}"
id="unit-{{elementId}}"
name="unit"
id="unit"
onchange={{action 'changedValue' 'unit'}}
>
{{#each unitOptions as |unitOption|}}

View File

@ -26,17 +26,6 @@
<p>
Make sure the policy for the path <code>{{model.policyPath}}</code> includes <code>capabilities = ['update']</code>.
</p>
{{else if (and
(eq model.httpStatus 403)
(eq model.policyPath 'sys/mounts')
)
}}
<p data-test-sys-mounts-warning>
Your auth token does not have access to {{model.policyPath}}. This is necessary in order to browse secret backends.
</p>
<p>
Make sure the policy for the path <code>{{model.policyPath}}</code> has <code>capabilities = ['list', 'read']</code>.
</p>
{{else}}
{{#if model.message}}
<p>{{model.message}}</p>

View File

@ -2,7 +2,7 @@
<header class="page-header">
<nav class="breadcrumb">
<li>
<a href={{href-to params=(if model.hasBackend
<a href={{href-to params=(if model.backend
(array "vault.cluster.secrets.backend.list-root")
(array "vault.cluster.secrets")
)
@ -30,9 +30,9 @@
{{#if (eq model.httpStatus 404)}}
<p data-test-secret-not-found>
Unable to find secret at <code>{{concat model.backend "/" model.secret}}</code>. Try going back to the
{{#link-to params=(if model.hasBackend
(reduce-to-array "vault.cluster.secrets.backend.list-root")
(reduce-to-array "vault.cluster.secrets")
{{#link-to params=(if model.backend
(array "vault.cluster.secrets.backend.list-root")
(array "vault.cluster.secrets")
)
}}root{{/link-to}}
and navigating from there.

View File

@ -27,9 +27,10 @@
<div class="level is-mobile">
<div class="level-left">
<div>
<a data-test-secret-path href={{href-to 'vault.cluster.secrets.backend.list-root' backend.id}} class="has-text-black has-text-weight-semibold">
{{i-con glyph="folder" size=14 class="has-text-grey-light"}}{{backend.path}}
</a>
<a data-test-secret-path
href={{href-to 'vault.cluster.secrets.backend.list-root' backend.id}}
class="has-text-black has-text-weight-semibold"
>{{i-con glyph="folder" size=14 class="has-text-grey-light"}}{{backend.path}}</a>
<br />
<span class="tag">
<code>

View File

@ -57,6 +57,9 @@ module.exports = function(environment) {
ENV.contentSecurityPolicyMeta = true;
ENV.contentSecurityPolicy = {
'connect-src': ["'self'"],
'img-src': ["'self'", 'data:'],
'form-action': ["'none'"],
'script-src': ["'self'"],
'style-src': ["'unsafe-inline'", "'self'"],
};
}

View File

@ -1,31 +0,0 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import listPage from 'vault/tests/pages/secrets/backend/list';
import { startMirage } from 'vault/initializers/ember-cli-mirage';
import Ember from 'ember';
let adapterException;
let loggerError;
moduleForAcceptance('Acceptance | secrets/secret/secret error', {
beforeEach() {
this.server = startMirage();
loggerError = Ember.Logger.error;
adapterException = Ember.Test.adapter.exception;
Ember.Test.adapter.exception = () => {};
Ember.Logger.error = () => {};
return authLogin();
},
afterEach() {
Ember.Test.adapter.exception = adapterException;
Ember.Logger.error = loggerError;
this.server.shutdown();
},
});
test('it shows a warning if dont have access to the secrets list', function(assert) {
listPage.visitRoot({ backend: 'secret' });
andThen(() => {
assert.ok(find('[data-test-sys-mounts-warning]').length, 'shows the warning for sys/mounts');
});
});

View File

@ -43,7 +43,7 @@ export default {
// we use name in the label `for` attribute
// this is consistent across all types of fields
//(otherwise we'd have to use name on select or input or textarea)
return this.filterBy('for', name)[0];
return this.toArray().findBy('for', name);
},
fillIn(name, value) {
return this.findByName(name).input(value);

View File

@ -0,0 +1,62 @@
import { moduleFor, test } from 'ember-qunit';
import apiStub from 'vault/tests/helpers/noop-all-api-requests';
moduleFor('adapter:secret-engine', 'Unit | Adapter | secret engine', {
needs: ['service:auth', 'service:flash-messages'],
beforeEach() {
this.server = apiStub();
},
afterEach() {
this.server.shutdown();
},
});
const storeStub = {
serializerFor() {
return {
serializeIntoHash() {},
};
},
};
const type = {
modelName: 'secret-engine',
};
const cases = [
{
description: 'Empty query',
adapterMethod: 'query',
args: [storeStub, type, {}],
url: '/v1/sys/internal/ui/mounts',
method: 'GET',
},
{
description: 'Query with a path',
adapterMethod: 'query',
args: [storeStub, type, { path: 'foo' }],
url: '/v1/sys/internal/ui/mounts/foo',
method: 'GET',
},
{
description: 'Query with nested path',
adapterMethod: 'query',
args: [storeStub, type, { path: 'foo/bar/baz' }],
url: '/v1/sys/internal/ui/mounts/foo/bar/baz',
method: 'GET',
},
];
cases.forEach(testCase => {
test(`secret-engine: ${testCase.description}`, function(assert) {
assert.expect(2);
let adapter = this.subject();
adapter[testCase.adapterMethod](...testCase.args);
let { url, method } = this.server.handledRequests[0];
assert.equal(url, testCase.url, `${testCase.adapterMethod} calls the correct url: ${testCase.url}`);
assert.equal(
method,
testCase.method,
`${testCase.adapterMethod} uses the correct http verb: ${testCase.method}`
);
});
});