diff --git a/internal/admission/server.go b/internal/admission/server.go index 4c30f2067f..980a6eb549 100644 --- a/internal/admission/server.go +++ b/internal/admission/server.go @@ -9,6 +9,7 @@ import ( configuration "github.com/kong/kubernetes-ingress-controller/internal/apis/configuration/v1" "github.com/pkg/errors" admission "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -78,6 +79,10 @@ var ( Group: configuration.SchemeGroupVersion.Group, Version: configuration.SchemeGroupVersion.Version, Resource: "kongplugins"} + secretGVResource = meta.GroupVersionResource{ + Group: corev1.SchemeGroupVersion.Group, + Version: corev1.SchemeGroupVersion.Version, + Resource: "secrets"} ) func (a Server) handleValidation(request admission.AdmissionRequest) ( @@ -115,6 +120,24 @@ func (a Server) handleValidation(request admission.AdmissionRequest) ( if err != nil { return nil, err } + case secretGVResource: + secret := corev1.Secret{} + deserializer := codecs.UniversalDeserializer() + _, _, err = deserializer.Decode(request.Object.Raw, + nil, &secret) + if err != nil { + return nil, err + } + if _, ok = secret.Data["credType"]; !ok { + // secret does not look like a credential resource in Kong + ok = true + break + } + + ok, message, err = a.Validator.ValidateCredential(secret) + if err != nil { + return nil, err + } default: return nil, errors.Errorf("unknown resource type to validate: %s/%s %s", request.Resource.Group, request.Resource.Version, diff --git a/internal/admission/server_test.go b/internal/admission/server_test.go index c1f83207eb..898cfd25bb 100644 --- a/internal/admission/server_test.go +++ b/internal/admission/server_test.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" admission "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" ) var decoder = codecs.UniversalDeserializer() @@ -30,6 +31,11 @@ func (v KongFakeValidator) ValidatePlugin( return v.Result, v.Message, v.Error } +func (v KongFakeValidator) ValidateCredential( + secret corev1.Secret) (bool, string, error) { + return v.Result, v.Message, v.Error +} + func TestServeHTTPBasic(t *testing.T) { assert := assert.New(t) res := httptest.NewRecorder() diff --git a/internal/admission/validator.go b/internal/admission/validator.go index 9a6e21174c..4a1ec146c5 100644 --- a/internal/admission/validator.go +++ b/internal/admission/validator.go @@ -1,16 +1,20 @@ package admission import ( + "strings" + "github.com/golang/glog" "github.com/hbagdi/go-kong/kong" configuration "github.com/kong/kubernetes-ingress-controller/internal/apis/configuration/v1" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" ) // KongValidator validates Kong entities. type KongValidator interface { ValidateConsumer(consumer configuration.KongConsumer) (bool, string, error) ValidatePlugin(consumer configuration.KongPlugin) (bool, string, error) + ValidateCredential(secret corev1.Secret) (bool, string, error) } // KongHTTPValidator implements KongValidator interface to validate Kong @@ -83,6 +87,58 @@ func (validator KongHTTPValidator) ValidatePlugin( return true, "", nil } +var ( + // TODO dynamically fetch these from Kong + credTypeToFields = map[string][]string{ + "key-auth": {"key"}, + "basic-auth": {"username", "password"}, + "hmac-auth": {"username", "secret"}, + "oauth2": {"name", "client_id", "client_secret", "redirect_uris"}, + "jwt": {"algorithm", "rsa_public_key", "key", "secret"}, + "acl": {"group"}, + } +) + +// ValidateCredential checks if the secret contains a credential meant to +// be installed in Kong. If so, then it verifies if all the required fields +// are present in it or not. If valid, it returns true with an empty string, +// else it returns false with the error messsage. If an error happens during +// validation, error is returned. +func (validator KongHTTPValidator) ValidateCredential( + secret corev1.Secret) (bool, string, error) { + + credTypeBytes, ok := secret.Data["credType"] + if !ok { + // doesn't look like a credential resource + return true, "", nil + } + credType := string(credTypeBytes) + + fields, ok := credTypeToFields[credType] + if !ok { + return false, "invalid credential type: " + credType, nil + } + + var missingFields []string + for _, field := range fields { + if _, ok := secret.Data[field]; !ok { + missingFields = append(missingFields, field) + } + } + if len(missingFields) != 0 { + return false, "missing required field(s): " + + strings.Join(missingFields, ", "), nil + } + + // TODO add unique key violation detection + // For each credential, there is a unique column, like key for key-auth, + // username for basic-auth; make an API call to Kong's Admin API + // and verify if there will be a violation, similar to how it's done + // for KongConsumer; return error if the resource is already present in + // Kong. + return true, "", nil +} + func empty(s *string) bool { return s == nil && *s == "" } diff --git a/internal/admission/validator_test.go b/internal/admission/validator_test.go new file mode 100644 index 0000000000..5e8b947b85 --- /dev/null +++ b/internal/admission/validator_test.go @@ -0,0 +1,91 @@ +package admission + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestKongHTTPValidator_ValidateCredential(t *testing.T) { + type args struct { + secret corev1.Secret + } + tests := []struct { + name string + args args + wantOK bool + wantMessage string + wantErr bool + }{ + { + name: "valid key-auth credential", + args: args{ + secret: corev1.Secret{ + Data: map[string][]byte{ + "key": []byte("foo"), + "credType": []byte("key-auth"), + }, + }, + }, + wantOK: true, + wantMessage: "", + wantErr: false, + }, + { + name: "invalid key-auth credential", + args: args{ + secret: corev1.Secret{ + Data: map[string][]byte{ + "key-wrong": []byte("foo"), + "credType": []byte("key-auth"), + }, + }, + }, + wantOK: false, + wantMessage: "missing required field(s): key", + wantErr: false, + }, + { + name: "invalid credential type", + args: args{ + secret: corev1.Secret{ + Data: map[string][]byte{ + "credType": []byte("foo"), + }, + }, + }, + wantOK: false, + wantMessage: "invalid credential type: foo", + wantErr: false, + }, + { + name: "non-kong secrets are passed", + args: args{ + secret: corev1.Secret{ + Data: map[string][]byte{ + "key": []byte("foo"), + }, + }, + }, + wantOK: true, + wantMessage: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validator := KongHTTPValidator{} + got, got1, err := validator.ValidateCredential(tt.args.secret) + if (err != nil) != tt.wantErr { + t.Errorf("KongHTTPValidator.ValidateCredential() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.wantOK { + t.Errorf("KongHTTPValidator.ValidateCredential() got = %v, want %v", got, tt.wantOK) + } + if got1 != tt.wantMessage { + t.Errorf("KongHTTPValidator.ValidateCredential() got1 = %v, want %v", got1, tt.wantMessage) + } + }) + } +}