blob: 726cbe4d565f0e278cb5fde7d62eaf3b83945272 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
2Copyright 2016 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package filters
18
19import (
20 "errors"
21 "fmt"
22 "net/http"
23 "net/url"
24 "strings"
25
26 "github.com/golang/glog"
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
41func 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 glog.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 for _, impersonationRequest := range impersonationRequests {
71 actingAsAttributes := &authorizer.AttributesRecord{
72 User: requestor,
73 Verb: "impersonate",
74 APIGroup: impersonationRequest.GetObjectKind().GroupVersionKind().Group,
75 Namespace: impersonationRequest.Namespace,
76 Name: impersonationRequest.Name,
77 ResourceRequest: true,
78 }
79
80 switch impersonationRequest.GetObjectKind().GroupVersionKind().GroupKind() {
81 case v1.SchemeGroupVersion.WithKind("ServiceAccount").GroupKind():
82 actingAsAttributes.Resource = "serviceaccounts"
83 username = serviceaccount.MakeUsername(impersonationRequest.Namespace, impersonationRequest.Name)
84 if !groupsSpecified {
85 // if groups aren't specified for a service account, we know the groups because its a fixed mapping. Add them
86 groups = serviceaccount.MakeGroupNames(impersonationRequest.Namespace)
87 }
88
89 case v1.SchemeGroupVersion.WithKind("User").GroupKind():
90 actingAsAttributes.Resource = "users"
91 username = impersonationRequest.Name
92
93 case v1.SchemeGroupVersion.WithKind("Group").GroupKind():
94 actingAsAttributes.Resource = "groups"
95 groups = append(groups, impersonationRequest.Name)
96
97 case authenticationv1.SchemeGroupVersion.WithKind("UserExtra").GroupKind():
98 extraKey := impersonationRequest.FieldPath
99 extraValue := impersonationRequest.Name
100 actingAsAttributes.Resource = "userextras"
101 actingAsAttributes.Subresource = extraKey
102 userExtra[extraKey] = append(userExtra[extraKey], extraValue)
103
104 default:
105 glog.V(4).Infof("unknown impersonation request type: %v", impersonationRequest)
106 responsewriters.Forbidden(ctx, actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest), s)
107 return
108 }
109
110 decision, reason, err := a.Authorize(actingAsAttributes)
111 if err != nil || decision != authorizer.DecisionAllow {
112 glog.V(4).Infof("Forbidden: %#v, Reason: %s, Error: %v", req.RequestURI, reason, err)
113 responsewriters.Forbidden(ctx, actingAsAttributes, w, req, reason, s)
114 return
115 }
116 }
117
118 if !groupsSpecified && username != user.Anonymous {
119 // When impersonating a non-anonymous user, if no groups were specified
120 // include the system:authenticated group in the impersonated user info
121 groups = append(groups, user.AllAuthenticated)
122 }
123
124 newUser := &user.DefaultInfo{
125 Name: username,
126 Groups: groups,
127 Extra: userExtra,
128 }
129 req = req.WithContext(request.WithUser(ctx, newUser))
130
131 oldUser, _ := request.UserFrom(ctx)
132 httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser)
133
134 ae := request.AuditEventFrom(ctx)
135 audit.LogImpersonatedUser(ae, newUser)
136
137 // clear all the impersonation headers from the request
138 req.Header.Del(authenticationv1.ImpersonateUserHeader)
139 req.Header.Del(authenticationv1.ImpersonateGroupHeader)
140 for headerName := range req.Header {
141 if strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
142 req.Header.Del(headerName)
143 }
144 }
145
146 handler.ServeHTTP(w, req)
147 })
148}
149
150func unescapeExtraKey(encodedKey string) string {
151 key, err := url.PathUnescape(encodedKey) // Decode %-encoded bytes.
152 if err != nil {
153 return encodedKey // Always record extra strings, even if malformed/unencoded.
154 }
155 return key
156}
157
158// buildImpersonationRequests returns a list of objectreferences that represent the different things we're requesting to impersonate.
159// Also includes a map[string][]string representing user.Info.Extra
160// Each request must be authorized against the current user before switching contexts.
161func buildImpersonationRequests(headers http.Header) ([]v1.ObjectReference, error) {
162 impersonationRequests := []v1.ObjectReference{}
163
164 requestedUser := headers.Get(authenticationv1.ImpersonateUserHeader)
165 hasUser := len(requestedUser) > 0
166 if hasUser {
167 if namespace, name, err := serviceaccount.SplitUsername(requestedUser); err == nil {
168 impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "ServiceAccount", Namespace: namespace, Name: name})
169 } else {
170 impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "User", Name: requestedUser})
171 }
172 }
173
174 hasGroups := false
175 for _, group := range headers[authenticationv1.ImpersonateGroupHeader] {
176 hasGroups = true
177 impersonationRequests = append(impersonationRequests, v1.ObjectReference{Kind: "Group", Name: group})
178 }
179
180 hasUserExtra := false
181 for headerName, values := range headers {
182 if !strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
183 continue
184 }
185
186 hasUserExtra = true
187 extraKey := unescapeExtraKey(strings.ToLower(headerName[len(authenticationv1.ImpersonateUserExtraHeaderPrefix):]))
188
189 // make a separate request for each extra value they're trying to set
190 for _, value := range values {
191 impersonationRequests = append(impersonationRequests,
192 v1.ObjectReference{
193 Kind: "UserExtra",
194 // we only parse out a group above, but the parsing will fail if there isn't SOME version
195 // using the internal version will help us fail if anyone starts using it
196 APIVersion: authenticationv1.SchemeGroupVersion.String(),
197 Name: value,
198 // ObjectReference doesn't have a subresource field. FieldPath is close and available, so we'll use that
199 // TODO fight the good fight for ObjectReference to refer to resources and subresources
200 FieldPath: extraKey,
201 })
202 }
203 }
204
205 if (hasGroups || hasUserExtra) && !hasUser {
206 return nil, fmt.Errorf("requested %v without impersonating a user", impersonationRequests)
207 }
208
209 return impersonationRequests, nil
210}