Skip to content

Commit

Permalink
Implement AnonymousAuthenticationProvider.
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin committed Nov 18, 2020
1 parent 982639f commit 03d11a3
Show file tree
Hide file tree
Showing 29 changed files with 1,557 additions and 99 deletions.
12 changes: 12 additions & 0 deletions test/functional/services/common/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,18 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
return await driver.get(url);
}

/**
* Retrieves the cookie with the given name. Returns null if there is no such cookie. The cookie will be returned as
* a JSON object as described by the WebDriver wire protocol.
* https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Options.html
*
* @param {string} cookieName
* @return {Promise<IWebDriverCookie>}
*/
public async getCookie(cookieName: string) {
return await driver.manage().getCookie(cookieName);
}

/**
* Pauses the execution in the browser, similar to setting a breakpoint for debugging.
* @return {Promise<void>}
Expand Down
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';
13 changes: 13 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 @@ -20,6 +20,19 @@ describe('#canUserChangePassword', () => {
} 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`, () => {
Expand Down
5 changes: 4 additions & 1 deletion x-pack/plugins/security/common/model/authenticated_user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,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 @@ -41,6 +41,14 @@
+ .secLoginCard {
border-top: $euiBorderThin;
}

&.secLoginCard-autoLogin {
border-color: transparent;

+ .secLoginCard {
padding-top: unset;
}
}
}

.secLoginCard__hint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ import {
EuiLoadingSpinner,
EuiLink,
EuiHorizontalRule,
EuiLoadingContent,
} from '@elastic/eui';
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 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,7 +111,17 @@ export class LoginForm extends Component<Props, State> {
};
}

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

public render() {
if (this.isLoadingState(LoadingStateType.AutoLogin)) {
return this.renderAutoLoginOverlay();
}

return (
<Fragment>
{this.renderLoginAssistanceMessage()}
Expand Down Expand Up @@ -267,7 +294,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 +387,32 @@ export class LoginForm extends Component<Props, State> {
return null;
};

private renderAutoLoginOverlay = () => {
return (
<Fragment>
<EuiPanel data-test-subj="loginSelector" paddingSize="none">
{this.props.selector.providers.map(() => (
<EuiLoadingContent className="secLoginCard secLoginCard-autoLogin" lines={2} />
))}
</EuiPanel>
<EuiSpacer />
<EuiFlexGroup alignItems="center" justifyContent="center" gutterSize="m" responsive={false}>
<EuiFlexItem grow={false}>
<EuiText size="s" className="eui-textCenter">
<FormattedMessage
id="xpack.security.loginPage.autoLoginAuthenticatingLabel"
defaultMessage="Authenticating…"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
};

private setUsernameInputRef(ref: HTMLInputElement) {
if (ref) {
ref.focus();
Expand Down Expand Up @@ -438,9 +491,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 +527,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 '@kbn/test/jest';
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 03d11a3

Please sign in to comment.