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:
parent
639dc005ee
commit
7bf3476be9
|
@ -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) {
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: {
|
||||
selectedAction: 'action',
|
||||
},
|
||||
|
||||
selectedAction: 'wrap',
|
||||
});
|
|
@ -116,7 +116,6 @@ Router.map(function() {
|
|||
});
|
||||
});
|
||||
|
||||
this.route('response-wrapping');
|
||||
this.route('not-found', { path: '/*path' });
|
||||
});
|
||||
this.route('not-found', { path: '/*path' });
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.store.query('secret-engine', {});
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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|}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'"],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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}`
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue