You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by kv...@apache.org on 2021/06/09 08:32:08 UTC

[apisix-ingress-controller] branch master updated: chore: add authentication for ApisixRoute (#528)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new fca6211  chore: add authentication for ApisixRoute (#528)
fca6211 is described below

commit fca62110b81958935263c816f71be96c4500a84e
Author: Jintao Zhang <ta...@163.com>
AuthorDate: Wed Jun 9 16:31:57 2021 +0800

    chore: add authentication for ApisixRoute (#528)
---
 pkg/kube/apisix/apis/config/v2alpha1/types.go      |  21 +-
 .../apis/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 83ecd88..f5d08c2 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 ee97f83..107459b 100644
--- a/pkg/kube/apisix/apis/config/v2alpha1/zz_generated.deepcopy.go
+++ b/pkg/kube/apisix/apis/config/v2alpha1/zz_generated.deepcopy.go
@@ -400,6 +400,39 @@ func (in *ApisixRoute) DeepCopyObject() runtime.Object {
 }
 
 // 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
 	if in.Match != nil {
@@ -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 9285bb6..ba2ef2b 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 974795e..619213e 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 b4eade6..0ee672c 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)
 	})
 })