You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ro...@apache.org on 2025/10/28 04:06:37 UTC

(apisix-ingress-controller) branch feat/csrf created (now ffe53367)

This is an automated email from the ASF dual-hosted git repository.

ronething pushed a change to branch feat/csrf
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


      at ffe53367 feat: support csrf annotations for ingress

This branch includes the following new commits:

     new ffe53367 feat: support csrf annotations for ingress

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(apisix-ingress-controller) 01/01: feat: support csrf annotations for ingress

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ronething pushed a commit to branch feat/csrf
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git

commit ffe53367f045a1031ccaa77321e57adeaba78fce
Author: Ashing Zheng <ax...@gmail.com>
AuthorDate: Tue Oct 28 12:05:52 2025 +0800

    feat: support csrf annotations for ingress
    
    Signed-off-by: Ashing Zheng <ax...@gmail.com>
---
 .../adc/translator/annotations/plugins/csrf.go     | 48 +++++++++++++++
 .../translator/annotations/plugins/csrf_test.go    | 52 ++++++++++++++++
 .../adc/translator/annotations/plugins/plugins.go  |  1 +
 test/e2e/ingress/annotations.go                    | 70 ++++++++++++++++++++++
 4 files changed, 171 insertions(+)

diff --git a/internal/adc/translator/annotations/plugins/csrf.go b/internal/adc/translator/annotations/plugins/csrf.go
new file mode 100644
index 00000000..1bee407c
--- /dev/null
+++ b/internal/adc/translator/annotations/plugins/csrf.go
@@ -0,0 +1,48 @@
+// 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 plugins
+
+import (
+	adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+type csrf struct{}
+
+// NewCSRFHandler creates a handler to convert annotations about
+// CSRF to APISIX csrf plugin.
+func NewCSRFHandler() PluginAnnotationsHandler {
+	return &csrf{}
+}
+
+func (c *csrf) PluginName() string {
+	return "csrf"
+}
+
+func (c *csrf) Handle(e annotations.Extractor) (any, error) {
+	if !e.GetBoolAnnotation(annotations.AnnotationsEnableCsrf) {
+		return nil, nil
+	}
+
+	key := e.GetStringAnnotation(annotations.AnnotationsCsrfKey)
+	if key == "" {
+		return nil, nil
+	}
+
+	return &adctypes.CSRFConfig{
+		Key: key,
+	}, nil
+}
diff --git a/internal/adc/translator/annotations/plugins/csrf_test.go b/internal/adc/translator/annotations/plugins/csrf_test.go
new file mode 100644
index 00000000..0f681703
--- /dev/null
+++ b/internal/adc/translator/annotations/plugins/csrf_test.go
@@ -0,0 +1,52 @@
+// 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 plugins
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+func TestCSRFHandler(t *testing.T) {
+	anno := map[string]string{
+		annotations.AnnotationsEnableCsrf: "true",
+		annotations.AnnotationsCsrfKey:    "my-secret-key",
+	}
+	p := NewCSRFHandler()
+	out, err := p.Handle(annotations.NewExtractor(anno))
+	assert.Nil(t, err, "checking given error")
+	config := out.(*adctypes.CSRFConfig)
+	assert.Equal(t, "my-secret-key", config.Key)
+
+	assert.Equal(t, "csrf", p.PluginName())
+
+	// Test with enable-csrf set to false
+	anno[annotations.AnnotationsEnableCsrf] = "false"
+	out, err = p.Handle(annotations.NewExtractor(anno))
+	assert.Nil(t, err, "checking given error")
+	assert.Nil(t, out, "checking given output")
+
+	// Test with enable-csrf true but no key
+	anno[annotations.AnnotationsEnableCsrf] = "true"
+	delete(anno, annotations.AnnotationsCsrfKey)
+	out, err = p.Handle(annotations.NewExtractor(anno))
+	assert.Nil(t, err, "checking given error")
+	assert.Nil(t, out, "checking given output when key is missing")
+}
diff --git a/internal/adc/translator/annotations/plugins/plugins.go b/internal/adc/translator/annotations/plugins/plugins.go
index ee1e6206..fb0f0b27 100644
--- a/internal/adc/translator/annotations/plugins/plugins.go
+++ b/internal/adc/translator/annotations/plugins/plugins.go
@@ -38,6 +38,7 @@ var (
 	handlers = []PluginAnnotationsHandler{
 		NewRedirectHandler(),
 		NewCorsHandler(),
+		NewCSRFHandler(),
 	}
 )
 
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 3128cb04..0bca460b 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -317,6 +317,28 @@ spec:
             name: httpbin-service-e2e-test
             port:
               number: 80
+`
+			ingressCSRF = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: csrf
+  annotations:
+    k8s.apisix.apache.org/enable-csrf: "true"
+    k8s.apisix.apache.org/csrf-key: "foo-key"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: httpbin.example
+    http:
+      paths:
+      - path: /anything
+        pathType: Prefix
+        backend:
+          service:
+            name: httpbin-service-e2e-test
+            port:
+              number: 80
 `
 		)
 		BeforeEach(func() {
@@ -359,5 +381,53 @@ spec:
 				Status(http.StatusPermanentRedirect).
 				Header("Location").IsEqual("/anything/ip")
 		})
+
+		It("csrf", func() {
+			Expect(s.CreateResourceFromString(fmt.Sprintf(ingressCSRF, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+			time.Sleep(5 * time.Second)
+			// Verify CSRF plugin is configured in the route
+			routes, err := s.DefaultDataplaneResource().Route().List(context.Background())
+			Expect(err).NotTo(HaveOccurred(), "listing Route")
+			Expect(routes).To(HaveLen(1), "checking Route length")
+			Expect(routes[0].Plugins).To(HaveKey("csrf"), "checking Route plugins")
+			jsonBytes, err := json.Marshal(routes[0].Plugins["csrf"])
+			Expect(err).NotTo(HaveOccurred(), "marshalling csrf plugin config")
+			var csrfConfig map[string]any
+			err = json.Unmarshal(jsonBytes, &csrfConfig)
+			Expect(err).NotTo(HaveOccurred(), "unmarshalling csrf plugin config")
+			Expect(csrfConfig["key"]).To(Equal("foo-key"), "checking csrf key")
+
+			// Request without CSRF token should fail
+			msg401 := s.NewAPISIXClient().
+				POST("/anything").
+				WithHeader("Host", "httpbin.example").
+				Expect().
+				Status(http.StatusUnauthorized).
+				Body().
+				Raw()
+			Expect(msg401).To(ContainSubstring("no csrf token in headers"), "checking error message")
+
+			// GET request should succeed and return CSRF token in cookie
+			resp := s.NewAPISIXClient().
+				GET("/anything").
+				WithHeader("Host", "httpbin.example").
+				Expect().
+				Status(http.StatusOK)
+			resp.Header("Set-Cookie").NotEmpty()
+
+			cookie := resp.Cookie("apisix-csrf-token")
+			token := cookie.Value().Raw()
+
+			// POST request with valid CSRF token should succeed
+			_ = s.NewAPISIXClient().
+				POST("/anything").
+				WithHeader("Host", "httpbin.example").
+				WithHeader("apisix-csrf-token", token).
+				WithCookie("apisix-csrf-token", token).
+				Expect().
+				Status(http.StatusOK)
+
+		})
 	})
 })