github.com/openfga/openfga@v1.5.4-rc1/internal/condition/condition_test.go (about) 1 package condition_test 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "testing" 8 "time" 9 10 openfgav1 "github.com/openfga/api/proto/openfga/v1" 11 "github.com/stretchr/testify/require" 12 "google.golang.org/protobuf/types/known/structpb" 13 14 "github.com/openfga/openfga/internal/condition" 15 "github.com/openfga/openfga/internal/condition/types" 16 ) 17 18 func TestNewCompiled(t *testing.T) { 19 var tests = []struct { 20 name string 21 condition *openfgav1.Condition 22 err *condition.CompilationError 23 }{ 24 { 25 name: "valid", 26 condition: &openfgav1.Condition{ 27 Name: "condition1", 28 Expression: "param1 == 'ok'", 29 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 30 "param1": { 31 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 32 }, 33 }, 34 }, 35 err: nil, 36 }, 37 { 38 name: "invalid_parameter_type", 39 condition: &openfgav1.Condition{ 40 Name: "condition1", 41 Expression: "param1 == 'ok'", 42 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 43 "param1": { 44 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_UNSPECIFIED, 45 }, 46 }, 47 }, 48 err: &condition.CompilationError{ 49 Condition: "condition1", 50 Cause: fmt.Errorf("failed to decode parameter type for parameter 'param1': unknown condition parameter type `TYPE_NAME_UNSPECIFIED`"), 51 }, 52 }, 53 { 54 name: "invalid_expression", 55 condition: &openfgav1.Condition{ 56 Name: "condition1", 57 Expression: "invalid", 58 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 59 "param1": { 60 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 61 }, 62 }, 63 }, 64 err: &condition.CompilationError{ 65 Condition: "condition1", 66 Cause: fmt.Errorf("ERROR: condition1:1:1: undeclared reference to 'invalid' (in container '')\n | invalid\n | ^"), 67 }, 68 }, 69 { 70 name: "invalid_output_type", 71 condition: &openfgav1.Condition{ 72 Name: "condition1", 73 Expression: "param1", 74 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 75 "param1": { 76 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 77 }, 78 }, 79 }, 80 err: &condition.CompilationError{ 81 Condition: "condition1", 82 Cause: fmt.Errorf("expected a bool condition expression output, but got 'string'"), 83 }, 84 }, 85 { 86 name: "ipaddress_literal_malformed_bool", 87 condition: &openfgav1.Condition{ 88 Name: "condition1", 89 Expression: `ipaddress(true).in_cidr("192.168.0.0/24")`, 90 Parameters: map[string]*openfgav1.ConditionParamTypeRef{}, 91 }, 92 err: &condition.CompilationError{ 93 Condition: "condition1", 94 Cause: fmt.Errorf("ERROR: condition1:1:10: found no matching overload for 'ipaddress' applied to '(bool)'\n | ipaddress(true).in_cidr(\"192.168.0.0/24\")\n | .........^"), 95 }, 96 }, 97 } 98 99 for _, test := range tests { 100 t.Run(test.name, func(t *testing.T) { 101 _, err := condition.NewCompiled(test.condition) 102 103 if test.err != nil { 104 require.EqualError(t, err, test.err.Error()) 105 } else { 106 require.NoError(t, err) 107 } 108 }) 109 } 110 } 111 112 func TestEvaluate(t *testing.T) { 113 var tests = []struct { 114 name string 115 condition *openfgav1.Condition 116 context map[string]interface{} 117 result condition.EvaluationResult 118 err *condition.EvaluationError 119 }{ 120 { 121 name: "success_condition_met", 122 condition: &openfgav1.Condition{ 123 Name: "condition1", 124 Expression: "param1 == 'ok'", 125 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 126 "param1": { 127 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 128 }, 129 }, 130 }, 131 context: map[string]interface{}{ 132 "param1": "ok", 133 }, 134 result: condition.EvaluationResult{ConditionMet: true}, 135 }, 136 { 137 name: "success_condition_unmet", 138 condition: &openfgav1.Condition{ 139 Name: "condition1", 140 Expression: "param1 == 'ok'", 141 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 142 "param1": { 143 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 144 }, 145 }, 146 }, 147 context: map[string]interface{}{"param1": "notok"}, 148 result: condition.EvaluationResult{ConditionMet: false}, 149 }, 150 { 151 name: "fail_no_such_attribute_nil_context", 152 condition: &openfgav1.Condition{ 153 Name: "condition1", 154 Expression: "param1 == 'ok'", 155 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 156 "param1": { 157 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 158 }, 159 }, 160 }, 161 context: nil, 162 result: condition.EvaluationResult{ 163 ConditionMet: false, 164 MissingParameters: []string{"param1"}, 165 }, 166 }, 167 { 168 name: "fail_no_such_attribute_empty_context", 169 condition: &openfgav1.Condition{ 170 Name: "condition1", 171 Expression: "param1 == 'ok'", 172 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 173 "param1": { 174 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 175 }, 176 }, 177 }, 178 context: map[string]interface{}{}, 179 result: condition.EvaluationResult{ 180 ConditionMet: false, 181 MissingParameters: []string{"param1"}, 182 }, 183 }, 184 { 185 name: "fail_found_invalid_context_parameter", 186 condition: &openfgav1.Condition{ 187 Name: "condition1", 188 Expression: "param1 == 'ok'", 189 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 190 "param1": { 191 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 192 }, 193 }, 194 }, 195 context: map[string]interface{}{ 196 "param2": "ok", 197 }, 198 result: condition.EvaluationResult{ 199 ConditionMet: false, 200 MissingParameters: []string{"param1"}, 201 }, 202 }, 203 { 204 name: "fail_unexpected_type", 205 condition: &openfgav1.Condition{ 206 Name: "condition1", 207 Expression: "param1 == 'ok'", 208 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 209 "param1": { 210 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 211 }, 212 }, 213 }, 214 context: map[string]interface{}{ 215 "param1": true, 216 }, 217 result: condition.EvaluationResult{ConditionMet: false}, 218 err: &condition.EvaluationError{ 219 Condition: "condition1", 220 Cause: &condition.ParameterTypeError{ 221 Condition: "condition1", 222 Cause: fmt.Errorf("failed to convert context parameter 'param1': expected type value 'string', but found 'bool'"), 223 }, 224 }, 225 }, 226 { 227 name: "ipaddress_literal", 228 condition: &openfgav1.Condition{ 229 Name: "condition1", 230 Expression: `ipaddress("192.168.0.1").in_cidr("192.168.0.0/24")`, 231 Parameters: map[string]*openfgav1.ConditionParamTypeRef{}, 232 }, 233 context: map[string]interface{}{}, 234 result: condition.EvaluationResult{ConditionMet: true}, 235 }, 236 { 237 name: "ipaddress_literal_malformed_addr", 238 condition: &openfgav1.Condition{ 239 Name: "condition1", 240 Expression: `ipaddress("192.168.0").in_cidr("192.168.0.0/24")`, 241 Parameters: map[string]*openfgav1.ConditionParamTypeRef{}, 242 }, 243 context: map[string]interface{}{}, 244 result: condition.EvaluationResult{ConditionMet: false}, 245 err: &condition.EvaluationError{ 246 Condition: "condition1", 247 Cause: fmt.Errorf("failed to evaluate condition expression: ParseAddr(\"192.168.0\"): IPv4 address too short"), 248 }, 249 }, 250 } 251 252 for _, test := range tests { 253 t.Run(test.name, func(t *testing.T) { 254 ctx := context.Background() 255 compiledCondition, err := condition.NewCompiled(test.condition) 256 require.NoError(t, err) 257 258 contextStruct, err := structpb.NewStruct(test.context) 259 require.NoError(t, err) 260 261 result, err := compiledCondition.Evaluate(ctx, contextStruct.GetFields()) 262 263 require.Equal(t, test.result, result) 264 if test.err != nil { 265 var evalError *condition.EvaluationError 266 require.ErrorAs(t, err, &evalError) 267 require.EqualError(t, evalError, test.err.Error()) 268 } else { 269 require.NoError(t, err) 270 } 271 }) 272 } 273 } 274 275 func TestEvaluateWithMaxCost(t *testing.T) { 276 var tests = []struct { 277 name string 278 condition *openfgav1.Condition 279 context map[string]any 280 maxCost uint64 281 result condition.EvaluationResult 282 err error 283 }{ 284 { 285 name: "cost_exceeded_int", 286 condition: &openfgav1.Condition{ 287 Name: "condition1", 288 Expression: "x < y", 289 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 290 "x": { 291 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_INT, 292 }, 293 "y": { 294 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_INT, 295 }, 296 }, 297 }, 298 context: map[string]interface{}{ 299 "x": int64(1), 300 "y": int64(2), 301 }, 302 maxCost: 2, 303 err: fmt.Errorf("operation cancelled: actual cost limit exceeded"), 304 }, 305 { 306 name: "cost_not_exceeded_int", 307 condition: &openfgav1.Condition{ 308 Name: "condition1", 309 Expression: "x < y", 310 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 311 "x": { 312 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_INT, 313 }, 314 "y": { 315 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_INT, 316 }, 317 }, 318 }, 319 context: map[string]interface{}{ 320 "x": int64(1), 321 "y": int64(2), 322 }, 323 maxCost: 3, 324 result: condition.EvaluationResult{ 325 Cost: 3, 326 ConditionMet: true, 327 }, 328 }, 329 { 330 name: "cost_exceeded_str", 331 condition: &openfgav1.Condition{ 332 Name: "condition1", 333 Expression: "x == y", 334 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 335 "x": { 336 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 337 }, 338 "y": { 339 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 340 }, 341 }, 342 }, 343 context: map[string]interface{}{ 344 "x": "ab", 345 "y": "ab", 346 }, 347 maxCost: 2, 348 err: fmt.Errorf("operation cancelled: actual cost limit exceeded"), 349 }, 350 { 351 name: "cost_exceeded_list", 352 condition: &openfgav1.Condition{ 353 Name: "condition1", 354 Expression: "'a' in strlist", 355 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 356 "strlist": { 357 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_LIST, 358 GenericTypes: []*openfgav1.ConditionParamTypeRef{ 359 { 360 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 361 }, 362 }, 363 }, 364 }, 365 }, 366 context: map[string]interface{}{ 367 "strlist": []interface{}{"c", "b", "a"}, 368 }, 369 maxCost: 3, 370 err: fmt.Errorf("operation cancelled: actual cost limit exceeded"), 371 }, 372 { 373 name: "cost_not_exceeded_list", 374 condition: &openfgav1.Condition{ 375 Name: "condition1", 376 Expression: "'d' in strlist", 377 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 378 "strlist": { 379 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_LIST, 380 GenericTypes: []*openfgav1.ConditionParamTypeRef{ 381 { 382 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 383 }, 384 }, 385 }, 386 }, 387 }, 388 context: map[string]interface{}{ 389 "strlist": []interface{}{"a", "b", "c"}, 390 }, 391 maxCost: 4, 392 result: condition.EvaluationResult{ 393 Cost: 4, 394 ConditionMet: false, 395 }, 396 }, 397 } 398 399 for _, test := range tests { 400 t.Run(test.name, func(t *testing.T) { 401 ctx := context.Background() 402 condition := condition.NewUncompiled(test.condition).WithMaxEvaluationCost(test.maxCost) 403 404 err := condition.Compile() 405 require.NoError(t, err) 406 407 contextStruct, err := structpb.NewStruct(test.context) 408 require.NoError(t, err) 409 410 result, err := condition.Evaluate(ctx, contextStruct.GetFields()) 411 412 require.Equal(t, test.result, result) 413 if test.err != nil { 414 require.ErrorContains(t, err, test.err.Error()) 415 } else { 416 require.NoError(t, err) 417 } 418 }) 419 } 420 } 421 422 func TestCastContextToTypedParameters(t *testing.T) { 423 tests := []struct { 424 name string 425 contextMap map[string]any 426 conditionParameterTypes map[string]*openfgav1.ConditionParamTypeRef 427 expectedParams map[string]any 428 expectedError *condition.ParameterTypeError 429 }{ 430 { 431 name: "valid", 432 contextMap: map[string]any{ 433 "param1": "ok", 434 }, 435 conditionParameterTypes: map[string]*openfgav1.ConditionParamTypeRef{ 436 "param1": { 437 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_STRING, 438 }, 439 }, 440 expectedParams: map[string]any{ 441 "param1": mustConvertValue(types.StringParamType, "ok"), 442 }, 443 }, 444 { 445 name: "empty_context_map", 446 contextMap: map[string]any{}, 447 conditionParameterTypes: map[string]*openfgav1.ConditionParamTypeRef{}, 448 expectedParams: nil, 449 }, 450 { 451 name: "empty_parameter_types", 452 contextMap: map[string]any{ 453 "param1": "ok", 454 }, 455 conditionParameterTypes: map[string]*openfgav1.ConditionParamTypeRef{}, 456 expectedParams: nil, 457 expectedError: &condition.ParameterTypeError{ 458 Condition: "condition1", 459 Cause: fmt.Errorf("no parameters defined for the condition"), 460 }, 461 }, 462 { 463 name: "failed_to_decode_condition_parameter_type", 464 contextMap: map[string]any{ 465 "param1": "ok", 466 }, 467 conditionParameterTypes: map[string]*openfgav1.ConditionParamTypeRef{ 468 "param1": { 469 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_UNSPECIFIED, 470 }, 471 }, 472 expectedParams: nil, 473 expectedError: &condition.ParameterTypeError{ 474 Condition: "condition1", 475 Cause: fmt.Errorf("failed to decode condition parameter type 'TYPE_NAME_UNSPECIFIED': unknown condition parameter type `TYPE_NAME_UNSPECIFIED`"), 476 }, 477 }, 478 } 479 480 for _, test := range tests { 481 t.Run(test.name, func(t *testing.T) { 482 c := condition.NewUncompiled(&openfgav1.Condition{ 483 Name: "condition1", 484 Expression: "param1 == 'ok'", 485 Parameters: test.conditionParameterTypes, 486 }) 487 488 contextStruct, err := structpb.NewStruct(test.contextMap) 489 require.NoError(t, err) 490 491 typedParams, err := c.CastContextToTypedParameters(contextStruct.GetFields()) 492 493 if test.expectedError != nil { 494 require.Error(t, err) 495 require.EqualError(t, err, test.expectedError.Error()) 496 } else { 497 require.NoError(t, err) 498 } 499 500 if !reflect.DeepEqual(typedParams, test.expectedParams) { 501 t.Errorf("expected %v, got %v", test.expectedParams, typedParams) 502 } 503 }) 504 } 505 } 506 507 func TestEvaluateWithInterruptCheckFrequency(t *testing.T) { 508 makeItems := func(size int) []interface{} { 509 items := make([]interface{}, size) 510 for i := int(0); i < size; i++ { 511 items[i] = i 512 } 513 return items 514 } 515 516 // numLoops is the number of loops being evaluated by a CEL 517 // expression. This number needs to be large enough to not 518 // be resolved before the 1 microsecond context timeout. 519 numLoops := 500 520 521 var tests = []struct { 522 name string 523 condition *openfgav1.Condition 524 context map[string]any 525 checkFrequency uint 526 result condition.EvaluationResult 527 err error 528 }{ 529 { 530 name: "operation_interrupted_one_comprehension", 531 condition: &openfgav1.Condition{ 532 Name: "condition1", 533 Expression: "items.map(i, i * 2).size() > 0", 534 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 535 "items": { 536 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_LIST, 537 GenericTypes: []*openfgav1.ConditionParamTypeRef{ 538 { 539 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_INT, 540 }, 541 }, 542 }, 543 }, 544 }, 545 checkFrequency: uint(numLoops), 546 context: map[string]interface{}{ 547 "items": makeItems(numLoops), 548 }, 549 result: condition.EvaluationResult{ 550 ConditionMet: false, 551 }, 552 err: fmt.Errorf("failed to evaluate relationship condition: 'condition1' - failed to evaluate condition expression: operation interrupted"), 553 }, 554 { 555 name: "operation_not_interrupted_one_comprehension", 556 condition: &openfgav1.Condition{ 557 Name: "condition1", 558 Expression: "items.map(i, i * 2).size() > 0", 559 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 560 "items": { 561 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_LIST, 562 GenericTypes: []*openfgav1.ConditionParamTypeRef{ 563 { 564 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_INT, 565 }, 566 }, 567 }, 568 }, 569 }, 570 checkFrequency: uint(numLoops), 571 context: map[string]interface{}{ 572 "items": makeItems(numLoops - 1), 573 }, 574 result: condition.EvaluationResult{ 575 ConditionMet: true, 576 }, 577 err: nil, 578 }, 579 { 580 name: "operation_interrupted_two_comprehensions", 581 condition: &openfgav1.Condition{ 582 Name: "condition1", 583 Expression: "items.map(i, i * 2).map(i, i * i).size() > 0", 584 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 585 "items": { 586 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_LIST, 587 GenericTypes: []*openfgav1.ConditionParamTypeRef{ 588 { 589 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_INT, 590 }, 591 }, 592 }, 593 }, 594 }, 595 checkFrequency: uint(numLoops), 596 context: map[string]interface{}{ 597 "items": makeItems(numLoops), 598 }, 599 result: condition.EvaluationResult{ 600 ConditionMet: false, 601 }, 602 err: fmt.Errorf("failed to evaluate relationship condition: 'condition1' - failed to evaluate condition expression: operation interrupted"), 603 }, 604 { 605 name: "operation_not_interrupted_two_comprehensions", 606 condition: &openfgav1.Condition{ 607 Name: "condition1", 608 Expression: "items.map(i, i * 2).map(i, i * i).size() > 0", 609 Parameters: map[string]*openfgav1.ConditionParamTypeRef{ 610 "items": { 611 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_LIST, 612 GenericTypes: []*openfgav1.ConditionParamTypeRef{ 613 { 614 TypeName: openfgav1.ConditionParamTypeRef_TYPE_NAME_INT, 615 }, 616 }, 617 }, 618 }, 619 }, 620 checkFrequency: uint(numLoops), 621 context: map[string]interface{}{ 622 "items": makeItems(numLoops - 1), 623 }, 624 result: condition.EvaluationResult{ 625 ConditionMet: false, 626 }, 627 err: fmt.Errorf("failed to evaluate relationship condition: 'condition1' - failed to evaluate condition expression: operation interrupted"), 628 }, 629 } 630 631 for _, test := range tests { 632 t.Run(test.name, func(t *testing.T) { 633 ctx := context.Background() 634 condition := condition.NewUncompiled(test.condition). 635 WithInterruptCheckFrequency(test.checkFrequency) 636 637 err := condition.Compile() 638 require.NoError(t, err) 639 640 contextStruct, err := structpb.NewStruct(test.context) 641 require.NoError(t, err) 642 643 evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond) 644 defer cancel() 645 646 result, err := condition.Evaluate(evalCtx, contextStruct.GetFields()) 647 648 require.Equal(t, test.result, result) 649 if test.err != nil { 650 require.ErrorContains(t, err, test.err.Error()) 651 } else { 652 require.NoError(t, err) 653 } 654 }) 655 } 656 } 657 658 func mustConvertValue(varType types.ParameterType, value any) any { 659 convertedParam, err := varType.ConvertValue(value) 660 if err != nil { 661 panic(err) 662 } 663 664 return convertedParam 665 }