Commit 1c3f2f89 authored by Jochen Kressin's avatar Jochen Kressin
Browse files

Merge branch 'feature/mt-compact'

parents 1b02aa3e d4025b65
#!/bin/bash
PLUGIN_NAME=searchguard-kibana
PLUGIN_VERSION=5.2.0-1
PLUGIN_VERSION=5.2.2-GA
KIBANA_VERSION=5.2.2
echo "Building $PLUGIN_NAME-$PLUGIN_VERSION.zip"
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd $DIR/..
git clone https://github.com/elastic/kibana.git
cd "kibana"
git checkout tags/v$PLUGIN_VERSION
git checkout tags/v$KIBANA_VERSION
hash nvm 2>/dev/null || export NVM_DIR=~/.nvm; mkdir -p $NVM_DIR; . $(brew --prefix nvm)/nvm.sh
nvm install "$(cat .node-version)"
cd "$DIR"
......
......@@ -8,6 +8,7 @@ export default function (kibana) {
let authenticationBackend;
let searchGuardConfiguration;
return new kibana.Plugin({
name: 'searchguard',
require: ['kibana', 'elasticsearch'],
......@@ -19,17 +20,30 @@ export default function (kibana) {
secure: Joi.boolean().default(false),
name: Joi.string().default('searchguard_authentication'),
password: Joi.string().min(32).default('searchguard_cookie_default_password'),
ttl: Joi.number().integer().min(1).default(60 * 60 * 1000),
ttl: Joi.number().integer().min(0).default(60 * 60 * 1000),
}).default(),
session: Joi.object().keys({
ttl: Joi.number().integer().min(1).default(60 * 60 * 1000),
ttl: Joi.number().integer().min(0).default(60 * 60 * 1000),
keepalive: Joi.boolean().default(true),
}).default(),
basicauth: Joi.object().keys({
enabled: Joi.boolean().default(true)
enabled: Joi.boolean().default(true),
login: Joi.object().keys({
title: Joi.string().allow('').default('Please login to Kibana'),
subtitle: Joi.string().allow('').default('If you have forgotten your username or password, please ask your system administrator'),
showbrandimage: Joi.boolean().default(true),
brandimage: Joi.string().default("/plugins/searchguard/assets/searchguard_logo.svg"),
buttonstyle: Joi.string().allow('').default("")
}).default(),
}).default(),
multitenancy: Joi.object().keys({
enabled: Joi.boolean().default(false)
enabled: Joi.boolean().default(false),
show_roles: Joi.boolean().default(false),
enable_filter: Joi.boolean().default(false),
tenants: Joi.object().keys({
enable_private: Joi.boolean().default(true),
enable_global: Joi.boolean().default(true),
}).default(),
}).default()
}).default();
return obj;
......@@ -37,19 +51,42 @@ export default function (kibana) {
uiExports: {
hacks: [
'plugins/searchguard/chrome/multitenancy/enable_multitenancy',
'plugins/searchguard/chrome/logout_button',
'plugins/searchguard/services/access_control'
],
apps: [{
id: 'searchguard-login',
title: 'Login',
main: 'plugins/searchguard/apps/login',
hidden: true,
auth: false
}],
apps: [
{
id: 'searchguard-login',
title: 'Login',
main: 'plugins/searchguard/apps/login',
hidden: true,
auth: false
}
,
{
id: 'searchguard-multitenancy',
title: 'Tenants',
main: 'plugins/searchguard/apps/multitenancy',
hidden: false,
auth: true,
order: 9010,
icon: 'plugins/searchguard/assets/networking.svg',
}
],
chromeNavControls: [
'plugins/searchguard/chrome/btn_logout/btn_logout.js'
]
,
injectDefaultVars(server, options) {
options.multitenancy_enabled = server.config().get('searchguard.multitenancy.enabled');
options.basicauth_enabled = server.config().get('searchguard.basicauth.enabled');
options.kibana_index = server.config().get('kibana.index');
options.kibana_server_user = server.config().get('elasticsearch.username');
return options;
}
},
init(server, options) {
......@@ -58,6 +95,16 @@ export default function (kibana) {
API_ROOT = `${APP_ROOT}/api`;
const config = server.config();
// all your routes are belong to us
require('./lib/auth/routes_authinfo')(pluginRoot, server, this, APP_ROOT, API_ROOT);
this.apps.byId['searchguard-multitenancy'].hidden = false;
// provides authentication methods against Search Guard
const BackendClass = pluginRoot(`lib/backend/searchguard`);
const searchguardBackend = new BackendClass(server, server.config);
server.expose('getSearchGuardBackend', () => searchguardBackend);
if(config.get('searchguard.basicauth.enabled')) {
server.register([
require('hapi-async-handler'),
......@@ -81,18 +128,11 @@ export default function (kibana) {
this.status.yellow("'searchguard.cookie.secure' is set to false, cookies are transmitted over unsecure HTTP connection. Consider using HTTPS and set this key to 'true'");
}
// provides authentication methods against Search Guard
const BackendClass = pluginRoot(`lib/backend/searchguard`);
const authenticationBackend = new BackendClass(server, server.config);
// we use the cookie strategy
require('./lib/hapi/auth')(pluginRoot, server, APP_ROOT, API_ROOT);
// all your routes are belong to us
require('./lib/routes/routes')(pluginRoot, server, this, APP_ROOT, API_ROOT);
// make auth backend available
server.expose('getAuthenticationBackend', () => authenticationBackend);
require('./lib/auth/routes')(pluginRoot, server, this, APP_ROOT, API_ROOT);
this.status.yellow('Search Guard HTTP Basic Authentication enabled.');
......@@ -102,6 +142,47 @@ export default function (kibana) {
this.status.yellow('Search Guard HTTP Basic Authentication is disabled.');
}
if(config.get('searchguard.multitenancy.enabled')) {
// sanity check - header whitelisted?
var headersWhitelist = config.get('elasticsearch.requestHeadersWhitelist');
if (headersWhitelist.indexOf('sg_tenant') == -1) {
this.status.red('No tenant header found in whitelist. Please add sg_tenant to elasticsearch.requestHeadersWhitelist in kibana.yml');
return;
}
require('./lib/multitenancy/routes')(pluginRoot, server, this, APP_ROOT, API_ROOT);
require('./lib/multitenancy/headers')(pluginRoot, server, this, APP_ROOT, API_ROOT);
server.state('searchguard_preferences', {
ttl: 2217100485000,
path: '/',
isSecure: false,
isHttpOnly: false,
clearInvalid: true, // remove invalid cookies
strictHeader: true, // don't allow violations of RFC 6265
encoding: 'iron',
password: config.get("searchguard.cookie.password")
});
server.state('searchguard_tenant', {
ttl: null,
path: '/',
isSecure: false,
isHttpOnly: false,
clearInvalid: true, // remove invalid cookies
strictHeader: true, // don't allow violations of RFC 6265
encoding: 'iron',
password: config.get("searchguard.cookie.password")
});
this.status.yellow("Search Guard multitenancy enabled");
} else {
this.status.yellow("Search Guard multitenancy disabled");
}
this.status.green('Search Guard plugin initialised.');
}
......
......@@ -16,6 +16,7 @@
import Boom from 'boom';
import Joi from 'joi';
import { isEmpty } from 'lodash';
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
......@@ -46,7 +47,7 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
handler: {
async: async (request, reply) => {
try {
let user = await server.plugins.searchguard.getAuthenticationBackend().authenticate(request.payload);
let user = await server.plugins.searchguard.getSearchGuardBackend().authenticate(request.payload);
let session = {
username: user.username,
credentials: user.credentials,
......@@ -56,14 +57,31 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
session.expiryTime = Date.now() + sessionTTL;
}
request.auth.session.set(session);
return reply({
username: user.username
});
// handle tenants if MT is enabled
if(server.config().get("searchguard.multitenancy.enabled")) {
// get the preferred tenant of the user
let globalTenantEnabled = server.config().get("searchguard.multitenancy.tenants.enable_global");
let privateTenantEnabled = server.config().get("searchguard.multitenancy.tenants.enable_private");
let preferredTenant = server.plugins.searchguard.getSearchGuardBackend().getTenantByPreference(request, user.username, user.tenants, globalTenantEnabled, privateTenantEnabled);
return reply({
username: user.username,
tenants: user.tenants
}).state('searchguard_tenant', preferredTenant);
} else {
// no MT, nothing more to do
return reply({
username: user.username,
tenants: user.tenants
});
}
} catch (error) {
if (error instanceof AuthenticationError) {
return reply(Boom.unauthorized(error.message));
} else {
return reply(Boom.badImplementation());
return reply(Boom.badImplementation(error.message));
}
}
}
......@@ -84,7 +102,10 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
path: `${API_ROOT}/v1/auth/logout`,
handler: (request, reply) => {
request.auth.session.clear();
reply({});
reply({}).unstate('searchguard_tenant');
},
config: {
auth: false
}
});
......
/**
* Copyright 2016 floragunn GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Boom from 'boom';
import Joi from 'joi';
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
const AuthenticationError = pluginRoot('lib/auth/authentication_error');
const config = server.config();
const sessionTTL = config.get('searchguard.session.ttl');
const loginApp = kbnServer.apps.byId['searchguard-login'];
server.route({
method: 'GET',
path: `${API_ROOT}/v1/auth/authinfo`,
handler: (request, reply) => {
try {
let authinfo = server.plugins.searchguard.getSearchGuardBackend().authinfo(request.headers.authorization);
return reply(authinfo);
} catch(error) {
return reply(Boom.badImplementation());
}
}
});
}; //end module
......@@ -33,6 +33,13 @@ export default class User {
return this._roles;
}
/**
* @property {Array} tenants - The user tenants.
*/
get tenants() {
return this._tenants;
}
/**
* @property {object} credentials - The credentials that were used to authenticate the user.
*/
......@@ -48,11 +55,12 @@ export default class User {
return this._proxyCredentials;
}
constructor(username, credentials, proxyCredentials, roles) {
constructor(username, credentials, proxyCredentials, roles, tenants) {
this._username = username;
this._credentials = credentials;
this._proxyCredentials = proxyCredentials;
this._roles = roles;
this._tenants = tenants;
}
}
......@@ -14,12 +14,13 @@
limitations under the License.
*/
import _ from 'lodash';
import SearchGuardPlugin from './searchguard_plugin';
import AuthenticationError from '../auth/authentication_error';
import User from '../auth/user';
/**
* The SearchGuard authentication backend.
* The SearchGuard backend.
*/
export default class SearchGuardBackend {
......@@ -39,7 +40,41 @@ export default class SearchGuardBackend {
authorization: `Basic ${authHeader}`
}
});
return new User(credentials.username, credentials, credentials, response.sg_roles);
return new User(credentials.username, credentials, credentials, response.sg_roles, response.sg_tenants);
} catch(error) {
if (error.status == 401) {
throw new AuthenticationError("Invalid username or password");
} else {
throw new Error(error.message);
}
}
}
async authinfo(authHeader) {
try {
const response = await this._client.searchguard.authinfo({
headers: {
authorization: authHeader
}
});
return response
} catch(error) {
if (error.status == 401) {
throw new AuthenticationError();
} else {
throw error;
}
}
}
async multitenancyinfo(authHeader) {
try {
const response = await this._client.searchguard.multitenancyinfo({
headers: {
authorization: authHeader
}
});
return response
} catch(error) {
if (error.status == 401) {
throw new AuthenticationError();
......@@ -56,4 +91,59 @@ export default class SearchGuardBackend {
'authorization': `Basic ${authHeader}`
};
}
updateAndGetTenantPreferences(request, user, tenant) {
var prefs = request.state.searchguard_preferences;
// no prefs cookie present
if (!prefs) {
var newPrefs = {};
newPrefs[user] = tenant;
return newPrefs;
}
prefs[user] = tenant;
return prefs;
}
getTenantByPreference(request, username, tenants, globalEnabled, privateEnabled) {
// delete user from tenants first to check if we have a tenant to choose from at all
// keep original preferences untouched, we need the original values again
// http://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object
var tenantsCopy = JSON.parse(JSON.stringify(tenants));
delete tenantsCopy[username];
// sanity check
if (!globalEnabled && !privateEnabled && _.isEmpty(tenantsCopy)) {
return null;
}
// get users preferred tenant
var prefs = request.state.searchguard_preferences;
if (prefs) {
var preferredTenant = prefs[username];
// user has a preferred tenant, check if it is accessible
if (preferredTenant && tenants[preferredTenant]) {
return preferredTenant;
}
// special case: in tenants returned from SG, the private tenant is
// the username of the logged in user, but the header value is __user__
if (preferredTenant == "__user__" && tenants[username] && privateEnabled) {
return "__user__";
}
}
// no preference, or tenant no accessible anymore, choose either global or private
if (globalEnabled) {
return "";
}
if (privateEnabled) {
return "__user__";
}
// this point can be reached if global and private are disabled,
// and the preferred tenant is not accessible anymore.
}
}
......@@ -32,5 +32,11 @@ export default function (Client, config, components) {
fmt: url
}
});
Client.prototype.searchguard.prototype.multitenancyinfo = ca({
url: {
fmt: '_searchguard/kibanainfo'
}
});
};
......@@ -38,7 +38,8 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
if (request.url.path.indexOf(API_ROOT) === 0 || request.method !== 'get') {
return reply(Boom.forbidden(error));
} else {
return reply.redirect(`${basePath}${APP_ROOT}/login`);
const nextUrl = encodeURIComponent(request.path);
return reply.redirect(`${basePath}${APP_ROOT}/login?nextUrl=${nextUrl}`);
}
}
reply.continue({credentials});
......@@ -49,9 +50,8 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
server.auth.strategy('sg_access_control', 'sg_access_control_scheme', true);
server.ext('onPostAuth', function (request, next) {
if (request.auth && request.auth.isAuthenticated) {
const backend = server.plugins.searchguard.getAuthenticationBackend();
const backend = server.plugins.searchguard.getSearchGuardBackend();
return backend.getAuthHeaders(request.auth.credentials)
.then((headers) => {
assign(request.headers, headers);
......
/**
* Copyright 2016 floragunn GmbH
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 {assign} from 'lodash';
export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
const config = server.config();
const basePath = config.get('server.basePath');
server.ext('onPostAuth', function (request, next) {
var selectedTenant = request.state.searchguard_tenant;
if (selectedTenant != null) {
assign(request.headers, {'sg_tenant' : selectedTenant});
}
return next.continue();
});
}
/**
* Copyright 2016 floragunn GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Boom from 'boom';
import Joi from 'joi';
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
const AuthenticationError = pluginRoot('lib/auth/authentication_error');
const config = server.config();
const sessionTTL = config.get('searchguard.session.ttl');
const loginApp = kbnServer.apps.byId['searchguard-login'];
const backend = server.plugins.searchguard.getSearchGuardBackend();
server.route({
method: 'POST',
path: `${API_ROOT}/v1/multitenancy/tenant`,
handler: (request, reply) => {
var username = request.payload.username;
var selectedTenant = request.payload.tenant;
var prefs = backend.updateAndGetTenantPreferences(request, username, selectedTenant);
return reply(request.payload.tenant).state('searchguard_tenant', selectedTenant).state('searchguard_preferences', prefs);
}
});
server.route({
method: 'GET',
path: `${API_ROOT}/v1/multitenancy/tenant`,
handler: (request, reply) => {
return reply(request.state.searchguard_tenant);
}
});
server.route({
method: 'GET',
path: `${API_ROOT}/v1/multitenancy/info`,
handler: (request, reply) => {
let mtinfo = server.plugins.searchguard.getSearchGuardBackend().multitenancyinfo(request.headers.authorization);
return reply(mtinfo);
}
});
}; //end module
......@@ -25,7 +25,7 @@ export default function (server) {
return function validate(request, session, callback) {
try {
const backend = server.plugins.searchguard.getAuthenticationBackend();
const backend = server.plugins.searchguard.getSearchGuardBackend();
if (sessionTTL) {
if (!session.expiryTime || session.expiryTime < Date.now()) {
return callback(new InvalidSessionError('Session expired.'), false);
......
{
"name": "searchguard",
"version": "5.2.0",
"version": "5.2.2",
"description": "Search Guard features for kibana",
"main": "index.js",
"homepage": "https://floragunn.com",
......
<div class="container login-wrapper">
<div class="container login-wrapper" style='{{ui.showbrandimage? "" : "top:30%"}}'>
<p class="login-title">Login to Kibana</p>
<div class="text-center brand-image-container" ng-show="ui.showbrandimage">
<img class="brand-image" src="{{ui.brandimage}}" width="300">
</div>
<p class="login-title" ng-show="ui.logintitle != ''">{{ui.logintitle}}</p>
<p class="login-subtitle" ng-show="ui.loginsubtitle != ''">{{ui.loginsubtitle}}</p>
<form class="login-form" ng-submit="ui.submit()" method="post">
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-user"></i></div>
<input type="text" class="form-control" id="username" name="username" placeholder="Username"
<input type="text" class="form-control kuiTextInput" id="username" name="username" placeholder="Username"
ng-model="ui.credentials.username" required/>
</div>