github.com/greenpau/go-authcrunch@v1.1.4/pkg/authz/validator/validator_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 validator 16 17 import ( 18 "context" 19 "fmt" 20 "net/http" 21 "net/http/httptest" 22 "testing" 23 "time" 24 25 "github.com/greenpau/go-authcrunch/internal/tests" 26 "github.com/greenpau/go-authcrunch/internal/testutils" 27 "github.com/greenpau/go-authcrunch/pkg/acl" 28 "github.com/greenpau/go-authcrunch/pkg/authz/options" 29 "github.com/greenpau/go-authcrunch/pkg/errors" 30 "github.com/greenpau/go-authcrunch/pkg/kms" 31 "github.com/greenpau/go-authcrunch/pkg/requests" 32 "github.com/greenpau/go-authcrunch/pkg/user" 33 logutil "github.com/greenpau/go-authcrunch/pkg/util/log" 34 35 "github.com/google/go-cmp/cmp" 36 ) 37 38 var ( 39 40 // Create access list with default deny that allows read:books only 41 defaultDenyACL = []*acl.RuleConfiguration{ 42 { 43 Comment: "allow read:books scope", 44 Conditions: []string{ 45 "match scopes read:books", 46 }, 47 Action: `allow log`, 48 }, 49 } 50 51 // Create access list with default allow that denies write:books 52 defaultAllowACL = []*acl.RuleConfiguration{ 53 { 54 Comment: "deny write:books scope", 55 Conditions: []string{ 56 "match scopes write:books", 57 }, 58 Action: `deny`, 59 }, 60 { 61 Comment: "allow all scopes", 62 Conditions: []string{ 63 "field scopes exists", 64 }, 65 Action: `allow`, 66 }, 67 } 68 69 // Create access list with default deny that allows 127.0.0.1 only 70 audienceDefaultDenyACL = []*acl.RuleConfiguration{ 71 { 72 Conditions: []string{ 73 "match aud https://127.0.0.1:2019/", 74 }, 75 Action: `allow`, 76 }, 77 } 78 79 // Create access list with default allow that denies localhost 80 audienceDefaultAllowACL = []*acl.RuleConfiguration{ 81 { 82 Conditions: []string{ 83 "match aud https://localhost/", 84 }, 85 Action: `deny`, 86 }, 87 { 88 Comment: "allow all audiences", 89 Conditions: []string{ 90 "field audience exists", 91 }, 92 Action: `allow`, 93 }, 94 } 95 96 // Create access list with default deny and HTTP Method and Path rules 97 customACL = []*acl.RuleConfiguration{ 98 { 99 Conditions: []string{ 100 "match scope write:books", 101 "match method GET", 102 "match path /app/page1/blocked", 103 }, 104 Action: `deny`, 105 }, 106 { 107 Conditions: []string{ 108 "match scope write:books", 109 "match method GET", 110 "match path /app/page2/blocked", 111 }, 112 Action: `deny`, 113 }, 114 { 115 Conditions: []string{ 116 "match scope write:books", 117 "match method GET", 118 "match path /app/page3/allowed", 119 }, 120 Action: `allow`, 121 }, 122 { 123 Conditions: []string{ 124 "match scope read:books", 125 }, 126 Action: `allow`, 127 }, 128 } 129 130 // Create access list with default deny and mixed claims 131 mixedACL = []*acl.RuleConfiguration{ 132 { 133 Conditions: []string{ 134 "match scope write:books", 135 }, 136 Action: `allow`, 137 }, 138 { 139 Conditions: []string{ 140 "match audience https://127.0.0.1:2019/", 141 }, 142 Action: `allow`, 143 }, 144 } 145 146 // Create viewer persona 147 viewer = `{ 148 "exp": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix()) + `, 149 "iat": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute*-1).Unix()) + `, 150 "nbf": ` + fmt.Sprintf("%d", time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix()) + `, 151 "aud": ["https://127.0.0.1:2019/", "https://google.com/"], 152 "sub": "smithj@outlook.com", 153 "scope": ["read:books"] 154 }` 155 156 editor = `{ 157 "exp": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix()) + `, 158 "iat": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute*-1).Unix()) + `, 159 "nbf": ` + fmt.Sprintf("%d", time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix()) + `, 160 "aud": "https://localhost/", 161 "sub": "jane.smith@outlook.com", 162 "scope": ["write:books"] 163 }` 164 165 // Create access list with default deny that allows viewer only 166 defaultRolesDenyACL = []*acl.RuleConfiguration{ 167 { 168 Conditions: []string{ 169 "match role viewer", 170 }, 171 Action: `allow`, 172 }, 173 } 174 175 denyViewerAllowOthersACL = []*acl.RuleConfiguration{ 176 { 177 Conditions: []string{ 178 "match role viewer", 179 }, 180 Action: `deny`, 181 }, 182 { 183 Conditions: []string{ 184 "field roles exists", 185 }, 186 Action: `allow`, 187 }, 188 } 189 190 // Create access list with default allow that denies editor 191 defaultRolesAllowACL = []*acl.RuleConfiguration{ 192 { 193 Conditions: []string{ 194 "match role editor", 195 }, 196 Action: `deny`, 197 }, 198 { 199 Conditions: []string{ 200 "field roles exists", 201 }, 202 Action: `allow`, 203 }, 204 } 205 206 // Create access list with default deny and HTTP Method and Path rules 207 customRolesACL = []*acl.RuleConfiguration{ 208 { 209 Conditions: []string{ 210 "match role editor", 211 "match method GET", 212 "match path /app/page1/blocked", 213 }, 214 Action: `deny log`, 215 }, 216 { 217 Conditions: []string{ 218 "match role editor", 219 "match method GET", 220 "match path /app/page2/blocked", 221 }, 222 Action: `deny log`, 223 }, 224 { 225 Conditions: []string{ 226 "match role editor", 227 "match method GET", 228 "match path /app/page3/allowed", 229 }, 230 Action: `allow log`, 231 }, 232 { 233 Conditions: []string{ 234 "match role viewer", 235 }, 236 Action: `allow log`, 237 }, 238 } 239 240 // Create viewer persona 241 viewer2 = `{ 242 "exp": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix()) + `, 243 "iat": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute*-1).Unix()) + `, 244 "nbf": ` + fmt.Sprintf("%d", time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix()) + `, 245 "name": "Smith, John", 246 "email": "smithj@outlook.com", 247 "origin": "localhost", 248 "sub": "smithj@outlook.com", 249 "roles": ["viewer"], 250 "addr": "10.10.10.10" 251 }` 252 253 editor2 = `{ 254 "exp": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix()) + `, 255 "iat": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute*-1).Unix()) + `, 256 "nbf": ` + fmt.Sprintf("%d", time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix()) + `, 257 "name": "Smith, Jane", 258 "email": "jane.smith@outlook.com", 259 "origin": "localhost", 260 "sub": "jane.smith@outlook.com", 261 "roles": ["editor"] 262 }` 263 264 viewer3 = `{ 265 "exp": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix()) + `, 266 "iat": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute*-1).Unix()) + `, 267 "nbf": ` + fmt.Sprintf("%d", time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix()) + `, 268 "name": "Smith, John", 269 "email": "smithj@outlook.com", 270 "origin": "localhost", 271 "sub": "smithj@outlook.com", 272 "roles": ["viewer"], 273 "acl":{ 274 "paths": { 275 "/**/allowed": {} 276 } 277 } 278 }` 279 280 viewer4 = `{ 281 "exp": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix()) + `, 282 "iat": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute*-1).Unix()) + `, 283 "nbf": ` + fmt.Sprintf("%d", time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix()) + `, 284 "name": "Smith, John", 285 "email": "smithj@outlook.com", 286 "origin": "localhost", 287 "sub": "smithj@outlook.com", 288 "roles": ["viewer"], 289 "addr": "10.10.10.10", 290 "acl":{ 291 "paths": { 292 "/**/allowed": {} 293 } 294 } 295 }` 296 297 viewer5 = `{ 298 "exp": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix()) + `, 299 "iat": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute*-1).Unix()) + `, 300 "nbf": ` + fmt.Sprintf("%d", time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix()) + `, 301 "name": "Smith, John", 302 "email": "smithj@outlook.com", 303 "origin": "localhost", 304 "sub": "smithj@outlook.com", 305 "roles": ["viewer"], 306 "addr": "2001:DB8::21f:5bff:febf:ce22:8a2e" 307 }` 308 ) 309 310 func TestAuthorize(t *testing.T) { 311 testcases := []struct { 312 name string 313 // disabled bool 314 claims string 315 config []*acl.RuleConfiguration 316 method string 317 path string 318 sourceAddress string 319 enableBearer bool 320 cacheUser bool 321 validateAccessListPathClaim bool 322 validateSourceAddress bool 323 validateMethodPath bool 324 optionsDisabled bool 325 want map[string]interface{} 326 shouldErr bool 327 err error 328 }{ 329 // Access list with default deny that allows viewer only 330 { 331 name: "user with viewer scope claim and default deny acl", 332 claims: viewer, config: defaultDenyACL, method: "GET", path: "/app/viewer", shouldErr: false, 333 validateMethodPath: true, 334 }, 335 { 336 name: "user with editor scope claim and default deny acl", 337 claims: editor, config: defaultDenyACL, method: "GET", path: "/app/viewer", shouldErr: true, err: errors.ErrAccessNotAllowed, 338 validateMethodPath: true, 339 }, 340 // Access list with default allow that denies editor 341 { 342 name: "user with viewer scope claim and default allow acl", 343 claims: viewer, config: defaultAllowACL, method: "GET", path: "/app/viewer", shouldErr: false, 344 validateMethodPath: true, 345 }, 346 { 347 name: "user with editor scope claim and default allow acl", 348 claims: editor, config: defaultAllowACL, method: "GET", path: "/app/viewer", shouldErr: true, err: errors.ErrAccessNotAllowed, 349 validateMethodPath: true, 350 }, 351 // Access list with default deny that allows 127.0.0.1 only 352 { 353 name: "user with viewer scope claim and audience deny acl", 354 claims: viewer, config: audienceDefaultDenyACL, method: "GET", path: "/app/viewer", shouldErr: false, 355 validateMethodPath: true, 356 }, 357 { 358 name: "user with editor scope claim and audience deny acl", 359 claims: editor, config: audienceDefaultDenyACL, method: "GET", path: "/app/viewer", shouldErr: true, err: errors.ErrAccessNotAllowed, 360 validateMethodPath: true, 361 }, 362 // Access list with default allow that denies localhost 363 { 364 name: "user with viewer scope claim and audience allow acl", 365 claims: viewer, config: audienceDefaultAllowACL, method: "GET", path: "/app/viewer", shouldErr: false, 366 validateMethodPath: true, 367 }, 368 { 369 name: "user with editor scope claim and audience allow acl", 370 claims: editor, config: audienceDefaultAllowACL, method: "GET", path: "/app/viewer", shouldErr: true, err: errors.ErrAccessNotAllowed, 371 validateMethodPath: true, 372 }, 373 // Custom ACL 374 { 375 name: "user with viewer scope claim and custom acl going to /app/page1/blocked via get", 376 claims: viewer, config: customACL, method: "GET", path: "/app/page1/blocked", shouldErr: false, 377 validateMethodPath: true, 378 }, 379 { 380 name: "user with viewer scope claim and custom acl going to /app/page2/blocked via get", 381 claims: viewer, config: customACL, method: "GET", path: "/app/page2/blocked", shouldErr: false, 382 validateMethodPath: true, 383 }, 384 { 385 name: "user with viewer scope claim and custom acl going to /app/page3/allowed via get", 386 claims: viewer, config: customACL, method: "GET", path: "/app/page3/allowed", shouldErr: false, 387 validateMethodPath: true, 388 }, 389 { 390 name: "user with editor scope claim and custom acl going to /app/page1/blocked via get", 391 claims: editor, config: customACL, method: "GET", path: "/app/page1/blocked", shouldErr: true, err: errors.ErrAccessNotAllowed, 392 validateMethodPath: true, 393 }, 394 { 395 name: "user with editor scope claim and custom acl going to /app/page2/blocked via get", 396 claims: editor, config: customACL, method: "GET", path: "/app/page2/blocked", shouldErr: true, err: errors.ErrAccessNotAllowed, 397 validateMethodPath: true, 398 }, 399 { 400 name: "user with editor scope claim and custom acl going to /app/page3/allowed via get", 401 claims: editor, config: customACL, method: "GET", path: "/app/page3/allowed", shouldErr: false, 402 validateMethodPath: true, 403 }, 404 // Mixed ACL 405 { 406 name: "user with viewer scope and audience claims and custom acl", 407 claims: viewer, config: mixedACL, method: "GET", path: "/app/page1/blocked", shouldErr: false, 408 validateMethodPath: true, 409 }, 410 { 411 name: "user with editor scope and localhost audience claims and mixed acl", 412 claims: editor, config: mixedACL, method: "GET", path: "/app/editor", shouldErr: false, 413 validateMethodPath: true, 414 }, 415 // Role-based ACLs. 416 { 417 name: "user with viewer role claim and default deny acl going to app/viewer via get", 418 claims: viewer2, config: defaultRolesDenyACL, method: "GET", path: "/app/viewer", shouldErr: false, 419 enableBearer: true, 420 validateMethodPath: true, 421 }, 422 { 423 name: "user with viewer role claim and default deny acl going to app/editor via get", 424 claims: viewer2, config: defaultRolesDenyACL, method: "GET", path: "/app/editor", shouldErr: false, 425 enableBearer: true, 426 validateMethodPath: true, 427 }, 428 { 429 name: "user with viewer role claim and default deny acl going to app/admin via get", 430 claims: viewer2, config: defaultRolesDenyACL, method: "GET", path: "/app/admin", shouldErr: false, 431 enableBearer: true, 432 validateMethodPath: true, 433 }, 434 { 435 name: "user with editor role claim and default deny acl going to app/viewer via get", 436 claims: editor2, config: defaultRolesDenyACL, method: "GET", path: "/app/viewer", shouldErr: true, err: errors.ErrAccessNotAllowed, 437 enableBearer: true, 438 validateMethodPath: true, 439 }, 440 { 441 name: "user with editor role claim and default deny acl going to app/editor via get", 442 claims: editor2, config: defaultRolesDenyACL, method: "GET", path: "/app/editor", shouldErr: true, err: errors.ErrAccessNotAllowed, 443 enableBearer: true, 444 validateMethodPath: true, 445 }, 446 { 447 name: "user with editor role claim and default deny acl going to app/admin via get", 448 claims: editor2, config: defaultRolesDenyACL, method: "GET", path: "/app/admin", shouldErr: true, err: errors.ErrAccessNotAllowed, 449 enableBearer: true, 450 validateMethodPath: true, 451 }, 452 // Access list with default allow that denies editor 453 { 454 name: "user with viewer role claim and default allow acl going to app/viewer via get", 455 claims: viewer2, config: defaultRolesAllowACL, method: "GET", path: "/app/viewer", shouldErr: false, 456 enableBearer: true, 457 validateMethodPath: true, 458 }, 459 { 460 name: "user with viewer role claim and default allow acl going to app/editor via get", 461 claims: viewer2, config: defaultRolesAllowACL, method: "GET", path: "/app/editor", shouldErr: false, 462 enableBearer: true, 463 validateMethodPath: true, 464 }, 465 { 466 name: "user with viewer role claim and default allow acl going to app/admin via get", 467 claims: viewer2, config: defaultRolesAllowACL, method: "GET", path: "/app/admin", shouldErr: false, 468 enableBearer: true, 469 validateMethodPath: true, 470 }, 471 { 472 name: "user with editor role claim and default allow acl going to app/viewer via get", 473 claims: editor2, config: defaultRolesAllowACL, method: "GET", path: "/app/viewer", shouldErr: true, err: errors.ErrAccessNotAllowed, 474 enableBearer: true, 475 validateMethodPath: true, 476 }, 477 { 478 name: "user with editor role claim and default allow acl going to app/editor via get", 479 claims: editor2, config: defaultRolesAllowACL, method: "GET", path: "/app/editor", shouldErr: true, err: errors.ErrAccessNotAllowed, 480 enableBearer: true, 481 validateMethodPath: true, 482 }, 483 { 484 name: "user with editor role claim and default allow acl going to app/admin via get", 485 claims: editor2, config: defaultRolesAllowACL, method: "GET", path: "/app/admin", shouldErr: true, err: errors.ErrAccessNotAllowed, 486 enableBearer: true, 487 validateMethodPath: true, 488 }, 489 // Custom ACL 490 { 491 name: "user with editor role claim and custom acl going to /app/page1/blocked via get", 492 claims: editor2, config: customRolesACL, method: "GET", path: "/app/page1/blocked", shouldErr: true, err: errors.ErrAccessNotAllowed, 493 enableBearer: true, 494 validateMethodPath: true, 495 }, 496 { 497 name: "user with editor role claim and custom acl going to /app/page2/blocked via get", 498 claims: editor2, config: customRolesACL, method: "GET", path: "/app/page2/blocked", shouldErr: true, err: errors.ErrAccessNotAllowed, 499 enableBearer: true, 500 validateMethodPath: true, 501 }, 502 { 503 name: "user with editor role claim and custom acl going to /app/page3/allowed via get", 504 claims: editor2, config: customRolesACL, method: "GET", path: "/app/page3/allowed", shouldErr: false, 505 enableBearer: true, 506 validateMethodPath: true, 507 }, 508 { 509 name: "user with viewer role claim and custom acl going to /app/page1/blocked via get", 510 claims: viewer2, config: customRolesACL, method: "GET", path: "/app/page1/blocked", shouldErr: false, 511 enableBearer: true, 512 validateMethodPath: true, 513 }, 514 { 515 name: "user with viewer role claim and custom acl going to /app/page2/blocked via get", 516 claims: viewer2, config: customRolesACL, method: "GET", path: "/app/page2/blocked", shouldErr: false, 517 enableBearer: true, 518 validateMethodPath: true, 519 }, 520 { 521 name: "user with viewer role claim and custom acl going to /app/page3/allowed via get", 522 claims: viewer2, config: customRolesACL, method: "GET", path: "/app/page3/allowed", shouldErr: false, 523 enableBearer: true, 524 validateMethodPath: true, 525 }, 526 // Token based ACL 527 { 528 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with src addr", 529 claims: viewer4, 530 config: defaultRolesDenyACL, 531 method: "GET", 532 path: "/app/page3/allowed", 533 shouldErr: false, 534 validateAccessListPathClaim: true, 535 validateMethodPath: true, 536 validateSourceAddress: true, 537 sourceAddress: "10.10.10.10", 538 }, 539 { 540 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with src addr and block path acl", 541 claims: viewer4, 542 config: defaultRolesDenyACL, 543 method: "GET", 544 path: "/app/page3/blocked", 545 validateAccessListPathClaim: true, 546 validateMethodPath: true, 547 validateSourceAddress: true, 548 sourceAddress: "10.10.10.10", 549 shouldErr: true, 550 err: errors.ErrAccessNotAllowedByPathACL, 551 }, 552 { 553 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with acl block with src addr", 554 claims: viewer3, 555 config: denyViewerAllowOthersACL, 556 method: "GET", 557 path: "/app/page3/denied", 558 validateAccessListPathClaim: true, 559 validateMethodPath: true, 560 validateSourceAddress: true, 561 sourceAddress: "10.10.10.10", 562 shouldErr: true, 563 err: errors.ErrAccessNotAllowed, 564 }, 565 { 566 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with acl block with src addr", 567 claims: viewer2, 568 config: defaultRolesAllowACL, 569 method: "GET", 570 path: "/app/page3/denied", 571 validateAccessListPathClaim: true, 572 validateMethodPath: true, 573 validateSourceAddress: true, 574 sourceAddress: "10.10.10.10", 575 shouldErr: true, 576 err: errors.ErrAccessNotAllowedByPathACL, 577 }, 578 { 579 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with acl block with src addr mismatch", 580 claims: viewer2, 581 config: defaultRolesAllowACL, 582 method: "GET", 583 path: "/app/page3/denied", 584 validateAccessListPathClaim: true, 585 validateMethodPath: true, 586 validateSourceAddress: true, 587 sourceAddress: "20.20.20.20", 588 shouldErr: true, 589 err: errors.ErrSourceAddressMismatch.WithArgs("10.10.10.10", "20.20.20.20"), 590 }, 591 { 592 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get", 593 claims: viewer3, 594 config: defaultRolesDenyACL, 595 method: "GET", 596 path: "/app/page3/allowed", 597 shouldErr: false, 598 validateAccessListPathClaim: true, 599 validateMethodPath: true, 600 }, 601 { 602 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with acl block", 603 claims: viewer3, 604 config: denyViewerAllowOthersACL, 605 method: "GET", 606 path: "/app/page3/denied", 607 validateAccessListPathClaim: true, 608 validateMethodPath: true, 609 shouldErr: true, 610 err: errors.ErrAccessNotAllowed, 611 }, 612 { 613 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with acl block", 614 claims: viewer2, 615 config: defaultRolesAllowACL, 616 method: "GET", 617 path: "/app/page3/denied", 618 validateAccessListPathClaim: true, 619 validateMethodPath: true, 620 shouldErr: true, 621 err: errors.ErrAccessNotAllowedByPathACL, 622 }, 623 { 624 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with deny acl", 625 claims: viewer3, 626 config: defaultRolesDenyACL, 627 method: "GET", 628 path: "/app/page3/denied", 629 validateAccessListPathClaim: true, 630 shouldErr: true, 631 err: errors.ErrAccessNotAllowedByPathACL, 632 }, 633 { 634 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get without method and path", 635 claims: viewer3, 636 config: denyViewerAllowOthersACL, 637 validateAccessListPathClaim: true, 638 method: "GET", 639 path: "/app/page3/allowed", 640 shouldErr: true, 641 err: errors.ErrAccessNotAllowed, 642 }, 643 { 644 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with src addr", 645 claims: viewer3, 646 config: defaultRolesDenyACL, 647 method: "GET", 648 path: "/app/page3/allowed", 649 validateAccessListPathClaim: true, 650 validateSourceAddress: true, 651 shouldErr: true, 652 err: errors.ErrSourceAddressNotFound, 653 }, 654 { 655 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with deny acl and with src addr and no ip match", 656 claims: viewer4, 657 config: defaultRolesDenyACL, 658 method: "GET", 659 path: "/app/page3/denied", 660 validateAccessListPathClaim: true, 661 validateSourceAddress: true, 662 sourceAddress: "20.20.20.20", 663 shouldErr: true, 664 err: errors.ErrSourceAddressMismatch.WithArgs("10.10.10.10", "20.20.20.20"), 665 }, 666 { 667 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with deny acl and with src addr and ip match", 668 claims: viewer4, 669 config: defaultRolesDenyACL, 670 method: "GET", 671 path: "/app/page3/allowed", 672 validateAccessListPathClaim: true, 673 validateSourceAddress: true, 674 sourceAddress: "10.10.10.10", 675 }, 676 { 677 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with deny acl and with src addr and no ip block", 678 claims: viewer4, 679 config: defaultRolesDenyACL, 680 method: "GET", 681 path: "/app/page3/denied", 682 validateAccessListPathClaim: true, 683 validateSourceAddress: true, 684 sourceAddress: "10.10.10.10", 685 shouldErr: true, 686 err: errors.ErrAccessNotAllowedByPathACL, 687 }, 688 { 689 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with deny acl and with src addr and no acl", 690 claims: viewer2, 691 config: defaultRolesDenyACL, 692 method: "GET", 693 path: "/app/page3/denied", 694 validateAccessListPathClaim: true, 695 validateSourceAddress: true, 696 sourceAddress: "10.10.10.10", 697 shouldErr: true, 698 err: errors.ErrAccessNotAllowedByPathACL, 699 }, 700 { 701 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get without method and path and with src addr", 702 claims: viewer3, 703 config: denyViewerAllowOthersACL, 704 validateAccessListPathClaim: true, 705 validateSourceAddress: true, 706 method: "GET", 707 path: "/app/page3/allowed", 708 shouldErr: true, 709 err: errors.ErrAccessNotAllowed, 710 }, 711 712 { 713 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get without acl", 714 claims: viewer, 715 config: defaultRolesAllowACL, 716 validateAccessListPathClaim: true, 717 method: "GET", 718 path: "/app/page3/allowed", 719 shouldErr: true, 720 err: errors.ErrAccessNotAllowedByPathACL, 721 }, 722 { 723 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get without method and path", 724 claims: viewer3, 725 config: defaultRolesDenyACL, 726 method: "GET", 727 path: "/app/page3/allowed", 728 shouldErr: false, 729 validateAccessListPathClaim: true, 730 }, 731 { 732 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with source address", 733 claims: viewer3, 734 config: defaultRolesDenyACL, 735 method: "GET", 736 path: "/app/page3/allowed", 737 shouldErr: true, 738 err: errors.ErrSourceAddressNotFound, 739 validateAccessListPathClaim: true, 740 validateSourceAddress: true, 741 validateMethodPath: true, 742 }, 743 { 744 name: "user with viewer role claim and token-based acl going to /app/page3/allowed via get with source address and without method and path", 745 claims: viewer3, 746 config: defaultRolesDenyACL, 747 method: "GET", 748 path: "/app/page3/allowed", 749 shouldErr: true, 750 err: errors.ErrSourceAddressNotFound, 751 validateAccessListPathClaim: true, 752 validateSourceAddress: true, 753 }, 754 { 755 name: "user with viewer role claim and token-based acl going to /app/page2/blocked via get", 756 claims: viewer3, 757 config: defaultRolesDenyACL, 758 method: "GET", 759 path: "/app/page2/blocked", 760 validateAccessListPathClaim: true, 761 validateMethodPath: true, 762 shouldErr: true, 763 err: errors.ErrAccessNotAllowedByPathACL, 764 }, 765 { 766 name: "user with viewer role claim going to /app/page2/blocked via get", 767 claims: viewer3, 768 config: denyViewerAllowOthersACL, 769 method: "GET", 770 path: "/app/page2/blocked", 771 shouldErr: true, 772 err: errors.ErrAccessNotAllowed, 773 }, 774 { 775 name: "access list not set", 776 claims: viewer, 777 method: "GET", 778 path: "/app/page3/allowed", 779 validateMethodPath: true, 780 shouldErr: true, 781 err: errors.ErrNoAccessList, 782 }, 783 { 784 name: "empty token", 785 config: defaultAllowACL, 786 method: "GET", 787 path: "/app/page3/allowed", 788 validateMethodPath: true, 789 shouldErr: true, 790 err: errors.ErrCryptoKeyStoreParseTokenFailed, 791 }, 792 { 793 name: "bad token", 794 config: defaultAllowACL, 795 method: "GET", 796 path: "/app/page3/allowed", 797 validateMethodPath: true, 798 shouldErr: true, 799 err: errors.ErrCryptoKeyStoreParseTokenFailed, 800 }, 801 { 802 name: "no acl rules", 803 claims: viewer, 804 config: defaultAllowACL, 805 method: "GET", 806 path: "/app/page3/allowed", 807 validateMethodPath: true, 808 shouldErr: true, 809 err: errors.ErrAccessListNoRules, 810 }, 811 { 812 name: "no verify keys", 813 claims: viewer, 814 config: defaultAllowACL, 815 method: "GET", 816 path: "/app/page3/allowed", 817 validateMethodPath: true, 818 shouldErr: true, 819 err: errors.ErrValidatorCryptoKeyStoreNoKeys, 820 }, 821 { 822 name: "token without ip address", 823 claims: viewer, 824 config: defaultAllowACL, 825 method: "GET", 826 path: "/app/page3/allowed", 827 validateSourceAddress: true, 828 shouldErr: true, 829 err: errors.ErrSourceAddressNotFound, 830 }, 831 { 832 name: "token ip address and client ip address not match", 833 claims: viewer2, 834 config: defaultRolesAllowACL, 835 method: "GET", 836 path: "/app/page3/allowed", 837 validateSourceAddress: true, 838 sourceAddress: "20.20.20.20", 839 shouldErr: true, 840 err: errors.ErrSourceAddressMismatch.WithArgs("10.10.10.10", "20.20.20.20"), 841 }, 842 { 843 name: "token ip address and client ip address match", 844 claims: viewer2, 845 config: defaultRolesAllowACL, 846 method: "GET", 847 path: "/app/page3/allowed", 848 validateSourceAddress: true, 849 sourceAddress: "10.10.10.10", 850 }, 851 { 852 name: "cached user", 853 claims: viewer2, 854 config: defaultRolesAllowACL, 855 method: "GET", 856 path: "/app/page3/allowed", 857 cacheUser: true, 858 }, 859 { 860 name: "token ip address and client ip address match but not roles", 861 claims: viewer2, 862 config: denyViewerAllowOthersACL, 863 method: "GET", 864 path: "/app/page3/allowed", 865 validateSourceAddress: true, 866 sourceAddress: "10.10.10.10", 867 shouldErr: true, 868 err: errors.ErrAccessNotAllowed, 869 }, 870 { 871 name: "token without ip address with method and path", 872 claims: viewer, 873 config: defaultAllowACL, 874 method: "GET", 875 path: "/app/page3/allowed", 876 validateSourceAddress: true, 877 validateMethodPath: true, 878 shouldErr: true, 879 err: errors.ErrSourceAddressNotFound, 880 }, 881 { 882 name: "token without ip address with method and path and with acl block", 883 claims: viewer, 884 config: defaultRolesDenyACL, 885 method: "GET", 886 path: "/app/page3/allowed", 887 validateSourceAddress: true, 888 validateMethodPath: true, 889 shouldErr: true, 890 err: errors.ErrAccessNotAllowed, 891 }, 892 { 893 name: "token without ip address with method and path and without acl block", 894 claims: viewer2, 895 config: defaultRolesAllowACL, 896 method: "GET", 897 path: "/app/page3/allowed", 898 validateSourceAddress: true, 899 validateMethodPath: true, 900 sourceAddress: "10.10.10.10", 901 }, 902 { 903 name: "token ip address and client ip address not match with method and path", 904 claims: viewer2, 905 config: defaultRolesAllowACL, 906 method: "GET", 907 path: "/app/page3/allowed", 908 validateSourceAddress: true, 909 validateMethodPath: true, 910 sourceAddress: "20.20.20.20", 911 shouldErr: true, 912 err: errors.ErrSourceAddressMismatch.WithArgs("10.10.10.10", "20.20.20.20"), 913 }, 914 { 915 name: "validator options disabled", 916 claims: viewer, 917 config: defaultAllowACL, 918 method: "GET", 919 path: "/app/page3/allowed", 920 optionsDisabled: true, 921 shouldErr: true, 922 err: errors.ErrTokenValidatorOptionsNotFound, 923 }, 924 // IPv6 source address. 925 { 926 name: "token ip6 address and client ip6 address match", 927 claims: viewer5, 928 config: defaultRolesAllowACL, 929 method: "GET", 930 path: "/app/page3/allowed", 931 validateSourceAddress: true, 932 sourceAddress: "2001:DB8::21f:5bff:febf:ce22:8a2e", 933 }, 934 { 935 name: "token ip6 address and client ip6 address do not match", 936 claims: viewer5, 937 config: defaultRolesAllowACL, 938 method: "GET", 939 path: "/app/page3/allowed", 940 validateSourceAddress: true, 941 sourceAddress: "2001:DB8::21f:5bff:febf:ce22:8a21", 942 shouldErr: true, 943 err: errors.ErrSourceAddressMismatch.WithArgs("2001:DB8::21f:5bff:febf:ce22:8a2e", "2001:DB8::21f:5bff:febf:ce22:8a21"), 944 }, 945 { 946 name: "token ip6 address with port and client ip6 address match", 947 claims: viewer5, 948 config: defaultRolesAllowACL, 949 method: "GET", 950 path: "/app/page3/allowed", 951 validateSourceAddress: true, 952 sourceAddress: "[2001:DB8::21f:5bff:febf:ce22:8a2e]:80", 953 }, 954 } 955 956 for _, tc := range testcases { 957 t.Run(tc.name, func(t *testing.T) { 958 //if tc.disabled { 959 // return 960 // } 961 var accessList *acl.AccessList 962 var opts *options.TokenValidatorOptions 963 var token string 964 ctx := context.Background() 965 logger := logutil.NewLogger() 966 967 ks := testutils.NewTestCryptoKeyStore() 968 keys := ks.GetKeys() 969 signingKey := keys[0] 970 971 validator := NewTokenValidator() 972 973 if !tc.optionsDisabled { 974 opts = options.NewTokenValidatorOptions() 975 if tc.enableBearer { 976 opts.ValidateBearerHeader = true 977 } 978 if tc.validateAccessListPathClaim { 979 opts.ValidateAccessListPathClaim = true 980 } 981 if tc.validateSourceAddress { 982 opts.ValidateSourceAddress = true 983 } 984 if tc.validateMethodPath { 985 opts.ValidateMethodPath = true 986 } 987 } 988 989 if len(tc.config) > 0 { 990 accessList = acl.NewAccessList() 991 accessList.SetLogger(logger) 992 if tc.name != "no acl rules" { 993 if err := accessList.AddRules(ctx, tc.config); err != nil { 994 t.Fatal(err) 995 } 996 } 997 } 998 999 if tc.name == "no verify keys" { 1000 keys = []*kms.CryptoKey{} 1001 } 1002 1003 if err := validator.Configure(ctx, keys, accessList, opts); err != nil { 1004 if tests.EvalErr(t, err, tc.config, tc.shouldErr, tc.err) { 1005 return 1006 } 1007 } 1008 1009 if tc.want == nil { 1010 tc.want = make(map[string]interface{}) 1011 } 1012 1013 if tc.claims != "" { 1014 usr, err := user.NewUser(tc.claims) 1015 if err != nil { 1016 t.Fatal(err) 1017 } 1018 tc.want["claims"] = usr.Claims 1019 if err := signingKey.SignToken("HS512", usr); err != nil { 1020 t.Fatal(err) 1021 } 1022 token = usr.Token 1023 } 1024 1025 if tc.name == "bad token" { 1026 token = `{"foobar", "barfoo"}` 1027 } 1028 1029 if tc.enableBearer { 1030 tc.want["token_name"] = "bearer" 1031 } else { 1032 tc.want["token_name"] = "access_token" 1033 } 1034 1035 handler := func(w http.ResponseWriter, r *http.Request) { 1036 ctx := context.Background() 1037 var msgs []string 1038 msgs = append(msgs, fmt.Sprintf("test name: %s", tc.name)) 1039 for _, entry := range tc.config { 1040 msgs = append(msgs, fmt.Sprintf("ACL: %+v", entry)) 1041 } 1042 msgs = append(msgs, fmt.Sprintf("claims: %+v", tc.claims)) 1043 msgs = append(msgs, fmt.Sprintf("path: %s", r.URL.Path)) 1044 msgs = append(msgs, fmt.Sprintf("method: %s", r.Method)) 1045 msgs = append(msgs, fmt.Sprintf("key\n%s", cmp.Diff(nil, keys[0]))) 1046 1047 ar := requests.NewAuthorizationRequest() 1048 ar.ID = "TEST_REQUEST_ID" 1049 ar.SessionID = "TEST_SESSION_ID" 1050 1051 usr, err := validator.Authorize(ctx, r, ar) 1052 if tests.EvalErrWithLog(t, err, tc.config, tc.shouldErr, tc.err, msgs) { 1053 return 1054 } 1055 got := make(map[string]interface{}) 1056 got["token_name"] = usr.TokenName 1057 got["claims"] = usr.Claims 1058 tests.CustomEvalObjectsWithLog(t, "eval", tc.want, got, msgs, user.Claims{}) 1059 1060 if tc.shouldErr { 1061 return 1062 } 1063 1064 if tc.cacheUser { 1065 if err := validator.CacheUser(usr); err != nil { 1066 if tests.EvalErrWithLog(t, err, "cache user", tc.shouldErr, tc.err, msgs) { 1067 return 1068 } 1069 } 1070 usr, err = validator.Authorize(ctx, r, ar) 1071 if tests.EvalErrWithLog(t, err, "cached auth", tc.shouldErr, tc.err, msgs) { 1072 return 1073 } 1074 } 1075 } 1076 1077 req, err := http.NewRequest(tc.method, tc.path, nil) 1078 if err != nil { 1079 t.Fatal(err) 1080 } 1081 1082 if tc.enableBearer { 1083 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1084 } else { 1085 req.Header.Set("Authorization", fmt.Sprintf("access_token=%s", token)) 1086 } 1087 1088 if tc.sourceAddress != "" { 1089 req.Header.Set("X-Real-Ip", tc.sourceAddress) 1090 } 1091 1092 w := httptest.NewRecorder() 1093 handler(w, req) 1094 w.Result() 1095 }) 1096 } 1097 } 1098 1099 func TestAddKeys(t *testing.T) { 1100 testcases := []struct { 1101 name string 1102 keys []*kms.CryptoKey 1103 verifyFound bool 1104 verifyNotCapable bool 1105 verifyNoTokenName bool 1106 verifyNoMaxLifetime bool 1107 verifyEmptyTokenName bool 1108 shouldErr bool 1109 err error 1110 }{ 1111 { 1112 name: "no keys", 1113 shouldErr: true, 1114 err: errors.ErrValidatorCryptoKeyStoreNoKeys, 1115 }, 1116 { 1117 name: "add keys", 1118 keys: []*kms.CryptoKey{ 1119 &kms.CryptoKey{}, 1120 }, 1121 verifyFound: true, 1122 }, 1123 { 1124 name: "add non verify key", 1125 keys: []*kms.CryptoKey{ 1126 &kms.CryptoKey{}, 1127 }, 1128 verifyFound: true, 1129 verifyNotCapable: true, 1130 shouldErr: true, 1131 err: errors.ErrValidatorCryptoKeyStoreNoVerifyKeys, 1132 }, 1133 { 1134 name: "add key without token name", 1135 keys: []*kms.CryptoKey{ 1136 &kms.CryptoKey{}, 1137 }, 1138 verifyFound: true, 1139 verifyNoTokenName: true, 1140 shouldErr: true, 1141 err: errors.ErrValidatorCryptoKeyStoreNoVerifyKeys, 1142 }, 1143 { 1144 name: "add key without token lifetime", 1145 keys: []*kms.CryptoKey{ 1146 &kms.CryptoKey{}, 1147 }, 1148 verifyFound: true, 1149 verifyNoMaxLifetime: true, 1150 shouldErr: true, 1151 err: errors.ErrValidatorCryptoKeyStoreNoVerifyKeys, 1152 }, 1153 { 1154 name: "add key with empty token name with spaces", 1155 keys: []*kms.CryptoKey{ 1156 &kms.CryptoKey{}, 1157 }, 1158 verifyFound: true, 1159 verifyEmptyTokenName: true, 1160 shouldErr: true, 1161 err: errors.ErrEmptyTokenName, 1162 }, 1163 } 1164 1165 for _, tc := range testcases { 1166 t.Run(tc.name, func(t *testing.T) { 1167 var err error 1168 ctx := context.Background() 1169 validator := NewTokenValidator() 1170 for _, k := range tc.keys { 1171 if tc.verifyFound { 1172 k.Verify = kms.NewCryptoKeyOperator() 1173 k.Verify.Token.Capable = true 1174 k.Verify.Token.Name = "access_token" 1175 k.Verify.Token.MaxLifetime = 900 1176 } 1177 if tc.verifyNotCapable { 1178 k.Verify.Token.Capable = false 1179 } 1180 if tc.verifyNoTokenName { 1181 k.Verify.Token.Name = "" 1182 } 1183 if tc.verifyNoMaxLifetime { 1184 k.Verify.Token.MaxLifetime = 0 1185 } 1186 if tc.verifyEmptyTokenName { 1187 k.Verify.Token.Name = " " 1188 } 1189 } 1190 err = validator.addKeys(ctx, tc.keys) 1191 if tests.EvalErr(t, err, "keys", tc.shouldErr, tc.err) { 1192 return 1193 } 1194 }) 1195 } 1196 } 1197 1198 func TestSetAllowedTokenNames(t *testing.T) { 1199 testcases := []struct { 1200 name string 1201 tokenNames []string 1202 want map[string]interface{} 1203 shouldErr bool 1204 err error 1205 }{ 1206 { 1207 name: "token names slice with duplicate values", 1208 tokenNames: []string{"foo", "foo"}, 1209 shouldErr: true, 1210 err: errors.ErrDuplicateTokenName.WithArgs("foo"), 1211 }, 1212 { 1213 name: "token names slice with empty values", 1214 tokenNames: []string{"foo", ""}, 1215 shouldErr: true, 1216 err: errors.ErrEmptyTokenName, 1217 }, 1218 { 1219 name: "valid token names", 1220 tokenNames: []string{"foo", "bar"}, 1221 want: map[string]interface{}{ 1222 "header": map[string]interface{}{ 1223 "foo": true, 1224 "bar": true, 1225 }, 1226 "cookie": map[string]interface{}{ 1227 "foo": true, 1228 "bar": true, 1229 }, 1230 "query": map[string]interface{}{ 1231 "foo": true, 1232 "bar": true, 1233 }, 1234 }, 1235 }, 1236 } 1237 1238 for _, tc := range testcases { 1239 t.Run(tc.name, func(t *testing.T) { 1240 validator := NewTokenValidator() 1241 err := validator.setAllowedTokenNames(tc.tokenNames) 1242 if tests.EvalErr(t, err, "token names", tc.shouldErr, tc.err) { 1243 return 1244 } 1245 got := make(map[string]interface{}) 1246 got["header"] = validator.authHeaders 1247 got["cookie"] = validator.GetAuthCookies() 1248 got["query"] = validator.authHeaders 1249 tests.EvalObjects(t, "token names", tc.want, got) 1250 }) 1251 } 1252 } 1253 1254 func TestSetSourcePriority(t *testing.T) { 1255 testcases := []struct { 1256 name string 1257 sources []string 1258 want map[string]interface{} 1259 shouldErr bool 1260 err error 1261 }{ 1262 { 1263 name: "empty allowed token sources slice", 1264 shouldErr: true, 1265 err: errors.ErrInvalidSourcePriority, 1266 }, 1267 { 1268 name: "allowed token sources slice exceeds three values", 1269 shouldErr: true, 1270 sources: []string{"foo", "foo", "foo", "foo"}, 1271 err: errors.ErrInvalidSourcePriority, 1272 }, 1273 { 1274 name: "allowed token sources slice has invalid source", 1275 sources: []string{"header", "cookie", "foo"}, 1276 shouldErr: true, 1277 err: errors.ErrInvalidSourceName.WithArgs("foo"), 1278 }, 1279 { 1280 name: "allowed token sources slice has duplicate source", 1281 sources: []string{"header", "query", "query"}, 1282 shouldErr: true, 1283 err: errors.ErrDuplicateSourceName.WithArgs("query"), 1284 }, 1285 { 1286 name: "reorder token source priority", 1287 sources: []string{"header", "cookie", "query"}, 1288 want: map[string]interface{}{ 1289 "sources": []string{"header", "cookie", "query"}, 1290 }, 1291 }, 1292 } 1293 for _, tc := range testcases { 1294 t.Run(tc.name, func(t *testing.T) { 1295 validator := NewTokenValidator() 1296 err := validator.SetSourcePriority(tc.sources) 1297 if tests.EvalErr(t, err, "token sources", tc.shouldErr, tc.err) { 1298 return 1299 } 1300 got := make(map[string]interface{}) 1301 got["sources"] = validator.GetSourcePriority() 1302 tests.EvalObjects(t, "token sources", tc.want, got) 1303 }) 1304 } 1305 }