Merge pull request #590 from enj/enj/f/sa_authn_impersonation_proxy
impersonator: add support for service account token authentication
This commit is contained in:
commit
1efa4da80c
@ -35,6 +35,13 @@ only shares this information with the audit stack). To keep things simple,
|
|||||||
we use the fake audit backend at the Metadata level for all requests. This
|
we use the fake audit backend at the Metadata level for all requests. This
|
||||||
guarantees that we always have an audit event on every request.
|
guarantees that we always have an audit event on every request.
|
||||||
|
|
||||||
|
One final wrinkle is that impersonation cannot impersonate UIDs (yet). This is
|
||||||
|
problematic because service account tokens always assert a UID. To handle this
|
||||||
|
case without losing authentication information, when we see an identity with a
|
||||||
|
UID that was asserted via a bearer token, we simply pass the request through
|
||||||
|
with the original bearer token and no impersonation headers set (as if the user
|
||||||
|
had made the request directly against the Kubernetes API server).
|
||||||
|
|
||||||
For all normal requests, we only use http/2.0 when proxying to the API server.
|
For all normal requests, we only use http/2.0 when proxying to the API server.
|
||||||
For upgrade requests, we only use http/1.1 since these always go from http/1.1
|
For upgrade requests, we only use http/1.1 since these always go from http/1.1
|
||||||
to either websockets or SPDY.
|
to either websockets or SPDY.
|
||||||
|
@ -15,6 +15,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -26,6 +28,8 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||||
"k8s.io/apiserver/pkg/audit/policy"
|
"k8s.io/apiserver/pkg/audit/policy"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/filterlatency"
|
"k8s.io/apiserver/pkg/endpoints/filterlatency"
|
||||||
@ -45,6 +49,7 @@ import (
|
|||||||
"go.pinniped.dev/internal/httputil/securityheader"
|
"go.pinniped.dev/internal/httputil/securityheader"
|
||||||
"go.pinniped.dev/internal/kubeclient"
|
"go.pinniped.dev/internal/kubeclient"
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
|
"go.pinniped.dev/internal/valuelesscontext"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FactoryFunc is a function which can create an impersonator server.
|
// FactoryFunc is a function which can create an impersonator server.
|
||||||
@ -176,6 +181,11 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
// See the genericapiserver.DefaultBuildHandlerChain func for details.
|
// See the genericapiserver.DefaultBuildHandlerChain func for details.
|
||||||
handler = defaultBuildHandlerChainFunc(handler, c)
|
handler = defaultBuildHandlerChainFunc(handler, c)
|
||||||
|
|
||||||
|
// we need to grab the bearer token before WithAuthentication deletes it.
|
||||||
|
handler = filterlatency.TrackCompleted(handler)
|
||||||
|
handler = withBearerTokenPreservation(handler)
|
||||||
|
handler = filterlatency.TrackStarted(handler, "bearertokenpreservation")
|
||||||
|
|
||||||
// Always set security headers so browsers do the right thing.
|
// Always set security headers so browsers do the right thing.
|
||||||
handler = filterlatency.TrackCompleted(handler)
|
handler = filterlatency.TrackCompleted(handler)
|
||||||
handler = securityheader.Wrap(handler)
|
handler = securityheader.Wrap(handler)
|
||||||
@ -189,6 +199,9 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
serverConfig.AuditPolicyChecker = policy.FakeChecker(auditinternal.LevelMetadata, nil)
|
serverConfig.AuditPolicyChecker = policy.FakeChecker(auditinternal.LevelMetadata, nil)
|
||||||
serverConfig.AuditBackend = &auditfake.Backend{}
|
serverConfig.AuditBackend = &auditfake.Backend{}
|
||||||
|
|
||||||
|
// if we ever start unioning a TCR bearer token authenticator with serverConfig.Authenticator
|
||||||
|
// then we will need to update the related assumption in tokenPassthroughRoundTripper
|
||||||
|
|
||||||
delegatingAuthorizer := serverConfig.Authorization.Authorizer
|
delegatingAuthorizer := serverConfig.Authorization.Authorizer
|
||||||
nestedImpersonationAuthorizer := &comparableAuthorizer{
|
nestedImpersonationAuthorizer := &comparableAuthorizer{
|
||||||
authorizerFunc: func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
authorizerFunc: func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
@ -290,6 +303,35 @@ func (f authorizerFunc) Authorize(ctx context.Context, a authorizer.Attributes)
|
|||||||
return f(ctx, a)
|
return f(ctx, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withBearerTokenPreservation(delegate http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// this looks a bit hacky but lets us avoid writing any logic for parsing out the bearer token
|
||||||
|
var reqToken string
|
||||||
|
_, _, _ = bearertoken.New(authenticator.TokenFunc(func(_ context.Context, token string) (*authenticator.Response, bool, error) {
|
||||||
|
reqToken = token
|
||||||
|
return nil, false, nil
|
||||||
|
})).AuthenticateRequest(r)
|
||||||
|
|
||||||
|
// smuggle the token through the context. this does mean that we need to avoid logging the context.
|
||||||
|
if len(reqToken) != 0 {
|
||||||
|
ctx := context.WithValue(r.Context(), tokenKey, reqToken)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenFrom(ctx context.Context) string {
|
||||||
|
token, _ := ctx.Value(tokenKey).(string)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextKey type is unexported to prevent collisions.
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const tokenKey contextKey = iota
|
||||||
|
|
||||||
func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapiserver.Config) http.Handler, error) {
|
func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapiserver.Config) http.Handler, error) {
|
||||||
serverURL, err := url.Parse(restConfig.Host)
|
serverURL, err := url.Parse(restConfig.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -300,11 +342,19 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get http/1.1 round tripper: %w", err)
|
return nil, fmt.Errorf("could not get http/1.1 round tripper: %w", err)
|
||||||
}
|
}
|
||||||
|
http1RoundTripperAnonymous, err := getTransportForProtocol(rest.AnonymousClientConfig(restConfig), "http/1.1")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get http/1.1 anonymous round tripper: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
http2RoundTripper, err := getTransportForProtocol(restConfig, "h2")
|
http2RoundTripper, err := getTransportForProtocol(restConfig, "h2")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get http/2.0 round tripper: %w", err)
|
return nil, fmt.Errorf("could not get http/2.0 round tripper: %w", err)
|
||||||
}
|
}
|
||||||
|
http2RoundTripperAnonymous, err := getTransportForProtocol(rest.AnonymousClientConfig(restConfig), "h2")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get http/2.0 anonymous round tripper: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return func(c *genericapiserver.Config) http.Handler {
|
return func(c *genericapiserver.Config) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -347,15 +397,18 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// grab the request's bearer token if present. this is optional and does not fail the request if missing.
|
||||||
|
token := tokenFrom(r.Context())
|
||||||
|
|
||||||
// KAS only supports upgrades via http/1.1 to websockets/SPDY (upgrades never use http/2.0)
|
// KAS only supports upgrades via http/1.1 to websockets/SPDY (upgrades never use http/2.0)
|
||||||
// Thus we default to using http/2.0 when the request is not an upgrade, otherwise we use http/1.1
|
// Thus we default to using http/2.0 when the request is not an upgrade, otherwise we use http/1.1
|
||||||
baseRT := http2RoundTripper
|
baseRT, baseRTAnonymous := http2RoundTripper, http2RoundTripperAnonymous
|
||||||
isUpgradeRequest := httpstream.IsUpgradeRequest(r)
|
isUpgradeRequest := httpstream.IsUpgradeRequest(r)
|
||||||
if isUpgradeRequest {
|
if isUpgradeRequest {
|
||||||
baseRT = http1RoundTripper
|
baseRT, baseRTAnonymous = http1RoundTripper, http1RoundTripperAnonymous
|
||||||
}
|
}
|
||||||
|
|
||||||
rt, err := getTransportForUser(userInfo, baseRT, ae)
|
rt, err := getTransportForUser(r.Context(), userInfo, baseRT, baseRTAnonymous, ae, token, c.Authentication.Authenticator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
plog.WarningErr("rejecting request as we cannot act as the current user", err,
|
plog.WarningErr("rejecting request as we cannot act as the current user", err,
|
||||||
"url", r.URL.String(),
|
"url", r.URL.String(),
|
||||||
@ -413,8 +466,26 @@ func ensureNoImpersonationHeaders(r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTransportForUser(userInfo user.Info, delegate http.RoundTripper, ae *auditinternal.Event) (http.RoundTripper, error) {
|
func getTransportForUser(ctx context.Context, userInfo user.Info, delegate, delegateAnonymous http.RoundTripper, ae *auditinternal.Event, token string, authenticator authenticator.Request) (http.RoundTripper, error) {
|
||||||
|
if canImpersonateFully(userInfo) {
|
||||||
|
return standardImpersonationRoundTripper(userInfo, ae, delegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenPassthroughRoundTripper(ctx, delegateAnonymous, ae, token, authenticator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canImpersonateFully(userInfo user.Info) bool {
|
||||||
|
// nolint: gosimple // this structure is on purpose because we plan to expand this function
|
||||||
if len(userInfo.GetUID()) == 0 {
|
if len(userInfo.GetUID()) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// once kube supports UID impersonation, add logic to detect if the KAS is
|
||||||
|
// new enough to have this functionality and return true in that case as well
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func standardImpersonationRoundTripper(userInfo user.Info, ae *auditinternal.Event, delegate http.RoundTripper) (http.RoundTripper, error) {
|
||||||
extra, err := buildExtra(userInfo.GetExtra(), ae)
|
extra, err := buildExtra(userInfo.GetExtra(), ae)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -428,19 +499,84 @@ func getTransportForUser(userInfo user.Info, delegate http.RoundTripper, ae *aud
|
|||||||
// transport.NewImpersonatingRoundTripper clones the request before setting headers
|
// transport.NewImpersonatingRoundTripper clones the request before setting headers
|
||||||
// thus it will not accidentally mutate the input request (see http.Handler docs)
|
// thus it will not accidentally mutate the input request (see http.Handler docs)
|
||||||
return transport.NewImpersonatingRoundTripper(impersonateConfig, delegate), nil
|
return transport.NewImpersonatingRoundTripper(impersonateConfig, delegate), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenPassthroughRoundTripper(ctx context.Context, delegateAnonymous http.RoundTripper, ae *auditinternal.Event, token string, authenticator authenticator.Request) (http.RoundTripper, error) {
|
||||||
|
// all code below assumes KAS does not support UID impersonation because that case is handled in the standard path
|
||||||
|
|
||||||
|
// it also assumes that the TCR API does not issue tokens - if this assumption changes, we will need
|
||||||
|
// some way to distinguish a token that is only valid against this impersonation proxy and not against KAS.
|
||||||
|
// this code will fail closed because said TCR token would not work against KAS and the request would fail.
|
||||||
|
|
||||||
|
// if we get here we know the final user info had a UID
|
||||||
|
// if the original user is also performing a nested impersonation, it means that said nested
|
||||||
|
// impersonation is trying to impersonate a UID since final user info == ae.ImpersonatedUser
|
||||||
|
// we know this KAS does not support UID impersonation so this request must be rejected
|
||||||
|
if ae.ImpersonatedUser != nil {
|
||||||
|
return nil, constable.Error("unable to impersonate uid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0. in the case of a request that is not attempting to do nested impersonation
|
// see what KAS thinks this token translates into
|
||||||
// 1. if we make the assumption that the TCR API does not issue tokens (or pass the TCR API bearer token
|
// this is important because certs have precedence over tokens and we want
|
||||||
// authenticator into this func - we need to know the authentication cred is something KAS would honor)
|
// to make sure that we do not get confused and pass along the wrong token
|
||||||
// 2. then if preserve the incoming authorization header into the request's context
|
tokenUser, err := tokenReview(ctx, token, authenticator)
|
||||||
// 3. we could reauthenticate it here (it would be a free cache hit)
|
if err != nil {
|
||||||
// 4. confirm that it matches the passed in user info (i.e. it was actually the cred used to authenticate and not a client cert)
|
return nil, err
|
||||||
// 5. then we could issue a reverse proxy request using an anonymous rest config and the bearer token
|
}
|
||||||
// 6. thus instead of impersonating the user, we would just be passing their request through
|
|
||||||
// 7. this would preserve the UID info and thus allow us to safely support all token based auth
|
// we want to compare the result of the token authentication with the original user that made the request
|
||||||
// 8. the above would be safe even if in the future Kube started supporting UIDs asserted by client certs
|
// if the user who made the request and the token do not match, we cannot go any further at this point
|
||||||
return nil, constable.Error("unexpected uid")
|
if !apiequality.Semantic.DeepEqual(ae.User, tokenUser) {
|
||||||
|
// this info leak seems fine for trace level logs
|
||||||
|
plog.Trace("failed to passthrough token due to user mismatch",
|
||||||
|
"original-username", ae.User.Username,
|
||||||
|
"original-uid", ae.User.UID,
|
||||||
|
"token-username", tokenUser.Username,
|
||||||
|
"token-uid", tokenUser.UID,
|
||||||
|
)
|
||||||
|
return nil, constable.Error("token authenticated as a different user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we know that if we send this token to KAS, it will authenticate correctly
|
||||||
|
return transport.NewBearerAuthRoundTripper(token, delegateAnonymous), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenReview(ctx context.Context, token string, authenticator authenticator.Request) (authenticationv1.UserInfo, error) {
|
||||||
|
if len(token) == 0 {
|
||||||
|
return authenticationv1.UserInfo{}, constable.Error("no token on request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a header that contains nothing but the token
|
||||||
|
// an astute observer may ask "but what about the token's audience?"
|
||||||
|
// in this case, we want to leave audiences unset per the token review docs:
|
||||||
|
// > If no audiences are provided, the audience will default to the audience of the Kubernetes apiserver.
|
||||||
|
// i.e. we want to make sure that the given token is valid against KAS
|
||||||
|
fakeReq := &http.Request{Header: http.Header{}}
|
||||||
|
fakeReq.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
// propagate cancellation of parent context (without any values such as audience)
|
||||||
|
fakeReq = fakeReq.WithContext(valuelesscontext.New(ctx))
|
||||||
|
|
||||||
|
// this will almost always be a free call that hits our 10 second cache TTL
|
||||||
|
resp, ok, err := authenticator.AuthenticateRequest(fakeReq)
|
||||||
|
if err != nil {
|
||||||
|
return authenticationv1.UserInfo{}, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return authenticationv1.UserInfo{}, constable.Error("token failed to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenUser := authenticationv1.UserInfo{
|
||||||
|
Username: resp.User.GetName(),
|
||||||
|
UID: resp.User.GetUID(),
|
||||||
|
Groups: resp.User.GetGroups(),
|
||||||
|
Extra: make(map[string]authenticationv1.ExtraValue, len(resp.User.GetExtra())),
|
||||||
|
}
|
||||||
|
for k, v := range resp.User.GetExtra() {
|
||||||
|
tokenUser.Extra[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildExtra(extra map[string][]string, ae *auditinternal.Event) (map[string][]string, error) {
|
func buildExtra(extra map[string][]string, ae *auditinternal.Event) (map[string][]string, error) {
|
||||||
|
@ -22,6 +22,8 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
@ -33,6 +35,7 @@ import (
|
|||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
|
"go.pinniped.dev/internal/constable"
|
||||||
"go.pinniped.dev/internal/dynamiccert"
|
"go.pinniped.dev/internal/dynamiccert"
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/internal/httputil/roundtripper"
|
"go.pinniped.dev/internal/httputil/roundtripper"
|
||||||
@ -176,6 +179,26 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "when there is no client cert on request but it has basic auth, it is still an anonymous request",
|
||||||
|
clientCert: &clientCert{},
|
||||||
|
clientMutateHeaders: func(header http.Header) {
|
||||||
|
header.Set("Test", "val")
|
||||||
|
req := &http.Request{Header: header}
|
||||||
|
req.SetBasicAuth("foo", "bar")
|
||||||
|
},
|
||||||
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
|
wantKubeAPIServerRequestHeaders: http.Header{
|
||||||
|
"Impersonate-User": {"system:anonymous"},
|
||||||
|
"Impersonate-Group": {"system:unauthenticated"},
|
||||||
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
|
"User-Agent": {"test-agent"},
|
||||||
|
"Accept": {"application/vnd.kubernetes.protobuf,application/json"},
|
||||||
|
"Accept-Encoding": {"gzip"},
|
||||||
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
|
"Test": {"val"},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "failed client cert authentication",
|
name: "failed client cert authentication",
|
||||||
clientCert: newClientCert(t, unrelatedCA, "test-username", []string{"test-group1"}),
|
clientCert: newClientCert(t, unrelatedCA, "test-username", []string{"test-group1"}),
|
||||||
@ -499,39 +522,12 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
"extra-2": {"some", "more", "extra", "stuff"},
|
"extra-2": {"some", "more", "extra", "stuff"},
|
||||||
}
|
}
|
||||||
|
|
||||||
validURL, _ := url.Parse("http://pinniped.dev/blah")
|
|
||||||
newRequest := func(h http.Header, userInfo user.Info, event *auditinternal.Event) *http.Request {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if userInfo != nil {
|
|
||||||
ctx = request.WithUser(ctx, userInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
ae := &auditinternal.Event{Level: auditinternal.LevelMetadata}
|
|
||||||
if event != nil {
|
|
||||||
ae = event
|
|
||||||
}
|
|
||||||
ctx = request.WithAuditEvent(ctx, ae)
|
|
||||||
|
|
||||||
reqInfo := &request.RequestInfo{
|
|
||||||
IsResourceRequest: false,
|
|
||||||
Path: validURL.Path,
|
|
||||||
Verb: "get",
|
|
||||||
}
|
|
||||||
ctx = request.WithRequestInfo(ctx, reqInfo)
|
|
||||||
|
|
||||||
r, err := http.NewRequestWithContext(ctx, http.MethodGet, validURL.String(), nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
r.Header = h
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
restConfig *rest.Config
|
restConfig *rest.Config
|
||||||
wantCreationErr string
|
wantCreationErr string
|
||||||
request *http.Request
|
request *http.Request
|
||||||
|
authenticator authenticator.Request
|
||||||
wantHTTPBody string
|
wantHTTPBody string
|
||||||
wantHTTPStatus int
|
wantHTTPStatus int
|
||||||
wantKubeAPIServerRequestHeaders http.Header
|
wantKubeAPIServerRequestHeaders http.Header
|
||||||
@ -563,50 +559,50 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Impersonate-User header already in request",
|
name: "Impersonate-User header already in request",
|
||||||
request: newRequest(map[string][]string{"Impersonate-User": {"some-user"}}, nil, nil),
|
request: newRequest(t, map[string][]string{"Impersonate-User": {"some-user"}}, nil, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Impersonate-Group header already in request",
|
name: "Impersonate-Group header already in request",
|
||||||
request: newRequest(map[string][]string{"Impersonate-Group": {"some-group"}}, nil, nil),
|
request: newRequest(t, map[string][]string{"Impersonate-Group": {"some-group"}}, nil, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Impersonate-Extra header already in request",
|
name: "Impersonate-Extra header already in request",
|
||||||
request: newRequest(map[string][]string{"Impersonate-Extra-something": {"something"}}, nil, nil),
|
request: newRequest(t, map[string][]string{"Impersonate-Extra-something": {"something"}}, nil, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Impersonate-* header already in request",
|
name: "Impersonate-* header already in request",
|
||||||
request: newRequest(map[string][]string{"Impersonate-Something": {"some-newfangled-impersonate-header"}}, nil, nil),
|
request: newRequest(t, map[string][]string{"Impersonate-Something": {"some-newfangled-impersonate-header"}}, nil, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid impersonation","reason":"InternalError","details":{"causes":[{"message":"invalid impersonation"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unexpected authorization header",
|
name: "unexpected authorization header",
|
||||||
request: newRequest(map[string][]string{"Authorization": {"panda"}}, nil, nil),
|
request: newRequest(t, map[string][]string{"Authorization": {"panda"}}, nil, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid authorization header","reason":"InternalError","details":{"causes":[{"message":"invalid authorization header"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid authorization header","reason":"InternalError","details":{"causes":[{"message":"invalid authorization header"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing user",
|
name: "missing user",
|
||||||
request: newRequest(map[string][]string{}, nil, nil),
|
request: newRequest(t, map[string][]string{}, nil, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid user","reason":"InternalError","details":{"causes":[{"message":"invalid user"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: invalid user","reason":"InternalError","details":{"causes":[{"message":"invalid user"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unexpected UID",
|
name: "unexpected UID",
|
||||||
request: newRequest(map[string][]string{}, &user.DefaultInfo{UID: "007"}, nil),
|
request: newRequest(t, map[string][]string{}, &user.DefaultInfo{UID: "007"}, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated user but missing audit event",
|
name: "authenticated user but missing audit event",
|
||||||
request: func() *http.Request {
|
request: func() *http.Request {
|
||||||
req := newRequest(map[string][]string{
|
req := newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Connection": {"Upgrade"},
|
"Connection": {"Upgrade"},
|
||||||
"Upgrade": {"some-upgrade"},
|
"Upgrade": {"some-upgrade"},
|
||||||
@ -615,7 +611,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
Name: testUser,
|
Name: testUser,
|
||||||
Groups: testGroups,
|
Groups: testGroups,
|
||||||
Extra: testExtra,
|
Extra: testExtra,
|
||||||
}, nil)
|
}, nil, "")
|
||||||
ctx := request.WithAuditEvent(req.Context(), nil)
|
ctx := request.WithAuditEvent(req.Context(), nil)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
return req
|
return req
|
||||||
@ -625,7 +621,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated user with upper case extra",
|
name: "authenticated user with upper case extra",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Connection": {"Upgrade"},
|
"Connection": {"Upgrade"},
|
||||||
"Upgrade": {"some-upgrade"},
|
"Upgrade": {"some-upgrade"},
|
||||||
@ -639,13 +635,13 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
"valid-key": {"valid-value"},
|
"valid-key": {"valid-value"},
|
||||||
"Invalid-key": {"still-valid-value"},
|
"Invalid-key": {"still-valid-value"},
|
||||||
},
|
},
|
||||||
}, nil),
|
}, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated user with upper case extra across multiple lines",
|
name: "authenticated user with upper case extra across multiple lines",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Connection": {"Upgrade"},
|
"Connection": {"Upgrade"},
|
||||||
"Upgrade": {"some-upgrade"},
|
"Upgrade": {"some-upgrade"},
|
||||||
@ -659,13 +655,13 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
"valid-key": {"valid-value"},
|
"valid-key": {"valid-value"},
|
||||||
"valid-data\nInvalid-key": {"still-valid-value"},
|
"valid-data\nInvalid-key": {"still-valid-value"},
|
||||||
},
|
},
|
||||||
}, nil),
|
}, nil, ""),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated user with reserved extra key",
|
name: "authenticated user with reserved extra key",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Connection": {"Upgrade"},
|
"Connection": {"Upgrade"},
|
||||||
"Upgrade": {"some-upgrade"},
|
"Upgrade": {"some-upgrade"},
|
||||||
@ -679,14 +675,164 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
"valid-key": {"valid-value"},
|
"valid-key": {"valid-value"},
|
||||||
"foo.impersonation-proxy.concierge.pinniped.dev": {"still-valid-value"},
|
"foo.impersonation-proxy.concierge.pinniped.dev": {"still-valid-value"},
|
||||||
},
|
},
|
||||||
}, nil),
|
}, nil, ""),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "authenticated user with UID but no bearer token",
|
||||||
|
request: newRequest(t, map[string][]string{
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Content-Length": {"some-length"},
|
||||||
|
"Other-Header": {"test-header-value-1"},
|
||||||
|
}, &user.DefaultInfo{
|
||||||
|
UID: "-", // anything non-empty, rest of the fields get ignored in this code path
|
||||||
|
},
|
||||||
|
&auditinternal.Event{
|
||||||
|
User: authenticationv1.UserInfo{
|
||||||
|
Username: testUser,
|
||||||
|
UID: "fancy-uid",
|
||||||
|
Groups: testGroups,
|
||||||
|
Extra: map[string]authenticationv1.ExtraValue{
|
||||||
|
"extra-1": {"some", "extra", "stuff"},
|
||||||
|
"extra-2": {"some", "more", "extra", "stuff"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImpersonatedUser: nil,
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
authenticator: nil,
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "authenticated user with UID and bearer token and nested impersonation",
|
||||||
|
request: newRequest(t, map[string][]string{
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Content-Length": {"some-length"},
|
||||||
|
"Other-Header": {"test-header-value-1"},
|
||||||
|
}, &user.DefaultInfo{
|
||||||
|
UID: "-", // anything non-empty, rest of the fields get ignored in this code path
|
||||||
|
},
|
||||||
|
&auditinternal.Event{
|
||||||
|
User: authenticationv1.UserInfo{
|
||||||
|
Username: "dude",
|
||||||
|
UID: "--1--",
|
||||||
|
Groups: []string{"--a--", "--b--"},
|
||||||
|
Extra: map[string]authenticationv1.ExtraValue{
|
||||||
|
"--c--": {"--d--"},
|
||||||
|
"--e--": {"--f--"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||||
|
},
|
||||||
|
"token-from-user-nested",
|
||||||
|
),
|
||||||
|
authenticator: nil,
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "authenticated user with UID and bearer token results in error",
|
||||||
|
request: newRequest(t, map[string][]string{
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Content-Length": {"some-length"},
|
||||||
|
"Other-Header": {"test-header-value-1"},
|
||||||
|
}, &user.DefaultInfo{
|
||||||
|
UID: "-", // anything non-empty, rest of the fields get ignored in this code path
|
||||||
|
},
|
||||||
|
&auditinternal.Event{
|
||||||
|
User: authenticationv1.UserInfo{
|
||||||
|
Username: "dude",
|
||||||
|
UID: "--1--",
|
||||||
|
Groups: []string{"--a--", "--b--"},
|
||||||
|
Extra: map[string]authenticationv1.ExtraValue{
|
||||||
|
"--c--": {"--d--"},
|
||||||
|
"--e--": {"--f--"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImpersonatedUser: nil,
|
||||||
|
},
|
||||||
|
"some-non-empty-token",
|
||||||
|
),
|
||||||
|
authenticator: testTokenAuthenticator(t, "", nil, constable.Error("some err")),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "authenticated user with UID and bearer token does not authenticate",
|
||||||
|
request: newRequest(t, map[string][]string{
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Content-Length": {"some-length"},
|
||||||
|
"Other-Header": {"test-header-value-1"},
|
||||||
|
}, &user.DefaultInfo{
|
||||||
|
UID: "-", // anything non-empty, rest of the fields get ignored in this code path
|
||||||
|
},
|
||||||
|
&auditinternal.Event{
|
||||||
|
User: authenticationv1.UserInfo{
|
||||||
|
Username: "dude",
|
||||||
|
UID: "--1--",
|
||||||
|
Groups: []string{"--a--", "--b--"},
|
||||||
|
Extra: map[string]authenticationv1.ExtraValue{
|
||||||
|
"--c--": {"--d--"},
|
||||||
|
"--e--": {"--f--"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImpersonatedUser: nil,
|
||||||
|
},
|
||||||
|
"this-token-does-not-work",
|
||||||
|
),
|
||||||
|
authenticator: testTokenAuthenticator(t, "some-other-token-works", nil, nil),
|
||||||
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "authenticated user with UID and bearer token authenticates as different user",
|
||||||
|
request: newRequest(t, map[string][]string{
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Content-Length": {"some-length"},
|
||||||
|
"Other-Header": {"test-header-value-1"},
|
||||||
|
}, &user.DefaultInfo{
|
||||||
|
UID: "-", // anything non-empty, rest of the fields get ignored in this code path
|
||||||
|
},
|
||||||
|
&auditinternal.Event{
|
||||||
|
User: authenticationv1.UserInfo{
|
||||||
|
Username: "dude",
|
||||||
|
UID: "--1--",
|
||||||
|
Groups: []string{"--a--", "--b--"},
|
||||||
|
Extra: map[string]authenticationv1.ExtraValue{
|
||||||
|
"--c--": {"--d--"},
|
||||||
|
"--e--": {"--f--"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImpersonatedUser: nil,
|
||||||
|
},
|
||||||
|
"this-token-does-work",
|
||||||
|
),
|
||||||
|
authenticator: testTokenAuthenticator(t, "this-token-does-work", &user.DefaultInfo{Name: "someone-else"}, nil),
|
||||||
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
wantHTTPBody: `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: unimplemented functionality - unable to act as current user","reason":"InternalError","details":{"causes":[{"message":"unimplemented functionality - unable to act as current user"}]},"code":500}` + "\n",
|
||||||
wantHTTPStatus: http.StatusInternalServerError,
|
wantHTTPStatus: http.StatusInternalServerError,
|
||||||
},
|
},
|
||||||
// happy path
|
// happy path
|
||||||
{
|
{
|
||||||
name: "authenticated user",
|
name: "authenticated user",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Accept": {"some-accepted-format"},
|
"Accept": {"some-accepted-format"},
|
||||||
"Accept-Encoding": {"some-accepted-encoding"},
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
@ -699,7 +845,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
Name: testUser,
|
Name: testUser,
|
||||||
Groups: testGroups,
|
Groups: testGroups,
|
||||||
Extra: testExtra,
|
Extra: testExtra,
|
||||||
}, nil),
|
}, nil, ""),
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Authorization": {"Bearer some-service-account-token"},
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
"Impersonate-Extra-Extra-1": {"some", "extra", "stuff"},
|
"Impersonate-Extra-Extra-1": {"some", "extra", "stuff"},
|
||||||
@ -717,9 +863,61 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
wantHTTPBody: "successful proxied response",
|
wantHTTPBody: "successful proxied response",
|
||||||
wantHTTPStatus: http.StatusOK,
|
wantHTTPStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "authenticated user with UID and bearer token",
|
||||||
|
request: newRequest(t, map[string][]string{
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Accept": {"some-accepted-format"},
|
||||||
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Content-Length": {"some-length"},
|
||||||
|
"Other-Header": {"test-header-value-1"},
|
||||||
|
}, &user.DefaultInfo{
|
||||||
|
UID: "-", // anything non-empty, rest of the fields get ignored in this code path
|
||||||
|
},
|
||||||
|
&auditinternal.Event{
|
||||||
|
User: authenticationv1.UserInfo{
|
||||||
|
Username: testUser,
|
||||||
|
UID: "fancy-uid",
|
||||||
|
Groups: testGroups,
|
||||||
|
Extra: map[string]authenticationv1.ExtraValue{
|
||||||
|
"extra-1": {"some", "extra", "stuff"},
|
||||||
|
"extra-2": {"some", "more", "extra", "stuff"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImpersonatedUser: nil,
|
||||||
|
},
|
||||||
|
"token-from-user",
|
||||||
|
),
|
||||||
|
authenticator: testTokenAuthenticator(
|
||||||
|
t,
|
||||||
|
"token-from-user",
|
||||||
|
&user.DefaultInfo{
|
||||||
|
Name: testUser,
|
||||||
|
UID: "fancy-uid",
|
||||||
|
Groups: testGroups,
|
||||||
|
Extra: testExtra,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
|
"Authorization": {"Bearer token-from-user"},
|
||||||
|
"User-Agent": {"test-user-agent"},
|
||||||
|
"Accept": {"some-accepted-format"},
|
||||||
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
|
"Connection": {"Upgrade"},
|
||||||
|
"Upgrade": {"some-upgrade"},
|
||||||
|
"Content-Type": {"some-type"},
|
||||||
|
"Other-Header": {"test-header-value-1"},
|
||||||
|
},
|
||||||
|
wantHTTPBody: "successful proxied response",
|
||||||
|
wantHTTPStatus: http.StatusOK,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated gke user",
|
name: "authenticated gke user",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Accept": {"some-accepted-format"},
|
"Accept": {"some-accepted-format"},
|
||||||
"Accept-Encoding": {"some-accepted-encoding"},
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
@ -736,7 +934,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
"iam.gke.io/user-assertion": {"ABC"},
|
"iam.gke.io/user-assertion": {"ABC"},
|
||||||
"user-assertion.cloud.google.com": {"XYZ"},
|
"user-assertion.cloud.google.com": {"XYZ"},
|
||||||
},
|
},
|
||||||
}, nil),
|
}, nil, ""),
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Authorization": {"Bearer some-service-account-token"},
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
"Impersonate-Extra-Iam.gke.io%2fuser-Assertion": {"ABC"},
|
"Impersonate-Extra-Iam.gke.io%2fuser-Assertion": {"ABC"},
|
||||||
@ -756,7 +954,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated openshift/openstack user",
|
name: "authenticated openshift/openstack user",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Accept": {"some-accepted-format"},
|
"Accept": {"some-accepted-format"},
|
||||||
"Accept-Encoding": {"some-accepted-encoding"},
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
@ -781,7 +979,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
"alpha.kubernetes.io/identity/user/domain/id": {"domain-id"},
|
"alpha.kubernetes.io/identity/user/domain/id": {"domain-id"},
|
||||||
"alpha.kubernetes.io/identity/user/domain/name": {"domain-name"},
|
"alpha.kubernetes.io/identity/user/domain/name": {"domain-name"},
|
||||||
},
|
},
|
||||||
}, nil),
|
}, nil, ""),
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Authorization": {"Bearer some-service-account-token"},
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
"Impersonate-Extra-Scopes.authorization.openshift.io": {"user:info", "user:full"},
|
"Impersonate-Extra-Scopes.authorization.openshift.io": {"user:info", "user:full"},
|
||||||
@ -805,7 +1003,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated user with almost reserved key",
|
name: "authenticated user with almost reserved key",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Accept": {"some-accepted-format"},
|
"Accept": {"some-accepted-format"},
|
||||||
"Accept-Encoding": {"some-accepted-encoding"},
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
@ -820,7 +1018,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
Extra: map[string][]string{
|
Extra: map[string][]string{
|
||||||
"foo.iimpersonation-proxy.concierge.pinniped.dev": {"still-valid-value"},
|
"foo.iimpersonation-proxy.concierge.pinniped.dev": {"still-valid-value"},
|
||||||
},
|
},
|
||||||
}, nil),
|
}, nil, ""),
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Authorization": {"Bearer some-service-account-token"},
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
"Impersonate-Extra-Foo.iimpersonation-Proxy.concierge.pinniped.dev": {"still-valid-value"},
|
"Impersonate-Extra-Foo.iimpersonation-Proxy.concierge.pinniped.dev": {"still-valid-value"},
|
||||||
@ -839,7 +1037,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated user with almost reserved key and nested impersonation",
|
name: "authenticated user with almost reserved key and nested impersonation",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Accept": {"some-accepted-format"},
|
"Accept": {"some-accepted-format"},
|
||||||
"Accept-Encoding": {"some-accepted-encoding"},
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
@ -866,6 +1064,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ImpersonatedUser: &authenticationv1.UserInfo{},
|
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
),
|
),
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Authorization": {"Bearer some-service-account-token"},
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
@ -886,7 +1085,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated user with nested impersonation",
|
name: "authenticated user with nested impersonation",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Accept": {"some-accepted-format"},
|
"Accept": {"some-accepted-format"},
|
||||||
"Accept-Encoding": {"some-accepted-encoding"},
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
@ -912,6 +1111,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ImpersonatedUser: &authenticationv1.UserInfo{},
|
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
),
|
),
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Authorization": {"Bearer some-service-account-token"},
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
@ -933,7 +1133,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated gke user with nested impersonation",
|
name: "authenticated gke user with nested impersonation",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Accept": {"some-accepted-format"},
|
"Accept": {"some-accepted-format"},
|
||||||
"Accept-Encoding": {"some-accepted-encoding"},
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
@ -959,6 +1159,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ImpersonatedUser: &authenticationv1.UserInfo{},
|
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
),
|
),
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Authorization": {"Bearer some-service-account-token"},
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
@ -980,7 +1181,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "authenticated user with nested impersonation of gke user",
|
name: "authenticated user with nested impersonation of gke user",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
"Accept": {"some-accepted-format"},
|
"Accept": {"some-accepted-format"},
|
||||||
"Accept-Encoding": {"some-accepted-encoding"},
|
"Accept-Encoding": {"some-accepted-encoding"},
|
||||||
@ -1010,6 +1211,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ImpersonatedUser: &authenticationv1.UserInfo{},
|
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||||
},
|
},
|
||||||
|
"",
|
||||||
),
|
),
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Authorization": {"Bearer some-service-account-token"},
|
"Authorization": {"Bearer some-service-account-token"},
|
||||||
@ -1031,13 +1233,13 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "user is authenticated but the kube API request returns an error",
|
name: "user is authenticated but the kube API request returns an error",
|
||||||
request: newRequest(map[string][]string{
|
request: newRequest(t, map[string][]string{
|
||||||
"User-Agent": {"test-user-agent"},
|
"User-Agent": {"test-user-agent"},
|
||||||
}, &user.DefaultInfo{
|
}, &user.DefaultInfo{
|
||||||
Name: testUser,
|
Name: testUser,
|
||||||
Groups: testGroups,
|
Groups: testGroups,
|
||||||
Extra: testExtra,
|
Extra: testExtra,
|
||||||
}, nil),
|
}, nil, ""),
|
||||||
kubeAPIServerStatusCode: http.StatusNotFound,
|
kubeAPIServerStatusCode: http.StatusNotFound,
|
||||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||||
"Accept-Encoding": {"gzip"}, // because the rest client used in this test does not disable compression
|
"Accept-Encoding": {"gzip"}, // because the rest client used in this test does not disable compression
|
||||||
@ -1095,6 +1297,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
metav1.AddToGroupVersion(scheme, metav1.Unversioned)
|
metav1.AddToGroupVersion(scheme, metav1.Unversioned)
|
||||||
codecs := serializer.NewCodecFactory(scheme)
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
serverConfig := genericapiserver.NewRecommendedConfig(codecs)
|
serverConfig := genericapiserver.NewRecommendedConfig(codecs)
|
||||||
|
serverConfig.Authentication.Authenticator = tt.authenticator
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
@ -1137,6 +1340,83 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRequest(t *testing.T, h http.Header, userInfo user.Info, event *auditinternal.Event, token string) *http.Request {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
validURL, err := url.Parse("http://pinniped.dev/blah")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if userInfo != nil {
|
||||||
|
ctx = request.WithUser(ctx, userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
ae := &auditinternal.Event{Level: auditinternal.LevelMetadata}
|
||||||
|
if event != nil {
|
||||||
|
ae = event
|
||||||
|
}
|
||||||
|
ctx = request.WithAuditEvent(ctx, ae)
|
||||||
|
|
||||||
|
reqInfo := &request.RequestInfo{
|
||||||
|
IsResourceRequest: false,
|
||||||
|
Path: validURL.Path,
|
||||||
|
Verb: "get",
|
||||||
|
}
|
||||||
|
ctx = request.WithRequestInfo(ctx, reqInfo)
|
||||||
|
|
||||||
|
ctx = authenticator.WithAudiences(ctx, authenticator.Audiences{"must-be-ignored"})
|
||||||
|
|
||||||
|
if len(token) != 0 {
|
||||||
|
ctx = context.WithValue(ctx, tokenKey, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Hour))
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
r, err := http.NewRequestWithContext(ctx, http.MethodGet, validURL.String(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r.Header = h
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTokenAuthenticator(t *testing.T, token string, userInfo user.Info, err error) authenticator.Request {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
return authenticator.RequestFunc(func(r *http.Request) (*authenticator.Response, bool, error) {
|
||||||
|
if auds, ok := authenticator.AudiencesFrom(r.Context()); ok || len(auds) != 0 {
|
||||||
|
t.Errorf("unexpected audiences on request: %v", auds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctxToken := tokenFrom(r.Context()); len(ctxToken) != 0 {
|
||||||
|
t.Errorf("unexpected token on request: %v", ctxToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := r.Context().Deadline(); !ok {
|
||||||
|
t.Error("request should always have deadline")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqToken string
|
||||||
|
_, _, _ = bearertoken.New(authenticator.TokenFunc(func(_ context.Context, token string) (*authenticator.Response, bool, error) {
|
||||||
|
reqToken = token
|
||||||
|
return nil, false, nil
|
||||||
|
})).AuthenticateRequest(r)
|
||||||
|
|
||||||
|
if reqToken != token {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &authenticator.Response{User: userInfo}, true, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type clientCert struct {
|
type clientCert struct {
|
||||||
certPEM, keyPEM []byte
|
certPEM, keyPEM []byte
|
||||||
}
|
}
|
||||||
@ -1242,7 +1522,9 @@ func Test_deleteKnownImpersonationHeaders(t *testing.T) {
|
|||||||
inputReq := (&http.Request{Header: tt.headers}).WithContext(context.Background())
|
inputReq := (&http.Request{Header: tt.headers}).WithContext(context.Background())
|
||||||
inputReqCopy := inputReq.Clone(inputReq.Context())
|
inputReqCopy := inputReq.Clone(inputReq.Context())
|
||||||
|
|
||||||
|
var called bool
|
||||||
delegate := http.HandlerFunc(func(w http.ResponseWriter, outputReq *http.Request) {
|
delegate := http.HandlerFunc(func(w http.ResponseWriter, outputReq *http.Request) {
|
||||||
|
called = true
|
||||||
require.Nil(t, w)
|
require.Nil(t, w)
|
||||||
|
|
||||||
// assert only headers mutated
|
// assert only headers mutated
|
||||||
@ -1259,6 +1541,85 @@ func Test_deleteKnownImpersonationHeaders(t *testing.T) {
|
|||||||
|
|
||||||
deleteKnownImpersonationHeaders(delegate).ServeHTTP(nil, inputReq)
|
deleteKnownImpersonationHeaders(delegate).ServeHTTP(nil, inputReq)
|
||||||
require.Equal(t, inputReqCopy, inputReq) // assert no mutation occurred
|
require.Equal(t, inputReqCopy, inputReq) // assert no mutation occurred
|
||||||
|
require.True(t, called)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_withBearerTokenPreservation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
headers http.Header
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "has bearer token",
|
||||||
|
headers: map[string][]string{
|
||||||
|
"Authorization": {"Bearer thingy"},
|
||||||
|
},
|
||||||
|
want: "thingy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has bearer token but too many preceding spaces",
|
||||||
|
headers: map[string][]string{
|
||||||
|
"Authorization": {"Bearer 1"},
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has bearer token with space, only keeps first part",
|
||||||
|
headers: map[string][]string{
|
||||||
|
"Authorization": {"Bearer panda man"},
|
||||||
|
},
|
||||||
|
want: "panda",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has bearer token with surrounding whitespace",
|
||||||
|
headers: map[string][]string{
|
||||||
|
"Authorization": {" Bearer cool beans "},
|
||||||
|
},
|
||||||
|
want: "cool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has multiple bearer tokens",
|
||||||
|
headers: map[string][]string{
|
||||||
|
"Authorization": {"Bearer this thing", "what does this mean?"},
|
||||||
|
},
|
||||||
|
want: "this",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no bearer token",
|
||||||
|
headers: map[string][]string{
|
||||||
|
"Not-Authorization": {"Bearer not a token"},
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
inputReq := (&http.Request{Header: tt.headers}).WithContext(context.Background())
|
||||||
|
inputReqCopy := inputReq.Clone(inputReq.Context())
|
||||||
|
|
||||||
|
var called bool
|
||||||
|
delegate := http.HandlerFunc(func(w http.ResponseWriter, outputReq *http.Request) {
|
||||||
|
called = true
|
||||||
|
require.Nil(t, w)
|
||||||
|
|
||||||
|
// assert only context is mutated
|
||||||
|
outputReqCopy := outputReq.Clone(inputReq.Context())
|
||||||
|
require.Equal(t, inputReqCopy, outputReqCopy)
|
||||||
|
|
||||||
|
require.Equal(t, tt.want, tokenFrom(outputReq.Context()))
|
||||||
|
|
||||||
|
if len(tt.want) == 0 {
|
||||||
|
require.True(t, inputReq == outputReq, "expect req to passed through when no token expected")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
withBearerTokenPreservation(delegate).ServeHTTP(nil, inputReq)
|
||||||
|
require.Equal(t, inputReqCopy, inputReq) // assert no mutation occurred
|
||||||
|
require.True(t, called)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||||
"go.pinniped.dev/internal/constable"
|
"go.pinniped.dev/internal/constable"
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
|
"go.pinniped.dev/internal/valuelesscontext"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNoSuchAuthenticator is returned by Cache.AuthenticateTokenCredentialRequest() when the requested authenticator is not configured.
|
// ErrNoSuchAuthenticator is returned by Cache.AuthenticateTokenCredentialRequest() when the requested authenticator is not configured.
|
||||||
@ -101,7 +102,7 @@ func (c *Cache) AuthenticateTokenCredentialRequest(ctx context.Context, req *log
|
|||||||
|
|
||||||
// The incoming context could have an audience. Since we do not want to handle audiences right now, do not pass it
|
// The incoming context could have an audience. Since we do not want to handle audiences right now, do not pass it
|
||||||
// through directly to the authentication webhook.
|
// through directly to the authentication webhook.
|
||||||
ctx = valuelessContext{ctx}
|
ctx = valuelesscontext.New(ctx)
|
||||||
|
|
||||||
// Call the selected authenticator.
|
// Call the selected authenticator.
|
||||||
resp, authenticated, err := val.AuthenticateToken(ctx, req.Spec.Token)
|
resp, authenticated, err := val.AuthenticateToken(ctx, req.Spec.Token)
|
||||||
@ -119,7 +120,3 @@ func (c *Cache) AuthenticateTokenCredentialRequest(ctx context.Context, req *log
|
|||||||
}
|
}
|
||||||
return respUser, nil
|
return respUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type valuelessContext struct{ context.Context }
|
|
||||||
|
|
||||||
func (valuelessContext) Value(interface{}) interface{} { return nil }
|
|
||||||
|
14
internal/valuelesscontext/valuelesscontext.go
Normal file
14
internal/valuelesscontext/valuelesscontext.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package valuelesscontext
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func New(ctx context.Context) context.Context {
|
||||||
|
return valuelessContext{Context: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
type valuelessContext struct{ context.Context }
|
||||||
|
|
||||||
|
func (valuelessContext) Value(interface{}) interface{} { return nil }
|
@ -6,7 +6,11 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
@ -28,6 +32,8 @@ import (
|
|||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
authenticationv1 "k8s.io/api/authentication/v1"
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
authorizationv1 "k8s.io/api/authorization/v1"
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
|
certificatesv1 "k8s.io/api/certificates/v1"
|
||||||
|
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
@ -42,6 +48,8 @@ import (
|
|||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/transport"
|
"k8s.io/client-go/transport"
|
||||||
|
"k8s.io/client-go/util/cert"
|
||||||
|
"k8s.io/client-go/util/certificate/csr"
|
||||||
"k8s.io/client-go/util/keyutil"
|
"k8s.io/client-go/util/keyutil"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
@ -780,32 +788,148 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
whoAmI,
|
whoAmI,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test using a service account token. Authenticating as Service Accounts through the impersonation
|
// Test using a service account token.
|
||||||
// proxy is not supported, so it should fail.
|
|
||||||
namespaceName := createTestNamespace(t, adminClient)
|
namespaceName := createTestNamespace(t, adminClient)
|
||||||
_, saToken, _ := createServiceAccountToken(ctx, t, adminClient, namespaceName)
|
saName, saToken, _ := createServiceAccountToken(ctx, t, adminClient, namespaceName)
|
||||||
impersonationProxyServiceAccountPinnipedConciergeClient := newImpersonationProxyClientWithCredentials(t,
|
impersonationProxyServiceAccountPinnipedConciergeClient := newImpersonationProxyClientWithCredentials(t,
|
||||||
&loginv1alpha1.ClusterCredential{Token: saToken},
|
&loginv1alpha1.ClusterCredential{Token: saToken},
|
||||||
impersonationProxyURL, impersonationProxyCACertPEM, nil).PinnipedConcierge
|
impersonationProxyURL, impersonationProxyCACertPEM, nil).PinnipedConcierge
|
||||||
_, err = impersonationProxyServiceAccountPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
|
whoAmI, err = impersonationProxyServiceAccountPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
|
||||||
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||||
require.EqualError(t, err, "Internal error occurred: unimplemented functionality - unable to act as current user")
|
require.NoError(t, err)
|
||||||
require.True(t, k8serrors.IsInternalError(err), err)
|
require.Equal(t,
|
||||||
require.Equal(t, &k8serrors.StatusError{
|
expectedWhoAmIRequestResponse(
|
||||||
ErrStatus: metav1.Status{
|
serviceaccount.MakeUsername(namespaceName, saName),
|
||||||
Status: metav1.StatusFailure,
|
[]string{"system:serviceaccounts", "system:serviceaccounts:" + namespaceName, "system:authenticated"},
|
||||||
Code: http.StatusInternalServerError,
|
nil,
|
||||||
Reason: metav1.StatusReasonInternalError,
|
),
|
||||||
Details: &metav1.StatusDetails{
|
whoAmI,
|
||||||
Causes: []metav1.StatusCause{
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WhoAmIRequests and SA token request", func(t *testing.T) {
|
||||||
|
namespaceName := createTestNamespace(t, adminClient)
|
||||||
|
kubeClient := adminClient.CoreV1()
|
||||||
|
saName, _, saUID := createServiceAccountToken(ctx, t, adminClient, namespaceName)
|
||||||
|
|
||||||
|
_, tokenRequestProbeErr := kubeClient.ServiceAccounts(namespaceName).CreateToken(ctx, saName, &authenticationv1.TokenRequest{}, metav1.CreateOptions{})
|
||||||
|
if k8serrors.IsNotFound(tokenRequestProbeErr) && tokenRequestProbeErr.Error() == "the server could not find the requested resource" {
|
||||||
|
return // stop test early since the token request API is not enabled on this cluster - other errors are caught below
|
||||||
|
}
|
||||||
|
|
||||||
|
pod, err := kubeClient.Pods(namespaceName).Create(ctx, &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
GenerateName: "test-impersonation-proxy-",
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Message: "unimplemented functionality - unable to act as current user",
|
Name: "ignored-but-required",
|
||||||
|
Image: "does-not-matter",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ServiceAccountName: saName,
|
||||||
},
|
},
|
||||||
Message: "Internal error occurred: unimplemented functionality - unable to act as current user",
|
}, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tokenRequestBadAudience, err := kubeClient.ServiceAccounts(namespaceName).CreateToken(ctx, saName, &authenticationv1.TokenRequest{
|
||||||
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
|
Audiences: []string{"should-fail-because-wrong-audience"}, // anything that is not an API server audience
|
||||||
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||||
|
Kind: "Pod",
|
||||||
|
APIVersion: "",
|
||||||
|
Name: pod.Name,
|
||||||
|
UID: pod.UID,
|
||||||
},
|
},
|
||||||
}, err)
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
impersonationProxySABadAudPinnipedConciergeClient := newImpersonationProxyClientWithCredentials(t,
|
||||||
|
&loginv1alpha1.ClusterCredential{Token: tokenRequestBadAudience.Status.Token},
|
||||||
|
impersonationProxyURL, impersonationProxyCACertPEM, nil).PinnipedConcierge
|
||||||
|
|
||||||
|
_, badAudErr := impersonationProxySABadAudPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
|
||||||
|
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||||
|
require.True(t, k8serrors.IsUnauthorized(badAudErr), library.Sdump(badAudErr))
|
||||||
|
|
||||||
|
tokenRequest, err := kubeClient.ServiceAccounts(namespaceName).CreateToken(ctx, saName, &authenticationv1.TokenRequest{
|
||||||
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
|
Audiences: []string{},
|
||||||
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||||
|
Kind: "Pod",
|
||||||
|
APIVersion: "",
|
||||||
|
Name: pod.Name,
|
||||||
|
UID: pod.UID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
impersonationProxySAClient := newImpersonationProxyClientWithCredentials(t,
|
||||||
|
&loginv1alpha1.ClusterCredential{Token: tokenRequest.Status.Token},
|
||||||
|
impersonationProxyURL, impersonationProxyCACertPEM, nil)
|
||||||
|
|
||||||
|
whoAmITokenReq, err := impersonationProxySAClient.PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
|
||||||
|
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// new service account tokens include the pod info in the extra fields
|
||||||
|
require.Equal(t,
|
||||||
|
expectedWhoAmIRequestResponse(
|
||||||
|
serviceaccount.MakeUsername(namespaceName, saName),
|
||||||
|
[]string{"system:serviceaccounts", "system:serviceaccounts:" + namespaceName, "system:authenticated"},
|
||||||
|
map[string]identityv1alpha1.ExtraValue{
|
||||||
|
"authentication.kubernetes.io/pod-name": {pod.Name},
|
||||||
|
"authentication.kubernetes.io/pod-uid": {string(pod.UID)},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
whoAmITokenReq,
|
||||||
|
)
|
||||||
|
|
||||||
|
// allow the test SA to create CSRs
|
||||||
|
library.CreateTestClusterRoleBinding(t,
|
||||||
|
rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Name: saName, Namespace: namespaceName},
|
||||||
|
rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "system:node-bootstrapper"},
|
||||||
|
)
|
||||||
|
library.WaitForUserToHaveAccess(t, serviceaccount.MakeUsername(namespaceName, saName), []string{}, &authorizationv1.ResourceAttributes{
|
||||||
|
Verb: "create", Group: certificatesv1.GroupName, Version: "*", Resource: "certificatesigningrequests",
|
||||||
|
})
|
||||||
|
|
||||||
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
csrPEM, err := cert.MakeCSR(privateKey, &pkix.Name{
|
||||||
|
CommonName: "panda-man",
|
||||||
|
Organization: []string{"living-the-dream", "need-more-sleep"},
|
||||||
|
}, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
csrName, _, err := csr.RequestCertificate(
|
||||||
|
impersonationProxySAClient.Kubernetes,
|
||||||
|
csrPEM,
|
||||||
|
"",
|
||||||
|
certificatesv1.KubeAPIServerClientSignerName,
|
||||||
|
[]certificatesv1.KeyUsage{certificatesv1.UsageClientAuth},
|
||||||
|
privateKey,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saCSR, err := impersonationProxySAClient.Kubernetes.CertificatesV1beta1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = adminClient.CertificatesV1beta1().CertificateSigningRequests().Delete(ctx, csrName, metav1.DeleteOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// make sure the user info that the CSR captured matches the SA, including the UID
|
||||||
|
require.Equal(t, serviceaccount.MakeUsername(namespaceName, saName), saCSR.Spec.Username)
|
||||||
|
require.Equal(t, string(saUID), saCSR.Spec.UID)
|
||||||
|
require.Equal(t, []string{"system:serviceaccounts", "system:serviceaccounts:" + namespaceName, "system:authenticated"}, saCSR.Spec.Groups)
|
||||||
|
require.Equal(t, map[string]certificatesv1beta1.ExtraValue{
|
||||||
|
"authentication.kubernetes.io/pod-name": {pod.Name},
|
||||||
|
"authentication.kubernetes.io/pod-uid": {string(pod.UID)},
|
||||||
|
}, saCSR.Spec.Extra)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("kubectl as a client", func(t *testing.T) {
|
t.Run("kubectl as a client", func(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user