Revert "Revert "Add Multi-Tenancy for keystone plugin""

This reverts commit 4752d35777.

Change-Id: Ie377d6675c901eb922b4e07e800e391805f69361
This commit is contained in:
Jakub Wachowski 2017-01-04 11:09:59 +01:00
parent 4752d35777
commit 0d84a5afa6
44 changed files with 2153 additions and 495 deletions

4
.jshintrc Normal file
View File

@ -0,0 +1,4 @@
{
"esversion": 6,
"esnext": true
}

View File

@ -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');
}
};

View File

@ -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"
}
}

View File

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

View File

@ -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);

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
});
});
});
});

View File

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

View File

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

31
server/binding/index.js Normal file
View File

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

22
server/const.js Normal file
View File

@ -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');

View File

@ -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;
}
};

View File

@ -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 = `<html>
<head><script type="text/javascript">window.location.reload();</script></head>
<body>reloading...</body>
</html>`;
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));
});
});
};
};

View File

@ -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;

View File

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

View File

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

View File

@ -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 <b>X-Keystone-Token</b>.
@ -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);
};

62
server/mt/auth/verify.js Normal file
View File

@ -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();
};
};

64
server/mt/index.js Normal file
View File

@ -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))
]);
}

View File

@ -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;
};

View File

@ -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')
});
};
}
};

View File

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

View File

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

39
server/mt/kibana/index.js Normal file
View File

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

View File

@ -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;
}

View File

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

View File

@ -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();

View File

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

View File

@ -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;
};

View File

@ -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();
};
};

View File

@ -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'));
}

View File

@ -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'));
});
};

View File

@ -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
}
}
};
};

View File

@ -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('/');
}
}

View File

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

View File

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

View File

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

48
server/mt/verify/index.js Normal file
View File

@ -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';
}
};
};

View File

@ -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');
}

View File

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

View File

@ -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');
}