Migrate config deprecations and ShieldUser functionality to the New Platform (#53768)

This commit is contained in:
Aleh Zasypkin 2020-01-06 11:43:15 +01:00 committed by GitHub
parent d64c4cb5fe
commit aa38fb68a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 328 additions and 215 deletions

View file

@ -11,6 +11,8 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { UIRoutes } from 'ui/routes';
import { isLeft } from 'fp-ts/lib/Either';
import { npSetup } from 'ui/new_platform';
import { SecurityPluginSetup } from '../../../../../../../plugins/security/public';
import { BufferedKibanaServiceCall, KibanaAdapterServiceRefs, KibanaUIConfig } from '../../types';
import {
FrameworkAdapter,
@ -58,7 +60,7 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
};
public async waitUntilFrameworkReady(): Promise<void> {
const $injector = await this.onKibanaReady();
await this.onKibanaReady();
const xpackInfo: any = this.xpackInfoService;
let xpackInfoUnpacked: FrameworkInfo;
@ -95,8 +97,10 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
}
this.xpackInfo = xpackInfoUnpacked;
const securitySetup = ((npSetup.plugins as unknown) as { security?: SecurityPluginSetup })
.security;
try {
this.shieldUser = await $injector.get('ShieldUser').getCurrent().$promise;
this.shieldUser = (await securitySetup?.authc.getCurrentUser()) || null;
const assertUser = RuntimeFrameworkUser.decode(this.shieldUser);
if (isLeft(assertUser)) {

View file

@ -8,6 +8,7 @@ import React from 'react';
import { render } from 'react-dom';
import { isEmpty } from 'lodash';
import { uiModules } from 'ui/modules';
import { npSetup } from 'ui/new_platform';
import { toastNotifications } from 'ui/notify';
import { I18nContext } from 'ui/i18n';
import { PipelineEditor } from '../../../../components/pipeline_editor';
@ -21,7 +22,6 @@ app.directive('pipelineEdit', function($injector) {
const pipelineService = $injector.get('pipelineService');
const licenseService = $injector.get('logstashLicenseService');
const kbnUrl = $injector.get('kbnUrl');
const shieldUser = $injector.get('ShieldUser');
const $route = $injector.get('$route');
return {
@ -32,7 +32,7 @@ app.directive('pipelineEdit', function($injector) {
scope.$evalAsync(kbnUrl.change(`/management/logstash/pipelines/${id}/edit`));
const userResource = logstashSecurity.isSecurityEnabled()
? await shieldUser.getCurrent().$promise
? await npSetup.plugins.security.authc.getCurrentUser()
: null;
render(

View file

@ -28,17 +28,10 @@ export const security = kibana =>
enabled: Joi.boolean().default(true),
cookieName: HANDLED_IN_NEW_PLATFORM,
encryptionKey: HANDLED_IN_NEW_PLATFORM,
session: Joi.object({
idleTimeout: HANDLED_IN_NEW_PLATFORM,
lifespan: HANDLED_IN_NEW_PLATFORM,
}).default(),
session: HANDLED_IN_NEW_PLATFORM,
secureCookies: HANDLED_IN_NEW_PLATFORM,
loginAssistanceMessage: HANDLED_IN_NEW_PLATFORM,
authorization: Joi.object({
legacyFallback: Joi.object({
enabled: Joi.boolean().default(true), // deprecated
}).default(),
}).default(),
authorization: HANDLED_IN_NEW_PLATFORM,
audit: Joi.object({
enabled: Joi.boolean().default(false),
}).default(),
@ -46,13 +39,6 @@ export const security = kibana =>
}).default();
},
deprecations: function({ rename, unused }) {
return [
unused('authorization.legacyFallback.enabled'),
rename('sessionTimeout', 'session.idleTimeout'),
];
},
uiExports: {
chromeNavControls: [],
managementSections: ['plugins/security/views/management'],

View file

@ -5,16 +5,12 @@
*/
import { kfetch } from 'ui/kfetch';
import { AuthenticatedUser, Role, User, EditUser } from '../../common/model';
import { Role, User, EditUser } from '../../common/model';
const usersUrl = '/internal/security/users';
const rolesUrl = '/api/security/role';
export class UserAPIClient {
public async getCurrentUser(): Promise<AuthenticatedUser> {
return await kfetch({ pathname: `/internal/security/me` });
}
public async getUsers(): Promise<User[]> {
return await kfetch({ pathname: usersUrl });
}

View file

@ -1,33 +0,0 @@
/*
* 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 'angular-resource';
import angular from 'angular';
import { uiModules } from 'ui/modules';
const module = uiModules.get('security', ['ngResource']);
module.service('ShieldUser', ($resource, chrome) => {
const baseUrl = chrome.addBasePath('/internal/security/users/:username');
const ShieldUser = $resource(
baseUrl,
{
username: '@username',
},
{
changePassword: {
method: 'POST',
url: `${baseUrl}/password`,
transformRequest: ({ password, newPassword }) => angular.toJson({ password, newPassword }),
},
getCurrent: {
method: 'GET',
url: chrome.addBasePath('/internal/security/me'),
},
}
);
return ShieldUser;
});

View file

@ -6,22 +6,13 @@
import routes from 'ui/routes';
import template from './account.html';
import '../../services/shield_user';
import { i18n } from '@kbn/i18n';
import { I18nContext } from 'ui/i18n';
import { npSetup } from 'ui/new_platform';
import { AccountManagementPage } from './components';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
const renderReact = (elem, user) => {
render(
<I18nContext>
<AccountManagementPage user={user} />
</I18nContext>,
elem
);
};
routes.when('/account', {
template,
k7Breadcrumbs: () => [
@ -31,13 +22,8 @@ routes.when('/account', {
}),
},
],
resolve: {
user(ShieldUser) {
return ShieldUser.getCurrent().$promise;
},
},
controllerAs: 'accountController',
controller($scope, $route) {
controller($scope) {
$scope.$on('$destroy', () => {
const elem = document.getElementById('userProfileReactRoot');
if (elem) {
@ -45,8 +31,12 @@ routes.when('/account', {
}
});
$scope.$$postDigest(() => {
const elem = document.getElementById('userProfileReactRoot');
renderReact(elem, $route.current.locals.user);
render(
<I18nContext>
<AccountManagementPage securitySetup={npSetup.plugins.security} />
</I18nContext>,
document.getElementById('userProfileReactRoot')
);
});
},
});

View file

@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { act } from '@testing-library/react';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { securityMock } from '../../../../../../../plugins/security/public/mocks';
import { AccountManagementPage } from './account_management_page';
import { AuthenticatedUser } from '../../../../common/model';
jest.mock('ui/kfetch');
@ -32,10 +35,24 @@ const createUser = ({ withFullName = true, withEmail = true, realm = 'native' }:
};
};
function getSecuritySetupMock({ currentUser }: { currentUser: AuthenticatedUser }) {
const securitySetupMock = securityMock.createSetup();
securitySetupMock.authc.getCurrentUser.mockResolvedValue(currentUser);
return securitySetupMock;
}
describe('<AccountManagementPage>', () => {
it(`displays users full name, username, and email address`, () => {
it(`displays users full name, username, and email address`, async () => {
const user = createUser();
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
const wrapper = mountWithIntl(
<AccountManagementPage securitySetup={getSecuritySetupMock({ currentUser: user })} />
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(
user.full_name
);
@ -43,28 +60,60 @@ describe('<AccountManagementPage>', () => {
expect(wrapper.find('[data-test-subj="email"]').text()).toEqual(user.email);
});
it(`displays username when full_name is not provided`, () => {
it(`displays username when full_name is not provided`, async () => {
const user = createUser({ withFullName: false });
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
const wrapper = mountWithIntl(
<AccountManagementPage securitySetup={getSecuritySetupMock({ currentUser: user })} />
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(user.username);
});
it(`displays a placeholder when no email address is provided`, () => {
it(`displays a placeholder when no email address is provided`, async () => {
const user = createUser({ withEmail: false });
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
const wrapper = mountWithIntl(
<AccountManagementPage securitySetup={getSecuritySetupMock({ currentUser: user })} />
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="email"]').text()).toEqual('no email address');
});
it(`displays change password form for users in the native realm`, () => {
it(`displays change password form for users in the native realm`, async () => {
const user = createUser();
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
const wrapper = mountWithIntl(
<AccountManagementPage securitySetup={getSecuritySetupMock({ currentUser: user })} />
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('EuiFieldText[data-test-subj="currentPassword"]')).toHaveLength(1);
expect(wrapper.find('EuiFieldText[data-test-subj="newPassword"]')).toHaveLength(1);
});
it(`does not display change password form for users in the saml realm`, () => {
it(`does not display change password form for users in the saml realm`, async () => {
const user = createUser({ realm: 'saml' });
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
const wrapper = mountWithIntl(
<AccountManagementPage securitySetup={getSecuritySetupMock({ currentUser: user })} />
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('EuiFieldText[data-test-subj="currentPassword"]')).toHaveLength(0);
expect(wrapper.find('EuiFieldText[data-test-subj="newPassword"]')).toHaveLength(0);
});

View file

@ -4,29 +4,41 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { SecurityPluginSetup } from '../../../../../../../plugins/security/public';
import { getUserDisplayName, AuthenticatedUser } from '../../../../common/model';
import { ChangePassword } from './change_password';
import { PersonalInfo } from './personal_info';
interface Props {
user: AuthenticatedUser;
securitySetup: SecurityPluginSetup;
}
export const AccountManagementPage: React.FC<Props> = props => (
<EuiPage>
<EuiPageBody restrictWidth>
<EuiPanel>
<EuiText data-test-subj={'userDisplayName'}>
<h1>{getUserDisplayName(props.user)}</h1>
</EuiText>
export const AccountManagementPage = (props: Props) => {
const [currentUser, setCurrentUser] = useState<AuthenticatedUser | null>(null);
useEffect(() => {
props.securitySetup.authc.getCurrentUser().then(setCurrentUser);
}, [props]);
<EuiSpacer size="xl" />
if (!currentUser) {
return null;
}
<PersonalInfo user={props.user} />
return (
<EuiPage>
<EuiPageBody restrictWidth>
<EuiPanel>
<EuiText data-test-subj={'userDisplayName'}>
<h1>{getUserDisplayName(currentUser)}</h1>
</EuiText>
<ChangePassword user={props.user} />
</EuiPanel>
</EuiPageBody>
</EuiPage>
);
<EuiSpacer size="xl" />
<PersonalInfo user={currentUser} />
<ChangePassword user={currentUser} />
</EuiPanel>
</EuiPageBody>
</EuiPage>
);
};

View file

@ -11,10 +11,10 @@ import { kfetch } from 'ui/kfetch';
import { fatalError, toastNotifications } from 'ui/notify';
import { npStart } from 'ui/new_platform';
import template from 'plugins/security/views/management/edit_role/edit_role.html';
import 'plugins/security/services/shield_user';
import 'plugins/security/services/shield_role';
import 'plugins/security/services/shield_indices';
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
import { UserAPIClient } from '../../../lib/api';
import { ROLES_PATH, CLONE_ROLES_PATH, EDIT_ROLES_PATH } from '../management_urls';
import { getEditRoleBreadcrumbs, getCreateRoleBreadcrumbs } from '../breadcrumbs';
@ -69,9 +69,8 @@ const routeDefinition = action => ({
return role.then(res => res.toJSON());
},
users(ShieldUser) {
// $promise is used here because the result is an ngResource, not a promise itself
return ShieldUser.query().$promise.then(users => _.map(users, 'username'));
users() {
return new UserAPIClient().getUsers().then(users => _.map(users, 'username'));
},
indexPatterns() {
return npStart.plugins.data.indexPatterns.getTitles();

View file

@ -4,38 +4,42 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { act } from '@testing-library/react';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { EditUserPage } from './edit_user_page';
import React from 'react';
import { securityMock } from '../../../../../../../../plugins/security/public/mocks';
import { UserAPIClient } from '../../../../lib/api';
import { User, Role } from '../../../../../common/model';
import { ReactWrapper } from 'enzyme';
import { mockAuthenticatedUser } from '../../../../../../../../plugins/security/common/model/authenticated_user.mock';
jest.mock('ui/kfetch');
const createUser = (username: string) => {
const user: User = {
username,
full_name: 'my full name',
email: 'foo@bar.com',
roles: ['idk', 'something'],
enabled: true,
};
if (username === 'reserved_user') {
user.metadata = {
_reserved: true,
};
}
return user;
};
const buildClient = () => {
const apiClient = new UserAPIClient();
const createUser = (username: string) => {
const user: User = {
username,
full_name: 'my full name',
email: 'foo@bar.com',
roles: ['idk', 'something'],
enabled: true,
};
if (username === 'reserved_user') {
user.metadata = {
_reserved: true,
};
}
return Promise.resolve(user);
};
apiClient.getUser = jest.fn().mockImplementation(createUser);
apiClient.getCurrentUser = jest.fn().mockImplementation(() => createUser('current_user'));
apiClient.getUser = jest
.fn()
.mockImplementation(async (username: string) => createUser(username));
apiClient.getRoles = jest.fn().mockImplementation(() => {
return Promise.resolve([
@ -63,6 +67,14 @@ const buildClient = () => {
return apiClient;
};
function buildSecuritySetup() {
const securitySetupMock = securityMock.createSetup();
securitySetupMock.authc.getCurrentUser.mockResolvedValue(
mockAuthenticatedUser(createUser('current_user'))
);
return securitySetupMock;
}
function expectSaveButton(wrapper: ReactWrapper<any, any>) {
expect(wrapper.find('EuiButton[data-test-subj="userFormSaveButton"]')).toHaveLength(1);
}
@ -74,10 +86,12 @@ function expectMissingSaveButton(wrapper: ReactWrapper<any, any>) {
describe('EditUserPage', () => {
it('allows reserved users to be viewed', async () => {
const apiClient = buildClient();
const securitySetup = buildSecuritySetup();
const wrapper = mountWithIntl(
<EditUserPage.WrappedComponent
username={'reserved_user'}
apiClient={apiClient}
securitySetup={securitySetup}
changeUrl={path => path}
intl={null as any}
/>
@ -86,17 +100,19 @@ describe('EditUserPage', () => {
await waitForRender(wrapper);
expect(apiClient.getUser).toBeCalledTimes(1);
expect(apiClient.getCurrentUser).toBeCalledTimes(1);
expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1);
expectMissingSaveButton(wrapper);
});
it('allows new users to be created', async () => {
const apiClient = buildClient();
const securitySetup = buildSecuritySetup();
const wrapper = mountWithIntl(
<EditUserPage.WrappedComponent
username={''}
apiClient={apiClient}
securitySetup={securitySetup}
changeUrl={path => path}
intl={null as any}
/>
@ -105,17 +121,19 @@ describe('EditUserPage', () => {
await waitForRender(wrapper);
expect(apiClient.getUser).toBeCalledTimes(0);
expect(apiClient.getCurrentUser).toBeCalledTimes(0);
expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(0);
expectSaveButton(wrapper);
});
it('allows existing users to be edited', async () => {
const apiClient = buildClient();
const securitySetup = buildSecuritySetup();
const wrapper = mountWithIntl(
<EditUserPage.WrappedComponent
username={'existing_user'}
apiClient={apiClient}
securitySetup={securitySetup}
changeUrl={path => path}
intl={null as any}
/>
@ -124,16 +142,15 @@ describe('EditUserPage', () => {
await waitForRender(wrapper);
expect(apiClient.getUser).toBeCalledTimes(1);
expect(apiClient.getCurrentUser).toBeCalledTimes(1);
expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1);
expectSaveButton(wrapper);
});
});
async function waitForRender(wrapper: ReactWrapper<any, any>) {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
wrapper.update();
await act(async () => {
await nextTick();
wrapper.update();
});
}

View file

@ -28,6 +28,7 @@ import {
} from '@elastic/eui';
import { toastNotifications } from 'ui/notify';
import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react';
import { SecurityPluginSetup } from '../../../../../../../../plugins/security/public';
import { UserValidator, UserValidationResult } from '../../../../lib/validate_user';
import { User, EditUser, Role } from '../../../../../common/model';
import { USERS_PATH } from '../../../../views/management/management_urls';
@ -40,6 +41,7 @@ interface Props {
intl: InjectedIntl;
changeUrl: (path: string) => void;
apiClient: UserAPIClient;
securitySetup: SecurityPluginSetup;
}
interface State {
@ -82,7 +84,7 @@ class EditUserPageUI extends Component<Props, State> {
}
public async componentDidMount() {
const { username, apiClient } = this.props;
const { username, apiClient, securitySetup } = this.props;
let { user, currentUser } = this.state;
if (username) {
try {
@ -91,7 +93,7 @@ class EditUserPageUI extends Component<Props, State> {
password: '',
confirmPassword: '',
};
currentUser = await apiClient.getCurrentUser();
currentUser = await securitySetup.authc.getCurrentUser();
} catch (err) {
toastNotifications.addDanger({
title: this.props.intl.formatMessage({

View file

@ -7,7 +7,6 @@ import routes from 'ui/routes';
import template from 'plugins/security/views/management/edit_user/edit_user.html';
import 'angular-resource';
import 'ui/angular_ui_select';
import 'plugins/security/services/shield_user';
import 'plugins/security/services/shield_role';
import { EDIT_USERS_PATH } from '../management_urls';
import { EditUserPage } from './components';
@ -15,12 +14,18 @@ import { UserAPIClient } from '../../../lib/api';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { I18nContext } from 'ui/i18n';
import { npSetup } from 'ui/new_platform';
import { getEditUserBreadcrumbs, getCreateUserBreadcrumbs } from '../breadcrumbs';
const renderReact = (elem, changeUrl, username) => {
render(
<I18nContext>
<EditUserPage changeUrl={changeUrl} username={username} apiClient={new UserAPIClient()} />
<EditUserPage
changeUrl={changeUrl}
username={username}
apiClient={new UserAPIClient()}
securitySetup={npSetup.plugins.security}
/>
</I18nContext>,
elem
);

View file

@ -13,10 +13,10 @@ import 'plugins/security/views/management/edit_user/edit_user';
import 'plugins/security/views/management/edit_role/index';
import routes from 'ui/routes';
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
import '../../services/shield_user';
import { ROLES_PATH, USERS_PATH, API_KEYS_PATH } from './management_urls';
import { management } from 'ui/management';
import { npSetup } from 'ui/new_platform';
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
@ -36,7 +36,7 @@ routes
})
.defaults(/\/management/, {
resolve: {
securityManagementSection: function(ShieldUser) {
securityManagementSection: function() {
const showSecurityLinks = xpackInfo.get('features.security.showLinks');
function deregisterSecurity() {
@ -93,12 +93,11 @@ routes
if (!showSecurityLinks) {
deregisterSecurity();
} else {
// getCurrent will reject if there is no authenticated user, so we prevent them from seeing the security
// management screens
//
// $promise is used here because the result is an ngResource, not a promise itself
return ShieldUser.getCurrent()
.$promise.then(ensureSecurityRegistered)
// getCurrentUser will reject if there is no authenticated user, so we prevent them from
// seeing the security management screens.
return npSetup.plugins.security.authc
.getCurrentUser()
.then(ensureSecurityRegistered)
.catch(deregisterSecurity);
}
},

View file

@ -8,7 +8,6 @@ import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import routes from 'ui/routes';
import template from 'plugins/security/views/management/users_grid/users.html';
import 'plugins/security/services/shield_user';
import { SECURITY_PATH, USERS_PATH } from '../management_urls';
import { UsersListPage } from './components';
import { UserAPIClient } from '../../../lib/api';

View file

@ -10,36 +10,40 @@ import React from 'react';
import { render } from 'react-dom';
import chrome from 'ui/chrome';
import { I18nContext } from 'ui/i18n';
import { npSetup } from 'ui/new_platform';
import { SecurityPluginSetup } from '../../../../../../plugins/security/public';
import { AuthenticatedUser } from '../../../common/model';
import { AuthenticationStatePage } from '../../components/authentication_state_page';
chrome
.setVisible(false)
.setRootTemplate('<div id="reactOverwrittenSessionRoot" />')
.setRootController('overwritten_session', ($scope: any, ShieldUser: any) => {
.setRootController('overwritten_session', ($scope: any) => {
$scope.$$postDigest(() => {
ShieldUser.getCurrent().$promise.then((user: AuthenticatedUser) => {
const overwrittenSessionPage = (
<I18nContext>
<AuthenticationStatePage
title={
<FormattedMessage
id="xpack.security.overwrittenSession.title"
defaultMessage="You previously logged in as a different user."
/>
}
>
<EuiButton href={chrome.addBasePath('/')}>
<FormattedMessage
id="xpack.security.overwrittenSession.continueAsUserText"
defaultMessage="Continue as {username}"
values={{ username: user.username }}
/>
</EuiButton>
</AuthenticationStatePage>
</I18nContext>
);
render(overwrittenSessionPage, document.getElementById('reactOverwrittenSessionRoot'));
});
((npSetup.plugins as unknown) as { security: SecurityPluginSetup }).security.authc
.getCurrentUser()
.then((user: AuthenticatedUser) => {
const overwrittenSessionPage = (
<I18nContext>
<AuthenticationStatePage
title={
<FormattedMessage
id="xpack.security.overwrittenSession.title"
defaultMessage="You previously logged in as a different user."
/>
}
>
<EuiButton href={chrome.addBasePath('/')}>
<FormattedMessage
id="xpack.security.overwrittenSession.continueAsUserText"
defaultMessage="Continue as {username}"
values={{ username: user.username }}
/>
</EuiButton>
</AuthenticationStatePage>
</I18nContext>
);
render(overwrittenSessionPage, document.getElementById('reactOverwrittenSessionRoot'));
});
});
});

View file

@ -0,0 +1,31 @@
/*
* 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 { HttpSetup } from 'src/core/public';
import { AuthenticatedUser } from '../../common/model';
interface SetupParams {
http: HttpSetup;
}
export interface AuthenticationServiceSetup {
/**
* Returns currently authenticated user and throws if current user isn't authenticated.
*/
getCurrentUser: () => Promise<AuthenticatedUser>;
}
export class AuthenticationService {
public setup({ http }: SetupParams): AuthenticationServiceSetup {
return {
async getCurrentUser() {
return (await http.get('/internal/security/me', {
headers: { 'kbn-system-api': true },
})) as AuthenticatedUser;
},
};
}
}

View file

@ -0,0 +1,13 @@
/*
* 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 { AuthenticationServiceSetup } from './authentication_service';
export const authenticationMock = {
createSetup: (): jest.Mocked<AuthenticationServiceSetup> => ({
getCurrentUser: jest.fn(),
}),
};

View file

@ -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 { AuthenticationService, AuthenticationServiceSetup } from './authentication_service';

View file

@ -6,7 +6,10 @@
import { PluginInitializer } from 'src/core/public';
import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin';
export { SecurityPluginSetup, SecurityPluginStart };
export { SessionInfo } from './types';
export { AuthenticatedUser } from '../common/model';
export const plugin: PluginInitializer<SecurityPluginSetup, SecurityPluginStart> = () =>
new SecurityPlugin();

View file

@ -0,0 +1,19 @@
/*
* 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 { authenticationMock } from './authentication/index.mock';
import { createSessionTimeoutMock } from './session/session_timeout.mock';
function createSetupMock() {
return {
authc: authenticationMock.createSetup(),
sessionTimeout: createSessionTimeoutMock(),
};
}
export const securityMock = {
createSetup: createSetupMock,
};

View file

@ -10,6 +10,8 @@ import { ILicense } from '../../../licensing/public';
import { SecurityNavControlService } from '.';
import { SecurityLicenseService } from '../../common/licensing';
import { nextTick } from 'test_utils/enzyme_helpers';
import { securityMock } from '../mocks';
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
const validLicense = {
isAvailable: true,
@ -29,13 +31,17 @@ describe('SecurityNavControlService', () => {
const license$ = new BehaviorSubject<ILicense>(validLicense);
const navControlService = new SecurityNavControlService();
const mockSecuritySetup = securityMock.createSetup();
mockSecuritySetup.authc.getCurrentUser.mockResolvedValue(
mockAuthenticatedUser({ username: 'some-user', full_name: undefined })
);
navControlService.setup({
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
authc: mockSecuritySetup.authc,
});
const coreStart = coreMock.createStart();
coreStart.chrome.navControls.registerRight = jest.fn();
coreStart.http.get.mockResolvedValue({ username: 'some-user' });
navControlService.start({ core: coreStart });
expect(coreStart.chrome.navControls.registerRight).toHaveBeenCalledTimes(1);
@ -93,6 +99,7 @@ describe('SecurityNavControlService', () => {
const navControlService = new SecurityNavControlService();
navControlService.setup({
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
authc: securityMock.createSetup().authc,
});
const coreStart = coreMock.createStart();
@ -111,6 +118,7 @@ describe('SecurityNavControlService', () => {
const navControlService = new SecurityNavControlService();
navControlService.setup({
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
authc: securityMock.createSetup().authc,
});
const coreStart = coreMock.createStart();
@ -126,6 +134,7 @@ describe('SecurityNavControlService', () => {
const navControlService = new SecurityNavControlService();
navControlService.setup({
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
authc: securityMock.createSetup().authc,
});
const coreStart = coreMock.createStart();
@ -146,6 +155,7 @@ describe('SecurityNavControlService', () => {
const navControlService = new SecurityNavControlService();
navControlService.setup({
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
authc: securityMock.createSetup().authc,
});
const coreStart = coreMock.createStart();

View file

@ -9,11 +9,12 @@ import { CoreStart } from 'src/core/public';
import ReactDOM from 'react-dom';
import React from 'react';
import { SecurityLicense } from '../../common/licensing';
import { AuthenticatedUser } from '../../common/model';
import { SecurityNavControl } from './nav_control_component';
import { AuthenticationServiceSetup } from '../authentication';
interface SetupDeps {
securityLicense: SecurityLicense;
authc: AuthenticationServiceSetup;
}
interface StartDeps {
@ -22,13 +23,15 @@ interface StartDeps {
export class SecurityNavControlService {
private securityLicense!: SecurityLicense;
private authc!: AuthenticationServiceSetup;
private navControlRegistered!: boolean;
private securityFeaturesSubscription?: Subscription;
public setup({ securityLicense }: SetupDeps) {
public setup({ securityLicense, authc }: SetupDeps) {
this.securityLicense = securityLicense;
this.authc = authc;
}
public start({ core }: StartDeps) {
@ -38,14 +41,8 @@ export class SecurityNavControlService {
const shouldRegisterNavControl =
!isAnonymousPath && showLinks && !this.navControlRegistered;
if (shouldRegisterNavControl) {
const user = core.http.get('/internal/security/me', {
headers: {
'kbn-system-api': true,
},
}) as Promise<AuthenticatedUser>;
this.registerSecurityNavControl(core, user);
this.registerSecurityNavControl(core);
}
}
);
@ -60,16 +57,16 @@ export class SecurityNavControlService {
}
private registerSecurityNavControl(
core: Pick<CoreStart, 'chrome' | 'http' | 'i18n' | 'application'>,
user: Promise<AuthenticatedUser>
core: Pick<CoreStart, 'chrome' | 'http' | 'i18n' | 'application'>
) {
const currentUserPromise = this.authc.getCurrentUser();
core.chrome.navControls.registerRight({
order: 2000,
mount: (el: HTMLElement) => {
const I18nContext = core.i18n.Context;
const props = {
user,
user: currentUserPromise,
editProfileUrl: core.http.basePath.prepend('/app/kibana#/account'),
logoutUrl: core.http.basePath.prepend(`/logout`),
};

View file

@ -9,18 +9,20 @@ import { LicensingPluginSetup } from '../../licensing/public';
import {
SessionExpired,
SessionTimeout,
ISessionTimeout,
SessionTimeoutHttpInterceptor,
UnauthorizedResponseHttpInterceptor,
} from './session';
import { SecurityLicenseService } from '../common/licensing';
import { SecurityNavControlService } from './nav_control';
import { AuthenticationService } from './authentication';
export interface PluginSetupDependencies {
licensing: LicensingPluginSetup;
}
export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPluginStart> {
private sessionTimeout!: SessionTimeout;
private sessionTimeout!: ISessionTimeout;
private navControlService!: SecurityNavControlService;
@ -43,12 +45,15 @@ export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPlugi
this.securityLicenseService = new SecurityLicenseService();
const { license } = this.securityLicenseService.setup({ license$: licensing.license$ });
const authc = new AuthenticationService().setup({ http: core.http });
this.navControlService.setup({
securityLicense: license,
authc,
});
return {
anonymousPaths,
authc,
sessionTimeout: this.sessionTimeout,
};
}

View file

@ -5,6 +5,6 @@
*/
export { SessionExpired } from './session_expired';
export { SessionTimeout } from './session_timeout';
export { SessionTimeout, ISessionTimeout } from './session_timeout';
export { SessionTimeoutHttpInterceptor } from './session_timeout_http_interceptor';
export { UnauthorizedResponseHttpInterceptor } from './unauthorized_response_http_interceptor';

View file

@ -41,7 +41,7 @@ export interface ISessionTimeout {
extend(url: string): void;
}
export class SessionTimeout {
export class SessionTimeout implements ISessionTimeout {
private channel?: BroadcastChannel<SessionInfo>;
private sessionInfo?: SessionInfo;
private fetchTimer?: number;

View file

@ -8,7 +8,6 @@ import crypto from 'crypto';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { schema, Type, TypeOf } from '@kbn/config-schema';
import { duration } from 'moment';
import { PluginInitializerContext } from '../../../../src/core/server';
export type ConfigType = ReturnType<typeof createConfig$> extends Observable<infer P>
@ -35,7 +34,6 @@ export const ConfigSchema = schema.object(
schema.maybe(schema.string({ minLength: 32 })),
schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) })
),
sessionTimeout: schema.maybe(schema.nullable(schema.number())), // DEPRECATED
session: schema.object({
idleTimeout: schema.nullable(schema.duration()),
lifespan: schema.nullable(schema.duration()),
@ -88,22 +86,11 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b
secureCookies = true;
}
// "sessionTimeout" is deprecated and replaced with "session.idleTimeout"
// however, NP does not yet have a mechanism to automatically rename deprecated keys
// for the time being, we'll do it manually:
const deprecatedSessionTimeout =
typeof config.sessionTimeout === 'number' ? duration(config.sessionTimeout) : null;
const val = {
return {
...config,
encryptionKey,
secureCookies,
session: {
...config.session,
idleTimeout: config.session.idleTimeout || deprecatedSessionTimeout,
},
};
delete val.sessionTimeout; // DEPRECATED
return val;
})
);
}

View file

@ -4,9 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from '../../../../src/core/server';
import { TypeOf } from '@kbn/config-schema';
import {
PluginConfigDescriptor,
PluginInitializer,
PluginInitializerContext,
RecursiveReadonly,
} from '../../../../src/core/server';
import { ConfigSchema } from './config';
import { Plugin } from './plugin';
import { Plugin, PluginSetupContract, PluginSetupDependencies } from './plugin';
// These exports are part of public Security plugin contract, any change in signature of exported
// functions or removal of exports should be considered as a breaking change.
@ -17,8 +23,17 @@ export {
InvalidateAPIKeyParams,
InvalidateAPIKeyResult,
} from './authentication';
export { PluginSetupContract } from './plugin';
export { PluginSetupContract };
export const config = { schema: ConfigSchema };
export const plugin = (initializerContext: PluginInitializerContext) =>
new Plugin(initializerContext);
export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = {
schema: ConfigSchema,
deprecations: ({ rename, unused }) => [
rename('sessionTimeout', 'session.idleTimeout'),
unused('authorization.legacyFallback.enabled'),
],
};
export const plugin: PluginInitializer<
RecursiveReadonly<PluginSetupContract>,
void,
PluginSetupDependencies
> = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext);

View file

@ -110,10 +110,7 @@ export class Plugin {
this.logger = this.initializerContext.logger.get();
}
public async setup(
core: CoreSetup,
{ features, licensing }: PluginSetupDependencies
): Promise<RecursiveReadonly<PluginSetupContract>> {
public async setup(core: CoreSetup, { features, licensing }: PluginSetupDependencies) {
const [config, legacyConfig] = await combineLatest([
createConfig$(this.initializerContext, core.http.isTlsEnabled),
this.initializerContext.config.legacy.globalConfig$,
@ -169,7 +166,7 @@ export class Plugin {
csp: core.http.csp,
});
return deepFreeze({
return deepFreeze<PluginSetupContract>({
authc,
authz: {