From 0d74915b2b770eb9aff008fb2e3eb11a9b0c1325 Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Thu, 19 Jan 2017 13:04:08 +0100 Subject: [PATCH] genericapiserver: move authz webhook plugins into k8s.io/apiserver --- plugin/pkg/authorizer/webhook/BUILD | 57 ++ plugin/pkg/authorizer/webhook/certs_test.go | 211 ++++++ plugin/pkg/authorizer/webhook/gencerts.sh | 102 +++ plugin/pkg/authorizer/webhook/webhook.go | 229 +++++++ plugin/pkg/authorizer/webhook/webhook_test.go | 620 ++++++++++++++++++ 5 files changed, 1219 insertions(+) create mode 100644 plugin/pkg/authorizer/webhook/BUILD create mode 100644 plugin/pkg/authorizer/webhook/certs_test.go create mode 100755 plugin/pkg/authorizer/webhook/gencerts.sh create mode 100644 plugin/pkg/authorizer/webhook/webhook.go create mode 100644 plugin/pkg/authorizer/webhook/webhook_test.go diff --git a/plugin/pkg/authorizer/webhook/BUILD b/plugin/pkg/authorizer/webhook/BUILD new file mode 100644 index 000000000..12e9ca1aa --- /dev/null +++ b/plugin/pkg/authorizer/webhook/BUILD @@ -0,0 +1,57 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = ["webhook.go"], + tags = ["automanaged"], + deps = [ + "//pkg/apis/authorization/install:go_default_library", + "//vendor:github.com/golang/glog", + "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + "//vendor:k8s.io/apiserver/pkg/authorization/authorizer", + "//vendor:k8s.io/apiserver/pkg/util/cache", + "//vendor:k8s.io/apiserver/pkg/util/webhook", + "//vendor:k8s.io/client-go/kubernetes/typed/authorization/v1beta1", + "//vendor:k8s.io/client-go/pkg/apis/authorization/install", + "//vendor:k8s.io/client-go/pkg/apis/authorization/v1beta1", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "certs_test.go", + "webhook_test.go", + ], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/apis/authorization/v1beta1:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + "//vendor:k8s.io/apimachinery/pkg/util/diff", + "//vendor:k8s.io/apiserver/pkg/authentication/user", + "//vendor:k8s.io/apiserver/pkg/authorization/authorizer", + "//vendor:k8s.io/client-go/tools/clientcmd/api/v1", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/plugin/pkg/authorizer/webhook/certs_test.go b/plugin/pkg/authorizer/webhook/certs_test.go new file mode 100644 index 000000000..816eef6b7 --- /dev/null +++ b/plugin/pkg/authorizer/webhook/certs_test.go @@ -0,0 +1,211 @@ +/* +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. +*/ + +// 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-----`) diff --git a/plugin/pkg/authorizer/webhook/gencerts.sh b/plugin/pkg/authorizer/webhook/gencerts.sh new file mode 100755 index 000000000..8d7896fa5 --- /dev/null +++ b/plugin/pkg/authorizer/webhook/gencerts.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# 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. + +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. + +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 diff --git a/plugin/pkg/authorizer/webhook/webhook.go b/plugin/pkg/authorizer/webhook/webhook.go new file mode 100644 index 000000000..ff591ffc4 --- /dev/null +++ b/plugin/pkg/authorizer/webhook/webhook.go @@ -0,0 +1,229 @@ +/* +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 implements the authorizer.Authorizer interface using HTTP webhooks. +package webhook + +import ( + "encoding/json" + "time" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/util/cache" + authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1" + authorization "k8s.io/client-go/pkg/apis/authorization/v1beta1" + + "k8s.io/apiserver/pkg/util/webhook" + + _ "k8s.io/client-go/pkg/apis/authorization/install" +) + +var ( + groupVersions = []schema.GroupVersion{authorization.SchemeGroupVersion} +) + +const retryBackoff = 500 * time.Millisecond + +// Ensure Webhook implements the authorizer.Authorizer interface. +var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil) + +type WebhookAuthorizer struct { + subjectAccessReview authorizationclient.SubjectAccessReviewInterface + responseCache *cache.LRUExpireCache + authorizedTTL time.Duration + unauthorizedTTL time.Duration + initialBackoff time.Duration +} + +// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client +func NewFromInterface(subjectAccessReview authorizationclient.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) { + return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff) +} + +// 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, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) { + subjectAccessReview, err := subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile) + if err != nil { + return nil, err + } + return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff) +} + +// newWithBackoff allows tests to skip the sleep. +func newWithBackoff(subjectAccessReview authorizationclient.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL, initialBackoff time.Duration) (*WebhookAuthorizer, error) { + return &WebhookAuthorizer{ + subjectAccessReview: subjectAccessReview, + responseCache: cache.NewLRUExpireCache(1024), + authorizedTTL: authorizedTTL, + unauthorizedTTL: unauthorizedTTL, + initialBackoff: initialBackoff, + }, 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) (authorized bool, reason string, err error) { + r := &authorization.SubjectAccessReview{} + if user := attr.GetUser(); user != nil { + r.Spec = authorization.SubjectAccessReviewSpec{ + User: user.GetName(), + Groups: user.GetGroups(), + Extra: convertToSARExtra(user.GetExtra()), + } + } + + if attr.IsResourceRequest() { + r.Spec.ResourceAttributes = &authorization.ResourceAttributes{ + Namespace: attr.GetNamespace(), + Verb: attr.GetVerb(), + Group: attr.GetAPIGroup(), + Version: attr.GetAPIVersion(), + Resource: attr.GetResource(), + Subresource: attr.GetSubresource(), + Name: attr.GetName(), + } + } else { + r.Spec.NonResourceAttributes = &authorization.NonResourceAttributes{ + Path: attr.GetPath(), + Verb: attr.GetVerb(), + } + } + key, err := json.Marshal(r.Spec) + if err != nil { + return false, "", err + } + if entry, ok := w.responseCache.Get(string(key)); ok { + r.Status = entry.(authorization.SubjectAccessReviewStatus) + } else { + var ( + result *authorization.SubjectAccessReview + err error + ) + webhook.WithExponentialBackoff(w.initialBackoff, func() error { + result, err = w.subjectAccessReview.Create(r) + return err + }) + if err != nil { + // An error here indicates bad configuration or an outage. Log for debugging. + glog.Errorf("Failed to make webhook authorizer request: %v", err) + return false, "", err + } + r.Status = result.Status + if r.Status.Allowed { + w.responseCache.Add(string(key), r.Status, w.authorizedTTL) + } else { + w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL) + } + } + return r.Status.Allowed, r.Status.Reason, nil +} + +func convertToSARExtra(extra map[string][]string) map[string]authorization.ExtraValue { + if extra == nil { + return nil + } + ret := map[string]authorization.ExtraValue{} + for k, v := range extra { + ret[k] = authorization.ExtraValue(v) + } + + return ret +} + +// subjectAccessReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file, +// 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. +func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string) (authorizationclient.SubjectAccessReviewInterface, error) { + gw, err := webhook.NewGenericWebhook(kubeConfigFile, groupVersions, 0) + if err != nil { + return nil, err + } + return &subjectAccessReviewClient{gw}, nil +} + +type subjectAccessReviewClient struct { + w *webhook.GenericWebhook +} + +func (t *subjectAccessReviewClient) Create(subjectAccessReview *authorization.SubjectAccessReview) (*authorization.SubjectAccessReview, error) { + result := &authorization.SubjectAccessReview{} + err := t.w.RestClient.Post().Body(subjectAccessReview).Do().Into(result) + return result, err +} diff --git a/plugin/pkg/authorizer/webhook/webhook_test.go b/plugin/pkg/authorizer/webhook/webhook_test.go new file mode 100644 index 000000000..85bca35ce --- /dev/null +++ b/plugin/pkg/authorizer/webhook/webhook_test.go @@ -0,0 +1,620 @@ +/* +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 ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "reflect" + "testing" + "text/template" + "time" + + 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" + "k8s.io/client-go/pkg/apis/authorization/v1beta1" + "k8s.io/client-go/tools/clientcmd/api/v1" +) + +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: 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) + 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) + } + } +} + +// Service mocks a remote service. +type Service interface { + Review(*v1beta1.SubjectAccessReview) + HTTPStatusCode() int +} + +// NewTestServer wraps a Service as an httptest.Server. +func NewTestServer(s Service, 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 v1beta1.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: v1beta1.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 mockService struct { + allow bool + statusCode int + called int +} + +func (m *mockService) Review(r *v1beta1.SubjectAccessReview) { + m.called++ + r.Status.Allowed = m.allow +} +func (m *mockService) Allow() { m.allow = true } +func (m *mockService) Deny() { m.allow = false } +func (m *mockService) HTTPStatusCode() int { return m.statusCode } + +// 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, 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) + if err != nil { + return nil, fmt.Errorf("error building sar client: %v", err) + } + return newWithBackoff(sarClient, cacheTime, cacheTime, 0) +} + +func TestTLSConfig(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(mockService) + service.statusCode = 200 + + 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, 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() + authorized, _, err := wh.Authorize(attr) + if tt.wantAuth { + if !authorized { + t.Errorf("expected successful authorization") + } + } else { + if authorized { + 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 authorized, _, _ := wh.Authorize(attr); authorized { + 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 (rec *recorderService) HTTPStatusCode() int { return 200 } + +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, 0) + if err != nil { + t.Fatal(err) + } + + expTypeMeta := metav1.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", + APIVersion: "v7beta3", + Resource: "pods", + Subresource: "proxy", + Name: "my-pod", + 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", + Version: "v7beta3", + Resource: "pods", + Subresource: "proxy", + Name: "my-pod", + }, + }, + }, + }, + } + + for i, tt := range tests { + authorized, _, err := wh.Authorize(tt.attr) + if err != nil { + t.Fatal(err) + } + if !authorized { + 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)) + } + } +} + +type webhookCacheTestCase struct { + attr authorizer.AttributesRecord + + allow bool + statusCode int + + expectedErr bool + expectedAuthorized bool + expectedCalls int +} + +func testWebhookCacheCases(t *testing.T, serv *mockService, wh *WebhookAuthorizer, tests []webhookCacheTestCase) { + for i, test := range tests { + serv.called = 0 + serv.allow = test.allow + serv.statusCode = test.statusCode + authorized, _, err := wh.Authorize(test.attr) + if test.expectedErr && err == nil { + t.Errorf("%d: Expected error", i) + continue + } else if !test.expectedErr && err != nil { + t.Errorf("%d: unexpected error: %v", i, err) + continue + } + + if test.expectedAuthorized != authorized { + 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) + } + } +} + +// TestWebhookCache verifies that error responses from the server are not +// cached, but successful responses are. +func TestWebhookCache(t *testing.T) { + serv := new(mockService) + s, err := NewTestServer(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 := newAuthorizer(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"}} + + tests := []webhookCacheTestCase{ + // server error and 429's retry + {attr: aliceAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, + {attr: aliceAttr, allow: false, statusCode: 429, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, + // regular errors return errors but do not retry + {attr: aliceAttr, allow: false, statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, + {attr: aliceAttr, allow: false, statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, + {attr: aliceAttr, allow: false, statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, + // successful responses are cached + {attr: aliceAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1}, + // later requests within the cache window don't hit the backend + {attr: aliceAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCalls: 0}, + + // a request with different attributes doesn't hit the cache + {attr: bobAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, + // successful response for other attributes is cached + {attr: bobAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1}, + // later requests within the cache window don't hit the backend + {attr: bobAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCalls: 0}, + } + + testWebhookCacheCases(t, serv, wh, tests) +}