-
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
[RAM] Category fields endpoint #138245
[RAM] Category fields endpoint #138245
Changes from 34 commits
c06a11a
861f5df
fad8830
61a5a49
c56de28
56d89be
c30862b
fbd357b
e0df512
f5cf16b
8461aeb
a3284bd
34a2cfd
2eb8d92
03cf83e
7148021
43c3d62
46423b9
f111f4c
17f6171
106fdf1
2c96b5c
018bd72
6af56ff
dc1b1e0
1969cff
b7324cb
f950a35
9fbc94c
9873093
0997730
1d32de9
cbb46d3
0d9c674
f9ba067
f10c273
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* 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 { FieldSpec } from '@kbn/data-plugin/common'; | ||
import { BrowserField, BrowserFields } from '../../types'; | ||
|
||
const getFieldCategory = (fieldCapability: FieldSpec) => { | ||
const name = fieldCapability.name.split('.'); | ||
|
||
if (name.length === 1) { | ||
return 'base'; | ||
} | ||
|
||
return name[0]; | ||
}; | ||
|
||
const browserFieldFactory = ( | ||
fieldCapability: FieldSpec, | ||
category: string | ||
): { [fieldName in string]: BrowserField } => { | ||
return { | ||
[fieldCapability.name]: { | ||
...fieldCapability, | ||
category, | ||
}, | ||
}; | ||
}; | ||
|
||
export const fieldDescriptorToBrowserFieldMapper = ( | ||
fieldDescriptor: FieldSpec[] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: We can call this |
||
): BrowserFields => { | ||
return fieldDescriptor.reduce((browserFields: BrowserFields, fieldCapability: FieldSpec) => { | ||
const category = getFieldCategory(fieldCapability); | ||
const field = browserFieldFactory(fieldCapability, category); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
|
||
if (browserFields[category]) { | ||
browserFields[category] = { fields: { ...browserFields[category].fields, ...field } }; | ||
} else { | ||
browserFields[category] = { fields: field }; | ||
} | ||
|
||
return browserFields; | ||
}, {}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/* | ||
* 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 { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; | ||
import { getBrowserFieldsByFeatureId } from './get_browser_fields_by_feature_id'; | ||
import { requestContextMock } from './__mocks__/request_context'; | ||
import { getO11yBrowserFields } from './__mocks__/request_responses'; | ||
import { requestMock, serverMock } from './__mocks__/server'; | ||
|
||
describe('getBrowserFieldsByFeatureId', () => { | ||
let server: ReturnType<typeof serverMock.create>; | ||
let { clients, context } = requestContextMock.createTools(); | ||
const path = `${BASE_RAC_ALERTS_API_PATH}/browser_fields`; | ||
|
||
beforeEach(async () => { | ||
server = serverMock.create(); | ||
({ clients, context } = requestContextMock.createTools()); | ||
}); | ||
|
||
describe('when racClient returns o11y indices', () => { | ||
beforeEach(() => { | ||
clients.rac.getAuthorizedAlertsIndices.mockResolvedValue([ | ||
'.alerts-observability.logs.alerts-default', | ||
]); | ||
|
||
getBrowserFieldsByFeatureId(server.router); | ||
}); | ||
|
||
test('route registered', async () => { | ||
const response = await server.inject(getO11yBrowserFields(), context); | ||
|
||
expect(response.status).toEqual(200); | ||
}); | ||
|
||
test('rejects invalid featureId type', async () => { | ||
await expect( | ||
server.inject( | ||
requestMock.create({ | ||
method: 'get', | ||
path, | ||
query: { featureIds: undefined }, | ||
}), | ||
context | ||
) | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"featureIds\\"'"` | ||
); | ||
}); | ||
|
||
test('returns error status if rac client "getAuthorizedAlertsIndices" fails', async () => { | ||
clients.rac.getAuthorizedAlertsIndices.mockRejectedValue(new Error('Unable to get index')); | ||
const response = await server.inject(getO11yBrowserFields(), context); | ||
|
||
expect(response.status).toEqual(500); | ||
expect(response.body).toEqual({ | ||
attributes: { success: false }, | ||
message: 'Unable to get index', | ||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* | ||
* 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 { IRouter } from '@kbn/core/server'; | ||
import { transformError } from '@kbn/securitysolution-es-utils'; | ||
import * as t from 'io-ts'; | ||
|
||
import { RacRequestHandlerContext } from '../types'; | ||
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; | ||
import { buildRouteValidation } from './utils/route_validation'; | ||
|
||
export const getBrowserFieldsByFeatureId = (router: IRouter<RacRequestHandlerContext>) => { | ||
router.get( | ||
{ | ||
path: `${BASE_RAC_ALERTS_API_PATH}/browser_fields`, | ||
validate: { | ||
query: buildRouteValidation( | ||
t.exact( | ||
t.type({ | ||
featureIds: t.union([t.string, t.array(t.string)]), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jcger That's one of the reason why your API integration was not working because before you only allow for array. However, when you just have one item in a query parameter, it is automatically converted to just be a string. |
||
}) | ||
) | ||
), | ||
}, | ||
options: { | ||
tags: ['access:rac'], | ||
}, | ||
}, | ||
async (context, request, response) => { | ||
try { | ||
const racContext = await context.rac; | ||
const alertsClient = await racContext.getAlertsClient(); | ||
const { featureIds = [] } = request.query; | ||
|
||
const indices = await alertsClient.getAuthorizedAlertsIndices( | ||
Array.isArray(featureIds) ? featureIds : [featureIds] | ||
); | ||
const o11yIndices = | ||
indices?.filter((index) => index.startsWith('.alerts-observability')) ?? []; | ||
if (o11yIndices.length === 0) { | ||
return response.notFound({ | ||
body: { | ||
message: `No alerts-observability indices found for featureIds [${featureIds}]`, | ||
attributes: { success: false }, | ||
}, | ||
}); | ||
} | ||
|
||
const browserFields = await alertsClient.getBrowserFields({ | ||
indices: o11yIndices, | ||
metaFields: ['_id', '_index'], | ||
allowNoIndex: true, | ||
}); | ||
|
||
return response.ok({ | ||
body: browserFields, | ||
}); | ||
} catch (error) { | ||
const formatedError = transformError(error); | ||
const contentType = { | ||
'content-type': 'application/json', | ||
}; | ||
const defaultedHeaders = { | ||
...contentType, | ||
}; | ||
|
||
return response.customError({ | ||
headers: defaultedHeaders, | ||
statusCode: formatedError.statusCode, | ||
body: { | ||
message: formatedError.message, | ||
attributes: { | ||
success: false, | ||
}, | ||
}, | ||
}); | ||
} | ||
} | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/* | ||
* 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 expect from '@kbn/expect'; | ||
|
||
import { superUser, obsOnlySpacesAll, secOnlyRead } from '../../../common/lib/authentication/users'; | ||
import type { User } from '../../../common/lib/authentication/types'; | ||
import { FtrProviderContext } from '../../../common/ftr_provider_context'; | ||
import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; | ||
|
||
// eslint-disable-next-line import/no-default-export | ||
export default ({ getService }: FtrProviderContext) => { | ||
const supertestWithoutAuth = getService('supertestWithoutAuth'); | ||
const esArchiver = getService('esArchiver'); | ||
const SPACE1 = 'space1'; | ||
const TEST_URL = '/internal/rac/alerts/browser_fields'; | ||
|
||
const getBrowserFieldsByFeatureId = async ( | ||
user: User, | ||
featureIds: string[], | ||
expectedStatusCode: number = 200 | ||
) => { | ||
const resp = await supertestWithoutAuth | ||
.get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jcger I had to add the space because your user There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks! |
||
.query({ featureIds }) | ||
.auth(user.username, user.password) | ||
.set('kbn-xsrf', 'true') | ||
.expect(expectedStatusCode); | ||
return resp.body; | ||
}; | ||
|
||
describe('Alert - Get browser fields by featureId', () => { | ||
before(async () => { | ||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); | ||
}); | ||
|
||
describe('Users:', () => { | ||
it(`${obsOnlySpacesAll.username} should be able to get browser fields for o11y featureIds`, async () => { | ||
const browserFields = await getBrowserFieldsByFeatureId(obsOnlySpacesAll, [ | ||
'apm', | ||
'infrastructure', | ||
'logs', | ||
'uptime', | ||
]); | ||
expect(Object.keys(browserFields)).to.eql(['base']); | ||
}); | ||
|
||
it(`${superUser.username} should be able to get browser fields for o11y featureIds`, async () => { | ||
const browserFields = await getBrowserFieldsByFeatureId(superUser, [ | ||
'apm', | ||
'infrastructure', | ||
'logs', | ||
'uptime', | ||
]); | ||
expect(Object.keys(browserFields)).to.eql(['base']); | ||
}); | ||
|
||
it(`${superUser.username} should NOT be able to get browser fields for siem featureId`, async () => { | ||
await getBrowserFieldsByFeatureId(superUser, ['siem'], 404); | ||
}); | ||
|
||
it(`${secOnlyRead.username} should NOT be able to get browser fields for siem featureId`, async () => { | ||
await getBrowserFieldsByFeatureId(secOnlyRead, ['siem'], 404); | ||
}); | ||
}); | ||
}); | ||
}; |
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: Since it is an array, names would be a better naming.
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'm not sure if I agree. It's still the name capability but split into one array, each element of the name would a part of the name but not a name by itself. Calling it names would mean that each part is a name but I think it isn't, not sure 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.
I see what you mean, maybe
nameParts
ornameSections
? In general, it helps when for arrays, we have a plural name but up to you :)