Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic tenancy configurations #1394

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
RyanL1997 marked this conversation as resolved.
Show resolved Hide resolved
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