| /* |
| Copyright 2016 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package filters |
| |
| import ( |
| "errors" |
| "fmt" |
| "net/http" |
| "net/url" |
| "strings" |
| |
| "github.com/golang/glog" |
| |
| authenticationv1 "k8s.io/api/authentication/v1" |
| "k8s.io/api/core/v1" |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/apiserver/pkg/audit" |
| "k8s.io/apiserver/pkg/authentication/serviceaccount" |
| "k8s.io/apiserver/pkg/authentication/user" |
| "k8s.io/apiserver/pkg/authorization/authorizer" |
| "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" |
| "k8s.io/apiserver/pkg/endpoints/request" |
| "k8s.io/apiserver/pkg/server/httplog" |
| ) |
| |
| // WithImpersonation is a filter that will inspect and check requests that attempt to change the user.Info for their requests |
| func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler { |
| return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| impersonationRequests, err := buildImpersonationRequests(req.Header) |
| if err != nil { |
| glog.V(4).Infof("%v", err) |
| responsewriters.InternalError(w, req, err) |
| return |
| } |
| if len(impersonationRequests) == 0 { |
| handler.ServeHTTP(w, req) |
| return |
| } |
| |
| ctx := req.Context() |
| requestor, exists := request.UserFrom(ctx) |
| if !exists { |
| responsewriters.InternalError(w, req, errors.New("no user found for request")) |
| return |
| } |
| |
| // if groups are not specified, then we need to look them up differently depending on the type of user |
| // if they are specified, then they are the authority (including the inclusion of system:authenticated/system:unauthenticated groups) |
| groupsSpecified := len(req.Header[authenticationv1.ImpersonateGroupHeader]) > 0 |
| |
| // make sure we're allowed to impersonate each thing we're requesting. While we're iterating through, start building username |
| // and group information |
| username := "" |
| groups := []string{} |
| userExtra := map[string][]string{} |
| for _, impersonationRequest := range impersonationRequests { |
| actingAsAttributes := &authorizer.AttributesRecord{ |
| User: requestor, |
| Verb: "impersonate", |
| APIGroup: impersonationRequest.GetObjectKind().GroupVersionKind().Group, |
| Namespace: impersonationRequest.Namespace, |
| Name: impersonationRequest.Name, |
| ResourceRequest: true, |
| } |
| |
| switch impersonationRequest.GetObjectKind().GroupVersionKind().GroupKind() { |
| case v1.SchemeGroupVersion.WithKind("ServiceAccount").GroupKind(): |
| actingAsAttributes.Resource = "serviceaccounts" |
| username = serviceaccount.MakeUsername(impersonationRequest.Namespace, impersonationRequest.Name) |
| if !groupsSpecified { |
| // if groups aren't specified for a service account, we know the groups because its a fixed mapping. Add them |
| groups = serviceaccount.MakeGroupNames(impersonationRequest.Namespace) |
| } |
| |
| case v1.SchemeGroupVersion.WithKind("User").GroupKind(): |
| actingAsAttributes.Resource = "users" |
| username = impersonationRequest.Name |
| |
| case v1.SchemeGroupVersion.WithKind("Group").GroupKind(): |
| actingAsAttributes.Resource = "groups" |
| groups = append(groups, impersonationRequest.Name) |
| |
| case authenticationv1.SchemeGroupVersion.WithKind("UserExtra").GroupKind(): |
| extraKey := impersonationRequest.FieldPath |
| extraValue := impersonationRequest.Name |
| actingAsAttributes.Resource = "userextras" |
| actingAsAttributes.Subresource = extraKey |
| userExtra[extraKey] = append(userExtra[extraKey], extraValue) |
| |
| default: |
| glog.V(4).Infof("unknown impersonation request type: %v", impersonationRequest) |
| responsewriters.Forbidden(ctx, actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest), s) |
| return |
| } |
| |
| decision, reason, err := a.Authorize(actingAsAttributes) |
| if err != nil || decision != authorizer.DecisionAllow { |
| glog.V(4).Infof("Forbidden: %#v, Reason: %s, Error: %v", req.RequestURI, reason, err) |
| responsewriters.Forbidden(ctx, actingAsAttributes, w, req, reason, s) |
| return |
| } |
| } |
| |
| if !groupsSpecified && username != user.Anonymous { |
| // When impersonating a non-anonymous user, if no groups were specified |
| // include the system:authenticated group in the impersonated user info |
| groups = append(groups, user.AllAuthenticated) |
| } |
| |
| newUser := &user.DefaultInfo{ |
| Name: username, |
| Groups: groups, |
| Extra: userExtra, |
| } |
| req = req.WithContext(request.WithUser(ctx, newUser)) |
| |
| oldUser, _ := request.UserFrom(ctx) |
| httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser) |
| |
| ae := request.AuditEventFrom(ctx) |
| audit.LogImpersonatedUser(ae, newUser) |
| |
| // clear all the impersonation headers from the request |
| req.Header.Del(authenticationv1.ImpersonateUserHeader) |
| req.Header.Del(authenticationv1.ImpersonateGroupHeader) |
| for headerName := range req.Header { |
| if strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) { |
| req.Header.Del(headerName) |
| } |
| } |
| |
| handler.ServeHTTP(w, req) |
| }) |
| } |
| |
| func unescapeExtraKey(encodedKey string) string { |
| key, err := url.PathUnescape(encodedKey) // Decode %-encoded bytes. |
| if err != nil { |
| return encodedKey // Always record extra strings, even if malformed/unencoded. |
| } |
| return key |
| } |
| |
| // buildImpersonationRequests returns a list of objectreferences that represent the different things we're requesting to impersonate. |
| // Also includes a map[string][]string representing user.Info.Extra |
| // Each request must be authorized against the current user before switching contexts. |
| func buildImpersonationRequests(headers http.Header) ([]v1.ObjectReference, error) { |
| impersonationRequests := []v1.ObjectReference{} |
| |
| requestedUser := headers.Get(authenticationv1.ImpersonateUserHeader) |
| hasUser := len(requestedUser) > 0 |
| if hasUser { |
| if namespace, name, err := serviceaccount.SplitUsername(requestedUser); err == nil { |
| impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "ServiceAccount", Namespace: namespace, Name: name}) |
| } else { |
| impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "User", Name: requestedUser}) |
| } |
| } |
| |
| hasGroups := false |
| for _, group := range headers[authenticationv1.ImpersonateGroupHeader] { |
| hasGroups = true |
| impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "Group", Name: group}) |
| } |
| |
| hasUserExtra := false |
| for headerName, values := range headers { |
| if !strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) { |
| continue |
| } |
| |
| hasUserExtra = true |
| extraKey := unescapeExtraKey(strings.ToLower(headerName[len(authenticationv1.ImpersonateUserExtraHeaderPrefix):])) |
| |
| // make a separate request for each extra value they're trying to set |
| for _, value := range values { |
| impersonationRequests = append(impersonationRequests, |
| v1.ObjectReference{ |
| Kind: "UserExtra", |
| // we only parse out a group above, but the parsing will fail if there isn't SOME version |
| // using the internal version will help us fail if anyone starts using it |
| APIVersion: authenticationv1.SchemeGroupVersion.String(), |
| Name: value, |
| // ObjectReference doesn't have a subresource field. FieldPath is close and available, so we'll use that |
| // TODO fight the good fight for ObjectReference to refer to resources and subresources |
| FieldPath: extraKey, |
| }) |
| } |
| } |
| |
| if (hasGroups || hasUserExtra) && !hasUser { |
| return nil, fmt.Errorf("requested %v without impersonating a user", impersonationRequests) |
| } |
| |
| return impersonationRequests, nil |
| } |