Initial commit

Kibana-Keystone plugin with keystone authentication.

Change-Id: I1fe1e5b028a753e8e22af4b6a31305d225f0a914
This commit is contained in:
Tomasz Trębski 2016-02-05 14:38:37 +01:00
parent bfd62b9e3d
commit fcd5df1ad3
17 changed files with 1321 additions and 0 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}

75
.eslintrc Normal file
View File

@ -0,0 +1,75 @@
---
parser: babel-eslint
plugins:
- mocha
env:
es6: true
amd: true
node: true
mocha: true
browser: true
rules:
block-scoped-var: 2
camelcase: [ 2, { properties: never } ]
comma-dangle: 0
comma-style: [ 2, last ]
consistent-return: 0
curly: [ 2, multi-line ]
dot-location: [ 2, property ]
dot-notation: [ 2, { allowKeywords: true } ]
eqeqeq: [ 2, allow-null ]
guard-for-in: 2
indent: [ 2, 2, { SwitchCase: 1 } ]
key-spacing: [ 0, { align: value } ]
max-len: [ 2, 140, 2, { ignoreComments: true, ignoreUrls: true } ]
new-cap: [ 2, { capIsNewExceptions: [ Private ] } ]
no-bitwise: 0
no-caller: 2
no-cond-assign: 0
no-debugger: 2
no-empty: 2
no-eval: 2
no-extend-native: 2
no-extra-parens: 0
no-irregular-whitespace: 2
no-iterator: 2
no-loop-func: 2
no-multi-spaces: 0
no-multi-str: 2
no-nested-ternary: 2
no-new: 0
no-path-concat: 0
no-proto: 2
no-return-assign: 0
no-script-url: 2
no-sequences: 2
no-shadow: 0
no-trailing-spaces: 2
no-undef: 2
no-underscore-dangle: 0
no-unused-expressions: 0
no-unused-vars: 0
no-use-before-define: [ 2, nofunc ]
no-with: 2
one-var: [ 2, never ]
quotes: [ 2, single ]
semi-spacing: [ 2, { before: false, after: true } ]
semi: [ 2, always ]
space-after-keywords: [ 2, always ]
space-before-blocks: [ 2, always ]
space-before-function-paren: [ 2, { anonymous: always, named: never } ]
space-in-parens: [ 2, never ]
space-infix-ops: [ 2, { int32Hint: false } ]
space-return-throw-case: [ 2 ]
space-unary-ops: [ 2 ]
strict: [ 2, never ]
valid-typeof: 2
wrap-iife: [ 2, outside ]
yoda: 0
mocha/no-exclusive-tests: 2
mocha/handle-done-callback: 2

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
build/
target/

11
LICENSE Normal file
View File

@ -0,0 +1,11 @@
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.

143
gulpfile.js Normal file
View File

@ -0,0 +1,143 @@
/*
* 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.
*/
var babel = require('babel-register')({
presets: ['es2015']
});
var gulp = require('gulp');
var path = require('path');
var mkdirp = require('mkdirp');
var Rsync = require('rsync');
var Promise = require('bluebird');
var eslint = require('gulp-eslint');
var rimraf = require('rimraf');
var tar = require('gulp-tar');
var gzip = require('gulp-gzip');
var fs = require('fs');
var mocha = require('gulp-mocha');
var pkg = require('./package.json');
var packageName = pkg.name + '-' + pkg.version;
// relative location of Kibana install
var pathToKibana = '../kibana';
var buildDir = path.resolve(__dirname, 'build');
var targetDir = path.resolve(__dirname, 'target');
var buildTarget = path.resolve(buildDir, pkg.name);
var kibanaPluginDir = path.resolve(__dirname, pathToKibana, 'installedPlugins', pkg.name);
var exclude = [
'.git',
'.idea',
'gulpfile.js',
'.babelrc',
'.gitignore',
'.eslintrc',
'__tests__'
];
Object.keys(pkg.devDependencies).forEach(function (name) {
exclude.push(path.join('node_modules', name));
});
function syncPluginTo(dest, done) {
mkdirp(dest, function (err) {
if (err) return done(err);
var source = path.resolve(__dirname) + '/';
var rsync = new Rsync();
rsync
.source(source)
.destination(dest)
.flags('uav')
.recursive(true)
.set('delete')
.exclude(exclude)
.output(function (data) {
process.stdout.write(data.toString('utf8'));
});
rsync.execute(function (err) {
if (err) {
console.log(err);
return done(err);
}
done();
});
});
}
gulp.task('sync', ['lint'], function (done) {
syncPluginTo(kibanaPluginDir, done);
});
gulp.task('lint', function () {
var filePaths = [
'gulpfile.js',
'server/**/*.js',
'public/**/*.js',
'public/**/*.jsx'
];
return gulp.src(filePaths)
// eslint() attaches the lint output to the eslint property
// of the file object so it can be used by other modules.
.pipe(eslint())
// eslint.format() outputs the lint results to the console.
// Alternatively use eslint.formatEach() (see Docs).
.pipe(eslint.formatEach())
// To have the process exit with an error code (1) on
// lint error, return the stream and pipe to failOnError last.
.pipe(eslint.failOnError());
});
gulp.task('test', function () {
return gulp.src(['server/**/*.spec.js'])
.pipe(mocha({
compilers: {
js: babel
}
}));
});
gulp.task('clean', function (done) {
Promise.each([buildDir, targetDir], function (dir) {
return new Promise(function (resolve, reject) {
rimraf(dir, function (err) {
if (err) return reject(err);
resolve();
});
});
}).nodeify(done);
});
gulp.task('build', ['clean'], function (done) {
syncPluginTo(buildTarget, done);
});
gulp.task('package', ['build'], function () {
return gulp.src(path.join(buildDir, '**', '*'))
.pipe(tar(packageName + '.tar'))
.pipe(gzip())
.pipe(gulp.dest(targetDir));
});
gulp.task('dev', ['sync'], function () {
gulp.watch(
['package.json', 'index.js', 'public/**/*', 'server/**/*'],
['sync']);
});

59
index.js Normal file
View File

@ -0,0 +1,59 @@
/*
* 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.
*/
module.exports = (kibana) => {
const session = require('./server/session');
const proxy = require('./server/proxy');
const healthCheck = require('./server/healthcheck');
return new kibana.Plugin({
require: ['elasticsearch'],
config : config,
init : init
});
function config(Joi) {
const cookie = Joi.object({
password : Joi.string()
.min(16)
.default(require('crypto').randomBytes(16).toString('hex')),
isSecure : Joi.boolean()
.default(false),
ignoreErrors: Joi.boolean()
.default(true),
expiresIn : Joi.number()
.positive()
.integer()
.default(24 * 60 * 60 * 1000) // 1 day
}).default();
return Joi.object({
enabled: Joi.boolean().default(true),
url : Joi.string()
.uri({scheme: ['http', 'https']})
.required(),
port : Joi.number().required(),
cookie : cookie
}).default();
}
function init(server) {
session(server);
proxy(server);
healthCheck(this, server).start();
}
};

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "fts-keystone",
"version": "0.0.1",
"description": "Keystone authentication support for Kibana 4.4.x",
"author": "Fujitsu Enabling Software Technology GmbH",
"licenses": "Apache-2.0",
"keywords": [
"kibana",
"authentication",
"keystone",
"plugin"
],
"scripts": {
"start": "gulp dev",
"build": "gulp build",
"package": "gulp package",
"test": "gulp test"
},
"engines": {
"node": "0.12.9",
"npm": "2.14.3"
},
"main": "gulpfile.js",
"dependencies": {
"yar": "^4.2.0",
"keystone-v3-client": "^0.0.7"
},
"repository": {
"type": "git",
"url": "http://github.com/FujitsuEnablingSoftwareTechnologyGmbH/fts-keystone.git"
},
"devDependencies": {
"babel-eslint": "^4.1.8",
"babel-preset-es2015": "^6.3.13",
"babel-register": "^6.4.3",
"bluebird": "^3.2.1",
"boom": "^2.8.0",
"chai": "^3.5.0",
"eslint-plugin-mocha": "^1.1.0",
"gulp": "^3.9.0",
"gulp-eslint": "^1.1.1",
"gulp-gzip": "^1.2.0",
"gulp-mocha": "^2.2.0",
"gulp-tar": "^1.8.0",
"gulp-util": "^3.0.7",
"lodash": "^4.2.1",
"mkdirp": "^0.5.1",
"proxyquire": "^1.7.4",
"rimraf": "^2.5.1",
"rsync": "^0.4.0",
"sinon": "^1.17.3"
}
}

View File

