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:        &regularRole,
   547  			filter:      &RoleFilter{},
   548  			shouldMatch: true,
   549  		},
   550  		{
   551  			name:        "correct search keyword should match the regular role",
   552  			role:        &regularRole,
   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:        &regularRole,
   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:        &regularRole,
   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:        &regularRole,
   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:        &regularRole,
   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  }