Skip to content

Commit

Permalink
Starting stats apis
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathan-buttner committed Apr 29, 2021
1 parent 6cfd24f commit efc6e82
Show file tree
Hide file tree
Showing 17 changed files with 91 additions and 30 deletions.
10 changes: 5 additions & 5 deletions x-pack/plugins/cases/common/api/cases/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CommentResponseRt } from './comment';
import { CasesStatusResponseRt, CaseStatusRt } from './status';
import { CaseConnectorRt, ESCaseConnector } from '../connectors';
import { SubCaseResponseRt } from './sub_case';
import { OWNER_FIELD } from '.';

export enum CaseType {
collection = 'collection',
Expand All @@ -38,8 +39,7 @@ const CaseBasicRt = rt.type({
[caseTypeField]: CaseTypeRt,
connector: CaseConnectorRt,
settings: SettingsRt,
// TODO: should a user be able to update the owner?
owner: rt.string,
[OWNER_FIELD]: rt.string,
});

const CaseExternalServiceBasicRt = rt.type({
Expand Down Expand Up @@ -80,7 +80,7 @@ const CasePostRequestNoTypeRt = rt.type({
title: rt.string,
connector: CaseConnectorRt,
settings: SettingsRt,
owner: rt.string,
[OWNER_FIELD]: rt.string,
});

/**
Expand Down Expand Up @@ -115,7 +115,7 @@ export const CasesFindRequestRt = rt.partial({
searchFields: rt.union([rt.array(rt.string), rt.string]),
sortField: rt.string,
sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]),
owner: rt.union([rt.array(rt.string), rt.string]),
[OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]),
});

export const CaseResponseRt = rt.intersection([
Expand Down Expand Up @@ -177,7 +177,7 @@ export const ExternalServiceResponseRt = rt.intersection([
]);

export const AllTagsFindRequestRt = rt.partial({
owner: rt.union([rt.array(rt.string), rt.string]),
[OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]),
});

export const AllReportersFindRequestRt = AllTagsFindRequestRt;
Expand Down
7 changes: 4 additions & 3 deletions x-pack/plugins/cases/common/api/cases/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import * as rt from 'io-ts';
import { OWNER_FIELD } from '.';
import { SavedObjectFindOptionsRt } from '../saved_object';

import { UserRT } from '../user';
Expand All @@ -28,7 +29,7 @@ export const CommentAttributesBasicRt = rt.type({
]),
created_at: rt.string,
created_by: UserRT,
owner: rt.string,
[OWNER_FIELD]: rt.string,
pushed_at: rt.union([rt.string, rt.null]),
pushed_by: rt.union([UserRT, rt.null]),
updated_at: rt.union([rt.string, rt.null]),
Expand All @@ -44,7 +45,7 @@ export enum CommentType {
export const ContextTypeUserRt = rt.type({
comment: rt.string,
type: rt.literal(CommentType.user),
owner: rt.string,
[OWNER_FIELD]: rt.string,
});

/**
Expand All @@ -60,7 +61,7 @@ export const AlertCommentRequestRt = rt.type({
id: rt.union([rt.string, rt.null]),
name: rt.union([rt.string, rt.null]),
}),
owner: rt.string,
[OWNER_FIELD]: rt.string,
});

const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]);
Expand Down
11 changes: 7 additions & 4 deletions x-pack/plugins/cases/common/api/cases/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ import * as rt from 'io-ts';
import { UserRT } from '../user';
import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors';
import { OmitProp } from '../runtime_types';
import { OWNER_FIELD } from '.';

// TODO: we will need to add this type rt.literal('close-by-third-party')
const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]);

const CasesConfigureBasicRt = rt.type({
connector: CaseConnectorRt,
closure_type: ClosureTypeRT,
owner: rt.string,
[OWNER_FIELD]: rt.string,
});

const CasesConfigureBasicWithoutOwnerRt = rt.type(OmitProp(CasesConfigureBasicRt.props, 'owner'));
const CasesConfigureBasicWithoutOwnerRt = rt.type(
OmitProp(CasesConfigureBasicRt.props, OWNER_FIELD)
);

export const CasesConfigureRequestRt = CasesConfigureBasicRt;
export const CasesConfigurePatchRt = rt.intersection([
Expand All @@ -45,12 +48,12 @@ export const CaseConfigureResponseRt = rt.intersection([
id: rt.string,
version: rt.string,
error: rt.union([rt.string, rt.null]),
owner: rt.string,
[OWNER_FIELD]: rt.string,
}),
]);

export const GetConfigureFindRequestRt = rt.partial({
owner: rt.union([rt.array(rt.string), rt.string]),
[OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]),
});

export const CaseConfigureRequestParamsRt = rt.type({
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/cases/common/api/cases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export * from './comment';
export * from './status';
export * from './user_actions';
export * from './sub_case';

/**
* The field used for authorization in various entities within cases.
*/
export const OWNER_FIELD = 'owner';
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/common/api/cases/user_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import * as rt from 'io-ts';
import { OWNER_FIELD } from '.';

import { UserRT } from '../user';

Expand All @@ -22,7 +23,7 @@ const UserActionFieldTypeRt = rt.union([
rt.literal('status'),
rt.literal('settings'),
rt.literal('sub_case'),
rt.literal('owner'),
rt.literal(OWNER_FIELD),
]);
const UserActionFieldRt = rt.array(UserActionFieldTypeRt);
const UserActionRt = rt.union([
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/common/api/connectors/mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import * as rt from 'io-ts';
import { OWNER_FIELD } from '..';

const ActionTypeRT = rt.union([
rt.literal('append'),
Expand All @@ -31,7 +32,7 @@ export const ConnectorMappingsAttributesRT = rt.type({

export const ConnectorMappingsRt = rt.type({
mappings: rt.array(ConnectorMappingsAttributesRT),
owner: rt.string,
[OWNER_FIELD]: rt.string,
});

export type ConnectorMappingsAttributes = rt.TypeOf<typeof ConnectorMappingsAttributesRT>;
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/cases/server/authorization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,13 @@ export const Operations: Record<ReadOperations | WriteOperations, OperationDetai
docType: 'comments',
savedObjectType: CASE_COMMENT_SAVED_OBJECT,
},
// stats operations
[ReadOperations.GetCaseStatuses]: {
type: EVENT_TYPES.access,
name: ACCESS_CASE_OPERATION,
action: 'find-case-statuses',
verbs: accessVerbs,
docType: 'cases',
savedObjectType: CASE_SAVED_OBJECT,
},
};
1 change: 1 addition & 0 deletions x-pack/plugins/cases/server/authorization/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type GetSpaceFn = (request: KibanaRequest) => Promise<Space | undefined>;
export enum ReadOperations {
GetCase = 'getCase',
FindCases = 'findCases',
GetCaseStatuses = 'getCaseStatuses',
GetComment = 'getComment',
GetAllComments = 'getAllComments',
FindComments = 'findComments',
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/cases/server/authorization/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

import { remove, uniq } from 'lodash';
import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common';
import { OWNER_FIELD } from '../../common/api';

export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => {
return nodeBuilder.or(
owners.reduce<KueryNode[]>((query, owner) => {
ensureFieldIsSafeForQuery('owner', owner);
ensureFieldIsSafeForQuery(OWNER_FIELD, owner);
query.push(nodeBuilder.is(`${savedObjectType}.attributes.owner`, owner));
return query;
}, [])
Expand Down Expand Up @@ -53,5 +54,5 @@ export const includeFieldsRequiredForAuthentication = (fields?: string[]): strin
if (fields === undefined) {
return;
}
return uniq([...fields, 'owner']);
return uniq([...fields, OWNER_FIELD]);
};
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/server/client/cases/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
CasesClientPostRequestRt,
CasePostRequest,
CaseType,
OWNER_FIELD,
} from '../../../common/api';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
import { ensureAuthorized, getConnectorFromConfiguration } from '../utils';
Expand Down Expand Up @@ -108,7 +109,7 @@ export const create = async (
actionAt: createdDate,
actionBy: { username, full_name, email },
caseId: newCase.id,
fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', 'owner'],
fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', OWNER_FIELD],
newValue: JSON.stringify(query),
}),
],
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/server/client/cases/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AttachmentService, CaseService } from '../../services';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
import { Operations } from '../../authorization';
import { ensureAuthorized } from '../utils';
import { OWNER_FIELD } from '../../../common/api';

async function deleteSubCases({
attachmentService,
Expand Down Expand Up @@ -146,7 +147,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
'title',
'connector',
'settings',
'owner',
OWNER_FIELD,
'comment',
...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []),
],
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/cases/server/client/cases/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ export const find = async (

ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]);

// TODO: Make sure we do not leak information when authorization is on
const [openCases, inProgressCases, closedCases] = await Promise.all([
...caseStatuses.map((status) => {
const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter });
return caseService.findCaseStatusStats({
soClient: savedObjectsClient,
caseOptions: statusQuery.case,
subCaseOptions: statusQuery.subCase,
ensureSavedObjectsAreAuthorized,
});
}),
]);
Expand Down
20 changes: 18 additions & 2 deletions x-pack/plugins/cases/server/client/stats/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

import { CasesClientArgs } from '..';
import { CasesStatusResponse, CasesStatusResponseRt, caseStatuses } from '../../../common/api';
import { Operations } from '../../authorization';
import { createCaseError } from '../../common/error';
import { constructQueryOptions } from '../utils';
import { constructQueryOptions, getAuthorizationFilter } from '../utils';

/**
* Statistics API contract.
Expand All @@ -30,19 +31,34 @@ async function getStatusTotalsByType({
savedObjectsClient: soClient,
caseService,
logger,
authorization,
auditLogger,
}: CasesClientArgs): Promise<CasesStatusResponse> {
try {
const {
filter: authorizationFilter,
ensureSavedObjectsAreAuthorized,
logSuccessfulAuthorization,
} = await getAuthorizationFilter({
authorization,
operation: Operations.getCaseStatuses,
auditLogger,
});

const [openCases, inProgressCases, closedCases] = await Promise.all([
...caseStatuses.map((status) => {
const statusQuery = constructQueryOptions({ status });
const statusQuery = constructQueryOptions({ status, authorizationFilter });
return caseService.findCaseStatusStats({
soClient,
caseOptions: statusQuery.case,
subCaseOptions: statusQuery.subCase,
ensureSavedObjectsAreAuthorized,
});
}),
]);

logSuccessfulAuthorization();

return CasesStatusResponseRt.encode({
count_open_cases: openCases,
count_in_progress_cases: inProgressCases,
Expand Down
12 changes: 9 additions & 3 deletions x-pack/plugins/cases/server/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
excess,
ContextTypeUserRt,
AlertCommentRequestRt,
OWNER_FIELD,
} from '../../common/api';
import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants';
import { AuditEvent } from '../../../security/server';
Expand Down Expand Up @@ -157,7 +158,7 @@ export const combineAuthorizedAndOwnerFilter = (
): KueryNode | undefined => {
const ownerFilter = buildFilter({
filters: owner,
field: 'owner',
field: OWNER_FIELD,
operator: 'or',
type: savedObjectType,
});
Expand Down Expand Up @@ -241,7 +242,7 @@ export const constructQueryOptions = ({
operator: 'or',
});
const sortField = sortToSnake(sortByField);
const ownerFilter = buildFilter({ filters: owner ?? [], field: 'owner', operator: 'or' });
const ownerFilter = buildFilter({ filters: owner ?? [], field: OWNER_FIELD, operator: 'or' });

switch (caseType) {
case CaseType.individual: {
Expand Down Expand Up @@ -570,9 +571,14 @@ interface OwnerEntity {
id: string;
}

/**
* Function callback for making sure the found saved objects are of the authorized owner
*/
export type EnsureSOAuthCallback = (entities: OwnerEntity[]) => void;

interface AuthFilterHelpers {
filter?: KueryNode;
ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => void;
ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback;
logSuccessfulAuthorization: () => void;
}

Expand Down
Loading

0 comments on commit efc6e82

Please sign in to comment.