diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..cefd7b5 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,4 @@ +{ + "esversion": 6, + "esnext": true +} diff --git a/index.js b/index.js index 47c68fe..8a64363 100644 --- a/index.js +++ b/index.js @@ -12,11 +12,12 @@ * the License. */ -module.exports = (kibana) => { +import session from './server/session'; +import binding from './server/binding'; +import healthCheck from './server/healthcheck'; - const session = require('./server/session'); - const proxy = require('./server/proxy'); - const healthCheck = require('./server/healthcheck'); +export default (kibana) => { + const COOKIE_PASSWORD_SIZE = 32; return new kibana.Plugin({ require: ['elasticsearch'], @@ -27,17 +28,19 @@ module.exports = (kibana) => { function config(Joi) { const cookie = Joi.object({ + name : Joi.string() + .default('keystone'), password : Joi.string() - .min(16) - .default(require('crypto').randomBytes(16).toString('hex')), + .min(COOKIE_PASSWORD_SIZE) + .default(require('crypto').randomBytes(COOKIE_PASSWORD_SIZE).toString('hex')), isSecure : Joi.boolean() - .default(false), + .default(process.env.NODE_ENV !== 'development'), ignoreErrors: Joi.boolean() .default(true), expiresIn : Joi.number() .positive() .integer() - .default(24 * 60 * 60 * 1000) // 1 day + .default(60 * 60 * 1000) // 1 hour }).default(); return Joi.object({ @@ -51,9 +54,11 @@ module.exports = (kibana) => { } function init(server) { - session(server); - proxy(server); + server.log(['status', 'debug', 'keystone'], 'Initializing keystone plugin'); + binding(server).start(); + session(server).start(); healthCheck(this, server).start(); + server.log(['status', 'debug', 'keystone'], 'Initialized keystone plugin'); } }; diff --git a/package.json b/package.json index a7af291..20b490b 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "fts-keystone", - "version": "0.0.1", - "description": "Keystone authentication support for Kibana 4.4.x", + "version": "0.0.2", + "description": "Keystone authentication & multitenancy support for Kibana 4.4.x", "author": "Fujitsu Enabling Software Technology GmbH", - "licenses": "Apache-2.0", + "license": "Apache-2.0", "keywords": [ "kibana", "authentication", "keystone", + "multitenancy", "plugin" ], "scripts": { @@ -22,8 +23,9 @@ }, "main": "gulpfile.js", "dependencies": { - "yar": "^4.2.0", - "keystone-v3-client": "^0.0.7" + "hoek": "^4.0.1", + "keystone-v3-client": "^0.0.7", + "yar": "^7.x.x" }, "repository": { "type": "git", @@ -43,11 +45,14 @@ "gulp-mocha": "^2.2.0", "gulp-tar": "^1.8.0", "gulp-util": "^3.0.7", + "joi": "^9.0.4", "lodash": "^4.2.1", "mkdirp": "^0.5.1", "proxyquire": "^1.7.4", "rimraf": "^2.5.1", "rsync": "^0.4.0", - "sinon": "^1.17.3" + "semver": "^5.3.0", + "sinon": "^1.17.3", + "wreck": "^8.0.0" } } diff --git a/server/__tests__/binding.spec.js b/server/__tests__/binding.spec.js new file mode 100644 index 0000000..1c752e6 --- /dev/null +++ b/server/__tests__/binding.spec.js @@ -0,0 +1,45 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +const sinon = require('sinon'); +const chai = require('chai'); +const proxyRequire = require('proxyquire'); + +describe('fts-keystone', () => { + describe('binding', () => { + + it('should expose tokens & users', () => { + + let tokens = sinon.spy(); + let users = sinon.spy(); + + let server = { + config: sinon.stub().returns({ + get: sinon.spy() + }), + expose: sinon.spy() + }; + + proxyRequire('../binding', { + 'keystone-v3-client/lib/keystone/tokens': tokens, + 'keystone-v3-client/lib/keystone/users' : users + })(server).start(); + + chai.expect(server.expose.callCount).to.be.eq(2); + chai.expect(server.expose.calledWith('tokens', tokens)); + chai.expect(server.expose.calledWith('users', users)); + + }); + }); +}); diff --git a/server/__tests__/healthcheck.spec.js b/server/__tests__/healthcheck.spec.js index 1da9106..da44619 100644 --- a/server/__tests__/healthcheck.spec.js +++ b/server/__tests__/healthcheck.spec.js @@ -52,6 +52,7 @@ describe('plugins/fts-keystone', ()=> { server = { log : sinon.stub(), + on : sinon.stub(), config: function () { return { get: configGet @@ -63,6 +64,7 @@ describe('plugins/fts-keystone', ()=> { it('should set status to green if keystone available', (done)=> { let expectedCode = 200; + let expectedStatus = true; let healthcheck = proxyRequire('../healthcheck', { 'http': { request: (_, callback)=> { @@ -82,8 +84,8 @@ describe('plugins/fts-keystone', ()=> { check .run() - .then((code) => { - chai.expect(expectedCode).to.be.equal(code); + .then((status) => { + chai.expect(expectedStatus).to.be.equal(status); chai.expect(plugin.status.green.calledWith('Ready')).to.be.ok; }) .finally(done); @@ -92,6 +94,7 @@ describe('plugins/fts-keystone', ()=> { it('should set status to red if keystone not available', (done) => { let expectedCode = 500; + let expectedStatus = false; let healthcheck = proxyRequire('../healthcheck', { 'http': { request: (_, callback)=> { @@ -111,8 +114,8 @@ describe('plugins/fts-keystone', ()=> { check .run() - .catch((code) => { - chai.expect(expectedCode).to.be.equal(code); + .catch((status) => { + chai.expect(expectedStatus).to.be.equal(status); chai.expect(plugin.status.red.calledWith('Unavailable')).to.be.ok; }) .finally(done); diff --git a/server/__tests__/proxy.spec.js b/server/__tests__/proxy.spec.js deleted file mode 100644 index ef07373..0000000 --- a/server/__tests__/proxy.spec.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2016 FUJITSU LIMITED - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -const proxyRequire = require('proxyquire'); -const Promise = require('bluebird'); -const sinon = require('sinon'); -const chai = require('chai'); - -describe('plugins/fts-keystone', ()=> { - describe('proxy', ()=> { - describe('proxy_check', ()=> { - - const keystoneUrl = 'http://localhost'; // mocking http - const keystonePort = 9000; - - let server; - let configGet; - - beforeEach(()=> { - configGet = sinon.stub(); - configGet.withArgs('fts-keystone.url').returns(keystoneUrl); - configGet.withArgs('fts-keystone.port').returns(keystonePort); - - server = { - log : sinon.stub(), - config: function () { - return { - get: configGet - }; - } - }; - }); - - it('should do nothing if not /elasticsearch call', ()=> { - let checkSpy = sinon.spy(); - let retrieveTokenSpy = sinon.spy(); - let proxy = proxyRequire('../proxy/proxy', { - 'keystone-v3-client/lib/keystone/tokens': () => { - return {check: checkSpy}; - }, - './retrieveToken' : retrieveTokenSpy - })(server); - let request = { - url: { - path: '/bundles/styles.css' - } - }; - let reply = { - 'continue': sinon.spy() - }; - - proxy(request, reply); - - chai.expect(reply.continue.calledOnce).to.be.ok; - chai.expect(checkSpy.called).to.not.be.ok; - chai.expect(retrieveTokenSpy.called).to.not.be.ok; - }); - - it('should authenticate with keystone', (done)=> { - - let token = '1234567890'; - let checkStub = sinon.stub().returns(Promise.resolve()); - let retrieveTokenStub = sinon.stub().returns(token); - - let proxy = proxyRequire('../proxy/proxy', { - 'keystone-v3-client/lib/keystone/tokens': () => { - return {check: checkStub}; - }, - './retrieveToken' : retrieveTokenStub - })(server); - let request = { - session: { - 'get' : sinon.stub(), - 'set' : sinon.stub() - }, - url : { - path: '/elasticsearch/.kibana' - } - }; - - let reply = { - 'continue': sinon.spy() - }; - let replyCall; - - proxy(request, reply) - .finally(verifyStubs) - .done(done); - - function verifyStubs() { - chai.expect(reply.continue.calledOnce).to.be.ok; - replyCall = reply.continue.firstCall.args; - - chai.expect(replyCall).to.be.empty; - - // other stubs - chai.expect(checkStub.calledOnce).to.be.ok; - chai.expect(checkStub.calledWithExactly({ - headers: { - 'X-Auth-Token' : token, - 'X-Subject-Token': token - } - })).to.be.ok; - - chai.expect(retrieveTokenStub.calledOnce).to.be.ok; - chai.expect(retrieveTokenStub.calledWithExactly(server, request)) - .to.be.ok; - } - }); - - it('should not authenticate with keystone', (done)=> { - let token = '1234567890'; - let checkStub = sinon.stub().returns(Promise.reject({ - statusCode: 666 - })); - let retrieveTokenStub = sinon.stub().returns(token); - let proxy = proxyRequire('../proxy/proxy', { - 'keystone-v3-client/lib/keystone/tokens': () => { - return {check: checkStub}; - }, - './retrieveToken' : retrieveTokenStub - })(server); - let request = { - session: { - 'get' : sinon.stub(), - 'set' : sinon.stub() - }, - url : { - path: '/elasticsearch/.kibana' - } - }; - let reply = sinon.spy(); - let replyCall; - - proxy(request, reply) - .finally(verifyStubs) - .done(done); - - function verifyStubs() { - chai.expect(reply.calledOnce).to.be.ok; - replyCall = reply.firstCall.args[0]; - - chai.expect(replyCall.isBoom).to.be.ok; - - // other stubs - chai.expect(checkStub.calledOnce).to.be.ok; - chai.expect(checkStub.calledWithExactly({ - headers: { - 'X-Auth-Token' : token, - 'X-Subject-Token': token - } - })).to.be.ok; - - chai.expect(retrieveTokenStub.calledOnce).to.be.ok; - chai.expect(retrieveTokenStub.calledWithExactly(server, request)) - .to.be.ok; - } - - }); - - }); - }); -}); diff --git a/server/__tests__/retrieveToken.spec.js b/server/__tests__/retrieveToken.spec.js index 03bb6b8..dba95ff 100644 --- a/server/__tests__/retrieveToken.spec.js +++ b/server/__tests__/retrieveToken.spec.js @@ -15,157 +15,176 @@ const sinon = require('sinon'); const chai = require('chai'); -const retrieveToken = require('../proxy/retrieveToken'); +const retrieveToken = require('../mt/auth/token'); +const CONSTANTS = require('../const'); +const RELOAD_SYMBOL = require('../mt/auth/reload'); describe('plugins/fts-keystone', ()=> { - describe('proxy', ()=> { - describe('retrieveToken', ()=> { + describe('mt', ()=> { + describe('auth', () => { + describe('token', ()=> { - let server; + let server; - beforeEach(()=> { - server = { - log: sinon.stub() - }; - }); + beforeEach(()=> { + server = { + log: sinon.stub() + }; + }); - it('should return isBoom if session not available', ()=> { - let request = {}; - let errMsg = /Session support is missing/; + it('should return isBoom if session not available', ()=> { + let request = {}; + let errMsg = /Session support is missing/; - chai.expect(()=> { - retrieveToken(server, request); - }).to.throw(errMsg); + chai.expect(()=> { + retrieveToken(server, request); + }).to.throw(errMsg); - request = { - session: undefined - }; - chai.expect(()=> { - retrieveToken(server, request); - }).to.throw(errMsg); + request = { + yar: undefined + }; + chai.expect(()=> { + retrieveToken(server, request); + }).to.throw(errMsg); - request = { - session: null - }; - chai.expect(()=> { - retrieveToken(server, request); - }).to.throw(errMsg); - }); + request = { + session: null + }; + chai.expect(()=> { + retrieveToken(server, request); + }).to.throw(errMsg); + }); - it('should Boom with unauthorized if token not in header or session', function () { - let expectedMsg = 'You\'re not logged into the OpenStack. Please login via Horizon Dashboard'; - let request = { - session: { - 'get': sinon - .stub() - .withArgs('keystone_token') - .returns(undefined) - }, - headers: {} - }; + it('should Boom with unauthorized if token not in header or session', function () { + let expectedMsg = 'You\'re not logged into the OpenStack. Please login via Horizon Dashboard'; + let request = { + yar : { + 'get': sinon + .stub() + .withArgs(CONSTANTS.SESSION_TOKEN_KEY) + .returns(undefined) + }, + headers: {} + }; - let result = retrieveToken(server, request); - chai.expect(result.isBoom).to.be.true; - chai.expect(result.output.payload.message).to.be.eq(expectedMsg); - chai.expect(result.output.statusCode).to.be.eq(401); - }); + let result = retrieveToken(server, request); + chai.expect(result.isBoom).to.be.true; + chai.expect(result.output.payload.message).to.be.eq(expectedMsg); + chai.expect(result.output.statusCode).to.be.eq(401); + }); - it('should use session token if requested does not have it', () => { - let expectedToken = 'SOME_RANDOM_TOKEN'; - let yar = { - 'set': sinon - .spy(), - 'get': sinon - .stub() - .withArgs('keystone_token') - .returns(expectedToken) - }; - let request = { - session: yar, - headers: {} - }; - let token; + it('should use session token if requested does not have it', () => { + let expectedToken = 'SOME_RANDOM_TOKEN'; + let yar = { + 'reset': sinon.spy(), + 'set' : sinon.spy(), + 'get' : sinon.stub() + }; + let request = { + yar : yar, + headers: {} + }; + let token; - token = retrieveToken(server, request); - chai.expect(token).not.to.be.undefined; - chai.expect(token).to.be.eql(expectedToken); + yar.get.returns(expectedToken); - chai.expect( - yar.get.calledOnce - ).to.be.ok; - chai.expect( - yar.set.calledOnce - ).not.to.be.ok; - chai.expect( - yar.set.calledWithExactly('keystone_token', expectedToken) - ).not.to.be.ok; - }); + token = retrieveToken(server, request); + chai.expect(token).not.to.be.undefined; + chai.expect(token).to.be.eql(expectedToken); - it('should set token in session if not there and request has it', () => { - let expectedToken = 'SOME_RANDOM_TOKEN'; - let yar = { - 'set': sinon - .spy(), - 'get': sinon - .stub() - .withArgs('keystone_token') - .returns(undefined) - }; - let request = { - session: yar, - headers: { + console.log(yar.get.callCount); + + chai.expect(yar.get.callCount).to.be.eq(2); + chai.expect(yar.set.calledOnce).not.to.be.ok; + chai.expect( + yar.set.calledWithExactly(CONSTANTS.SESSION_TOKEN_KEY, expectedToken) + ).not.to.be.ok; + }); + + it('should set token in session if not there and request has it', () => { + let expectedToken = 'SOME_RANDOM_TOKEN'; + let yar = { + 'reset': sinon.spy(), + 'set' : sinon.spy(), + 'get' : sinon.stub() + }; + let request = { + yar : yar, + headers: { + 'x-auth-token': expectedToken + } + }; + let token; + + yar.get + .withArgs(CONSTANTS.SESSION_TOKEN_KEY) + .onCall(0).returns(undefined) + .onCall(1).returns(expectedToken); + + token = retrieveToken(server, request); + chai.expect(token).to.not.be.undefined; + chai.expect(token).to.be.eql(expectedToken); + + chai.expect(yar.get.callCount).to.be.eq(2); + chai.expect(yar.set.calledOnce).to.be.ok; + + chai.expect( + yar.set.calledWithExactly( + CONSTANTS.SESSION_TOKEN_KEY, + expectedToken + ) + ).to.be.ok; + chai.expect( + yar.set.calledWithExactly( + CONSTANTS.SESSION_TOKEN_CHANGED, + CONSTANTS.TOKEN_CHANGED_VALUE + ) + ).to.not.be.ok; + }); + + it('should update token in session if request\'s token is different', ()=> { + let expectedToken = 'SOME_RANDOM_TOKEN'; + let oldToken = 'OLD_TOKEN'; + + let headers = { 'x-auth-token': expectedToken - } - }; - let token; + }; + let yar = { + 'reset': sinon.stub(), + 'get' : sinon + .stub() + .withArgs(CONSTANTS.SESSION_TOKEN_KEY) + .returns(oldToken), + 'set' : sinon.spy() + }; + let token; + let request = { + yar : yar, + headers: headers + }; - token = retrieveToken(server, request); - chai.expect(token).to.not.be.undefined; - chai.expect(token).to.be.eql(expectedToken); + token = retrieveToken(server, request); + chai.expect(token).to.not.be.undefined; + chai.expect(token).to.be.eql(RELOAD_SYMBOL); - chai.expect( - yar.get.calledOnce - ).to.be.ok; - chai.expect( - yar.set.calledOnce - ).to.be.ok; - chai.expect( - yar.set.calledWithExactly('keystone_token', expectedToken) - ).to.be.ok; - }); + chai.expect(yar.reset.calledOnce).to.be.ok; + chai.expect(yar.get.calledOnce).to.be.ok; - it('should update token in session if request\'s token is different', ()=> { - let expectedToken = 'SOME_RANDOM_TOKEN'; - let headers = { - 'x-auth-token': expectedToken - }; - let yar = { - 'get': sinon - .stub() - .withArgs('keystone_token') - .returns('SOME_OLD_TOKEN'), - 'set': sinon - .spy() - }; - let token; - let request = { - session: yar, - headers: headers - }; + chai.expect(yar.set.callCount).to.be.eq(2); + chai.expect( + yar.set.calledWithExactly( + CONSTANTS.SESSION_TOKEN_KEY, + expectedToken + ) + ).to.be.ok; + chai.expect( + yar.set.calledWithExactly( + CONSTANTS.SESSION_TOKEN_CHANGED, + CONSTANTS.TOKEN_CHANGED_VALUE + ) + ).to.be.ok; - token = retrieveToken(server, request); - chai.expect(token).to.not.be.undefined; - chai.expect(token).to.be.eql(expectedToken); - - chai.expect( - yar.get.calledOnce - ).to.be.ok; - chai.expect( - yar.set.calledOnce - ).to.be.ok; - chai.expect( - yar.set.calledWithExactly('keystone_token', expectedToken) - ).to.be.ok; + }); }); }); }); diff --git a/server/__tests__/routing.createProxy.spec.js b/server/__tests__/routing.createProxy.spec.js new file mode 100644 index 0000000..9f3c95a --- /dev/null +++ b/server/__tests__/routing.createProxy.spec.js @@ -0,0 +1,132 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +const sinon = require('sinon'); +const chai = require('chai'); +const proxyRequire = require('proxyquire'); + +describe('plugins/fts-keystone', () => { + describe('mt', () => { + describe('routing', () => { + describe('createProxy', () => { + const kibanaIndex = '.kibana'; + const server = { + log : sinon.spy(), + config: sinon.stub().returns({ + get: sinon.stub().withArgs('kibana.index').returns(kibanaIndex) + }) + }; + + let mgetHandler; + let pathsHandler; + let kibanaIndexHandler; + let defaultHandler; + + beforeEach(() => { + mgetHandler = sinon.stub().returns({}); + pathsHandler = sinon.stub().returns({}); + kibanaIndexHandler = sinon.stub().returns({}); + defaultHandler = sinon.stub().returns({}); + + server.route = sinon.spy(); + }); + + it('should load mget handler if that is the route', () => { + const route = '/_mget'; + const method = sinon.spy(); + + proxyRequire('../mt/routing/_create_proxy', { + './routes/mget' : mgetHandler, + './routes/paths' : pathsHandler, + './routes/kibana_index': kibanaIndexHandler, + './routes/default' : defaultHandler + })(server, method, route); + + chai.expect(mgetHandler.calledOnce).to.be.ok; + chai.expect(mgetHandler.calledWith(server, method, sinon.match.string)).to.be.ok; + + chai.expect(pathsHandler.calledOnce).to.not.be.ok; + chai.expect(kibanaIndexHandler.calledOnce).to.not.be.ok; + chai.expect(defaultHandler.calledOnce).to.not.be.ok; + + chai.expect(server.route.calledWith(sinon.match.object)).to.be.ok; + }); + + it('should load paths handler if that is the route', () => { + const route = '/{paths*}'; + const method = sinon.spy(); + + proxyRequire('../mt/routing/_create_proxy', { + './routes/mget' : mgetHandler, + './routes/paths' : pathsHandler, + './routes/kibana_index': kibanaIndexHandler, + './routes/default' : defaultHandler + })(server, method, route); + + chai.expect(pathsHandler.calledOnce).to.be.ok; + chai.expect(pathsHandler.calledWith(server, method, sinon.match.string)).to.be.ok; + + chai.expect(mgetHandler.calledOnce).to.not.be.ok; + chai.expect(kibanaIndexHandler.calledOnce).to.not.be.ok; + chai.expect(defaultHandler.calledOnce).to.not.be.ok; + + chai.expect(server.route.calledWith(sinon.match.object)).to.be.ok; + }); + + it('should load default handler if that is the route', () => { + const route = '/other'; + const method = sinon.spy(); + + proxyRequire('../mt/routing/_create_proxy', { + './routes/mget' : mgetHandler, + './routes/paths' : pathsHandler, + './routes/kibana_index': kibanaIndexHandler, + './routes/default' : defaultHandler + })(server, method, route); + + chai.expect(defaultHandler.calledOnce).to.be.ok; + chai.expect(defaultHandler.calledWith(server, method, sinon.match.string)).to.be.ok; + + chai.expect(mgetHandler.calledOnce).to.not.be.ok; + chai.expect(kibanaIndexHandler.calledOnce).to.not.be.ok; + chai.expect(pathsHandler.calledOnce).to.not.be.ok; + + chai.expect(server.route.calledWith(sinon.match.object)).to.be.ok; + }); + + it('should load kibana handler if that is the route', () => { + const route = `/${kibanaIndex}/{paths*}`; + const method = sinon.spy(); + + proxyRequire('../mt/routing/_create_proxy', { + './routes/mget' : mgetHandler, + './routes/paths' : pathsHandler, + './routes/kibana_index': kibanaIndexHandler, + './routes/default' : defaultHandler + })(server, method, route); + + chai.expect(kibanaIndexHandler.calledOnce).to.be.ok; + chai.expect(kibanaIndexHandler.calledWith(server, method, sinon.match.string)).to.be.ok; + + chai.expect(mgetHandler.calledOnce).to.not.be.ok; + chai.expect(defaultHandler.calledOnce).to.not.be.ok; + chai.expect(pathsHandler.calledOnce).to.not.be.ok; + + chai.expect(server.route.calledWith(sinon.match.object)).to.be.ok; + }); + + }); + }); + }); +}); diff --git a/server/__tests__/routing.reRouting.spec.js b/server/__tests__/routing.reRouting.spec.js new file mode 100644 index 0000000..8704809 --- /dev/null +++ b/server/__tests__/routing.reRouting.spec.js @@ -0,0 +1,86 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +const sinon = require('sinon'); +const chai = require('chai'); +const proxyRequire = require('proxyquire'); + +describe('plugins/fts-keystone', () => { + describe('mt', () => { + describe('routing', () => { + describe('reRoute', () => { + const prefix = '/test'; + const server = { + log: sinon.spy() + }; + + it('should re-route ElasticSearch request', () => { + const requestPath = '/elasticsearch/something/a/b/c'; + + let request = { + setUrl: sinon.spy() + }; + let reply = { + continue: sinon.spy() + }; + let utils = { + requestPath: sinon.stub().withArgs(request).returns(requestPath), + isESRequest: sinon.stub().withArgs(request).returns(true) + }; + + proxyRequire('../mt/routing/_re_route', { + '../../util': utils, + './_utils' : { + PREFIX: prefix + } + })(server)(request, reply); + + chai.expect(request.setUrl.calledOnce).to.be.ok; + chai.expect(request.setUrl.calledWith(`${prefix}${requestPath}`)).to.be.ok; + chai.expect(reply.continue.calledOnce).to.be.ok; + + }); + + it('should not re-route non-ElasticSearch request', () => { + const requestPath = '/resources/cool.ico'; + + let request = { + setUrl: sinon.spy() + }; + let reply = { + continue: sinon.spy() + }; + let utils = { + requestPath: sinon.stub().withArgs(request).returns(requestPath), + isESRequest: sinon.stub().withArgs(request).returns(false) + }; + + proxyRequire('../mt/routing/_re_route', { + '../../util': utils, + './_utils' : { + PREFIX: prefix + } + })(server)(request, reply); + + chai.expect(request.setUrl.calledOnce).to.not.be.ok; + chai.expect(request.setUrl.calledWith(`${prefix}${requestPath}`)).to.not.be.ok; + chai.expect(reply.continue.calledOnce).to.be.ok; + + }); + + + }); + }); + }); +}); diff --git a/server/__tests__/routing.spec.js b/server/__tests__/routing.spec.js new file mode 100644 index 0000000..4896c82 --- /dev/null +++ b/server/__tests__/routing.spec.js @@ -0,0 +1,57 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +const sinon = require('sinon'); +const chai = require('chai'); +const proxyRequire = require('proxyquire'); + +describe('plugins/fts-keystone', ()=> { + describe('mt', ()=> { + describe('routing', () => { + + const createProxy = sinon.spy(); + const serverLog = sinon.spy(); + + let serverExt; + let server; + let reRoute; + + beforeEach(() => { + serverExt = sinon.spy(); + server = { + log: serverLog, + ext: serverExt + }; + reRoute = sinon.spy(); + }); + + it('should load re-route logic', (done) => { + + proxyRequire('../mt/routing', { + './_create_proxy': createProxy, + './_re_route' : reRoute + })(server).then(verify); + + function verify(route) { + chai.expect(serverExt.calledOnce).to.be.ok; + chai.expect(serverExt.calledWith('onRequest', reRoute)); + chai.expect(createProxy).to.be.eq(route); + done(); + } + + }); + + }); + }); +}); diff --git a/server/__tests__/verify.spec.js b/server/__tests__/verify.spec.js new file mode 100644 index 0000000..d8e6000 --- /dev/null +++ b/server/__tests__/verify.spec.js @@ -0,0 +1,114 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +const sinon = require('sinon'); +const chai = require('chai'); +const proxyRequire = require('proxyquire'); + +const CONSTANTS = require('../const'); + +describe('plugins/fts-keystone', ()=> { + describe('mt', ()=> { + describe('verify', () => { + + it('should skip if session not available', () => { + let server = { + log: sinon.spy() + }; + let request = { + yar: { + _store: undefined + } + }; + let reply = { + continue: sinon.spy() + }; + + require('../mt/verify')(server)(request, reply); + chai.expect(reply.continue.calledOnce).to.be.ok; + }); + + it('should skip if session available but user object not found', () => { + let server = { + log: sinon.spy() + }; + let request = { + yar: { + _store: { + '1': 1 + } + } + }; + let reply = { + continue: sinon.spy() + }; + + require('../mt/verify')(server)(request, reply); + chai.expect(reply.continue.calledOnce).to.be.ok; + }); + + it('should skip non ElasticSearch requests', () => { + let store = {}; + + store[CONSTANTS.SESSION_USER_KEY] = {'id': 1}; + + let server = { + log: sinon.spy() + }; + let request = { + url : { + path: '/some/other/path' + }, + yar: { + _store: store + } + }; + let reply = { + continue: sinon.spy() + }; + + require('../mt/verify')(server)(request, reply); + chai.expect(reply.continue.calledOnce).to.be.ok; + }); + + it('should call verify indexPattern', () => { + let store = {}; + let indexPattern = '*'; + + store[CONSTANTS.SESSION_USER_KEY] = {'id': 1}; + + let server = { + log: sinon.spy() + }; + let request = { + method: 'GET', + url : { + path: `/elasticsearch/${indexPattern}/_mapping/field/` + }, + yar: { + _store: store + } + }; + let verifyIndexPattern = sinon.spy(); + + proxyRequire('../mt/verify', { + './_verify_index_pattern': verifyIndexPattern + })(server)(request); + + chai.expect(verifyIndexPattern.calledOnce).to.be.ok; + }); + + }); + }); +}); diff --git a/server/__tests__/verifyIndexPattern.spec.js b/server/__tests__/verifyIndexPattern.spec.js new file mode 100644 index 0000000..fa23a1d --- /dev/null +++ b/server/__tests__/verifyIndexPattern.spec.js @@ -0,0 +1,103 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +const sinon = require('sinon'); +const chai = require('chai'); + +const CONSTANTS = require('../const'); +const verifyIndexPattern = require('../mt/verify/_verify_index_pattern'); + +describe('plugins/fts-keystone', ()=> { + describe('mt', ()=> { + describe('verify', () => { + describe('verify_index_pattern', () => { + + it('should reject * as index-pattern', () => { + let indexPattern = '*'; + let request = { + url: { + path: `/a/${indexPattern}/` + }, + yar: { + _store: {} + } + }; + verifyIndexPattern(request, (result) => { + chai.expect(result.isBoom) + .to.be.true; + chai.expect(result.output.payload.message) + .to.be.eq('* as pattern is not supported at the moment'); + chai.expect(result.output.statusCode) + .to.be.eq(422); + }); + }); + + it('should reject index-pattern if it does not match user`s projects', () => { + let projects = [ + { id: 'project_1'}, { id: 'project_2' }, {id: 'project_3'} + ]; + let indexPattern = 'test'; + let store = {}; + + store[CONSTANTS.SESSION_PROJECTS_KEY] = projects; + + let request = { + url: { + path: `/a/${indexPattern}/` + }, + yar: { + _store: store + } + }; + let reply = sinon.spy(); + + verifyIndexPattern(request, (result) => { + chai.expect(result.isBoom) + .to.be.true; + chai.expect(result.output.payload.message) + .to.be.eq(`${indexPattern} do not match any project of current user`); + chai.expect(result.output.statusCode) + .to.be.eq(422); + }); + }); + + it('should accept index-pattern it it constaint project id', () => { + let projects = [ + { id: 'project_1'}, { id: 'project_2' }, {id: 'project_3'} + ]; + let indexPattern = projects[1].id; + let store = {}; + + store[CONSTANTS.SESSION_PROJECTS_KEY] = projects; + + let request = { + url: { + path: `/a/${indexPattern}/` + }, + yar: { + _store: store + } + }; + let reply = { + continue: sinon.spy() + }; + + verifyIndexPattern(request, reply); + chai.expect(reply.continue.calledOnce).to.be.ok; + }); + + }); + }); + }); +}); diff --git a/server/binding/index.js b/server/binding/index.js new file mode 100644 index 0000000..b997e9e --- /dev/null +++ b/server/binding/index.js @@ -0,0 +1,31 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import TokensApi from 'keystone-v3-client/lib/keystone/tokens'; +import UsersApi from 'keystone-v3-client/lib/keystone/users'; + +module.exports = function binding(server) { + const config = server.config(); + const keystoneCfg = { + url: `${config.get('fts-keystone.url')}:${config.get('fts-keystone.port')}` + }; + + return { + start: () => { + server.expose('tokens', new TokensApi(keystoneCfg)); + server.expose('users', new UsersApi(keystoneCfg)); + } + }; + +}; diff --git a/server/const.js b/server/const.js new file mode 100644 index 0000000..81850f9 --- /dev/null +++ b/server/const.js @@ -0,0 +1,22 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +const NOW_TIME = (new Date().valueOf() / 1000); + +export const SESSION_USER_KEY = `fts-keystone-user-${NOW_TIME}`; +export const SESSION_TOKEN_KEY = `fts-keystone-token-${NOW_TIME}`; +export const SESSION_PROJECTS_KEY = `fts-keystone-projects-${NOW_TIME}`; +export const SESSION_TOKEN_CHANGED = `fts-keystone-token-changed-${NOW_TIME}`; + +export const TOKEN_CHANGED_VALUE = Symbol('token-changed'); diff --git a/server/healthcheck/index.js b/server/healthcheck/index.js index ade00be..5c69b2f 100644 --- a/server/healthcheck/index.js +++ b/server/healthcheck/index.js @@ -12,18 +12,19 @@ * the License. */ -const Promise = require('bluebird'); -const url = require('url'); +import url from 'url'; +import Promise from 'bluebird'; -const util = require('../util/'); - -module.exports = function (plugin, server) { - let timeoutId; +import util from '../util'; +module.exports = function healthcheck(plugin, server) { const config = server.config(); const keystoneUrl = config.get('fts-keystone.url'); const keystonePort = config.get('fts-keystone.port'); const request = getRequest(); + + let timeoutId; + const service = { run : check, start : start, @@ -33,22 +34,12 @@ module.exports = function (plugin, server) { } }; + server.on('stop', stop); + return service; - function getRequest() { - let required; - if (util.startsWith(keystoneUrl, 'https')) { - required = require('https'); - } else { - required = require('http'); - } - return required.request; - } - function check() { - return new Promise((resolve, reject)=> { - const req = request({ hostname: getHostname(), port : keystonePort, @@ -57,12 +48,13 @@ module.exports = function (plugin, server) { const statusCode = res.statusCode; if (statusCode >= 400) { plugin.status.red('Unavailable'); - reject(statusCode); + reject(false); } else { plugin.status.green('Ready'); - resolve(statusCode); + resolve(true); } }); + req.on('error', (error)=> { plugin.status.red('Unavailable: Failed to communicate with Keystone'); server.log(['keystone', 'healthcheck', 'error'], `${error.message}`); @@ -70,14 +62,9 @@ module.exports = function (plugin, server) { }); req.end(); - }); } - function getHostname() { - return url.parse(keystoneUrl).hostname; - } - function start() { scheduleCheck(service.stop() ? 10000 : 1); } @@ -109,4 +96,18 @@ module.exports = function (plugin, server) { return true; } + function getHostname() { + return url.parse(keystoneUrl).hostname; + } + + function getRequest() { + let required; + if (util.startsWith(keystoneUrl, 'https')) { + required = require('https'); + } else { + required = require('http'); + } + return required.request; + } + }; diff --git a/server/mt/auth/_authenticate.js b/server/mt/auth/_authenticate.js new file mode 100644 index 0000000..2569694 --- /dev/null +++ b/server/mt/auth/_authenticate.js @@ -0,0 +1,91 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Boom from 'boom'; +import Joi from 'joi'; + +import { SESSION_USER_KEY } from '../../const'; +import lookupToken from './token'; +import RELOAD from './reload'; + +const RELOAD_MARKUP = ` + +reloading... +`; +const NOOP = ()=> { +}; +const SCHEMA = { + tokenOk : Joi.func().default(NOOP), + tokenBad: Joi.func().default(NOOP) +}; + +export default (server, opts) => { + Joi.assert(opts, SCHEMA, 'Invalid keystone auth options'); + + const tokensApi = server.plugins['fts-keystone'].tokens; + const callbackOk = opts.tokenOk; + const callbackBad = opts.tokenBad; + + return (request, reply) => { + const token = lookupToken(server, request); + const session = request.yar; + + let userObj = session.get(SESSION_USER_KEY); + + if (token.isBoom) { + server.log(['status', 'debug', 'keystone'], + 'Received error object from token lookup' + ); + return reply(token); + } else if (token === RELOAD) { + server.log(['status', 'debug', 'keystone'], + 'Received reload markup object from token lookup' + ); + return reply(RELOAD_MARKUP).type('text/html'); + } else if (userObj && 'project' in userObj) { + server.log(['status','info','keystone'], `${token} already authorized`); + return reply.continue({credentials:token}); + } + + server.log(['status', 'debug', 'keystone'], + 'About to validate token with keystone' + ); + + return tokensApi + .validate({ + headers: { + 'X-Auth-Token' : token, + 'X-Subject-Token': token + } + }) + .then( + (data) => { + userObj = data.data.token; + return callbackOk(token, userObj, session) + .then(()=> { + server.log(['status', 'debug', 'keystone'], + `Auth process completed for user ${userObj.user.id}`); + return reply.continue({credentials: token}); + }); + }) + .catch((error) => { + return callbackBad(token, error, session) + .then((err)=> { + server.log(['status', 'error', 'keystone'], `Auth process did not complete for token ${token}`); + server.log(['status', 'error', 'keystone'], `${err}`); + return reply(Boom.wrap(err)); + }); + }); + }; +}; diff --git a/server/mt/auth/reload/index.js b/server/mt/auth/reload/index.js new file mode 100644 index 0000000..f3d48d4 --- /dev/null +++ b/server/mt/auth/reload/index.js @@ -0,0 +1,17 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +const RELOAD = Symbol('reload-me'); + +module.exports = RELOAD; diff --git a/server/proxy/index.js b/server/mt/auth/scheme.js similarity index 77% rename from server/proxy/index.js rename to server/mt/auth/scheme.js index 79abed2..fc56580 100644 --- a/server/proxy/index.js +++ b/server/mt/auth/scheme.js @@ -12,14 +12,10 @@ * the License. */ -const proxy = require('./proxy'); +import authenticateFactory from './_authenticate'; -module.exports = function createProxy(server) { - server.ext( - 'onPreAuth', - proxy(server), - { - after: ['yar'] - } - ); +export default (server, opts) => { + return { + authenticate: authenticateFactory(server, opts) + }; }; diff --git a/server/mt/auth/strategy.js b/server/mt/auth/strategy.js new file mode 100644 index 0000000..d176c67 --- /dev/null +++ b/server/mt/auth/strategy.js @@ -0,0 +1,74 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Boom from 'boom'; +import Promise from 'bluebird'; + +import kibanaIndex from '../kibana'; +import userProjects from '../projects'; + +import { + SESSION_TOKEN_KEY, + SESSION_USER_KEY +} from '../../const'; + +export default (server) => { + + return { + tokenOk : tokenOk, + tokenBad: tokenBad + }; + + function tokenOk(token, userObj, session) { + session.reset(); + session.set(SESSION_TOKEN_KEY, token); + session.set(SESSION_USER_KEY, userObj); + + return Promise + .all([ + userProjects(server, session, userObj), + kibanaIndex(server, userObj) + ]) + .then(() => { + server.log(['status', 'info', 'keystone'], `User ${userObj.user.id} authorized with keystone`); + return token; + }) + .catch(err => { + server.log(['status', 'info', 'keystone'], + `Error caught in process of authorization, err was ${err}`); + throw err; + }); + } + + function tokenBad(token, error, session) { + return new Promise((resolve)=> { + server.log(['keystone', 'error'], `Failed to authenticate token ${token} with keystone, error is ${error.statusCode}.`); + session.reset(); + + let err; + + if (error.statusCode === 401) { + err = Boom.forbidden('\You\'re not logged in as a user who\'s authorized to access log information'); + } else { + err = Boom.internal( + error.message || 'Unexpected error during Keystone communication', + {}, + error.statusCode + ); + } + return resolve(err); + }); + } + +}; diff --git a/server/proxy/retrieveToken.js b/server/mt/auth/token/index.js similarity index 63% rename from server/proxy/retrieveToken.js rename to server/mt/auth/token/index.js index af6ba14..2bbcdbe 100644 --- a/server/proxy/retrieveToken.js +++ b/server/mt/auth/token/index.js @@ -12,10 +12,15 @@ * the License. */ -const Boom = require('boom'); +import Boom from 'boom'; +import { + SESSION_TOKEN_KEY, + SESSION_TOKEN_CHANGED, + TOKEN_CHANGED_VALUE +} from '../../../const'; +import RELOAD_SYMBOL from '../reload'; -/** @module */ -module.exports = retrieveToken; +const HEADER_NAME = 'x-auth-token'; /** * Retrieves token from the response header using key X-Keystone-Token. @@ -31,21 +36,21 @@ module.exports = retrieveToken; * * @returns {string} current token value */ +module.exports = (server, request) => { -const HEADER_NAME = 'x-auth-token'; - -function retrieveToken(server, request) { - - if (!request.session || request.session === null) { - server.log(['keystone', 'error'], 'Session is not enabled'); + if (!request.yar || request.yar === null) { + server.log(['status', 'keystone', 'error'], 'Session is not enabled'); throw new Error('Session support is missing'); } - let tokenFromSession = request.session.get('keystone_token'); + // DEV PURPOSE ONLY + // request.yar.set(SESSION_TOKEN_KEY, 'a60e832483c34526a0c2bc3c6f8fa320'); + + let tokenFromSession = request.yar.get(SESSION_TOKEN_KEY); let token = request.headers[HEADER_NAME]; if (!token && !tokenFromSession) { - server.log(['keystone', 'error'], + server.log(['status', 'keystone', 'error'], 'Token hasn\'t been located, looked in headers and session'); return Boom.unauthorized( 'You\'re not logged into the OpenStack. Please login via Horizon Dashboard' @@ -54,15 +59,28 @@ function retrieveToken(server, request) { if (!token && tokenFromSession) { token = tokenFromSession; - server.log(['keystone', 'debug'], + server.log(['status', 'debug', 'keystone'], 'Token lookup status: Found token in session' ); } else if ((token && !tokenFromSession) || (token !== tokenFromSession)) { - server.log(['keystone', 'debug'], + server.log(['status', 'debug', 'keystone'], 'Token lookup status: Token located in header/session or token changed' ); - request.session.set('keystone_token', token); + + if ((token !== tokenFromSession) && (token && tokenFromSession)) { + server.log(['status', 'info', 'keystone'], + 'Reseting session because token has changed' + ); + request.yar.reset(); + + request.yar.set(SESSION_TOKEN_CHANGED, TOKEN_CHANGED_VALUE); + request.yar.set(SESSION_TOKEN_KEY, token); + + return RELOAD_SYMBOL; + } + + request.yar.set(SESSION_TOKEN_KEY, token); } - return token; -} + return request.yar.get(SESSION_TOKEN_KEY); +}; diff --git a/server/mt/auth/verify.js b/server/mt/auth/verify.js new file mode 100644 index 0000000..055da34 --- /dev/null +++ b/server/mt/auth/verify.js @@ -0,0 +1,62 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Boom from 'boom'; + +import { + SESSION_PROJECTS_KEY, + SESSION_USER_KEY, + SESSION_TOKEN_CHANGED, + TOKEN_CHANGED_VALUE +} from '../../const'; +import reload from './reload'; + +export default () => { + return (request, reply) => { + + let session = request.yar; + let userObj = session.get(SESSION_USER_KEY); + let tokenChanged = session.get(SESSION_TOKEN_CHANGED); + + if (tokenChanged === TOKEN_CHANGED_VALUE) { + request.log(['status', 'info', 'keystone'], + 'Detected that token has been changed, replaying the request' + ); + session.clear(SESSION_TOKEN_CHANGED); + return reply(reload.markup).type('text/html'); + } else if (userObj) { + let expiresAt = new Date(userObj.expires_at).valueOf(); + let now = new Date().valueOf(); + let diff = now - expiresAt; + + if (diff >= 0) { + session.reset(); + return reply(Boom.unauthorized('User token has expired')).takeover(); + } else { + return reply.continue({ + credentials: userObj, + artifacts : { + projects: session.get(SESSION_PROJECTS_KEY) + }, + log : { + tags: 'keystone ok' + } + }); + } + } + + // TODO(trebskit) should actually throw error here I guess + return reply.continue(); + }; +}; diff --git a/server/mt/index.js b/server/mt/index.js new file mode 100644 index 0000000..7c03b3a --- /dev/null +++ b/server/mt/index.js @@ -0,0 +1,64 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +export default { + bind: (server) => { + server.log(['status', 'info', 'keystone'], 'Registering keystone-auth schema'); + return Promise.all([ + bindAuthScheme(server), + bindExt(server), + bindRouting(server) + ]); + } +}; + +function bindRouting(server) { + const kibanaIndex = server.config().get('kibana.index'); + return require('./routing')(server) + .then((route)=> { + route(server, 'GET', '/{paths*}'); + route(server, 'POST', '/_mget'); + route(server, 'POST', '/{index}/_search'); + route(server, 'POST', '/{index}/_field_stats'); + route(server, 'POST', '/_msearch'); + route(server, 'POST', '/_search/scroll'); + route(server, ['PUT', 'POST', 'DELETE'], '/' + kibanaIndex + '/{paths*}'); + }); +} + +function bindAuthScheme(server) { + return Promise.all([ + server.auth.scheme( + 'keystone-token', + require('./auth/scheme') + ), + server.auth.strategy( + 'session', + 'keystone-token', + true, + require('./auth/strategy')(server) + ) + ]); +} + +function bindExt(server) { + return Promise.all([ + server.ext( + 'onPreAuth', + require('./auth/verify')(server), + {after: ['yar']} + ), + server.ext('onRequest', require('./verify')(server)) + ]); +} diff --git a/server/mt/kibana/_can_upgrade.js b/server/mt/kibana/_can_upgrade.js new file mode 100644 index 0000000..bfb85f6 --- /dev/null +++ b/server/mt/kibana/_can_upgrade.js @@ -0,0 +1,57 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import semver from 'semver'; + +const VERSION_REGEX = /(\d+\.\d+\.\d+)\-rc(\d+)/i; + +export default (server, doc) => { + const config = server.config(); + + if (/beta|snapshot/i.test(doc._id)) { + return false; + } + if (!doc._id) { + return false; + } + if (doc._id === config.get('pkg.version')) { + return false; + } + + let packageRcRelease = Infinity; + let rcRelease = Infinity; + let packageVersion = config.get('pkg.version'); + let version = doc._id; + let matches = doc._id.match(VERSION_REGEX); + let packageMatches = config.get('pkg.version').match(VERSION_REGEX); + + if (matches) { + version = matches[1]; + rcRelease = parseInt(matches[2], 10); + } + + if (packageMatches) { + packageVersion = packageMatches[1]; + packageRcRelease = parseInt(packageMatches[2], 10); + } + + try { + if (semver.gte(version, packageVersion) && rcRelease >= packageRcRelease) { + return false; + } + } catch (e) { + return false; + } + return true; +}; diff --git a/server/mt/kibana/_configure.js b/server/mt/kibana/_configure.js new file mode 100644 index 0000000..d0cc1a5 --- /dev/null +++ b/server/mt/kibana/_configure.js @@ -0,0 +1,107 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import { find } from 'lodash'; +import Promise from 'bluebird'; + +import canUpgradeConfig from './_can_upgrade'; + +export default (server, indexName) => { + const config = server.config(); + const client = server.plugins.elasticsearch.client; + const options = { + index: indexName, + type : 'config', + body : { + size: 1000, + sort: [ + { + buildNum: { + order : 'desc', + ignore_unmapped: true + } + } + ] + } + }; + + server.log(['status', 'debug', 'keystone'], `Configuring index ${indexName}`); + + return client + .search(options) + .then(upgradeConfig(server, indexName)) + .then(()=>{ + return Promise + .delay(666) + .then(() => { + server.log(['status', 'debug', 'keystone'], `Index ${indexName} has been configured`); + return indexName; + }); + }) + .catch((err)=> { + throw new Error(`Configuring ${indexName} failed, error is ${err}`); + }); + + function upgradeConfig(server, indexName) { + const client = server.plugins.elasticsearch.client; + const config = server.config(); + + return (response) => { + if (response.hits.hits.length === 0) { + return client.create({ + index: indexName, + type : 'config', + body : { + buildNum: config.get('pkg.buildNum') + }, + id : config.get('pkg.version') + }); + } + + // if we already have a the current version in the index then we need to stop + var devConfig = find(response.hits.hits, function currentVersion(hit) { + return hit._id !== '@@version' && hit._id === config.get('pkg.version'); + }); + + if (devConfig) { + return Promise.resolve(); + } + + // Look for upgradeable configs. If none of them are upgradeable + // then resolve with null. + let body = find(response.hits.hits, canUpgradeConfig.bind(null, server)); + if (!body) { + return Promise.resolve(); + } + + // if the build number is still the template string (which it wil be in development) + // then we need to set it to the max interger. Otherwise we will set it to the build num + body._source.buildNum = config.get('pkg.buildNum'); + + server.log(['plugin', 'elasticsearch'], { + tmpl : 'Upgrade config from <%= prevVersion %> to <%= newVersion %>', + prevVersion: body._id, + newVersion : config.get('pkg.version') + }); + + return client.create({ + index: indexName, + type : 'config', + body : body._source, + id : config.get('pkg.version') + }); + }; + } + +}; diff --git a/server/mt/kibana/_create.js b/server/mt/kibana/_create.js new file mode 100644 index 0000000..e2ae0ea --- /dev/null +++ b/server/mt/kibana/_create.js @@ -0,0 +1,60 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Boom from 'boom'; + +import { exists as indexExists } from './_exists'; + +export default (server, indexName) => { + const client = server.plugins.elasticsearch.client; + + server.log(['status', 'info', 'keystone'], `Creating user index ${indexName}`); + + return client.indices + .create({ + index: indexName, + body : { + settings: { + number_of_shards: 1 + }, + mappings: { + config: { + properties: { + buildNum: { + type : 'string', + index: 'not_analyzed' + } + } + } + } + } + }) + .catch((err)=> { + throw Boom.wrap(err, 500, + `Failed to create index ${indexName}`); + }) + .then(() => { + return indexExists(server, indexName, 'yellow') + .catch((err)=> { + throw Boom.wrap(err, 500, + `Waiting for index ${indexName} to come online failed`); + }) + .then(()=> { + server.log(['status', 'info', 'keystone'], + `Index ${indexName} has been created`); + return Promise.resolve(indexName); + }); + }); + +}; diff --git a/server/mt/kibana/_exists.js b/server/mt/kibana/_exists.js new file mode 100644 index 0000000..f16a989 --- /dev/null +++ b/server/mt/kibana/_exists.js @@ -0,0 +1,38 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import kibanaIndex from './kibanaIndex'; + +export default (server, userObj) => { + const indexName = kibanaIndex(server, userObj); + return exists(server, indexName) + .then((resp) => { + return {indexName, resp}; + }); +}; + +export function exists(server, indexName, status) { + const es = server.plugins.elasticsearch.client; + const opts = { + timeout : '5s', + index : indexName, + ignore : [408], + waitForActiveShards: 1 + }; + if (status) { + opts.status = status; + } + return es.cluster.health(opts); +} + diff --git a/server/mt/kibana/index.js b/server/mt/kibana/index.js new file mode 100644 index 0000000..1971457 --- /dev/null +++ b/server/mt/kibana/index.js @@ -0,0 +1,39 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import indexExists from './_exists'; +import createIndex from './_create'; +import configureIndex from './_configure'; + +export default (server, userObj) => { + return doCheck(); + + function doCheck() { + return indexExists(server, userObj) + .then(({indexName, resp}) => { + if (!resp || resp.timed_out) { + server.log(['status', 'warning', 'keystone'], `Index ${indexName} does not exists`); + return createIndex(server, indexName); + } + if (resp.status === 'red') { + server.log(['status', 'warning', 'keystone'], `Shards not ready for index ${indexName}`); + return Promise.delay(2500).then(doCheck); + } + return Promise.resolve(indexName); + }) + .then((indexName)=> { + return configureIndex(server, indexName); + }); + } +}; diff --git a/server/mt/kibana/kibanaIndex.js b/server/mt/kibana/kibanaIndex.js new file mode 100644 index 0000000..8d914a2 --- /dev/null +++ b/server/mt/kibana/kibanaIndex.js @@ -0,0 +1,29 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * Returns tenant/project-aware kibana index + * + * @param server server object + * @param userObj user details as retrieved from keystone + * @returns {string} project aware kibana index + * + */ +export default (server, userObj) => { + return `${server.config().get('kibana.index')}-${getProjectId(userObj)}`; +}; + +function getProjectId(userObj) { + return userObj.project.id; +} diff --git a/server/mt/projects/index.js b/server/mt/projects/index.js new file mode 100644 index 0000000..dec11dd --- /dev/null +++ b/server/mt/projects/index.js @@ -0,0 +1,52 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Promise from 'bluebird'; +import { pick, sortBy } from 'lodash'; + +import { SESSION_PROJECTS_KEY, SESSION_TOKEN_KEY } from '../../const'; + +export default (server, session, userObj) => { + + const usersApi = server.plugins['fts-keystone'].users; + + return new Promise((resolve) => { + return usersApi + .allProjects({ + params : { + user_id: userObj.user.id + }, + headers: { + 'X-Auth-Token' : session.get(SESSION_TOKEN_KEY), + 'X-Subject-Token': session.get(SESSION_TOKEN_KEY) + } + }) + .then((response) => { + const data = response.data; + const projects = data.projects; + + return sortBy( + projects.map( + project=>pick(project, ['id', 'name', 'description', 'domain_id']) + ), + 'name' + ); + }) + .then((projects) => { + session.set(SESSION_PROJECTS_KEY, projects); + return resolve(projects); + }); + }); + +}; diff --git a/server/mt/routing/_create_agent.js b/server/mt/routing/_create_agent.js new file mode 100644 index 0000000..4fd7d87 --- /dev/null +++ b/server/mt/routing/_create_agent.js @@ -0,0 +1,58 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +/* + This file was copied and modified for this plugin needs. + Original file can be found here => https://github.com/elastic/kibana/blob/4.4/src/plugins/elasticsearch/lib/create_agent.js + */ + +import url from 'url'; +import {memoize, size} from 'lodash'; +import fs from 'fs'; +import http from 'http'; +import https from 'https'; + +const readFile = (file) => fs.readFileSync(file, 'utf8'); + +module.exports = memoize(function (server) { + const config = server.config(); + const target = url.parse(config.get('elasticsearch.url')); + + if (!/^https/.test(target.protocol)) { + return new http.Agent(); + } + + const agentOptions = { + rejectUnauthorized: config.get('elasticsearch.ssl.verify') + }; + + if (size(config.get('elasticsearch.ssl.ca'))) { + agentOptions.ca = config.get('elasticsearch.ssl.ca').map(readFile); + } + + if (hasSSLEnabled()) { + agentOptions.cert = readFile(config.get('elasticsearch.ssl.cert')); + agentOptions.key = readFile(config.get('elasticsearch.ssl.key')); + } + + return new https.Agent(agentOptions); + + function hasSSLEnabled() { + return config.get('elasticsearch.ssl.cert') + && config.get('elasticsearch.ssl.key'); + } + +}); + +module.exports.cache = new Map(); diff --git a/server/mt/routing/_create_proxy.js b/server/mt/routing/_create_proxy.js new file mode 100644 index 0000000..e6f962d --- /dev/null +++ b/server/mt/routing/_create_proxy.js @@ -0,0 +1,42 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import { PREFIX } from './_utils'; + +module.exports = (server, method, route) => { + + const serverConfig = server.config(); + const pre = '/elasticsearch'; + const sep = route[0] === '/' ? '' : '/'; + const path = `${PREFIX}${pre}${sep}${route}`; + + let options; + + switch (route) { + case '/_mget': + options = require('./routes/mget')(server, method, path); + break; + case '/{paths*}': + options = require('./routes/paths')(server, method, path); + break; + default: + if (route === `/${serverConfig.get('kibana.index')}/{paths*}`) { + options = require('./routes/kibana_index')(server, method, path); + } else { + options = require('./routes/default')(server, method, path); + } + } + + return server.route(options); +}; diff --git a/server/mt/routing/_map_uri.js b/server/mt/routing/_map_uri.js new file mode 100644 index 0000000..8a478ed --- /dev/null +++ b/server/mt/routing/_map_uri.js @@ -0,0 +1,43 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +/* +This file was copied and modified for this plugin needs. +Original file can be found here => https://github.com/elastic/kibana/blob/4.4/src/plugins/elasticsearch/lib/map_uri.js + */ + +import querystring from 'querystring'; + +import { PREFIX } from './_utils'; + +export default (server, request) => { + const config = server.config(); + const path = request.path.replace(`${PREFIX}/elasticsearch`, ''); + const query = querystring.stringify(request.query); + + let url = config.get('elasticsearch.url'); + + if (path) { + if (/\/$/.test(url)) { + url = url.substring(0, url.length - 1); + } + url += path; + } + + if (query) { + url += '?' + query; + } + + return url; +}; diff --git a/server/mt/routing/_re_route.js b/server/mt/routing/_re_route.js new file mode 100644 index 0000000..275d145 --- /dev/null +++ b/server/mt/routing/_re_route.js @@ -0,0 +1,27 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import utils from '../../util'; +import { PREFIX } from './_utils'; + +module.exports = function reRoute(server) { + return (request, reply) => { + const requestPath = utils.requestPath(request); + if (utils.isESRequest(request)) { + server.log(['status', 'debug', 'keystone'], `Routing ${requestPath} onto ${PREFIX}${requestPath}`); + request.setUrl(`${PREFIX}${requestPath}`); + } + return reply.continue(); + }; +}; diff --git a/server/mt/routing/_utils.js b/server/mt/routing/_utils.js new file mode 100644 index 0000000..2408003 --- /dev/null +++ b/server/mt/routing/_utils.js @@ -0,0 +1,70 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import createAgent from './_create_agent'; + +export const PREFIX = '/mt'; + +export function getOpts(server, request, url, payload) { + + let options = { + headers : {}, + redirects : true, + passThrough : true, + xforward : true, + timeout : 1000 * 60 * 3, + localStatePassThrough: false, + agent : createAgent(server), + }; + let protocol = url.split(':', 1)[0]; + + if (payload) { + options.payload = JSON.stringify(payload); + } + + if (options.passThrough) { + options.headers = require('hoek').clone(request.headers); + delete options.headers.host; + if (options.acceptEncoding === false) { + delete options.headers['accept-encoding']; + } + } + + if (options.xforward && + request.info.remoteAddress && + request.info.remotePort) { + + options.headers['x-forwarded-for'] = (options.headers['x-forwarded-for'] ? + options.headers['x-forwarded-for'] + ',' : '') + request.info.remoteAddress; + options.headers['x-forwarded-port'] = (options.headers['x-forwarded-port'] ? + options.headers['x-forwarded-port'] + ',' : '') + request.info.remotePort; + options.headers['x-forwarded-proto'] = (options.headers['x-forwarded-proto'] ? + options.headers['x-forwarded-proto'] + ',' : '') + protocol; + } + + const contentType = request.headers['content-type']; + if (contentType) { + options.headers['content-type'] = contentType; + } + + return options; +} + +export function parsePayload(request) { + let payload = request.payload; + if (!payload || payload.length <= 0) { + return {}; + } + return JSON.parse(payload.toString('utf-8')); +} diff --git a/server/mt/routing/index.js b/server/mt/routing/index.js new file mode 100644 index 0000000..08d8f16 --- /dev/null +++ b/server/mt/routing/index.js @@ -0,0 +1,23 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Promise from 'bluebird'; +import reRoute from './_re_route'; + +module.exports = function routing(server) { + return new Promise((resolve) => { + server.ext('onRequest', reRoute(server)); + resolve(require('./_create_proxy')); + }); +}; diff --git a/server/mt/routing/routes/default.js b/server/mt/routing/routes/default.js new file mode 100644 index 0000000..28a937c --- /dev/null +++ b/server/mt/routing/routes/default.js @@ -0,0 +1,35 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import mapUri from '../_map_uri'; +import createAgent from '../_create_agent'; + +module.exports = function defaultHandler(server, method, path) { + return { + method : method, + path : path, + handler: { + proxy: { + mapUri : (request, done) => { + server.log(['status', 'debug', 'keystone'], + `mapUri for path ${request.path}`); + done(null, mapUri(server, request)); + }, + agent : createAgent(server), + passThrough: true, + xforward : true + } + } + }; +}; diff --git a/server/mt/routing/routes/kibana_index.js b/server/mt/routing/routes/kibana_index.js new file mode 100644 index 0000000..d1ede83 --- /dev/null +++ b/server/mt/routing/routes/kibana_index.js @@ -0,0 +1,57 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Wreck from 'wreck'; + +import { SESSION_USER_KEY } from '../../../const'; +import { getOpts, parsePayload } from '../_utils'; +import kibanaIndex from '../../kibana/kibanaIndex'; +import mapUri from '../_map_uri'; + +export default function (server, method, path) { + + const defaultKibanaIndex = server.config().get('kibana.index'); + + return { + method : method, + path : path, + config : { + auth : false, + payload: { + output: 'data', + parse : false + } + }, + handler: handler + }; + + function handler(request, reply) { + const url = getUrl(request); + const opts = getOpts(server, request, url, parsePayload(request)); + return Wreck.request(request.method, url, opts, (err, res) => { + return reply(res).code(res.statusCode).passThrough(!!opts.passThrough); + }); + } + + function getUrl(request) { + const session = request.yar._store; + + let url = mapUri(server, request).split('/'); + let indexPos = url.findIndex((item) => item === defaultKibanaIndex); + + url[indexPos] = kibanaIndex(server, session[SESSION_USER_KEY]); + + return url.join('/'); + } +} diff --git a/server/mt/routing/routes/mget.js b/server/mt/routing/routes/mget.js new file mode 100644 index 0000000..5dfade5 --- /dev/null +++ b/server/mt/routing/routes/mget.js @@ -0,0 +1,69 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Boom from 'boom'; +import Wreck from 'wreck'; + +import { SESSION_USER_KEY } from '../../../const'; +import { getOpts, parsePayload } from '../_utils'; +import kibanaIndex from '../../kibana/kibanaIndex'; +import mapUri from '../_map_uri'; + +export default function (server, method, path) { + + return { + method : method, + path : path, + config : { + auth : false, + payload: { + output: 'data', + parse : false + } + }, + handler: handler + }; + + function handler(request, reply) { + const url = mapUri(server, request); + const session = request.yar._store; + const payload = parsePayload(request); + + payload.docs.forEach((doc) => { + doc._index = kibanaIndex(server, session[SESSION_USER_KEY]); + }); + + const opts = getOpts(server, request, url, payload); + return Wreck.request(request.method, url, opts, (err, res) => { + if (err) { + server.log( + ['status', 'error', 'keystone'], + `Failed to request ${url}, error is ${err}`); + return reply(Boom.wrap(err)); + } + return Wreck.read(res, {json: true}, (err, body)=> { + if (err) { + server.log( + ['status', 'error', 'keystone'], + `Failed to read response from ${url}, error is ${err}`); + return reply(Boom.wrap(err)); + } + + return reply(body) + .code(res.statusCode) + .passThrough(!!opts.passThrough); + }); + }); + } +} diff --git a/server/mt/routing/routes/paths.js b/server/mt/routing/routes/paths.js new file mode 100644 index 0000000..4622d22 --- /dev/null +++ b/server/mt/routing/routes/paths.js @@ -0,0 +1,66 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Wreck from 'wreck'; + +import { SESSION_USER_KEY } from '../../../const'; +import { getOpts } from '../_utils'; +import kibanaIndex from '../../kibana/kibanaIndex'; +import mapUri from '../_map_uri'; + +export default function (server, method, path) { + const defaultKibanaIndex = defaultKibanaIndex; + + return { + method : method, + path : path, + config : { + tags: ['elasticsearch', 'multitenancy'], + auth: false + }, + handler: handler + }; + + function handler(request, reply) { + const session = request.yar._store; + + let url = mapUri(server, request).split('/'); + let kibanaIndexRequest = false; + + let indexPos = url.findIndex((item) => item === defaultKibanaIndex); + if (indexPos > -1) { + url[indexPos] = kibanaIndex(server, session[SESSION_USER_KEY]); + kibanaIndexRequest = true; + } + url = url.join('/'); + + const opts = getOpts(server, request, url); + return Wreck.request(request.method, url, opts, (err, res) => { + return Wreck.read(res, {json: true}, (err, body)=> { + let newData = {}; + + if (kibanaIndexRequest) { + let tenantAwareIndex = Object.keys(body)[0]; + newData[defaultKibanaIndex] = body[tenantAwareIndex]; + } else { + newData = body; + } + + return reply(newData) + .code(res.statusCode) + .passThrough(!!opts.passThrough); + }); + }); + } +} diff --git a/server/mt/verify/_verify_index_pattern.js b/server/mt/verify/_verify_index_pattern.js new file mode 100644 index 0000000..2384c79 --- /dev/null +++ b/server/mt/verify/_verify_index_pattern.js @@ -0,0 +1,41 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import Boom from 'boom'; + +import { SESSION_PROJECTS_KEY } from '../../const'; +import util from '../../util'; + +const INDEX_PATTER_POS = 2; + +module.exports = (request, reply) => { + const session = request.yar._store; + const requestPath = util.requestPath(request); + const splittedPath = requestPath.split('/'); + + let pattern = splittedPath[INDEX_PATTER_POS]; + let projects = session[SESSION_PROJECTS_KEY]; + + if ('*' === pattern) { + return reply(Boom.badData('* as pattern is not supported at the moment')); + } else if (projects.filter(filter).length === 0) { + return reply(Boom.badData(`${pattern} do not match any project of current user`)); + } + + return reply.continue(); + + function filter(project) { + return new RegExp(`${project.id}.*`, 'gi').test(pattern); + } +}; diff --git a/server/mt/verify/index.js b/server/mt/verify/index.js new file mode 100644 index 0000000..0b009cb --- /dev/null +++ b/server/mt/verify/index.js @@ -0,0 +1,48 @@ +/* + * Copyright 2016 FUJITSU LIMITED + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +import { SESSION_USER_KEY } from '../../const'; +import util from '../../util'; + +module.exports = (server) => { + + return (request, reply) => { + const session = request.yar._store; + if (!(session && (SESSION_USER_KEY in session))) { + server.log(['status', 'warning', 'keystone'], 'Session not yet available'); + return reply.continue(); + } + + let requestPath = util.requestPath(request); + let requestMethod = request.method; + + if (util.isESRequest(request)) { + let handler; + if (isIndexPatternLookup()) { + handler = require('./_verify_index_pattern'); + } + if (handler) { + return handler(request, reply); + } + } + + return reply.continue(); + + function isIndexPatternLookup() { + let regExp = /\/elasticsearch\/.*\/_mapping\/field\/.*/; + return regExp.test(requestPath) && requestMethod.toLowerCase() === 'get'; + } + + }; +}; diff --git a/server/proxy/proxy.js b/server/proxy/proxy.js deleted file mode 100644 index 29d3f6a..0000000 --- a/server/proxy/proxy.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2016 FUJITSU LIMITED - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -const Boom = require('boom'); -const retrieveToken = require('./retrieveToken'); -const TokensApi = require('keystone-v3-client/lib/keystone/tokens'); - -const util = require('../util/'); - -module.exports = function (server) { - const config = server.config(); - const tokensApi = new TokensApi({ - url: `${config.get('fts-keystone.url')}:${config.get('fts-keystone.port')}` - }); - - return (request, reply) => { - const requestPath = getRequestPath(request); - let token; - - if (shouldCallKeystone(requestPath)) { - server.log( - ['keystone', 'debug'], - `Call for ${requestPath} detected, authenticating with keystone` - ); - - token = retrieveToken(server, request); - if (token.isBoom) { - return reply(token); - } - - return tokensApi - .check({ - headers: { - 'X-Auth-Token' : token, - 'X-Subject-Token': token - } - }) - .then(onFulfilled, onFailed); - - } - - return reply.continue(); - - function onFulfilled() { - reply.continue(); - } - - function onFailed(error) { - - server.log( - ['keystone', 'error'], - `Failed to authenticate token ${token} with keystone, - error is ${error.statusCode}.` - ); - - if (error.statusCode === 401) { - request.session.clear('keystone_token'); - reply(Boom.forbidden( - ` - You\'re not logged in as a - user who\'s authenticated to access log information - ` - )); - } else { - reply(Boom.internal( - error.message || 'Unexpected error during Keystone communication', - {}, - error.statusCode - )); - } - } - - }; -}; - -function getRequestPath(request) { - return request.url.path; -} - -function shouldCallKeystone(path) { - return util.startsWith(path, '/elasticsearch'); -} diff --git a/server/session/index.js b/server/session/index.js index da04fb3..57a8a35 100644 --- a/server/session/index.js +++ b/server/session/index.js @@ -12,34 +12,39 @@ * the License. */ -module.exports = function initSession(server) { +import yarCookie from 'yar'; +import multiTenancy from '../mt'; +export default (server) => { const config = server.config(); - const registerOpts = { - register: require('yar'), - options : { - name : 'kibana_session', - storeBlank : false, - cache : { - expiresIn: config.get('fts-keystone.cookie.expiresIn') - }, - cookieOptions: { - password : config.get('fts-keystone.cookie.password'), - isSecure : config.get('fts-keystone.cookie.isSecure'), - ignoreErrors: config.get('fts-keystone.cookie.ignoreErrors'), - clearInvalid: true - } + return { + start: ()=> { + server.register({ + register: yarCookie, + options : { + maxCookieSize: 4096, + name : config.get('fts-keystone.cookie.name'), + storeBlank : false, + cache : { + expiresIn: config.get('fts-keystone.cookie.expiresIn') + }, + cookieOptions: { + password : config.get('fts-keystone.cookie.password'), + isSecure : config.get('fts-keystone.cookie.isSecure'), + ignoreErrors: config.get('fts-keystone.cookie.ignoreErrors'), + clearInvalid: false + } + } + }, (error) => { + if (!error) { + server.log(['status', 'info', 'keystone'], 'Session registered'); + multiTenancy.bind(server); + } else { + server.log(['status', 'error', 'keystone'], error); + throw error; + } + }); } }; - const callback = (error) => { - if (!error) { - server.log(['session', 'debug'], 'Session registered'); - } else { - server.log(['session', 'error'], error); - throw error; - } - }; - - server.register(registerOpts, callback); }; diff --git a/server/util/index.js b/server/util/index.js index 6bffcad..3211722 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -13,7 +13,9 @@ */ module.exports = { - startsWith: startsWith + startsWith: startsWith, + requestPath: getRequestPath, + isESRequest: isESRequest }; function startsWith(str) { @@ -25,3 +27,11 @@ function startsWith(str) { } return false; } + +function getRequestPath(request) { + return request.url.path; +} + +function isESRequest(request) { + return startsWith(getRequestPath(request), '/elasticsearch'); +}