Skip to content

Commit

Permalink
Initial commit for multiple authentication (opensearch-project#1110) (o…
Browse files Browse the repository at this point in the history
…pensearch-project#1191)

Signed-off-by: Aozixuan Priscilla Guan <aoguan@amazon.com>

Co-authored-by: Craig Perkins <cwperx@amazon.com>
Co-authored-by: Ryan Liang <109499885+RyanL1997@users.noreply.github.com>
Co-authored-by: anijain-Amazon <110471048+anijain-Amazon@users.noreply.github.com>
Co-authored-by: Deepak Devarakonda <devardee@amazon.com>
Co-authored-by: Chang Liu <lc12251109@gmail.com>
(cherry picked from commit eee08a5f1d91850c741a0085ea5f2beccaf0c343)
  • Loading branch information
opensearch-trigger-bot[bot] authored Nov 3, 2022
1 parent cd748d0 commit c2fe92a
Show file tree
Hide file tree
Showing 34 changed files with 1,419 additions and 215 deletions.
14 changes: 14 additions & 0 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,29 @@ export const PLUGIN_NAME = 'security-dashboards-plugin';

export const APP_ID_LOGIN = 'login';
export const APP_ID_CUSTOMERROR = 'customerror';
export const OPENDISTRO_SECURITY_ANONYMOUS = 'opendistro_security_anonymous';

export const API_PREFIX = '/api/v1';
export const CONFIGURATION_API_PREFIX = 'configuration';
export const API_ENDPOINT_AUTHINFO = API_PREFIX + '/auth/authinfo';
export const API_ENDPOINT_AUTHTYPE = API_PREFIX + '/auth/type';
export const LOGIN_PAGE_URI = '/app/' + APP_ID_LOGIN;
export const CUSTOM_ERROR_PAGE_URI = '/app/' + APP_ID_CUSTOMERROR;
export const API_AUTH_LOGIN = '/auth/login';
export const API_AUTH_LOGOUT = '/auth/logout';
export const OPENID_AUTH_LOGIN = '/auth/openid/login';
export const SAML_AUTH_LOGIN = '/auth/saml/login';
export const ANONYMOUS_AUTH_LOGIN = '/auth/anonymous';
export const SAML_AUTH_LOGIN_WITH_FRAGMENT = '/auth/saml/captureUrlFragment?nextUrl=%2F';

export const OPENID_AUTH_LOGOUT = '/auth/openid/logout';
export const SAML_AUTH_LOGOUT = '/auth/saml/logout';
export const ANONYMOUS_AUTH_LOGOUT = '/auth/anonymous/logout';

export const ERROR_MISSING_ROLE_PATH = '/missing-role';
export const AUTH_HEADER_NAME = 'authorization';
export const AUTH_GRANT_TYPE = 'authorization_code';
export const AUTH_RESPONSE_TYPE = 'code';

export const GLOBAL_TENANT_SYMBOL = '';
export const PRIVATE_TENANT_SYMBOL = '__user__';
Expand All @@ -42,6 +55,7 @@ export enum AuthType {
JWT = 'jwt',
SAML = 'saml',
PROXY = 'proxy',
ANONYMOUS = 'anonymous',
}

/**
Expand Down
18 changes: 17 additions & 1 deletion public/apps/account/account-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import { CoreStart } from 'opensearch-dashboards/public';
import { AccountNavButton } from './account-nav-button';
import { fetchAccountInfoSafe } from './utils';
import { ClientConfigType } from '../../types';
import { CUSTOM_ERROR_PAGE_URI, ERROR_MISSING_ROLE_PATH } from '../../../common';
import { AuthType, CUSTOM_ERROR_PAGE_URI, ERROR_MISSING_ROLE_PATH } from '../../../common';
import { fetchCurrentTenant, selectTenant } from '../configuration/utils/tenant-utils';
import {
getSavedTenant,
getShouldShowTenantPopup,
setShouldShowTenantPopup,
} from '../../utils/storage-utils';
import { constructErrorMessageAndLog } from '../error-utils';
import { fetchCurrentAuthType } from '../../utils/logout-utils';

function tenantSpecifiedInUrl() {
return (
Expand All @@ -36,6 +37,20 @@ function tenantSpecifiedInUrl() {
}

export async function setupTopNavButton(coreStart: CoreStart, config: ClientConfigType) {
const authType = config.auth?.type;
let currAuthType = '';
if (typeof authType === 'string') {
currAuthType = authType;
} else if (Array.isArray(authType) && authType.length === 1) {
currAuthType = authType[0];
} else {
try {
currAuthType = (await fetchCurrentAuthType(coreStart.http))?.currentAuthType;
} catch (e) {
currAuthType = AuthType.BASIC;
}
}

const accountInfo = (await fetchAccountInfoSafe(coreStart.http))?.data;
if (accountInfo) {
// Missing role error
Expand Down Expand Up @@ -94,6 +109,7 @@ export async function setupTopNavButton(coreStart: CoreStart, config: ClientConf
username={accountInfo.user_name}
tenant={tenant}
config={config}
currAuthType={currAuthType.toLowerCase()}
/>,
element
);
Expand Down
3 changes: 2 additions & 1 deletion public/apps/account/account-nav-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function AccountNavButton(props: {
username: string;
tenant?: string;
config: ClientConfigType;
currAuthType: string;
}) {
const [isPopoverOpen, setPopoverOpen] = React.useState<boolean>(false);
const [modal, setModal] = React.useState<React.ReactNode>(null);
Expand Down Expand Up @@ -137,7 +138,7 @@ export function AccountNavButton(props: {
</>
)}
<LogoutButton
authType={props.config.auth.type}
authType={props.currAuthType}
http={props.coreStart.http}
divider={horizontalRule}
logoutUrl={props.config.auth.logout_url}
Expand Down
15 changes: 9 additions & 6 deletions public/apps/account/log-out-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,46 @@
import React from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { HttpStart } from 'opensearch-dashboards/public';
import { logout, samlLogout } from './utils';
import { externalLogout, logout } from './utils';
import { AuthType, OPENID_AUTH_LOGOUT, SAML_AUTH_LOGOUT } from '../../../common';
import { setShouldShowTenantPopup } from '../../utils/storage-utils';

export function LogoutButton(props: {
authType: string;
http: HttpStart;
divider: JSX.Element;
logoutUrl?: string;
}) {
if (props.authType === 'openid') {
if (props.authType === AuthType.OPEN_ID) {
return (
<div>
{props.divider}
<EuiButtonEmpty
data-test-subj="log-out-2"
color="danger"
size="xs"
href={`${props.http.basePath.serverBasePath}/auth/logout`}
onClick={() => externalLogout(props.http, OPENID_AUTH_LOGOUT)}
>
Log out
</EuiButtonEmpty>
</div>
);
} else if (props.authType === 'saml') {
} else if (props.authType === AuthType.SAML) {
return (
<div>
{props.divider}
<EuiButtonEmpty
data-test-subj="log-out-1"
color="danger"
size="xs"
onClick={() => samlLogout(props.http)}
onClick={() => externalLogout(props.http, SAML_AUTH_LOGOUT)}
>
Log out
</EuiButtonEmpty>
</div>
);
} else if (props.authType === 'proxy') {
} else if (props.authType === AuthType.PROXY) {
setShouldShowTenantPopup(null);
return <div />;
} else {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Account menu - Log out button renders renders when auth type is MultiAuth: basicauth 1`] = `
<div>
<EuiButtonEmpty
color="danger"
data-test-subj="log-out-3"
onClick={[Function]}
size="xs"
>
Log out
</EuiButtonEmpty>
</div>
`;

exports[`Account menu - Log out button renders renders when auth type is MultiAuth: openid 1`] = `
<div>
<EuiButtonEmpty
color="danger"
data-test-subj="log-out-2"
onClick={[Function]}
size="xs"
>
Log out
</EuiButtonEmpty>
</div>
`;

exports[`Account menu - Log out button renders renders when auth type is MultiAuth: saml 1`] = `
<div>
<EuiButtonEmpty
color="danger"
data-test-subj="log-out-1"
onClick={[Function]}
size="xs"
>
Log out
</EuiButtonEmpty>
</div>
`;

exports[`Account menu - Log out button renders renders when auth type is OpenId 1`] = `
<div>
<EuiButtonEmpty
color="danger"
data-test-subj="log-out-2"
href="/auth/logout"
onClick={[Function]}
size="xs"
>
Log out
Expand Down
6 changes: 6 additions & 0 deletions public/apps/account/test/account-app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
getSavedTenant,
} from '../../../utils/storage-utils';
import { fetchAccountInfoSafe } from '../utils';
import { fetchCurrentAuthType } from '../../../utils/logout-utils';
import { fetchCurrentTenant, selectTenant } from '../../configuration/utils/tenant-utils';

jest.mock('../../../utils/storage-utils', () => ({
Expand All @@ -34,6 +35,10 @@ jest.mock('../utils', () => ({
fetchAccountInfoSafe: jest.fn(),
}));

jest.mock('../../../utils/logout-utils', () => ({
fetchCurrentAuthType: jest.fn(),
}));

jest.mock('../../configuration/utils/tenant-utils', () => ({
selectTenant: jest.fn(),
fetchCurrentTenant: jest.fn(),
Expand Down Expand Up @@ -66,6 +71,7 @@ describe('Account app', () => {

beforeAll(() => {
(fetchAccountInfoSafe as jest.Mock).mockResolvedValue(mockAccountInfo);
(fetchCurrentAuthType as jest.Mock).mockResolvedValue('dummy');
(fetchCurrentTenant as jest.Mock).mockResolvedValue(mockTenant);
});

Expand Down
3 changes: 3 additions & 0 deletions public/apps/account/test/account-nav-button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('Account navigation button', () => {
username={userName}
tenant="tenant1"
config={config as any}
currAuthType={'dummy'}
/>
);
});
Expand All @@ -77,6 +78,7 @@ describe('Account navigation button', () => {
username={userName}
tenant="tenant1"
config={config as any}
currAuthType={'dummy'}
/>
);
expect(setState).toBeCalledTimes(1);
Expand Down Expand Up @@ -137,6 +139,7 @@ describe('Account navigation button, multitenancy disabled', () => {
isInternalUser={true}
username={userName}
config={config as any}
currAuthType={'dummy'}
/>
);
expect(setState).toBeCalledTimes(0);
Expand Down
23 changes: 23 additions & 0 deletions public/apps/account/test/log-out-button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,37 @@ describe('Account menu - Log out button', () => {
OpenId = 'openid',
SAML = 'saml',
Proxy = 'proxy',
Basic = 'basicauth',
}

const mockHttpStart = {
basePath: {
serverBasePath: '',
},
};
const mockDivider = <></>;
describe('renders', () => {
it('renders when auth type is MultiAuth: openid', () => {
const component = shallow(
<LogoutButton authType={authType.OpenId} http={mockHttpStart} divider={mockDivider} />
);
expect(component).toMatchSnapshot();
});

it('renders when auth type is MultiAuth: saml', () => {
const component = shallow(
<LogoutButton authType={authType.SAML} http={mockHttpStart} divider={mockDivider} />
);
expect(component).toMatchSnapshot();
});

it('renders when auth type is MultiAuth: basicauth', () => {
const component = shallow(
<LogoutButton authType={authType.Basic} http={mockHttpStart} divider={mockDivider} />
);
expect(component).toMatchSnapshot();
});

it('renders when auth type is OpenId', () => {
const component = shallow(
<LogoutButton authType={authType.OpenId} http={mockHttpStart} divider={mockDivider} />
Expand Down
23 changes: 21 additions & 2 deletions public/apps/account/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
*/

import { HttpStart } from 'opensearch-dashboards/public';
import { API_AUTH_LOGOUT, LOGIN_PAGE_URI } from '../../../common';
import {
API_AUTH_LOGOUT,
LOGIN_PAGE_URI,
OPENID_AUTH_LOGOUT,
SAML_AUTH_LOGOUT,
} from '../../../common';
import { API_ENDPOINT_ACCOUNT_INFO } from './constants';
import { AccountInfo } from './types';
import { httpGet, httpGetWithIgnores, httpPost } from '../configuration/utils/request-utils';
Expand Down Expand Up @@ -43,7 +48,21 @@ export async function logout(http: HttpStart, logoutUrl?: string): Promise<void>
export async function samlLogout(http: HttpStart): Promise<void> {
// This will ensure tenancy is picked up from local storage in the next login.
setShouldShowTenantPopup(null);
window.location.href = `${http.basePath.serverBasePath}${API_AUTH_LOGOUT}`;
window.location.href = `${http.basePath.serverBasePath}${SAML_AUTH_LOGOUT}`;
}

export async function openidLogout(http: HttpStart): Promise<void> {
// This will ensure tenancy is picked up from local storage in the next login.
setShouldShowTenantPopup(null);
sessionStorage.clear();
window.location.href = `${http.basePath.serverBasePath}${OPENID_AUTH_LOGOUT}`;
}

export async function externalLogout(http: HttpStart, logoutEndpoint: string): Promise<void> {
// This will ensure tenancy is picked up from local storage in the next login.
setShouldShowTenantPopup(null);
sessionStorage.clear();
window.location.href = `${http.basePath.serverBasePath}${logoutEndpoint}`;
}

export async function updateNewPassword(
Expand Down
2 changes: 1 addition & 1 deletion public/apps/login/login-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { ClientConfigType } from '../../types';
export function renderApp(
coreStart: CoreStart,
params: AppMountParameters,
config: ClientConfigType['ui']['basicauth']['login']
config: ClientConfigType
) {
ReactDOM.render(<LoginPage http={coreStart.http} config={config} />, params.element);
return () => ReactDOM.unmountComponentAtNode(params.element);
Expand Down
Loading

0 comments on commit c2fe92a

Please sign in to comment.