Skip to content

Commit

Permalink
Saved Object Aggregation View (#1142)
Browse files Browse the repository at this point in the history
Co-authored-by: Craig Perkins <[email protected]>
Co-authored-by: Ryan Liang <[email protected]>
Co-authored-by: Yan Zeng <[email protected]>

Signed-off-by: Chang Liu <[email protected]>
  • Loading branch information
cliu123 committed Oct 17, 2022
1 parent 7b80f9b commit 89a6a57
Show file tree
Hide file tree
Showing 16 changed files with 367 additions and 30 deletions.
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": "3.0.0.0",
"opensearchDashboardsVersion": "3.0.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

0 comments on commit 89a6a57

Please sign in to comment.