diff --git a/pkg/apis/authentication/types.go b/pkg/apis/authentication/types.go index bf65f1407c0..8a474ccf9d7 100644 --- a/pkg/apis/authentication/types.go +++ b/pkg/apis/authentication/types.go @@ -21,6 +21,15 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" ) +const ( + // ImpersonateUserHeader is used to impersonate a particular user during an API server request + ImpersonateUserHeader = "Impersonate-User" + + // ImpersonateGroupHeader is used to impersonate a particular group during an API server request. + // It can be repeated multipled times for multiple groups. + ImpersonateGroupHeader = "Impersonate-Group" +) + // +genclient=true // +nonNamespaced=true // +noMethods=true diff --git a/pkg/apiserver/handler_impersonation.go b/pkg/apiserver/handler_impersonation.go new file mode 100644 index 00000000000..9aedf237eb1 --- /dev/null +++ b/pkg/apiserver/handler_impersonation.go @@ -0,0 +1,139 @@ +/* +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 apiserver + +import ( + "net/http" + + "github.com/golang/glog" + + "k8s.io/kubernetes/pkg/api" + authenticationapi "k8s.io/kubernetes/pkg/apis/authentication" + "k8s.io/kubernetes/pkg/auth/authorizer" + "k8s.io/kubernetes/pkg/auth/user" + "k8s.io/kubernetes/pkg/httplog" + "k8s.io/kubernetes/pkg/serviceaccount" +) + +// 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, requestContextMapper api.RequestContextMapper, a authorizer.Authorizer) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + requestedUser := req.Header.Get(authenticationapi.ImpersonateUserHeader) + if len(requestedUser) == 0 { + if len(req.Header[authenticationapi.ImpersonateGroupHeader]) > 0 { + glog.V(4).Infof("attempt to impersonate groups without impersonating a user: %v", req.Header[authenticationapi.ImpersonateGroupHeader]) + forbidden(w, req) + return + } + + handler.ServeHTTP(w, req) + return + } + + impersonationRequests := buildImpersonationRequests(requestedUser, req.Header[authenticationapi.ImpersonateGroupHeader]) + + ctx, exists := requestContextMapper.Get(req) + if !exists { + forbidden(w, req) + return + } + requestor, exists := api.UserFrom(ctx) + if !exists { + forbidden(w, req) + 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 + groupsSpecified := len(req.Header[authenticationapi.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{} + 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 api.Kind("ServiceAccount"): + 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, impersonationRequest.Name) + } + + case api.Kind("User"): + actingAsAttributes.Resource = "users" + username = impersonationRequest.Name + + case api.Kind("Group"): + actingAsAttributes.Resource = "groups" + groups = append(groups, impersonationRequest.Name) + + default: + glog.V(4).Infof("unknown impersonation request type: %v", impersonationRequest) + forbidden(w, req) + return + } + + allowed, reason, err := a.Authorize(actingAsAttributes) + if err != nil || !allowed { + glog.V(4).Infof("Forbidden: %#v, Reason: %s, Error: %v", req.RequestURI, reason, err) + forbidden(w, req) + return + } + } + + newUser := &user.DefaultInfo{ + Name: username, + Groups: groups, + Extra: map[string][]string{}, + } + requestContextMapper.Update(req, api.WithUser(ctx, newUser)) + + oldUser, _ := api.UserFrom(ctx) + httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser) + + handler.ServeHTTP(w, req) + }) +} + +// buildImpersonationRequests returns a list of objectreferences that represent the different things we're requesting to impersonate. +// Each request must be authorized against the current user before switching contexts +func buildImpersonationRequests(requestedUser string, requestedGroups []string) []api.ObjectReference { + impersonationRequests := []api.ObjectReference{} + + if namespace, name, err := serviceaccount.SplitUsername(requestedUser); err == nil { + impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "ServiceAccount", Namespace: namespace, Name: name}) + } else { + impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "User", Name: requestedUser}) + } + + for _, group := range requestedGroups { + impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "Group", Name: group}) + } + + return impersonationRequests +} diff --git a/pkg/apiserver/handler_impersonation_test.go b/pkg/apiserver/handler_impersonation_test.go new file mode 100644 index 00000000000..228405afc54 --- /dev/null +++ b/pkg/apiserver/handler_impersonation_test.go @@ -0,0 +1,256 @@ +/* +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 apiserver + +import ( + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "sync" + "testing" + + "k8s.io/kubernetes/pkg/api" + authenticationapi "k8s.io/kubernetes/pkg/apis/authentication" + "k8s.io/kubernetes/pkg/auth/authorizer" + "k8s.io/kubernetes/pkg/auth/user" +) + +type impersonateAuthorizer struct{} + +func (impersonateAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, reason string, err error) { + user := a.GetUser() + + switch { + case user.GetName() == "system:admin": + return true, "", nil + + case user.GetName() == "tester": + return false, "", fmt.Errorf("works on my machine") + + case user.GetName() == "deny-me": + return false, "denied", nil + } + + if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "wheel" && a.GetVerb() == "impersonate" && a.GetResource() == "users" { + return true, "", nil + } + + if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "sa-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "serviceaccounts" { + return true, "", nil + } + + if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "regular-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "users" { + return true, "", nil + } + + if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "group-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "groups" { + return true, "", nil + } + + return false, "deny by default", nil +} + +func TestImpersonationFilter(t *testing.T) { + testCases := []struct { + name string + user user.Info + impersonationUser string + impersonationGroups []string + expectedUser user.Info + expectedCode int + }{ + { + name: "not-impersonating", + user: &user.DefaultInfo{ + Name: "tester", + }, + expectedUser: &user.DefaultInfo{ + Name: "tester", + }, + expectedCode: http.StatusOK, + }, + { + name: "impersonating-error", + user: &user.DefaultInfo{ + Name: "tester", + }, + impersonationUser: "anyone", + expectedUser: &user.DefaultInfo{ + Name: "tester", + }, + expectedCode: http.StatusForbidden, + }, + { + name: "impersonating-group-without-user", + user: &user.DefaultInfo{ + Name: "tester", + }, + impersonationGroups: []string{"some-group"}, + expectedUser: &user.DefaultInfo{ + Name: "tester", + }, + expectedCode: http.StatusForbidden, + }, + { + name: "disallowed-group", + user: &user.DefaultInfo{ + Name: "dev", + Groups: []string{"wheel"}, + }, + impersonationUser: "system:admin", + impersonationGroups: []string{"some-group"}, + expectedUser: &user.DefaultInfo{ + Name: "dev", + Groups: []string{"wheel"}, + }, + expectedCode: http.StatusForbidden, + }, + { + name: "allowed-group", + user: &user.DefaultInfo{ + Name: "dev", + Groups: []string{"wheel", "group-impersonater"}, + }, + impersonationUser: "system:admin", + impersonationGroups: []string{"some-group"}, + expectedUser: &user.DefaultInfo{ + Name: "system:admin", + Groups: []string{"some-group"}, + Extra: map[string][]string{}, + }, + expectedCode: http.StatusOK, + }, + { + name: "allowed-users-impersonation", + user: &user.DefaultInfo{ + Name: "dev", + Groups: []string{"regular-impersonater"}, + }, + impersonationUser: "tester", + expectedUser: &user.DefaultInfo{ + Name: "tester", + Groups: []string{}, + Extra: map[string][]string{}, + }, + expectedCode: http.StatusOK, + }, + { + name: "disallowed-impersonating", + user: &user.DefaultInfo{ + Name: "dev", + Groups: []string{"sa-impersonater"}, + }, + impersonationUser: "tester", + expectedUser: &user.DefaultInfo{ + Name: "dev", + Groups: []string{"sa-impersonater"}, + }, + expectedCode: http.StatusForbidden, + }, + { + name: "allowed-sa-impersonating", + user: &user.DefaultInfo{ + Name: "dev", + Groups: []string{"sa-impersonater"}, + Extra: map[string][]string{}, + }, + impersonationUser: "system:serviceaccount:foo:default", + expectedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:foo:default", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:foo"}, + Extra: map[string][]string{}, + }, + expectedCode: http.StatusOK, + }, + } + + requestContextMapper = api.NewRequestContextMapper() + var ctx api.Context + var actualUser user.Info + var lock sync.Mutex + + doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + currentCtx, _ := requestContextMapper.Get(req) + user, exists := api.UserFrom(currentCtx) + if !exists { + actualUser = nil + return + } + + actualUser = user + }) + handler := func(delegate http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Recovered %v", r) + } + }() + lock.Lock() + defer lock.Unlock() + requestContextMapper.Update(req, ctx) + currentCtx, _ := requestContextMapper.Get(req) + + user, exists := api.UserFrom(currentCtx) + if !exists { + actualUser = nil + return + } else { + actualUser = user + } + + delegate.ServeHTTP(w, req) + }) + }(WithImpersonation(doNothingHandler, requestContextMapper, impersonateAuthorizer{})) + handler, _ = api.NewRequestContextFilter(requestContextMapper, handler) + + server := httptest.NewServer(handler) + defer server.Close() + + for _, tc := range testCases { + func() { + lock.Lock() + defer lock.Unlock() + ctx = api.WithUser(api.NewContext(), tc.user) + }() + + req, err := http.NewRequest("GET", server.URL, nil) + if err != nil { + t.Errorf("%s: unexpected error: %v", tc.name, err) + continue + } + req.Header.Add(authenticationapi.ImpersonateUserHeader, tc.impersonationUser) + for _, group := range tc.impersonationGroups { + req.Header.Add(authenticationapi.ImpersonateGroupHeader, group) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("%s: unexpected error: %v", tc.name, err) + continue + } + if resp.StatusCode != tc.expectedCode { + t.Errorf("%s: expected %v, actual %v", tc.name, tc.expectedCode, resp.StatusCode) + continue + } + + if !reflect.DeepEqual(actualUser, tc.expectedUser) { + t.Errorf("%s: expected %#v, actual %#v", tc.name, tc.expectedUser, actualUser) + continue + } + } +} diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go index 696a3522d71..35d4e5af789 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/handlers.go @@ -32,9 +32,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/auth/authorizer" - "k8s.io/kubernetes/pkg/auth/user" "k8s.io/kubernetes/pkg/httplog" - "k8s.io/kubernetes/pkg/serviceaccount" "k8s.io/kubernetes/pkg/util/runtime" "k8s.io/kubernetes/pkg/util/sets" ) @@ -462,75 +460,6 @@ func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attrib return &attribs } -func WithImpersonation(handler http.Handler, requestContextMapper api.RequestContextMapper, a authorizer.Authorizer) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - requestedSubject := req.Header.Get("Impersonate-User") - if len(requestedSubject) == 0 { - handler.ServeHTTP(w, req) - return - } - - ctx, exists := requestContextMapper.Get(req) - if !exists { - forbidden(w, req) - return - } - requestor, exists := api.UserFrom(ctx) - if !exists { - forbidden(w, req) - return - } - - actingAsAttributes := &authorizer.AttributesRecord{ - User: requestor, - Verb: "impersonate", - APIGroup: api.GroupName, - Resource: "users", - Name: requestedSubject, - ResourceRequest: true, - } - if namespace, name, err := serviceaccount.SplitUsername(requestedSubject); err == nil { - actingAsAttributes.Resource = "serviceaccounts" - actingAsAttributes.Namespace = namespace - actingAsAttributes.Name = name - } - - authorized, reason, err := a.Authorize(actingAsAttributes) - if err != nil { - internalError(w, req, err) - return - } - if !authorized { - glog.V(4).Infof("Forbidden: %#v, Reason: %s", req.RequestURI, reason) - forbidden(w, req) - return - } - - switch { - case strings.HasPrefix(requestedSubject, serviceaccount.ServiceAccountUsernamePrefix): - namespace, name, err := serviceaccount.SplitUsername(requestedSubject) - if err != nil { - forbidden(w, req) - return - } - requestContextMapper.Update(req, api.WithUser(ctx, serviceaccount.UserInfo(namespace, name, ""))) - - default: - newUser := &user.DefaultInfo{ - Name: requestedSubject, - } - requestContextMapper.Update(req, api.WithUser(ctx, newUser)) - } - - newCtx, _ := requestContextMapper.Get(req) - oldUser, _ := api.UserFrom(ctx) - newUser, _ := api.UserFrom(newCtx) - httplog.LogOf(req, w).Addf("%v is acting as %v", oldUser, newUser) - - handler.ServeHTTP(w, req) - }) -} - // WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise. func WithAuthorizationCheck(handler http.Handler, getAttribs RequestAttributeGetter, a authorizer.Authorizer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {