Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Create Bulk get cases internal API #147674

Merged
merged 13 commits into from
Jan 11, 2023
Merged

Conversation

cnasikas
Copy link
Member

@cnasikas cnasikas commented Dec 16, 2022

Summary

This PR creates the bulk get cases internal API. The endpoint is needed for the alerts table to be able to get all cases the alerts are attached to with one call.

Reference: #146864

Request

  • ids: (Required, array) An array of IDs of the retrieved cases.
  • fields: (Optional, array) The fields to return in the attributes key of the object response.
POST <kibana host>:<port>/internal/cases/_bulk_get
{
    "ids": ["case-id-1", "case-id-2", "123", "not-authorized"],
    "fields": ["title"]
}

Response

{
    "cases": [
     {
        "title": "case1",
        "owner": "securitySolution",
        "id": "case-id-1",
        "version": "WzIzMTU0NSwxNV0="
     },
     {
        "title": "case2",
        "owner": "observability",
        "id": "case-id-2",
        "version": "WzIzMTU0NSwxNV0="
      }
    ],
    "errors": [
        {
            "error": "Not Found",
            "message": "Saved object [cases/123] not found",
            "status": 404,
            "caseId": "123"
        },
        {
            "error": "Forbidden",
            "message": "Unauthorized to access case with owner: \"cases\"",
            "status": 403,
            "caseId": "not-authorized"
        }
    ]
}

Checklist

Delete any items that are not applicable to this PR.

For maintainers

@cnasikas cnasikas added release_note:skip Skip the PR/issue when compiling release notes Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) Feature:Cases Cases feature v8.7.0 labels Dec 16, 2022
@cnasikas cnasikas requested a review from a team as a code owner December 16, 2022 09:12
@cnasikas cnasikas self-assigned this Dec 16, 2022
@elasticmachine
Copy link
Contributor

Pinging @elastic/response-ops (Team:ResponseOps)

@elasticmachine
Copy link
Contributor

Pinging @elastic/response-ops-cases (Feature:Cases)

@cnasikas cnasikas mentioned this pull request Dec 16, 2022
8 tasks
Copy link
Contributor

@thomheymann thomheymann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looking good. Only thing I'd say is to update docs when you add new audit events: docs/user/security/audit-logging.asciidoc

Copy link
Contributor

@jonathan-buttner jonathan-buttner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Left a few comments and questions

Comment on lines 252 to 253
| `success` | User has accessed multiple case.
| `failure` | User is not authorized to access multiple case.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `success` | User has accessed multiple case.
| `failure` | User is not authorized to access multiple case.
| `success` | User has accessed multiple cases.
| `failure` | User is not authorized to access multiple cases.

