Merge pull request #84768 from liggitt/delegated-authnz-v1
switch delegated authnz to v1 APIs
This commit is contained in:
@@ -53,7 +53,7 @@ go_test(
|
|||||||
"//pkg/kubeapiserver/options:go_default_library",
|
"//pkg/kubeapiserver/options:go_default_library",
|
||||||
"//pkg/kubelet/client:go_default_library",
|
"//pkg/kubelet/client:go_default_library",
|
||||||
"//pkg/master/reconcilers:go_default_library",
|
"//pkg/master/reconcilers:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
@@ -64,6 +64,8 @@ go_test(
|
|||||||
"//staging/src/k8s.io/component-base/cli/flag:go_default_library",
|
"//staging/src/k8s.io/component-base/cli/flag:go_default_library",
|
||||||
"//staging/src/k8s.io/component-base/cli/globalflag:go_default_library",
|
"//staging/src/k8s.io/component-base/cli/globalflag:go_default_library",
|
||||||
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
|
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
|
||||||
|
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
|
||||||
|
"//vendor/github.com/google/go-cmp/cmp/cmpopts:go_default_library",
|
||||||
"//vendor/github.com/spf13/pflag:go_default_library",
|
"//vendor/github.com/spf13/pflag:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -22,9 +22,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/diff"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
apiserveroptions "k8s.io/apiserver/pkg/server/options"
|
apiserveroptions "k8s.io/apiserver/pkg/server/options"
|
||||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||||
auditbuffered "k8s.io/apiserver/plugin/pkg/audit/buffered"
|
auditbuffered "k8s.io/apiserver/plugin/pkg/audit/buffered"
|
||||||
@@ -266,6 +268,7 @@ func TestAddFlags(t *testing.T) {
|
|||||||
WebHook: &kubeoptions.WebHookAuthenticationOptions{
|
WebHook: &kubeoptions.WebHookAuthenticationOptions{
|
||||||
CacheTTL: 180000000000,
|
CacheTTL: 180000000000,
|
||||||
ConfigFile: "/token-webhook-config",
|
ConfigFile: "/token-webhook-config",
|
||||||
|
Version: "v1beta1",
|
||||||
},
|
},
|
||||||
BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{},
|
BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{},
|
||||||
OIDC: &kubeoptions.OIDCAuthenticationOptions{
|
OIDC: &kubeoptions.OIDCAuthenticationOptions{
|
||||||
@@ -287,6 +290,7 @@ func TestAddFlags(t *testing.T) {
|
|||||||
WebhookConfigFile: "/webhook-config",
|
WebhookConfigFile: "/webhook-config",
|
||||||
WebhookCacheAuthorizedTTL: 180000000000,
|
WebhookCacheAuthorizedTTL: 180000000000,
|
||||||
WebhookCacheUnauthorizedTTL: 60000000000,
|
WebhookCacheUnauthorizedTTL: 60000000000,
|
||||||
|
WebhookVersion: "v1beta1",
|
||||||
},
|
},
|
||||||
CloudProvider: &kubeoptions.CloudProviderOptions{
|
CloudProvider: &kubeoptions.CloudProviderOptions{
|
||||||
CloudConfigFile: "/cloud-config",
|
CloudConfigFile: "/cloud-config",
|
||||||
@@ -305,6 +309,6 @@ func TestAddFlags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(expected, s) {
|
if !reflect.DeepEqual(expected, s) {
|
||||||
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", diff.ObjectReflectDiff(expected, s))
|
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -125,8 +125,8 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiserver/pkg/server/healthz:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/server/healthz:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1beta1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1beta1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||||
|
@@ -28,8 +28,8 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
||||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
|
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1"
|
||||||
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
|
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||||
|
|
||||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||||
"k8s.io/kubernetes/pkg/kubelet/server"
|
"k8s.io/kubernetes/pkg/kubelet/server"
|
||||||
@@ -43,8 +43,8 @@ func BuildAuth(nodeName types.NodeName, client clientset.Interface, config kubel
|
|||||||
sarClient authorizationclient.SubjectAccessReviewInterface
|
sarClient authorizationclient.SubjectAccessReviewInterface
|
||||||
)
|
)
|
||||||
if client != nil && !reflect.ValueOf(client).IsNil() {
|
if client != nil && !reflect.ValueOf(client).IsNil() {
|
||||||
tokenClient = client.AuthenticationV1beta1().TokenReviews()
|
tokenClient = client.AuthenticationV1().TokenReviews()
|
||||||
sarClient = client.AuthorizationV1beta1().SubjectAccessReviews()
|
sarClient = client.AuthorizationV1().SubjectAccessReviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticator, err := BuildAuthn(tokenClient, config.Authentication)
|
authenticator, err := BuildAuthn(tokenClient, config.Authentication)
|
||||||
|
@@ -68,6 +68,7 @@ type Config struct {
|
|||||||
ServiceAccountIssuer string
|
ServiceAccountIssuer string
|
||||||
APIAudiences authenticator.Audiences
|
APIAudiences authenticator.Audiences
|
||||||
WebhookTokenAuthnConfigFile string
|
WebhookTokenAuthnConfigFile string
|
||||||
|
WebhookTokenAuthnVersion string
|
||||||
WebhookTokenAuthnCacheTTL time.Duration
|
WebhookTokenAuthnCacheTTL time.Duration
|
||||||
|
|
||||||
TokenSuccessCacheTTL time.Duration
|
TokenSuccessCacheTTL time.Duration
|
||||||
@@ -179,7 +180,7 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er
|
|||||||
tokenAuthenticators = append(tokenAuthenticators, oidcAuth)
|
tokenAuthenticators = append(tokenAuthenticators, oidcAuth)
|
||||||
}
|
}
|
||||||
if len(config.WebhookTokenAuthnConfigFile) > 0 {
|
if len(config.WebhookTokenAuthnConfigFile) > 0 {
|
||||||
webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL, config.APIAudiences)
|
webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnVersion, config.WebhookTokenAuthnCacheTTL, config.APIAudiences)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -305,8 +306,8 @@ func newServiceAccountAuthenticator(iss string, keyfiles []string, apiAudiences
|
|||||||
return tokenAuthenticator, nil
|
return tokenAuthenticator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
|
func newWebhookTokenAuthenticator(webhookConfigFile string, version string, ttl time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
|
||||||
webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, implicitAuds)
|
webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, version, implicitAuds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -46,6 +46,8 @@ type Config struct {
|
|||||||
|
|
||||||
// Kubeconfig file for Webhook authorization plugin.
|
// Kubeconfig file for Webhook authorization plugin.
|
||||||
WebhookConfigFile string
|
WebhookConfigFile string
|
||||||
|
// API version of subject access reviews to send to the webhook (e.g. "v1", "v1beta1")
|
||||||
|
WebhookVersion string
|
||||||
// TTL for caching of authorized responses from the webhook server.
|
// TTL for caching of authorized responses from the webhook server.
|
||||||
WebhookCacheAuthorizedTTL time.Duration
|
WebhookCacheAuthorizedTTL time.Duration
|
||||||
// TTL for caching of unauthorized responses from the webhook server.
|
// TTL for caching of unauthorized responses from the webhook server.
|
||||||
@@ -98,6 +100,7 @@ func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, erro
|
|||||||
ruleResolvers = append(ruleResolvers, abacAuthorizer)
|
ruleResolvers = append(ruleResolvers, abacAuthorizer)
|
||||||
case modes.ModeWebhook:
|
case modes.ModeWebhook:
|
||||||
webhookAuthorizer, err := webhook.New(config.WebhookConfigFile,
|
webhookAuthorizer, err := webhook.New(config.WebhookConfigFile,
|
||||||
|
config.WebhookVersion,
|
||||||
config.WebhookCacheAuthorizedTTL,
|
config.WebhookCacheAuthorizedTTL,
|
||||||
config.WebhookCacheUnauthorizedTTL)
|
config.WebhookCacheUnauthorizedTTL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -90,6 +90,7 @@ type TokenFileAuthenticationOptions struct {
|
|||||||
|
|
||||||
type WebHookAuthenticationOptions struct {
|
type WebHookAuthenticationOptions struct {
|
||||||
ConfigFile string
|
ConfigFile string
|
||||||
|
Version string
|
||||||
CacheTTL time.Duration
|
CacheTTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +156,7 @@ func (s *BuiltInAuthenticationOptions) WithTokenFile() *BuiltInAuthenticationOpt
|
|||||||
|
|
||||||
func (s *BuiltInAuthenticationOptions) WithWebHook() *BuiltInAuthenticationOptions {
|
func (s *BuiltInAuthenticationOptions) WithWebHook() *BuiltInAuthenticationOptions {
|
||||||
s.WebHook = &WebHookAuthenticationOptions{
|
s.WebHook = &WebHookAuthenticationOptions{
|
||||||
|
Version: "v1beta1",
|
||||||
CacheTTL: 2 * time.Minute,
|
CacheTTL: 2 * time.Minute,
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
@@ -303,6 +305,9 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
|
|||||||
"File with webhook configuration for token authentication in kubeconfig format. "+
|
"File with webhook configuration for token authentication in kubeconfig format. "+
|
||||||
"The API server will query the remote service to determine authentication for bearer tokens.")
|
"The API server will query the remote service to determine authentication for bearer tokens.")
|
||||||
|
|
||||||
|
fs.StringVar(&s.WebHook.Version, "authentication-token-webhook-version", s.WebHook.Version, ""+
|
||||||
|
"The API version of the authentication.k8s.io TokenReview to send to and expect from the webhook.")
|
||||||
|
|
||||||
fs.DurationVar(&s.WebHook.CacheTTL, "authentication-token-webhook-cache-ttl", s.WebHook.CacheTTL,
|
fs.DurationVar(&s.WebHook.CacheTTL, "authentication-token-webhook-cache-ttl", s.WebHook.CacheTTL,
|
||||||
"The duration to cache responses from the webhook token authenticator.")
|
"The duration to cache responses from the webhook token authenticator.")
|
||||||
}
|
}
|
||||||
@@ -370,6 +375,7 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
|
|||||||
|
|
||||||
if s.WebHook != nil {
|
if s.WebHook != nil {
|
||||||
ret.WebhookTokenAuthnConfigFile = s.WebHook.ConfigFile
|
ret.WebhookTokenAuthnConfigFile = s.WebHook.ConfigFile
|
||||||
|
ret.WebhookTokenAuthnVersion = s.WebHook.Version
|
||||||
ret.WebhookTokenAuthnCacheTTL = s.WebHook.CacheTTL
|
ret.WebhookTokenAuthnCacheTTL = s.WebHook.CacheTTL
|
||||||
|
|
||||||
if len(s.WebHook.ConfigFile) > 0 && s.WebHook.CacheTTL > 0 {
|
if len(s.WebHook.ConfigFile) > 0 && s.WebHook.CacheTTL > 0 {
|
||||||
|
@@ -33,6 +33,7 @@ type BuiltInAuthorizationOptions struct {
|
|||||||
Modes []string
|
Modes []string
|
||||||
PolicyFile string
|
PolicyFile string
|
||||||
WebhookConfigFile string
|
WebhookConfigFile string
|
||||||
|
WebhookVersion string
|
||||||
WebhookCacheAuthorizedTTL time.Duration
|
WebhookCacheAuthorizedTTL time.Duration
|
||||||
WebhookCacheUnauthorizedTTL time.Duration
|
WebhookCacheUnauthorizedTTL time.Duration
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ type BuiltInAuthorizationOptions struct {
|
|||||||
func NewBuiltInAuthorizationOptions() *BuiltInAuthorizationOptions {
|
func NewBuiltInAuthorizationOptions() *BuiltInAuthorizationOptions {
|
||||||
return &BuiltInAuthorizationOptions{
|
return &BuiltInAuthorizationOptions{
|
||||||
Modes: []string{authzmodes.ModeAlwaysAllow},
|
Modes: []string{authzmodes.ModeAlwaysAllow},
|
||||||
|
WebhookVersion: "v1beta1",
|
||||||
WebhookCacheAuthorizedTTL: 5 * time.Minute,
|
WebhookCacheAuthorizedTTL: 5 * time.Minute,
|
||||||
WebhookCacheUnauthorizedTTL: 30 * time.Second,
|
WebhookCacheUnauthorizedTTL: 30 * time.Second,
|
||||||
}
|
}
|
||||||
@@ -99,6 +101,9 @@ func (s *BuiltInAuthorizationOptions) AddFlags(fs *pflag.FlagSet) {
|
|||||||
"File with webhook configuration in kubeconfig format, used with --authorization-mode=Webhook. "+
|
"File with webhook configuration in kubeconfig format, used with --authorization-mode=Webhook. "+
|
||||||
"The API server will query the remote service to determine access on the API server's secure port.")
|
"The API server will query the remote service to determine access on the API server's secure port.")
|
||||||
|
|
||||||
|
fs.StringVar(&s.WebhookVersion, "authorization-webhook-version", s.WebhookVersion, ""+
|
||||||
|
"The API version of the authorization.k8s.io SubjectAccessReview to send to and expect from the webhook.")
|
||||||
|
|
||||||
fs.DurationVar(&s.WebhookCacheAuthorizedTTL, "authorization-webhook-cache-authorized-ttl",
|
fs.DurationVar(&s.WebhookCacheAuthorizedTTL, "authorization-webhook-cache-authorized-ttl",
|
||||||
s.WebhookCacheAuthorizedTTL,
|
s.WebhookCacheAuthorizedTTL,
|
||||||
"The duration to cache 'authorized' responses from the webhook authorizer.")
|
"The duration to cache 'authorized' responses from the webhook authorizer.")
|
||||||
|
@@ -27,7 +27,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook:go_default_library",
|
"//staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1beta1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1:go_default_library",
|
||||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -32,7 +32,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/request/x509"
|
"k8s.io/apiserver/pkg/authentication/request/x509"
|
||||||
"k8s.io/apiserver/pkg/authentication/token/cache"
|
"k8s.io/apiserver/pkg/authentication/token/cache"
|
||||||
webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
|
webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
|
||||||
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
|
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DelegatingAuthenticatorConfig is the minimal configuration needed to create an authenticator
|
// DelegatingAuthenticatorConfig is the minimal configuration needed to create an authenticator
|
||||||
|
@@ -28,7 +28,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook:go_default_library",
|
"//staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1beta1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
|
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
|
||||||
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
|
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DelegatingAuthorizerConfig is the minimal configuration needed to create an authenticator
|
// DelegatingAuthorizerConfig is the minimal configuration needed to create an authenticator
|
||||||
|
@@ -248,7 +248,7 @@ func (s *DelegatingAuthenticationOptions) ApplyTo(authenticationInfo *server.Aut
|
|||||||
|
|
||||||
// configure token review
|
// configure token review
|
||||||
if client != nil {
|
if client != nil {
|
||||||
cfg.TokenAccessReviewClient = client.AuthenticationV1beta1().TokenReviews()
|
cfg.TokenAccessReviewClient = client.AuthenticationV1().TokenReviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
// look into configmaps/external-apiserver-authentication for missing authn info
|
// look into configmaps/external-apiserver-authentication for missing authn info
|
||||||
|
@@ -146,7 +146,7 @@ func (s *DelegatingAuthorizationOptions) toAuthorizer(client kubernetes.Interfac
|
|||||||
klog.Warningf("No authorization-kubeconfig provided, so SubjectAccessReview of authorization tokens won't work.")
|
klog.Warningf("No authorization-kubeconfig provided, so SubjectAccessReview of authorization tokens won't work.")
|
||||||
} else {
|
} else {
|
||||||
cfg := authorizerfactory.DelegatingAuthorizerConfig{
|
cfg := authorizerfactory.DelegatingAuthorizerConfig{
|
||||||
SubjectAccessReviewClient: client.AuthorizationV1beta1().SubjectAccessReviews(),
|
SubjectAccessReviewClient: client.AuthorizationV1().SubjectAccessReviews(),
|
||||||
AllowCacheTTL: s.AllowCacheTTL,
|
AllowCacheTTL: s.AllowCacheTTL,
|
||||||
DenyCacheTTL: s.DenyCacheTTL,
|
DenyCacheTTL: s.DenyCacheTTL,
|
||||||
}
|
}
|
||||||
|
@@ -10,16 +10,21 @@ go_test(
|
|||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = [
|
srcs = [
|
||||||
"certs_test.go",
|
"certs_test.go",
|
||||||
"webhook_test.go",
|
"round_trip_test.go",
|
||||||
|
"webhook_v1_test.go",
|
||||||
|
"webhook_v1beta1_test.go",
|
||||||
],
|
],
|
||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//staging/src/k8s.io/api/authentication/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/authentication/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/authentication/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/token/cache:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/token/cache:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library",
|
||||||
|
"//vendor/github.com/google/gofuzz:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +34,7 @@ go_library(
|
|||||||
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook",
|
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook",
|
||||||
importpath = "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook",
|
importpath = "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook",
|
||||||
deps = [
|
deps = [
|
||||||
|
"//staging/src/k8s.io/api/authentication/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/authentication/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/authentication/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
@@ -36,7 +42,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1beta1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1:go_default_library",
|
||||||
"//vendor/k8s.io/klog:go_default_library",
|
"//vendor/k8s.io/klog:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package webhook implements the authorizer.Authorizer interface using HTTP webhooks.
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fuzz "github.com/google/gofuzz"
|
||||||
|
|
||||||
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
|
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoundTrip(t *testing.T) {
|
||||||
|
f := fuzz.New()
|
||||||
|
seed := time.Now().UnixNano()
|
||||||
|
t.Logf("seed = %v", seed)
|
||||||
|
f.RandSource(rand.New(rand.NewSource(seed)))
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
original := &authenticationv1.TokenReview{}
|
||||||
|
f.Fuzz(&original.Spec)
|
||||||
|
f.Fuzz(&original.Status)
|
||||||
|
converted := &authenticationv1beta1.TokenReview{
|
||||||
|
Spec: v1SpecToV1beta1Spec(&original.Spec),
|
||||||
|
Status: v1StatusToV1beta1Status(original.Status),
|
||||||
|
}
|
||||||
|
roundtripped := &authenticationv1.TokenReview{
|
||||||
|
Spec: v1beta1SpecToV1Spec(converted.Spec),
|
||||||
|
Status: v1beta1StatusToV1Status(&converted.Status),
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(original, roundtripped) {
|
||||||
|
t.Errorf("diff %s", diff.ObjectReflectDiff(original, roundtripped))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1StatusToV1beta1Status(in authenticationv1.TokenReviewStatus) authenticationv1beta1.TokenReviewStatus {
|
||||||
|
return authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: in.Authenticated,
|
||||||
|
User: v1UserToV1beta1User(in.User),
|
||||||
|
Audiences: in.Audiences,
|
||||||
|
Error: in.Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1UserToV1beta1User(u authenticationv1.UserInfo) authenticationv1beta1.UserInfo {
|
||||||
|
var extra map[string]authenticationv1beta1.ExtraValue
|
||||||
|
if u.Extra != nil {
|
||||||
|
extra = make(map[string]authenticationv1beta1.ExtraValue, len(u.Extra))
|
||||||
|
for k, v := range u.Extra {
|
||||||
|
extra[k] = authenticationv1beta1.ExtraValue(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authenticationv1beta1.UserInfo{
|
||||||
|
Username: u.Username,
|
||||||
|
UID: u.UID,
|
||||||
|
Groups: u.Groups,
|
||||||
|
Extra: extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1beta1SpecToV1Spec(in authenticationv1beta1.TokenReviewSpec) authenticationv1.TokenReviewSpec {
|
||||||
|
return authenticationv1.TokenReviewSpec{
|
||||||
|
Token: in.Token,
|
||||||
|
Audiences: in.Audiences,
|
||||||
|
}
|
||||||
|
}
|
@@ -20,30 +20,32 @@ package webhook
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
authentication "k8s.io/api/authentication/v1beta1"
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
|
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
|
authenticationv1client "k8s.io/client-go/kubernetes/typed/authentication/v1"
|
||||||
"k8s.io/klog"
|
"k8s.io/klog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
groupVersions = []schema.GroupVersion{authentication.SchemeGroupVersion}
|
|
||||||
)
|
|
||||||
|
|
||||||
const retryBackoff = 500 * time.Millisecond
|
const retryBackoff = 500 * time.Millisecond
|
||||||
|
|
||||||
// Ensure WebhookTokenAuthenticator implements the authenticator.Token interface.
|
// Ensure WebhookTokenAuthenticator implements the authenticator.Token interface.
|
||||||
var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil)
|
var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil)
|
||||||
|
|
||||||
|
type tokenReviewer interface {
|
||||||
|
CreateContext(ctx context.Context, review *authenticationv1.TokenReview) (*authenticationv1.TokenReview, error)
|
||||||
|
}
|
||||||
|
|
||||||
type WebhookTokenAuthenticator struct {
|
type WebhookTokenAuthenticator struct {
|
||||||
tokenReview authenticationclient.TokenReviewInterface
|
tokenReview tokenReviewer
|
||||||
initialBackoff time.Duration
|
initialBackoff time.Duration
|
||||||
implicitAuds authenticator.Audiences
|
implicitAuds authenticator.Audiences
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,7 @@ type WebhookTokenAuthenticator struct {
|
|||||||
// client. It is recommend to wrap this authenticator with the token cache
|
// client. It is recommend to wrap this authenticator with the token cache
|
||||||
// authenticator implemented in
|
// authenticator implemented in
|
||||||
// k8s.io/apiserver/pkg/authentication/token/cache.
|
// k8s.io/apiserver/pkg/authentication/token/cache.
|
||||||
func NewFromInterface(tokenReview authenticationclient.TokenReviewInterface, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
func NewFromInterface(tokenReview authenticationv1client.TokenReviewInterface, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
||||||
return newWithBackoff(tokenReview, retryBackoff, implicitAuds)
|
return newWithBackoff(tokenReview, retryBackoff, implicitAuds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +62,8 @@ func NewFromInterface(tokenReview authenticationclient.TokenReviewInterface, imp
|
|||||||
// file. It is recommend to wrap this authenticator with the token cache
|
// file. It is recommend to wrap this authenticator with the token cache
|
||||||
// authenticator implemented in
|
// authenticator implemented in
|
||||||
// k8s.io/apiserver/pkg/authentication/token/cache.
|
// k8s.io/apiserver/pkg/authentication/token/cache.
|
||||||
func New(kubeConfigFile string, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
func New(kubeConfigFile string, version string, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
||||||
tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile)
|
tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,7 @@ func New(kubeConfigFile string, implicitAuds authenticator.Audiences) (*WebhookT
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newWithBackoff allows tests to skip the sleep.
|
// newWithBackoff allows tests to skip the sleep.
|
||||||
func newWithBackoff(tokenReview authenticationclient.TokenReviewInterface, initialBackoff time.Duration, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
func newWithBackoff(tokenReview tokenReviewer, initialBackoff time.Duration, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
||||||
return &WebhookTokenAuthenticator{tokenReview, initialBackoff, implicitAuds}, nil
|
return &WebhookTokenAuthenticator{tokenReview, initialBackoff, implicitAuds}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +89,14 @@ func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token
|
|||||||
// intersection in the response.
|
// intersection in the response.
|
||||||
// * otherwise return unauthenticated.
|
// * otherwise return unauthenticated.
|
||||||
wantAuds, checkAuds := authenticator.AudiencesFrom(ctx)
|
wantAuds, checkAuds := authenticator.AudiencesFrom(ctx)
|
||||||
r := &authentication.TokenReview{
|
r := &authenticationv1.TokenReview{
|
||||||
Spec: authentication.TokenReviewSpec{
|
Spec: authenticationv1.TokenReviewSpec{
|
||||||
Token: token,
|
Token: token,
|
||||||
Audiences: wantAuds,
|
Audiences: wantAuds,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
result *authentication.TokenReview
|
result *authenticationv1.TokenReview
|
||||||
err error
|
err error
|
||||||
auds authenticator.Audiences
|
auds authenticator.Audiences
|
||||||
)
|
)
|
||||||
@@ -150,32 +152,99 @@ func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token
|
|||||||
// tokenReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file,
|
// tokenReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file,
|
||||||
// and returns a TokenReviewInterface that uses that client. Note that the client submits TokenReview
|
// and returns a TokenReviewInterface that uses that client. Note that the client submits TokenReview
|
||||||
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
|
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
|
||||||
func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string) (authenticationclient.TokenReviewInterface, error) {
|
func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string, version string) (tokenReviewer, error) {
|
||||||
localScheme := runtime.NewScheme()
|
localScheme := runtime.NewScheme()
|
||||||
if err := scheme.AddToScheme(localScheme); err != nil {
|
if err := scheme.AddToScheme(localScheme); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
|
||||||
return nil, err
|
switch version {
|
||||||
|
case authenticationv1.SchemeGroupVersion.Version:
|
||||||
|
groupVersions := []schema.GroupVersion{authenticationv1.SchemeGroupVersion}
|
||||||
|
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tokenReviewV1Client{gw}, nil
|
||||||
|
|
||||||
|
case authenticationv1beta1.SchemeGroupVersion.Version:
|
||||||
|
groupVersions := []schema.GroupVersion{authenticationv1beta1.SchemeGroupVersion}
|
||||||
|
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tokenReviewV1beta1Client{gw}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"unsupported authentication webhook version %q, supported versions are %q, %q",
|
||||||
|
version,
|
||||||
|
authenticationv1.SchemeGroupVersion.Version,
|
||||||
|
authenticationv1beta1.SchemeGroupVersion.Version,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &tokenReviewClient{gw}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type tokenReviewClient struct {
|
type tokenReviewV1Client struct {
|
||||||
w *webhook.GenericWebhook
|
w *webhook.GenericWebhook
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tokenReviewClient) Create(tokenReview *authentication.TokenReview) (*authentication.TokenReview, error) {
|
func (t *tokenReviewV1Client) CreateContext(ctx context.Context, review *authenticationv1.TokenReview) (*authenticationv1.TokenReview, error) {
|
||||||
return t.CreateContext(context.Background(), tokenReview)
|
result := &authenticationv1.TokenReview{}
|
||||||
}
|
err := t.w.RestClient.Post().Context(ctx).Body(review).Do().Into(result)
|
||||||
|
|
||||||
func (t *tokenReviewClient) CreateContext(ctx context.Context, tokenReview *authentication.TokenReview) (*authentication.TokenReview, error) {
|
|
||||||
result := &authentication.TokenReview{}
|
|
||||||
err := t.w.RestClient.Post().Context(ctx).Body(tokenReview).Do().Into(result)
|
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tokenReviewV1beta1Client struct {
|
||||||
|
w *webhook.GenericWebhook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tokenReviewV1beta1Client) CreateContext(ctx context.Context, review *authenticationv1.TokenReview) (*authenticationv1.TokenReview, error) {
|
||||||
|
v1beta1Review := &authenticationv1beta1.TokenReview{Spec: v1SpecToV1beta1Spec(&review.Spec)}
|
||||||
|
v1beta1Result := &authenticationv1beta1.TokenReview{}
|
||||||
|
err := t.w.RestClient.Post().Context(ctx).Body(v1beta1Review).Do().Into(v1beta1Result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
review.Status = v1beta1StatusToV1Status(&v1beta1Result.Status)
|
||||||
|
return review, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1SpecToV1beta1Spec(in *authenticationv1.TokenReviewSpec) authenticationv1beta1.TokenReviewSpec {
|
||||||
|
return authenticationv1beta1.TokenReviewSpec{
|
||||||
|
Token: in.Token,
|
||||||
|
Audiences: in.Audiences,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1beta1StatusToV1Status(in *authenticationv1beta1.TokenReviewStatus) authenticationv1.TokenReviewStatus {
|
||||||
|
return authenticationv1.TokenReviewStatus{
|
||||||
|
Authenticated: in.Authenticated,
|
||||||
|
User: v1beta1UserToV1User(in.User),
|
||||||
|
Audiences: in.Audiences,
|
||||||
|
Error: in.Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1beta1UserToV1User(u authenticationv1beta1.UserInfo) authenticationv1.UserInfo {
|
||||||
|
var extra map[string]authenticationv1.ExtraValue
|
||||||
|
if u.Extra != nil {
|
||||||
|
extra = make(map[string]authenticationv1.ExtraValue, len(u.Extra))
|
||||||
|
for k, v := range u.Extra {
|
||||||
|
extra[k] = authenticationv1.ExtraValue(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authenticationv1.UserInfo{
|
||||||
|
Username: u.Username,
|
||||||
|
UID: u.UID,
|
||||||
|
Groups: u.Groups,
|
||||||
|
Extra: extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -31,26 +31,24 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/api/authentication/v1beta1"
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/token/cache"
|
"k8s.io/apiserver/pkg/authentication/token/cache"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/client-go/tools/clientcmd/api/v1"
|
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var apiAuds = authenticator.Audiences{"api"}
|
// V1Service mocks a remote authentication service.
|
||||||
|
type V1Service interface {
|
||||||
// Service mocks a remote authentication service.
|
|
||||||
type Service interface {
|
|
||||||
// Review looks at the TokenReviewSpec and provides an authentication
|
// Review looks at the TokenReviewSpec and provides an authentication
|
||||||
// response in the TokenReviewStatus.
|
// response in the TokenReviewStatus.
|
||||||
Review(*v1beta1.TokenReview)
|
Review(*authenticationv1.TokenReview)
|
||||||
HTTPStatusCode() int
|
HTTPStatusCode() int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestServer wraps a Service as an httptest.Server.
|
// NewV1TestServer wraps a V1Service as an httptest.Server.
|
||||||
func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
func NewV1TestServer(s V1Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
||||||
const webhookPath = "/testserver"
|
const webhookPath = "/testserver"
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
if cert != nil {
|
if cert != nil {
|
||||||
@@ -81,14 +79,14 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var review v1beta1.TokenReview
|
var review authenticationv1.TokenReview
|
||||||
bodyData, _ := ioutil.ReadAll(r.Body)
|
bodyData, _ := ioutil.ReadAll(r.Body)
|
||||||
if err := json.Unmarshal(bodyData, &review); err != nil {
|
if err := json.Unmarshal(bodyData, &review); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// ensure we received the serialized tokenreview as expected
|
// ensure we received the serialized tokenreview as expected
|
||||||
if review.APIVersion != "authentication.k8s.io/v1beta1" {
|
if review.APIVersion != "authentication.k8s.io/v1" {
|
||||||
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -124,7 +122,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
|
|||||||
Status status `json:"status"`
|
Status status `json:"status"`
|
||||||
}{
|
}{
|
||||||
Kind: "TokenReview",
|
Kind: "TokenReview",
|
||||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
APIVersion: authenticationv1.SchemeGroupVersion.String(),
|
||||||
Status: status{
|
Status: status{
|
||||||
review.Status.Authenticated,
|
review.Status.Authenticated,
|
||||||
userInfo{
|
userInfo{
|
||||||
@@ -153,26 +151,26 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A service that can be set to say yes or no to authentication requests.
|
// A service that can be set to say yes or no to authentication requests.
|
||||||
type mockService struct {
|
type mockV1Service struct {
|
||||||
allow bool
|
allow bool
|
||||||
statusCode int
|
statusCode int
|
||||||
called int
|
called int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockService) Review(r *v1beta1.TokenReview) {
|
func (m *mockV1Service) Review(r *authenticationv1.TokenReview) {
|
||||||
m.called++
|
m.called++
|
||||||
r.Status.Authenticated = m.allow
|
r.Status.Authenticated = m.allow
|
||||||
if m.allow {
|
if m.allow {
|
||||||
r.Status.User.Username = "realHooman@email.com"
|
r.Status.User.Username = "realHooman@email.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (m *mockService) Allow() { m.allow = true }
|
func (m *mockV1Service) Allow() { m.allow = true }
|
||||||
func (m *mockService) Deny() { m.allow = false }
|
func (m *mockV1Service) Deny() { m.allow = false }
|
||||||
func (m *mockService) HTTPStatusCode() int { return m.statusCode }
|
func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode }
|
||||||
|
|
||||||
// newTokenAuthenticator creates a temporary kubeconfig file from the provided
|
// newV1TokenAuthenticator creates a temporary kubeconfig file from the provided
|
||||||
// arguments and attempts to load a new WebhookTokenAuthenticator from it.
|
// arguments and attempts to load a new WebhookTokenAuthenticator from it.
|
||||||
func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
|
func newV1TokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
|
||||||
tempfile, err := ioutil.TempFile("", "")
|
tempfile, err := ioutil.TempFile("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -195,7 +193,7 @@ func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, c
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := tokenReviewInterfaceFromKubeconfig(p)
|
c, err := tokenReviewInterfaceFromKubeconfig(p, "v1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -208,7 +206,7 @@ func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, c
|
|||||||
return cache.New(authn, false, cacheTime, cacheTime), nil
|
return cache.New(authn, false, cacheTime, cacheTime), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSConfig(t *testing.T) {
|
func TestV1TLSConfig(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
test string
|
test string
|
||||||
clientCert, clientKey, clientCA []byte
|
clientCert, clientKey, clientCA []byte
|
||||||
@@ -251,17 +249,17 @@ func TestTLSConfig(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
// Use a closure so defer statements trigger between loop iterations.
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
func() {
|
func() {
|
||||||
service := new(mockService)
|
service := new(mockV1Service)
|
||||||
service.statusCode = 200
|
service.statusCode = 200
|
||||||
|
|
||||||
server, err := NewTestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
|
server, err := NewV1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
wh, err := newTokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil)
|
wh, err := newV1TokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||||
return
|
return
|
||||||
@@ -293,47 +291,47 @@ func TestTLSConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// recorderService records all token review requests, and responds with the
|
// recorderV1Service records all token review requests, and responds with the
|
||||||
// provided TokenReviewStatus.
|
// provided TokenReviewStatus.
|
||||||
type recorderService struct {
|
type recorderV1Service struct {
|
||||||
lastRequest v1beta1.TokenReview
|
lastRequest authenticationv1.TokenReview
|
||||||
response v1beta1.TokenReviewStatus
|
response authenticationv1.TokenReviewStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rec *recorderService) Review(r *v1beta1.TokenReview) {
|
func (rec *recorderV1Service) Review(r *authenticationv1.TokenReview) {
|
||||||
rec.lastRequest = *r
|
rec.lastRequest = *r
|
||||||
r.Status = rec.response
|
r.Status = rec.response
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rec *recorderService) HTTPStatusCode() int { return 200 }
|
func (rec *recorderV1Service) HTTPStatusCode() int { return 200 }
|
||||||
|
|
||||||
func TestWebhookTokenAuthenticator(t *testing.T) {
|
func TestV1WebhookTokenAuthenticator(t *testing.T) {
|
||||||
serv := &recorderService{}
|
serv := &recorderV1Service{}
|
||||||
|
|
||||||
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
|
s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
expTypeMeta := metav1.TypeMeta{
|
expTypeMeta := metav1.TypeMeta{
|
||||||
APIVersion: "authentication.k8s.io/v1beta1",
|
APIVersion: "authentication.k8s.io/v1",
|
||||||
Kind: "TokenReview",
|
Kind: "TokenReview",
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
description string
|
description string
|
||||||
implicitAuds, reqAuds authenticator.Audiences
|
implicitAuds, reqAuds authenticator.Audiences
|
||||||
serverResponse v1beta1.TokenReviewStatus
|
serverResponse authenticationv1.TokenReviewStatus
|
||||||
expectedAuthenticated bool
|
expectedAuthenticated bool
|
||||||
expectedUser *user.DefaultInfo
|
expectedUser *user.DefaultInfo
|
||||||
expectedAuds authenticator.Audiences
|
expectedAuds authenticator.Audiences
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
description: "successful response should pass through all user info.",
|
description: "successful response should pass through all user info.",
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "somebody",
|
Username: "somebody",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -344,13 +342,13 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "successful response should pass through all user info.",
|
description: "successful response should pass through all user info.",
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "person@place.com",
|
Username: "person@place.com",
|
||||||
UID: "abcd-1234",
|
UID: "abcd-1234",
|
||||||
Groups: []string{"stuff-dev", "main-eng"},
|
Groups: []string{"stuff-dev", "main-eng"},
|
||||||
Extra: map[string]v1beta1.ExtraValue{"foo": {"bar", "baz"}},
|
Extra: map[string]authenticationv1.ExtraValue{"foo": {"bar", "baz"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedAuthenticated: true,
|
expectedAuthenticated: true,
|
||||||
@@ -363,9 +361,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "unauthenticated shouldn't even include extra provided info.",
|
description: "unauthenticated shouldn't even include extra provided info.",
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: false,
|
Authenticated: false,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "garbage",
|
Username: "garbage",
|
||||||
UID: "abcd-1234",
|
UID: "abcd-1234",
|
||||||
Groups: []string{"not-actually-used"},
|
Groups: []string{"not-actually-used"},
|
||||||
@@ -376,7 +374,7 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "unauthenticated shouldn't even include extra provided info.",
|
description: "unauthenticated shouldn't even include extra provided info.",
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: false,
|
Authenticated: false,
|
||||||
},
|
},
|
||||||
expectedAuthenticated: false,
|
expectedAuthenticated: false,
|
||||||
@@ -386,9 +384,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
description: "good audience",
|
description: "good audience",
|
||||||
implicitAuds: apiAuds,
|
implicitAuds: apiAuds,
|
||||||
reqAuds: apiAuds,
|
reqAuds: apiAuds,
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "somebody",
|
Username: "somebody",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -402,9 +400,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
description: "good audience",
|
description: "good audience",
|
||||||
implicitAuds: append(apiAuds, "other"),
|
implicitAuds: append(apiAuds, "other"),
|
||||||
reqAuds: apiAuds,
|
reqAuds: apiAuds,
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "somebody",
|
Username: "somebody",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -418,7 +416,7 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
description: "bad audiences",
|
description: "bad audiences",
|
||||||
implicitAuds: apiAuds,
|
implicitAuds: apiAuds,
|
||||||
reqAuds: authenticator.Audiences{"other"},
|
reqAuds: authenticator.Audiences{"other"},
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: false,
|
Authenticated: false,
|
||||||
},
|
},
|
||||||
expectedAuthenticated: false,
|
expectedAuthenticated: false,
|
||||||
@@ -428,9 +426,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
implicitAuds: apiAuds,
|
implicitAuds: apiAuds,
|
||||||
reqAuds: authenticator.Audiences{"other"},
|
reqAuds: authenticator.Audiences{"other"},
|
||||||
// webhook authenticator hasn't been upgraded to support audience.
|
// webhook authenticator hasn't been upgraded to support audience.
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "somebody",
|
Username: "somebody",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -440,9 +438,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
description: "audience aware backend",
|
description: "audience aware backend",
|
||||||
implicitAuds: apiAuds,
|
implicitAuds: apiAuds,
|
||||||
reqAuds: apiAuds,
|
reqAuds: apiAuds,
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "somebody",
|
Username: "somebody",
|
||||||
},
|
},
|
||||||
Audiences: []string(apiAuds),
|
Audiences: []string(apiAuds),
|
||||||
@@ -455,9 +453,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "audience aware backend",
|
description: "audience aware backend",
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "somebody",
|
Username: "somebody",
|
||||||
},
|
},
|
||||||
Audiences: []string(apiAuds),
|
Audiences: []string(apiAuds),
|
||||||
@@ -471,9 +469,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
description: "audience aware backend",
|
description: "audience aware backend",
|
||||||
implicitAuds: apiAuds,
|
implicitAuds: apiAuds,
|
||||||
reqAuds: apiAuds,
|
reqAuds: apiAuds,
|
||||||
serverResponse: v1beta1.TokenReviewStatus{
|
serverResponse: authenticationv1.TokenReviewStatus{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
User: v1beta1.UserInfo{
|
User: authenticationv1.UserInfo{
|
||||||
Username: "somebody",
|
Username: "somebody",
|
||||||
},
|
},
|
||||||
Audiences: []string{"other"},
|
Audiences: []string{"other"},
|
||||||
@@ -484,7 +482,7 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
token := "my-s3cr3t-t0ken"
|
token := "my-s3cr3t-t0ken"
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.description, func(t *testing.T) {
|
t.Run(tt.description, func(t *testing.T) {
|
||||||
wh, err := newTokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds)
|
wh, err := newV1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -523,13 +521,13 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type authenticationUserInfo v1beta1.UserInfo
|
type authenticationV1UserInfo authenticationv1.UserInfo
|
||||||
|
|
||||||
func (a *authenticationUserInfo) GetName() string { return a.Username }
|
func (a *authenticationV1UserInfo) GetName() string { return a.Username }
|
||||||
func (a *authenticationUserInfo) GetUID() string { return a.UID }
|
func (a *authenticationV1UserInfo) GetUID() string { return a.UID }
|
||||||
func (a *authenticationUserInfo) GetGroups() []string { return a.Groups }
|
func (a *authenticationV1UserInfo) GetGroups() []string { return a.Groups }
|
||||||
|
|
||||||
func (a *authenticationUserInfo) GetExtra() map[string][]string {
|
func (a *authenticationV1UserInfo) GetExtra() map[string][]string {
|
||||||
if a.Extra == nil {
|
if a.Extra == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -541,23 +539,23 @@ func (a *authenticationUserInfo) GetExtra() map[string][]string {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure v1beta1.UserInfo contains the fields necessary to implement the
|
// Ensure authenticationv1.UserInfo contains the fields necessary to implement the
|
||||||
// user.Info interface.
|
// user.Info interface.
|
||||||
var _ user.Info = (*authenticationUserInfo)(nil)
|
var _ user.Info = (*authenticationV1UserInfo)(nil)
|
||||||
|
|
||||||
// TestWebhookCache verifies that error responses from the server are not
|
// TestWebhookCache verifies that error responses from the server are not
|
||||||
// cached, but successful responses are. It also ensures that the webhook
|
// cached, but successful responses are. It also ensures that the webhook
|
||||||
// call is retried on 429 and 500+ errors
|
// call is retried on 429 and 500+ errors
|
||||||
func TestWebhookCacheAndRetry(t *testing.T) {
|
func TestV1WebhookCacheAndRetry(t *testing.T) {
|
||||||
serv := new(mockService)
|
serv := new(mockV1Service)
|
||||||
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
|
s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
// Create an authenticator that caches successful responses "forever" (100 days).
|
// Create an authenticator that caches successful responses "forever" (100 days).
|
||||||
wh, err := newTokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil)
|
wh, err := newV1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
@@ -0,0 +1,686 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/token/cache"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var apiAuds = authenticator.Audiences{"api"}
|
||||||
|
|
||||||
|
// V1beta1Service mocks a remote authentication service.
|
||||||
|
type V1beta1Service interface {
|
||||||
|
// Review looks at the TokenReviewSpec and provides an authentication
|
||||||
|
// response in the TokenReviewStatus.
|
||||||
|
Review(*authenticationv1beta1.TokenReview)
|
||||||
|
HTTPStatusCode() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewV1beta1TestServer wraps a V1beta1Service as an httptest.Server.
|
||||||
|
func NewV1beta1TestServer(s V1beta1Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
||||||
|
const webhookPath = "/testserver"
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
if cert != nil {
|
||||||
|
cert, err := tls.X509KeyPair(cert, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if caCert != nil {
|
||||||
|
rootCAs := x509.NewCertPool()
|
||||||
|
rootCAs.AppendCertsFromPEM(caCert)
|
||||||
|
if tlsConfig == nil {
|
||||||
|
tlsConfig = &tls.Config{}
|
||||||
|
}
|
||||||
|
tlsConfig.ClientCAs = rootCAs
|
||||||
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
}
|
||||||
|
|
||||||
|
serveHTTP := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.URL.Path != webhookPath {
|
||||||
|
http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var review authenticationv1beta1.TokenReview
|
||||||
|
bodyData, _ := ioutil.ReadAll(r.Body)
|
||||||
|
if err := json.Unmarshal(bodyData, &review); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ensure we received the serialized tokenreview as expected
|
||||||
|
if review.APIVersion != "authentication.k8s.io/v1beta1" {
|
||||||
|
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// once we have a successful request, always call the review to record that we were called
|
||||||
|
s.Review(&review)
|
||||||
|
if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 {
|
||||||
|
http.Error(w, "HTTP Error", s.HTTPStatusCode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type userInfo struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
UID string `json:"uid"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
Extra map[string][]string `json:"extra"`
|
||||||
|
}
|
||||||
|
type status struct {
|
||||||
|
Authenticated bool `json:"authenticated"`
|
||||||
|
User userInfo `json:"user"`
|
||||||
|
Audiences []string `json:"audiences"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var extra map[string][]string
|
||||||
|
if review.Status.User.Extra != nil {
|
||||||
|
extra = map[string][]string{}
|
||||||
|
for k, v := range review.Status.User.Extra {
|
||||||
|
extra[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
APIVersion string `json:"apiVersion"`
|
||||||
|
Status status `json:"status"`
|
||||||
|
}{
|
||||||
|
Kind: "TokenReview",
|
||||||
|
APIVersion: authenticationv1beta1.SchemeGroupVersion.String(),
|
||||||
|
Status: status{
|
||||||
|
review.Status.Authenticated,
|
||||||
|
userInfo{
|
||||||
|
Username: review.Status.User.Username,
|
||||||
|
UID: review.Status.User.UID,
|
||||||
|
Groups: review.Status.User.Groups,
|
||||||
|
Extra: extra,
|
||||||
|
},
|
||||||
|
review.Status.Audiences,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
|
||||||
|
server.TLS = tlsConfig
|
||||||
|
server.StartTLS()
|
||||||
|
|
||||||
|
// Adjust the path to point to our custom path
|
||||||
|
serverURL, _ := url.Parse(server.URL)
|
||||||
|
serverURL.Path = webhookPath
|
||||||
|
server.URL = serverURL.String()
|
||||||
|
|
||||||
|
return server, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A service that can be set to say yes or no to authentication requests.
|
||||||
|
type mockV1beta1Service struct {
|
||||||
|
allow bool
|
||||||
|
statusCode int
|
||||||
|
called int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockV1beta1Service) Review(r *authenticationv1beta1.TokenReview) {
|
||||||
|
m.called++
|
||||||
|
r.Status.Authenticated = m.allow
|
||||||
|
if m.allow {
|
||||||
|
r.Status.User.Username = "realHooman@email.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *mockV1beta1Service) Allow() { m.allow = true }
|
||||||
|
func (m *mockV1beta1Service) Deny() { m.allow = false }
|
||||||
|
func (m *mockV1beta1Service) HTTPStatusCode() int { return m.statusCode }
|
||||||
|
|
||||||
|
// newV1beta1TokenAuthenticator creates a temporary kubeconfig file from the provided
|
||||||
|
// arguments and attempts to load a new WebhookTokenAuthenticator from it.
|
||||||
|
func newV1beta1TokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
|
||||||
|
tempfile, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := tempfile.Name()
|
||||||
|
defer os.Remove(p)
|
||||||
|
config := v1.Config{
|
||||||
|
Clusters: []v1.NamedCluster{
|
||||||
|
{
|
||||||
|
Cluster: v1.Cluster{Server: serverURL, CertificateAuthorityData: ca},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuthInfos: []v1.NamedAuthInfo{
|
||||||
|
{
|
||||||
|
AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := tokenReviewInterfaceFromKubeconfig(p, "v1beta1")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authn, err := newWithBackoff(c, 0, implicitAuds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache.New(authn, false, cacheTime, cacheTime), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV1beta1TLSConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
test string
|
||||||
|
clientCert, clientKey, clientCA []byte
|
||||||
|
serverCert, serverKey, serverCA []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
test: "TLS setup between client and server",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not require client auth",
|
||||||
|
clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not require client auth, client provides it",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Client does not trust server",
|
||||||
|
clientCert: clientCert, clientKey: clientKey,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not trust client",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Plugin does not support insecure configurations.
|
||||||
|
test: "Server is using insecure connection",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
|
func() {
|
||||||
|
service := new(mockV1beta1Service)
|
||||||
|
service.statusCode = 200
|
||||||
|
|
||||||
|
server, err := NewV1beta1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wh, err := newV1beta1TokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow all and see if we get an error.
|
||||||
|
service.Allow()
|
||||||
|
_, authenticated, err := wh.AuthenticateToken(context.Background(), "t0k3n")
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error making authorization request: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !authenticated {
|
||||||
|
t.Errorf("%s: failed to authenticate token", tt.test)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Deny()
|
||||||
|
_, authenticated, err = wh.AuthenticateToken(context.Background(), "t0k3n")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: unexpectedly failed AuthenticateToken", tt.test)
|
||||||
|
}
|
||||||
|
if authenticated {
|
||||||
|
t.Errorf("%s: incorrectly authenticated token", tt.test)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recorderV1beta1Service records all token review requests, and responds with the
|
||||||
|
// provided TokenReviewStatus.
|
||||||
|
type recorderV1beta1Service struct {
|
||||||
|
lastRequest authenticationv1beta1.TokenReview
|
||||||
|
response authenticationv1beta1.TokenReviewStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rec *recorderV1beta1Service) Review(r *authenticationv1beta1.TokenReview) {
|
||||||
|
rec.lastRequest = *r
|
||||||
|
r.Status = rec.response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rec *recorderV1beta1Service) HTTPStatusCode() int { return 200 }
|
||||||
|
|
||||||
|
func TestV1beta1WebhookTokenAuthenticator(t *testing.T) {
|
||||||
|
serv := &recorderV1beta1Service{}
|
||||||
|
|
||||||
|
s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
expTypeMeta := metav1.TypeMeta{
|
||||||
|
APIVersion: "authentication.k8s.io/v1beta1",
|
||||||
|
Kind: "TokenReview",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
description string
|
||||||
|
implicitAuds, reqAuds authenticator.Audiences
|
||||||
|
serverResponse authenticationv1beta1.TokenReviewStatus
|
||||||
|
expectedAuthenticated bool
|
||||||
|
expectedUser *user.DefaultInfo
|
||||||
|
expectedAuds authenticator.Audiences
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "successful response should pass through all user info.",
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "somebody",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedAuthenticated: true,
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "somebody",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "successful response should pass through all user info.",
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "person@place.com",
|
||||||
|
UID: "abcd-1234",
|
||||||
|
Groups: []string{"stuff-dev", "main-eng"},
|
||||||
|
Extra: map[string]authenticationv1beta1.ExtraValue{"foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedAuthenticated: true,
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "person@place.com",
|
||||||
|
UID: "abcd-1234",
|
||||||
|
Groups: []string{"stuff-dev", "main-eng"},
|
||||||
|
Extra: map[string][]string{"foo": {"bar", "baz"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "unauthenticated shouldn't even include extra provided info.",
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: false,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "garbage",
|
||||||
|
UID: "abcd-1234",
|
||||||
|
Groups: []string{"not-actually-used"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedAuthenticated: false,
|
||||||
|
expectedUser: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "unauthenticated shouldn't even include extra provided info.",
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: false,
|
||||||
|
},
|
||||||
|
expectedAuthenticated: false,
|
||||||
|
expectedUser: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "good audience",
|
||||||
|
implicitAuds: apiAuds,
|
||||||
|
reqAuds: apiAuds,
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "somebody",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedAuthenticated: true,
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "somebody",
|
||||||
|
},
|
||||||
|
expectedAuds: apiAuds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "good audience",
|
||||||
|
implicitAuds: append(apiAuds, "other"),
|
||||||
|
reqAuds: apiAuds,
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "somebody",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedAuthenticated: true,
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "somebody",
|
||||||
|
},
|
||||||
|
expectedAuds: apiAuds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "bad audiences",
|
||||||
|
implicitAuds: apiAuds,
|
||||||
|
reqAuds: authenticator.Audiences{"other"},
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: false,
|
||||||
|
},
|
||||||
|
expectedAuthenticated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "bad audiences",
|
||||||
|
implicitAuds: apiAuds,
|
||||||
|
reqAuds: authenticator.Audiences{"other"},
|
||||||
|
// webhook authenticator hasn't been upgraded to support audience.
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "somebody",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedAuthenticated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "audience aware backend",
|
||||||
|
implicitAuds: apiAuds,
|
||||||
|
reqAuds: apiAuds,
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "somebody",
|
||||||
|
},
|
||||||
|
Audiences: []string(apiAuds),
|
||||||
|
},
|
||||||
|
expectedAuthenticated: true,
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "somebody",
|
||||||
|
},
|
||||||
|
expectedAuds: apiAuds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "audience aware backend",
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "somebody",
|
||||||
|
},
|
||||||
|
Audiences: []string(apiAuds),
|
||||||
|
},
|
||||||
|
expectedAuthenticated: true,
|
||||||
|
expectedUser: &user.DefaultInfo{
|
||||||
|
Name: "somebody",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "audience aware backend",
|
||||||
|
implicitAuds: apiAuds,
|
||||||
|
reqAuds: apiAuds,
|
||||||
|
serverResponse: authenticationv1beta1.TokenReviewStatus{
|
||||||
|
Authenticated: true,
|
||||||
|
User: authenticationv1beta1.UserInfo{
|
||||||
|
Username: "somebody",
|
||||||
|
},
|
||||||
|
Audiences: []string{"other"},
|
||||||
|
},
|
||||||
|
expectedAuthenticated: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token := "my-s3cr3t-t0ken"
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.description, func(t *testing.T) {
|
||||||
|
wh, err := newV1beta1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if tt.reqAuds != nil {
|
||||||
|
ctx = authenticator.WithAudiences(ctx, tt.reqAuds)
|
||||||
|
}
|
||||||
|
|
||||||
|
serv.response = tt.serverResponse
|
||||||
|
resp, authenticated, err := wh.AuthenticateToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("authentication failed: %v", err)
|
||||||
|
}
|
||||||
|
if serv.lastRequest.Spec.Token != token {
|
||||||
|
t.Errorf("Server did not see correct token. Got %q, expected %q.",
|
||||||
|
serv.lastRequest.Spec.Token, token)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(serv.lastRequest.TypeMeta, expTypeMeta) {
|
||||||
|
t.Errorf("Server did not see correct TypeMeta. Got %v, expected %v",
|
||||||
|
serv.lastRequest.TypeMeta, expTypeMeta)
|
||||||
|
}
|
||||||
|
if authenticated != tt.expectedAuthenticated {
|
||||||
|
t.Errorf("Plugin returned incorrect authentication response. Got %t, expected %t.",
|
||||||
|
authenticated, tt.expectedAuthenticated)
|
||||||
|
}
|
||||||
|
if resp != nil && tt.expectedUser != nil && !reflect.DeepEqual(resp.User, tt.expectedUser) {
|
||||||
|
t.Errorf("Plugin returned incorrect user. Got %#v, expected %#v",
|
||||||
|
resp.User, tt.expectedUser)
|
||||||
|
}
|
||||||
|
if resp != nil && tt.expectedAuds != nil && !reflect.DeepEqual(resp.Audiences, tt.expectedAuds) {
|
||||||
|
t.Errorf("Plugin returned incorrect audiences. Got %#v, expected %#v",
|
||||||
|
resp.Audiences, tt.expectedAuds)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticationV1beta1UserInfo authenticationv1beta1.UserInfo
|
||||||
|
|
||||||
|
func (a *authenticationV1beta1UserInfo) GetName() string { return a.Username }
|
||||||
|
func (a *authenticationV1beta1UserInfo) GetUID() string { return a.UID }
|
||||||
|
func (a *authenticationV1beta1UserInfo) GetGroups() []string { return a.Groups }
|
||||||
|
|
||||||
|
func (a *authenticationV1beta1UserInfo) GetExtra() map[string][]string {
|
||||||
|
if a.Extra == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := map[string][]string{}
|
||||||
|
for k, v := range a.Extra {
|
||||||
|
ret[k] = []string(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure authenticationv1beta1.UserInfo contains the fields necessary to implement the
|
||||||
|
// user.Info interface.
|
||||||
|
var _ user.Info = (*authenticationV1beta1UserInfo)(nil)
|
||||||
|
|
||||||
|
// TestWebhookCache verifies that error responses from the server are not
|
||||||
|
// cached, but successful responses are. It also ensures that the webhook
|
||||||
|
// call is retried on 429 and 500+ errors
|
||||||
|
func TestV1beta1WebhookCacheAndRetry(t *testing.T) {
|
||||||
|
serv := new(mockV1beta1Service)
|
||||||
|
s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Create an authenticator that caches successful responses "forever" (100 days).
|
||||||
|
wh, err := newV1beta1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
description string
|
||||||
|
|
||||||
|
token string
|
||||||
|
allow bool
|
||||||
|
code int
|
||||||
|
|
||||||
|
expectError bool
|
||||||
|
expectOk bool
|
||||||
|
expectCalls int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "t0k3n, 500 error, retries and fails",
|
||||||
|
|
||||||
|
token: "t0k3n",
|
||||||
|
allow: false,
|
||||||
|
code: 500,
|
||||||
|
|
||||||
|
expectError: true,
|
||||||
|
expectOk: false,
|
||||||
|
expectCalls: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "t0k3n, 404 error, fails (but no retry)",
|
||||||
|
|
||||||
|
token: "t0k3n",
|
||||||
|
allow: false,
|
||||||
|
code: 404,
|
||||||
|
|
||||||
|
expectError: true,
|
||||||
|
expectOk: false,
|
||||||
|
expectCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "t0k3n, 200 response, allowed, succeeds with a single call",
|
||||||
|
|
||||||
|
token: "t0k3n",
|
||||||
|
allow: true,
|
||||||
|
code: 200,
|
||||||
|
|
||||||
|
expectError: false,
|
||||||
|
expectOk: true,
|
||||||
|
expectCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "t0k3n, 500 response, disallowed, but never called because previous 200 response was cached",
|
||||||
|
|
||||||
|
token: "t0k3n",
|
||||||
|
allow: false,
|
||||||
|
code: 500,
|
||||||
|
|
||||||
|
expectError: false,
|
||||||
|
expectOk: true,
|
||||||
|
expectCalls: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
description: "an0th3r_t0k3n, 500 response, disallowed, should be called again with retries",
|
||||||
|
|
||||||
|
token: "an0th3r_t0k3n",
|
||||||
|
allow: false,
|
||||||
|
code: 500,
|
||||||
|
|
||||||
|
expectError: true,
|
||||||
|
expectOk: false,
|
||||||
|
expectCalls: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "an0th3r_t0k3n, 429 response, disallowed, should be called again with retries",
|
||||||
|
|
||||||
|
token: "an0th3r_t0k3n",
|
||||||
|
allow: false,
|
||||||
|
code: 429,
|
||||||
|
|
||||||
|
expectError: true,
|
||||||
|
expectOk: false,
|
||||||
|
expectCalls: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "an0th3r_t0k3n, 200 response, allowed, succeeds with a single call",
|
||||||
|
|
||||||
|
token: "an0th3r_t0k3n",
|
||||||
|
allow: true,
|
||||||
|
code: 200,
|
||||||
|
|
||||||
|
expectError: false,
|
||||||
|
expectOk: true,
|
||||||
|
expectCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "an0th3r_t0k3n, 500 response, disallowed, but never called because previous 200 response was cached",
|
||||||
|
|
||||||
|
token: "an0th3r_t0k3n",
|
||||||
|
allow: false,
|
||||||
|
code: 500,
|
||||||
|
|
||||||
|
expectError: false,
|
||||||
|
expectOk: true,
|
||||||
|
expectCalls: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
t.Run(testcase.description, func(t *testing.T) {
|
||||||
|
serv.allow = testcase.allow
|
||||||
|
serv.statusCode = testcase.code
|
||||||
|
serv.called = 0
|
||||||
|
|
||||||
|
_, ok, err := wh.AuthenticateToken(context.Background(), testcase.token)
|
||||||
|
hasError := err != nil
|
||||||
|
if hasError != testcase.expectError {
|
||||||
|
t.Errorf("Webhook returned HTTP %d, expected error=%v, but got error %v", testcase.code, testcase.expectError, err)
|
||||||
|
}
|
||||||
|
if serv.called != testcase.expectCalls {
|
||||||
|
t.Errorf("Expected %d calls, got %d", testcase.expectCalls, serv.called)
|
||||||
|
}
|
||||||
|
if ok != testcase.expectOk {
|
||||||
|
t.Errorf("Expected ok=%v, got %v", testcase.expectOk, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -10,16 +10,20 @@ go_test(
|
|||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = [
|
srcs = [
|
||||||
"certs_test.go",
|
"certs_test.go",
|
||||||
"webhook_test.go",
|
"round_trip_test.go",
|
||||||
|
"webhook_v1_test.go",
|
||||||
|
"webhook_v1beta1_test.go",
|
||||||
],
|
],
|
||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/authorization/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/authorization/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library",
|
"//staging/src/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library",
|
||||||
|
"//vendor/github.com/google/gofuzz:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +33,7 @@ go_library(
|
|||||||
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook",
|
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook",
|
||||||
importpath = "k8s.io/apiserver/plugin/pkg/authorizer/webhook",
|
importpath = "k8s.io/apiserver/plugin/pkg/authorizer/webhook",
|
||||||
deps = [
|
deps = [
|
||||||
|
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/authorization/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/authorization/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
@@ -37,7 +42,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1beta1:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
|
||||||
"//vendor/k8s.io/klog:go_default_library",
|
"//vendor/k8s.io/klog:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package webhook implements the authorizer.Authorizer interface using HTTP webhooks.
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fuzz "github.com/google/gofuzz"
|
||||||
|
|
||||||
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
|
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoundTrip(t *testing.T) {
|
||||||
|
f := fuzz.New()
|
||||||
|
seed := time.Now().UnixNano()
|
||||||
|
t.Logf("seed = %v", seed)
|
||||||
|
f.RandSource(rand.New(rand.NewSource(seed)))
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
original := &authorizationv1.SubjectAccessReview{}
|
||||||
|
f.Fuzz(&original.Spec)
|
||||||
|
f.Fuzz(&original.Status)
|
||||||
|
converted := &authorizationv1beta1.SubjectAccessReview{
|
||||||
|
Spec: v1SpecToV1beta1Spec(&original.Spec),
|
||||||
|
Status: v1StatusToV1beta1Status(original.Status),
|
||||||
|
}
|
||||||
|
roundtripped := &authorizationv1.SubjectAccessReview{
|
||||||
|
Spec: v1beta1SpecToV1Spec(converted.Spec),
|
||||||
|
Status: v1beta1StatusToV1Status(&converted.Status),
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(original, roundtripped) {
|
||||||
|
t.Errorf("diff %s", diff.ObjectReflectDiff(original, roundtripped))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1StatusToV1beta1Status is only needed to verify round-trip fidelity
|
||||||
|
func v1StatusToV1beta1Status(in authorizationv1.SubjectAccessReviewStatus) authorizationv1beta1.SubjectAccessReviewStatus {
|
||||||
|
return authorizationv1beta1.SubjectAccessReviewStatus{
|
||||||
|
Allowed: in.Allowed,
|
||||||
|
Denied: in.Denied,
|
||||||
|
Reason: in.Reason,
|
||||||
|
EvaluationError: in.EvaluationError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1beta1SpecToV1Spec is only needed to verify round-trip fidelity
|
||||||
|
func v1beta1SpecToV1Spec(in authorizationv1beta1.SubjectAccessReviewSpec) authorizationv1.SubjectAccessReviewSpec {
|
||||||
|
return authorizationv1.SubjectAccessReviewSpec{
|
||||||
|
ResourceAttributes: v1beta1ResourceAttributesToV1ResourceAttributes(in.ResourceAttributes),
|
||||||
|
NonResourceAttributes: v1beta1NonResourceAttributesToV1NonResourceAttributes(in.NonResourceAttributes),
|
||||||
|
User: in.User,
|
||||||
|
Groups: in.Groups,
|
||||||
|
Extra: v1beta1ExtraToV1Extra(in.Extra),
|
||||||
|
UID: in.UID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.ResourceAttributes) *authorizationv1.ResourceAttributes {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &authorizationv1.ResourceAttributes{
|
||||||
|
Namespace: in.Namespace,
|
||||||
|
Verb: in.Verb,
|
||||||
|
Group: in.Group,
|
||||||
|
Version: in.Version,
|
||||||
|
Resource: in.Resource,
|
||||||
|
Subresource: in.Subresource,
|
||||||
|
Name: in.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1beta1NonResourceAttributesToV1NonResourceAttributes(in *authorizationv1beta1.NonResourceAttributes) *authorizationv1.NonResourceAttributes {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &authorizationv1.NonResourceAttributes{
|
||||||
|
Path: in.Path,
|
||||||
|
Verb: in.Verb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1beta1ExtraToV1Extra(in map[string]authorizationv1beta1.ExtraValue) map[string]authorizationv1.ExtraValue {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make(map[string]authorizationv1.ExtraValue, len(in))
|
||||||
|
for k, v := range in {
|
||||||
|
ret[k] = authorizationv1.ExtraValue(v)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
@@ -25,7 +25,8 @@ import (
|
|||||||
|
|
||||||
"k8s.io/klog"
|
"k8s.io/klog"
|
||||||
|
|
||||||
authorization "k8s.io/api/authorization/v1beta1"
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
|
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/cache"
|
"k8s.io/apimachinery/pkg/util/cache"
|
||||||
@@ -33,11 +34,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
|
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
groupVersions = []schema.GroupVersion{authorization.SchemeGroupVersion}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -49,8 +46,12 @@ const (
|
|||||||
// Ensure Webhook implements the authorizer.Authorizer interface.
|
// Ensure Webhook implements the authorizer.Authorizer interface.
|
||||||
var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil)
|
var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil)
|
||||||
|
|
||||||
|
type subjectAccessReviewer interface {
|
||||||
|
CreateContext(context.Context, *authorizationv1.SubjectAccessReview) (*authorizationv1.SubjectAccessReview, error)
|
||||||
|
}
|
||||||
|
|
||||||
type WebhookAuthorizer struct {
|
type WebhookAuthorizer struct {
|
||||||
subjectAccessReview authorizationclient.SubjectAccessReviewInterface
|
subjectAccessReview subjectAccessReviewer
|
||||||
responseCache *cache.LRUExpireCache
|
responseCache *cache.LRUExpireCache
|
||||||
authorizedTTL time.Duration
|
authorizedTTL time.Duration
|
||||||
unauthorizedTTL time.Duration
|
unauthorizedTTL time.Duration
|
||||||
@@ -59,12 +60,11 @@ type WebhookAuthorizer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
|
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
|
||||||
func NewFromInterface(subjectAccessReview authorizationclient.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
|
func NewFromInterface(subjectAccessReview authorizationv1client.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
|
||||||
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff)
|
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new WebhookAuthorizer from the provided kubeconfig file.
|
// New creates a new WebhookAuthorizer from the provided kubeconfig file.
|
||||||
//
|
|
||||||
// The config's cluster field is used to refer to the remote service, user refers to the returned authorizer.
|
// The config's cluster field is used to refer to the remote service, user refers to the returned authorizer.
|
||||||
//
|
//
|
||||||
// # clusters refers to the remote service.
|
// # clusters refers to the remote service.
|
||||||
@@ -83,8 +83,8 @@ func NewFromInterface(subjectAccessReview authorizationclient.SubjectAccessRevie
|
|||||||
//
|
//
|
||||||
// For additional HTTP configuration, refer to the kubeconfig documentation
|
// For additional HTTP configuration, refer to the kubeconfig documentation
|
||||||
// https://kubernetes.io/docs/user-guide/kubeconfig-file/.
|
// https://kubernetes.io/docs/user-guide/kubeconfig-file/.
|
||||||
func New(kubeConfigFile string, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
|
func New(kubeConfigFile string, version string, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
|
||||||
subjectAccessReview, err := subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile)
|
subjectAccessReview, err := subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ func New(kubeConfigFile string, authorizedTTL, unauthorizedTTL time.Duration) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newWithBackoff allows tests to skip the sleep.
|
// newWithBackoff allows tests to skip the sleep.
|
||||||
func newWithBackoff(subjectAccessReview authorizationclient.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL, initialBackoff time.Duration) (*WebhookAuthorizer, error) {
|
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL, initialBackoff time.Duration) (*WebhookAuthorizer, error) {
|
||||||
return &WebhookAuthorizer{
|
return &WebhookAuthorizer{
|
||||||
subjectAccessReview: subjectAccessReview,
|
subjectAccessReview: subjectAccessReview,
|
||||||
responseCache: cache.NewLRUExpireCache(1024),
|
responseCache: cache.NewLRUExpireCache(1024),
|
||||||
@@ -151,9 +151,9 @@ func newWithBackoff(subjectAccessReview authorizationclient.SubjectAccessReviewI
|
|||||||
// encounter an error. We are failing open now to preserve backwards compatible
|
// encounter an error. We are failing open now to preserve backwards compatible
|
||||||
// behavior.
|
// behavior.
|
||||||
func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
|
||||||
r := &authorization.SubjectAccessReview{}
|
r := &authorizationv1.SubjectAccessReview{}
|
||||||
if user := attr.GetUser(); user != nil {
|
if user := attr.GetUser(); user != nil {
|
||||||
r.Spec = authorization.SubjectAccessReviewSpec{
|
r.Spec = authorizationv1.SubjectAccessReviewSpec{
|
||||||
User: user.GetName(),
|
User: user.GetName(),
|
||||||
UID: user.GetUID(),
|
UID: user.GetUID(),
|
||||||
Groups: user.GetGroups(),
|
Groups: user.GetGroups(),
|
||||||
@@ -162,7 +162,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
|
|||||||
}
|
}
|
||||||
|
|
||||||
if attr.IsResourceRequest() {
|
if attr.IsResourceRequest() {
|
||||||
r.Spec.ResourceAttributes = &authorization.ResourceAttributes{
|
r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{
|
||||||
Namespace: attr.GetNamespace(),
|
Namespace: attr.GetNamespace(),
|
||||||
Verb: attr.GetVerb(),
|
Verb: attr.GetVerb(),
|
||||||
Group: attr.GetAPIGroup(),
|
Group: attr.GetAPIGroup(),
|
||||||
@@ -172,7 +172,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
|
|||||||
Name: attr.GetName(),
|
Name: attr.GetName(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
r.Spec.NonResourceAttributes = &authorization.NonResourceAttributes{
|
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
|
||||||
Path: attr.GetPath(),
|
Path: attr.GetPath(),
|
||||||
Verb: attr.GetVerb(),
|
Verb: attr.GetVerb(),
|
||||||
}
|
}
|
||||||
@@ -182,10 +182,10 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
|
|||||||
return w.decisionOnError, "", err
|
return w.decisionOnError, "", err
|
||||||
}
|
}
|
||||||
if entry, ok := w.responseCache.Get(string(key)); ok {
|
if entry, ok := w.responseCache.Get(string(key)); ok {
|
||||||
r.Status = entry.(authorization.SubjectAccessReviewStatus)
|
r.Status = entry.(authorizationv1.SubjectAccessReviewStatus)
|
||||||
} else {
|
} else {
|
||||||
var (
|
var (
|
||||||
result *authorization.SubjectAccessReview
|
result *authorizationv1.SubjectAccessReview
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
webhook.WithExponentialBackoff(ctx, w.initialBackoff, func() error {
|
webhook.WithExponentialBackoff(ctx, w.initialBackoff, func() error {
|
||||||
@@ -229,13 +229,13 @@ func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]author
|
|||||||
return resourceRules, nonResourceRules, incomplete, fmt.Errorf("webhook authorizer does not support user rule resolution")
|
return resourceRules, nonResourceRules, incomplete, fmt.Errorf("webhook authorizer does not support user rule resolution")
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToSARExtra(extra map[string][]string) map[string]authorization.ExtraValue {
|
func convertToSARExtra(extra map[string][]string) map[string]authorizationv1.ExtraValue {
|
||||||
if extra == nil {
|
if extra == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ret := map[string]authorization.ExtraValue{}
|
ret := map[string]authorizationv1.ExtraValue{}
|
||||||
for k, v := range extra {
|
for k, v := range extra {
|
||||||
ret[k] = authorization.ExtraValue(v)
|
ret[k] = authorizationv1.ExtraValue(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@@ -244,36 +244,69 @@ func convertToSARExtra(extra map[string][]string) map[string]authorization.Extra
|
|||||||
// subjectAccessReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file,
|
// subjectAccessReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file,
|
||||||
// and returns a SubjectAccessReviewInterface that uses that client. Note that the client submits SubjectAccessReview
|
// and returns a SubjectAccessReviewInterface that uses that client. Note that the client submits SubjectAccessReview
|
||||||
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
|
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
|
||||||
func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string) (authorizationclient.SubjectAccessReviewInterface, error) {
|
func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string, version string) (subjectAccessReviewer, error) {
|
||||||
localScheme := runtime.NewScheme()
|
localScheme := runtime.NewScheme()
|
||||||
if err := scheme.AddToScheme(localScheme); err != nil {
|
if err := scheme.AddToScheme(localScheme); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
|
switch version {
|
||||||
if err != nil {
|
case authorizationv1.SchemeGroupVersion.Version:
|
||||||
return nil, err
|
groupVersions := []schema.GroupVersion{authorizationv1.SchemeGroupVersion}
|
||||||
|
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &subjectAccessReviewV1Client{gw}, nil
|
||||||
|
|
||||||
|
case authorizationv1beta1.SchemeGroupVersion.Version:
|
||||||
|
groupVersions := []schema.GroupVersion{authorizationv1beta1.SchemeGroupVersion}
|
||||||
|
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &subjectAccessReviewV1beta1Client{gw}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"unsupported webhook authorizer version %q, supported versions are %q, %q",
|
||||||
|
version,
|
||||||
|
authorizationv1.SchemeGroupVersion.Version,
|
||||||
|
authorizationv1beta1.SchemeGroupVersion.Version,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return &subjectAccessReviewClient{gw}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type subjectAccessReviewClient struct {
|
type subjectAccessReviewV1Client struct {
|
||||||
w *webhook.GenericWebhook
|
w *webhook.GenericWebhook
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *subjectAccessReviewClient) Create(subjectAccessReview *authorization.SubjectAccessReview) (*authorization.SubjectAccessReview, error) {
|
func (t *subjectAccessReviewV1Client) CreateContext(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview) (*authorizationv1.SubjectAccessReview, error) {
|
||||||
return t.CreateContext(context.Background(), subjectAccessReview)
|
result := &authorizationv1.SubjectAccessReview{}
|
||||||
}
|
|
||||||
|
|
||||||
func (t *subjectAccessReviewClient) CreateContext(ctx context.Context, subjectAccessReview *authorization.SubjectAccessReview) (*authorization.SubjectAccessReview, error) {
|
|
||||||
result := &authorization.SubjectAccessReview{}
|
|
||||||
err := t.w.RestClient.Post().Context(ctx).Body(subjectAccessReview).Do().Into(result)
|
err := t.w.RestClient.Post().Context(ctx).Body(subjectAccessReview).Do().Into(result)
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type subjectAccessReviewV1beta1Client struct {
|
||||||
|
w *webhook.GenericWebhook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *subjectAccessReviewV1beta1Client) CreateContext(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview) (*authorizationv1.SubjectAccessReview, error) {
|
||||||
|
v1beta1Review := &authorizationv1beta1.SubjectAccessReview{Spec: v1SpecToV1beta1Spec(&subjectAccessReview.Spec)}
|
||||||
|
v1beta1Result := &authorizationv1beta1.SubjectAccessReview{}
|
||||||
|
err := t.w.RestClient.Post().Context(ctx).Body(v1beta1Review).Do().Into(v1beta1Result)
|
||||||
|
if err == nil {
|
||||||
|
subjectAccessReview.Status = v1beta1StatusToV1Status(&v1beta1Result.Status)
|
||||||
|
}
|
||||||
|
return subjectAccessReview, err
|
||||||
|
}
|
||||||
|
|
||||||
// shouldCache determines whether it is safe to cache the given request attributes. If the
|
// shouldCache determines whether it is safe to cache the given request attributes. If the
|
||||||
// requester-controlled attributes are too large, this may be a DoS attempt, so we skip the cache.
|
// requester-controlled attributes are too large, this may be a DoS attempt, so we skip the cache.
|
||||||
func shouldCache(attr authorizer.Attributes) bool {
|
func shouldCache(attr authorizer.Attributes) bool {
|
||||||
@@ -287,3 +320,59 @@ func shouldCache(attr authorizer.Attributes) bool {
|
|||||||
int64(len(attr.GetPath()))
|
int64(len(attr.GetPath()))
|
||||||
return controlledAttrSize < maxControlledAttrCacheSize
|
return controlledAttrSize < maxControlledAttrCacheSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func v1beta1StatusToV1Status(in *authorizationv1beta1.SubjectAccessReviewStatus) authorizationv1.SubjectAccessReviewStatus {
|
||||||
|
return authorizationv1.SubjectAccessReviewStatus{
|
||||||
|
Allowed: in.Allowed,
|
||||||
|
Denied: in.Denied,
|
||||||
|
Reason: in.Reason,
|
||||||
|
EvaluationError: in.EvaluationError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1SpecToV1beta1Spec(in *authorizationv1.SubjectAccessReviewSpec) authorizationv1beta1.SubjectAccessReviewSpec {
|
||||||
|
return authorizationv1beta1.SubjectAccessReviewSpec{
|
||||||
|
ResourceAttributes: v1ResourceAttributesToV1beta1ResourceAttributes(in.ResourceAttributes),
|
||||||
|
NonResourceAttributes: v1NonResourceAttributesToV1beta1NonResourceAttributes(in.NonResourceAttributes),
|
||||||
|
User: in.User,
|
||||||
|
Groups: in.Groups,
|
||||||
|
Extra: v1ExtraToV1beta1Extra(in.Extra),
|
||||||
|
UID: in.UID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.ResourceAttributes) *authorizationv1beta1.ResourceAttributes {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &authorizationv1beta1.ResourceAttributes{
|
||||||
|
Namespace: in.Namespace,
|
||||||
|
Verb: in.Verb,
|
||||||
|
Group: in.Group,
|
||||||
|
Version: in.Version,
|
||||||
|
Resource: in.Resource,
|
||||||
|
Subresource: in.Subresource,
|
||||||
|
Name: in.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1NonResourceAttributesToV1beta1NonResourceAttributes(in *authorizationv1.NonResourceAttributes) *authorizationv1beta1.NonResourceAttributes {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &authorizationv1beta1.NonResourceAttributes{
|
||||||
|
Path: in.Path,
|
||||||
|
Verb: in.Verb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func v1ExtraToV1beta1Extra(in map[string]authorizationv1.ExtraValue) map[string]authorizationv1beta1.ExtraValue {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make(map[string]authorizationv1beta1.ExtraValue, len(in))
|
||||||
|
for k, v := range in {
|
||||||
|
ret[k] = authorizationv1beta1.ExtraValue(v)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
@@ -34,15 +34,15 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/api/authorization/v1beta1"
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/diff"
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/client-go/tools/clientcmd/api/v1"
|
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewFromConfig(t *testing.T) {
|
func TestV1NewFromConfig(t *testing.T) {
|
||||||
dir, err := ioutil.TempDir("", "")
|
dir, err := ioutil.TempDir("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -186,7 +186,7 @@ current-context: default
|
|||||||
return fmt.Errorf("failed to execute test template: %v", err)
|
return fmt.Errorf("failed to execute test template: %v", err)
|
||||||
}
|
}
|
||||||
// Create a new authorizer
|
// Create a new authorizer
|
||||||
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p)
|
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error building sar client: %v", err)
|
return fmt.Errorf("error building sar client: %v", err)
|
||||||
}
|
}
|
||||||
@@ -202,14 +202,14 @@ current-context: default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service mocks a remote service.
|
// V1Service mocks a remote service.
|
||||||
type Service interface {
|
type V1Service interface {
|
||||||
Review(*v1beta1.SubjectAccessReview)
|
Review(*authorizationv1.SubjectAccessReview)
|
||||||
HTTPStatusCode() int
|
HTTPStatusCode() int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestServer wraps a Service as an httptest.Server.
|
// NewV1TestServer wraps a V1Service as an httptest.Server.
|
||||||
func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
func NewV1TestServer(s V1Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
||||||
const webhookPath = "/testserver"
|
const webhookPath = "/testserver"
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
if cert != nil {
|
if cert != nil {
|
||||||
@@ -240,7 +240,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var review v1beta1.SubjectAccessReview
|
var review authorizationv1.SubjectAccessReview
|
||||||
bodyData, _ := ioutil.ReadAll(r.Body)
|
bodyData, _ := ioutil.ReadAll(r.Body)
|
||||||
if err := json.Unmarshal(bodyData, &review); err != nil {
|
if err := json.Unmarshal(bodyData, &review); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
||||||
@@ -248,7 +248,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensure we received the serialized review as expected
|
// ensure we received the serialized review as expected
|
||||||
if review.APIVersion != "authorization.k8s.io/v1beta1" {
|
if review.APIVersion != "authorization.k8s.io/v1" {
|
||||||
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
|
|||||||
APIVersion string `json:"apiVersion"`
|
APIVersion string `json:"apiVersion"`
|
||||||
Status status `json:"status"`
|
Status status `json:"status"`
|
||||||
}{
|
}{
|
||||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
APIVersion: authorizationv1.SchemeGroupVersion.String(),
|
||||||
Status: status{review.Status.Allowed, review.Status.Reason, review.Status.EvaluationError},
|
Status: status{review.Status.Allowed, review.Status.Reason, review.Status.EvaluationError},
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -287,23 +287,23 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A service that can be set to allow all or deny all authorization requests.
|
// A service that can be set to allow all or deny all authorization requests.
|
||||||
type mockService struct {
|
type mockV1Service struct {
|
||||||
allow bool
|
allow bool
|
||||||
statusCode int
|
statusCode int
|
||||||
called int
|
called int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockService) Review(r *v1beta1.SubjectAccessReview) {
|
func (m *mockV1Service) Review(r *authorizationv1.SubjectAccessReview) {
|
||||||
m.called++
|
m.called++
|
||||||
r.Status.Allowed = m.allow
|
r.Status.Allowed = m.allow
|
||||||
}
|
}
|
||||||
func (m *mockService) Allow() { m.allow = true }
|
func (m *mockV1Service) Allow() { m.allow = true }
|
||||||
func (m *mockService) Deny() { m.allow = false }
|
func (m *mockV1Service) Deny() { m.allow = false }
|
||||||
func (m *mockService) HTTPStatusCode() int { return m.statusCode }
|
func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode }
|
||||||
|
|
||||||
// newAuthorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
|
// newV1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
|
||||||
// a new WebhookAuthorizer from it.
|
// a new WebhookAuthorizer from it.
|
||||||
func newAuthorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration) (*WebhookAuthorizer, error) {
|
func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration) (*WebhookAuthorizer, error) {
|
||||||
tempfile, err := ioutil.TempFile("", "")
|
tempfile, err := ioutil.TempFile("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -325,14 +325,14 @@ func newAuthorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTi
|
|||||||
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
|
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p)
|
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error building sar client: %v", err)
|
return nil, fmt.Errorf("error building sar client: %v", err)
|
||||||
}
|
}
|
||||||
return newWithBackoff(sarClient, cacheTime, cacheTime, 0)
|
return newWithBackoff(sarClient, cacheTime, cacheTime, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSConfig(t *testing.T) {
|
func TestV1TLSConfig(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
test string
|
test string
|
||||||
clientCert, clientKey, clientCA []byte
|
clientCert, clientKey, clientCA []byte
|
||||||
@@ -378,17 +378,17 @@ func TestTLSConfig(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
// Use a closure so defer statements trigger between loop iterations.
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
func() {
|
func() {
|
||||||
service := new(mockService)
|
service := new(mockV1Service)
|
||||||
service.statusCode = 200
|
service.statusCode = 200
|
||||||
|
|
||||||
server, err := NewTestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
|
server, err := NewV1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
wh, err := newAuthorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0)
|
wh, err := newV1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||||
return
|
return
|
||||||
@@ -427,62 +427,62 @@ func TestTLSConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// recorderService records all access review requests.
|
// recorderV1Service records all access review requests.
|
||||||
type recorderService struct {
|
type recorderV1Service struct {
|
||||||
last v1beta1.SubjectAccessReview
|
last authorizationv1.SubjectAccessReview
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rec *recorderService) Review(r *v1beta1.SubjectAccessReview) {
|
func (rec *recorderV1Service) Review(r *authorizationv1.SubjectAccessReview) {
|
||||||
rec.last = v1beta1.SubjectAccessReview{}
|
rec.last = authorizationv1.SubjectAccessReview{}
|
||||||
rec.last = *r
|
rec.last = *r
|
||||||
r.Status.Allowed = true
|
r.Status.Allowed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rec *recorderService) Last() (v1beta1.SubjectAccessReview, error) {
|
func (rec *recorderV1Service) Last() (authorizationv1.SubjectAccessReview, error) {
|
||||||
return rec.last, rec.err
|
return rec.last, rec.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rec *recorderService) HTTPStatusCode() int { return 200 }
|
func (rec *recorderV1Service) HTTPStatusCode() int { return 200 }
|
||||||
|
|
||||||
func TestWebhook(t *testing.T) {
|
func TestV1Webhook(t *testing.T) {
|
||||||
serv := new(recorderService)
|
serv := new(recorderV1Service)
|
||||||
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
|
s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
wh, err := newAuthorizer(s.URL, clientCert, clientKey, caCert, 0)
|
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expTypeMeta := metav1.TypeMeta{
|
expTypeMeta := metav1.TypeMeta{
|
||||||
APIVersion: "authorization.k8s.io/v1beta1",
|
APIVersion: "authorization.k8s.io/v1",
|
||||||
Kind: "SubjectAccessReview",
|
Kind: "SubjectAccessReview",
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
attr authorizer.Attributes
|
attr authorizer.Attributes
|
||||||
want v1beta1.SubjectAccessReview
|
want authorizationv1.SubjectAccessReview
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}},
|
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}},
|
||||||
want: v1beta1.SubjectAccessReview{
|
want: authorizationv1.SubjectAccessReview{
|
||||||
TypeMeta: expTypeMeta,
|
TypeMeta: expTypeMeta,
|
||||||
Spec: v1beta1.SubjectAccessReviewSpec{
|
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||||
NonResourceAttributes: &v1beta1.NonResourceAttributes{},
|
NonResourceAttributes: &authorizationv1.NonResourceAttributes{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}},
|
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}},
|
||||||
want: v1beta1.SubjectAccessReview{
|
want: authorizationv1.SubjectAccessReview{
|
||||||
TypeMeta: expTypeMeta,
|
TypeMeta: expTypeMeta,
|
||||||
Spec: v1beta1.SubjectAccessReviewSpec{
|
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||||
User: "jane",
|
User: "jane",
|
||||||
NonResourceAttributes: &v1beta1.NonResourceAttributes{},
|
NonResourceAttributes: &authorizationv1.NonResourceAttributes{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -503,13 +503,13 @@ func TestWebhook(t *testing.T) {
|
|||||||
ResourceRequest: true,
|
ResourceRequest: true,
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
},
|
},
|
||||||
want: v1beta1.SubjectAccessReview{
|
want: authorizationv1.SubjectAccessReview{
|
||||||
TypeMeta: expTypeMeta,
|
TypeMeta: expTypeMeta,
|
||||||
Spec: v1beta1.SubjectAccessReviewSpec{
|
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||||
User: "jane",
|
User: "jane",
|
||||||
UID: "1",
|
UID: "1",
|
||||||
Groups: []string{"group1", "group2"},
|
Groups: []string{"group1", "group2"},
|
||||||
ResourceAttributes: &v1beta1.ResourceAttributes{
|
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||||
Verb: "GET",
|
Verb: "GET",
|
||||||
Namespace: "kittensandponies",
|
Namespace: "kittensandponies",
|
||||||
Group: "group3",
|
Group: "group3",
|
||||||
@@ -546,16 +546,16 @@ func TestWebhook(t *testing.T) {
|
|||||||
|
|
||||||
// TestWebhookCache verifies that error responses from the server are not
|
// TestWebhookCache verifies that error responses from the server are not
|
||||||
// cached, but successful responses are.
|
// cached, but successful responses are.
|
||||||
func TestWebhookCache(t *testing.T) {
|
func TestV1WebhookCache(t *testing.T) {
|
||||||
serv := new(mockService)
|
serv := new(mockV1Service)
|
||||||
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
|
s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
// Create an authorizer that caches successful responses "forever" (100 days).
|
// Create an authorizer that caches successful responses "forever" (100 days).
|
||||||
wh, err := newAuthorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour)
|
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
@@ -0,0 +1,647 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV1beta1NewFromConfig(t *testing.T) {
|
||||||
|
dir, err := ioutil.TempDir("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
CA string
|
||||||
|
Cert string
|
||||||
|
Key string
|
||||||
|
}{
|
||||||
|
CA: filepath.Join(dir, "ca.pem"),
|
||||||
|
Cert: filepath.Join(dir, "clientcert.pem"),
|
||||||
|
Key: filepath.Join(dir, "clientkey.pem"),
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
}{
|
||||||
|
{data.CA, caCert},
|
||||||
|
{data.Cert, clientCert},
|
||||||
|
{data.Key, clientKey},
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
msg string
|
||||||
|
configTmpl string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
msg: "a single cluster and single user",
|
||||||
|
configTmpl: `
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: {{ .CA }}
|
||||||
|
server: https://authz.example.com
|
||||||
|
name: foobar
|
||||||
|
users:
|
||||||
|
- name: a cluster
|
||||||
|
user:
|
||||||
|
client-certificate: {{ .Cert }}
|
||||||
|
client-key: {{ .Key }}
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
msg: "multiple clusters with no context",
|
||||||
|
configTmpl: `
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: {{ .CA }}
|
||||||
|
server: https://authz.example.com
|
||||||
|
name: foobar
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: a bad certificate path
|
||||||
|
server: https://authz.example.com
|
||||||
|
name: barfoo
|
||||||
|
users:
|
||||||
|
- name: a name
|
||||||
|
user:
|
||||||
|
client-certificate: {{ .Cert }}
|
||||||
|
client-key: {{ .Key }}
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
msg: "multiple clusters with a context",
|
||||||
|
configTmpl: `
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: a bad certificate path
|
||||||
|
server: https://authz.example.com
|
||||||
|
name: foobar
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: {{ .CA }}
|
||||||
|
server: https://authz.example.com
|
||||||
|
name: barfoo
|
||||||
|
users:
|
||||||
|
- name: a name
|
||||||
|
user:
|
||||||
|
client-certificate: {{ .Cert }}
|
||||||
|
client-key: {{ .Key }}
|
||||||
|
contexts:
|
||||||
|
- name: default
|
||||||
|
context:
|
||||||
|
cluster: barfoo
|
||||||
|
user: a name
|
||||||
|
current-context: default
|
||||||
|
`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
msg: "cluster with bad certificate path specified",
|
||||||
|
configTmpl: `
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: a bad certificate path
|
||||||
|
server: https://authz.example.com
|
||||||
|
name: foobar
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: {{ .CA }}
|
||||||
|
server: https://authz.example.com
|
||||||
|
name: barfoo
|
||||||
|
users:
|
||||||
|
- name: a name
|
||||||
|
user:
|
||||||
|
client-certificate: {{ .Cert }}
|
||||||
|
client-key: {{ .Key }}
|
||||||
|
contexts:
|
||||||
|
- name: default
|
||||||
|
context:
|
||||||
|
cluster: foobar
|
||||||
|
user: a name
|
||||||
|
current-context: default
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
|
err := func() error {
|
||||||
|
tempfile, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p := tempfile.Name()
|
||||||
|
defer os.Remove(p)
|
||||||
|
|
||||||
|
tmpl, err := template.New("test").Parse(tt.configTmpl)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse test template: %v", err)
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(tempfile, data); err != nil {
|
||||||
|
return fmt.Errorf("failed to execute test template: %v", err)
|
||||||
|
}
|
||||||
|
// Create a new authorizer
|
||||||
|
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1beta1")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error building sar client: %v", err)
|
||||||
|
}
|
||||||
|
_, err = newWithBackoff(sarClient, 0, 0, 0)
|
||||||
|
return err
|
||||||
|
}()
|
||||||
|
if err != nil && !tt.wantErr {
|
||||||
|
t.Errorf("failed to load plugin from config %q: %v", tt.msg, err)
|
||||||
|
}
|
||||||
|
if err == nil && tt.wantErr {
|
||||||
|
t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// V1beta1Service mocks a remote service.
|
||||||
|
type V1beta1Service interface {
|
||||||
|
Review(*authorizationv1beta1.SubjectAccessReview)
|
||||||
|
HTTPStatusCode() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewV1beta1TestServer wraps a V1beta1Service as an httptest.Server.
|
||||||
|
func NewV1beta1TestServer(s V1beta1Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
||||||
|
const webhookPath = "/testserver"
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
if cert != nil {
|
||||||
|
cert, err := tls.X509KeyPair(cert, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if caCert != nil {
|
||||||
|
rootCAs := x509.NewCertPool()
|
||||||
|
rootCAs.AppendCertsFromPEM(caCert)
|
||||||
|
if tlsConfig == nil {
|
||||||
|
tlsConfig = &tls.Config{}
|
||||||
|
}
|
||||||
|
tlsConfig.ClientCAs = rootCAs
|
||||||
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
}
|
||||||
|
|
||||||
|
serveHTTP := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.URL.Path != webhookPath {
|
||||||
|
http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var review authorizationv1beta1.SubjectAccessReview
|
||||||
|
bodyData, _ := ioutil.ReadAll(r.Body)
|
||||||
|
if err := json.Unmarshal(bodyData, &review); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we received the serialized review as expected
|
||||||
|
if review.APIVersion != "authorization.k8s.io/v1beta1" {
|
||||||
|
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// once we have a successful request, always call the review to record that we were called
|
||||||
|
s.Review(&review)
|
||||||
|
if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 {
|
||||||
|
http.Error(w, "HTTP Error", s.HTTPStatusCode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type status struct {
|
||||||
|
Allowed bool `json:"allowed"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
EvaluationError string `json:"evaluationError"`
|
||||||
|
}
|
||||||
|
resp := struct {
|
||||||
|
APIVersion string `json:"apiVersion"`
|
||||||
|
Status status `json:"status"`
|
||||||
|
}{
|
||||||
|
APIVersion: authorizationv1beta1.SchemeGroupVersion.String(),
|
||||||
|
Status: status{review.Status.Allowed, review.Status.Reason, review.Status.EvaluationError},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
|
||||||
|
server.TLS = tlsConfig
|
||||||
|
server.StartTLS()
|
||||||
|
|
||||||
|
// Adjust the path to point to our custom path
|
||||||
|
serverURL, _ := url.Parse(server.URL)
|
||||||
|
serverURL.Path = webhookPath
|
||||||
|
server.URL = serverURL.String()
|
||||||
|
|
||||||
|
return server, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A service that can be set to allow all or deny all authorization requests.
|
||||||
|
type mockV1beta1Service struct {
|
||||||
|
allow bool
|
||||||
|
statusCode int
|
||||||
|
called int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockV1beta1Service) Review(r *authorizationv1beta1.SubjectAccessReview) {
|
||||||
|
m.called++
|
||||||
|
r.Status.Allowed = m.allow
|
||||||
|
}
|
||||||
|
func (m *mockV1beta1Service) Allow() { m.allow = true }
|
||||||
|
func (m *mockV1beta1Service) Deny() { m.allow = false }
|
||||||
|
func (m *mockV1beta1Service) HTTPStatusCode() int { return m.statusCode }
|
||||||
|
|
||||||
|
// newV1beta1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
|
||||||
|
// a new WebhookAuthorizer from it.
|
||||||
|
func newV1beta1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration) (*WebhookAuthorizer, error) {
|
||||||
|
tempfile, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := tempfile.Name()
|
||||||
|
defer os.Remove(p)
|
||||||
|
config := v1.Config{
|
||||||
|
Clusters: []v1.NamedCluster{
|
||||||
|
{
|
||||||
|
Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuthInfos: []v1.NamedAuthInfo{
|
||||||
|
{
|
||||||
|
AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1beta1")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error building sar client: %v", err)
|
||||||
|
}
|
||||||
|
return newWithBackoff(sarClient, cacheTime, cacheTime, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV1beta1TLSConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
test string
|
||||||
|
clientCert, clientKey, clientCA []byte
|
||||||
|
serverCert, serverKey, serverCA []byte
|
||||||
|
wantAuth, wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
test: "TLS setup between client and server",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
|
||||||
|
wantAuth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not require client auth",
|
||||||
|
clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
wantAuth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not require client auth, client provides it",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
wantAuth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Client does not trust server",
|
||||||
|
clientCert: clientCert, clientKey: clientKey,
|
||||||
|
serverCert: serverCert, serverKey: serverKey,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Server does not trust client",
|
||||||
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
||||||
|
serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Plugin does not support insecure configurations.
|
||||||
|
test: "Server is using insecure connection",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
// Use a closure so defer statements trigger between loop iterations.
|
||||||
|
func() {
|
||||||
|
service := new(mockV1beta1Service)
|
||||||
|
service.statusCode = 200
|
||||||
|
|
||||||
|
server, err := NewV1beta1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wh, err := newV1beta1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attr := authorizer.AttributesRecord{User: &user.DefaultInfo{}}
|
||||||
|
|
||||||
|
// Allow all and see if we get an error.
|
||||||
|
service.Allow()
|
||||||
|
decision, _, err := wh.Authorize(context.Background(), attr)
|
||||||
|
if tt.wantAuth {
|
||||||
|
if decision != authorizer.DecisionAllow {
|
||||||
|
t.Errorf("expected successful authorization")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if decision == authorizer.DecisionAllow {
|
||||||
|
t.Errorf("expected failed authorization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error making authorization request: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: failed to authorize with AllowAll policy: %v", tt.test, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Deny()
|
||||||
|
if decision, _, _ := wh.Authorize(context.Background(), attr); decision == authorizer.DecisionAllow {
|
||||||
|
t.Errorf("%s: incorrectly authorized with DenyAll policy", tt.test)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recorderV1beta1Service records all access review requests.
|
||||||
|
type recorderV1beta1Service struct {
|
||||||
|
last authorizationv1beta1.SubjectAccessReview
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rec *recorderV1beta1Service) Review(r *authorizationv1beta1.SubjectAccessReview) {
|
||||||
|
rec.last = authorizationv1beta1.SubjectAccessReview{}
|
||||||
|
rec.last = *r
|
||||||
|
r.Status.Allowed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rec *recorderV1beta1Service) Last() (authorizationv1beta1.SubjectAccessReview, error) {
|
||||||
|
return rec.last, rec.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rec *recorderV1beta1Service) HTTPStatusCode() int { return 200 }
|
||||||
|
|
||||||
|
func TestV1beta1Webhook(t *testing.T) {
|
||||||
|
serv := new(recorderV1beta1Service)
|
||||||
|
s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
wh, err := newV1beta1Authorizer(s.URL, clientCert, clientKey, caCert, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expTypeMeta := metav1.TypeMeta{
|
||||||
|
APIVersion: "authorization.k8s.io/v1beta1",
|
||||||
|
Kind: "SubjectAccessReview",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
attr authorizer.Attributes
|
||||||
|
want authorizationv1beta1.SubjectAccessReview
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}},
|
||||||
|
want: authorizationv1beta1.SubjectAccessReview{
|
||||||
|
TypeMeta: expTypeMeta,
|
||||||
|
Spec: authorizationv1beta1.SubjectAccessReviewSpec{
|
||||||
|
NonResourceAttributes: &authorizationv1beta1.NonResourceAttributes{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}},
|
||||||
|
want: authorizationv1beta1.SubjectAccessReview{
|
||||||
|
TypeMeta: expTypeMeta,
|
||||||
|
Spec: authorizationv1beta1.SubjectAccessReviewSpec{
|
||||||
|
User: "jane",
|
||||||
|
NonResourceAttributes: &authorizationv1beta1.NonResourceAttributes{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attr: authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{
|
||||||
|
Name: "jane",
|
||||||
|
UID: "1",
|
||||||
|
Groups: []string{"group1", "group2"},
|
||||||
|
},
|
||||||
|
Verb: "GET",
|
||||||
|
Namespace: "kittensandponies",
|
||||||
|
APIGroup: "group3",
|
||||||
|
APIVersion: "v7beta3",
|
||||||
|
Resource: "pods",
|
||||||
|
Subresource: "proxy",
|
||||||
|
Name: "my-pod",
|
||||||
|
ResourceRequest: true,
|
||||||
|
Path: "/foo",
|
||||||
|
},
|
||||||
|
want: authorizationv1beta1.SubjectAccessReview{
|
||||||
|
TypeMeta: expTypeMeta,
|
||||||
|
Spec: authorizationv1beta1.SubjectAccessReviewSpec{
|
||||||
|
User: "jane",
|
||||||
|
UID: "1",
|
||||||
|
Groups: []string{"group1", "group2"},
|
||||||
|
ResourceAttributes: &authorizationv1beta1.ResourceAttributes{
|
||||||
|
Verb: "GET",
|
||||||
|
Namespace: "kittensandponies",
|
||||||
|
Group: "group3",
|
||||||
|
Version: "v7beta3",
|
||||||
|
Resource: "pods",
|
||||||
|
Subresource: "proxy",
|
||||||
|
Name: "my-pod",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
decision, _, err := wh.Authorize(context.Background(), tt.attr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if decision != authorizer.DecisionAllow {
|
||||||
|
t.Errorf("case %d: authorization failed", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
gotAttr, err := serv.Last()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("case %d: failed to deserialize webhook request: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotAttr, tt.want) {
|
||||||
|
t.Errorf("case %d: got != want:\n%s", i, diff.ObjectGoPrintDiff(gotAttr, tt.want))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWebhookCache verifies that error responses from the server are not
|
||||||
|
// cached, but successful responses are.
|
||||||
|
func TestV1beta1WebhookCache(t *testing.T) {
|
||||||
|
serv := new(mockV1beta1Service)
|
||||||
|
s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Create an authorizer that caches successful responses "forever" (100 days).
|
||||||
|
wh, err := newV1beta1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "alice"}}
|
||||||
|
bobAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "bob"}}
|
||||||
|
aliceRidiculousAttr := authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{Name: "alice"},
|
||||||
|
ResourceRequest: true,
|
||||||
|
Verb: strings.Repeat("v", 2000),
|
||||||
|
APIGroup: strings.Repeat("g", 2000),
|
||||||
|
APIVersion: strings.Repeat("a", 2000),
|
||||||
|
Resource: strings.Repeat("r", 2000),
|
||||||
|
Name: strings.Repeat("n", 2000),
|
||||||
|
}
|
||||||
|
bobRidiculousAttr := authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{Name: "bob"},
|
||||||
|
ResourceRequest: true,
|
||||||
|
Verb: strings.Repeat("v", 2000),
|
||||||
|
APIGroup: strings.Repeat("g", 2000),
|
||||||
|
APIVersion: strings.Repeat("a", 2000),
|
||||||
|
Resource: strings.Repeat("r", 2000),
|
||||||
|
Name: strings.Repeat("n", 2000),
|
||||||
|
}
|
||||||
|
|
||||||
|
type webhookCacheTestCase struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
attr authorizer.AttributesRecord
|
||||||
|
|
||||||
|
allow bool
|
||||||
|
statusCode int
|
||||||
|
|
||||||
|
expectedErr bool
|
||||||
|
expectedAuthorized bool
|
||||||
|
expectedCalls int
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []webhookCacheTestCase{
|
||||||
|
// server error and 429's retry
|
||||||
|
{name: "server errors retry", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
|
||||||
|
{name: "429s retry", attr: aliceAttr, allow: false, statusCode: 429, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
|
||||||
|
// regular errors return errors but do not retry
|
||||||
|
{name: "404 doesnt retry", attr: aliceAttr, allow: false, statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
|
||||||
|
{name: "403 doesnt retry", attr: aliceAttr, allow: false, statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
|
||||||
|
{name: "401 doesnt retry", attr: aliceAttr, allow: false, statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
|
||||||
|
// successful responses are cached
|
||||||
|
{name: "alice successful request", attr: aliceAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
|
||||||
|
// later requests within the cache window don't hit the backend
|
||||||
|
{name: "alice cached request", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCalls: 0},
|
||||||
|
|
||||||
|
// a request with different attributes doesn't hit the cache
|
||||||
|
{name: "bob failed request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
|
||||||
|
// successful response for other attributes is cached
|
||||||
|
{name: "bob unauthorized request", attr: bobAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
|
||||||
|
// later requests within the cache window don't hit the backend
|
||||||
|
{name: "bob unauthorized cached request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: false, expectedCalls: 0},
|
||||||
|
// ridiculous unauthorized requests are not cached.
|
||||||
|
{name: "ridiculous unauthorized request", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
|
||||||
|
// later ridiculous requests within the cache window still hit the backend
|
||||||
|
{name: "ridiculous unauthorized request again", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
|
||||||
|
// ridiculous authorized requests are not cached.
|
||||||
|
{name: "ridiculous authorized request", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
|
||||||
|
// later ridiculous requests within the cache window still hit the backend
|
||||||
|
{name: "ridiculous authorized request again", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
serv.called = 0
|
||||||
|
serv.allow = test.allow
|
||||||
|
serv.statusCode = test.statusCode
|
||||||
|
authorized, _, err := wh.Authorize(context.Background(), test.attr)
|
||||||
|
if test.expectedErr && err == nil {
|
||||||
|
t.Fatalf("%d: Expected error", i)
|
||||||
|
} else if !test.expectedErr && err != nil {
|
||||||
|
t.Fatalf("%d: unexpected error: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.expectedAuthorized != (authorized == authorizer.DecisionAllow) {
|
||||||
|
t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedAuthorized, authorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.expectedCalls != serv.called {
|
||||||
|
t.Errorf("%d: expected %d calls, got %d", i, test.expectedCalls, serv.called)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -86,7 +86,7 @@ func getTestWebhookTokenAuth(serverURL string) (authenticator.Request, error) {
|
|||||||
if err := json.NewEncoder(kubecfgFile).Encode(config); err != nil {
|
if err := json.NewEncoder(kubecfgFile).Encode(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
webhookTokenAuth, err := webhook.New(kubecfgFile.Name(), nil)
|
webhookTokenAuth, err := webhook.New(kubecfgFile.Name(), "v1beta1", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user