mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 03:41:45 +00:00
This introduces an Impersonate-Uid header to server side code.
UserInfo contains a uid field alongside groups, username and extra. This change makes it possible to pass a UID through as an impersonation header like you can with Impersonate-Group, Impersonate-User and Impersonate-Extra. This PR contains: * Changes to impersonation.go to parse the Impersonate-Uid header and authorize uid impersonation * Unit tests for allowed and disallowed impersonation cases * An integration test that creates a CertificateSigningRequest using impersonation, and ensures that the API server populates the correct impersonated spec.uid upon creation.
This commit is contained in:
parent
60a714058b
commit
74f5ed6b17
@ -31,6 +31,9 @@ const (
|
|||||||
// It can be repeated multiplied times for multiple groups.
|
// It can be repeated multiplied times for multiple groups.
|
||||||
ImpersonateGroupHeader = "Impersonate-Group"
|
ImpersonateGroupHeader = "Impersonate-Group"
|
||||||
|
|
||||||
|
// ImpersonateUIDHeader is used to impersonate a particular UID during an API server request
|
||||||
|
ImpersonateUIDHeader = "Impersonate-Uid"
|
||||||
|
|
||||||
// ImpersonateUserExtraHeaderPrefix is a prefix for any header used to impersonate an entry in the
|
// 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.
|
// extra map[string][]string for user.Info. The key will be every after the prefix.
|
||||||
// It can be repeated multiplied times for multiple map keys and the same key can be repeated multiple
|
// It can be repeated multiplied times for multiple map keys and the same key can be repeated multiple
|
||||||
|
@ -67,6 +67,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
|
|||||||
username := ""
|
username := ""
|
||||||
groups := []string{}
|
groups := []string{}
|
||||||
userExtra := map[string][]string{}
|
userExtra := map[string][]string{}
|
||||||
|
uid := ""
|
||||||
for _, impersonationRequest := range impersonationRequests {
|
for _, impersonationRequest := range impersonationRequests {
|
||||||
gvk := impersonationRequest.GetObjectKind().GroupVersionKind()
|
gvk := impersonationRequest.GetObjectKind().GroupVersionKind()
|
||||||
actingAsAttributes := &authorizer.AttributesRecord{
|
actingAsAttributes := &authorizer.AttributesRecord{
|
||||||
@ -103,6 +104,10 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
|
|||||||
actingAsAttributes.Subresource = extraKey
|
actingAsAttributes.Subresource = extraKey
|
||||||
userExtra[extraKey] = append(userExtra[extraKey], extraValue)
|
userExtra[extraKey] = append(userExtra[extraKey], extraValue)
|
||||||
|
|
||||||
|
case authenticationv1.SchemeGroupVersion.WithKind("UID").GroupKind():
|
||||||
|
uid = string(impersonationRequest.Name)
|
||||||
|
actingAsAttributes.Resource = "uids"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
klog.V(4).InfoS("unknown impersonation request type", "Request", impersonationRequest)
|
klog.V(4).InfoS("unknown impersonation request type", "Request", impersonationRequest)
|
||||||
responsewriters.Forbidden(ctx, actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest), s)
|
responsewriters.Forbidden(ctx, actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest), s)
|
||||||
@ -154,6 +159,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
|
|||||||
Name: username,
|
Name: username,
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
Extra: userExtra,
|
Extra: userExtra,
|
||||||
|
UID: uid,
|
||||||
}
|
}
|
||||||
req = req.WithContext(request.WithUser(ctx, newUser))
|
req = req.WithContext(request.WithUser(ctx, newUser))
|
||||||
|
|
||||||
@ -166,6 +172,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
|
|||||||
// clear all the impersonation headers from the request
|
// clear all the impersonation headers from the request
|
||||||
req.Header.Del(authenticationv1.ImpersonateUserHeader)
|
req.Header.Del(authenticationv1.ImpersonateUserHeader)
|
||||||
req.Header.Del(authenticationv1.ImpersonateGroupHeader)
|
req.Header.Del(authenticationv1.ImpersonateGroupHeader)
|
||||||
|
req.Header.Del(authenticationv1.ImpersonateUIDHeader)
|
||||||
for headerName := range req.Header {
|
for headerName := range req.Header {
|
||||||
if strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
|
if strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
|
||||||
req.Header.Del(headerName)
|
req.Header.Del(headerName)
|
||||||
@ -231,7 +238,17 @@ func buildImpersonationRequests(headers http.Header) ([]v1.ObjectReference, erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasGroups || hasUserExtra) && !hasUser {
|
requestedUID := headers.Get(authenticationv1.ImpersonateUIDHeader)
|
||||||
|
hasUID := len(requestedUID) > 0
|
||||||
|
if hasUID {
|
||||||
|
impersonationRequests = append(impersonationRequests, v1.ObjectReference{
|
||||||
|
Kind: "UID",
|
||||||
|
Name: requestedUID,
|
||||||
|
APIVersion: authenticationv1.SchemeGroupVersion.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasGroups || hasUserExtra || hasUID) && !hasUser {
|
||||||
return nil, fmt.Errorf("requested %v without impersonating a user", impersonationRequests)
|
return nil, fmt.Errorf("requested %v without impersonating a user", impersonationRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,11 +75,27 @@ func (impersonateAuthorizer) Authorize(ctx context.Context, a authorizer.Attribu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-particular-scopes" &&
|
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" {
|
a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" && a.GetName() == "scope-a" && a.GetAPIGroup() == "authentication.k8s.io" {
|
||||||
return authorizer.DecisionAllow, "", nil
|
return authorizer.DecisionAllow, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-project" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "project" {
|
if len(user.GetGroups()) > 1 && user.GetGroups()[1] == "extra-setter-project" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "project" && a.GetAPIGroup() == "authentication.k8s.io" {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "everything-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "users" && a.GetAPIGroup() == "" {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "everything-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "uids" && a.GetName() == "some-uid" && a.GetAPIGroup() == "authentication.k8s.io" {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "everything-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "groups" && a.GetAPIGroup() == "" {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.GetGroups()) > 0 && user.GetGroups()[0] == "everything-impersonater" && a.GetVerb() == "impersonate" && a.GetResource() == "userextras" && a.GetSubresource() == "scopes" && a.GetAPIGroup() == "authentication.k8s.io" {
|
||||||
return authorizer.DecisionAllow, "", nil
|
return authorizer.DecisionAllow, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +109,7 @@ func TestImpersonationFilter(t *testing.T) {
|
|||||||
impersonationUser string
|
impersonationUser string
|
||||||
impersonationGroups []string
|
impersonationGroups []string
|
||||||
impersonationUserExtras map[string][]string
|
impersonationUserExtras map[string][]string
|
||||||
|
impersonationUid string
|
||||||
expectedUser user.Info
|
expectedUser user.Info
|
||||||
expectedCode int
|
expectedCode int
|
||||||
}{
|
}{
|
||||||
@ -139,6 +156,17 @@ func TestImpersonationFilter(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedCode: http.StatusInternalServerError,
|
expectedCode: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "impersonating-uid-without-user",
|
||||||
|
user: &user.DefaultInfo{
|
||||||
|
Name: "tester",
|
||||||
|
},
|
||||||
|
impersonationUid: "some-uid",
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "tester",
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "disallowed-group",
|
name: "disallowed-group",
|
||||||
user: &user.DefaultInfo{
|
user: &user.DefaultInfo{
|
||||||
@ -383,6 +411,60 @@ func TestImpersonationFilter(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedCode: http.StatusOK,
|
expectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "allowed-user-impersonation-with-uid",
|
||||||
|
user: &user.DefaultInfo{
|
||||||
|
Name: "dev",
|
||||||
|
Groups: []string{
|
||||||
|
"everything-impersonater",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
impersonationUser: "tester",
|
||||||
|
impersonationUid: "some-uid",
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "tester",
|
||||||
|
Groups: []string{"system:authenticated"},
|
||||||
|
Extra: map[string][]string{},
|
||||||
|
UID: "some-uid",
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disallowed-user-impersonation-with-uid",
|
||||||
|
user: &user.DefaultInfo{
|
||||||
|
Name: "dev",
|
||||||
|
Groups: []string{
|
||||||
|
"everything-impersonater",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
impersonationUser: "tester",
|
||||||
|
impersonationUid: "disallowed-uid",
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "dev",
|
||||||
|
Groups: []string{"everything-impersonater"},
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allowed-impersonation-with-all-headers",
|
||||||
|
user: &user.DefaultInfo{
|
||||||
|
Name: "dev",
|
||||||
|
Groups: []string{
|
||||||
|
"everything-impersonater",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
impersonationUser: "tester",
|
||||||
|
impersonationUid: "some-uid",
|
||||||
|
impersonationGroups: []string{"system:authenticated"},
|
||||||
|
impersonationUserExtras: map[string][]string{"scopes": {"scope-a", "scope-b"}},
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "tester",
|
||||||
|
Groups: []string{"system:authenticated"},
|
||||||
|
UID: "some-uid",
|
||||||
|
Extra: map[string][]string{"scopes": {"scope-a", "scope-b"}},
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
@ -410,6 +492,9 @@ func TestImpersonationFilter(t *testing.T) {
|
|||||||
t.Fatalf("extra header still present: %v", key)
|
t.Fatalf("extra header still present: %v", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if _, ok := req.Header[authenticationapi.ImpersonateUIDHeader]; ok {
|
||||||
|
t.Fatal("uid header still present")
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
handler := func(delegate http.Handler) http.Handler {
|
handler := func(delegate http.Handler) http.Handler {
|
||||||
@ -463,6 +548,9 @@ func TestImpersonationFilter(t *testing.T) {
|
|||||||
req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value)
|
req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(tc.impersonationUid) > 0 {
|
||||||
|
req.Header.Add(authenticationapi.ImpersonateUIDHeader, tc.impersonationUid)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -23,7 +23,12 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -35,7 +40,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
||||||
|
certificatesv1 "k8s.io/api/certificates/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
@ -49,15 +59,25 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
||||||
"k8s.io/apiserver/plugin/pkg/authenticator/token/tokentest"
|
"k8s.io/apiserver/plugin/pkg/authenticator/token/tokentest"
|
||||||
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
|
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
|
||||||
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
"k8s.io/kubernetes/pkg/apis/autoscaling"
|
"k8s.io/kubernetes/pkg/apis/autoscaling"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||||
"k8s.io/kubernetes/pkg/auth/authorizer/abac"
|
"k8s.io/kubernetes/pkg/auth/authorizer/abac"
|
||||||
"k8s.io/kubernetes/test/integration"
|
"k8s.io/kubernetes/test/integration"
|
||||||
|
"k8s.io/kubernetes/test/integration/authutil"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
return f(req)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AliceToken string = "abc123" // username: alice. Present in token file.
|
AliceToken string = "abc123" // username: alice. Present in token file.
|
||||||
BobToken string = "xyz987" // username: bob. Present in token file.
|
BobToken string = "xyz987" // username: bob. Present in token file.
|
||||||
@ -875,6 +895,169 @@ func TestImpersonateIsForbidden(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImpersonateWithUID(t *testing.T) {
|
||||||
|
server := kubeapiservertesting.StartTestServerOrDie(
|
||||||
|
t,
|
||||||
|
nil,
|
||||||
|
[]string{
|
||||||
|
"--authorization-mode=RBAC",
|
||||||
|
"--anonymous-auth",
|
||||||
|
},
|
||||||
|
framework.SharedEtcd(),
|
||||||
|
)
|
||||||
|
t.Cleanup(server.TearDownFn)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
setUIDWrapper := func(rt http.RoundTripper) http.RoundTripper {
|
||||||
|
return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||||
|
req.Header.Set("Impersonate-Uid", "1234")
|
||||||
|
return rt.RoundTrip(req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("impersonation with uid header", func(t *testing.T) {
|
||||||
|
adminClient := clientset.NewForConfigOrDie(server.ClientConfig)
|
||||||
|
|
||||||
|
authutil.GrantUserAuthorization(t, ctx, adminClient, "alice",
|
||||||
|
rbacv1.PolicyRule{
|
||||||
|
Verbs: []string{"create"},
|
||||||
|
APIGroups: []string{"certificates.k8s.io"},
|
||||||
|
Resources: []string{"certificatesigningrequests"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
req := csrPEM(t)
|
||||||
|
|
||||||
|
clientConfig := rest.CopyConfig(server.ClientConfig)
|
||||||
|
clientConfig.Impersonate = rest.ImpersonationConfig{
|
||||||
|
UserName: "alice",
|
||||||
|
}
|
||||||
|
clientConfig.Wrap(setUIDWrapper)
|
||||||
|
|
||||||
|
client := clientset.NewForConfigOrDie(clientConfig)
|
||||||
|
createdCsr, err := client.CertificatesV1().CertificateSigningRequests().Create(
|
||||||
|
ctx,
|
||||||
|
&certificatesv1.CertificateSigningRequest{
|
||||||
|
Spec: certificatesv1.CertificateSigningRequestSpec{
|
||||||
|
SignerName: "kubernetes.io/kube-apiserver-client",
|
||||||
|
Request: req,
|
||||||
|
Usages: []certificatesv1.KeyUsage{"client auth"},
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "impersonated-csr",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metav1.CreateOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error creating Certificate Signing Request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// require that all the original fields and the impersonated user's info
|
||||||
|
// is in the returned spec.
|
||||||
|
expectedCsrSpec := certificatesv1.CertificateSigningRequestSpec{
|
||||||
|
Groups: []string{"system:authenticated"},
|
||||||
|
SignerName: "kubernetes.io/kube-apiserver-client",
|
||||||
|
Request: req,
|
||||||
|
Usages: []certificatesv1.KeyUsage{"client auth"},
|
||||||
|
Username: "alice",
|
||||||
|
UID: "1234",
|
||||||
|
}
|
||||||
|
actualCsrSpec := createdCsr.Spec
|
||||||
|
|
||||||
|
if diff := cmp.Diff(expectedCsrSpec, actualCsrSpec); diff != "" {
|
||||||
|
t.Fatalf("CSR spec was different than expected, -got, +want:\n %s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("impersonation with only UID fails", func(t *testing.T) {
|
||||||
|
clientConfig := rest.CopyConfig(server.ClientConfig)
|
||||||
|
clientConfig.Wrap(setUIDWrapper)
|
||||||
|
|
||||||
|
client := clientset.NewForConfigOrDie(clientConfig)
|
||||||
|
_, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
|
||||||
|
|
||||||
|
if !errors.IsInternalError(err) {
|
||||||
|
t.Fatalf("expected internal error, got %T %v", err, err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(
|
||||||
|
`an error on the server ("Internal Server Error: \"/api/v1/nodes\": `+
|
||||||
|
`requested [{UID 1234 authentication.k8s.io/v1 }] without impersonating a user") `+
|
||||||
|
`has prevented the request from succeeding (get nodes)`,
|
||||||
|
err.Error(),
|
||||||
|
); diff != "" {
|
||||||
|
t.Fatalf("internal error different than expected, -got, +want:\n %s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("impersonating UID without authorization fails", func(t *testing.T) {
|
||||||
|
adminClient := clientset.NewForConfigOrDie(server.ClientConfig)
|
||||||
|
|
||||||
|
authutil.GrantUserAuthorization(t, ctx, adminClient, "system:anonymous",
|
||||||
|
rbacv1.PolicyRule{
|
||||||
|
Verbs: []string{"impersonate"},
|
||||||
|
APIGroups: []string{""},
|
||||||
|
Resources: []string{"users"},
|
||||||
|
ResourceNames: []string{"some-user-anonymous-can-impersonate"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
clientConfig := rest.AnonymousClientConfig(server.ClientConfig)
|
||||||
|
clientConfig.Impersonate = rest.ImpersonationConfig{
|
||||||
|
UserName: "some-user-anonymous-can-impersonate",
|
||||||
|
}
|
||||||
|
clientConfig.Wrap(setUIDWrapper)
|
||||||
|
|
||||||
|
client := clientset.NewForConfigOrDie(clientConfig)
|
||||||
|
_, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
|
||||||
|
|
||||||
|
if !errors.IsForbidden(err) {
|
||||||
|
t.Fatalf("expected forbidden error, got %T %v", err, err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(
|
||||||
|
`uids.authentication.k8s.io "1234" is forbidden: `+
|
||||||
|
`User "system:anonymous" cannot impersonate resource "uids" in API group "authentication.k8s.io" at the cluster scope`,
|
||||||
|
err.Error(),
|
||||||
|
); diff != "" {
|
||||||
|
t.Fatalf("forbidden error different than expected, -got, +want:\n %s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func csrPEM(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error generating ed25519 key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
csrDER, err := x509.CreateCertificateRequest(
|
||||||
|
rand.Reader,
|
||||||
|
&x509.CertificateRequest{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
privateKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error creating x509 certificate request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
csrPemBlock := &pem.Block{
|
||||||
|
Type: "CERTIFICATE REQUEST",
|
||||||
|
Bytes: csrDER,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := pem.EncodeToMemory(csrPemBlock)
|
||||||
|
if req == nil {
|
||||||
|
t.Fatalf("Failed to encode PEM to memory.")
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
func newAuthorizerWithContents(t *testing.T, contents string) authorizer.Authorizer {
|
func newAuthorizerWithContents(t *testing.T, contents string) authorizer.Authorizer {
|
||||||
f, err := ioutil.TempFile("", "auth_test")
|
f, err := ioutil.TempFile("", "auth_test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
121
test/integration/authutil/authutil.go
Normal file
121
test/integration/authutil/authutil.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 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 authutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
|
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WaitForNamedAuthorizationUpdate checks if the given user can perform the named verb and action on the named resource.
|
||||||
|
// Copied from k8s.io/kubernetes/test/e2e/framework/auth.
|
||||||
|
func WaitForNamedAuthorizationUpdate(t *testing.T, ctx context.Context, c authorizationv1client.SubjectAccessReviewsGetter, user, namespace, verb, resourceName string, resource schema.GroupResource, allowed bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
review := &authorizationv1.SubjectAccessReview{
|
||||||
|
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||||
|
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||||
|
Group: resource.Group,
|
||||||
|
Verb: verb,
|
||||||
|
Resource: resource.Resource,
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: resourceName,
|
||||||
|
},
|
||||||
|
User: user,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wait.Poll(200*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
|
||||||
|
response, err := c.SubjectAccessReviews().Create(ctx, review, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if response.Status.Allowed != allowed {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GrantUserAuthorization(t *testing.T, ctx context.Context, adminClient clientset.Interface, username string, rule rbacv1.PolicyRule) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cr, err := adminClient.RbacV1().ClusterRoles().Create(ctx, &rbacv1.ClusterRole{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{GenerateName: strings.Replace(t.Name(), "/", "--", -1)},
|
||||||
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
rule,
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = adminClient.RbacV1().ClusterRoles().Delete(ctx, cr.Name, metav1.DeleteOptions{})
|
||||||
|
})
|
||||||
|
|
||||||
|
crb, err := adminClient.RbacV1().ClusterRoleBindings().Create(ctx, &rbacv1.ClusterRoleBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{GenerateName: strings.Replace(t.Name(), "/", "--", -1)},
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
Kind: rbacv1.UserKind,
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Name: username,
|
||||||
|
Namespace: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleRef: rbacv1.RoleRef{
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Kind: "ClusterRole",
|
||||||
|
Name: cr.Name,
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = adminClient.RbacV1().ClusterRoleBindings().Delete(ctx, crb.Name, metav1.DeleteOptions{})
|
||||||
|
})
|
||||||
|
|
||||||
|
var resourceName string
|
||||||
|
if len(rule.ResourceNames) > 0 {
|
||||||
|
resourceName = rule.ResourceNames[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
WaitForNamedAuthorizationUpdate(
|
||||||
|
t,
|
||||||
|
ctx,
|
||||||
|
adminClient.AuthorizationV1(),
|
||||||
|
username,
|
||||||
|
"",
|
||||||
|
rule.Verbs[0],
|
||||||
|
resourceName,
|
||||||
|
schema.GroupResource{Group: rule.APIGroups[0], Resource: rule.Resources[0]},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
@ -27,8 +27,8 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
|
|
||||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
|
"k8s.io/kubernetes/test/integration/authutil"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -102,8 +102,8 @@ func grantUserPermissionToApproveFor(t *testing.T, client clientset.Interface, u
|
|||||||
}
|
}
|
||||||
approveRule := cr.Rules[0]
|
approveRule := cr.Rules[0]
|
||||||
updateRule := cr.Rules[1]
|
updateRule := cr.Rules[1]
|
||||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", approveRule.Verbs[0], approveRule.ResourceNames[0], schema.GroupResource{Group: approveRule.APIGroups[0], Resource: approveRule.Resources[0]}, true)
|
authutil.WaitForNamedAuthorizationUpdate(t, context.TODO(), client.AuthorizationV1(), username, "", approveRule.Verbs[0], approveRule.ResourceNames[0], schema.GroupResource{Group: approveRule.APIGroups[0], Resource: approveRule.Resources[0]}, true)
|
||||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", updateRule.Verbs[0], "", schema.GroupResource{Group: updateRule.APIGroups[0], Resource: updateRule.Resources[0]}, true)
|
authutil.WaitForNamedAuthorizationUpdate(t, context.TODO(), client.AuthorizationV1(), username, "", updateRule.Verbs[0], "", schema.GroupResource{Group: updateRule.APIGroups[0], Resource: updateRule.Resources[0]}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildApprovalClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
func buildApprovalClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
||||||
|
@ -26,8 +26,8 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
|
|
||||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
|
"k8s.io/kubernetes/test/integration/authutil"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -123,8 +123,8 @@ func grantUserPermissionToSignFor(t *testing.T, client clientset.Interface, user
|
|||||||
}
|
}
|
||||||
signRule := cr.Rules[0]
|
signRule := cr.Rules[0]
|
||||||
statusRule := cr.Rules[1]
|
statusRule := cr.Rules[1]
|
||||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", signRule.Verbs[0], signRule.ResourceNames[0], schema.GroupResource{Group: signRule.APIGroups[0], Resource: signRule.Resources[0]}, true)
|
authutil.WaitForNamedAuthorizationUpdate(t, context.TODO(), client.AuthorizationV1(), username, "", signRule.Verbs[0], signRule.ResourceNames[0], schema.GroupResource{Group: signRule.APIGroups[0], Resource: signRule.Resources[0]}, true)
|
||||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", statusRule.Verbs[0], "", schema.GroupResource{Group: statusRule.APIGroups[0], Resource: statusRule.Resources[0]}, true)
|
authutil.WaitForNamedAuthorizationUpdate(t, context.TODO(), client.AuthorizationV1(), username, "", statusRule.Verbs[0], "", schema.GroupResource{Group: statusRule.APIGroups[0], Resource: statusRule.Resources[0]}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSigningClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
func buildSigningClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 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 certificates
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
authorizationv1 "k8s.io/api/authorization/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
|
||||||
v1authorization "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// waitForNamedAuthorizationUpdate checks if the given user can perform the named verb and action on the named resource.
|
|
||||||
// Copied from k8s.io/kubernetes/test/e2e/framework/auth.
|
|
||||||
func waitForNamedAuthorizationUpdate(t *testing.T, c v1authorization.SubjectAccessReviewsGetter, user, namespace, verb, resourceName string, resource schema.GroupResource, allowed bool) {
|
|
||||||
review := &authorizationv1.SubjectAccessReview{
|
|
||||||
Spec: authorizationv1.SubjectAccessReviewSpec{
|
|
||||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
|
||||||
Group: resource.Group,
|
|
||||||
Verb: verb,
|
|
||||||
Resource: resource.Resource,
|
|
||||||
Namespace: namespace,
|
|
||||||
Name: resourceName,
|
|
||||||
},
|
|
||||||
User: user,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) {
|
|
||||||
response, err := c.SubjectAccessReviews().Create(context.TODO(), review, metav1.CreateOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if response.Status.Allowed != allowed {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,10 +33,10 @@ import (
|
|||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
|
|
||||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
"k8s.io/kubernetes/pkg/controller/certificates"
|
"k8s.io/kubernetes/pkg/controller/certificates"
|
||||||
"k8s.io/kubernetes/pkg/controller/certificates/approver"
|
"k8s.io/kubernetes/pkg/controller/certificates/approver"
|
||||||
|
"k8s.io/kubernetes/test/integration/authutil"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -196,7 +196,7 @@ func grantUserNodeClientPermissions(t *testing.T, client clientset.Interface, us
|
|||||||
t.Fatalf("failed to create test fixtures: %v", err)
|
t.Fatalf("failed to create test fixtures: %v", err)
|
||||||
}
|
}
|
||||||
rule := cr.Rules[0]
|
rule := cr.Rules[0]
|
||||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", rule.Verbs[0], "", schema.GroupResource{Group: rule.APIGroups[0], Resource: rule.Resources[0]}, true)
|
authutil.WaitForNamedAuthorizationUpdate(t, context.TODO(), client.AuthorizationV1(), username, "", rule.Verbs[0], "", schema.GroupResource{Group: rule.APIGroups[0], Resource: rule.Resources[0]}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildNodeClientRoleForUser(name string, resourceType string) *rbacv1.ClusterRole {
|
func buildNodeClientRoleForUser(name string, resourceType string) *rbacv1.ClusterRole {
|
||||||
|
Loading…
Reference in New Issue
Block a user