Skip to content

Commit

Permalink
Dynamic tenancy configurations (#1394) (#1404)
Browse files Browse the repository at this point in the history
* Dynamic multitenancy feature.

Signed-off-by: Abhi Kalra <[email protected]>
(cherry picked from commit dc3c745)
  • Loading branch information
opensearch-trigger-bot[bot] authored Apr 18, 2023
1 parent b1fc467 commit 3cff125
Show file tree
Hide file tree
Showing 38 changed files with 2,046 additions and 497 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/cypress-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,5 @@ jobs:
cd opensearch-dashboards-functional-test
npm install cypress --save-dev
yarn cypress:run-with-security-and-aggregation-view --browser chrome --spec "cypress/integration/plugins/security-dashboards-plugin/aggregation_view.js"
yarn cypress:run-with-security --browser chrome --spec "cypress/integration/plugins/security-dashboards-plugin/multi_tenancy.js"
yarn cypress:run-with-security --browser chrome --spec "cypress/integration/plugins/security-dashboards-plugin/default_tenant.js"
1 change: 1 addition & 0 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const OPENDISTRO_SECURITY_ANONYMOUS = 'opendistro_security_anonymous';
export const API_PREFIX = '/api/v1';
export const CONFIGURATION_API_PREFIX = 'configuration';
export const API_ENDPOINT_AUTHINFO = API_PREFIX + '/auth/authinfo';
export const API_ENDPOINT_DASHBOARDSINFO = API_PREFIX + '/auth/dashboardsinfo';
export const API_ENDPOINT_AUTHTYPE = API_PREFIX + '/auth/type';
export const LOGIN_PAGE_URI = '/app/' + APP_ID_LOGIN;
export const CUSTOM_ERROR_PAGE_URI = '/app/' + APP_ID_CUSTOMERROR;
Expand Down
30 changes: 25 additions & 5 deletions public/apps/account/account-nav-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ClientConfigType } from '../../types';
import { LogoutButton } from './log-out-button';
import { resolveTenantName } from '../configuration/utils/tenant-utils';
import { getShouldShowTenantPopup, setShouldShowTenantPopup } from '../../utils/storage-utils';
import { getDashboardsInfo } from '../../utils/dashboards-info-utils';

