mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Fix: Canvas socket auth (#24094)
## Summary Closes https://github.com/elastic/kibana/issues/23303 ~(@cqliu1 can you confirm this too?)~ confirmed Fixes the way we capture the request info when configuring the socket and providing it to plugins via `callWithRequest`. Instead of exposing a route that returns the info, simply use the request object that comes back from `server.inject`. Also adds a check in the `elasticsearchClient` handler exposed to plugins to ensure the session is still valid because using `callWithRequest`.  *Note:* the actual error message is a bit different, but this is how the failure is exposed to the user
This commit is contained in:
parent
ee335d86bb
commit
5009efde13
7 changed files with 148 additions and 78 deletions
103
x-pack/plugins/canvas/server/lib/__tests__/create_handlers.js
Normal file
103
x-pack/plugins/canvas/server/lib/__tests__/create_handlers.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import { createHandlers } from '../create_handlers';
|
||||
|
||||
let securityMode = 'pass';
|
||||
const authError = new Error('auth error');
|
||||
|
||||
const mockRequest = {
|
||||
headers: 'i can haz headers',
|
||||
};
|
||||
|
||||
const mockServer = {
|
||||
plugins: {
|
||||
security: {
|
||||
authenticate: () => ({
|
||||
succeeded: () => (securityMode === 'pass' ? true : false),
|
||||
error: securityMode === 'pass' ? null : authError,
|
||||
}),
|
||||
},
|
||||
elasticsearch: {
|
||||
getCluster: () => ({
|
||||
callWithRequest: (...args) => Promise.resolve(args),
|
||||
}),
|
||||
},
|
||||
},
|
||||
config: () => ({
|
||||
has: () => false,
|
||||
get: val => val,
|
||||
}),
|
||||
info: {
|
||||
uri: 'serveruri',
|
||||
},
|
||||
};
|
||||
|
||||
describe('server createHandlers', () => {
|
||||
let handlers;
|
||||
|
||||
beforeEach(() => {
|
||||
securityMode = 'pass';
|
||||
handlers = createHandlers(mockRequest, mockServer);
|
||||
});
|
||||
|
||||
it('provides helper methods and properties', () => {
|
||||
expect(handlers).to.have.property('environment', 'server');
|
||||
expect(handlers).to.have.property('serverUri');
|
||||
expect(handlers).to.have.property('httpHeaders', mockRequest.headers);
|
||||
expect(handlers).to.have.property('elasticsearchClient');
|
||||
});
|
||||
|
||||
describe('elasticsearchClient', () => {
|
||||
it('executes callWithRequest', async () => {
|
||||
const [request, endpoint, payload] = await handlers.elasticsearchClient(
|
||||
'endpoint',
|
||||
'payload'
|
||||
);
|
||||
expect(request).to.equal(mockRequest);
|
||||
expect(endpoint).to.equal('endpoint');
|
||||
expect(payload).to.equal('payload');
|
||||
});
|
||||
|
||||
it('rejects when authentication check fails', () => {
|
||||
securityMode = 'fail';
|
||||
return handlers
|
||||
.elasticsearchClient('endpoint', 'payload')
|
||||
.then(() => {
|
||||
throw new Error('elasticsearchClient should fail when authentication fails');
|
||||
})
|
||||
.catch(err => {
|
||||
// note: boom pre-pends error messages with "Error: "
|
||||
expect(err.message).to.be.equal(`Error: ${authError.message}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('works without security', async () => {
|
||||
// create server without security plugin
|
||||
const mockServerClone = {
|
||||
...mockServer,
|
||||
plugins: { ...mockServer.plugins },
|
||||
};
|
||||
delete mockServerClone.plugins.security;
|
||||
expect(mockServer.plugins).to.have.property('security'); // confirm original server object
|
||||
expect(mockServerClone.plugins).to.not.have.property('security');
|
||||
|
||||
// this shouldn't do anything
|
||||
securityMode = 'fail';
|
||||
|
||||
// make sure the method still works
|
||||
handlers = createHandlers(mockRequest, mockServerClone);
|
||||
const [request, endpoint, payload] = await handlers.elasticsearchClient(
|
||||
'endpoint',
|
||||
'payload'
|
||||
);
|
||||
expect(request).to.equal(mockRequest);
|
||||
expect(endpoint).to.equal('endpoint');
|
||||
expect(payload).to.equal('payload');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { partial } from 'lodash';
|
||||
import boom from 'boom';
|
||||
|
||||
export const createHandlers = (request, server) => {
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
|
||||
|
@ -17,6 +17,14 @@ export const createHandlers = (request, server) => {
|
|||
? `${server.info.uri}${config.get('server.basePath')}`
|
||||
: server.info.uri,
|
||||
httpHeaders: request.headers,
|
||||
elasticsearchClient: partial(callWithRequest, request),
|
||||
elasticsearchClient: async (...args) => {
|
||||
// check if the session is valid because continuing to use it
|
||||
if (server.plugins.security) {
|
||||
const authenticationResult = await server.plugins.security.authenticate(request);
|
||||
if (!authenticationResult.succeeded()) throw boom.unauthorized(authenticationResult.error);
|
||||
}
|
||||
|
||||
return callWithRequest(request, ...args);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,6 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid/v4';
|
||||
export function getRequest(server, { headers }) {
|
||||
const basePath = server.config().get('server.basePath') || '/';
|
||||
|
||||
export const insecureAuthRoute = `/api/canvas/ar-${uuid()}`;
|
||||
return server
|
||||
.inject({
|
||||
method: 'GET',
|
||||
url: basePath,
|
||||
headers,
|
||||
})
|
||||
.then(res => res.request);
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { insecureAuthRoute } from './insecure_auth_route';
|
||||
|
||||
// TODO: OMG. No. Need a better way of setting to this than our wacky route thing.
|
||||
export function getAuthHeader(request, server) {
|
||||
const basePath = server.config().get('server.basePath') || '';
|
||||
const fullPath = `${basePath}${insecureAuthRoute}`;
|
||||
|
||||
return server
|
||||
.inject({
|
||||
method: 'GET',
|
||||
url: fullPath,
|
||||
headers: request.headers,
|
||||
})
|
||||
.then(res => res.result);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { insecureAuthRoute } from './insecure_auth_route';
|
||||
|
||||
// TODO: Fix this first. This route returns decrypts the cookie and returns the basic auth header. It is used because
|
||||
// the pre-route hapi hook doesn't work on the socket and there are no exposed methods for doing the conversion from cookie
|
||||
// to auth header. We will need to add that to x-pack security
|
||||
// In theory this is pretty difficult to exploit, but not impossible.
|
||||
//
|
||||
export function getAuth(server) {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: insecureAuthRoute,
|
||||
handler: function(request, reply) {
|
||||
reply(request.headers.authorization);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -9,7 +9,6 @@ import { socketApi } from './socket';
|
|||
import { translate } from './translate';
|
||||
import { esFields } from './es_fields';
|
||||
import { esIndices } from './es_indices';
|
||||
import { getAuth } from './get_auth';
|
||||
import { plugins } from './plugins';
|
||||
|
||||
export function routes(server) {
|
||||
|
@ -18,6 +17,5 @@ export function routes(server) {
|
|||
translate(server);
|
||||
esFields(server);
|
||||
esIndices(server);
|
||||
getAuth(server);
|
||||
plugins(server);
|
||||
}
|
||||
|
|
|
@ -11,17 +11,12 @@ import { serializeProvider } from '../../common/lib/serialize';
|
|||
import { functionsRegistry } from '../../common/lib/functions_registry';
|
||||
import { typesRegistry } from '../../common/lib/types_registry';
|
||||
import { loadServerPlugins } from '../lib/load_server_plugins';
|
||||
import { getAuthHeader } from './get_auth/get_auth_header';
|
||||
import { getRequest } from '../lib/get_request';
|
||||
|
||||
export function socketApi(server) {
|
||||
const io = socket(server.listener, { path: '/socket.io' });
|
||||
|
||||
io.on('connection', socket => {
|
||||
// This is the HAPI request object
|
||||
const request = socket.handshake;
|
||||
|
||||
const authHeader = getAuthHeader(request, server);
|
||||
|
||||
// Create the function list
|
||||
socket.emit('getFunctionList');
|
||||
const getClientFunctions = new Promise(resolve => socket.once('functionList', resolve));
|
||||
|
@ -31,30 +26,31 @@ export function socketApi(server) {
|
|||
});
|
||||
|
||||
const handler = ({ ast, context, id }) => {
|
||||
Promise.all([getClientFunctions, authHeader]).then(([clientFunctions, authHeader]) => {
|
||||
if (server.plugins.security) request.headers.authorization = authHeader;
|
||||
|
||||
const types = typesRegistry.toJS();
|
||||
const interpret = socketInterpreterProvider({
|
||||
types,
|
||||
functions: functionsRegistry.toJS(),
|
||||
handlers: createHandlers(request, server),
|
||||
referableFunctions: clientFunctions,
|
||||
socket: socket,
|
||||
});
|
||||
|
||||
const { serialize, deserialize } = serializeProvider(types);
|
||||
return interpret(ast, deserialize(context))
|
||||
.then(value => {
|
||||
socket.emit(`resp:${id}`, { value: serialize(value) });
|
||||
})
|
||||
.catch(e => {
|
||||
socket.emit(`resp:${id}`, {
|
||||
error: e.message,
|
||||
stack: e.stack,
|
||||
});
|
||||
Promise.all([getClientFunctions, getRequest(server, socket.handshake)]).then(
|
||||
([clientFunctions, request]) => {
|
||||
// request is the modified hapi request object
|
||||
const types = typesRegistry.toJS();
|
||||
const interpret = socketInterpreterProvider({
|
||||
types,
|
||||
functions: functionsRegistry.toJS(),
|
||||
handlers: createHandlers(request, server),
|
||||
referableFunctions: clientFunctions,
|
||||
socket: socket,
|
||||
});
|
||||
});
|
||||
|
||||
const { serialize, deserialize } = serializeProvider(types);
|
||||
return interpret(ast, deserialize(context))
|
||||
.then(value => {
|
||||
socket.emit(`resp:${id}`, { value: serialize(value) });
|
||||
})
|
||||
.catch(e => {
|
||||
socket.emit(`resp:${id}`, {
|
||||
error: e.message,
|
||||
stack: e.stack,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
socket.on('run', handler);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue