diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts new file mode 100644 index 00000000000000..3c890a2c7ad5be --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -0,0 +1,131 @@ +/* + * 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 { OperationDetails } from '.'; +import { AuditLogger, EventCategory, EventOutcome } from '../../../security/server'; + +enum AuthorizationResult { + Unauthorized = 'Unauthorized', + Authorized = 'Authorized', +} + +export class AuthorizationAuditLogger { + private readonly auditLogger?: AuditLogger; + + constructor(logger: AuditLogger | undefined) { + this.auditLogger = logger; + } + + private createMessage({ + result, + owner, + operation, + }: { + result: AuthorizationResult; + owner?: string; + operation: OperationDetails; + }): string { + const ownerMsg = owner == null ? 'of any owner' : `with "${owner}" as the owner`; + /** + * This will take the form: + * `Unauthorized to create case with "securitySolution" as the owner` + * `Unauthorized to find cases of any owner`. + */ + return `${result} to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`; + } + + private logSuccessEvent({ + message, + operation, + username, + }: { + message: string; + operation: OperationDetails; + username?: string; + }) { + this.auditLogger?.log({ + message: `${username ?? 'unknown user'} ${message}`, + event: { + action: operation.action, + category: EventCategory.DATABASE, + type: operation.type, + outcome: EventOutcome.SUCCESS, + }, + ...(username != null && { + user: { + name: username, + }, + }), + }); + } + + public failure({ + username, + owner, + operation, + }: { + username?: string; + owner?: string; + operation: OperationDetails; + }): string { + const message = this.createMessage({ + result: AuthorizationResult.Unauthorized, + owner, + operation, + }); + this.auditLogger?.log({ + message: `${username ?? 'unknown user'} ${message}`, + event: { + action: operation.action, + category: EventCategory.DATABASE, + type: operation.type, + outcome: EventOutcome.FAILURE, + }, + // add the user information if we have it + ...(username != null && { + user: { + name: username, + }, + }), + }); + return message; + } + + public success({ + username, + operation, + owner, + }: { + username: string; + owner: string; + operation: OperationDetails; + }): string { + const message = this.createMessage({ + result: AuthorizationResult.Authorized, + owner, + operation, + }); + this.logSuccessEvent({ message, operation, username }); + return message; + } + + public bulkSuccess({ + username, + operation, + owners, + }: { + username?: string; + owners: string[]; + operation: OperationDetails; + }): string { + const message = `${AuthorizationResult.Authorized} to ${operation.verbs.present} ${ + operation.docType + } of owner: ${owners.join(', ')}`; + this.logSuccessEvent({ message, operation, username }); + return message; + } +} diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index ab6f9c0f6fef23..5a1d6af0f4a061 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -7,11 +7,11 @@ import { KibanaRequest } from 'kibana/server'; import Boom from '@hapi/boom'; -import { KueryNode } from '../../../../../src/plugins/data/server'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; -import { GetSpaceFn, ReadOperations, WriteOperations } from './types'; +import { AuthorizationFilter, GetSpaceFn } from './types'; import { getOwnersFilter } from './utils'; +import { AuthorizationAuditLogger, OperationDetails, Operations } from '.'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -21,25 +21,23 @@ export class Authorization { private readonly request: KibanaRequest; private readonly securityAuth: SecurityPluginStart['authz'] | undefined; private readonly featureCaseOwners: Set; - private readonly isAuthEnabled: boolean; - // TODO: create this - // private readonly auditLogger: AuthorizationAuditLogger; + private readonly auditLogger: AuthorizationAuditLogger; private constructor({ request, securityAuth, caseOwners, - isAuthEnabled, + auditLogger, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; caseOwners: Set; - isAuthEnabled: boolean; + auditLogger: AuthorizationAuditLogger; }) { this.request = request; this.securityAuth = securityAuth; this.featureCaseOwners = caseOwners; - this.isAuthEnabled = isAuthEnabled; + this.auditLogger = auditLogger; } /** @@ -50,13 +48,13 @@ export class Authorization { securityAuth, getSpace, features, - isAuthEnabled, + auditLogger, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; getSpace: GetSpaceFn; features: FeaturesPluginStart; - isAuthEnabled: boolean; + auditLogger: AuthorizationAuditLogger; }): Promise { // Since we need to do async operations, this static method handles that before creating the Auth class let caseOwners: Set; @@ -74,34 +72,26 @@ export class Authorization { caseOwners = new Set(); } - return new Authorization({ request, securityAuth, caseOwners, isAuthEnabled }); + return new Authorization({ request, securityAuth, caseOwners, auditLogger }); } private shouldCheckAuthorization(): boolean { return this.securityAuth?.mode?.useRbacForRequest(this.request) ?? false; } - public async ensureAuthorized(owner: string, operation: ReadOperations | WriteOperations) { - // TODO: remove - if (!this.isAuthEnabled) { - return; - } - + public async ensureAuthorized(owner: string, operation: OperationDetails) { const { securityAuth } = this; const isOwnerAvailable = this.featureCaseOwners.has(owner); - // TODO: throw if the request is not authorized if (securityAuth && this.shouldCheckAuthorization()) { - // TODO: implement ensure logic - const requiredPrivileges: string[] = [securityAuth.actions.cases.get(owner, operation)]; + const requiredPrivileges: string[] = [securityAuth.actions.cases.get(owner, operation.name)]; const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username, privileges } = await checkPrivileges({ + const { hasAllRequested, username } = await checkPrivileges({ kibana: requiredPrivileges, }); if (!isOwnerAvailable) { - // TODO: throw if any of the owner are not available /** * Under most circumstances this would have been caught by `checkPrivileges` as * a user can't have Privileges to an unknown owner, but super users @@ -109,67 +99,54 @@ export class Authorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - // TODO: audit log using `username` - throw Boom.forbidden('User does not have permissions for this owner'); + throw Boom.forbidden(this.auditLogger.failure({ username, owner, operation })); } if (hasAllRequested) { - // TODO: user authorized. log success + this.auditLogger.success({ username, operation, owner }); } else { - const authorizedPrivileges = privileges.kibana.reduce((acc, privilege) => { - if (privilege.authorized) { - return [...acc, privilege.privilege]; - } - return acc; - }, []); - - const unauthorizedPrivilages = requiredPrivileges.filter( - (privilege) => !authorizedPrivileges.includes(privilege) - ); - - // TODO: audit log - // TODO: User unauthorized. throw an error. authorizedPrivileges & unauthorizedPrivilages are needed for logging. - throw Boom.forbidden('Not authorized for this owner'); + throw Boom.forbidden(this.auditLogger.failure({ owner, operation, username })); } } else if (!isOwnerAvailable) { - // TODO: throw an error - throw Boom.forbidden('Security is disabled but no owner was found'); + throw Boom.forbidden(this.auditLogger.failure({ owner, operation })); } // else security is disabled so let the operation proceed } - public async getFindAuthorizationFilter( - savedObjectType: string - ): Promise<{ - filter?: KueryNode; - ensureSavedObjectIsAuthorized: (owner: string) => void; - }> { + public async getFindAuthorizationFilter(savedObjectType: string): Promise { const { securityAuth } = this; + const operation = Operations.findCases; if (securityAuth && this.shouldCheckAuthorization()) { - const { authorizedOwners } = await this.getAuthorizedOwners([ReadOperations.Find]); + const { username, authorizedOwners } = await this.getAuthorizedOwners([operation]); if (!authorizedOwners.length) { - // TODO: Better error message, log error - throw Boom.forbidden('Not authorized for this owner'); + throw Boom.forbidden(this.auditLogger.failure({ username, operation })); } return { filter: getOwnersFilter(savedObjectType, authorizedOwners), ensureSavedObjectIsAuthorized: (owner: string) => { if (!authorizedOwners.includes(owner)) { - // TODO: log error - throw Boom.forbidden('Not authorized for this owner'); + throw Boom.forbidden(this.auditLogger.failure({ username, operation, owner })); + } + }, + logSuccessfulAuthorization: () => { + if (authorizedOwners.length) { + this.auditLogger.bulkSuccess({ username, owners: authorizedOwners, operation }); } }, }; } - return { ensureSavedObjectIsAuthorized: (owner: string) => {} }; + return { + ensureSavedObjectIsAuthorized: (owner: string) => {}, + logSuccessfulAuthorization: () => {}, + }; } private async getAuthorizedOwners( - operations: Array + operations: OperationDetails[] ): Promise<{ username?: string; hasAllRequested: boolean; @@ -182,7 +159,7 @@ export class Authorization { for (const owner of featureCaseOwners) { for (const operation of operations) { - requiredPrivileges.set(securityAuth.actions.cases.get(owner, operation), [owner]); + requiredPrivileges.set(securityAuth.actions.cases.get(owner, operation.name), [owner]); } } diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts new file mode 100644 index 00000000000000..3203398ff51a55 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -0,0 +1,85 @@ +/* + * 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 { EventType } from '../../../security/server'; +import { CASE_SAVED_OBJECT } from '../../common/constants'; +import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; + +export * from './authorization'; +export * from './audit_logger'; +export * from './types'; + +const createVerbs: Verbs = { + present: 'create', + progressive: 'creating', + past: 'created', +}; + +const accessVerbs: Verbs = { + present: 'access', + progressive: 'accessing', + past: 'accessed', +}; + +const updateVerbs: Verbs = { + present: 'update', + progressive: 'updating', + past: 'updated', +}; + +const deleteVerbs: Verbs = { + present: 'delete', + progressive: 'deleting', + past: 'deleted', +}; + +/** + * Definition of all APIs within the cases backend. + */ +export const Operations: Record = { + // case operations + [WriteOperations.CreateCase]: { + type: EventType.CREATION, + name: WriteOperations.CreateCase, + action: 'create-case', + verbs: createVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [WriteOperations.DeleteCase]: { + type: EventType.DELETION, + name: WriteOperations.DeleteCase, + action: 'delete-case', + verbs: deleteVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [WriteOperations.UpdateCase]: { + type: EventType.CHANGE, + name: WriteOperations.UpdateCase, + action: 'update-case', + verbs: updateVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.GetCase]: { + type: EventType.ACCESS, + name: ReadOperations.GetCase, + action: 'get-case', + verbs: accessVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.FindCases]: { + type: EventType.ACCESS, + name: ReadOperations.FindCases, + action: 'find-cases', + verbs: accessVerbs, + docType: 'cases', + savedObjectType: CASE_SAVED_OBJECT, + }, +}; diff --git a/x-pack/plugins/cases/server/authorization/types.ts b/x-pack/plugins/cases/server/authorization/types.ts index 07249d858c1872..91b7c0f1180d93 100644 --- a/x-pack/plugins/cases/server/authorization/types.ts +++ b/x-pack/plugins/cases/server/authorization/types.ts @@ -6,20 +6,52 @@ */ import { KibanaRequest } from 'kibana/server'; +import { KueryNode } from 'src/plugins/data/common'; +import { EventType } from '../../../security/server'; import { Space } from '../../../spaces/server'; +/** + * The tenses for describing the action performed by a API route + */ +export interface Verbs { + present: string; + progressive: string; + past: string; +} + export type GetSpaceFn = (request: KibanaRequest) => Promise; // TODO: we need to have an operation per entity route so I think we need to create a bunch like // getCase, getComment, getSubCase etc for each, need to think of a clever way of creating them for all the routes easily? export enum ReadOperations { - Get = 'get', - Find = 'find', + GetCase = 'getCase', + FindCases = 'findCases', } // TODO: comments export enum WriteOperations { - Create = 'create', - Delete = 'delete', - Update = 'update', + CreateCase = 'createCase', + DeleteCase = 'deleteCase', + UpdateCase = 'updateCase', +} + +/** + * Defines the structure for a case API route. + */ +export interface OperationDetails { + type: EventType; + name: ReadOperations | WriteOperations; + action: string; + verbs: Verbs; + docType: string; + savedObjectType: string; +} + +/** + * Defines the helper methods and necessary information for authorizing the find API's request. + */ +export interface AuthorizationFilter { + filter?: KueryNode; + ensureSavedObjectIsAuthorized: (owner: string) => void; + logSuccessfulAuthorization: () => void; } diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 9c9bf1fa7641d2..a77bfa01e6ec8d 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -57,6 +57,7 @@ export const createCasesSubClient = ( userActionService, logger, authorization, + auditLogger, } = args; const casesSubClient: CasesSubClient = { @@ -70,6 +71,7 @@ export const createCasesSubClient = ( theCase, logger, auth: authorization, + auditLogger, }), find: (options: CasesFindRequest) => find({ @@ -78,6 +80,7 @@ export const createCasesSubClient = ( logger, auth: authorization, options, + auditLogger, }), get: (params: CaseGet) => get({ diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index bd9f4da2b0131c..1542b025ab96cd 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -26,6 +26,11 @@ describe('create', () => { const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; spyOnDate.mockImplementation(() => ({ toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + // when we create a case we generate an ID that is used for the saved object. Internally the ID generation code + // calls Date.getTime so we need it to return something even though the inject saved object client is going to + // override it with a different ID anyway + // Otherwise we'll get an error when the function is called + getTime: jest.fn().mockReturnValue(1), })); }); @@ -45,7 +50,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -57,7 +62,6 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -80,6 +84,7 @@ describe('create', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-it", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -121,7 +126,7 @@ describe('create', () => { "connector", "settings", ], - "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"owner\\":\\"awesome\\"}", + "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"owner\\":\\"securitySolution\\"}", "old_value": null, }, "references": Array [ @@ -151,7 +156,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -162,7 +167,6 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -181,6 +185,7 @@ describe('create', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-it", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -216,7 +221,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -230,7 +235,6 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -249,6 +253,7 @@ describe('create', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-it", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -429,7 +434,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -458,7 +463,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 935ca6d3199d2f..61f36050758502 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -9,9 +9,14 @@ import Boom from '@hapi/boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; - import type { PublicMethodsOf } from '@kbn/utility-types'; -import { SavedObjectsClientContract, Logger } from 'src/core/server'; + +import { + SavedObjectsClientContract, + Logger, + SavedObjectsUtils, +} from '../../../../../../src/core/server'; + import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; import { @@ -33,8 +38,10 @@ import { import { CaseConfigureService, CaseService, CaseUserActionService } from '../../services'; import { createCaseError } from '../../common/error'; import { Authorization } from '../../authorization/authorization'; -import { WriteOperations } from '../../authorization/types'; +import { Operations } from '../../authorization'; +import { AuditLogger, EventOutcome } from '../../../../security/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { createAuditMsg } from '../../common'; interface CreateCaseArgs { caseConfigureService: CaseConfigureService; @@ -45,6 +52,7 @@ interface CreateCaseArgs { theCase: CasePostRequest; logger: Logger; auth: PublicMethodsOf; + auditLogger?: AuditLogger; } /** @@ -59,6 +67,7 @@ export const create = async ({ theCase, logger, auth, + auditLogger, }: CreateCaseArgs): Promise => { // default to an individual case if the type is not defined. const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; @@ -79,13 +88,23 @@ export const create = async ({ ); try { + const savedObjectID = SavedObjectsUtils.generateId(); try { - await auth.ensureAuthorized(query.owner, WriteOperations.Create); + await auth.ensureAuthorized(query.owner, Operations.createCase); } catch (error) { - // TODO: log error using audit logger + auditLogger?.log(createAuditMsg({ operation: Operations.createCase, error, savedObjectID })); throw error; } + // log that we're attempting to create a case + auditLogger?.log( + createAuditMsg({ + operation: Operations.createCase, + outcome: EventOutcome.UNKNOWN, + savedObjectID, + }) + ); + // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); @@ -102,6 +121,7 @@ export const create = async ({ email, connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), }), + id: savedObjectID, }); await userActionService.bulkCreate({ diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 33545a39258893..aebecb821b4498 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -29,6 +29,9 @@ import { constructQueryOptions } from '../../routes/api/cases/helpers'; import { transformCases } from '../../routes/api/utils'; import { Authorization } from '../../authorization/authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; +import { AuthorizationFilter, Operations } from '../../authorization'; +import { AuditLogger } from '../../../../security/server'; +import { createAuditMsg } from '../../common'; interface FindParams { savedObjectsClient: SavedObjectsClientContract; @@ -36,6 +39,7 @@ interface FindParams { logger: Logger; auth: PublicMethodsOf; options: CasesFindRequest; + auditLogger?: AuditLogger; } /** @@ -46,6 +50,7 @@ export const find = async ({ caseService, logger, auth, + auditLogger, options, }: FindParams): Promise => { try { @@ -54,11 +59,19 @@ export const find = async ({ fold(throwErrors(Boom.badRequest), identity) ); - // TODO: Maybe surround it with try/catch + let authFindHelpers: AuthorizationFilter; + try { + authFindHelpers = await auth.getFindAuthorizationFilter(CASE_SAVED_OBJECT); + } catch (error) { + auditLogger?.log(createAuditMsg({ operation: Operations.findCases, error })); + throw error; + } + const { filter: authorizationFilter, ensureSavedObjectIsAuthorized, - } = await auth.getFindAuthorizationFilter(CASE_SAVED_OBJECT); + logSuccessfulAuthorization, + } = authFindHelpers; const queryArgs = { tags: queryParams.tags, @@ -89,7 +102,18 @@ export const find = async ({ }); for (const theCase of cases.casesMap.values()) { - ensureSavedObjectIsAuthorized(theCase.owner); + try { + ensureSavedObjectIsAuthorized(theCase.owner); + // log each of the found cases + auditLogger?.log( + createAuditMsg({ operation: Operations.findCases, savedObjectID: theCase.id }) + ); + } catch (error) { + auditLogger?.log( + createAuditMsg({ operation: Operations.findCases, error, savedObjectID: theCase.id }) + ); + throw error; + } } // TODO: Make sure we do not leak information when authorization is on @@ -104,6 +128,8 @@ export const find = async ({ }), ]); + logSuccessfulAuthorization(); + return CasesFindResponseRt.encode( transformCases({ casesMap: cases.casesMap, diff --git a/x-pack/plugins/cases/server/client/cases/update.test.ts b/x-pack/plugins/cases/server/client/cases/update.test.ts index 79c3b2838c3b20..1269545bf485c3 100644 --- a/x-pack/plugins/cases/server/client/cases/update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/update.test.ts @@ -68,6 +68,7 @@ describe('update', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -164,6 +165,7 @@ describe('update', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -232,6 +234,7 @@ describe('update', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-4", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -372,6 +375,7 @@ describe('update', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index d622861ac65b40..87a2b9583dac07 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -24,6 +24,7 @@ import { AttachmentService, } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; +import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; interface CasesClientFactoryArgs { @@ -37,7 +38,6 @@ interface CasesClientFactoryArgs { securityPluginStart?: SecurityPluginStart; getSpace: GetSpaceFn; featuresPluginStart: FeaturesPluginStart; - isAuthEnabled: boolean; } /** @@ -85,12 +85,14 @@ export class CasesClientFactory { ); } + const auditLogger = this.options.securityPluginSetup?.audit.asScoped(request); + const auth = await Authorization.create({ request, securityAuth: this.options.securityPluginStart?.authz, getSpace: this.options.getSpace, features: this.options.featuresPluginStart, - isAuthEnabled: this.options.isAuthEnabled, + auditLogger: new AuthorizationAuditLogger(auditLogger), }); const user = this.options.caseService.getUser({ request }); @@ -109,6 +111,7 @@ export class CasesClientFactory { attachmentService: this.options.attachmentService, logger: this.logger, authorization: auth, + auditLogger, }); } } diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 174904c1f66be6..cf964e5e53c4fe 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -22,8 +22,8 @@ import { import { CasesClient } from './types'; import { authenticationMock } from '../routes/api/__fixtures__'; import { featuresPluginMock } from '../../../features/server/mocks'; -import { securityMock } from '../../../security/server/mocks'; import { CasesClientFactory } from './factory'; +import { KibanaFeature } from '../../../features/common'; export type CasesClientPluginContractMock = jest.Mocked; export const createExternalCasesClientMock = (): CasesClientPluginContractMock => ({ @@ -83,6 +83,13 @@ export const createCasesClientWithMockSavedObjectsClient = async ({ const savedObjectsService = savedObjectsServiceMock.createStartContract(); savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + // create a fake feature + const featureStart = featuresPluginMock.createStart(); + featureStart.getKibanaFeatures.mockReturnValue([ + // all the authorization class cares about is the `cases` field in the kibana feature so just cast it to that + ({ cases: ['securitySolution'] } as unknown) as KibanaFeature, + ]); + const factory = new CasesClientFactory(log); factory.initialize({ alertsService, @@ -90,11 +97,9 @@ export const createCasesClientWithMockSavedObjectsClient = async ({ caseService, connectorMappingsService, userActionService, - featuresPluginStart: featuresPluginMock.createStart(), + featuresPluginStart: featureStart, getSpace: async (req: KibanaRequest) => undefined, - isAuthEnabled: false, - securityPluginSetup: securityMock.createSetup(), - securityPluginStart: securityMock.createStart(), + // intentionally not passing the security plugin so that security will be disabled }); // create a single reference to the caseClient so we can mock its methods diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 0592dd321819de..7d50fdbb533826 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -8,6 +8,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { User } from '../../common/api'; +import { AuditLogger } from '../../../security/server'; import { Authorization } from '../authorization/authorization'; import { AlertServiceContract, @@ -30,4 +31,5 @@ export interface CasesClientArgs { readonly attachmentService: AttachmentService; readonly logger: Logger; readonly authorization: PublicMethodsOf; + readonly auditLogger?: AuditLogger; } diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 36f5dc9cbb00a8..af638c39d66093 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -6,6 +6,7 @@ */ import { SavedObjectsFindResult, SavedObjectsFindResponse } from 'kibana/server'; +import { AuditEvent, EventCategory, EventOutcome } from '../../../security/server'; import { CaseStatuses, CommentAttributes, @@ -13,6 +14,7 @@ import { CommentType, User, } from '../../common/api'; +import { OperationDetails } from '../authorization'; import { UpdateAlertRequest } from '../client/alerts/client'; import { getAlertInfoFromComments } from '../routes/api/utils'; @@ -97,3 +99,49 @@ export const countAlertsForID = ({ }): number | undefined => { return groupTotalAlertsByID({ comments }).get(id); }; + +/** + * Creates an AuditEvent describing the state of a request. + */ +export function createAuditMsg({ + operation, + outcome, + error, + savedObjectID, +}: { + operation: OperationDetails; + savedObjectID?: string; + outcome?: EventOutcome; + error?: Error; +}): AuditEvent { + const doc = + savedObjectID != null + ? `${operation.savedObjectType} [id=${savedObjectID}]` + : `a ${operation.docType}`; + const message = error + ? `Failed attempt to ${operation.verbs.present} ${doc}` + : outcome === EventOutcome.UNKNOWN + ? `User is ${operation.verbs.progressive} ${doc}` + : `User has ${operation.verbs.past} ${doc}`; + + return { + message, + event: { + action: operation.action, + category: EventCategory.DATABASE, + type: operation.type, + outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS), + }, + ...(savedObjectID != null && { + kibana: { + saved_object: { type: operation.savedObjectType, id: savedObjectID }, + }, + }), + ...(error != null && { + error: { + code: error.name, + message: error.message, + }, + }), + }; +} diff --git a/x-pack/plugins/cases/server/config.ts b/x-pack/plugins/cases/server/config.ts index c4dca0f9ff9559..7679a5a389051c 100644 --- a/x-pack/plugins/cases/server/config.ts +++ b/x-pack/plugins/cases/server/config.ts @@ -9,8 +9,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - // TODO: remove once authorization is complete - enableAuthorization: schema.boolean({ defaultValue: false }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 95fe562d9e140d..edf7e3d3fdbf17 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -61,7 +61,6 @@ describe('case connector', () => { userActionService, featuresPluginStart: featuresPluginMock.createStart(), getSpace: async (req: KibanaRequest) => undefined, - isAuthEnabled: true, securityPluginSetup: securityMock.createSetup(), securityPluginStart: securityMock.createStart(), }); @@ -1130,7 +1129,6 @@ describe('case connector', () => { totalComment: 0, totalAlerts: 0, version: 'WzksMV0=', - closed_at: null, closed_by: null, connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 2ccc362280b9f7..8a504ce73dee8b 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -62,7 +62,6 @@ export class CasePlugin { private attachmentService?: AttachmentService; private clientFactory: CasesClientFactory; private securityPluginSetup?: SecurityPluginSetup; - private config?: ConfigType; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get('plugins', 'cases'); @@ -76,8 +75,6 @@ export class CasePlugin { return; } - // save instance variables for the client factor initialization call - this.config = config; this.securityPluginSetup = plugins.security; core.savedObjects.registerType(caseCommentSavedObjectType); @@ -146,8 +143,6 @@ export class CasePlugin { return plugins.spaces?.spacesService.getActiveSpace(request); }, featuresPluginStart: plugins.features, - // we'll be removing this eventually but let's just default it to false if it wasn't specified explicitly in the config file - isAuthEnabled: this.config?.enableAuthorization ?? false, }); const getCasesClientWithRequestAndContext = async ( diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts index 3306712c1e550f..284b01ce993258 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts @@ -20,12 +20,11 @@ import { CaseUserActionService, } from '../../../services'; import { authenticationMock } from '../__fixtures__'; -import type { CasesRequestHandlerContext } from '../../../types'; import { createActionsClient } from './mock_actions_client'; import { featuresPluginMock } from '../../../../../features/server/mocks'; -import { securityMock } from '../../../../../security/server/mocks'; import { CasesClientFactory } from '../../../client/factory'; import { xpackMocks } from '../../../../../../mocks'; +import { KibanaFeature } from '../../../../../features/common'; export const createRouteContext = async (client: any, badAuth = false) => { const actionsMock = createActionsClient(); @@ -56,6 +55,13 @@ export const createRouteContext = async (client: any, badAuth = false) => { contextMock.core.savedObjects.getClient = jest.fn(() => client); contextMock.core.savedObjects.client = client; + // create a fake feature + const featureStart = featuresPluginMock.createStart(); + featureStart.getKibanaFeatures.mockReturnValue([ + // all the authorization class cares about is the `cases` field in the kibana feature so just cast it to that + ({ cases: ['securitySolution'] } as unknown) as KibanaFeature, + ]); + const factory = new CasesClientFactory(log); factory.initialize({ alertsService, @@ -63,11 +69,9 @@ export const createRouteContext = async (client: any, badAuth = false) => { caseService, connectorMappingsService, userActionService, - featuresPluginStart: featuresPluginMock.createStart(), + featuresPluginStart: featureStart, getSpace: async (req: KibanaRequest) => undefined, - isAuthEnabled: false, - securityPluginSetup: securityMock.createSetup(), - securityPluginStart: securityMock.createStart(), + // intentionally not passing the security plugin so that security will be disabled }); // create a single reference to the caseClient so we can mock its methods @@ -79,13 +83,13 @@ export const createRouteContext = async (client: any, badAuth = false) => { scopedClusterClient: esClient, }); - const context = ({ + const context = { ...contextMock, actions: { getActionsClient: () => actionsMock }, cases: { getCasesClient: async () => caseClient, }, - } as unknown) as CasesRequestHandlerContext; + }; return { context, services: { userActionService } }; }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts index 0735671384845b..5f6e25f6c8a6d6 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts @@ -144,12 +144,13 @@ describe('GET configuration', () => { cases: { ...context.cases, getCasesClient: async () => { - return { + return ({ ...(await context?.cases?.getCasesClient()), - getMappings: () => { + getMappings: async () => { throw new Error(); }, - } as CasesClient; + // This avoids ts errors with overriding getMappings + } as unknown) as CasesClient; }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts index a131061f2ba86d..f94d2e462a336f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts @@ -164,12 +164,13 @@ describe('PATCH configuration', () => { cases: { ...context.cases, getCasesClient: async () => { - return { + return ({ ...(await context?.cases?.getCasesClient()), getMappings: () => { throw new Error(); }, - } as CasesClient; + // This avoids ts errors with overriding getMappings + } as unknown) as CasesClient; }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts index db0488d87dc5cb..e690d9f870c343 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts @@ -84,12 +84,13 @@ describe('POST configuration', () => { cases: { ...context.cases, getCasesClient: async () => { - return { + return ({ ...(await context?.cases?.getCasesClient()), getMappings: () => { throw new Error(); }, - } as CasesClient; + // This avoids ts errors with overriding getMappings + } as unknown) as CasesClient; }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts index b3f87211c95475..073c447460875f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts @@ -77,6 +77,7 @@ describe('PATCH cases', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -151,6 +152,7 @@ describe('PATCH cases', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-4", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -220,6 +222,7 @@ describe('PATCH cases', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts index d75dcada0a9638..3991340612c745 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts @@ -46,7 +46,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -86,7 +86,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -120,7 +120,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -146,7 +146,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -180,7 +180,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -196,7 +196,6 @@ describe('POST cases', () => { expect(response.status).toEqual(200); expect(response.payload).toMatchInlineSnapshot(` Object { - "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -215,6 +214,7 @@ describe('POST cases', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-it", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, diff --git a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts index 1c399a415e4704..ca12ed9c92831b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts @@ -16,6 +16,7 @@ import { } from '../../__fixtures__'; import { initGetCasesStatusApi } from './get_status'; import { CASE_STATUS_URL } from '../../../../../common/constants'; +import { esKuery } from 'src/plugins/data/server'; import { CaseType } from '../../../../../common/api'; describe('GET status', () => { @@ -47,17 +48,23 @@ describe('GET status', () => { const response = await routeHandler(context, request, kibanaResponseFactory); expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, { ...findArgs, - filter: `((cases.attributes.status: open AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})`, + filter: esKuery.fromKueryExpression( + `((cases.attributes.status: open AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` + ), }); expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, { ...findArgs, - filter: `((cases.attributes.status: in-progress AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})`, + filter: esKuery.fromKueryExpression( + `((cases.attributes.status: in-progress AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` + ), }); expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, { ...findArgs, - filter: `((cases.attributes.status: closed AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})`, + filter: esKuery.fromKueryExpression( + `((cases.attributes.status: closed AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` + ), }); expect(response.payload).toEqual({ diff --git a/x-pack/plugins/cases/server/routes/api/utils.test.ts b/x-pack/plugins/cases/server/routes/api/utils.test.ts index f6bc1e4f718971..99d2c1509538cc 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.test.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.test.ts @@ -87,6 +87,7 @@ describe('Utils', () => { }, "description": "A description", "external_service": null, + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -143,6 +144,7 @@ describe('Utils', () => { }, "description": "A description", "external_service": null, + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -202,6 +204,7 @@ describe('Utils', () => { }, "description": "A description", "external_service": null, + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -397,6 +400,7 @@ describe('Utils', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -437,6 +441,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie destroying data!", "external_service": null, "id": "mock-id-2", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -481,6 +486,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -529,6 +535,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-4", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -594,6 +601,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -650,6 +658,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -729,6 +738,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index bbb82214d70a54..99d6129dc54b3e 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -105,6 +105,7 @@ interface FindSubCasesStatusStats { interface PostCaseArgs extends ClientArgs { attributes: ESCaseAttributes; + id: string; } interface CreateSubCaseArgs extends ClientArgs { @@ -933,12 +934,10 @@ export class CaseService { } } - public async postNewCase({ soClient, attributes }: PostCaseArgs) { + public async postNewCase({ soClient, attributes, id }: PostCaseArgs) { try { this.log.debug(`Attempting to POST a new case`); - return await soClient.create(CASE_SAVED_OBJECT, { - ...attributes, - }); + return await soClient.create(CASE_SAVED_OBJECT, attributes, { id }); } catch (error) { this.log.error(`Error on POST a new case: ${error}`); throw error; diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap index 2208105694fe9f..33140f180ad0ad 100644 --- a/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap @@ -1,17 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#get class of "" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "{}" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "1" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "null" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "true" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "undefined" 1`] = `"class is required and must be a string"`; - exports[`#get operation of "" 1`] = `"operation is required and must be a string"`; exports[`#get operation of "{}" 1`] = `"operation is required and must be a string"`; @@ -23,3 +11,15 @@ exports[`#get operation of "null" 1`] = `"operation is required and must be a st exports[`#get operation of "true" 1`] = `"operation is required and must be a string"`; exports[`#get operation of "undefined" 1`] = `"operation is required and must be a string"`; + +exports[`#get owner of "" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "{}" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "1" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "null" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "true" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "undefined" 1`] = `"owner is required and must be a string"`; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index 55920aabe993d8..1b1932f8640906 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -70,8 +70,8 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "cases:1.0.0-zeta1:observability/get", - "cases:1.0.0-zeta1:observability/find", + "cases:1.0.0-zeta1:observability/getCase", + "cases:1.0.0-zeta1:observability/findCases", ] `); }); @@ -105,11 +105,11 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "cases:1.0.0-zeta1:security/get", - "cases:1.0.0-zeta1:security/find", - "cases:1.0.0-zeta1:security/create", - "cases:1.0.0-zeta1:security/delete", - "cases:1.0.0-zeta1:security/update", + "cases:1.0.0-zeta1:security/getCase", + "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/createCase", + "cases:1.0.0-zeta1:security/deleteCase", + "cases:1.0.0-zeta1:security/updateCase", ] `); }); @@ -144,13 +144,13 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "cases:1.0.0-zeta1:security/get", - "cases:1.0.0-zeta1:security/find", - "cases:1.0.0-zeta1:security/create", - "cases:1.0.0-zeta1:security/delete", - "cases:1.0.0-zeta1:security/update", - "cases:1.0.0-zeta1:obs/get", - "cases:1.0.0-zeta1:obs/find", + "cases:1.0.0-zeta1:security/getCase", + "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/createCase", + "cases:1.0.0-zeta1:security/deleteCase", + "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:obs/getCase", + "cases:1.0.0-zeta1:obs/findCases", ] `); }); @@ -185,20 +185,20 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "cases:1.0.0-zeta1:security/get", - "cases:1.0.0-zeta1:security/find", - "cases:1.0.0-zeta1:security/create", - "cases:1.0.0-zeta1:security/delete", - "cases:1.0.0-zeta1:security/update", - "cases:1.0.0-zeta1:other-security/get", - "cases:1.0.0-zeta1:other-security/find", - "cases:1.0.0-zeta1:other-security/create", - "cases:1.0.0-zeta1:other-security/delete", - "cases:1.0.0-zeta1:other-security/update", - "cases:1.0.0-zeta1:obs/get", - "cases:1.0.0-zeta1:obs/find", - "cases:1.0.0-zeta1:other-obs/get", - "cases:1.0.0-zeta1:other-obs/find", + "cases:1.0.0-zeta1:security/getCase", + "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/createCase", + "cases:1.0.0-zeta1:security/deleteCase", + "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:other-security/getCase", + "cases:1.0.0-zeta1:other-security/findCases", + "cases:1.0.0-zeta1:other-security/createCase", + "cases:1.0.0-zeta1:other-security/deleteCase", + "cases:1.0.0-zeta1:other-security/updateCase", + "cases:1.0.0-zeta1:obs/getCase", + "cases:1.0.0-zeta1:obs/findCases", + "cases:1.0.0-zeta1:other-obs/getCase", + "cases:1.0.0-zeta1:other-obs/findCases", ] `); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index aacff3082fbca2..8608653c41b345 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -10,8 +10,10 @@ import { uniq } from 'lodash'; import type { FeatureKibanaPrivileges, KibanaFeature } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; -const readOperations: string[] = ['get', 'find']; -const writeOperations: string[] = ['create', 'delete', 'update']; +// if you add a value here you'll likely also need to make changes here: +// x-pack/plugins/cases/server/authorization/index.ts +const readOperations: string[] = ['getCase', 'findCases']; +const writeOperations: string[] = ['createCase', 'deleteCase', 'updateCase']; const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 0fa6c553c2e80b..574e37fdd18413 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -83,6 +83,9 @@ describe('Security Plugin', () => { "app": AppActions { "prefix": "app:version:", }, + "cases": CasesActions { + "prefix": "cases:version:", + }, "login": "login:", "savedObject": SavedObjectActions { "prefix": "saved_object:version:", @@ -150,6 +153,9 @@ describe('Security Plugin', () => { "app": AppActions { "prefix": "app:version:", }, + "cases": CasesActions { + "prefix": "cases:version:", + }, "login": "login:", "savedObject": SavedObjectActions { "prefix": "saved_object:version:", diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 3ac0084e96fb3b..27f702431e8982 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -769,6 +769,7 @@ describe('AllCases', () => { }, }, id: '1', + owner: 'securitySolution', status: 'open', subCaseIds: [], tags: ['coke', 'pepsi'], diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 4559f6000493f4..947de140ccbb0d 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -75,6 +75,7 @@ export const alertComment: Comment = { export const basicCase: Case = { type: CaseType.individual, + owner: 'securitySolution', closedAt: null, closedBy: null, id: basicCaseId, @@ -105,6 +106,7 @@ export const basicCase: Case = { export const collectionCase: Case = { type: CaseType.collection, + owner: 'securitySolution', closedAt: null, closedBy: null, id: 'collection-id', diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index 6feb5a1501a76b..66636d2e547041 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -56,6 +56,7 @@ export interface CaseExternalService { interface BasicCase { id: string; + owner: string; closedAt: string | null; closedBy: ElasticUser | null; comments: Comment[]; diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 9b6c066c3f813b..0d9a1030d68088 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -87,7 +87,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.cases.enableAuthorization=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), // Actions simulators plugin. Needed for testing push to external services.