mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-14 13:45:06 +00:00
Merge pull request #30803 from deads2k/impersonate-groups
Automatic merge from submit-queue allow group impersonation Adds an "Impersonate-Group" header that can be used to specify exactly which groups to use on an impersonation request. This also restructures the code to make it easier to add the scopes header next. This closely parallels the "Impersonate-User" header, so I figured I'd start easy. @kubernetes/sig-auth @ericchiang are you comfortable reviewing?
This commit is contained in:
@@ -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
|
||||
|
139
pkg/apiserver/handler_impersonation.go
Normal file
139
pkg/apiserver/handler_impersonation.go
Normal file
@@ -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
|
||||
}
|
256
pkg/apiserver/handler_impersonation_test.go
Normal file
256
pkg/apiserver/handler_impersonation_test.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user