Skip to content

Commit b25c4b4

Browse files
authored
feat: support csrf annotations for ingress (#2626)
1 parent f554569 commit b25c4b4

File tree

5 files changed

+171
-2
lines changed

5 files changed

+171
-2
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package plugins
17+
18+
import (
19+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
20+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
21+
)
22+
23+
type csrf struct{}
24+
25+
// NewCSRFHandler creates a handler to convert annotations about
26+
// CSRF to APISIX csrf plugin.
27+
func NewCSRFHandler() PluginAnnotationsHandler {
28+
return &csrf{}
29+
}
30+
31+
func (c *csrf) PluginName() string {
32+
return "csrf"
33+
}
34+
35+
func (c *csrf) Handle(e annotations.Extractor) (any, error) {
36+
if !e.GetBoolAnnotation(annotations.AnnotationsEnableCsrf) {
37+
return nil, nil
38+
}
39+
40+
key := e.GetStringAnnotation(annotations.AnnotationsCsrfKey)
41+
if key == "" {
42+
return nil, nil
43+
}
44+
45+
return &adctypes.CSRFConfig{
46+
Key: key,
47+
}, nil
48+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package plugins
17+
18+
import (
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
23+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
24+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
25+
)
26+
27+
func TestCSRFHandler(t *testing.T) {
28+
anno := map[string]string{
29+
annotations.AnnotationsEnableCsrf: "true",
30+
annotations.AnnotationsCsrfKey: "my-secret-key",
31+
}
32+
p := NewCSRFHandler()
33+
out, err := p.Handle(annotations.NewExtractor(anno))
34+
assert.Nil(t, err, "checking given error")
35+
config := out.(*adctypes.CSRFConfig)
36+
assert.Equal(t, "my-secret-key", config.Key)
37+
38+
assert.Equal(t, "csrf", p.PluginName())
39+
40+
// Test with enable-csrf set to false
41+
anno[annotations.AnnotationsEnableCsrf] = "false"
42+
out, err = p.Handle(annotations.NewExtractor(anno))
43+
assert.Nil(t, err, "checking given error")
44+
assert.Nil(t, out, "checking given output")
45+
46+
// Test with enable-csrf true but no key
47+
anno[annotations.AnnotationsEnableCsrf] = "true"
48+
delete(anno, annotations.AnnotationsCsrfKey)
49+
out, err = p.Handle(annotations.NewExtractor(anno))
50+
assert.Nil(t, err, "checking given error")
51+
assert.Nil(t, out, "checking given output when key is missing")
52+
}

internal/adc/translator/annotations/plugins/plugins.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ var (
3838
handlers = []PluginAnnotationsHandler{
3939
NewRedirectHandler(),
4040
NewCorsHandler(),
41+
NewCSRFHandler(),
4142
}
4243
)
4344

internal/webhook/v1/ingress_webhook.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ var ingresslog = logf.Log.WithName("ingress-resource")
4141
var unsupportedAnnotations = []string{
4242
"k8s.apisix.apache.org/use-regex",
4343
"k8s.apisix.apache.org/plugin-config-name",
44-
"k8s.apisix.apache.org/enable-csrf",
45-
"k8s.apisix.apache.org/csrf-key",
4644
"k8s.apisix.apache.org/rewrite-target",
4745
"k8s.apisix.apache.org/rewrite-target-regex",
4846
"k8s.apisix.apache.org/rewrite-target-regex-template",

test/e2e/ingress/annotations.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,28 @@ spec:
317317
name: httpbin-service-e2e-test
318318
port:
319319
number: 80
320+
`
321+
ingressCSRF = `
322+
apiVersion: networking.k8s.io/v1
323+
kind: Ingress
324+
metadata:
325+
name: csrf
326+
annotations:
327+
k8s.apisix.apache.org/enable-csrf: "true"
328+
k8s.apisix.apache.org/csrf-key: "foo-key"
329+
spec:
330+
ingressClassName: %s
331+
rules:
332+
- host: httpbin.example
333+
http:
334+
paths:
335+
- path: /anything
336+
pathType: Prefix
337+
backend:
338+
service:
339+
name: httpbin-service-e2e-test
340+
port:
341+
number: 80
320342
`
321343
)
322344
BeforeEach(func() {
@@ -359,5 +381,53 @@ spec:
359381
Status(http.StatusPermanentRedirect).
360382
Header("Location").IsEqual("/anything/ip")
361383
})
384+
385+
It("csrf", func() {
386+
Expect(s.CreateResourceFromString(fmt.Sprintf(ingressCSRF, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
387+
388+
time.Sleep(5 * time.Second)
389+
390+
By("Request without CSRF token should fail")
391+
msg401 := s.NewAPISIXClient().
392+
POST("/anything").
393+
WithHeader("Host", "httpbin.example").
394+
Expect().
395+
Status(http.StatusUnauthorized).
396+
Body().
397+
Raw()
398+
Expect(msg401).To(ContainSubstring("no csrf token in headers"), "checking error message")
399+
400+
By("GET request should succeed and return CSRF token in cookie")
401+
resp := s.NewAPISIXClient().
402+
GET("/anything").
403+
WithHeader("Host", "httpbin.example").
404+
Expect().
405+
Status(http.StatusOK)
406+
resp.Header("Set-Cookie").NotEmpty()
407+
408+
cookie := resp.Cookie("apisix-csrf-token")
409+
token := cookie.Value().Raw()
410+
411+
By("POST request with valid CSRF token should succeed")
412+
_ = s.NewAPISIXClient().
413+
POST("/anything").
414+
WithHeader("Host", "httpbin.example").
415+
WithHeader("apisix-csrf-token", token).
416+
WithCookie("apisix-csrf-token", token).
417+
Expect().
418+
Status(http.StatusOK)
419+
420+
By("Verify CSRF plugin is configured in the route")
421+
routes, err := s.DefaultDataplaneResource().Route().List(context.Background())
422+
Expect(err).NotTo(HaveOccurred(), "listing Route")
423+
Expect(routes).To(HaveLen(1), "checking Route length")
424+
Expect(routes[0].Plugins).To(HaveKey("csrf"), "checking Route plugins")
425+
jsonBytes, err := json.Marshal(routes[0].Plugins["csrf"])
426+
Expect(err).NotTo(HaveOccurred(), "marshalling csrf plugin config")
427+
var csrfConfig map[string]any
428+
err = json.Unmarshal(jsonBytes, &csrfConfig)
429+
Expect(err).NotTo(HaveOccurred(), "unmarshalling csrf plugin config")
430+
Expect(csrfConfig["key"]).To(Equal("foo-key"), "checking csrf key")
431+
})
362432
})
363433
})

0 commit comments

Comments
 (0)