diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index cb90568982282..5246e09f6b0f3 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -104,29 +104,3 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => { }, [fetchUser]); return user; }; - -export interface UseGetUserSavedObjectPermissions { - crud: boolean; - read: boolean; -} - -export const useGetUserSavedObjectPermissions = () => { - const [ - savedObjectsPermissions, - setSavedObjectsPermissions, - ] = useState(null); - const uiCapabilities = useKibana().services.application.capabilities; - - useEffect(() => { - const capabilitiesCanUserCRUD: boolean = - typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; - const capabilitiesCanUserRead: boolean = - typeof uiCapabilities.siem.show === 'boolean' ? uiCapabilities.siem.show : false; - setSavedObjectsPermissions({ - crud: capabilitiesCanUserCRUD, - read: capabilitiesCanUserRead, - }); - }, [uiCapabilities]); - - return savedObjectsPermissions; -}; diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts new file mode 100644 index 0000000000000..4d6c60e93ee20 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts @@ -0,0 +1,234 @@ +/* + * 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 { TestCaseWithoutTimeline } from '../../objects/case'; +import { ALL_CASES_NAME } from '../../screens/all_cases'; + +import { goToCreateNewCase } from '../../tasks/all_cases'; +import { cleanKibana, deleteCases } from '../../tasks/common'; + +import { + backToCases, + createCase, + fillCasesMandatoryfields, + filterStatusOpen, +} from '../../tasks/create_new_case'; +import { + constructUrlWithUser, + getEnvAuth, + loginWithUserAndWaitForPageWithoutDateRange, +} from '../../tasks/login'; + +import { CASES_URL } from '../../urls/navigation'; + +interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +interface UserInfo { + username: string; + full_name: string; + email: string; +} + +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +interface ElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +interface ElasticSearchPrivilege { + cluster?: string[]; + indices?: ElasticsearchIndices[]; +} + +interface KibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +interface Role { + name: string; + privileges: { + elasticsearch?: ElasticSearchPrivilege; + kibana?: KibanaPrivilege[]; + }; +} + +const secAll: Role = { + name: 'sec_all_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +const secAllUser: User = { + username: 'sec_all_user', + password: 'password', + roles: [secAll.name], +}; + +const secReadCasesAll: Role = { + name: 'sec_read_cases_all_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['minimal_read', 'cases_all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +const secReadCasesAllUser: User = { + username: 'sec_read_cases_all_user', + password: 'password', + roles: [secReadCasesAll.name], +}; + +const usersToCreate = [secAllUser, secReadCasesAllUser]; +const rolesToCreate = [secAll, secReadCasesAll]; + +const getUserInfo = (user: User): UserInfo => ({ + username: user.username, + full_name: user.username.replace('_', ' '), + email: `${user.username}@elastic.co`, +}); + +const createUsersAndRoles = (users: User[], roles: Role[]) => { + const envUser = getEnvAuth(); + for (const role of roles) { + cy.log(`Creating role: ${JSON.stringify(role)}`); + cy.request({ + body: role.privileges, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'PUT', + url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), + }) + .its('status') + .should('eql', 204); + } + + for (const user of users) { + const userInfo = getUserInfo(user); + cy.log(`Creating user: ${JSON.stringify(user)}`); + cy.request({ + body: { + username: user.username, + password: user.password, + roles: user.roles, + full_name: userInfo.full_name, + email: userInfo.email, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), + }) + .its('status') + .should('eql', 200); + } +}; + +const deleteUsersAndRoles = (users: User[], roles: Role[]) => { + const envUser = getEnvAuth(); + for (const user of users) { + cy.log(`Deleting user: ${JSON.stringify(user)}`); + cy.request({ + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'DELETE', + url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), + failOnStatusCode: false, + }) + .its('status') + .should('oneOf', [204, 404]); + } + + for (const role of roles) { + cy.log(`Deleting role: ${JSON.stringify(role)}`); + cy.request({ + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'DELETE', + url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), + failOnStatusCode: false, + }) + .its('status') + .should('oneOf', [204, 404]); + } +}; + +const testCase: TestCaseWithoutTimeline = { + name: 'This is the title of the case', + tags: ['Tag1', 'Tag2'], + description: 'This is the case description', + reporter: 'elastic', + owner: 'securitySolution', +}; + +describe('Cases privileges', () => { + before(() => { + cleanKibana(); + createUsersAndRoles(usersToCreate, rolesToCreate); + }); + + after(() => { + deleteUsersAndRoles(usersToCreate, rolesToCreate); + cleanKibana(); + }); + + beforeEach(() => { + deleteCases(); + }); + + for (const user of [secAllUser, secReadCasesAllUser]) { + it(`User ${user.username} with role(s) ${user.roles.join()} can create a case`, () => { + loginWithUserAndWaitForPageWithoutDateRange(CASES_URL, user); + goToCreateNewCase(); + fillCasesMandatoryfields(testCase); + createCase(); + backToCases(); + filterStatusOpen(); + + cy.get(ALL_CASES_NAME).should('have.text', testCase.name); + }); + } +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index 278eab29f0a62..847236688dee7 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -7,11 +7,14 @@ import { CompleteTimeline, timeline } from './timeline'; -export interface TestCase { +export interface TestCase extends TestCaseWithoutTimeline { + timeline: CompleteTimeline; +} + +export interface TestCaseWithoutTimeline { name: string; tags: string[]; description: string; - timeline: CompleteTimeline; reporter: string; owner: string; } diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index 468b0e22838dd..d726d5daa5cbc 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -106,19 +106,7 @@ export const cleanKibana = () => { }, }); - cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { - query: { - bool: { - filter: [ - { - match: { - type: 'cases', - }, - }, - ], - }, - }, - }); + deleteCases(); cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { query: { @@ -149,4 +137,21 @@ export const cleanKibana = () => { esArchiverResetKibana(); }; +export const deleteCases = () => { + const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`; + cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { + query: { + bool: { + filter: [ + { + match: { + type: 'cases', + }, + }, + ], + }, + }, + }); +}; + export const scrollToBottom = () => cy.scrollTo('bottom'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts index ed9174e2a74bb..6f1868d047c06 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts @@ -10,6 +10,7 @@ import { JiraConnectorOptions, ServiceNowconnectorOptions, TestCase, + TestCaseWithoutTimeline, } from '../objects/case'; import { ALL_CASES_OPEN_CASES_COUNT, ALL_CASES_OPEN_FILTER } from '../screens/all_cases'; @@ -46,7 +47,7 @@ export const filterStatusOpen = () => { cy.get(ALL_CASES_OPEN_FILTER).click(); }; -export const fillCasesMandatoryfields = (newCase: TestCase) => { +export const fillCasesMandatoryfields = (newCase: TestCaseWithoutTimeline) => { cy.get(TITLE_INPUT).type(newCase.name, { force: true }); newCase.tags.forEach((tag) => { cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index 0a0e578ffd382..be447993273fb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -67,6 +67,32 @@ export const getUrlWithRoute = (role: ROLES, route: string) => { return theUrl; }; +interface User { + username: string; + password: string; +} + +/** + * Builds a URL with basic auth using the passed in user. + * + * @param user the user information to build the basic auth with + * @param route string route to visit + */ +export const constructUrlWithUser = (user: User, route: string) => { + const hostname = Cypress.env('hostname'); + const username = user.username; + const password = user.password; + const protocol = Cypress.env('protocol'); + const port = Cypress.env('configport'); + + const path = `${route.startsWith('/') ? '' : '/'}${route}`; + const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`; + const builtUrl = new URL(strUrl); + + cy.log(`origin: ${builtUrl.href}`); + return builtUrl.href; +}; + export const getCurlScriptEnvVars = () => ({ ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'), ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'), @@ -102,6 +128,23 @@ export const deleteRoleAndUser = (role: ROLES) => { }); }; +export const loginWithUser = (user: User) => { + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: user.username, + password: user.password, + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: constructUrlWithUser(user, LOGIN_API_ENDPOINT), + }); +}; + export const loginWithRole = async (role: ROLES) => { postRoleAndUser(role); const theUrl = Url.format({ @@ -214,6 +257,28 @@ const loginViaConfig = () => { }); }; +/** + * Get the configured auth details that were used to spawn cypress + * + * @returns the default Elasticsearch username and password for this environment + */ +export const getEnvAuth = (): User => { + if (credentialsProvidedByEnvironment()) { + return { + username: Cypress.env(ELASTICSEARCH_USERNAME), + password: Cypress.env(ELASTICSEARCH_PASSWORD), + }; + } else { + let user: User = { username: '', password: '' }; + cy.readFile(KIBANA_DEV_YML_PATH).then((devYml) => { + const config = yaml.safeLoad(devYml); + user = { username: config.elasticsearch.username, password: config.elasticsearch.password }; + }); + + return user; + } +}; + /** * Authenticates with Kibana, visits the specified `url`, and waits for the * Kibana global nav to be displayed before continuing @@ -232,6 +297,12 @@ export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) = cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); }; +export const loginWithUserAndWaitForPageWithoutDateRange = (url: string, user: User) => { + loginWithUser(user); + cy.visit(constructUrlWithUser(user, url)); + cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); +}; + export const loginAndWaitForTimeline = (timelineId: string, role?: ROLES) => { const route = `/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`; diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index 162758a90b7ba..77fa9e8b3cc8c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { EuiGlobalToastList } from '@elastic/eui'; -import { useKibana, useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; +import { useKibana, useGetUserCasesPermissions } from '../../../common/lib/kibana'; import { useStateToaster } from '../../../common/components/toasters'; import { TestProviders } from '../../../common/mock'; import { AddToCaseAction } from './add_to_case_action'; @@ -62,7 +62,7 @@ describe('AddToCaseAction', () => { getAllCasesSelectorModal: mockAllCasesModal.mockImplementation(() => <>{'test'}), }; (useStateToaster as jest.Mock).mockReturnValue([jest.fn(), mockDispatchToaster]); - (useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({ + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ crud: true, read: true, }); @@ -201,7 +201,7 @@ describe('AddToCaseAction', () => { }); it('disabled when user does not have crud permissions', () => { - (useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({ + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ crud: false, read: true, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 19c59f2f57d87..538c1f2346101 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -27,7 +27,7 @@ import { } from '../../../common/components/link_to'; import { useStateToaster } from '../../../common/components/toasters'; import { useControl } from '../../../common/hooks/use_control'; -import { useGetUserSavedObjectPermissions, useKibana } from '../../../common/lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item'; import { CreateCaseFlyout } from '../create/flyout'; import { createUpdateSuccessToaster } from './helpers'; @@ -67,7 +67,7 @@ const AddToCaseActionComponent: React.FC = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const openPopover = useCallback(() => setIsPopoverOpen(true), []); const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const isEventSupported = !isEmpty(ecsRowData.signal?.rule?.id); const userCanCrud = userPermissions?.crud ?? false; diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx index ff7589e9deb2a..4f0163eb8190a 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { AllCases } from '../components/all_cases'; @@ -17,7 +17,7 @@ import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; import { SecurityPageName } from '../../app/types'; export const CasesPage = React.memo(() => { - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); return userPermissions == null || userPermissions?.read ? ( <> diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index 1841ca39ae853..03407c7a5adaa 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -12,7 +12,7 @@ import { SecurityPageName } from '../../app/types'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; -import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { getCaseUrl } from '../../common/components/link_to'; import { navTabs } from '../../app/home/home_navigations'; import { CaseView } from '../components/case_view'; @@ -20,7 +20,7 @@ import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/call export const CaseDetailsPage = React.memo(() => { const history = useHistory(); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const { detailName: caseId, subCaseId } = useParams<{ detailName?: string; subCaseId?: string; diff --git a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx index c735fd5bc8567..905167c232c7d 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx @@ -13,7 +13,7 @@ import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { useGetUserSavedObjectPermissions, useKibana } from '../../common/lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { navTabs } from '../../app/home/home_navigations'; import { CaseHeaderPage } from '../components/case_header_page'; @@ -24,7 +24,7 @@ import { APP_ID } from '../../../common/constants'; const ConfigureCasesPageComponent: React.FC = () => { const { cases } = useKibana().services; const history = useHistory(); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const search = useGetUrlSearch(navTabs.case); const backOptions = useMemo( diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx index 24b179f4a41bf..41344a8deb3b1 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx @@ -12,7 +12,7 @@ import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { navTabs } from '../../app/home/home_navigations'; import { CaseHeaderPage } from '../components/case_header_page'; @@ -21,7 +21,7 @@ import * as i18n from './translations'; export const CreateCasePage = React.memo(() => { const history = useHistory(); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const search = useGetUrlSearch(navTabs.case); const backOptions = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 79c7b21158005..eb0ae1ae1dee9 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -45,4 +45,4 @@ export const useToasts = jest export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); -export const useGetUserSavedObjectPermissions = jest.fn(); +export const useGetUserCasesPermissions = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index 6b5599292f6d4..4a2caefba1b97 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -138,28 +138,25 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => { return user; }; -export interface UseGetUserSavedObjectPermissions { +export interface UseGetUserCasesPermissions { crud: boolean; read: boolean; } -export const useGetUserSavedObjectPermissions = () => { - const [ - savedObjectsPermissions, - setSavedObjectsPermissions, - ] = useState(null); +export const useGetUserCasesPermissions = () => { + const [casesPermissions, setCasesPermissions] = useState(null); const uiCapabilities = useKibana().services.application.capabilities; useEffect(() => { const capabilitiesCanUserCRUD: boolean = - typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; + typeof uiCapabilities.siem.crud_cases === 'boolean' ? uiCapabilities.siem.crud_cases : false; const capabilitiesCanUserRead: boolean = - typeof uiCapabilities.siem.show === 'boolean' ? uiCapabilities.siem.show : false; - setSavedObjectsPermissions({ + typeof uiCapabilities.siem.read_cases === 'boolean' ? uiCapabilities.siem.read_cases : false; + setCasesPermissions({ crud: capabilitiesCanUserCRUD, read: capabilitiesCanUserRead, }); }, [uiCapabilities]); - return savedObjectsPermissions; + return casesPermissions; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 0f583b838d86c..dd21b33afa5b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -15,7 +15,7 @@ import { APP_ID } from '../../../../../common/constants'; import { timelineSelectors } from '../../../../timelines/store/timeline'; import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useGetUserSavedObjectPermissions, useKibana } from '../../../../common/lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; import { TimelineStatus, TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { getCreateCaseUrl, @@ -71,7 +71,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { ); const { formatUrl } = useFormatUrl(SecurityPageName.case); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const goToCreateCase = useCallback( (ev) => { ev.preventDefault(); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 153b3e04681a6..c9b898270c96c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -237,7 +237,9 @@ export class Plugin implements IPlugin + ui: ['crud_cases', 'read_cases'], // uiCapabilities.siem.crud_cases cases: { all: [APP_ID], }, @@ -250,7 +252,9 @@ export class Plugin implements IPlugin + ui: ['read_cases'], // uiCapabilities.siem.read_cases cases: { read: [APP_ID], }, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 50294201f6fbe..787ce533dbaf4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -15,6 +15,7 @@ import { ConnectorJiraTypeFields, CaseStatuses, CaseUserActionResponse, + CaseType, } from '../../../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; import { @@ -151,6 +152,10 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('unhappy path', () => { + it('should not allow creating a collection style case', async () => { + await createCase(supertest, getPostCaseRequest({ type: CaseType.collection }), 400); + }); + it('400s when bad query supplied', async () => { await supertest .post(CASES_URL)