Skip to content

Commit

Permalink
[Cases] API to return a case's connectors information (#147295)
Browse files Browse the repository at this point in the history
This PR implements a new internal API to return the information relating
to all the connectors used throughout a case's lifespan.

Fixes: #134346

```
GET http://localhost:5601/internal/cases/<case id>/_connectors

Response

{
    "my-jira": {
        "name": "preconfigured-jira",
        "type": ".jira",
        "fields": {
            "issueType": "10001",
            "parent": null,
            "priority": null
        },
        "id": "my-jira",
        "needsToBePushed": true,
        "hasBeenPushed": false
    }
}
```

<details><summary>cURL example</summary>

```
curl --location --request GET 'http://localhost:5601/internal/cases/ae038370-91d9-11ed-97ce-c35961718f7b/_connectors' \
--header 'kbn-xsrf: hello' \
--header 'Authorization: Basic <token>' \
--data-raw ''
```

Response
```
{
    "my-jira": {
        "name": "preconfigured-jira",
        "type": ".jira",
        "fields": {
            "issueType": "10001",
            "parent": null,
            "priority": null
        },
        "id": "my-jira",
        "needsToBePushed": true,
        "hasBeenPushed": false
    }
}
```

</details>


Notable changes:
- Refactored the user actions service to move the functions that create
user actions (builders etc) to its own class `UserActionPersister`
- Refactored the `CaseUserActionService` class to pull the saved object
client, logger, and other fields passed to each function via parameters
to be wrapped in a `context` member field within the class
- Plumbed in `savedObjectsService.createSerializer` to transform a raw
elasticsearch document into the saved object representation
- Added new internal `_connectors` route and `getConnectors` client
function
- Refactored the integration tests by extracting the connector related
utility functions into their own file

## Needs to be pushed algorithm

To determine whether a case needs to be pushed for a certain connector
we follow this algorithm:
- Get all unique connectors
  - For each connector
- Find the most recent user action contain the connector's fields, this
will be in the most recent `connector` user action or if the connector
was configured only once when the case was initially created it'll be on
the `create_case` user action
    - Grab the most recent push user action if it exists
- For each push search for the connector fields that were used in that
push
- Get the most recent user action that would cause a push to occur
(title, description, tags, or comment change)
- For each connector
  - If a push does not exist, we need to push
- If a push exists but the fields do not match the field of the most
recent connector fields, we need to push because the fields changed
- If the timestamp of the most recent user action is more recent than
that of the last push (aka the user changed something since we last
pushed) we need to push

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
jonathan-buttner and kibanamachine authored Jan 17, 2023
1 parent b345f75 commit 92418a6
Show file tree
Hide file tree
Showing 44 changed files with 2,481 additions and 771 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,36 @@ const existsSchema = s.object({
})
),
});

const rangeSchema = s.object({
range: s.recordOf(
s.string(),
s.object({
lt: s.maybe(s.string()),
lte: s.maybe(s.string()),
gt: s.maybe(s.string()),
gte: s.maybe(s.string()),
})
),
});

const termValueSchema = s.object({
term: s.recordOf(s.string(), s.object({ value: s.string() })),
});

const nestedSchema = s.object({
nested: s.object({
path: s.string(),
query: s.object({
bool: s.object({
filter: s.arrayOf(termValueSchema),
}),
}),
}),
});

const arraySchema = s.arrayOf(s.oneOf([nestedSchema, rangeSchema]));

// TODO: it would be great if we could recursively build the schema since the aggregation have be nested
// For more details see how the types are defined in the elasticsearch javascript client:
// https://github.com/elastic/elasticsearch-js/blob/4ad5daeaf401ce8ebb28b940075e0a67e56ff9ce/src/api/typesWithBodyKey.ts#L5295
Expand All @@ -70,7 +100,7 @@ const boolSchema = s.object({
must_not: s.oneOf([termSchema, existsSchema]),
}),
s.object({
filter: s.oneOf([termSchema, existsSchema]),
filter: s.oneOf([termSchema, existsSchema, arraySchema]),
}),
]),
});
Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/cases/common/api/connectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ export const CaseConnectorRt = rt.intersection([
CaseUserActionConnectorRt,
]);

