Skip to content

Commit ffe5336

Browse files
committed
feat: support csrf annotations for ingress
Signed-off-by: Ashing Zheng <[email protected]>
1 parent f554569 commit ffe5336

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
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

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+
// Verify CSRF plugin is configured in the route
390+
routes, err := s.DefaultDataplaneResource().Route().List(context.Background())
391+
Expect(err).NotTo(HaveOccurred(), "listing Route")
392+
Expect(routes).To(HaveLen(1), "checking Route length")
393+
Expect(routes[0].Plugins).To(HaveKey("csrf"), "checking Route plugins")
394+
jsonBytes, err := json.Marshal(routes[0].Plugins["csrf"])
395+
Expect(err).NotTo(HaveOccurred(), "marshalling csrf plugin config")
396+
var csrfConfig map[string]any
397+
err = json.Unmarshal(jsonBytes, &csrfConfig)
398+
Expect(err).NotTo(HaveOccurred(), "unmarshalling csrf plugin config")
399+
Expect(csrfConfig["key"]).To(Equal("foo-key"), "checking csrf key")
400+
401+
// Request without CSRF token should fail
402+
msg401 := s.NewAPISIXClient().
403+
POST("/anything").
404+
WithHeader("Host", "httpbin.example").
405+
Expect().
406+
Status(http.StatusUnauthorized).
407+
Body().
408+
Raw()
409+
Expect(msg401).To(ContainSubstring("no csrf token in headers"), "checking error message")
410+
411+
// GET request should succeed and return CSRF token in cookie
412+
resp := s.NewAPISIXClient().
413+
GET("/anything").
414+
WithHeader("Host", "httpbin.example").
415+
Expect().
416+
Status(http.StatusOK)
417+
resp.Header("Set-Cookie").NotEmpty()
418+
419+
cookie := resp.Cookie("apisix-csrf-token")
420+
token := cookie.Value().Raw()
421+
422+
// POST request with valid CSRF token should succeed
423+
_ = s.NewAPISIXClient().
424+
POST("/anything").
425+
WithHeader("Host", "httpbin.example").
426+
WithHeader("apisix-csrf-token", token).
427+
WithCookie("apisix-csrf-token", token).
428+
Expect().
429+
Status(http.StatusOK)
430+
431+
})
362432
})
363433
})

0 commit comments

Comments
 (0)