This is an automated email from the ASF dual-hosted git repository. squakez pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit 3e7578b08666619a33a9db473321decc5def1761 Author: AllanAlmeida <[email protected]> AuthorDate: Thu Sep 17 11:51:44 2026 -0300 Fix #6778: Validate service account permission by resource name Generated-by: OpenAI Codex --- pkg/internal/client.go | 3 +- pkg/trait/kamelets.go | 53 +++++++++++++++------------- pkg/trait/kamelets_test.go | 23 +++++++++++++ pkg/util/bindings/catalog.go | 1 + pkg/util/bindings/catalog_test.go | 26 ++++++++++++++ pkg/util/kubernetes/permission.go | 5 +-- pkg/util/kubernetes/permission_test.go | 63 ++++++++++++++++++++++++++++++++++ 7 files changed, 148 insertions(+), 26 deletions(-) diff --git a/pkg/internal/client.go b/pkg/internal/client.go index 50f014123..3de56810c 100644 --- a/pkg/internal/client.go +++ b/pkg/internal/client.go @@ -269,7 +269,8 @@ type FakeSAR struct { // Create fake create implementation (needed in cross namespace Kamelets test). Only allow `cross-ns-sa` user in `default` namespace. func (f *FakeSAR) Create(ctx context.Context, sar *authv1.SubjectAccessReview, opts metav1.CreateOptions) (*authv1.SubjectAccessReview, error) { ra := sar.Spec.ResourceAttributes - allowed := sar.Spec.User == "system:serviceaccount:default:cross-ns-sa" && ra.Verb == "get" && ra.Resource == "kamelets" + allowed := sar.Spec.User == "system:serviceaccount:default:cross-ns-sa" && ra.Verb == "get" && + ra.Resource == "kamelets" && ra.Name != "restricted-kamelet" sar.Status.Allowed = allowed sar.Status.Reason = "mocked" diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go index a2b421f96..6b3c16133 100644 --- a/pkg/trait/kamelets.go +++ b/pkg/trait/kamelets.go @@ -202,33 +202,40 @@ func (t *kameletsTrait) collectKamelets(e *Environment) (map[string]*v1.Kamelet, // calculateNamespaces is in charge to scan the kamelets specification and provide a list of // namespaces where to look for Kamelets. func (t *kameletsTrait) calculateNamespaces(e *Environment, defaultNamespaces ...string) ([]string, error) { - namespaces, err := calculateNamespaces(strings.Split(t.List, ",")) + kamelets := strings.Split(t.List, ",") + namespaces, err := calculateNamespaces(kamelets) if err != nil { return namespaces, err } - if len(namespaces) > 0 { - if e.Integration.Spec.ServiceAccountName == "" { - return nil, errors.New("you must to use an authorized ServiceAccount to access cross-namespace resources kamelets. " + - "Set it in the Integration spec accordingly") + if len(namespaces) > 0 && e.Integration.Spec.ServiceAccountName == "" { + return nil, errors.New("you must to use an authorized ServiceAccount to access cross-namespace resources kamelets. " + + "Set it in the Integration spec accordingly") + } + // verify an SA exists and it is authorized for each Kamelet + for _, kml := range kamelets { + ns, err := getKameletNamespace(kml) + if err != nil { + return nil, fmt.Errorf("could not parse kamelet namespace: %w", err) } - // verify an SA exists and it is authorized for Kamelets in that namespace - for _, ns := range namespaces { - ok, err := kubernetes.CheckServiceAccountPermission( - e.Ctx, - e.Client, - fmt.Sprintf("system:serviceaccount:%s:%s", e.Integration.Namespace, e.Integration.Spec.ServiceAccountName), - v1.SchemeGroupVersion.Group, - "kamelets", - ns, - "get", - ) - if err != nil { - return nil, err - } - if !ok { - return nil, fmt.Errorf("cross-namespace Integration reference authorization denied for the ServiceAccount %s and resources kamelets", - e.Integration.Spec.ServiceAccountName) - } + if ns == "" { + continue + } + ok, err := kubernetes.CheckServiceAccountPermission( + e.Ctx, + e.Client, + fmt.Sprintf("system:serviceaccount:%s:%s", e.Integration.Namespace, e.Integration.Spec.ServiceAccountName), + v1.SchemeGroupVersion.Group, + "kamelets", + ns, + getKameletKey(kml), + "get", + ) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("cross-namespace Integration reference authorization denied for the ServiceAccount %s and resources kamelets", + e.Integration.Spec.ServiceAccountName) } } diff --git a/pkg/trait/kamelets_test.go b/pkg/trait/kamelets_test.go index 3afcaf77a..91652f4dd 100644 --- a/pkg/trait/kamelets_test.go +++ b/pkg/trait/kamelets_test.go @@ -850,6 +850,29 @@ func TestKameletMultiNamespace(t *testing.T) { "Kamelets [extra,timer] found in cluster") } +func TestKameletMultiNamespaceDeniedResource(t *testing.T) { + flow := ` +- from: + uri: kamelet:timer + steps: + - to: kamelet:extra?kameletNamespace=ns1 + - to: kamelet:restricted-kamelet?kameletNamespace=ns1 +` + trait, environment := createKameletsTestEnvironment(flow) + environment.Integration.Namespace = "default" + environment.Integration.Spec.ServiceAccountName = "cross-ns-sa" + + enabled, condition, err := trait.Configure(environment) + require.NoError(t, err) + assert.True(t, enabled) + assert.Nil(t, condition) + + err = trait.Apply(environment) + require.Error(t, err) + assert.Equal(t, "cross-namespace Integration reference authorization denied for the ServiceAccount cross-ns-sa "+ + "and resources kamelets", err.Error()) +} + func TestKameletMultiNamespaceMissing(t *testing.T) { flow := ` - from: diff --git a/pkg/util/bindings/catalog.go b/pkg/util/bindings/catalog.go index 6e9f5bd33..36e6eb88b 100644 --- a/pkg/util/bindings/catalog.go +++ b/pkg/util/bindings/catalog.go @@ -105,6 +105,7 @@ func verifyResourceRBAC(ctx BindingContext, e v1.Endpoint) error { e.Ref.GroupVersionKind().Group, resources, e.Ref.Namespace, + e.Ref.Name, "get", ) diff --git a/pkg/util/bindings/catalog_test.go b/pkg/util/bindings/catalog_test.go index 5a73b68db..44e0f2422 100644 --- a/pkg/util/bindings/catalog_test.go +++ b/pkg/util/bindings/catalog_test.go @@ -192,6 +192,32 @@ func TestValidateEndpointKameletCrossNS(t *testing.T) { require.NoError(t, err) } +func TestValidateEndpointKameletCrossNSDeniedResource(t *testing.T) { + client, err := internal.NewFakeClient() + require.NoError(t, err) + + endpoint := v1.Endpoint{ + Ref: &corev1.ObjectReference{ + Kind: v1.KameletKind, + APIVersion: v1.SchemeGroupVersion.String(), + Name: "restricted-kamelet", + Namespace: "kamelet-ns", + }, + } + + bindingContext := BindingContext{ + Namespace: "default", + Client: client, + Ctx: context.Background(), + ServiceAccountName: "cross-ns-sa", + } + + err = validateEndpoint(bindingContext, endpoint) + require.Error(t, err) + require.Equal(t, "cross-namespace Pipe reference authorization denied for the ServiceAccount cross-ns-sa"+ + " and resources kamelets", err.Error()) +} + func TestValidateEndpointKameletCrossNSNoSA(t *testing.T) { client, err := internal.NewFakeClient() require.NoError(t, err) diff --git a/pkg/util/kubernetes/permission.go b/pkg/util/kubernetes/permission.go index 867d93019..7e2870aec 100644 --- a/pkg/util/kubernetes/permission.go +++ b/pkg/util/kubernetes/permission.go @@ -56,14 +56,15 @@ func CheckSelfPermission(ctx context.Context, client kubernetes.Interface, group // CheckServiceAccountPermission verify if a given Service Account can access a given resource. // Service Account must be provided as "system:serviceaccount:namespace:name" format. -func CheckServiceAccountPermission(ctx context.Context, client kubernetes.Interface, sa, group, resources, namespace, verb string) (bool, error) { +func CheckServiceAccountPermission(ctx context.Context, client kubernetes.Interface, sa, group, resource, namespace, name, verb string) (bool, error) { sarReview := &authorizationv1.SubjectAccessReview{ Spec: authorizationv1.SubjectAccessReviewSpec{ User: sa, ResourceAttributes: &authorizationv1.ResourceAttributes{ Group: group, Namespace: namespace, - Resource: resources, + Resource: resource, + Name: name, Verb: verb, }, }, diff --git a/pkg/util/kubernetes/permission_test.go b/pkg/util/kubernetes/permission_test.go new file mode 100644 index 000000000..10ed4f878 --- /dev/null +++ b/pkg/util/kubernetes/permission_test.go @@ -0,0 +1,63 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubernetes + +import ( + "context" + "testing" + + authorizationv1 "k8s.io/api/authorization/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + k8stesting "k8s.io/client-go/testing" + + "github.com/stretchr/testify/require" +) + +func TestCheckServiceAccountPermissionByResourceName(t *testing.T) { + client := fake.NewSimpleClientset() + client.PrependReactor("create", "subjectaccessreviews", func(action k8stesting.Action) (bool, runtime.Object, error) { + createAction, ok := action.(k8stesting.CreateAction) + require.True(t, ok) + review, ok := createAction.GetObject().(*authorizationv1.SubjectAccessReview) + require.True(t, ok) + require.Equal(t, "system:serviceaccount:source:integration", review.Spec.User) + require.Equal(t, "camel.apache.org", review.Spec.ResourceAttributes.Group) + require.Equal(t, "kamelets", review.Spec.ResourceAttributes.Resource) + require.Equal(t, "target", review.Spec.ResourceAttributes.Namespace) + require.Equal(t, "get", review.Spec.ResourceAttributes.Verb) + + review.Status.Allowed = review.Spec.ResourceAttributes.Name == "allowed-kamelet" + + return true, review, nil + }) + + allowed, err := CheckServiceAccountPermission( + context.Background(), client, "system:serviceaccount:source:integration", + "camel.apache.org", "kamelets", "target", "allowed-kamelet", "get", + ) + require.NoError(t, err) + require.True(t, allowed) + + allowed, err = CheckServiceAccountPermission( + context.Background(), client, "system:serviceaccount:source:integration", + "camel.apache.org", "kamelets", "target", "denied-kamelet", "get", + ) + require.NoError(t, err) + require.False(t, allowed) +}
