Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(admission) implement validation for secret-based credentials #446

Merged
merged 1 commit into from
Oct 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
})
}
}