export function AccountNavButton(props: {
coreStart: CoreStart;
Expand All @@ -48,6 +49,7 @@ export function AccountNavButton(props: {
const [modal, setModal] = React.useState<React.ReactNode>(null);
const horizontalRule = <EuiHorizontalRule margin="xs" />;
const username = props.username;
const [isMultiTenancyEnabled, setIsMultiTenancyEnabled] = React.useState<boolean>(true);

const showTenantSwitchPanel = useCallback(
() =>
Expand All @@ -67,9 +69,23 @@ export function AccountNavButton(props: {
),
[props.config, props.coreStart, props.tenant]
);
React.useEffect(() => {
const fetchData = async () => {
try {
setIsMultiTenancyEnabled(
(await getDashboardsInfo(props.coreStart.http)).multitenancy_enabled
);
} catch (e) {
// TODO: switch to better error display.
console.error(e);
}
};

fetchData();
}, [props.coreStart.http]);

// Check if the tenant modal should be shown on load
if (props.config.multitenancy.enabled && getShouldShowTenantPopup()) {
if (isMultiTenancyEnabled && getShouldShowTenantPopup()) {
setShouldShowTenantPopup(false);
showTenantSwitchPanel();
}
Expand Down Expand Up @@ -112,10 +128,14 @@ export function AccountNavButton(props: {
>
View roles and identities
</EuiButtonEmpty>
{horizontalRule}
<EuiButtonEmpty data-test-subj="switch-tenants" size="xs" onClick={showTenantSwitchPanel}>
Switch tenants
</EuiButtonEmpty>
{isMultiTenancyEnabled && (
<>
{horizontalRule}
<EuiButtonEmpty data-test-subj="switch-tenants" size="xs" onClick={showTenantSwitchPanel}>
Switch tenants
</EuiButtonEmpty>
</>
)}
{props.isInternalUser && (
<>
{horizontalRule}
Expand Down
18 changes: 10 additions & 8 deletions public/apps/account/tenant-switch-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiCheckbox,
EuiComboBox,
EuiComboBoxOptionOption,
EuiModal,
Expand All @@ -31,7 +30,7 @@ import {
} from '@elastic/eui';
import { CoreStart } from 'opensearch-dashboards/public';
import { keys } from 'lodash';
import React from 'react';
import React, { useState } from 'react';
import { ClientConfigType } from '../../types';
import {
RESOLVED_GLOBAL_TENANT,
Expand All @@ -42,6 +41,7 @@ import {
import { fetchAccountInfo } from './utils';
import { constructErrorMessageAndLog } from '../error-utils';
import { getSavedTenant, setSavedTenant } from '../../utils/storage-utils';
import { getDashboardsInfo } from '../../utils/dashboards-info-utils';

interface TenantSwitchPanelProps {
coreStart: CoreStart;
Expand All @@ -65,6 +65,10 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) {
const [selectedCustomTenantOption, setSelectedCustomTenantOption] = React.useState<
EuiComboBoxOptionOption[]
>([]);
const [isPrivateEnabled, setIsPrivateEnabled] = useState(
props.config.multitenancy.tenants.enable_private
);
const [isMultiTenancyEnabled, setIsMultiTenancyEnabled] = useState(true);

const setCurrentTenant = (currentRawTenantName: string, currentUserName: string) => {
const resolvedTenantName = resolveTenantName(currentRawTenantName, currentUserName);
Expand All @@ -84,7 +88,9 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) {
try {
const accountInfo = await fetchAccountInfo(props.coreStart.http);
setRoles(accountInfo.data.roles);

const dashboardsInfo = await getDashboardsInfo(props.coreStart.http);
setIsMultiTenancyEnabled(dashboardsInfo.multitenancy_enabled);
setIsPrivateEnabled(dashboardsInfo.private_tenant_enabled);
const tenantsInfo = accountInfo.data.tenants || {};
setTenants(keys(tenantsInfo));

Expand Down Expand Up @@ -122,16 +128,12 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) {
label: option,
}));

const isMultiTenancyEnabled = props.config.multitenancy.enabled;
const isGlobalEnabled = props.config.multitenancy.tenants.enable_global;
const isPrivateEnabled = props.config.multitenancy.tenants.enable_private;

const DEFAULT_READONLY_ROLES = ['kibana_read_only'];
const readonly = roles.some(
(role) =>
props.config.readonly_mode?.roles.includes(role) || DEFAULT_READONLY_ROLES.includes(role)
);

const isGlobalEnabled = props.config.multitenancy.tenants.enable_global;
const shouldDisableGlobal = !isGlobalEnabled || !tenants.includes(GLOBAL_TENANT_KEY_NAME);
const getGlobalDisabledInstruction = () => {
if (!isGlobalEnabled) {
Expand Down
31 changes: 30 additions & 1 deletion public/apps/account/test/account-nav-button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,25 @@ import { shallow } from 'enzyme';
import React from 'react';
import { AccountNavButton } from '../account-nav-button';
import { getShouldShowTenantPopup, setShouldShowTenantPopup } from '../../../utils/storage-utils';
import { getDashboardsInfo } from '../../../utils/dashboards-info-utils';

jest.mock('../../../utils/storage-utils', () => ({
getShouldShowTenantPopup: jest.fn(),
setShouldShowTenantPopup: jest.fn(),
}));

jest.mock('../../../utils/dashboards-info-utils', () => ({
getDashboardsInfo: jest.fn().mockImplementation(() => {
return mockDashboardsInfo;
}),
}));

const mockDashboardsInfo = {
multitenancy_enabled: true,
private_tenant_enabled: true,
default_tenant: '',
};

describe('Account navigation button', () => {
const mockCoreStart = {
http: 1,
Expand All @@ -49,6 +62,9 @@ describe('Account navigation button', () => {

beforeEach(() => {
useStateSpy.mockImplementation((init) => [init, setState]);
(getDashboardsInfo as jest.Mock).mockImplementation(() => {
return mockDashboardsInfo;
});
component = shallow(
<AccountNavButton
coreStart={mockCoreStart}
Expand All @@ -66,10 +82,16 @@ describe('Account navigation button', () => {
});

it('renders', () => {
(getDashboardsInfo as jest.Mock).mockImplementationOnce(() => {
return mockDashboardsInfo;
});
expect(component).toMatchSnapshot();
});

it('should set modal when show popup is true', () => {
(getDashboardsInfo as jest.Mock).mockImplementation(() => {
return mockDashboardsInfo;
});
(getShouldShowTenantPopup as jest.Mock).mockReturnValueOnce(true);
shallow(
<AccountNavButton
Expand Down Expand Up @@ -132,6 +154,13 @@ describe('Account navigation button, multitenancy disabled', () => {
});

it('should not set modal when show popup is true', () => {
(getDashboardsInfo as jest.Mock).mockImplementation(() => {
return {
multitenancy_enabled: false,
private_tenant_enabled: false,
default_tenant: '',
};
});
(getShouldShowTenantPopup as jest.Mock).mockReturnValueOnce(true);
shallow(
<AccountNavButton
Expand All @@ -142,6 +171,6 @@ describe('Account navigation button, multitenancy disabled', () => {
currAuthType={'dummy'}
/>
);
expect(setState).toBeCalledTimes(0);
expect(setState).toBeCalledTimes(1);
});
});
Loading

0 comments on commit 3cff125

Please sign in to comment.