Merge pull request #30881 from deads2k/impersonate-user-extra

Automatic merge from submit-queue

Impersonate user extra

Second commit builds on https://github.com/kubernetes/kubernetes/pull/30803.

This adds a restriction to `user.Info.Extra`, keys must be lower case.  This is because HTTP headers are case insensitive, so we can't be sure that we'll get the right case through proxies or even Go (the go library capitalizes after dashes).  I don't think anyone is using them, if they are, they'll need to update to properly plumb through an impersonation flow.

@kubernetes/sig-auth 
@ericchiang since you have background here.
This commit is contained in:
Kubernetes Submit Queue 2016-08-22 06:19:38 -07:00 committed by GitHub
commit 2b8e95624a
4 changed files with 168 additions and 26 deletions

View File

@ -28,6 +28,12 @@ const (
// ImpersonateGroupHeader is used to impersonate a particular group during an API server request. // ImpersonateGroupHeader is used to impersonate a particular group during an API server request.
// It can be repeated multipled times for multiple groups. // It can be repeated multipled times for multiple groups.
ImpersonateGroupHeader = "Impersonate-Group" ImpersonateGroupHeader = "Impersonate-Group"
// ImpersonateUserExtraHeaderPrefix is a prefix for any header used to impersonate an entry in the
// extra map[string][]string for user.Info. The key will be every after the prefix.
// It can be repeated multipled times for multiple map keys and the same key can be repeated multiple
// times to have multiple elements in the slice under a single key
ImpersonateUserExtraHeaderPrefix = "Impersonate-Extra-"
) )
// +genclient=true // +genclient=true

View File

