mirror of
https://github.com/valitydev/opendistro-security-kibana-plugin.git
synced 2024-11-06 01:55:16 +00:00
commit
b102040d0c
111
index.js
111
index.js
@ -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
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
|
@ -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}`);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
|
104
lib/auth/user.js
104
lib/auth/user.js
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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'),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
34
lib/session/sessionPlugin.js
Executable file → Normal 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
|
||||
};
|
13
package.json
13
package.json
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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>
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user