From 7c45316d2db61de32c4f7b8a6d850cd77678af15 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 29 Nov 2023 09:48:24 -0800 Subject: [PATCH 01/34] Create sign in options panel Signed-off-by: David Osorno --- .../auth-view/dashboard-signin-options.tsx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx new file mode 100644 index 000000000..a2741331b --- /dev/null +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -0,0 +1,128 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { + EuiFlexGroup, + EuiHealth, + EuiHorizontalRule, + EuiInMemoryTable, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPanel, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { get, keys, map } from 'lodash'; +import React from 'react'; +import { SignInOptionsModal } from './signin-options-modal'; + + +export enum DashboardSignInOptions { + BASIC, + SAML, + OPENID, + ANONYMOUS, +} + +export type DashboardOption = { + name: DashboardSignInOptions, + status: boolean +} + +interface SignInOptionsPanelProps { + authc: [], + signInEnabledOptions: [] +} + +export const columns: DashboardOption[] = [ + { + field: 'name', + name: 'Name', + 'data-test-subj': 'name', + mobileOptions: { + render: (opt: DashboardOption) => ( + {opt.name} + ), + header: false, + truncateText: false, + enlarge: true, + width: '100%', + }, + sortable: true, + }, + { + field: 'status', + name: 'SignIn Option Status', + dataType: 'boolean', + render: (enable: DashboardOption['status']) => { + const color = enable ? 'success' : 'danger'; + const label = enable ? 'Enable' : 'Disable'; + return {label}; + } + } +]; + +function getDashboardOptionInfo(option:DashboardSignInOptions, signInEnabledOptions: []) { + if(option in DashboardSignInOptions){ + const dashboardOption: DashboardOption = { + name: option, + status: signInEnabledOptions.indexOf(option) > -1 + } + return dashboardOption; + } +} + +export function SignInOptionsPanel(props: SignInOptionsPanelProps) { + const domains = keys(props.authc); + + const options = map(domains, function (domain: string) { + const data = get(props.authc, domain); + + return getDashboardOptionInfo(data.http_authenticator.type.toUpperCase(), props.signInEnabledOptions); + }) + .filter((option) => option != null) + //Remove duplicates + .filter((option, index, arr) => arr.findIndex(opt => opt?.name == option?.name) === index) as DashboardOption[]; + + const headerText = 'Dashboard Sign In Options'; + + return ( + + + + +

Dashboard Sign In Options

+
+ +

Configure one or multiple authentication options to appear on the sign-in windows for OpenSearch Dashboard.

+
+
+ + + + + +
+ + +
+ ); +} From 2ff33f06f2d45e9c7ea2a966205b0744e1dfc812 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 29 Nov 2023 09:49:01 -0800 Subject: [PATCH 02/34] Edit sign in options modal Signed-off-by: David Osorno --- .../panels/auth-view/signin-options-modal.tsx | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 public/apps/configuration/panels/auth-view/signin-options-modal.tsx diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx new file mode 100644 index 000000000..a3b0671fc --- /dev/null +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -0,0 +1,85 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { + EuiBasicTable, + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { DashboardOption, columns } from './dashboard-signin-options'; + +interface DashboardSignInProps { + options: DashboardOption[] +} + +export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { + + const [signInOptions, setSignInOptions] = React.useState([]); + + const [isModalVisible, setIsModalVisible] = useState(false); + const closeModal = () => setIsModalVisible(false); + const showModal = () => setIsModalVisible(true); + + const handleSave = () => { + closeModal(); + } + + let modal; + + if (isModalVisible) { + modal = ( + + + Dashboard Sign In Options + + + Enable/Disable sign-in options for OpenSearch Dashboard. + + opt.status), + }} + /> + + + + Close + + + Save + + + + ); + } + return ( +
+ Edit + {modal} +
+ ); +}; From 909e2e9b1cb0d72d86b1a96cb0369770bfc6d4e0 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 29 Nov 2023 09:49:25 -0800 Subject: [PATCH 03/34] Getting sign in options from backend Signed-off-by: David Osorno --- public/apps/configuration/panels/auth-view/auth-view.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index 80a8651bf..da916b508 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -23,10 +23,12 @@ 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'; export function AuthView(props: AppDependencies) { const [authentication, setAuthentication] = React.useState([]); const [authorization, setAuthorization] = React.useState([]); + const [dashboardSignInOptions, setDashboardSignInOptions] = React.useState([]); const [loading, setLoading] = useState(false); React.useEffect(() => { @@ -37,6 +39,7 @@ export function AuthView(props: AppDependencies) { setAuthentication(config.authc); setAuthorization(config.authz); + setDashboardSignInOptions(config.kibana.dashboardSignInOptions) } catch (e) { console.log(e); } finally { @@ -68,6 +71,9 @@ export function AuthView(props: AppDependencies) { {/* @ts-ignore */} + + + {/* @ts-ignore */} ); From 7625f9c5e02dd567b3882d5c666c5d9486ad83af Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 29 Nov 2023 18:30:05 -0800 Subject: [PATCH 04/34] Change buttons name and column name Signed-off-by: David Osorno --- .../panels/auth-view/dashboard-signin-options.tsx | 5 +++-- .../panels/auth-view/signin-options-modal.tsx | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index a2741331b..67d103ce7 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -14,6 +14,7 @@ */ import { + EuiBasicTableColumn, EuiFlexGroup, EuiHealth, EuiHorizontalRule, @@ -46,7 +47,7 @@ interface SignInOptionsPanelProps { signInEnabledOptions: [] } -export const columns: DashboardOption[] = [ +export const columns: EuiBasicTableColumn[] = [ { field: 'name', name: 'Name', @@ -64,7 +65,7 @@ export const columns: DashboardOption[] = [ }, { field: 'status', - name: 'SignIn Option Status', + name: 'Status', dataType: 'boolean', render: (enable: DashboardOption['status']) => { const color = enable ? 'success' : 'danger'; diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index a3b0671fc..e900cc706 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -38,7 +38,7 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { const closeModal = () => setIsModalVisible(false); const showModal = () => setIsModalVisible(true); - const handleSave = () => { + const handleUpdate = () => { closeModal(); } @@ -57,7 +57,7 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { tableCaption="Dashboard sign in options available" items={props.options} rowHeader="name" - columns={columns.slice(0,2)} + columns={columns.slice(0, 1)} itemId={'name'} selection={{ onSelectionChange: setSignInOptions, @@ -67,10 +67,10 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { - Close + Cancel - - Save + + Update From f1127dd7d6600418d17b2359a3961d063293a30d Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 1 Dec 2023 09:37:01 -0800 Subject: [PATCH 05/34] Move dashboard sign in option to types Signed-off-by: David Osorno --- .../auth-view/dashboard-signin-options.tsx | 22 +++++-------------- public/apps/configuration/types.ts | 12 ++++++++++ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index 67d103ce7..db7a54b83 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -28,23 +28,13 @@ import { import { get, keys, map } from 'lodash'; import React from 'react'; import { SignInOptionsModal } from './signin-options-modal'; - - -export enum DashboardSignInOptions { - BASIC, - SAML, - OPENID, - ANONYMOUS, -} - -export type DashboardOption = { - name: DashboardSignInOptions, - status: boolean -} +import { HttpSetup } from 'opensearch-dashboards/public'; +import { DashboardSignInOptions, DashboardOption } from '../../types'; interface SignInOptionsPanelProps { authc: [], - signInEnabledOptions: [] + signInEnabledOptions: DashboardSignInOptions[], + http: HttpSetup } export const columns: EuiBasicTableColumn[] = [ @@ -75,7 +65,7 @@ export const columns: EuiBasicTableColumn[] = [ } ]; -function getDashboardOptionInfo(option:DashboardSignInOptions, signInEnabledOptions: []) { +function getDashboardOptionInfo(option:DashboardSignInOptions, signInEnabledOptions: DashboardSignInOptions[]) { if(option in DashboardSignInOptions){ const dashboardOption: DashboardOption = { name: option, @@ -112,7 +102,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { - + diff --git a/public/apps/configuration/types.ts b/public/apps/configuration/types.ts index fd64e1095..37247995f 100644 --- a/public/apps/configuration/types.ts +++ b/public/apps/configuration/types.ts @@ -161,3 +161,15 @@ export interface FormRowDeps { helpLink?: string; helpText?: string; } + +export enum DashboardSignInOptions { + BASIC, + SAML, + OPENID, + ANONYMOUS, +} + +export type DashboardOption = { + name: DashboardSignInOptions, + status: boolean +} \ No newline at end of file From ecfe292e26546d476e0589115a298149b5ac2296 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 1 Dec 2023 09:37:55 -0800 Subject: [PATCH 06/34] Disable update button Signed-off-by: David Osorno --- .../panels/auth-view/auth-view.tsx | 7 ++-- .../panels/auth-view/signin-options-modal.tsx | 34 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index da916b508..d3bf30d83 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -24,11 +24,12 @@ 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([]); + const [dashboardSignInOptions, setDashboardSignInOptions] = React.useState([]); const [loading, setLoading] = useState(false); React.useEffect(() => { @@ -71,7 +72,9 @@ export function AuthView(props: AppDependencies) { {/* @ts-ignore */} - + {/* @ts-ignore */} diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index e900cc706..ab1c1bb49 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -23,20 +23,42 @@ import { EuiModalHeaderTitle, EuiSpacer } from '@elastic/eui'; -import React, { useState } from 'react'; -import { DashboardOption, columns } from './dashboard-signin-options'; +import React, { useEffect, useState } from 'react'; +import { columns } from './dashboard-signin-options'; +import { updateDashboardSignInOptions } from '../../utils/auth-view-utils'; +import { DashboardSignInOptions, DashboardOption } from '../../types'; +import { HttpSetup } from 'opensearch-dashboards/public'; interface DashboardSignInProps { - options: DashboardOption[] + options: DashboardOption[], + http: HttpSetup } export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { - const [signInOptions, setSignInOptions] = React.useState([]); + const [signInOptions, setSignInOptions] = useState([]); + const [disableUpdate, disableUpdateButton] = useState(false); + const actualSignInOptions: DashboardOption[] = props.options.filter(opt => opt.status); const [isModalVisible, setIsModalVisible] = useState(false); const closeModal = () => setIsModalVisible(false); const showModal = () => setIsModalVisible(true); + + useEffect(() => { + if(actualSignInOptions.length != signInOptions.length && signInOptions.length > 0){ + disableUpdateButton(false); + } else { + let sameOptions = true; + signInOptions.forEach(option => { + if(actualSignInOptions.includes(option) == false){ + sameOptions = false; + return; + } + }); + disableUpdateButton(sameOptions); + } + + }, [signInOptions]) const handleUpdate = () => { closeModal(); @@ -61,7 +83,7 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { itemId={'name'} selection={{ onSelectionChange: setSignInOptions, - initialSelected: props.options.filter(opt => opt.status), + initialSelected: actualSignInOptions, }} /> @@ -69,7 +91,7 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { Cancel - + Update From b95b66383f430e58604982a66300175857893850 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 1 Dec 2023 23:57:59 -0800 Subject: [PATCH 07/34] Add sign in options property to schema Signed-off-by: David Osorno --- public/apps/configuration/panels/tenancy-config/types.tsx | 3 +++ server/backend/opensearch_security_client.ts | 1 + server/multitenancy/routes.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/public/apps/configuration/panels/tenancy-config/types.tsx b/public/apps/configuration/panels/tenancy-config/types.tsx index 4ac29edcc..b5a18276e 100644 --- a/public/apps/configuration/panels/tenancy-config/types.tsx +++ b/public/apps/configuration/panels/tenancy-config/types.tsx @@ -13,8 +13,11 @@ * permissions and limitations under the License. */ +import { DashboardOption, DashboardSignInOptions } from "../../types"; + export interface TenancyConfigSettings { multitenancy_enabled?: boolean; private_tenant_enabled?: boolean; default_tenant: string; + dashboard_signin_options?: DashboardSignInOptions[] } diff --git a/server/backend/opensearch_security_client.ts b/server/backend/opensearch_security_client.ts index 79fcd7571..c8d2fa4ba 100755 --- a/server/backend/opensearch_security_client.ts +++ b/server/backend/opensearch_security_client.ts @@ -150,6 +150,7 @@ export class SecurityClient { multitenancy_enabled: tenancyConfigSettings.multitenancy_enabled, private_tenant_enabled: tenancyConfigSettings.private_tenant_enabled, default_tenant: tenancyConfigSettings.default_tenant, + dashboard_signin_options: tenancyConfigSettings.dashboard_signin_options }; try { return await this.esClient diff --git a/server/multitenancy/routes.ts b/server/multitenancy/routes.ts index d4dce3bf5..2b2f4e7a7 100644 --- a/server/multitenancy/routes.ts +++ b/server/multitenancy/routes.ts @@ -123,6 +123,7 @@ export function setupMultitenantRoutes( multitenancy_enabled: schema.boolean(), private_tenant_enabled: schema.boolean(), default_tenant: schema.string(), + dashboard_signin_options:schema.arrayOf(schema.any(), { defaultValue: [] }), }), }, }, From 4e0b1c93a4c19186fd38574847afac22e3ea7a97 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Sat, 2 Dec 2023 02:05:48 -0800 Subject: [PATCH 08/34] Update backend sign in options. Signed-off-by: David Osorno --- .../panels/auth-view/signin-options-modal.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index ab1c1bb49..b31299fbf 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -25,9 +25,11 @@ import { } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; import { columns } from './dashboard-signin-options'; -import { updateDashboardSignInOptions } from '../../utils/auth-view-utils'; -import { DashboardSignInOptions, DashboardOption } from '../../types'; +import { DashboardOption } from '../../types'; import { HttpSetup } from 'opensearch-dashboards/public'; +import { updateTenancyConfiguration } from '../../utils/tenant-utils'; +import { TenancyConfigSettings } from '../tenancy-config/types'; +import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; interface DashboardSignInProps { options: DashboardOption[], @@ -39,12 +41,29 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { const [signInOptions, setSignInOptions] = useState([]); const [disableUpdate, disableUpdateButton] = useState(false); const actualSignInOptions: DashboardOption[] = props.options.filter(opt => opt.status); + const [tenantConfig, setTenantConfig] = useState({default_tenant: ""}); const [isModalVisible, setIsModalVisible] = useState(false); const closeModal = () => setIsModalVisible(false); const showModal = () => setIsModalVisible(true); + + useEffect(() => { + const getTenantConfiguration = async () => { + const dashboardsInfo = await getDashboardsInfo(props.http); + setTenantConfig( { + multitenancy_enabled: dashboardsInfo.multitenancy_enabled, + default_tenant: dashboardsInfo.default_tenant, + private_tenant_enabled: dashboardsInfo.private_tenant_enabled, + dashboard_signin_options: [] + }); + } + + getTenantConfiguration(); + }, []) + useEffect(() => { + setTenantConfig({...tenantConfig, dashboard_signin_options: signInOptions.map(opt => opt.name)}); if(actualSignInOptions.length != signInOptions.length && signInOptions.length > 0){ disableUpdateButton(false); } else { @@ -60,7 +79,8 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { }, [signInOptions]) - const handleUpdate = () => { + const handleUpdate = async () => { + await updateTenancyConfiguration(props.http, tenantConfig); closeModal(); } From f3b10fff193570b37a7f4ae0f548720db8a20213 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Sat, 2 Dec 2023 02:12:05 -0800 Subject: [PATCH 09/34] Display Toast when updating options Signed-off-by: David Osorno --- .../panels/auth-view/dashboard-signin-options.tsx | 6 +++++- .../panels/auth-view/signin-options-modal.tsx | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index db7a54b83..42a502e3c 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -16,6 +16,7 @@ import { EuiBasicTableColumn, EuiFlexGroup, + EuiGlobalToastList, EuiHealth, EuiHorizontalRule, EuiInMemoryTable, @@ -30,6 +31,7 @@ import React from 'react'; import { SignInOptionsModal } from './signin-options-modal'; import { HttpSetup } from 'opensearch-dashboards/public'; import { DashboardSignInOptions, DashboardOption } from '../../types'; +import { useToastState } from '../../utils/toast-utils'; interface SignInOptionsPanelProps { authc: [], @@ -77,6 +79,7 @@ function getDashboardOptionInfo(option:DashboardSignInOptions, signInEnabledOpti export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const domains = keys(props.authc); + const [toasts, addToast, removeToast] = useToastState(); const options = map(domains, function (domain: string) { const data = get(props.authc, domain); @@ -102,7 +105,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { - + @@ -114,6 +117,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { itemId={'signin_options'} sorting={{ sort: { field: 'name', direction: 'asc' } }} /> + ); } diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index b31299fbf..c8ee23410 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -30,10 +30,13 @@ import { HttpSetup } from 'opensearch-dashboards/public'; import { updateTenancyConfiguration } from '../../utils/tenant-utils'; import { TenancyConfigSettings } from '../tenancy-config/types'; import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; +import { createTenancySuccessToast } from '../../utils/toast-utils'; +import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; interface DashboardSignInProps { options: DashboardOption[], - http: HttpSetup + http: HttpSetup, + addToast: ((toAdd: Toast) => void) } export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { @@ -64,6 +67,7 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { useEffect(() => { setTenantConfig({...tenantConfig, dashboard_signin_options: signInOptions.map(opt => opt.name)}); + if(actualSignInOptions.length != signInOptions.length && signInOptions.length > 0){ disableUpdateButton(false); } else { @@ -82,6 +86,13 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { const handleUpdate = async () => { await updateTenancyConfiguration(props.http, tenantConfig); closeModal(); + props.addToast( + createTenancySuccessToast( + 'savePassed', + 'Dashboard SignIn Options', + "Changes applied." + ) + ); } let modal; From a17081c608bcf22b5d1c8db0ef1c9aa848b12629 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 6 Dec 2023 06:18:26 -0800 Subject: [PATCH 10/34] Create sign in options route Signed-off-by: David Osorno --- common/index.ts | 1 + public/types.ts | 1 + server/routes/index.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/common/index.ts b/common/index.ts index c688731d6..e7a50e1ff 100644 --- a/common/index.ts +++ b/common/index.ts @@ -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; diff --git a/public/types.ts b/public/types.ts index 4acfc442f..d23366891 100644 --- a/public/types.ts +++ b/public/types.ts @@ -47,6 +47,7 @@ export interface DashboardsInfo { private_tenant_enabled?: boolean; default_tenant: string; password_validation_error_message: string; + dashboard_signin_options: [] } export interface ClientConfigType { diff --git a/server/routes/index.ts b/server/routes/index.ts index ad3dfbd58..dc8722457 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -574,6 +574,32 @@ export function defineRoutes(router: IRouter) { } ); + router.get( + { + path: `${API_PREFIX}/auth/dashboardsinfo/signinoptions`, + validate: false, + options: { + authRequired: false, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const client = context.security_plugin.esClient.asScoped(request); + let esResp; + try { + esResp = await client.callAsInternalUser('opensearch_security.dashboardsinfo'); + return response.ok({ + body: esResp.dashboard_signin_options, + }); + } catch (error) { + return errorResponse(response, error); + } + } + ); + /** * Gets audit log configuration。 * From 8c61b57efcf8076c291e8f7deffdab20a42ae161 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 6 Dec 2023 06:20:03 -0800 Subject: [PATCH 11/34] Display sign in options Signed-off-by: David Osorno --- public/apps/login/login-page.tsx | 77 +++++++++++++------------- public/utils/dashboards-info-utils.tsx | 9 ++- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 22421e3a7..4d6b5a1c0 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiText, EuiFieldText, @@ -35,6 +35,9 @@ import { OPENID_AUTH_LOGIN, SAML_AUTH_LOGIN_WITH_FRAGMENT, } from '../../../common'; +import { getDashboardsInfo, getDashboardsSignInOptions } from '../../utils/dashboards-info-utils'; +import { DashboardSignInOptions } from '../configuration/types'; +import { string } from 'joi'; interface LoginPageDeps { http: CoreStart['http']; @@ -81,6 +84,20 @@ export function LoginPage(props: LoginPageDeps) { const [loginError, setloginError] = useState(''); const [usernameValidationFailed, setUsernameValidationFailed] = useState(false); const [passwordValidationFailed, setPasswordValidationFailed] = useState(false); + const [signInOptions, setSignInOptions] = useState([]); + + useEffect(() => { + const getSignInOptions = async () => { + try { + let dashboardSignInOptions = (await getDashboardsSignInOptions(props.http)); + setSignInOptions(dashboardSignInOptions); + } catch (e) { + console.error(`Unable to get sign in options ${e}`); + } + }; + + getSignInOptions(); + }, []); let errorLabel: any = null; if (loginFailed) { @@ -146,28 +163,13 @@ export function LoginPage(props: LoginPageDeps) { ); }; - const formOptions = (options: string | string[]) => { + const formOptions = () => { let formBody = []; const formBodyOp = []; - let authOpts = []; - - if (typeof options === 'string') { - if (options === '') { - authOpts.push(AuthType.BASIC); - } else { - authOpts.push(options.toLowerCase()); - } - } else { - if (options && options.length === 1 && options[0] === '') { - authOpts.push(AuthType.BASIC); - } else { - authOpts = [...options]; - } - } - - for (let i = 0; i < authOpts.length; i++) { - switch (authOpts[i].toLowerCase()) { - case AuthType.BASIC: { + + for (let i = 0; i < signInOptions.length; i++) { + switch (DashboardSignInOptions[signInOptions[i]]) { + case DashboardSignInOptions.BASIC: { formBody.push( ); - - if (authOpts.length > 1) { - if (props.config.auth.anonymous_auth_enabled) { - const anonymousConfig = props.config.ui[AuthType.ANONYMOUS].login; - formBody.push( - renderLoginButton(AuthType.ANONYMOUS, ANONYMOUS_AUTH_LOGIN, anonymousConfig) - ); - } - - formBody.push(); - formBody.push(); - formBody.push(); - } break; } - case AuthType.OPEN_ID: { + case DashboardSignInOptions.OPENID: { const oidcConfig = props.config.ui[AuthType.OPEN_ID].login; formBodyOp.push(renderLoginButton(AuthType.OPEN_ID, OPENID_AUTH_LOGIN, oidcConfig)); break; } - case AuthType.SAML: { + case DashboardSignInOptions.SAML: { const samlConfig = props.config.ui[AuthType.SAML].login; const nextUrl = extractNextUrlFromWindowLocation(); const samlAuthLoginUrl = SAML_AUTH_LOGIN_WITH_FRAGMENT + nextUrl; formBodyOp.push(renderLoginButton(AuthType.SAML, samlAuthLoginUrl, samlConfig)); break; } + case DashboardSignInOptions.ANONYMOUS: { + const anonymousConfig = props.config.ui[AuthType.ANONYMOUS].login; + formBody.push( + renderLoginButton(AuthType.ANONYMOUS, ANONYMOUS_AUTH_LOGIN, anonymousConfig) + ); + break; + } default: { setloginFailed(true); setloginError( - `Authentication Type: ${authOpts[i]} is not supported for multiple authentication.` + `Authentication Type: ${signInOptions[i]} is not supported for multiple authentication.` ); break; } } } + if (signInOptions.length > 1) { + formBody.push(); + formBody.push(); + formBody.push(); + } formBody = formBody.concat(formBodyOp); return formBody; @@ -273,7 +274,7 @@ export function LoginPage(props: LoginPageDeps) { - {formOptions(props.config.auth.type)} + {formOptions()} {errorLabel} diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index 55804eb04..b4516e7fe 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -14,11 +14,10 @@ */ import { HttpStart } from 'opensearch-dashboards/public'; -import { API_ENDPOINT_DASHBOARDSINFO } from '../../common'; +import { API_ENDPOINT_DASHBOARDSINFO, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS } from '../../common'; import { httpGet, httpGetWithIgnores } from '../apps/configuration/utils/request-utils'; import { DashboardsInfo } from '../types'; -import { AccountInfo } from '../apps/account/types'; -import { API_ENDPOINT_ACCOUNT_INFO } from '../apps/account/constants'; +import { DashboardSignInOptions } from '../apps/configuration/types'; export async function getDashboardsInfo(http: HttpStart) { return await httpGet(http, API_ENDPOINT_DASHBOARDSINFO); @@ -27,3 +26,7 @@ export async function getDashboardsInfo(http: HttpStart) { export async function getDashboardsInfoSafe(http: HttpStart): Promise { return httpGetWithIgnores(http, API_ENDPOINT_DASHBOARDSINFO, [401]); } + +export async function getDashboardsSignInOptions(http: HttpStart) { + return await httpGet(http, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS); +} \ No newline at end of file From 452415db0060fe7a53bb48a54a985e5517ee8072 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 6 Dec 2023 09:54:28 -0800 Subject: [PATCH 12/34] Apply prettier Signed-off-by: David Osorno --- .../panels/auth-view/auth-view.tsx | 10 ++-- .../auth-view/dashboard-signin-options.tsx | 49 +++++++++++-------- public/apps/login/login-page.tsx | 10 ++-- server/routes/index.ts | 2 +- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index d3bf30d83..4ea27128e 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -40,7 +40,7 @@ export function AuthView(props: AppDependencies) { setAuthentication(config.authc); setAuthorization(config.authz); - setDashboardSignInOptions(config.kibana.dashboardSignInOptions) + setDashboardSignInOptions(config.kibana.dashboardSignInOptions); } catch (e) { console.log(e); } finally { @@ -72,9 +72,11 @@ export function AuthView(props: AppDependencies) { {/* @ts-ignore */} - + {/* @ts-ignore */} diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index 42a502e3c..ec124ae38 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -34,9 +34,9 @@ import { DashboardSignInOptions, DashboardOption } from '../../types'; import { useToastState } from '../../utils/toast-utils'; interface SignInOptionsPanelProps { - authc: [], - signInEnabledOptions: DashboardSignInOptions[], - http: HttpSetup + authc: []; + signInEnabledOptions: DashboardSignInOptions[]; + http: HttpSetup; } export const columns: EuiBasicTableColumn[] = [ @@ -45,9 +45,7 @@ export const columns: EuiBasicTableColumn[] = [ name: 'Name', 'data-test-subj': 'name', mobileOptions: { - render: (opt: DashboardOption) => ( - {opt.name} - ), + render: (opt: DashboardOption) => {opt.name}, header: false, truncateText: false, enlarge: true, @@ -63,16 +61,19 @@ export const columns: EuiBasicTableColumn[] = [ const color = enable ? 'success' : 'danger'; const label = enable ? 'Enable' : 'Disable'; return {label}; - } - } + }, + }, ]; -function getDashboardOptionInfo(option:DashboardSignInOptions, signInEnabledOptions: DashboardSignInOptions[]) { - if(option in DashboardSignInOptions){ +function getDashboardOptionInfo( + option: DashboardSignInOptions, + signInEnabledOptions: DashboardSignInOptions[] +) { + if (option in DashboardSignInOptions) { const dashboardOption: DashboardOption = { name: option, - status: signInEnabledOptions.indexOf(option) > -1 - } + status: signInEnabledOptions.indexOf(option) > -1, + }; return dashboardOption; } } @@ -80,17 +81,22 @@ function getDashboardOptionInfo(option:DashboardSignInOptions, signInEnabledOpti export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const domains = keys(props.authc); const [toasts, addToast, removeToast] = useToastState(); - + const options = map(domains, function (domain: string) { const data = get(props.authc, domain); - - return getDashboardOptionInfo(data.http_authenticator.type.toUpperCase(), props.signInEnabledOptions); + + return getDashboardOptionInfo( + data.http_authenticator.type.toUpperCase(), + props.signInEnabledOptions + ); }) .filter((option) => option != null) //Remove duplicates - .filter((option, index, arr) => arr.findIndex(opt => opt?.name == option?.name) === index) as DashboardOption[]; - - const headerText = 'Dashboard Sign In Options'; + .filter( + (option, index, arr) => arr.findIndex((opt) => opt?.name == option?.name) === index + ) as DashboardOption[]; + + const headerText = 'Dashboard Sign In Options'; return ( @@ -100,7 +106,10 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) {

