This is an automated email from the ASF dual-hosted git repository.
pcongiusti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git
The following commit(s) were added to refs/heads/main by this push:
new b262ccc48 chore(ctrl): check SA permission to access cross namespace
resources
b262ccc48 is described below
commit b262ccc4853546b7ea5ec4b24555e7c4c65406a1
Author: Pasquale Congiusti <[email protected]>
AuthorDate: Sat Nov 15 08:59:50 2025 +0100
chore(ctrl): check SA permission to access cross namespace resources
We make sure that the pipe/integration SA is authorized to access to the
resources in other namespaces before binding.
---
.../cross-ns}/my-timer-source-ns.kamelet.yaml | 0
.../cross-ns/pipe-cross-ns.yaml} | 29 +++----
.../cross-ns/sa-role.yaml} | 27 ++-----
.../cross-ns/sa-rolebinding.yaml} | 31 +++-----
.../cross-ns/sa.yaml} | 23 +-----
e2e/common/misc/pipe_cross_ns_test.go | 91 ++++++++++++++++++++++
.../my-timer-source-ns.kamelet.yaml | 0
.../sa-role.yaml} | 27 ++-----
.../sa-rolebinding.yaml} | 31 +++-----
.../sa.yaml} | 23 +-----
e2e/common/traits/kamelet_test.go | 17 +++-
.../integration/integration_controller.go | 2 +-
pkg/controller/pipe/integration.go | 11 +--
pkg/controller/synthetic/synthetic.go | 2 +-
pkg/install/openshift.go | 2 +-
pkg/internal/client.go | 20 +++++
.../rbac/descoped/operator-cluster-role.yaml | 7 ++
pkg/trait/kamelets.go | 46 +++++++++--
pkg/trait/kamelets_test.go | 26 +++++--
pkg/util/bindings/api.go | 11 +--
pkg/util/bindings/catalog.go | 34 ++++++++
pkg/util/bindings/catalog_test.go | 67 +++++++++++++++-
pkg/util/kubernetes/permission.go | 30 ++++++-
23 files changed, 391 insertions(+), 166 deletions(-)
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/misc/cross-ns/my-timer-source-ns.kamelet.yaml
similarity index 100%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/my-timer-source-ns.kamelet.yaml
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/misc/cross-ns/pipe-cross-ns.yaml
similarity index 71%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/pipe-cross-ns.yaml
index dca31ad89..13e31be8f 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/misc/cross-ns/pipe-cross-ns.yaml
@@ -14,23 +14,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ---------------------------------------------------------------------------
+
apiVersion: camel.apache.org/v1
-kind: Kamelet
+kind: Pipe
metadata:
- name: my-timer-source
- labels:
- camel.apache.org/kamelet.type: "source"
+ name: pipe-cross-ns
spec:
- definition:
- title: "Timer Example"
- description: "Produces periodic events with a custom payload"
- types:
- out:
- mediaType: text/plain
- template:
- from:
- uri: timer:tick
- steps:
- - setBody:
- constant: "Kamelet NS"
- - to: "kamelet:sink"
+ serviceAccountName: my-cross-ns-sa
+ sink:
+ uri: log:bye
+ source:
+ ref:
+ apiVersion: camel.apache.org/v1
+ kind: Kamelet
+ name: my-timer-source
+ namespace: %%%
\ No newline at end of file
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/misc/cross-ns/sa-role.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/sa-role.yaml
index dca31ad89..60787d812 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/misc/cross-ns/sa-role.yaml
@@ -14,23 +14,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
metadata:
- name: my-timer-source
- labels:
- camel.apache.org/kamelet.type: "source"
-spec:
- definition:
- title: "Timer Example"
- description: "Produces periodic events with a custom payload"
- types:
- out:
- mediaType: text/plain
- template:
- from:
- uri: timer:tick
- steps:
- - setBody:
- constant: "Kamelet NS"
- - to: "kamelet:sink"
+ name: test-get-kamelets
+rules:
+- apiGroups: ["camel.apache.org"]
+ resources: ["kamelets"]
+ verbs: ["get"]
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/misc/cross-ns/sa-rolebinding.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/sa-rolebinding.yaml
index dca31ad89..0dc04e183 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/misc/cross-ns/sa-rolebinding.yaml
@@ -14,23 +14,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
metadata:
- name: my-timer-source
- labels:
- camel.apache.org/kamelet.type: "source"
-spec:
- definition:
- title: "Timer Example"
- description: "Produces periodic events with a custom payload"
- types:
- out:
- mediaType: text/plain
- template:
- from:
- uri: timer:tick
- steps:
- - setBody:
- constant: "Kamelet NS"
- - to: "kamelet:sink"
+ name: sa-get-kamelets
+subjects:
+- kind: ServiceAccount
+ name: my-cross-ns-sa
+ namespace: %%%
+roleRef:
+ kind: Role
+ name: test-get-kamelets
+ apiGroup: rbac.authorization.k8s.io
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/misc/cross-ns/sa.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/sa.yaml
index dca31ad89..9d6e9b0d3 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/misc/cross-ns/sa.yaml
@@ -14,23 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: v1
+kind: ServiceAccount
metadata:
- name: my-timer-source
- labels:
- camel.apache.org/kamelet.type: "source"
-spec:
- definition:
- title: "Timer Example"
- description: "Produces periodic events with a custom payload"
- types:
- out:
- mediaType: text/plain
- template:
- from:
- uri: timer:tick
- steps:
- - setBody:
- constant: "Kamelet NS"
- - to: "kamelet:sink"
+ name: my-cross-ns-sa
diff --git a/e2e/common/misc/pipe_cross_ns_test.go
b/e2e/common/misc/pipe_cross_ns_test.go
new file mode 100644
index 000000000..8d8821dd2
--- /dev/null
+++ b/e2e/common/misc/pipe_cross_ns_test.go
@@ -0,0 +1,91 @@
+//go:build integration
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go ->
Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+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 common
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+
+ . "github.com/apache/camel-k/v2/e2e/support"
+ v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+)
+
+func TestPipeCrossNamespaceKamelet(t *testing.T) {
+ t.Parallel()
+ WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns1 string)
{
+ t.Run("store kamelet", func(t *testing.T) {
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
"cross-ns/my-timer-source-ns.kamelet.yaml", "-n", ns1))
+ })
+
+ WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns2
string) {
+ t.Run("set privileges", func(t *testing.T) {
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
"cross-ns/sa.yaml", "-n", ns2))
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
"cross-ns/sa-role.yaml", "-n", ns1))
+ saRbFile := cloneAndReplaceNamespace(t,
"cross-ns/sa-rolebinding.yaml", ns2)
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
saRbFile, "-n", ns1))
+ })
+ t.Run("cross namespace pipe", func(t *testing.T) {
+ // Clone the resource in a temporary file as it
will require to be changed
+ pipeFile := cloneAndReplaceNamespace(t,
"cross-ns/pipe-cross-ns.yaml", ns1)
+ name := "pipe-cross-ns"
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
pipeFile, "-n", ns2))
+ g.Eventually(IntegrationConditionStatus(t, ctx,
ns2, name, v1.IntegrationConditionReady), TestTimeoutMedium).
+ Should(Equal(corev1.ConditionTrue))
+ g.Eventually(IntegrationPodPhase(t, ctx, ns2,
name), TestTimeoutShort).Should(Equal(corev1.PodRunning))
+ g.Eventually(IntegrationLogs(t, ctx, ns2,
name), TestTimeoutShort).Should(ContainSubstring("Kamelet NS"))
+ })
+ })
+ })
+}
+
+// cloneAndReplaceNamespace clones and replace the content marked as %%% with
the namespace passed as parameter.
+func cloneAndReplaceNamespace(t *testing.T, srcPath, namespace string) string {
+ t.Helper()
+
+ tempDir := t.TempDir()
+ tempPath := filepath.Join(tempDir, filepath.Base(srcPath))
+
+ dstFile, err := os.Create(tempPath)
+ if err != nil {
+ t.Fatalf("failed to create temp file: %v", err)
+ }
+ defer dstFile.Close()
+
+ content, err := os.ReadFile(srcPath)
+ if err != nil {
+ t.Fatalf("failed to read src file: %v", err)
+ }
+ updated := strings.ReplaceAll(string(content), "%%%", namespace)
+ err = os.WriteFile(tempPath, []byte(updated), 0644)
+ if err != nil {
+ t.Fatalf("failed to write dst file: %v", err)
+ }
+
+ return tempPath
+}
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/traits/cross-ns/my-timer-source-ns.kamelet.yaml
similarity index 100%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/traits/cross-ns/my-timer-source-ns.kamelet.yaml
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/traits/cross-ns/sa-role.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/traits/cross-ns/sa-role.yaml
index dca31ad89..60787d812 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/traits/cross-ns/sa-role.yaml
@@ -14,23 +14,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
metadata:
- name: my-timer-source
- labels:
- camel.apache.org/kamelet.type: "source"
-spec:
- definition:
- title: "Timer Example"
- description: "Produces periodic events with a custom payload"
- types:
- out:
- mediaType: text/plain
- template:
- from:
- uri: timer:tick
- steps:
- - setBody:
- constant: "Kamelet NS"
- - to: "kamelet:sink"
+ name: test-get-kamelets
+rules:
+- apiGroups: ["camel.apache.org"]
+ resources: ["kamelets"]
+ verbs: ["get"]
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/traits/cross-ns/sa-rolebinding.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/traits/cross-ns/sa-rolebinding.yaml
index dca31ad89..0dc04e183 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/traits/cross-ns/sa-rolebinding.yaml
@@ -14,23 +14,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
metadata:
- name: my-timer-source
- labels:
- camel.apache.org/kamelet.type: "source"
-spec:
- definition:
- title: "Timer Example"
- description: "Produces periodic events with a custom payload"
- types:
- out:
- mediaType: text/plain
- template:
- from:
- uri: timer:tick
- steps:
- - setBody:
- constant: "Kamelet NS"
- - to: "kamelet:sink"
+ name: sa-get-kamelets
+subjects:
+- kind: ServiceAccount
+ name: my-cross-ns-sa
+ namespace: %%%
+roleRef:
+ kind: Role
+ name: test-get-kamelets
+ apiGroup: rbac.authorization.k8s.io
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
b/e2e/common/traits/cross-ns/sa.yaml
similarity index 69%
rename from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
rename to e2e/common/traits/cross-ns/sa.yaml
index dca31ad89..9d6e9b0d3 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/traits/cross-ns/sa.yaml
@@ -14,23 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: v1
+kind: ServiceAccount
metadata:
- name: my-timer-source
- labels:
- camel.apache.org/kamelet.type: "source"
-spec:
- definition:
- title: "Timer Example"
- description: "Produces periodic events with a custom payload"
- types:
- out:
- mediaType: text/plain
- template:
- from:
- uri: timer:tick
- steps:
- - setBody:
- constant: "Kamelet NS"
- - to: "kamelet:sink"
+ name: my-cross-ns-sa
diff --git a/e2e/common/traits/kamelet_test.go
b/e2e/common/traits/kamelet_test.go
index 1dedf9c7e..9fa431969 100644
--- a/e2e/common/traits/kamelet_test.go
+++ b/e2e/common/traits/kamelet_test.go
@@ -89,15 +89,24 @@ func TestKameletNamespaced(t *testing.T) {
t.Parallel()
WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns1 string)
{
t.Run("store kamelet", func(t *testing.T) {
- ExpectExecSucceed(t, g, Kubectl("apply", "-f",
"files/my-timer-source-ns.kamelet.yaml", "-n", ns1))
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
"cross-ns/my-timer-source-ns.kamelet.yaml", "-n", ns1))
})
WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns2
string) {
- t.Run("namespaced kamelet", func(t *testing.T) {
+ t.Run("set privileges", func(t *testing.T) {
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
"cross-ns/sa.yaml", "-n", ns2))
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
"cross-ns/sa-role.yaml", "-n", ns1))
+ saRbFile := cloneAndReplaceNamespace(t,
"cross-ns/sa-rolebinding.yaml", ns2)
+ ExpectExecSucceed(t, g, Kubectl("apply", "-f",
saRbFile, "-n", ns1))
+ })
+ t.Run("cross namespace kamelet", func(t *testing.T) {
// Clone the resource in a temporary file as it
will require to be changed
routeFile := cloneAndReplaceNamespace(t,
"files/kamelet-it-ns.yaml", ns1)
- name :=
RandomizedSuffixName("namespaced-kamelet")
- g.Expect(KamelRun(t, ctx, ns2, routeFile,
"--name", name).Execute()).To(Succeed())
+ name :=
RandomizedSuffixName("cross-namespace-kamelet")
+ g.Expect(KamelRun(t, ctx, ns2, routeFile,
+ "--service-account", "my-cross-ns-sa",
+ "--name", name,
+ ).Execute()).To(Succeed())
g.Eventually(IntegrationConditionStatus(t, ctx,
ns2, name, v1.IntegrationConditionReady), TestTimeoutMedium).
Should(Equal(corev1.ConditionTrue))
g.Eventually(IntegrationPodPhase(t, ctx, ns2,
name), TestTimeoutShort).Should(Equal(corev1.PodRunning))
diff --git a/pkg/controller/integration/integration_controller.go
b/pkg/controller/integration/integration_controller.go
index 4d6e1548c..28b3ed861 100644
--- a/pkg/controller/integration/integration_controller.go
+++ b/pkg/controller/integration/integration_controller.go
@@ -412,7 +412,7 @@ func watchKnativeResources(ctx context.Context, c
client.Client, b *builder.Buil
// Check for permission to watch the Knative Service resource
checkCtx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
- if ok, err = kubernetes.CheckPermission(checkCtx, c, serving.GroupName,
"services", platform.GetOperatorWatchNamespace(), "", "watch"); err != nil {
+ if ok, err = kubernetes.CheckSelfPermission(checkCtx, c,
serving.GroupName, "services", platform.GetOperatorWatchNamespace(), "",
"watch"); err != nil {
return err
} else if ok {
log.Info("KnativeService resources installed in the cluster.
RBAC privileges assigned correctly, you can use Knative features.")
diff --git a/pkg/controller/pipe/integration.go
b/pkg/controller/pipe/integration.go
index ffe3d5cbc..f31fbdab2 100644
--- a/pkg/controller/pipe/integration.go
+++ b/pkg/controller/pipe/integration.go
@@ -110,11 +110,12 @@ func CreateIntegrationFor(ctx context.Context, c
client.Client, pipe *v1.Pipe) (
}
bindingContext := bindings.BindingContext{
- Ctx: ctx,
- Client: c,
- Namespace: it.Namespace,
- Profile: profile,
- Metadata: it.Annotations,
+ Ctx: ctx,
+ Client: c,
+ Namespace: it.Namespace,
+ Profile: profile,
+ Metadata: it.Annotations,
+ ServiceAccountName: it.Spec.ServiceAccountName,
}
from, err := bindings.Translate(bindingContext,
endpointTypeSourceContext, pipe.Spec.Source)
diff --git a/pkg/controller/synthetic/synthetic.go
b/pkg/controller/synthetic/synthetic.go
index 286eea16a..a3673e4b2 100644
--- a/pkg/controller/synthetic/synthetic.go
+++ b/pkg/controller/synthetic/synthetic.go
@@ -141,7 +141,7 @@ func getInformers(ctx context.Context, cl client.Client, c
cache.Cache) ([]cache
}
// Watch for the Knative Services conditionally
if ok, err := kubernetes.IsAPIResourceInstalled(cl,
servingv1.SchemeGroupVersion.String(),
reflect.TypeOf(servingv1.Service{}).Name()); ok && err == nil {
- if ok, err := kubernetes.CheckPermission(ctx, cl,
serving.GroupName, "services", platform.GetOperatorWatchNamespace(), "",
"watch"); ok && err == nil {
+ if ok, err := kubernetes.CheckSelfPermission(ctx, cl,
serving.GroupName, "services", platform.GetOperatorWatchNamespace(), "",
"watch"); ok && err == nil {
ksvc, err := c.GetInformer(ctx, &servingv1.Service{})
if err != nil {
return nil, err
diff --git a/pkg/install/openshift.go b/pkg/install/openshift.go
index 0d72152b7..26a4484c3 100644
--- a/pkg/install/openshift.go
+++ b/pkg/install/openshift.go
@@ -69,7 +69,7 @@ func OpenShiftConsoleDownloadLink(ctx context.Context, c
client.Client) error {
}
// Check for permission to create the ConsoleCLIDownload resource
- ok, err = kubernetes.CheckPermission(ctx, c, console.GroupName,
"consoleclidownloads", "", KamelCLIDownloadName, "create")
+ ok, err = kubernetes.CheckSelfPermission(ctx, c, console.GroupName,
"consoleclidownloads", "", KamelCLIDownloadName, "create")
if err != nil {
return err
}
diff --git a/pkg/internal/client.go b/pkg/internal/client.go
index acc9cd2c9..8bfb1a3ef 100644
--- a/pkg/internal/client.go
+++ b/pkg/internal/client.go
@@ -28,6 +28,7 @@ import (
fakecamelclientset
"github.com/apache/camel-k/v2/pkg/client/camel/clientset/versioned/fake"
camelv1
"github.com/apache/camel-k/v2/pkg/client/camel/clientset/versioned/typed/camel/v1"
"github.com/apache/camel-k/v2/pkg/util"
+ authv1 "k8s.io/api/authorization/v1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
@@ -249,6 +250,25 @@ func (f *FakeAuthorization) SelfSubjectRulesReviews()
authorizationv1.SelfSubjec
return f.AuthorizationV1Interface.SelfSubjectRulesReviews()
}
+// Returns a fake SAR interface.
+func (f *FakeAuthorization) SubjectAccessReviews()
authorizationv1.SubjectAccessReviewInterface {
+ return &FakeSAR{}
+}
+
+type FakeSAR struct {
+ authorizationv1.SubjectAccessReviewInterface
+}
+
+// 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"
+
+ sar.Status.Allowed = allowed
+ sar.Status.Reason = "mocked"
+ return sar, nil
+}
+
type FakeDiscovery struct {
discovery.DiscoveryInterface
diff --git a/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
b/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
index 1c2e4bb4a..dc20bfb86 100644
--- a/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
+++ b/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
@@ -183,3 +183,10 @@ rules:
verbs:
- get
- list
+# Required to check if a ServiceAccount can access other namespaces resources
+- apiGroups:
+ - authorization.k8s.io
+ resources:
+ - subjectaccessreviews
+ verbs:
+ - create
\ No newline at end of file
diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go
index 860dd5bd8..dfcf86cdb 100644
--- a/pkg/trait/kamelets.go
+++ b/pkg/trait/kamelets.go
@@ -39,6 +39,7 @@ import (
"github.com/apache/camel-k/v2/pkg/util/camel"
"github.com/apache/camel-k/v2/pkg/util/digest"
"github.com/apache/camel-k/v2/pkg/util/dsl"
+ "github.com/apache/camel-k/v2/pkg/util/kubernetes"
)
const (
@@ -101,7 +102,7 @@ func (t *kameletsTrait) Apply(e *Environment) error {
// collectKamelets load a Kamelet specification setting the specific version
specification.
func (t *kameletsTrait) collectKamelets(e *Environment)
(map[string]*v1.Kamelet, error) {
- namespaces, err := t.calculateNamespaces(e.Integration.Namespace,
platform.GetOperatorNamespace())
+ namespaces, err := t.calculateNamespaces(e, e.Integration.Namespace,
platform.GetOperatorNamespace())
if err != nil {
return nil, err
}
@@ -167,7 +168,8 @@ func (t *kameletsTrait) collectKamelets(e *Environment)
(map[string]*v1.Kamelet,
kameletsAvailabilityMessage := ""
cond := corev1.ConditionTrue
if len(missingKamelets) > 0 {
- kameletsAvailabilityMessage = fmt.Sprintf("Kamelets [%s] not
found in cluster. Make sure to include the Kamelets dependency in the
Integration.",
+ kameletsAvailabilityMessage = fmt.Sprintf("Kamelets [%s] not
found in cluster. "+
+ "Make sure to include the Kamelets dependency in the
Integration.",
strings.Join(missingKamelets, ","))
cond = corev1.ConditionUnknown
}
@@ -189,12 +191,44 @@ 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(defaultNamespaces ...string)
([]string, error) {
- return calculateNamespaces(strings.Split(t.List, ","),
defaultNamespaces...)
+func (t *kameletsTrait) calculateNamespaces(e *Environment, defaultNamespaces
...string) ([]string, error) {
+ namespaces, err := calculateNamespaces(strings.Split(t.List, ","))
+ if err != nil {
+ return namespaces, err
+ }
+ if len(namespaces) > 0 {
+ if e.Integration.Spec.ServiceAccountName == "" {
+ return nil, fmt.Errorf("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 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)
+ }
+ }
+ }
+
+ // Also append the default namespaces
+ namespaces = append(namespaces, defaultNamespaces...)
+ return namespaces, nil
}
-func calculateNamespaces(kamelets []string, defaultNamespaces ...string)
([]string, error) {
- namespaces := defaultNamespaces
+func calculateNamespaces(kamelets []string) ([]string, error) {
+ var namespaces []string
for _, kml := range kamelets {
ns, err := getKameletNamespace(kml)
if err != nil {
diff --git a/pkg/trait/kamelets_test.go b/pkg/trait/kamelets_test.go
index 48dc27fcb..9c0944b4c 100644
--- a/pkg/trait/kamelets_test.go
+++ b/pkg/trait/kamelets_test.go
@@ -773,14 +773,11 @@ func TestKameletNamespaceParameter(t *testing.T) {
func TestCalculateKameletNamespaces(t *testing.T) {
namespaces, err := calculateNamespaces(
[]string{"my-kamelet", "my-kamelet?kameletNamespace=ns1",
"my-kamelet?kameletVersion=v2&kameletNamespace=ns2"},
- "default", "camel-k",
)
require.NoError(t, err)
- assert.Len(t, namespaces, 4)
+ assert.Len(t, namespaces, 2)
assert.Contains(t, namespaces, "ns1")
assert.Contains(t, namespaces, "ns2")
- assert.Contains(t, namespaces, "default")
- assert.Contains(t, namespaces, "camel-k")
}
func TestKameletMultiNamespace(t *testing.T) {
@@ -794,7 +791,7 @@ func TestKameletMultiNamespace(t *testing.T) {
flow,
&v1.Kamelet{
ObjectMeta: metav1.ObjectMeta{
- Namespace: "test",
+ Namespace: "default",
Name: "timer",
},
Spec: v1.KameletSpec{
@@ -828,6 +825,20 @@ func TestKameletMultiNamespace(t *testing.T) {
assert.True(t, enabled)
assert.Nil(t, condition)
+ // Must fail, no ServiceAccount
+ err = trait.Apply(environment)
+ require.Error(t, err)
+ assert.Equal(t, "you must to use an authorized ServiceAccount to access
cross-namespace resources kamelets. "+
+ "Set it in the Integration spec accordingly", err.Error())
+ // Must fail, unauthorized ServiceAccount
+ environment.Integration.Spec.ServiceAccountName = "unauth-sa"
+ err = trait.Apply(environment)
+ require.Error(t, err)
+ assert.Equal(t, "cross-namespace Integration reference authorization
denied for the ServiceAccount unauth-sa "+
+ "and resources kamelets", err.Error())
+ // Now we should good to go
+ environment.Integration.Namespace = "default"
+ environment.Integration.Spec.ServiceAccountName = "cross-ns-sa"
err = trait.Apply(environment)
require.NoError(t, err)
assert.Equal(t, "extra?kameletNamespace=ns1,timer", trait.List)
@@ -850,7 +861,7 @@ func TestKameletMultiNamespaceMissing(t *testing.T) {
flow,
&v1.Kamelet{
ObjectMeta: metav1.ObjectMeta{
- Namespace: "test",
+ Namespace: "default",
Name: "timer",
},
Spec: v1.KameletSpec{
@@ -879,6 +890,9 @@ func TestKameletMultiNamespaceMissing(t *testing.T) {
},
})
+ // Cross namespaces authorized user
+ 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)
diff --git a/pkg/util/bindings/api.go b/pkg/util/bindings/api.go
index 47dbe7ff2..80f3d142a 100644
--- a/pkg/util/bindings/api.go
+++ b/pkg/util/bindings/api.go
@@ -57,11 +57,12 @@ type BindingProvider interface {
//nolint:containedctx
type BindingContext struct {
- Ctx context.Context
- Client client.Client
- Namespace string
- Profile v1.TraitProfile
- Metadata map[string]string
+ Ctx context.Context
+ Client client.Client
+ Namespace string
+ Profile v1.TraitProfile
+ Metadata map[string]string
+ ServiceAccountName string
}
type EndpointContext struct {
diff --git a/pkg/util/bindings/catalog.go b/pkg/util/bindings/catalog.go
index 3c0c7c8f6..37be81be2 100644
--- a/pkg/util/bindings/catalog.go
+++ b/pkg/util/bindings/catalog.go
@@ -21,8 +21,10 @@ import (
"errors"
"fmt"
"sort"
+ "strings"
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+ "github.com/apache/camel-k/v2/pkg/util/kubernetes"
"k8s.io/utils/ptr"
)
@@ -78,6 +80,38 @@ func validateEndpoint(ctx BindingContext, e v1.Endpoint)
error {
}
return errors.New("cross-namespace Pipe references are
not allowed for Knative")
}
+ // only check this when there is a cross-namespace access
+ return verifyResourceRBAC(ctx, e)
+ }
+
+ return nil
+}
+
+// verifyResourceRBAC verify if the ServiceAccount which is running the
Integration has enough
+// privileges to access the resource. We avoid any potential cross-namespace
vulnerability access.
+func verifyResourceRBAC(ctx BindingContext, e v1.Endpoint) error {
+ resources := strings.ToLower(e.Ref.Kind) + "s"
+ if ctx.ServiceAccountName == "" {
+ return fmt.Errorf("you must to use an authorized ServiceAccount
to access cross-namespace resources %s. "+
+ "Set it in the Pipe spec accordingly", resources)
+ }
+ ok, err := kubernetes.CheckServiceAccountPermission(
+ ctx.Ctx,
+ ctx.Client,
+ fmt.Sprintf("system:serviceaccount:%s:%s", ctx.Namespace,
ctx.ServiceAccountName),
+ e.Ref.GroupVersionKind().Group,
+ resources,
+ e.Ref.Namespace,
+ "get",
+ )
+
+ if err != nil {
+ return err
+ }
+
+ if !ok {
+ return fmt.Errorf("cross-namespace Pipe reference authorization
denied for the ServiceAccount %s and resources %s",
+ ctx.ServiceAccountName, resources)
}
return nil
}
diff --git a/pkg/util/bindings/catalog_test.go
b/pkg/util/bindings/catalog_test.go
index 53538280d..5a73b68db 100644
--- a/pkg/util/bindings/catalog_test.go
+++ b/pkg/util/bindings/catalog_test.go
@@ -122,9 +122,6 @@ func TestValidateEndpoint(t *testing.T) {
for i, tc := range testcases {
t.Run(fmt.Sprintf("test-%d-%s", i, tc.name), func(t *testing.T)
{
- if tc.operatorNamespace != "" {
- t.Setenv("NAMESPACE", tc.operatorNamespace)
- }
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -138,6 +135,11 @@ func TestValidateEndpoint(t *testing.T) {
Namespace: tc.namespace,
Profile: v1.DefaultTraitProfile,
}
+ if tc.operatorNamespace != "" {
+ // special privileges required for cross
namespace
+ bindingContext.ServiceAccountName =
"cross-ns-sa"
+ bindingContext.Namespace = "default"
+ }
err = validateEndpoint(bindingContext, tc.endpoint)
require.NoError(t, err)
@@ -167,6 +169,33 @@ func TestValidateEndpointErrorRefURI(t *testing.T) {
}
func TestValidateEndpointKameletCrossNS(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: "foo-kamelet",
+ Namespace: "kamelet-ns",
+ },
+ }
+
+ bindingContext := BindingContext{
+ Namespace: "default",
+ Client: client,
+ Ctx: context.Background(),
+ ServiceAccountName: "cross-ns-sa",
+ }
+
+ err = validateEndpoint(bindingContext, endpoint)
+ require.NoError(t, err)
+}
+
+func TestValidateEndpointKameletCrossNSNoSA(t *testing.T) {
+ client, err := internal.NewFakeClient()
+ require.NoError(t, err)
+
endpoint := v1.Endpoint{
Ref: &corev1.ObjectReference{
Kind: v1.KameletKind,
@@ -178,10 +207,40 @@ func TestValidateEndpointKameletCrossNS(t *testing.T) {
bindingContext := BindingContext{
Namespace: "default",
+ Client: client,
+ Ctx: context.Background(),
}
- err := validateEndpoint(bindingContext, endpoint)
+ err = validateEndpoint(bindingContext, endpoint)
+ require.Error(t, err)
+ require.Equal(t, "you must to use an authorized ServiceAccount to
access cross-namespace resources kamelets. "+
+ "Set it in the Pipe spec accordingly", err.Error())
+}
+
+func TestValidateEndpointKameletCrossNSDenied(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: "foo-kamelet",
+ Namespace: "kamelet-ns",
+ },
+ }
+
+ bindingContext := BindingContext{
+ Namespace: "default",
+ Client: client,
+ Ctx: context.Background(),
+ ServiceAccountName: "my-sa",
+ }
+
+ err = validateEndpoint(bindingContext, endpoint)
+ require.Error(t, err)
+ require.Equal(t, "cross-namespace Pipe reference authorization denied
for the ServiceAccount my-sa"+
+ " and resources kamelets", err.Error())
}
func TestValidateEndpointErrorKnativeCrossNS(t *testing.T) {
diff --git a/pkg/util/kubernetes/permission.go
b/pkg/util/kubernetes/permission.go
index e8f3c9466..1f3af7e1c 100644
--- a/pkg/util/kubernetes/permission.go
+++ b/pkg/util/kubernetes/permission.go
@@ -26,10 +26,10 @@ import (
"k8s.io/client-go/kubernetes"
)
-// CheckPermission can be used to check if the current user/service-account is
allowed to execute a given operation
+// CheckSelfPermission can be used to check if the current
user/service-account is allowed to execute a given operation
// in the cluster.
// E.g. checkPermission(client, olmv1alpha1.GroupName,
"clusterserviceversions", namespace, "camel-k", "get").
-func CheckPermission(ctx context.Context, client kubernetes.Interface, group,
resource, namespace, name, verb string) (bool, error) {
+func CheckSelfPermission(ctx context.Context, client kubernetes.Interface,
group, resource, namespace, name, verb string) (bool, error) {
sarReview := &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
@@ -52,3 +52,29 @@ func CheckPermission(ctx context.Context, client
kubernetes.Interface, group, re
return sar.Status.Allowed, nil
}
+
+// 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) {
+ sarReview := &authorizationv1.SubjectAccessReview{
+ Spec: authorizationv1.SubjectAccessReviewSpec{
+ User: sa,
+ ResourceAttributes: &authorizationv1.ResourceAttributes{
+ Group: group,
+ Namespace: namespace,
+ Resource: resources,
+ Verb: verb,
+ },
+ },
+ }
+
+ sar, err := client.AuthorizationV1().SubjectAccessReviews().Create(ctx,
sarReview, metav1.CreateOptions{})
+ if err != nil {
+ if k8serrors.IsForbidden(err) {
+ return false, nil
+ }
+ return false, err
+ }
+
+ return sar.Status.Allowed, nil
+}