([]);
+ const [disableUpdate, disableUpdateButton] = React.useState(false);
+ const actualSignInOptions: DashboardOption[] = props.dashboardOptions.filter((opt) => opt.status);
+
+ const [isModalVisible, setIsModalVisible] = React.useState(false);
+ const closeModal = () => setIsModalVisible(false);
+ const showModal = () => setIsModalVisible(true);
+
+ React.useEffect(() => {
+ if (actualSignInOptions.length !== newSignInOptions.length && newSignInOptions.length > 0) {
+ disableUpdateButton(false);
+ } else {
+ let sameOptions = true;
+ newSignInOptions.forEach((option) => {
+ if (actualSignInOptions.includes(option) === false) {
+ sameOptions = false;
+ return;
+ }
+ });
+ disableUpdateButton(sameOptions);
+ }
+ }, [newSignInOptions, actualSignInOptions]);
+
+ let modal;
+
+ if (isModalVisible) {
+ modal = (
+
+
+ Dashboards sign-in options
+
+
+ Select one or multiple authentication options to appear on the sign-in window for
+ OpenSearch Dashboards.
+
+
+
+
+ Cancel
+ {
+ props.handleUpdate(newSignInOptions);
+ closeModal();
+ }}
+ fill
+ disabled={disableUpdate}
+ >
+ Update
+
+
+
+ );
+ }
+ return (
+
+
+ Edit
+
+ {modal}
+
+ );
+}
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..899af910c
--- /dev/null
+++ b/public/apps/configuration/panels/auth-view/test/signin-options-modal.test.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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';
+
+describe('Test sign-in options modal', () => {
+ const initialValues = [
+ { name: DashboardSignInOptions[DashboardSignInOptions.BASIC], status: true, displayName: "Basic Authentication" },
+ { name: DashboardSignInOptions[DashboardSignInOptions.SAML], status: false, displayName: "SAML" },
+ ];
+
+ 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);
+ });
+ });
+});
diff --git a/public/apps/configuration/types.ts b/public/apps/configuration/types.ts
index 967072d18..eb753a85a 100644
--- a/public/apps/configuration/types.ts
+++ b/public/apps/configuration/types.ts
@@ -149,3 +149,16 @@ export interface FormRowDeps {
helpLink?: string;
helpText?: string;
}
+
+export enum DashboardSignInOptions {
+ BASIC,
+ SAML,
+ OPENID,
+ ANONYMOUS,
+}
+
+export interface DashboardOption {
+ name: DashboardSignInOptions | string;
+ status: boolean;
+ displayName: string;
+}
diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx
index a22a36dc7..2edd40483 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,8 @@ import {
OPENID_AUTH_LOGIN_WITH_FRAGMENT,
SAML_AUTH_LOGIN_WITH_FRAGMENT,
} from '../../../common';
+import { getDashboardsSignInOptions } from '../../utils/dashboards-info-utils';
+import { DashboardSignInOptions } from '../configuration/types';
import { getSavedTenant } from '../../utils/storage-utils';
interface LoginPageDeps {
@@ -100,6 +102,29 @@ export function LoginPage(props: LoginPageDeps) {
const [loginError, setloginError] = useState('');
const [usernameValidationFailed, setUsernameValidationFailed] = useState(false);
const [passwordValidationFailed, setPasswordValidationFailed] = useState(false);
+ const [signInOptions, setSignInOptions] = React.useState([]);
+
+ // 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();
+ }
+ };
+
+ React.useEffect(() => {
+ const getSignInOptions = async () => {
+ try {
+ const dashboardSignInOptions = await getDashboardsSignInOptions(props.http);
+ setSignInOptions(dashboardSignInOptions);
+ } catch (e) {
+ console.error(`Unable to get sign in options ${e}`);
+ }
+ };
+
+ getSignInOptions();
+ }, [props.http]);
let errorLabel: any = null;
if (loginFailed) {
@@ -130,6 +155,8 @@ export function LoginPage(props: LoginPageDeps) {
return;
}
+ await reValidateSignInOption(DashboardSignInOptions.BASIC);
+
try {
await validateCurrentPassword(props.http, username, password);
redirect(props.http.basePath.serverBasePath);
@@ -156,6 +183,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,22 +195,40 @@ export function LoginPage(props: LoginPageDeps) {
);
};
+ const mapSignInOptions = (options: DashboardSignInOptions[]) => {
+ const authOpts = [];
+ for (let i = 0; i < options.length; i++) {
+ // 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 {
+ const authType = AuthType[options[i]];
+ if (authType) {
+ authOpts.push(authType);
+ }
+ }
+ }
+ return authOpts;
+ };
+
const formOptions = (options: string | string[]) => {
let formBody = [];
const formBodyOp = [];
- let authOpts = [];
+ let authOpts = mapSignInOptions(signInOptions);
- 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);
+ if (authOpts.length === 0) {
+ if (typeof options === 'string') {
+ if (options === '') {
+ authOpts.push(AuthType.BASIC);
+ } else {
+ authOpts.push(options.toLowerCase());
+ }
} else {
- authOpts = [...options];
+ if (options && options.length === 1 && options[0] === '') {
+ authOpts.push(AuthType.BASIC);
+ } else {
+ authOpts = [...options];
+ }
}
}
@@ -191,6 +239,7 @@ export function LoginPage(props: LoginPageDeps) {
}
@@ -204,6 +253,7 @@ export function LoginPage(props: LoginPageDeps) {
}
@@ -218,6 +268,7 @@ export function LoginPage(props: LoginPageDeps) {
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: {
@@ -259,6 +297,13 @@ export function LoginPage(props: LoginPageDeps) {
formBodyOp.push(renderLoginButton(AuthType.SAML, samlAuthLoginUrl, samlConfig));
break;
}
+ case AuthType.ANONYMOUS: {
+ const anonymousConfig = props.config.ui[AuthType.ANONYMOUS].login;
+ formBody.push(
+ renderLoginButton(AuthType.ANONYMOUS, ANONYMOUS_AUTH_LOGIN, anonymousConfig)
+ );
+ break;
+ }
default: {
setloginFailed(true);
setloginError(
@@ -269,6 +314,12 @@ export function LoginPage(props: LoginPageDeps) {
}
}
+ if (authOpts.length > 1) {
+ formBody.push();
+ formBody.push();
+ formBody.push();
+ }
+
formBody = formBody.concat(formBodyOp);
return formBody;
};
diff --git a/public/types.ts b/public/types.ts
index 4acfc442f..d15af6d41 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;
+ sign_in_options: [];
}
export interface ClientConfigType {
diff --git a/public/utils/dashboards-info-utils.tsx b/public/utils/dashboards-info-utils.tsx
index eeb76a345..ea73a041b 100644
--- a/public/utils/dashboards-info-utils.tsx
+++ b/public/utils/dashboards-info-utils.tsx
@@ -14,9 +14,10 @@
*/
import { HttpStart } from 'opensearch-dashboards/public';
-import { API_ENDPOINT_DASHBOARDSINFO } from '../../common';
-import { httpGet, httpGetWithIgnores } from '../apps/configuration/utils/request-utils';
+import { API_ENDPOINT_DASHBOARDSINFO, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS } from '../../common';
+import { httpGet, httpGetWithIgnores, httpPut } from '../apps/configuration/utils/request-utils';
import { DashboardsInfo } from '../types';
+import { DashboardSignInOptions } from '../apps/configuration/types';
export async function getDashboardsInfo(http: HttpStart) {
return await httpGet(http, API_ENDPOINT_DASHBOARDSINFO);
@@ -25,3 +26,16 @@ 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);
+}
+
+export async function updateDashboardSignInOptions(
+ http: HttpStart,
+ signInOptions: DashboardSignInOptions[]
+) {
+ return await httpPut(http, API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS, {
+ sign_in_options: signInOptions,
+ });
+}
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(
diff --git a/server/auth/types/basic/routes.ts b/server/auth/types/basic/routes.ts
index bae3e338c..3b567df9e 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,
- availableTenants: 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,
+ availableTenants: 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.sign_in_options.includes(DashboardSignInOptions[DashboardSignInOptions.ANONYMOUS]);
+
+ return (isAnonymousEnabled && !isAutologin) || (isAnonymousEnabled && dashboardsInfo.sign_in_options.length === 1);
+}
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: '' }),
diff --git a/server/plugin.ts b/server/plugin.ts
index 5f5f50913..6e9f5a7ae 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,24 @@ export class SecurityPlugin implements Plugin
+ data.sign_in_options.map((opt: string) => {
+ if (DashboardSignInOptions[opt] === DashboardSignInOptions.BASIC) {
+ return AuthType.BASIC;
+ } else {
+ return opt.toString().toLowerCase();
+ }
+ })
+ );
+
+ // Combine sign in options with auth.type in case there are JWT, PROXY or more auth types.
+ dashboardSignInOptions = new Set([...dashboardSignInOptions, ...config.auth.type]);
+ dashboardSignInOptions = [...dashboardSignInOptions];
+
const auth: IAuthenticationType = await getAuthenticationHandler(
- config.auth.type,
+ dashboardSignInOptions,
router,
config,
core,
diff --git a/server/routes/index.ts b/server/routes/index.ts
index 934c63da7..3ea21d9da 100644
--- a/server/routes/index.ts
+++ b/server/routes/index.ts
@@ -20,7 +20,13 @@ import {
IOpenSearchDashboardsResponse,
OpenSearchDashboardsResponseFactory,
} from 'opensearch-dashboards/server';
-import { API_PREFIX, CONFIGURATION_API_PREFIX, isValidResourceName } from '../../common';
+import {
+ API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS,
+ API_PREFIX,
+ CONFIGURATION_API_PREFIX,
+ isValidResourceName,
+} from '../../common';
+import { DashboardSignInOptions } from '../../public/apps/configuration/types';
import { ResourceType } from '../../common';
// TODO: consider to extract entity CRUD operations and put it into a client class
@@ -574,6 +580,65 @@ export function defineRoutes(router: IRouter) {
}
);
+ router.get(
+ {
+ path: `${API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS}`,
+ 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.sign_in_options,
+ });
+ } catch (error) {
+ return errorResponse(response, error);
+ }
+ }
+ );
+
+ router.put(
+ {
+ path: `${API_ENDPOINT_DASHBOARD_SIGNIN_OPTIONS}`,
+ validate: {
+ body: schema.object({
+ sign_in_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。
*
diff --git a/test/cypress/e2e/dashboard-signin-options.cy.spec.js b/test/cypress/e2e/dashboard-signin-options.cy.spec.js
new file mode 100644
index 000000000..cfd7eb7dc
--- /dev/null
+++ b/test/cypress/e2e/dashboard-signin-options.cy.spec.js
@@ -0,0 +1,95 @@
+/*
+ * 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 { ADMIN_AUTH } from '../support/constants';
+
+function login() {
+ cy.visit('http://localhost:5601/app/login?nextUrl=%2Fapp%2Fsecurity-dashboards-plugin#/auth');
+ cy.get('[data-testid="username"]').type(ADMIN_AUTH.username);
+ cy.get('[data-testid="password"]').type(ADMIN_AUTH.password);
+ cy.get('[data-testid="login"]').click();
+ if (cy.contains('Cancel')) {
+ cy.contains('Cancel').click();
+ }
+}
+
+function logout() {
+ cy.get('.euiAvatar').click();
+ cy.contains('Log out').click();
+}
+
+// OpenSearch backend must have BASIC and SAML authentication options for tests to work.
+describe('Testing Dashboard SignIn Options', () => {
+ 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');
+ });
+});
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..9d5e10b75
--- /dev/null
+++ b/test/jest_integration/get-dashboard-signin-options.test.ts
@@ -0,0 +1,76 @@
+/*
+ * 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);
+ });
+});