From fca62110b81958935263c816f71be96c4500a84e Mon Sep 17 00:00:00 2001 From: Jintao Zhang Date: Wed, 9 Jun 2021 16:31:57 +0800 Subject: [PATCH] chore: add authentication for ApisixRoute (#528) --- pkg/kube/apisix/apis/config/v2alpha1/types.go | 21 +- .../config/v2alpha1/zz_generated.deepcopy.go | 38 ++ pkg/kube/translation/apisix_route.go | 13 + samples/deploy/crd/v1beta1/ApisixRoute.yaml | 15 + test/e2e/features/consumer.go | 378 +++++++++++++++++- 5 files changed, 456 insertions(+), 9 deletions(-) diff --git a/pkg/kube/apisix/apis/config/v2alpha1/types.go b/pkg/kube/apisix/apis/config/v2alpha1/types.go index 83ecd88a00..f5d08c2e98 100644 --- a/pkg/kube/apisix/apis/config/v2alpha1/types.go +++ b/pkg/kube/apisix/apis/config/v2alpha1/types.go @@ -96,9 +96,10 @@ type ApisixRouteHTTP struct { // Backends represents potential backends to proxy after the route // rule matched. When number of backends are more than one, traffic-split // plugin in APISIX will be used to split traffic based on the backend weight. - Backends []*ApisixRouteHTTPBackend `json:"backends" yaml:"backends"` - Websocket bool `json:"websocket" yaml:"websocket"` - Plugins []*ApisixRouteHTTPPlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"` + Backends []*ApisixRouteHTTPBackend `json:"backends" yaml:"backends"` + Websocket bool `json:"websocket" yaml:"websocket"` + Plugins []*ApisixRouteHTTPPlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"` + Authentication *ApisixRouteAuthentication `json:"authentication,omitempty" yaml:"authentication,omitempty"` } // ApisixRouteHTTPMatch represents the match condition for hitting this route. @@ -194,6 +195,20 @@ type ApisixRouteHTTPPlugin struct { // any plugins. type ApisixRouteHTTPPluginConfig map[string]interface{} +// ApisixRouteAuthentication is the authentication-related +// configuration in ApisixRoute. +type ApisixRouteAuthentication struct { + Enable bool `json:"enable" yaml:"enable"` + Type string `json:"type" yaml:"type"` + KeyAuth ApisixRouteAuthenticationKeyAuth `json:"keyauth,omitempty" yaml:"keyauth,omitempty"` +} + +// ApisixRouteAuthenticationKeyAuth is the keyAuth-related +// configuration in ApisixRouteAuthentication. +type ApisixRouteAuthenticationKeyAuth struct { + Header string `json:"header,omitempty" yaml:"header,omitempty"` +} + func (p ApisixRouteHTTPPluginConfig) DeepCopyInto(out *ApisixRouteHTTPPluginConfig) { b, _ := json.Marshal(&p) _ = json.Unmarshal(b, out) diff --git a/pkg/kube/apisix/apis/config/v2alpha1/zz_generated.deepcopy.go b/pkg/kube/apisix/apis/config/v2alpha1/zz_generated.deepcopy.go index ee97f838f2..107459ba89 100644 --- a/pkg/kube/apisix/apis/config/v2alpha1/zz_generated.deepcopy.go +++ b/pkg/kube/apisix/apis/config/v2alpha1/zz_generated.deepcopy.go @@ -399,6 +399,39 @@ func (in *ApisixRoute) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApisixRouteAuthentication) DeepCopyInto(out *ApisixRouteAuthentication) { + *out = *in + out.KeyAuth = in.KeyAuth + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteAuthentication. +func (in *ApisixRouteAuthentication) DeepCopy() *ApisixRouteAuthentication { + if in == nil { + return nil + } + out := new(ApisixRouteAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApisixRouteAuthenticationKeyAuth) DeepCopyInto(out *ApisixRouteAuthenticationKeyAuth) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteAuthenticationKeyAuth. +func (in *ApisixRouteAuthenticationKeyAuth) DeepCopy() *ApisixRouteAuthenticationKeyAuth { + if in == nil { + return nil + } + out := new(ApisixRouteAuthenticationKeyAuth) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRouteHTTP) DeepCopyInto(out *ApisixRouteHTTP) { *out = *in @@ -434,6 +467,11 @@ func (in *ApisixRouteHTTP) DeepCopyInto(out *ApisixRouteHTTP) { } } } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(ApisixRouteAuthentication) + **out = **in + } return } diff --git a/pkg/kube/translation/apisix_route.go b/pkg/kube/translation/apisix_route.go index 9285bb6c75..ba2ef2baa5 100644 --- a/pkg/kube/translation/apisix_route.go +++ b/pkg/kube/translation/apisix_route.go @@ -130,6 +130,19 @@ func (t *translator) translateHTTPRoute(ctx *TranslateContext, ar *configv2alpha pluginMap[plugin.Name] = make(map[string]interface{}) } } + + // add KeyAuth and basicAuth plugin + if part.Authentication != nil && part.Authentication.Enable { + switch part.Authentication.Type { + case "keyAuth": + pluginMap["key-auth"] = part.Authentication.KeyAuth + case "basicAuth": + pluginMap["basic-auth"] = make(map[string]interface{}) + default: + pluginMap["basic-auth"] = make(map[string]interface{}) + } + } + var exprs [][]apisixv1.StringOrSlice if part.Match.NginxVars != nil { exprs, err = t.translateRouteMatchExprs(part.Match.NginxVars) diff --git a/samples/deploy/crd/v1beta1/ApisixRoute.yaml b/samples/deploy/crd/v1beta1/ApisixRoute.yaml index 974795e130..619213e928 100644 --- a/samples/deploy/crd/v1beta1/ApisixRoute.yaml +++ b/samples/deploy/crd/v1beta1/ApisixRoute.yaml @@ -210,6 +210,21 @@ spec: required: - name - enable + authentication: + type: object + properties: + enable: + type: boolean + type: + type: string + enum: ["basicAuth", "keyAuth"] + keyAuth: + type: object + properties: + header: + type: string + required: + - enable tcp: type: array minItems: 1 diff --git a/test/e2e/features/consumer.go b/test/e2e/features/consumer.go index b4eade65f8..0ee672c9dd 100644 --- a/test/e2e/features/consumer.go +++ b/test/e2e/features/consumer.go @@ -15,6 +15,8 @@ package features import ( + "fmt" + "net/http" "time" "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" @@ -25,7 +27,7 @@ import ( var _ = ginkgo.Describe("ApisixConsumer", func() { s := scaffold.NewDefaultV2Scaffold() - ginkgo.It("create basicAuth consumer using value", func() { + ginkgo.It("ApisixRoute with basicAuth consumer", func() { ac := ` apiVersion: apisix.apache.org/v2alpha1 kind: ApisixConsumer @@ -38,12 +40,103 @@ spec: username: foo password: bar ` - assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ac), "creating ApisixConsumer") + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ac), "creating basicAuth ApisixConsumer") - defer func() { - err := s.RemoveResourceByString(ac) - assert.Nil(ginkgo.GinkgoT(), err) - }() + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + grs, err := s.ListApisixConsumers() + assert.Nil(ginkgo.GinkgoT(), err, "listing consumer") + assert.Len(ginkgo.GinkgoT(), grs, 1) + assert.Len(ginkgo.GinkgoT(), grs[0].Plugins, 1) + basicAuth, _ := grs[0].Plugins["basic-auth"] + assert.Equal(ginkgo.GinkgoT(), basicAuth, map[string]interface{}{ + "username": "foo", + "password": "bar", + }) + + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2alpha1 +kind: ApisixRoute +metadata: + name: httpbin-route +spec: + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + exprs: + - subject: + scope: Header + name: X-Foo + op: Equal + value: bar + backend: + serviceName: %s + servicePort: %d + authentication: + enable: true + type: basicAuth +`, backendSvc, backendPorts[0]) + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ar), "creating ApisixRoute with basicAuth") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixRoutesCreated(1), "Checking number of routes") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixUpstreamsCreated(1), "Checking number of upstreams") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + WithHeader("Authorization", "Basic Zm9vOmJhcg=="). + Expect(). + Status(http.StatusOK) + + msg := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg, "Missing authorization in request") + + msg = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "baz"). + WithHeader("Authorization", "Basic Zm9vOmJhcg=="). + Expect(). + Status(http.StatusNotFound). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg, "404 Route Not Found") + }) + + ginkgo.It("ApisixRoute with basicAuth consumer using secret", func() { + secret := ` +apiVersion: v1 +kind: Secret +metadata: + name: basic +data: + password: YmFy + username: Zm9v +` + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(secret), "creating basic secret for ApisixConsumer") + + ac := ` +apiVersion: apisix.apache.org/v2alpha1 +kind: ApisixConsumer +metadata: + name: basicvalue +spec: + authParameter: + basicAuth: + secretRef: + name: basic +` + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ac), "creating basicAuth ApisixConsumer") // Wait until the ApisixConsumer create event was delivered. time.Sleep(6 * time.Second) @@ -57,5 +150,278 @@ spec: "username": "foo", "password": "bar", }) + + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2alpha1 +kind: ApisixRoute +metadata: + name: httpbin-route +spec: + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + exprs: + - subject: + scope: Header + name: X-Foo + op: Equal + value: bar + backend: + serviceName: %s + servicePort: %d + authentication: + enable: true + type: basicAuth +`, backendSvc, backendPorts[0]) + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ar), "creating ApisixRoute with basicAuth") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixRoutesCreated(1), "Checking number of routes") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixUpstreamsCreated(1), "Checking number of upstreams") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + WithHeader("Authorization", "Basic Zm9vOmJhcg=="). + Expect(). + Status(http.StatusOK) + + msg := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg, "Missing authorization in request") + + msg = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "baz"). + WithHeader("Authorization", "Basic Zm9vOmJhcg=="). + Expect(). + Status(http.StatusNotFound). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg, "404 Route Not Found") + }) + + ginkgo.It("ApisixRoute with keyAuth consumer", func() { + ac := ` +apiVersion: apisix.apache.org/v2alpha1 +kind: ApisixConsumer +metadata: + name: keyvalue +spec: + authParameter: + keyAuth: + value: + key: foo +` + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ac), "creating keyAuth ApisixConsumer") + + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + grs, err := s.ListApisixConsumers() + assert.Nil(ginkgo.GinkgoT(), err, "listing consumer") + assert.Len(ginkgo.GinkgoT(), grs, 1) + assert.Len(ginkgo.GinkgoT(), grs[0].Plugins, 1) + basicAuth, _ := grs[0].Plugins["key-auth"] + assert.Equal(ginkgo.GinkgoT(), basicAuth, map[string]interface{}{ + "key": "foo", + }) + + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2alpha1 +kind: ApisixRoute +metadata: + name: httpbin-route +spec: + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + exprs: + - subject: + scope: Header + name: X-Foo + op: Equal + value: bar + backend: + serviceName: %s + servicePort: %d + authentication: + enable: true + type: keyAuth +`, backendSvc, backendPorts[0]) + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ar), "creating ApisixRoute with keyAuth") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixRoutesCreated(1), "Checking number of routes") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixUpstreamsCreated(1), "Checking number of upstreams") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + WithHeader("apikey", "foo"). + Expect(). + Status(http.StatusOK) + + msg := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg, "Missing API key found in request") + + msg = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "baz"). + WithHeader("apikey", "baz"). + Expect(). + Status(http.StatusNotFound). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg, "404 Route Not Found") + }) + + ginkgo.It("ApisixRoute with keyAuth consumer using secret", func() { + secret := ` +apiVersion: v1 +kind: Secret +metadata: + name: keyauth +data: + key: Zm9v +` + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(secret), "creating keyauth secret for ApisixConsumer") + + ac := ` +apiVersion: apisix.apache.org/v2alpha1 +kind: ApisixConsumer +metadata: + name: keyvalue +spec: + authParameter: + keyAuth: + secretRef: + name: keyauth +` + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ac), "creating keyAuth ApisixConsumer") + + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + grs, err := s.ListApisixConsumers() + assert.Nil(ginkgo.GinkgoT(), err, "listing consumer") + assert.Len(ginkgo.GinkgoT(), grs, 1) + assert.Len(ginkgo.GinkgoT(), grs[0].Plugins, 1) + basicAuth, _ := grs[0].Plugins["key-auth"] + assert.Equal(ginkgo.GinkgoT(), basicAuth, map[string]interface{}{ + "key": "foo", + }) + + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2alpha1 +kind: ApisixRoute +metadata: + name: httpbin-route +spec: + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + exprs: + - subject: + scope: Header + name: X-Foo + op: Equal + value: bar + backend: + serviceName: %s + servicePort: %d + authentication: + enable: true + type: keyAuth +`, backendSvc, backendPorts[0]) + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ar), "creating ApisixRoute with keyAuth") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixRoutesCreated(1), "Checking number of routes") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixUpstreamsCreated(1), "Checking number of upstreams") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + WithHeader("apikey", "foo"). + Expect(). + Status(http.StatusOK) + + msg := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg, "Missing API key found in request") + + msg = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "baz"). + WithHeader("apikey", "baz"). + Expect(). + Status(http.StatusNotFound). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg, "404 Route Not Found") + }) + + ginkgo.It("ApisixRoute without authentication", func() { + backendSvc, backendPorts := s.DefaultHTTPBackend() + ar := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2alpha1 +kind: ApisixRoute +metadata: + name: httpbin-route +spec: + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + exprs: + - subject: + scope: Header + name: X-Foo + op: Equal + value: bar + backend: + serviceName: %s + servicePort: %d + authentication: + enable: false +`, backendSvc, backendPorts[0]) + assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(ar), "creating ApisixRoute without authentication") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixRoutesCreated(1), "Checking number of routes") + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixUpstreamsCreated(1), "Checking number of upstreams") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("X-Foo", "bar"). + Expect(). + Status(http.StatusOK) }) })