Skip to content

Commit

Permalink
feat(admission) implement validation for secret-based credentials
Browse files Browse the repository at this point in the history
The validation currently consists of verifying if each and every
required field is provided by the user or not.

In future, the validation can be made smarter to include an Admin API
call to Kong to verify if the creation of such a credential will succeed
or not (see TODO in code).
  • Loading branch information
hbagdi committed Oct 30, 2019
1 parent 5f4d79e commit ea157de
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 0 deletions.
23 changes: 23 additions & 0 deletions internal/admission/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) (
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions internal/admission/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
56 changes: 56 additions & 0 deletions internal/admission/validator.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 == ""
}
91 changes: 91 additions & 0 deletions internal/admission/validator_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit ea157de

Please sign in to comment.