@ -17,7 +17,9 @@ limitations under the License.
package apiserver package apiserver
import ( import (
"fmt"
"net/http" "net/http"
"strings"
"github.com/golang/glog" "github.com/golang/glog"
@ -32,20 +34,17 @@ import (
// WithImpersonation is a filter that will inspect and check requests that attempt to change the user.Info for their requests // 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 { func WithImpersonation(handler http.Handler, requestContextMapper api.RequestContextMapper, a authorizer.Authorizer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
requestedUser := req.Header.Get(authenticationapi.ImpersonateUserHeader) impersonationRequests, err := buildImpersonationRequests(req.Header)
if len(requestedUser) == 0 { if err != nil {
if len(req.Header[authenticationapi.ImpersonateGroupHeader]) > 0 { glog.V(4).Infof("%v", err)
glog.V(4).Infof("attempt to impersonate groups without impersonating a user: %v", req.Header[authenticationapi.ImpersonateGroupHeader]) forbidden(w, req)
forbidden(w, req) return
return }
} if len(impersonationRequests) == 0 {
handler.ServeHTTP(w, req) handler.ServeHTTP(w, req)
return return
} }
impersonationRequests := buildImpersonationRequests(requestedUser, req.Header[authenticationapi.ImpersonateGroupHeader])
ctx, exists := requestContextMapper.Get(req) ctx, exists := requestContextMapper.Get(req)
if !exists { if !exists {
forbidden(w, req) forbidden(w, req)
@ -65,6 +64,7 @@ func WithImpersonation(handler http.Handler, requestContextMapper api.RequestCon
// and group information // and group information
username := "" username := ""
groups := []string{} groups := []string{}
userExtra := map[string][]string{}
for _, impersonationRequest := range impersonationRequests { for _, impersonationRequest := range impersonationRequests {
actingAsAttributes := &authorizer.AttributesRecord{ actingAsAttributes := &authorizer.AttributesRecord{
User: requestor, User: requestor,
@ -92,8 +92,15 @@ func WithImpersonation(handler http.Handler, requestContextMapper api.RequestCon
actingAsAttributes.Resource = "groups" actingAsAttributes.Resource = "groups"
groups = append(groups, impersonationRequest.Name) groups = append(groups, impersonationRequest.Name)
case authenticationapi.Kind("UserExtra"):
extraKey := impersonationRequest.FieldPath
extraValue := impersonationRequest.Name
actingAsAttributes.Resource = "userextras"
actingAsAttributes.Subresource = extraKey
userExtra[extraKey] = append(userExtra[extraKey], extraValue)
default: default:
glog.V(4).Infof("unknown impersonation request type: %v", impersonationRequest) glog.V(4).Infof("unknown impersonation request type: %v\n", impersonationRequest)
forbidden(w, req) forbidden(w, req)
return return
} }
@ -109,7 +116,7 @@ func WithImpersonation(handler http.Handler, requestContextMapper api.RequestCon
newUser := &user.DefaultInfo{ newUser := &user.DefaultInfo{
Name: username, Name: username,
Groups: groups, Groups: groups,
Extra: map[string][]string{}, Extra: userExtra,
} }
requestContextMapper.Update(req, api.WithUser(ctx, newUser)) requestContextMapper.Update(req, api.WithUser(ctx, newUser))
@ -121,19 +128,55 @@ func WithImpersonation(handler http.Handler, requestContextMapper api.RequestCon
} }
// buildImpersonationRequests returns a list of objectreferences that represent the different things we're requesting to impersonate. // 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 // Also includes a map[string][]string representing user.Info.Extra
func buildImpersonationRequests(requestedUser string, requestedGroups []string) []api.ObjectReference { // Each request must be authorized against the current user before switching contexts.
func buildImpersonationRequests(headers http.Header) ([]api.ObjectReference, error) {
impersonationRequests := []api.ObjectReference{} impersonationRequests := []api.ObjectReference{}
if namespace, name, err := serviceaccount.SplitUsername(requestedUser); err == nil { requestedUser := headers.Get(authenticationapi.ImpersonateUserHeader)
impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "ServiceAccount", Namespace: namespace, Name: name}) hasUser := len(requestedUser) > 0
} else { if hasUser {
impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "User", Name: requestedUser}) 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 { hasGroups := false
for _, group := range headers[authenticationapi.ImpersonateGroupHeader] {
hasGroups = true
impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "Group", Name: group}) impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "Group", Name: group})
} }
return impersonationRequests hasUserExtra := false
for headerName, values := range headers {
if !strings.HasPrefix(headerName, authenticationapi.ImpersonateUserExtraHeaderPrefix) {
continue
}
hasUserExtra = true
extraKey := strings.ToLower(headerName[len(authenticationapi.ImpersonateUserExtraHeaderPrefix):])
// make a separate request for each extra value they're trying to set
for _, value := range values {
impersonationRequests = append(impersonationRequests,
api.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: authenticationapi.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
} }

View File

@ -62,17 +62,31 @@ func (impersonateAuthorizer) Authorize(a authorizer.Attributes) (authorized bool
return true, "", nil return true, "", nil
} }
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-scopes" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" {
return true, "", nil
}
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-particular-scopes" &&
a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" && a.GetName() == "scope-a" {
return true, "", nil
}
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-project" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "project" {
return true, "", nil
}
return false, "deny by default", nil return false, "deny by default", nil
} }
func TestImpersonationFilter(t *testing.T) { func TestImpersonationFilter(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
user user.Info user user.Info
impersonationUser string impersonationUser string
impersonationGroups []string impersonationGroups []string
expectedUser user.Info impersonationUserExtras map[string][]string
expectedCode int expectedUser user.Info
expectedCode int
}{ }{
{ {
name: "not-impersonating", name: "not-impersonating",
@ -106,6 +120,17 @@ func TestImpersonationFilter(t *testing.T) {
}, },
expectedCode: http.StatusForbidden, expectedCode: http.StatusForbidden,
}, },
{
name: "impersonating-extra-without-user",
user: &user.DefaultInfo{
Name: "tester",
},
impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}},
expectedUser: &user.DefaultInfo{
Name: "tester",
},
expectedCode: http.StatusForbidden,
},
{ {
name: "disallowed-group", name: "disallowed-group",
user: &user.DefaultInfo{ user: &user.DefaultInfo{
@ -135,6 +160,66 @@ func TestImpersonationFilter(t *testing.T) {
}, },
expectedCode: http.StatusOK, expectedCode: http.StatusOK,
}, },
{
name: "disallowed-userextra-1",
user: &user.DefaultInfo{
Name: "dev",
Groups: []string{"wheel"},
},
impersonationUser: "system:admin",
impersonationGroups: []string{"some-group"},
impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}},
expectedUser: &user.DefaultInfo{
Name: "dev",
Groups: []string{"wheel"},
},
expectedCode: http.StatusForbidden,
},
{
name: "disallowed-userextra-2",
user: &user.DefaultInfo{
Name: "dev",
Groups: []string{"wheel", "extra-setter-project"},
},
impersonationUser: "system:admin",
impersonationGroups: []string{"some-group"},
impersonationUserExtras: map[string][]string{"scopes": {"scope-a"}},
expectedUser: &user.DefaultInfo{
Name: "dev",
Groups: []string{"wheel", "extra-setter-project"},
},
expectedCode: http.StatusForbidden,
},
{
name: "disallowed-userextra-3",
user: &user.DefaultInfo{
Name: "dev",
Groups: []string{"wheel", "extra-setter-particular-scopes"},
},
impersonationUser: "system:admin",
impersonationGroups: []string{"some-group"},
impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}},
expectedUser: &user.DefaultInfo{
Name: "dev",
Groups: []string{"wheel", "extra-setter-particular-scopes"},
},
expectedCode: http.StatusForbidden,
},
{
name: "allowed-userextras",
user: &user.DefaultInfo{
Name: "dev",
Groups: []string{"wheel", "extra-setter-scopes"},
},
impersonationUser: "system:admin",
impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}},
expectedUser: &user.DefaultInfo{
Name: "system:admin",
Groups: []string{},
Extra: map[string][]string{"scopes": {"scope-a", "scope-b"}},
},
expectedCode: http.StatusOK,
},
{ {
name: "allowed-users-impersonation", name: "allowed-users-impersonation",
user: &user.DefaultInfo{ user: &user.DefaultInfo{
@ -238,6 +323,12 @@ func TestImpersonationFilter(t *testing.T) {
for _, group := range tc.impersonationGroups { for _, group := range tc.impersonationGroups {
req.Header.Add(authenticationapi.ImpersonateGroupHeader, group) req.Header.Add(authenticationapi.ImpersonateGroupHeader, group)
} }
for extraKey, values := range tc.impersonationUserExtras {
for _, value := range values {
req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value)
}
}
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
t.Errorf("%s: unexpected error: %v", tc.name, err) t.Errorf("%s: unexpected error: %v", tc.name, err)

View File

@ -36,6 +36,8 @@ type Info interface {
// This is a map[string][]string because it needs to be serializeable into // This is a map[string][]string because it needs to be serializeable into
// a SubjectAccessReviewSpec.authorization.k8s.io for proper authorization // a SubjectAccessReviewSpec.authorization.k8s.io for proper authorization
// delegation flows // delegation flows
// In order to faithfully round-trip through an impersonation flow, these keys
// MUST be lowercase.
GetExtra() map[string][]string GetExtra() map[string][]string
} }