diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engines.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engines.mock.ts new file mode 100644 index 0000000000000..4076af058d6b7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engines.mock.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EngineTypes } from '../components/engine/types'; + +export const defaultEngine = { + id: 'e1', + name: 'engine1', + type: EngineTypes.default, + language: null, + result_fields: {}, +}; + +export const indexedEngine = { + id: 'e2', + name: 'engine2', + type: EngineTypes.indexed, + language: null, + result_fields: {}, +}; + +export const engines = [defaultEngine, indexedEngine]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 44416b596e6ef..5f7dc683d93b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -13,7 +13,7 @@ import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; -interface AppValues { +export interface AppValues { ilmEnabled: boolean; configuredLimits: ConfiguredLimits; account: Account; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts index 74b8c6e640db1..6232ba0fb4668 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts @@ -11,3 +11,32 @@ export const ROLE_MAPPINGS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.roleMappings.title', { defaultMessage: 'Role Mappings' } ); + +export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage', + { + defaultMessage: + 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.', + } +); + +export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage', + { + defaultMessage: 'Successfully deleted role mapping', + } +); + +export const ROLE_MAPPING_CREATED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingCreatedMessage', + { + defaultMessage: 'Role mapping successfully created.', + } +); + +export const ROLE_MAPPING_UPDATED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingUpdatedMessage', + { + defaultMessage: 'Role mapping successfully updated.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts new file mode 100644 index 0000000000000..fa51c0036d0db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -0,0 +1,455 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; + +import { engines } from '../../__mocks__/engines.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; + +import { RoleMappingsLogic } from './role_mappings_logic'; + +describe('RoleMappingsLogic', () => { + const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; + const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { mount } = new LogicMounter(RoleMappingsLogic); + const DEFAULT_VALUES = { + attributes: [], + availableAuthProviders: [], + elasticsearchRoles: [], + roleMapping: null, + roleMappings: [], + roleType: 'owner', + attributeValue: '', + attributeName: 'username', + dataLoading: true, + hasAdvancedRoles: false, + multipleAuthProvidersConfig: false, + availableEngines: [], + selectedEngines: new Set(), + accessAllEngines: true, + selectedAuthProviders: [ANY_AUTH_PROVIDER], + }; + + const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [asRoleMapping] }; + const mappingServerProps = { + attributes: ['email', 'metadata', 'username', 'role'], + authProviders: [ANY_AUTH_PROVIDER], + availableEngines: engines, + elasticsearchRoles: [], + hasAdvancedRoles: false, + multipleAuthProvidersConfig: false, + roleMapping: asRoleMapping, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setRoleMappingsData', () => { + it('sets data based on server response from the `mappings` (plural) endpoint', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + + expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true); + }); + }); + + describe('setRoleMappingData', () => { + it('sets state based on server response from the `mapping` (singular) endpoint', () => { + RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + roleMapping: asRoleMapping, + dataLoading: false, + attributes: mappingServerProps.attributes, + availableAuthProviders: mappingServerProps.authProviders, + availableEngines: mappingServerProps.availableEngines, + accessAllEngines: true, + attributeName: 'role', + attributeValue: 'superuser', + elasticsearchRoles: mappingServerProps.elasticsearchRoles, + selectedEngines: new Set(engines.map((e) => e.name)), + }); + }); + + it('will remove all selected engines if no roleMapping was returned from the server', () => { + RoleMappingsLogic.actions.setRoleMappingData({ + ...mappingServerProps, + roleMapping: undefined, + }); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + selectedEngines: new Set(), + attributes: mappingServerProps.attributes, + availableAuthProviders: mappingServerProps.authProviders, + availableEngines: mappingServerProps.availableEngines, + }); + }); + }); + + it('handleRoleChange', () => { + RoleMappingsLogic.actions.handleRoleChange('dev'); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + roleType: 'dev', + accessAllEngines: false, + }); + }); + + describe('handleEngineSelectionChange', () => { + const engine = engines[0]; + const otherEngine = engines[1]; + const mountedValues = { + ...mappingServerProps, + roleMapping: { + ...asRoleMapping, + engines: [engine, otherEngine], + }, + selectedEngines: new Set([engine.name]), + }; + + beforeEach(() => { + mount(mountedValues); + }); + + it('handles adding an engine to selected engines', () => { + RoleMappingsLogic.actions.handleEngineSelectionChange(otherEngine.name, true); + + expect(RoleMappingsLogic.values.selectedEngines).toEqual( + new Set([engine.name, otherEngine.name]) + ); + }); + it('handles removing an engine from selected engines', () => { + RoleMappingsLogic.actions.handleEngineSelectionChange(otherEngine.name, false); + + expect(RoleMappingsLogic.values.selectedEngines).toEqual(new Set([engine.name])); + }); + }); + + it('handleAccessAllEnginesChange', () => { + RoleMappingsLogic.actions.handleAccessAllEnginesChange(); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + accessAllEngines: false, + }); + }); + + describe('handleAttributeSelectorChange', () => { + const elasticsearchRoles = ['foo', 'bar']; + + it('sets values correctly', () => { + mount({ + ...mappingServerProps, + elasticsearchRoles, + }); + RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + attributeValue: elasticsearchRoles[0], + roleMapping: asRoleMapping, + attributes: mappingServerProps.attributes, + availableEngines: mappingServerProps.availableEngines, + accessAllEngines: true, + attributeName: 'role', + elasticsearchRoles, + selectedEngines: new Set(), + }); + }); + + it('correctly handles "role" fallback', () => { + RoleMappingsLogic.actions.handleAttributeSelectorChange('username', elasticsearchRoles[0]); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + attributeValue: '', + }); + }); + }); + + it('handleAttributeValueChange', () => { + RoleMappingsLogic.actions.handleAttributeValueChange('changed_value'); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + attributeValue: 'changed_value', + }); + }); + + describe('handleAuthProviderChange', () => { + beforeEach(() => { + mount({ + ...mappingServerProps, + roleMapping: { + ...asRoleMapping, + authProvider: ['foo'], + }, + }); + }); + const providers = ['bar', 'baz']; + const providerWithAny = [ANY_AUTH_PROVIDER, providers[1]]; + it('handles empty state', () => { + RoleMappingsLogic.actions.handleAuthProviderChange([]); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([ANY_AUTH_PROVIDER]); + }); + + it('handles single value', () => { + RoleMappingsLogic.actions.handleAuthProviderChange([providers[0]]); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[0]]); + }); + + it('handles multiple values', () => { + RoleMappingsLogic.actions.handleAuthProviderChange(providers); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual(providers); + }); + + it('handles "any" auth in previous state', () => { + mount({ + ...mappingServerProps, + roleMapping: { + ...asRoleMapping, + authProvider: [ANY_AUTH_PROVIDER], + }, + }); + RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[1]]); + }); + }); + + it('resetState', () => { + mount(mappingsServerProps); + mount(mappingServerProps); + RoleMappingsLogic.actions.resetState(); + + expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('listeners', () => { + describe('initializeRoleMappings', () => { + it('calls API and sets values', async () => { + const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData'); + http.get.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.initializeRoleMappings(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings'); + await nextTick(); + expect(setRoleMappingsDataSpy).toHaveBeenCalledWith(mappingsServerProps); + }); + + it('handles error', async () => { + http.get.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.initializeRoleMappings(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('initializeRoleMapping', () => { + it('calls API and sets values for new mapping', async () => { + const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData'); + http.get.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.initializeRoleMapping(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/new'); + await nextTick(); + expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps); + }); + + it('calls API and sets values for existing mapping', async () => { + const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData'); + http.get.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.initializeRoleMapping('123'); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/123'); + await nextTick(); + expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps); + }); + + it('handles error', async () => { + http.get.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.initializeRoleMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + + it('redirects when there is a 404 status', async () => { + http.get.mockReturnValue(Promise.reject({ status: 404 })); + RoleMappingsLogic.actions.initializeRoleMapping(); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + }); + }); + + describe('handleResetMappings', () => { + const callback = jest.fn(); + it('calls API and executes callback', async () => { + http.post.mockReturnValue(Promise.resolve({})); + RoleMappingsLogic.actions.handleResetMappings(callback); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/role_mappings/reset'); + await nextTick(); + expect(callback).toHaveBeenCalled(); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.handleResetMappings(callback); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('handleSaveMapping', () => { + const body = { + roleType: 'owner', + accessAllEngines: true, + authProvider: [ANY_AUTH_PROVIDER], + rules: { + username: '', + }, + engines: [], + }; + + it('calls API and navigates when new mapping', async () => { + mount(mappingsServerProps); + + http.post.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.handleSaveMapping(); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/role_mappings', { + body: JSON.stringify(body), + }); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + }); + + it('calls API and navigates when existing mapping', async () => { + mount(mappingServerProps); + + http.put.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.handleSaveMapping(); + + expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, { + body: JSON.stringify(body), + }); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + expect(setSuccessMessage).toHaveBeenCalled(); + }); + + it('sends array when "accessAllEngines" is false', () => { + const engine = engines[0]; + + mount({ + ...mappingServerProps, + accessAllEngines: false, + selectedEngines: new Set([engine.name]), + }); + + http.put.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.handleSaveMapping(); + + expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, { + body: JSON.stringify({ + ...body, + accessAllEngines: false, + engines: [engine.name], + }), + }); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.handleSaveMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('handleDeleteMapping', () => { + let confirmSpy: any; + + beforeEach(() => { + confirmSpy = jest.spyOn(window, 'confirm'); + confirmSpy.mockImplementation(jest.fn(() => true)); + }); + + afterEach(() => { + confirmSpy.mockRestore(); + }); + + it('returns when no mapping', () => { + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).not.toHaveBeenCalled(); + }); + + it('calls API and navigates', async () => { + mount(mappingServerProps); + http.delete.mockReturnValue(Promise.resolve({})); + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).toHaveBeenCalledWith( + `/api/app_search/role_mappings/${asRoleMapping.id}` + ); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + expect(setSuccessMessage).toHaveBeenCalled(); + }); + + it('handles error', async () => { + mount(mappingServerProps); + http.delete.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.handleDeleteMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + + it('will do nothing if not confirmed', () => { + mount(mappingServerProps); + jest.spyOn(window, 'confirm').mockReturnValueOnce(false); + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts new file mode 100644 index 0000000000000..f1b81a59779ac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -0,0 +1,356 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { + clearFlashMessages, + flashAPIErrors, + setSuccessMessage, +} from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; +import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { AttributeName } from '../../../shared/types'; +import { ROLE_MAPPINGS_PATH } from '../../routes'; +import { ASRoleMapping, RoleTypes } from '../../types'; +import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; +import { Engine } from '../engine/types'; + +import { + DELETE_ROLE_MAPPING_MESSAGE, + ROLE_MAPPING_DELETED_MESSAGE, + ROLE_MAPPING_CREATED_MESSAGE, + ROLE_MAPPING_UPDATED_MESSAGE, +} from './constants'; + +interface RoleMappingsServerDetails { + roleMappings: ASRoleMapping[]; + multipleAuthProvidersConfig: boolean; +} + +interface RoleMappingServerDetails { + attributes: string[]; + authProviders: string[]; + availableEngines: Engine[]; + elasticsearchRoles: string[]; + hasAdvancedRoles: boolean; + multipleAuthProvidersConfig: boolean; + roleMapping?: ASRoleMapping; +} + +const getFirstAttributeName = (roleMapping: ASRoleMapping) => + Object.entries(roleMapping.rules)[0][0] as AttributeName; +const getFirstAttributeValue = (roleMapping: ASRoleMapping) => + Object.entries(roleMapping.rules)[0][1] as AttributeName; + +export interface RoleMappingsActions { + handleAccessAllEnginesChange(): void; + handleAuthProviderChange(value: string[]): { value: string[] }; + handleAttributeSelectorChange( + value: AttributeName, + firstElasticsearchRole: string + ): { value: AttributeName; firstElasticsearchRole: string }; + handleAttributeValueChange(value: string): { value: string }; + handleDeleteMapping(): void; + handleEngineSelectionChange( + engineName: string, + selected: boolean + ): { + engineName: string; + selected: boolean; + }; + handleResetMappings(callback: () => void): Function; + handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; + handleSaveMapping(): void; + initializeRoleMapping(roleId?: string): { roleId?: string }; + initializeRoleMappings(): void; + resetState(): void; + setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails; + setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; +} + +export interface RoleMappingsValues { + accessAllEngines: boolean; + attributeName: AttributeName; + attributeValue: string; + attributes: string[]; + availableAuthProviders: string[]; + availableEngines: Engine[]; + dataLoading: boolean; + elasticsearchRoles: string[]; + hasAdvancedRoles: boolean; + multipleAuthProvidersConfig: boolean; + roleMapping: ASRoleMapping | null; + roleMappings: ASRoleMapping[]; + roleType: RoleTypes; + selectedAuthProviders: string[]; + selectedEngines: Set; +} + +export const RoleMappingsLogic = kea>({ + path: ['enterprise_search', 'app_search', 'role_mappings'], + actions: { + setRoleMappingsData: (data: RoleMappingsServerDetails) => data, + setRoleMappingData: (data: RoleMappingServerDetails) => data, + handleAuthProviderChange: (value: string) => ({ value }), + handleRoleChange: (roleType: RoleTypes) => ({ roleType }), + handleEngineSelectionChange: (engineName: string, selected: boolean) => ({ + engineName, + selected, + }), + handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ + value, + firstElasticsearchRole, + }), + handleAttributeValueChange: (value: string) => ({ value }), + handleAccessAllEnginesChange: true, + resetState: true, + initializeRoleMappings: true, + initializeRoleMapping: (roleId) => ({ roleId }), + handleDeleteMapping: true, + handleResetMappings: (callback) => callback, + handleSaveMapping: true, + }, + reducers: { + dataLoading: [ + true, + { + setRoleMappingsData: () => false, + setRoleMappingData: () => false, + resetState: () => true, + }, + ], + roleMappings: [ + [], + { + setRoleMappingsData: (_, { roleMappings }) => roleMappings, + resetState: () => [], + }, + ], + multipleAuthProvidersConfig: [ + false, + { + setRoleMappingsData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, + setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, + resetState: () => false, + }, + ], + hasAdvancedRoles: [ + false, + { + setRoleMappingData: (_, { hasAdvancedRoles }) => hasAdvancedRoles, + }, + ], + availableEngines: [ + [], + { + setRoleMappingData: (_, { availableEngines }) => availableEngines, + resetState: () => [], + }, + ], + attributes: [ + [], + { + setRoleMappingData: (_, { attributes }) => attributes, + resetState: () => [], + }, + ], + elasticsearchRoles: [ + [], + { + setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles, + }, + ], + roleMapping: [ + null, + { + setRoleMappingData: (_, { roleMapping }) => roleMapping || null, + resetState: () => null, + }, + ], + roleType: [ + 'owner', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? (roleMapping.roleType as RoleTypes) : 'owner', + handleRoleChange: (_, { roleType }) => roleType, + }, + ], + accessAllEngines: [ + true, + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? roleMapping.accessAllEngines : true, + handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType), + handleAccessAllEnginesChange: (accessAllEngines) => !accessAllEngines, + }, + ], + attributeValue: [ + '', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? getFirstAttributeValue(roleMapping) : '', + handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) => + value === 'role' ? firstElasticsearchRole : '', + handleAttributeValueChange: (_, { value }) => value, + resetState: () => '', + }, + ], + attributeName: [ + 'username', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? getFirstAttributeName(roleMapping) : 'username', + handleAttributeSelectorChange: (_, { value }) => value, + resetState: () => 'username', + }, + ], + selectedEngines: [ + new Set(), + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? new Set(roleMapping.engines.map((engine) => engine.name)) : new Set(), + handleAccessAllEnginesChange: () => new Set(), + handleEngineSelectionChange: (engines, { engineName, selected }) => { + const newSelectedEngineNames = new Set(engines as Set); + if (selected) { + newSelectedEngineNames.add(engineName); + } else { + newSelectedEngineNames.delete(engineName); + } + + return newSelectedEngineNames; + }, + }, + ], + availableAuthProviders: [ + [], + { + setRoleMappingData: (_, { authProviders }) => authProviders, + }, + ], + selectedAuthProviders: [ + [ANY_AUTH_PROVIDER], + { + handleAuthProviderChange: (previous, { value }) => { + const previouslyContainedAny = previous.includes(ANY_AUTH_PROVIDER); + const newSelectionsContainAny = value.includes(ANY_AUTH_PROVIDER); + const hasItems = value.length > 0; + + if (value.length === 1) return value; + if (!newSelectionsContainAny && hasItems) return value; + if (previouslyContainedAny && hasItems) + return value.filter((v) => v !== ANY_AUTH_PROVIDER); + return [ANY_AUTH_PROVIDER]; + }, + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER], + }, + ], + }, + listeners: ({ actions, values }) => ({ + initializeRoleMappings: async () => { + const { http } = HttpLogic.values; + const route = '/api/app_search/role_mappings'; + + try { + const response = await http.get(route); + actions.setRoleMappingsData(response); + } catch (e) { + flashAPIErrors(e); + } + }, + initializeRoleMapping: async ({ roleId }) => { + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + const route = roleId + ? `/api/app_search/role_mappings/${roleId}` + : '/api/app_search/role_mappings/new'; + + try { + const response = await http.get(route); + actions.setRoleMappingData(response); + } catch (e) { + navigateToUrl(ROLE_MAPPINGS_PATH); + flashAPIErrors(e); + } + }, + handleDeleteMapping: async () => { + const { roleMapping } = values; + if (!roleMapping) return; + + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + const route = `/api/app_search/role_mappings/${roleMapping.id}`; + + if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) { + try { + await http.delete(route); + navigateToUrl(ROLE_MAPPINGS_PATH); + setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + } + }, + handleResetMappings: async (callback) => { + const { http } = HttpLogic.values; + try { + await http.post('/api/app_search/role_mappings/reset'); + actions.initializeRoleMappings(); + } catch (e) { + flashAPIErrors(e); + } finally { + callback(); + } + }, + handleSaveMapping: async () => { + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + + const { + attributeName, + attributeValue, + roleType, + roleMapping, + accessAllEngines, + selectedEngines, + selectedAuthProviders: authProvider, + } = values; + + const body = JSON.stringify({ + roleType, + accessAllEngines, + authProvider, + rules: { + [attributeName]: attributeValue, + }, + engines: accessAllEngines ? [] : Array.from(selectedEngines), + }); + + const request = !roleMapping + ? http.post('/api/app_search/role_mappings', { body }) + : http.put(`/api/app_search/role_mappings/${roleMapping.id}`, { body }); + + const SUCCESS_MESSAGE = !roleMapping + ? ROLE_MAPPING_CREATED_MESSAGE + : ROLE_MAPPING_UPDATED_MESSAGE; + + try { + await request; + navigateToUrl(ROLE_MAPPINGS_PATH); + setSuccessMessage(SUCCESS_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + }, + resetState: () => { + clearFlashMessages(); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts index 1576fa178cfa9..15dec753351ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts @@ -5,19 +5,21 @@ * 2.0. */ +import { engines } from '../../../app_search/__mocks__/engines.mock'; + import { AttributeName } from '../../types'; export const asRoleMapping = { - id: null, + id: 'sdgfasdgadf123', attributeName: 'role' as AttributeName, - attributeValue: ['superuser'], + attributeValue: 'superuser', authProvider: ['*'], roleType: 'owner', rules: { role: 'superuser', }, accessAllEngines: true, - engines: [], + engines, toolTip: { content: 'Elasticsearch superusers will always be able to log in as the owner', }, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 9c47378302890..5589309d00ef8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -81,7 +81,7 @@ describe('RoleMappingsTable', () => { }); it('handles display when no items present', () => { - const noItemsRoleMapping = { ...asRoleMapping }; + const noItemsRoleMapping = { ...asRoleMapping, engines: [] }; noItemsRoleMapping.accessAllEngines = false; const wrapper = shallow(