export const GetCaseConnectorsResponseRt = rt.record(
rt.string,
rt.intersection([
rt.type({ needsToBePushed: rt.boolean, hasBeenPushed: rt.boolean }),
rt.partial(rt.type({ latestPushDate: rt.string }).props),
CaseConnectorRt,
])
);

export type CaseUserActionConnector = rt.TypeOf<typeof CaseUserActionConnectorRt>;
export type CaseConnector = rt.TypeOf<typeof CaseConnectorRt>;
export type ConnectorTypeFields = rt.TypeOf<typeof ConnectorTypeFieldsRt>;
Expand All @@ -130,3 +139,5 @@ export type ConnectorServiceNowSIRTypeFields = rt.TypeOf<typeof ConnectorService

// we need to change these types back and forth for storing in ES (arrays overwrite, objects merge)
export type ConnectorFields = rt.TypeOf<typeof ConnectorFieldsRt>;

export type GetCaseConnectorsResponse = rt.TypeOf<typeof GetCaseConnectorsResponseRt>;
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const INTERNAL_BULK_CREATE_ATTACHMENTS_URL =
`${CASES_INTERNAL_URL}/{case_id}/attachments/_bulk_create` as const;
export const INTERNAL_SUGGEST_USER_PROFILES_URL =
`${CASES_INTERNAL_URL}/_suggest_user_profiles` as const;
export const INTERNAL_CONNECTORS_URL = `${CASES_INTERNAL_URL}/{case_id}/_connectors` as const;
export const INTERNAL_BULK_GET_CASES_URL = `${CASES_INTERNAL_URL}/_bulk_get` as const;

