Skip to content

Commit

Permalink
Implement AnonymousAuthenticationProvider.
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin committed Nov 2, 2020
1 parent 2dcb6b4 commit 5801797
Show file tree
Hide file tree
Showing 35 changed files with 1,318 additions and 290 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/security/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const UNKNOWN_SPACE = '?';
export const GLOBAL_RESOURCE = '*';
export const APPLICATION_PREFIX = 'kibana-';
export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*';

export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint';
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function mockAuthenticatedUser(user: Partial<AuthenticatedUser> = {}) {
enabled: true,
authentication_realm: { name: 'native1', type: 'native' },
lookup_realm: { name: 'native1', type: 'native' },
authentication_provider: 'basic1',
authentication_provider: { type: 'basic', name: 'basic1' },
authentication_type: 'realm',
...user,
};
Expand Down
15 changes: 15 additions & 0 deletions x-pack/plugins/security/common/model/authenticated_user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,34 @@ describe('#canUserChangePassword', () => {
expect(
canUserChangePassword({
username: 'foo',
authentication_provider: { type: 'basic', name: 'basic1' },
authentication_realm: {
name: 'the realm name',
type: realm,
},
} as AuthenticatedUser)
).toEqual(true);
});

it(`returns false for users in the ${realm} realm if used for anonymous access`, () => {
expect(
canUserChangePassword({
username: 'foo',
authentication_provider: { type: 'anonymous', name: 'does not matter' },
authentication_realm: {
name: 'the realm name',
type: realm,
},
} as AuthenticatedUser)
).toEqual(false);
});
});

