diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 0eecc2f191ebb9..73ca65d52e5666 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -140,7 +140,11 @@ export async function getTags( fold(throwErrors(Boom.badRequest), identity) ); - const { filter: authorizationFilter } = await getAuthorizationFilter({ + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ authorization: auth, operation: Operations.findCases, auditLogger, @@ -148,11 +152,30 @@ export async function getTags( const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); - // TODO: ensureSavedObjectsAreAuthorized + logSuccessfulAuthorization if possible - return await caseService.getTags({ + const cases = await caseService.getTags({ soClient, filter, }); + + const tags = new Set(); + const mappedCases: Array<{ + owner: string; + id: string; + }> = []; + + // Gather all necessary information in one pass + cases.saved_objects.forEach((theCase) => { + theCase.attributes.tags.forEach((tag) => tags.add(tag)); + mappedCases.push({ + id: theCase.id, + owner: theCase.attributes.owner, + }); + }); + + ensureSavedObjectsAreAuthorized(mappedCases); + logSuccessfulAuthorization(); + + return [...tags.values()]; } catch (error) { throw createCaseError({ message: `Failed to get tags: ${error}`, error, logger }); } @@ -179,7 +202,11 @@ export async function getReporters( fold(throwErrors(Boom.badRequest), identity) ); - const { filter: authorizationFilter } = await getAuthorizationFilter({ + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ authorization: auth, operation: Operations.getReporters, auditLogger, @@ -187,13 +214,34 @@ export async function getReporters( const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); - // TODO: ensureSavedObjectsAreAuthorized + logSuccessfulAuthorization if possible - const reporters = await caseService.getReporters({ + const cases = await caseService.getReporters({ soClient, filter, }); - return UsersRt.encode(reporters); + const reporters = new Map(); + const mappedCases: Array<{ + owner: string; + id: string; + }> = []; + + // Gather all necessary information in one pass + cases.saved_objects.forEach((theCase) => { + const user = theCase.attributes.created_by; + if (user.username != null) { + reporters.set(user.username, user); + } + + mappedCases.push({ + id: theCase.id, + owner: theCase.attributes.owner, + }); + }); + + ensureSavedObjectsAreAuthorized(mappedCases); + logSuccessfulAuthorization(); + + return UsersRt.encode([...reporters.values()]); } catch (error) { throw createCaseError({ message: `Failed to get reporters: ${error}`, error, logger }); } diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 691a20613b60f2..b61de9f2beb6ae 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -155,7 +155,7 @@ export const combineAuthorizedAndOwnerFilter = ( return authorizationFilter != null && ownerFilter != null ? combineFilterWithAuthorizationFilter(ownerFilter, authorizationFilter) - : ownerFilter ?? undefined; + : authorizationFilter ?? ownerFilter ?? undefined; }; /** diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index c993b4cffc2932..151edee5d512e8 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -48,7 +48,6 @@ import { SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; import { readReporters } from './read_reporters'; -import { readTags } from './read_tags'; import { ClientArgs } from '..'; interface PushedArgs { @@ -916,19 +915,54 @@ export class CaseService { } } - public async getReporters({ soClient, filter }: GetReportersArgs) { + public async getReporters({ + soClient, + filter, + }: GetReportersArgs): Promise> { try { this.log.debug(`Attempting to GET all reporters`); - return await readReporters({ soClient, filter }); + const firstReporters = await soClient.find({ + type: CASE_SAVED_OBJECT, + fields: ['created_by', 'owner'], + page: 1, + perPage: 1, + filter: cloneDeep(filter), + }); + + return await soClient.find({ + type: CASE_SAVED_OBJECT, + fields: ['created_by', 'owner'], + page: 1, + perPage: firstReporters.total, + filter: cloneDeep(filter), + }); } catch (error) { this.log.error(`Error on GET all reporters: ${error}`); throw error; } } - public async getTags({ soClient, filter }: GetTagsArgs) { + + public async getTags({ + soClient, + filter, + }: GetTagsArgs): Promise> { try { this.log.debug(`Attempting to GET all cases`); - return await readTags({ soClient, filter }); + const firstTags = await soClient.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags', 'owner'], + page: 1, + perPage: 1, + filter: cloneDeep(filter), + }); + + return await soClient.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags', 'owner'], + page: 1, + perPage: firstTags.total, + filter: cloneDeep(filter), + }); } catch (error) { this.log.error(`Error on GET cases: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/cases/read_tags.ts b/x-pack/plugins/cases/server/services/cases/read_tags.ts deleted file mode 100644 index 2c8a1adc5b4b07..00000000000000 --- a/x-pack/plugins/cases/server/services/cases/read_tags.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 { cloneDeep } from 'lodash'; -import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; - -import { KueryNode } from '../../../../../../src/plugins/data/common'; -import { CaseAttributes } from '../../../common/api'; -import { CASE_SAVED_OBJECT } from '../../../common/constants'; - -export const convertToTags = (tagObjects: Array>): string[] => - tagObjects.reduce((accum, tagObj) => { - if (tagObj && tagObj.attributes && tagObj.attributes.tags) { - return [...accum, ...tagObj.attributes.tags]; - } else { - return accum; - } - }, []); - -export const convertTagsToSet = (tagObjects: Array>): Set => { - return new Set(convertToTags(tagObjects)); -}; - -// Note: This is doing an in-memory aggregation of the tags by calling each of the case -// records in batches of this const setting and uses the fields to try to get the least -// amount of data per record back. If saved objects at some point supports aggregations -// then this should be replaced with a an aggregation call. -// Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html -export const readTags = async ({ - soClient, - filter, -}: { - soClient: SavedObjectsClientContract; - filter?: KueryNode; - perPage?: number; -}): Promise => { - const tags = await readRawTags({ soClient, filter }); - return tags; -}; - -export const readRawTags = async ({ - soClient, - filter, -}: { - soClient: SavedObjectsClientContract; - filter?: KueryNode; -}): Promise => { - const firstTags = await soClient.find({ - type: CASE_SAVED_OBJECT, - fields: ['tags'], - page: 1, - perPage: 1, - filter: cloneDeep(filter), - }); - const tags = await soClient.find({ - type: CASE_SAVED_OBJECT, - fields: ['tags'], - page: 1, - perPage: firstTags.total, - filter: cloneDeep(filter), - }); - - return Array.from(convertTagsToSet(tags.saved_objects)); -}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts index 74e1bdc1bbd5af..e34d9ccad39ac6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts @@ -41,6 +41,14 @@ export default ({ getService }: FtrProviderContext): void => { expect(reporters).to.eql([defaultUser]); }); + it('should return unique reporters', async () => { + await createCase(supertest, getPostCaseRequest()); + await createCase(supertest, getPostCaseRequest()); + const reporters = await getReporters({ supertest: supertestWithoutAuth }); + + expect(reporters).to.eql([defaultUser]); + }); + describe('rbac', () => { it('User: security solution only - should read the correct reporters', async () => { await createCase( diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts index b4c6cc68182bf7..0c7237683666fd 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts @@ -41,8 +41,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(tags).to.eql(['defacement', 'unique']); }); + it('should return unique tags', async () => { + await createCase(supertest, getPostCaseRequest()); + await createCase(supertest, getPostCaseRequest()); + + const tags = await getTags({ supertest }); + expect(tags).to.eql(['defacement']); + }); + describe('rbac', () => { - it('User: security solution only - should read the correct tags', async () => { + it('should read the correct tags', async () => { await createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }),