Allowing validators to have multiple admits

Changes the validator interface to allow a given validator to
use multiple admit functions
This commit is contained in:
Michael Bolot 2023-03-28 09:12:24 -05:00
parent 6fea515099
commit 572d5cc9bc
24 changed files with 822 additions and 170 deletions

View File

@ -44,10 +44,12 @@ type WebhookHandler interface {
// Operations returns list of operations that this WebhookHandler supports.
// Handlers will only be sent request with operations that are contained in the provided list.
Operations() []v1.OperationType
}
// Admit handles the webhook admission request sent to this webhook.
// The response returned by the WebhookHandler will be forwarded to the kube-api server.
// If the WebhookHandler can not accurately evaluate the request it should return an error.
// Admitter handles webhook admission requests sent to this webhook.
// The response returned by the WebhookHandler will be forwarded to the kube-api server.
// If the WebhookHandler can not accurately evaluate the request it should return an error.
type Admitter interface {
Admit(*Request) (*admissionv1.AdmissionResponse, error)
}
@ -61,11 +63,17 @@ type ValidatingAdmissionHandler interface {
// A default configuration can be made using NewDefaultValidatingWebhook(...)
// Most Webhooks implementing ValidatingWebhook will only return one configuration.
ValidatingWebhook(clientConfig v1.WebhookClientConfig) []v1.ValidatingWebhook
// Admitters returns the admitters that this handler will call when evaluating a resource. If any one of these
// fails or encounters an error, the failure/error is immediately returned and the rest are short-circuted.
Admitters() []Admitter
}
// MutatingAdmissionHandler is a handler used for creating a MutatingAdmission Webhook.
type MutatingAdmissionHandler interface {
WebhookHandler
// Since mutators can change a resource, each MutatingAdmissionHandler can only use 1 admit function.
Admitter
// MutatingWebhook returns a list of configurations to route to this handler.
//
@ -170,60 +178,117 @@ func SubPath(gvr schema.GroupVersionResource) string {
return gvr.GroupResource().String()
}
// NewHandlerFunc returns a new HandlerFunc that will call the WebhookHandler's admit function.
func NewHandlerFunc(handler WebhookHandler) http.HandlerFunc {
// NewValidatingHandlerFunc returns a new HandlerFunc that will call the functions returned by the ValidatingAdmissionHandler's AdmitFuncs() call.
// If it encounters a failure or an error, it short-circuts and returns immediately.
func NewValidatingHandlerFunc(handler ValidatingAdmissionHandler) http.HandlerFunc {
return func(responseWriter http.ResponseWriter, req *http.Request) {
review := &admissionv1.AdmissionReview{}
err := json.NewDecoder(req.Body).Decode(review)
review, webReq, err := getReviewAndRequestForHandler(req, handler)
if err != nil {
sendError(responseWriter, review, err)
return
}
if review.Request == nil {
sendError(responseWriter, review, fmt.Errorf("request is not set: %w", ErrInvalidRequest))
return
// save the response from the loop so we can return on success
var response *admissionv1.AdmissionResponse
for _, admitter := range handler.Admitters() {
if admitter == nil {
continue
}
response, err = admitter.Admit(webReq)
if response == nil {
response = &admissionv1.AdmissionResponse{}
}
logrus.Debugf("admit result: %s %s %s user=%s allowed=%v err=%v", webReq.Operation, webReq.Kind.String(), resourceString(webReq.Namespace, webReq.Name), webReq.UserInfo.Username, response.Allowed, err)
// if we get an error or are not allowed, short circuit the admits
if err != nil {
review.Response = response
sendError(responseWriter, review, err)
return
}
if !response.Allowed {
sendResponse(responseWriter, review, response)
return
}
}
webReq := &Request{
AdmissionRequest: *review.Request,
Context: req.Context(),
}
// validate that this handler can handle the provided operation.
if !canHandleOperation(handler, review.Request.Operation) {
sendError(responseWriter, review, fmt.Errorf("can not handle '%s' for '%s': %w", review.Request.Operation, SubPath(handler.GVR()), ErrUnsupportedOperation))
return
}
review.Response, err = handler.Admit(webReq)
if review.Response == nil {
review.Response = &admissionv1.AdmissionResponse{}
}
logrus.Debugf("admit result: %s %s %s user=%s allowed=%v err=%v", webReq.Operation, webReq.Kind.String(), resourceString(webReq.Namespace, webReq.Name), webReq.UserInfo.Username, review.Response.Allowed, err)
if err != nil {
sendError(responseWriter, review, err)
return
}
review.Response.UID = review.Request.UID
writeResponse(responseWriter, review)
// if we have reached this point, all admits approved
sendResponse(responseWriter, review, response)
}
}
// NewMutatingHandlerFunc returns a new HandlerFunc that will call the function returned by the MutatingAdmissionHandler's AdmitFunc() call.
func NewMutatingHandlerFunc(handler MutatingAdmissionHandler) http.HandlerFunc {
return func(responseWriter http.ResponseWriter, req *http.Request) {
review, webReq, err := getReviewAndRequestForHandler(req, handler)
if err != nil {
// review could not be valid, so initialize some safe defaults
sendError(responseWriter, review, err)
return
}
response, err := handler.Admit(webReq)
if response == nil {
response = &admissionv1.AdmissionResponse{}
}
logrus.Debugf("admit result: %s %s %s user=%s allowed=%v err=%v", webReq.Operation, webReq.Kind.String(), resourceString(webReq.Namespace, webReq.Name), webReq.UserInfo.Username, response.Allowed, err)
if err != nil {
review.Response = response
sendError(responseWriter, review, err)
return
}
sendResponse(responseWriter, review, response)
}
}
// getReviewAndRequestForHandler produces a admission.AdmissionReview and a Request for a given http request and handler.
// Returns an error if this handler can't handle this request or if the http.Request couldn't be decoded into an admissionReview.
func getReviewAndRequestForHandler(req *http.Request, handler WebhookHandler) (*admissionv1.AdmissionReview, *Request, error) {
review := admissionv1.AdmissionReview{}
err := json.NewDecoder(req.Body).Decode(&review)
if err != nil {
return nil, nil, err
}
if review.Request == nil {
return &review, nil, fmt.Errorf("request is not set: %w", ErrInvalidRequest)
}
webReq := &Request{
AdmissionRequest: *review.Request,
Context: req.Context(),
}
// validate that this handler can handle the provided operation
if !canHandleOperation(handler, review.Request.Operation) {
return &review, nil, fmt.Errorf("can not handle '%s' for '%s': %w", review.Request.Operation, SubPath(handler.GVR()), ErrUnsupportedOperation)
}
return &review, webReq, nil
}
// Ptr is a generic function that returns the pointer of a string.
func Ptr[T ~string](str T) *T {
newStr := str
return &newStr
}
func sendResponse(responseWriter http.ResponseWriter, review *admissionv1.AdmissionReview, response *admissionv1.AdmissionResponse) {
review.Response = response
review.Response.UID = review.Request.UID
writeResponse(responseWriter, review)
}
func sendError(responseWriter http.ResponseWriter, review *admissionv1.AdmissionReview, err error) {
logrus.Error(err)
if review == nil || review.Request == nil {
http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
return
}
if review.Response == nil {
review.Response = &admissionv1.AdmissionResponse{}
}
// set the response to 500 so that k8s knows that the request got an error. If we just set the Result status the
// failure policy won't apply
responseWriter.WriteHeader(http.StatusInternalServerError)
review.Response.UID = review.Request.UID
review.Response.Result = &errors.NewInternalError(err).ErrStatus
review.Response.Result.Code = http.StatusInternalServerError
writeResponse(responseWriter, review)
}

View File

@ -0,0 +1,415 @@
package admission_test
import (
"encoding/json"
"fmt"
"net/http/httptest"
"strings"
"testing"
"github.com/rancher/webhook/pkg/admission"
"github.com/stretchr/testify/assert"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/admissionregistration/v1"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
)
type handlerResponse struct {
hasAllow bool
hasError bool
}
type reviewResponse struct {
wantReviewAllow bool
wantReviewError bool
}
func TestNewValidatingHandlerFunc(t *testing.T) {
tests := []struct {
name string
operationMatchesHandler bool
firstHandlerResponse *handlerResponse
secondHandlerResponse *handlerResponse
hasDecodeError bool
hasMissingRequest bool
wantHTTPError bool
wantResponse *reviewResponse
}{
{
name: "handler matches, both allow",
operationMatchesHandler: true,
firstHandlerResponse: &handlerResponse{
hasAllow: true,
},
secondHandlerResponse: &handlerResponse{
hasAllow: true,
},
wantResponse: &reviewResponse{
wantReviewAllow: true,
},
},
{
name: "handler matches, first denies, second allows",
operationMatchesHandler: true,
firstHandlerResponse: &handlerResponse{
hasAllow: false,
},
secondHandlerResponse: &handlerResponse{
hasAllow: true,
},
wantResponse: &reviewResponse{
wantReviewAllow: false,
},
},
{
name: "handler matches, first allows, second denies",
operationMatchesHandler: true,
firstHandlerResponse: &handlerResponse{
hasAllow: true,
},
secondHandlerResponse: &handlerResponse{
hasAllow: false,
},
wantResponse: &reviewResponse{
wantReviewAllow: false,
},
},
{
name: "handler matches, both deny",
operationMatchesHandler: true,
firstHandlerResponse: &handlerResponse{
hasAllow: false,
},
secondHandlerResponse: &handlerResponse{
hasAllow: false,
},
wantResponse: &reviewResponse{
wantReviewAllow: false,
},
},
{
name: "handler matches, first error",
operationMatchesHandler: true,
firstHandlerResponse: &handlerResponse{
hasError: true,
},
wantHTTPError: true,
wantResponse: &reviewResponse{
wantReviewAllow: false,
wantReviewError: true,
},
},
{
name: "handler matches, first allow, second error",
operationMatchesHandler: true,
firstHandlerResponse: &handlerResponse{
hasAllow: true,
},
secondHandlerResponse: &handlerResponse{
hasError: true,
},
wantHTTPError: true,
wantResponse: &reviewResponse{
wantReviewAllow: false,
wantReviewError: true,
},
},
{
name: "handler doesn't match",
operationMatchesHandler: false,
wantHTTPError: true,
},
{
name: "decode error",
hasDecodeError: true,
wantHTTPError: true,
},
{
name: "missing request",
hasDecodeError: true,
wantHTTPError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
firstAdmitter := setupAdmitter(test.firstHandlerResponse)
secondAdmitter := setupAdmitter(test.secondHandlerResponse)
handler := fakeValidatingAdmissionHandler{
gvr: schema.GroupVersionResource{
Group: "test.cattle.io",
Version: "v1alpha1",
Resource: "resources",
},
operations: []v1.OperationType{
v1.Create,
},
admitters: []fakeAdmitter{firstAdmitter, secondAdmitter},
}
var bodyBytes []byte
var err error
if test.hasMissingRequest {
review := admissionv1.AdmissionReview{}
bodyBytes, err = json.Marshal(review)
assert.NoError(t, err)
} else if test.hasDecodeError {
data := map[string]any{
"request": "value",
}
bodyBytes, err = json.Marshal(data)
assert.NoError(t, err)
} else {
review := admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Operation: admissionv1.Delete,
Kind: metav1.GroupVersionKind{
Group: "test.cattle.io",
Version: "v1alpha1",
Kind: "Resource",
},
Namespace: "test-ns",
Name: "test",
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
},
UID: "1",
},
}
if test.operationMatchesHandler {
review.Request.Operation = admissionv1.Create
}
bodyBytes, err = json.Marshal(review)
assert.NoError(t, err)
}
body := strings.NewReader(string(bodyBytes))
request := httptest.NewRequest("get", "/testEndpoint", body)
response := httptest.NewRecorder()
handlerFunc := admission.NewValidatingHandlerFunc(&handler)
handlerFunc(response, request)
if test.wantHTTPError {
assert.Greater(t, response.Code, 399, "expected an error code of 400 or higher")
}
if test.wantResponse != nil {
review := admissionv1.AdmissionReview{}
err := json.NewDecoder(response.Result().Body).Decode(&review)
assert.NoError(t, err)
assert.Equal(t, types.UID("1"), review.Response.UID)
assert.Equal(t, test.wantResponse.wantReviewAllow, review.Response.Allowed)
if test.wantResponse.wantReviewError {
assert.Greater(t, int(review.Response.Result.Code), 399, "expected an error code of 400 or higher")
}
}
})
}
}
func TestNewMutatingHandlerFunc(t *testing.T) {
tests := []struct {
name string
operationMatchesHandler bool
handlerResponse *handlerResponse
hasDecodeError bool
hasMissingRequest bool
wantHTTPError bool
wantReviewAllow bool
wantResponse *reviewResponse
}{
{
name: "handler matches and allows",
operationMatchesHandler: true,
handlerResponse: &handlerResponse{
hasAllow: true,
},
wantResponse: &reviewResponse{
wantReviewAllow: true,
},
},
{
name: "handler matches and denies",
operationMatchesHandler: true,
handlerResponse: &handlerResponse{
hasAllow: false,
},
wantResponse: &reviewResponse{
wantReviewAllow: false,
},
},
{
name: "handler does not match",
operationMatchesHandler: false,
wantResponse: &reviewResponse{
wantReviewAllow: false,
},
},
{
name: "handler matches but gets an error",
operationMatchesHandler: true,
handlerResponse: &handlerResponse{
hasError: true,
},
wantHTTPError: true,
wantResponse: &reviewResponse{
wantReviewAllow: false,
wantReviewError: true,
},
},
{
name: "decode error",
hasDecodeError: true,
wantHTTPError: true,
},
{
name: "missing request",
hasMissingRequest: true,
wantHTTPError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
admitter := setupAdmitter(test.handlerResponse)
handler := fakeMutatingAdmissionHandler{
gvr: schema.GroupVersionResource{
Group: "test.cattle.io",
Version: "v1alpha1",
Resource: "resources",
},
operations: []v1.OperationType{
v1.Create,
},
admitter: admitter,
}
var bodyBytes []byte
var err error
if test.hasMissingRequest {
review := admissionv1.AdmissionReview{}
bodyBytes, err = json.Marshal(review)
assert.NoError(t, err)
} else if test.hasDecodeError {
data := map[string]any{
"request": "value",
}
bodyBytes, err = json.Marshal(data)
assert.NoError(t, err)
} else {
review := admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Operation: admissionv1.Delete,
Kind: metav1.GroupVersionKind{
Group: "test.cattle.io",
Version: "v1alpha1",
Kind: "Resource",
},
Namespace: "test-ns",
Name: "test",
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
},
UID: "1",
},
}
if test.operationMatchesHandler {
review.Request.Operation = admissionv1.Create
}
bodyBytes, err = json.Marshal(review)
assert.NoError(t, err)
}
body := strings.NewReader(string(bodyBytes))
request := httptest.NewRequest("get", "/testEndpoint", body)
response := httptest.NewRecorder()
handlerFunc := admission.NewMutatingHandlerFunc(&handler)
handlerFunc(response, request)
if test.wantHTTPError {
assert.Greater(t, response.Code, 399, "expected an error code of 400 or higher")
}
if test.wantResponse != nil {
review := admissionv1.AdmissionReview{}
err := json.NewDecoder(response.Result().Body).Decode(&review)
assert.NoError(t, err)
assert.Equal(t, types.UID("1"), review.Response.UID)
assert.Equal(t, test.wantResponse.wantReviewAllow, review.Response.Allowed)
if test.wantResponse.wantReviewError {
assert.Greater(t, int(review.Response.Result.Code), 399, "expected an error code of 400 or higher")
}
}
})
}
}
func setupAdmitter(response *handlerResponse) fakeAdmitter {
admitter := fakeAdmitter{}
if response == nil {
return admitter
}
if response.hasError {
admitter.err = fmt.Errorf("handler/admitter error")
}
admitter.response = admissionv1.AdmissionResponse{
Allowed: response.hasAllow,
}
return admitter
}
type fakeValidatingAdmissionHandler struct {
gvr schema.GroupVersionResource
operations []v1.OperationType
admitters []fakeAdmitter
}
func (f *fakeValidatingAdmissionHandler) GVR() schema.GroupVersionResource {
return f.gvr
}
func (f *fakeValidatingAdmissionHandler) Operations() []v1.OperationType {
return f.operations
}
func (f *fakeValidatingAdmissionHandler) ValidatingWebhook(clientConfig v1.WebhookClientConfig) []v1.ValidatingWebhook {
return nil
}
func (f *fakeValidatingAdmissionHandler) Admitters() []admission.Admitter {
var admitters []admission.Admitter
for _, admitter := range f.admitters {
admitter := admitter
admitters = append(admitters, &admitter)
}
return admitters
}
type fakeMutatingAdmissionHandler struct {
gvr schema.GroupVersionResource
operations []v1.OperationType
admitter fakeAdmitter
}
func (f *fakeMutatingAdmissionHandler) GVR() schema.GroupVersionResource {
return f.gvr
}
func (f *fakeMutatingAdmissionHandler) Operations() []v1.OperationType {
return f.operations
}
func (f *fakeMutatingAdmissionHandler) Admit(req *admission.Request) (*admissionv1.AdmissionResponse, error) {
return f.admitter.Admit(req)
}
func (f *fakeMutatingAdmissionHandler) MutatingWebhook(clientConfig v1.WebhookClientConfig) []v1.MutatingWebhook {
return nil
}
type fakeAdmitter struct {
response admissionv1.AdmissionResponse
err error
}
func (f *fakeAdmitter) Admit(req *admission.Request) (*admissionv1.AdmissionResponse, error) {
return &f.response, f.err
}

View File

@ -20,13 +20,15 @@ import (
// Validator validates the namespace admission request.
type Validator struct {
sar authorizationv1.SubjectAccessReviewInterface
admitter admitter
}
// NewValidator returns a new validator used for validation of namespace requests.
func NewValidator(sar authorizationv1.SubjectAccessReviewInterface) *Validator {
return &Validator{
sar: sar,
admitter: admitter{
sar: sar,
},
}
}
@ -84,9 +86,17 @@ func (v *Validator) ValidatingWebhook(clientConfig admv1.WebhookClientConfig) []
return []admv1.ValidatingWebhook{*standardWebhook, *createWebhook, *kubeSystemCreateWebhook}
}
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
sar authorizationv1.SubjectAccessReviewInterface
}
// Admit is the entrypoint for the validator.
// Admit will return an error if it is unable to process the request.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("Namespace Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)
@ -121,7 +131,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
extras[k] = v1.ExtraValue(v)
}
resp, err := v.sar.Create(request.Context, &v1.SubjectAccessReview{
resp, err := a.sar.Create(request.Context, &v1.SubjectAccessReview{
Spec: v1.SubjectAccessReviewSpec{
ResourceAttributes: &v1.ResourceAttributes{
Verb: "updatepsa",

View File

@ -342,7 +342,9 @@ func TestValidator_Admit(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
request := createNamespaceRequest(t, &tt)
resp, err := validator.Admit(request)
admitters := validator.Admitters()
assert.Len(t, admitters, 1)
resp, err := admitters[0].Admit(request)
assert.Equal(t, tt.wantErr, err != nil)
if !tt.wantErr {
assert.Equal(t, tt.allowed, resp.Allowed)

View File

@ -10,15 +10,12 @@ import (
"github.com/sirupsen/logrus"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/trace"
)
var gvr = schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "secrets",
}
var gvr = corev1.SchemeGroupVersion.WithResource("secrets")
// Mutator implements admission.MutatingAdmissionWebhook.
type Mutator struct{}

View File

@ -28,12 +28,11 @@ const (
// Validator implements admission.ValidatingAdmissionWebhook.
type Validator struct {
roleCache v1.RoleCache
roleBindingCache v1.RoleBindingCache
admitter admitter
}
// NewValidator creates a new secret validator which ensures secrets which own rbac objects aren't deleted with options
// to oprhan those RBAC resources
// to orphan those RBAC resources.
func NewValidator(roleCache v1.RoleCache, roleBindingCache v1.RoleBindingCache) *Validator {
roleCache.AddIndexer(roleOwnerIndex, func(obj *rbacv1.Role) ([]string, error) {
return secretOwnerIndexer(obj.ObjectMeta), nil
@ -42,8 +41,10 @@ func NewValidator(roleCache v1.RoleCache, roleBindingCache v1.RoleBindingCache)
return secretOwnerIndexer(obj.ObjectMeta), nil
})
return &Validator{
roleCache: roleCache,
roleBindingCache: roleBindingCache,
admitter: admitter{
roleCache: roleCache,
roleBindingCache: roleBindingCache,
},
}
}
@ -75,8 +76,18 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*validatingWebhook}
}
// Admitters returns the admitter objects used to validate secrets.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
roleCache v1.RoleCache
roleBindingCache v1.RoleBindingCache
}
// Admit is the entrypoint for the validator. Admit will return an error if it is unable to process the request.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
if request.DryRun != nil && *request.DryRun {
return admission.ResponseAllowed(), nil
}
@ -99,7 +110,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
if err != nil {
return nil, fmt.Errorf("unable to read secret from request: %w", err)
}
roles, roleBindings, err := v.getRbacRefs(secret)
roles, roleBindings, err := a.getRbacRefs(secret)
if logrus.IsLevelEnabled(logrus.DebugLevel) {
roleNames := make([]string, len(roles))
roleBindingNames := make([]string, len(roleBindings))
@ -131,12 +142,12 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}
// getRbacRefs checks to see if there are any existing rbac resources which could be orphaned by this delete call
func (v *Validator) getRbacRefs(secret *corev1.Secret) ([]*rbacv1.Role, []*rbacv1.RoleBinding, error) {
roles, err := v.roleCache.GetByIndex(roleOwnerIndex, fmt.Sprintf(ownerFormat, secret.Namespace, secret.Name))
func (a *admitter) getRbacRefs(secret *corev1.Secret) ([]*rbacv1.Role, []*rbacv1.RoleBinding, error) {
roles, err := a.roleCache.GetByIndex(roleOwnerIndex, fmt.Sprintf(ownerFormat, secret.Namespace, secret.Name))
if err != nil {
return nil, nil, err
}
roleBindings, err := v.roleBindingCache.GetByIndex(roleBindingOwnerIndex, fmt.Sprintf(ownerFormat, secret.Namespace, secret.Name))
roleBindings, err := a.roleBindingCache.GetByIndex(roleBindingOwnerIndex, fmt.Sprintf(ownerFormat, secret.Namespace, secret.Name))
if err != nil {
return nil, nil, err
}

View File

@ -255,7 +255,9 @@ func TestAdmit(t *testing.T) {
roleBindingCache.EXPECT().AddIndexer(roleBindingOwnerIndex, gomock.Any())
validator := NewValidator(roleCache, roleBindingCache)
response, err := validator.Admit(&req)
admitters := validator.Admitters()
assert.Len(t, admitters, 1)
response, err := admitters[0].Admit(&req)
if test.wantError {
assert.Error(t, err)
} else {

View File

@ -29,15 +29,16 @@ var parsedRangeLessThan123 = semver.MustParseRange("< 1.23.0-rancher0")
// NewValidator returns a new validator for management clusters.
func NewValidator(sar authorizationv1.SubjectAccessReviewInterface, cache v3.PodSecurityAdmissionConfigurationTemplateCache) *Validator {
return &Validator{
sar: sar,
psact: cache,
admitter: admitter{
sar: sar,
psact: cache,
},
}
}
// Validator ValidatingWebhook for management clusters.
type Validator struct {
sar authorizationv1.SubjectAccessReviewInterface
psact v3.PodSecurityAdmissionConfigurationTemplateCache
admitter admitter
}
// GVR returns the GroupVersionKind for this CRD.
@ -57,9 +58,19 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*valWebhook}
}
// Admitters returns the admitter objects used to validate clusters.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
sar authorizationv1.SubjectAccessReviewInterface
psact v3.PodSecurityAdmissionConfigurationTemplateCache
}
// Admit handles the webhook admission request sent to this webhook.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
response, err := v.validateFleetPermissions(request)
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
response, err := a.validateFleetPermissions(request)
if err != nil {
return nil, fmt.Errorf("failed to validate fleet permissions: %w", err)
}
@ -76,14 +87,14 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
if cluster.Name == "local" || cluster.Spec.RancherKubernetesEngineConfig == nil {
return admission.ResponseAllowed(), nil
}
response, err = v.validatePSACT(request)
response, err = a.validatePSACT(request)
if err != nil {
return nil, fmt.Errorf("failed to validate PodSecurityAdmissionConfigurationTemplate(PSACT): %w", err)
}
if !response.Allowed {
return response, nil
}
response, err = v.validatePSP(request)
response, err = a.validatePSP(request)
if err != nil {
return nil, fmt.Errorf("failed to validate PSP: %w", err)
}
@ -103,7 +114,7 @@ func toExtra(extra map[string]authenticationv1.ExtraValue) map[string]v1.ExtraVa
}
// validateFleetPermissions validates whether the request maker has required permissions around FleetWorkspace.
func (v *Validator) validateFleetPermissions(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) validateFleetPermissions(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
oldCluster, newCluster, err := objectsv3.ClusterOldAndNewFromRequest(&request.AdmissionRequest)
if err != nil {
return nil, fmt.Errorf("failed to get old and new clusters from request: %w", err)
@ -132,7 +143,7 @@ func (v *Validator) validateFleetPermissions(request *admission.Request) (*admis
}, nil
}
resp, err := v.sar.Create(request.Context, &v1.SubjectAccessReview{
resp, err := a.sar.Create(request.Context, &v1.SubjectAccessReview{
Spec: v1.SubjectAccessReviewSpec{
ResourceAttributes: &v1.ResourceAttributes{
Verb: "fleetaddcluster",
@ -166,7 +177,7 @@ func (v *Validator) validateFleetPermissions(request *admission.Request) (*admis
}
// validatePSACT validates the cluster spec when PodSecurityAdmissionConfigurationTemplate is used.
func (v *Validator) validatePSACT(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) validatePSACT(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
oldCluster, newCluster, err := objectsv3.ClusterOldAndNewFromRequest(&request.AdmissionRequest)
if err != nil {
return nil, fmt.Errorf("failed to get old and new clusters from request: %w", err)
@ -181,7 +192,7 @@ func (v *Validator) validatePSACT(request *admission.Request) (*admissionv1.Admi
return admission.ResponseBadRequest("PodSecurityAdmissionConfigurationTemplate(PSACT) is only supported in k8s version 1.23 and above"), nil
}
if newTemplateName != "" {
response, err := v.checkPSAConfigOnCluster(newCluster)
response, err := a.checkPSAConfigOnCluster(newCluster)
if err != nil {
return nil, fmt.Errorf("failed to check the PodSecurity Config in the cluster %s: %w", newCluster.Name, err)
}
@ -216,7 +227,7 @@ func (v *Validator) validatePSACT(request *admission.Request) (*admissionv1.Admi
}
// checkPSAConfigOnCluster validates the cluster spec when DefaultPodSecurityAdmissionConfigurationTemplateName is set.
func (v *Validator) checkPSAConfigOnCluster(cluster *apisv3.Cluster) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) checkPSAConfigOnCluster(cluster *apisv3.Cluster) (*admissionv1.AdmissionResponse, error) {
// validate that extra_args.admission-control-config-file is not set at the same time
_, found := cluster.Spec.RancherKubernetesEngineConfig.Services.KubeAPI.ExtraArgs["admission-control-config-file"]
if found {
@ -225,7 +236,7 @@ func (v *Validator) checkPSAConfigOnCluster(cluster *apisv3.Cluster) (*admission
// validate that the configuration for PodSecurityAdmission under the kube-api.admission_configuration section
// matches the content of the PodSecurityAdmissionConfigurationTemplate specified in the cluster
name := cluster.Spec.DefaultPodSecurityAdmissionConfigurationTemplateName
template, err := v.psact.Get(name)
template, err := a.psact.Get(name)
if err != nil {
if apierrors.IsNotFound(err) {
return admission.ResponseBadRequest(err.Error()), nil
@ -257,7 +268,7 @@ func (v *Validator) checkPSAConfigOnCluster(cluster *apisv3.Cluster) (*admission
}
// validatePSP validates if the PSP feature is enabled in a cluster which version is 1.25 or above.
func (v *Validator) validatePSP(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) validatePSP(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
cluster, err := objectsv3.ClusterFromRequest(&request.AdmissionRequest)
if err != nil {
return nil, fmt.Errorf("failed to get cluster from request: %w", err)

View File

@ -77,7 +77,9 @@ func TestAdmit(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := &Validator{
sar: &mockReviewer{},
admitter: admitter{
sar: &mockReviewer{},
},
}
oldClusterBytes, err := json.Marshal(tt.oldCluster)
@ -85,7 +87,10 @@ func TestAdmit(t *testing.T) {
newClusterBytes, err := json.Marshal(tt.newCluster)
assert.NoError(t, err)
res, err := v.Admit(&admission.Request{
admitters := v.Admitters()
assert.Len(t, admitters, 1)
res, err := admitters[0].Admit(&admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Object: runtime.RawExtension{
Raw: newClusterBytes,

View File

@ -30,15 +30,16 @@ func NewValidator(crtb *resolvers.CRTBRuleResolver, defaultResolver k8validation
roleTemplateResolver *auth.RoleTemplateResolver) *Validator {
resolver := resolvers.NewAggregateRuleResolver(defaultResolver, crtb)
return &Validator{
resolver: resolver,
roleTemplateResolver: roleTemplateResolver,
admitter: admitter{
resolver: resolver,
roleTemplateResolver: roleTemplateResolver,
},
}
}
// Validator conforms to the webhook.Handler interface and is used for validating request for clusteroletemplatebindings.
type Validator struct {
resolver k8validation.AuthorizationRuleResolver
roleTemplateResolver *auth.RoleTemplateResolver
admitter admitter
}
// GVR returns the GroupVersionKind for this CRD.
@ -56,9 +57,19 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.NamespacedScope, v.Operations())}
}
// Admitters returns the admitter objects used to validate clusterRoleTemplateBindings.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
resolver k8validation.AuthorizationRuleResolver
roleTemplateResolver *auth.RoleTemplateResolver
}
// Admit is the entrypoint for the validator. Admit will return an error if it unable to process the request.
// If this function is called without NewValidator(..) calls will panic.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("clusterRoleTemplateBindingValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)
@ -87,7 +98,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}
if request.Operation == admissionv1.Create {
if err = v.validateCreateFields(crtb); err != nil {
if err = a.validateCreateFields(crtb); err != nil {
return &admissionv1.AdmissionResponse{
Result: &metav1.Status{
Status: "Failure",
@ -100,7 +111,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}
}
roleTemplate, err := v.roleTemplateResolver.RoleTemplateCache().Get(crtb.RoleTemplateName)
roleTemplate, err := a.roleTemplateResolver.RoleTemplateCache().Get(crtb.RoleTemplateName)
if err != nil {
if apierrors.IsNotFound(err) {
return &admissionv1.AdmissionResponse{Allowed: true}, nil
@ -108,12 +119,12 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
return nil, fmt.Errorf("failed to get roletemplate '%s': %w", crtb.RoleTemplateName, err)
}
rules, err := v.roleTemplateResolver.RulesFromTemplate(roleTemplate)
rules, err := a.roleTemplateResolver.RulesFromTemplate(roleTemplate)
if err != nil {
return nil, fmt.Errorf("failed to resolve rules from roletemplate '%s': %w", crtb.RoleTemplateName, err)
}
response := &admissionv1.AdmissionResponse{}
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, rules, crtb.ClusterName, v.resolver))
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, rules, crtb.ClusterName, a.resolver))
return response, nil
}
@ -144,7 +155,7 @@ func validateUpdateFields(oldCRTB, newCRTB *apisv3.ClusterRoleTemplateBinding) e
}
// validateCreateFields checks if all required fields are present and valid.
func (v *Validator) validateCreateFields(newCRTB *apisv3.ClusterRoleTemplateBinding) error {
func (a *admitter) validateCreateFields(newCRTB *apisv3.ClusterRoleTemplateBinding) error {
hasUserTarget := newCRTB.UserName != "" || newCRTB.UserPrincipalName != ""
hasGroupTarget := newCRTB.GroupName != "" || newCRTB.GroupPrincipalName != ""
@ -156,7 +167,7 @@ func (v *Validator) validateCreateFields(newCRTB *apisv3.ClusterRoleTemplateBind
return fmt.Errorf("missing required field 'clusterName': %w", admission.ErrInvalidRequest)
}
roleTemplate, err := v.roleTemplateResolver.RoleTemplateCache().Get(newCRTB.RoleTemplateName)
roleTemplate, err := a.roleTemplateResolver.RoleTemplateCache().Get(newCRTB.RoleTemplateName)
if err != nil {
return fmt.Errorf("unknown reference roleTemplate '%s': %w", newCRTB.RoleTemplateName, err)
}

View File

@ -244,7 +244,9 @@ func (c *ClusterRoleTemplateBindingSuite) Test_PrivilegeEscalation() {
test := tests[i]
c.Run(test.name, func() {
req := createCRTBRequest(c.T(), test.args.oldCRTB(), test.args.newCRTB(), test.args.username)
resp, err := validator.Admit(req)
admitters := validator.Admitters()
assert.Len(c.T(), admitters, 1)
resp, err := admitters[0].Admit(req)
c.NoError(err, "Admit failed")
if resp.Allowed != test.allowed {
c.Failf("Response was incorrectly validated", "Wanted response.Allowed = '%v' got %v: result=%+v", test.allowed, resp.Allowed, resp.Result)
@ -544,7 +546,9 @@ func (c *ClusterRoleTemplateBindingSuite) Test_UpdateValidation() {
c.Run(test.name, func() {
c.T().Parallel()
req := createCRTBRequest(c.T(), test.args.oldCRTB(), test.args.newCRTB(), test.args.username)
resp, err := validator.Admit(req)
admitters := validator.Admitters()
assert.Len(c.T(), admitters, 1)
resp, err := admitters[0].Admit(req)
c.NoError(err, "Admit failed")
if resp.Allowed != test.allowed {
c.Failf("Response was incorrectly validated", "Wanted response.Allowed = '%v' got %v: result=%+v", test.allowed, resp.Allowed, resp.Result)
@ -704,7 +708,9 @@ func (c *ClusterRoleTemplateBindingSuite) Test_Create() {
c.Run(test.name, func() {
c.T().Parallel()
req := createCRTBRequest(c.T(), test.args.oldCRTB(), test.args.newCRTB(), test.args.username)
resp, err := validator.Admit(req)
admitters := validator.Admitters()
assert.Len(c.T(), admitters, 1)
resp, err := admitters[0].Admit(req)
c.NoError(err, "Admit failed")
if resp.Allowed != test.allowed {
c.Failf("Response was incorrectly validated", "Wanted response.Allowed = '%v' got %v: result=%+v", test.allowed, resp.Allowed, resp.Result)

View File

@ -20,7 +20,16 @@ var gvr = schema.GroupVersionResource{
}
// Validator for validating features.
type Validator struct{}
type Validator struct {
admitter admitter
}
// NewValidator returns a new validator for features.
func NewValidator() *Validator {
return &Validator{
admitter: admitter{},
}
}
// GVR returns the GroupVersionKind for this CRD.
func (v *Validator) GVR() schema.GroupVersionResource {
@ -39,8 +48,15 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*valWebhook}
}
// Admitters returns the admitter objects used to validate features.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct{}
// Admit handles the webhook admission request sent to this webhook.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("featureValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

View File

@ -23,13 +23,15 @@ var gvr = schema.GroupVersionResource{
// NewValidator returns a new validator used for validation globalRoles.
func NewValidator(resolver validation.AuthorizationRuleResolver) *Validator {
return &Validator{
resolver: resolver,
admitter: admitter{
resolver: resolver,
},
}
}
// Validator implements admission.ValidatingAdmissionHandler
// Validator implements admission.ValidatingAdmissionHandler.
type Validator struct {
resolver validation.AuthorizationRuleResolver
admitter admitter
}
// GVR returns the GroupVersionKind for this CRD.
@ -47,9 +49,18 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.ClusterScope, v.Operations())}
}
// Admitters returns the admitter objects used to validate globalRoles.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
resolver validation.AuthorizationRuleResolver
}
// Admit is the entrypoint for the validator. Admit will return an error if it unable to process the request.
// If this function is called without NewValidator(..) calls will panic.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("globalRoleValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)
@ -82,7 +93,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}
response := &admissionv1.AdmissionResponse{}
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, newGR.Rules, "", v.resolver))
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, newGR.Rules, "", a.resolver))
return response, nil
}

View File

@ -26,15 +26,16 @@ var gvr = schema.GroupVersionResource{
// NewValidator returns a new validator for GlobalRoleBindings.
func NewValidator(grCache v3.GlobalRoleCache, resolver rbacvalidation.AuthorizationRuleResolver) *Validator {
return &Validator{
resolver: resolver,
globalRoles: grCache,
admitter: admitter{
resolver: resolver,
globalRoles: grCache,
},
}
}
// Validator is used to validate operations to GlobalRoleBindings.
type Validator struct {
resolver rbacvalidation.AuthorizationRuleResolver
globalRoles v3.GlobalRoleCache
admitter admitter
}
// GVR returns the GroupVersionKind for this CRD.
@ -52,8 +53,18 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.ClusterScope, v.Operations())}
}
// Admitters returns the admitter objects used to validate globalRoleBindings.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
resolver rbacvalidation.AuthorizationRuleResolver
globalRoles v3.GlobalRoleCache
}
// Admit handles the webhook admission request sent to this webhook.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("globalRoleBindingValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)
@ -63,7 +74,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}
// Pull the global role to get the rules
globalRole, err := v.globalRoles.Get(newGRB.GlobalRoleName)
globalRole, err := a.globalRoles.Get(newGRB.GlobalRoleName)
if err != nil {
if !errors.IsNotFound(err) {
return nil, err
@ -93,7 +104,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}
response := &admissionv1.AdmissionResponse{}
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, globalRole.Rules, "", v.resolver))
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, globalRole.Rules, "", a.resolver))
return response, nil
}

View File

@ -32,8 +32,7 @@ var gvr = schema.GroupVersionResource{
// Validator validates the PodSecurityAdmissionConfigurationTemplate admission request.
type Validator struct {
ManagementClusterCache v3.ClusterCache
provisioningClusterCache v1.ClusterCache
admitter admitter
}
const (
@ -42,15 +41,18 @@ const (
rancherRestrictedPSACTName = "rancher-restricted"
)
// NewValidator returns a validator for PodSecurityAdmissionConfigurationTemplates
// NewValidator returns a validator for PodSecurityAdmissionConfigurationTemplates.
func NewValidator(managementCache v3.ClusterCache, provisioningCache v1.ClusterCache) *Validator {
val := &Validator{
adm := admitter{
ManagementClusterCache: managementCache,
provisioningClusterCache: provisioningCache,
}
val.ManagementClusterCache.AddIndexer(byPodSecurityAdmissionConfigurationName, byPodSecurityAdmissionConfigurationTemplateV3)
val.provisioningClusterCache.AddIndexer(byPodSecurityAdmissionConfigurationName, byPodSecurityAdmissionConfigurationTemplateV1)
return val
adm.ManagementClusterCache.AddIndexer(byPodSecurityAdmissionConfigurationName, byPodSecurityAdmissionConfigurationTemplateV3)
adm.provisioningClusterCache.AddIndexer(byPodSecurityAdmissionConfigurationName, byPodSecurityAdmissionConfigurationTemplateV1)
return &Validator{
admitter: adm,
}
}
// ValidatingWebhook returns the ValidatingWebhook used for this CRD.
@ -84,8 +86,18 @@ func (v *Validator) Operations() []admissionregistrationv1.OperationType {
return []admissionregistrationv1.OperationType{admissionregistrationv1.Update, admissionregistrationv1.Create, admissionregistrationv1.Delete}
}
// Admitters returns the admitter objects used to validate podsecurityadmissionconfigurationtemplate.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
ManagementClusterCache v3.ClusterCache
provisioningClusterCache v1.ClusterCache
}
// Admit handles the webhook admission request sent to this webhook.
func (v *Validator) Admit(req *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(req *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("PodSecurityAdmissionConfigurationTemplate Admit", trace.Field{Key: "user", Value: req.UserInfo.Username})
defer listTrace.LogIfLong(2 * time.Second)
@ -97,7 +109,7 @@ func (v *Validator) Admit(req *admission.Request) (*admissionv1.AdmissionRespons
switch req.Operation {
case admissionv1.Create, admissionv1.Update:
err = v.validateConfiguration(newTemplate)
err = a.validateConfiguration(newTemplate)
if err != nil {
resp.Result = &metav1.Status{
Status: "Failure",
@ -122,7 +134,7 @@ func (v *Validator) Admit(req *admission.Request) (*admissionv1.AdmissionRespons
break
}
clustersUsingTemplate, clusterType, err := v.handleDeletion(oldTemplate)
clustersUsingTemplate, clusterType, err := a.handleDeletion(oldTemplate)
if err != nil {
// error encountered with indexer
resp.Result = &metav1.Status{
@ -159,18 +171,18 @@ func (v *Validator) Admit(req *admission.Request) (*admissionv1.AdmissionRespons
return resp, nil
}
func (v *Validator) handleDeletion(oldTemplate *mgmtv3.PodSecurityAdmissionConfigurationTemplate) (clustersUsingTemplate int, clusterType string, err error) {
func (a *admitter) handleDeletion(oldTemplate *mgmtv3.PodSecurityAdmissionConfigurationTemplate) (clustersUsingTemplate int, clusterType string, err error) {
// we can't allow templates to be deleted if they are being used by active clusters. Depending on the distro,
// the template reference could be stored on the v1.Cluster or v3.Cluster.
mgmtClusters, err := v.ManagementClusterCache.GetByIndex(byPodSecurityAdmissionConfigurationName, oldTemplate.Name)
mgmtClusters, err := a.ManagementClusterCache.GetByIndex(byPodSecurityAdmissionConfigurationName, oldTemplate.Name)
if err != nil {
return 0, "management", fmt.Errorf("error encountered within management cluster indexer: %w", err)
} else if len(mgmtClusters) > 0 {
return len(mgmtClusters), "management", nil
}
provClusters, err := v.provisioningClusterCache.GetByIndex(byPodSecurityAdmissionConfigurationName, oldTemplate.Name)
provClusters, err := a.provisioningClusterCache.GetByIndex(byPodSecurityAdmissionConfigurationName, oldTemplate.Name)
if err != nil {
return 0, "provisioning", fmt.Errorf("error encountered within provisioning cluster indexer: %w", err)
} else if len(provClusters) > 0 {
@ -180,7 +192,7 @@ func (v *Validator) handleDeletion(oldTemplate *mgmtv3.PodSecurityAdmissionConfi
return 0, "", nil
}
func (v *Validator) validateConfiguration(configurationTemplate *mgmtv3.PodSecurityAdmissionConfigurationTemplate) error {
func (a *admitter) validateConfiguration(configurationTemplate *mgmtv3.PodSecurityAdmissionConfigurationTemplate) error {
defaults := configurationTemplate.Configuration.Defaults
// validate any provided defaults

View File

@ -36,8 +36,10 @@ var (
func TestAdmit(t *testing.T) {
validator = Validator{
ManagementClusterCache: mockMgmtCache{},
provisioningClusterCache: mockProvisioningCache{},
admitter: admitter{
ManagementClusterCache: mockMgmtCache{},
provisioningClusterCache: mockProvisioningCache{},
},
}
validConfiguration := v3.PodSecurityAdmissionConfigurationTemplateSpec{
Defaults: v3.PodSecurityAdmissionConfigurationTemplateDefaults{
@ -117,7 +119,6 @@ func TestAdmit(t *testing.T) {
wantAllowed: true,
},
}
validationTests = []validationTest{
{
testName: "Completely Valid Template Test",
@ -549,7 +550,11 @@ func TestAdmit(t *testing.T) {
t.Log(fmt.Errorf("failed to create DELETE request for PodSecurityAdmissionConfigurationTemplate object: %w", err))
t.Fail()
}
resp, _ := validator.Admit(&req)
admitters := validator.Admitters()
if len(admitters) != 1 {
t.Logf("wanted only one admitter but got = %d", len(admitters))
}
resp, _ := admitters[0].Admit(&req)
if resp.Allowed != testcase.wantAllowed {
t.Logf("wanted allowed = %t, got allowed = %t", testcase.wantAllowed, resp.Allowed)
t.Fail()
@ -564,7 +569,11 @@ func TestAdmit(t *testing.T) {
t.Log(fmt.Errorf("failed to create CREATE request for PodSecurityAdmissionConfigurationTemplate object: %w", err))
t.Fail()
}
resp, _ := validator.Admit(&req)
admitters := validator.Admitters()
if len(admitters) != 1 {
t.Logf("wanted only one admitter but got = %d", len(admitters))
}
resp, _ := admitters[0].Admit(&req)
if resp.Allowed != testcase.wantAllowed {
t.Logf("wanted allowed = %t, got allowed = %t", testcase.wantAllowed, resp.Allowed)
t.Fail()

View File

@ -32,17 +32,17 @@ func NewValidator(prtb *resolvers.PRTBRuleResolver, crtb *resolvers.CRTBRuleReso
clusterResolver := resolvers.NewAggregateRuleResolver(defaultResolver, crtb)
projectResolver := resolvers.NewAggregateRuleResolver(defaultResolver, prtb)
return &Validator{
clusterResolver: clusterResolver,
projectResolver: projectResolver,
roleTemplateResolver: roleTemplateResolver,
admitter: admitter{
clusterResolver: clusterResolver,
projectResolver: projectResolver,
roleTemplateResolver: roleTemplateResolver,
},
}
}
// Validator validates PRTB admission request.
type Validator struct {
clusterResolver k8validation.AuthorizationRuleResolver
projectResolver k8validation.AuthorizationRuleResolver
roleTemplateResolver *auth.RoleTemplateResolver
admitter admitter
}
// GVR returns the GroupVersionKind for this CRD.
@ -60,9 +60,20 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.NamespacedScope, v.Operations())}
}
// Admitters returns the admitter objects used to validate ProjectRoleTemplateBindings.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
clusterResolver k8validation.AuthorizationRuleResolver
projectResolver k8validation.AuthorizationRuleResolver
roleTemplateResolver *auth.RoleTemplateResolver
}
// Admit is the entrypoint for the validator. Admit will return an error if it unable to process the request.
// If this function is called without NewValidator(..) calls will panic.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("projectRoleTemplateBindingValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)
@ -91,7 +102,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}
if request.Operation == admissionv1.Create {
if err = v.validateCreateFields(prtb); err != nil {
if err = a.validateCreateFields(prtb); err != nil {
return &admissionv1.AdmissionResponse{
Result: &metav1.Status{
Status: "Failure",
@ -106,7 +117,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
clusterNS, projectNS := clusterFromProject(prtb.ProjectName)
roleTemplate, err := v.roleTemplateResolver.RoleTemplateCache().Get(prtb.RoleTemplateName)
roleTemplate, err := a.roleTemplateResolver.RoleTemplateCache().Get(prtb.RoleTemplateName)
if err != nil {
if apierrors.IsNotFound(err) {
return &admissionv1.AdmissionResponse{
@ -116,18 +127,18 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
return nil, fmt.Errorf("failed to get referenced roleTemplate '%s' for PRTB: %w", roleTemplate.Name, err)
}
rules, err := v.roleTemplateResolver.RulesFromTemplate(roleTemplate)
rules, err := a.roleTemplateResolver.RulesFromTemplate(roleTemplate)
if err != nil {
return nil, fmt.Errorf("failed to get rules from referenced roleTemplate '%s': %w", roleTemplate.Name, err)
}
err = auth.ConfirmNoEscalation(request, rules, clusterNS, v.clusterResolver)
err = auth.ConfirmNoEscalation(request, rules, clusterNS, a.clusterResolver)
if err == nil {
return &admissionv1.AdmissionResponse{Allowed: true}, nil
}
response := &admissionv1.AdmissionResponse{}
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, rules, projectNS, v.projectResolver))
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, rules, projectNS, a.projectResolver))
return response, nil
}
@ -166,7 +177,7 @@ func validateUpdateFields(oldPRTB, newPRTB *apisv3.ProjectRoleTemplateBinding) e
}
// validateCreateFields checks if all required fields are present and valid.
func (v *Validator) validateCreateFields(newPRTB *apisv3.ProjectRoleTemplateBinding) error {
func (a *admitter) validateCreateFields(newPRTB *apisv3.ProjectRoleTemplateBinding) error {
hasUserTarget := newPRTB.UserName != "" || newPRTB.UserPrincipalName != ""
hasGroupTarget := newPRTB.GroupName != "" || newPRTB.GroupPrincipalName != ""
@ -178,7 +189,7 @@ func (v *Validator) validateCreateFields(newPRTB *apisv3.ProjectRoleTemplateBind
return fmt.Errorf("binding must have field projectName set: %w", admission.ErrInvalidRequest)
}
roleTemplate, err := v.roleTemplateResolver.RoleTemplateCache().Get(newPRTB.RoleTemplateName)
roleTemplate, err := a.roleTemplateResolver.RoleTemplateCache().Get(newPRTB.RoleTemplateName)
if err != nil {
return fmt.Errorf("unknown reference roleTemplate '%s': %w", newPRTB.RoleTemplateName, err)
}

View File

@ -277,7 +277,9 @@ func (p *ProjectRoleTemplateBindingSuite) Test_PrivilegeEscalation() {
p.Run(test.name, func() {
p.T().Parallel()
req := createPRTBRequest(p.T(), test.args.oldPRTB(), test.args.newPRTB(), test.args.username)
resp, err := validator.Admit(req)
admitters := validator.Admitters()
p.Len(admitters, 1)
resp, err := admitters[0].Admit(req)
p.NoError(err, "Admit failed")
if resp.Allowed != test.allowed {
p.Failf("Response was incorrectly validated", "Wanted response.Allowed = '%v' got %v: result=%+v", test.allowed, resp.Allowed, resp.Result)
@ -578,7 +580,9 @@ func (p *ProjectRoleTemplateBindingSuite) Test_UpdateValidation() {
p.Run(test.name, func() {
p.T().Parallel()
req := createPRTBRequest(p.T(), test.args.oldPRTB(), test.args.newPRTB(), test.args.username)
resp, err := validator.Admit(req)
admitters := validator.Admitters()
p.Len(admitters, 1)
resp, err := admitters[0].Admit(req)
p.NoError(err, "Admit failed")
if resp.Allowed != test.allowed {
p.Failf("Response was incorrectly validated", "Wanted response.Allowed = '%v' got %v: result=%+v", test.allowed, resp.Allowed, resp.Result)
@ -741,7 +745,9 @@ func (p *ProjectRoleTemplateBindingSuite) Test_Create() {
p.Run(test.name, func() {
p.T().Parallel()
req := createPRTBRequest(p.T(), test.args.oldPRTB(), test.args.newPRTB(), test.args.username)
resp, err := validator.Admit(req)
admitters := validator.Admitters()
p.Len(admitters, 1)
resp, err := admitters[0].Admit(req)
p.NoError(err, "Admit failed")
if resp.Allowed != test.allowed {
p.Failf("Response was incorrectly validated", "Wanted response.Allowed = '%v' got %v: result=%+v", test.allowed, resp.Allowed, resp.Result)

View File

@ -25,20 +25,21 @@ var roleTemplateGVR = schema.GroupVersionResource{
Resource: "roletemplates",
}
// NewValidator returns a new validator used for validating roleTemplates.
func NewValidator(resolver validation.AuthorizationRuleResolver, roleTemplateResolver *auth.RoleTemplateResolver,
sar authorizationv1.SubjectAccessReviewInterface) *Validator {
return &Validator{
resolver: resolver,
roleTemplateResolver: roleTemplateResolver,
sar: sar,
admitter: admitter{
resolver: resolver,
roleTemplateResolver: roleTemplateResolver,
sar: sar,
},
}
}
// Validator for validating roleTemplates.
type Validator struct {
resolver validation.AuthorizationRuleResolver
roleTemplateResolver *auth.RoleTemplateResolver
sar authorizationv1.SubjectAccessReviewInterface
admitter admitter
}
// GVR returns the GroupVersionKind for this CRD.
@ -56,8 +57,19 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.ClusterScope, v.Operations())}
}
// Admitters returns the admitter objects used to validate RoleTemplates.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct {
resolver validation.AuthorizationRuleResolver
roleTemplateResolver *auth.RoleTemplateResolver
sar authorizationv1.SubjectAccessReviewInterface
}
// Admit handles the webhook admission request sent to this webhook.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("Validator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)
@ -72,7 +84,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
return &admissionv1.AdmissionResponse{Allowed: true}, nil
}
//check for circular references produced by this role
circularTemplate, err := v.checkCircularRef(roleTemplate)
circularTemplate, err := a.checkCircularRef(roleTemplate)
if err != nil {
logrus.Errorf("Error when trying to check for a circular ref: %s", err)
return nil, err
@ -89,7 +101,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}, nil
}
rules, err := v.roleTemplateResolver.RulesFromTemplate(roleTemplate)
rules, err := a.roleTemplateResolver.RulesFromTemplate(roleTemplate)
if err != nil {
return nil, err
}
@ -109,7 +121,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
}
}
allowed, err := auth.EscalationAuthorized(request, roleTemplateGVR, v.sar, "")
allowed, err := auth.EscalationAuthorized(request, roleTemplateGVR, a.sar, "")
if err != nil {
logrus.Warnf("Failed to check for the 'escalate' verb on RoleTemplates: %v", err)
}
@ -118,7 +130,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
return &admissionv1.AdmissionResponse{Allowed: true}, nil
}
response := &admissionv1.AdmissionResponse{}
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, rules, "", v.resolver))
auth.SetEscalationResponse(response, auth.ConfirmNoEscalation(request, rules, "", a.resolver))
return response, nil
}
@ -126,7 +138,7 @@ func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionRes
// for example - template 1 inherits template 2 which inherits template 1. These setups can cause high cpu usage/crashes
// If a circular ref was found, returns the first template which inherits this role template. Returns nil otherwise.
// Can return an error if any role template was not found.
func (v *Validator) checkCircularRef(template *v3.RoleTemplate) (*v3.RoleTemplate, error) {
func (a *admitter) checkCircularRef(template *v3.RoleTemplate) (*v3.RoleTemplate, error) {
seen := make(map[string]struct{})
queue := []*v3.RoleTemplate{template}
for len(queue) > 0 {
@ -141,7 +153,7 @@ func (v *Validator) checkCircularRef(template *v3.RoleTemplate) (*v3.RoleTemplat
}
// if we haven't seen this yet, we add to the queue to process
if _, ok := seen[inherited]; !ok {
newTemplate, err := v.roleTemplateResolver.RoleTemplateCache().Get(inherited)
newTemplate, err := a.roleTemplateResolver.RoleTemplateCache().Get(inherited)
if err != nil {
return nil, fmt.Errorf("unable to get roletemplate %s with error %w", inherited, err)
}

View File

@ -85,7 +85,7 @@ func TestCheckCircularRef(t *testing.T) {
}
inputRole := createNestedRoleTemplate(rtName, mockCache, testCase.depth, testCase.circleDepth, testCase.errorDepth)
validator := createValidator(mockCache)
result, err := validator.checkCircularRef(inputRole)
result, err := validator.admitter.checkCircularRef(inputRole)
if testCase.errDesired {
assert.NotNil(t, err, "checkCircularRef(), expected err but did not get an error")
} else {
@ -139,6 +139,8 @@ func createRoleTemplate(name string, rules []rbacv1.PolicyRule) *v3.RoleTemplate
func createValidator(cache controllerv3.RoleTemplateCache) *Validator {
return &Validator{
roleTemplateResolver: auth.NewRoleTemplateResolver(cache, nil),
admitter: admitter{
roleTemplateResolver: auth.NewRoleTemplateResolver(cache, nil),
},
}
}

View File

@ -34,18 +34,17 @@ var fleetNameRegex = regexp.MustCompile("^[^-][-a-z0-9]+$")
// NewProvisioningClusterValidator returns a new validator for provisioning clusters
func NewProvisioningClusterValidator(client *clients.Clients) *ProvisioningClusterValidator {
return &ProvisioningClusterValidator{
sar: client.K8s.AuthorizationV1().SubjectAccessReviews(),
mgmtClusterClient: client.Management.Cluster(),
secretCache: client.Core.Secret().Cache(),
psactCache: client.Management.PodSecurityAdmissionConfigurationTemplate().Cache(),
admitter: provisioningAdmitter{
sar: client.K8s.AuthorizationV1().SubjectAccessReviews(),
mgmtClusterClient: client.Management.Cluster(),
secretCache: client.Core.Secret().Cache(),
psactCache: client.Management.PodSecurityAdmissionConfigurationTemplate().Cache(),
},
}
}
type ProvisioningClusterValidator struct {
sar authorizationv1.SubjectAccessReviewInterface
mgmtClusterClient v3.ClusterClient
secretCache corev1controller.SecretCache
psactCache v3.PodSecurityAdmissionConfigurationTemplateCache
admitter provisioningAdmitter
}
// GVR returns the GroupVersionKind for this CRD.
@ -63,8 +62,20 @@ func (p *ProvisioningClusterValidator) ValidatingWebhook(clientConfig admissionr
return []admissionregistrationv1.ValidatingWebhook{*admission.NewDefaultValidatingWebhook(p, clientConfig, admissionregistrationv1.NamespacedScope, p.Operations())}
}
// Admitters returns the admitter objects used to validate provisioning clusters.
func (p *ProvisioningClusterValidator) Admitters() []admission.Admitter {
return []admission.Admitter{&p.admitter}
}
type provisioningAdmitter struct {
sar authorizationv1.SubjectAccessReviewInterface
mgmtClusterClient v3.ClusterClient
secretCache corev1controller.SecretCache
psactCache v3.PodSecurityAdmissionConfigurationTemplateCache
}
// Admit handles the webhook admission request sent to this webhook.
func (p *ProvisioningClusterValidator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (p *provisioningAdmitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("provisioningClusterValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)
oldCluster, cluster, err := objectsv1.ClusterOldAndNewFromRequest(&request.AdmissionRequest)
@ -98,7 +109,7 @@ func (p *ProvisioningClusterValidator) Admit(request *admission.Request) (*admis
return response, nil
}
func (p *ProvisioningClusterValidator) validateCloudCredentialAccess(request *admission.Request, response *admissionv1.AdmissionResponse, oldCluster, newCluster *v1.Cluster) error {
func (p *provisioningAdmitter) validateCloudCredentialAccess(request *admission.Request, response *admissionv1.AdmissionResponse, oldCluster, newCluster *v1.Cluster) error {
if newCluster.Spec.CloudCredentialSecretName == "" ||
oldCluster.Spec.CloudCredentialSecretName == newCluster.Spec.CloudCredentialSecretName {
return nil
@ -149,7 +160,7 @@ func getCloudCredentialSecretInfo(namespace, name string) (string, string) {
return namespace, name
}
func (p *ProvisioningClusterValidator) validateClusterName(request *admission.Request, response *admissionv1.AdmissionResponse, cluster *v1.Cluster) error {
func (p *provisioningAdmitter) validateClusterName(request *admission.Request, response *admissionv1.AdmissionResponse, cluster *v1.Cluster) error {
if request.Operation != admissionv1.Create {
return nil
}
@ -174,7 +185,7 @@ func (p *ProvisioningClusterValidator) validateClusterName(request *admission.Re
}
// validatePSACT validate if the cluster and underlying secret are configured properly when PSACT is enabled or disabled
func (p *ProvisioningClusterValidator) validatePSACT(request *admission.Request, response *admissionv1.AdmissionResponse, cluster *v1.Cluster) error {
func (p *provisioningAdmitter) validatePSACT(request *admission.Request, response *admissionv1.AdmissionResponse, cluster *v1.Cluster) error {
if cluster.Name == "local" || cluster.Spec.RKEConfig == nil {
return nil
}

View File

@ -17,7 +17,16 @@ var gvr = schema.GroupVersionResource{
}
// Validator for validating machineconfigs.
type Validator struct{}
type Validator struct {
admitter admitter
}
// NewValidator returns a new machineconfig validator.
func NewValidator() *Validator {
return &Validator{
admitter: admitter{},
}
}
// GVR returns the GroupVersionKind for this CRD.
func (v *Validator) GVR() schema.GroupVersionResource {
@ -34,8 +43,15 @@ func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.Webho
return []admissionregistrationv1.ValidatingWebhook{*admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.NamespacedScope, v.Operations())}
}
// Admitters returns the admitter objects used to validate machineconfigs.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}
type admitter struct{}
// Admit handles the webhook admission request sent to this webhook.
func (v *Validator) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("machineConfigValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

View File

@ -22,10 +22,10 @@ import (
// Validation returns a list of all ValidatingAdmissionHandlers used by the webhook.
func Validation(clients *clients.Clients) ([]admission.ValidatingAdmissionHandler, error) {
handlers := []admission.ValidatingAdmissionHandler{
&feature.Validator{},
feature.NewValidator(),
managementCluster.NewValidator(clients.K8s.AuthorizationV1().SubjectAccessReviews(), clients.Management.PodSecurityAdmissionConfigurationTemplate().Cache()),
provisioningCluster.NewProvisioningClusterValidator(clients),
&machineconfig.Validator{},
machineconfig.NewValidator(),
nshandler.NewValidator(clients.K8s.AuthorizationV1().SubjectAccessReviews()),
}

View File

@ -122,12 +122,12 @@ func listenAndServe(ctx context.Context, clients *clients.Clients, validators []
logrus.Debug("Creating Webhook routes")
for _, webhook := range validators {
route := router.HandleFunc(admission.Path(validationPath, webhook), admission.NewHandlerFunc(webhook))
route := router.HandleFunc(admission.Path(validationPath, webhook), admission.NewValidatingHandlerFunc(webhook))
path, _ := route.GetPathTemplate()
logrus.Debugf("creating route: %s", path)
}
for _, webhook := range mutators {
route := router.HandleFunc(admission.Path(mutationPath, webhook), admission.NewHandlerFunc(webhook))
route := router.HandleFunc(admission.Path(mutationPath, webhook), admission.NewMutatingHandlerFunc(webhook))
path, _ := route.GetPathTemplate()
logrus.Debugf("creating route: %s", path)
}