@@ -325,6 +325,22 @@ export const AllTagsFindRequestRt = rt.partial({

export const AllReportersFindRequestRt = AllTagsFindRequestRt;

export const CasesBulkGetRequestRt = rt.type({
ids: rt.array(rt.string),
fields: rt.union([rt.undefined, rt.array(rt.string), rt.string]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this optional instead of having it accept undefined?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my understanding, this is how you define "optional" to io-ts. At least this is what we have for other optional query parameters in the find request. I tested without the fields query parameter and is working as expected. Are you aware of another way to do it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's weird, I don't know why that's working. I believe to make it optional we need to do an intersection and partial:

const CasesBulkGetRequestRt = t.intersection([
  t.type({
    ids: rt.array(rt.string),
  }),
  t.partial({
    fields: rt.union([rt.undefined, rt.array(rt.string), rt.string]),
  })
])

AuthorizationAuditLogger.createFailureMessage({ owners: authorizedOwners, operation })
);

this.auditLogger.log({ error, operation });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a user attempts to delete multiple cases and the authorization fails we will log an entry for each entity (delete is an all or nothing so we would log for each entity in the request). How about we call this.logSavedObjects() and loop over the entities as well?

savedObjects: Array<SavedObject<T>>,
authorizedEntities: OwnerEntity[]
): [Array<SavedObject<T>>, Array<SavedObject<T>>] =>
partition(savedObjects, (so) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we return an object that names which is which? Like:

{
  authorized: [...],
  unauthorized: [...]
}

@@ -64,3 +66,11 @@ export const includeFieldsRequiredForAuthentication = (fields?: string[]): strin
}
return uniq([...fields, OWNER_FIELD]);
};

export const getAuthorizedAndUnauthorizedSavedObjects = <T>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we rename this to groupByAuthorization or partitionByAuthorization something like that.

(caseInfo) => caseInfo.error === undefined
) as [CaseSavedObject[], SOWithErrors];

const authorizedEntities = await authorization.getAuthorizedEntities({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm do you think we'll ever call getAuthorizedEntities without calling ensureAuthorized? I wonder if we should create a new method in the Authorization class that abstracts the calls to both and maybe returns the authorized and unauthorized cases like getAuthorizedAndUnauthorizedSavedObjects. That way it'd be harder to forget to call ensureAuthorized. We could also have this top level function handle throwing if the request resulted in no authorized entities and have getAuthorizedEntities simply return the information instead of needing to check if none of the owners were authorized.

Copy link
Member Author

@cnasikas cnasikas Jan 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good idea! I don't think we'll even call getAuthorizedEntities without calling ensureAuthorize. What about getAndEnsureAuthorizedEntities as a name for the new method?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

@@ -41,6 +42,7 @@ export interface ICaseResponse extends CaseResponse {}
export interface ICaseResolveResponse extends CaseResolveResponse {}
export interface ICasesResponse extends CasesResponse {}
export interface ICasesFindResponse extends CasesFindResponse {}
export interface ICasesBulkGetResponse extends CasesBulkGetResponse {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self we need to remove this file too 😬

throwErrorIfCaseIdsReachTheLimit(request.ids);
throwErrorIfFieldsAreInvalid(fields);

const finalFields = fields?.length ? [...fields, 'id', 'version'] : fields;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm do we need to include id and version? I thought the fields parameter only governed fields we defined in our mapping.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the same but if I do not add them they are not returned 🤷‍♀️.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I think they aren't returned because of the pick at the end, I guess it doesn't hurt to have them here, might be a bit confusing though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, you are right! I will fix it.

});

describe('hasAllRequested: true', () => {
it('returns correct entities', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test showing that we filter out entities that don't have a valid registered owner? In this case owner c?

Copy link
Member Author

@cnasikas cnasikas Jan 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my understanding, yes. If hasAllRequested === true then the valid owners are all owners returned by featuresStart.getKibanaFeatures and not the ones the user has access (the privlages returned by checkPrivileges is an empty array in this test). Then. we filter out. Basically, owner c is not a valid one because is not registered as a feature (security) by any plugin.

Copy link
Contributor

@jonathan-buttner jonathan-buttner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, a few suggestions

});

describe('hasAllRequested: true', () => {
it('returns correct entities', async () => {
it('returns correct entities when the registered features (owners) are a and b', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe change this to categorizes the registered owners a and b as authorized and the unregistered owner c as unauthorized. Something like that.

await expect(bulkGet({ ids, fields: [field] }, clientArgs)).resolves.not.toThrow();
});

it('throws if the requested field is not valid', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could we add a test that shows the dot notation is not supported?


for (const theCase of unauthorizedCases) {
errors.push({
message: 'Unauthorized',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object we return when a user is unauthorized for a retrieving an individual case looks like this:

{
    "statusCode": 403,
    "error": "Forbidden",
    "message": "Unauthorized to access case with owners: \"observability\""
}

Should we add the nested error: 'Forbidden' field? I don't know if it adds much 🤷‍♂️ . It would probably be helpful make the message equivalent though. So have it be Unauthorized to access case with owner: <owner> (I don't know why we have it as plural in the single get case scenario 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sound good to me. What would you suggest for the text of the error attribute in the case of an error from the SO API (the loop before this loop)?

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
cases 125.3KB 125.6KB +248.0B

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

cc @cnasikas

@cnasikas cnasikas merged commit a8902e1 into elastic:main Jan 11, 2023
@cnasikas cnasikas deleted the bulk_get_cases branch January 11, 2023 14:32
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label Jan 11, 2023
jennypavlova pushed a commit to jennypavlova/kibana that referenced this pull request Jan 13, 2023
## Summary

This PR creates the bulk get cases internal API. The endpoint is needed
for the alerts table to be able to get all cases the alerts are attached
to with one call.

Reference: elastic#146864

### Request

- ids: (Required, array) An array of IDs of the retrieved cases.
- fields: (Optional, array) The fields to return in the attributes key
of the object response.

```
POST <kibana host>:<port>/internal/cases/_bulk_get
{
    "ids": ["case-id-1", "case-id-2", "123", "not-authorized"],
    "fields": ["title"]
}
```

### Response
```
{
    "cases": [
     {
        "title": "case1",
        "owner": "securitySolution",
        "id": "case-id-1",
        "version": "WzIzMTU0NSwxNV0="
     },
     {
        "title": "case2",
        "owner": "observability",
        "id": "case-id-2",
        "version": "WzIzMTU0NSwxNV0="
      }
    ],
    "errors": [
        {
            "error": "Not Found",
            "message": "Saved object [cases/123] not found",
            "status": 404,
            "caseId": "123"
        },
        {
            "error": "Forbidden",
            "message": "Unauthorized to access case with owner: \"cases\"",
            "status": 403,
            "caseId": "not-authorized"
        }
    ]
}
```

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios


### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:Cases Cases feature release_note:skip Skip the PR/issue when compiling release notes Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) v8.7.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants