Merge branch 'multitenancy' into 'master'
Change-Id: I488ab185cef848d129a4970b70c8e2c89803c9fe
This commit is contained in:
commit
2cdbe4b1e4
@ -13,6 +13,7 @@ where keystone_port should be the keystone admin port (default: 35357) not the k
|
||||
fts-keystone.port: ${keystone_port}
|
||||
fts-keystone.url: http://${keystone_host}
|
||||
fts-keystone.enabled: True
|
||||
fts-keystone.defaultTimeField: '@timestamp'
|
||||
```
|
||||
|
||||
To install the fts-keystone plugin, first the latest release should be downloaded:
|
||||
|
1
index.js
1
index.js
@ -49,6 +49,7 @@ export default (kibana) => {
|
||||
.uri({scheme: ['http', 'https']})
|
||||
.required(),
|
||||
port : Joi.number().required(),
|
||||
defaultTimeField: Joi.string().default('@timestamp'),
|
||||
cookie : cookie
|
||||
}).default();
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "fts-keystone",
|
||||
"version": "0.0.2",
|
||||
"description": "Keystone authentication & multitenancy support for Kibana 4.4.x",
|
||||
"version": "0.0.5",
|
||||
"description": "Keystone authentication & multitenancy support for Kibana 4.5.x",
|
||||
"author": "Fujitsu Enabling Software Technology GmbH",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
|
128
server/__tests__/defaultIndexPattern.spec.js
Normal file
128
server/__tests__/defaultIndexPattern.spec.js
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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 sinon = require('sinon');
|
||||
|
||||
describe('plugins/fts-keystone', () => {
|
||||
const indexName = '.kibana-testdefaultindex';
|
||||
let server;
|
||||
let userObj;
|
||||
let configGet;
|
||||
|
||||
|
||||
beforeEach(function () {
|
||||
configGet = sinon.stub();
|
||||
configGet.withArgs('pkg.version').returns('4.4.0');
|
||||
configGet.withArgs('fts-keystone.defaultTimeField').returns('@timestamp');
|
||||
|
||||
server = {
|
||||
log : sinon.stub(),
|
||||
config: function () {
|
||||
return {
|
||||
get: configGet
|
||||
};
|
||||
},
|
||||
plugins: {
|
||||
elasticsearch: {
|
||||
client: {
|
||||
indices: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
userObj = {
|
||||
project: {
|
||||
id: 'abcdef'
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
describe('defaultIndexPattern_exists', ()=> {
|
||||
it('should return false if default pattern does not exist', (done) => {
|
||||
let exists = require('../mt/kibana/defaultIndexPattern/_exists').default;
|
||||
|
||||
let count = sinon.stub();
|
||||
count.returns(Promise.resolve({ count: 0 }));
|
||||
server.plugins.elasticsearch.client.count = count;
|
||||
|
||||
exists(server, indexName)
|
||||
.then((resp) => {
|
||||
chai.assert.equal(resp, false);
|
||||
chai.assert.isOk(count.calledOnce);
|
||||
chai.assert.equal(count.args[0][0].index, '.kibana-testdefaultindex');
|
||||
chai.assert.equal(count.args[0][0].type, 'index-pattern');
|
||||
})
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should return true if default pattern already exists', (done) => {
|
||||
let patternExists = require('../mt/kibana/defaultIndexPattern/_exists').default;
|
||||
|
||||
let count = sinon.stub();
|
||||
count.returns(Promise.resolve({ count: 1 }));
|
||||
server.plugins.elasticsearch.client.count = count;
|
||||
|
||||
patternExists(server, indexName)
|
||||
.then((resp) => {
|
||||
chai.assert.equal(resp, true);
|
||||
chai.assert.isOk(count.calledOnce);
|
||||
chai.assert.equal(count.args[0][0].index, '.kibana-testdefaultindex');
|
||||
chai.assert.equal(count.args[0][0].type, 'index-pattern');
|
||||
})
|
||||
.then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultIndexPattern_create', () => {
|
||||
it('should create pattern with proper value', (done) => {
|
||||
let createPattern = require('../mt/kibana/defaultIndexPattern/_create').default;
|
||||
|
||||
let create = sinon.stub();
|
||||
create.returns(Promise.resolve(true));
|
||||
server.plugins.elasticsearch.client.create = create;
|
||||
|
||||
let update = sinon.stub();
|
||||
update.returns(Promise.resolve(true));
|
||||
server.plugins.elasticsearch.client.update = update;
|
||||
|
||||
let refresh = sinon.stub();
|
||||
refresh.returns(Promise.resolve(true));
|
||||
server.plugins.elasticsearch.client.indices.refresh = refresh;
|
||||
|
||||
createPattern(server, indexName, userObj)
|
||||
.then((resp) => {
|
||||
chai.assert.isOk(create.calledOnce);
|
||||
chai.assert.equal(create.args[0][0].index, '.kibana-testdefaultindex');
|
||||
chai.assert.equal(create.args[0][0].type, 'index-pattern');
|
||||
chai.assert.equal(create.args[0][0].id, 'abcdef*');
|
||||
chai.assert.equal(create.args[0][0].body.title, 'abcdef*');
|
||||
chai.assert.equal(create.args[0][0].body.timeFieldName, '@timestamp');
|
||||
|
||||
chai.assert.isOk(update.calledOnce);
|
||||
chai.assert.equal(update.args[0][0].index, '.kibana-testdefaultindex');
|
||||
chai.assert.equal(update.args[0][0].type, 'config');
|
||||
chai.assert.equal(update.args[0][0].body.doc.defaultIndex, 'abcdef*');
|
||||
|
||||
chai.assert.isOk(refresh.called);
|
||||
chai.assert.equal(refresh.args[0][0].index, '.kibana-testdefaultindex');
|
||||
})
|
||||
.then(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -27,13 +27,22 @@ describe('plugins/fts-keystone', ()=> {
|
||||
let server;
|
||||
|
||||
beforeEach(()=> {
|
||||
let configGet = sinon.stub();
|
||||
configGet.withArgs('fts-keystone.cookie.name').returns('keystone');
|
||||
server = {
|
||||
log: sinon.stub()
|
||||
log: sinon.stub(),
|
||||
config: function () {
|
||||
return {
|
||||
get: configGet
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should return isBoom if session not available', ()=> {
|
||||
let request = {};
|
||||
let request = {
|
||||
state: {}
|
||||
};
|
||||
let errMsg = /Session support is missing/;
|
||||
|
||||
chai.expect(()=> {
|
||||
@ -41,14 +50,16 @@ describe('plugins/fts-keystone', ()=> {
|
||||
}).to.throw(errMsg);
|
||||
|
||||
request = {
|
||||
yar: undefined
|
||||
yar: undefined,
|
||||
state: {}
|
||||
};
|
||||
chai.expect(()=> {
|
||||
retrieveToken(server, request);
|
||||
}).to.throw(errMsg);
|
||||
|
||||
request = {
|
||||
session: null
|
||||
session: null,
|
||||
state: {}
|
||||
};
|
||||
chai.expect(()=> {
|
||||
retrieveToken(server, request);
|
||||
@ -62,9 +73,11 @@ describe('plugins/fts-keystone', ()=> {
|
||||
'get': sinon
|
||||
.stub()
|
||||
.withArgs(CONSTANTS.SESSION_TOKEN_KEY)
|
||||
.returns(undefined)
|
||||
.returns(undefined),
|
||||
'reset': sinon.stub()
|
||||
},
|
||||
headers: {}
|
||||
headers: {},
|
||||
state: {}
|
||||
};
|
||||
|
||||
let result = retrieveToken(server, request);
|
||||
@ -82,7 +95,8 @@ describe('plugins/fts-keystone', ()=> {
|
||||
};
|
||||
let request = {
|
||||
yar : yar,
|
||||
headers: {}
|
||||
headers: {},
|
||||
state: {}
|
||||
};
|
||||
let token;
|
||||
|
||||
@ -92,8 +106,6 @@ describe('plugins/fts-keystone', ()=> {
|
||||
chai.expect(token).not.to.be.undefined;
|
||||
chai.expect(token).to.be.eql(expectedToken);
|
||||
|
||||
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(
|
||||
@ -104,7 +116,7 @@ describe('plugins/fts-keystone', ()=> {
|
||||
it('should set token in session if not there and request has it', () => {
|
||||
let expectedToken = 'SOME_RANDOM_TOKEN';
|
||||
let yar = {
|
||||
'reset': sinon.spy(),
|
||||
'reset': sinon.stub(),
|
||||
'set' : sinon.spy(),
|
||||
'get' : sinon.stub()
|
||||
};
|
||||
@ -112,7 +124,8 @@ describe('plugins/fts-keystone', ()=> {
|
||||
yar : yar,
|
||||
headers: {
|
||||
'x-auth-token': expectedToken
|
||||
}
|
||||
},
|
||||
state: {}
|
||||
};
|
||||
let token;
|
||||
|
||||
@ -160,14 +173,15 @@ describe('plugins/fts-keystone', ()=> {
|
||||
let token;
|
||||
let request = {
|
||||
yar : yar,
|
||||
headers: headers
|
||||
headers: headers,
|
||||
state: {}
|
||||
};
|
||||
|
||||
token = retrieveToken(server, request);
|
||||
chai.expect(token).to.not.be.undefined;
|
||||
chai.expect(token).to.be.eql(RELOAD_SYMBOL);
|
||||
|
||||
chai.expect(yar.reset.calledOnce).to.be.ok;
|
||||
chai.expect(yar.reset.calledTwice).to.be.ok;
|
||||
chai.expect(yar.get.calledOnce).to.be.ok;
|
||||
|
||||
chai.expect(yar.set.callCount).to.be.eq(2);
|
||||
|
@ -20,3 +20,8 @@ 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');
|
||||
|
||||
export const RELOAD_MARKUP = `<html>
|
||||
<head><script type="text/javascript">window.location.reload();</script></head>
|
||||
<body>reloading...</body>
|
||||
</html>`;
|
||||
|
@ -15,14 +15,10 @@
|
||||
import Boom from 'boom';
|
||||
import Joi from 'joi';
|
||||
|
||||
import { SESSION_USER_KEY } from '../../const';
|
||||
import { SESSION_USER_KEY, RELOAD_MARKUP } 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 = {
|
||||
|
@ -16,6 +16,7 @@ import Boom from 'boom';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import kibanaIndex from '../kibana';
|
||||
import defaultIndexPattern from '../kibana/defaultIndexPattern';
|
||||
import userProjects from '../projects';
|
||||
|
||||
import {
|
||||
@ -40,6 +41,7 @@ export default (server) => {
|
||||
userProjects(server, session, userObj),
|
||||
kibanaIndex(server, userObj)
|
||||
])
|
||||
.then(defaultIndexPattern(server, userObj))
|
||||
.then(() => {
|
||||
server.log(['status', 'info', 'keystone'], `User ${userObj.user.id} authorized with keystone`);
|
||||
return token;
|
||||
|
@ -43,6 +43,14 @@ module.exports = (server, request) => {
|
||||
throw new Error('Session support is missing');
|
||||
}
|
||||
|
||||
// this is a workaround for problem with 'default' session:
|
||||
// when there is no session cookie present, then yar uses default session,
|
||||
// as a result many clients use the same session - security risk!
|
||||
const cookieName = server.config().get('fts-keystone.cookie.name');
|
||||
if (!request.state[cookieName]) {
|
||||
request.yar.reset();
|
||||
}
|
||||
|
||||
// DEV PURPOSE ONLY
|
||||
// request.yar.set(SESSION_TOKEN_KEY, 'a60e832483c34526a0c2bc3c6f8fa320');
|
||||
|
||||
|
@ -18,9 +18,9 @@ import {
|
||||
SESSION_PROJECTS_KEY,
|
||||
SESSION_USER_KEY,
|
||||
SESSION_TOKEN_CHANGED,
|
||||
TOKEN_CHANGED_VALUE
|
||||
TOKEN_CHANGED_VALUE,
|
||||
RELOAD_MARKUP
|
||||
} from '../../const';
|
||||
import reload from './reload';
|
||||
|
||||
export default () => {
|
||||
return (request, reply) => {
|
||||
@ -34,7 +34,7 @@ export default () => {
|
||||
'Detected that token has been changed, replaying the request'
|
||||
);
|
||||
session.clear(SESSION_TOKEN_CHANGED);
|
||||
return reply(reload.markup).type('text/html');
|
||||
return reply(RELOAD_MARKUP).type('text/html');
|
||||
} else if (userObj) {
|
||||
let expiresAt = new Date(userObj.expires_at).valueOf();
|
||||
let now = new Date().valueOf();
|
||||
@ -42,7 +42,7 @@ export default () => {
|
||||
|
||||
if (diff >= 0) {
|
||||
session.reset();
|
||||
return reply(Boom.unauthorized('User token has expired')).takeover();
|
||||
return reply(Boom.unauthorized('User token has expired'));
|
||||
} else {
|
||||
return reply.continue({
|
||||
credentials: userObj,
|
||||
|
@ -46,7 +46,7 @@ function bindAuthScheme(server) {
|
||||
server.auth.strategy(
|
||||
'session',
|
||||
'keystone-token',
|
||||
true,
|
||||
false,
|
||||
require('./auth/strategy')(server)
|
||||
)
|
||||
]);
|
||||
|
@ -36,6 +36,22 @@ export default (server, indexName) => {
|
||||
index: 'not_analyzed'
|
||||
}
|
||||
}
|
||||
},
|
||||
'index-pattern': {
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string'
|
||||
},
|
||||
timeFieldName: {
|
||||
type: 'string'
|
||||
},
|
||||
notExpandable: {
|
||||
type: 'boolean'
|
||||
},
|
||||
intervalName: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export function exists(server, indexName, status) {
|
||||
waitForActiveShards: 1
|
||||
};
|
||||
if (status) {
|
||||
opts.status = status;
|
||||
opts.waitForStatus = status;
|
||||
}
|
||||
return es.cluster.health(opts);
|
||||
}
|
||||
|
56
server/mt/kibana/defaultIndexPattern/_create.js
Normal file
56
server/mt/kibana/defaultIndexPattern/_create.js
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
export default (server, indexName, userObj) => {
|
||||
server.log(['status', 'info', 'keystone'], `Creating default index pattern for ${indexName}`);
|
||||
|
||||
const client = server.plugins.elasticsearch.client;
|
||||
const pattern = `${userObj.project.id}*`;
|
||||
|
||||
return client.create({
|
||||
index: indexName,
|
||||
type : 'index-pattern',
|
||||
body : {
|
||||
title: pattern,
|
||||
timeFieldName : server.config().get('fts-keystone.defaultTimeField')
|
||||
},
|
||||
id : pattern
|
||||
})
|
||||
.then(() => {
|
||||
return client.update({
|
||||
index: indexName,
|
||||
type: 'config',
|
||||
id: server.config().get('pkg.version'),
|
||||
body: {
|
||||
doc: {
|
||||
defaultIndex: pattern
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return client.indices.refresh({
|
||||
index: indexName,
|
||||
force: true
|
||||
});
|
||||
})
|
||||
.then((response) => {
|
||||
return Promise.resolve(response);
|
||||
})
|
||||
.catch((err)=> {
|
||||
throw new Error(`Unable to setup index pattern, error is ${err}`);
|
||||
});
|
||||
};
|
35
server/mt/kibana/defaultIndexPattern/_exists.js
Normal file
35
server/mt/kibana/defaultIndexPattern/_exists.js
Normal 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.
|
||||
*/
|
||||
|
||||
export default (server, indexName) => {
|
||||
const client = server.plugins.elasticsearch.client;
|
||||
const options = {
|
||||
index: indexName,
|
||||
type : 'index-pattern',
|
||||
ignoreUnavailable: true
|
||||
};
|
||||
|
||||
server.log(['status', 'debug', 'keystone'],
|
||||
`Checking if default index pattern for ${indexName} exists...`);
|
||||
|
||||
return client
|
||||
.count(options)
|
||||
.then((resp) => {
|
||||
return resp.count > 0;
|
||||
})
|
||||
.catch((err)=> {
|
||||
throw new Error(`Getting index-pattern for ${indexName} failed, error is ${err}`);
|
||||
});
|
||||
|
||||
};
|
35
server/mt/kibana/defaultIndexPattern/index.js
Normal file
35
server/mt/kibana/defaultIndexPattern/index.js
Normal 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 Promise from 'bluebird';
|
||||
import defaultIndexExists from './_exists';
|
||||
import createDefaultIndex from './_create';
|
||||
import kibanaIndex from '../kibanaIndex';
|
||||
|
||||
export default (server, userObj) => {
|
||||
return () => {
|
||||
const indexName = kibanaIndex(server, userObj);
|
||||
return defaultIndexExists(server, indexName)
|
||||
.then((exists) => {
|
||||
if (!exists) {
|
||||
server.log(['status', 'warning', 'keystone'],
|
||||
`Default index pattern for ${indexName} does not exist`);
|
||||
return createDefaultIndex(server, indexName, userObj);
|
||||
}
|
||||
server.log(['status', 'debug', 'keystone'],
|
||||
`Default index pattern for ${indexName} already exists`);
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
};
|
@ -19,6 +19,9 @@ module.exports = function defaultHandler(server, method, path) {
|
||||
return {
|
||||
method : method,
|
||||
path : path,
|
||||
config : {
|
||||
auth : 'session'
|
||||
},
|
||||
handler: {
|
||||
proxy: {
|
||||
mapUri : (request, done) => {
|
||||
|
@ -27,7 +27,7 @@ export default function (server, method, path) {
|
||||
method : method,
|
||||
path : path,
|
||||
config : {
|
||||
auth : false,
|
||||
auth : 'session',
|
||||
payload: {
|
||||
output: 'data',
|
||||
parse : false
|
||||
|
@ -26,7 +26,7 @@ export default function (server, method, path) {
|
||||
method : method,
|
||||
path : path,
|
||||
config : {
|
||||
auth : false,
|
||||
auth : 'session',
|
||||
payload: {
|
||||
output: 'data',
|
||||
parse : false
|
||||
|
@ -13,6 +13,7 @@
|
||||
*/
|
||||
|
||||
import Wreck from 'wreck';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { SESSION_USER_KEY } from '../../../const';
|
||||
import { getOpts } from '../_utils';
|
||||
@ -20,14 +21,15 @@ import kibanaIndex from '../../kibana/kibanaIndex';
|
||||
import mapUri from '../_map_uri';
|
||||
|
||||
export default function (server, method, path) {
|
||||
const defaultKibanaIndex = defaultKibanaIndex;
|
||||
const defaultKibanaIndex = server.config().get('kibana.index');
|
||||
const logIndexPostionInUrl = 3;
|
||||
|
||||
return {
|
||||
method : method,
|
||||
path : path,
|
||||
config : {
|
||||
tags: ['elasticsearch', 'multitenancy'],
|
||||
auth: false
|
||||
auth: 'session'
|
||||
},
|
||||
handler: handler
|
||||
};
|
||||
@ -42,7 +44,11 @@ export default function (server, method, path) {
|
||||
if (indexPos > -1) {
|
||||
url[indexPos] = kibanaIndex(server, session[SESSION_USER_KEY]);
|
||||
kibanaIndexRequest = true;
|
||||
} else if (url.length > logIndexPostionInUrl
|
||||
&& !url[logIndexPostionInUrl].startsWith(session[SESSION_USER_KEY].project.id)) {
|
||||
return reply(Boom.unauthorized('User does not have access to this resource'));
|
||||
}
|
||||
|
||||
url = url.join('/');
|
||||
|
||||
const opts = getOpts(server, request, url);
|
||||
|
Loading…
x
Reference in New Issue
Block a user