k8s.io/apiserver@v0.31.1/pkg/endpoints/filters/impersonation.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package filters
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"net/http"
    23  	"net/url"
    24  	"strings"
    25  
    26  	"k8s.io/klog/v2"
    27  
    28  	authenticationv1 "k8s.io/api/authentication/v1"
    29  	"k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apiserver/pkg/audit"
    32  	"k8s.io/apiserver/pkg/authentication/serviceaccount"
    33  	"k8s.io/apiserver/pkg/authentication/user"
    34  	"k8s.io/apiserver/pkg/authorization/authorizer"
    35  	"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
    36  	"k8s.io/apiserver/pkg/endpoints/request"
    37  	"k8s.io/apiserver/pkg/server/httplog"
    38  )
    39  
    40  // WithImpersonation is a filter that will inspect and check requests that attempt to change the user.Info for their requests
    41  func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler {
    42  	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    43  		impersonationRequests, err := buildImpersonationRequests(req.Header)
    44  		if err != nil {
    45  			klog.V(4).Infof("%v", err)
    46  			responsewriters.InternalError(w, req, err)
    47  			return
    48  		}
    49  		if len(impersonationRequests) == 0 {
    50  			handler.ServeHTTP(w, req)
    51  			return
    52  		}
    53  
    54  		ctx := req.Context()
    55  		requestor, exists := request.UserFrom(ctx)
    56  		if !exists {
    57  			responsewriters.InternalError(w, req, errors.New("no user found for request"))
    58  			return
    59  		}
    60  
    61  		// if groups are not specified, then we need to look them up differently depending on the type of user
    62  		// if they are specified, then they are the authority (including the inclusion of system:authenticated/system:unauthenticated groups)
    63  		groupsSpecified := len(req.Header[authenticationv1.ImpersonateGroupHeader]) > 0
    64  
    65  		// make sure we're allowed to impersonate each thing we're requesting.  While we're iterating through, start building username
    66  		// and group information
    67  		username := ""
    68  		groups := []string{}
    69  		userExtra := map[string][]string{}
    70  		uid := ""
    71  		for _, impersonationRequest := range impersonationRequests {
    72  			gvk := impersonationRequest.GetObjectKind().GroupVersionKind()
    73  			actingAsAttributes := &authorizer.AttributesRecord{
    74  				User:            requestor,
    75  				Verb:            "impersonate",
    76  				APIGroup:        gvk.Group,
    77  				APIVersion:      gvk.Version,
    78  				Namespace:       impersonationRequest.Namespace,
    79  				Name:            impersonationRequest.Name,
    80  				ResourceRequest: true,
    81  			}
    82  
    83  			switch gvk.GroupKind() {
    84  			case v1.SchemeGroupVersion.WithKind("ServiceAccount").GroupKind():
    85  				actingAsAttributes.Resource = "serviceaccounts"
    86  				username = serviceaccount.MakeUsername(impersonationRequest.Namespace, impersonationRequest.Name)
    87  				if !groupsSpecified {
    88  					// if groups aren't specified for a service account, we know the groups because its a fixed mapping.  Add them
    89  					groups = serviceaccount.MakeGroupNames(impersonationRequest.Namespace)
    90  				}
    91  
    92  			case v1.SchemeGroupVersion.WithKind("User").GroupKind():
    93  				actingAsAttributes.Resource = "users"
    94  				username = impersonationRequest.Name
    95  
    96  			case v1.SchemeGroupVersion.WithKind("Group").GroupKind():
    97  				actingAsAttributes.Resource = "groups"
    98  				groups = append(groups, impersonationRequest.Name)
    99  
   100  			case authenticationv1.SchemeGroupVersion.WithKind("UserExtra").GroupKind():
   101  				extraKey := impersonationRequest.FieldPath
   102  				extraValue := impersonationRequest.Name
   103  				actingAsAttributes.Resource = "userextras"
   104  				actingAsAttributes.Subresource = extraKey
   105  				userExtra[extraKey] = append(userExtra[extraKey], extraValue)
   106  
   107  			case authenticationv1.SchemeGroupVersion.WithKind("UID").GroupKind():
   108  				uid = string(impersonationRequest.Name)
   109  				actingAsAttributes.Resource = "uids"
   110  
   111  			default:
   112  				klog.V(4).InfoS("unknown impersonation request type", "request", impersonationRequest)
   113  				responsewriters.Forbidden(ctx, actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest), s)
   114  				return
   115  			}
   116  
   117  			decision, reason, err := a.Authorize(ctx, actingAsAttributes)
   118  			if err != nil || decision != authorizer.DecisionAllow {
   119  				klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "reason", reason, "err", err)
   120  				responsewriters.Forbidden(ctx, actingAsAttributes, w, req, reason, s)
   121  				return
   122  			}
   123  		}
   124  
   125  		if username != user.Anonymous {
   126  			// When impersonating a non-anonymous user, include the 'system:authenticated' group
   127  			// in the impersonated user info:
   128  			// - if no groups were specified
   129  			// - if a group has been specified other than 'system:authenticated'
   130  			//
   131  			// If 'system:unauthenticated' group has been specified we should not include
   132  			// the 'system:authenticated' group.
   133  			addAuthenticated := true
   134  			for _, group := range groups {
   135  				if group == user.AllAuthenticated || group == user.AllUnauthenticated {
   136  					addAuthenticated = false
   137  					break
   138  				}
   139  			}
   140  
   141  			if addAuthenticated {
   142  				groups = append(groups, user.AllAuthenticated)
   143  			}
   144  		} else {
   145  			addUnauthenticated := true
   146  			for _, group := range groups {
   147  				if group == user.AllUnauthenticated {
   148  					addUnauthenticated = false
   149  					break
   150  				}
   151  			}
   152  
   153  			if addUnauthenticated {
   154  				groups = append(groups, user.AllUnauthenticated)
   155  			}
   156  		}
   157  
   158  		newUser := &user.DefaultInfo{
   159  			Name:   username,
   160  			Groups: groups,
   161  			Extra:  userExtra,
   162  			UID:    uid,
   163  		}
   164  		req = req.WithContext(request.WithUser(ctx, newUser))
   165  
   166  		oldUser, _ := request.UserFrom(ctx)
   167  		httplog.LogOf(req, w).Addf("%v is impersonating %v", userString(oldUser), userString(newUser))
   168  
   169  		ae := audit.AuditEventFrom(ctx)
   170  		audit.LogImpersonatedUser(ae, newUser)
   171  
   172  		// clear all the impersonation headers from the request
   173  		req.Header.Del(authenticationv1.ImpersonateUserHeader)
   174  		req.Header.Del(authenticationv1.ImpersonateGroupHeader)
   175  		req.Header.Del(authenticationv1.ImpersonateUIDHeader)
   176  		for headerName := range req.Header {
   177  			if strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
   178  				req.Header.Del(headerName)
   179  			}
   180  		}
   181  
   182  		handler.ServeHTTP(w, req)
   183  	})
   184  }
   185  
   186  func userString(u user.Info) string {
   187  	if u == nil {
   188  		return "<none>"
   189  	}
   190  	b := strings.Builder{}
   191  	if name := u.GetName(); name == "" {
   192  		b.WriteString("<empty>")
   193  	} else {
   194  		b.WriteString(name)
   195  	}
   196  	if groups := u.GetGroups(); len(groups) > 0 {
   197  		b.WriteString("[")
   198  		b.WriteString(strings.Join(groups, ","))
   199  		b.WriteString("]")
   200  	}
   201  	return b.String()
   202  }
   203  
   204  func unescapeExtraKey(encodedKey string) string {
   205  	key, err := url.PathUnescape(encodedKey) // Decode %-encoded bytes.
   206  	if err != nil {
   207  		return encodedKey // Always record extra strings, even if malformed/unencoded.
   208  	}
   209  	return key
   210  }
   211  
   212  // buildImpersonationRequests returns a list of objectreferences that represent the different things we're requesting to impersonate.
   213  // Also includes a map[string][]string representing user.Info.Extra
   214  // Each request must be authorized against the current user before switching contexts.
   215  func buildImpersonationRequests(headers http.Header) ([]v1.ObjectReference, error) {
   216  	impersonationRequests := []v1.ObjectReference{}
   217  
   218  	requestedUser := headers.Get(authenticationv1.ImpersonateUserHeader)
   219  	hasUser := len(requestedUser) > 0
   220  	if hasUser {
   221  		if namespace, name, err := serviceaccount.SplitUsername(requestedUser); err == nil {
   222  			impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "ServiceAccount", Namespace: namespace, Name: name})
   223  		} else {
   224  			impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "User", Name: requestedUser})
   225  		}
   226  	}
   227  
   228  	hasGroups := false
   229  	for _, group := range headers[authenticationv1.ImpersonateGroupHeader] {
   230  		hasGroups = true
   231  		impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "Group", Name: group})
   232  	}
   233  
   234  	hasUserExtra := false
   235  	for headerName, values := range headers {
   236  		if !strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
   237  			continue
   238  		}
   239  
   240  		hasUserExtra = true
   241  		extraKey := unescapeExtraKey(strings.ToLower(headerName[len(authenticationv1.ImpersonateUserExtraHeaderPrefix):]))
   242  
   243  		// make a separate request for each extra value they're trying to set
   244  		for _, value := range values {
   245  			impersonationRequests = append(impersonationRequests,
   246  				v1.ObjectReference{
   247  					Kind: "UserExtra",
   248  					// we only parse out a group above, but the parsing will fail if there isn't SOME version
   249  					// using the internal version will help us fail if anyone starts using it
   250  					APIVersion: authenticationv1.SchemeGroupVersion.String(),
   251  					Name:       value,
   252  					// ObjectReference doesn't have a subresource field.  FieldPath is close and available, so we'll use that
   253  					// TODO fight the good fight for ObjectReference to refer to resources and subresources
   254  					FieldPath: extraKey,
   255  				})
   256  		}
   257  	}
   258  
   259  	requestedUID := headers.Get(authenticationv1.ImpersonateUIDHeader)
   260  	hasUID := len(requestedUID) > 0
   261  	if hasUID {
   262  		impersonationRequests = append(impersonationRequests, v1.ObjectReference{
   263  			Kind:       "UID",
   264  			Name:       requestedUID,
   265  			APIVersion: authenticationv1.SchemeGroupVersion.String(),
   266  		})
   267  	}
   268  
   269  	if (hasGroups || hasUserExtra || hasUID) && !hasUser {
   270  		return nil, fmt.Errorf("requested %v without impersonating a user", impersonationRequests)
   271  	}
   272  
   273  	return impersonationRequests, nil
   274  }