Merge branch 'master-oss' into approle-local-secretid

This commit is contained in:
vishalnayak 2018-04-24 17:53:50 -04:00
commit 19e3f43573
21 changed files with 156 additions and 135 deletions

View File

@ -46,6 +46,9 @@ IMPROVEMENTS:
the rate of writes committed
* secret/ssh: Update dynamic key install script to use shell locking to avoid
concurrent modifications [GH-4358]
* ui: Access to `sys/mounts` is no longer needed to use the UI - the list of
engines will show you the ones you implicitly have access to (because you have
access to to secrets in those engines) [GH-4439]
BUG FIXES:

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}`
);
});
});