@@ -11,8 +11,10 @@ import { withSpan } from '@kbn/apm-utils';
1111import type { SavedObjectsBulkCreateObject } from '@kbn/core/server' ;
1212import { SavedObjectsUtils } from '@kbn/core/server' ;
1313import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects' ;
14- import { getRuleCircuitBreakerErrorMessage } from '../../../../../common' ;
15- import { updateMeta } from '../../../../rules_client/lib' ;
14+ import { getRuleCircuitBreakerErrorMessage , parseDuration } from '../../../../../common' ;
15+ import { addGeneratedActionValues , updateMeta } from '../../../../rules_client/lib' ;
16+ import { validateRuleTypeParams } from '../../../../lib' ;
17+ import { WriteOperations , AlertingAuthorizationEntity } from '../../../../authorization' ;
1618import {
1719 API_KEY_GENERATE_CONCURRENCY ,
1820 DEFAULT_BULK_CREATE_BATCH_SIZE ,
@@ -24,6 +26,7 @@ import { bulkCreateRulesSo } from '../../../../data/rule';
2426import type { RawRule } from '../../../../types' ;
2527import type { RulesClientContext } from '../../../../rules_client/types' ;
2628import type { RuleParams } from '../../types' ;
29+ import { createRuleDataSchema } from '../create/schemas' ;
2730import { validateScheduleLimit } from '../get_schedule_frequency' ;
2831import type {
2932 ApiKeyEntry ,
@@ -40,7 +43,6 @@ import {
4043 demotePreparedRules ,
4144 flushKeysToInvalidate ,
4245 prepareRule ,
43- preValidate ,
4446} from './utils' ;
4547
4648export async function bulkCreateRules < Params extends RuleParams = never > (
@@ -131,6 +133,123 @@ export async function bulkCreateRules<Params extends RuleParams = never>(
131133 return { successfulIds, errors, total } ;
132134}
133135
136+ async function preValidate < Params extends RuleParams > ( {
137+ context,
138+ inputs,
139+ } : {
140+ context : RulesClientContext ;
141+ inputs : Array < { id : string ; rule : BulkCreateRulesItem < Params > } > ;
142+ } ) : Promise < {
143+ validated : Array < { id : string ; rule : BulkCreateRulesItem < Params > } > ;
144+ errors : BulkCreateOperationError [ ] ;
145+ } > {
146+ const validated = new Map < string , { id : string ; rule : BulkCreateRulesItem < Params > } > ( ) ;
147+ const errors : BulkCreateOperationError [ ] = [ ] ;
148+ const authPairs = new Map <
149+ string ,
150+ { ruleTypeId : string ; consumer : string ; ids : string [ ] ; names : Map < string , string > }
151+ > ( ) ;
152+
153+ // Phase A1: per-rule in-memory checks, sequential, cheapest-first.
154+ await withSpan ( { name : 'preValidate.checkInMemory' , type : 'rules' } , async ( ) => {
155+ for ( const { id, rule } of inputs ) {
156+ try {
157+ const { actions : genActions , systemActions : genSystemActions } =
158+ await addGeneratedActionValues ( rule . data . actions , rule . data . systemActions , context ) ;
159+ const data = { ...rule . data , actions : genActions , systemActions : genSystemActions } ;
160+
161+ try {
162+ createRuleDataSchema . validate ( data ) ;
163+ } catch ( err ) {
164+ throw Boom . badRequest ( `Error validating create data - ${ err . message } ` ) ;
165+ }
166+
167+ // ruleTypeRegistry.get throws 400 if not registered.
168+ const ruleType = context . ruleTypeRegistry . get ( data . alertTypeId ) ;
169+ context . ruleTypeRegistry . ensureRuleTypeEnabled ( data . alertTypeId ) ;
170+ validateRuleTypeParams ( data . params , ruleType . validate . params ) ;
171+
172+ const intervalInMs = parseDuration ( data . schedule . interval ) ;
173+ if (
174+ intervalInMs < context . minimumScheduleIntervalInMs &&
175+ context . minimumScheduleInterval . enforce
176+ ) {
177+ throw Boom . badRequest (
178+ `Error creating rule: the interval is less than the allowed minimum interval of ${ context . minimumScheduleInterval . value } `
179+ ) ;
180+ }
181+ if (
182+ intervalInMs < context . minimumScheduleIntervalInMs &&
183+ ! context . minimumScheduleInterval . enforce
184+ ) {
185+ context . logger . warn (
186+ `Rule schedule interval (${ data . schedule . interval } ) for "${ ruleType . id } " rule type with ID "${ id } " is less than the minimum value (${ context . minimumScheduleInterval . value } ). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.`
187+ ) ;
188+ }
189+
190+ const authzKey = `${ data . alertTypeId } ::${ data . consumer } ` ;
191+ const pair = authPairs . get ( authzKey ) ;
192+ if ( pair ) {
193+ pair . ids . push ( id ) ;
194+ pair . names . set ( id , data . name ) ;
195+ } else {
196+ authPairs . set ( authzKey , {
197+ ruleTypeId : data . alertTypeId ,
198+ consumer : data . consumer ,
199+ ids : [ id ] ,
200+ names : new Map ( [ [ id , data . name ] ] ) ,
201+ } ) ;
202+ }
203+
204+ validated . set ( id , { id, rule } ) ;
205+ } catch ( err ) {
206+ errors . push ( {
207+ message : err . message ,
208+ status : err . output ?. statusCode ,
209+ rule : { id, name : rule . data ?. name ?? 'n/a' } ,
210+ } ) ;
211+ }
212+ }
213+ } ) ;
214+
215+ if ( validated . size === 0 ) {
216+ return { validated : [ ] , errors } ;
217+ }
218+
219+ // Phase A2: deduped per-pair authorization.
220+ await withSpan ( { name : 'preValidate.ensureAuthorized' , type : 'rules' } , async ( ) => {
221+ for ( const { ruleTypeId, consumer, ids, names } of authPairs . values ( ) ) {
222+ try {
223+ await context . authorization . ensureAuthorized ( {
224+ ruleTypeId,
225+ consumer,
226+ operation : WriteOperations . Create ,
227+ entity : AlertingAuthorizationEntity . Rule ,
228+ } ) ;
229+ } catch ( authzError ) {
230+ // One audit per rule in failing set. Following single rule `create()`.
231+ for ( const ruleId of ids ) {
232+ context . auditLogger ?. log (
233+ ruleAuditEvent ( {
234+ action : RuleAuditAction . CREATE ,
235+ savedObject : { type : RULE_SAVED_OBJECT_TYPE , id : ruleId , name : names . get ( ruleId ) ! } ,
236+ error : authzError ,
237+ } )
238+ ) ;
239+ errors . push ( {
240+ message : authzError . message ,
241+ status : authzError . output ?. statusCode ,
242+ rule : { id : ruleId , name : names . get ( ruleId ) ?? 'n/a' } ,
243+ } ) ;
244+ validated . delete ( ruleId ) ;
245+ }
246+ }
247+ }
248+ } ) ;
249+
250+ return { validated : [ ...validated . values ( ) ] , errors } ;
251+ }
252+
134253interface RunBatchArgs < Params extends RuleParams > {
135254 context : RulesClientContext ;
136255 username : string | null ;
@@ -154,22 +273,24 @@ async function runBatch<Params extends RuleParams>({
154273 const errors : BulkCreateOperationError [ ] = [ ] ;
155274
156275 // Phase B1: per-rule prepare (high latency validation + API key generation).
157- await pMap (
158- batch ,
159- async ( { id, rule } ) => {
160- const { prepared, error } = await prepareRule ( {
161- context,
162- actionsClient,
163- username,
164- id,
165- rule,
166- errors,
167- apiKeysMap,
168- } ) ;
169- if ( prepared ) preparedRules . set ( id , prepared ) ;
170- else if ( error ) errors . push ( error ) ;
171- } ,
172- { concurrency : API_KEY_GENERATE_CONCURRENCY }
276+ await withSpan ( { name : 'runBatch.pMap.prepareRule' , type : 'rules' } , ( ) =>
277+ pMap (
278+ batch ,
279+ async ( { id, rule } ) => {
280+ const { prepared, error } = await prepareRule ( {
281+ context,
282+ actionsClient,
283+ username,
284+ id,
285+ rule,
286+ errors,
287+ apiKeysMap,
288+ } ) ;
289+ if ( prepared ) preparedRules . set ( id , prepared ) ;
290+ else if ( error ) errors . push ( error ) ;
291+ } ,
292+ { concurrency : API_KEY_GENERATE_CONCURRENCY }
293+ )
173294 ) ;
174295
175296 // No survivors? Flush any keys created and return.
@@ -182,10 +303,14 @@ async function runBatch<Params extends RuleParams>({
182303 const enabled = [ ...preparedRules . values ( ) ] . filter ( ( p ) => p . enabled ) ;
183304
184305 if ( enabled . length > 0 ) {
185- const validationPayload = await validateScheduleLimit ( {
186- context,
187- updatedInterval : enabled . map ( ( r ) => r . schedule . interval ) ,
188- } ) ;
306+ const validationPayload = await withSpan (
307+ { name : 'runBatch.validateScheduleLimit' , type : 'rules' } ,
308+ ( ) =>
309+ validateScheduleLimit ( {
310+ context,
311+ updatedInterval : enabled . map ( ( r ) => r . schedule . interval ) ,
312+ } )
313+ ) ;
189314 if ( validationPayload ) {
190315 const enabledIds = enabled . map ( ( p ) => p . id ) ;
191316 const reasonMessage = getRuleCircuitBreakerErrorMessage ( {
@@ -221,7 +346,7 @@ async function runBatch<Params extends RuleParams>({
221346 const survivingEnabledIds = survivingEnabled . map ( ( p ) => p . id ) ;
222347 try {
223348 const scheduledTasks = await withSpan (
224- { name : 'taskManager .bulkSchedule' , type : 'tasks' } ,
349+ { name : 'runBatch .bulkSchedule' , type : 'tasks' } ,
225350 ( ) => context . taskManager . bulkSchedule ( tasksToSchedule )
226351 ) ;
227352 scheduledIds = scheduledTasks . map ( ( task ) => task . id ) ;
@@ -291,7 +416,7 @@ async function runBatch<Params extends RuleParams>({
291416 let bulkResponse ;
292417 try {
293418 bulkResponse = await withSpan (
294- { name : 'unsecuredSavedObjectsClient.bulkCreate ' , type : 'rules' } ,
419+ { name : 'runBatch.bulkCreateRulesSo ' , type : 'rules' } ,
295420 ( ) =>
296421 bulkCreateRulesSo ( {
297422 savedObjectsClient : context . unsecuredSavedObjectsClient ,
0 commit comments