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

Dynamic Sign in options #1703

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7c45316
Create sign in options panel
davidosorno Nov 29, 2023
2ff33f0
Edit sign in options modal
davidosorno Nov 29, 2023
909e2e9
Getting sign in options from backend
davidosorno Nov 29, 2023
7625f9c
Change buttons name and column name
davidosorno Nov 30, 2023
f1127dd
Move dashboard sign in option to types
davidosorno Dec 1, 2023
ecfe292
Disable update button
davidosorno Dec 1, 2023
b95b663
Add sign in options property to schema
davidosorno Dec 2, 2023
4e0b1c9
Update backend sign in options.
davidosorno Dec 2, 2023
f3b10ff
Display Toast when updating options
davidosorno Dec 2, 2023
a17081c
Create sign in options route
davidosorno Dec 6, 2023
8c61b57
Display sign in options
davidosorno Dec 6, 2023
452415d
Apply prettier
davidosorno Dec 6, 2023
d707cd6
Update backend sign in options
davidosorno Dec 6, 2023
efbd2dd
Track sign in options changes to update UI.
davidosorno Dec 7, 2023
0fa6012
Meet lint requirements
davidosorno Dec 8, 2023
a81d900
Merge branch 'main' into DynamicSignInOptions
davidosorno Dec 11, 2023
c402993
Add sign-in options update route
davidosorno Dec 12, 2023
7a6ba28
Update authOpts with backend sign in options.
davidosorno Dec 12, 2023
9e96c9e
Combine auth types from backend and yml file
davidosorno Dec 13, 2023
ac2a8c4
Enable MultiAuth when more than 1 auth types are present
davidosorno Dec 13, 2023
539751b
Add anonymous sign in option
davidosorno Dec 13, 2023
c2686f0
Test getting dashboard sign-in options from backend
davidosorno Dec 15, 2023
99b62a8
Test dashboard sign in options functionality.
davidosorno Dec 16, 2023
2025a51
Merge branch 'main' into DynamicSignInOptions
davidosorno Dec 16, 2023
76b380e
Apply prettier format
davidosorno Dec 16, 2023
313c0a4
Fix lint errors
davidosorno Dec 16, 2023
8765a6b
Improve anonymous option logic
davidosorno Dec 16, 2023
d51996b
Refactor code to allow isolated test
davidosorno Dec 18, 2023
ce25d3a
Add modal unit test
davidosorno Dec 18, 2023
a765f21
Merge branch 'main' into DynamicSignInOptions
davidosorno Jan 3, 2024
db51bc2
Revalidate sign in options when attemp to login
davidosorno Jan 4, 2024
6bdd973
UI tweaks for OUI patterns
EduardoCorazon Jan 5, 2024
9b5e486
Merge pull request #1 from EduardoCorazon/DynamicSignInOptions
davidosorno Jan 5, 2024
affbaa1
Ensure sign-in with anonymous is not an auto login
davidosorno Jan 9, 2024
95b1b17
Merge branch 'main' into DynamicSignInOptions
davidosorno Feb 5, 2024
512baa7
Add property to display readable auth type names
davidosorno Feb 5, 2024
2db9d84
Change to elastic components based on feedback received
davidosorno Feb 5, 2024
16695d9
Add single sign-on name to buttons
davidosorno Feb 5, 2024
fd44557
Merge branch 'main' into DynamicSignInOptions
DarshitChanpura Feb 6, 2024
33c1c43
Rename variable to match security config pattern
davidosorno Feb 7, 2024
c7391e4
Merge branch 'main' into DynamicSignInOptions
cwperks Mar 19, 2024
e73e72a
Merge branch 'opensearch-project:main' into DynamicSignInOptions
davidosorno Mar 21, 2024
ff11db3
Merge branch 'main' into DynamicSignInOptions
stephen-crawford Apr 1, 2024
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
1 change: 1 addition & 0 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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_DASHBOARDSINFO = API_PREFIX + '/auth/dashboardsinfo';
export const API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS = API_ENDPOINT_DASHBOARDSINFO + '/signinoptions';
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;
Expand Down
16 changes: 16 additions & 0 deletions public/apps/configuration/panels/auth-view/auth-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@ import { AppDependencies } from '../../../types';
import { ExternalLinkButton } from '../../utils/display-utils';
import { getSecurityConfig } from '../../utils/auth-view-utils';
import { InstructionView } from './instruction-view';
import { SignInOptionsPanel } from './dashboard-signin-options';
import { DashboardSignInOptions } from '../../types';

