Update loginrequest/REST.Create to issue client certificates.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
Matt Moyer 2020-07-27 08:08:39 -05:00
parent 613f324a47
commit 8606cc9662
2 changed files with 103 additions and 33 deletions

View File

@ -118,32 +118,36 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
klog.Warningf("webhook authentication failure: %v", err) klog.Warningf("webhook authentication failure: %v", err)
return failureResponse(), nil return failureResponse(), nil
} }
if !authenticated || authResponse.User == nil || authResponse.User.GetName() == "" {
var out *placeholderapi.LoginRequest return failureResponse(), nil
if authenticated && authResponse.User != nil && authResponse.User.GetName() != "" {
out = successfulResponse(authResponse)
} else {
out = failureResponse()
} }
return out, nil certPEM, keyPEM, err := r.issuer.IssuePEM(
} pkix.Name{
CommonName: authResponse.User.GetName(),
OrganizationalUnit: authResponse.User.GetGroups(),
},
[]string{},
5*time.Minute,
)
if err != nil {
klog.Warningf("failed to issue short lived client certificate: %v", err)
return failureResponse(), nil
}
func successfulResponse(authResponse *authenticator.Response) *placeholderapi.LoginRequest {
return &placeholderapi.LoginRequest{ return &placeholderapi.LoginRequest{
Status: placeholderapi.LoginRequestStatus{ Status: placeholderapi.LoginRequestStatus{
Credential: &placeholderapi.LoginRequestCredential{ Credential: &placeholderapi.LoginRequestCredential{
ExpirationTimestamp: nil, ExpirationTimestamp: nil,
Token: "snorlax", ClientCertificateData: string(certPEM),
ClientCertificateData: "", ClientKeyData: string(keyPEM),
ClientKeyData: "",
}, },
User: &placeholderapi.User{ User: &placeholderapi.User{
Name: authResponse.User.GetName(), Name: authResponse.User.GetName(),
Groups: authResponse.User.GetGroups(), Groups: authResponse.User.GetGroups(),
}, },
}, },
} }, nil
} }
func failureResponse() *placeholderapi.LoginRequest { func failureResponse() *placeholderapi.LoginRequest {

View File

@ -7,11 +7,13 @@ package loginrequest
import ( import (
"context" "context"
"crypto/x509/pkix"
"errors" "errors"
"fmt" "fmt"
"testing" "testing"
"time" "time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -22,6 +24,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
placeholderapi "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder" placeholderapi "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder"
"github.com/suzerain-io/placeholder-name/internal/mocks/mockcertissuer"
) )
type contextKey struct{} type contextKey struct{}
@ -113,7 +116,18 @@ func requireSuccessfulResponseWithAuthenticationFailureMessage(t *testing.T, err
}) })
} }
func successfulIssuer(ctrl *gomock.Controller) CertIssuer {
issuer := mockcertissuer.NewMockCertIssuer(ctrl)
issuer.EXPECT().
IssuePEM(gomock.Any(), gomock.Any(), gomock.Any()).
Return([]byte("test-cert"), []byte("test-key"), nil)
return issuer
}
func TestCreateSucceedsWhenGivenATokenAndTheWebhookAuthenticatesTheToken(t *testing.T) { func TestCreateSucceedsWhenGivenATokenAndTheWebhookAuthenticatesTheToken(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
webhook := FakeToken{ webhook := FakeToken{
returnResponse: &authenticator.Response{ returnResponse: &authenticator.Response{
User: &user.DefaultInfo{ User: &user.DefaultInfo{
@ -123,7 +137,17 @@ func TestCreateSucceedsWhenGivenATokenAndTheWebhookAuthenticatesTheToken(t *test
}, },
returnUnauthenticated: false, returnUnauthenticated: false,
} }
storage := NewREST(&webhook)
issuer := mockcertissuer.NewMockCertIssuer(ctrl)
issuer.EXPECT().IssuePEM(
pkix.Name{
CommonName: "test-user",
OrganizationalUnit: []string{"test-group-1", "test-group-2"}},
[]string{},
5*time.Minute,
).Return([]byte("test-cert"), []byte("test-key"), nil)
storage := NewREST(&webhook, issuer)
requestToken := "a token" requestToken := "a token"
response, err := callCreate(context.Background(), storage, validLoginRequestWithToken(requestToken)) response, err := callCreate(context.Background(), storage, validLoginRequestWithToken(requestToken))
@ -137,9 +161,8 @@ func TestCreateSucceedsWhenGivenATokenAndTheWebhookAuthenticatesTheToken(t *test
}, },
Credential: &placeholderapi.LoginRequestCredential{ Credential: &placeholderapi.LoginRequestCredential{
ExpirationTimestamp: nil, ExpirationTimestamp: nil,
Token: "snorlax", ClientCertificateData: "test-cert",
ClientCertificateData: "", ClientKeyData: "test-key",
ClientKeyData: "",
}, },
Message: "", Message: "",
}, },
@ -147,11 +170,38 @@ func TestCreateSucceedsWhenGivenATokenAndTheWebhookAuthenticatesTheToken(t *test
require.Equal(t, requestToken, webhook.calledWithToken) require.Equal(t, requestToken, webhook.calledWithToken)
} }
func TestCreateFailsWithValidTokenWhenCertIssuerFails(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
webhook := FakeToken{
returnResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: "test-user",
Groups: []string{"test-group-1", "test-group-2"},
},
},
returnUnauthenticated: false,
}
issuer := mockcertissuer.NewMockCertIssuer(ctrl)
issuer.EXPECT().
IssuePEM(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, nil, fmt.Errorf("some certificate authority error"))
storage := NewREST(&webhook, issuer)
requestToken := "a token"
response, err := callCreate(context.Background(), storage, validLoginRequestWithToken(requestToken))
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
require.Equal(t, requestToken, webhook.calledWithToken)
}
func TestCreateSucceedsWithAnUnauthenticatedStatusWhenGivenATokenAndTheWebhookDoesNotAuthenticateTheToken(t *testing.T) { func TestCreateSucceedsWithAnUnauthenticatedStatusWhenGivenATokenAndTheWebhookDoesNotAuthenticateTheToken(t *testing.T) {
webhook := FakeToken{ webhook := FakeToken{
returnUnauthenticated: true, returnUnauthenticated: true,
} }
storage := NewREST(&webhook) storage := NewREST(&webhook, nil)
requestToken := "a token" requestToken := "a token"
response, err := callCreate(context.Background(), storage, validLoginRequestWithToken(requestToken)) response, err := callCreate(context.Background(), storage, validLoginRequestWithToken(requestToken))
@ -164,7 +214,7 @@ func TestCreateSucceedsWithAnUnauthenticatedStatusWhenWebhookFails(t *testing.T)
webhook := FakeToken{ webhook := FakeToken{
returnErr: errors.New("some webhook error"), returnErr: errors.New("some webhook error"),
} }
storage := NewREST(&webhook) storage := NewREST(&webhook, nil)
response, err := callCreate(context.Background(), storage, validLoginRequest()) response, err := callCreate(context.Background(), storage, validLoginRequest())
@ -175,7 +225,7 @@ func TestCreateSucceedsWithAnUnauthenticatedStatusWhenWebhookDoesNotReturnAnyUse
webhook := FakeToken{ webhook := FakeToken{
returnResponse: &authenticator.Response{}, returnResponse: &authenticator.Response{},
} }
storage := NewREST(&webhook) storage := NewREST(&webhook, nil)
response, err := callCreate(context.Background(), storage, validLoginRequest()) response, err := callCreate(context.Background(), storage, validLoginRequest())
@ -190,7 +240,7 @@ func TestCreateSucceedsWithAnUnauthenticatedStatusWhenWebhookReturnsAnEmptyUsern
}, },
}, },
} }
storage := NewREST(&webhook) storage := NewREST(&webhook, nil)
response, err := callCreate(context.Background(), storage, validLoginRequest()) response, err := callCreate(context.Background(), storage, validLoginRequest())
@ -198,10 +248,13 @@ func TestCreateSucceedsWithAnUnauthenticatedStatusWhenWebhookReturnsAnEmptyUsern
} }
func TestCreateDoesNotPassAdditionalContextInfoToTheWebhook(t *testing.T) { func TestCreateDoesNotPassAdditionalContextInfoToTheWebhook(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
webhook := FakeToken{ webhook := FakeToken{
returnResponse: webhookSuccessResponse(), returnResponse: webhookSuccessResponse(),
} }
storage := NewREST(&webhook) storage := NewREST(&webhook, successfulIssuer(ctrl))
ctx := context.WithValue(context.Background(), contextKey{}, "context-value") ctx := context.WithValue(context.Background(), contextKey{}, "context-value")
_, err := callCreate(ctx, storage, validLoginRequest()) _, err := callCreate(ctx, storage, validLoginRequest())
@ -212,7 +265,7 @@ func TestCreateDoesNotPassAdditionalContextInfoToTheWebhook(t *testing.T) {
func TestCreateFailsWhenGivenTheWrongInputType(t *testing.T) { func TestCreateFailsWhenGivenTheWrongInputType(t *testing.T) {
notALoginRequest := runtime.Unknown{} notALoginRequest := runtime.Unknown{}
response, err := NewREST(&FakeToken{}).Create( response, err := NewREST(&FakeToken{}, nil).Create(
genericapirequest.NewContext(), genericapirequest.NewContext(),
&notALoginRequest, &notALoginRequest,
rest.ValidateAllObjectFunc, rest.ValidateAllObjectFunc,
@ -222,7 +275,7 @@ func TestCreateFailsWhenGivenTheWrongInputType(t *testing.T) {
} }
func TestCreateFailsWhenTokenIsNilInRequest(t *testing.T) { func TestCreateFailsWhenTokenIsNilInRequest(t *testing.T) {
storage := NewREST(&FakeToken{}) storage := NewREST(&FakeToken{}, nil)
response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{ response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{
Type: placeholderapi.TokenLoginCredentialType, Type: placeholderapi.TokenLoginCredentialType,
Token: nil, Token: nil,
@ -233,7 +286,7 @@ func TestCreateFailsWhenTokenIsNilInRequest(t *testing.T) {
} }
func TestCreateFailsWhenTypeInRequestIsMissing(t *testing.T) { func TestCreateFailsWhenTypeInRequestIsMissing(t *testing.T) {
storage := NewREST(&FakeToken{}) storage := NewREST(&FakeToken{}, nil)
response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{ response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{
Type: "", Type: "",
Token: &placeholderapi.LoginRequestTokenCredential{Value: "a token"}, Token: &placeholderapi.LoginRequestTokenCredential{Value: "a token"},
@ -244,7 +297,7 @@ func TestCreateFailsWhenTypeInRequestIsMissing(t *testing.T) {
} }
func TestCreateFailsWhenTypeInRequestIsNotLegal(t *testing.T) { func TestCreateFailsWhenTypeInRequestIsNotLegal(t *testing.T) {
storage := NewREST(&FakeToken{}) storage := NewREST(&FakeToken{}, nil)
response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{ response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{
Type: "this in an invalid type", Type: "this in an invalid type",
Token: &placeholderapi.LoginRequestTokenCredential{Value: "a token"}, Token: &placeholderapi.LoginRequestTokenCredential{Value: "a token"},
@ -255,7 +308,7 @@ func TestCreateFailsWhenTypeInRequestIsNotLegal(t *testing.T) {
} }
func TestCreateFailsWhenTokenValueIsEmptyInRequest(t *testing.T) { func TestCreateFailsWhenTokenValueIsEmptyInRequest(t *testing.T) {
storage := NewREST(&FakeToken{}) storage := NewREST(&FakeToken{}, nil)
response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{ response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{
Type: placeholderapi.TokenLoginCredentialType, Type: placeholderapi.TokenLoginCredentialType,
Token: &placeholderapi.LoginRequestTokenCredential{Value: ""}, Token: &placeholderapi.LoginRequestTokenCredential{Value: ""},
@ -266,7 +319,7 @@ func TestCreateFailsWhenTokenValueIsEmptyInRequest(t *testing.T) {
} }
func TestCreateFailsWhenValidationFails(t *testing.T) { func TestCreateFailsWhenValidationFails(t *testing.T) {
storage := NewREST(&FakeToken{}) storage := NewREST(&FakeToken{}, nil)
response, err := storage.Create( response, err := storage.Create(
context.Background(), context.Background(),
validLoginRequest(), validLoginRequest(),
@ -279,10 +332,13 @@ func TestCreateFailsWhenValidationFails(t *testing.T) {
} }
func TestCreateDoesNotAllowValidationFunctionToMutateRequest(t *testing.T) { func TestCreateDoesNotAllowValidationFunctionToMutateRequest(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
webhook := FakeToken{ webhook := FakeToken{
returnResponse: webhookSuccessResponse(), returnResponse: webhookSuccessResponse(),
} }
storage := NewREST(&webhook) storage := NewREST(&webhook, successfulIssuer(ctrl))
requestToken := "a token" requestToken := "a token"
response, err := storage.Create( response, err := storage.Create(
context.Background(), context.Background(),
@ -299,10 +355,14 @@ func TestCreateDoesNotAllowValidationFunctionToMutateRequest(t *testing.T) {
} }
func TestCreateDoesNotAllowValidationFunctionToSeeTheActualRequestToken(t *testing.T) { func TestCreateDoesNotAllowValidationFunctionToSeeTheActualRequestToken(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
webhook := FakeToken{ webhook := FakeToken{
returnResponse: webhookSuccessResponse(), returnResponse: webhookSuccessResponse(),
} }
storage := NewREST(&webhook)
storage := NewREST(&webhook, successfulIssuer(ctrl))
validationFunctionWasCalled := false validationFunctionWasCalled := false
var validationFunctionSawTokenValue string var validationFunctionSawTokenValue string
response, err := storage.Create( response, err := storage.Create(
@ -322,7 +382,7 @@ func TestCreateDoesNotAllowValidationFunctionToSeeTheActualRequestToken(t *testi
} }
func TestCreateFailsWhenRequestOptionsDryRunIsNotEmpty(t *testing.T) { func TestCreateFailsWhenRequestOptionsDryRunIsNotEmpty(t *testing.T) {
response, err := NewREST(&FakeToken{}).Create( response, err := NewREST(&FakeToken{}, nil).Create(
genericapirequest.NewContext(), genericapirequest.NewContext(),
validLoginRequest(), validLoginRequest(),
rest.ValidateAllObjectFunc, rest.ValidateAllObjectFunc,
@ -335,13 +395,16 @@ func TestCreateFailsWhenRequestOptionsDryRunIsNotEmpty(t *testing.T) {
} }
func TestCreateCancelsTheWebhookInvocationWhenTheCallToCreateIsCancelledItself(t *testing.T) { func TestCreateCancelsTheWebhookInvocationWhenTheCallToCreateIsCancelledItself(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
webhookStartedRunningNotificationChan := make(chan bool) webhookStartedRunningNotificationChan := make(chan bool)
webhook := FakeToken{ webhook := FakeToken{
timeout: time.Second * 2, timeout: time.Second * 2,
webhookStartedRunningNotificationChan: webhookStartedRunningNotificationChan, webhookStartedRunningNotificationChan: webhookStartedRunningNotificationChan,
returnResponse: webhookSuccessResponse(), returnResponse: webhookSuccessResponse(),
} }
storage := NewREST(&webhook) storage := NewREST(&webhook, successfulIssuer(ctrl))
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
c := make(chan bool) c := make(chan bool)
@ -362,13 +425,16 @@ func TestCreateCancelsTheWebhookInvocationWhenTheCallToCreateIsCancelledItself(t
} }
func TestCreateAllowsTheWebhookInvocationToFinishWhenTheCallToCreateIsNotCancelledItself(t *testing.T) { func TestCreateAllowsTheWebhookInvocationToFinishWhenTheCallToCreateIsNotCancelledItself(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
webhookStartedRunningNotificationChan := make(chan bool) webhookStartedRunningNotificationChan := make(chan bool)
webhook := FakeToken{ webhook := FakeToken{
timeout: 0, timeout: 0,
webhookStartedRunningNotificationChan: webhookStartedRunningNotificationChan, webhookStartedRunningNotificationChan: webhookStartedRunningNotificationChan,
returnResponse: webhookSuccessResponse(), returnResponse: webhookSuccessResponse(),
} }
storage := NewREST(&webhook) storage := NewREST(&webhook, successfulIssuer(ctrl))
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()