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 d81ab4183..acc3c3c31 --- 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,12 @@ 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 +139,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 +148,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 186f394b2..8e8375b04 --- 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 53df57309..2b1cfdf75 --- 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 48cd151b5..07fa82761 --- 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 @@ -202,6 +203,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 24f839c09..a6e566a48 --- 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 securityTenant_ = request?.url?.searchParams?.get('securityTenant_'); const securitytenant = request?.url?.searchParams?.get('securitytenant'); @@ -57,10 +59,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 globalTenantEnabled = config.multitenancy?.tenants.enable_global; + const privateTenantEnabled = config.multitenancy?.tenants.enable_private && !isReadonly; return resolve( username,