Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security - Don't allow unauthorized users to login to Kibana #37127

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
70abbfa
WIP
legrego May 9, 2019
addc3b1
semi-functional
legrego May 23, 2019
f9bb630
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego May 23, 2019
67eb4c8
cleanup
legrego May 24, 2019
f9ed532
dedicated messaging for users who aren't authorized Kibana users
legrego May 24, 2019
af4ecb8
rename not_found page to unavailable
legrego May 24, 2019
576da6f
tests is_authorized_kibana_user
legrego May 24, 2019
9c6f8e7
simplify privileges check - YAGNI
legrego May 24, 2019
0bd4a5c
move logic and rendering to xpack_main
legrego May 24, 2019
61d2235
updating functional tests
legrego May 24, 2019
72d376d
cleanup
legrego May 24, 2019
4c69ab2
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego May 24, 2019
bab5b79
fix i18n message ids
legrego May 24, 2019
d08f8c8
update mock user
legrego May 24, 2019
690500c
update route matching
legrego May 24, 2019
2313680
update expectForbidden assertion
legrego May 24, 2019
c62c62a
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego May 29, 2019
1586ae5
attempt to reduce test flakyness
legrego May 29, 2019
b6dfb56
handle missing credentials
legrego May 29, 2019
e3ea08b
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego May 30, 2019
50433ab
guard call to isAuthorizedKibanaUser
legrego May 30, 2019
c62f7c1
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Jun 3, 2019
f2f681d
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Jun 4, 2019
feffa9d
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Jun 5, 2019
d49b7f4
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Jun 7, 2019
1654c9d
don't disable animations twice
legrego Jun 7, 2019
20243cf
Revert "don't disable animations twice"
legrego Jun 7, 2019
0d34e2b
undo forcing disable animations
legrego Jun 7, 2019
cdd1d0e
wait for animations to complete before asserting page contents
legrego Jun 7, 2019
8effcf9
consolidate auth logic
legrego Jun 10, 2019
3f845e9
update test fixture
legrego Jun 10, 2019
66e68ea
fix mock
legrego Jun 11, 2019
0f8b287
remove optional user parameter
legrego Jun 12, 2019
6a04c97
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Jun 12, 2019
e9df3f7
remove duplicate import
legrego Jun 13, 2019
456823d
simplify check for credentials
legrego Jun 13, 2019
5235254
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Jun 13, 2019
640bd1b
extract es application privilege parsing for re-use
legrego Jun 13, 2019
ad4c461
update authorization check
legrego Jun 14, 2019
64d793f
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Jun 20, 2019
260c2ef
fix merge from master
legrego Jun 20, 2019
b5400e5
add missing files
legrego Jun 20, 2019
5fcf289
remove incorrectly migrated files
legrego Jun 20, 2019
d3c43d7
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Jul 3, 2019
0eda529
fix merge
legrego Jul 3, 2019
175b4d2
remove stray files
legrego Jul 3, 2019
81c9892
Merge branch 'master' of github.com:elastic/kibana into security/deny…
legrego Aug 27, 2019
3b01d82
fix merge
legrego Aug 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 17 additions & 18 deletions test/functional/page_objects/error_page.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,28 @@
*/
import expect from '@kbn/expect';

