mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[RAM] Bug api find/index alerts (#131338)
* fix bug * fix unit test * bing back tests alive after a long CPR * fix test and bring back recursive aggs * I need to do an intersectiona and not union * fix last integration test
This commit is contained in:
parent
5f06375fe6
commit
956612d071
17 changed files with 305 additions and 263 deletions
|
@ -32,6 +32,7 @@ export enum ReadOperations {
|
|||
GetAlertSummary = 'getAlertSummary',
|
||||
GetExecutionLog = 'getExecutionLog',
|
||||
Find = 'find',
|
||||
GetAuthorizedAlertsIndices = 'getAuthorizedAlertsIndices',
|
||||
}
|
||||
|
||||
export enum WriteOperations {
|
||||
|
|
|
@ -75,9 +75,8 @@ interface BucketAggsSchemas {
|
|||
| { [x: string]: SortOrderSchema }
|
||||
| Array<{ [x: string]: SortOrderSchema }>;
|
||||
};
|
||||
aggs?: {
|
||||
[x: string]: BucketAggsSchemas;
|
||||
};
|
||||
aggs?: BucketAggsSchemas;
|
||||
aggregations?: BucketAggsSchemas;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,78 +113,83 @@ interface BucketAggsSchemas {
|
|||
* - significant_text
|
||||
* - variable_width_histogram
|
||||
*/
|
||||
export const BucketAggsSchemas: t.Type<BucketAggsSchemas> = t.recursion('BucketAggsSchemas', () =>
|
||||
t.exact(
|
||||
t.partial({
|
||||
filter: t.exact(
|
||||
t.partial({
|
||||
term: t.record(t.string, t.union([t.string, t.boolean, t.number])),
|
||||
})
|
||||
),
|
||||
date_histogram: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
fixed_interval: t.string,
|
||||
min_doc_count: t.number,
|
||||
extended_bounds: t.type({
|
||||
min: t.string,
|
||||
max: t.string,
|
||||
}),
|
||||
})
|
||||
),
|
||||
histogram: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
interval: t.number,
|
||||
min_doc_count: t.number,
|
||||
extended_bounds: t.exact(
|
||||
t.type({
|
||||
min: t.number,
|
||||
max: t.number,
|
||||
})
|
||||
),
|
||||
hard_bounds: t.exact(
|
||||
t.type({
|
||||
min: t.number,
|
||||
max: t.number,
|
||||
})
|
||||
),
|
||||
missing: t.number,
|
||||
keyed: t.boolean,
|
||||
order: t.exact(
|
||||
t.type({
|
||||
_count: t.string,
|
||||
_key: t.string,
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
nested: t.type({
|
||||
path: t.string,
|
||||
}),
|
||||
terms: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
collect_mode: t.string,
|
||||
exclude: t.union([t.string, t.array(t.string)]),
|
||||
include: t.union([t.string, t.array(t.string)]),
|
||||
execution_hint: t.string,
|
||||
missing: t.union([t.number, t.string]),
|
||||
min_doc_count: t.number,
|
||||
size: t.number,
|
||||
show_term_doc_count_error: t.boolean,
|
||||
order: t.union([
|
||||
sortOrderSchema,
|
||||
t.record(t.string, sortOrderSchema),
|
||||
t.array(t.record(t.string, sortOrderSchema)),
|
||||
]),
|
||||
})
|
||||
),
|
||||
aggs: t.record(t.string, BucketAggsSchemas),
|
||||
})
|
||||
)
|
||||
const bucketAggsTempsSchemas: t.Type<BucketAggsSchemas> = t.exact(
|
||||
t.partial({
|
||||
filter: t.exact(
|
||||
t.partial({
|
||||
term: t.record(t.string, t.union([t.string, t.boolean, t.number])),
|
||||
})
|
||||
),
|
||||
date_histogram: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
fixed_interval: t.string,
|
||||
min_doc_count: t.number,
|
||||
extended_bounds: t.type({
|
||||
min: t.string,
|
||||
max: t.string,
|
||||
}),
|
||||
})
|
||||
),
|
||||
histogram: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
interval: t.number,
|
||||
min_doc_count: t.number,
|
||||
extended_bounds: t.exact(
|
||||
t.type({
|
||||
min: t.number,
|
||||
max: t.number,
|
||||
})
|
||||
),
|
||||
hard_bounds: t.exact(
|
||||
t.type({
|
||||
min: t.number,
|
||||
max: t.number,
|
||||
})
|
||||
),
|
||||
missing: t.number,
|
||||
keyed: t.boolean,
|
||||
order: t.exact(
|
||||
t.type({
|
||||
_count: t.string,
|
||||
_key: t.string,
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
nested: t.type({
|
||||
path: t.string,
|
||||
}),
|
||||
terms: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
collect_mode: t.string,
|
||||
exclude: t.union([t.string, t.array(t.string)]),
|
||||
include: t.union([t.string, t.array(t.string)]),
|
||||
execution_hint: t.string,
|
||||
missing: t.union([t.number, t.string]),
|
||||
min_doc_count: t.number,
|
||||
size: t.number,
|
||||
show_term_doc_count_error: t.boolean,
|
||||
order: t.union([
|
||||
sortOrderSchema,
|
||||
t.record(t.string, sortOrderSchema),
|
||||
t.array(t.record(t.string, sortOrderSchema)),
|
||||
]),
|
||||
})
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
export const bucketAggsSchemas = t.intersection([
|
||||
bucketAggsTempsSchemas,
|
||||
t.partial({
|
||||
aggs: t.union([t.record(t.string, bucketAggsTempsSchemas), t.undefined]),
|
||||
aggregations: t.union([t.record(t.string, bucketAggsTempsSchemas), t.undefined]),
|
||||
}),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Schemas for the metrics Aggregations
|
||||
*
|
||||
|
@ -215,57 +219,75 @@ export const BucketAggsSchemas: t.Type<BucketAggsSchemas> = t.recursion('BucketA
|
|||
* - t_test
|
||||
* - value_count
|
||||
*/
|
||||
export const metricsAggsSchemas = t.partial({
|
||||
avg: t.partial({
|
||||
field: t.string,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
}),
|
||||
cardinality: t.partial({
|
||||
field: t.string,
|
||||
precision_threshold: t.number,
|
||||
rehash: t.boolean,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
}),
|
||||
min: t.partial({
|
||||
field: t.string,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
format: t.string,
|
||||
}),
|
||||
max: t.partial({
|
||||
field: t.string,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
format: t.string,
|
||||
}),
|
||||
sum: t.partial({
|
||||
field: t.string,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
}),
|
||||
top_hits: t.partial({
|
||||
explain: t.boolean,
|
||||
docvalue_fields: t.union([t.string, t.array(t.string)]),
|
||||
stored_fields: t.union([t.string, t.array(t.string)]),
|
||||
from: t.number,
|
||||
size: t.number,
|
||||
sort: sortSchema,
|
||||
seq_no_primary_term: t.boolean,
|
||||
version: t.boolean,
|
||||
track_scores: t.boolean,
|
||||
highlight: t.any,
|
||||
_source: t.union([t.boolean, t.string, t.array(t.string)]),
|
||||
}),
|
||||
weighted_avg: t.partial({
|
||||
format: t.string,
|
||||
value_type: t.string,
|
||||
value: t.partial({
|
||||
field: t.string,
|
||||
missing: t.number,
|
||||
}),
|
||||
weight: t.partial({
|
||||
field: t.string,
|
||||
missing: t.number,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
export const metricsAggsSchemas = t.exact(
|
||||
t.partial({
|
||||
avg: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
})
|
||||
),
|
||||
cardinality: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
precision_threshold: t.number,
|
||||
rehash: t.boolean,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
})
|
||||
),
|
||||
min: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
format: t.string,
|
||||
})
|
||||
),
|
||||
max: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
format: t.string,
|
||||
})
|
||||
),
|
||||
sum: t.exact(
|
||||
t.partial({
|
||||
field: t.string,
|
||||
missing: t.union([t.string, t.number, t.boolean]),
|
||||
})
|
||||
),
|
||||
top_hits: t.exact(
|
||||
t.partial({
|
||||
explain: t.boolean,
|
||||
docvalue_fields: t.union([t.string, t.array(t.string)]),
|
||||
stored_fields: t.union([t.string, t.array(t.string)]),
|
||||
from: t.number,
|
||||
size: t.number,
|
||||
sort: sortSchema,
|
||||
seq_no_primary_term: t.boolean,
|
||||
version: t.boolean,
|
||||
track_scores: t.boolean,
|
||||
highlight: t.any,
|
||||
_source: t.union([t.boolean, t.string, t.array(t.string)]),
|
||||
})
|
||||
),
|
||||
weighted_avg: t.exact(
|
||||
t.partial({
|
||||
format: t.string,
|
||||
value_type: t.string,
|
||||
value: t.partial({
|
||||
field: t.string,
|
||||
missing: t.number,
|
||||
}),
|
||||
weight: t.partial({
|
||||
field: t.string,
|
||||
missing: t.number,
|
||||
}),
|
||||
})
|
||||
),
|
||||
aggs: t.undefined,
|
||||
aggregations: t.undefined,
|
||||
})
|
||||
);
|
||||
|
||||
export type PutIndexTemplateRequest = estypes.IndicesPutIndexTemplateRequest & {
|
||||
body?: { composed_of?: string[] };
|
||||
|
|
|
@ -647,7 +647,6 @@ export class AlertsClient {
|
|||
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
|
||||
AlertingAuthorizationEntity.Alert
|
||||
);
|
||||
|
||||
// As long as the user can read a minimum of one type of rule type produced by the provided feature,
|
||||
// the user should be provided that features' alerts index.
|
||||
// Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter
|
||||
|
@ -655,19 +654,16 @@ export class AlertsClient {
|
|||
for (const ruleType of augmentedRuleTypes.authorizedRuleTypes) {
|
||||
authorizedFeatures.add(ruleType.producer);
|
||||
}
|
||||
|
||||
const validAuthorizedFeatures = Array.from(authorizedFeatures).filter(
|
||||
(feature): feature is ValidFeatureId =>
|
||||
featureIds.includes(feature) && isValidFeatureId(feature)
|
||||
);
|
||||
|
||||
const toReturn = validAuthorizedFeatures.flatMap((feature) => {
|
||||
const indices = this.ruleDataService.findIndicesByFeature(feature, Dataset.alerts);
|
||||
if (feature === 'siem') {
|
||||
return indices.map((i) => `${i.baseName}-${this.spaceId}`);
|
||||
} else {
|
||||
return indices.map((i) => i.baseName);
|
||||
const toReturn = validAuthorizedFeatures.map((feature) => {
|
||||
const index = this.ruleDataService.findIndexByFeature(feature, Dataset.alerts);
|
||||
if (index == null) {
|
||||
throw new Error(`This feature id ${feature} should be associated to an alert index`);
|
||||
}
|
||||
return index?.getPrimaryAlias(this.spaceId ?? '*') ?? '';
|
||||
});
|
||||
|
||||
return toReturn;
|
||||
|
|
|
@ -14,7 +14,7 @@ import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
|
|||
import { RacRequestHandlerContext } from '../types';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
|
||||
import { buildRouteValidation } from './utils/route_validation';
|
||||
import { BucketAggsSchemas } from '../../common/types';
|
||||
import { bucketAggsSchemas, metricsAggsSchemas } from '../../common/types';
|
||||
|
||||
export const findAlertsByQueryRoute = (router: IRouter<RacRequestHandlerContext>) => {
|
||||
router.post(
|
||||
|
@ -26,7 +26,11 @@ export const findAlertsByQueryRoute = (router: IRouter<RacRequestHandlerContext>
|
|||
t.partial({
|
||||
index: t.string,
|
||||
query: t.object,
|
||||
aggs: t.union([t.record(t.string, BucketAggsSchemas), t.undefined]),
|
||||
aggs: t.union([
|
||||
t.record(t.string, bucketAggsSchemas),
|
||||
t.record(t.string, metricsAggsSchemas),
|
||||
t.undefined,
|
||||
]),
|
||||
size: t.union([PositiveInteger, t.undefined]),
|
||||
track_total_hits: t.union([t.boolean, t.undefined]),
|
||||
_source: t.union([t.array(t.string), t.undefined]),
|
||||
|
|
|
@ -16,7 +16,7 @@ export const ruleDataServiceMock = {
|
|||
initializeService: jest.fn(),
|
||||
initializeIndex: jest.fn(),
|
||||
findIndexByName: jest.fn(),
|
||||
findIndicesByFeature: jest.fn(),
|
||||
findIndexByFeature: jest.fn(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ export interface IRuleDataService {
|
|||
* Looks up the index information associated with the given Kibana "feature".
|
||||
* Note: features are used in RBAC.
|
||||
*/
|
||||
findIndicesByFeature(featureId: ValidFeatureId, dataset?: Dataset): IndexInfo[];
|
||||
findIndexByFeature(featureId: ValidFeatureId, dataset: Dataset): IndexInfo | null;
|
||||
}
|
||||
|
||||
// TODO: This is a leftover. Remove its usage from the "observability" plugin and delete it.
|
||||
|
@ -214,8 +214,13 @@ export class RuleDataService implements IRuleDataService {
|
|||
return this.indicesByBaseName.get(baseName) ?? null;
|
||||
}
|
||||
|
||||
public findIndicesByFeature(featureId: ValidFeatureId, dataset?: Dataset): IndexInfo[] {
|
||||
public findIndexByFeature(featureId: ValidFeatureId, dataset: Dataset): IndexInfo | null {
|
||||
const foundIndices = this.indicesByFeatureId.get(featureId) ?? [];
|
||||
return dataset ? foundIndices.filter((i) => i.indexOptions.dataset === dataset) : foundIndices;
|
||||
if (dataset && foundIndices.length > 0) {
|
||||
return foundIndices.filter((i) => i.indexOptions.dataset === dataset)[0];
|
||||
} else if (foundIndices.length > 0) {
|
||||
return foundIndices[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,12 +78,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
const searchStrategySearch = jest.fn().mockImplementation(() => of(response));
|
||||
|
||||
beforeEach(() => {
|
||||
ruleDataService.findIndicesByFeature.mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
baseName: 'test',
|
||||
} as IndexInfo,
|
||||
];
|
||||
ruleDataService.findIndexByFeature.mockImplementation(() => {
|
||||
return {
|
||||
baseName: 'test',
|
||||
} as IndexInfo;
|
||||
});
|
||||
|
||||
data.search.getSearchStrategy.mockImplementation(() => {
|
||||
|
@ -108,7 +106,7 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
ruleDataService.findIndicesByFeature.mockClear();
|
||||
ruleDataService.findIndexByFeature.mockClear();
|
||||
data.search.getSearchStrategy.mockClear();
|
||||
(data.search.searchAsInternalUser.search as jest.Mock).mockClear();
|
||||
getAuthzFilterSpy.mockClear();
|
||||
|
@ -156,12 +154,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
};
|
||||
});
|
||||
|
||||
ruleDataService.findIndicesByFeature.mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
baseName: 'myTestIndex',
|
||||
} as unknown as IndexInfo,
|
||||
];
|
||||
ruleDataService.findIndexByFeature.mockImplementation(() => {
|
||||
return {
|
||||
baseName: 'myTestIndex',
|
||||
} as unknown as IndexInfo;
|
||||
});
|
||||
|
||||
let searchRequest: RuleRegistrySearchRequest = {} as unknown as RuleRegistrySearchRequest;
|
||||
|
@ -199,8 +195,8 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
request: {},
|
||||
};
|
||||
|
||||
ruleDataService.findIndicesByFeature.mockImplementationOnce(() => {
|
||||
return [];
|
||||
ruleDataService.findIndexByFeature.mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
|
|
|
@ -89,17 +89,16 @@ export const ruleRegistrySearchStrategyProvider = (
|
|||
);
|
||||
return accum;
|
||||
}
|
||||
|
||||
return [
|
||||
...accum,
|
||||
...ruleDataService
|
||||
.findIndicesByFeature(featureId, Dataset.alerts)
|
||||
.map((indexInfo) => {
|
||||
return featureId === 'siem'
|
||||
? `${indexInfo.baseName}-${space?.id ?? ''}*`
|
||||
: `${indexInfo.baseName}*`;
|
||||
}),
|
||||
];
|
||||
const alertIndexInfo = ruleDataService.findIndexByFeature(featureId, Dataset.alerts);
|
||||
if (alertIndexInfo) {
|
||||
return [
|
||||
...accum,
|
||||
featureId === 'siem'
|
||||
? `${alertIndexInfo.baseName}-${space?.id ?? ''}*`
|
||||
: `${alertIndexInfo.baseName}*`,
|
||||
];
|
||||
}
|
||||
return accum;
|
||||
}, []);
|
||||
|
||||
if (indices.length === 0) {
|
||||
|
|
|
@ -128,6 +128,7 @@ describe(`feature_privilege_builder`, () => {
|
|||
Array [
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -175,6 +176,7 @@ describe(`feature_privilege_builder`, () => {
|
|||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -263,12 +265,13 @@ describe(`feature_privilege_builder`, () => {
|
|||
});
|
||||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/update",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/update",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('grants `all` privileges to rules and alerts under feature consumer', () => {
|
||||
|
@ -326,6 +329,7 @@ describe(`feature_privilege_builder`, () => {
|
|||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unsnooze",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/update",
|
||||
]
|
||||
`);
|
||||
|
@ -420,14 +424,16 @@ describe(`feature_privilege_builder`, () => {
|
|||
});
|
||||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/update",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/update",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('grants both `all` and `read` to rules and alerts privileges under feature consumer', () => {
|
||||
|
@ -490,9 +496,11 @@ describe(`feature_privilege_builder`, () => {
|
|||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find",
|
||||
"alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/update",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ enum AlertingEntity {
|
|||
|
||||
const readOperations: Record<AlertingEntity, string[]> = {
|
||||
rule: ['get', 'getRuleState', 'getAlertSummary', 'getExecutionLog', 'find'],
|
||||
alert: ['get', 'find'],
|
||||
alert: ['get', 'find', 'getAuthorizedAlertsIndices'],
|
||||
};
|
||||
|
||||
const writeOperations: Record<AlertingEntity, string[]> = {
|
||||
|
|
|
@ -133,6 +133,9 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
|
|||
rule: {
|
||||
all: ruleTypes,
|
||||
},
|
||||
alert: {
|
||||
all: ruleTypes,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
|
@ -156,6 +159,9 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
|
|||
rule: {
|
||||
read: ruleTypes,
|
||||
},
|
||||
alert: {
|
||||
all: ruleTypes,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
"source": {
|
||||
"event.kind" : "signal",
|
||||
"@timestamp": "2020-12-16T15:16:18.570Z",
|
||||
"kibana.alert.rule.rule_type_id": "siem.signals",
|
||||
"kibana.alert.rule.rule_type_id": "siem.queryRule",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.rule.consumer": "siem",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
|
@ -74,7 +74,7 @@
|
|||
"source": {
|
||||
"event.kind" : "signal",
|
||||
"@timestamp": "2020-12-16T15:16:18.570Z",
|
||||
"kibana.alert.rule.rule_type_id": "siem.customRule",
|
||||
"kibana.alert.rule.rule_type_id": "siem.queryRule",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.rule.consumer": "siem",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
|
@ -90,7 +90,7 @@
|
|||
"id": "space1securityalert",
|
||||
"source": {
|
||||
"@timestamp": "2020-12-16T15:16:18.570Z",
|
||||
"kibana.alert.rule.rule_type_id": "siem.signals",
|
||||
"kibana.alert.rule.rule_type_id": "siem.queryRule",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.rule.consumer": "siem",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
|
@ -106,7 +106,7 @@
|
|||
"id": "space2securityalert",
|
||||
"source": {
|
||||
"@timestamp": "2020-12-16T15:16:18.570Z",
|
||||
"kibana.alert.rule.rule_type_id": "siem.signals",
|
||||
"kibana.alert.rule.rule_type_id": "siem.queryRule",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.rule.consumer": "siem",
|
||||
"kibana.alert.workflow_status": "open",
|
||||
|
|
|
@ -50,7 +50,6 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const TEST_URL = '/internal/rac/alerts';
|
||||
const ALERTS_INDEX_URL = `${TEST_URL}/index`;
|
||||
const SPACE1 = 'space1';
|
||||
const SPACE2 = 'space2';
|
||||
const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV';
|
||||
|
@ -58,38 +57,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const SECURITY_SOLUTION_ALERT_ID = '020202';
|
||||
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';
|
||||
|
||||
const getAPMIndexName = async (user: User) => {
|
||||
const { body: indexNames }: { body: { index_name: string[] | undefined } } =
|
||||
await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
const observabilityIndex = indexNames?.index_name?.find(
|
||||
(indexName) => indexName === APM_ALERT_INDEX
|
||||
);
|
||||
expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
};
|
||||
|
||||
const getSecuritySolutionIndexName = async (user: User) => {
|
||||
const { body: indexNames }: { body: { index_name: string[] | undefined } } =
|
||||
await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
const securitySolution = indexNames?.index_name?.find((indexName) =>
|
||||
indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX)
|
||||
);
|
||||
expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
};
|
||||
|
||||
describe('Alert - Find - RBAC - spaces', () => {
|
||||
before(async () => {
|
||||
await getSecuritySolutionIndexName(superUser);
|
||||
await getAPMIndexName(superUser);
|
||||
});
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
});
|
||||
|
@ -98,6 +66,32 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
});
|
||||
|
||||
it(`${superUser.username} should reject at route level when aggs contains script alerts which match query in ${SPACE1}/${SECURITY_SOLUTION_ALERT_INDEX}`, async () => {
|
||||
const found = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}/find`)
|
||||
.auth(superUser.username, superUser.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } },
|
||||
aggs: {
|
||||
alertsByGroupingCount: {
|
||||
terms: {
|
||||
field: 'kibana.alert.rule.name',
|
||||
order: {
|
||||
_count: 'desc',
|
||||
},
|
||||
script: {
|
||||
source: 'SCRIPT',
|
||||
},
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
index: SECURITY_SOLUTION_ALERT_INDEX,
|
||||
});
|
||||
expect(found.statusCode).to.eql(400);
|
||||
});
|
||||
|
||||
it(`${superUser.username} should reject at route level when nested aggs contains script alerts which match query in ${SPACE1}/${SECURITY_SOLUTION_ALERT_INDEX}`, async () => {
|
||||
const found = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}/find`)
|
||||
|
@ -164,6 +158,26 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
expect(found.body.hits.total.value).to.be.above(0);
|
||||
});
|
||||
|
||||
it(`${superUser.username} should allow cardinality aggs in ${SPACE1}/${SECURITY_SOLUTION_ALERT_INDEX}`, async () => {
|
||||
const found = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}/find`)
|
||||
.auth(superUser.username, superUser.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
size: 1,
|
||||
aggs: {
|
||||
nbr_consumer: {
|
||||
cardinality: {
|
||||
field: 'kibana.alert.rule.consumer',
|
||||
},
|
||||
},
|
||||
},
|
||||
index: '.alerts*',
|
||||
});
|
||||
expect(found.statusCode).to.eql(200);
|
||||
expect(found.body.aggregations.nbr_consumer.value).to.be.equal(2);
|
||||
});
|
||||
|
||||
function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) {
|
||||
authorizedUsers.forEach(({ username, password }) => {
|
||||
it(`${username} should finds alerts which match query in ${space}/${index}`, async () => {
|
||||
|
|
|
@ -20,17 +20,16 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const TEST_URL = '/internal/rac/alerts';
|
||||
const ALERTS_INDEX_URL = `${TEST_URL}/index`;
|
||||
const SPACE1 = 'space1';
|
||||
const APM_ALERT_INDEX = '.alerts-observability-apm';
|
||||
const APM_ALERT_INDEX = '.alerts-observability.apm.alerts';
|
||||
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';
|
||||
|
||||
const getAPMIndexName = async (user: User, space: string, expected: number = 200) => {
|
||||
const { body: indexNames }: { body: { index_name: string[] | undefined } } =
|
||||
await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=apm`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(expected);
|
||||
return indexNames;
|
||||
const getAPMIndexName = async (user: User, space: string, expectedStatusCode: number = 200) => {
|
||||
const resp = await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=apm`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(expectedStatusCode);
|
||||
return resp.body.index_name as string[];
|
||||
};
|
||||
|
||||
const getSecuritySolutionIndexName = async (
|
||||
|
@ -38,13 +37,13 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
space: string,
|
||||
expectedStatusCode: number = 200
|
||||
) => {
|
||||
const { body: indexNames }: { body: { index_name: string[] | undefined } } =
|
||||
await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=siem`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(expectedStatusCode);
|
||||
return indexNames;
|
||||
const resp = await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=siem`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(expectedStatusCode);
|
||||
|
||||
return resp.body.index_name as string[];
|
||||
};
|
||||
|
||||
describe('Alert - Get Index - RBAC - spaces', () => {
|
||||
|
@ -54,31 +53,22 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
describe('Users:', () => {
|
||||
it(`${obsOnlySpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => {
|
||||
const indexNames = await getAPMIndexName(obsOnlySpacesAll, SPACE1);
|
||||
const observabilityIndex = indexNames?.index_name?.find(
|
||||
(indexName) => indexName === APM_ALERT_INDEX
|
||||
);
|
||||
expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
expect(indexNames.includes(`${APM_ALERT_INDEX}-${SPACE1}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
});
|
||||
|
||||
it(`${superUser.username} should be able to access the APM alert in ${SPACE1}`, async () => {
|
||||
const indexNames = await getAPMIndexName(superUser, SPACE1);
|
||||
const observabilityIndex = indexNames?.index_name?.find(
|
||||
(indexName) => indexName === APM_ALERT_INDEX
|
||||
);
|
||||
expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
expect(indexNames.includes(`${APM_ALERT_INDEX}-${SPACE1}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
});
|
||||
|
||||
it(`${secOnlyRead.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => {
|
||||
const indexNames = await getAPMIndexName(secOnlyRead, SPACE1);
|
||||
expect(indexNames?.index_name?.length).to.eql(0);
|
||||
expect(indexNames?.length).to.eql(0);
|
||||
});
|
||||
|
||||
it(`${secOnlyRead.username} should be able to access the security solution alert in ${SPACE1}`, async () => {
|
||||
const indexNames = await getSecuritySolutionIndexName(secOnlyRead, SPACE1);
|
||||
const securitySolution = indexNames?.index_name?.find((indexName) =>
|
||||
indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX)
|
||||
);
|
||||
expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
expect(indexNames.includes(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,8 +27,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
|||
// loadTestFile(require.resolve('./get_alert_by_id'));
|
||||
// loadTestFile(require.resolve('./update_alert'));
|
||||
// loadTestFile(require.resolve('./bulk_update_alerts'));
|
||||
// loadTestFile(require.resolve('./find_alerts'));
|
||||
// loadTestFile(require.resolve('./get_alerts_index'));
|
||||
|
||||
loadTestFile(require.resolve('./get_alerts_index'));
|
||||
loadTestFile(require.resolve('./find_alerts'));
|
||||
loadTestFile(require.resolve('./search_strategy'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -31,10 +31,9 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
const observabilityIndex = indexNames?.index_name?.find(
|
||||
(indexName) => indexName === APM_ALERT_INDEX
|
||||
);
|
||||
expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
expect(
|
||||
indexNames?.index_name?.filter((indexName) => indexName.startsWith(APM_ALERT_INDEX)).length
|
||||
).to.eql(1); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
};
|
||||
|
||||
const getSecuritySolutionIndexName = async (user: User) => {
|
||||
|
@ -43,10 +42,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
const securitySolution = indexNames?.index_name?.find((indexName) =>
|
||||
indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX)
|
||||
);
|
||||
expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
expect(
|
||||
indexNames?.index_name?.filter((indexName) =>
|
||||
indexName.startsWith(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`)
|
||||
).length
|
||||
).to.eql(1); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
};
|
||||
|
||||
describe('Alerts - GET - RBAC', () => {
|
||||
|
|
|
@ -31,10 +31,9 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
const observabilityIndex = indexNames?.index_name?.find(
|
||||
(indexName) => indexName === APM_ALERT_INDEX
|
||||
);
|
||||
expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
expect(
|
||||
indexNames?.index_name?.filter((indexName) => indexName.startsWith(APM_ALERT_INDEX)).length
|
||||
).to.eql(1); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
};
|
||||
|
||||
const getSecuritySolutionIndexName = async (user: User) => {
|
||||
|
@ -43,10 +42,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
const securitySolution = indexNames?.index_name?.find((indexName) =>
|
||||
indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX)
|
||||
);
|
||||
expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
expect(
|
||||
indexNames?.index_name?.filter((indexName) =>
|
||||
indexName.startsWith(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`)
|
||||
).length
|
||||
).to.eql(1); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
};
|
||||
|
||||
describe('Alert - Update - RBAC - spaces', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue