Merge pull request #670 from enj/enj/f/impersonator_always_authz
impersonator: always authorize every request
This commit is contained in:
commit
e06c696bea
@ -11,7 +11,9 @@ The specifics of how it is implemented are of interest. The most novel detail
|
|||||||
about the implementation is that we use the "front-end" of the aggregated API
|
about the implementation is that we use the "front-end" of the aggregated API
|
||||||
server logic, mainly the DefaultBuildHandlerChain func, to handle how incoming
|
server logic, mainly the DefaultBuildHandlerChain func, to handle how incoming
|
||||||
requests are authenticated, authorized, etc. The "back-end" of the proxy is a
|
requests are authenticated, authorized, etc. The "back-end" of the proxy is a
|
||||||
reverse proxy that impersonates the user (instead of serving REST APIs).
|
reverse proxy that impersonates the user (instead of serving REST APIs). Since
|
||||||
|
impersonation fails open, we impersonate users via a secondary service account
|
||||||
|
that has no other permissions on the cluster.
|
||||||
|
|
||||||
In terms of authentication, we aim to handle every type of authentication that
|
In terms of authentication, we aim to handle every type of authentication that
|
||||||
the Kubernetes API server supports by delegating most of the checks to it. We
|
the Kubernetes API server supports by delegating most of the checks to it. We
|
||||||
@ -25,9 +27,12 @@ in that Pinniped itself provides the Token Credential Request API which is used
|
|||||||
specifically by anonymous users to retrieve credentials. This API is the single
|
specifically by anonymous users to retrieve credentials. This API is the single
|
||||||
API that will remain available even when anonymous authentication is disabled.
|
API that will remain available even when anonymous authentication is disabled.
|
||||||
|
|
||||||
In terms of authorization, we rely mostly on the Kubernetes API server. Since we
|
In terms of authorization, in addition to the regular checks that the Kubernetes
|
||||||
impersonate the user, the proxied request will be authorized against that user.
|
API server will make for the impersonated user, we perform the same authorization
|
||||||
Thus for all regular REST verbs, we perform no authorization checks.
|
checks via subject access review calls. This protects us from scenarios where
|
||||||
|
we fail to correctly impersonate the user due to some bug in our proxy logic.
|
||||||
|
We rely completely on the Kubernetes API server to perform admission checks on
|
||||||
|
the impersonated requests.
|
||||||
|
|
||||||
Nested impersonation is handled by performing the same authorization checks the
|
Nested impersonation is handled by performing the same authorization checks the
|
||||||
Kubernetes API server would (we get this mostly for free by using the aggregated
|
Kubernetes API server would (we get this mostly for free by using the aggregated
|
||||||
|
@ -70,7 +70,7 @@ func New(
|
|||||||
dynamicCertProvider dynamiccert.Private,
|
dynamicCertProvider dynamiccert.Private,
|
||||||
impersonationProxySignerCA dynamiccert.Public,
|
impersonationProxySignerCA dynamiccert.Public,
|
||||||
) (func(stopCh <-chan struct{}) error, error) {
|
) (func(stopCh <-chan struct{}) error, error) {
|
||||||
return newInternal(port, dynamicCertProvider, impersonationProxySignerCA, nil, nil)
|
return newInternal(port, dynamicCertProvider, impersonationProxySignerCA, nil, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInternal( //nolint:funlen // yeah, it's kind of long.
|
func newInternal( //nolint:funlen // yeah, it's kind of long.
|
||||||
@ -79,6 +79,7 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
impersonationProxySignerCA dynamiccert.Public,
|
impersonationProxySignerCA dynamiccert.Public,
|
||||||
clientOpts []kubeclient.Option, // for unit testing, should always be nil in production
|
clientOpts []kubeclient.Option, // for unit testing, should always be nil in production
|
||||||
recOpts func(*genericoptions.RecommendedOptions), // for unit testing, should always be nil in production
|
recOpts func(*genericoptions.RecommendedOptions), // for unit testing, should always be nil in production
|
||||||
|
recConfig func(*genericapiserver.RecommendedConfig), // for unit testing, should always be nil in production
|
||||||
) (func(stopCh <-chan struct{}) error, error) {
|
) (func(stopCh <-chan struct{}) error, error) {
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
|
|
||||||
@ -256,33 +257,38 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
serverConfig.Authentication.Authenticator = blockAnonymousAuthenticator
|
serverConfig.Authentication.Authenticator = blockAnonymousAuthenticator
|
||||||
|
|
||||||
delegatingAuthorizer := serverConfig.Authorization.Authorizer
|
delegatingAuthorizer := serverConfig.Authorization.Authorizer
|
||||||
nestedImpersonationAuthorizer := &comparableAuthorizer{
|
customReasonAuthorizer := &comparableAuthorizer{
|
||||||
authorizerFunc: func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
authorizerFunc: func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
const baseReason = "decision made by impersonation-proxy.concierge.pinniped.dev"
|
||||||
switch a.GetVerb() {
|
switch a.GetVerb() {
|
||||||
case "":
|
case "":
|
||||||
// Empty string is disallowed because request info has had bugs in the past where it would leave it empty.
|
// Empty string is disallowed because request info has had bugs in the past where it would leave it empty.
|
||||||
return authorizer.DecisionDeny, "invalid verb", nil
|
return authorizer.DecisionDeny, "invalid verb, " + baseReason, nil
|
||||||
case "create",
|
|
||||||
"update",
|
|
||||||
"delete",
|
|
||||||
"deletecollection",
|
|
||||||
"get",
|
|
||||||
"list",
|
|
||||||
"watch",
|
|
||||||
"patch",
|
|
||||||
"proxy":
|
|
||||||
// we know these verbs are from the request info parsing which is safe to delegate to KAS
|
|
||||||
return authorizer.DecisionAllow, "deferring standard verb authorization to kube API server", nil
|
|
||||||
default:
|
default:
|
||||||
// assume everything else is internal SAR checks that we need to run against the requesting user
|
// Since we authenticate the requesting user, we are in the best position to correctly authorize them.
|
||||||
// because when KAS does the check, it may run the check against our service account and not the
|
// When KAS does the check, it may run the check against our service account and not the requesting user
|
||||||
// requesting user. This also handles the impersonate verb to allow for nested impersonation.
|
// (due to a bug in the code or any other internal SAR checks that the request processing does).
|
||||||
return delegatingAuthorizer.Authorize(ctx, a)
|
// This also handles the impersonate verb to allow for nested impersonation.
|
||||||
|
decision, reason, err := delegatingAuthorizer.Authorize(ctx, a)
|
||||||
|
|
||||||
|
// make it easier to detect when the impersonation proxy is authorizing a request vs KAS
|
||||||
|
switch len(reason) {
|
||||||
|
case 0:
|
||||||
|
reason = baseReason
|
||||||
|
default:
|
||||||
|
reason = reason + ", " + baseReason
|
||||||
|
}
|
||||||
|
|
||||||
|
return decision, reason, err
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Set our custom authorizer before calling Compete(), which will use it.
|
// Set our custom authorizer before calling Compete(), which will use it.
|
||||||
serverConfig.Authorization.Authorizer = nestedImpersonationAuthorizer
|
serverConfig.Authorization.Authorizer = customReasonAuthorizer
|
||||||
|
|
||||||
|
if recConfig != nil {
|
||||||
|
recConfig(serverConfig)
|
||||||
|
}
|
||||||
|
|
||||||
completedConfig := serverConfig.Complete()
|
completedConfig := serverConfig.Complete()
|
||||||
impersonationProxyServer, err := completedConfig.New("impersonation-proxy", genericapiserver.NewEmptyDelegate())
|
impersonationProxyServer, err := completedConfig.New("impersonation-proxy", genericapiserver.NewEmptyDelegate())
|
||||||
@ -298,7 +304,7 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check. Make sure that our custom authorizer is still in place and did not get changed or wrapped.
|
// Sanity check. Make sure that our custom authorizer is still in place and did not get changed or wrapped.
|
||||||
if preparedRun.Authorizer != nestedImpersonationAuthorizer {
|
if preparedRun.Authorizer != customReasonAuthorizer {
|
||||||
return nil, fmt.Errorf("invalid mutation of impersonation authorizer detected: %#v", preparedRun.Authorizer)
|
return nil, fmt.Errorf("invalid mutation of impersonation authorizer detected: %#v", preparedRun.Authorizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
@ -84,6 +86,7 @@ func TestImpersonator(t *testing.T) {
|
|||||||
wantKubeAPIServerRequestHeaders http.Header
|
wantKubeAPIServerRequestHeaders http.Header
|
||||||
wantError string
|
wantError string
|
||||||
wantConstructionError string
|
wantConstructionError string
|
||||||
|
wantAuthorizerAttributes []authorizer.AttributesRecord
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "happy path",
|
name: "happy path",
|
||||||
@ -98,6 +101,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username", UID: "", Groups: []string{"test-group1", "test-group2", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path with forbidden healthz",
|
name: "happy path with forbidden healthz",
|
||||||
@ -116,6 +125,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username", UID: "", Groups: []string{"test-group1", "test-group2", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path with unauthorized healthz",
|
name: "happy path with unauthorized healthz",
|
||||||
@ -135,6 +150,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username", UID: "", Groups: []string{"test-group1", "test-group2", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path with upgrade",
|
name: "happy path with upgrade",
|
||||||
@ -160,6 +181,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Connection": {"Upgrade"},
|
"Connection": {"Upgrade"},
|
||||||
"Upgrade": {"spdy/3.1"},
|
"Upgrade": {"spdy/3.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username2", UID: "", Groups: []string{"test-group3", "test-group4", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path ignores forwarded header",
|
name: "happy path ignores forwarded header",
|
||||||
@ -177,6 +204,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username2", UID: "", Groups: []string{"test-group3", "test-group4", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path ignores forwarded header canonicalization",
|
name: "happy path ignores forwarded header canonicalization",
|
||||||
@ -194,6 +227,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username2", UID: "", Groups: []string{"test-group3", "test-group4", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "user is authenticated but the kube API request returns an error",
|
name: "user is authenticated but the kube API request returns an error",
|
||||||
@ -210,6 +249,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username", UID: "", Groups: []string{"test-group1", "test-group2", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "when there is no client cert on request, it is an anonymous request",
|
name: "when there is no client cert on request, it is an anonymous request",
|
||||||
@ -224,6 +269,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "when there is no client cert on request but it has basic auth, it is still an anonymous request",
|
name: "when there is no client cert on request but it has basic auth, it is still an anonymous request",
|
||||||
@ -244,12 +295,19 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
"Test": {"val"},
|
"Test": {"val"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "failed client cert authentication",
|
name: "failed client cert authentication",
|
||||||
clientCert: newClientCert(t, unrelatedCA, "test-username", []string{"test-group1"}),
|
clientCert: newClientCert(t, unrelatedCA, "test-username", []string{"test-group1"}),
|
||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
wantError: "Unauthorized",
|
wantError: "Unauthorized",
|
||||||
|
wantAuthorizerAttributes: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested impersonation by regular users calls delegating authorizer",
|
name: "nested impersonation by regular users calls delegating authorizer",
|
||||||
@ -258,7 +316,14 @@ func TestImpersonator(t *testing.T) {
|
|||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
// this fails because the delegating authorizer in this test only allows system:masters and fails everything else
|
// this fails because the delegating authorizer in this test only allows system:masters and fails everything else
|
||||||
wantError: `users "some-other-username" is forbidden: User "test-username" ` +
|
wantError: `users "some-other-username" is forbidden: User "test-username" ` +
|
||||||
`cannot impersonate resource "users" in API group "" at the cluster scope`,
|
`cannot impersonate resource "users" in API group "" at the cluster scope: ` +
|
||||||
|
`decision made by impersonation-proxy.concierge.pinniped.dev`,
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username", UID: "", Groups: []string{"test-group1", "test-group2", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "users", Subresource: "", Name: "some-other-username", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested impersonation by admin users calls delegating authorizer",
|
name: "nested impersonation by admin users calls delegating authorizer",
|
||||||
@ -304,6 +369,96 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "users", Subresource: "", Name: "fire", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "groups", Subresource: "", Name: "elements", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "iam.gke.io/user-assertion", Name: "good", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "iam.gke.io/user-assertion", Name: "stuff", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "alpha.kubernetes.io/identity/roles", Name: "a-role1", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "alpha.kubernetes.io/identity/roles", Name: "a-role2", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "user-assertion.cloud.google.com", Name: "smaller", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "user-assertion.cloud.google.com", Name: "things", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "colors", Name: "red", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "colors", Name: "orange", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "colors", Name: "blue", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "scopes.authorization.openshift.io", Name: "user:info", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "scopes.authorization.openshift.io", Name: "user:full", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "scopes.authorization.openshift.io", Name: "user:check-access", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "alpha.kubernetes.io/identity/project/name", Name: "a-project-name", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "alpha.kubernetes.io/identity/user/domain/id", Name: "a-domain-id", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "alpha.kubernetes.io/identity/user/domain/name", Name: "a-domain-name", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "alpha.kubernetes.io/identity/project/id", Name: "a-project-id", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "fire", UID: "", Groups: []string{"elements", "system:authenticated"},
|
||||||
|
Extra: map[string][]string{
|
||||||
|
"alpha.kubernetes.io/identity/project/id": {"a-project-id"},
|
||||||
|
"alpha.kubernetes.io/identity/project/name": {"a-project-name"},
|
||||||
|
"alpha.kubernetes.io/identity/roles": {"a-role1", "a-role2"},
|
||||||
|
"alpha.kubernetes.io/identity/user/domain/id": {"a-domain-id"},
|
||||||
|
"alpha.kubernetes.io/identity/user/domain/name": {"a-domain-name"},
|
||||||
|
"colors": {"red", "orange", "blue"},
|
||||||
|
"iam.gke.io/user-assertion": {"good", "stuff"},
|
||||||
|
"scopes.authorization.openshift.io": {"user:info", "user:full", "user:check-access"},
|
||||||
|
"user-assertion.cloud.google.com": {"smaller", "things"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested impersonation by admin users cannot impersonate UID",
|
name: "nested impersonation by admin users cannot impersonate UID",
|
||||||
@ -314,6 +469,16 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
wantError: "Internal error occurred: invalid impersonation",
|
wantError: "Internal error occurred: invalid impersonation",
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "users", Subresource: "", Name: "some-other-username", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "some-other-username", UID: "", Groups: []string{"system:authenticated"}, Extra: map[string][]string{}},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested impersonation by admin users cannot impersonate UID header canonicalization",
|
name: "nested impersonation by admin users cannot impersonate UID header canonicalization",
|
||||||
@ -324,6 +489,16 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
wantError: "Internal error occurred: invalid impersonation",
|
wantError: "Internal error occurred: invalid impersonation",
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "users", Subresource: "", Name: "some-other-username", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "some-other-username", UID: "", Groups: []string{"system:authenticated"}, Extra: map[string][]string{}},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested impersonation by admin users cannot use reserved key",
|
name: "nested impersonation by admin users cannot use reserved key",
|
||||||
@ -338,6 +513,33 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
wantError: "Internal error occurred: unimplemented functionality - unable to act as current user",
|
wantError: "Internal error occurred: unimplemented functionality - unable to act as current user",
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "users", Subresource: "", Name: "other-user-to-impersonate", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "groups", Subresource: "", Name: "other-peeps", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "something.impersonation-proxy.concierge.pinniped.dev", Name: "bad data", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "key", Name: "good", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "other-user-to-impersonate", UID: "", Groups: []string{"other-peeps", "system:authenticated"},
|
||||||
|
Extra: map[string][]string{
|
||||||
|
"key": {"good"},
|
||||||
|
"something.impersonation-proxy.concierge.pinniped.dev": {"bad data"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested impersonation by admin users cannot use invalid key",
|
name: "nested impersonation by admin users cannot use invalid key",
|
||||||
@ -351,6 +553,24 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
wantError: "Internal error occurred: unimplemented functionality - unable to act as current user",
|
wantError: "Internal error occurred: unimplemented functionality - unable to act as current user",
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "users", Subresource: "", Name: "panda", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "groups", Subresource: "", Name: "other-peeps", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "party~~time", Name: "danger", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "panda", UID: "", Groups: []string{"other-peeps", "system:authenticated"}, Extra: map[string][]string{"party~~time": {"danger"}}},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nested impersonation by admin users can use uppercase key because impersonation is lossy",
|
name: "nested impersonation by admin users can use uppercase key because impersonation is lossy",
|
||||||
@ -374,10 +594,29 @@ func TestImpersonator(t *testing.T) {
|
|||||||
"Accept-Encoding": {"gzip"},
|
"Accept-Encoding": {"gzip"},
|
||||||
"X-Forwarded-For": {"127.0.0.1"},
|
"X-Forwarded-For": {"127.0.0.1"},
|
||||||
},
|
},
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "users", Subresource: "", Name: "panda", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "groups", Subresource: "", Name: "other-peeps", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-admin", UID: "", Groups: []string{"test-group2", "system:masters", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "authentication.k8s.io", APIVersion: "v1", Resource: "userextras", Subresource: "roar", Name: "tiger", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "panda", UID: "", Groups: []string{"other-peeps", "system:authenticated"}, Extra: map[string][]string{"roar": {"tiger"}}},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no bearer token file in Kube API server client config",
|
name: "no bearer token file in Kube API server client config",
|
||||||
wantConstructionError: "invalid impersonator loopback rest config has wrong bearer token semantics",
|
wantConstructionError: "invalid impersonator loopback rest config has wrong bearer token semantics",
|
||||||
|
wantAuthorizerAttributes: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unexpected healthz response",
|
name: "unexpected healthz response",
|
||||||
@ -385,7 +624,8 @@ func TestImpersonator(t *testing.T) {
|
|||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, _ = w.Write([]byte("broken"))
|
_, _ = w.Write([]byte("broken"))
|
||||||
}),
|
}),
|
||||||
wantConstructionError: `could not detect if anonymous authentication is enabled: an error on the server ("broken") has prevented the request from succeeding`,
|
wantConstructionError: `could not detect if anonymous authentication is enabled: an error on the server ("broken") has prevented the request from succeeding`,
|
||||||
|
wantAuthorizerAttributes: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "header canonicalization user header",
|
name: "header canonicalization user header",
|
||||||
@ -395,7 +635,14 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
wantError: `users "PANDA" is forbidden: User "test-username" ` +
|
wantError: `users "PANDA" is forbidden: User "test-username" ` +
|
||||||
`cannot impersonate resource "users" in API group "" at the cluster scope`,
|
`cannot impersonate resource "users" in API group "" at the cluster scope: ` +
|
||||||
|
`decision made by impersonation-proxy.concierge.pinniped.dev`,
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username", UID: "", Groups: []string{"test-group1", "test-group2", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "impersonate", Namespace: "", APIGroup: "", APIVersion: "", Resource: "users", Subresource: "", Name: "PANDA", ResourceRequest: true, Path: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "header canonicalization future UID header",
|
name: "header canonicalization future UID header",
|
||||||
@ -405,6 +652,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
wantError: "Internal error occurred: invalid impersonation",
|
wantError: "Internal error occurred: invalid impersonation",
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username", UID: "", Groups: []string{"test-group1", "test-group2", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "future UID header",
|
name: "future UID header",
|
||||||
@ -414,6 +667,12 @@ func TestImpersonator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
kubeAPIServerClientBearerTokenFile: "required-to-be-set",
|
||||||
wantError: "Internal error occurred: invalid impersonation",
|
wantError: "Internal error occurred: invalid impersonation",
|
||||||
|
wantAuthorizerAttributes: []authorizer.AttributesRecord{
|
||||||
|
{
|
||||||
|
User: &user.DefaultInfo{Name: "test-username", UID: "", Groups: []string{"test-group1", "test-group2", "system:authenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "", APIVersion: "v1", Resource: "namespaces", Subresource: "", Name: "", ResourceRequest: true, Path: "/api/v1/namespaces",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -548,8 +807,29 @@ func TestImpersonator(t *testing.T) {
|
|||||||
options.SecureServing.Listener = listener // use our listener with the dynamic port
|
options.SecureServing.Listener = listener // use our listener with the dynamic port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recorder := &attributeRecorder{}
|
||||||
|
defer func() {
|
||||||
|
require.ElementsMatch(t, tt.wantAuthorizerAttributes, recorder.attributes)
|
||||||
|
require.Len(t, recorder.attributes, len(tt.wantAuthorizerAttributes))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Allow standard REST verbs to be authorized so that tests pass without invasive changes
|
||||||
|
recConfig := func(config *genericapiserver.RecommendedConfig) {
|
||||||
|
authz := config.Authorization.Authorizer.(*comparableAuthorizer)
|
||||||
|
delegate := authz.authorizerFunc
|
||||||
|
authz.authorizerFunc = func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
recorder.record(a)
|
||||||
|
switch a.GetVerb() {
|
||||||
|
case "create", "get", "list":
|
||||||
|
return authorizer.DecisionAllow, "standard verbs are allowed in tests", nil
|
||||||
|
default:
|
||||||
|
return delegate(ctx, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create an impersonator. Use an invalid port number to make sure our listener override works.
|
// Create an impersonator. Use an invalid port number to make sure our listener override works.
|
||||||
runner, constructionErr := newInternal(-1000, certKeyContent, caContent, clientOpts, recOpts)
|
runner, constructionErr := newInternal(-1000, certKeyContent, caContent, clientOpts, recOpts, recConfig)
|
||||||
if len(tt.wantConstructionError) > 0 {
|
if len(tt.wantConstructionError) > 0 {
|
||||||
require.EqualError(t, constructionErr, tt.wantConstructionError)
|
require.EqualError(t, constructionErr, tt.wantConstructionError)
|
||||||
require.Nil(t, runner)
|
require.Nil(t, runner)
|
||||||
@ -620,6 +900,34 @@ func TestImpersonator(t *testing.T) {
|
|||||||
// of the original request mutated by the impersonator. Otherwise the headers should be nil.
|
// of the original request mutated by the impersonator. Otherwise the headers should be nil.
|
||||||
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
|
require.Equal(t, tt.wantKubeAPIServerRequestHeaders, testKubeAPIServerSawHeaders)
|
||||||
|
|
||||||
|
// these authorization checks are caused by the anonymous auth checks below
|
||||||
|
tt.wantAuthorizerAttributes = append(tt.wantAuthorizerAttributes,
|
||||||
|
authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil},
|
||||||
|
Verb: "create", Namespace: "", APIGroup: "login.concierge.pinniped.dev", APIVersion: "v1alpha1", Resource: "tokencredentialrequests", Subresource: "", Name: "", ResourceRequest: true, Path: "/apis/login.concierge.pinniped.dev/v1alpha1/tokencredentialrequests",
|
||||||
|
},
|
||||||
|
authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil},
|
||||||
|
Verb: "create", Namespace: "", APIGroup: "login.concierge.walrus.tld", APIVersion: "v1alpha1", Resource: "tokencredentialrequests", Subresource: "", Name: "", ResourceRequest: true, Path: "/apis/login.concierge.walrus.tld/v1alpha1/tokencredentialrequests",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if !tt.anonymousAuthDisabled {
|
||||||
|
tt.wantAuthorizerAttributes = append(tt.wantAuthorizerAttributes,
|
||||||
|
authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil},
|
||||||
|
Verb: "get", Namespace: "", APIGroup: "", APIVersion: "", Resource: "", Subresource: "", Name: "", ResourceRequest: false, Path: "/probe",
|
||||||
|
},
|
||||||
|
authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "not-concierge.walrus.tld", APIVersion: "v1", Resource: "tokencredentialrequests", Subresource: "", Name: "", ResourceRequest: true, Path: "/apis/not-concierge.walrus.tld/v1/tokencredentialrequests",
|
||||||
|
},
|
||||||
|
authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{Name: "system:anonymous", UID: "", Groups: []string{"system:unauthenticated"}, Extra: nil},
|
||||||
|
Verb: "list", Namespace: "", APIGroup: "not-concierge.walrus.tld", APIVersion: "v1", Resource: "ducks", Subresource: "", Name: "", ResourceRequest: true, Path: "/apis/not-concierge.walrus.tld/v1/ducks",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// anonymous TCR should always work
|
// anonymous TCR should always work
|
||||||
|
|
||||||
tcrRegGroup, err := kubeclient.New(kubeclient.WithConfig(rest.AnonymousClientConfig(clientKubeconfig)))
|
tcrRegGroup, err := kubeclient.New(kubeclient.WithConfig(rest.AnonymousClientConfig(clientKubeconfig)))
|
||||||
@ -1705,3 +2013,14 @@ func Test_withBearerTokenPreservation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type attributeRecorder struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
attributes []authorizer.AttributesRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *attributeRecorder) record(attributes authorizer.Attributes) {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
r.attributes = append(r.attributes, *attributes.(*authorizer.AttributesRecord))
|
||||||
|
}
|
||||||
|
@ -566,7 +566,8 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
require.True(t, k8serrors.IsForbidden(err), err)
|
require.True(t, k8serrors.IsForbidden(err), err)
|
||||||
require.EqualError(t, err, fmt.Sprintf(
|
require.EqualError(t, err, fmt.Sprintf(
|
||||||
`users "other-user-to-impersonate" is forbidden: `+
|
`users "other-user-to-impersonate" is forbidden: `+
|
||||||
`User "%s" cannot impersonate resource "users" in API group "" at the cluster scope`,
|
`User "%s" cannot impersonate resource "users" in API group "" at the cluster scope: `+
|
||||||
|
`decision made by impersonation-proxy.concierge.pinniped.dev`,
|
||||||
env.TestUser.ExpectedUsername))
|
env.TestUser.ExpectedUsername))
|
||||||
|
|
||||||
// impersonate the GC service account instead which can read anything (the binding to edit allows this)
|
// impersonate the GC service account instead which can read anything (the binding to edit allows this)
|
||||||
@ -614,7 +615,8 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
require.True(t, k8serrors.IsForbidden(err), err)
|
require.True(t, k8serrors.IsForbidden(err), err)
|
||||||
require.EqualError(t, err, fmt.Sprintf(
|
require.EqualError(t, err, fmt.Sprintf(
|
||||||
`userextras.authentication.k8s.io "with a dangerous value" is forbidden: `+
|
`userextras.authentication.k8s.io "with a dangerous value" is forbidden: `+
|
||||||
`User "%s" cannot impersonate resource "userextras/some-fancy-key" in API group "authentication.k8s.io" at the cluster scope`,
|
`User "%s" cannot impersonate resource "userextras/some-fancy-key" in API group "authentication.k8s.io" at the cluster scope: `+
|
||||||
|
`decision made by impersonation-proxy.concierge.pinniped.dev`,
|
||||||
env.TestUser.ExpectedUsername))
|
env.TestUser.ExpectedUsername))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -672,7 +674,8 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
// the impersonated user lacks the RBAC to perform this call
|
// the impersonated user lacks the RBAC to perform this call
|
||||||
require.True(t, k8serrors.IsForbidden(err), err)
|
require.True(t, k8serrors.IsForbidden(err), err)
|
||||||
require.EqualError(t, err, fmt.Sprintf(
|
require.EqualError(t, err, fmt.Sprintf(
|
||||||
`secrets "%s" is forbidden: User "other-user-to-impersonate" cannot get resource "secrets" in API group "" in the namespace "%s"`,
|
`secrets "%s" is forbidden: User "other-user-to-impersonate" cannot get resource "secrets" in API group "" in the namespace "%s": `+
|
||||||
|
`decision made by impersonation-proxy.concierge.pinniped.dev`,
|
||||||
impersonationProxyTLSSecretName(env), env.ConciergeNamespace,
|
impersonationProxyTLSSecretName(env), env.ConciergeNamespace,
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -702,7 +705,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
clusterAdminCredentials, impersonationProxyURL, impersonationProxyCACertPEM,
|
clusterAdminCredentials, impersonationProxyURL, impersonationProxyCACertPEM,
|
||||||
&rest.ImpersonationConfig{
|
&rest.ImpersonationConfig{
|
||||||
UserName: "other-user-to-impersonate",
|
UserName: "other-user-to-impersonate",
|
||||||
Groups: []string{"other-group-1", "other-group-2"},
|
Groups: []string{"other-group-1", "other-group-2", "system:masters"}, // impersonate system:masters so we get past authorization checks
|
||||||
Extra: map[string][]string{
|
Extra: map[string][]string{
|
||||||
"this-good-key": {"to this good value"},
|
"this-good-key": {"to this good value"},
|
||||||
"something.impersonation-proxy.concierge.pinniped.dev": {"super sneaky value"},
|
"something.impersonation-proxy.concierge.pinniped.dev": {"super sneaky value"},
|
||||||
@ -744,7 +747,8 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
require.True(t, k8serrors.IsForbidden(err), err)
|
require.True(t, k8serrors.IsForbidden(err), err)
|
||||||
require.EqualError(t, err, fmt.Sprintf(
|
require.EqualError(t, err, fmt.Sprintf(
|
||||||
`serviceaccounts "root-ca-cert-publisher" is forbidden: `+
|
`serviceaccounts "root-ca-cert-publisher" is forbidden: `+
|
||||||
`User "%s" cannot impersonate resource "serviceaccounts" in API group "" in the namespace "kube-system"`,
|
`User "%s" cannot impersonate resource "serviceaccounts" in API group "" in the namespace "kube-system": `+
|
||||||
|
`decision made by impersonation-proxy.concierge.pinniped.dev`,
|
||||||
serviceaccount.MakeUsername(namespaceName, saName)))
|
serviceaccount.MakeUsername(namespaceName, saName)))
|
||||||
|
|
||||||
// webhook authorizer deny cache TTL is 10 seconds so we need to wait long enough for it to drain
|
// webhook authorizer deny cache TTL is 10 seconds so we need to wait long enough for it to drain
|
||||||
@ -1271,7 +1275,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
|
|
||||||
healthzLog, errHealthzLog := impersonationProxyAdminRestClientAsAnonymous.Get().AbsPath("/healthz/log").DoRaw(ctx)
|
healthzLog, errHealthzLog := impersonationProxyAdminRestClientAsAnonymous.Get().AbsPath("/healthz/log").DoRaw(ctx)
|
||||||
require.True(t, k8serrors.IsForbidden(errHealthzLog), "%s\n%s", library.Sdump(errHealthzLog), string(healthzLog))
|
require.True(t, k8serrors.IsForbidden(errHealthzLog), "%s\n%s", library.Sdump(errHealthzLog), string(healthzLog))
|
||||||
require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User \"system:anonymous\" cannot get path \"/healthz/log\"","reason":"Forbidden","details":{},"code":403}`+"\n", string(healthzLog))
|
require.Equal(t, `{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"forbidden: User \"system:anonymous\" cannot get path \"/healthz/log\": decision made by impersonation-proxy.concierge.pinniped.dev","reason":"Forbidden","details":{},"code":403}`+"\n", string(healthzLog))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1302,7 +1306,8 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
pod, err := impersonationProxyAnonymousClient.Kubernetes.CoreV1().Pods(metav1.NamespaceSystem).
|
pod, err := impersonationProxyAnonymousClient.Kubernetes.CoreV1().Pods(metav1.NamespaceSystem).
|
||||||
Get(ctx, "does-not-matter", metav1.GetOptions{})
|
Get(ctx, "does-not-matter", metav1.GetOptions{})
|
||||||
require.True(t, k8serrors.IsForbidden(err), library.Sdump(err))
|
require.True(t, k8serrors.IsForbidden(err), library.Sdump(err))
|
||||||
require.EqualError(t, err, `pods "does-not-matter" is forbidden: User "system:anonymous" cannot get resource "pods" in API group "" in the namespace "kube-system"`, library.Sdump(err))
|
require.EqualError(t, err, `pods "does-not-matter" is forbidden: User "system:anonymous" cannot get resource "pods" in API group "" in the namespace "kube-system": `+
|
||||||
|
`decision made by impersonation-proxy.concierge.pinniped.dev`, library.Sdump(err))
|
||||||
require.Equal(t, &corev1.Pod{}, pod)
|
require.Equal(t, &corev1.Pod{}, pod)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1373,15 +1378,18 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("assert correct impersonator service account is being used", func(t *testing.T) {
|
t.Run("assert correct impersonator service account is being used", func(t *testing.T) {
|
||||||
impersonationProxyNodesClient := impersonationProxyKubeClient(t).CoreV1().Nodes() // pick some resource the test user cannot access
|
// pick an API that everyone can access but always make invalid requests to it
|
||||||
|
// we can tell that the request is reaching KAS because only it has the validation logic
|
||||||
|
impersonationProxySSRRClient := impersonationProxyKubeClient(t).AuthorizationV1().SelfSubjectRulesReviews()
|
||||||
crbClient := adminClient.RbacV1().ClusterRoleBindings()
|
crbClient := adminClient.RbacV1().ClusterRoleBindings()
|
||||||
impersonationProxyName := env.ConciergeAppName + "-impersonation-proxy"
|
impersonationProxyName := env.ConciergeAppName + "-impersonation-proxy"
|
||||||
saFullName := serviceaccount.MakeUsername(env.ConciergeNamespace, impersonationProxyName)
|
saFullName := serviceaccount.MakeUsername(env.ConciergeNamespace, impersonationProxyName)
|
||||||
|
invalidSSRR := &authorizationv1.SelfSubjectRulesReview{}
|
||||||
|
|
||||||
// sanity check default expected error message
|
// sanity check default expected error message
|
||||||
_, err := impersonationProxyNodesClient.List(ctx, metav1.ListOptions{})
|
_, err := impersonationProxySSRRClient.Create(ctx, invalidSSRR, metav1.CreateOptions{})
|
||||||
require.True(t, k8serrors.IsForbidden(err), library.Sdump(err))
|
require.True(t, k8serrors.IsBadRequest(err), library.Sdump(err))
|
||||||
require.EqualError(t, err, `nodes is forbidden: User "`+env.TestUser.ExpectedUsername+`" cannot list resource "nodes" in API group "" at the cluster scope`)
|
require.EqualError(t, err, "no namespace on request")
|
||||||
|
|
||||||
// remove the impersonation proxy SA's permissions
|
// remove the impersonation proxy SA's permissions
|
||||||
crb, err := crbClient.Get(ctx, impersonationProxyName, metav1.GetOptions{})
|
crb, err := crbClient.Get(ctx, impersonationProxyName, metav1.GetOptions{})
|
||||||
@ -1418,26 +1426,23 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
|
|||||||
|
|
||||||
// assert that the impersonation proxy stops working when we remove its permissions
|
// assert that the impersonation proxy stops working when we remove its permissions
|
||||||
library.RequireEventuallyWithoutError(t, func() (bool, error) {
|
library.RequireEventuallyWithoutError(t, func() (bool, error) {
|
||||||
_, errList := impersonationProxyNodesClient.List(ctx, metav1.ListOptions{})
|
_, errCreate := impersonationProxySSRRClient.Create(ctx, invalidSSRR, metav1.CreateOptions{})
|
||||||
if errList == nil {
|
|
||||||
return false, fmt.Errorf("unexpected nil error for test user node list")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !k8serrors.IsForbidden(errList) {
|
switch {
|
||||||
return false, fmt.Errorf("unexpected error for test user node list: %w", errList)
|
case errCreate == nil:
|
||||||
}
|
return false, fmt.Errorf("unexpected nil error for test user create invalid SSRR")
|
||||||
|
|
||||||
switch errList.Error() {
|
case k8serrors.IsBadRequest(errCreate) && errCreate.Error() == "no namespace on request":
|
||||||
case `nodes is forbidden: User "` + env.TestUser.ExpectedUsername + `" cannot list resource "nodes" in API group "" at the cluster scope`:
|
|
||||||
t.Log("waiting for impersonation proxy service account to lose impersonate permissions")
|
t.Log("waiting for impersonation proxy service account to lose impersonate permissions")
|
||||||
return false, nil // RBAC change has not rolled out yet
|
return false, nil // RBAC change has not rolled out yet
|
||||||
|
|
||||||
case `users "` + env.TestUser.ExpectedUsername + `" is forbidden: User "` + saFullName +
|
case k8serrors.IsForbidden(errCreate) && errCreate.Error() ==
|
||||||
`" cannot impersonate resource "users" in API group "" at the cluster scope`:
|
`users "`+env.TestUser.ExpectedUsername+`" is forbidden: User "`+saFullName+
|
||||||
|
`" cannot impersonate resource "users" in API group "" at the cluster scope`:
|
||||||
return true, nil // expected RBAC error
|
return true, nil // expected RBAC error
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf("unexpected forbidden error for test user node list: %w", errList)
|
return false, fmt.Errorf("unexpected error for test user create invalid SSRR: %w", errCreate)
|
||||||
}
|
}
|
||||||
}, time.Minute, time.Second)
|
}, time.Minute, time.Second)
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user