From 0ed2cf2ad6c4e27a28a7edadbe12d643a251331e Mon Sep 17 00:00:00 2001 From: Derek Ho Date: Thu, 25 Jul 2024 15:03:41 -0400 Subject: [PATCH] Do not register tenancy app if disabled in yml (#2057) * Do not register tenancy app if disabled in yml Signed-off-by: Derek Ho * Adds a test Signed-off-by: Derek Ho * Revert type and export Signed-off-by: Derek Ho * Use constants Signed-off-by: Derek Ho * Remove extra license field Signed-off-by: Derek Ho * Refactor with constants Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho --- common/index.ts | 7 ++ public/plugin.ts | 62 ++++++++----- public/test/plugin.test.ts | 178 +++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 24 deletions(-) create mode 100644 public/test/plugin.test.ts diff --git a/common/index.ts b/common/index.ts index 23ad3e1b6..b33c099a7 100644 --- a/common/index.ts +++ b/common/index.ts @@ -15,6 +15,13 @@ export const PLUGIN_ID = 'opensearchDashboardsSecurity'; export const PLUGIN_NAME = 'security-dashboards-plugin'; +export const PLUGIN_GET_STARTED_APP_ID = `${PLUGIN_NAME}_getstarted`; +export const PLUGIN_AUTH_APP_ID = `${PLUGIN_NAME}_auth`; +export const PLUGIN_ROLES_APP_ID = `${PLUGIN_NAME}_roles`; +export const PLUGIN_USERS_APP_ID = `${PLUGIN_NAME}_users`; +export const PLUGIN_PERMISSIONS_APP_ID = `${PLUGIN_NAME}_permissions`; +export const PLUGIN_TENANTS_APP_ID = `${PLUGIN_NAME}_tenants`; +export const PLUGIN_AUDITLOG_APP_ID = `${PLUGIN_NAME}_auditlog`; export const APP_ID_LOGIN = 'login'; export const APP_ID_CUSTOMERROR = 'customerror'; diff --git a/public/plugin.ts b/public/plugin.ts index fb71c9c88..5eec1a034 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -30,7 +30,19 @@ import { PluginInitializerContext, WorkspaceAvailability, } from '../../../src/core/public'; -import { APP_ID_LOGIN, CUSTOM_ERROR_PAGE_URI, LOGIN_PAGE_URI, PLUGIN_NAME } from '../common'; +import { + APP_ID_LOGIN, + CUSTOM_ERROR_PAGE_URI, + LOGIN_PAGE_URI, + PLUGIN_AUDITLOG_APP_ID, + PLUGIN_AUTH_APP_ID, + PLUGIN_GET_STARTED_APP_ID, + PLUGIN_NAME, + PLUGIN_PERMISSIONS_APP_ID, + PLUGIN_ROLES_APP_ID, + PLUGIN_TENANTS_APP_ID, + PLUGIN_USERS_APP_ID, +} from '../common'; import { APP_ID_CUSTOMERROR } from '../common'; import { setupTopNavButton } from './apps/account/account-app'; import { fetchAccountInfoSafe } from './apps/account/utils'; @@ -175,7 +187,7 @@ export class SecurityPlugin if (core.chrome.navGroup.getNavGroupEnabled()) { core.application.register({ - id: `security-dashboards-plugin_getstarted`, + id: PLUGIN_GET_STARTED_APP_ID, title: 'Get Started', order: 8040, workspaceAvailability: WorkspaceAvailability.outsideWorkspace, @@ -185,7 +197,7 @@ export class SecurityPlugin }, }); core.application.register({ - id: `security-dashboards-plugin_auth`, + id: PLUGIN_AUTH_APP_ID, title: 'Authentication', order: 8040, workspaceAvailability: WorkspaceAvailability.outsideWorkspace, @@ -195,7 +207,7 @@ export class SecurityPlugin }, }); core.application.register({ - id: `security-dashboards-plugin_roles`, + id: PLUGIN_ROLES_APP_ID, title: 'Roles', order: 8040, workspaceAvailability: WorkspaceAvailability.outsideWorkspace, @@ -205,7 +217,7 @@ export class SecurityPlugin }, }); core.application.register({ - id: `security-dashboards-plugin_users`, + id: PLUGIN_USERS_APP_ID, title: 'Internal users', order: 8040, workspaceAvailability: WorkspaceAvailability.outsideWorkspace, @@ -215,7 +227,7 @@ export class SecurityPlugin }, }); core.application.register({ - id: `security-dashboards-plugin_permissions`, + id: PLUGIN_PERMISSIONS_APP_ID, title: 'Permissions', order: 8040, workspaceAvailability: WorkspaceAvailability.outsideWorkspace, @@ -224,18 +236,20 @@ export class SecurityPlugin return mountWrapper(params, '/permissions'); }, }); + if (config.multitenancy.enabled) { + core.application.register({ + id: PLUGIN_TENANTS_APP_ID, + title: 'Tenants', + order: 8040, + workspaceAvailability: WorkspaceAvailability.outsideWorkspace, + updater$: this.appStateUpdater, + mount: async (params: AppMountParameters) => { + return mountWrapper(params, '/tenants'); + }, + }); + } core.application.register({ - id: `security-dashboards-plugin_tenants`, - title: 'Tenants', - order: 8040, - workspaceAvailability: WorkspaceAvailability.outsideWorkspace, - updater$: this.appStateUpdater, - mount: async (params: AppMountParameters) => { - return mountWrapper(params, '/tenants'); - }, - }); - core.application.register({ - id: `security-dashboards-plugin_auditlog`, + id: PLUGIN_AUDITLOG_APP_ID, title: 'Audit logs', order: 8040, workspaceAvailability: WorkspaceAvailability.outsideWorkspace, @@ -248,31 +262,31 @@ export class SecurityPlugin core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.dataAdministration, [ { - id: `security-dashboards-plugin_getstarted`, + id: PLUGIN_GET_STARTED_APP_ID, category: dataAccessUsersCategory, }, { - id: `security-dashboards-plugin_auth`, + id: PLUGIN_AUTH_APP_ID, category: dataAccessUsersCategory, }, { - id: `security-dashboards-plugin_roles`, + id: PLUGIN_ROLES_APP_ID, category: dataAccessUsersCategory, }, { - id: `security-dashboards-plugin_users`, + id: PLUGIN_USERS_APP_ID, category: dataAccessUsersCategory, }, { - id: `security-dashboards-plugin_permissions`, + id: PLUGIN_PERMISSIONS_APP_ID, category: dataAccessUsersCategory, }, { - id: `security-dashboards-plugin_tenants`, + id: PLUGIN_TENANTS_APP_ID, category: dataAccessUsersCategory, }, { - id: `security-dashboards-plugin_auditlog`, + id: PLUGIN_AUDITLOG_APP_ID, category: dataAccessUsersCategory, }, ]); diff --git a/public/test/plugin.test.ts b/public/test/plugin.test.ts new file mode 100644 index 000000000..e33c83d08 --- /dev/null +++ b/public/test/plugin.test.ts @@ -0,0 +1,178 @@ +/* + * 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 { coreMock } from '../../../../src/core/public/mocks'; +import { SecurityPlugin } from '../plugin.ts'; +import * as pluginModule from '../plugin'; // Import the entire module to mock specific functions +import { + PLUGIN_AUDITLOG_APP_ID, + PLUGIN_AUTH_APP_ID, + PLUGIN_GET_STARTED_APP_ID, + PLUGIN_PERMISSIONS_APP_ID, + PLUGIN_ROLES_APP_ID, + PLUGIN_TENANTS_APP_ID, + PLUGIN_USERS_APP_ID, +} from '../../common/index.ts'; + +// Mock the hasApiPermission function +jest.mock('../plugin', () => { + const originalModule = jest.requireActual('../plugin'); + return { + ...originalModule, + hasApiPermission: jest.fn(), // Mock the function here + }; +}); + +describe('SecurityPlugin', () => { + let plugin; + let coreSetup; + let coreStart; + let initializerContext; + let deps; + + beforeEach(() => { + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + initializerContext = { + config: { + get: jest.fn().mockReturnValue({ + readonly_mode: { roles: [] }, + multitenancy: { enabled: true, enable_aggregation_view: false }, + clusterPermissions: { include: [] }, + indexPermissions: { include: [] }, + disabledTransportCategories: { exclude: [] }, + disabledRestCategories: { exclude: [] }, + ui: { autologout: false }, + }), + }, + }; + deps = { + dataSource: { dataSourceEnabled: true }, + savedObjectsManagement: { createSetup: jest.fn() }, + }; + }); + + it('does not call register function for certain applications when getNavGroupEnabled is off', async () => { + // Mock hasApiPermission to return false + pluginModule.hasApiPermission.mockResolvedValue(false); // Access the mock via the imported module + + // Instantiate the plugin after mocking + plugin = new SecurityPlugin(initializerContext); + + // Override getNavGroupEnabled to return false + coreSetup.chrome.navGroup = { + ...coreSetup.chrome.navGroup, + getNavGroupEnabled: () => false, + }; + // Mock the core.application.register function + const registerSpy = jest.spyOn(coreSetup.application, 'register'); + + // Execute the setup function + await plugin.setup(coreSetup, deps); + + // Assert that the register function was not called for specific applications + const registeredApps = registerSpy.mock.calls.map((call) => call[0].id); + const expectedApps = [ + PLUGIN_GET_STARTED_APP_ID, + PLUGIN_AUTH_APP_ID, + PLUGIN_ROLES_APP_ID, + PLUGIN_USERS_APP_ID, + PLUGIN_PERMISSIONS_APP_ID, + PLUGIN_TENANTS_APP_ID, + PLUGIN_AUDITLOG_APP_ID, + ]; + + expectedApps.forEach((app) => { + expect(registeredApps).not.toContain(app); + }); + }); + + it('calls register function for certain applications when getNavGroupEnabled is on', async () => { + // Mock hasApiPermission to return true + pluginModule.hasApiPermission.mockResolvedValue(true); // Access the mock via the imported module + + // Instantiate the plugin after mocking + plugin = new SecurityPlugin(initializerContext); + + // Override getNavGroupEnabled to return true + coreSetup.chrome.navGroup = { + ...coreSetup.chrome.navGroup, + getNavGroupEnabled: () => true, + }; + // Mock the core.application.register function + const registerSpy = jest.spyOn(coreSetup.application, 'register'); + + // Execute the setup function + await plugin.setup(coreSetup, deps); + + // Assert that the register function was called for specific applications + const registeredApps = registerSpy.mock.calls.map((call) => call[0].id); + const expectedApps = [ + PLUGIN_GET_STARTED_APP_ID, + PLUGIN_AUTH_APP_ID, + PLUGIN_ROLES_APP_ID, + PLUGIN_USERS_APP_ID, + PLUGIN_PERMISSIONS_APP_ID, + PLUGIN_TENANTS_APP_ID, + PLUGIN_AUDITLOG_APP_ID, + ]; + + expectedApps.forEach((app) => { + expect(registeredApps).toContain(app); + }); + }); + + it('does not call register function for tenant app when multitenancy is off', async () => { + // Mock hasApiPermission to return true + pluginModule.hasApiPermission.mockResolvedValue(true); + + // InitializerContext with multitenancy disabled + initializerContext = { + config: { + get: jest.fn().mockReturnValue({ + readonly_mode: { roles: [] }, + multitenancy: { enabled: false, enable_aggregation_view: false }, + clusterPermissions: { include: [] }, + indexPermissions: { include: [] }, + disabledTransportCategories: { exclude: [] }, + disabledRestCategories: { exclude: [] }, + ui: { autologout: false }, + }), + }, + }; + + // Instantiate the plugin after mocking + plugin = new SecurityPlugin(initializerContext); + + // Override getNavGroupEnabled to return true + coreSetup.chrome.navGroup = { + ...coreSetup.chrome.navGroup, + getNavGroupEnabled: () => true, + }; + // Mock the core.application.register function + const registerSpy = jest.spyOn(coreSetup.application, 'register'); + + // Execute the setup function + await plugin.setup(coreSetup, deps); + + // Assert that the register function was not called for tenancy app + const registeredApps = registerSpy.mock.calls.map((call) => call[0].id); + + expect(registeredApps).not.toContain(PLUGIN_TENANTS_APP_ID); + + // Assert that other apps are registered because the feature flag is on + expect(registeredApps).toContain(PLUGIN_GET_STARTED_APP_ID); + }); +});