Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions cmd/apiserver/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,7 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
return
}
if h, err := crdRuntimeMgr.Runtime(cid, genericConfig.DrainedNotify()); err == nil && h != nil {
h = genericfilters.WithRequestInfo(h, conf.RequestInfoResolver)
h = genericfilters.WithAuditInit(h)
h = serverfilters.WithPanicRecovery(h, conf.RequestInfoResolver)
h.ServeHTTP(w, r)
wrapClusterCRDHandler(h, conf, cid, false).ServeHTTP(w, r)
return
}
klog.Errorf("mc.crdRuntime unresolved at kube cluster=%s path=%s", cid, r.URL.Path)
Expand Down Expand Up @@ -361,13 +358,7 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
http.Error(w, "cluster CRD runtime unavailable", http.StatusServiceUnavailable)
return true
}
// Ensure RequestInfo is computed from the normalized /apis path
// before entering the cluster-scoped CRD runtime handler.
h = genericfilters.WithRequestInfo(h, conf.RequestInfoResolver)
h = withClusterCRDRequestInfoRewrite(h, clusterID)
h = genericfilters.WithAuditInit(h)
h = serverfilters.WithPanicRecovery(h, conf.RequestInfoResolver)
h.ServeHTTP(w, r)
wrapClusterCRDHandler(h, conf, clusterID, false).ServeHTTP(w, r)
return true
}
// Ensure CRDs are also routed through the multicluster handler
Expand Down Expand Up @@ -480,6 +471,25 @@ func withClusterCRDRequestInfoRewrite(next http.Handler, clusterID string) http.
})
}

func wrapClusterCRDHandler(next http.Handler, conf *server.Config, clusterID string, rewriteRequestInfo bool) http.Handler {
if next == nil || conf == nil {
return next
}
h := next
if rewriteRequestInfo {
h = withClusterCRDRequestInfoRewrite(h, clusterID)
}
h = genericfilters.WithAuthorization(h, conf.Authorization.Authorizer, conf.Serializer)
failedHandler := genericfilters.Unauthorized(conf.Serializer)
failedHandler = genericfilters.WithFailedAuthenticationAudit(failedHandler, conf.AuditBackend, conf.AuditPolicyRuleEvaluator)
h = genericfilters.WithAuthentication(h, conf.Authentication.Authenticator, failedHandler, conf.Authentication.APIAudiences, conf.Authentication.RequestHeaderConfig)
// RequestInfo must be available before rewrite/authn/authz wrappers execute.
h = genericfilters.WithRequestInfo(h, conf.RequestInfoResolver)
h = genericfilters.WithAuditInit(h)
h = serverfilters.WithPanicRecovery(h, conf.RequestInfoResolver)
return h
}

func decorateRESTOptionsGetter(server string, getter generic.RESTOptionsGetter, opts mc.Options) generic.RESTOptionsGetter {
if _, ok := getter.(mc.RESTOptionsDecorator); ok {
klog.Infof("mc.restOptionsGetter server=%s alreadyDecorated=true", server)
Expand Down
33 changes: 33 additions & 0 deletions pkg/multicluster/admission/webhook/generic/versioned_attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package generic

import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
)

// EnsureVersionedAttributesUserInfo guarantees that AdmissionReview construction
// always sees a non-nil user.Info, preventing nil dereferences in upstream
// request builders when attributes are missing user context.
func EnsureVersionedAttributesUserInfo(attr *admission.VersionedAttributes) *admission.VersionedAttributes {
if attr == nil || attr.Attributes == nil || attr.Attributes.GetUserInfo() != nil {
return attr
}

cloned := *attr
cloned.Attributes = userInfoFallbackAttributes{Attributes: attr.Attributes}
return &cloned
}

type userInfoFallbackAttributes struct {
admission.Attributes
}

func (a userInfoFallbackAttributes) GetUserInfo() user.Info {
if a.Attributes == nil {
return &user.DefaultInfo{}
}
if info := a.Attributes.GetUserInfo(); info != nil {
return info
}
return &user.DefaultInfo{}
}
2 changes: 1 addition & 1 deletion pkg/multicluster/admission/webhook/mutating/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admiss
}
}

uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation)
uid, request, response, err := webhookrequest.CreateAdmissionObjects(generic.EnsureVersionedAttributesUserInfo(attr), invocation)
if err != nil {
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not create admission objects: %w", err), Status: apierrors.NewBadRequest("error creating admission objects")}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWeb
}
}

uid, request, response, err := webhookrequest.CreateAdmissionObjects(versionedAttr, invocation)
uid, request, response, err := webhookrequest.CreateAdmissionObjects(generic.EnsureVersionedAttributesUserInfo(versionedAttr), invocation)
if err != nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not create admission objects: %w", err), Status: apierrors.NewBadRequest("error creating admission objects")}
}
Expand Down
110 changes: 97 additions & 13 deletions pkg/multicluster/auth/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package auth

import (
"context"
"fmt"
"net/http"
"reflect"
"strings"

mc "github.com/kplane-dev/apiserver/pkg/multicluster"
Expand Down Expand Up @@ -48,8 +50,8 @@ func (c *ClusterAuthenticator) AuthenticateRequest(req *http.Request) (*authenti
}
}
useRoot := cid == "" || cid == c.rootCluster
if useRoot && c.root != nil {
return c.root.AuthenticateRequest(req)
if useRoot && !isNil(c.root) {
return authenticateSafely(c.root, req, "root")
}
if c.resolver == nil {
return nil, false, nil
Expand All @@ -58,10 +60,10 @@ func (c *ClusterAuthenticator) AuthenticateRequest(req *http.Request) (*authenti
if err != nil {
return nil, false, err
}
if authn == nil {
if isNil(authn) {
return nil, false, nil
}
return authn.AuthenticateRequest(req)
return authenticateSafely(authn, req, cid)
}

// ClusterAuthorizer dispatches authorization per cluster.
Expand Down Expand Up @@ -93,27 +95,33 @@ func NewClusterAuthorizer(rootCluster string, root authorizer.Authorizer, rootRe
// Authorize dispatches by cluster context.
func (c *ClusterAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
cid := clusterFromContext(ctx)
if cid == "" || (cid == c.rootCluster && c.root != nil) || c.resolver == nil {
if c.root == nil {
if cid == "" || (cid == c.rootCluster && !isNil(c.root)) || c.resolver == nil {
if isNil(c.root) {
return authorizer.DecisionNoOpinion, "no root authorizer", nil
}
return c.root.Authorize(ctx, a)
if err := validateAttributesForAuthorize(a, "root", authorizerType(c.root)); err != nil {
return authorizer.DecisionDeny, "", err
}
return authorizeSafely(c.root, ctx, a, "root")
}
authz, _, err := c.resolver.AuthorizerForCluster(cid)
if err != nil {
return authorizer.DecisionNoOpinion, "", err
return authorizer.DecisionDeny, "", err
}
if authz == nil {
if isNil(authz) {
return authorizer.DecisionNoOpinion, "no cluster authorizer", nil
}
return authz.Authorize(ctx, a)
if err := validateAttributesForAuthorize(a, cid, authorizerType(authz)); err != nil {
return authorizer.DecisionDeny, "", err
}
return authorizeSafely(authz, ctx, a, cid)
}

// RulesFor dispatches rule resolution per cluster.
func (c *ClusterAuthorizer) RulesFor(ctx context.Context, u user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
cid := clusterFromContext(ctx)
if cid == "" || (cid == c.rootCluster && c.rootResolver != nil) || c.resolver == nil {
if c.rootResolver == nil {
if cid == "" || (cid == c.rootCluster && !isNil(c.rootResolver)) || c.resolver == nil {
if isNil(c.rootResolver) {
return nil, nil, false, nil
}
return c.rootResolver.RulesFor(ctx, u, namespace)
Expand All @@ -122,12 +130,88 @@ func (c *ClusterAuthorizer) RulesFor(ctx context.Context, u user.Info, namespace
if err != nil {
return nil, nil, false, err
}
if resolver == nil {
if isNil(resolver) {
return nil, nil, false, nil
}
return resolver.RulesFor(ctx, u, namespace)
}

func isNil(v any) bool {
if v == nil {
return true
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return rv.IsNil()
default:
return false
}
}

func authorizeSafely(authz authorizer.Authorizer, ctx context.Context, a authorizer.Attributes, target string) (decision authorizer.Decision, reason string, err error) {
defer func() {
if r := recover(); r != nil {
decision = authorizer.DecisionDeny
reason = ""
err = fmt.Errorf("authorizer panic for cluster %q (type=%s): %v", target, authorizerType(authz), r)
}
}()
return authz.Authorize(ctx, a)
}

func validateAttributesForAuthorize(a authorizer.Attributes, clusterID, authzType string) error {
if isNil(a) {
return fmt.Errorf("invalid authorization attributes for cluster %q (authorizer=%s): attributes is nil", clusterID, authzType)
}
u, err := userFromAttributes(a)
if err != nil {
return fmt.Errorf("invalid authorization attributes for cluster %q (authorizer=%s): %w", clusterID, authzType, err)
}
if isNil(u) {
return fmt.Errorf("invalid authorization attributes for cluster %q (authorizer=%s): user is nil", clusterID, authzType)
}
return nil
}

func userFromAttributes(a authorizer.Attributes) (u user.Info, err error) {
defer func() {
if r := recover(); r != nil {
u = nil
err = fmt.Errorf("GetUser panic: %v", r)
}
}()
return a.GetUser(), nil
}

func authorizerType(authz authorizer.Authorizer) string {
if isNil(authz) {
return "<nil>"
}
return fmt.Sprintf("%T", authz)
}

func authenticateSafely(authn authenticator.Request, req *http.Request, target string) (resp *authenticator.Response, ok bool, err error) {
defer func() {
if r := recover(); r != nil {
resp = nil
ok = false
err = fmt.Errorf("authenticator panic for cluster %q (type=%T): %v", target, authn, r)
}
}()
resp, ok, err = authn.AuthenticateRequest(req)
if err != nil || !ok {
return resp, ok, err
}
if resp == nil {
return nil, false, fmt.Errorf("invalid authenticator response for cluster %q (type=%T): response is nil", target, authn)
}
if isNil(resp.User) {
return nil, false, fmt.Errorf("invalid authenticator response for cluster %q (type=%T): user is nil", target, authn)
}
return resp, ok, nil
}

func clusterFromContext(ctx context.Context) string {
cid, _, _ := mc.FromContext(ctx)
return cid
Expand Down
Loading