/**
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions x-pack/plugins/cases/server/authorization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,14 @@ export const Operations: Record<ReadOperations | WriteOperations, OperationDetai
docType: 'user actions',
savedObjectType: CASE_USER_ACTION_SAVED_OBJECT,
},
[ReadOperations.GetConnectors]: {
ecsType: EVENT_TYPES.access,
name: ACCESS_USER_ACTION_OPERATION,
action: 'case_connectors_get',
verbs: accessVerbs,
docType: 'user actions',
savedObjectType: CASE_USER_ACTION_SAVED_OBJECT,
},
[ReadOperations.GetUserActionMetrics]: {
ecsType: EVENT_TYPES.access,
name: ACCESS_USER_ACTION_OPERATION,
Expand Down
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 @@ -38,6 +38,7 @@ export enum ReadOperations {
GetReporters = 'getReporters',
FindConfigurations = 'findConfigurations',
GetUserActions = 'getUserActions',
GetConnectors = 'getConnectors',
GetAlertsAttachedToCase = 'getAlertsAttachedToCase',
GetAttachmentMetrics = 'getAttachmentMetrics',
GetCaseMetrics = 'getCaseMetrics',
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/cases/server/client/attachments/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export async function deleteAll(
concurrency: MAX_CONCURRENT_SEARCHES,
});

await userActionService.bulkCreateAttachmentDeletion({
await userActionService.creator.bulkCreateAttachmentDeletion({
caseId: caseID,
attachments: comments.saved_objects.map((comment) => ({
id: comment.id,
Expand Down Expand Up @@ -150,7 +150,7 @@ export async function deleteComment(
refresh: false,
});

await userActionService.createUserAction({
await userActionService.creator.createUserAction({
type: ActionTypes.comment,
action: Actions.delete,
caseId: id,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/cases/server/client/cases/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const create = async (
refresh: false,
});

await userActionService.createUserAction({
await userActionService.creator.createUserAction({
type: ActionTypes.create_case,
caseId: newCase.id,
user,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/cases/server/client/cases/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
options: { refresh: 'wait_for' },
});

await userActionService.bulkAuditLogCaseDeletion(
await userActionService.creator.bulkAuditLogCaseDeletion(
cases.saved_objects.map((caseInfo) => caseInfo.id)
);
} catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/cases/server/client/cases/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ export const push = async (
]);

if (shouldMarkAsClosed) {
await userActionService.createUserAction({
await userActionService.creator.createUserAction({
type: ActionTypes.status,
payload: { status: CaseStatuses.closed },
user,
Expand All @@ -271,7 +271,7 @@ export const push = async (
}
}

await userActionService.createUserAction({
await userActionService.creator.createUserAction({
type: ActionTypes.pushed,
payload: { externalService },
user,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/cases/server/client/cases/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ export const update = async (
];
}, [] as CaseResponse[]);

await userActionService.bulkCreateUpdateCase({
await userActionService.creator.bulkCreateUpdateCase({
originalCases: myCases.saved_objects,
updatedCases: updatedCases.saved_objects,
user,
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/cases/server/client/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
SavedObjectsClientContract,
IBasePath,
} from '@kbn/core/server';
import type { ISavedObjectsSerializer } from '@kbn/core-saved-objects-server';
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
import type {
AuditLogger,
Expand Down Expand Up @@ -119,8 +120,11 @@ export class CasesClientFactory {
excludedExtensions: [SECURITY_EXTENSION_ID],
});

const savedObjectsSerializer = savedObjectsService.createSerializer();

const services = this.createServices({
unsecuredSavedObjectsClient,
savedObjectsSerializer,
esClient: scopedClusterClient,
request,
auditLogger,
Expand Down Expand Up @@ -152,11 +156,13 @@ export class CasesClientFactory {

private createServices({
unsecuredSavedObjectsClient,
savedObjectsSerializer,
esClient,
request,
auditLogger,
}: {
unsecuredSavedObjectsClient: SavedObjectsClientContract;
savedObjectsSerializer: ISavedObjectsSerializer;
esClient: ElasticsearchClient;
request: KibanaRequest;
auditLogger: AuditLogger;
Expand Down Expand Up @@ -201,6 +207,7 @@ export class CasesClientFactory {
log: this.logger,
persistableStateAttachmentTypeRegistry: this.options.persistableStateAttachmentTypeRegistry,
unsecuredSavedObjectsClient,
savedObjectsSerializer,
auditLogger,
}),
attachmentService,
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cases/server/client/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type UserActionsSubClientMock = jest.Mocked<UserActionsSubClient>;
const createUserActionsSubClientMock = (): UserActionsSubClientMock => {
return {
getAll: jest.fn(),
getConnectors: jest.fn(),
};
};

Expand Down
20 changes: 8 additions & 12 deletions x-pack/plugins/cases/server/client/user_actions/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,12 @@
* 2.0.
*/

import type { GetCaseConnectorsResponse } from '../../../common/api';
import type { ICaseUserActionsResponse } from '../typedoc_interfaces';
import type { CasesClientArgs } from '../types';
import { get } from './get';

/**
* Parameters for retrieving user actions for a particular case
*/
export interface UserActionGet {
/**
* The ID of the case
*/
caseId: string;
}
import { getConnectors } from './connectors';
import type { GetConnectorsRequest, UserActionGet } from './types';

/**
* API for interacting the actions performed by a user when interacting with the cases entities.
Expand All @@ -27,16 +20,19 @@ export interface UserActionsSubClient {
* Retrieves all user actions for a particular case.
*/
getAll(clientArgs: UserActionGet): Promise<ICaseUserActionsResponse>;
/**
* Retrieves all the connectors used within a given case
*/
getConnectors(clientArgs: GetConnectorsRequest): Promise<GetCaseConnectorsResponse>;
}

/**
* Creates an API object for interacting with the user action entities
*
* @ignore
*/
export const createUserActionsSubClient = (clientArgs: CasesClientArgs): UserActionsSubClient => {
const attachmentSubClient: UserActionsSubClient = {
getAll: (params: UserActionGet) => get(params, clientArgs),
getConnectors: (params: GetConnectorsRequest) => getConnectors(params, clientArgs),
};

return Object.freeze(attachmentSubClient);
Expand Down
Loading

0 comments on commit 92418a6

Please sign in to comment.