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 }