From 221c3a88e1f91c21c4147cafbde11b09fadca829 Mon Sep 17 00:00:00 2001 From: Gio Collina Date: Mon, 25 Oct 2021 21:37:50 +0200 Subject: [PATCH 1/2] Disable private tenant for read only users Signed-off-by: David Bennett --- public/apps/account/tenant-switch-panel.tsx | 14 +- .../tenant-switch-panel.test.tsx.snap | 228 ++++++++++++++++++ .../account/test/tenant-switch-panel.test.tsx | 70 +++++- server/auth/types/authentication_type.ts | 1 + server/auth/types/basic/routes.ts | 2 + server/backend/opensearch_security_client.ts | 0 server/multitenancy/tenant_resolver.ts | 7 +- 7 files changed, 319 insertions(+), 3 deletions(-) mode change 100644 => 100755 public/apps/account/tenant-switch-panel.tsx mode change 100644 => 100755 public/apps/account/test/tenant-switch-panel.test.tsx mode change 100644 => 100755 server/auth/types/authentication_type.ts mode change 100644 => 100755 server/auth/types/basic/routes.ts mode change 100644 => 100755 server/backend/opensearch_security_client.ts mode change 100644 => 100755 server/multitenancy/tenant_resolver.ts diff --git a/public/apps/account/tenant-switch-panel.tsx b/public/apps/account/tenant-switch-panel.tsx old mode 100644 new mode 100755 index d381cd931..4958acd94 --- a/public/apps/account/tenant-switch-panel.tsx +++ b/public/apps/account/tenant-switch-panel.tsx @@ -58,6 +58,7 @@ export const CUSTOM_TENANT_RADIO_ID = 'custom'; export function TenantSwitchPanel(props: TenantSwitchPanelProps) { const [tenants, setTenants] = React.useState([]); const [username, setUsername] = React.useState(''); + const [roles, setRoles] = React.useState([]); const [errorCallOut, setErrorCallOut] = React.useState(''); const [tenantSwitchRadioIdSelected, setTenantSwitchRadioIdSelected] = React.useState(); const [selectedCustomTenantOption, setSelectedCustomTenantOption] = React.useState< @@ -81,6 +82,8 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) { const fetchData = async () => { try { const accountInfo = await fetchAccountInfo(props.coreStart.http); + setRoles(accountInfo.data.roles); + const tenantsInfo = accountInfo.data.tenants || {}; setTenants(keys(tenantsInfo)); @@ -118,6 +121,11 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) { 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 shouldDisableGlobal = !isGlobalEnabled || !tenants.includes(GLOBAL_TENANT_KEY_NAME); const getGlobalDisabledInstruction = () => { if (!isGlobalEnabled) { @@ -130,7 +138,7 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) { }; // The key for private tenant is the user name. - const shouldDisablePrivate = !isPrivateEnabled || !tenants.includes(username); + const shouldDisablePrivate = !isPrivateEnabled || !tenants.includes(username) || readonly; const getPrivateDisabledInstruction = () => { if (!isPrivateEnabled) { return 'Contact the administrator to enable private tenant.'; @@ -139,6 +147,10 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) { if (!tenants.includes(username)) { return 'Contact the administrator to get access to private tenant.'; } + + if (readonly) { + return 'Your account has read-only privileges only, using the private tenant is not possible.'; + } }; // Tenant switch radios related. diff --git a/public/apps/account/test/__snapshots__/tenant-switch-panel.test.tsx.snap b/public/apps/account/test/__snapshots__/tenant-switch-panel.test.tsx.snap index a36d026f2..7c14be0e7 100644 --- a/public/apps/account/test/__snapshots__/tenant-switch-panel.test.tsx.snap +++ b/public/apps/account/test/__snapshots__/tenant-switch-panel.test.tsx.snap @@ -338,3 +338,231 @@ exports[`Account menu -tenant switch panel confirm button and renders renders wh `; + +exports[`Account menu -tenant switch panel confirm button and renders renders when user has default read only role 1`] = ` + + + + + +

+ Select your tenant +

+
+ + + Tenants are useful for safely sharing your work with other OpenSearch Dashboards users. You can switch your tenant anytime by clicking the user avatar on top right. + + + + Global + + The global tenant is shared between every OpenSearch Dashboards user. + + + , + }, + Object { + "disabled": true, + "id": "private", + "label": + Private + + The private tenant is exclusive to each user and can't be shared. You might use the private tenant for exploratory work. + + + Your account has read-only privileges only, using the private tenant is not possible. + + + , + }, + Object { + "disabled": false, + "id": "custom", + "label": + Choose from custom + , + }, + ] + } + /> + + + +
+ + + Cancel + + + Confirm + + +
+
+`; + +exports[`Account menu -tenant switch panel confirm button and renders renders when user has read only role 1`] = ` + + + + + +

+ Select your tenant +

+
+ + + Tenants are useful for safely sharing your work with other OpenSearch Dashboards users. You can switch your tenant anytime by clicking the user avatar on top right. + + + + Global + + The global tenant is shared between every OpenSearch Dashboards user. + + + , + }, + Object { + "disabled": true, + "id": "private", + "label": + Private + + The private tenant is exclusive to each user and can't be shared. You might use the private tenant for exploratory work. + + + Your account has read-only privileges only, using the private tenant is not possible. + + + , + }, + Object { + "disabled": false, + "id": "custom", + "label": + Choose from custom + , + }, + ] + } + /> + + + +
+ + + Cancel + + + Confirm + + +
+
+`; diff --git a/public/apps/account/test/tenant-switch-panel.test.tsx b/public/apps/account/test/tenant-switch-panel.test.tsx old mode 100644 new mode 100755 index e5b6e10eb..5763704c6 --- a/public/apps/account/test/tenant-switch-panel.test.tsx +++ b/public/apps/account/test/tenant-switch-panel.test.tsx @@ -34,6 +34,7 @@ const mockAccountInfo = { ['user1']: true, }, user_name: 'user1', + roles: ["readall", "readonly"], user_requested_tenant: '', }, }; @@ -104,6 +105,7 @@ describe('Account menu -tenant switch panel', () => { ['tenant1']: true, }, user_name: 'user1', + roles: ["role1", "role2"], user_requested_tenant: '__user__', }, }; @@ -131,6 +133,7 @@ describe('Account menu -tenant switch panel', () => { ['tenant1']: true, }, user_name: 'user1', + roles: ["role1", "role2"], user_requested_tenant: 'tenant1', }, }; @@ -239,10 +242,11 @@ describe('Account menu -tenant switch panel', () => { beforeEach(() => { useState.mockImplementationOnce(() => [keys(mockAccountInfo.data.tenants), setState]); useState.mockImplementationOnce(() => [mockAccountInfo.data.user_name, setState]); - useState.mockImplementationOnce(() => ['', setState]); }); it('should handle tenant confirmation on "confirm" button click when selected tenant is Global tenant', () => { + useState.mockImplementationOnce(() => [[], setState]); + useState.mockImplementationOnce(() => ['', setState]); useState.mockImplementationOnce(() => [GLOBAL_TENANT_RADIO_ID, setState]); useState.mockImplementationOnce(() => ['', setState]); const component = shallow( @@ -258,6 +262,8 @@ describe('Account menu -tenant switch panel', () => { }); it('should handle tenant confirmation on "confirm" button click when selected tenant is Private tenant', () => { + useState.mockImplementationOnce(() => [[], setState]); + useState.mockImplementationOnce(() => ['', setState]); useState.mockImplementationOnce(() => [PRIVATE_TENANT_RADIO_ID, setState]); useState.mockImplementationOnce(() => ['', setState]); const component = shallow( @@ -273,6 +279,8 @@ describe('Account menu -tenant switch panel', () => { }); it('should handle tenant confirmation on "confirm" button click when selected tenant is Custom tenant', () => { + useState.mockImplementationOnce(() => [[], setState]); + useState.mockImplementationOnce(() => ['', setState]); useState.mockImplementationOnce(() => [CUSTOM_TENANT_RADIO_ID, setState]); useState.mockImplementationOnce(() => [[{ label: 'tenant1' }], setState]); const component = shallow( @@ -288,6 +296,8 @@ describe('Account menu -tenant switch panel', () => { }); it('should set error call out when error occurred while changing the tenant', (done) => { + useState.mockImplementationOnce(() => [[], setState]); + useState.mockImplementationOnce(() => ['', setState]); useState.mockImplementationOnce(() => [GLOBAL_TENANT_RADIO_ID, setState]); useState.mockImplementationOnce(() => ['', setState]); (selectTenant as jest.Mock).mockImplementationOnce(() => { @@ -364,5 +374,63 @@ describe('Account menu -tenant switch panel', () => { done(); }); }); + + it('renders when user has read only role', (done) => { + useState.mockImplementationOnce(() => [["readonly"], setState]); + useState.mockImplementationOnce(() => ['', setState]); + const config = { + readonly_mode: { + roles: ["readonly"] + }, + multitenancy: { + enabled: true, + tenants: { + enable_private: true, + enable_global: true, + }, + }, + }; + const component = shallow( + + ); + process.nextTick(() => { + expect(component).toMatchSnapshot(); + done(); + }); + }); + + it('renders when user has default read only role', (done) => { + useState.mockImplementationOnce(() => [["kibana_read_only"], setState]); + useState.mockImplementationOnce(() => ['', setState]); + const config = { + readonly_mode: { + roles: [] + }, + multitenancy: { + enabled: true, + tenants: { + enable_private: true, + enable_global: true, + }, + }, + }; + const component = shallow( + + ); + process.nextTick(() => { + expect(component).toMatchSnapshot(); + done(); + }); + }); }); }); diff --git a/server/auth/types/authentication_type.ts b/server/auth/types/authentication_type.ts old mode 100644 new mode 100755 index 1f4ff486e..2cdcafccc --- a/server/auth/types/authentication_type.ts +++ b/server/auth/types/authentication_type.ts @@ -217,6 +217,7 @@ export abstract class AuthenticationType implements IAuthenticationType { const selectedTenant = resolveTenant( request, authInfo.user_name, + authInfo.roles, authInfo.tenants, this.config, cookie diff --git a/server/auth/types/basic/routes.ts b/server/auth/types/basic/routes.ts old mode 100644 new mode 100755 index 5020786e8..beb99c789 --- a/server/auth/types/basic/routes.ts +++ b/server/auth/types/basic/routes.ts @@ -116,6 +116,7 @@ export class BasicAuthRoutes { const selectTenant = resolveTenant( request, user.username, + user.roles, user.tenants, this.config, sessionStorage @@ -200,6 +201,7 @@ export class BasicAuthRoutes { const selectTenant = resolveTenant( request, user.username, + user.roles, user.tenants, this.config, sessionStorage diff --git a/server/backend/opensearch_security_client.ts b/server/backend/opensearch_security_client.ts old mode 100644 new mode 100755 diff --git a/server/multitenancy/tenant_resolver.ts b/server/multitenancy/tenant_resolver.ts old mode 100644 new mode 100755 index f73710b48..81cf94ae7 --- a/server/multitenancy/tenant_resolver.ts +++ b/server/multitenancy/tenant_resolver.ts @@ -37,10 +37,12 @@ export const GLOBAL_TENANTS: string[] = ['global', GLOBAL_TENANT_SYMBOL]; export function resolveTenant( request: OpenSearchDashboardsRequest, username: string, + roles: string[] | undefined, availabeTenants: any, config: SecurityPluginConfigType, cookie: SecuritySessionCookie ): string | undefined { + const DEFAULT_READONLY_ROLES = ['kibana_read_only']; let selectedTenant: string | undefined; const query: any = request.url.query as any; if (query && (query.security_tenant || query.securitytenant)) { @@ -54,10 +56,13 @@ export function resolveTenant( } else { selectedTenant = undefined; } + const isReadonly = roles?.some((role) => + config.readonly_mode?.roles.includes(role) || DEFAULT_READONLY_ROLES.includes(role) + ); const preferredTenants = config.multitenancy?.tenants.preferred; const globalTenantEnabled = config.multitenancy?.tenants.enable_global || false; - const privateTenantEnabled = config.multitenancy?.tenants.enable_private || false; + const privateTenantEnabled = config.multitenancy?.tenants.enable_private && !isReadonly; return resolve( username, From cc4cb6196952400eba768b7e081fc8634174cdcc Mon Sep 17 00:00:00 2001 From: Gio Collina Date: Thu, 4 Nov 2021 20:55:55 +0100 Subject: [PATCH 2/2] removed uneccesary check for enable_global. Fixed lint errors. Signed-off-by: David Bennett --- public/apps/account/tenant-switch-panel.tsx | 5 +++-- .../apps/account/test/tenant-switch-panel.test.tsx | 14 +++++++------- server/multitenancy/tenant_resolver.ts | 6 +++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/public/apps/account/tenant-switch-panel.tsx b/public/apps/account/tenant-switch-panel.tsx index 4958acd94..7c3af87c7 100755 --- a/public/apps/account/tenant-switch-panel.tsx +++ b/public/apps/account/tenant-switch-panel.tsx @@ -122,8 +122,9 @@ export function TenantSwitchPanel(props: TenantSwitchPanelProps) { 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 readonly = roles.some( + (role) => + props.config.readonly_mode?.roles.includes(role) || DEFAULT_READONLY_ROLES.includes(role) ); const shouldDisableGlobal = !isGlobalEnabled || !tenants.includes(GLOBAL_TENANT_KEY_NAME); diff --git a/public/apps/account/test/tenant-switch-panel.test.tsx b/public/apps/account/test/tenant-switch-panel.test.tsx index 5763704c6..1741759dd 100755 --- a/public/apps/account/test/tenant-switch-panel.test.tsx +++ b/public/apps/account/test/tenant-switch-panel.test.tsx @@ -34,7 +34,7 @@ const mockAccountInfo = { ['user1']: true, }, user_name: 'user1', - roles: ["readall", "readonly"], + roles: ['readall', 'readonly'], user_requested_tenant: '', }, }; @@ -105,7 +105,7 @@ describe('Account menu -tenant switch panel', () => { ['tenant1']: true, }, user_name: 'user1', - roles: ["role1", "role2"], + roles: ['role1', 'role2'], user_requested_tenant: '__user__', }, }; @@ -133,7 +133,7 @@ describe('Account menu -tenant switch panel', () => { ['tenant1']: true, }, user_name: 'user1', - roles: ["role1", "role2"], + roles: ['role1', 'role2'], user_requested_tenant: 'tenant1', }, }; @@ -376,11 +376,11 @@ describe('Account menu -tenant switch panel', () => { }); it('renders when user has read only role', (done) => { - useState.mockImplementationOnce(() => [["readonly"], setState]); + useState.mockImplementationOnce(() => [['readonly'], setState]); useState.mockImplementationOnce(() => ['', setState]); const config = { readonly_mode: { - roles: ["readonly"] + roles: ['readonly'], }, multitenancy: { enabled: true, @@ -405,11 +405,11 @@ describe('Account menu -tenant switch panel', () => { }); it('renders when user has default read only role', (done) => { - useState.mockImplementationOnce(() => [["kibana_read_only"], setState]); + useState.mockImplementationOnce(() => [['kibana_read_only'], setState]); useState.mockImplementationOnce(() => ['', setState]); const config = { readonly_mode: { - roles: [] + roles: [], }, multitenancy: { enabled: true, diff --git a/server/multitenancy/tenant_resolver.ts b/server/multitenancy/tenant_resolver.ts index 81cf94ae7..2039a9e67 100755 --- a/server/multitenancy/tenant_resolver.ts +++ b/server/multitenancy/tenant_resolver.ts @@ -56,12 +56,12 @@ export function resolveTenant( } else { selectedTenant = undefined; } - const isReadonly = roles?.some((role) => - config.readonly_mode?.roles.includes(role) || DEFAULT_READONLY_ROLES.includes(role) + const isReadonly = roles?.some( + (role) => config.readonly_mode?.roles.includes(role) || DEFAULT_READONLY_ROLES.includes(role) ); const preferredTenants = config.multitenancy?.tenants.preferred; - const globalTenantEnabled = config.multitenancy?.tenants.enable_global || false; + const globalTenantEnabled = config.multitenancy?.tenants.enable_global; const privateTenantEnabled = config.multitenancy?.tenants.enable_private && !isReadonly; return resolve(