@@ -489,6 +489,137 @@ private async Task<T> queryMachineryDefault<T>(Input input)
489
489
throw new Exception ( "Impossible error" ) ;
490
490
}
491
491
492
+ /// <summary>
493
+ /// Evaluate a policy, using the provided map of query inputs. Results will
494
+ /// be returned in an identically-structured pair of maps, one for
495
+ /// successful evals, and one for errors. In the event that the OPA server
496
+ /// does not support the /v1/batch/data endpoint, this method will fall back
497
+ /// to performing sequential queries against the OPA server.
498
+ /// </summary>
499
+ /// <param name="path">The rule to evaluate. (Example: "app/rbac")</param>
500
+ /// <param name="inputs">The input Dictionary OPA will use for evaluating the rule. The keys are arbitrary ID strings, the values are the input values intended for each query.</param>
501
+ /// <returns>A pair of mappings, between string keys, and generic type T, or ServerErrors.</returns>
502
+ public async Task < ( OpaBatchResultGeneric < T > , OpaBatchErrors ) > evaluateBatch < T > ( string path , Dictionary < string , Dictionary < string , object > > inputs )
503
+ {
504
+ return await queryMachineryBatch < T > ( path , inputs ) ;
505
+ }
506
+
507
+ /// <exclude />
508
+ private async Task < ( OpaBatchResultGeneric < T > , OpaBatchErrors ) > queryMachineryBatch < T > ( string path , Dictionary < string , Dictionary < string , object > > inputs )
509
+ {
510
+ OpaBatchResultGeneric < T > successResults = new ( ) ;
511
+ OpaBatchErrors failureResults = new ( ) ;
512
+
513
+ // Attempt using the /v1/batch/data endpoint. If we ever receive a 404, then it's a vanilla OPA instance, and we should skip straight to fallback mode.
514
+ if ( opaSupportsBatchQueryAPI )
515
+ {
516
+ var req = new ExecuteBatchPolicyWithInputRequest ( )
517
+ {
518
+ Path = path ,
519
+ RequestBody = new ExecuteBatchPolicyWithInputRequestBody ( )
520
+ {
521
+ Inputs = inputs . ToOpaBatchInputRaw ( ) ,
522
+ } ,
523
+ Pretty = requestPretty ,
524
+ Provenance = requestProvenance ,
525
+ Explain = requestExplain ,
526
+ Metrics = requestMetrics ,
527
+ Instrument = requestInstrument ,
528
+ StrictBuiltinErrors = requestStrictBuiltinErrors ,
529
+ } ;
530
+
531
+ // Launch query. The all-errors case is handled in the exception handler block.
532
+ ExecuteBatchPolicyWithInputResponse res ;
533
+ try
534
+ {
535
+ res = await opa . ExecuteBatchPolicyWithInputAsync ( req ) ;
536
+ switch ( res . StatusCode )
537
+ {
538
+ // All-success case.
539
+ case 200 :
540
+ successResults = res . BatchSuccessfulPolicyEvaluation ! . Responses ! . ToOpaBatchResults < T > ( ) ; // Should not be null here.
541
+ return ( successResults , failureResults ) ;
542
+ // Mixed results case.
543
+ case 207 :
544
+ var mixedResponses = res . BatchMixedResults ? . Responses ! ; // Should not be null here.
545
+ foreach ( var ( key , value ) in mixedResponses )
546
+ {
547
+ switch ( value . Type . ToString ( ) )
548
+ {
549
+ case "200" :
550
+ successResults . Add ( key , convertResult < T > ( value . SuccessfulPolicyResponseWithStatusCode ! . Result ! ) ) ;
551
+ break ;
552
+ case "500" :
553
+ failureResults . Add ( key , ( OpaError ) value . ServerErrorWithStatusCode ! ) ; // Should not be null.
554
+ break ;
555
+ }
556
+ }
557
+
558
+ return ( successResults , failureResults ) ;
559
+ default :
560
+ // TODO: Throw exception if we reach the end of this block without a successful return.
561
+ // This *should* never happen. It means we didn't return from the batch or fallback handler blocks earlier.
562
+ throw new Exception ( "Impossible error" ) ;
563
+ }
564
+ }
565
+ catch ( ClientError ce )
566
+ {
567
+ throw ce ; // Rethrow for the caller to deal with. Request was malformed.
568
+ }
569
+ catch ( BatchServerError bse )
570
+ {
571
+ failureResults = bse . Responses ! . ToOpaBatchErrors ( ) ; // Should not be null here.
572
+ return ( successResults , failureResults ) ;
573
+ }
574
+ catch ( SDKException se ) when ( se . StatusCode == 404 )
575
+ {
576
+ // We know we've got an issue now.
577
+ opaSupportsBatchQueryAPI = false ;
578
+ LogMessages . LogBatchQueryFallback ( _logger ) ;
579
+ // Fall-through to the "unsupported" case.
580
+ }
581
+ }
582
+ // Implicitly rethrow all other exceptions.
583
+
584
+ // Fall back to sequential queries against the OPA instance.
585
+ if ( ! opaSupportsBatchQueryAPI )
586
+ {
587
+ foreach ( var ( key , value ) in inputs )
588
+ {
589
+ try
590
+ {
591
+ var res = await evalPolicySingle ( path , Input . CreateMapOfAny ( value ) ) ;
592
+ successResults . Add ( key , convertResult < T > ( res . SuccessfulPolicyResponse ! . Result ! ) ) ;
593
+ }
594
+ catch ( ClientError ce )
595
+ {
596
+ throw ce ; // Rethrow for the caller to deal with. Request was malformed.
597
+ }
598
+ catch ( Styra . Opa . OpenApi . Models . Errors . ServerError se )
599
+ {
600
+ failureResults . Add ( key , ( OpaError ) se ) ;
601
+ }
602
+ // Implicitly rethrow all other exceptions.
603
+ }
604
+
605
+ // If we have the mixed case, add the HttpStatusCode fields.
606
+ if ( successResults . Count > 0 && failureResults . Count > 0 )
607
+ {
608
+ // Modifying the dictionary element while iterating is a language feature since 2020, apparently.
609
+ // Ref: https://github.com/dotnet/runtime/pull/34667
610
+ foreach ( var key in failureResults . Keys )
611
+ {
612
+ failureResults [ key ] . HttpStatusCode = "500" ;
613
+ }
614
+ }
615
+
616
+ return ( successResults , failureResults ) ;
617
+ }
618
+
619
+ // This *should* never happen. It means we didn't return from the batch or fallback handler blocks earlier.
620
+ throw new Exception ( "Impossible error" ) ;
621
+ }
622
+
492
623
/// <exclude />
493
624
private async Task < ExecutePolicyWithInputResponse > evalPolicySingle ( string path , Input input )
494
625
{
@@ -512,7 +643,7 @@ private async Task<ExecutePolicyWithInputResponse> evalPolicySingle(string path,
512
643
513
644
/// <exclude />
514
645
// Designed to respect the nullability of the incoming generic type when possible.
515
- private static T convertResult < T > ( Result resultValue )
646
+ protected internal static T convertResult < T > ( Result resultValue )
516
647
{
517
648
// We check to see if T maps to any of the core JSON types.
518
649
// We do the type-switch here, so that high-level clients don't have to.
0 commit comments