Skip to content

Commit

Permalink
This introduces an Impersonate-Uid header to server side code.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
margocrawf committed Jul 2, 2021
1 parent 60a7140 commit 96a18bd
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 3 deletions.
3 changes: 3 additions & 0 deletions staging/src/k8s.io/api/authentication/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (
// It can be repeated multiplied times for multiple groups.
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
// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
username := ""
groups := []string{}
userExtra := map[string][]string{}
uid := ""
for _, impersonationRequest := range impersonationRequests {
gvk := impersonationRequest.GetObjectKind().GroupVersionKind()
actingAsAttributes := &authorizer.AttributesRecord{
Expand Down Expand Up @@ -103,6 +104,10 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
actingAsAttributes.Subresource = extraKey
userExtra[extraKey] = append(userExtra[extraKey], extraValue)

case authenticationv1.SchemeGroupVersion.WithKind("UID").GroupKind():
uid = string(impersonationRequest.Name)
actingAsAttributes.Resource = "uids"

default:
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)
Expand Down Expand Up @@ -154,6 +159,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
Name: username,
Groups: groups,
Extra: userExtra,
UID: uid,
}
req = req.WithContext(request.WithUser(ctx, newUser))

Expand All @@ -166,6 +172,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.
// clear all the impersonation headers from the request
req.Header.Del(authenticationv1.ImpersonateUserHeader)
req.Header.Del(authenticationv1.ImpersonateGroupHeader)
req.Header.Del(authenticationv1.ImpersonateUIDHeader)
for headerName := range req.Header {
if strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) {
req.Header.Del(headerName)
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" &&
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
}

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
}

Expand All @@ -93,6 +109,7 @@ func TestImpersonationFilter(t *testing.T) {
impersonationUser string
impersonationGroups []string
impersonationUserExtras map[string][]string
impersonationUid string
expectedUser user.Info
expectedCode int
}{
Expand Down Expand Up @@ -139,6 +156,17 @@ func TestImpersonationFilter(t *testing.T) {
},
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",
user: &user.DefaultInfo{
Expand Down Expand Up @@ -383,6 +411,60 @@ func TestImpersonationFilter(t *testing.T) {
},
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
Expand Down Expand Up @@ -410,6 +492,9 @@ func TestImpersonationFilter(t *testing.T) {
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 {
Expand Down Expand Up @@ -463,6 +548,9 @@ func TestImpersonationFilter(t *testing.T) {
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)
if err != nil {
Expand Down
110 changes: 110 additions & 0 deletions test/integration/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,19 @@ package auth
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"github.com/google/go-cmp/cmp"
"io/ioutil"
certificatesv1 "k8s.io/api/certificates/v1"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"net/http"
"net/http/httptest"
"net/url"
Expand Down Expand Up @@ -58,6 +68,12 @@ import (
"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 (
AliceToken string = "abc123" // username: alice. Present in token file.
BobToken string = "xyz987" // username: bob. Present in token file.
Expand Down Expand Up @@ -875,6 +891,100 @@ func TestImpersonateIsForbidden(t *testing.T) {

}

func TestImpersonateWithUID(t *testing.T) {
t.Run("impersonation with uid header", func(t *testing.T) {
req := csrPEM(t)

result := kubeapiservertesting.StartTestServerOrDie(
t,
nil,
nil,
framework.SharedEtcd(),
)
t.Cleanup(result.TearDownFn)

clientConfig := result.ClientConfig
clientConfig.Impersonate = rest.ImpersonationConfig{
UserName: "alice",
}
clientConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
req.Header.Set("Impersonate-Uid", "1")
return rt.RoundTrip(req)
})
})

client := clientset.NewForConfigOrDie(clientConfig)
createdCsr, err := client.CertificatesV1().CertificateSigningRequests().Create(
context.Background(),
&certificatesv1.CertificateSigningRequest{
Spec: certificatesv1.CertificateSigningRequestSpec{
Groups: []string{"system:authenticated"},
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: "1",
}
actualCsrSpec := createdCsr.Spec

if diff := cmp.Diff(expectedCsrSpec, actualCsrSpec); diff != "" {
t.Fatalf("CSR spec was 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 {
f, err := ioutil.TempFile("", "auth_test")
if err != nil {
Expand Down

0 comments on commit 96a18bd

Please sign in to comment.