github.com/greenpau/go-authcrunch@v1.1.4/pkg/acl/acl_test.go (about) 1 // Copyright 2022 Paul Greenberg greenpau@outlook.com 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package acl 16 17 import ( 18 "context" 19 "fmt" 20 "github.com/google/go-cmp/cmp" 21 "github.com/greenpau/go-authcrunch/internal/tests" 22 "github.com/greenpau/go-authcrunch/pkg/errors" 23 logutil "github.com/greenpau/go-authcrunch/pkg/util/log" 24 "testing" 25 "time" 26 ) 27 28 func TestNewAccessList(t *testing.T) { 29 var testcases = []struct { 30 name string 31 config []*RuleConfiguration 32 batch bool 33 defaultAllow bool 34 input map[string]interface{} 35 want map[string]interface{} 36 shouldErr bool 37 err error 38 }{ 39 { 40 name: "new access list with logging", 41 config: []*RuleConfiguration{ 42 { 43 Comment: "foobar barfoo", 44 Conditions: []string{ 45 "exact match roles foobar", 46 "exact match org nyc", 47 }, 48 Action: `allow any stop log`, 49 }, 50 }, 51 input: map[string]interface{}{ 52 "name": "John Smith", 53 "roles": []string{"foobar"}, 54 }, 55 want: map[string]interface{}{ 56 "allow": true, 57 }, 58 }, 59 { 60 name: "new access list with batched conditions", 61 config: []*RuleConfiguration{ 62 { 63 Comment: "foobar barfoo", 64 Conditions: []string{ 65 "exact match roles foobar", 66 "exact match org nyc", 67 }, 68 Action: `allow any stop log`, 69 }, 70 }, 71 batch: true, 72 input: map[string]interface{}{ 73 "name": "John Smith", 74 "roles": []string{"foobar"}, 75 }, 76 want: map[string]interface{}{ 77 "allow": true, 78 }, 79 }, 80 { 81 name: "new access list with default allow", 82 config: []*RuleConfiguration{ 83 { 84 Comment: "foobar barfoo", 85 Conditions: []string{ 86 "exact match roles foobar", 87 "exact match org nyc", 88 }, 89 Action: `allow any stop log`, 90 }, 91 }, 92 defaultAllow: true, 93 input: map[string]interface{}{ 94 "name": "John Smith", 95 }, 96 want: map[string]interface{}{ 97 "allow": true, 98 }, 99 }, 100 { 101 name: "new access list with invalid conditions", 102 config: []*RuleConfiguration{ 103 { 104 Comment: "foobar barfoo", 105 Conditions: []string{ 106 "", 107 "", 108 }, 109 Action: `allow any stop log`, 110 }, 111 }, 112 shouldErr: true, 113 err: fmt.Errorf("invalid rule syntax, failed to extract condition tokens: EOF"), 114 }, 115 { 116 name: "new access list with invalid batched conditions", 117 config: []*RuleConfiguration{ 118 { 119 Comment: "foobar barfoo", 120 Conditions: []string{ 121 "", 122 "", 123 }, 124 Action: `allow any stop log`, 125 }, 126 }, 127 batch: true, 128 shouldErr: true, 129 err: fmt.Errorf("invalid rule syntax, failed to extract condition tokens: EOF"), 130 }, 131 132 { 133 name: "new access list with allow verdict", 134 config: []*RuleConfiguration{ 135 { 136 Comment: "foobar barfoo", 137 Conditions: []string{ 138 "exact match roles foobar", 139 "exact match org nyc", 140 }, 141 Action: `allow any log`, 142 }, 143 }, 144 input: map[string]interface{}{ 145 "name": "John Smith", 146 "roles": []string{"foobar"}, 147 }, 148 want: map[string]interface{}{ 149 "allow": true, 150 }, 151 }, 152 { 153 name: "new access list with deny verdict", 154 config: []*RuleConfiguration{ 155 { 156 Comment: "foobar barfoo", 157 Conditions: []string{ 158 "exact match roles foobar", 159 "exact match org nyc", 160 }, 161 Action: `deny any log`, 162 }, 163 }, 164 input: map[string]interface{}{ 165 "name": "John Smith", 166 "roles": []string{"foobar"}, 167 }, 168 want: map[string]interface{}{ 169 "allow": false, 170 }, 171 }, 172 { 173 name: "new access list with deny and stop verdict", 174 config: []*RuleConfiguration{ 175 { 176 Comment: "foobar barfoo", 177 Conditions: []string{ 178 "exact match roles foobar", 179 "exact match org nyc", 180 }, 181 Action: `deny any stop log`, 182 }, 183 }, 184 input: map[string]interface{}{ 185 "name": "John Smith", 186 "roles": []string{"foobar"}, 187 }, 188 want: map[string]interface{}{ 189 "allow": false, 190 }, 191 }, 192 { 193 name: "new access list with default deny", 194 config: []*RuleConfiguration{ 195 { 196 Comment: "foobar barfoo", 197 Conditions: []string{ 198 "exact match roles foobar", 199 "exact match org nyc", 200 }, 201 Action: `deny any stop log`, 202 }, 203 }, 204 input: map[string]interface{}{ 205 "name": "John Smith", 206 }, 207 want: map[string]interface{}{ 208 "allow": false, 209 }, 210 }, 211 } 212 213 for _, tc := range testcases { 214 t.Run(tc.name, func(t *testing.T) { 215 var err error 216 ctx := context.Background() 217 logger := logutil.NewLogger() 218 accessList := NewAccessList() 219 accessList.SetLogger(logger) 220 if tc.defaultAllow { 221 accessList.SetDefaultAllowAction() 222 } 223 if tc.batch { 224 err = accessList.AddRules(ctx, tc.config) 225 if tests.EvalErr(t, err, tc.config, tc.shouldErr, tc.err) { 226 return 227 } 228 } else { 229 for _, rule := range tc.config { 230 err = accessList.AddRule(ctx, rule) 231 if tests.EvalErr(t, err, tc.config, tc.shouldErr, tc.err) { 232 return 233 } 234 } 235 } 236 237 tc.want["rule_count"] = len(tc.config) 238 got := make(map[string]interface{}) 239 got["allow"] = accessList.Allow(ctx, tc.input) 240 got["rule_count"] = len(accessList.GetRules()) 241 242 tests.EvalObjects(t, "eval", tc.want, got) 243 }) 244 } 245 } 246 247 func TestCustomAccessList(t *testing.T) { 248 var testcases = []struct { 249 name string 250 config []*RuleConfiguration 251 disabled bool 252 defaultAllow bool 253 input map[string]interface{} 254 // want map[string]interface{} 255 want string 256 shouldErr bool 257 err error 258 }{ 259 { 260 name: "deny roles foobar", 261 config: []*RuleConfiguration{ 262 { 263 Comment: "match roles foobar and deny", 264 Conditions: []string{ 265 "match roles foobar", 266 }, 267 Action: `deny stop log`, 268 }, 269 }, 270 input: map[string]interface{}{ 271 "name": "John Smith", 272 "roles": []string{"foobar"}, 273 }, 274 want: `{ 275 "allow": false, 276 "config": { 277 "count": 1, 278 "rules": [ 279 { 280 "action": "ruleActionDeny", 281 "comment": "match roles foobar and deny", 282 "conditions": [ 283 { 284 "always_true": false, 285 "condition_type": "ruleStrCondExactMatchListStrInput", 286 "expr_data_type": "dataTypeStr", 287 "field": "roles", 288 "input_data_type": "dataTypeListStr", 289 "match_any": false, 290 "match_strategy": "fieldMatchExact", 291 "regex_enabled": false, 292 "values": [ 293 "foobar" 294 ] 295 } 296 ], 297 "counter_enabled": false, 298 "fields": [ 299 "roles" 300 ], 301 "log_level": "info", 302 "log_enabled": true, 303 "match_all": true, 304 "rule_type": "aclRuleDenyWithInfoLoggerStop", 305 "tag": "rule0" 306 } 307 ] 308 } 309 }`, 310 }, 311 { 312 name: "deny role foobar with email outside of specific email domain", 313 config: []*RuleConfiguration{ 314 { 315 Comment: "deny role foobar with email outside of @bar.foo", 316 Conditions: []string{ 317 "no suffix match email @bar.foo", 318 "match role foobar", 319 }, 320 Action: `deny stop log`, 321 }, 322 { 323 Comment: "default allow", 324 Conditions: []string{ 325 "match any", 326 }, 327 Action: `allow stop log`, 328 }, 329 }, 330 input: map[string]interface{}{ 331 "name": "John Smith", 332 "email": "jsmith@bar.foo", 333 "roles": []string{"foobar"}, 334 "exp": time.Now().Add(time.Duration(180) * time.Second).UTC().Unix(), 335 }, 336 want: `{ 337 "allow": true, 338 "config": { 339 "count": 2, 340 "rules": [ 341 { 342 "action": "ruleActionDeny", 343 "comment": "deny role foobar with email outside of @bar.foo", 344 "conditions": [ 345 { 346 "always_true": false, 347 "condition_type": "ruleStrCondSuffixNegativeMatchStrInput", 348 "expr_data_type": "dataTypeStr", 349 "field": "email", 350 "input_data_type": "dataTypeStr", 351 "match_any": false, 352 "match_strategy": "fieldMatchSuffix", 353 "regex_enabled": false, 354 "values": [ 355 "@bar.foo" 356 ] 357 }, 358 { 359 "always_true": false, 360 "condition_type": "ruleStrCondExactMatchListStrInput", 361 "expr_data_type": "dataTypeStr", 362 "field": "roles", 363 "input_data_type": "dataTypeListStr", 364 "match_any": false, 365 "match_strategy": "fieldMatchExact", 366 "regex_enabled": false, 367 "values": [ 368 "foobar" 369 ] 370 } 371 ], 372 "counter_enabled": false, 373 "fields": [ 374 "email", 375 "roles" 376 ], 377 "log_enabled": true, 378 "log_level": "info", 379 "match_all": true, 380 "rule_type": "aclRuleDenyWithInfoLoggerMatchAllStop", 381 "tag": "rule0" 382 }, 383 { 384 "action": "ruleActionAllow", 385 "comment": "default allow", 386 "conditions": [ 387 { 388 "always_true": true, 389 "condition_type": "ruleAnyCondAlwaysMatchAnyInput", 390 "expr_data_type": "dataTypeAny", 391 "field": "exp", 392 "input_data_type": "dataTypeAny", 393 "match_any": false, 394 "match_strategy": "fieldMatchAlways", 395 "regex_enabled": false 396 } 397 ], 398 "counter_enabled": false, 399 "fields": [ 400 "exp" 401 ], 402 "log_enabled": true, 403 "log_level": "info", 404 "match_all": true, 405 "rule_type": "aclRuleAllowWithInfoLoggerStop", 406 "tag": "rule1" 407 } 408 ] 409 } 410 }`, 411 }, 412 { 413 name: "allow when roles field exists, mutiple conditions, match all", 414 config: []*RuleConfiguration{ 415 { 416 Conditions: []string{ 417 "field roles exists", 418 "suffix match email @bar.foo", 419 }, 420 Action: `allow stop`, 421 }, 422 { 423 Comment: "default deny", 424 Conditions: []string{ 425 "match any", 426 }, 427 Action: `deny stop log`, 428 }, 429 }, 430 input: map[string]interface{}{ 431 "name": "John Smith", 432 "email": "jsmith@bar.foo", 433 "roles": []string{"foobar"}, 434 "exp": time.Now().Add(time.Duration(180) * time.Second).UTC().Unix(), 435 }, 436 want: `{ 437 "allow": true, 438 "config": { 439 "count": 2, 440 "rules": [ 441 { 442 "action": "ruleActionAllow", 443 "check_fields": { 444 "roles": true 445 }, 446 "conditions": [ 447 { 448 "always_true": false, 449 "condition_type": "ruleCondFieldFound", 450 "expr_data_type": "dataTypeAny", 451 "field": "roles", 452 "input_data_type": "dataTypeAny", 453 "match_any": false, 454 "match_strategy": "fieldFound", 455 "regex_enabled": false 456 }, 457 { 458 "always_true": false, 459 "condition_type": "ruleStrCondSuffixMatchStrInput", 460 "expr_data_type": "dataTypeStr", 461 "field": "email", 462 "input_data_type": "dataTypeStr", 463 "match_any": false, 464 "match_strategy": "fieldMatchSuffix", 465 "regex_enabled": false, 466 "values": [ 467 "@bar.foo" 468 ] 469 } 470 ], 471 "counter_enabled": false, 472 "fields": [ 473 "roles", 474 "email" 475 ], 476 "log_enabled": false, 477 "match_all": true, 478 "rule_type": "aclRuleFieldCheckAllowMatchAllStop" 479 }, 480 { 481 "action": "ruleActionDeny", 482 "comment": "default deny", 483 "conditions": [ 484 { 485 "always_true": true, 486 "condition_type": "ruleAnyCondAlwaysMatchAnyInput", 487 "expr_data_type": "dataTypeAny", 488 "field": "exp", 489 "input_data_type": "dataTypeAny", 490 "match_any": false, 491 "match_strategy": "fieldMatchAlways", 492 "regex_enabled": false 493 } 494 ], 495 "counter_enabled": false, 496 "fields": [ 497 "exp" 498 ], 499 "log_enabled": true, 500 "log_level": "info", 501 "match_all": true, 502 "rule_type": "aclRuleDenyWithInfoLoggerStop", 503 "tag": "rule1" 504 } 505 ] 506 } 507 }`, 508 }, 509 { 510 name: "allow when roles field exists, mutiple conditions, match any", 511 config: []*RuleConfiguration{ 512 { 513 Conditions: []string{ 514 "field roles exists", 515 "suffix match email @bar.foo", 516 }, 517 Action: `allow any stop`, 518 }, 519 { 520 Comment: "default deny", 521 Conditions: []string{ 522 "match any", 523 }, 524 Action: `deny stop log`, 525 }, 526 }, 527 input: map[string]interface{}{ 528 "name": "John Smith", 529 // "email": "jsmith@bar.foo", 530 "roles": []string{"foobar"}, 531 "exp": time.Now().Add(time.Duration(180) * time.Second).UTC().Unix(), 532 }, 533 want: `{ 534 "allow": true, 535 "config": { 536 "count": 2, 537 "rules": [ 538 { 539 "action": "ruleActionAllow", 540 "check_fields": { 541 "roles": true 542 }, 543 "conditions": [ 544 { 545 "always_true": false, 546 "condition_type": "ruleCondFieldFound", 547 "expr_data_type": "dataTypeAny", 548 "field": "roles", 549 "input_data_type": "dataTypeAny", 550 "match_any": false, 551 "match_strategy": "fieldFound", 552 "regex_enabled": false 553 }, 554 { 555 "always_true": false, 556 "condition_type": "ruleStrCondSuffixMatchStrInput", 557 "expr_data_type": "dataTypeStr", 558 "field": "email", 559 "input_data_type": "dataTypeStr", 560 "match_any": false, 561 "match_strategy": "fieldMatchSuffix", 562 "regex_enabled": false, 563 "values": [ 564 "@bar.foo" 565 ] 566 } 567 ], 568 "counter_enabled": false, 569 "fields": [ 570 "roles", 571 "email" 572 ], 573 "log_enabled": false, 574 "match_all": false, 575 "rule_type": "aclRuleFieldCheckAllowMatchAnyStop" 576 }, 577 { 578 "action": "ruleActionDeny", 579 "comment": "default deny", 580 "conditions": [ 581 { 582 "always_true": true, 583 "condition_type": "ruleAnyCondAlwaysMatchAnyInput", 584 "expr_data_type": "dataTypeAny", 585 "field": "exp", 586 "input_data_type": "dataTypeAny", 587 "match_any": false, 588 "match_strategy": "fieldMatchAlways", 589 "regex_enabled": false 590 } 591 ], 592 "counter_enabled": false, 593 "fields": [ 594 "exp" 595 ], 596 "log_enabled": true, 597 "log_level": "info", 598 "match_all": true, 599 "rule_type": "aclRuleDenyWithInfoLoggerStop", 600 "tag": "rule1" 601 } 602 ] 603 } 604 }`, 605 }, 606 { 607 name: "allow when roles field exists and role matches", 608 config: []*RuleConfiguration{ 609 { 610 Conditions: []string{ 611 "field roles exists", 612 "match role foobar", 613 }, 614 Action: `allow stop`, 615 }, 616 { 617 Conditions: []string{ 618 "match any", 619 }, 620 Action: `deny stop log`, 621 }, 622 }, 623 input: map[string]interface{}{ 624 "name": "John Smith", 625 "email": "jsmith@bar.foo", 626 "roles": []string{"foobar"}, 627 "exp": time.Now().Add(time.Duration(180) * time.Second).UTC().Unix(), 628 }, 629 shouldErr: true, 630 err: errors.ErrACLRuleSyntaxDuplicateField.WithArgs("roles"), 631 }, 632 { 633 name: "allow when roles field exists", 634 config: []*RuleConfiguration{ 635 { 636 Conditions: []string{ 637 "field roles exists", 638 }, 639 Action: `allow stop`, 640 }, 641 { 642 Comment: "default deny", 643 Conditions: []string{ 644 "match any", 645 }, 646 Action: `deny stop log`, 647 }, 648 }, 649 input: map[string]interface{}{ 650 "name": "John Smith", 651 "email": "jsmith@bar.foo", 652 "roles": []string{"foobar"}, 653 "exp": time.Now().Add(time.Duration(180) * time.Second).UTC().Unix(), 654 }, 655 want: `{ 656 "allow": true, 657 "config": { 658 "count": 2, 659 "rules": [ 660 { 661 "action": "ruleActionAllow", 662 "conditions": [ 663 { 664 "always_true": false, 665 "condition_type": "ruleCondFieldFound", 666 "expr_data_type": "dataTypeAny", 667 "field": "roles", 668 "input_data_type": "dataTypeAny", 669 "match_any": false, 670 "match_strategy": "fieldFound", 671 "regex_enabled": false 672 } 673 ], 674 "counter_enabled": false, 675 "fields": [ 676 "roles" 677 ], 678 "check_fields": { 679 "roles": true 680 }, 681 "log_enabled": false, 682 "match_all": true, 683 "rule_type": "aclRuleFieldCheckAllowStop" 684 }, 685 { 686 "action": "ruleActionDeny", 687 "comment": "default deny", 688 "conditions": [ 689 { 690 "always_true": true, 691 "condition_type": "ruleAnyCondAlwaysMatchAnyInput", 692 "expr_data_type": "dataTypeAny", 693 "field": "exp", 694 "input_data_type": "dataTypeAny", 695 "match_any": false, 696 "match_strategy": "fieldMatchAlways", 697 "regex_enabled": false 698 } 699 ], 700 "counter_enabled": false, 701 "fields": [ 702 "exp" 703 ], 704 "log_enabled": true, 705 "log_level": "info", 706 "match_all": true, 707 "rule_type": "aclRuleDenyWithInfoLoggerStop", 708 "tag": "rule1" 709 } 710 ] 711 } 712 }`, 713 }, 714 { 715 name: "allow when roles field does not exists", 716 disabled: true, 717 config: []*RuleConfiguration{ 718 { 719 Conditions: []string{ 720 "field roles not exists", 721 }, 722 Action: `allow stop log counter`, 723 }, 724 { 725 Comment: "default deny", 726 Conditions: []string{ 727 "match any", 728 }, 729 Action: `deny stop log`, 730 }, 731 }, 732 input: map[string]interface{}{ 733 "name": "John Smith", 734 "email": "jsmith@bar.foo", 735 "exp": time.Now().Add(time.Duration(180) * time.Second).UTC().Unix(), 736 }, 737 want: `{ 738 "allow": true, 739 "config": { 740 "count": 2, 741 "rules": [ 742 { 743 "action": "ruleActionAllow", 744 "check_fields": { 745 "roles": false 746 }, 747 "conditions": [ 748 { 749 "always_true": false, 750 "condition_type": "ruleCondFieldNotFound", 751 "expr_data_type": "dataTypeAny", 752 "field": "roles", 753 "input_data_type": "dataTypeAny", 754 "match_any": false, 755 "match_strategy": "fieldNotFound", 756 "regex_enabled": false 757 } 758 ], 759 "counter_enabled": true, 760 "fields": [ 761 "roles" 762 ], 763 "log_enabled": true, 764 "log_level": "info", 765 "match_all": true, 766 "rule_type": "aclRuleFieldCheckAllowWithInfoLoggerCounterStop", 767 "tag": "rule0" 768 }, 769 { 770 "action": "ruleActionDeny", 771 "comment": "default deny", 772 "conditions": [ 773 { 774 "always_true": true, 775 "condition_type": "ruleAnyCondAlwaysMatchAnyInput", 776 "expr_data_type": "dataTypeAny", 777 "field": "exp", 778 "input_data_type": "dataTypeAny", 779 "match_any": false, 780 "match_strategy": "fieldMatchAlways", 781 "regex_enabled": false 782 } 783 ], 784 "counter_enabled": false, 785 "fields": [ 786 "exp" 787 ], 788 "log_enabled": true, 789 "log_level": "info", 790 "match_all": true, 791 "rule_type": "aclRuleDenyWithInfoLoggerStop", 792 "tag": "rule1" 793 } 794 ] 795 } 796 }`, 797 }, 798 { 799 name: "allow when metadata field exists", 800 disabled: true, 801 config: []*RuleConfiguration{ 802 { 803 Conditions: []string{ 804 "field metadata exists", 805 }, 806 Action: `allow stop`, 807 }, 808 }, 809 input: map[string]interface{}{ 810 "metadata": map[string]interface{}{ 811 "foo": "bar", 812 }, 813 }, 814 want: `{ 815 }`, 816 }, 817 { 818 name: "allow when email is not in foo.bar domain", 819 disabled: true, 820 config: []*RuleConfiguration{ 821 { 822 Conditions: []string{ 823 "no suffix match email @foo.bar", 824 }, 825 Action: `allow stop`, 826 }, 827 }, 828 input: map[string]interface{}{ 829 "email": "jsmith@bar.foo", 830 }, 831 want: `{ 832 }`, 833 }, 834 { 835 name: "allow when custom field foo equals to bar", 836 disabled: true, 837 config: []*RuleConfiguration{ 838 { 839 Conditions: []string{ 840 "match foo bar", 841 }, 842 Action: `allow stop`, 843 }, 844 }, 845 input: map[string]interface{}{ 846 "foo": "bar", 847 }, 848 want: `{ 849 }`, 850 }, 851 { 852 name: "allow when exp field does not exceed 2 hours lifetime", 853 disabled: true, 854 config: []*RuleConfiguration{ 855 { 856 Conditions: []string{ 857 "match expires_at less than 2 hours from now", 858 }, 859 Action: `allow stop`, 860 }, 861 { 862 Conditions: []string{ 863 "match any", 864 }, 865 Action: `deny stop`, 866 }, 867 }, 868 input: map[string]interface{}{ 869 "exp": 0, 870 }, 871 want: `{ 872 }`, 873 }, 874 } 875 876 for _, tc := range testcases { 877 t.Run(tc.name, func(t *testing.T) { 878 if tc.disabled { 879 t.SkipNow() 880 } 881 var err error 882 ctx := context.Background() 883 logger := logutil.NewLogger() 884 accessList := NewAccessList() 885 accessList.SetLogger(logger) 886 if tc.defaultAllow { 887 accessList.SetDefaultAllowAction() 888 } 889 err = accessList.AddRules(ctx, tc.config) 890 if tests.EvalErr(t, err, tc.config, tc.shouldErr, tc.err) { 891 return 892 } 893 got := make(map[string]interface{}) 894 got["config"] = tests.Unpack(t, accessList.AsMap()) 895 got["allow"] = accessList.Allow(ctx, tc.input) 896 want := tests.Unpack(t, tc.want) 897 898 if diff := cmp.Diff(want, got); diff != "" { 899 t.Logf("JSON: %s", tests.UnpackJSON(t, got)) 900 t.Errorf("NewPortal() rule mismatch (-want +got):\n%s", diff) 901 } 902 }) 903 } 904 }