Skip to content
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: 1 addition & 1 deletion opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "2.4.0.0",
"opensearchDashboardsVersion": "2.4.0",
"configPath": ["opensearch_security"],
"requiredPlugins": ["navigation"],
"requiredPlugins": ["navigation", "savedObjectsManagement"],
"server": true,
"ui": true
}
12 changes: 9 additions & 3 deletions public/apps/account/account-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { AccountNavButton } from './account-nav-button';
import { fetchAccountInfoSafe } from './utils';
import { ClientConfigType } from '../../types';
import { CUSTOM_ERROR_PAGE_URI, ERROR_MISSING_ROLE_PATH } from '../../../common';
import { selectTenant } from '../configuration/utils/tenant-utils';
import { fetchCurrentTenant, selectTenant } from '../configuration/utils/tenant-utils';
import {
getSavedTenant,
getShouldShowTenantPopup,
Expand All @@ -37,14 +37,20 @@ function tenantSpecifiedInUrl() {

export async function setupTopNavButton(coreStart: CoreStart, config: ClientConfigType) {
const accountInfo = (await fetchAccountInfoSafe(coreStart.http))?.data;
const currentTenant = await fetchCurrentTenant(coreStart.http);
if (accountInfo) {
// Missing role error
if (accountInfo.roles.length === 0 && !window.location.href.includes(CUSTOM_ERROR_PAGE_URI)) {
window.location.href =
coreStart.http.basePath.serverBasePath + CUSTOM_ERROR_PAGE_URI + ERROR_MISSING_ROLE_PATH;
}

let tenant = accountInfo.user_requested_tenant;
let tenant: string | undefined;
if (config.multitenancy.enable_aggregation_view) {
tenant = currentTenant;
} else {
tenant = accountInfo.user_requested_tenant;
}
let shouldShowTenantPopup = true;

if (tenantSpecifiedInUrl() || getShouldShowTenantPopup() === false) {
Expand All @@ -67,7 +73,7 @@ export async function setupTopNavButton(coreStart: CoreStart, config: ClientConf
window.location.reload();
}
}
} catch (e) {
} catch (e: any) {
constructErrorMessageAndLog(e, `Failed to switch to ${tenant} tenant.`);
}
}
Expand Down
3 changes: 2 additions & 1 deletion public/apps/account/account-nav-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ export function AccountNavButton(props: {
setModal(null);
window.location.reload();
}}
tenant={props.tenant!}
/>
),
[props.config, props.coreStart]
[props.config, props.coreStart, props.tenant]
);

// Check if the tenant modal should be shown on load
Expand Down
10 changes: 8 additions & 2 deletions public/apps/account/tenant-switch-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface TenantSwitchPanelProps {
handleClose: () => void;
handleSwitchAndClose: () => void;
config: ClientConfigType;
tenant: string;
}

const GLOBAL_TENANT_KEY_NAME = 'global_tenant';
Expand Down Expand Up @@ -91,7 +92,12 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) {
setUsername(currentUserName);

// @ts-ignore
const currentRawTenantName = accountInfo.data.user_requested_tenant;
let currentRawTenantName: string | undefined;
if (props.config.multitenancy.enable_aggregation_view) {
currentRawTenantName = props.tenant;
} else {
currentRawTenantName = accountInfo.data.user_requested_tenant;
}
setCurrentTenant(currentRawTenantName || '', currentUserName);
} catch (e) {
// TODO: switch to better error display.
Expand All @@ -100,7 +106,7 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) {
};

fetchData();
}, [props.coreStart.http]);
}, [props.coreStart.http, props.tenant, props.config.multitenancy.enable_aggregation_view]);

// Custom tenant super select related.
const onCustomTenantChange = (selectedOption: EuiComboBoxOptionOption[]) => {
Expand Down
18 changes: 14 additions & 4 deletions public/apps/account/test/account-app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
getSavedTenant,
} from '../../../utils/storage-utils';
import { fetchAccountInfoSafe } from '../utils';
import { selectTenant } from '../../configuration/utils/tenant-utils';
import { fetchCurrentTenant, selectTenant } from '../../configuration/utils/tenant-utils';

jest.mock('../../../utils/storage-utils', () => ({
getShouldShowTenantPopup: jest.fn(),
Expand All @@ -36,6 +36,7 @@ jest.mock('../utils', () => ({

jest.mock('../../configuration/utils/tenant-utils', () => ({
selectTenant: jest.fn(),
fetchCurrentTenant: jest.fn(),
}));

describe('Account app', () => {
Expand All @@ -47,6 +48,12 @@ describe('Account app', () => {
},
};

const mockConfig = {
multitenancy: {
enable_aggregation_view: true,
},
};

const mockAccountInfo = {
data: {
roles: {
Expand All @@ -55,8 +62,11 @@ describe('Account app', () => {
},
};

const mockTenant = 'test1';

beforeAll(() => {
(fetchAccountInfoSafe as jest.Mock).mockResolvedValue(mockAccountInfo);
(fetchCurrentTenant as jest.Mock).mockResolvedValue(mockTenant);
});

it('Should skip if auto swich if securitytenant in url', (done) => {
Expand All @@ -65,7 +75,7 @@ describe('Account app', () => {
delete window.location;
window.location = new URL('http://www.example.com?securitytenant=abc') as any;

setupTopNavButton(mockCoreStart, {} as any);
setupTopNavButton(mockCoreStart, mockConfig as any);

process.nextTick(() => {
expect(setShouldShowTenantPopup).toBeCalledWith(false);
Expand All @@ -77,7 +87,7 @@ describe('Account app', () => {
it('Should switch to saved tenant when securitytenant not in url', (done) => {
(getSavedTenant as jest.Mock).mockReturnValueOnce('tenant1');

setupTopNavButton(mockCoreStart, {} as any);
setupTopNavButton(mockCoreStart, mockConfig as any);

process.nextTick(() => {
expect(getSavedTenant).toBeCalledTimes(1);
Expand All @@ -92,7 +102,7 @@ describe('Account app', () => {
it('Should show tenant selection popup when neither securitytenant in url nor saved tenant', (done) => {
(getSavedTenant as jest.Mock).mockReturnValueOnce(null);

setupTopNavButton(mockCoreStart, {} as any);
setupTopNavButton(mockCoreStart, mockConfig as any);

process.nextTick(() => {
expect(getSavedTenant).toBeCalledTimes(1);
Expand Down
4 changes: 2 additions & 2 deletions public/apps/configuration/configuration-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@osd/i18n/react';
import { AppMountParameters, CoreStart } from '../../../../../src/core/public';
import { AppPluginStartDependencies, ClientConfigType } from '../../types';
import { SecurityPluginStartDependencies, ClientConfigType } from '../../types';
import { AppRouter } from './app-router';

export function renderApp(
coreStart: CoreStart,
navigation: AppPluginStartDependencies,
navigation: SecurityPluginStartDependencies,
params: AppMountParameters,
config: ClientConfigType
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function ClusterPermissionPanel(props: {
options={optionUniverse}
selectedOptions={state}
onChange={setState}
id="cluster-permission-box"
/>
</EuiFlexItem>
{/* TODO: 'Browse and select' button with a pop-up modal for selection */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,16 @@ export function IndexPatternRow(props: {
}) {
return (
<FormRow headerText="Index" helpText="Specify index pattern using *">
<EuiComboBox
noSuggestions
placeholder="Search for index name or type in index pattern"
selectedOptions={props.value}
onChange={props.onChangeHandler}
onCreateOption={props.onCreateHandler}
/>
<EuiFlexItem className={LIMIT_WIDTH_INPUT_CLASS}>
<EuiComboBox
noSuggestions
placeholder="Search for index name or type in index pattern"
selectedOptions={props.value}
onChange={props.onChangeHandler}
onCreateOption={props.onCreateHandler}
id="index-input-box"
/>
</EuiFlexItem>
</FormRow>
);
}
Expand All @@ -150,6 +153,7 @@ export function IndexPermissionRow(props: {
options={props.permisionOptionsSet}
selectedOptions={props.value}
onChange={props.onChangeHandler}
id="index-permission-box"
/>
</EuiFlexItem>
{/* TODO: 'Browse and select' button with a pop-up modal for selection */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ function generateTenantPermissionPanels(
onChange={onValueChangeHandler('tenantPatterns')}
onCreateOption={onCreateOptionHandler('tenantPatterns')}
options={permisionOptionsSet}
id="tenant-permission-box"
/>
</EuiFlexItem>
<EuiFlexItem style={{ maxWidth: '170px' }}>
Expand Down
4 changes: 2 additions & 2 deletions public/apps/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
*/

import { AppMountParameters, CoreStart } from '../../../../src/core/public';
import { AppPluginStartDependencies, ClientConfigType } from '../types';
import { SecurityPluginStartDependencies, ClientConfigType } from '../types';

export interface AppDependencies {
coreStart: CoreStart;
navigation: AppPluginStartDependencies;
navigation: SecurityPluginStartDependencies;
params: AppMountParameters;
config: ClientConfigType;
}
Expand Down
58 changes: 53 additions & 5 deletions public/plugin.ts → public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
*/

import { BehaviorSubject } from 'rxjs';
import {
SavedObjectsManagementColumn,
SavedObjectsManagementRecord,
} from 'src/plugins/saved_objects_management/public';
import { EuiTableFieldDataColumnType } from '@elastic/eui';
import { string } from 'joi';
import React from 'react';
import { i18n } from '@osd/i18n';
import {
AppMountParameters,
AppStatus,
Expand All @@ -37,10 +45,11 @@ import {
excludeFromDisabledTransportCategories,
} from './apps/configuration/panels/audit-logging/constants';
import {
AppPluginStartDependencies,
SecurityPluginStartDependencies,
ClientConfigType,
SecurityPluginSetup,
SecurityPluginStart,
SecurityPluginSetupDependencies,
} from './types';
import { addTenantToShareURL } from './services/shared-link';
import { interceptError } from './utils/logout-utils';
Expand All @@ -63,11 +72,21 @@ const APP_ID_DASHBOARDS = 'dashboards';
const APP_ID_OPENSEARCH_DASHBOARDS = 'kibana';
const APP_LIST_FOR_READONLY_ROLE = [APP_ID_HOME, APP_ID_DASHBOARDS, APP_ID_OPENSEARCH_DASHBOARDS];

export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPluginStart> {
export class SecurityPlugin
implements
Plugin<
SecurityPluginSetup,
SecurityPluginStart,
SecurityPluginSetupDependencies,
SecurityPluginStartDependencies
> {
// @ts-ignore : initializerContext not used
constructor(private readonly initializerContext: PluginInitializerContext) {}

public async setup(core: CoreSetup): Promise<SecurityPluginSetup> {
public async setup(
core: CoreSetup,
deps: SecurityPluginSetupDependencies
): Promise<SecurityPluginSetup> {
const apiPermission = await hasApiPermission(core);

const config = this.initializerContext.config.get<ClientConfigType>();
Expand All @@ -93,7 +112,7 @@ export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPlugi
excludeFromDisabledTransportCategories(config.disabledTransportCategories.exclude);
excludeFromDisabledRestCategories(config.disabledRestCategories.exclude);

return renderApp(coreStart, depsStart as AppPluginStartDependencies, params, config);
return renderApp(coreStart, depsStart as SecurityPluginStartDependencies, params, config);
},
category: {
id: 'opensearch',
Expand Down Expand Up @@ -138,11 +157,35 @@ export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPlugi
})
);

if (config.multitenancy.enable_aggregation_view) {
deps.savedObjectsManagement.columns.register(({
id: 'tenant_column',
euiColumn: {
field: 'namespaces',
name: <div>Tenant</div>,
dataType: 'string',
render: (value: any[][]) => {
let text = value[0][0];
if (text === null || text === '') {
text = 'Global';
} else if (text.startsWith('__user__')) {
text = 'Private';
}
text = i18n.translate('savedObjectsManagement.objectsTable.table.columnTenantName', {
defaultMessage: text,
});
return <div>{text}</div>;
},
},
loadData: () => {},
} as unknown) as SavedObjectsManagementColumn<string>);
}

// Return methods that should be available to other plugins
return {};
}

public start(core: CoreStart): SecurityPluginStart {
public start(core: CoreStart, deps: SecurityPluginStartDependencies): SecurityPluginStart {
const config = this.initializerContext.config.get<ClientConfigType>();

setupTopNavButton(core, config);
Expand All @@ -157,6 +200,11 @@ export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPlugi
if (config.multitenancy.enabled) {
addTenantToShareURL(core);
}

if (config.multitenancy.enable_aggregation_view) {
const columns = deps.savedObjectsManagement.columns.getAll();
}

return {};
}

Expand Down
12 changes: 11 additions & 1 deletion public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@
*/

import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public';
import {
SavedObjectsManagementPluginSetup,
SavedObjectsManagementPluginStart,
} from '../../../src/plugins/saved_objects_management/public';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SecurityPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SecurityPluginStart {}

export interface AppPluginStartDependencies {
export interface SecurityPluginSetupDependencies {
savedObjectsManagement: SavedObjectsManagementPluginSetup;
}

export interface SecurityPluginStartDependencies {
navigation: NavigationPublicPluginStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
}

export interface AuthInfo {
Expand Down Expand Up @@ -49,6 +58,7 @@ export interface ClientConfigType {
backend_configurable: boolean;
};
multitenancy: {
enable_aggregation_view: boolean;
enabled: boolean;
tenants: {
enable_private: boolean;
Expand Down
Loading