github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/role_test.go (about) 1 /* 2 Copyright 2023 Gravitational, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package types 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "testing" 23 24 "github.com/gravitational/trace" 25 "github.com/stretchr/testify/require" 26 "gopkg.in/yaml.v2" 27 28 "github.com/gravitational/teleport/api/types/wrappers" 29 ) 30 31 func TestAccessRequestConditionsIsEmpty(t *testing.T) { 32 tests := []struct { 33 name string 34 arc AccessRequestConditions 35 expected bool 36 }{ 37 { 38 name: "empty", 39 arc: AccessRequestConditions{}, 40 expected: true, 41 }, 42 { 43 name: "annotations", 44 arc: AccessRequestConditions{ 45 Annotations: wrappers.Traits{ 46 "test": []string{"test"}, 47 }, 48 }, 49 expected: false, 50 }, 51 { 52 name: "claims to roles", 53 arc: AccessRequestConditions{ 54 ClaimsToRoles: []ClaimMapping{ 55 {}, 56 }, 57 }, 58 expected: false, 59 }, 60 { 61 name: "roles", 62 arc: AccessRequestConditions{ 63 Roles: []string{"test"}, 64 }, 65 expected: false, 66 }, 67 { 68 name: "search as roles", 69 arc: AccessRequestConditions{ 70 SearchAsRoles: []string{"test"}, 71 }, 72 expected: false, 73 }, 74 { 75 name: "suggested reviewers", 76 arc: AccessRequestConditions{ 77 SuggestedReviewers: []string{"test"}, 78 }, 79 expected: false, 80 }, 81 { 82 name: "thresholds", 83 arc: AccessRequestConditions{ 84 Thresholds: []AccessReviewThreshold{ 85 { 86 Name: "test", 87 }, 88 }, 89 }, 90 expected: false, 91 }, 92 } 93 94 for _, test := range tests { 95 t.Run(test.name, func(t *testing.T) { 96 require.Equal(t, test.expected, test.arc.IsEmpty()) 97 }) 98 } 99 } 100 101 func TestAccessReviewConditionsIsEmpty(t *testing.T) { 102 tests := []struct { 103 name string 104 arc AccessReviewConditions 105 expected bool 106 }{ 107 { 108 name: "empty", 109 arc: AccessReviewConditions{}, 110 expected: true, 111 }, 112 { 113 name: "claims to roles", 114 arc: AccessReviewConditions{ 115 ClaimsToRoles: []ClaimMapping{ 116 {}, 117 }, 118 }, 119 expected: false, 120 }, 121 { 122 name: "preview as roles", 123 arc: AccessReviewConditions{ 124 PreviewAsRoles: []string{"test"}, 125 }, 126 expected: false, 127 }, 128 { 129 name: "roles", 130 arc: AccessReviewConditions{ 131 Roles: []string{"test"}, 132 }, 133 expected: false, 134 }, 135 { 136 name: "where", 137 arc: AccessReviewConditions{ 138 Where: "test", 139 }, 140 expected: false, 141 }, 142 } 143 144 for _, test := range tests { 145 t.Run(test.name, func(t *testing.T) { 146 require.Equal(t, test.expected, test.arc.IsEmpty()) 147 }) 148 } 149 } 150 151 func TestRole_GetKubeResources(t *testing.T) { 152 kubeLabels := Labels{ 153 Wildcard: {Wildcard}, 154 } 155 labelsExpression := "contains(user.spec.traits[\"groups\"], \"prod\")" 156 type args struct { 157 version string 158 labels Labels 159 labelsExpression string 160 resources []KubernetesResource 161 } 162 tests := []struct { 163 name string 164 args args 165 want []KubernetesResource 166 assertErrorCreation require.ErrorAssertionFunc 167 }{ 168 { 169 name: "v7 with error", 170 args: args{ 171 version: V7, 172 labels: kubeLabels, 173 resources: []KubernetesResource{ 174 { 175 Kind: "invalid resource", 176 Namespace: "test", 177 Name: "test", 178 }, 179 }, 180 }, 181 assertErrorCreation: require.Error, 182 }, 183 { 184 name: "v7", 185 args: args{ 186 version: V7, 187 labels: kubeLabels, 188 resources: []KubernetesResource{ 189 { 190 Kind: KindKubePod, 191 Namespace: "test", 192 Name: "test", 193 }, 194 }, 195 }, 196 assertErrorCreation: require.NoError, 197 want: []KubernetesResource{ 198 { 199 Kind: KindKubePod, 200 Namespace: "test", 201 Name: "test", 202 }, 203 }, 204 }, 205 { 206 name: "v7 with labels expression", 207 args: args{ 208 version: V7, 209 labelsExpression: labelsExpression, 210 resources: []KubernetesResource{ 211 { 212 Kind: KindKubePod, 213 Namespace: "test", 214 Name: "test", 215 }, 216 }, 217 }, 218 assertErrorCreation: require.NoError, 219 want: []KubernetesResource{ 220 { 221 Kind: KindKubePod, 222 Namespace: "test", 223 Name: "test", 224 }, 225 }, 226 }, 227 { 228 name: "v6 to v7 without wildcard; labels expression", 229 args: args{ 230 version: V6, 231 labelsExpression: labelsExpression, 232 resources: []KubernetesResource{ 233 { 234 Kind: KindKubePod, 235 Namespace: "test", 236 Name: "test", 237 }, 238 }, 239 }, 240 assertErrorCreation: require.NoError, 241 want: append([]KubernetesResource{ 242 { 243 Kind: KindKubePod, 244 Namespace: "test", 245 Name: "test", 246 Verbs: []string{Wildcard}, 247 }, 248 }, 249 appendV7KubeResources()...), 250 }, 251 { 252 name: "v6 to v7 with wildcard", 253 args: args{ 254 version: V6, 255 labels: kubeLabels, 256 resources: []KubernetesResource{ 257 { 258 Kind: KindKubePod, 259 Namespace: Wildcard, 260 Name: Wildcard, 261 }, 262 }, 263 }, 264 assertErrorCreation: require.NoError, 265 want: []KubernetesResource{ 266 { 267 Kind: Wildcard, 268 Namespace: Wildcard, 269 Name: Wildcard, 270 Verbs: []string{Wildcard}, 271 }, 272 }, 273 }, 274 { 275 name: "v6 to v7 without wildcard", 276 args: args{ 277 version: V6, 278 labels: kubeLabels, 279 resources: []KubernetesResource{ 280 { 281 Kind: KindKubePod, 282 Namespace: "test", 283 Name: "test", 284 }, 285 }, 286 }, 287 assertErrorCreation: require.NoError, 288 want: append([]KubernetesResource{ 289 { 290 Kind: KindKubePod, 291 Namespace: "test", 292 Name: "test", 293 Verbs: []string{Wildcard}, 294 }, 295 }, 296 appendV7KubeResources()...), 297 }, 298 { 299 name: "v5 to v7: populate with defaults.", 300 args: args{ 301 version: V5, 302 labels: kubeLabels, 303 resources: nil, 304 }, 305 assertErrorCreation: require.NoError, 306 want: []KubernetesResource{ 307 { 308 Kind: Wildcard, 309 Namespace: Wildcard, 310 Name: Wildcard, 311 Verbs: []string{Wildcard}, 312 }, 313 }, 314 }, 315 { 316 name: "v5 to v7 without kube labels", 317 args: args{ 318 version: V5, 319 resources: nil, 320 }, 321 assertErrorCreation: require.NoError, 322 want: nil, 323 }, 324 } 325 for _, tt := range tests { 326 t.Run(tt.name, func(t *testing.T) { 327 r, err := NewRoleWithVersion( 328 "test", 329 tt.args.version, 330 RoleSpecV6{ 331 Allow: RoleConditions{ 332 Namespaces: []string{"default"}, 333 KubernetesLabels: tt.args.labels, 334 KubernetesResources: tt.args.resources, 335 KubernetesLabelsExpression: tt.args.labelsExpression, 336 }, 337 }, 338 ) 339 tt.assertErrorCreation(t, err) 340 if err != nil { 341 return 342 } 343 got := r.GetKubeResources(Allow) 344 require.Equal(t, tt.want, got) 345 got = r.GetKubeResources(Deny) 346 require.Empty(t, got) 347 }) 348 } 349 } 350 351 func appendV7KubeResources() []KubernetesResource { 352 resources := []KubernetesResource{} 353 // append other kubernetes resources 354 for _, resource := range KubernetesResourcesKinds { 355 if resource == KindKubePod || resource == KindKubeNamespace { 356 continue 357 } 358 resources = append(resources, KubernetesResource{ 359 Kind: resource, 360 Namespace: Wildcard, 361 Name: Wildcard, 362 Verbs: []string{Wildcard}, 363 }, 364 ) 365 } 366 return resources 367 } 368 369 func TestMarshallCreateHostUserModeJSON(t *testing.T) { 370 for _, tc := range []struct { 371 input CreateHostUserMode 372 expected string 373 }{ 374 {input: CreateHostUserMode_HOST_USER_MODE_OFF, expected: "off"}, 375 {input: CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED, expected: ""}, 376 {input: CreateHostUserMode_HOST_USER_MODE_KEEP, expected: "keep"}, 377 {input: CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP, expected: "insecure-drop"}, 378 } { 379 got, err := json.Marshal(&tc.input) 380 require.NoError(t, err) 381 require.Equal(t, fmt.Sprintf("%q", tc.expected), string(got)) 382 } 383 } 384 385 func TestMarshallCreateHostUserModeYAML(t *testing.T) { 386 for _, tc := range []struct { 387 input CreateHostUserMode 388 expected string 389 }{ 390 {input: CreateHostUserMode_HOST_USER_MODE_OFF, expected: "\"off\""}, 391 {input: CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED, expected: "\"\""}, 392 {input: CreateHostUserMode_HOST_USER_MODE_KEEP, expected: "keep"}, 393 {input: CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP, expected: "insecure-drop"}, 394 } { 395 got, err := yaml.Marshal(&tc.input) 396 require.NoError(t, err) 397 require.Equal(t, fmt.Sprintf("%s\n", tc.expected), string(got)) 398 } 399 } 400 401 func TestUnmarshallCreateHostUserModeJSON(t *testing.T) { 402 for _, tc := range []struct { 403 expected CreateHostUserMode 404 input any 405 }{ 406 {expected: CreateHostUserMode_HOST_USER_MODE_OFF, input: "\"off\""}, 407 {expected: CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED, input: "\"\""}, 408 {expected: CreateHostUserMode_HOST_USER_MODE_KEEP, input: "\"keep\""}, 409 {expected: CreateHostUserMode_HOST_USER_MODE_KEEP, input: 3}, 410 {expected: CreateHostUserMode_HOST_USER_MODE_OFF, input: 1}, 411 {expected: CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP, input: 4}, 412 } { 413 var got CreateHostUserMode 414 err := json.Unmarshal([]byte(fmt.Sprintf("%v", tc.input)), &got) 415 require.NoError(t, err) 416 require.Equal(t, tc.expected, got) 417 } 418 } 419 420 func TestUnmarshallCreateHostUserModeYAML(t *testing.T) { 421 for _, tc := range []struct { 422 expected CreateHostUserMode 423 input string 424 }{ 425 {expected: CreateHostUserMode_HOST_USER_MODE_OFF, input: "\"off\""}, 426 {expected: CreateHostUserMode_HOST_USER_MODE_OFF, input: "off"}, 427 {expected: CreateHostUserMode_HOST_USER_MODE_UNSPECIFIED, input: "\"\""}, 428 {expected: CreateHostUserMode_HOST_USER_MODE_KEEP, input: "keep"}, 429 {expected: CreateHostUserMode_HOST_USER_MODE_INSECURE_DROP, input: "insecure-drop"}, 430 } { 431 var got CreateHostUserMode 432 err := yaml.Unmarshal([]byte(tc.input), &got) 433 require.NoError(t, err) 434 require.Equal(t, tc.expected, got) 435 } 436 } 437 438 func TestRoleV6_CheckAndSetDefaults(t *testing.T) { 439 t.Parallel() 440 requireBadParameterContains := func(contains string) require.ErrorAssertionFunc { 441 return func(t require.TestingT, err error, msgAndArgs ...interface{}) { 442 require.True(t, trace.IsBadParameter(err)) 443 require.ErrorContains(t, err, contains) 444 } 445 } 446 newRole := func(t *testing.T, spec RoleSpecV6) *RoleV6 { 447 return &RoleV6{ 448 Metadata: Metadata{ 449 Name: "test", 450 }, 451 Spec: spec, 452 } 453 } 454 455 tests := []struct { 456 name string 457 role *RoleV6 458 requireError require.ErrorAssertionFunc 459 }{ 460 { 461 name: "spiffe: valid", 462 role: newRole(t, RoleSpecV6{ 463 Allow: RoleConditions{ 464 SPIFFE: []*SPIFFERoleCondition{{Path: "/test"}}, 465 }, 466 }), 467 requireError: require.NoError, 468 }, 469 { 470 name: "spiffe: valid regex path", 471 role: newRole(t, RoleSpecV6{ 472 Allow: RoleConditions{ 473 SPIFFE: []*SPIFFERoleCondition{{Path: `^\/svc\/foo\/.*\/bar$`}}, 474 }, 475 }), 476 requireError: require.NoError, 477 }, 478 { 479 name: "spiffe: missing path", 480 role: newRole(t, RoleSpecV6{ 481 Allow: RoleConditions{ 482 SPIFFE: []*SPIFFERoleCondition{{Path: ""}}, 483 }, 484 }), 485 requireError: requireBadParameterContains("path: should be non-empty"), 486 }, 487 { 488 name: "spiffe: path not prepended", 489 role: newRole(t, RoleSpecV6{ 490 Allow: RoleConditions{ 491 SPIFFE: []*SPIFFERoleCondition{{Path: "foo"}}, 492 }, 493 }), 494 requireError: requireBadParameterContains("path: should start with /"), 495 }, 496 { 497 name: "spiffe: invalid ip cidr", 498 role: newRole(t, RoleSpecV6{ 499 Allow: RoleConditions{ 500 SPIFFE: []*SPIFFERoleCondition{ 501 { 502 Path: "/foo", 503 IPSANs: []string{ 504 "10.0.0.1/24", 505 "llama", 506 }, 507 }, 508 }, 509 }, 510 }), 511 requireError: requireBadParameterContains("validating ip_sans[1]: invalid CIDR address: llama"), 512 }, 513 } 514 515 for _, tt := range tests { 516 t.Run(tt.name, func(t *testing.T) { 517 err := tt.role.CheckAndSetDefaults() 518 tt.requireError(t, err) 519 }) 520 } 521 } 522 523 func TestRoleFilterMatch(t *testing.T) { 524 regularRole := RoleV6{ 525 Metadata: Metadata{ 526 Name: "request-approver", 527 }, 528 } 529 systemRole := RoleV6{ 530 Metadata: Metadata{ 531 Name: "bot", 532 Labels: map[string]string{ 533 TeleportInternalResourceType: SystemResource, 534 }, 535 }, 536 } 537 538 tests := []struct { 539 name string 540 role *RoleV6 541 filter *RoleFilter 542 shouldMatch bool 543 }{ 544 { 545 name: "empty filter should match everything", 546 role: ®ularRole, 547 filter: &RoleFilter{}, 548 shouldMatch: true, 549 }, 550 { 551 name: "correct search keyword should match the regular role", 552 role: ®ularRole, 553 filter: &RoleFilter{SearchKeywords: []string{"appr"}}, 554 shouldMatch: true, 555 }, 556 { 557 name: "correct search keyword should match the system role", 558 role: &systemRole, 559 filter: &RoleFilter{SearchKeywords: []string{"bot"}}, 560 shouldMatch: true, 561 }, 562 { 563 name: "incorrect search keyword shouldn't match the role", 564 role: ®ularRole, 565 filter: &RoleFilter{SearchKeywords: []string{"xyz"}}, 566 shouldMatch: false, 567 }, 568 { 569 name: "skip system roles filter shouldn't match the system role", 570 role: &systemRole, 571 filter: &RoleFilter{SkipSystemRoles: true}, 572 shouldMatch: false, 573 }, 574 { 575 name: "skip system roles filter should match the regular role", 576 role: ®ularRole, 577 filter: &RoleFilter{SkipSystemRoles: true}, 578 shouldMatch: true, 579 }, 580 { 581 name: "skip system roles filter and incorrect search keywords shouldn't match the regular role", 582 role: ®ularRole, 583 filter: &RoleFilter{SkipSystemRoles: true, SearchKeywords: []string{"xyz"}}, 584 shouldMatch: false, 585 }, 586 { 587 name: "skip system roles filter and correct search keywords shouldn't match the system role", 588 role: &systemRole, 589 filter: &RoleFilter{SkipSystemRoles: true, SearchKeywords: []string{"bot"}}, 590 shouldMatch: false, 591 }, 592 { 593 name: "skip system roles filter and correct search keywords should match the regular role", 594 role: ®ularRole, 595 filter: &RoleFilter{SkipSystemRoles: true, SearchKeywords: []string{"appr"}}, 596 shouldMatch: true, 597 }, 598 } 599 600 for _, tt := range tests { 601 t.Run(tt.name, func(t *testing.T) { 602 err := tt.role.CheckAndSetDefaults() 603 require.NoError(t, err) 604 require.Equal(t, tt.shouldMatch, tt.filter.Match(tt.role)) 605 }) 606 } 607 }