diff --git a/pkg/kube/translation/annotations.go b/pkg/kube/translation/annotations.go index 767f7da039..3f06b81340 100644 --- a/pkg/kube/translation/annotations.go +++ b/pkg/kube/translation/annotations.go @@ -29,6 +29,8 @@ var ( annotations.NewRewriteHandler(), annotations.NewRedirectHandler(), annotations.NewForwardAuthHandler(), + annotations.NewBasicAuthHandler(), + annotations.NewKeyAuthHandler(), } ) diff --git a/pkg/kube/translation/annotations/authorization.go b/pkg/kube/translation/annotations/authorization.go new file mode 100644 index 0000000000..c97567a4a5 --- /dev/null +++ b/pkg/kube/translation/annotations/authorization.go @@ -0,0 +1,64 @@ +// 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 annotations + +import ( + apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" +) + +const ( + // auth-type: keyAuth | basicAuth + _authType = AnnotationsPrefix + "auth-type" +) + +type basicAuth struct{} + +// NewkeyBasicHandler creates a handler to convert +// annotations about basicAuth control to APISIX basic-auth plugin. +func NewBasicAuthHandler() Handler { + return &basicAuth{} +} + +func (b *basicAuth) PluginName() string { + return "basic-auth" +} + +func (b *basicAuth) Handle(e Extractor) (interface{}, error) { + if e.GetStringAnnotation(_authType) != "basicAuth" { + return nil, nil + } + plugin := apisixv1.BasicAuthConfig{} + return &plugin, nil +} + +type keyAuth struct{} + +// NewkeyAuthHandler creates a handler to convert +// annotations about keyAuth control to APISIX key-auth plugin. +func NewKeyAuthHandler() Handler { + return &keyAuth{} +} + +func (k *keyAuth) PluginName() string { + return "key-auth" +} + +func (k *keyAuth) Handle(e Extractor) (interface{}, error) { + if e.GetStringAnnotation(_authType) != "keyAuth" { + return nil, nil + } + plugin := apisixv1.KeyAuthConfig{} + return &plugin, nil +} diff --git a/pkg/types/apisix/v1/plugin_types.go b/pkg/types/apisix/v1/plugin_types.go index daf0c4327f..96035b8c83 100644 --- a/pkg/types/apisix/v1/plugin_types.go +++ b/pkg/types/apisix/v1/plugin_types.go @@ -97,3 +97,13 @@ type ForwardAuthConfig struct { UpstreamHeaders []string `json:"upstream_headers,omitempty"` ClientHeaders []string `json:"client_headers,omitempty"` } + +// BasicAuthConfig is the rule config for basic-auth plugin. +// +k8s:deepcopy-gen=true +type BasicAuthConfig struct { +} + +// KeyAuthConfig is the rule config for key-auth plugin. +// +k8s:deepcopy-gen=true +type KeyAuthConfig struct { +} diff --git a/pkg/types/apisix/v1/zz_generated.deepcopy.go b/pkg/types/apisix/v1/zz_generated.deepcopy.go index eabfe8b9ce..4016a71321 100644 --- a/pkg/types/apisix/v1/zz_generated.deepcopy.go +++ b/pkg/types/apisix/v1/zz_generated.deepcopy.go @@ -19,6 +19,22 @@ package v1 +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthConfig) DeepCopyInto(out *BasicAuthConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthConfig. +func (in *BasicAuthConfig) DeepCopy() *BasicAuthConfig { + if in == nil { + return nil + } + out := new(BasicAuthConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BasicAuthConsumerConfig) DeepCopyInto(out *BasicAuthConsumerConfig) { *out = *in @@ -165,6 +181,22 @@ func (in *IPRestrictConfig) DeepCopy() *IPRestrictConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyAuthConfig) DeepCopyInto(out *KeyAuthConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyAuthConfig. +func (in *KeyAuthConfig) DeepCopy() *KeyAuthConfig { + if in == nil { + return nil + } + out := new(KeyAuthConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeyAuthConsumerConfig) DeepCopyInto(out *KeyAuthConsumerConfig) { *out = *in diff --git a/test/e2e/scaffold/consumer.go b/test/e2e/scaffold/consumer.go index 3c44a7c7cc..5cee9b2448 100644 --- a/test/e2e/scaffold/consumer.go +++ b/test/e2e/scaffold/consumer.go @@ -16,8 +16,8 @@ package scaffold import "fmt" -func (s *Scaffold) ApisixConsumerBasicAuthCreated(name, username, password string) error { - ac := fmt.Sprintf(` +var ( + _apisixConsumerBasicAuth = ` apiVersion: apisix.apache.org/v2beta3 kind: ApisixConsumer metadata: @@ -28,6 +28,26 @@ spec: value: username: %s password: %s -`, name, username, password) +` + _apisixConsumerKeyAuth = ` + apiVersion: apisix.apache.org/v2beta3 + kind: ApisixConsumer + metadata: + name: %s + spec: + authParameter: + keyAuth: + value: + key: %s + ` +) + +func (s *Scaffold) ApisixConsumerBasicAuthCreated(name, username, password string) error { + ac := fmt.Sprintf(_apisixConsumerBasicAuth, name, username, password) + return s.CreateResourceFromString(ac) +} + +func (s *Scaffold) ApisixConsumerKeyAuthCreated(name, key string) error { + ac := fmt.Sprintf(_apisixConsumerKeyAuth, name, key) return s.CreateResourceFromString(ac) } diff --git a/test/e2e/suite-annotations/authorization.go b/test/e2e/suite-annotations/authorization.go new file mode 100644 index 0000000000..d181865121 --- /dev/null +++ b/test/e2e/suite-annotations/authorization.go @@ -0,0 +1,310 @@ +// 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 annotations + +import ( + "fmt" + "net/http" + "time" + + "github.com/onsi/ginkgo" + "github.com/stretchr/testify/assert" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = ginkgo.Describe("suite-annotations: authorization annotations", func() { + s := scaffold.NewDefaultScaffold() + + ginkgo.It("enable keyAuth in ingress networking/v1", func() { + err := s.ApisixConsumerKeyAuthCreated("foo", "bar") + assert.Nil(ginkgo.GinkgoT(), err, "creating keyAuth ApisixConsumer") + + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/auth-type: "keyAuth" + name: ingress-v1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /ip + pathType: Exact + backend: + service: + name: %s + port: + number: %d +`, backendSvc, backendPort[0]) + err = s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + msg401 := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg401, "Missing API key found in request") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("apikey", "bar"). + Expect(). + Status(http.StatusOK) + }) + + ginkgo.It("enable keyAuth in ingress networking/v1beta1", func() { + err := s.ApisixConsumerKeyAuthCreated("foo", "bar") + assert.Nil(ginkgo.GinkgoT(), err, "creating keyAuth ApisixConsumer") + + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/auth-type: "keyAuth" + name: ingress-v1beta1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /ip + pathType: Exact + backend: + serviceName: %s + servicePort: %d +`, backendSvc, backendPort[0]) + err = s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + msg401 := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg401, "Missing API key found in request") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("apikey", "bar"). + Expect(). + Status(http.StatusOK) + }) + + ginkgo.It("enable keyAuth in ingress extensions/v1beta1", func() { + err := s.ApisixConsumerKeyAuthCreated("foo", "bar") + assert.Nil(ginkgo.GinkgoT(), err, "creating keyAuth ApisixConsumer") + + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/auth-type: "keyAuth" + name: ingress-extensions-v1beta1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /ip + pathType: Exact + backend: + serviceName: %s + servicePort: %d +`, backendSvc, backendPort[0]) + err = s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + msg401 := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg401, "Missing API key found in request") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("apikey", "bar"). + Expect(). + Status(http.StatusOK) + }) + + ginkgo.It("enable basicAuth in ingress networking/v1", func() { + err := s.ApisixConsumerBasicAuthCreated("jack1", "jack1-username", "jack1-password") + assert.Nil(ginkgo.GinkgoT(), err, "creating keyAuth ApisixConsumer") + + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/auth-type: "basicAuth" + name: ingress-v1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /ip + pathType: Exact + backend: + service: + name: %s + port: + number: %d +`, backendSvc, backendPort[0]) + err = s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + msg401 := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg401, "Missing authorization in request") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("Authorization", "Basic amFjazEtdXNlcm5hbWU6amFjazEtcGFzc3dvcmQ="). + Expect(). + Status(http.StatusOK) + }) + + ginkgo.It("enable basicAuth in ingress networking/v1beta1", func() { + err := s.ApisixConsumerBasicAuthCreated("jack1", "jack1-username", "jack1-password") + assert.Nil(ginkgo.GinkgoT(), err, "creating keyAuth ApisixConsumer") + + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/auth-type: "basicAuth" + name: ingress-v1beta1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /ip + pathType: Exact + backend: + serviceName: %s + servicePort: %d +`, backendSvc, backendPort[0]) + err = s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + msg401 := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg401, "Missing authorization in request") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("Authorization", "Basic amFjazEtdXNlcm5hbWU6amFjazEtcGFzc3dvcmQ="). + Expect(). + Status(http.StatusOK) + }) + + ginkgo.It("enable basicAuth in ingress networking/v1beta1", func() { + err := s.ApisixConsumerBasicAuthCreated("jack1", "jack1-username", "jack1-password") + assert.Nil(ginkgo.GinkgoT(), err, "creating keyAuth ApisixConsumer") + + // Wait until the ApisixConsumer create event was delivered. + time.Sleep(6 * time.Second) + + backendSvc, backendPort := s.DefaultHTTPBackend() + ing := fmt.Sprintf(` +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: apisix + k8s.apisix.apache.org/auth-type: "basicAuth" + name: ingress-extensions-v1beta1 +spec: + rules: + - host: httpbin.org + http: + paths: + - path: /ip + pathType: Exact + backend: + serviceName: %s + servicePort: %d +`, backendSvc, backendPort[0]) + err = s.CreateResourceFromString(ing) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress") + time.Sleep(5 * time.Second) + + msg401 := s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + Expect(). + Status(http.StatusUnauthorized). + Body(). + Raw() + assert.Contains(ginkgo.GinkgoT(), msg401, "Missing authorization in request") + + _ = s.NewAPISIXClient().GET("/ip"). + WithHeader("Host", "httpbin.org"). + WithHeader("Authorization", "Basic amFjazEtdXNlcm5hbWU6amFjazEtcGFzc3dvcmQ="). + Expect(). + Status(http.StatusOK) + }) +})