2022-06-09 13:45:21 -07:00
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package clientsecretrequest provides REST functionality for the CredentialRequest resource.
package clientsecretrequest
import (
"context"
2022-07-14 17:07:59 -04:00
"crypto/rand"
"encoding/hex"
2022-06-09 13:45:21 -07:00
"fmt"
2022-07-14 17:07:59 -04:00
"io"
2022-06-09 13:45:21 -07:00
2022-07-14 17:07:59 -04:00
"golang.org/x/crypto/bcrypt"
2022-06-09 13:45:21 -07:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
2022-06-15 09:38:21 -07:00
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
2022-06-09 13:45:21 -07:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
2022-06-15 09:38:21 -07:00
"k8s.io/apimachinery/pkg/runtime/schema"
2022-07-20 21:39:49 -04:00
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
2022-06-09 13:45:21 -07:00
"k8s.io/apiserver/pkg/registry/rest"
2022-07-15 16:11:10 -04:00
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
2022-06-09 13:45:21 -07:00
"k8s.io/utils/trace"
2022-06-13 14:28:05 -07:00
clientsecretapi "go.pinniped.dev/generated/latest/apis/supervisor/clientsecret"
2022-07-14 17:07:59 -04:00
configv1alpha1clientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1"
"go.pinniped.dev/internal/oidcclientsecretstorage"
2022-06-09 13:45:21 -07:00
)
2022-07-15 11:55:30 -04:00
// cost is a good bcrypt cost for 2022, should take about a second to validate
// this is meant to scale up automatically if bcrypt.DefaultCost increases
// it must be kept private because validation of client secrets cannot rely
// on a cost that changes without some form client secret storage migration
// TODO write a unit test that fails when this changes so that we know if/when it happens
// also write a unit test that fails in 2023 to ask this to be updated to latest recommendation
2022-07-20 15:41:05 -04:00
const cost = 12
2022-07-14 17:07:59 -04:00
2022-07-15 16:11:10 -04:00
func NewREST ( resource schema . GroupResource , secrets corev1client . SecretInterface , clients configv1alpha1clientset . OIDCClientInterface , namespace string ) * REST {
2022-06-15 09:38:21 -07:00
return & REST {
tableConvertor : rest . NewDefaultTableConvertor ( resource ) ,
2022-07-15 16:11:10 -04:00
secretStorage : oidcclientsecretstorage . New ( secrets ) ,
clients : clients ,
namespace : namespace ,
2022-07-14 17:07:59 -04:00
rand : rand . Reader ,
2022-06-15 09:38:21 -07:00
}
2022-06-09 13:45:21 -07:00
}
type REST struct {
2022-06-15 09:38:21 -07:00
tableConvertor rest . TableConvertor
2022-07-14 17:07:59 -04:00
secretStorage * oidcclientsecretstorage . OIDCClientSecretStorage
clients configv1alpha1clientset . OIDCClientInterface
2022-07-20 21:39:49 -04:00
namespace string
2022-07-14 17:07:59 -04:00
rand io . Reader
2022-06-09 13:45:21 -07:00
}
// Assert that our *REST implements all the optional interfaces that we expect it to implement.
var _ interface {
rest . Creater
rest . NamespaceScopedStrategy
rest . Scoper
rest . Storage
2022-06-15 09:38:21 -07:00
rest . CategoriesProvider
rest . Lister
rest . TableConvertor
2022-06-09 13:45:21 -07:00
} = ( * REST ) ( nil )
func ( * REST ) New ( ) runtime . Object {
2022-06-13 14:28:05 -07:00
return & clientsecretapi . OIDCClientSecretRequest { }
2022-06-09 13:45:21 -07:00
}
2022-06-15 09:38:21 -07:00
func ( * REST ) NewList ( ) runtime . Object {
return & clientsecretapi . OIDCClientSecretRequestList { }
}
2022-07-14 17:07:59 -04:00
// support `kubectl get pinniped`
// to make sure all resources are in the pinniped category and
// avoid kubectl errors when kubectl lists you must support the list verb
2022-06-15 09:38:21 -07:00
func ( * REST ) List ( _ context . Context , _ * metainternalversion . ListOptions ) ( runtime . Object , error ) {
return & clientsecretapi . OIDCClientSecretRequestList {
ListMeta : metav1 . ListMeta {
ResourceVersion : "0" , // this resource version means "from the API server cache"
} ,
Items : [ ] clientsecretapi . OIDCClientSecretRequest { } , // avoid sending nil items list
} , nil
}
func ( r * REST ) ConvertToTable ( ctx context . Context , obj runtime . Object , tableOptions runtime . Object ) ( * metav1 . Table , error ) {
return r . tableConvertor . ConvertToTable ( ctx , obj , tableOptions )
}
2022-06-09 13:45:21 -07:00
func ( * REST ) NamespaceScoped ( ) bool {
return true
}
func ( * REST ) Categories ( ) [ ] string {
2022-06-15 09:38:21 -07:00
return [ ] string { "pinniped" }
2022-06-09 13:45:21 -07:00
}
func ( r * REST ) Create ( ctx context . Context , obj runtime . Object , createValidation rest . ValidateObjectFunc , options * metav1 . CreateOptions ) ( runtime . Object , error ) {
t := trace . FromContext ( ctx ) . Nest ( "create" , trace . Field {
Key : "kind" ,
Value : "OIDCClientSecretRequest" ,
} )
defer t . Log ( )
2022-07-20 21:39:49 -04:00
req , err := r . validateRequest ( ctx , obj , createValidation , options , t )
2022-06-09 13:45:21 -07:00
if err != nil {
return nil , err
}
2022-07-20 15:41:05 -04:00
t . Step ( "validateRequest" )
2022-06-09 13:45:21 -07:00
2022-07-14 17:07:59 -04:00
oidcClient , err := r . clients . Get ( ctx , req . Name , metav1 . GetOptions { } )
if err != nil {
return nil , err // TODO obfuscate
}
2022-07-20 15:41:05 -04:00
t . Step ( "clients.Get" )
2022-07-14 17:07:59 -04:00
2022-07-20 16:44:41 -04:00
rv , hashes , err := r . secretStorage . Get ( ctx , oidcClient . UID )
2022-07-14 17:07:59 -04:00
if err != nil {
return nil , err // TODO obfuscate
}
2022-07-20 15:41:05 -04:00
t . Step ( "secretStorage.Get" )
2022-07-14 17:07:59 -04:00
var secret string
if req . Spec . GenerateNewSecret {
secret , err = generateSecret ( r . rand )
if err != nil {
return nil , err // TODO obfuscate
}
2022-07-20 15:41:05 -04:00
t . Step ( "generateSecret" )
2022-07-14 17:07:59 -04:00
hash , err := bcrypt . GenerateFromPassword ( [ ] byte ( secret ) , cost )
if err != nil {
return nil , err // TODO obfuscate
}
2022-07-20 15:41:05 -04:00
t . Step ( "bcrypt.GenerateFromPassword" )
2022-07-14 17:07:59 -04:00
hashes = append ( [ ] string { string ( hash ) } , hashes ... )
}
2022-07-15 11:55:30 -04:00
needsRevoke := req . Spec . RevokeOldSecrets && len ( hashes ) > 0
if needsRevoke {
2022-07-14 17:07:59 -04:00
hashes = [ ] string { hashes [ 0 ] }
}
2022-07-15 11:55:30 -04:00
if req . Spec . GenerateNewSecret || needsRevoke {
2022-07-20 21:58:47 -04:00
// each bcrypt comparison is expensive and we do not want a large list to cause wasted CPU
if len ( hashes ) > 5 {
return nil , apierrors . NewRequestEntityTooLargeError ( fmt . Sprintf ( "OIDCClient %s has too many secrets, spec.revokeOldSecrets must be true" , oidcClient . Name ) )
}
2022-07-20 16:44:41 -04:00
if err := r . secretStorage . Set ( ctx , rv , oidcClient . Name , oidcClient . UID , hashes ) ; err != nil {
return nil , err // TODO obfuscate, also return good errors for cases like when the secret now exists but previously did not
2022-07-15 11:55:30 -04:00
}
2022-07-20 15:41:05 -04:00
t . Step ( "secretStorage.Set" )
2022-07-15 11:55:30 -04:00
}
2022-07-14 17:07:59 -04:00
2022-06-13 14:28:05 -07:00
return & clientsecretapi . OIDCClientSecretRequest {
Status : clientsecretapi . OIDCClientSecretRequestStatus {
2022-07-14 17:07:59 -04:00
GeneratedSecret : secret ,
TotalClientSecrets : len ( hashes ) , // TODO what about validation of hashes??
2022-06-09 13:45:21 -07:00
} ,
} , nil
}
2022-07-20 21:39:49 -04:00
func ( r * REST ) validateRequest (
ctx context . Context ,
obj runtime . Object ,
createValidation rest . ValidateObjectFunc ,
options * metav1 . CreateOptions ,
t * trace . Trace ,
) ( * clientsecretapi . OIDCClientSecretRequest , error ) {
2022-06-13 14:28:05 -07:00
clientSecretRequest , ok := obj . ( * clientsecretapi . OIDCClientSecretRequest )
2022-06-09 13:45:21 -07:00
if ! ok {
traceValidationFailure ( t , "not an OIDCClientSecretRequest" )
return nil , apierrors . NewBadRequest ( fmt . Sprintf ( "not an OIDCClientSecretRequest: %#v" , obj ) )
}
2022-07-20 21:39:49 -04:00
// just a sanity check, not sure how to honor a dry run on a virtual API
if options != nil {
if len ( options . DryRun ) != 0 {
traceValidationFailure ( t , "dryRun not supported" )
errs := field . ErrorList { field . NotSupported ( field . NewPath ( "dryRun" ) , options . DryRun , nil ) }
return nil , apierrors . NewInvalid ( clientsecretapi . Kind ( clientSecretRequest . Kind ) , clientSecretRequest . Name , errs )
}
}
if namespace := genericapirequest . NamespaceValue ( ctx ) ; namespace != r . namespace {
msg := fmt . Sprintf ( "namespace must be %s on OIDCClientSecretRequest, was %s" , r . namespace , namespace )
traceValidationFailure ( t , msg )
return nil , apierrors . NewBadRequest ( msg )
}
if createValidation != nil {
if err := createValidation ( ctx , obj . DeepCopyObject ( ) ) ; err != nil {
traceFailureWithError ( t , "validation webhook" , err )
return nil , err
}
}
2022-06-09 13:45:21 -07:00
return clientSecretRequest , nil
}
func traceValidationFailure ( t * trace . Trace , msg string ) {
t . Step ( "failure" ,
trace . Field { Key : "failureType" , Value : "request validation" } ,
trace . Field { Key : "msg" , Value : msg } ,
)
}
2022-07-14 17:07:59 -04:00
2022-07-20 21:39:49 -04:00
func traceFailureWithError ( t * trace . Trace , failureType string , err error ) {
t . Step ( "failure" ,
trace . Field { Key : "failureType" , Value : failureType } ,
trace . Field { Key : "msg" , Value : err . Error ( ) } ,
)
}
2022-07-14 17:07:59 -04:00
func generateSecret ( rand io . Reader ) ( string , error ) {
var buf [ 32 ] byte
if _ , err := io . ReadFull ( rand , buf [ : ] ) ; err != nil {
return "" , fmt . Errorf ( "could not generate client secret: %w" , err )
}
return hex . EncodeToString ( buf [ : ] ) , nil
}