Merge pull request #3 from vulnbe/6.6.1

Kibana 6.6.1
This commit is contained in:
Alexey 2019-04-04 11:40:46 +03:00 committed by GitHub
commit b102040d0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1024 additions and 681 deletions

111
index.js
View File

@ -3,6 +3,7 @@ import { resolve, join, sep } from 'path';
import { has } from 'lodash';
import indexTemplate from './lib/elasticsearch/setup_index_template';
import { migrateTenants } from './lib/multitenancy/migrate_tenants';
import { version as opendistro_version } from './package.json';
export default function (kibana) {
@ -29,6 +30,8 @@ export default function (kibana) {
name: Joi.string().default('security_authentication'),
password: Joi.string().min(32).default('security_cookie_default_password'),
ttl: Joi.number().integer().min(0).default(60 * 60 * 1000),
domain: Joi.string(),
isSameSite: Joi.valid('Strict', 'Lax').allow(false).default(false),
}).default(),
session: Joi.object().keys({
ttl: Joi.number().integer().min(0).default(60 * 60 * 1000),
@ -97,12 +100,15 @@ export default function (kibana) {
proxycache: Joi.object().keys({
user_header: Joi.string(),
roles_header: Joi.string(),
proxy_header: Joi.string().default('x-forwarded-for'),
proxy_header_ip: Joi.string(),
login_endpoint: Joi.string().allow('', null).default(null),
}).default().when('auth.type', {
is: 'proxycache',
then: Joi.object({
user_header: Joi.required(),
roles_header: Joi.required()
roles_header: Joi.required(),
proxy_header_ip: Joi.required()
})
}),
jwt: Joi.object().keys({
@ -142,6 +148,7 @@ export default function (kibana) {
replaceInjectedVars: async function(originalInjectedVars, request, server) {
const authType = server.config().get('opendistro_security.auth.type');
// Make sure securityDynamic is always available to the frontend, no matter what
// Remember that these values are only updated on page load.
let securityDynamic = {};
let userInfo = null;
@ -166,13 +173,36 @@ export default function (kibana) {
}
if (userInfo) {
securityDynamic.user = userInfo;
securityDynamic.user = userInfo;
}
} catch (error) {
// Don't to anything here.
// If there's an error, it's probably because x-pack security is enabled.
}
if(server.config().get('opendistro_security.multitenancy.enabled')) {
let currentTenantName = 'global';
let currentTenant = '';
if (typeof request.headers['securitytenant'] !== 'undefined') {
currentTenant = request.headers['securitytenant'];
} else if (request.headers['security_tenant'] !== 'undefined') {
currentTenant = request.headers['security_tenant'];
}
currentTenantName = currentTenant;
if (currentTenant === '') {
currentTenantName = 'global';
} else if (currentTenant === '__user__') {
currentTenantName = 'private';
}
securityDynamic.multiTenancy = {
currentTenantName: currentTenantName,
currentTenant: currentTenant
};
}
return {
...originalInjectedVars,
securityDynamic
@ -236,13 +266,14 @@ export default function (kibana) {
options.basicauth_enabled = server.config().get('opendistro_security.basicauth.enabled');
options.kibana_index = server.config().get('kibana.index');
options.kibana_server_user = server.config().get('elasticsearch.username');
options.opendistro_version = opendistro_version;
return options;
}
},
init(server, options) {
async init(server, options) {
APP_ROOT = '';
API_ROOT = `${APP_ROOT}/api/v1`;
@ -257,9 +288,9 @@ export default function (kibana) {
}
});
if (xpackInstalled && config.get('xpack.opendistro_security.enabled') !== false) {
if (xpackInstalled && config.get('xpack.security.enabled') !== false) {
// It seems like X-Pack is installed and enabled, so we show an error message and then exit.
this.status.red("X-Pack Security needs to be disabled for Security to work properly. Please set 'xpack.opendistro_security.enabled' to false in your kibana.yml");
this.status.red("X-Pack Security needs to be disabled for Security to work properly. Please set 'xpack.security.enabled' to false in your kibana.yml");
return false;
}
} catch (error) {
@ -280,8 +311,6 @@ export default function (kibana) {
const securityConfigurationBackend = new ConfigurationBackendClass(server, server.config);
server.expose('getSecurityConfigurationBackend', () => securityConfigurationBackend);
server.register([require('hapi-async-handler')]);
let authType = config.get('opendistro_security.auth.type');
let authClass = null;
@ -300,25 +329,28 @@ export default function (kibana) {
}
// Set up the storage cookie
server.state('security_storage', {
let storageCookieConf = {
path: '/',
ttl: null, // Cookie deleted when the browser is closed
password: config.get('opendistro_security.cookie.password'),
encoding: 'iron',
isSecure: config.get('opendistro_security.cookie.secure'),
});
isSameSite: config.get('opendistro_security.cookie.isSameSite')
};
if (config.get('opendistro_security.cookie.domain')) {
storageCookieConf["domain"] = config.get('opendistro_security.cookie.domain');
}
server.state('security_storage', storageCookieConf);
if (authType && authType !== '' && ['basicauth', 'jwt', 'openid', 'saml', 'proxycache'].indexOf(authType) > -1) {
server.register([
require('hapi-auth-cookie'),
], (error) => {
if (error) {
server.log(['error', 'security'], `An error occurred registering server plugins: ${error}`);
this.status.red('An error occurred during initialisation, please check the logs.');
return;
}
try {
await server.register({
plugin: require('hapi-auth-cookie')
});
this.status.yellow('Initialising Security authentication plugin.');
@ -350,15 +382,28 @@ export default function (kibana) {
}
if (authClass) {
authClass.init();
try {
// At the moment this is mainly to catch an error where the openid connect_url is wrong
await authClass.init();
} catch (error) {
server.log(['error', 'security'], `An error occurred while enabling session management: ${error}`);
this.status.red('An error occurred during initialisation, please check the logs.');
return;
}
this.status.yellow('Security session management enabled.');
}
});
} catch (error) {
server.log(['error', 'security'], `An error occurred registering server plugins: ${error}`);
this.status.red('An error occurred during initialisation, please check the logs.');
return;
}
} else {
// @todo await/async
// Register the storage plugin for the other auth types
server.register({
register: pluginRoot('lib/session/sessionPlugin'),
plugin: pluginRoot('lib/session/sessionPlugin'),
options: {
authType: null,
}
@ -379,14 +424,14 @@ export default function (kibana) {
}
if (config.has('xpack.spaces.enabled') && config.get('xpack.spaces.enabled')) {
this.status.red('At the moment it is not possible to have both Spaces and multitenancy enabled. Please set xpack.spaces.enabled to false.');
return;
this.status.red('At the moment it is not possible to have both Spaces and multitenancy enabled. Please set xpack.spaces.enabled to false.');
return;
}
require('./lib/multitenancy/routes')(pluginRoot, server, this, APP_ROOT, API_ROOT);
require('./lib/multitenancy/headers')(pluginRoot, server, this, APP_ROOT, API_ROOT, authClass);
server.state('security_preferences', {
let preferenceCookieConf = {
ttl: 2217100485000,
path: '/',
isSecure: false,
@ -394,8 +439,15 @@ export default function (kibana) {
clearInvalid: true, // remove invalid cookies
strictHeader: true, // don't allow violations of RFC 6265
encoding: 'iron',
password: config.get("opendistro_security.cookie.password")
});
password: config.get("opendistro_security.cookie.password"),
isSameSite: config.get('opendistro_security.cookie.isSameSite')
};
if (config.get('opendistro_security.cookie.domain')) {
preferenceCookieConf["domain"] = config.get('opendistro_security.cookie.domain');
}
server.state('security_preferences', preferenceCookieConf);
this.status.yellow("Security multitenancy registered.");
} else {
@ -417,7 +469,6 @@ export default function (kibana) {
// create index template for tenant indices
if(config.get('opendistro_security.multitenancy.enabled')) {
const { setupIndexTemplate, waitForElasticsearchGreen } = indexTemplate(this, server);
//const {migrateTenants} = tenantMigrator(this, server);
waitForElasticsearchGreen().then( () => {
this.status.yellow('Setting up index template.');
@ -425,7 +476,7 @@ export default function (kibana) {
migrateTenants(server)
.then( () => {
this.status.green('Tenant indices migrated.');
this.status.green('Opendistro Security plugin version '+ opendistro_version + ' initialized.');
})
.catch((error) => {
this.status.yellow('Tenant indices migration failed');
@ -434,7 +485,7 @@ export default function (kibana) {
});
} else {
this.status.green('Security plugin initialised.');
this.status.green('Opendistro Security plugin version '+ opendistro_version + ' initialized.');
}
// Using an admin certificate may lead to unintended consequences

View File

@ -46,9 +46,12 @@ export function parseNextUrl(nextUrl, basePath) {
if (protocol !== null || hostname !== null || port !== null) {
return `${basePath}/`;
}
// We always need the base path
if (!String(pathname).startsWith(basePath)) {
if (nextUrl && nextUrl != null && nextUrl.startsWith("/")) {
nextUrl = nextUrl.substring(1);
}
return `${basePath}/${nextUrl}`;
}

View File

@ -37,13 +37,13 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${API_ROOT}/auth/authinfo`,
handler: (request, reply) => {
handler: async(request, h) => {
try {
let authinfo = server.plugins.opendistro_security.getSecurityBackend().authinfo(request.headers);
return reply(authinfo);
return authinfo;
} catch(error) {
if (error.isBoom) {
return reply(error);
return error;
}
throw error;
}

View File

@ -68,17 +68,29 @@ export default class AuthType {
* @type {string}
*/
this.authHeaderName = 'authorization';
/**
* This is a workaround for keeping track of what caused hapi-auth-cookie's validateFunc to fail.
* There seems to be an issue with how the plugin checks the thrown error and instead of passing
* it on, it throws its own error.
*
* @type {null}
* @private
*/
this._cookieValidationError;
}
init() {
async init() {
this.setupStorage();
// Setting up routes before the auth scheme, mainly for the case where something goes wrong
// when OpenId tries to get the connect_url
await this.setupRoutes();
this.setupAuthScheme();
this.setupRoutes();
}
setupStorage() {
this.server.register({
register: this.pluginRoot('lib/session/sessionPlugin'),
plugin: this.pluginRoot('lib/session/sessionPlugin'),
options: {
authType: this.type,
authHeaderName: this.authHeaderName,
@ -95,9 +107,14 @@ export default class AuthType {
isSecure: this.config.get('opendistro_security.cookie.secure'),
validateFunc: this.sessionValidator(this.server),
clearInvalid: true,
ttl: this.config.get('opendistro_security.cookie.ttl')
ttl: this.config.get('opendistro_security.cookie.ttl'),
isSameSite: this.config.get('opendistro_security.cookie.isSameSite')
};
if (this.config.get('opendistro_security.cookie.domain')) {
cookieConfig["domain"] = this.config.get('opendistro_security.cookie.domain');
}
return cookieConfig;
}
@ -149,7 +166,7 @@ export default class AuthType {
throw new Error('The authenticate method must be implemented by the sub class');
}
onUnAuthenticated(request, reply) {
onUnAuthenticated(request, h) {
throw new Error('The onUnAuthenticated method must be implemented by the sub class');
}
@ -158,57 +175,55 @@ export default class AuthType {
}
setupAuthScheme() {
this.server.auth.strategy('security_access_control_cookie', 'cookie', false, this.getCookieConfig());
this.server.auth.scheme('security_access_control_scheme', (server, options) => ({
authenticate: (request, reply) => {
authenticate: async(request, h) => {
let credentials = null;
// let configured routes that are not under our control pass,
// for example /api/status to check Kibana status without a logged in user
if (this.unauthenticatedRoutes.includes(request.path)) {
var credentials = this.server.plugins.opendistro_security.getSecurityBackend().getServerUser();
reply.continue({credentials});
return;
credentials = this.server.plugins.opendistro_security.getSecurityBackend().getServerUser();
return h.authenticated({credentials});
};
this.server.auth.test('security_access_control_cookie', request, async(error, credentials) => {
if (error) {
let authHeaderCredentials = this.detectAuthHeaderCredentials(request);
if (authHeaderCredentials) {
try {
let {session} = await request.auth.securitySessionStorage.authenticate(authHeaderCredentials);
try {
credentials = await this.server.auth.test('security_access_control_cookie', request);
return h.authenticated({credentials})
} catch(error) {
if (this._cookieValidationError) {
return this.onUnAuthenticated(request, h, this._cookieValidationError).takeover();
}
// Returning the session equals setting the values with hapi-auth-cookie@set()
return reply.continue({
// Watch out here - hapi-auth-cookie requires us to send back an object with credentials
// as a key. Otherwise other values than the credentials will be overwritten
credentials: session
});
} catch (authError) {
return this.onUnAuthenticated(request, reply, authError);
}
}
let authHeaderCredentials = this.detectAuthHeaderCredentials(request);
if (authHeaderCredentials) {
try {
let {session} = await request.auth.securitySessionStorage.authenticate(authHeaderCredentials);
if (request.headers) {
// If the session has expired, we may receive ajax requests that can't handle a 302 redirect.
// In this case, we trigger a 401 and let the interceptor handle the redirect on the client side.
if ((request.headers.accept && request.headers.accept.split(',').indexOf('application/json') > -1)
|| (request.headers['content-type'] && request.headers['content-type'].indexOf('application/json') > -1)) {
return reply({message: 'Session expired', redirectTo: 'login'}).code(401);
}
// Cookie auth failed, user is not authenticated
return this.onUnAuthenticated(request, reply, error);
// Returning the session equals setting the values with hapi-auth-cookie@set()
return h.authenticated({
// Watch out here - hapi-auth-cookie requires us to send back an object with credentials
// as a key. Otherwise other values than the credentials will be overwritten
credentials: session
});
} catch (authError) {
return this.onUnAuthenticated(request, h, authError).takeover();
}
}
// credentials are everything that is in the auth cookie
reply.continue(credentials);
});
}
return this.onUnAuthenticated(request, h).takeover();
}
}));
this.server.auth.strategy('security_access_control', 'security_access_control_scheme', this.getCookieConfig());
this.server.auth.strategy('security_access_control_cookie', 'cookie', this.getCookieConfig());
// Activates hapi-auth-cookie for ALL routes, unless
// a) the route is listed in "unauthenticatedRoutes" or
// b) the auth option in the route definition is explicitly set to false
this.server.auth.strategy('security_access_control', 'security_access_control_scheme', true);
this.server.auth.default({
mode: 'required', // @todo Investigate best mode here
strategy: 'security_access_control' // This seems to be the only way to apply the strategy to ALL routes, even those defined before we add the strategy.
});
}
/**
@ -218,10 +233,13 @@ export default class AuthType {
*/
sessionValidator(server) {
let validate = async(request, session, callback) => {
let validate = async(request, session) => {
this._cookieValidationError = null;
if (session.authType !== this.type) {
return callback(new InvalidSessionError('Invalid session'), false, null);
this._cookieValidationError = new InvalidSessionError('Invalid session');
request.auth.securitySessionStorage.clearStorage();
return {valid: false};
}
// Check if we have auth header credentials set that are different from the session credentials
@ -229,22 +247,24 @@ export default class AuthType {
if (differentAuthHeaderCredentials) {
try {
let authResponse = await request.auth.securitySessionStorage.authenticate(differentAuthHeaderCredentials);
return callback(null, true, {credentials: authResponse.session});
return {valid: true, credentials: authResponse.session};
} catch(error) {
request.auth.securitySessionStorage.clearStorage();
return callback(error, false);
return {valid: false};
}
}
// If we are still here, we need to compare the expiration time
// JWT's .exp is denoted in seconds, not milliseconds.
if (session.exp && session.exp < Math.floor(Date.now() / 1000)) {
this._cookieValidationError = new SessionExpiredError('Session expired');
request.auth.securitySessionStorage.clearStorage();
return callback(new SessionExpiredError('Session expired.'), false);
return {valid: false};
} else if (!session.exp && this.sessionTTL) {
if (!session.expiryTime || session.expiryTime < Date.now()) {
this._cookieValidationError = new SessionExpiredError('Session expired');
request.auth.securitySessionStorage.clearStorage();
return callback(new SessionExpiredError('Session expired.'), false);
return {valid: false};
}
if (this.sessionKeepAlive) {
@ -253,12 +273,12 @@ export default class AuthType {
// should be equivalent to calling request.auth.session.set(),
// but it seems like the cookie's browser lifetime isn't updated.
// Hence, we need to set it explicitly.
request.auth.session.set(session);
// @todo TEST IF THIS HAS BEEN FIXED IN HAPI-AUTH-COOKIE
request.cookieAuth.set(session);
}
}
// All good, return the session as it was
return callback(null, true, {credentials: session});
return {valid: true, credentials: session};
};
@ -269,15 +289,31 @@ export default class AuthType {
* Add credential headers to the passed request.
* @param request
*/
assignAuthHeader(request) {
async assignAuthHeader(request) {
if (! request.headers[this.authHeaderName]) {
const session = request.state[this.config.get('opendistro_security.cookie.name')];
let session = request.state[this.config.get('opendistro_security.cookie.name')];
if (session) {
const sessionValidator = this.sessionValidator();
try {
const sessionValidationResult = await sessionValidator(request, session);
if (sessionValidationResult.valid) {
session = sessionValidationResult.credentials;
} else {
session = false;
}
} catch(error) {
this.server.log(['security', 'error'], `An error occurred while computing auth headers, clearing session: ${error}`);
}
}
if (session && session.credentials) {
try {
let authHeader = this.getAuthHeader(session);
if (authHeader !== false) {
this.addAdditionalAuthHeaders(request, authHeader);
assign(request.headers, authHeader);
}
} catch (error) {
@ -295,14 +331,24 @@ export default class AuthType {
* Used to add the credentials header to the request.
*/
registerAssignAuthHeader() {
this.server.ext('onPreAuth', (request, next) => {
this.server.ext('onPreAuth', (request, h) => {
try {
this.assignAuthHeader(request);
} catch(error) {
return next.redirect(this.basePath + '/customerror?type=authError');
return h.redirect(this.basePath + '/customerror?type=authError');
}
return next.continue();
return h.continue;
});
}
/**
* Method for adding additional auth type specific authentication headers.
* Override this in the auth type for type specific headers.
* @param request
* @param authHeader
*/
addAdditionalAuthHeaders(request, authHeader) {
}
}

View File

@ -54,7 +54,7 @@ export default class BasicAuth extends AuthType {
* @type {boolean}
*/
this.loadBalancerURL = this.config.get('opendistro_security.basicauth.loadbalancer_url');
/**
* Allow anonymous access?
* @type {boolean}
@ -125,23 +125,23 @@ export default class BasicAuth extends AuthType {
}
}
onUnAuthenticated(request, reply, error) {
onUnAuthenticated(request, h, error) {
if (error instanceof MissingRoleError) {
return reply.redirect(this.basePath + '/customerror?type=missingRole')
return h.redirect(this.basePath + '/customerror?type=missingRole')
}
const nextUrl = encodeURIComponent(request.url.path);
if (this.anonymousAuthEnabled) {
return reply.redirect(`${this.basePath}${this.APP_ROOT}/auth/anonymous?nextUrl=${nextUrl}`);
return h.redirect(`${this.basePath}${this.APP_ROOT}/auth/anonymous?nextUrl=${nextUrl}`);
}
if (this.loadBalancerURL) {
return reply.redirect(`${this.loadBalancerURL}${this.basePath}${this.APP_ROOT}/login?nextUrl=${nextUrl}`);
return h.redirect(`${this.loadBalancerURL}${this.basePath}${this.APP_ROOT}/login?nextUrl=${nextUrl}`);
}
return reply.redirect(`${this.basePath}${this.APP_ROOT}/login?nextUrl=${nextUrl}`);
return h.redirect(`${this.basePath}${this.APP_ROOT}/login?nextUrl=${nextUrl}`);
}
setupRoutes() {

View File

@ -49,45 +49,43 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${APP_ROOT}/login`,
handler: {
async: async(request, reply) => {
try {
const basePath = config.get('server.basePath');
async handler(request, h) {
try {
const basePath = config.get('server.basePath');
// Check if we have alternative login headers
const alternativeHeaders = config.get('opendistro_security.basicauth.alternative_login.headers');
if (alternativeHeaders && alternativeHeaders.length) {
let requestHeaders = Object.keys(request.headers).map(header => header.toLowerCase());
let foundHeaders = alternativeHeaders.filter(header => requestHeaders.indexOf(header.toLowerCase()) > -1);
if (foundHeaders.length) {
let {session} = await request.auth.securitySessionStorage.authenticateWithHeaders(request.headers);
// Check if we have alternative login headers
const alternativeHeaders = config.get('opendistro_security.basicauth.alternative_login.headers');
if (alternativeHeaders && alternativeHeaders.length) {
let requestHeaders = Object.keys(request.headers).map(header => header.toLowerCase());
let foundHeaders = alternativeHeaders.filter(header => requestHeaders.indexOf(header.toLowerCase()) > -1);
if (foundHeaders.length) {
let {session} = await request.auth.securitySessionStorage.authenticateWithHeaders(request.headers);
let nextUrl = null;
if (request.url && request.url.query && request.url.query.nextUrl) {
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
}
if (nextUrl) {
nextUrl = parseNextUrl(nextUrl, basePath);
return reply.redirect(nextUrl);
}
return reply.redirect(basePath + '/app/kibana');
let nextUrl = null;
if (request.url && request.url.query && request.url.query.nextUrl) {
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
}
}
} catch (error) {
if (error instanceof MissingRoleError) {
return reply.redirect(basePath + '/customerror?type=missingRole');
} else if (error instanceof MissingTenantError) {
return reply.redirect(basePath + '/customerror?type=missingTenant');
}
// Let normal authentication errors through(?) and just go to the regular login page?
}
return reply.renderAppWithDefaultConfig(loginApp);
if (nextUrl) {
nextUrl = parseNextUrl(nextUrl, basePath);
return h.redirect(nextUrl);
}
return h.redirect(basePath + '/app/kibana');
}
}
} catch (error) {
if (error instanceof MissingRoleError) {
return h.redirect(basePath + '/customerror?type=missingRole');
} else if (error instanceof MissingTenantError) {
return h.redirect(basePath + '/customerror?type=missingTenant');
}
// Let normal authentication errors through(?) and just go to the regular login page?
}
return h.renderAppWithDefaultConfig(loginApp);
},
config: {
options: {
auth: false
}
});
@ -95,67 +93,68 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${API_ROOT}/auth/login`,
handler: {
async: async (request, reply) => {
try {
// In order to prevent direct access for certain usernames (e.g. service users like
// kibanaserver, logstash etc.) we can add them to basicauth.forbidden_usernames.
// If the username in the payload matches an item in the forbidden array, we throw an AuthenticationError
const basicAuthConfig = server.config().get('opendistro_security.basicauth');
if (basicAuthConfig.forbidden_usernames && basicAuthConfig.forbidden_usernames.length) {
if (request.payload && request.payload.username && basicAuthConfig.forbidden_usernames.indexOf(request.payload.username) > -1) {
throw new AuthenticationError('Invalid username or password');
}
async handler (request, h) {
try {
// In order to prevent direct access for certain usernames (e.g. service users like
// kibanaserver, logstash etc.) we can add them to basicauth.forbidden_usernames.
// If the username in the payload matches an item in the forbidden array, we throw an AuthenticationError
const basicAuthConfig = server.config().get('opendistro_security.basicauth');
if (basicAuthConfig.forbidden_usernames && basicAuthConfig.forbidden_usernames.length) {
if (request.payload && request.payload.username && basicAuthConfig.forbidden_usernames.indexOf(request.payload.username) > -1) {
throw new AuthenticationError('Invalid username or password');
}
}
const authHeaderValue = new Buffer(`${request.payload.username}:${request.payload.password}`).toString('base64');
let {user} = await request.auth.securitySessionStorage.authenticate({
authHeaderValue: 'Basic ' + authHeaderValue
const authHeaderValue = new Buffer(`${request.payload.username}:${request.payload.password}`).toString('base64');
let {user} = await request.auth.securitySessionStorage.authenticate({
authHeaderValue: 'Basic ' + authHeaderValue
});
// handle tenants if MT is enabled
if(server.config().get("opendistro_security.multitenancy.enabled")) {
// get the preferred tenant of the user
let globalTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_global");
let privateTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_private");
let preferredTenants = server.config().get("opendistro_security.multitenancy.tenants.preferred");
let finalTenant = server.plugins.opendistro_security.getSecurityBackend().getTenantByPreference(request, user.username, user.tenants, preferredTenants, globalTenantEnabled, privateTenantEnabled);
request.auth.securitySessionStorage.putStorage('tenant', {
selected: finalTenant
});
// handle tenants if MT is enabled
if(server.config().get("opendistro_security.multitenancy.enabled")) {
// get the preferred tenant of the user
let globalTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_global");
let privateTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_private");
let preferredTenants = server.config().get("opendistro_security.multitenancy.tenants.preferred");
let finalTenant = server.plugins.opendistro_security.getSecurityBackend().getTenantByPreference(request, user.username, user.tenants, preferredTenants, globalTenantEnabled, privateTenantEnabled);
request.auth.securitySessionStorage.putStorage('tenant', {
selected: finalTenant
});
return reply({
username: user.username,
tenants: user.tenants,
roles: user.roles,
backendroles: user.backendroles,
selectedTenant: user.selectedTenant,
});
} 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 if (error instanceof MissingTenantError) {
return reply(Boom.notFound(error.message));
} else if (error instanceof MissingRoleError) {
return reply(Boom.notFound(error.message));
} else {
return reply(Boom.badImplementation(error.message));
}
return {
username: user.username,
tenants: user.tenants,
roles: user.roles,
backendroles: user.backendroles,
selectedTenant: user.selectedTenant,
};
} else {
// no MT, nothing more to do
return {
username: user.username,
tenants: user.tenants
};
}
} catch (error) {
if (error instanceof AuthenticationError) {
throw Boom.unauthorized(error.message);
} else if (error instanceof MissingTenantError) {
throw Boom.notFound(error.message);
} else if (error instanceof MissingRoleError) {
throw Boom.notFound(error.message);
} else {
throw Boom.badImplementation(error.message);
}
}
},
config: {
options: {
validate: {
payload: {
username: Joi.string().required(),
@ -169,11 +168,12 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${API_ROOT}/auth/logout`,
handler: (request, reply) => {
handler: (request, h) => {
request.auth.securitySessionStorage.clear();
reply({});
return {};
},
config: {
options: {
auth: false
}
});
@ -181,42 +181,41 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${APP_ROOT}/auth/anonymous`,
handler: {
async: async (request, reply) => {
async handler(request, h) {
if (server.config().get('opendistro_security.auth.anonymous_auth_enabled')) {
const basePath = server.config().get('server.basePath');
try {
let {session} = await request.auth.securitySessionStorage.authenticate({}, {isAnonymousAuth: true});
if (server.config().get('opendistro_security.auth.anonymous_auth_enabled')) {
const basePath = server.config().get('server.basePath');
try {
let {session} = await request.auth.securitySessionStorage.authenticate({}, {isAnonymousAuth: true});
let nextUrl = null;
if (request.url && request.url.query && request.url.query.nextUrl) {
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
}
if (nextUrl) {
nextUrl = parseNextUrl(nextUrl, basePath);
return reply.redirect(nextUrl);
}
return reply.redirect(basePath + '/app/kibana');
} catch (error) {
if (error instanceof MissingRoleError) {
return reply.redirect(basePath + '/customerror?type=missingRole');
} else if (error instanceof MissingTenantError) {
return reply.redirect(basePath + '/customerror?type=missingTenant');
} else {
return reply.redirect(basePath + '/customerror?type=anonymousAuthError');
}
let nextUrl = null;
if (request.url && request.url.query && request.url.query.nextUrl) {
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
}
if (nextUrl) {
nextUrl = parseNextUrl(nextUrl, basePath);
return h.redirect(nextUrl);
}
return h.redirect(basePath + '/app/kibana');
} catch (error) {
if (error instanceof MissingRoleError) {
return h.redirect(basePath + '/customerror?type=missingRole');
} else if (error instanceof MissingTenantError) {
return h.redirect(basePath + '/customerror?type=missingTenant');
} else {
return h.redirect(basePath + '/customerror?type=anonymousAuthError');
}
} else {
return reply.redirect(`${APP_ROOT}/login`);
}
} else {
return h.redirect(`${APP_ROOT}/login`);
}
},
config: {
options: {
auth: false
}
});
@ -227,10 +226,10 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${APP_ROOT}/customerror`,
handler(request, reply) {
return reply.renderAppWithDefaultConfig(customErrorApp);
handler(request, h) {
return h.renderAppWithDefaultConfig(customErrorApp);
},
config: {
options: {
auth: false
}
});

View File

@ -83,7 +83,7 @@ export default class Jwt extends AuthType {
try {
authHeaderValue = request.headers[this.authHeaderName];
} catch (error) {
console.log('Something went wrong when getting the JWT bearer from the header', request.headers)
console.log('Something went wrong when getting the JWT bearer from the header', request.headers);
}
}
@ -139,12 +139,12 @@ export default class Jwt extends AuthType {
}
}
onUnAuthenticated(request, reply, error) {
onUnAuthenticated(request, h, error) {
if (error instanceof MissingTenantError) {
return reply.redirect(this.basePath + '/customerror?type=missingTenant');
return h.redirect(this.basePath + '/customerror?type=missingTenant');
} else if (error instanceof MissingRoleError) {
return reply.redirect(this.basePath + '/customerror?type=missingRole');
return h.redirect(this.basePath + '/customerror?type=missingRole');
} else {
// The customer may use a login endpoint, to which we can redirect
// if the user isn't authenticated.
@ -164,16 +164,16 @@ export default class Jwt extends AuthType {
loginEndpointURLObject.query['nextUrl'] = nextUrl;
}
// Format the parsed endpoint object into a URL and redirect
return reply.redirect(format(loginEndpointURLObject));
return h.redirect(format(loginEndpointURLObject));
} catch(error) {
this.server.log(['error', 'security'], 'An error occured while parsing the opendistro_security.jwt.login_endpoint value');
return reply.redirect(this.basePath + '/customerror?type=authError');
return h.redirect(this.basePath + '/customerror?type=authError');
}
} else if (error instanceof SessionExpiredError) {
return reply.redirect(this.basePath + '/customerror?type=sessionExpired');
return h.redirect(this.basePath + '/customerror?type=sessionExpired');
} else {
return reply.redirect(this.basePath + '/customerror?type=authError');
return h.redirect(this.basePath + '/customerror?type=authError');
}
}
}

View File

@ -31,46 +31,46 @@
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
const customErrorApp = server.getHiddenUiAppById('security-customerror');
const customErrorApp = server.getHiddenUiAppById('security-customerror');
/**
* After a logout we are redirected to a login page
*/
server.route({
method: 'GET',
path: `${APP_ROOT}/login`,
handler(request, reply) {
return reply.renderAppWithDefaultConfig(customErrorApp);
},
config: {
auth: false
}
});
/**
* After a logout we are redirected to a login page
*/
server.route({
method: 'GET',
path: `${APP_ROOT}/login`,
handler(request, h) {
return h.renderAppWithDefaultConfig(customErrorApp);
},
options: {
auth: false
}
});
/**
* The error page.
*/
server.route({
method: 'GET',
path: `${APP_ROOT}/customerror`,
handler(request, reply) {
return reply.renderAppWithDefaultConfig(customErrorApp);
},
config: {
auth: false
}
});
/**
* The error page.
*/
server.route({
method: 'GET',
path: `${APP_ROOT}/customerror`,
handler(request, h) {
return h.renderAppWithDefaultConfig(customErrorApp);
},
options: {
auth: false
}
});
server.route({
method: 'POST',
path: `${API_ROOT}/auth/logout`,
handler: (request, reply) => {
request.auth.securitySessionStorage.clear();
reply({});
},
config: {
auth: false
}
});
server.route({
method: 'POST',
path: `${API_ROOT}/auth/logout`,
handler: (request, h) => {
request.auth.securitySessionStorage.clear();
return {};
},
options: {
auth: false
}
});
}; //end module

View File

@ -110,29 +110,24 @@ export default class OpenId extends AuthType {
}
}
onUnAuthenticated(request, reply, error) {
onUnAuthenticated(request, h, error) {
// If we don't have any tenant we need to show the custom error page
if (error instanceof MissingTenantError) {
return reply.redirect(this.basePath + '/customerror?type=missingTenant')
return h.redirect(this.basePath + '/customerror?type=missingTenant')
} else if (error instanceof MissingRoleError) {
return reply.redirect(this.basePath + '/customerror?type=missingRole')
return h.redirect(this.basePath + '/customerror?type=missingRole')
} else if (error instanceof AuthenticationError) {
return reply.redirect(this.basePath + '/customerror?type=authError')
return h.redirect(this.basePath + '/customerror?type=authError')
}
const nextUrl = encodeURIComponent(request.url.path);
return reply.redirect(`${this.basePath}/auth/openid/login?nextUrl=${nextUrl}`);
return h.redirect(`${this.basePath}/auth/openid/login?nextUrl=${nextUrl}`);
}
async setupRoutes() {
Wreck.get(this.config.get('opendistro_security.openid.connect_url'), (err, response, payload) => {
if (err ||
response.statusCode < 200 ||
response.statusCode > 299) {
this.server.log(["error", "openid"], err);
throw new Error('Failed when trying to obtain the endpoints from your IdP');
}
try {
const {response, payload} = await Wreck.get(this.config.get('opendistro_security.openid.connect_url'));
const parsedPayload = JSON.parse(payload.toString());
@ -143,8 +138,13 @@ export default class OpenId extends AuthType {
};
require('./routes')(this.pluginRoot, this.server, this.kbnServer, this.APP_ROOT, this.API_ROOT, endPoints);
});
} catch (error) {
if (error ||
error.output.statusCode < 200 ||
error.output.statusCode > 299) {
throw new Error('Failed when trying to obtain the endpoints from your IdP');
}
}
}
}

View File

@ -33,7 +33,7 @@ import Boom from 'boom';
import {parseNextUrl} from '../../parseNextUrl'
import MissingTenantError from "../../errors/missing_tenant_error";
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, openIdEndPoints) {
module.exports = async function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, openIdEndPoints) {
const AuthenticationError = pluginRoot('lib/auth/errors/authentication_error');
const config = server.config();
@ -78,17 +78,18 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, op
* Error handler for the cases where we can't catch errors while obtaining the token.
* Mainly happens when Wreck within Bell
*/
server.ext('onPreResponse', function(request, reply) {
server.ext('onPreResponse', function(request, h) {
// Make sure we only handle errors for the login route
if (request.response.isBoom && request.path.indexOf(`${APP_ROOT}${routesPath}login`) > -1 && request.response.output.statusCode === 500) {
return reply.redirect(basePath + '/customerror?type=authError');
return h.redirect(basePath + '/customerror?type=authError');
}
reply.continue();
return h.continue;
});
// Register bell with the server
server.register(require('bell'), function (err) {
try {
await server.register(require('bell'));
let baseRedirectUrl = getBaseRedirectUrl();
let location = `${baseRedirectUrl}${basePath}`;
@ -108,53 +109,54 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, op
isSecure: config.get('opendistro_security.cookie.secure'),
});
/**
* The login page.
*/
server.route({
method: ['GET', 'POST'],
path: `${APP_ROOT}${routesPath}login`,
config: {
options: {
auth: 'customOAuth'
},
handler: {
async: async (request, reply) => {
if (!request.auth.isAuthenticated) {
return reply.redirect(basePath + '/customerror?type=authError');
handler: async(request, h) => {
if (!request.auth.isAuthenticated) {
return h.redirect(basePath + '/customerror?type=authError');
}
let credentials = request.auth.credentials;
let nextUrl = (credentials.query && credentials.query.nextUrl) ? credentials.query.nextUrl : null;
try {
// Bell gives us the access token to identify with here,
// but we want the id_token returned from the IDP
let {user} = await request.auth.securitySessionStorage.authenticate({
authHeaderValue: 'Bearer ' + request.auth.artifacts['id_token']
});
if (nextUrl) {
nextUrl = parseNextUrl(nextUrl, basePath);
return h.redirect(nextUrl);
}
let credentials = request.auth.credentials;
let nextUrl = (credentials.query && credentials.query.nextUrl) ? credentials.query.nextUrl : null;
try {
// Bell gives us the access token to identify with here,
// but we want the id_token returned from the IDP
let {user} = await request.auth.securitySessionStorage.authenticate({
authHeaderValue: 'Bearer ' + request.auth.artifacts['id_token']
});
if (nextUrl) {
nextUrl = parseNextUrl(nextUrl, basePath);
return reply.redirect(nextUrl);
}
return reply.redirect(basePath + '/app/kibana');
}
catch (error) {
if (error instanceof AuthenticationError) {
return reply.redirect(basePath + '/customerror?type=authError');
} else if (error instanceof MissingTenantError) {
return reply.redirect(basePath + '/customerror?type=missingTenant');
} else {
return reply.redirect(basePath + '/customerror?type=authError');
}
return h.redirect(basePath + '/app/kibana');
}
catch (error) {
if (error instanceof AuthenticationError) {
return h.redirect(basePath + '/customerror?type=authError');
} else if (error instanceof MissingTenantError) {
return h.redirect(basePath + '/customerror?type=missingTenant');
} else {
return h.redirect(basePath + '/customerror?type=authError');
}
}
}
});
});
} catch (error) {
// @todo How do we want catch this?
}
/**
* The error page.
@ -162,10 +164,10 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, op
server.route({
method: 'GET',
path: `${APP_ROOT}/customerror`,
handler(request, reply) {
return reply.renderAppWithDefaultConfig(customErrorApp);
handler(request, h) {
return h.renderAppWithDefaultConfig(customErrorApp);
},
config: {
options: {
auth: false
}
});
@ -177,7 +179,7 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, op
server.route({
method: 'POST',
path: `${API_ROOT}/auth/logout`,
handler: (request, reply) => {
handler: (request, h) => {
request.auth.securitySessionStorage.clear();
// Build the redirect uri needed by the provider
@ -201,9 +203,9 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, op
endSessionUrl = openIdEndPoints.end_session_endpoint + requestQueryParameters + '&id_token_hint=' + token;
}
reply({redirectURL: endSessionUrl});
return {redirectURL: endSessionUrl};
},
config: {
options: {
auth: false
}
});

View File

@ -144,11 +144,11 @@ export default class ProxyCache extends AuthType {
}
}
onUnAuthenticated(request, reply, error) {
onUnAuthenticated(request, h, error) {
if (error instanceof MissingTenantError) {
return reply.redirect(this.basePath + '/customerror?type=missingTenant');
return h.redirect(this.basePath + '/customerror?type=missingTenant');
} else if (error instanceof MissingRoleError) {
return reply.redirect(this.basePath + '/customerror?type=missingRole');
return h.redirect(this.basePath + '/customerror?type=missingRole');
} else {
// The customer may use a login endpoint, to which we can redirect
// if the user isn't authenticated.
@ -156,15 +156,15 @@ export default class ProxyCache extends AuthType {
if (loginEndpoint) {
try {
const redirectUrl = parseLoginEndpoint(loginEndpoint, request);
return reply.redirect(redirectUrl);
return h.redirect(redirectUrl);
} catch(error) {
this.server.log(['error', 'security'], 'An error occured while parsing the opendistro_security.proxycache.login_endpoint value');
return reply.redirect(this.basePath + '/customerror?type=proxycacheAuthError');
return h.redirect(this.basePath + '/customerror?type=proxycacheAuthError');
}
} else if (error instanceof SessionExpiredError) {
return reply.redirect(this.basePath + '/customerror?type=sessionExpired');
return h.redirect(this.basePath + '/customerror?type=sessionExpired');
} else {
return reply.redirect(this.basePath + '/customerror?type=proxycacheAuthError');
return h.redirect(this.basePath + '/customerror?type=proxycacheAuthError');
}
}
}
@ -173,4 +173,18 @@ export default class ProxyCache extends AuthType {
require('./routes')(this.pluginRoot, this.server, this.kbnServer, this.APP_ROOT, this.API_ROOT);
}
addAdditionalAuthHeaders(request, authHeader) {
// for proxy cache mode, make it possible to assign the proxy ip,
// usually as x-forwarded-for header. Only if no headers are already present
let existingProxyHeaders = request.headers[this.config.get('opendistro_security.proxycache.proxy_header')];
// do not overwrite existing headers from existing proxy
if (existingProxyHeaders) {
return;
}
let remoteIP = request.info.remoteAddress;
let proxyIP = this.config.get('opendistro_security.proxycache.proxy_header_ip');
authHeader[this.config.get('opendistro_security.proxycache.proxy_header')] = remoteIP+","+proxyIP
}
}

View File

@ -45,7 +45,7 @@ export function parseLoginEndpoint(loginEndpoint, request = null) {
// Make sure we don't overwrite an existing "nextUrl" parameter,
// just in case the customer is using that name for something else
if (typeof loginEndpointURLObject.query['nextUrl'] === 'undefined' && request) {
const nextUrl = encodeURIComponent(request.url.path);
const nextUrl = request.url.path;
// Delete the search parameter - otherwise format() will use its value instead of the .query property
delete loginEndpointURLObject.search;
loginEndpointURLObject.query['nextUrl'] = nextUrl;

View File

@ -41,22 +41,22 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${APP_ROOT}/login`,
handler(request, reply) {
handler(request, h) {
// The customer may use a login endpoint, to which we can redirect
// if the user isn't authenticated.
let loginEndpoint = server.config().get('opendistro_security.proxycache.login_endpoint');
if (loginEndpoint) {
try {
const redirectUrl = parseLoginEndpoint(loginEndpoint);
return reply.redirect(redirectUrl);
return h.redirect(redirectUrl);
} catch(error) {
this.server.log(['error', 'security'], 'An error occured while parsing the opendistro_security.proxycache.login_endpoint value');
}
} else {
return reply.renderAppWithDefaultConfig(customErrorApp);
return h.renderAppWithDefaultConfig(customErrorApp);
}
},
config: {
options: {
auth: false
}
});
@ -67,10 +67,10 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${APP_ROOT}/customerror`,
handler(request, reply) {
return reply.renderAppWithDefaultConfig(customErrorApp);
handler(request, h) {
return h.renderAppWithDefaultConfig(customErrorApp);
},
config: {
options: {
auth: false
}
});
@ -78,11 +78,11 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${API_ROOT}/auth/logout`,
handler: (request, reply) => {
handler: (request, h) => {
request.auth.securitySessionStorage.clear();
reply({});
return {};
},
config: {
options: {
auth: false
}
});

View File

@ -85,19 +85,18 @@ export default class Saml extends AuthType {
}
}
onUnAuthenticated(request, reply, error) {
onUnAuthenticated(request, h, error) {
// If we don't have any tenant we need to show the custom error page
if (error instanceof MissingTenantError) {
return reply.redirect(this.basePath + '/customerror?type=missingTenant')
return h.redirect(this.basePath + '/customerror?type=missingTenant')
} else if (error instanceof MissingRoleError) {
return reply.redirect(this.basePath + '/customerror?type=missingRole')
return h.redirect(this.basePath + '/customerror?type=missingRole')
} else if (error instanceof AuthenticationError) {
return reply.redirect(this.basePath + '/customerror?type=samlAuthError')
return h.redirect(this.basePath + '/customerror?type=samlAuthError')
}
const nextUrl = encodeURIComponent(request.url.path);
return reply.redirect(`${this.basePath}/auth/saml/login?nextUrl=${nextUrl}`);
return h.redirect(`${this.basePath}/auth/saml/login?nextUrl=${nextUrl}`);
}

View File

@ -49,43 +49,34 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${APP_ROOT}${routesPath}login`,
config: {
options: {
auth: false
},
handler: {
async: async (request, reply) => {
if (request.auth.isAuthenticated) {
return reply.redirect(basePath + '/app/kibana');
}
async handler(request, h) {
if (request.auth.isAuthenticated) {
return h.redirect(basePath + '/app/kibana');
}
let nextUrl = null;
if (request.url && request.url.query && request.url.query.nextUrl) {
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
}
let nextUrl = null;
if (request.url && request.url.query && request.url.query.nextUrl) {
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
}
try {
// Grab the request for SAML
server.plugins.opendistro_security.getSecurityBackend().getSamlHeader()
.then((samlHeader) => {
request.auth.securitySessionStorage.putStorage('temp-saml', {
requestId: samlHeader.requestId,
nextUrl: nextUrl
});
return reply.redirect(samlHeader.location);
})
.catch(() => {
return reply.redirect(basePath + '/customerror?type=samlConfigError');
});
} catch (error) {
return reply.redirect(basePath + '/customerror?type=samlConfigError');
}
// Grab the request for SAML
try {
const samlHeader = await server.plugins.opendistro_security.getSecurityBackend().getSamlHeader()
request.auth.securitySessionStorage.putStorage('temp-saml', {
requestId: samlHeader.requestId,
nextUrl: nextUrl
});
return h.redirect(samlHeader.location).takeover();
} catch (error) {
return h.redirect(basePath + '/customerror?type=samlConfigError');
}
}
});
/**
@ -94,46 +85,45 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${APP_ROOT}/_opendistro/_security/saml/acs`,
config: {
options: {
auth: false
},
handler: {
async: async (request, reply) => {
handler: async (request, h) => {
let storedRequestInfo = request.auth.securitySessionStorage.getStorage('temp-saml', {});
request.auth.securitySessionStorage.clearStorage('temp-saml');
let storedRequestInfo = request.auth.securitySessionStorage.getStorage('temp-saml', {});
request.auth.securitySessionStorage.clearStorage('temp-saml');
if (! storedRequestInfo.requestId) {
return reply.redirect(basePath + '/customerror?type=samlAuthError');
if (! storedRequestInfo.requestId) {
return h.redirect(basePath + '/customerror?type=samlAuthError');
}
try {
let credentials = await server.plugins.opendistro_security.getSecurityBackend().authtoken(storedRequestInfo.requestId || null, request.payload.SAMLResponse);
let {user} = await request.auth.securitySessionStorage.authenticate({
authHeaderValue: credentials.authorization
});
let nextUrl = storedRequestInfo.nextUrl;
if (nextUrl) {
nextUrl = parseNextUrl(nextUrl, basePath);
return h.redirect(nextUrl);
}
try {
let credentials = await server.plugins.opendistro_security.getSecurityBackend().authtoken(storedRequestInfo.requestId || null, request.payload.SAMLResponse);
return h.redirect(basePath + '/app/kibana');
let {user} = await request.auth.securitySessionStorage.authenticate({
authHeaderValue: credentials.authorization
});
let nextUrl = storedRequestInfo.nextUrl;
if (nextUrl) {
nextUrl = parseNextUrl(nextUrl, basePath);
return reply.redirect(nextUrl);
}
return reply.redirect(basePath + '/app/kibana');
} catch (error) {
if (error instanceof AuthenticationError) {
return reply.redirect(basePath + '/customerror?type=samlAuthError');
} else if (error instanceof MissingTenantError) {
return reply.redirect(basePath + '/customerror?type=missingTenant');
} else {
return reply.redirect(basePath + '/customerror?type=samlAuthError');
}
} catch (error) {
if (error instanceof AuthenticationError) {
return h.redirect(basePath + '/customerror?type=samlAuthError');
} else if (error instanceof MissingTenantError) {
return h.redirect(basePath + '/customerror?type=missingTenant');
} else {
return h.redirect(basePath + '/customerror?type=samlAuthError');
}
}
}
});
/**
@ -142,33 +132,32 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${APP_ROOT}/_opendistro/_security/saml/acs/idpinitiated`,
config: {
options: {
auth: false
},
handler: {
async: async (request, reply) => {
handler: async (request, h) => {
try {
const acsEndpoint = `${APP_ROOT}/_opendistro/_security/saml/acs/idpinitiated`;
let credentials = await server.plugins.opendistro_security.getSecurityBackend().authtoken(null, request.payload.SAMLResponse, acsEndpoint);
try {
const acsEndpoint = `${APP_ROOT}/_opendistro/_security/saml/acs/idpinitiated`;
let credentials = await server.plugins.opendistro_security.getSecurityBackend().authtoken(null, request.payload.SAMLResponse, acsEndpoint);
let {user} = await request.auth.securitySessionStorage.authenticate({
authHeaderValue: credentials.authorization
});
let {user} = await request.auth.securitySessionStorage.authenticate({
authHeaderValue: credentials.authorization
});
return reply.redirect(basePath + '/app/kibana');
return h.redirect(basePath + '/app/kibana');
} catch (error) {
if (error instanceof AuthenticationError) {
return reply.redirect(basePath + '/customerror?type=samlAuthError');
} else if (error instanceof MissingTenantError) {
return reply.redirect(basePath + '/customerror?type=missingTenant');
} else {
return reply.redirect(basePath + '/customerror?type=samlAuthError');
}
} catch (error) {
if (error instanceof AuthenticationError) {
return h.redirect(basePath + '/customerror?type=samlAuthError');
} else if (error instanceof MissingTenantError) {
return h.redirect(basePath + '/customerror?type=missingTenant');
} else {
return h.redirect(basePath + '/customerror?type=samlAuthError');
}
}
}
});
/**
@ -177,10 +166,10 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: ['GET', 'POST'],
path: `${APP_ROOT}/_opendistro/_security/saml/logout`,
handler(request, reply) {
return reply.redirect(`${APP_ROOT}/customerror?type=samlLogoutSuccess`);
handler(request, h) {
return h.redirect(`${APP_ROOT}/customerror?type=samlLogoutSuccess`);
},
config: {
options: {
auth: false
}
});
@ -191,10 +180,10 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${APP_ROOT}/customerror`,
handler(request, reply) {
return reply.renderAppWithDefaultConfig(customErrorApp);
handler(request, h) {
return h.renderAppWithDefaultConfig(customErrorApp);
},
config: {
options: {
auth: false
}
});
@ -205,28 +194,27 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${API_ROOT}/auth/logout`,
handler: {
async: async(request, reply) => {
handler: async(request, h) => {
const cookieName = config.get('opendistro_security.cookie.name');
let authInfo = null;
const cookieName = config.get('opendistro_security.cookie.name');
let authInfo = null;
try {
let authHeader = {
[request.auth.securitySessionStorage.getAuthHeaderName()]: request.state[cookieName].credentials.authHeaderValue
};
authInfo = await server.plugins.opendistro_security.getSecurityBackend().authinfo(authHeader);
} catch(error) {
// Not much we can do here, so we'll just fall back to the login page if we don't get an sso_logout_url
}
request.auth.securitySessionStorage.clear();
const redirectURL = (authInfo && authInfo.sso_logout_url) ? authInfo.sso_logout_url : `${APP_ROOT}/customerror?type=samlLogoutSuccess`;
reply({redirectURL});
try {
let authHeader = {
[request.auth.securitySessionStorage.getAuthHeaderName()]: request.state[cookieName].credentials.authHeaderValue
};
authInfo = await server.plugins.opendistro_security.getSecurityBackend().authinfo(authHeader);
} catch(error) {
// Not much we can do here, so we'll just fall back to the login page if we don't get an sso_logout_url
}
request.auth.securitySessionStorage.clear();
const redirectURL = (authInfo && authInfo.sso_logout_url) ? authInfo.sso_logout_url : `${APP_ROOT}/customerror?type=samlLogoutSuccess`;
return {redirectURL};
},
config: {
options: {
auth: false
}
});

View File

@ -34,63 +34,63 @@
*/
export default class User {
/**
* @property {string} username - The username.
*/
get username() {
return this._username;
}
/**
* @property {string} username - The username.
*/
get username() {
return this._username;
}
/**
* @property {Array} roles - The user roles.
*/
get roles() {
return this._roles;
}
/**
* @property {Array} roles - The user roles.
*/
get roles() {
return this._roles;
}
/**
* @property {Array} roles - The users unmapped backend roles.
*/
get backendroles() {
return this._backendroles;
}
/**
* @property {Array} roles - The users unmapped backend roles.
*/
get backendroles() {
return this._backendroles;
}
/**
* @property {Array} tenants - The user tenants.
*/
get tenants() {
return this._tenants;
}
/**
* @property {Array} tenants - The user tenants.
*/
get tenants() {
return this._tenants;
}
/**
* @property {Array} tenants - The user tenants.
*/
get selectedTenant() {
return this._selectedTenant;
}
/**
* @property {object} credentials - The credentials that were used to authenticate the user.
*/
get credentials() {
return this._credentials;
}
/**
* @property {Array} tenants - The user tenants.
*/
get selectedTenant() {
return this._selectedTenant;
}
/**
* @property {object} credentials - The credentials that were used to authenticate the user.
*/
get credentials() {
return this._credentials;
}
/**
* @property {object} proxyCredentials - User credentials to be used in requests to Elasticsearch performed by either the transport client
* or the query engine.
*/
get proxyCredentials() {
return this._proxyCredentials;
}
/**
* @property {object} proxyCredentials - User credentials to be used in requests to Elasticsearch performed by either the transport client
* or the query engine.
*/
get proxyCredentials() {
return this._proxyCredentials;
}
constructor(username, credentials, proxyCredentials, roles, backendroles, tenants, selectedTenant) {
this._username = username;
this._credentials = credentials;
this._proxyCredentials = proxyCredentials;
this._roles = roles;
this._selectedTenant = selectedTenant;
this._backendroles = backendroles;
this._tenants = tenants;
}
constructor(username, credentials, proxyCredentials, roles, backendroles, tenants, selectedTenant) {
this._username = username;
this._credentials = credentials;
this._proxyCredentials = proxyCredentials;
this._roles = roles;
this._selectedTenant = selectedTenant;
this._backendroles = backendroles;
this._tenants = tenants;
}
}

View File

@ -90,6 +90,7 @@ export default class SecurityBackend {
const response = await this._client.opendistro_security.authinfo({
headers: headers
});
return new User(response.user_name, credentials, null, response.roles, response.backend_roles, response.tenants, response.user_requested_tenant);
} catch(error) {
if (error.status == 401) {
@ -113,6 +114,7 @@ export default class SecurityBackend {
const response = await this._client.opendistro_security.authinfo({
headers: headers
});
return new User(response.user_name, credentials, null, response.roles, response.backend_roles, response.tenants, response.user_requested_tenant);
} catch(error) {
if (error.status == 401) {

View File

@ -33,8 +33,8 @@ import _ from 'lodash';
import Boom from 'boom';
import elasticsearch from 'elasticsearch';
import SecurityConfigurationPlugin from './opendistro_security_configuration_plugin';
import wrapElasticsearchError from '../../backend/errors/wrap_elasticsearch_error';
import NotFoundError from '../../backend/errors/not_found';
import wrapElasticsearchError from './../../backend/errors/wrap_elasticsearch_error';
import NotFoundError from './../../backend/errors/not_found';
import filterAuthHeaders from '../../auth/filter_auth_headers';
import Joi from 'joi'
import internalusers_schema from '../validation/internalusers'
@ -134,7 +134,7 @@ export default class SecurityConfigurationBackend {
async save(headers, resourceName, id, body) {
const result = Joi.validate(body, this.getValidator(resourceName));
if (result.error) {
throw Boom.create(500, "Resource not valid");
throw new Boom("Resource not valid", {statusCode: 500});
}
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
try {

View File

@ -21,23 +21,23 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${API_ROOT}/configuration/{resourceName}`,
handler: {
async: async (request, reply) => {
async handler (request, reply) {
try {
const results = await backend.list(request.headers, request.params.resourceName);
return reply({
return {
total: Object.keys(results).length,
data: results
});
};
} catch (error) {
if (error.isBoom) {
return reply(error);
return error;
}
throw error;
}
}
},
config: {
},
options: {
validate: {
params: {
resourceName: Joi.string().required()
@ -58,24 +58,24 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${API_ROOT}/configuration/{resourceName}/{id}`,
handler: {
async: async (request, reply) => {
async handler (request, h) {
try {
const result = await backend.get(request.headers, request.params.resourceName, request.params.id);
return reply(result);
return result;
} catch (error) {
if (error.name === 'NotFoundError') {
return reply(Boom.notFound(`${request.params.id} not found.`));
return Boom.notFound(`${request.params.id} not found.`);
} else {
if (error.isBoom) {
return reply(error);
return error;
}
throw error;
}
}
}
},
config: {
},
options: {
validate: {
params: {
resourceName: Joi.string().required(),
@ -97,22 +97,22 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
server.route({
method: 'DELETE',
path: `${API_ROOT}/configuration/{resourceName}/{id}`,
handler: {
async: async (request, reply) => {
async handler(request, h) {
try {
const response = await backend.delete(request.headers, request.params.resourceName, request.params.id);
return reply({
return {
message: response.message
});
};
} catch (error) {
if (error.isBoom) {
return reply(error);
return error;
}
throw error;
}
}
},
config: {
},
options: {
validate: {
params: {
resourceName: Joi.string().required(),
@ -135,40 +135,37 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${API_ROOT}/configuration/{resourceName}/{id}`,
handler: {
async: async (request, reply) => {
async handler(request, h) {
try {
const response = await backend.save(request.headers, request.params.resourceName, request.params.id, request.payload);
return reply({
message: response.message
});
return {
message: response.message
};
} catch (error) {
if (error.isBoom) {
return reply(error);
return error;
}
throw error;
}
}
}
});
server.route({
method: 'DELETE',
path: `${API_ROOT}/configuration/cache`,
handler: {
async: async (request, reply) => {
async handler (request, h) {
try {
const response = await backend.clearCache(request.headers);
return reply({
return {
message: response.message
});
};
} catch (error) {
if (error.isBoom) {
return reply(error);
return error;
}
throw error;
}
}
}
});
@ -176,31 +173,29 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
server.route({
method: 'GET',
path: `${API_ROOT}/restapiinfo`,
handler: {
async: async (request, reply) => {
async handler (request, reply) {
try {
const response = await backend.restapiinfo(request.headers);
return reply(response);
return response;
} catch (error) {
if (error.isBoom) {
return reply(error);
return error;
}
throw error;
}
}
}
});
server.route({
method: 'GET',
path: `${API_ROOT}/configuration/indices`,
handler: (request, reply) => {
async handler (request, h) {
try {
let response = backend.indices(request.headers);
return reply(response);
return response;
} catch (error) {
if (error.isBoom) {
return reply(error);
return error;
}
throw error;
}
@ -210,17 +205,15 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${API_ROOT}/configuration/validatedls/{indexName}`,
handler: {
async: async (request, reply) => {
try {
const response = await backend.validateDls(request.headers, request.params.indexName, request.payload);
return reply(response);
} catch (error) {
if (error.isBoom) {
return reply(error);
}
throw error;
async handler(request, reply) {
try {
const response = await backend.validateDls(request.headers, request.params.indexName, request.payload);
return response;
} catch (error) {
if (error.isBoom) {
return error;
}
throw error;
}
}
});

View File

@ -38,7 +38,7 @@ export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
const config = server.config();
const basePath = config.get('server.basePath');
const unauthenticatedRoutes = config.get('opendistro_security.basicauth.unauthenticated_routes');
// START add default unauthenticated routes
// END add default unauthenticated routes
const cookieConfig = {
password: config.get('opendistro_security.cookie.password'),

View File

@ -43,7 +43,7 @@ export default function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, auth
const defaultSpaceId = 'default';
server.ext('onPreAuth', async function (request, next) {
server.ext('onPreAuth', async function (request, h) {
// default is the tenant stored in the tenants cookie
const storedSelectedTenant = request.auth.securitySessionStorage.getStorage('tenant', {}).selected;
@ -75,19 +75,19 @@ export default function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, auth
// MT is only relevant for these paths
if (!request.path.startsWith("/elasticsearch") && !request.path.startsWith("/api") && !request.path.startsWith("/app") && request.path != "/" && !selectedTenant) {
return next.continue();
return h.continue;
}
var response;
try {
if (authClass) {
authClass.assignAuthHeader(request);
await authClass.assignAuthHeader(request);
}
response = await request.auth.securitySessionStorage.getAuthInfo(request.headers);
} catch(error) {
return next.continue();
return h.continue;
}
// if we have a tenant, check validity and set it
@ -108,7 +108,7 @@ export default function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, auth
request.auth.securitySessionStorage.putStorage('tenant', {
selected: selectedTenant
});
next.state('security_preferences', prefcookie);
h.state('security_preferences', prefcookie);
}
if (debugEnabled) {
@ -125,7 +125,7 @@ export default function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, auth
// We can't add a default space for RO tenants at the moment
if (selectedTenant && response.tenants[selectedTenant] === false) {
return next.continue();
return h.continue;
}
const spacesClient = server.plugins.spaces.spacesClient.getScopedClient(request);
@ -139,19 +139,83 @@ export default function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, auth
if (defaultSpace === null) {
try {
await spacesClient.create({
id: defaultSpaceId,
name: 'Default',
description: 'This is your default space!',
color: '#00bfb3',
_reserved: true
});
if (selectedTenant && response.tenants[selectedTenant] === false) {
await addDefaultSpaceToReadOnlyTenant(server, spacesClient, request, backend, defaultSpaceId, selectedTenant);
} else {
await addDefaultSpaceToWriteTenant(server, spacesClient, defaultSpaceId);
}
} catch(error) {
server.log(['security', 'error'], `An error occurred while creating a default space`);
// We can't really recover from this error, so we'll just continue for now.
// The specific error should have been logged in the respective create method.
}
}
}
return next.continue();
return h.continue;
});
}
async function addDefaultSpaceToWriteTenant(server, spacesClient, defaultSpaceId) {
try {
await spacesClient.create({
id: defaultSpaceId,
name: 'Default',
description: 'This is your default space!',
color: '#00bfb3',
_reserved: true
});
return true;
} catch(error) {
server.log(['security', 'error'], `An error occurred while creating a default space`);
throw error;
}
}
async function addDefaultSpaceToReadOnlyTenant(server, spacesClient, request, backend, defaultSpaceId, tenantName) {
try {
let tenantInfo = await backend.getTenantInfo(backend.getServerUserAuthHeader());
let indexName = null;
for (let tenantIndexName in tenantInfo) {
if (tenantInfo[tenantIndexName] === tenantName) {
indexName = tenantIndexName;
}
}
// We have one known issue here. If a read only tenant is completely, the index will not yet have been created.
// Hence, we won't retrieve an index name for that tenant.
if (!indexName) {
server.log(['security', 'error'], `Could not find the index name for the tenant while creating a default space for a read only tenant. The tenant is probably empty.`);
throw new Error('Could not find the index name for the tenant');
}
// Call elasticsearch directly without using the Saved Objects Client
const adminCluster = server.plugins.elasticsearch.getCluster('admin');
const { callWithRequest } = adminCluster;
const clientParams = {
id: `space:${defaultSpaceId}`, // Should this be the default space id?
type: 'doc',
index: indexName,
refresh: 'wait_for',
body: {
space: {
name: 'Default',
description: 'This is your default space',
color: '#00bfb3',
_reserved: true
},
type: 'space',
updated_at: new Date().toISOString(),
}
};
// Create the space
callWithRequest(request, 'create', clientParams);
} catch (error) {
server.log(['security', 'error'], `An error occurred while creating a default space for a read only tenant`);
throw error;
}
}

View File

@ -43,7 +43,7 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
server.route({
method: 'POST',
path: `${API_ROOT}/multitenancy/tenant`,
handler: (request, reply) => {
handler: (request, h) => {
var username = request.payload.username;
var selectedTenant = request.payload.tenant;
@ -57,46 +57,47 @@ module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
request.log(['info', 'security', 'tenant_POST'], selectedTenant);
}
return reply(request.payload.tenant).state('security_preferences', prefs);
return h.response(request.payload.tenant).state('security_preferences', prefs);
}
});
server.route({
method: 'GET',
path: `${API_ROOT}/multitenancy/tenant`,
handler: (request, reply) => {
handler: (request, h) => {
let selectedTenant = request.auth.securitySessionStorage.getStorage('tenant', {}).selected;
if (debugEnabled) {
request.log(['info', 'security', 'tenant_GET'], selectedTenant);
}
return reply(selectedTenant);
return selectedTenant;
}
});
server.route({
method: 'GET',
path: `${API_ROOT}/multitenancy/info`,
handler: (request, reply) => {
handler: (request, h) => {
let mtinfo = server.plugins.opendistro_security.getSecurityBackend().multitenancyinfo(request.headers);
return reply(mtinfo);
return mtinfo;
}
});
server.route({
method: 'POST',
path: `${API_ROOT}/multitenancy/migrate/{tenantindex}`,
handler: async (request, reply) => {
handler: async (request, h) => {
// @TODO HOW TO TEST THIS?????
if (!request.params.tenantindex) {
return reply(Boom.badRequest, "Please provide a tenant index name.")
return h.response(Boom.badRequest, "Please provide a tenant index name.")
}
let forceMigration = false;
if (request.query.force && request.query.force == "true") {
forceMigration = true
}
let result = await migrateTenant(request.params.tenantindex, forceMigration, server);
reply (result);
return result;
}
});

34
lib/session/sessionPlugin.js Executable file → Normal file
View File

@ -21,14 +21,14 @@ internals.config = Joi.object({
}).required();
exports.register = async function (server, options, next) {
const register = function (server, options) {
let results = Joi.validate(options, internals.config);
Hoek.assert(!results.error, results.error);
let settings = results.value;
// @todo Don't register e.g. authenticate() when we have Kerberos or Proxy-Auth?
server.ext('onPreAuth', function (request, reply) {
server.ext('onPreAuth', function (request, h) {
request.auth.securitySessionStorage = {
/**
@ -67,7 +67,7 @@ exports.register = async function (server, options, next) {
let sessionTTL = server.config().get('opendistro_security.session.ttl')
if(sessionTTL) {
if (sessionTTL) {
session.expiryTime = Date.now() + sessionTTL
}
@ -77,7 +77,7 @@ exports.register = async function (server, options, next) {
};
return this._handleAuthResponse(credentials, authResponse)
} catch(error) {
} catch (error) {
// Make sure we clear any existing cookies if something went wrong
this.clear();
throw error;
@ -111,7 +111,7 @@ exports.register = async function (server, options, next) {
throw new MissingRoleError('No roles available for this user, please contact your system administrator.');
}
request.auth.session.set(authResponse.session);
request.cookieAuth.set(authResponse.session);
this.setAuthInfo(authResponse.user.username, authResponse.user.backendroles, authResponse.user.roles, authResponse.user.tenants, authResponse.user.selectedTenant);
@ -157,9 +157,9 @@ exports.register = async function (server, options, next) {
/**
* Clears the cookies associated with the authenticated user
*/
clear: function() {
request.auth.session.clear();
reply.unstate(storageCookieName);
clear: function () {
request.cookieAuth.clear();
h.unstate(storageCookieName);
},
/**
@ -201,7 +201,7 @@ exports.register = async function (server, options, next) {
storage[key] = value;
reply.state(storageCookieName, storage);
h.state(storageCookieName, storage);
},
/**
@ -218,7 +218,7 @@ exports.register = async function (server, options, next) {
*/
clearStorage: function(key = null) {
if (key === null) {
reply.unstate(storageCookieName);
h.unstate(storageCookieName);
return;
}
@ -226,7 +226,7 @@ exports.register = async function (server, options, next) {
if (storage && storage[key]) {
delete storage[key];
reply.state(storageCookieName, storage);
h.state(storageCookieName, storage);
}
},
@ -312,7 +312,7 @@ exports.register = async function (server, options, next) {
} catch (error) {
// Remove the storage cookie if something went wrong
if (this.authType !== null) {
reply.unstate(storageCookieName);
h.unstate(storageCookieName);
}
throw error;
@ -320,13 +320,13 @@ exports.register = async function (server, options, next) {
}
};
return reply.continue();
return h.continue;
});
next();
};
}
exports.register.attributes = {
name: 'security-session-storage'
exports.plugin = {
name: 'security-session-storage',
register
};

View File

@ -1,6 +1,6 @@
{
"name": "opendistro_security",
"version": "6.5.4",
"version": "6.6.1",
"description": "Security features for kibana",
"main": "index.js",
"homepage": "https://github.com/opendistro-for-elasticsearch/security-kibana-plugin",
@ -11,17 +11,12 @@
},
"dependencies": {
"@elastic/kibana-ui-framework": "0.0.11",
"bell": "^8.8.0",
"boom": "5.2.0",
"cookie": "^0.3.1",
"hapi": "^16.0.1",
"hapi-async-handler": "^1.0.3",
"hapi-auth-cookie": "^3.1.0",
"hapi-authorization": "^3.0.2",
"bell": "9.4.0",
"hapi-auth-cookie": "^9.1.0",
"joi": "10.6.0",
"js-yaml": "^3.7.0",
"requirefrom": "^0.2.0",
"wreck": "10.x.x"
"wreck": "14.x.x"
},
"devDependencies": {
"ui-select": "^0.19.8"

View File

@ -38,39 +38,39 @@
<h2 class="kuiSubTitle" style="margin-bottom:10px;">Permissions and Roles</h2>
<div class="kuiGallery">
<div class="FeaturePanelList">
<a id="opendistro_security.link.rolesmapping" ng-if="endpointAndMethodEnabled('ROLESMAPPING', 'GET')" class="kuiGalleryItem ng-scope"
<a id="opendistro_security.link.rolesmapping" ng-if="endpointAndMethodEnabled('ROLESMAPPING', 'GET')" class="FeaturePanel ng-scope"
ng-href="#/rolesmapping"
tooltip="Map users, backend roles and hostnames to roles."
tooltip-placement="bottom" href="#/rolesmapping">
<div class="kuiGalleryButton__image">
<div class="FeaturePanelButton__image">
<img src="{{roleMappingsSvgURL}}" width="56" />
</div>
<div class="kuiGalleryButton__label">
<div class="FeaturePanelButton__label">
Role Mappings
</div>
</a>
<a id="opendistro_security.link.roles" ng-if="endpointAndMethodEnabled('ROLES', 'GET')" class="kuiGalleryItem ng-scope"
<a id="opendistro_security.link.roles" ng-if="endpointAndMethodEnabled('ROLES', 'GET')" class="FeaturePanel ng-scope"
ng-href="#/roles" tooltip="Configure Roles and their permissions."
tooltip-placement="bottom" href="#/roles">
<div class="kuiGalleryButton__image">
<div class="FeaturePanelButton__image">
<img src="{{rolesSvgURL}}" width="56" />
</div>
<div class="kuiGalleryButton__label">
<div class="FeaturePanelButton__label">
Roles
</div>
</a>
<a id="opendistro_security.link.actiongroups" ng-if="endpointAndMethodEnabled('ACTIONGROUPS', 'GET')" class="kuiGalleryItem ng-scope"
<a id="opendistro_security.link.actiongroups" ng-if="endpointAndMethodEnabled('ACTIONGROUPS', 'GET')" class="FeaturePanel ng-scope"
ng-href="#/actiongroups"
tooltip="Configure named groups of permissions that can be applied to roles."
tooltip-placement="bottom" href="#/actiongroups">
<div class="kuiGalleryButton__image">
<div class="FeaturePanelButton__image">
<img src="{{actionGroupsSvgURL}}" width="56" />
</div>
<div class="kuiGalleryButton__label">
<div class="FeaturePanelButton__label">
Action Groups
</div>
</a>
@ -81,16 +81,16 @@
<div class="kuiVerticalRhythm kuiVerticalRhythm--medium ">
<h2 class="kuiSubTitle" style="margin-bottom:10px;">Authentication Backends</h2>
<div class="kuiGallery">
<div class="FeaturePanelList">
<a id="opendistro_security.link.internalusers" ng-if="endpointAndMethodEnabled('INTERNALUSERS', 'GET')" class="kuiGalleryItem"
<a id="opendistro_security.link.internalusers" ng-if="endpointAndMethodEnabled('INTERNALUSERS', 'GET')" class="FeaturePanel"
ng-href="#/internalusers"
tooltip="Use the Internal Users Database if you don't have any external authentication systems in place."
tooltip-placement="bottom" href="#/internalusers">
<div class="kuiGalleryButton__image">
<div class="FeaturePanelButton__image">
<img src="{{internalUserDatabaseSvgURL}}" width="56" />
</div>
<div class="kuiGalleryButton__label">
<div class="FeaturePanelButton__label">
Internal User Database
</div>
</a>
@ -99,25 +99,25 @@
<div class="kuiVerticalRhythm kuiVerticalRhythm--medium ">
<h2 class="kuiSubTitle" style="margin-bottom:10px;">System</h2>
<div class="kuiGallery">
<a id="opendistro_security.link.securityconfig" ng-if="endpointAndMethodEnabled('SECURITYCONFIG', 'GET')" class="kuiGalleryItem ng-scope"
<div class="FeaturePanelList">
<a id="opendistro_security.link.securityconfig" ng-if="endpointAndMethodEnabled('SECURITYCONFIG', 'GET')" class="FeaturePanel ng-scope"
ng-href="#/securityconfiguration"
tooltip="View the configured authentication and authorization modules."
href="#/securityconfiguration">
<div class="kuiGalleryButton__image">
<div class="FeaturePanelButton__image">
<img src="{{authenticationSvgURL}}" width="56" />
</div>
<div class="kuiGalleryButton__label">
<div class="FeaturePanelButton__label">
Authentication & Authorization
</div>
</a>
<a id="opendistro_security.link.cache" ng-if="endpointAndMethodEnabled('CACHE', 'DELETE')" class="kuiGalleryItem ng-scope"
<a id="opendistro_security.link.cache" ng-if="endpointAndMethodEnabled('CACHE', 'DELETE')" class="FeaturePanel ng-scope"
ng-click="clearCache()" tooltip="Purge all Security caches"
tooltip-placement="bottom">
<div class="kuiGalleryButton__image">
<div class="FeaturePanelButton__image">
<img src="{{purgeCacheSvgURL}}" width="56" />
</div>
<div class="kuiGalleryButton__label">
<div class="FeaturePanelButton__label">
Purge Cache
</div>
</a>

View File

@ -143,22 +143,6 @@ h5 {
padding: 0px;
}
.kuiGalleryButton {
width: 130px;
height: 130px;
padding: 0px;
}
.kuiGalleryButton__label {
font-size: 14px;
color: #191E23;
text-align: center;
max-width: 100%;
white-space: inherit;
overflow: hidden;
text-overflow: ellipsis;
}
.kuiTableRowCell {
padding: 7px 8px 8px;
}
@ -287,3 +271,78 @@ h5 {
color: #2D2D2D;
border: 1px solid #D9D9D9;
}
.FeaturePanelList {
display: flex;
flex-wrap: wrap;
}
.FeaturePanel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
margin: 0 20px 20px 0;
padding: 25px 10px 10px;
width: 140px;
height: 140px;
border: 1px solid #CED5DA;
border-radius: 4px;
background-color: #F6F6F6;
line-height: 1.5;
text-decoration: none;
}
.FeaturePanel:hover {
border-color: #00A6FF;
background-color: #FFFFFF;
}
.FeaturePanel__image {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
width: 50px;
height: 50px;
}
.FeaturePanel__label {
max-width: 100%;
font-size: 14px;
color: #191E23;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.FeaturePanel__icon {
position: absolute;
right: 5px;
top: 5px;
color: #666;
}
.FeaturePanelButton {
width: 130px;
height: 130px;
padding: 0px;
}
.FeaturePanelButton__label {
font-size: 14px;
color: #191E23;
text-align: center;
max-width: 100%;
white-space: inherit;
overflow: hidden;
text-overflow: ellipsis;
}
.sgLogoutLink__icon-image {
margin-left: 5px;
font-size: 20px;
vertical-align: middle;
}

View File

@ -85,12 +85,17 @@
<tr class="kuiTableRow" data-ng-repeat="permission in permissionsResource.permissions track by $index">
<td class="kuiTableRowCell cellAlignTop">
<fieldset class="marginbottom--small" id="object-form-actiongroups">
<ui-select ng-model="permissionsResource.permissions[$index]">
<ui-select
uis-open-close="onCloseNewSinglePermission(isOpen, $select)"
ng-model="permissionsResource.permissions[$index]">
<ui-select-match placeholder="Start with cluster: or indices:">
{{permission}}
{{permission.name || permission}}
</ui-select-match>
<ui-select-choices repeat="item in (permissionItems | filter: $select.search) track by $index">
<span ng-bind="item"></span>
<ui-select-choices
refresh="refreshNewSinglePermission($select)"
refresh-delay="0"
repeat="item in (permissionItems | filter: $select.search) track by $index">
<span ng-bind="item.name"></span>
</ui-select-choices>
</ui-select>
</fieldset>

View File

@ -47,9 +47,64 @@ app.directive('securitycPermissions', function () {
// UI-Select seems to work best with a plain array in this case
scope.permissionItems = scope.allpermissionsAutoComplete.map((item) => {
return item.name;
return item;
});
/**
* This is a helper for when the autocomplete was closed an item being explicitly selected (mouse, tab or enter).
* When you e.g. type a custom value and then click somewhere outside of the autocomplete, it looks like the
* custom value was selected, but it is never saved to the model. This function calls the "select" method
* every time the autocomplete is closed, no matter how. This may mean that the select function is called
* twice, so the select handler should mitigate that if necessary.
* @param isOpen
* @param $select
*/
scope.onCloseNewSinglePermission = function(isOpen, $select, index) {
if (isOpen || !$select.select || !$select.selected) {
return;
}
if ($select.selected.name) {
$select.select($select.selected.name);
}
};
/**
* Allow custom values for the single permission autocomplete
*
* @credit https://medium.com/angularjs-meetup-south-london/angular-extending-ui-select-to-accept-user-input-937bc925267c
* @param $select
*/
scope.refreshNewSinglePermission = function($select) {
var search = $select.search,
list = angular.copy($select.items),
FLAG = -1; // Identifies the custom value
// Clean up any previous custom input
list = list.filter(function(item) {
return item.id !== FLAG;
});
if (!search) {
$select.items = list;
} else {
if (typeof scope.application === 'undefined') {
// For "non-application" permissions, we need custom entries to start with cluster: or indices:
if (search.indexOf('cluster:') !== 0 && search.indexOf('indices:') !== 0) {
return;
}
}
// Add and select the custom value
let customItem = {
id: FLAG,
name: search
};
$select.items = [customItem].concat(list);
$select.selected = customItem;
}
};
/**
* This is a weird workaround for the autocomplete where
* we have can't or don't want to use the model item

View File

@ -32,6 +32,9 @@
import chrome from 'ui/chrome';
import { uiModules } from 'ui/modules';
// This fixes an issue where the app icons would disappear while having a non-Kibana app open.
// Should be fixed starting from Kibana 6.6.2
import 'ui/autoload/modules';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
require ('../../apps/configuration/systemstate/systemstate');
@ -106,19 +109,19 @@ export function enableConfiguration($http, $window, systemstate) {
// rest module installed, check if user has access to the API
systemstate.loadRestInfo().then(function(){
chrome.getNavLinkById("security-configuration").hidden = false;
FeatureCatalogueRegistryProvider.register(() => {
return {
id: 'security-configuration',
title: 'Security Configuration',
description: 'Configure users, roles and permissions for Open Distro Security.',
icon: 'securityApp',
path: '/app/security-configuration',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN
};
});
});
chrome.getNavLinkById("security-configuration").hidden = false;
FeatureCatalogueRegistryProvider.register(() => {
return {
id: 'security-configuration',
title: 'Security Configuration',
description: 'Configure users, roles and permissions for Open Distro Security.',
icon: 'securityApp',
path: '/app/security-configuration',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN
};
});
});
}
uiModules.get('security').run(enableConfiguration);

View File

@ -1,14 +1,12 @@
<div class="global-nav-link" tooltip="{{logoutTooltip}}" tooltip-placement="right" tooltip-popup-delay="0" tooltip-append-to-body="1" icon="'plugins/opendistro_security/images/logout.svg'" tooltip-content="Logout" title="'Logout'">
<div class="kbnGlobalNavLink sgLogoutLink" tooltip="{{logoutTooltip}}" tooltip-placement="right" tooltip-popup-delay="0" tooltip-append-to-body="1" icon="'plugins/opendistro_security/images/logout.svg'" tooltip-content="Logout" title="'Logout'">
<a class="global-nav-link__anchor" ng-click="logout()">
<div class="global-nav-link__icon">
<i class="fa fa-sign-out global-nav-link__icon-image"></i>
<a class="kbnGlobalNavLink__anchor" ng-click="logout()">
<div class="kbnGlobalNavLink__icon">
<i class="fa fa-sign-out sgLogoutLink__icon-image"></i>
</div>
<div class="global-nav-link__title ng-binding" style="overflow: hidden; text-overflow: ellipsis; padding-right: 5px">
<div class="kbnGlobalNavLink__title ng-binding" style="overflow: hidden; text-overflow: ellipsis; padding-right: 5px">
{{logoutButtonLabel}}
</div>
</a>
</div>
</div>

View File

@ -34,8 +34,10 @@ import chrome from 'ui/chrome';
import { uiModules } from 'ui/modules';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { EuiIcon } from '@elastic/eui';
import { parse } from 'url';
export function toggleNavLink(Private) {
export function enableMultiTenancy(Private) {
const securityDynamic = chrome.getInjected().securityDynamic;
var enabled = chrome.getInjected('multitenancy_enabled');
chrome.getNavLinkById("security-multitenancy").hidden = !enabled;
if (enabled) {
@ -50,9 +52,79 @@ export function toggleNavLink(Private) {
category: FeatureCatalogueCategory.DATA
};
});
}
}
// Add tenant info to the request
if (securityDynamic && securityDynamic.multiTenancy) {
// Add the tenant to URLs copied from the share panel
document.addEventListener('copy', (event) => {
const shareButton = document.querySelector('[data-share-url]');
const target = document.querySelector('body > span');
// The copy event listens to Cmd + C too, so we need to make sure
// that we're actually copied something via the share panel
if (shareButton && target && shareButton.getAttribute('data-share-url') == target.textContent) {
let originalValue = target.textContent;
let urlPart = originalValue;
// We need to figure out where in the value to add the tenant.
// Since Kibana sometimes adds values that aren't in the current location/url,
// we need to use the actual input values to do a sanity check.
try {
// For the iFrame urls we need to parse out the src
if (originalValue.toLowerCase().indexOf('<iframe') === 0) {
const regex = /<iframe[^>]*src="([^"]*)"/i;
let match = regex.exec(originalValue);
if (match) {
urlPart = match[1]; // Contains the matched src, [0] contains the string where the match was found
}
}
let newValue = addTenantToURL(urlPart, originalValue, securityDynamic.multiTenancy.currentTenantName);
if (newValue !== originalValue) {
target.textContent = newValue;
}
} catch (error) {
// Probably wasn't an url, so we just ignore this
}
}
});
}
}
uiModules.get('security').run(toggleNavLink);
/**
* Add the tenant the value. The originalValue may include more than just an URL, e.g. for iFrame embeds.
* @param url - The url we will append the tenant to
* @param originalValue - In the case of iFrame embeds, we can't just replace the url itself
* @returns {*}
*/
function addTenantToURL(url, originalValue = null, userRequestedTenant) {
const tenantKey = 'tenant';
const tenantKeyAndValue = tenantKey + '=' + userRequestedTenant;
if (! originalValue) {
originalValue = url;
}
let {host, pathname, search} = parse(url);
let queryDelimiter = (!search) ? '?' : '&';
// The url parser returns null if the search is empty. Change that to an empty
// string so that we can use it to build the values later
if (search === null) {
search = '';
} else if (search.toLowerCase().indexOf(tenantKey) > - 1) {
// If we for some reason already have a tenant in the URL we skip any updates
return originalValue;
}
// A helper for finding the part in the string that we want to extend/replace
let valueToReplace = host + pathname + search;
let replaceWith = valueToReplace + queryDelimiter + tenantKeyAndValue;
return originalValue.replace(valueToReplace, replaceWith);
}
uiModules.get('security').run(enableMultiTenancy);

View File

@ -32,7 +32,6 @@
import chrome from 'ui/chrome';
import { uiModules } from 'ui/modules';
import { toastNotifications } from 'ui/notify';
import setupShareObserver from '../../chrome/multitenancy/observe_share_links';
import './readonly.less';
// Needed to access the dashboardProvider
@ -456,11 +455,6 @@ export function enableReadOnly($rootScope, $http, $window, $timeout, $q, $locati
$rootScope.$on('$routeChangeSuccess', function(event, next, current) {
if (next && next.$$route && next.$$route.originalPath) {
document.body.setAttribute('security_path', next.$$route.originalPath.replace(':', '').split('/').join('_'));
if (chrome.getInjected('multitenancy_enabled') && next.locals && next.locals.security_resolvedInfo) {
setupShareObserver($timeout, next.locals.security_resolvedInfo.userRequestedTenant);
}
}
});