export function AuthView(props: AppDependencies) {
const [authentication, setAuthentication] = React.useState([]);
const [authorization, setAuthorization] = React.useState([]);
const [dashboardSignInOptions, setDashboardSignInOptions] = React.useState<
DashboardSignInOptions[]
>([]);
const [isAnonymousAuthEnable, setAnonymousAuthEnable] = useState(false);
const [loading, setLoading] = useState(false);

React.useEffect(() => {
Expand All @@ -37,6 +43,8 @@ export function AuthView(props: AppDependencies) {

setAuthentication(config.authc);
setAuthorization(config.authz);
setDashboardSignInOptions(config.kibana.sign_in_options);
setAnonymousAuthEnable(config.http.anonymous_auth_enabled);
} catch (e) {
console.log(e);
} finally {
Expand Down Expand Up @@ -68,6 +76,14 @@ export function AuthView(props: AppDependencies) {
<AuthenticationSequencePanel authc={authentication} loading={loading} />
<EuiSpacer size="m" />
{/* @ts-ignore */}
<SignInOptionsPanel
authc={authentication}
signInEnabledOptions={dashboardSignInOptions}
http={props.coreStart.http}
isAnonymousAuthEnable={isAnonymousAuthEnable}
/>
<EuiSpacer size="m" />
{/* @ts-ignore */}
<AuthorizationPanel authz={authorization} loading={loading} config={props.config} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*

Check failure on line 1 in public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (ubuntu-latest)

File must start with a license header

Check failure on line 1 in public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (macos-latest)

File must start with a license header
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

davidosorno marked this conversation as resolved.
Show resolved Hide resolved
import {
EuiBasicTableColumn,
EuiFlexGroup,
EuiGlobalToastList,
EuiHealth,
EuiHorizontalRule,
EuiInMemoryTable,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPanel,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { get, keys } from 'lodash';
import { HttpSetup } from 'opensearch-dashboards/public';
import React, { useEffect, useState } from 'react';
import { updateDashboardSignInOptions } from '../../../../utils/dashboards-info-utils';
import { DashboardOption, DashboardSignInOptions } from '../../types';
import { createErrorToast, createSuccessToast, useToastState } from '../../utils/toast-utils';
import { SignInOptionsModal } from './signin-options-modal';

interface SignInOptionsPanelProps {
authc: [];
signInEnabledOptions: DashboardSignInOptions[];
http: HttpSetup;
isAnonymousAuthEnable: boolean;
}

export const columns: Array<EuiBasicTableColumn<DashboardOption>> = [
{
field: 'displayName',
name: 'Name',
'data-test-subj': 'name',
mobileOptions: {
render: (opt: DashboardOption) => <span>{opt.name}</span>,
header: false,
truncateText: false,
enlarge: true,
width: '100%',
},
sortable: true,
},
{
field: 'status',
name: 'Status',
dataType: 'boolean',
render: (enable: DashboardOption['status']) => {
const color = enable ? 'success' : 'danger';
const label = enable ? 'Enable' : 'Disable';
return <EuiHealth color={color}>{label}</EuiHealth>;
},
},
];

export function SignInOptionsPanel(props: SignInOptionsPanelProps) {
const { authc, signInEnabledOptions, http, isAnonymousAuthEnable } = props;

const domains = keys(authc);
const [toasts, addToast, removeToast] = useToastState();
const [dashboardOptions, setDashboardOptions] = useState<DashboardOption[]>([]);

enum makeAuthTypeHumanReadable {
BASIC = 'Basic Authentication',
SAML = 'SAML',
OPENID = 'OpenID Connect',
ANONYMOUS = 'Anonymous',
}

useEffect(() => {
const getDasboardOptions = () => {
const options = domains
.map((domain) => {
const data = get(authc, domain);

const option = data.http_authenticator.type.toUpperCase();
if (option in DashboardSignInOptions) {
const dashboardOption: DashboardOption = {
name: option,
status: signInEnabledOptions.indexOf(option) > -1,
displayName: makeAuthTypeHumanReadable[option],
};
return dashboardOption;
}
})
.filter((option) => option != null)
.filter(
(option, index, arr) => arr.findIndex((opt) => opt?.name === option?.name) === index
) as DashboardOption[];
setDashboardOptions(options);
};

if (signInEnabledOptions.length > 0 && dashboardOptions.length === 0) {
getDasboardOptions();
}
}, [signInEnabledOptions, authc, dashboardOptions, domains]);

Check failure on line 106 in public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (ubuntu-latest)

React Hook useEffect has a missing dependency: 'makeAuthTypeHumanReadable'. Either include it or remove the dependency array

Check failure on line 106 in public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (macos-latest)

React Hook useEffect has a missing dependency: 'makeAuthTypeHumanReadable'. Either include it or remove the dependency array

useEffect(() => {
if (isAnonymousAuthEnable) {
const option = DashboardSignInOptions.ANONYMOUS;
const anonymousOption: DashboardOption = {
name: DashboardSignInOptions[option],
status: signInEnabledOptions.indexOf(DashboardSignInOptions[option]) > -1,
displayName: makeAuthTypeHumanReadable.ANONYMOUS,
};

setDashboardOptions((prevState) => [...prevState, anonymousOption]);
}
}, [signInEnabledOptions, isAnonymousAuthEnable]);

Check failure on line 119 in public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (ubuntu-latest)

React Hook useEffect has a missing dependency: 'makeAuthTypeHumanReadable.ANONYMOUS'. Either include it or remove the dependency array

Check failure on line 119 in public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (macos-latest)

React Hook useEffect has a missing dependency: 'makeAuthTypeHumanReadable.ANONYMOUS'. Either include it or remove the dependency array

const handleUpdate = async (newSignInOptions: DashboardOption[]) => {
await updateDashboardSignInOptions(
props.http,
newSignInOptions.map((opt) => opt.name as DashboardSignInOptions)
)
.then(() => {
setDashboardOptions((prevOptions) =>
prevOptions.map((option) => {
option.status = newSignInOptions.includes(option);
return option;
})
);

addToast(
createSuccessToast('updatePassed', 'Dashboard SignIn Options', 'Changes applied.')
);
})
.catch((e) => {
console.log('The sign in options could not be updated');
console.log(e);
addToast(
createErrorToast('updatedError', 'Dashboard SignIn Options', 'Error updating values.')
);
});
};

return (
<EuiPanel>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle size="s">
<h3>Dashboards sign-in options</h3>
</EuiTitle>
<EuiText size="xs" color="subdued">
<p>
Configure one or multiple authentication options to appear on the sign-in window for
OpenSearch Dashboards.
</p>
</EuiText>
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
<EuiFlexGroup>
<SignInOptionsModal
dashboardOptions={dashboardOptions}
setDashboardOptions={setDashboardOptions}
handleUpdate={handleUpdate}
/>
</EuiFlexGroup>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiHorizontalRule margin="m" />
<EuiInMemoryTable
tableLayout={'auto'}
columns={columns}
items={dashboardOptions}
itemId={'signin_options'}
sorting={{ sort: { field: 'displayName', direction: 'asc' } }}
/>
<EuiGlobalToastList toasts={toasts} toastLifeTimeMs={3000} dismissToast={removeToast} />
</EuiPanel>
);
}
106 changes: 106 additions & 0 deletions public/apps/configuration/panels/auth-view/signin-options-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*

Check failure on line 1 in public/apps/configuration/panels/auth-view/signin-options-modal.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (ubuntu-latest)

File must start with a license header

Check failure on line 1 in public/apps/configuration/panels/auth-view/signin-options-modal.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (macos-latest)

File must start with a license header
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

import React, { Dispatch, SetStateAction } from 'react';
import { DashboardOption } from '../../types';

import {

Check failure on line 15 in public/apps/configuration/panels/auth-view/signin-options-modal.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (ubuntu-latest)

`@elastic/eui` import should occur before import of `../../types`

Check failure on line 15 in public/apps/configuration/panels/auth-view/signin-options-modal.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests (macos-latest)

`@elastic/eui` import should occur before import of `../../types`
EuiButton,
EuiInMemoryTable,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiSpacer,
} from '@elastic/eui';
import { columns } from './dashboard-signin-options';

interface DashboardSignInProps {
dashboardOptions: DashboardOption[];
setDashboardOptions: Dispatch<SetStateAction<DashboardOption[]>>;
handleUpdate: Function;
}

export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element {
const [newSignInOptions, setNewSignInOptions] = React.useState<DashboardOption[]>([]);
const [disableUpdate, disableUpdateButton] = React.useState(false);
const actualSignInOptions: DashboardOption[] = props.dashboardOptions.filter((opt) => opt.status);

const [isModalVisible, setIsModalVisible] = React.useState(false);
const closeModal = () => setIsModalVisible(false);
const showModal = () => setIsModalVisible(true);

React.useEffect(() => {
if (actualSignInOptions.length !== newSignInOptions.length && newSignInOptions.length > 0) {
disableUpdateButton(false);
} else {
let sameOptions = true;
newSignInOptions.forEach((option) => {
if (actualSignInOptions.includes(option) === false) {
sameOptions = false;
return;
}
});
disableUpdateButton(sameOptions);
}
}, [newSignInOptions, actualSignInOptions]);

let modal;

if (isModalVisible) {
modal = (
<EuiModal onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle>Dashboards sign-in options</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
Select one or multiple authentication options to appear on the sign-in window for
OpenSearch Dashboards.
<EuiSpacer />
<EuiInMemoryTable
tableLayout={'auto'}
columns={columns.slice(0, 1)}
items={props.dashboardOptions}
itemId={'name'}
selection={{
onSelectionChange: setNewSignInOptions,
initialSelected: actualSignInOptions,
}}
sorting={{ sort: { field: 'displayName', direction: 'asc' } }}
/>
</EuiModalBody>
<EuiModalFooter>
<EuiButton onClick={closeModal}>Cancel</EuiButton>
<EuiButton
data-testid="update"
onClick={() => {
props.handleUpdate(newSignInOptions);
closeModal();
}}
fill
disabled={disableUpdate}
>
Update
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
}
return (
<div>
<EuiButton data-testid="edit" onClick={showModal}>
Edit
</EuiButton>
{modal}
</div>
);
}
Loading
Loading