UI - unauthed login methods (#4854)
* fetch auth methods when going to the auth route and pass them to the auth form component * add boolean editType for form-fields * look in the data hash in the serializer * remove renderInPlace for info-tooltips as it does something goofy with widths * add new fields for auth methods * fix console refresh command on routes that use lazyPaginatedQuery * add wrapped_token param that logs you in via the token backend and show other backends if your list contains supported ones * handle casing when looking up supported backends * change listingVisibility to match the new API * move wrapped_token up to the vault route level so it works from the app root
This commit is contained in:
parent
32c94b2638
commit
f38a50b6b2
|
@ -24,7 +24,7 @@ export default DS.RESTAdapter.extend({
|
|||
},
|
||||
|
||||
_preRequest(url, options) {
|
||||
const token = this.get('auth.currentToken');
|
||||
const token = options.clientToken || this.get('auth.currentToken');
|
||||
if (token && !options.unauthenticated) {
|
||||
options.headers = Ember.assign(options.headers || {}, {
|
||||
'X-Vault-Token': token,
|
||||
|
|
|
@ -13,7 +13,22 @@ export default ApplicationAdapter.extend({
|
|||
return 'mounts/auth';
|
||||
},
|
||||
|
||||
findAll() {
|
||||
findAll(store, type, sinceToken, snapshotRecordArray) {
|
||||
let isUnauthenticated = Ember.get(snapshotRecordArray || {}, 'adapterOptions.unauthenticated');
|
||||
if (isUnauthenticated) {
|
||||
let url = `/${this.urlPrefix()}/internal/ui/mounts`;
|
||||
return this.ajax(url, 'GET', {
|
||||
unauthenticated: true,
|
||||
})
|
||||
.then(result => {
|
||||
return {
|
||||
data: result.data.auth,
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
return this.ajax(this.url(), 'GET').catch(e => {
|
||||
if (e instanceof DS.AdapterError) {
|
||||
Ember.set(e, 'policyPath', 'sys/auth');
|
||||
|
|
|
@ -15,9 +15,9 @@ export default ApplicationAdapter.extend({
|
|||
},
|
||||
|
||||
toolAction(action, data, options = {}) {
|
||||
const { wrapTTL } = options;
|
||||
const { wrapTTL, clientToken } = options;
|
||||
const url = this.toolUrlFor(action);
|
||||
const ajaxOptions = wrapTTL ? { data, wrapTTL } : { data };
|
||||
const ajaxOptions = wrapTTL ? { data, wrapTTL, clientToken } : { data, clientToken };
|
||||
return this.ajax(url, 'POST', ajaxOptions);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Ember from 'ember';
|
||||
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
|
||||
import { task } from 'ember-concurrency';
|
||||
const BACKENDS = supportedAuthBackends();
|
||||
const { computed, inject, get } = Ember;
|
||||
|
||||
|
@ -11,57 +12,125 @@ const DEFAULTS = {
|
|||
|
||||
export default Ember.Component.extend(DEFAULTS, {
|
||||
classNames: ['auth-form'],
|
||||
routing: inject.service('-routing'),
|
||||
router: inject.service(),
|
||||
auth: inject.service(),
|
||||
flashMessages: inject.service(),
|
||||
store: inject.service(),
|
||||
csp: inject.service('csp-event'),
|
||||
|
||||
// set during init and potentially passed in via a query param
|
||||
selectedAuth: null,
|
||||
methods: null,
|
||||
cluster: null,
|
||||
redirectTo: null,
|
||||
|
||||
didRender() {
|
||||
this._super(...arguments);
|
||||
// on very narrow viewports the active tab may be overflowed, so we scroll it into view here
|
||||
this.$('li.is-active').get(0).scrollIntoView();
|
||||
let activeEle = this.element.querySelector('li.is-active');
|
||||
if (activeEle) {
|
||||
activeEle.scrollIntoView();
|
||||
}
|
||||
// this is here because we're changing the `with` attr and there's no way to short-circuit rendering,
|
||||
// so we'll just nav -> get new attrs -> re-render
|
||||
if (!this.get('selectedAuth') || (this.get('selectedAuth') && !this.get('selectedAuthBackend'))) {
|
||||
this.get('router').replaceWith('vault.cluster.auth', this.get('cluster.name'), {
|
||||
queryParams: {
|
||||
with: this.firstMethod(),
|
||||
wrappedToken: this.get('wrappedToken'),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
firstMethod() {
|
||||
let firstMethod = this.get('methodsToShow.firstObject');
|
||||
// prefer backends with a path over those with a type
|
||||
return get(firstMethod, 'path') || get(firstMethod, 'type');
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
let newMethod = this.get('selectedAuthType');
|
||||
let oldMethod = this.get('oldSelectedAuthType');
|
||||
let token = this.get('wrappedToken');
|
||||
let newMethod = this.get('selectedAuth');
|
||||
let oldMethod = this.get('oldSelectedAuth');
|
||||
|
||||
if (oldMethod && oldMethod !== newMethod) {
|
||||
this.resetDefaults();
|
||||
}
|
||||
this.set('oldSelectedAuthType', newMethod);
|
||||
this.set('oldSelectedAuth', newMethod);
|
||||
|
||||
if (token) {
|
||||
this.get('unwrapToken').perform(token);
|
||||
}
|
||||
},
|
||||
|
||||
resetDefaults() {
|
||||
this.setProperties(DEFAULTS);
|
||||
},
|
||||
|
||||
cluster: null,
|
||||
redirectTo: null,
|
||||
selectedAuthIsPath: computed.match('selectedAuth', /\/$/),
|
||||
selectedAuthBackend: Ember.computed(
|
||||
'allSupportedMethods',
|
||||
'selectedAuth',
|
||||
'selectedAuthIsPath',
|
||||
function() {
|
||||
let methods = this.get('allSupportedMethods');
|
||||
let keyIsPath = this.get('selectedAuthIsPath');
|
||||
let findKey = keyIsPath ? 'path' : 'type';
|
||||
return methods.findBy(findKey, this.get('selectedAuth'));
|
||||
}
|
||||
),
|
||||
|
||||
selectedAuthType: 'token',
|
||||
selectedAuthBackend: Ember.computed('selectedAuthType', function() {
|
||||
return BACKENDS.findBy('type', this.get('selectedAuthType'));
|
||||
}),
|
||||
|
||||
providerPartialName: Ember.computed('selectedAuthType', function() {
|
||||
const type = Ember.String.dasherize(this.get('selectedAuthType'));
|
||||
return `partials/auth-form/${type}`;
|
||||
providerPartialName: computed('selectedAuthBackend', function() {
|
||||
let type = this.get('selectedAuthBackend.type') || 'token';
|
||||
type = type.toLowerCase();
|
||||
let templateName = Ember.String.dasherize(type);
|
||||
return `partials/auth-form/${templateName}`;
|
||||
}),
|
||||
|
||||
hasCSPError: computed.alias('csp.connectionViolations.firstObject'),
|
||||
|
||||
cspErrorText: `This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`,
|
||||
|
||||
allSupportedMethods: computed('methodsToShow', 'hasMethodsWithPath', function() {
|
||||
let hasMethodsWithPath = this.get('hasMethodsWithPath');
|
||||
let methodsToShow = this.get('methodsToShow');
|
||||
return hasMethodsWithPath ? methodsToShow.concat(BACKENDS) : methodsToShow;
|
||||
}),
|
||||
|
||||
hasMethodsWithPath: computed('methodsToShow', function() {
|
||||
return this.get('methodsToShow').isAny('path');
|
||||
}),
|
||||
methodsToShow: computed('methods', 'methods.[]', function() {
|
||||
let methods = this.get('methods') || [];
|
||||
let shownMethods = methods.filter(m =>
|
||||
BACKENDS.find(b => get(b, 'type').toLowerCase() === get(m, 'type').toLowerCase())
|
||||
);
|
||||
return shownMethods.length ? shownMethods : BACKENDS;
|
||||
}),
|
||||
|
||||
unwrapToken: task(function*(token) {
|
||||
// will be using the token auth method, so set it here
|
||||
this.set('selectedAuth', 'token');
|
||||
let adapter = this.get('store').adapterFor('tools');
|
||||
try {
|
||||
let response = yield adapter.toolAction('unwrap', null, { clientToken: token });
|
||||
this.set('token', response.auth.client_token);
|
||||
this.send('doSubmit');
|
||||
} catch (e) {
|
||||
this.set('error', `Token unwrap failed: ${e.errors[0]}`);
|
||||
}
|
||||
}),
|
||||
|
||||
handleError(e) {
|
||||
this.set('loading', false);
|
||||
|
||||
let errors = e.errors.map(error => {
|
||||
if (error.detail) {
|
||||
return error.detail;
|
||||
}
|
||||
return error;
|
||||
});
|
||||
|
||||
this.set('error', `Authentication failed: ${errors.join('.')}`);
|
||||
},
|
||||
|
||||
|
@ -73,19 +142,22 @@ export default Ember.Component.extend(DEFAULTS, {
|
|||
error: null,
|
||||
});
|
||||
let targetRoute = this.get('redirectTo') || 'vault.cluster';
|
||||
let backend = this.get('selectedAuthBackend');
|
||||
let path = this.get('customPath');
|
||||
let attributes = get(backend, 'formAttributes');
|
||||
let backend = this.get('selectedAuthBackend') || {};
|
||||
let path = get(backend, 'path') || this.get('customPath');
|
||||
let backendMeta = BACKENDS.find(
|
||||
b => get(b, 'type').toLowerCase() === get(backend, 'type').toLowerCase()
|
||||
);
|
||||
let attributes = get(backendMeta, 'formAttributes');
|
||||
|
||||
data = Ember.assign(data, this.getProperties(...attributes));
|
||||
if (this.get('useCustomPath') && path) {
|
||||
if (get(backend, 'path') || (this.get('useCustomPath') && path)) {
|
||||
data.path = path;
|
||||
}
|
||||
const clusterId = this.get('cluster.id');
|
||||
this.get('auth').authenticate({ clusterId, backend: get(backend, 'type'), data }).then(
|
||||
({ isRoot }) => {
|
||||
this.set('loading', false);
|
||||
const transition = this.get('routing.router').transitionTo(targetRoute);
|
||||
const transition = this.get('router').transitionTo(targetRoute);
|
||||
if (isRoot) {
|
||||
transition.followRedirects().then(() => {
|
||||
this.get('flashMessages').warning(
|
||||
|
|
|
@ -17,6 +17,7 @@ export default Ember.Component.extend({
|
|||
isFullscreen: false,
|
||||
console: inject.service(),
|
||||
router: inject.service(),
|
||||
store: inject.service(),
|
||||
inputValue: null,
|
||||
log: computed.alias('console.log'),
|
||||
|
||||
|
@ -86,6 +87,7 @@ export default Ember.Component.extend({
|
|||
let route = owner.lookup(`route:${routeName}`);
|
||||
|
||||
try {
|
||||
this.get('store').clearAllDatasets();
|
||||
yield route.refresh();
|
||||
this.logAndOutput(null, { type: 'success', content: 'The current screen has been refreshed!' });
|
||||
} catch (error) {
|
||||
|
|
|
@ -97,6 +97,11 @@ export default Ember.Component.extend({
|
|||
this.get('onChange')(path, value);
|
||||
},
|
||||
|
||||
setAndBroadcastBool(path, trueVal, falseVal, value) {
|
||||
let valueToSet = value === true ? trueVal : falseVal;
|
||||
this.send('setAndBroadcast', path, valueToSet);
|
||||
},
|
||||
|
||||
codemirrorUpdated(path, value, codemirror) {
|
||||
codemirror.performLint();
|
||||
const hasErrors = codemirror.state.lint.marked.length > 0;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: [
|
||||
{
|
||||
wrappedToken: 'wrapped_token',
|
||||
},
|
||||
],
|
||||
wrappedToken: '',
|
||||
});
|
|
@ -1,11 +1,9 @@
|
|||
import Ember from 'ember';
|
||||
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ['with'],
|
||||
with: Ember.computed(function() {
|
||||
return supportedAuthBackends()[0].type;
|
||||
}),
|
||||
|
||||
vaultController: Ember.inject.controller('vault'),
|
||||
queryParams: [{ authMethod: 'with' }],
|
||||
wrappedToken: Ember.computed.alias('vaultController.wrappedToken'),
|
||||
authMethod: '',
|
||||
redirectTo: null,
|
||||
});
|
||||
|
|
|
@ -3,11 +3,11 @@ import Ember from 'ember';
|
|||
const { Helper, inject } = Ember;
|
||||
|
||||
export default Helper.extend({
|
||||
routing: inject.service('-routing'),
|
||||
router: inject.service(),
|
||||
|
||||
compute([routeName, ...models], { replace = false }) {
|
||||
return () => {
|
||||
const router = this.get('routing.router');
|
||||
const router = this.get('router');
|
||||
const method = replace ? router.replaceWith : router.transitionTo;
|
||||
return method.call(router, routeName, ...models);
|
||||
};
|
||||
|
|
|
@ -44,7 +44,10 @@ export default DS.Model.extend({
|
|||
}),
|
||||
|
||||
tuneAttrs: computed(function() {
|
||||
return expandAttributeMeta(this, ['description', 'config.{defaultLeaseTtl,maxLeaseTtl}']);
|
||||
return expandAttributeMeta(this, [
|
||||
'description',
|
||||
'config.{listingVisibility,defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}',
|
||||
]);
|
||||
}),
|
||||
|
||||
//sys/mounts/auth/[auth-path]/tune.
|
||||
|
@ -61,12 +64,20 @@ export default DS.Model.extend({
|
|||
'accessor',
|
||||
'local',
|
||||
'sealWrap',
|
||||
'config.{defaultLeaseTtl,maxLeaseTtl}',
|
||||
'config.{listingVisibility,defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}',
|
||||
],
|
||||
|
||||
formFieldGroups: [
|
||||
{ default: ['type', 'path'] },
|
||||
{ 'Method Options': ['description', 'local', 'sealWrap', 'config.{defaultLeaseTtl,maxLeaseTtl}'] },
|
||||
{
|
||||
'Method Options': [
|
||||
'description',
|
||||
'config.listingVisibility',
|
||||
'local',
|
||||
'sealWrap',
|
||||
'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
attrs: computed('formFields', function() {
|
||||
|
|
|
@ -10,4 +10,25 @@ export default Fragment.extend({
|
|||
label: 'Max Lease TTL',
|
||||
editType: 'ttl',
|
||||
}),
|
||||
auditNonHmacRequestKeys: attr({
|
||||
label: 'Request keys excluded from HMACing in audit',
|
||||
editType: 'stringArray',
|
||||
helpText: "Keys that will not be HMAC'd by audit devices in the request data object.",
|
||||
}),
|
||||
auditNonHmacResponseKeys: attr({
|
||||
label: 'Response keys excluded from HMACing in audit',
|
||||
editType: 'stringArray',
|
||||
helpText: "Keys that will not be HMAC'd by audit devices in the response data object.",
|
||||
}),
|
||||
listingVisibility: attr('string', {
|
||||
editType: 'boolean',
|
||||
label: 'List method when unauthenticated',
|
||||
trueValue: 'unauth',
|
||||
falseValue: 'hidden',
|
||||
}),
|
||||
passthroughRequestHeaders: attr({
|
||||
label: 'Allowed passthrough request headers',
|
||||
helpText: 'Headers to whitelist and pass from the request to the backend',
|
||||
editType: 'stringArray',
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1 +1,29 @@
|
|||
export { default } from './cluster-route-base';
|
||||
import ClusterRouteBase from './cluster-route-base';
|
||||
import Ember from 'ember';
|
||||
|
||||
const { RSVP } = Ember;
|
||||
|
||||
export default ClusterRouteBase.extend({
|
||||
beforeModel() {
|
||||
return this.store.unloadAll('auth-method');
|
||||
},
|
||||
model() {
|
||||
let cluster = this._super(...arguments);
|
||||
return this.store
|
||||
.findAll('auth-method', {
|
||||
adapterOptions: {
|
||||
unauthenticated: true,
|
||||
},
|
||||
})
|
||||
.then(result => {
|
||||
return RSVP.hash({
|
||||
cluster,
|
||||
methods: result,
|
||||
});
|
||||
});
|
||||
},
|
||||
resetController(controller) {
|
||||
controller.set('wrappedToken', '');
|
||||
controller.set('authMethod', '');
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,10 +2,7 @@ import ApplicationSerializer from './application';
|
|||
|
||||
export default ApplicationSerializer.extend({
|
||||
normalizeBackend(path, backend) {
|
||||
let struct = {};
|
||||
for (let attribute in backend) {
|
||||
struct[attribute] = backend[attribute];
|
||||
}
|
||||
let struct = { ...backend };
|
||||
// strip the trailing slash off of the path so we
|
||||
// can navigate to it without getting `//` in the url
|
||||
struct.id = path.slice(0, -1);
|
||||
|
@ -17,7 +14,7 @@ export default ApplicationSerializer.extend({
|
|||
const isCreate = requestType === 'createRecord';
|
||||
const backends = isCreate
|
||||
? payload.data
|
||||
: Object.keys(payload.data).map(id => this.normalizeBackend(id, payload[id]));
|
||||
: Object.keys(payload.data).map(path => this.normalizeBackend(path, payload.data[path]));
|
||||
|
||||
return this._super(store, primaryModelClass, backends, id, requestType);
|
||||
},
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
background: none;
|
||||
color: inherit;
|
||||
font-size: $body-size;
|
||||
min-height: 2rem;
|
||||
|
||||
&:not(.console-ui-command):not(.CodeMirror-line) {
|
||||
padding-left: $console-spacing;
|
||||
|
@ -57,6 +58,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.console-ui-panel .hover-copy-button,
|
||||
.console-ui-panel .hover-copy-button-static {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.console-ui-input {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
@ -82,25 +90,13 @@
|
|||
}
|
||||
|
||||
.console-ui-output {
|
||||
transition: background-color $speed;
|
||||
transition: background-color $speed ease-in-out;
|
||||
will-change: background-color;
|
||||
padding-right: $size-2;
|
||||
position: relative;
|
||||
|
||||
.console-ui-output-actions {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: opacity $speed;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
background-color: rgba(#000, 0);
|
||||
&:hover {
|
||||
background: rgba($black, 0.25);
|
||||
|
||||
.console-ui-output-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
background-color: rgba(#000, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,29 @@
|
|||
<nav class="tabs sub-nav is-marginless">
|
||||
<ul>
|
||||
{{#each (supported-auth-backends) as |backend|}}
|
||||
<li class="{{if (eq selectedAuthBackend.type backend.type) 'is-active' ''}}" data-test-auth-method>
|
||||
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=backend.type)}}" data-test-auth-method-link={{backend.type}}>
|
||||
{{capitalize backend.type}}
|
||||
{{#each methodsToShow as |method|}}
|
||||
{{#with (or method.path method.type) as |methodKey|}}
|
||||
{{#if hasMethodsWithPath}}
|
||||
<li class="{{if (and selectedAuthIsPath (eq (or selectedAuthBackend.path selectedAuthBackend.type) methodKey)) 'is-active' ''}}" data-test-auth-method>
|
||||
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=methodKey)}}" data-test-auth-method-link={{method.type}}>
|
||||
{{or method.id (capitalize method.type)}}
|
||||
</a>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="{{if (eq (or selectedAuthBackend.path selectedAuthBackend.type) methodKey) 'is-active' ''}}" data-test-auth-method>
|
||||
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=methodKey)}}" data-test-auth-method-link={{method.type}}>
|
||||
{{or method.id (capitalize method.type)}}
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
{{#if hasMethodsWithPath}}
|
||||
<li class="{{if (not selectedAuthIsPath) 'is-active' ''}}" data-test-auth-method>
|
||||
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with='token')}}" data-test-auth-method-link=other>
|
||||
Other
|
||||
</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
<form
|
||||
|
@ -19,8 +36,31 @@
|
|||
{{else}}
|
||||
{{message-error errorMessage=error data-test-auth-error=true}}
|
||||
{{/if}}
|
||||
{{#if (and hasMethodsWithPath (not selectedAuthIsPath))}}
|
||||
<div class="field">
|
||||
<label for="selectedMethod" class="is-label">
|
||||
Method
|
||||
</label>
|
||||
<div class="control is-expanded" >
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="selectedMethod"
|
||||
id="selectedMethod"
|
||||
onchange={{action (mut selectedAuth) value="target.value"}}
|
||||
data-test-method-select
|
||||
>
|
||||
{{#each (supported-auth-backends) as |method|}}
|
||||
<option selected={{eq selectedAuthBackend.type method.type}} value={{method.type}}>
|
||||
{{capitalize method.type}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{partial providerPartialName}}
|
||||
{{#unless (eq selectedAuthBackend.type "token")}}
|
||||
{{#unless (or selectedAuthIsPath (eq selectedAuthBackend.type "token"))}}
|
||||
<div class="box has-slim-padding is-shadowless">
|
||||
{{toggle-button toggleTarget=this toggleAttr="useCustomPath"}}
|
||||
<div class="field">
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
{{json-editor
|
||||
value=(stringify content)
|
||||
options=(hash
|
||||
readOnly=true
|
||||
lineNumbers=false
|
||||
autoHeight=true
|
||||
gutters=false
|
||||
theme='hashi auto-height'
|
||||
)
|
||||
}}
|
||||
<div class="console-ui-output has-copy-button">
|
||||
{{json-editor
|
||||
value=(stringify content)
|
||||
options=(hash
|
||||
readOnly=true
|
||||
lineNumbers=false
|
||||
autoHeight=true
|
||||
gutters=false
|
||||
theme='hashi auto-height'
|
||||
)
|
||||
}}
|
||||
<HoverCopyButton @copyValue={{stringify content}} />
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,8 @@
|
|||
<div class="console-ui-output">
|
||||
<div class="console-ui-output has-copy-button">
|
||||
<pre>Keys
|
||||
{{#each list as |item|}}
|
||||
{{item}}
|
||||
{{/each}}
|
||||
</pre>
|
||||
<div class="console-ui-output-actions">
|
||||
{{#tool-tip renderInPlace=true as |d|}}
|
||||
{{#d.trigger data-test-tool-tip-trigger=true}}
|
||||
{{#copy-button clipboardText=(multi-line-join list) class="button is-compact"}}
|
||||
{{i-con glyph="copy" aria-hidden="true" size=16}}
|
||||
{{/copy-button}}
|
||||
{{/d.trigger}}
|
||||
{{#d.content class="tool-tip"}}
|
||||
<div class="box">
|
||||
Copy
|
||||
</div>
|
||||
{{/d.content}}
|
||||
{{/tool-tip}}
|
||||
</div>
|
||||
<HoverCopyButton @copyValue={{multi-line-join list}} />
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,4 @@
|
|||
<div class="console-ui-output">
|
||||
<pre>{{columns}}</pre>
|
||||
|
||||
<div class="console-ui-output-actions">
|
||||
{{#tool-tip renderInPlace=true as |d|}}
|
||||
{{#d.trigger data-test-tool-tip-trigger=true}}
|
||||
{{#copy-button clipboardText=columns class="button is-compact"}}
|
||||
{{i-con glyph="copy" aria-hidden="true" size=16}}
|
||||
{{/copy-button}}
|
||||
{{/d.trigger}}
|
||||
{{#d.content class="tool-tip"}}
|
||||
<div class="box">
|
||||
Copy
|
||||
</div>
|
||||
{{/d.content}}
|
||||
{{/tool-tip}}
|
||||
</div>
|
||||
<div class="console-ui-output has-copy-button">
|
||||
<pre>{{columns}}</pre>
|
||||
<HoverCopyButton @copyValue={{columns}} />
|
||||
</div>
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
<pre>{{content}}</pre>
|
||||
<div class="console-ui-output has-copy-button">
|
||||
<pre>{{content}}</pre>
|
||||
<HoverCopyButton @copyValue={{content}} @alwaysShow=true />
|
||||
</div>
|
||||
|
|
|
@ -27,6 +27,25 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{else if (and (eq attr.type 'string') (eq attr.options.editType 'boolean'))}}
|
||||
<div class="b-checkbox">
|
||||
<input type="checkbox"
|
||||
id="{{attr.name}}"
|
||||
class="styled"
|
||||
checked={{eq (get model valuePath) attr.options.trueValue}}
|
||||
onchange={{action (action "setAndBroadcastBool" valuePath attr.options.trueValue attr.options.falseValue) value="target.checked"}}
|
||||
data-test-input={{attr.name}}
|
||||
/>
|
||||
<label for="{{attr.name}}" class="is-label">
|
||||
{{labelString}}
|
||||
{{#if attr.options.helpText}}
|
||||
{{#info-tooltip}}
|
||||
{{attr.options.helpText}}
|
||||
{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{else if (eq attr.options.editType 'mountAccessor')}}
|
||||
{{mount-accessor-select
|
||||
name=attr.name
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<ToolTip @renderInPlace={{true}} @onClose={{action (mut tooltipText) "Copy"}} as |T|>
|
||||
<ToolTip @onClose={{action (mut tooltipText) "Copy"}} as |T|>
|
||||
<T.trigger data-test-tooltip-trigger tabindex=false>
|
||||
<CopyButton
|
||||
data-test-hover-copy-button
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{#tool-tip renderInPlace=true as |d|}}
|
||||
{{#tool-tip as |d|}}
|
||||
{{#d.trigger tagName="button" type="button" class=(concat "tool-tip-trigger button") data-test-tool-tip-trigger=true}}
|
||||
{{i-con
|
||||
glyph="information-reversed"
|
||||
|
|
|
@ -13,11 +13,10 @@
|
|||
</PageHeader>
|
||||
|
||||
{{#each (sort-by "path" model) as |method|}}
|
||||
{{#linked-block
|
||||
"vault.cluster.access.methods"
|
||||
class="box is-sideless is-marginless has-pointer "
|
||||
data-test-auth-backend-link=method.id
|
||||
}}
|
||||
<div
|
||||
class="box is-sideless is-marginless"
|
||||
data-test-auth-backend-link={{method.id}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
|
@ -76,5 +75,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/linked-block}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
{{!-- {{i-con glyph="unlocked" size=20}} {{capitalize model.name}} is {{if model.unsealed 'unsealed' 'sealed'}} --}}
|
||||
<SplashPage as |Page|>
|
||||
<Page.header>
|
||||
<h1 class="title is-3">
|
||||
|
@ -7,10 +6,12 @@
|
|||
</Page.header>
|
||||
<Page.content>
|
||||
<AuthForm
|
||||
@cluster={{model}}
|
||||
@wrappedToken={{wrappedToken}}
|
||||
@cluster={{model.cluster}}
|
||||
@methods={{model.methods}}
|
||||
@redirectTo={{redirectTo}}
|
||||
@selectedAuthType={{with}}
|
||||
/>
|
||||
@selectedAuth={{authMethod}}
|
||||
/>
|
||||
</Page.content>
|
||||
<Page.footer>
|
||||
<div class="has-short-padding">
|
||||
|
|
|
@ -21,7 +21,7 @@ test('auth query params', function(assert) {
|
|||
const backends = supportedAuthBackends();
|
||||
visit('/vault/auth');
|
||||
andThen(() => {
|
||||
assert.equal(currentURL(), '/vault/auth');
|
||||
assert.equal(currentURL(), '/vault/auth?with=token');
|
||||
});
|
||||
backends.reverse().forEach(backend => {
|
||||
click(`[data-test-auth-method-link="${backend.type}"]`);
|
||||
|
@ -38,7 +38,7 @@ test('auth query params', function(assert) {
|
|||
test('it clears token when changing selected auth method', function(assert) {
|
||||
visit('/vault/auth');
|
||||
andThen(() => {
|
||||
assert.equal(currentURL(), '/vault/auth');
|
||||
assert.equal(currentURL(), '/vault/auth?with=token');
|
||||
});
|
||||
component.token('token').tabs.filterBy('name', 'GitHub')[0].link();
|
||||
component.tabs.filterBy('name', 'Token')[0].link();
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { moduleForComponent, test } from 'ember-qunit';
|
||||
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
|
||||
import Ember from 'ember';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
import sinon from 'sinon';
|
||||
import Pretender from 'pretender';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import authForm from '../../pages/components/auth-form';
|
||||
|
||||
const component = create(authForm);
|
||||
const BACKENDS = supportedAuthBackends();
|
||||
|
||||
const authService = Ember.Service.extend({
|
||||
authenticate() {
|
||||
|
@ -15,11 +17,28 @@ const authService = Ember.Service.extend({
|
|||
},
|
||||
});
|
||||
|
||||
const workingAuthService = Ember.Service.extend({
|
||||
authenticate() {
|
||||
return Ember.RSVP.resolve({});
|
||||
},
|
||||
setLastFetch() {},
|
||||
});
|
||||
|
||||
const routerService = Ember.Service.extend({
|
||||
transitionTo() {
|
||||
return Ember.RSVP.resolve();
|
||||
},
|
||||
replaceWith() {
|
||||
return Ember.RSVP.resolve();
|
||||
},
|
||||
});
|
||||
moduleForComponent('auth-form', 'Integration | Component | auth form', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
Ember.getOwner(this).lookup('service:csp-event').attach();
|
||||
component.setContext(this);
|
||||
this.register('service:router', routerService);
|
||||
this.inject.service('router');
|
||||
},
|
||||
|
||||
afterEach() {
|
||||
|
@ -33,7 +52,8 @@ test('it renders error on CSP violation', function(assert) {
|
|||
this.register('service:auth', authService);
|
||||
this.inject.service('auth');
|
||||
this.set('cluster', Ember.Object.create({ standby: true }));
|
||||
this.render(hbs`{{auth-form cluster=cluster}}`);
|
||||
this.set('selectedAuth', 'token');
|
||||
this.render(hbs`{{auth-form cluster=cluster selectedAuth=selectedAuth}}`);
|
||||
assert.equal(component.errorText, '');
|
||||
component.login();
|
||||
// because this is an ember-concurrency backed service,
|
||||
|
@ -58,7 +78,8 @@ test('it renders with vault style errors', function(assert) {
|
|||
});
|
||||
|
||||
this.set('cluster', Ember.Object.create({}));
|
||||
this.render(hbs`{{auth-form cluster=cluster}}`);
|
||||
this.set('selectedAuth', 'token');
|
||||
this.render(hbs`{{auth-form cluster=cluster selectedAuth=selectedAuth}}`);
|
||||
return component.login().then(() => {
|
||||
assert.equal(component.errorText, 'Error Authentication failed: Not allowed');
|
||||
server.shutdown();
|
||||
|
@ -73,9 +94,113 @@ test('it renders AdapterError style errors', function(assert) {
|
|||
});
|
||||
|
||||
this.set('cluster', Ember.Object.create({}));
|
||||
this.render(hbs`{{auth-form cluster=cluster}}`);
|
||||
this.set('selectedAuth', 'token');
|
||||
this.render(hbs`{{auth-form cluster=cluster selectedAuth=selectedAuth}}`);
|
||||
return component.login().then(() => {
|
||||
assert.equal(component.errorText, 'Error Authentication failed: Bad Request');
|
||||
server.shutdown();
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders all the supported tabs when no methods are passed', function(assert) {
|
||||
this.render(hbs`{{auth-form cluster=cluster}}`);
|
||||
assert.equal(component.tabs.length, BACKENDS.length, 'renders a tab for every backend');
|
||||
});
|
||||
|
||||
test('it renders all the supported methods and Other tab when methods are present', function(assert) {
|
||||
let methods = [
|
||||
{
|
||||
type: 'userpass',
|
||||
id: 'foo',
|
||||
path: 'foo/',
|
||||
},
|
||||
{
|
||||
type: 'approle',
|
||||
id: 'approle',
|
||||
path: 'approle/',
|
||||
},
|
||||
];
|
||||
this.set('methods', methods);
|
||||
|
||||
this.render(hbs`{{auth-form cluster=cluster methods=methods}}`);
|
||||
assert.equal(component.tabs.length, 2, 'renders a tab for userpass and Other');
|
||||
assert.equal(component.tabs.objectAt(0).name, 'foo', 'uses the path in the label');
|
||||
assert.equal(component.tabs.objectAt(1).name, 'Other', 'second tab is the Other tab');
|
||||
});
|
||||
|
||||
test('it renders all the supported methods when no supported methods are present in passed methods', function(
|
||||
assert
|
||||
) {
|
||||
let methods = [
|
||||
{
|
||||
type: 'approle',
|
||||
id: 'approle',
|
||||
path: 'approle/',
|
||||
},
|
||||
];
|
||||
this.set('methods', methods);
|
||||
this.render(hbs`{{auth-form cluster=cluster methods=methods}}`);
|
||||
assert.equal(component.tabs.length, BACKENDS.length, 'renders a tab for every backend');
|
||||
});
|
||||
|
||||
test('it makes a request to unwrap if passed a wrappedToken and logs in', function(assert) {
|
||||
this.register('service:auth', workingAuthService);
|
||||
this.inject.service('auth');
|
||||
let authSpy = sinon.spy(this.get('auth'), 'authenticate');
|
||||
let server = new Pretender(function() {
|
||||
this.post('/v1/sys/wrapping/unwrap', () => {
|
||||
return [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({
|
||||
auth: {
|
||||
client_token: '12345',
|
||||
},
|
||||
}),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
let wrappedToken = '54321';
|
||||
this.set('wrappedToken', wrappedToken);
|
||||
this.render(hbs`{{auth-form cluster=cluster wrappedToken=wrappedToken}}`);
|
||||
Ember.run.later(() => Ember.run.cancelTimers(), 50);
|
||||
return wait().then(() => {
|
||||
assert.equal(server.handledRequests[0].url, '/v1/sys/wrapping/unwrap', 'makes call to unwrap the token');
|
||||
assert.equal(
|
||||
server.handledRequests[0].requestHeaders['X-Vault-Token'],
|
||||
wrappedToken,
|
||||
'uses passed wrapped token for the unwrap'
|
||||
);
|
||||
assert.ok(authSpy.calledOnce, 'a call to authenticate was made');
|
||||
server.shutdown();
|
||||
authSpy.restore();
|
||||
});
|
||||
});
|
||||
|
||||
test('it shows an error if unwrap errors', function(assert) {
|
||||
let server = new Pretender(function() {
|
||||
this.post('/v1/sys/wrapping/unwrap', () => {
|
||||
return [
|
||||
400,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({
|
||||
errors: ['There was an error unwrapping!'],
|
||||
}),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
this.set('wrappedToken', '54321');
|
||||
this.render(hbs`{{auth-form cluster=cluster wrappedToken=wrappedToken}}`);
|
||||
Ember.run.later(() => Ember.run.cancelTimers(), 50);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.equal(
|
||||
component.errorText,
|
||||
'Error Token unwrap failed: There was an error unwrapping!',
|
||||
'shows the error'
|
||||
);
|
||||
server.shutdown();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,27 +14,28 @@ test('wrapping api urls', function(assert) {
|
|||
},
|
||||
});
|
||||
|
||||
let clientToken;
|
||||
let data = { foo: 'bar' };
|
||||
adapter.toolAction('wrap', data, { wrapTTL: '30m' });
|
||||
assert.equal('/v1/sys/wrapping/wrap', url, 'wrapping:wrap url OK');
|
||||
assert.equal('POST', method, 'wrapping:wrap method OK');
|
||||
assert.deepEqual({ data: data, wrapTTL: '30m' }, options, 'wrapping:wrap options OK');
|
||||
assert.deepEqual({ data: data, wrapTTL: '30m', clientToken }, options, 'wrapping:wrap options OK');
|
||||
|
||||
data = { token: 'token' };
|
||||
adapter.toolAction('lookup', data);
|
||||
assert.equal('/v1/sys/wrapping/lookup', url, 'wrapping:lookup url OK');
|
||||
assert.equal('POST', method, 'wrapping:lookup method OK');
|
||||
assert.deepEqual({ data }, options, 'wrapping:lookup options OK');
|
||||
assert.deepEqual({ data, clientToken }, options, 'wrapping:lookup options OK');
|
||||
|
||||
adapter.toolAction('unwrap', data);
|
||||
assert.equal('/v1/sys/wrapping/unwrap', url, 'wrapping:unwrap url OK');
|
||||
assert.equal('POST', method, 'wrapping:unwrap method OK');
|
||||
assert.deepEqual({ data }, options, 'wrapping:unwrap options OK');
|
||||
assert.deepEqual({ data, clientToken }, options, 'wrapping:unwrap options OK');
|
||||
|
||||
adapter.toolAction('rewrap', data);
|
||||
assert.equal('/v1/sys/wrapping/rewrap', url, 'wrapping:rewrap url OK');
|
||||
assert.equal('POST', method, 'wrapping:rewrap method OK');
|
||||
assert.deepEqual({ data }, options, 'wrapping:rewrap options OK');
|
||||
assert.deepEqual({ data, clientToken }, options, 'wrapping:rewrap options OK');
|
||||
});
|
||||
|
||||
test('tools api urls', function(assert) {
|
||||
|
|
Loading…
Reference in New Issue