-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Conversation
Pinging @elastic/response-ops (Team:ResponseOps) |
Pinging @elastic/response-ops-cases (Feature:Cases) |
There was a problem hiding this 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
x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_get_cases.ts
Show resolved
Hide resolved
There was a problem hiding this 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
| `success` | User has accessed multiple case. | ||
| `failure` | User is not authorized to access multiple case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| `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]), |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 }); |
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
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>( |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 {} |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🤷♀️.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
b5bbea0
to
fcff3d0
Compare
fcff3d0
to
516447d
Compare
There was a problem hiding this 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 () => { |
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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)?
Better types
c2abc71
to
fa623f5
Compare
💚 Build Succeeded
Metrics [docs]Page load bundle
History
To update your PR or re-run it, just comment with: cc @cnasikas |
## 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)
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
Response
Checklist
Delete any items that are not applicable to this PR.
For maintainers