mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-06 08:07:08 +00:00
Merge branch 'main' into initial_ldap
This commit is contained in:
@@ -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
|
||||
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 upgrade requests, we only use http/1.1 since these always go from http/1.1
|
||||
to either websockets or SPDY.
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@@ -26,6 +28,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"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/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/filterlatency"
|
||||
@@ -45,6 +49,7 @@ import (
|
||||
"go.pinniped.dev/internal/httputil/securityheader"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/valuelesscontext"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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.
|
||||
handler = filterlatency.TrackCompleted(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.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
|
||||
nestedImpersonationAuthorizer := &comparableAuthorizer{
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
serverURL, err := url.Parse(restConfig.Host)
|
||||
if err != nil {
|
||||
@@ -300,11 +342,19 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
|
||||
if err != nil {
|
||||
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")
|
||||
if err != nil {
|
||||
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 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -347,15 +397,18 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
|
||||
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)
|
||||
// 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)
|
||||
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 {
|
||||
plog.WarningErr("rejecting request as we cannot act as the current user", err,
|
||||
"url", r.URL.String(),
|
||||
@@ -413,34 +466,117 @@ func ensureNoImpersonationHeaders(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTransportForUser(userInfo user.Info, delegate http.RoundTripper, ae *auditinternal.Event) (http.RoundTripper, error) {
|
||||
if len(userInfo.GetUID()) == 0 {
|
||||
extra, err := buildExtra(userInfo.GetExtra(), ae)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
impersonateConfig := transport.ImpersonationConfig{
|
||||
UserName: userInfo.GetName(),
|
||||
Groups: userInfo.GetGroups(),
|
||||
Extra: extra,
|
||||
}
|
||||
// transport.NewImpersonatingRoundTripper clones the request before setting headers
|
||||
// thus it will not accidentally mutate the input request (see http.Handler docs)
|
||||
return transport.NewImpersonatingRoundTripper(impersonateConfig, delegate), nil
|
||||
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)
|
||||
}
|
||||
|
||||
// 0. in the case of a request that is not attempting to do nested impersonation
|
||||
// 1. if we make the assumption that the TCR API does not issue tokens (or pass the TCR API bearer token
|
||||
// authenticator into this func - we need to know the authentication cred is something KAS would honor)
|
||||
// 2. then if preserve the incoming authorization header into the request's context
|
||||
// 3. we could reauthenticate it here (it would be a free cache hit)
|
||||
// 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)
|
||||
// 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
|
||||
// 8. the above would be safe even if in the future Kube started supporting UIDs asserted by client certs
|
||||
return nil, constable.Error("unexpected uid")
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
impersonateConfig := transport.ImpersonationConfig{
|
||||
UserName: userInfo.GetName(),
|
||||
Groups: userInfo.GetGroups(),
|
||||
Extra: extra,
|
||||
}
|
||||
// transport.NewImpersonatingRoundTripper clones the request before setting headers
|
||||
// thus it will not accidentally mutate the input request (see http.Handler docs)
|
||||
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")
|
||||
}
|
||||
|
||||
// see what KAS thinks this token translates into
|
||||
// this is important because certs have precedence over tokens and we want
|
||||
// to make sure that we do not get confused and pass along the wrong token
|
||||
tokenUser, err := tokenReview(ctx, token, authenticator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we want to compare the result of the token authentication with the original user that made the request
|
||||
// if the user who made the request and the token do not match, we cannot go any further at this point
|
||||
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) {
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
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/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
@@ -33,6 +35,7 @@ import (
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/httputil/roundtripper"
|
||||
@@ -176,6 +179,26 @@ func TestImpersonator(t *testing.T) {
|
||||
"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",
|
||||
clientCert: newClientCert(t, unrelatedCA, "test-username", []string{"test-group1"}),
|
||||
@@ -499,39 +522,12 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
"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 {
|
||||
name string
|
||||
restConfig *rest.Config
|
||||
wantCreationErr string
|
||||
request *http.Request
|
||||
authenticator authenticator.Request
|
||||
wantHTTPBody string
|
||||
wantHTTPStatus int
|
||||
wantKubeAPIServerRequestHeaders http.Header
|
||||
@@ -563,50 +559,50 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "authenticated user but missing audit event",
|
||||
request: func() *http.Request {
|
||||
req := newRequest(map[string][]string{
|
||||
req := newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Connection": {"Upgrade"},
|
||||
"Upgrade": {"some-upgrade"},
|
||||
@@ -615,7 +611,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
Name: testUser,
|
||||
Groups: testGroups,
|
||||
Extra: testExtra,
|
||||
}, nil)
|
||||
}, nil, "")
|
||||
ctx := request.WithAuditEvent(req.Context(), nil)
|
||||
req = req.WithContext(ctx)
|
||||
return req
|
||||
@@ -625,7 +621,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "authenticated user with upper case extra",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Connection": {"Upgrade"},
|
||||
"Upgrade": {"some-upgrade"},
|
||||
@@ -639,13 +635,13 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
"valid-key": {"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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
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"},
|
||||
"Connection": {"Upgrade"},
|
||||
"Upgrade": {"some-upgrade"},
|
||||
@@ -659,13 +655,13 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
"valid-key": {"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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "authenticated user with reserved extra key",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Connection": {"Upgrade"},
|
||||
"Upgrade": {"some-upgrade"},
|
||||
@@ -679,14 +675,164 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
"valid-key": {"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",
|
||||
wantHTTPStatus: http.StatusInternalServerError,
|
||||
},
|
||||
// happy path
|
||||
{
|
||||
name: "authenticated user",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Accept": {"some-accepted-format"},
|
||||
"Accept-Encoding": {"some-accepted-encoding"},
|
||||
@@ -699,7 +845,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
Name: testUser,
|
||||
Groups: testGroups,
|
||||
Extra: testExtra,
|
||||
}, nil),
|
||||
}, nil, ""),
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"Authorization": {"Bearer some-service-account-token"},
|
||||
"Impersonate-Extra-Extra-1": {"some", "extra", "stuff"},
|
||||
@@ -717,9 +863,61 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
wantHTTPBody: "successful proxied response",
|
||||
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",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Accept": {"some-accepted-format"},
|
||||
"Accept-Encoding": {"some-accepted-encoding"},
|
||||
@@ -736,7 +934,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
"iam.gke.io/user-assertion": {"ABC"},
|
||||
"user-assertion.cloud.google.com": {"XYZ"},
|
||||
},
|
||||
}, nil),
|
||||
}, nil, ""),
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"Authorization": {"Bearer some-service-account-token"},
|
||||
"Impersonate-Extra-Iam.gke.io%2fuser-Assertion": {"ABC"},
|
||||
@@ -756,7 +954,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "authenticated openshift/openstack user",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Accept": {"some-accepted-format"},
|
||||
"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/name": {"domain-name"},
|
||||
},
|
||||
}, nil),
|
||||
}, nil, ""),
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"Authorization": {"Bearer some-service-account-token"},
|
||||
"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",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Accept": {"some-accepted-format"},
|
||||
"Accept-Encoding": {"some-accepted-encoding"},
|
||||
@@ -820,7 +1018,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
Extra: map[string][]string{
|
||||
"foo.iimpersonation-proxy.concierge.pinniped.dev": {"still-valid-value"},
|
||||
},
|
||||
}, nil),
|
||||
}, nil, ""),
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"Authorization": {"Bearer some-service-account-token"},
|
||||
"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",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Accept": {"some-accepted-format"},
|
||||
"Accept-Encoding": {"some-accepted-encoding"},
|
||||
@@ -866,6 +1064,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||
},
|
||||
"",
|
||||
),
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"Authorization": {"Bearer some-service-account-token"},
|
||||
@@ -886,7 +1085,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "authenticated user with nested impersonation",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Accept": {"some-accepted-format"},
|
||||
"Accept-Encoding": {"some-accepted-encoding"},
|
||||
@@ -912,6 +1111,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||
},
|
||||
"",
|
||||
),
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"Authorization": {"Bearer some-service-account-token"},
|
||||
@@ -933,7 +1133,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "authenticated gke user with nested impersonation",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
"Accept": {"some-accepted-format"},
|
||||
"Accept-Encoding": {"some-accepted-encoding"},
|
||||
@@ -959,6 +1159,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||
},
|
||||
"",
|
||||
),
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"Authorization": {"Bearer some-service-account-token"},
|
||||
@@ -980,7 +1181,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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"},
|
||||
"Accept": {"some-accepted-format"},
|
||||
"Accept-Encoding": {"some-accepted-encoding"},
|
||||
@@ -1010,6 +1211,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
||||
},
|
||||
ImpersonatedUser: &authenticationv1.UserInfo{},
|
||||
},
|
||||
"",
|
||||
),
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"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",
|
||||
request: newRequest(map[string][]string{
|
||||
request: newRequest(t, map[string][]string{
|
||||
"User-Agent": {"test-user-agent"},
|
||||
}, &user.DefaultInfo{
|
||||
Name: testUser,
|
||||
Groups: testGroups,
|
||||
Extra: testExtra,
|
||||
}, nil),
|
||||
}, nil, ""),
|
||||
kubeAPIServerStatusCode: http.StatusNotFound,
|
||||
wantKubeAPIServerRequestHeaders: map[string][]string{
|
||||
"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)
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
serverConfig := genericapiserver.NewRecommendedConfig(codecs)
|
||||
serverConfig.Authentication.Authenticator = tt.authenticator
|
||||
|
||||
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 {
|
||||
certPEM, keyPEM []byte
|
||||
}
|
||||
@@ -1242,7 +1522,9 @@ func Test_deleteKnownImpersonationHeaders(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 headers mutated
|
||||
@@ -1259,6 +1541,85 @@ func Test_deleteKnownImpersonationHeaders(t *testing.T) {
|
||||
|
||||
deleteKnownImpersonationHeaders(delegate).ServeHTTP(nil, inputReq)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,9 @@ func validateNames(names *NamesConfigSpec) error {
|
||||
if names.ImpersonationSignerSecret == "" {
|
||||
missingNames = append(missingNames, "impersonationSignerSecret")
|
||||
}
|
||||
if names.AgentServiceAccount == "" {
|
||||
missingNames = append(missingNames, "agentServiceAccount")
|
||||
}
|
||||
if len(missingNames) > 0 {
|
||||
return constable.Error("missing required names: " + strings.Join(missingNames, ", "))
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
labels:
|
||||
myLabelKey1: myLabelValue1
|
||||
myLabelKey2: myLabelValue2
|
||||
@@ -72,6 +73,7 @@ func TestFromPath(t *testing.T) {
|
||||
ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value",
|
||||
ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value",
|
||||
ImpersonationSignerSecret: "impersonationSignerSecret-value",
|
||||
AgentServiceAccount: "agentServiceAccount-value",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"myLabelKey1": "myLabelValue1",
|
||||
@@ -98,6 +100,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantConfig: &Config{
|
||||
DiscoveryInfo: DiscoveryInfoSpec{
|
||||
@@ -119,6 +122,7 @@ func TestFromPath(t *testing.T) {
|
||||
ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value",
|
||||
ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value",
|
||||
ImpersonationSignerSecret: "impersonationSignerSecret-value",
|
||||
AgentServiceAccount: "agentServiceAccount-value",
|
||||
},
|
||||
Labels: map[string]string{},
|
||||
KubeCertAgentConfig: KubeCertAgentSpec{
|
||||
@@ -133,7 +137,7 @@ func TestFromPath(t *testing.T) {
|
||||
wantError: "validate names: missing required names: servingCertificateSecret, credentialIssuer, " +
|
||||
"apiService, impersonationConfigMap, impersonationLoadBalancerService, " +
|
||||
"impersonationTLSCertificateSecret, impersonationCACertificateSecret, " +
|
||||
"impersonationSignerSecret",
|
||||
"impersonationSignerSecret, agentServiceAccount",
|
||||
},
|
||||
{
|
||||
name: "Missing apiService name",
|
||||
@@ -147,6 +151,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: apiService",
|
||||
},
|
||||
@@ -162,6 +167,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: credentialIssuer",
|
||||
},
|
||||
@@ -177,6 +183,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: servingCertificateSecret",
|
||||
},
|
||||
@@ -192,6 +199,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: impersonationConfigMap",
|
||||
},
|
||||
@@ -207,6 +215,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: impersonationLoadBalancerService",
|
||||
},
|
||||
@@ -222,6 +231,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: impersonationTLSCertificateSecret",
|
||||
},
|
||||
@@ -237,6 +247,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: impersonationCACertificateSecret",
|
||||
},
|
||||
@@ -252,6 +263,7 @@ func TestFromPath(t *testing.T) {
|
||||
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
|
||||
impersonationCACertificateSecret: impersonationCACertificateSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: impersonationSignerSecret",
|
||||
},
|
||||
@@ -265,6 +277,7 @@ func TestFromPath(t *testing.T) {
|
||||
apiService: pinniped-api
|
||||
impersonationLoadBalancerService: impersonationLoadBalancerService-value
|
||||
impersonationSignerSecret: impersonationSignerSecret-value
|
||||
agentServiceAccount: agentServiceAccount-value
|
||||
`),
|
||||
wantError: "validate names: missing required names: impersonationConfigMap, " +
|
||||
"impersonationTLSCertificateSecret, impersonationCACertificateSecret",
|
||||
|
||||
@@ -41,6 +41,7 @@ type NamesConfigSpec struct {
|
||||
ImpersonationTLSCertificateSecret string `json:"impersonationTLSCertificateSecret"`
|
||||
ImpersonationCACertificateSecret string `json:"impersonationCACertificateSecret"`
|
||||
ImpersonationSignerSecret string `json:"impersonationSignerSecret"`
|
||||
AgentServiceAccount string `json:"agentServiceAccount"`
|
||||
}
|
||||
|
||||
// ServingCertificateConfigSpec contains the configuration knobs for the API's
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
package authenticator
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||
)
|
||||
@@ -22,8 +24,18 @@ type Closer interface {
|
||||
// nil CA bundle will be returned. If the provided spec contains a CA bundle that is not properly
|
||||
// encoded, an error will be returned.
|
||||
func CABundle(spec *auth1alpha1.TLSSpec) ([]byte, error) {
|
||||
if spec == nil {
|
||||
if spec == nil || len(spec.CertificateAuthorityData) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return base64.StdEncoding.DecodeString(spec.CertificateAuthorityData)
|
||||
|
||||
pem, err := base64.StdEncoding.DecodeString(spec.CertificateAuthorityData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok := x509.NewCertPool().AppendCertsFromPEM(pem); !ok {
|
||||
return nil, fmt.Errorf("certificateAuthorityData is not valid PEM")
|
||||
}
|
||||
|
||||
return pem, nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/valuelesscontext"
|
||||
)
|
||||
|
||||
// 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
|
||||
// through directly to the authentication webhook.
|
||||
ctx = valuelessContext{ctx}
|
||||
ctx = valuelesscontext.New(ctx)
|
||||
|
||||
// Call the selected authenticator.
|
||||
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
|
||||
}
|
||||
|
||||
type valuelessContext struct{ context.Context }
|
||||
|
||||
func (valuelessContext) Value(interface{}) interface{} { return nil }
|
||||
|
||||
@@ -135,6 +135,15 @@ func TestNewWebhookAuthenticator(t *testing.T) {
|
||||
require.EqualError(t, err, "invalid TLS configuration: illegal base64 data at input byte 7")
|
||||
})
|
||||
|
||||
t.Run("invalid pem data", func(t *testing.T) {
|
||||
res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
|
||||
Endpoint: "https://example.com",
|
||||
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("bad data"))},
|
||||
}, ioutil.TempFile, clientcmd.WriteToFile)
|
||||
require.Nil(t, res)
|
||||
require.EqualError(t, err, "invalid TLS configuration: certificateAuthorityData is not valid PEM")
|
||||
})
|
||||
|
||||
t.Run("valid config with no TLS spec", func(t *testing.T) {
|
||||
res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
|
||||
Endpoint: "https://example.com",
|
||||
|
||||
@@ -64,6 +64,9 @@ type AgentConfig struct {
|
||||
// NamePrefix will be prefixed to all agent pod names.
|
||||
NamePrefix string
|
||||
|
||||
// ServiceAccountName is the service account under which to run the agent pods.
|
||||
ServiceAccountName string
|
||||
|
||||
// ContainerImagePullSecrets is a list of names of Kubernetes Secret objects that will be used as
|
||||
// ImagePullSecrets on the kube-cert-agent pods.
|
||||
ContainerImagePullSecrets []string
|
||||
@@ -472,6 +475,7 @@ func (c *agentController) newAgentDeployment(controllerManagerPod *corev1.Pod) *
|
||||
RestartPolicy: corev1.RestartPolicyAlways,
|
||||
NodeSelector: controllerManagerPod.Spec.NodeSelector,
|
||||
AutomountServiceAccountToken: pointer.BoolPtr(false),
|
||||
ServiceAccountName: c.cfg.ServiceAccountName,
|
||||
NodeName: controllerManagerPod.Spec.NodeName,
|
||||
Tolerations: controllerManagerPod.Spec.Tolerations,
|
||||
// We need to run the agent pod as root since the file permissions
|
||||
|
||||
@@ -123,6 +123,7 @@ func TestAgentController(t *testing.T) {
|
||||
}},
|
||||
RestartPolicy: corev1.RestartPolicyAlways,
|
||||
TerminationGracePeriodSeconds: pointer.Int64Ptr(0),
|
||||
ServiceAccountName: "test-service-account-name",
|
||||
AutomountServiceAccountToken: pointer.BoolPtr(false),
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsUser: pointer.Int64Ptr(0),
|
||||
@@ -672,6 +673,7 @@ func TestAgentController(t *testing.T) {
|
||||
AgentConfig{
|
||||
Namespace: "concierge",
|
||||
ContainerImage: "pinniped-server-image",
|
||||
ServiceAccountName: "test-service-account-name",
|
||||
NamePrefix: "pinniped-concierge-kube-cert-agent-",
|
||||
ContainerImagePullSecrets: []string{"pinniped-image-pull-secret"},
|
||||
CredentialIssuerName: "pinniped-concierge-config",
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
@@ -269,11 +270,17 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1
|
||||
|
||||
discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer)
|
||||
if err != nil {
|
||||
const klogLevelTrace = 6
|
||||
c.log.V(klogLevelTrace).WithValues(
|
||||
"namespace", upstream.Namespace,
|
||||
"name", upstream.Name,
|
||||
"issuer", upstream.Spec.Issuer,
|
||||
).Error(err, "failed to perform OIDC discovery")
|
||||
return &v1alpha1.Condition{
|
||||
Type: typeOIDCDiscoverySucceeded,
|
||||
Status: v1alpha1.ConditionFalse,
|
||||
Reason: reasonUnreachable,
|
||||
Message: fmt.Sprintf("failed to perform OIDC discovery against %q", upstream.Spec.Issuer),
|
||||
Message: fmt.Sprintf("failed to perform OIDC discovery against %q:\n%s", upstream.Spec.Issuer, truncateNonOIDCErr(err)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,3 +435,14 @@ func (*oidcWatcherController) computeScopes(additionalScopes []string) []string
|
||||
sort.Strings(scopes)
|
||||
return scopes
|
||||
}
|
||||
|
||||
func truncateNonOIDCErr(err error) string {
|
||||
const max = 100
|
||||
msg := err.Error()
|
||||
|
||||
if len(msg) <= max || strings.HasPrefix(msg, "oidc:") {
|
||||
return msg
|
||||
}
|
||||
|
||||
return msg[:max] + fmt.Sprintf(" [truncated %d chars]", len(msg)-max)
|
||||
}
|
||||
|
||||
+171
-4
@@ -370,7 +370,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
|
||||
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: "invalid-url",
|
||||
Issuer: "invalid-url-that-is-really-really-long",
|
||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||
},
|
||||
@@ -382,9 +382,10 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
|
||||
}},
|
||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||
wantLogs: []string{
|
||||
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"invalid-url-that-is-really-really-long/.well-known/openid-configuration\": unsupported protocol scheme \"\"" "issuer"="invalid-url-that-is-really-really-long" "name"="test-name" "namespace"="test-namespace"`,
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long\":\nGet \"invalid-url-that-is-really-really-long/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long\":\nGet \"invalid-url-that-is-really-really-long/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
||||
},
|
||||
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
||||
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||
@@ -404,7 +405,8 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
|
||||
Status: "False",
|
||||
LastTransitionTime: now,
|
||||
Reason: "Unreachable",
|
||||
Message: `failed to perform OIDC discovery against "invalid-url"`,
|
||||
Message: `failed to perform OIDC discovery against "invalid-url-that-is-really-really-long":
|
||||
Get "invalid-url-that-is-really-really-long/.well-known/openid-configuration": unsupported protocol [truncated 9 chars]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -600,6 +602,151 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "existing valid upstream with trailing slash",
|
||||
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
||||
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: testIssuerURL + "/ends-with-slash/",
|
||||
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||
Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim},
|
||||
},
|
||||
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||
Phase: "Ready",
|
||||
Conditions: []v1alpha1.Condition{
|
||||
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"},
|
||||
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||
Type: "secrets.pinniped.dev/oidc-client",
|
||||
Data: testValidSecretData,
|
||||
}},
|
||||
wantLogs: []string{
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`,
|
||||
},
|
||||
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{
|
||||
&oidctestutil.TestUpstreamOIDCIdentityProvider{
|
||||
Name: testName,
|
||||
ClientID: testClientID,
|
||||
AuthorizationURL: *testIssuerAuthorizeURL,
|
||||
Scopes: testExpectedScopes,
|
||||
UsernameClaim: testUsernameClaim,
|
||||
GroupsClaim: testGroupsClaim,
|
||||
},
|
||||
},
|
||||
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
||||
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||
Phase: "Ready",
|
||||
Conditions: []v1alpha1.Condition{
|
||||
{Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234},
|
||||
{Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "issuer is invalid URL, missing trailing slash",
|
||||
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: testIssuerURL + "/ends-with-slash",
|
||||
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||
Type: "secrets.pinniped.dev/oidc-client",
|
||||
Data: testValidSecretData,
|
||||
}},
|
||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||
wantLogs: []string{
|
||||
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "issuer"="` + testIssuerURL + `/ends-with-slash" "name"="test-name" "namespace"="test-namespace"`,
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
||||
},
|
||||
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
||||
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||
Phase: "Error",
|
||||
Conditions: []v1alpha1.Condition{
|
||||
{
|
||||
Type: "ClientCredentialsValid",
|
||||
Status: "True",
|
||||
LastTransitionTime: now,
|
||||
Reason: "Success",
|
||||
Message: "loaded client credentials",
|
||||
},
|
||||
{
|
||||
Type: "OIDCDiscoverySucceeded",
|
||||
Status: "False",
|
||||
LastTransitionTime: now,
|
||||
Reason: "Unreachable",
|
||||
Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/ends-with-slash":
|
||||
oidc: issuer did not match the issuer returned by provider, expected "` + testIssuerURL + `/ends-with-slash" got "` + testIssuerURL + `/ends-with-slash/"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "issuer is invalid URL, extra trailing slash",
|
||||
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: testIssuerURL + "/",
|
||||
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||
Type: "secrets.pinniped.dev/oidc-client",
|
||||
Data: testValidSecretData,
|
||||
}},
|
||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||
wantLogs: []string{
|
||||
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "issuer"="` + testIssuerURL + `/" "name"="test-name" "namespace"="test-namespace"`,
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
||||
},
|
||||
wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{},
|
||||
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||
Phase: "Error",
|
||||
Conditions: []v1alpha1.Condition{
|
||||
{
|
||||
Type: "ClientCredentialsValid",
|
||||
Status: "True",
|
||||
LastTransitionTime: now,
|
||||
Reason: "Success",
|
||||
Message: "loaded client credentials",
|
||||
},
|
||||
{
|
||||
Type: "OIDCDiscoverySucceeded",
|
||||
Status: "False",
|
||||
LastTransitionTime: now,
|
||||
Reason: "Unreachable",
|
||||
Message: `failed to perform OIDC discovery against "` + testIssuerURL + `/":
|
||||
oidc: issuer did not match the issuer returned by provider, expected "` + testIssuerURL + `/" got "` + testIssuerURL + `"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
@@ -728,5 +875,25 @@ func newTestIssuer(t *testing.T) (string, string) {
|
||||
})
|
||||
})
|
||||
|
||||
// handle the four issuer with trailing slash configs
|
||||
|
||||
// valid case in= out=
|
||||
// handled above at the root of testURL
|
||||
|
||||
// valid case in=/ out=/
|
||||
mux.HandleFunc("/ends-with-slash/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("content-type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||
Issuer: testURL + "/ends-with-slash/",
|
||||
AuthURL: "https://example.com/authorize",
|
||||
})
|
||||
})
|
||||
|
||||
// invalid case in= out=/
|
||||
// can be tested using /ends-with-slash/ endpoint
|
||||
|
||||
// invalid case in=/ out=
|
||||
// can be tested using root endpoint
|
||||
|
||||
return caBundlePEM, testURL
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package supervisorstorage
|
||||
@@ -59,7 +59,7 @@ func GarbageCollectorController(
|
||||
return isSecretWithGCAnnotation(oldObj) || isSecretWithGCAnnotation(newObj)
|
||||
},
|
||||
DeleteFunc: func(obj metav1.Object) bool { return false }, // ignore all deletes
|
||||
ParentFunc: nil,
|
||||
ParentFunc: pinnipedcontroller.SingletonQueue(),
|
||||
},
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
@@ -67,16 +67,20 @@ func GarbageCollectorController(
|
||||
}
|
||||
|
||||
func (c *garbageCollectorController) Sync(ctx controllerlib.Context) error {
|
||||
// make sure we have a consistent, static meaning for the current time during the sync loop
|
||||
frozenClock := clock.NewFakeClock(c.clock.Now())
|
||||
|
||||
// The Sync method is triggered upon any change to any Secret, which would make this
|
||||
// controller too chatty, so it rate limits itself to a more reasonable interval.
|
||||
// Note that even during a period when no secrets are changing, it will still run
|
||||
// at the informer's full-resync interval (as long as there are some secrets).
|
||||
if c.clock.Now().Sub(c.timeOfMostRecentSweep) < minimumRepeatInterval {
|
||||
if since := frozenClock.Since(c.timeOfMostRecentSweep); since < minimumRepeatInterval {
|
||||
ctx.Queue.AddAfter(ctx.Key, minimumRepeatInterval-since)
|
||||
return nil
|
||||
}
|
||||
|
||||
plog.Info("starting storage garbage collection sweep")
|
||||
c.timeOfMostRecentSweep = c.clock.Now()
|
||||
c.timeOfMostRecentSweep = frozenClock.Now()
|
||||
|
||||
listOfSecrets, err := c.secretInformer.Lister().List(labels.Everything())
|
||||
if err != nil {
|
||||
@@ -97,7 +101,7 @@ func (c *garbageCollectorController) Sync(ctx controllerlib.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if garbageCollectAfterTime.Before(c.clock.Now()) {
|
||||
if garbageCollectAfterTime.Before(frozenClock.Now()) {
|
||||
err = c.kubeClient.CoreV1().Secrets(secret.Namespace).Delete(ctx.Context, secret.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
plog.WarningErr("failed to garbage collect resource", err, logKV(secret))
|
||||
|
||||
@@ -66,6 +66,10 @@ func TestGarbageCollectorControllerInformerFilters(t *testing.T) {
|
||||
r.True(subject.Update(secretWithAnnotation, otherSecret))
|
||||
r.True(subject.Update(otherSecret, secretWithAnnotation))
|
||||
})
|
||||
|
||||
it("returns the same singleton key", func() {
|
||||
r.Equal(controllerlib.Key{}, subject.Parent(secretWithAnnotation))
|
||||
})
|
||||
})
|
||||
|
||||
when("any Secret with the required annotation is deleted", func() {
|
||||
@@ -136,9 +140,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
||||
Context: cancelContext,
|
||||
Name: subject.Name(),
|
||||
Key: controllerlib.Key{
|
||||
Namespace: "",
|
||||
Name: "",
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Queue: &testQueue{t: t},
|
||||
}
|
||||
|
||||
// Must start informers before calling TestRunSynchronously()
|
||||
@@ -262,16 +267,23 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
||||
// Run sync once with the current time set to frozenTime.
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
require.Empty(t, kubeClient.Actions())
|
||||
r.False(syncContext.Queue.(*testQueue).called)
|
||||
|
||||
// Run sync again when not enough time has passed since the most recent run, so no delete
|
||||
// operations should happen even though there is a expired secret now.
|
||||
fakeClock.Step(29 * time.Second)
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
require.Empty(t, kubeClient.Actions())
|
||||
r.True(syncContext.Queue.(*testQueue).called)
|
||||
r.Equal(controllerlib.Key{Namespace: "foo", Name: "bar"}, syncContext.Queue.(*testQueue).key) // assert key is passed through
|
||||
r.Equal(time.Second, syncContext.Queue.(*testQueue).duration) // assert that we get the exact requeue time
|
||||
|
||||
syncContext.Queue = &testQueue{t: t} // reset the queue for the next sync
|
||||
|
||||
// Step to the exact threshold and run Sync again. Now we are past the rate limiting period.
|
||||
fakeClock.Step(1*time.Second + 1*time.Millisecond)
|
||||
fakeClock.Step(time.Second)
|
||||
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
|
||||
r.False(syncContext.Queue.(*testQueue).called)
|
||||
|
||||
// It should have deleted the expired secret.
|
||||
r.ElementsMatch(
|
||||
@@ -381,3 +393,23 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
||||
})
|
||||
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||
}
|
||||
|
||||
type testQueue struct {
|
||||
t *testing.T
|
||||
|
||||
called bool
|
||||
key controllerlib.Key
|
||||
duration time.Duration
|
||||
|
||||
controllerlib.Queue // panic if any other methods called
|
||||
}
|
||||
|
||||
func (q *testQueue) AddAfter(key controllerlib.Key, duration time.Duration) {
|
||||
q.t.Helper()
|
||||
|
||||
require.False(q.t, q.called, "AddAfter should only be called once")
|
||||
|
||||
q.called = true
|
||||
q.key = key
|
||||
q.duration = duration
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) {
|
||||
|
||||
agentConfig := kubecertagent.AgentConfig{
|
||||
Namespace: c.ServerInstallationInfo.Namespace,
|
||||
ServiceAccountName: c.NamesConfig.AgentServiceAccount,
|
||||
ContainerImage: *c.KubeCertAgentConfig.Image,
|
||||
NamePrefix: *c.KubeCertAgentConfig.NamePrefix,
|
||||
ContainerImagePullSecrets: c.KubeCertAgentConfig.ImagePullSecrets,
|
||||
|
||||
@@ -164,13 +164,8 @@ type TimeoutsConfiguration struct {
|
||||
OIDCSessionStorageLifetime time.Duration
|
||||
|
||||
// AccessTokenSessionStorageLifetime is the length of time after which an access token's session data is allowed
|
||||
// to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid.
|
||||
// Therefore, this can be just slightly longer than the AccessTokenLifespan. Access tokens are handed back to
|
||||
// the token endpoint for the token exchange use case. During a token exchange, if the access token is expired
|
||||
// and still exists in storage, then the endpoint will be able to give a slightly more specific error message,
|
||||
// rather than a more generic error that is returned when the token does not exist. If this is desirable, then
|
||||
// the AccessTokenSessionStorageLifetime can be made to be significantly larger than AccessTokenLifespan, at the
|
||||
// cost of slower cleanup.
|
||||
// to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid
|
||||
// or else the refresh flow will not work properly. So this must be longer than RefreshTokenLifespan.
|
||||
AccessTokenSessionStorageLifetime time.Duration
|
||||
|
||||
// RefreshTokenSessionStorageLifetime is the length of time after which a refresh token's session data is allowed
|
||||
@@ -186,7 +181,7 @@ type TimeoutsConfiguration struct {
|
||||
|
||||
// Get the defaults for the Supervisor server.
|
||||
func DefaultOIDCTimeoutsConfiguration() TimeoutsConfiguration {
|
||||
accessTokenLifespan := 15 * time.Minute
|
||||
accessTokenLifespan := 2 * time.Minute
|
||||
authorizationCodeLifespan := 10 * time.Minute
|
||||
refreshTokenLifespan := 9 * time.Hour
|
||||
|
||||
@@ -199,7 +194,7 @@ func DefaultOIDCTimeoutsConfiguration() TimeoutsConfiguration {
|
||||
AuthorizationCodeSessionStorageLifetime: authorizationCodeLifespan + refreshTokenLifespan,
|
||||
PKCESessionStorageLifetime: authorizationCodeLifespan + (1 * time.Minute),
|
||||
OIDCSessionStorageLifetime: authorizationCodeLifespan + (1 * time.Minute),
|
||||
AccessTokenSessionStorageLifetime: accessTokenLifespan + (1 * time.Minute),
|
||||
AccessTokenSessionStorageLifetime: refreshTokenLifespan + accessTokenLifespan,
|
||||
RefreshTokenSessionStorageLifetime: refreshTokenLifespan + accessTokenLifespan,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ const (
|
||||
hmacSecret = "this needs to be at least 32 characters to meet entropy requirements"
|
||||
|
||||
authCodeExpirationSeconds = 10 * 60 // Current, we set our auth code expiration to 10 minutes
|
||||
accessTokenExpirationSeconds = 15 * 60 // Currently, we set our access token expiration to 15 minutes
|
||||
idTokenExpirationSeconds = 15 * 60 // Currently, we set our ID token expiration to 15 minutes
|
||||
accessTokenExpirationSeconds = 2 * 60 // Currently, we set our access token expiration to 2 minutes
|
||||
idTokenExpirationSeconds = 2 * 60 // Currently, we set our ID token expiration to 2 minutes
|
||||
|
||||
timeComparisonFudgeSeconds = 15
|
||||
)
|
||||
|
||||
+116
-15
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package plog implements a thin layer over klog to help enforce pinniped's logging convention.
|
||||
@@ -26,56 +26,157 @@
|
||||
// act of desperation to determine why the system is broken.
|
||||
package plog
|
||||
|
||||
import "k8s.io/klog/v2"
|
||||
import (
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const errorKey = "error"
|
||||
|
||||
// Use Error to log an unexpected system error.
|
||||
func Error(msg string, err error, keysAndValues ...interface{}) {
|
||||
klog.ErrorS(err, msg, keysAndValues...)
|
||||
type _ interface {
|
||||
Error(msg string, err error, keysAndValues ...interface{})
|
||||
Warning(msg string, keysAndValues ...interface{})
|
||||
WarningErr(msg string, err error, keysAndValues ...interface{})
|
||||
Info(msg string, keysAndValues ...interface{})
|
||||
InfoErr(msg string, err error, keysAndValues ...interface{})
|
||||
Debug(msg string, keysAndValues ...interface{})
|
||||
DebugErr(msg string, err error, keysAndValues ...interface{})
|
||||
Trace(msg string, keysAndValues ...interface{})
|
||||
TraceErr(msg string, err error, keysAndValues ...interface{})
|
||||
All(msg string, keysAndValues ...interface{})
|
||||
}
|
||||
|
||||
func Warning(msg string, keysAndValues ...interface{}) {
|
||||
type PLogger struct {
|
||||
prefix string
|
||||
depth int
|
||||
}
|
||||
|
||||
func New(prefix string) PLogger {
|
||||
return PLogger{
|
||||
depth: 0,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PLogger) Error(msg string, err error, keysAndValues ...interface{}) {
|
||||
klog.ErrorSDepth(p.depth+1, err, p.prefix+msg, keysAndValues...)
|
||||
}
|
||||
|
||||
func (p *PLogger) warningDepth(msg string, depth int, keysAndValues ...interface{}) {
|
||||
// klog's structured logging has no concept of a warning (i.e. no WarningS function)
|
||||
// Thus we use info at log level zero as a proxy
|
||||
// klog's info logs have an I prefix and its warning logs have a W prefix
|
||||
// Since we lose the W prefix by using InfoS, just add a key to make these easier to find
|
||||
keysAndValues = append([]interface{}{"warning", "true"}, keysAndValues...)
|
||||
klog.V(klogLevelWarning).InfoS(msg, keysAndValues...)
|
||||
if klog.V(klogLevelWarning).Enabled() {
|
||||
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PLogger) Warning(msg string, keysAndValues ...interface{}) {
|
||||
p.warningDepth(msg, p.depth+1, keysAndValues...)
|
||||
}
|
||||
|
||||
// Use WarningErr to issue a Warning message with an error object as part of the message.
|
||||
func (p *PLogger) WarningErr(msg string, err error, keysAndValues ...interface{}) {
|
||||
p.warningDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
|
||||
}
|
||||
|
||||
func (p *PLogger) infoDepth(msg string, depth int, keysAndValues ...interface{}) {
|
||||
if klog.V(klogLevelInfo).Enabled() {
|
||||
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||
p.infoDepth(msg, p.depth+1, keysAndValues...)
|
||||
}
|
||||
|
||||
// Use InfoErr to log an expected error, e.g. validation failure of an http parameter.
|
||||
func (p *PLogger) InfoErr(msg string, err error, keysAndValues ...interface{}) {
|
||||
p.infoDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
|
||||
}
|
||||
|
||||
func (p *PLogger) debugDepth(msg string, depth int, keysAndValues ...interface{}) {
|
||||
if klog.V(klogLevelDebug).Enabled() {
|
||||
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PLogger) Debug(msg string, keysAndValues ...interface{}) {
|
||||
p.debugDepth(msg, p.depth+1, keysAndValues...)
|
||||
}
|
||||
|
||||
// Use DebugErr to issue a Debug message with an error object as part of the message.
|
||||
func (p *PLogger) DebugErr(msg string, err error, keysAndValues ...interface{}) {
|
||||
p.debugDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
|
||||
}
|
||||
|
||||
func (p *PLogger) traceDepth(msg string, depth int, keysAndValues ...interface{}) {
|
||||
if klog.V(klogLevelTrace).Enabled() {
|
||||
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PLogger) Trace(msg string, keysAndValues ...interface{}) {
|
||||
p.traceDepth(msg, p.depth+1, keysAndValues...)
|
||||
}
|
||||
|
||||
// Use TraceErr to issue a Trace message with an error object as part of the message.
|
||||
func (p *PLogger) TraceErr(msg string, err error, keysAndValues ...interface{}) {
|
||||
p.traceDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
|
||||
}
|
||||
|
||||
func (p *PLogger) All(msg string, keysAndValues ...interface{}) {
|
||||
if klog.V(klogLevelAll).Enabled() {
|
||||
klog.InfoSDepth(p.depth+1, p.prefix+msg, keysAndValues...)
|
||||
}
|
||||
}
|
||||
|
||||
var pLogger = PLogger{ //nolint:gochecknoglobals
|
||||
depth: 1,
|
||||
}
|
||||
|
||||
// Use Error to log an unexpected system error.
|
||||
func Error(msg string, err error, keysAndValues ...interface{}) {
|
||||
pLogger.Error(msg, err, keysAndValues...)
|
||||
}
|
||||
|
||||
func Warning(msg string, keysAndValues ...interface{}) {
|
||||
pLogger.Warning(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// Use WarningErr to issue a Warning message with an error object as part of the message.
|
||||
func WarningErr(msg string, err error, keysAndValues ...interface{}) {
|
||||
Warning(msg, append([]interface{}{errorKey, err}, keysAndValues...)...)
|
||||
pLogger.WarningErr(msg, err, keysAndValues...)
|
||||
}
|
||||
|
||||
func Info(msg string, keysAndValues ...interface{}) {
|
||||
klog.V(klogLevelInfo).InfoS(msg, keysAndValues...)
|
||||
pLogger.Info(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// Use InfoErr to log an expected error, e.g. validation failure of an http parameter.
|
||||
func InfoErr(msg string, err error, keysAndValues ...interface{}) {
|
||||
Info(msg, append([]interface{}{errorKey, err}, keysAndValues...)...)
|
||||
pLogger.InfoErr(msg, err, keysAndValues...)
|
||||
}
|
||||
|
||||
func Debug(msg string, keysAndValues ...interface{}) {
|
||||
klog.V(klogLevelDebug).InfoS(msg, keysAndValues...)
|
||||
pLogger.Debug(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// Use DebugErr to issue a Debug message with an error object as part of the message.
|
||||
func DebugErr(msg string, err error, keysAndValues ...interface{}) {
|
||||
Debug(msg, append([]interface{}{errorKey, err}, keysAndValues...)...)
|
||||
pLogger.DebugErr(msg, err, keysAndValues...)
|
||||
}
|
||||
|
||||
func Trace(msg string, keysAndValues ...interface{}) {
|
||||
klog.V(klogLevelTrace).InfoS(msg, keysAndValues...)
|
||||
pLogger.Trace(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// Use TraceErr to issue a Trace message with an error object as part of the message.
|
||||
func TraceErr(msg string, err error, keysAndValues ...interface{}) {
|
||||
Trace(msg, append([]interface{}{errorKey, err}, keysAndValues...)...)
|
||||
pLogger.TraceErr(msg, err, keysAndValues...)
|
||||
}
|
||||
|
||||
func All(msg string, keysAndValues ...interface{}) {
|
||||
klog.V(klogLevelAll).InfoS(msg, keysAndValues...)
|
||||
pLogger.All(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package valuelesscontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type contextKey int
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
f func(*testing.T, context.Context) context.Context
|
||||
wantReg, wantNew, wantBoth func(*testing.T, context.Context)
|
||||
}{
|
||||
{
|
||||
name: "empty context",
|
||||
f: func(t *testing.T, ctx context.Context) context.Context {
|
||||
return ctx
|
||||
},
|
||||
wantReg: func(t *testing.T, ctx context.Context) {},
|
||||
wantNew: func(t *testing.T, ctx context.Context) {},
|
||||
wantBoth: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.False(t, ok)
|
||||
require.Nil(t, auds)
|
||||
|
||||
val, ok := ctx.Value(contextKey(0xDEADBEEF)).(string)
|
||||
require.False(t, ok)
|
||||
require.Zero(t, val)
|
||||
|
||||
deadline, ok := ctx.Deadline()
|
||||
require.False(t, ok)
|
||||
require.Zero(t, deadline)
|
||||
|
||||
require.Nil(t, ctx.Done())
|
||||
|
||||
require.NoError(t, ctx.Err())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "context with audience",
|
||||
f: func(t *testing.T, ctx context.Context) context.Context {
|
||||
return authenticator.WithAudiences(ctx, authenticator.Audiences{"1", "2"})
|
||||
},
|
||||
wantReg: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, authenticator.Audiences{"1", "2"}, auds)
|
||||
},
|
||||
wantNew: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.False(t, ok)
|
||||
require.Nil(t, auds)
|
||||
},
|
||||
wantBoth: func(t *testing.T, ctx context.Context) {
|
||||
val, ok := ctx.Value(contextKey(0xDEADBEEF)).(string)
|
||||
require.False(t, ok)
|
||||
require.Zero(t, val)
|
||||
|
||||
deadline, ok := ctx.Deadline()
|
||||
require.False(t, ok)
|
||||
require.Zero(t, deadline)
|
||||
|
||||
require.Nil(t, ctx.Done())
|
||||
|
||||
require.NoError(t, ctx.Err())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "context with audience and past deadline",
|
||||
f: func(t *testing.T, ctx context.Context) context.Context {
|
||||
ctx = authenticator.WithAudiences(ctx, authenticator.Audiences{"3", "4"})
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, time.Now().Add(-time.Hour))
|
||||
t.Cleanup(cancel)
|
||||
return ctx
|
||||
},
|
||||
wantReg: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, authenticator.Audiences{"3", "4"}, auds)
|
||||
},
|
||||
wantNew: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.False(t, ok)
|
||||
require.Nil(t, auds)
|
||||
},
|
||||
wantBoth: func(t *testing.T, ctx context.Context) {
|
||||
val, ok := ctx.Value(contextKey(0xDEADBEEF)).(string)
|
||||
require.False(t, ok)
|
||||
require.Zero(t, val)
|
||||
|
||||
deadline, ok := ctx.Deadline()
|
||||
require.True(t, ok)
|
||||
require.NotZero(t, deadline)
|
||||
require.True(t, deadline.Before(time.Now()))
|
||||
|
||||
ch := ctx.Done()
|
||||
require.NotNil(t, ch)
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Error("expected closed done channel")
|
||||
}
|
||||
|
||||
require.Equal(t, context.DeadlineExceeded, ctx.Err())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "context with audience and custom value and past deadline",
|
||||
f: func(t *testing.T, ctx context.Context) context.Context {
|
||||
ctx = authenticator.WithAudiences(ctx, authenticator.Audiences{"3", "4"})
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, time.Now().Add(-time.Hour))
|
||||
t.Cleanup(cancel)
|
||||
ctx = context.WithValue(ctx, contextKey(0xDEADBEEF), "mooo")
|
||||
return ctx
|
||||
},
|
||||
wantReg: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, authenticator.Audiences{"3", "4"}, auds)
|
||||
|
||||
val, ok := ctx.Value(contextKey(0xDEADBEEF)).(string)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "mooo", val)
|
||||
},
|
||||
wantNew: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.False(t, ok)
|
||||
require.Nil(t, auds)
|
||||
|
||||
val, ok := ctx.Value(contextKey(0xDEADBEEF)).(string)
|
||||
require.False(t, ok)
|
||||
require.Zero(t, val)
|
||||
},
|
||||
wantBoth: func(t *testing.T, ctx context.Context) {
|
||||
deadline, ok := ctx.Deadline()
|
||||
require.True(t, ok)
|
||||
require.NotZero(t, deadline)
|
||||
require.True(t, deadline.Before(time.Now()))
|
||||
|
||||
ch := ctx.Done()
|
||||
require.NotNil(t, ch)
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Error("expected closed done channel")
|
||||
}
|
||||
|
||||
require.Equal(t, context.DeadlineExceeded, ctx.Err())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "context with audience and custom value and future deadline",
|
||||
f: func(t *testing.T, ctx context.Context) context.Context {
|
||||
ctx = authenticator.WithAudiences(ctx, authenticator.Audiences{"3", "4"})
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Hour))
|
||||
t.Cleanup(cancel)
|
||||
ctx = context.WithValue(ctx, contextKey(0xDEADBEEF), "mooo")
|
||||
return ctx
|
||||
},
|
||||
wantReg: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, authenticator.Audiences{"3", "4"}, auds)
|
||||
|
||||
val, ok := ctx.Value(contextKey(0xDEADBEEF)).(string)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "mooo", val)
|
||||
},
|
||||
wantNew: func(t *testing.T, ctx context.Context) {
|
||||
auds, ok := authenticator.AudiencesFrom(ctx)
|
||||
require.False(t, ok)
|
||||
require.Nil(t, auds)
|
||||
|
||||
val, ok := ctx.Value(contextKey(0xDEADBEEF)).(string)
|
||||
require.False(t, ok)
|
||||
require.Zero(t, val)
|
||||
},
|
||||
wantBoth: func(t *testing.T, ctx context.Context) {
|
||||
deadline, ok := ctx.Deadline()
|
||||
require.True(t, ok)
|
||||
require.NotZero(t, deadline)
|
||||
require.True(t, deadline.After(time.Now()))
|
||||
|
||||
ch := ctx.Done()
|
||||
require.NotNil(t, ch)
|
||||
select {
|
||||
case <-ch:
|
||||
t.Error("expected not closed done channel")
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
|
||||
require.NoError(t, ctx.Err())
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := tt.f(t, context.Background())
|
||||
|
||||
t.Run("reg", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt.wantReg(t, ctx)
|
||||
})
|
||||
|
||||
t.Run("reg-both", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt.wantBoth(t, ctx)
|
||||
})
|
||||
|
||||
t.Run("new", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt.wantNew(t, New(ctx))
|
||||
})
|
||||
|
||||
t.Run("new-both", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt.wantBoth(t, New(ctx))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user