mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-19 16:49:35 +00:00
*: add webhook implementation of authorizer.Authorizer plugin
This commit is contained in:
parent
6a199706cb
commit
3116346161
@ -47,7 +47,7 @@ type APIServer struct {
|
||||
AdvertiseAddress net.IP
|
||||
AllowPrivileged bool
|
||||
AuthorizationMode string
|
||||
AuthorizationPolicyFile string
|
||||
AuthorizationConfig apiserver.AuthorizationConfig
|
||||
BasicAuthFile string
|
||||
CloudConfigFile string
|
||||
CloudProvider string
|
||||
@ -214,7 +214,8 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
|
||||
fs.StringVar(&s.KeystoneURL, "experimental-keystone-url", s.KeystoneURL, "If passed, activates the keystone authentication plugin")
|
||||
fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
|
||||
fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
|
||||
fs.StringVar(&s.AuthorizationConfig.PolicyFile, "authorization-policy-file", s.AuthorizationConfig.PolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
|
||||
fs.StringVar(&s.AuthorizationConfig.WebhookConfigFile, "authorization-webhook-config-file", s.AuthorizationConfig.WebhookConfigFile, "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.")
|
||||
fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", "))
|
||||
fs.StringVar(&s.AdmissionControlConfigFile, "admission-control-config-file", s.AdmissionControlConfigFile, "File with admission control configuration.")
|
||||
fs.StringSliceVar(&s.EtcdServerList, "etcd-servers", s.EtcdServerList, "List of etcd servers to watch (http://ip:port), comma separated. Mutually exclusive with -etcd-config")
|
||||
|
@ -406,7 +406,7 @@ func Run(s *options.APIServer) error {
|
||||
}
|
||||
|
||||
authorizationModeNames := strings.Split(s.AuthorizationMode, ",")
|
||||
authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(authorizationModeNames, s.AuthorizationPolicyFile)
|
||||
authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(authorizationModeNames, s.AuthorizationConfig)
|
||||
if err != nil {
|
||||
glog.Fatalf("Invalid Authorization Config: %v", err)
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/auth/authorizer"
|
||||
"k8s.io/kubernetes/pkg/auth/authorizer/abac"
|
||||
"k8s.io/kubernetes/pkg/auth/authorizer/union"
|
||||
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/webhook"
|
||||
)
|
||||
|
||||
// Attributes implements authorizer.Attributes interface.
|
||||
@ -60,15 +61,28 @@ const (
|
||||
ModeAlwaysAllow string = "AlwaysAllow"
|
||||
ModeAlwaysDeny string = "AlwaysDeny"
|
||||
ModeABAC string = "ABAC"
|
||||
ModeWebhook string = "Webhook"
|
||||
)
|
||||
|
||||
// Keep this list in sync with constant list above.
|
||||
var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC}
|
||||
var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC, ModeWebhook}
|
||||
|
||||
type AuthorizationConfig struct {
|
||||
// Options for ModeABAC
|
||||
|
||||
// Path to a ABAC policy file.
|
||||
PolicyFile string
|
||||
|
||||
// Options for ModeWebhook
|
||||
|
||||
// Kubeconfig file for Webhook authorization plugin.
|
||||
WebhookConfigFile string
|
||||
}
|
||||
|
||||
// NewAuthorizerFromAuthorizationConfig returns the right sort of union of multiple authorizer.Authorizer objects
|
||||
// based on the authorizationMode or an error. authorizationMode should be a comma separated values
|
||||
// of AuthorizationModeChoices.
|
||||
func NewAuthorizerFromAuthorizationConfig(authorizationModes []string, authorizationPolicyFile string) (authorizer.Authorizer, error) {
|
||||
func NewAuthorizerFromAuthorizationConfig(authorizationModes []string, config AuthorizationConfig) (authorizer.Authorizer, error) {
|
||||
|
||||
if len(authorizationModes) == 0 {
|
||||
return nil, errors.New("Atleast one authorization mode should be passed")
|
||||
@ -88,23 +102,35 @@ func NewAuthorizerFromAuthorizationConfig(authorizationModes []string, authoriza
|
||||
case ModeAlwaysDeny:
|
||||
authorizers = append(authorizers, NewAlwaysDenyAuthorizer())
|
||||
case ModeABAC:
|
||||
if authorizationPolicyFile == "" {
|
||||
if config.PolicyFile == "" {
|
||||
return nil, errors.New("ABAC's authorization policy file not passed")
|
||||
}
|
||||
abacAuthorizer, err := abac.NewFromFile(authorizationPolicyFile)
|
||||
abacAuthorizer, err := abac.NewFromFile(config.PolicyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorizers = append(authorizers, abacAuthorizer)
|
||||
case ModeWebhook:
|
||||
if config.WebhookConfigFile == "" {
|
||||
return nil, errors.New("Webhook's configuration file not passed")
|
||||
}
|
||||
webhookAuthorizer, err := webhook.New(config.WebhookConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorizers = append(authorizers, webhookAuthorizer)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown authorization mode %s specified", authorizationMode)
|
||||
}
|
||||
authorizerMap[authorizationMode] = true
|
||||
}
|
||||
|
||||
if !authorizerMap[ModeABAC] && authorizationPolicyFile != "" {
|
||||
if !authorizerMap[ModeABAC] && config.PolicyFile != "" {
|
||||
return nil, errors.New("Cannot specify --authorization-policy-file without mode ABAC")
|
||||
}
|
||||
if !authorizerMap[ModeWebhook] && config.WebhookConfigFile != "" {
|
||||
return nil, errors.New("Cannot specify --authorization-webhook-config-file without mode Webhook")
|
||||
}
|
||||
|
||||
return union.New(authorizers...), nil
|
||||
}
|
||||
|
@ -41,31 +41,75 @@ func TestNewAlwaysDenyAuthorizer(t *testing.T) {
|
||||
// NewAuthorizerFromAuthorizationConfig has multiple return possibilities. This test
|
||||
// validates that errors are returned only when proper.
|
||||
func TestNewAuthorizerFromAuthorizationConfig(t *testing.T) {
|
||||
// Unknown modes should return errors
|
||||
if _, err := NewAuthorizerFromAuthorizationConfig([]string{"DoesNotExist"}, ""); err == nil {
|
||||
t.Errorf("NewAuthorizerFromAuthorizationConfig using a fake mode should have returned an error")
|
||||
|
||||
examplePolicyFile := "../auth/authorizer/abac/example_policy_file.jsonl"
|
||||
|
||||
tests := []struct {
|
||||
modes []string
|
||||
config AuthorizationConfig
|
||||
wantErr bool
|
||||
msg string
|
||||
}{
|
||||
{
|
||||
// Unknown modes should return errors
|
||||
modes: []string{"DoesNotExist"},
|
||||
wantErr: true,
|
||||
msg: "using a fake mode should have returned an error",
|
||||
},
|
||||
{
|
||||
// ModeAlwaysAllow and ModeAlwaysDeny should return without authorizationPolicyFile
|
||||
// but error if one is given
|
||||
modes: []string{ModeAlwaysAllow, ModeAlwaysDeny},
|
||||
msg: "returned an error for valid config",
|
||||
},
|
||||
{
|
||||
// ModeABAC requires a policy file
|
||||
modes: []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC},
|
||||
wantErr: true,
|
||||
msg: "specifying ABAC with no policy file should return an error",
|
||||
},
|
||||
{
|
||||
// ModeABAC should not error if a valid policy path is provided
|
||||
modes: []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC},
|
||||
config: AuthorizationConfig{PolicyFile: examplePolicyFile},
|
||||
msg: "errored while using a valid policy file",
|
||||
},
|
||||
{
|
||||
|
||||
// Authorization Policy file cannot be used without ModeABAC
|
||||
modes: []string{ModeAlwaysAllow, ModeAlwaysDeny},
|
||||
config: AuthorizationConfig{PolicyFile: examplePolicyFile},
|
||||
wantErr: true,
|
||||
msg: "should have errored when Authorization Policy File is used without ModeABAC",
|
||||
},
|
||||
{
|
||||
// Atleast one authorizationMode is necessary
|
||||
modes: []string{},
|
||||
config: AuthorizationConfig{PolicyFile: examplePolicyFile},
|
||||
wantErr: true,
|
||||
msg: "should have errored when no authorization modes are passed",
|
||||
},
|
||||
{
|
||||
// ModeWebhook requires at minimum a target.
|
||||
modes: []string{ModeWebhook},
|
||||
wantErr: true,
|
||||
msg: "should have errored when config was empty with ModeWebhook",
|
||||
},
|
||||
{
|
||||
// Cannot provide webhook flags without ModeWebhook
|
||||
modes: []string{ModeAlwaysAllow},
|
||||
config: AuthorizationConfig{WebhookConfigFile: "authz_webhook_config.yml"},
|
||||
wantErr: true,
|
||||
msg: "should have errored when Webhook config file is used without ModeWebhook",
|
||||
},
|
||||
}
|
||||
|
||||
// ModeAlwaysAllow and ModeAlwaysDeny should return without authorizationPolicyFile
|
||||
// but error if one is given
|
||||
if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny}, ""); err != nil {
|
||||
t.Errorf("NewAuthorizerFromAuthorizationConfig returned an error: %s", err)
|
||||
}
|
||||
|
||||
// ModeABAC requires a policy file
|
||||
if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC}, ""); err == nil {
|
||||
t.Errorf("NewAuthorizerFromAuthorizationConfig using a fake mode should have returned an error")
|
||||
}
|
||||
// ModeABAC should not error if a valid policy path is provided
|
||||
if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC}, "../auth/authorizer/abac/example_policy_file.jsonl"); err != nil {
|
||||
t.Errorf("NewAuthorizerFromAuthorizationConfig errored while using a valid policy file: %s", err)
|
||||
}
|
||||
// Authorization Policy file cannot be used without ModeABAC
|
||||
if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny}, "../auth/authorizer/abac/example_policy_file.jsonl"); err == nil {
|
||||
t.Errorf("NewAuthorizerFromAuthorizationConfig should have errored when Authorization Policy File is used without ModeABAC")
|
||||
}
|
||||
// Atleast one authorizationMode is necessary
|
||||
if _, err := NewAuthorizerFromAuthorizationConfig([]string{}, "../auth/authorizer/abac/example_policy_file.jsonl"); err == nil {
|
||||
t.Errorf("NewAuthorizerFromAuthorizationConfig should have errored when no authorization modes are passed")
|
||||
for _, tt := range tests {
|
||||
_, err := NewAuthorizerFromAuthorizationConfig(tt.modes, tt.config)
|
||||
if tt.wantErr && (err == nil) {
|
||||
t.Errorf("NewAuthorizerFromAuthorizationConfig %s", tt.msg)
|
||||
} else if !tt.wantErr && (err != nil) {
|
||||
t.Errorf("NewAuthorizerFromAuthorizationConfig %s: %v", tt.msg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
plugin/pkg/auth/authorizer/doc.go
Normal file
18
plugin/pkg/auth/authorizer/doc.go
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 authorizer contains implementations for pkg/auth/authorizer interfaces
|
||||
package authorizer
|
211
plugin/pkg/auth/authorizer/webhook/certs_test.go
Normal file
211
plugin/pkg/auth/authorizer/webhook/certs_test.go
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// This file was generated using openssl by the gencerts.sh script
|
||||
// and holds raw certificates for the webhook tests.
|
||||
|
||||
package webhook
|
||||
|
||||
var caKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA6IVXGPX5yP2Q6TAlQXIQsavzSqZ973iZvpQBGTI6M98gTSVm
|
||||
eBYE3o7S8e6WTI3DCnWwqc8Md1rT92FtaQLwv+uMNXijLio5RVBqjUEbunD5In/+
|
||||
T/y5sE9P3CzcWy6CEhIvORAZj6UlvgZzbRwI91+EVFR5jd8JU0e/L9Ds1jLZFyQw
|
||||
Kc1ADo+Tj9O4l0WtpRlrhzTgoor4C3fAQZm0mq+llTnxCmw+lhy8t88bPG1cMwdd
|
||||
DtUTbpetc++2JZ62Q3F1nqcX1EcHDidR0x3j+3357BLkXRK4MQsWLYLzeZ3X1ghW
|
||||
XT062H866PcIV+MX4H58spMN5cVYk5YTneGihQIDAQABAoIBAHU7FQieq4ssXK1U
|
||||
+tOeQNBzUzxl6MSd11YApPUhH7sbWdvLaXhOEbJr6+rSUbDTIGzbnXBf1XcvsgLd
|
||||
eh4hv2PjzFMBObSC0VEjFDWXh/VeFB3SzlNhpfVAZ5EohQjrz+RwiqKIfXqw1vCR
|
||||
rAxswBCIdd1WodpngvocCEaBXYc4MblaPhJDVtxQe8ndEakkSDlX9Z3qIaIGyXRa
|
||||
NvY/yURVuXhwDDd7C2QBT6CXGWhldAg7xrRVTcIoqAUfZCgfis0H8cQOa1cGNsbW
|
||||
t/oHm1fYTxMKFPhWQG0oimx+XJ07BeGgraDRLnxxNnGWTg/W33bc0ZCxCVT0Q5p9
|
||||
kMMfQUECgYEA9cewTK4ZRKC4bTdwqLTh3cyMkbyN4kBHmB1mS2FV/T0l4oZThM//
|
||||
OZ6KFnRCuvfuJIOa70s2bqUYky8NTQAidnnbTW2nZ/E5JdeIBs1fAfadAqiPdmkf
|
||||
MhvjBF/XfLnbCuXx3jA7GmNCpunJysuLtQzwlQlZLojN231uS+3LFbkCgYEA8jCC
|
||||
MgKYaDWssQbT7zfk5MxyZIH3F9N8K2RBIDSVuMo/E1LCIJ06/k+4jdv8nAWYJXcN
|
||||
eyLG7l0SXqrpMBSc9+ZTJgmbo0Mw+npvJHbJvAtD/XOSPjlIqkzPAUrxuiBYxa5S
|
||||
IfKZibygXKAbQMEwY7I4sTbBtIyiQmo9csxt2S0CgYEAiBi1VSCquUfOGBw09BaF
|
||||
Y85aoHCqmHhDrMXK2T7i4MG1csQzBz4t8/gIOvrR4LpdUjbV2l/pmkctXoMVeGf0
|
||||
rWo4t51ar8HxhTTeC/Y4/9tRgiFYn5cCQTsT8F4p8tTvqA9AaWqHr8r7I3Yd2X/w
|
||||
sqahqcVtbskuRLYmF0FrzXECgYAeiR0xPwCGSxYt78Vy6OI0Ms7Ne1FzMJf8RJSt
|
||||
gdPKy70uK4YMZKaWf+iuAimUZmQrfRo3B0h7r0JsqzHhfQfZfbHIHvf/mq4nNp6i
|
||||
w1NmISl+YD71F3Xg+vQynodhx0hKDFOQsizHn/+8DffBr1nxh/v75AKCSCUBKLH8
|
||||
sme7NQKBgDHQac2TmDSelE2uXTGxEVDQs/EpdJh7oCTLQ99Xud/DsaCOrt2s7aRX
|
||||
1FEohsCaUnqwS07/iH2o6Qb/qOteufB9I7FG85nAvqmP5dI4crGNNa8Rl6fXJaR8
|
||||
TUwpZmylTKEJ9zLt2PADglyDrQ2D+1WNzh966Oo9c+kZt4WJM0aF
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var caCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDCzCCAfOgAwIBAgIJAKK9m2Cfg5uhMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAMMEHdlYmhvb2tfYXV0aHpfY2EwIBcNMTYwMjE2MjM0NDI4WhgPMjI4OTEyMDEy
|
||||
MzQ0MjhaMBsxGTAXBgNVBAMMEHdlYmhvb2tfYXV0aHpfY2EwggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQDohVcY9fnI/ZDpMCVBchCxq/NKpn3veJm+lAEZ
|
||||
Mjoz3yBNJWZ4FgTejtLx7pZMjcMKdbCpzwx3WtP3YW1pAvC/64w1eKMuKjlFUGqN
|
||||
QRu6cPkif/5P/LmwT0/cLNxbLoISEi85EBmPpSW+BnNtHAj3X4RUVHmN3wlTR78v
|
||||
0OzWMtkXJDApzUAOj5OP07iXRa2lGWuHNOCiivgLd8BBmbSar6WVOfEKbD6WHLy3
|
||||
zxs8bVwzB10O1RNul61z77YlnrZDcXWepxfURwcOJ1HTHeP7ffnsEuRdErgxCxYt
|
||||
gvN5ndfWCFZdPTrYfzro9whX4xfgfnyykw3lxViTlhOd4aKFAgMBAAGjUDBOMB0G
|
||||
A1UdDgQWBBSumZL6MMwmFGyhQAwl/v0lYDzdZjAfBgNVHSMEGDAWgBSumZL6MMwm
|
||||
FGyhQAwl/v0lYDzdZjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAG
|
||||
6k+bZxKYq4PVZHWTKA7RSjv95FMMr4RSFwKn/n8TUD44ANWYqDrEfVmxAMn3NVK9
|
||||
ckA8mIRym4IGiWD9eBGgPNNtbAq8Wl/9+5qbDMerpXuRnG3wNY7RU75Rl008m52r
|
||||
c2i86ZPUi2fAJZyMf5StWE21oKiDYYQqlB6xxsIj6OHhf7536vEysoztNX5FpS2n
|
||||
q8wG0EhJVhG+Qyww8IlZA5Cjoh71Eqkcwb4cuLjPypxmLm0ywZ/6KgzV+IF+CT2v
|
||||
TJIpMokDUKlRi9cWSqkWXFE6xbCmhrrwKYsi0X6Vvi7a0pmOnSzKCQl8jN8u4A9R
|
||||
xar2YeJ6mCCzSAPM69DP
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAon7dRV4Br10dLcf8zgs/hOHouELveFr8tuWVIFivxSdnac2k
|
||||
6dM4iQ2uYS9nTXxNhyJJ/TX/MHEYc4gSXoqUbtx9jE3VA4mCKDhO7cJtCYxq0QV/
|
||||
PlQCiAPjn5nUMt9ACdii7/uTFDl46bK9K6ajvKHfHoWeYaJsF54kxBq5IMj+QaB2
|
||||
nc+pba00bGG09sYcHyD37QH+ugx64x+21xMYj2LB/uPoqZM0kj1GHPxAs8GqFq2P
|
||||
gwkv589AlHqt2iMCTAqED2jcg4FeS2r1DeYHwGyGAPfWTdA8RZ+gZ/P0Gj91T+4B
|
||||
9srR7BybUFjf1KxEcvPXBvP5r8OwOiYjS8hx/wIDAQABAoIBAQCVBQ9bfDjDX/tQ
|
||||
buVS+FHKRXss8IW4tIiqGqXGQk7/2YEnMKaaoVBpsBhJnDV6hBJ9aV69TnW3MSCh
|
||||
YxqlhSVW/fJNZ1uAoOyygeEwfmuMpC+ZfRcSS+z+W8K2LVbDSKXr4babqvVZSNOw
|
||||
TnDZxTrH1RNPZG65T0Ed77P7/B3nB7aeB2UMuHMQNZ3KrYDTck2R2uTGp+29TplN
|
||||
blS4VAg2/9KqFr7jkS3/C4jjxVd7d9mm0VdAvLcvENVXqSTYV8xDp+VLTnmtXi5f
|
||||
LXcopS+zKtKqT7MM7RA2sKrmSfrQBIXW2E1kfDFtpZHajhDutdYkSTH665W1G23M
|
||||
dIgy3ajhAoGBANE4AhMUVfQqXUCU0UjUDxiOy/8XcKiW/dKhRR1DOQY24J/k+UWv
|
||||
PEGVcBW4tgalYkTl/AW6hsNfubZaJuw05cHIKdL3df6ug7BUiJpmIv3sjrvPRYvA
|
||||
WY1UTb3EJrswGz8S2l5+2S3WFTCfK7S6N6Stfi1x6rMJBuOss7HGqdh3AoGBAMbU
|
||||
WavRqGRsvJFfE5bahXbFpkGWT++BTMP+lzK31z24JjmJdwO+ABWU4/xaXayA4skH
|
||||
PrzlYUcGJWIedb6W4dvz0sA59yflQzYmREkQPE+wbyor003y7mB8LpFiCnfaFhRn
|
||||
hoowkyIY+xM4UeDXWWt3DhBElgfA8fYZdiNJEhy5AoGBAMwYUw3BvMffu/CQPElL
|
||||
dR6DzsUeXKxZ/2pGIGIXfb1uM1pHyFQOSj3ARgMqmYeKNn73zA7akzRsYYJeF7I9
|
||||
OBT96q7+8IBuRdDx5gCYunHzHppf7HwUPEf+gYgpnY7lsu6ouZWNMNfiC/HOlJhN
|
||||
QJLJHFnA0y+sEqhvhSxbnLypAoGBALHCZ+kVKFegX3YYaosUEv589obsu8qE7vzL
|
||||
QKI3elfTq1kFbUILPEgPNUUIBXeUQy03LP/0k2PMOt/eG6apfoQHGQSCzlT8w3pF
|
||||
/AbWXRVhyAEL7X5jEntwirGv1WwRrmvPopkplGGHs/EbCRjbbzaE2i3xI7EK70f2
|
||||
u4gQbAEBAoGAVR4u8g5Tx2Gunzh7tfJJ5e3xGBGS3Yq+JqUVNI6t6KIAPh0rM+aD
|
||||
9tDgcwn8Vn5YU7YkqA2T8OOFsbJfrfZ7y7+oeMFukuIyxgmy9n/V/tCIrV/lR7A5
|
||||
3iYhanTUbQswx19pSRgsXi7fo9Fi/dmUwyHi18uz5FdLyCTsMbf3uA8=
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var badCACert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDCzCCAfOgAwIBAgIJAPqJyUfmRxGLMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAMMEHdlYmhvb2tfYXV0aHpfY2EwIBcNMTYwMjE2MjM0NDI4WhgPMjI4OTEyMDEy
|
||||
MzQ0MjhaMBsxGTAXBgNVBAMMEHdlYmhvb2tfYXV0aHpfY2EwggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQCift1FXgGvXR0tx/zOCz+E4ei4Qu94Wvy25ZUg
|
||||
WK/FJ2dpzaTp0ziJDa5hL2dNfE2HIkn9Nf8wcRhziBJeipRu3H2MTdUDiYIoOE7t
|
||||
wm0JjGrRBX8+VAKIA+OfmdQy30AJ2KLv+5MUOXjpsr0rpqO8od8ehZ5homwXniTE
|
||||
GrkgyP5BoHadz6ltrTRsYbT2xhwfIPftAf66DHrjH7bXExiPYsH+4+ipkzSSPUYc
|
||||
/ECzwaoWrY+DCS/nz0CUeq3aIwJMCoQPaNyDgV5LavUN5gfAbIYA99ZN0DxFn6Bn
|
||||
8/QaP3VP7gH2ytHsHJtQWN/UrERy89cG8/mvw7A6JiNLyHH/AgMBAAGjUDBOMB0G
|
||||
A1UdDgQWBBS6IGeGHZCylibt0GzY0dP6C0J9VjAfBgNVHSMEGDAWgBS6IGeGHZCy
|
||||
libt0GzY0dP6C0J9VjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAi
|
||||
A1dp75kbePFZsUNjxN6B/Pv0vSoaOjQkc4hpxKbI4VRCuPGmMRFYTlKCzoZ53OqQ
|
||||
2Jmu1Zbzel/bV5vXrW0BOfUpfWYzd/usIJEuTgU8ijBIB+IHAXYwwxeKRcz3C+7+
|
||||
9RBMF7gSg9pU2hrSvjhh7Q96IMJ42Z7tI3WD8SZaQLjY1NW1jrQVsg66ktdMke7x
|
||||
zC8oIRIBH4W6l5s7jtZx1k305NE04pigcFLxCxOmicKd66ysI5hAZkD7y0dgwgtL
|
||||
IqCQy6t7uJDydRiNRfPFr9Eg7uOu83JGw11f3bGVhJVCbzHyKddvkQsQbdaMHRgZ
|
||||
zgmWLORg+ls1H1oaJiNW
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAtegsP499au5ZxlwM26rk3TnRgakchQi/9bhfMr0LaEKng1lR
|
||||
XopzzGuGeZQswzbx7iiH89JzFkurZoEmZwtS4Aybit92VOSv0EUnyx7WR3V21ObZ
|
||||
iQO0rr0UmG84NjdzATkqF+R5Z+HN9shwgBI4PR1j/ybCt7jNz+OM/VmqsgzoKLoa
|
||||
bGrx7LCTPk8y5G8AoPOrIAP+9WHJsKQSRT8Lru4lYqseBxvhjqo8NRqzZLg79ldY
|
||||
aKFqa2N2zr5qp94sG3/zihNDxjZvyyn9c8qvPBL0xOyayvOJG8eZUmjQpUMv7Jk3
|
||||
qFmdMgGaDJRw0Qg6+/Zt6MHNs6Rbb8hmwuMSpwIDAQABAoIBAQCjzeFijwzKKL4w
|
||||
0B1IBhi3WeReFPG4nkt1ssQPBYrrJPKBZgHO13A1STI78wFn/OdYpajfF8hI8HT1
|
||||
BiGVsu27Eb9TC60b/x6OtmeCEk+044LRbtu+9NZUb7HHHogI0l++X0KXZ0coE38L
|
||||
1izwNvfrmLa+QaIgHMtAg9EnJwJ993n4L31GovWh8MGmVyJX/F92y+agNwWkNYYp
|
||||
iLWFyon+HbNVL13WOOYnYEdA8Me3+Gucy1EOfWMF7mgmuO2vcfnxXd6b16VjAwtE
|
||||
jGCQfzgpWGHLpgwoBgDmnPUbdNPUT3MbA9jqG2mlnBSBQveYgKrmFdDYnAjnCM4L
|
||||
uF2ztBzhAoGBAOYc3sF3YjpIIMsyH9omqtfOuxO+oZkpb2vB9kgdXCDcG870M+BC
|
||||
bNzV7DCSV8QAUqjKQK1r3gq62UZMLXZbG8x5UnM8/EK0X1CSqygwSWjGpYxIQEhh
|
||||
O2lq69WipkNDnX1ZmrvEdHD2cxqkkXZ7bdRKRasrFJgvJa3XbiJ18KYxAoGBAMpe
|
||||
/72EcX9oL3KT8tJSpvasrw17p/XkMMCxTp3IDb3krF/4k5bYF61F68/LNSy3xkos
|
||||
ZrPUK/U160iuHSYCpMq4pPmlWgKq4hmUMOt+8Yy622zDlugarq9VLqvSdGHm+r6F
|
||||
5fHilXB0UsTXXOuLZWLcSQ0MBgiaVCLb2AmXZhhXAoGAEjSchw/r7JKCTbE0hezj
|
||||
PVm0wVYmsNhvYUYiNwhjnpHrfU8iv45h0IL4QcuCOBaSc5o0zcOn+I9Z207xldiV
|
||||
dXLvzAA6MQjWNai08+QGGs0EkfmxZEiVC70S1X8dylqSHjW1oT9kuv80khoNDCOt
|
||||
x8rsgiNRaMzqHTvbEczk8jECgYB2Od+wSULBSw2FI5fVdcHjFGlEODycs44j1LH4
|
||||
DZqxmHl3q9IVavMSIGouQCo1kLuAM8ZgQpDXtYNaN5YB0cOSRyLiUc5vBoQGq4OU
|
||||
4Nme/L8aIH315TiuZ9ZXPSEO3REZ40G9+UCSrPJ52tOHLC2z/ruSqraPqhGDN+pT
|
||||
WCamCwKBgEPa+kVrPs0khQH8+sbFbU9ifj4fhPAiSwj2fKuXFro2mE205vAMHye/
|
||||
SYs/mPzYzKSd7F+7Zk6oVrgFVskTiReW3phF+cIl+CdcnIenF0jW1PVgGw8znu+P
|
||||
SbHSdqV+tB7AW2J7sH8TZtfMUPAK2MJ4S+1uaHK86K79ym4Rz0E2
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIC/zCCAeegAwIBAgIJAN7rkfhaX8FZMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAMMEHdlYmhvb2tfYXV0aHpfY2EwIBcNMTYwMjE2MjM0NDI4WhgPMjI4OTEyMDEy
|
||||
MzQ0MjhaMB8xHTAbBgNVBAMMFHdlYmhvb2tfYXV0aHpfc2VydmVyMIIBIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtegsP499au5ZxlwM26rk3TnRgakchQi/
|
||||
9bhfMr0LaEKng1lRXopzzGuGeZQswzbx7iiH89JzFkurZoEmZwtS4Aybit92VOSv
|
||||
0EUnyx7WR3V21ObZiQO0rr0UmG84NjdzATkqF+R5Z+HN9shwgBI4PR1j/ybCt7jN
|
||||
z+OM/VmqsgzoKLoabGrx7LCTPk8y5G8AoPOrIAP+9WHJsKQSRT8Lru4lYqseBxvh
|
||||
jqo8NRqzZLg79ldYaKFqa2N2zr5qp94sG3/zihNDxjZvyyn9c8qvPBL0xOyayvOJ
|
||||
G8eZUmjQpUMv7Jk3qFmdMgGaDJRw0Qg6+/Zt6MHNs6Rbb8hmwuMSpwIDAQABo0Aw
|
||||
PjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAKBggrBgEFBQcDATAP
|
||||
BgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCZHB9UCl2CfylWP3db
|
||||
xUamawnRoTYlsOcUh4f2tlHMY+vYiEStN+LECk62YpeaHl/nz/lk7g1Jx9aua39z
|
||||
wFIHiXYhwSWOtgmzpbxYLye1yajKXbbA1T7mEZJTjewDB9i1LcB9W3EV5VJ8Y1GY
|
||||
AYKuKQ4Cb1HrqLsrw/1PDm0VouWzf2ESv8CBvAv/pYLVfwgS6WsUqn9wycpLEnqQ
|
||||
RK66/AoiOaxUIjEP0O1q6pi6Mag7XAfeNtx8J0VGt4cRG4rvWCbKVUyvKfUCkipN
|
||||
gJu09S+KIz3x1CJLRuJX9tB+cFnnykDLQ2IKg7x44O83ikNk8+Di3iT/awCguWPE
|
||||
rHh5
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEA5ij4WXWGvbmfAYhEafKRvLEHSkUCYIDjwQAlnHoLf/lz+Fh2
|
||||
DEv4lcBaycwk3+LVUGKgYOg91txYJvGD3HcmVThXZvcgJd4V9Ll3aY/6xVRCenWi
|
||||
UNgVQVQITGkMn09ZkSXbZCK4wqz9oTVh0Ti5a7apOS2V07yL0q7vw003v5TBqzC/
|
||||
FgRwE0bv1rKYYQ80WbDlYkkYGf216zQTwS4g/nShCZAX9eqSfbBg6B/A3OwpbIfx
|
||||
09BWuwWhp5QnS4w002gGWavRFNzu8pUHUv6zMN8OKpasv+Na+ZB+gMt4+e2Y7qNz
|
||||
76QL23eGwc6oWn8lQBtkDLmLIa6jbWX067U76QIDAQABAoIBAQCJpGzJSzC2W8DM
|
||||
sMqBNdCUMKZ0cwq13b7W2BimGJKyCOOi3HxUZEaYf/2Leyt+PPBm72SML7dzvDh3
|
||||
qa269gKVqmkSqa2vF763qQbRuYo14msTQzA7+s3TUMbZs2UaDOE6nZIzs1QdEElp
|
||||
1DvYXHz+/rD7Adj9VF+mMnouqQoy5kgJTnVZ8sOyl/9R6F67xKBIvcrtPfqVZzuG
|
||||
2hGAMUnawxFUajQC7BynIeCWrk79SUmQgilyNgRdY6+rGh2uRupIxuiAukPtuag1
|
||||
Li+wnNl1UGECtv9ZnnboKvg2334k5vhYScGRJbwbr7Zt3ZaNd0Z/DE9kTtnhBS7v
|
||||
9qWdc7CBAoGBAPR4hz1fhHFiPmMEAGuiNms6WdyIfyonIRYas8ZDKUQGdxn/aO8a
|
||||
CURktHRlm6iYT+j1cbf3RnLEN9pNr3V2EySOMc+rXUNifcP7Vl53akAQmISUfQWG
|
||||
UfwaNLicbavf6m9UCiwWByAZghqDZSLiwmLHIjGcSJQiFuhZryioDydxAoGBAPED
|
||||
q1Z7oNhzwRYie9OB5ylnrCH8G3yFl8egBmQrPJKIQHA9mAGg01LEJwQNoWewyAWx
|
||||
jfeFtWvIgZkj49cluZgHYyF81jApaNraxtXAgIwC1n7oAIttmeklZ/V1HntknG3Y
|
||||
ow2bV/NA3aPOTPYxW8oDv7U9lvwve7kIFxeWjE/5AoGASfXI3G1wUSkqvKPySJ3b
|
||||
ntcZZpm49xS9csWDS+D3tAfMsoXNxkB3O0TIP0qaLAhgbJcM314k5wWr7BSCl6Ow
|
||||
KOgH887hOUirycXZHF0+PMGIktulcy1u0jlPZ+aTW2MztpiTN0E2yKRO8xx7VXGK
|
||||
431hP+cLIh2qFoNDdaZaZ1ECgYEArw++PWQxMefqgVxs2vXJZY7TPiA0Ct+ynqKC
|
||||
4fFx3vGu9JgYuF4MAVtPB6eq7HlA4LnWZ8ssOuz6DbU/AoB5bY84FxPpNDRv4D/3
|
||||
Gz3nYUuSZ72234+tsuaju2vlxzUOVs97qB+E48Di/N+VkWHKzVKpxkjFScpnsL/K
|
||||
niyRIGkCgYEAriuxbOCczL/j6u2Xq1ngEsGg+RXjtOYGoJWo7B8qlVL4nF8w1Nbd
|
||||
FxEmOChQgUnBdwb93qHCSq0Fidf7OfewrfJJkstWIh3zPS4umLZo7R3YblncpdfT
|
||||
M197uckIWccZml2jF/c7nvK+MjwDRhkOl2a6HzMxcdBwYUJmSwmIZ4k=
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var clientCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIC7jCCAdagAwIBAgIJAN7rkfhaX8FaMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAMMEHdlYmhvb2tfYXV0aHpfY2EwIBcNMTYwMjE2MjM0NDI4WhgPMjI4OTEyMDEy
|
||||
MzQ0MjhaMB8xHTAbBgNVBAMMFHdlYmhvb2tfYXV0aHpfY2xpZW50MIIBIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5ij4WXWGvbmfAYhEafKRvLEHSkUCYIDj
|
||||
wQAlnHoLf/lz+Fh2DEv4lcBaycwk3+LVUGKgYOg91txYJvGD3HcmVThXZvcgJd4V
|
||||
9Ll3aY/6xVRCenWiUNgVQVQITGkMn09ZkSXbZCK4wqz9oTVh0Ti5a7apOS2V07yL
|
||||
0q7vw003v5TBqzC/FgRwE0bv1rKYYQ80WbDlYkkYGf216zQTwS4g/nShCZAX9eqS
|
||||
fbBg6B/A3OwpbIfx09BWuwWhp5QnS4w002gGWavRFNzu8pUHUv6zMN8OKpasv+Na
|
||||
+ZB+gMt4+e2Y7qNz76QL23eGwc6oWn8lQBtkDLmLIa6jbWX067U76QIDAQABoy8w
|
||||
LTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAKBggrBgEFBQcDAjAN
|
||||
BgkqhkiG9w0BAQsFAAOCAQEA2IZNhkVrSTAIeP2N2WzOHqbFbGyO+NA8G9Hb5fiX
|
||||
e1YS2Ku3ERYNr+HLxNHCsXiSUKjjBmXMc4z0XaHJznEKEbotZftjTlTQlHi3/5vm
|
||||
dIG18pmO/E5ebVXl6pU96v/hBd8N5rWp9WUKgP0y59r/JA+oNpmd10A+RyaOyrFK
|
||||
rBm8Z8rvDYMrXSpOwx9BNDuhqzbdG8MYw5vO55Er3hwTXoapsMqSh5s9+OFFpUJi
|
||||
2uEoQlwWiYRtQj6g4wgr4woDEbv8XxsHqGfs+GSnmRsB69xRI24lEtC+nS6Rz3Sh
|
||||
YWeN0gD8PsQC1KJVv6xCGo1yXSEwytRMB23XYtAZahLdLg==
|
||||
-----END CERTIFICATE-----`)
|
102
plugin/pkg/auth/authorizer/webhook/gencerts.sh
Executable file
102
plugin/pkg/auth/authorizer/webhook/gencerts.sh
Executable file
@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
|
||||
# gencerts.sh generates the certificates for the webhook authz plugin tests.
|
||||
#
|
||||
# It is not expected to be run often (there is no go generate rule), and mainly
|
||||
# exists for documentation purposes.
|
||||
|
||||
cat > server.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
cat > client.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth
|
||||
EOF
|
||||
|
||||
# Create a certificate authority
|
||||
openssl genrsa -out caKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=webhook_authz_ca"
|
||||
|
||||
# Create a second certificate authority
|
||||
openssl genrsa -out badCAKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=webhook_authz_ca"
|
||||
|
||||
# Create a server certiticate
|
||||
openssl genrsa -out serverKey.pem 2048
|
||||
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=webhook_authz_server" -config server.conf
|
||||
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
|
||||
|
||||
# Create a client certiticate
|
||||
openssl genrsa -out clientKey.pem 2048
|
||||
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=webhook_authz_client" -config client.conf
|
||||
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
|
||||
|
||||
outfile=certs_test.go
|
||||
|
||||
cat > $outfile << EOF
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
EOF
|
||||
|
||||
echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile
|
||||
echo "// and holds raw certificates for the webhook tests." >> $outfile
|
||||
echo "" >> $outfile
|
||||
echo "package webhook" >> $outfile
|
||||
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
|
||||
data=$(cat ${file}.pem)
|
||||
echo "" >> $outfile
|
||||
echo "var $file = []byte(\`$data\`)" >> $outfile
|
||||
done
|
||||
|
||||
# Clean up after we're done.
|
||||
rm *.pem
|
||||
rm *.csr
|
||||
rm *.srl
|
||||
rm *.conf
|
180
plugin/pkg/auth/authorizer/webhook/webhook.go
Normal file
180
plugin/pkg/auth/authorizer/webhook/webhook.go
Normal file
@ -0,0 +1,180 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||
"k8s.io/kubernetes/pkg/apimachinery/registered"
|
||||
"k8s.io/kubernetes/pkg/apis/authorization/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/auth/authorizer"
|
||||
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
|
||||
"k8s.io/kubernetes/pkg/runtime"
|
||||
"k8s.io/kubernetes/pkg/runtime/serializer/json"
|
||||
"k8s.io/kubernetes/pkg/runtime/serializer/versioning"
|
||||
|
||||
_ "k8s.io/kubernetes/pkg/apis/authorization/install"
|
||||
)
|
||||
|
||||
var (
|
||||
encodeVersions = []unversioned.GroupVersion{v1beta1.SchemeGroupVersion}
|
||||
decodeVersions = []unversioned.GroupVersion{v1beta1.SchemeGroupVersion}
|
||||
|
||||
requireEnabled = []unversioned.GroupVersion{v1beta1.SchemeGroupVersion}
|
||||
)
|
||||
|
||||
// Ensure Webhook implements the authorizer.Authorizer interface.
|
||||
var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil)
|
||||
|
||||
type WebhookAuthorizer struct {
|
||||
restClient *client.RESTClient
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// # clusters refers to the remote service.
|
||||
// clusters:
|
||||
// - name: name-of-remote-authz-service
|
||||
// cluster:
|
||||
// certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
|
||||
// server: https://authz.example.com/authorize # URL of remote service to query. Must use 'https'.
|
||||
//
|
||||
// # users refers to the API server's webhook configuration.
|
||||
// users:
|
||||
// - name: name-of-api-server
|
||||
// user:
|
||||
// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
|
||||
// client-key: /path/to/key.pem # key matching the cert
|
||||
//
|
||||
// For additional HTTP configuration, refer to the kubeconfig documentation
|
||||
// http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html.
|
||||
func New(kubeConfigFile string) (*WebhookAuthorizer, error) {
|
||||
|
||||
for _, groupVersion := range requireEnabled {
|
||||
if !registered.IsEnabledVersion(groupVersion) {
|
||||
return nil, fmt.Errorf("webhook authz plugin requires enabling extension resource: %s", groupVersion)
|
||||
}
|
||||
}
|
||||
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.ExplicitPath = kubeConfigFile
|
||||
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
|
||||
|
||||
clientConfig, err := loader.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serializer := json.NewSerializer(json.DefaultMetaFactory, api.Scheme, runtime.ObjectTyperToTyper(api.Scheme), false)
|
||||
clientConfig.ContentConfig.Codec = versioning.NewCodecForScheme(api.Scheme, serializer, encodeVersions, decodeVersions)
|
||||
|
||||
restClient, err := client.UnversionedRESTClientFor(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(ericchiang): Can we ensure remote service is reachable?
|
||||
|
||||
return &WebhookAuthorizer{restClient}, nil
|
||||
}
|
||||
|
||||
// Authorize makes a REST request to the remote service describing the attempted action as a JSON
|
||||
// serialized api.authorization.v1beta1.SubjectAccessReview object. An example request body is
|
||||
// provided bellow.
|
||||
//
|
||||
// {
|
||||
// "apiVersion": "authorization.k8s.io/v1beta1",
|
||||
// "kind": "SubjectAccessReview",
|
||||
// "spec": {
|
||||
// "resourceAttributes": {
|
||||
// "namespace": "kittensandponies",
|
||||
// "verb": "GET",
|
||||
// "group": "group3",
|
||||
// "resource": "pods"
|
||||
// },
|
||||
// "user": "jane",
|
||||
// "group": [
|
||||
// "group1",
|
||||
// "group2"
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The remote service is expected to fill the SubjectAccessReviewStatus field to either allow or
|
||||
// disallow access. A permissive response would return:
|
||||
//
|
||||
// {
|
||||
// "apiVersion": "authorization.k8s.io/v1beta1",
|
||||
// "kind": "SubjectAccessReview",
|
||||
// "status": {
|
||||
// "allowed": true
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// To disallow access, the remote service would return:
|
||||
//
|
||||
// {
|
||||
// "apiVersion": "authorization.k8s.io/v1beta1",
|
||||
// "kind": "SubjectAccessReview",
|
||||
// "status": {
|
||||
// "allowed": false,
|
||||
// "reason": "user does not have read access to the namespace"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (err error) {
|
||||
r := &v1beta1.SubjectAccessReview{
|
||||
Spec: v1beta1.SubjectAccessReviewSpec{
|
||||
User: attr.GetUserName(),
|
||||
Groups: attr.GetGroups(),
|
||||
},
|
||||
}
|
||||
if attr.IsResourceRequest() {
|
||||
r.Spec.ResourceAttributes = &v1beta1.ResourceAttributes{
|
||||
Namespace: attr.GetNamespace(),
|
||||
Verb: attr.GetVerb(),
|
||||
Group: attr.GetAPIGroup(),
|
||||
Resource: attr.GetResource(),
|
||||
}
|
||||
} else {
|
||||
r.Spec.NonResourceAttributes = &v1beta1.NonResourceAttributes{
|
||||
Path: attr.GetPath(),
|
||||
Verb: attr.GetVerb(),
|
||||
}
|
||||
}
|
||||
result := w.restClient.Post().Body(r).Do()
|
||||
if err := result.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := result.Into(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Status.Allowed {
|
||||
return nil
|
||||
}
|
||||
if r.Status.Reason != "" {
|
||||
return errors.New(r.Status.Reason)
|
||||
}
|
||||
return errors.New("unauthorized")
|
||||
}
|
473
plugin/pkg/auth/authorizer/webhook/webhook_test.go
Normal file
473
plugin/pkg/auth/authorizer/webhook/webhook_test.go
Normal file
@ -0,0 +1,473 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||
"k8s.io/kubernetes/pkg/apis/authorization/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/auth/authorizer"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api/v1"
|
||||
"k8s.io/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
func TestNewFromConfig(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: false,
|
||||
},
|
||||
{
|
||||
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: false,
|
||||
},
|
||||
{
|
||||
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
|
||||
_, err = New(p)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Service mocks a remote service.
|
||||
type Service interface {
|
||||
Review(*v1beta1.SubjectAccessReview)
|
||||
}
|
||||
|
||||
// NewTestServer wraps a Service as an httptest.Server.
|
||||
func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
||||
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) {
|
||||
var review v1beta1.SubjectAccessReview
|
||||
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.Review(&review)
|
||||
type status struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
resp := struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Status status `json:"status"`
|
||||
}{
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
Status: status{review.Status.Allowed, review.Status.Reason},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
|
||||
server.TLS = tlsConfig
|
||||
server.StartTLS()
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// A service that can be set to allow all or deny all authorization requests.
|
||||
type mockService struct {
|
||||
allow bool
|
||||
}
|
||||
|
||||
func (m *mockService) Review(r *v1beta1.SubjectAccessReview) {
|
||||
r.Status.Allowed = m.allow
|
||||
}
|
||||
func (m *mockService) Allow() { m.allow = true }
|
||||
func (m *mockService) Deny() { m.allow = false }
|
||||
|
||||
// newAuthorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
|
||||
// a new WebhookAuthorizer from it.
|
||||
func newAuthorizer(callbackURL string, clientCert, clientKey, ca []byte) (*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
|
||||
}
|
||||
return New(p)
|
||||
}
|
||||
|
||||
func TestTLSConfig(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(mockService)
|
||||
|
||||
server, err := NewTestServer(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 := newAuthorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA)
|
||||
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()
|
||||
err = wh.Authorize(attr)
|
||||
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 err := wh.Authorize(attr); err == nil {
|
||||
t.Errorf("%s: incorrectly authorized with DenyAll policy", tt.test)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// recorderService records all access review requests.
|
||||
type recorderService struct {
|
||||
last v1beta1.SubjectAccessReview
|
||||
err error
|
||||
}
|
||||
|
||||
func (rec *recorderService) Review(r *v1beta1.SubjectAccessReview) {
|
||||
rec.last = v1beta1.SubjectAccessReview{}
|
||||
rec.last = *r
|
||||
r.Status.Allowed = true
|
||||
}
|
||||
|
||||
func (rec *recorderService) Last() (v1beta1.SubjectAccessReview, error) {
|
||||
return rec.last, rec.err
|
||||
}
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
serv := new(recorderService)
|
||||
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wh, err := newAuthorizer(s.URL, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expTypeMeta := unversioned.TypeMeta{
|
||||
APIVersion: "authorization.k8s.io/v1beta1",
|
||||
Kind: "SubjectAccessReview",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
attr authorizer.Attributes
|
||||
want v1beta1.SubjectAccessReview
|
||||
}{
|
||||
{
|
||||
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}},
|
||||
want: v1beta1.SubjectAccessReview{
|
||||
TypeMeta: expTypeMeta,
|
||||
Spec: v1beta1.SubjectAccessReviewSpec{
|
||||
NonResourceAttributes: &v1beta1.NonResourceAttributes{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}},
|
||||
want: v1beta1.SubjectAccessReview{
|
||||
TypeMeta: expTypeMeta,
|
||||
Spec: v1beta1.SubjectAccessReviewSpec{
|
||||
User: "jane",
|
||||
NonResourceAttributes: &v1beta1.NonResourceAttributes{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
attr: authorizer.AttributesRecord{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
UID: "1",
|
||||
Groups: []string{"group1", "group2"},
|
||||
},
|
||||
Verb: "GET",
|
||||
Namespace: "kittensandponies",
|
||||
APIGroup: "group3",
|
||||
Resource: "pods",
|
||||
ResourceRequest: true,
|
||||
Path: "/foo",
|
||||
},
|
||||
want: v1beta1.SubjectAccessReview{
|
||||
TypeMeta: expTypeMeta,
|
||||
Spec: v1beta1.SubjectAccessReviewSpec{
|
||||
User: "jane",
|
||||
Groups: []string{"group1", "group2"},
|
||||
ResourceAttributes: &v1beta1.ResourceAttributes{
|
||||
Verb: "GET",
|
||||
Namespace: "kittensandponies",
|
||||
Group: "group3",
|
||||
Resource: "pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
if err := wh.Authorize(tt.attr); err != nil {
|
||||
t.Errorf("case %d: authorization failed: %v", i, err)
|
||||
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, util.ObjectGoPrintDiff(gotAttr, tt.want))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user