mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-09 12:07:47 +00:00
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:
commit
2b8e95624a
@ -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
|
||||||
|
@ -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{}
|
||||||
|
|
||||||
|
requestedUser := headers.Get(authenticationapi.ImpersonateUserHeader)
|
||||||
|
hasUser := len(requestedUser) > 0
|
||||||
|
if hasUser {
|
||||||
if namespace, name, err := serviceaccount.SplitUsername(requestedUser); err == nil {
|
if namespace, name, err := serviceaccount.SplitUsername(requestedUser); err == nil {
|
||||||
impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "ServiceAccount", Namespace: namespace, Name: name})
|
impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "ServiceAccount", Namespace: namespace, Name: name})
|
||||||
} else {
|
} else {
|
||||||
impersonationRequests = append(impersonationRequests, api.ObjectReference{Kind: "User", Name: requestedUser})
|
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
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,19 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +84,7 @@ func TestImpersonationFilter(t *testing.T) {
|
|||||||
user user.Info
|
user user.Info
|
||||||
impersonationUser string
|
impersonationUser string
|
||||||
impersonationGroups []string
|
impersonationGroups []string
|
||||||
|
impersonationUserExtras map[string][]string
|
||||||
expectedUser user.Info
|
expectedUser user.Info
|
||||||
expectedCode int
|
expectedCode int
|
||||||
}{
|
}{
|
||||||
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user