@ -0,0 +1,222 @@
/*
* 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('healthcheck', ()=> {
const keystoneUrl = 'http://localhost'; // mocking http
const keystonePort = 9000;
let healthcheck; // placeholder for the require healthcheck
let plugin;
let configGet;
let server;
let clock;
before(function () {
clock = sinon.useFakeTimers();
});
after(function () {
clock.restore();
});
beforeEach(function () {
plugin = {
name : 'fts-keystone',
status: {
red : sinon.stub(),
green : sinon.stub(),
yellow: sinon.stub()
}
};
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 set status to green if keystone available', (done)=> {
let expectedCode = 200;
let healthcheck = proxyRequire('../healthcheck', {
'http': {
request: (_, callback)=> {
return {
end: () => {
let res = {
statusCode: expectedCode
};
callback(res);
},
on : sinon.stub()
};
}
}
});
let check = healthcheck(plugin, server);
check
.run()
.then((code) => {
chai.expect(expectedCode).to.be.equal(code);
chai.expect(plugin.status.green.calledWith('Ready')).to.be.ok;
})
.finally(done);
});
it('should set status to red if keystone not available', (done) => {
let expectedCode = 500;
let healthcheck = proxyRequire('../healthcheck', {
'http': {
request: (_, callback)=> {
return {
end: () => {
let res = {
statusCode: expectedCode
};
callback(res);
},
on : sinon.stub()
};
}
}
});
let check = healthcheck(plugin, server);
check
.run()
.catch((code) => {
chai.expect(expectedCode).to.be.equal(code);
chai.expect(plugin.status.red.calledWith('Unavailable')).to.be.ok;
})
.finally(done);
});
it('should set status to red if available but cannot communicate', (done)=> {
let errorListener;
let healthcheck = proxyRequire('../healthcheck', {
'http': {
request: ()=> {
return {
on : (_, listener)=> {
errorListener = sinon.spy(listener);
},
end: ()=> {
errorListener(new Error('test'));
}
};
}
}
});
let check = healthcheck(plugin, server);
check
.run()
.catch((error)=> {
let msg = 'Unavailable: Failed to communicate with Keystone';
chai.expect(errorListener).to.be.ok;
chai.expect(errorListener.calledOnce).to.be.ok;
chai.expect(plugin.status.red.calledWith(msg)).to.be.ok;
chai.expect(error.message).to.be.equal('test');
})
.done(done);
});
it('should run check in period `10000`', ()=> {
let healthcheck = proxyRequire('../healthcheck', {
'http': {
request: sinon.stub().returns({
end: sinon.stub(),
on : sinon.stub()
})
}
});
let runChecks = 3;
let timeout = 10000;
let check = healthcheck(plugin, server);
sinon.spy(check, 'run');
// first call
chai.expect(check.isRunning()).to.be.eq(false);
check.start();
validateFirstCall();
// next calls
for (let it = 0; it < runChecks; it++) {
validateNextCallWithTick(it);
}
function validateFirstCall() {
clock.tick(1); // first call is immediate
chai.expect(check.run.calledOnce).to.be.ok;
chai.expect(check.isRunning()).to.be.eq(true);
}
function validateNextCallWithTick(it) {
// should be called once for the sake of first call
chai.assert.equal(check.run.callCount, it + 1);
// run check again
check.start();
// assert that tick did not kick in
chai.assert.equal(check.run.callCount, it + 1);
// kick it in
clock.tick(timeout);
// and we have another call
chai.expect(check.run.callCount).to.be.eq(it + 2);
}
});
it('should return false from stop if not run before', ()=> {
let healthcheck = proxyRequire('../healthcheck', {
'http': {
request: sinon.stub().returns({
end: sinon.stub(),
on : sinon.stub()
})
}
});
let check = healthcheck(plugin, server);
sinon.spy(check, 'run');
chai.expect(check.stop()).to.be.eq(false);
chai.expect(check.run.called).to.be.eq(false);
});
});
});

View File

@ -0,0 +1,174 @@
/*
* 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

@ -0,0 +1,172 @@
/*
* 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 retrieveToken = require('../proxy/retrieveToken');
describe('plugins/fts-keystone', ()=> {
describe('proxy', ()=> {
describe('retrieveToken', ()=> {
let server;
beforeEach(()=> {
server = {
log: sinon.stub()
};
});
it('should return isBoom if session not available', ()=> {
let request = {};
let errMsg = /Session support is missing/;
chai.expect(()=> {
retrieveToken(server, request);
}).to.throw(errMsg);
request = {
session: undefined
};
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: {}
};
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;
token = retrieveToken(server, request);
chai.expect(token).not.to.be.undefined;
chai.expect(token).to.be.eql(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;
});
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: {
'x-auth-token': expectedToken
}
};
let token;
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;
});
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
};
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,36 @@
/*
* 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 chai = require('chai');
const util = require('../util');
describe('plugins/fts-keystone', ()=> {
describe('util', ()=> {
const CHECK_STR = 'test.str';
it('should return true if starts with ok', ()=> {
chai.expect(util.startsWith(CHECK_STR, 'test')).to.be.ok;
});
it('should return false if does not start with', ()=> {
chai.expect(util.startsWith(CHECK_STR, 'str')).not.to.be.ok;
});
it('should return false if no prefixes supplied', ()=> {
chai.expect(util.startsWith(CHECK_STR)).not.to.be.ok;
});
});
});

112
server/healthcheck/index.js Normal file
View File

@ -0,0 +1,112 @@
/*
* 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 Promise = require('bluebird');
const url = require('url');
const util = require('../util/');
module.exports = function (plugin, server) {
let timeoutId;
const config = server.config();
const keystoneUrl = config.get('fts-keystone.url');
const keystonePort = config.get('fts-keystone.port');
const request = getRequest();
const service = {
run : check,
start : start,
stop : stop,
isRunning: ()=> {
return !!timeoutId;
}
};
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,
method : 'HEAD'
}, (res)=> {
const statusCode = res.statusCode;
if (statusCode >= 400) {
plugin.status.red('Unavailable');
reject(statusCode);
} else {
plugin.status.green('Ready');
resolve(statusCode);
}
});
req.on('error', (error)=> {
plugin.status.red('Unavailable: Failed to communicate with Keystone');
server.log(['keystone', 'healthcheck', 'error'], `${error.message}`);
reject(error);
});
req.end();
});
}
function getHostname() {
return url.parse(keystoneUrl).hostname;
}
function start() {
scheduleCheck(service.stop() ? 10000 : 1);
}
function stop() {
if (!timeoutId) {
return false;
}
clearTimeout(timeoutId);
timeoutId = undefined;
return true;
}
function scheduleCheck(ms) {
if (timeoutId) {
return false;
}
const currentId = setTimeout(function () {
service.run().finally(function () {
if (timeoutId === currentId) {
start();
}
});
}, ms);
timeoutId = currentId;
return true;
}
};

25
server/proxy/index.js Normal file
View File

@ -0,0 +1,25 @@
/*
* 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 proxy = require('./proxy');
module.exports = function createProxy(server) {
server.ext(
'onPreAuth',
proxy(server),
{
after: ['yar']
}
);
};

93
server/proxy/proxy.js Normal file
View File

@ -0,0 +1,93 @@
/*
* 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

@ -0,0 +1,68 @@
/*
* 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');
/** @module */
module.exports = retrieveToken;
/**
* Retrieves token from the response header using key <b>X-Keystone-Token</b>.
* If token is found there following actions are taken:
* - if token is not in session, it is set there
* - if token is in session but it differs from the one in request's header, session's token is replaced with new one
* If token is not found in request following actions are taken:
* - if token is also not available in session, error is produced
* - if token is available in session it is used
*
* @param {object} server server object
* @param {object} request current request
*
* @returns {string} current token value
*/
const HEADER_NAME = 'x-auth-token';
function retrieveToken(server, request) {
if (!request.session || request.session === null) {
server.log(['keystone', 'error'], 'Session is not enabled');
throw new Error('Session support is missing');
}
let tokenFromSession = request.session.get('keystone_token');
let token = request.headers[HEADER_NAME];
if (!token && !tokenFromSession) {
server.log(['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'
);
}
if (!token && tokenFromSession) {
token = tokenFromSession;
server.log(['keystone', 'debug'],
'Token lookup status: Found token in session'
);
} else if ((token && !tokenFromSession) || (token !== tokenFromSession)) {
server.log(['keystone', 'debug'],
'Token lookup status: Token located in header/session or token changed'
);
request.session.set('keystone_token', token);
}
return token;
}

45
server/session/index.js Normal file
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.
*/
module.exports = function initSession(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
}
}
};
const callback = (error) => {
if (!error) {
server.log(['session', 'debug'], 'Session registered');
} else {
server.log(['session', 'error'], error);
throw error;
}
};
server.register(registerOpts, callback);
};

27
server/util/index.js Normal file
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.
*/
module.exports = {
startsWith: startsWith
};
function startsWith(str) {
var prefixes = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < prefixes.length; ++i) {
if (str.lastIndexOf(prefixes[i], 0) === 0) {
return true;
}
}
return false;
}