Initial commit
Kibana-Keystone plugin with keystone authentication. Change-Id: I1fe1e5b028a753e8e22af4b6a31305d225f0a914
This commit is contained in:
parent
bfd62b9e3d
commit
fcd5df1ad3
75
.eslintrc
Normal file
75
.eslintrc
Normal 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
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
build/
|
||||
target/
|
11
LICENSE
Normal file
11
LICENSE
Normal 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
143
gulpfile.js
Normal 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
59
index.js
Normal 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
53
package.json
Normal 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"
|
||||
}
|
||||
}
|
222
server/__tests__/healthcheck.spec.js
Normal file
222
server/__tests__/healthcheck.spec.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
174
server/__tests__/proxy.spec.js
Normal file
174
server/__tests__/proxy.spec.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
172
server/__tests__/retrieveToken.spec.js
Normal file
172
server/__tests__/retrieveToken.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
36
server/__tests__/util.spec.js
Normal file
36
server/__tests__/util.spec.js
Normal 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
112
server/healthcheck/index.js
Normal 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
25
server/proxy/index.js
Normal 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
93
server/proxy/proxy.js
Normal 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');
|
||||
}
|
68
server/proxy/retrieveToken.js
Normal file
68
server/proxy/retrieveToken.js
Normal 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
45
server/session/index.js
Normal 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
27
server/util/index.js
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user