it(`returns false for all other realms`, () => {
expect(
canUserChangePassword({
username: 'foo',
authentication_provider: { type: 'the provider type', name: 'does not matter' },
authentication_realm: {
name: 'the realm name',
type: 'does not matter',
Expand Down
10 changes: 7 additions & 3 deletions x-pack/plugins/security/common/model/authenticated_user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import type { AuthenticationProvider } from '../types';
import { User } from './user';

const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native'];
Expand All @@ -28,9 +29,9 @@ export interface AuthenticatedUser extends User {
lookup_realm: UserRealm;

/**
* Name of the Kibana authentication provider that used to authenticate user.
* The authentication provider that used to authenticate user.
*/
authentication_provider: string;
authentication_provider: AuthenticationProvider;

/**
* The AuthenticationType used by ES to authenticate the user.
Expand All @@ -41,5 +42,8 @@ export interface AuthenticatedUser extends User {
}

export function canUserChangePassword(user: AuthenticatedUser) {
return REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE.includes(user.authentication_realm.type);
return (
REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE.includes(user.authentication_realm.type) &&
user.authentication_provider.type !== 'anonymous'
);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/public';
import { LoginSelector } from '../../../../../common/login_state';
import { SectionLoading } from '../../../../../../../../src/plugins/es_ui_shared/public';
import type { LoginSelector, LoginSelectorProvider } from '../../../../../common/login_state';
import { LoginValidator } from './validate_login';

interface Props {
Expand All @@ -39,12 +40,12 @@ interface Props {
infoMessage?: string;
loginAssistanceMessage: string;
loginHelp?: string;
authProviderHint?: string;
}

interface State {
loadingState:
| { type: LoadingStateType.None }
| { type: LoadingStateType.Form }
| { type: LoadingStateType.None | LoadingStateType.Form | LoadingStateType.AutoLogin }
| { type: LoadingStateType.Selector; providerName: string };
username: string;
password: string;
Expand All @@ -59,6 +60,7 @@ enum LoadingStateType {
None,
Form,
Selector,
AutoLogin,
}

enum MessageType {
Expand All @@ -76,11 +78,26 @@ export enum PageMode {
export class LoginForm extends Component<Props, State> {
private readonly validator: LoginValidator;

/**
* Optional provider that was suggested by the `authProviderHint={providerName}` query string parameter. If provider
* doesn't require Kibana native login form then login process is triggered automatically, otherwise Login Selector
* just switches to the Login Form mode.
*/
private readonly suggestedProvider?: LoginSelectorProvider;

constructor(props: Props) {
super(props);
this.validator = new LoginValidator({ shouldValidate: false });

const mode = this.showLoginSelector() ? PageMode.Selector : PageMode.Form;
this.suggestedProvider = this.props.authProviderHint
? this.props.selector.providers.find(({ name }) => name === this.props.authProviderHint)
: undefined;

// Switch to the Form mode right away if provider from the hint requires it.
const mode =
this.showLoginSelector() && !this.suggestedProvider?.usesLoginForm
? PageMode.Selector
: PageMode.Form;

this.state = {
loadingState: { type: LoadingStateType.None },
Expand All @@ -94,6 +111,12 @@ export class LoginForm extends Component<Props, State> {
};
}

async componentDidMount() {
if (this.suggestedProvider?.usesLoginForm === false) {
await this.loginWithSelector({ provider: this.suggestedProvider, autoLogin: true });
}
}

public render() {
return (
<Fragment>
Expand Down Expand Up @@ -160,7 +183,9 @@ export class LoginForm extends Component<Props, State> {
case PageMode.Form:
return this.renderLoginForm();
case PageMode.Selector:
return this.renderSelector();
return this.isLoadingState(LoadingStateType.AutoLogin)
? this.renderAutoLoginOverlay()
: this.renderSelector();
case PageMode.LoginHelp:
return this.renderLoginHelp();
}
Expand Down Expand Up @@ -267,7 +292,7 @@ export class LoginForm extends Component<Props, State> {
onClick={() =>
provider.usesLoginForm
? this.onPageModeChange(PageMode.Form)
: this.loginWithSelector(provider.type, provider.name)
: this.loginWithSelector({ provider })
}
className={`secLoginCard ${
this.isLoadingState(LoadingStateType.Selector, provider.name)
Expand Down Expand Up @@ -360,6 +385,19 @@ export class LoginForm extends Component<Props, State> {
return null;
};

private renderAutoLoginOverlay = () => {
return (
<EuiPanel data-test-subj="loginSelector" paddingSize="none">
<SectionLoading>
<FormattedMessage
id="xpack.security.loginPage.autoLoginLabel"
defaultMessage="Logging in…"
/>
</SectionLoading>
</EuiPanel>
);
};

private setUsernameInputRef(ref: HTMLInputElement) {
if (ref) {
ref.focus();
Expand Down Expand Up @@ -438,9 +476,17 @@ export class LoginForm extends Component<Props, State> {
}
};

private loginWithSelector = async (providerType: string, providerName: string) => {
private loginWithSelector = async ({
provider: { type: providerType, name: providerName },
autoLogin,
}: {
provider: LoginSelectorProvider;
autoLogin?: boolean;
}) => {
this.setState({
loadingState: { type: LoadingStateType.Selector, providerName },
loadingState: autoLogin
? { type: LoadingStateType.AutoLogin }
: { type: LoadingStateType.Selector, providerName },
message: { type: MessageType.None },
});

Expand All @@ -466,7 +512,9 @@ export class LoginForm extends Component<Props, State> {
}
};

private isLoadingState(type: LoadingStateType.None | LoadingStateType.Form): boolean;
private isLoadingState(
type: LoadingStateType.None | LoadingStateType.Form | LoadingStateType.AutoLogin
): boolean;
private isLoadingState(type: LoadingStateType.Selector, providerName: string): boolean;
private isLoadingState(type: LoadingStateType, providerName?: string) {
const { loadingState } = this.state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { act } from '@testing-library/react';
import { nextTick } from 'test_utils/enzyme_helpers';
import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants';
import { LoginState } from '../../../common/login_state';
import { LoginPage } from './login_page';
import { coreMock } from '../../../../../../src/core/public/mocks';
Expand Down Expand Up @@ -37,14 +38,12 @@ describe('LoginPage', () => {
httpMock.addLoadingCountSource.mockReset();
};

beforeAll(() => {
beforeEach(() => {
Object.defineProperty(window, 'location', {
value: { href: 'http://some-host/bar', protocol: 'http' },
writable: true,
});
});

beforeEach(() => {
resetHttpMock();
});

Expand Down Expand Up @@ -206,10 +205,10 @@ describe('LoginPage', () => {
expect(wrapper.find(LoginForm)).toMatchSnapshot();
});

it('renders as expected when info message is set', async () => {
it('properly passes query string parameters to the form', async () => {
const coreStartMock = coreMock.createStart();
httpMock.get.mockResolvedValue(createLoginState());
window.location.href = 'http://some-host/bar?msg=SESSION_EXPIRED';
window.location.href = `http://some-host/bar?msg=SESSION_EXPIRED&${AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER}=basic1`;

const wrapper = shallow(
<LoginPage
Expand All @@ -226,7 +225,9 @@ describe('LoginPage', () => {
resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot
});

expect(wrapper.find(LoginForm)).toMatchSnapshot();
const { authProviderHint, infoMessage } = wrapper.find(LoginForm).props();
expect(authProviderHint).toBe('basic1');
expect(infoMessage).toBe('Your session has timed out. Please log in again.');
});

it('renders as expected when loginAssistanceMessage is set', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle } from '@elasti
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public';
import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants';
import { LoginState } from '../../../common/login_state';
import { LoginForm, DisabledLoginForm } from './components';

Expand Down Expand Up @@ -212,14 +213,16 @@ export class LoginPage extends Component<Props, State> {
);
}

const query = parse(window.location.href, true).query;
return (
<LoginForm
http={this.props.http}
notifications={this.props.notifications}
selector={selector}
infoMessage={infoMessageMap.get(parse(window.location.href, true).query.msg?.toString())}
infoMessage={infoMessageMap.get(query.msg?.toString())}
loginAssistanceMessage={this.props.loginAssistanceMessage}
loginHelp={loginHelp}
authProviderHint={query[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]?.toString()}
/>
);
};
Expand Down
Loading

0 comments on commit 5801797

Please sign in to comment.