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 <[email protected]>
AuthorDate: Tue Oct 28 12:05:52 2025 +0800

    feat: support csrf annotations for ingress
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 .../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)
+
+               })
        })
 })

Reply via email to