diff --git a/lib/kube/proxy/forwarder.go b/lib/kube/proxy/forwarder.go index 3ad776eda6234..6d0f3ed5a277b 100644 --- a/lib/kube/proxy/forwarder.go +++ b/lib/kube/proxy/forwarder.go @@ -1124,14 +1124,14 @@ func matchKubernetesResource(resource types.KubernetesResource, allowed, denied // utils.KubeResourceMatchesRegex checks if the resource.Kind is strictly equal // to each entry and validates if the Name and Namespace fields matches the // regex allowed by each entry. - result, err := utils.KubeResourceMatchesRegex(resource, denied) + result, err := utils.KubeResourceMatchesRegex(resource, denied, types.Deny) if err != nil { return false, trace.Wrap(err) } else if result { return false, nil } - result, err = utils.KubeResourceMatchesRegex(resource, allowed) + result, err = utils.KubeResourceMatchesRegex(resource, allowed, types.Allow) if err != nil { return false, trace.Wrap(err) } diff --git a/lib/services/role.go b/lib/services/role.go index 85ef239e9b85a..5d600a63ff2a2 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -2258,7 +2258,7 @@ func NewKubernetesResourceMatcher(resource types.KubernetesResource) *Kubernetes // Match matches a Kubernetes Resource against provided role and condition. func (m *KubernetesResourceMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) { - result, err := utils.KubeResourceMatchesRegex(m.resource, role.GetKubeResources(condition)) + result, err := utils.KubeResourceMatchesRegex(m.resource, role.GetKubeResources(condition), condition) return result, trace.Wrap(err) } diff --git a/lib/utils/replace.go b/lib/utils/replace.go index 8c49934c4ddc5..22632ab6ca80b 100644 --- a/lib/utils/replace.go +++ b/lib/utils/replace.go @@ -145,14 +145,14 @@ const ( // input is the resource we are checking for access. // resources is a list of resources that the user has access to - collected from // their roles that match the Kubernetes cluster where the resource is defined. -func KubeResourceMatchesRegex(input types.KubernetesResource, resources []types.KubernetesResource) (bool, error) { +// cond is the deny or allow condition of the role that we are evaluating. +func KubeResourceMatchesRegex(input types.KubernetesResource, resources []types.KubernetesResource, cond types.RoleConditionType) (bool, error) { if len(input.Verbs) != 1 { return false, trace.BadParameter("only one verb is supported, input: %v", input.Verbs) } // isClusterWideResource is true if the resource is cluster-wide, e.g. a // namespace resource or a clusterrole. isClusterWideResource := slices.Contains(types.KubernetesClusterWideResourceKinds, input.Kind) - verb := input.Verbs[0] // If the user is list/read/watch a namespace, they should be able to see the // namespace they have resources defined for. @@ -187,7 +187,7 @@ func KubeResourceMatchesRegex(input types.KubernetesResource, resources []types. if ok, err := MatchString(input.Namespace, resource.Name); err != nil || ok { return ok, trace.Wrap(err) } - case targetsReadOnlyNamespace && resource.Kind != types.KindKubeNamespace && resource.Namespace != "": + case targetsReadOnlyNamespace && cond == types.Allow && resource.Kind != types.KindKubeNamespace && resource.Namespace != "": // If the user requests a read-only namespace get/list/watch, they should // be able to see the list of namespaces they have resources defined in. // This means that if the user has access to pods in the "foo" namespace, diff --git a/lib/utils/replace_test.go b/lib/utils/replace_test.go index 09a88bc1a359d..7ba15f4d47f0d 100644 --- a/lib/utils/replace_test.go +++ b/lib/utils/replace_test.go @@ -158,6 +158,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { name string input types.KubernetesResource resources []types.KubernetesResource + action types.RoleConditionType matches bool assert require.ErrorAssertionFunc }{ @@ -176,8 +177,102 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.Error, + action: types.Allow, matches: false, }, + { + name: "list namespace matches resource", + input: types.KubernetesResource{ + Kind: types.KindNamespace, + Verbs: []string{types.KubeVerbList}, + }, + resources: []types.KubernetesResource{ + { + Kind: types.KindKubeSecret, + Namespace: "*", + Name: "*", + Verbs: []string{types.Wildcard}, + }, + }, + assert: require.NoError, + action: types.Allow, + matches: true, + }, + { + name: "list namespace doesn't match denying secrets", + input: types.KubernetesResource{ + Kind: types.KindNamespace, + Verbs: []string{types.KubeVerbList}, + }, + resources: []types.KubernetesResource{ + { + Kind: types.KindKubeSecret, + Namespace: "*", + Name: "*", + Verbs: []string{types.Wildcard}, + }, + }, + assert: require.NoError, + action: types.Deny, + matches: false, + }, + { + name: "get namespace match denying everything", + input: types.KubernetesResource{ + Kind: types.KindNamespace, + Name: "default", + Verbs: []string{types.KubeVerbGet}, + }, + resources: []types.KubernetesResource{ + { + Kind: types.Wildcard, + Namespace: types.Wildcard, + Name: types.Wildcard, + Verbs: []string{types.Wildcard}, + }, + }, + assert: require.NoError, + action: types.Deny, + matches: true, + }, + { + name: "get namespace doesn't match denying secrets", + input: types.KubernetesResource{ + Kind: types.KindNamespace, + Name: "default", + Verbs: []string{types.KubeVerbGet}, + }, + resources: []types.KubernetesResource{ + { + Kind: types.KindKubeSecret, + Namespace: "*", + Name: "*", + Verbs: []string{types.Wildcard}, + }, + }, + assert: require.NoError, + action: types.Deny, + matches: false, + }, + { + name: "get secret matches denying secrets", + input: types.KubernetesResource{ + Kind: types.KindKubeSecret, + Name: "default", + Verbs: []string{types.KubeVerbGet}, + }, + resources: []types.KubernetesResource{ + { + Kind: types.KindKubeSecret, + Namespace: "*", + Name: "*", + Verbs: []string{types.Wildcard}, + }, + }, + assert: require.NoError, + action: types.Deny, + matches: true, + }, { name: "input matches single resource with wildcard verb", input: types.KubernetesResource{ @@ -195,6 +290,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, { @@ -214,6 +310,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, { @@ -233,6 +330,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: false, }, { @@ -251,6 +349,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: false, }, { @@ -282,6 +381,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, { @@ -301,6 +401,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, { @@ -320,6 +421,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: false, }, { @@ -338,6 +440,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { Verbs: []string{types.Wildcard}, }, }, + action: types.Allow, assert: require.Error, }, { @@ -355,6 +458,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { Name: "podname", }, }, + action: types.Allow, assert: require.NoError, }, { @@ -372,6 +476,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, { @@ -390,6 +495,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, { @@ -408,6 +514,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: false, }, { @@ -426,6 +533,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, @@ -445,6 +553,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: false, }, @@ -464,6 +573,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, { @@ -482,6 +592,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: false, }, @@ -501,6 +612,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: true, }, { @@ -519,6 +631,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: false, }, { @@ -543,12 +656,13 @@ func TestKubeResourceMatchesRegex(t *testing.T) { }, }, assert: require.NoError, + action: types.Allow, matches: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := KubeResourceMatchesRegex(tt.input, tt.resources) + got, err := KubeResourceMatchesRegex(tt.input, tt.resources, tt.action) tt.assert(t, err) require.Equal(t, tt.matches, got) })