export function ErrorPageProvider({ getPageObjects }) {
const PageObjects = getPageObjects(['common']);
export function ErrorPageProvider({ getService }) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');

class ErrorPage {
async expectForbidden() {
const messageText = await PageObjects.common.getBodyText();
expect(messageText).to.eql(
JSON.stringify({
statusCode: 403,
error: 'Forbidden',
message: 'Forbidden'
})
);
await retry.try(async () => {
const title = await testSubjects.getVisibleText('unavailable-unauthorized-title');
const message = await testSubjects.getVisibleText('unavailable-unauthorized-message');

expect(title).to.eql('No access to Kibana');
expect(message).to.eql('Your account does not have access to Kibana.');
});
}
async expectNotFound() {
const messageText = await PageObjects.common.getBodyText();
expect(messageText).to.eql(
JSON.stringify({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
})
);
await retry.try(async () => {
const title = await testSubjects.getVisibleText('unavailable-notFound-title');
const message = await testSubjects.getVisibleText('unavailable-notFound-message');

expect(title).to.eql('Not found');
expect(message).to.eql('Sorry, the requested resource was not found.');
});
}
}

Expand Down
14 changes: 13 additions & 1 deletion x-pack/legacy/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import {
initAPIAuthorization,
initAppAuthorization,
registerPrivilegesWithCluster,
validateFeaturePrivileges
validateFeaturePrivileges,
isAuthorizedKibanaUser
} from './server/lib/authorization';
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper';
import { deepFreeze } from './server/lib/deep_freeze';
import { createOptionalPlugin } from '../../server/lib/optional_plugin';
import { get } from 'lodash';
import { KibanaRequest } from '../../../../src/core/server';

export const security = (kibana) => new kibana.Plugin({
Expand Down Expand Up @@ -106,6 +108,16 @@ export const security = (kibana) => new kibana.Plugin({
enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'),
};
},
replaceInjectedVars: async function (injectedVars, request, server) {
const { security } = server.plugins;

const userRoles = get(request, 'auth.credentials.roles', []);

return {
...injectedVars,
canAccessKibana: await isAuthorizedKibanaUser(security.authorization, request, userRoles),
};
}
},

async postInit(server) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EuiButton, EuiCallOut, EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer }
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react';
import { LoginState } from '../../../../../common/login_state';
import { UnauthorizedLoginForm } from '../unauthorized_login_form';

interface Props {
http: any;
Expand All @@ -19,7 +20,7 @@ interface Props {
}

interface State {
hasError: boolean;
errorStatusCode: number | null;
isLoading: boolean;
username: string;
password: string;
Expand All @@ -28,14 +29,17 @@ interface State {

class BasicLoginFormUI extends Component<Props, State> {
public state = {
hasError: false,
errorStatusCode: null,
isLoading: false,
username: '',
password: '',
message: '',
};

public render() {
if (this.state.errorStatusCode === 403) {
return <UnauthorizedLoginForm window={this.props.window} />;
}
return (
<Fragment>
{this.renderMessage()}
Expand Down Expand Up @@ -192,7 +196,7 @@ class BasicLoginFormUI extends Component<Props, State> {
}

this.setState({
hasError: true,
errorStatusCode: statusCode,
message,
isLoading: false,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export { UnauthorizedLoginForm } from './unauthorized_login_form';
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import chrome from 'ui/chrome';
import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';

interface Props {
window: any;
intl: InjectedIntl;
}

export const UnauthorizedLoginForm = injectI18n((props: Props) => {
function handleLogoutClick() {
props.window.location = chrome.addBasePath('/logout');
}

return (
<EuiPanel data-test-subj="unauthorized-login-form">
<EuiEmptyPrompt
iconType="lock"
title={
<h2>
<FormattedMessage
id="xpack.security.login.unauthorizedLoginForm.noAccessTitle"
defaultMessage="No access to Kibana"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.security.login.unauthorizedLoginForm.noAccessMessage"
defaultMessage="Your account does not allow access to Kibana. Please contact your administrator."
/>
</p>
}
actions={
<EuiButton color="primary" fill onClick={handleLogoutClick}>
Logout
</EuiButton>
}
/>
</EuiPanel>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export function serverFixture() {
getUser: stub(),
authenticate: stub(),
deauthenticate: stub(),
authorization: {
mode: { useRbacForRequest: () => true },
},
},

xpack_main: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Legacy } from 'kibana';
import { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es';
import { TransformApplicationsFromEsResponse } from './types';

export type GetPrivilegesWithRequest = (
request: Legacy.Request
) => Promise<TransformApplicationsFromEsResponse>;

export function getPrivilegesWithRequestFactory(
application: string,
shieldClient: any
): GetPrivilegesWithRequest {
const { callWithRequest } = shieldClient;

return async function getPrivilegesWithRequest(
request: Legacy.Request
): Promise<TransformApplicationsFromEsResponse> {
const userPrivilegesResponse = await callWithRequest(request, 'shield.userPrivileges');

return transformKibanaApplicationsFromEs(application, userPrivilegesResponse.applications);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export { PrivilegeSerializer } from './privilege_serializer';
export { registerPrivilegesWithCluster } from './register_privileges_with_cluster';
export { ResourceSerializer } from './resource_serializer';
export { validateFeaturePrivileges } from './validate_feature_privileges';
export { transformKibanaApplicationsFromEs } from './transform_kibana_applications_from_es';
export { isAuthorizedKibanaUser } from './is_authorized_kibana_user';
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Legacy } from 'kibana';
import { AuthorizationService } from './service';
import { isAuthorizedKibanaUser } from './is_authorized_kibana_user';
import { TransformApplicationsFromEsResponse } from './types';

function buildAuthorizationService(
privilegesResponse: TransformApplicationsFromEsResponse = { success: true, value: [] }
) {
return ({
application: 'kibana-.kibana',
getPrivilegesWithRequest: jest.fn().mockResolvedValue(privilegesResponse),
mode: {
useRbacForRequest: jest.fn().mockReturnValue(true),
},
privileges: {
get: () => ({
global: {
all: ['actions'],
read: ['actions'],
},
space: {
all: ['actions'],
read: ['actions'],
},
features: {
feature1: {
all: ['actions'],
},
},
reserved: {
reserved_feature_1: ['actions'],
},
}),
},
} as unknown) as AuthorizationService;
}

function buildRequest(): Legacy.Request {
const request: Legacy.Request = ({
headers: { authorization: 'Basic: somegarbage' },
} as unknown) as Legacy.Request;

return request;
}

describe('isAuthorizedKibanaUser', () => {
it('returns true for superusers', async () => {
const request = buildRequest();
const authService = buildAuthorizationService();

await expect(isAuthorizedKibanaUser(authService, request, ['superuser'])).resolves.toEqual(
true
);
});

it('returns false for users with no privileges', async () => {
const request = buildRequest();
const authService = buildAuthorizationService();

await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false);
});

it('returns false for users with only reserved privileges', async () => {
const request = buildRequest();
const authService = buildAuthorizationService({
success: true,
value: [
{
base: [],
feature: {},
_reserved: ['foo'],
spaces: ['*'],
},
],
});

await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false);
});

it('returns true for users with a base privilege', async () => {
const request = buildRequest();
const authService = buildAuthorizationService({
success: true,
value: [
{
base: ['all'],
feature: {},
spaces: ['*'],
},
],
});

await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true);
});

it('returns true for users with a feature privilege', async () => {
const request = buildRequest();
const authService = buildAuthorizationService({
success: true,
value: [
{
base: [],
feature: {
feature1: ['all'],
},
spaces: ['*'],
},
],
});

await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true);
});

it('returns true for users with both reserved and non-reserved privileges', async () => {
const request = buildRequest();
const authService = buildAuthorizationService({
success: true,
value: [
{
base: [],
feature: {
feature1: ['all'],
},
_reserved: ['foo'],
spaces: ['*'],
},
],
});

await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(true);
});

it('returns false for users with unknown privileges', async () => {
const request = buildRequest();
const authService = buildAuthorizationService({
success: true,
value: [
{
base: [],
feature: {
feature1: ['unknown'],
},
spaces: ['*'],
},
],
});

await expect(isAuthorizedKibanaUser(authService, request)).resolves.toEqual(false);
});
});
Loading