Dashboard Sign In Options

-

Configure one or multiple authentication options to appear on the sign-in windows for OpenSearch Dashboard.

+

+ Configure one or multiple authentication options to appear on the sign-in windows for + OpenSearch Dashboard. +

@@ -117,7 +126,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { itemId={'signin_options'} sorting={{ sort: { field: 'name', direction: 'asc' } }} /> - +
); } diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 4d6b5a1c0..15001df3d 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -89,7 +89,7 @@ export function LoginPage(props: LoginPageDeps) { useEffect(() => { const getSignInOptions = async () => { try { - let dashboardSignInOptions = (await getDashboardsSignInOptions(props.http)); + let dashboardSignInOptions = await getDashboardsSignInOptions(props.http); setSignInOptions(dashboardSignInOptions); } catch (e) { console.error(`Unable to get sign in options ${e}`); @@ -166,7 +166,7 @@ export function LoginPage(props: LoginPageDeps) { const formOptions = () => { let formBody = []; const formBodyOp = []; - + for (let i = 0; i < signInOptions.length; i++) { switch (DashboardSignInOptions[signInOptions[i]]) { case DashboardSignInOptions.BASIC: { @@ -229,9 +229,9 @@ export function LoginPage(props: LoginPageDeps) { } case DashboardSignInOptions.ANONYMOUS: { const anonymousConfig = props.config.ui[AuthType.ANONYMOUS].login; - formBody.push( - renderLoginButton(AuthType.ANONYMOUS, ANONYMOUS_AUTH_LOGIN, anonymousConfig) - ); + formBody.push( + renderLoginButton(AuthType.ANONYMOUS, ANONYMOUS_AUTH_LOGIN, anonymousConfig) + ); break; } default: { diff --git a/server/routes/index.ts b/server/routes/index.ts index dc8722457..50583ec23 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -590,7 +590,7 @@ export function defineRoutes(router: IRouter) { const client = context.security_plugin.esClient.asScoped(request); let esResp; try { - esResp = await client.callAsInternalUser('opensearch_security.dashboardsinfo'); + esResp = await client.callAsInternalUser('opensearch_security.dashboardsinfo'); return response.ok({ body: esResp.dashboard_signin_options, }); From d707cd6f0ad5201867a7a80c2b46d932f331e415 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 6 Dec 2023 10:36:17 -0800 Subject: [PATCH 13/34] Update backend sign in options Signed-off-by: David Osorno --- .../panels/tenancy-config/types.tsx | 3 -- public/utils/dashboards-info-utils.tsx | 13 ++++++-- server/backend/opensearch_security_client.ts | 20 ++++++++++++- server/multitenancy/routes.ts | 30 ++++++++++++++++++- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/public/apps/configuration/panels/tenancy-config/types.tsx b/public/apps/configuration/panels/tenancy-config/types.tsx index b5a18276e..4ac29edcc 100644 --- a/public/apps/configuration/panels/tenancy-config/types.tsx +++ b/public/apps/configuration/panels/tenancy-config/types.tsx @@ -13,11 +13,8 @@ * permissions and limitations under the License. */ -import { DashboardOption, DashboardSignInOptions } from "../../types"; - export interface TenancyConfigSettings { multitenancy_enabled?: boolean; private_tenant_enabled?: boolean; default_tenant: string; - dashboard_signin_options?: DashboardSignInOptions[] } diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index b4516e7fe..8deb6aed4 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -15,7 +15,7 @@ import { HttpStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_DASHBOARDSINFO, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS } from '../../common'; -import { httpGet, httpGetWithIgnores } from '../apps/configuration/utils/request-utils'; +import { httpGet, httpGetWithIgnores, httpPut } from '../apps/configuration/utils/request-utils'; import { DashboardsInfo } from '../types'; import { DashboardSignInOptions } from '../apps/configuration/types'; @@ -29,4 +29,13 @@ export async function getDashboardsInfoSafe(http: HttpStart): Promise(http, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS); -} \ No newline at end of file +} + +export async function updateDashboardSignInOptions( + http: HttpStart, + signInOptions: DashboardSignInOptions[] +) { + await httpPut(http, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS, { + dashboard_signin_options: signInOptions, + }); +} diff --git a/server/backend/opensearch_security_client.ts b/server/backend/opensearch_security_client.ts index c8d2fa4ba..06fd45542 100755 --- a/server/backend/opensearch_security_client.ts +++ b/server/backend/opensearch_security_client.ts @@ -17,6 +17,7 @@ import { ILegacyClusterClient, OpenSearchDashboardsRequest } from '../../../../s import { User } from '../auth/user'; import { getAuthInfo } from '../../public/utils/auth-info-utils'; import { TenancyConfigSettings } from '../../public/apps/configuration/panels/tenancy-config/types'; +import { DashboardSignInOptions } from '../../public/apps/configuration/types'; export class SecurityClient { constructor(private readonly esClient: ILegacyClusterClient) {} @@ -131,6 +132,24 @@ export class SecurityClient { } } + public async putDashboardSignInOptions( + request: OpenSearchDashboardsRequest, + signInOptions: DashboardSignInOptions[] + ) { + const body = { + dashboard_signin_options: signInOptions, + }; + try { + return await this.esClient + .asScoped(request) + .callAsCurrentUser('opensearch_security.tenancy_configs', { + body, + }); + } catch (error: any) { + throw new Error(error.message); + } + } + // Multi-tenancy APIs public async getMultitenancyInfo(request: OpenSearchDashboardsRequest) { try { @@ -150,7 +169,6 @@ export class SecurityClient { multitenancy_enabled: tenancyConfigSettings.multitenancy_enabled, private_tenant_enabled: tenancyConfigSettings.private_tenant_enabled, default_tenant: tenancyConfigSettings.default_tenant, - dashboard_signin_options: tenancyConfigSettings.dashboard_signin_options }; try { return await this.esClient diff --git a/server/multitenancy/routes.ts b/server/multitenancy/routes.ts index 2b2f4e7a7..0b1eccd65 100644 --- a/server/multitenancy/routes.ts +++ b/server/multitenancy/routes.ts @@ -123,7 +123,6 @@ export function setupMultitenantRoutes( multitenancy_enabled: schema.boolean(), private_tenant_enabled: schema.boolean(), default_tenant: schema.string(), - dashboard_signin_options:schema.arrayOf(schema.any(), { defaultValue: [] }), }), }, }, @@ -147,6 +146,35 @@ export function setupMultitenantRoutes( } ); + router.put( + { + path: '/api/v1/auth/dashboardsinfo/signinoptions', + validate: { + body: schema.object({ + dashboard_signin_options: schema.arrayOf(schema.any(), { defaultValue: [] }), + }), + }, + }, + async (context, request, response) => { + try { + const esResponse = await securityClient.putDashboardSignInOptions( + request, + request.body.dashboard_signin_options + ); + return response.ok({ + body: esResponse, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); + router.post( { // FIXME: Seems this is not being used, confirm and delete if not used anymore From efbd2ddfd4519cb9c7387f345ec2cd71ebb19f2c Mon Sep 17 00:00:00 2001 From: David Osorno Date: Thu, 7 Dec 2023 07:47:39 -0800 Subject: [PATCH 14/34] Track sign in options changes to update UI. Signed-off-by: David Osorno --- .../panels/auth-view/auth-view.tsx | 4 +- .../auth-view/dashboard-signin-options.tsx | 71 ++++++----- .../panels/auth-view/signin-options-modal.tsx | 111 +++++++++--------- public/apps/configuration/types.ts | 6 +- public/apps/login/login-page.tsx | 3 +- public/utils/dashboards-info-utils.tsx | 2 +- 6 files changed, 102 insertions(+), 95 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index 4ea27128e..15a00d96b 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -29,7 +29,9 @@ import { DashboardSignInOptions } from '../../types'; export function AuthView(props: AppDependencies) { const [authentication, setAuthentication] = React.useState([]); const [authorization, setAuthorization] = React.useState([]); - const [dashboardSignInOptions, setDashboardSignInOptions] = React.useState([]); + const [dashboardSignInOptions, setDashboardSignInOptions] = React.useState< + DashboardSignInOptions[] + >([]); const [loading, setLoading] = useState(false); React.useEffect(() => { diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index ec124ae38..9f558bd97 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -26,12 +26,12 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { get, keys, map } from 'lodash'; -import React from 'react'; -import { SignInOptionsModal } from './signin-options-modal'; +import { get, keys } from 'lodash'; import { HttpSetup } from 'opensearch-dashboards/public'; -import { DashboardSignInOptions, DashboardOption } from '../../types'; +import React, { useEffect, useState } from 'react'; +import { DashboardOption, DashboardSignInOptions } from '../../types'; import { useToastState } from '../../utils/toast-utils'; +import { SignInOptionsModal } from './signin-options-modal'; interface SignInOptionsPanelProps { authc: []; @@ -65,38 +65,40 @@ export const columns: EuiBasicTableColumn[] = [ }, ]; -function getDashboardOptionInfo( - option: DashboardSignInOptions, - signInEnabledOptions: DashboardSignInOptions[] -) { - if (option in DashboardSignInOptions) { - const dashboardOption: DashboardOption = { - name: option, - status: signInEnabledOptions.indexOf(option) > -1, - }; - return dashboardOption; - } -} - export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const domains = keys(props.authc); const [toasts, addToast, removeToast] = useToastState(); + const [dashboardOptions, setDashboardOptions] = useState([]); + + useEffect(() => { + const getDasboardOptions = () => { + const options = domains + .map((domain) => { + const data = get(props.authc, domain); - const options = map(domains, function (domain: string) { - const data = get(props.authc, domain); + let option = data.http_authenticator.type.toUpperCase(); + if (option in DashboardSignInOptions) { + const dashboardOption: DashboardOption = { + name: option, + status: props.signInEnabledOptions.indexOf(option) > -1, + }; - return getDashboardOptionInfo( - data.http_authenticator.type.toUpperCase(), - props.signInEnabledOptions - ); - }) - .filter((option) => option != null) - //Remove duplicates - .filter( - (option, index, arr) => arr.findIndex((opt) => opt?.name == option?.name) === index - ) as DashboardOption[]; + return dashboardOption; + } + }) + .filter((option) => option != null) + //Remove duplicates + .filter( + (option, index, arr) => arr.findIndex((opt) => opt?.name == option?.name) === index + ) as DashboardOption[]; + + setDashboardOptions(options); + }; - const headerText = 'Dashboard Sign In Options'; + if (props.signInEnabledOptions.length > 0 && dashboardOptions.length == 0) { + getDasboardOptions(); + } + }, [props.signInEnabledOptions, dashboardOptions]); return ( @@ -114,7 +116,12 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { - + @@ -122,7 +129,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index c8ee23410..b57bb2d71 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -21,82 +21,83 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiSpacer + EuiSpacer, } from '@elastic/eui'; +import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { HttpSetup } from 'opensearch-dashboards/public'; import React, { useEffect, useState } from 'react'; -import { columns } from './dashboard-signin-options'; +import { updateDashboardSignInOptions } from '../../../../utils/dashboards-info-utils'; import { DashboardOption } from '../../types'; -import { HttpSetup } from 'opensearch-dashboards/public'; -import { updateTenancyConfiguration } from '../../utils/tenant-utils'; -import { TenancyConfigSettings } from '../tenancy-config/types'; -import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; -import { createTenancySuccessToast } from '../../utils/toast-utils'; -import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { createErrorToast, createSuccessToast } from '../../utils/toast-utils'; +import { columns } from './dashboard-signin-options'; +import { Dispatch } from 'react'; +import { SetStateAction } from 'react'; interface DashboardSignInProps { - options: DashboardOption[], - http: HttpSetup, - addToast: ((toAdd: Toast) => void) + dashboardOptions: DashboardOption[]; + setDashboardOptions: Dispatch>; + http: HttpSetup; + addToast: (toAdd: Toast) => void; } export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { - - const [signInOptions, setSignInOptions] = useState([]); + const [newSignInOptions, setNewSignInOptions] = useState([]); const [disableUpdate, disableUpdateButton] = useState(false); - const actualSignInOptions: DashboardOption[] = props.options.filter(opt => opt.status); - const [tenantConfig, setTenantConfig] = useState({default_tenant: ""}); - + const actualSignInOptions: DashboardOption[] = props.dashboardOptions.filter((opt) => opt.status); + const [isModalVisible, setIsModalVisible] = useState(false); const closeModal = () => setIsModalVisible(false); const showModal = () => setIsModalVisible(true); useEffect(() => { - const getTenantConfiguration = async () => { - const dashboardsInfo = await getDashboardsInfo(props.http); - setTenantConfig( { - multitenancy_enabled: dashboardsInfo.multitenancy_enabled, - default_tenant: dashboardsInfo.default_tenant, - private_tenant_enabled: dashboardsInfo.private_tenant_enabled, - dashboard_signin_options: [] - }); - } - - getTenantConfiguration(); - }, []) - - - useEffect(() => { - setTenantConfig({...tenantConfig, dashboard_signin_options: signInOptions.map(opt => opt.name)}); - - if(actualSignInOptions.length != signInOptions.length && signInOptions.length > 0){ + if (actualSignInOptions.length != newSignInOptions.length && newSignInOptions.length > 0) { disableUpdateButton(false); } else { let sameOptions = true; - signInOptions.forEach(option => { - if(actualSignInOptions.includes(option) == false){ + newSignInOptions.forEach((option) => { + if (actualSignInOptions.includes(option) == false) { sameOptions = false; return; } }); disableUpdateButton(sameOptions); } - - }, [signInOptions]) + }, [newSignInOptions]); const handleUpdate = async () => { - await updateTenancyConfiguration(props.http, tenantConfig); - closeModal(); - props.addToast( - createTenancySuccessToast( - 'savePassed', - 'Dashboard SignIn Options', - "Changes applied." - ) - ); - } + await updateDashboardSignInOptions( + props.http, + newSignInOptions.map((opt) => opt.name) + ) + .then(() => { + changeDashboardSignInOptionsStatus(); + props.addToast( + createSuccessToast('updatePassed', 'Dashboard SignIn Options', 'Changes applied.') + ); + }) + .catch((e) => { + console.log('The sign in options could not be updated'); + console.log(e); + props.addToast( + createErrorToast('updatedError', 'Dashboard SignIn Options', 'Error updating values.') + ); + }) + .finally(() => { + closeModal(); + }); + }; let modal; + const changeDashboardSignInOptionsStatus = () => { + props.setDashboardOptions((prevOptions) => + prevOptions.map((option) => { + option.status = newSignInOptions.includes(option); + return option; + }) + ); + }; + if (isModalVisible) { modal = ( @@ -108,20 +109,18 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { - - Cancel - + Cancel Update @@ -135,4 +134,4 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { {modal} ); -}; +} diff --git a/public/apps/configuration/types.ts b/public/apps/configuration/types.ts index 37247995f..a275ad9a2 100644 --- a/public/apps/configuration/types.ts +++ b/public/apps/configuration/types.ts @@ -170,6 +170,6 @@ export enum DashboardSignInOptions { } export type DashboardOption = { - name: DashboardSignInOptions, - status: boolean -} \ No newline at end of file + name: DashboardSignInOptions; + status: boolean; +}; diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 15001df3d..c8b62e64c 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -35,9 +35,8 @@ import { OPENID_AUTH_LOGIN, SAML_AUTH_LOGIN_WITH_FRAGMENT, } from '../../../common'; -import { getDashboardsInfo, getDashboardsSignInOptions } from '../../utils/dashboards-info-utils'; +import { getDashboardsSignInOptions } from '../../utils/dashboards-info-utils'; import { DashboardSignInOptions } from '../configuration/types'; -import { string } from 'joi'; interface LoginPageDeps { http: CoreStart['http']; diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index 8deb6aed4..b12fd6158 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -35,7 +35,7 @@ export async function updateDashboardSignInOptions( http: HttpStart, signInOptions: DashboardSignInOptions[] ) { - await httpPut(http, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS, { + return await httpPut(http, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS, { dashboard_signin_options: signInOptions, }); } From 0fa601266f7ad0d7474f877245a1654a38277b8a Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 8 Dec 2023 06:45:45 -0800 Subject: [PATCH 15/34] Meet lint requirements Signed-off-by: David Osorno --- .../panels/auth-view/dashboard-signin-options.tsx | 11 +++++------ .../panels/auth-view/signin-options-modal.tsx | 12 +++++------- public/apps/configuration/types.ts | 4 ++-- public/apps/login/login-page.tsx | 4 ++-- public/types.ts | 2 +- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index 9f558bd97..ffe6348d3 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -39,7 +39,7 @@ interface SignInOptionsPanelProps { http: HttpSetup; } -export const columns: EuiBasicTableColumn[] = [ +export const columns: Array> = [ { field: 'name', name: 'Name', @@ -76,7 +76,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { .map((domain) => { const data = get(props.authc, domain); - let option = data.http_authenticator.type.toUpperCase(); + const option = data.http_authenticator.type.toUpperCase(); if (option in DashboardSignInOptions) { const dashboardOption: DashboardOption = { name: option, @@ -87,18 +87,17 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { } }) .filter((option) => option != null) - //Remove duplicates .filter( - (option, index, arr) => arr.findIndex((opt) => opt?.name == option?.name) === index + (option, index, arr) => arr.findIndex((opt) => opt?.name === option?.name) === index ) as DashboardOption[]; setDashboardOptions(options); }; - if (props.signInEnabledOptions.length > 0 && dashboardOptions.length == 0) { + if (props.signInEnabledOptions.length > 0 && dashboardOptions.length === 0) { getDasboardOptions(); } - }, [props.signInEnabledOptions, dashboardOptions]); + }, [props.signInEnabledOptions, props.authc, dashboardOptions, domains]); return ( diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index b57bb2d71..a3d9791f0 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -25,13 +26,10 @@ import { } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; import { HttpSetup } from 'opensearch-dashboards/public'; -import React, { useEffect, useState } from 'react'; -import { updateDashboardSignInOptions } from '../../../../utils/dashboards-info-utils'; import { DashboardOption } from '../../types'; import { createErrorToast, createSuccessToast } from '../../utils/toast-utils'; import { columns } from './dashboard-signin-options'; -import { Dispatch } from 'react'; -import { SetStateAction } from 'react'; +import { updateDashboardSignInOptions } from '../../../../utils/dashboards-info-utils'; interface DashboardSignInProps { dashboardOptions: DashboardOption[]; @@ -50,19 +48,19 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { const showModal = () => setIsModalVisible(true); useEffect(() => { - if (actualSignInOptions.length != newSignInOptions.length && newSignInOptions.length > 0) { + if (actualSignInOptions.length !== newSignInOptions.length && newSignInOptions.length > 0) { disableUpdateButton(false); } else { let sameOptions = true; newSignInOptions.forEach((option) => { - if (actualSignInOptions.includes(option) == false) { + if (actualSignInOptions.includes(option) === false) { sameOptions = false; return; } }); disableUpdateButton(sameOptions); } - }, [newSignInOptions]); + }, [newSignInOptions, actualSignInOptions]); const handleUpdate = async () => { await updateDashboardSignInOptions( diff --git a/public/apps/configuration/types.ts b/public/apps/configuration/types.ts index a275ad9a2..55b485171 100644 --- a/public/apps/configuration/types.ts +++ b/public/apps/configuration/types.ts @@ -169,7 +169,7 @@ export enum DashboardSignInOptions { ANONYMOUS, } -export type DashboardOption = { +export interface DashboardOption { name: DashboardSignInOptions; status: boolean; -}; +} diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index c8b62e64c..fdf459eaa 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -88,7 +88,7 @@ export function LoginPage(props: LoginPageDeps) { useEffect(() => { const getSignInOptions = async () => { try { - let dashboardSignInOptions = await getDashboardsSignInOptions(props.http); + const dashboardSignInOptions = await getDashboardsSignInOptions(props.http); setSignInOptions(dashboardSignInOptions); } catch (e) { console.error(`Unable to get sign in options ${e}`); @@ -96,7 +96,7 @@ export function LoginPage(props: LoginPageDeps) { }; getSignInOptions(); - }, []); + }, [props.http]); let errorLabel: any = null; if (loginFailed) { diff --git a/public/types.ts b/public/types.ts index d23366891..9c884d823 100644 --- a/public/types.ts +++ b/public/types.ts @@ -47,7 +47,7 @@ export interface DashboardsInfo { private_tenant_enabled?: boolean; default_tenant: string; password_validation_error_message: string; - dashboard_signin_options: [] + dashboard_signin_options: []; } export interface ClientConfigType { From c402993abf20121be86935149f1dbbd3269ec6df Mon Sep 17 00:00:00 2001 From: David Osorno Date: Tue, 12 Dec 2023 09:14:00 -0800 Subject: [PATCH 16/34] Add sign-in options update route Signed-off-by: David Osorno --- server/backend/opensearch_security_client.ts | 18 ---------- server/multitenancy/routes.ts | 30 +--------------- server/routes/index.ts | 37 ++++++++++++++++++-- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/server/backend/opensearch_security_client.ts b/server/backend/opensearch_security_client.ts index 06fd45542..5ea4cb468 100755 --- a/server/backend/opensearch_security_client.ts +++ b/server/backend/opensearch_security_client.ts @@ -132,24 +132,6 @@ export class SecurityClient { } } - public async putDashboardSignInOptions( - request: OpenSearchDashboardsRequest, - signInOptions: DashboardSignInOptions[] - ) { - const body = { - dashboard_signin_options: signInOptions, - }; - try { - return await this.esClient - .asScoped(request) - .callAsCurrentUser('opensearch_security.tenancy_configs', { - body, - }); - } catch (error: any) { - throw new Error(error.message); - } - } - // Multi-tenancy APIs public async getMultitenancyInfo(request: OpenSearchDashboardsRequest) { try { diff --git a/server/multitenancy/routes.ts b/server/multitenancy/routes.ts index 0b1eccd65..dcf21947d 100644 --- a/server/multitenancy/routes.ts +++ b/server/multitenancy/routes.ts @@ -146,35 +146,6 @@ export function setupMultitenantRoutes( } ); - router.put( - { - path: '/api/v1/auth/dashboardsinfo/signinoptions', - validate: { - body: schema.object({ - dashboard_signin_options: schema.arrayOf(schema.any(), { defaultValue: [] }), - }), - }, - }, - async (context, request, response) => { - try { - const esResponse = await securityClient.putDashboardSignInOptions( - request, - request.body.dashboard_signin_options - ); - return response.ok({ - body: esResponse, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } - } - ); - router.post( { // FIXME: Seems this is not being used, confirm and delete if not used anymore @@ -193,3 +164,4 @@ export function setupMultitenantRoutes( } ); } + diff --git a/server/routes/index.ts b/server/routes/index.ts index 50583ec23..2d953255f 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -20,8 +20,8 @@ import { IOpenSearchDashboardsResponse, OpenSearchDashboardsResponseFactory, } from 'opensearch-dashboards/server'; -import { API_PREFIX, CONFIGURATION_API_PREFIX, isValidResourceName } from '../../common'; -import { ResourceType } from '../../public/apps/configuration/types'; +import { API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS, API_PREFIX, CONFIGURATION_API_PREFIX, isValidResourceName } from '../../common'; +import { DashboardSignInOptions, ResourceType } from '../../public/apps/configuration/types'; // TODO: consider to extract entity CRUD operations and put it into a client class export function defineRoutes(router: IRouter) { @@ -576,7 +576,7 @@ export function defineRoutes(router: IRouter) { router.get( { - path: `${API_PREFIX}/auth/dashboardsinfo/signinoptions`, + path: `${API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS}`, validate: false, options: { authRequired: false, @@ -600,6 +600,37 @@ export function defineRoutes(router: IRouter) { } ); + router.put( + { + path: `${API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS}`, + validate: { + body: schema.object({ + dashboard_signin_options: schema.arrayOf(schema.any(), { defaultValue: [DashboardSignInOptions.BASIC] }), + }), + }, + }, + async ( + context, + request, + response + ): Promise> => { + const client = context.security_plugin.esClient.asScoped(request); + let esResp; + try { + esResp = await client.callAsCurrentUser('opensearch_security.tenancy_configs', { + body: request.body, + }); + return response.ok({ + body: { + message: esResp.message, + }, + }); + } catch (error) { + return errorResponse(response, error); + } + } + ); + /** * Gets audit log configuration。 * From 7a6ba286e022d55a96c5b066516fb89adf3b90f5 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Tue, 12 Dec 2023 10:47:47 -0800 Subject: [PATCH 17/34] Update authOpts with backend sign in options. Signed-off-by: David Osorno --- public/apps/login/login-page.tsx | 48 ++++++++++++++++---- server/backend/opensearch_security_client.ts | 2 - 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 20ba4570a..8cbe2149c 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -162,13 +162,42 @@ export function LoginPage(props: LoginPageDeps) { ); }; - const formOptions = () => { + const formOptions = (options: string | string[]) => { let formBody = []; const formBodyOp = []; + let authOpts = []; for (let i = 0; i < signInOptions.length; i++) { - switch (DashboardSignInOptions[signInOptions[i]]) { - case DashboardSignInOptions.BASIC: { + // Dashboard sign-in options are gotten from HTTP type property where the value is 'openid' and it needs to match with AuthType open_id; + if (DashboardSignInOptions[signInOptions[i]] === DashboardSignInOptions.OPENID) { + authOpts.push(AuthType.OPEN_ID); + } else { + let authType = AuthType[signInOptions[i]]; + if (authType) { + authOpts.push(authType); + } + } + } + + if (authOpts.length == 0) { + if (typeof options === 'string') { + if (options === '') { + authOpts.push(AuthType.BASIC); + } else { + authOpts.push(options.toLowerCase()); + } + } else { + if (options && options.length === 1 && options[0] === '') { + authOpts.push(AuthType.BASIC); + } else { + authOpts = [...options]; + } + } + } + + for (let i = 0; i < authOpts.length; i++) { + switch (authOpts[i].toLowerCase()) { + case AuthType.BASIC: { formBody.push( 1) { + + if (authOpts.length > 1) { formBody.push(); formBody.push(); formBody.push(); @@ -275,7 +305,7 @@ export function LoginPage(props: LoginPageDeps) { - {formOptions()} + {formOptions(props.config.auth.type)} {errorLabel} diff --git a/server/backend/opensearch_security_client.ts b/server/backend/opensearch_security_client.ts index 5ea4cb468..61a2912a4 100755 --- a/server/backend/opensearch_security_client.ts +++ b/server/backend/opensearch_security_client.ts @@ -15,9 +15,7 @@ import { ILegacyClusterClient, OpenSearchDashboardsRequest } from '../../../../src/core/server'; import { User } from '../auth/user'; -import { getAuthInfo } from '../../public/utils/auth-info-utils'; import { TenancyConfigSettings } from '../../public/apps/configuration/panels/tenancy-config/types'; -import { DashboardSignInOptions } from '../../public/apps/configuration/types'; export class SecurityClient { constructor(private readonly esClient: ILegacyClusterClient) {} From 9e96c9ec76b81a4f49c141d3658dcbbd5ba39adc Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 13 Dec 2023 10:53:45 -0800 Subject: [PATCH 18/34] Combine auth types from backend and yml file Signed-off-by: David Osorno --- public/apps/login/login-page.tsx | 15 ++++++++++----- server/plugin.ts | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 8cbe2149c..57fe99953 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -162,11 +162,8 @@ export function LoginPage(props: LoginPageDeps) { ); }; - const formOptions = (options: string | string[]) => { - let formBody = []; - const formBodyOp = []; - let authOpts = []; - + const mapSignInOptions = (signInOptions: DashboardSignInOptions[]) => { + const authOpts = []; for (let i = 0; i < signInOptions.length; i++) { // Dashboard sign-in options are gotten from HTTP type property where the value is 'openid' and it needs to match with AuthType open_id; if (DashboardSignInOptions[signInOptions[i]] === DashboardSignInOptions.OPENID) { @@ -178,6 +175,14 @@ export function LoginPage(props: LoginPageDeps) { } } } + return authOpts; + } + + const formOptions = (options: string | string[]) => { + let formBody = []; + const formBodyOp = []; + let authOpts = mapSignInOptions(signInOptions); + if (authOpts.length == 0) { if (typeof options === 'string') { diff --git a/server/plugin.ts b/server/plugin.ts index 0f266c1fe..f64f4222f 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -46,6 +46,8 @@ import { createMigrationOpenSearchClient } from '../../../src/core/server/saved_ import { SecuritySavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper'; import { addTenantParameterToResolvedShortLink } from './multitenancy/tenant_resolver'; import { ReadonlyService } from './readonly/readonly_service'; +import { DashboardSignInOptions } from '../public/apps/configuration/types'; +import { AuthType } from '../common'; export interface SecurityPluginRequestContext { logger: Logger; @@ -113,8 +115,23 @@ export class SecurityPlugin implements Plugin data.dashboard_signin_options + .map((opt: string) => { + if (DashboardSignInOptions[opt] === DashboardSignInOptions.BASIC) { + return AuthType.BASIC + } else { + return opt.toString().toLowerCase() + } + } + )); + + dashboard_signin_options = new Set([...dashboard_signin_options, ...config.auth.type]); + dashboard_signin_options = [...dashboard_signin_options]; + const auth: IAuthenticationType = await getAuthenticationHandler( - config.auth.type, + dashboard_signin_options, router, config, core, From ac2a8c49913df57506e003a9bd0cea2c9380cb3d Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 13 Dec 2023 10:55:22 -0800 Subject: [PATCH 19/34] Enable MultiAuth when more than 1 auth types are present Signed-off-by: David Osorno --- server/auth/auth_handler_factory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/auth/auth_handler_factory.ts b/server/auth/auth_handler_factory.ts index 2cd3c7c25..0613ae6cc 100644 --- a/server/auth/auth_handler_factory.ts +++ b/server/auth/auth_handler_factory.ts @@ -57,6 +57,7 @@ export async function getAuthenticationHandler( logger: Logger ): Promise { let authHandlerType: IAuthHandlerConstructor; + if (typeof authType === 'string' || authType.length === 1) { const currType = typeof authType === 'string' ? authType : authType[0]; switch (currType.toLowerCase()) { @@ -80,7 +81,7 @@ export async function getAuthenticationHandler( throw new Error(`Unsupported authentication type: ${currType}`); } } else { - if (config.auth.multiple_auth_enabled) { + if (authType.length > 1) { authHandlerType = MultipleAuthentication; } else { throw new Error( From 539751b0b54fed124edfd432f6eb321eac9fea34 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 13 Dec 2023 11:45:39 -0800 Subject: [PATCH 20/34] Add anonymous sign in option Signed-off-by: David Osorno --- .../configuration/panels/auth-view/auth-view.tsx | 3 +++ .../panels/auth-view/dashboard-signin-options.tsx | 14 ++++++++++++++ server/plugin.ts | 1 + 3 files changed, 18 insertions(+) diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index 15a00d96b..975fc833a 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -32,6 +32,7 @@ export function AuthView(props: AppDependencies) { const [dashboardSignInOptions, setDashboardSignInOptions] = React.useState< DashboardSignInOptions[] >([]); + const [anonymousOption, setAnonymousOption] = useState(false); const [loading, setLoading] = useState(false); React.useEffect(() => { @@ -43,6 +44,7 @@ export function AuthView(props: AppDependencies) { setAuthentication(config.authc); setAuthorization(config.authz); setDashboardSignInOptions(config.kibana.dashboardSignInOptions); + setAnonymousOption(config.http.anonymous_auth_enabled); } catch (e) { console.log(e); } finally { @@ -78,6 +80,7 @@ export function AuthView(props: AppDependencies) { authc={authentication} signInEnabledOptions={dashboardSignInOptions} http={props.coreStart.http} + anonymousOption={anonymousOption} /> {/* @ts-ignore */} diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index ffe6348d3..99f3ff3e9 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -37,6 +37,7 @@ interface SignInOptionsPanelProps { authc: []; signInEnabledOptions: DashboardSignInOptions[]; http: HttpSetup; + anonymousOption: boolean } export const columns: Array> = [ @@ -99,6 +100,19 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { } }, [props.signInEnabledOptions, props.authc, dashboardOptions, domains]); + useEffect(() => { + if(props.anonymousOption){ + const option = "ANONYMOUS"; + const anonymousOption: DashboardOption = { + name: option, + status: props.signInEnabledOptions.indexOf(option) > -1, + }; + + setDashboardOptions((prevState) => [...prevState, anonymousOption]); + props.anonymousOption = false; + } + }, [props.anonymousOption]) + return ( diff --git a/server/plugin.ts b/server/plugin.ts index f64f4222f..424248687 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -127,6 +127,7 @@ export class SecurityPlugin implements Plugin([...dashboard_signin_options, ...config.auth.type]); dashboard_signin_options = [...dashboard_signin_options]; From c2686f0d51bb37bac4c0d5775d81c2d63273d8d8 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 15 Dec 2023 13:47:54 -0800 Subject: [PATCH 21/34] Test getting dashboard sign-in options from backend Signed-off-by: David Osorno --- .../get-dashboard-signin-options.test.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/jest_integration/get-dashboard-signin-options.test.ts diff --git a/test/jest_integration/get-dashboard-signin-options.test.ts b/test/jest_integration/get-dashboard-signin-options.test.ts new file mode 100644 index 000000000..de4d6340b --- /dev/null +++ b/test/jest_integration/get-dashboard-signin-options.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { Root } from "../../../../src/core/server/root"; +import * as osdTestServer from '../../../../src/core/test_helpers/osd_server'; +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { resolve } from 'path'; + +import { + AUTHORIZATION_HEADER_NAME, + OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, + OPENSEARCH_DASHBOARDS_SERVER_USER +} from '../constant'; +import { API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS } from "../../common"; +import { DashboardSignInOptions } from "../../public/apps/configuration/types"; + +describe('start OpenSearch Dashboards server', () => { + let root: Root; + + beforeAll(async () => { + root = osdTestServer.createRootWithSettings( + { + plugins: { + scanDirs: [resolve(__dirname, '../..')], + }, + opensearch: { + hosts: ['https://localhost:9200'], + ignoreVersionMismatch: true, + ssl: { verificationMode: 'none' }, + username: OPENSEARCH_DASHBOARDS_SERVER_USER, + password: OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, + }, + opensearch_security: { + multitenancy: { enabled: true, tenants: { preferred: ['Private', 'Global'] } }, + }, + }, + { + // to make ignoreVersionMismatch setting work + // can be removed when we have corresponding ES version + dev: true, + } + ); + + console.log('Starting OpenSearchDashboards server..'); + await root.setup(); + await root.start(); + console.log('Started OpenSearchDashboards server'); + }); + + afterAll(async () => { + // shutdown OpenSearchDashboards server + await root.shutdown(); + }); + + it(`get ${API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS} should return all dashboard sign-in options from backend.`, async () => { + const response = await osdTestServer + .request + .get(root, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS) + .unset(AUTHORIZATION_HEADER_NAME); + + expect(response.status).toEqual(200); + expect(response.text).toContain(DashboardSignInOptions[DashboardSignInOptions.BASIC]); + expect(JSON.parse(response.text)).toBeInstanceOf(Array); + }); +}); From 99b62a884628f1d4bdf9d2492ad739ecf009879e Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 15 Dec 2023 19:41:55 -0800 Subject: [PATCH 22/34] Test dashboard sign in options functionality. Signed-off-by: David Osorno --- .../panels/auth-view/signin-options-modal.tsx | 13 ++- public/apps/login/login-page.tsx | 3 + .../e2e/dashboard-signin-options.cy.spec.js | 95 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 test/cypress/e2e/dashboard-signin-options.cy.spec.js diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index a3d9791f0..d7a3370c8 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -119,7 +119,12 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { Cancel - + Update @@ -128,7 +133,11 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { } return (
- Edit + + Edit {modal}
); diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 57fe99953..7954a45c9 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -207,6 +207,7 @@ export function LoginPage(props: LoginPageDeps) { } @@ -220,6 +221,7 @@ export function LoginPage(props: LoginPageDeps) { } @@ -234,6 +236,7 @@ export function LoginPage(props: LoginPageDeps) { formBody.push( { + it('Dashboard Plugin Auth shows dashboard sign in options with their state.', () => { + login(); + cy.contains('BASIC'); + cy.contains('Enable'); + cy.contains('SAML'); + cy.contains('Disable'); + }); + + it('Login page shows Basic and Single Sign-On options.', () => { + login(); + + cy.get('[data-testid="edit"]').click(); + cy.get('#_selection_column_SAML-checkbox').check(); + cy.get('[data-testid="update"]').click(); + + logout(); + + cy.get('[data-testid="username"]').should('exist'); + cy.get('[data-testid="password"]').should('exist'); + cy.contains('Log in with single sign-on').should('exist'); + }); + + it('Login page shows only Single Sign-On option (SAML).', () => { + login(); + + cy.get('[data-testid="edit"]').click(); + cy.get('#_selection_column_SAML-checkbox').check(); + cy.get('#_selection_column_BASIC-checkbox').uncheck(); + cy.get('[data-testid="update"]').click(); + + logout(); + + cy.contains('Log in with single sign-on'); + cy.get('[data-testid="username"]').should('not.exist'); + cy.get('[data-testid="password"]').should('not.exist'); + }); + + it('Login page shows only Basic sign-in option.', () => { + cy.visit('http://localhost:5601/app/login?nextUrl=%2Fapp%2Fsecurity-dashboards-plugin#/auth'); + cy.contains('Log in with single sign-on').click(); + + cy.get('[name="username"]').type('user1'); + cy.get('[name="password"]').type('user1pass'); + cy.contains('Login').click(); + + if (cy.contains('Cancel')) { + cy.contains('Cancel').click(); + } + + cy.get('[data-testid="edit"]').click(); + cy.get('#_selection_column_SAML-checkbox').uncheck(); + cy.get('#_selection_column_BASIC-checkbox').check(); + cy.get('[data-testid="update"]').click(); + + cy.visit('http://localhost:5601/app/login'); + + cy.contains('Log in with single sign-on').should('not.exist'); + cy.get('[data-testid="username"]').should('exist'); + cy.get('[data-testid="password"]').should('exist'); + }); +}); From 76b380ef7e4280d464207c4265d1debaca2c94b5 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 15 Dec 2023 21:01:17 -0800 Subject: [PATCH 23/34] Apply prettier format Signed-off-by: David Osorno --- .../panels/auth-view/dashboard-signin-options.tsx | 8 ++++---- .../panels/auth-view/signin-options-modal.tsx | 15 ++++----------- server/multitenancy/routes.ts | 1 - server/plugin.ts | 12 ++++++------ server/routes/index.ts | 11 +++++++++-- .../get-dashboard-signin-options.test.ts | 11 +++++------ 6 files changed, 28 insertions(+), 30 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index 99f3ff3e9..633fecedf 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -37,7 +37,7 @@ interface SignInOptionsPanelProps { authc: []; signInEnabledOptions: DashboardSignInOptions[]; http: HttpSetup; - anonymousOption: boolean + anonymousOption: boolean; } export const columns: Array> = [ @@ -101,8 +101,8 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { }, [props.signInEnabledOptions, props.authc, dashboardOptions, domains]); useEffect(() => { - if(props.anonymousOption){ - const option = "ANONYMOUS"; + if (props.anonymousOption) { + const option = 'ANONYMOUS'; const anonymousOption: DashboardOption = { name: option, status: props.signInEnabledOptions.indexOf(option) > -1, @@ -111,7 +111,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { setDashboardOptions((prevState) => [...prevState, anonymousOption]); props.anonymousOption = false; } - }, [props.anonymousOption]) + }, [props.anonymousOption]); return ( diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index d7a3370c8..9638aaa9a 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -119,12 +119,7 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { Cancel - + Update @@ -133,11 +128,9 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { } return (
- - Edit + + Edit + {modal}
); diff --git a/server/multitenancy/routes.ts b/server/multitenancy/routes.ts index dcf21947d..d4dce3bf5 100644 --- a/server/multitenancy/routes.ts +++ b/server/multitenancy/routes.ts @@ -164,4 +164,3 @@ export function setupMultitenantRoutes( } ); } - diff --git a/server/plugin.ts b/server/plugin.ts index 424248687..3b3998e49 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -117,15 +117,15 @@ export class SecurityPlugin implements Plugin data.dashboard_signin_options - .map((opt: string) => { + .then((data) => + data.dashboard_signin_options.map((opt: string) => { if (DashboardSignInOptions[opt] === DashboardSignInOptions.BASIC) { - return AuthType.BASIC + return AuthType.BASIC; } else { - return opt.toString().toLowerCase() + return opt.toString().toLowerCase(); } - } - )); + }) + ); // Combine sign in options with auth.type in case there are JWT, PROXY or more auth types. dashboard_signin_options = new Set([...dashboard_signin_options, ...config.auth.type]); diff --git a/server/routes/index.ts b/server/routes/index.ts index 2d953255f..68cf15a76 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -20,7 +20,12 @@ import { IOpenSearchDashboardsResponse, OpenSearchDashboardsResponseFactory, } from 'opensearch-dashboards/server'; -import { API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS, API_PREFIX, CONFIGURATION_API_PREFIX, isValidResourceName } from '../../common'; +import { + API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS, + API_PREFIX, + CONFIGURATION_API_PREFIX, + isValidResourceName, +} from '../../common'; import { DashboardSignInOptions, ResourceType } from '../../public/apps/configuration/types'; // TODO: consider to extract entity CRUD operations and put it into a client class @@ -605,7 +610,9 @@ export function defineRoutes(router: IRouter) { path: `${API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS}`, validate: { body: schema.object({ - dashboard_signin_options: schema.arrayOf(schema.any(), { defaultValue: [DashboardSignInOptions.BASIC] }), + dashboard_signin_options: schema.arrayOf(schema.any(), { + defaultValue: [DashboardSignInOptions.BASIC], + }), }), }, }, diff --git a/test/jest_integration/get-dashboard-signin-options.test.ts b/test/jest_integration/get-dashboard-signin-options.test.ts index de4d6340b..9d5e10b75 100644 --- a/test/jest_integration/get-dashboard-signin-options.test.ts +++ b/test/jest_integration/get-dashboard-signin-options.test.ts @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import { Root } from "../../../../src/core/server/root"; +import { Root } from '../../../../src/core/server/root'; import * as osdTestServer from '../../../../src/core/test_helpers/osd_server'; import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import { resolve } from 'path'; @@ -21,10 +21,10 @@ import { resolve } from 'path'; import { AUTHORIZATION_HEADER_NAME, OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, - OPENSEARCH_DASHBOARDS_SERVER_USER + OPENSEARCH_DASHBOARDS_SERVER_USER, } from '../constant'; -import { API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS } from "../../common"; -import { DashboardSignInOptions } from "../../public/apps/configuration/types"; +import { API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS } from '../../common'; +import { DashboardSignInOptions } from '../../public/apps/configuration/types'; describe('start OpenSearch Dashboards server', () => { let root: Root; @@ -65,8 +65,7 @@ describe('start OpenSearch Dashboards server', () => { }); it(`get ${API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS} should return all dashboard sign-in options from backend.`, async () => { - const response = await osdTestServer - .request + const response = await osdTestServer.request .get(root, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS) .unset(AUTHORIZATION_HEADER_NAME); From 313c0a4a4ee654102fb2d04727d2c77aeed64f39 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 15 Dec 2023 22:49:09 -0800 Subject: [PATCH 24/34] Fix lint errors Signed-off-by: David Osorno --- .../panels/auth-view/auth-view.tsx | 6 ++--- .../auth-view/dashboard-signin-options.tsx | 24 ++++++++++--------- .../panels/auth-view/signin-options-modal.tsx | 4 ++-- public/apps/configuration/types.ts | 2 +- public/apps/login/login-page.tsx | 15 ++++++------ server/plugin.ts | 8 +++---- 6 files changed, 30 insertions(+), 29 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index 975fc833a..aa811b0bf 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -32,7 +32,7 @@ export function AuthView(props: AppDependencies) { const [dashboardSignInOptions, setDashboardSignInOptions] = React.useState< DashboardSignInOptions[] >([]); - const [anonymousOption, setAnonymousOption] = useState(false); + const [isAnonymousAuthEnable, setAnonymousAuthEnable] = useState(false); const [loading, setLoading] = useState(false); React.useEffect(() => { @@ -44,7 +44,7 @@ export function AuthView(props: AppDependencies) { setAuthentication(config.authc); setAuthorization(config.authz); setDashboardSignInOptions(config.kibana.dashboardSignInOptions); - setAnonymousOption(config.http.anonymous_auth_enabled); + setAnonymousAuthEnable(config.http.anonymous_auth_enabled); } catch (e) { console.log(e); } finally { @@ -80,7 +80,7 @@ export function AuthView(props: AppDependencies) { authc={authentication} signInEnabledOptions={dashboardSignInOptions} http={props.coreStart.http} - anonymousOption={anonymousOption} + isAnonymousAuthEnable={isAnonymousAuthEnable} /> {/* @ts-ignore */} diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index 633fecedf..4b3ba9a36 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -37,7 +37,7 @@ interface SignInOptionsPanelProps { authc: []; signInEnabledOptions: DashboardSignInOptions[]; http: HttpSetup; - anonymousOption: boolean; + isAnonymousAuthEnable: boolean; } export const columns: Array> = [ @@ -67,7 +67,9 @@ export const columns: Array> = [ ]; export function SignInOptionsPanel(props: SignInOptionsPanelProps) { - const domains = keys(props.authc); + const { authc, signInEnabledOptions, http } = props; + + const domains = keys(authc); const [toasts, addToast, removeToast] = useToastState(); const [dashboardOptions, setDashboardOptions] = useState([]); @@ -75,13 +77,13 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const getDasboardOptions = () => { const options = domains .map((domain) => { - const data = get(props.authc, domain); + const data = get(authc, domain); const option = data.http_authenticator.type.toUpperCase(); if (option in DashboardSignInOptions) { const dashboardOption: DashboardOption = { name: option, - status: props.signInEnabledOptions.indexOf(option) > -1, + status: signInEnabledOptions.indexOf(option) > -1, }; return dashboardOption; @@ -95,23 +97,23 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { setDashboardOptions(options); }; - if (props.signInEnabledOptions.length > 0 && dashboardOptions.length === 0) { + if (signInEnabledOptions.length > 0 && dashboardOptions.length === 0) { getDasboardOptions(); } - }, [props.signInEnabledOptions, props.authc, dashboardOptions, domains]); + }, [signInEnabledOptions, authc, dashboardOptions, domains]); useEffect(() => { - if (props.anonymousOption) { + if (props.isAnonymousAuthEnable) { const option = 'ANONYMOUS'; const anonymousOption: DashboardOption = { name: option, - status: props.signInEnabledOptions.indexOf(option) > -1, + status: signInEnabledOptions.indexOf(option) > -1, }; setDashboardOptions((prevState) => [...prevState, anonymousOption]); - props.anonymousOption = false; + props.isAnonymousAuthEnable = false; } - }, [props.anonymousOption]); + }, [props.isAnonymousAuthEnable, signInEnabledOptions, props]); return ( @@ -132,7 +134,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index 9638aaa9a..600e952cf 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -26,7 +26,7 @@ import { } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; import { HttpSetup } from 'opensearch-dashboards/public'; -import { DashboardOption } from '../../types'; +import { DashboardOption, DashboardSignInOptions } from '../../types'; import { createErrorToast, createSuccessToast } from '../../utils/toast-utils'; import { columns } from './dashboard-signin-options'; import { updateDashboardSignInOptions } from '../../../../utils/dashboards-info-utils'; @@ -65,7 +65,7 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { const handleUpdate = async () => { await updateDashboardSignInOptions( props.http, - newSignInOptions.map((opt) => opt.name) + newSignInOptions.map((opt) => opt.name as DashboardSignInOptions) ) .then(() => { changeDashboardSignInOptionsStatus(); diff --git a/public/apps/configuration/types.ts b/public/apps/configuration/types.ts index 55b485171..be94bb671 100644 --- a/public/apps/configuration/types.ts +++ b/public/apps/configuration/types.ts @@ -170,6 +170,6 @@ export enum DashboardSignInOptions { } export interface DashboardOption { - name: DashboardSignInOptions; + name: DashboardSignInOptions | string; status: boolean; } diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 7954a45c9..8df631503 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -162,29 +162,28 @@ export function LoginPage(props: LoginPageDeps) { ); }; - const mapSignInOptions = (signInOptions: DashboardSignInOptions[]) => { + const mapSignInOptions = (options: DashboardSignInOptions[]) => { const authOpts = []; - for (let i = 0; i < signInOptions.length; i++) { + for (let i = 0; i < options.length; i++) { // Dashboard sign-in options are gotten from HTTP type property where the value is 'openid' and it needs to match with AuthType open_id; - if (DashboardSignInOptions[signInOptions[i]] === DashboardSignInOptions.OPENID) { + if (DashboardSignInOptions[options[i]] === DashboardSignInOptions.OPENID) { authOpts.push(AuthType.OPEN_ID); } else { - let authType = AuthType[signInOptions[i]]; + const authType = AuthType[options[i]]; if (authType) { authOpts.push(authType); } } } return authOpts; - } + }; const formOptions = (options: string | string[]) => { let formBody = []; const formBodyOp = []; - let authOpts = mapSignInOptions(signInOptions); - + let authOpts = mapSignInOptions(signInOptions); - if (authOpts.length == 0) { + if (authOpts.length === 0) { if (typeof options === 'string') { if (options === '') { authOpts.push(AuthType.BASIC); diff --git a/server/plugin.ts b/server/plugin.ts index 3b3998e49..1f06534fb 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -115,7 +115,7 @@ export class SecurityPlugin implements Plugin data.dashboard_signin_options.map((opt: string) => { @@ -128,11 +128,11 @@ export class SecurityPlugin implements Plugin([...dashboard_signin_options, ...config.auth.type]); - dashboard_signin_options = [...dashboard_signin_options]; + dashboardSignInOptions = new Set([...dashboardSignInOptions, ...config.auth.type]); + dashboardSignInOptions = [...dashboardSignInOptions]; const auth: IAuthenticationType = await getAuthenticationHandler( - dashboard_signin_options, + dashboardSignInOptions, router, config, core, From 8765a6b63e0333ac4a8afc88e03bba2078429a0a Mon Sep 17 00:00:00 2001 From: David Osorno Date: Fri, 15 Dec 2023 22:50:55 -0800 Subject: [PATCH 25/34] Improve anonymous option logic Signed-off-by: David Osorno --- .../panels/auth-view/dashboard-signin-options.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index 4b3ba9a36..a5703abd2 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -67,7 +67,7 @@ export const columns: Array> = [ ]; export function SignInOptionsPanel(props: SignInOptionsPanelProps) { - const { authc, signInEnabledOptions, http } = props; + const { authc, signInEnabledOptions, http, isAnonymousAuthEnable } = props; const domains = keys(authc); const [toasts, addToast, removeToast] = useToastState(); @@ -103,17 +103,16 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { }, [signInEnabledOptions, authc, dashboardOptions, domains]); useEffect(() => { - if (props.isAnonymousAuthEnable) { - const option = 'ANONYMOUS'; + if (isAnonymousAuthEnable) { + const option = DashboardSignInOptions.ANONYMOUS; const anonymousOption: DashboardOption = { - name: option, - status: signInEnabledOptions.indexOf(option) > -1, + name: DashboardSignInOptions[option], + status: signInEnabledOptions.indexOf(DashboardSignInOptions[option]) > -1, }; setDashboardOptions((prevState) => [...prevState, anonymousOption]); - props.isAnonymousAuthEnable = false; } - }, [props.isAnonymousAuthEnable, signInEnabledOptions, props]); + }, [signInEnabledOptions, isAnonymousAuthEnable]); return ( From d51996b9486ef36ba661f9c0dae7ab51f7608b2d Mon Sep 17 00:00:00 2001 From: David Osorno Date: Mon, 18 Dec 2023 15:42:28 -0800 Subject: [PATCH 26/34] Refactor code to allow isolated test Signed-off-by: David Osorno --- .../auth-view/dashboard-signin-options.tsx | 32 +++++++++- .../panels/auth-view/signin-options-modal.tsx | 61 +++++-------------- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index a5703abd2..9837d95b9 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -29,8 +29,9 @@ import { 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 { useToastState } from '../../utils/toast-utils'; +import { createErrorToast, createSuccessToast, useToastState } from '../../utils/toast-utils'; import { SignInOptionsModal } from './signin-options-modal'; interface SignInOptionsPanelProps { @@ -114,6 +115,32 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { } }, [signInEnabledOptions, isAnonymousAuthEnable]); + 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 ( @@ -133,8 +160,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index 600e952cf..add4b62c2 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -24,30 +23,26 @@ import { EuiModalHeaderTitle, EuiSpacer, } from '@elastic/eui'; -import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; -import { HttpSetup } from 'opensearch-dashboards/public'; -import { DashboardOption, DashboardSignInOptions } from '../../types'; -import { createErrorToast, createSuccessToast } from '../../utils/toast-utils'; +import React, { Dispatch, SetStateAction } from 'react'; +import { DashboardOption } from '../../types'; import { columns } from './dashboard-signin-options'; -import { updateDashboardSignInOptions } from '../../../../utils/dashboards-info-utils'; interface DashboardSignInProps { dashboardOptions: DashboardOption[]; setDashboardOptions: Dispatch>; - http: HttpSetup; - addToast: (toAdd: Toast) => void; + handleUpdate: Function; } export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { - const [newSignInOptions, setNewSignInOptions] = useState([]); - const [disableUpdate, disableUpdateButton] = useState(false); + const [newSignInOptions, setNewSignInOptions] = React.useState([]); + const [disableUpdate, disableUpdateButton] = React.useState(false); const actualSignInOptions: DashboardOption[] = props.dashboardOptions.filter((opt) => opt.status); - const [isModalVisible, setIsModalVisible] = useState(false); + const [isModalVisible, setIsModalVisible] = React.useState(false); const closeModal = () => setIsModalVisible(false); const showModal = () => setIsModalVisible(true); - useEffect(() => { + React.useEffect(() => { if (actualSignInOptions.length !== newSignInOptions.length && newSignInOptions.length > 0) { disableUpdateButton(false); } else { @@ -62,40 +57,8 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { } }, [newSignInOptions, actualSignInOptions]); - const handleUpdate = async () => { - await updateDashboardSignInOptions( - props.http, - newSignInOptions.map((opt) => opt.name as DashboardSignInOptions) - ) - .then(() => { - changeDashboardSignInOptionsStatus(); - props.addToast( - createSuccessToast('updatePassed', 'Dashboard SignIn Options', 'Changes applied.') - ); - }) - .catch((e) => { - console.log('The sign in options could not be updated'); - console.log(e); - props.addToast( - createErrorToast('updatedError', 'Dashboard SignIn Options', 'Error updating values.') - ); - }) - .finally(() => { - closeModal(); - }); - }; - let modal; - const changeDashboardSignInOptionsStatus = () => { - props.setDashboardOptions((prevOptions) => - prevOptions.map((option) => { - option.status = newSignInOptions.includes(option); - return option; - }) - ); - }; - if (isModalVisible) { modal = ( @@ -119,7 +82,15 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { Cancel - + { + props.handleUpdate(newSignInOptions); + closeModal(); + }} + fill + disabled={disableUpdate} + > Update From ce25d3ae019c0016010dfd90b31de939473141d4 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Mon, 18 Dec 2023 15:44:44 -0800 Subject: [PATCH 27/34] Add modal unit test Signed-off-by: David Osorno --- .../signin-options-modal.test.tsx.snap | 88 +++++++++++++++++++ .../test/signin-options-modal.test.tsx | 71 +++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 public/apps/configuration/panels/auth-view/test/__snapshots__/signin-options-modal.test.tsx.snap create mode 100644 public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx diff --git a/public/apps/configuration/panels/auth-view/test/__snapshots__/signin-options-modal.test.tsx.snap b/public/apps/configuration/panels/auth-view/test/__snapshots__/signin-options-modal.test.tsx.snap new file mode 100644 index 000000000..50abdde1d --- /dev/null +++ b/public/apps/configuration/panels/auth-view/test/__snapshots__/signin-options-modal.test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test sign-in options modal Render should render SignIn Options modal with 2 options 1`] = ` +
+ + Edit + + + + + Dashboard Sign In Options + + + + Enable/Disable sign-in options for OpenSearch Dashboard. + + + + + + Cancel + + + Update + + + +
+`; diff --git a/public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx b/public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx new file mode 100644 index 000000000..a279cf374 --- /dev/null +++ b/public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { EuiBasicTable } from '@elastic/eui'; +import { shallow } from 'enzyme'; +import React from 'react'; +import { DashboardSignInOptions } from '../../../types'; +import { SignInOptionsModal } from '../signin-options-modal'; +import { OuiGlobalToastListToast } from '@opensearch-project/oui'; + +describe('Test sign-in options modal', () => { + const initialValues = [ + { name: DashboardSignInOptions[DashboardSignInOptions.BASIC], status: true }, + { name: DashboardSignInOptions[DashboardSignInOptions.SAML], status: false }, + ]; + + const useEffectMock = jest.spyOn(React, 'useEffect'); + const handleUpdate = jest.fn(); + + let component; + + beforeEach(() => { + component = shallow( + {}} + handleUpdate={handleUpdate} + /> + ); + }); + + describe('Render', () => { + it('should render SignIn Options modal with 2 options', () => { + component.find('[data-testid="edit"]').simulate('click'); + + expect(component).toMatchSnapshot(); + expect(component.find(EuiBasicTable).length).toBe(1); + expect(component.find(EuiBasicTable).prop('items').length).toBe(2); + }); + + it("'Update' button should be disabled", () => { + useEffectMock.mockImplementationOnce((f) => f()); + + component.find('[data-testid="edit"]').simulate('click'); + + expect(component.find('[data-testid="update"]').length).toBe(1); + expect(component.find('[data-testid="update"]').prop('disabled')).toBe(true); + }); + }); + + describe('Action', () => { + it('click Update should call handleUpdate function', () => { + component.find('[data-testid="edit"]').simulate('click'); + component.find('[data-testid="update"]').simulate('click'); + + expect(handleUpdate).toBeCalledTimes(1); + }); + }); +}); From db51bc276fdd5a2debcfd899228e6047aea3408e Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 3 Jan 2024 17:47:26 -0800 Subject: [PATCH 28/34] Revalidate sign in options when attemp to login Signed-off-by: David Osorno --- public/apps/login/login-page.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 8df631503..8110ac2cd 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -83,9 +83,19 @@ export function LoginPage(props: LoginPageDeps) { const [loginError, setloginError] = useState(''); const [usernameValidationFailed, setUsernameValidationFailed] = useState(false); const [passwordValidationFailed, setPasswordValidationFailed] = useState(false); - const [signInOptions, setSignInOptions] = useState([]); + const [signInOptions, setSignInOptions] = React.useState([]); - useEffect(() => { + // It will confirm that the sign-in option is still available. If not, it will reload the login page with the available options. + const reValidateSignInOption = async (option: DashboardSignInOptions) => { + const dashboardSignInOptions = await getDashboardsSignInOptions(props.http); + const isValidOption = dashboardSignInOptions.includes(DashboardSignInOptions[option]); + if (isValidOption === false) { + window.location.reload(); + } + return; + }; + + React.useEffect(() => { const getSignInOptions = async () => { try { const dashboardSignInOptions = await getDashboardsSignInOptions(props.http); @@ -126,6 +136,8 @@ export function LoginPage(props: LoginPageDeps) { setPasswordValidationFailed(true); return; } + + await reValidateSignInOption(DashboardSignInOptions.BASIC); try { await validateCurrentPassword(props.http, username, password); @@ -153,6 +165,9 @@ export function LoginPage(props: LoginPageDeps) { size="s" type="prime" className={buttonConfig.buttonstyle || 'btn-login'} + onClick={async () => + await reValidateSignInOption(DashboardSignInOptions[authType.toUpperCase()]) + } href={loginEndPointWithPath} iconType={buttonConfig.showbrandimage ? buttonConfig.brandimage : ''} > @@ -165,7 +180,7 @@ export function LoginPage(props: LoginPageDeps) { const mapSignInOptions = (options: DashboardSignInOptions[]) => { const authOpts = []; for (let i = 0; i < options.length; i++) { - // Dashboard sign-in options are gotten from HTTP type property where the value is 'openid' and it needs to match with AuthType open_id; + // Dashboard sign-in options are taken from HTTP type property where the value is 'openid' and it needs to match with AuthType open_id; if (DashboardSignInOptions[options[i]] === DashboardSignInOptions.OPENID) { authOpts.push(AuthType.OPEN_ID); } else { From 6bdd973ec1cbffd62de7ac5b023fcff054eafed3 Mon Sep 17 00:00:00 2001 From: EduardoCorazon Date: Fri, 5 Jan 2024 10:55:30 -0600 Subject: [PATCH 29/34] UI tweaks for OUI patterns Signed-off-by: EduardoCorazon --- .../auth-view/dashboard-signin-options.tsx | 19 +++-- .../panels/auth-view/signin-options-modal.tsx | 71 ++++++++++--------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index 9837d95b9..29a5fb8ae 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -75,6 +75,15 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const [dashboardOptions, setDashboardOptions] = useState([]); useEffect(() => { + const MakeAuthTypeHumanReadable = (authType) =>{ + const authTypeMap = { + BASIC:"Basic Authentication", + SAML:"SAML", + OPENID:"OpenID Connect", + ANONYMOUS:"Anonymous", + }; + return authTypeMap[authType] || authType; + } const getDasboardOptions = () => { const options = domains .map((domain) => { @@ -82,11 +91,11 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const option = data.http_authenticator.type.toUpperCase(); if (option in DashboardSignInOptions) { + const displayName = MakeAuthTypeHumanReadable(option); const dashboardOption: DashboardOption = { - name: option, + name: displayName, status: signInEnabledOptions.indexOf(option) > -1, }; - return dashboardOption; } }) @@ -94,7 +103,6 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { .filter( (option, index, arr) => arr.findIndex((opt) => opt?.name === option?.name) === index ) as DashboardOption[]; - setDashboardOptions(options); }; @@ -146,12 +154,11 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { -

Dashboard Sign In Options

+

Dashboards sign-in options

- Configure one or multiple authentication options to appear on the sign-in windows for - OpenSearch Dashboard. + Configure one or multiple authentication options to appear on the sign-in window for OpenSearch Dashboards.

diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index add4b62c2..57b8023d8 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -13,19 +13,20 @@ * permissions and limitations under the License. */ -import { - EuiBasicTable, - EuiButton, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiSpacer, -} from '@elastic/eui'; import React, { Dispatch, SetStateAction } from 'react'; import { DashboardOption } from '../../types'; import { columns } from './dashboard-signin-options'; +import { + OuiButtonEmpty, + OuiButton, + OuiModal, + OuiModalBody, + OuiModalFooter, + OuiModalHeader, + OuiModalHeaderTitle, + OuiSpacer, + OuiCheckboxGroup +} from '@opensearch-project/oui'; interface DashboardSignInProps { dashboardOptions: DashboardOption[]; @@ -61,28 +62,28 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { if (isModalVisible) { modal = ( - - - Dashboard Sign In Options - - - Enable/Disable sign-in options for OpenSearch Dashboard. - - + + Dashboards sign-in options + + + Select one or multiple authentication options to appear on the sign-in window for OpenSearch Dashboards. + + + ({ + label: option.name, + value: option, + checked: option.status, + }))} + onChange={(selectedOptions) => { + setNewSignInOptions(selectedOptions.map((option) => option.value)); }} /> - - - Cancel - + + Cancel + { props.handleUpdate(newSignInOptions); @@ -92,16 +93,16 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { disabled={disableUpdate} > Update - - - + + + ); } return (
- + Edit - + {modal}
); From affbaa1f2a37dd64d1ab0c59e91ad7fe4aa02a7b Mon Sep 17 00:00:00 2001 From: David Osorno Date: Mon, 8 Jan 2024 16:43:02 -0800 Subject: [PATCH 30/34] Ensure sign-in with anonymous is not an auto login Signed-off-by: David Osorno --- public/apps/login/login-page.tsx | 3 +- server/auth/types/basic/routes.ts | 94 +++++++++++++++++-------------- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx index 8110ac2cd..b7660b941 100644 --- a/public/apps/login/login-page.tsx +++ b/public/apps/login/login-page.tsx @@ -92,7 +92,6 @@ export function LoginPage(props: LoginPageDeps) { if (isValidOption === false) { window.location.reload(); } - return; }; React.useEffect(() => { @@ -136,7 +135,7 @@ export function LoginPage(props: LoginPageDeps) { setPasswordValidationFailed(true); return; } - + await reValidateSignInOption(DashboardSignInOptions.BASIC); try { diff --git a/server/auth/types/basic/routes.ts b/server/auth/types/basic/routes.ts index a82c395bd..d491298aa 100755 --- a/server/auth/types/basic/routes.ts +++ b/server/auth/types/basic/routes.ts @@ -29,8 +29,8 @@ import { LOGIN_PAGE_URI, } from '../../../../common'; import { resolveTenant } from '../../../multitenancy/tenant_resolver'; -import { encodeUriQuery } from '../../../../../../src/plugins/opensearch_dashboards_utils/common/url/encode_uri_query'; import { AuthType } from '../../../../common'; +import { DashboardSignInOptions } from '../../../../public/apps/configuration/types' export class BasicAuthRoutes { constructor( @@ -179,50 +179,50 @@ export class BasicAuthRoutes { let redirectUrl: string = this.coreSetup.http.basePath.serverBasePath ? this.coreSetup.http.basePath.serverBasePath : '/'; - const requestQuery = request.url.searchParams; - const nextUrl = requestQuery?.get('nextUrl'); - if (nextUrl) { - redirectUrl = nextUrl; - } - context.security_plugin.logger.info('The Redirect Path is ' + redirectUrl); - try { - user = await this.securityClient.authenticateWithHeaders(request, {}); - } catch (error) { - context.security_plugin.logger.error( - `Failed authentication: ${error}. Redirecting to Login Page` - ); - return response.redirected({ - headers: { - location: `${this.coreSetup.http.basePath.serverBasePath}${LOGIN_PAGE_URI}${ - nextUrl ? '?nextUrl=' + encodeUriQuery(redirectUrl) : '' - }`, - }, - }); - } - this.sessionStorageFactory.asScoped(request).clear(); - const sessionStorage: SecuritySessionCookie = { - username: user.username, - authType: AuthType.BASIC, - isAnonymousAuth: true, - expiryTime: Date.now() + this.config.session.ttl, - }; - - if (user.multitenancy_enabled) { - const selectTenant = resolveTenant({ - request, + const anonymousAccessAllowed = await verifyAnonymousAccess(this.securityClient, request); + + if (anonymousAccessAllowed) { + context.security_plugin.logger.info('The Redirect Path is ' + redirectUrl); + try { + user = await this.securityClient.authenticateWithHeaders(request, {}); + } catch (error) { + context.security_plugin.logger.error( + `Failed authentication: ${error}. Redirecting to Login Page` + ); + return response.redirected({ + headers: { + location: `${this.coreSetup.http.basePath.serverBasePath}${LOGIN_PAGE_URI}`, + }, + }); + } + + this.sessionStorageFactory.asScoped(request).clear(); + const sessionStorage: SecuritySessionCookie = { username: user.username, - roles: user.roles, - availabeTenants: user.tenants, - config: this.config, - cookie: sessionStorage, - multitenancyEnabled: user.multitenancy_enabled, - privateTenantEnabled: user.private_tenant_enabled, - defaultTenant: user.default_tenant, - }); - sessionStorage.tenant = selectTenant; + authType: AuthType.BASIC, + isAnonymousAuth: true, + expiryTime: Date.now() + this.config.session.ttl, + }; + + if (user.multitenancy_enabled) { + const selectTenant = resolveTenant({ + request, + username: user.username, + roles: user.roles, + availabeTenants: user.tenants, + config: this.config, + cookie: sessionStorage, + multitenancyEnabled: user.multitenancy_enabled, + privateTenantEnabled: user.private_tenant_enabled, + defaultTenant: user.default_tenant, + }); + sessionStorage.tenant = selectTenant; + } + this.sessionStorageFactory.asScoped(request).set(sessionStorage); + } else { + redirectUrl = LOGIN_PAGE_URI; } - this.sessionStorageFactory.asScoped(request).set(sessionStorage); return response.redirected({ headers: { @@ -243,3 +243,13 @@ export class BasicAuthRoutes { ); } } + +async function verifyAnonymousAccess(securityClient: SecurityClient, request: any) { + //Preventing auto login. + const isAutologin = request.url.href.includes("anonymous?"); + + const dashboardsInfo = await securityClient.dashboardsinfo(request); + const isAnonymousEnabled = dashboardsInfo.dashboard_signin_options.includes(DashboardSignInOptions[DashboardSignInOptions.ANONYMOUS]); + + return (isAnonymousEnabled && !isAutologin) || (isAnonymousEnabled && dashboardsInfo.dashboard_signin_options.length === 1); +} From 512baa7a3ebb5c063da439c89ee255945b8b88d4 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Mon, 5 Feb 2024 14:52:37 -0800 Subject: [PATCH 31/34] Add property to display readable auth type names Signed-off-by: David Osorno --- .../auth-view/dashboard-signin-options.tsx | 44 +++++++++---------- public/apps/configuration/types.ts | 1 + 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index 29a5fb8ae..a9857d6c9 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -1,16 +1,12 @@ /* - * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import { @@ -43,7 +39,7 @@ interface SignInOptionsPanelProps { export const columns: Array> = [ { - field: 'name', + field: 'displayName', name: 'Name', 'data-test-subj': 'name', mobileOptions: { @@ -74,16 +70,14 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const [toasts, addToast, removeToast] = useToastState(); const [dashboardOptions, setDashboardOptions] = useState([]); + enum makeAuthTypeHumanReadable { + BASIC = 'Basic Authentication', + SAML = 'SAML', + OPENID = 'OpenID Connect', + ANONYMOUS = 'Anonymous', + } + useEffect(() => { - const MakeAuthTypeHumanReadable = (authType) =>{ - const authTypeMap = { - BASIC:"Basic Authentication", - SAML:"SAML", - OPENID:"OpenID Connect", - ANONYMOUS:"Anonymous", - }; - return authTypeMap[authType] || authType; - } const getDasboardOptions = () => { const options = domains .map((domain) => { @@ -91,10 +85,10 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const option = data.http_authenticator.type.toUpperCase(); if (option in DashboardSignInOptions) { - const displayName = MakeAuthTypeHumanReadable(option); const dashboardOption: DashboardOption = { - name: displayName, + name: option, status: signInEnabledOptions.indexOf(option) > -1, + displayName: makeAuthTypeHumanReadable[option], }; return dashboardOption; } @@ -117,6 +111,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const anonymousOption: DashboardOption = { name: DashboardSignInOptions[option], status: signInEnabledOptions.indexOf(DashboardSignInOptions[option]) > -1, + displayName: makeAuthTypeHumanReadable[option], }; setDashboardOptions((prevState) => [...prevState, anonymousOption]); @@ -158,7 +153,8 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) {

- Configure one or multiple authentication options to appear on the sign-in window for OpenSearch Dashboards. + Configure one or multiple authentication options to appear on the sign-in window for + OpenSearch Dashboards.

@@ -178,7 +174,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { columns={columns} items={dashboardOptions} itemId={'signin_options'} - sorting={{ sort: { field: 'name', direction: 'asc' } }} + sorting={{ sort: { field: 'displayName', direction: 'asc' } }} />
diff --git a/public/apps/configuration/types.ts b/public/apps/configuration/types.ts index e7c8f07e3..eb753a85a 100644 --- a/public/apps/configuration/types.ts +++ b/public/apps/configuration/types.ts @@ -160,4 +160,5 @@ export enum DashboardSignInOptions { export interface DashboardOption { name: DashboardSignInOptions | string; status: boolean; + displayName: string; } From 2db9d844e4fd9907b54848e2f2c4f9813cb011bb Mon Sep 17 00:00:00 2001 From: David Osorno Date: Mon, 5 Feb 2024 14:54:22 -0800 Subject: [PATCH 32/34] Change to elastic components based on feedback received Signed-off-by: David Osorno --- .../panels/auth-view/signin-options-modal.tsx | 89 +++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx index 57b8023d8..84fef3566 100644 --- a/public/apps/configuration/panels/auth-view/signin-options-modal.tsx +++ b/public/apps/configuration/panels/auth-view/signin-options-modal.tsx @@ -1,32 +1,28 @@ /* - * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. */ import React, { Dispatch, SetStateAction } from 'react'; import { DashboardOption } from '../../types'; -import { columns } from './dashboard-signin-options'; + import { - OuiButtonEmpty, - OuiButton, - OuiModal, - OuiModalBody, - OuiModalFooter, - OuiModalHeader, - OuiModalHeaderTitle, - OuiSpacer, - OuiCheckboxGroup -} from '@opensearch-project/oui'; + EuiButton, + EuiInMemoryTable, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, +} from '@elastic/eui'; +import { columns } from './dashboard-signin-options'; interface DashboardSignInProps { dashboardOptions: DashboardOption[]; @@ -62,28 +58,29 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { if (isModalVisible) { modal = ( - - - Dashboards sign-in options - - - Select one or multiple authentication options to appear on the sign-in window for OpenSearch Dashboards. - - - ({ - label: option.name, - value: option, - checked: option.status, - }))} - onChange={(selectedOptions) => { - setNewSignInOptions(selectedOptions.map((option) => option.value)); + + + Dashboards sign-in options + + + Select one or multiple authentication options to appear on the sign-in window for + OpenSearch Dashboards. + + - - - Cancel - + + Cancel + { props.handleUpdate(newSignInOptions); @@ -93,16 +90,16 @@ export function SignInOptionsModal(props: DashboardSignInProps): JSX.Element { disabled={disableUpdate} > Update - - - +
+
+
); } return (
- + Edit - + {modal}
); From 16695d988d5394655f4ae12e5e38fd46962bf472 Mon Sep 17 00:00:00 2001 From: David Osorno Date: Mon, 5 Feb 2024 15:30:37 -0800 Subject: [PATCH 33/34] Add single sign-on name to buttons Signed-off-by: David Osorno --- .../panels/auth-view/dashboard-signin-options.tsx | 2 +- server/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx index a9857d6c9..eb15e7c98 100644 --- a/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx +++ b/public/apps/configuration/panels/auth-view/dashboard-signin-options.tsx @@ -111,7 +111,7 @@ export function SignInOptionsPanel(props: SignInOptionsPanelProps) { const anonymousOption: DashboardOption = { name: DashboardSignInOptions[option], status: signInEnabledOptions.indexOf(DashboardSignInOptions[option]) > -1, - displayName: makeAuthTypeHumanReadable[option], + displayName: makeAuthTypeHumanReadable.ANONYMOUS, }; setDashboardOptions((prevState) => [...prevState, anonymousOption]); diff --git a/server/index.ts b/server/index.ts index 68a20f533..e00d39d25 100644 --- a/server/index.ts +++ b/server/index.ts @@ -271,7 +271,7 @@ export const configSchema = schema.object({ }), openid: schema.object({ login: schema.object({ - buttonname: schema.string({ defaultValue: 'Log in with single sign-on' }), + buttonname: schema.string({ defaultValue: 'Log in with single sign-on (OpenID)' }), showbrandimage: schema.boolean({ defaultValue: false }), brandimage: schema.string({ defaultValue: '' }), buttonstyle: schema.string({ defaultValue: '' }), @@ -279,7 +279,7 @@ export const configSchema = schema.object({ }), saml: schema.object({ login: schema.object({ - buttonname: schema.string({ defaultValue: 'Log in with single sign-on' }), + buttonname: schema.string({ defaultValue: 'Log in with single sign-on (SAML)' }), showbrandimage: schema.boolean({ defaultValue: false }), brandimage: schema.string({ defaultValue: '' }), buttonstyle: schema.string({ defaultValue: '' }), From 33c1c4381148a16dcc75e2caa1d734dcd94fbdde Mon Sep 17 00:00:00 2001 From: David Osorno Date: Wed, 7 Feb 2024 06:01:39 -0800 Subject: [PATCH 34/34] Rename variable to match security config pattern Signed-off-by: David Osorno --- public/apps/configuration/panels/auth-view/auth-view.tsx | 2 +- .../panels/auth-view/test/signin-options-modal.test.tsx | 5 ++--- public/types.ts | 2 +- public/utils/dashboards-info-utils.tsx | 2 +- server/auth/types/basic/routes.ts | 4 ++-- server/plugin.ts | 2 +- server/routes/index.ts | 4 ++-- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index aa811b0bf..b6d6f8fc2 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -43,7 +43,7 @@ export function AuthView(props: AppDependencies) { setAuthentication(config.authc); setAuthorization(config.authz); - setDashboardSignInOptions(config.kibana.dashboardSignInOptions); + setDashboardSignInOptions(config.kibana.sign_in_options); setAnonymousAuthEnable(config.http.anonymous_auth_enabled); } catch (e) { console.log(e); diff --git a/public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx b/public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx index a279cf374..899af910c 100644 --- a/public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx +++ b/public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx @@ -18,12 +18,11 @@ import { shallow } from 'enzyme'; import React from 'react'; import { DashboardSignInOptions } from '../../../types'; import { SignInOptionsModal } from '../signin-options-modal'; -import { OuiGlobalToastListToast } from '@opensearch-project/oui'; describe('Test sign-in options modal', () => { const initialValues = [ - { name: DashboardSignInOptions[DashboardSignInOptions.BASIC], status: true }, - { name: DashboardSignInOptions[DashboardSignInOptions.SAML], status: false }, + { name: DashboardSignInOptions[DashboardSignInOptions.BASIC], status: true, displayName: "Basic Authentication" }, + { name: DashboardSignInOptions[DashboardSignInOptions.SAML], status: false, displayName: "SAML" }, ]; const useEffectMock = jest.spyOn(React, 'useEffect'); diff --git a/public/types.ts b/public/types.ts index 9c884d823..d15af6d41 100644 --- a/public/types.ts +++ b/public/types.ts @@ -47,7 +47,7 @@ export interface DashboardsInfo { private_tenant_enabled?: boolean; default_tenant: string; password_validation_error_message: string; - dashboard_signin_options: []; + sign_in_options: []; } export interface ClientConfigType { diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx index b12fd6158..ea73a041b 100644 --- a/public/utils/dashboards-info-utils.tsx +++ b/public/utils/dashboards-info-utils.tsx @@ -36,6 +36,6 @@ export async function updateDashboardSignInOptions( signInOptions: DashboardSignInOptions[] ) { return await httpPut(http, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS, { - dashboard_signin_options: signInOptions, + sign_in_options: signInOptions, }); } diff --git a/server/auth/types/basic/routes.ts b/server/auth/types/basic/routes.ts index b42e5e6c6..3b567df9e 100755 --- a/server/auth/types/basic/routes.ts +++ b/server/auth/types/basic/routes.ts @@ -249,7 +249,7 @@ async function verifyAnonymousAccess(securityClient: SecurityClient, request: an const isAutologin = request.url.href.includes("anonymous?"); const dashboardsInfo = await securityClient.dashboardsinfo(request); - const isAnonymousEnabled = dashboardsInfo.dashboard_signin_options.includes(DashboardSignInOptions[DashboardSignInOptions.ANONYMOUS]); + const isAnonymousEnabled = dashboardsInfo.sign_in_options.includes(DashboardSignInOptions[DashboardSignInOptions.ANONYMOUS]); - return (isAnonymousEnabled && !isAutologin) || (isAnonymousEnabled && dashboardsInfo.dashboard_signin_options.length === 1); + return (isAnonymousEnabled && !isAutologin) || (isAnonymousEnabled && dashboardsInfo.sign_in_options.length === 1); } diff --git a/server/plugin.ts b/server/plugin.ts index 8085c359c..6e9f5a7ae 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -118,7 +118,7 @@ export class SecurityPlugin implements Plugin - data.dashboard_signin_options.map((opt: string) => { + data.sign_in_options.map((opt: string) => { if (DashboardSignInOptions[opt] === DashboardSignInOptions.BASIC) { return AuthType.BASIC; } else { diff --git a/server/routes/index.ts b/server/routes/index.ts index 6c40ced89..3ea21d9da 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -598,7 +598,7 @@ export function defineRoutes(router: IRouter) { try { esResp = await client.callAsInternalUser('opensearch_security.dashboardsinfo'); return response.ok({ - body: esResp.dashboard_signin_options, + body: esResp.sign_in_options, }); } catch (error) { return errorResponse(response, error); @@ -611,7 +611,7 @@ export function defineRoutes(router: IRouter) { path: `${API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS}`, validate: { body: schema.object({ - dashboard_signin_options: schema.arrayOf(schema.any(), { + sign_in_options: schema.arrayOf(schema.any(), { defaultValue: [DashboardSignInOptions.BASIC], }), }),