admission_test.go 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package imagepolicy
  14. import (
  15. "crypto/tls"
  16. "crypto/x509"
  17. "encoding/json"
  18. "math/rand"
  19. "net/http"
  20. "net/http/httptest"
  21. "reflect"
  22. "strconv"
  23. "testing"
  24. "time"
  25. "k8s.io/api/imagepolicy/v1alpha1"
  26. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  27. "k8s.io/apiserver/pkg/admission"
  28. "k8s.io/apiserver/pkg/authentication/user"
  29. v1 "k8s.io/client-go/tools/clientcmd/api/v1"
  30. api "k8s.io/kubernetes/pkg/apis/core"
  31. "fmt"
  32. "io/ioutil"
  33. "os"
  34. "path/filepath"
  35. "text/template"
  36. _ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
  37. )
  38. const defaultConfigTmplJSON = `
  39. {
  40. "imagePolicy": {
  41. "kubeConfigFile": "{{ .KubeConfig }}",
  42. "allowTTL": {{ .AllowTTL }},
  43. "denyTTL": {{ .DenyTTL }},
  44. "retryBackoff": {{ .RetryBackoff }},
  45. "defaultAllow": {{ .DefaultAllow }}
  46. }
  47. }
  48. `
  49. const defaultConfigTmplYAML = `
  50. imagePolicy:
  51. kubeConfigFile: "{{ .KubeConfig }}"
  52. allowTTL: {{ .AllowTTL }}
  53. denyTTL: {{ .DenyTTL }}
  54. retryBackoff: {{ .RetryBackoff }}
  55. defaultAllow: {{ .DefaultAllow }}
  56. `
  57. func TestNewFromConfig(t *testing.T) {
  58. dir, err := ioutil.TempDir("", "")
  59. if err != nil {
  60. t.Fatal(err)
  61. }
  62. defer os.RemoveAll(dir)
  63. data := struct {
  64. CA string
  65. Cert string
  66. Key string
  67. }{
  68. CA: filepath.Join(dir, "ca.pem"),
  69. Cert: filepath.Join(dir, "clientcert.pem"),
  70. Key: filepath.Join(dir, "clientkey.pem"),
  71. }
  72. files := []struct {
  73. name string
  74. data []byte
  75. }{
  76. {data.CA, caCert},
  77. {data.Cert, clientCert},
  78. {data.Key, clientKey},
  79. }
  80. for _, file := range files {
  81. if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil {
  82. t.Fatal(err)
  83. }
  84. }
  85. tests := []struct {
  86. msg string
  87. kubeConfigTmpl string
  88. wantErr bool
  89. }{
  90. {
  91. msg: "a single cluster and single user",
  92. kubeConfigTmpl: `
  93. clusters:
  94. - cluster:
  95. certificate-authority: {{ .CA }}
  96. server: https://admission.example.com
  97. name: foobar
  98. users:
  99. - name: a cluster
  100. user:
  101. client-certificate: {{ .Cert }}
  102. client-key: {{ .Key }}
  103. `,
  104. wantErr: true,
  105. },
  106. {
  107. msg: "multiple clusters with no context",
  108. kubeConfigTmpl: `
  109. clusters:
  110. - cluster:
  111. certificate-authority: {{ .CA }}
  112. server: https://admission.example.com
  113. name: foobar
  114. - cluster:
  115. certificate-authority: a bad certificate path
  116. server: https://admission.example.com
  117. name: barfoo
  118. users:
  119. - name: a name
  120. user:
  121. client-certificate: {{ .Cert }}
  122. client-key: {{ .Key }}
  123. `,
  124. wantErr: true,
  125. },
  126. {
  127. msg: "multiple clusters with a context",
  128. kubeConfigTmpl: `
  129. clusters:
  130. - cluster:
  131. certificate-authority: a bad certificate path
  132. server: https://admission.example.com
  133. name: foobar
  134. - cluster:
  135. certificate-authority: {{ .CA }}
  136. server: https://admission.example.com
  137. name: barfoo
  138. users:
  139. - name: a name
  140. user:
  141. client-certificate: {{ .Cert }}
  142. client-key: {{ .Key }}
  143. contexts:
  144. - name: default
  145. context:
  146. cluster: barfoo
  147. user: a name
  148. current-context: default
  149. `,
  150. wantErr: false,
  151. },
  152. {
  153. msg: "cluster with bad certificate path specified",
  154. kubeConfigTmpl: `
  155. clusters:
  156. - cluster:
  157. certificate-authority: a bad certificate path
  158. server: https://admission.example.com
  159. name: foobar
  160. - cluster:
  161. certificate-authority: {{ .CA }}
  162. server: https://admission.example.com
  163. name: barfoo
  164. users:
  165. - name: a name
  166. user:
  167. client-certificate: {{ .Cert }}
  168. client-key: {{ .Key }}
  169. contexts:
  170. - name: default
  171. context:
  172. cluster: foobar
  173. user: a name
  174. current-context: default
  175. `,
  176. wantErr: true,
  177. },
  178. }
  179. for _, tt := range tests {
  180. // Use a closure so defer statements trigger between loop iterations.
  181. t.Run(tt.msg, func(t *testing.T) {
  182. err := func() error {
  183. tempfile, err := ioutil.TempFile("", "")
  184. if err != nil {
  185. return err
  186. }
  187. p := tempfile.Name()
  188. defer os.Remove(p)
  189. tmpl, err := template.New("test").Parse(tt.kubeConfigTmpl)
  190. if err != nil {
  191. return fmt.Errorf("failed to parse test template: %v", err)
  192. }
  193. if err := tmpl.Execute(tempfile, data); err != nil {
  194. return fmt.Errorf("failed to execute test template: %v", err)
  195. }
  196. tempconfigfile, err := ioutil.TempFile("", "")
  197. if err != nil {
  198. return err
  199. }
  200. pc := tempconfigfile.Name()
  201. defer os.Remove(pc)
  202. configTmpl, err := template.New("testconfig").Parse(defaultConfigTmplJSON)
  203. if err != nil {
  204. return fmt.Errorf("failed to parse test template: %v", err)
  205. }
  206. dataConfig := struct {
  207. KubeConfig string
  208. AllowTTL int
  209. DenyTTL int
  210. RetryBackoff int
  211. DefaultAllow bool
  212. }{
  213. KubeConfig: p,
  214. AllowTTL: 500,
  215. DenyTTL: 500,
  216. RetryBackoff: 500,
  217. DefaultAllow: true,
  218. }
  219. if err := configTmpl.Execute(tempconfigfile, dataConfig); err != nil {
  220. return fmt.Errorf("failed to execute test template: %v", err)
  221. }
  222. // Create a new admission controller
  223. configFile, err := os.Open(pc)
  224. if err != nil {
  225. return fmt.Errorf("failed to read test config: %v", err)
  226. }
  227. defer configFile.Close()
  228. _, err = NewImagePolicyWebhook(configFile)
  229. return err
  230. }()
  231. if err != nil && !tt.wantErr {
  232. t.Errorf("failed to load plugin from config %q: %v", tt.msg, err)
  233. }
  234. if err == nil && tt.wantErr {
  235. t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg)
  236. }
  237. })
  238. }
  239. }
  240. // Service mocks a remote service.
  241. type Service interface {
  242. Review(*v1alpha1.ImageReview)
  243. HTTPStatusCode() int
  244. }
  245. // NewTestServer wraps a Service as an httptest.Server.
  246. func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) {
  247. var tlsConfig *tls.Config
  248. if cert != nil {
  249. cert, err := tls.X509KeyPair(cert, key)
  250. if err != nil {
  251. return nil, err
  252. }
  253. tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
  254. }
  255. if caCert != nil {
  256. rootCAs := x509.NewCertPool()
  257. rootCAs.AppendCertsFromPEM(caCert)
  258. if tlsConfig == nil {
  259. tlsConfig = &tls.Config{}
  260. }
  261. tlsConfig.ClientCAs = rootCAs
  262. tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
  263. }
  264. serveHTTP := func(w http.ResponseWriter, r *http.Request) {
  265. var review v1alpha1.ImageReview
  266. if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
  267. http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
  268. return
  269. }
  270. if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 {
  271. http.Error(w, "HTTP Error", s.HTTPStatusCode())
  272. return
  273. }
  274. s.Review(&review)
  275. type status struct {
  276. Allowed bool `json:"allowed"`
  277. Reason string `json:"reason"`
  278. AuditAnnotations map[string]string `json:"auditAnnotations"`
  279. }
  280. resp := struct {
  281. APIVersion string `json:"apiVersion"`
  282. Kind string `json:"kind"`
  283. Status status `json:"status"`
  284. }{
  285. APIVersion: v1alpha1.SchemeGroupVersion.String(),
  286. Kind: "ImageReview",
  287. Status: status{
  288. review.Status.Allowed,
  289. review.Status.Reason,
  290. review.Status.AuditAnnotations,
  291. },
  292. }
  293. w.Header().Set("Content-Type", "application/json")
  294. json.NewEncoder(w).Encode(resp)
  295. }
  296. server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
  297. server.TLS = tlsConfig
  298. server.StartTLS()
  299. return server, nil
  300. }
  301. // A service that can be set to allow all or deny all authorization requests.
  302. type mockService struct {
  303. allow bool
  304. statusCode int
  305. outAnnotations map[string]string
  306. }
  307. func (m *mockService) Review(r *v1alpha1.ImageReview) {
  308. r.Status.Allowed = m.allow
  309. // hardcoded overrides
  310. if r.Spec.Containers[0].Image == "good" {
  311. r.Status.Allowed = true
  312. }
  313. for _, c := range r.Spec.Containers {
  314. if c.Image == "bad" {
  315. r.Status.Allowed = false
  316. }
  317. }
  318. if !r.Status.Allowed {
  319. r.Status.Reason = "not allowed"
  320. }
  321. r.Status.AuditAnnotations = m.outAnnotations
  322. }
  323. func (m *mockService) Allow() { m.allow = true }
  324. func (m *mockService) Deny() { m.allow = false }
  325. func (m *mockService) HTTPStatusCode() int { return m.statusCode }
  326. // newImagePolicyWebhook creates a temporary kubeconfig file from the provided arguments and attempts to load
  327. // a new newImagePolicyWebhook from it.
  328. func newImagePolicyWebhook(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, defaultAllow bool) (*Plugin, error) {
  329. tempfile, err := ioutil.TempFile("", "")
  330. if err != nil {
  331. return nil, err
  332. }
  333. p := tempfile.Name()
  334. defer os.Remove(p)
  335. config := v1.Config{
  336. Clusters: []v1.NamedCluster{
  337. {
  338. Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca},
  339. },
  340. },
  341. AuthInfos: []v1.NamedAuthInfo{
  342. {
  343. AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
  344. },
  345. },
  346. }
  347. if err := json.NewEncoder(tempfile).Encode(config); err != nil {
  348. return nil, err
  349. }
  350. tempconfigfile, err := ioutil.TempFile("", "")
  351. if err != nil {
  352. return nil, err
  353. }
  354. pc := tempconfigfile.Name()
  355. defer os.Remove(pc)
  356. configTmpl, err := template.New("testconfig").Parse(defaultConfigTmplYAML)
  357. if err != nil {
  358. return nil, fmt.Errorf("failed to parse test template: %v", err)
  359. }
  360. dataConfig := struct {
  361. KubeConfig string
  362. AllowTTL int64
  363. DenyTTL int64
  364. RetryBackoff int64
  365. DefaultAllow bool
  366. }{
  367. KubeConfig: p,
  368. AllowTTL: cacheTime.Nanoseconds(),
  369. DenyTTL: cacheTime.Nanoseconds(),
  370. RetryBackoff: 0,
  371. DefaultAllow: defaultAllow,
  372. }
  373. if err := configTmpl.Execute(tempconfigfile, dataConfig); err != nil {
  374. return nil, fmt.Errorf("failed to execute test template: %v", err)
  375. }
  376. // Create a new admission controller
  377. configFile, err := os.Open(pc)
  378. if err != nil {
  379. return nil, fmt.Errorf("failed to read test config: %v", err)
  380. }
  381. defer configFile.Close()
  382. wh, err := NewImagePolicyWebhook(configFile)
  383. if err != nil {
  384. return nil, err
  385. }
  386. return wh, err
  387. }
  388. func TestTLSConfig(t *testing.T) {
  389. tests := []struct {
  390. test string
  391. clientCert, clientKey, clientCA []byte
  392. serverCert, serverKey, serverCA []byte
  393. wantAllowed, wantErr bool
  394. }{
  395. {
  396. test: "TLS setup between client and server",
  397. clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
  398. serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
  399. wantAllowed: true,
  400. },
  401. {
  402. test: "Server does not require client auth",
  403. clientCA: caCert,
  404. serverCert: serverCert, serverKey: serverKey,
  405. wantAllowed: true,
  406. },
  407. {
  408. test: "Server does not require client auth, client provides it",
  409. clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
  410. serverCert: serverCert, serverKey: serverKey,
  411. wantAllowed: true,
  412. },
  413. {
  414. test: "Client does not trust server",
  415. clientCert: clientCert, clientKey: clientKey,
  416. serverCert: serverCert, serverKey: serverKey,
  417. wantErr: true,
  418. },
  419. {
  420. test: "Server does not trust client",
  421. clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
  422. serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
  423. wantErr: true,
  424. },
  425. {
  426. // Plugin does not support insecure configurations.
  427. test: "Server is using insecure connection",
  428. wantErr: true,
  429. },
  430. }
  431. for _, tt := range tests {
  432. // Use a closure so defer statements trigger between loop iterations.
  433. t.Run(tt.test, func(t *testing.T) {
  434. service := new(mockService)
  435. service.statusCode = 200
  436. server, err := NewTestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
  437. if err != nil {
  438. t.Errorf("%s: failed to create server: %v", tt.test, err)
  439. return
  440. }
  441. defer server.Close()
  442. wh, err := newImagePolicyWebhook(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, -1, false)
  443. if err != nil {
  444. t.Errorf("%s: failed to create client: %v", tt.test, err)
  445. return
  446. }
  447. pod := goodPod(strconv.Itoa(rand.Intn(1000)))
  448. attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  449. // Allow all and see if we get an error.
  450. service.Allow()
  451. err = wh.Validate(attr, nil)
  452. if tt.wantAllowed {
  453. if err != nil {
  454. t.Errorf("expected successful admission")
  455. }
  456. } else {
  457. if err == nil {
  458. t.Errorf("expected failed admission")
  459. }
  460. }
  461. if tt.wantErr {
  462. if err == nil {
  463. t.Errorf("expected error making admission request: %v", err)
  464. }
  465. return
  466. }
  467. if err != nil {
  468. t.Errorf("%s: failed to admit with AllowAll policy: %v", tt.test, err)
  469. return
  470. }
  471. service.Deny()
  472. if err := wh.Validate(attr, nil); err == nil {
  473. t.Errorf("%s: incorrectly admitted with DenyAll policy", tt.test)
  474. }
  475. })
  476. }
  477. }
  478. type webhookCacheTestCase struct {
  479. statusCode int
  480. expectedErr bool
  481. expectedAuthorized bool
  482. expectedCached bool
  483. }
  484. func testWebhookCacheCases(t *testing.T, serv *mockService, wh *Plugin, attr admission.Attributes, tests []webhookCacheTestCase) {
  485. for _, test := range tests {
  486. serv.statusCode = test.statusCode
  487. err := wh.Validate(attr, nil)
  488. authorized := err == nil
  489. if test.expectedErr && err == nil {
  490. t.Errorf("Expected error")
  491. } else if !test.expectedErr && err != nil {
  492. t.Fatal(err)
  493. }
  494. if test.expectedAuthorized && !authorized {
  495. if test.expectedCached {
  496. t.Errorf("Webhook should have successful response cached, but authorizer reported unauthorized.")
  497. } else {
  498. t.Errorf("Webhook returned HTTP %d, but authorizer reported unauthorized.", test.statusCode)
  499. }
  500. } else if !test.expectedAuthorized && authorized {
  501. t.Errorf("Webhook returned HTTP %d, but authorizer reported success.", test.statusCode)
  502. }
  503. }
  504. }
  505. // TestWebhookCache verifies that error responses from the server are not
  506. // cached, but successful responses are.
  507. func TestWebhookCache(t *testing.T) {
  508. serv := new(mockService)
  509. s, err := NewTestServer(serv, serverCert, serverKey, caCert)
  510. if err != nil {
  511. t.Fatal(err)
  512. }
  513. defer s.Close()
  514. // Create an admission controller that caches successful responses.
  515. wh, err := newImagePolicyWebhook(s.URL, clientCert, clientKey, caCert, 200, false)
  516. if err != nil {
  517. t.Fatal(err)
  518. }
  519. tests := []webhookCacheTestCase{
  520. {statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCached: false},
  521. {statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCached: false},
  522. {statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCached: false},
  523. {statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCached: false},
  524. {statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCached: false},
  525. {statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCached: true},
  526. }
  527. attr := admission.NewAttributesRecord(goodPod("test"), nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  528. serv.allow = true
  529. testWebhookCacheCases(t, serv, wh, attr, tests)
  530. // For a different request, webhook should be called again.
  531. tests = []webhookCacheTestCase{
  532. {statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCached: false},
  533. {statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCached: false},
  534. {statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCached: true},
  535. }
  536. attr = admission.NewAttributesRecord(goodPod("test2"), nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  537. testWebhookCacheCases(t, serv, wh, attr, tests)
  538. }
  539. func TestContainerCombinations(t *testing.T) {
  540. tests := []struct {
  541. test string
  542. pod *api.Pod
  543. wantAllowed, wantErr bool
  544. }{
  545. {
  546. test: "Single container allowed",
  547. pod: goodPod("good"),
  548. wantAllowed: true,
  549. },
  550. {
  551. test: "Single container denied",
  552. pod: goodPod("bad"),
  553. wantAllowed: false,
  554. wantErr: true,
  555. },
  556. {
  557. test: "One good container, one bad",
  558. pod: &api.Pod{
  559. Spec: api.PodSpec{
  560. ServiceAccountName: "default",
  561. SecurityContext: &api.PodSecurityContext{},
  562. Containers: []api.Container{
  563. {
  564. Image: "bad",
  565. SecurityContext: &api.SecurityContext{},
  566. },
  567. {
  568. Image: "good",
  569. SecurityContext: &api.SecurityContext{},
  570. },
  571. },
  572. },
  573. },
  574. wantAllowed: false,
  575. wantErr: true,
  576. },
  577. {
  578. test: "Multiple good containers",
  579. pod: &api.Pod{
  580. Spec: api.PodSpec{
  581. ServiceAccountName: "default",
  582. SecurityContext: &api.PodSecurityContext{},
  583. Containers: []api.Container{
  584. {
  585. Image: "good",
  586. SecurityContext: &api.SecurityContext{},
  587. },
  588. {
  589. Image: "good",
  590. SecurityContext: &api.SecurityContext{},
  591. },
  592. },
  593. },
  594. },
  595. wantAllowed: true,
  596. wantErr: false,
  597. },
  598. {
  599. test: "Multiple bad containers",
  600. pod: &api.Pod{
  601. Spec: api.PodSpec{
  602. ServiceAccountName: "default",
  603. SecurityContext: &api.PodSecurityContext{},
  604. Containers: []api.Container{
  605. {
  606. Image: "bad",
  607. SecurityContext: &api.SecurityContext{},
  608. },
  609. {
  610. Image: "bad",
  611. SecurityContext: &api.SecurityContext{},
  612. },
  613. },
  614. },
  615. },
  616. wantAllowed: false,
  617. wantErr: true,
  618. },
  619. {
  620. test: "Good container, bad init container",
  621. pod: &api.Pod{
  622. Spec: api.PodSpec{
  623. ServiceAccountName: "default",
  624. SecurityContext: &api.PodSecurityContext{},
  625. Containers: []api.Container{
  626. {
  627. Image: "good",
  628. SecurityContext: &api.SecurityContext{},
  629. },
  630. },
  631. InitContainers: []api.Container{
  632. {
  633. Image: "bad",
  634. SecurityContext: &api.SecurityContext{},
  635. },
  636. },
  637. },
  638. },
  639. wantAllowed: false,
  640. wantErr: true,
  641. },
  642. {
  643. test: "Bad container, good init container",
  644. pod: &api.Pod{
  645. Spec: api.PodSpec{
  646. ServiceAccountName: "default",
  647. SecurityContext: &api.PodSecurityContext{},
  648. Containers: []api.Container{
  649. {
  650. Image: "bad",
  651. SecurityContext: &api.SecurityContext{},
  652. },
  653. },
  654. InitContainers: []api.Container{
  655. {
  656. Image: "good",
  657. SecurityContext: &api.SecurityContext{},
  658. },
  659. },
  660. },
  661. },
  662. wantAllowed: false,
  663. wantErr: true,
  664. },
  665. {
  666. test: "Good container, good init container",
  667. pod: &api.Pod{
  668. Spec: api.PodSpec{
  669. ServiceAccountName: "default",
  670. SecurityContext: &api.PodSecurityContext{},
  671. Containers: []api.Container{
  672. {
  673. Image: "good",
  674. SecurityContext: &api.SecurityContext{},
  675. },
  676. },
  677. InitContainers: []api.Container{
  678. {
  679. Image: "good",
  680. SecurityContext: &api.SecurityContext{},
  681. },
  682. },
  683. },
  684. },
  685. wantAllowed: true,
  686. wantErr: false,
  687. },
  688. }
  689. for _, tt := range tests {
  690. // Use a closure so defer statements trigger between loop iterations.
  691. t.Run(tt.test, func(t *testing.T) {
  692. service := new(mockService)
  693. service.statusCode = 200
  694. server, err := NewTestServer(service, serverCert, serverKey, caCert)
  695. if err != nil {
  696. t.Errorf("%s: failed to create server: %v", tt.test, err)
  697. return
  698. }
  699. defer server.Close()
  700. wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, false)
  701. if err != nil {
  702. t.Errorf("%s: failed to create client: %v", tt.test, err)
  703. return
  704. }
  705. attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  706. err = wh.Validate(attr, nil)
  707. if tt.wantAllowed {
  708. if err != nil {
  709. t.Errorf("expected successful admission: %s", tt.test)
  710. }
  711. } else {
  712. if err == nil {
  713. t.Errorf("expected failed admission: %s", tt.test)
  714. }
  715. }
  716. if tt.wantErr {
  717. if err == nil {
  718. t.Errorf("expected error making admission request: %v", err)
  719. }
  720. return
  721. }
  722. if err != nil {
  723. t.Errorf("%s: failed to admit: %v", tt.test, err)
  724. return
  725. }
  726. })
  727. }
  728. }
  729. // fakeAttributes decorate kadmission.Attributes. It's used to trace the added annotations.
  730. type fakeAttributes struct {
  731. admission.Attributes
  732. annotations map[string]string
  733. }
  734. func (f fakeAttributes) AddAnnotation(k, v string) error {
  735. f.annotations[k] = v
  736. return f.Attributes.AddAnnotation(k, v)
  737. }
  738. func TestDefaultAllow(t *testing.T) {
  739. tests := []struct {
  740. test string
  741. pod *api.Pod
  742. defaultAllow bool
  743. wantAllowed, wantErr, wantFailOpen bool
  744. }{
  745. {
  746. test: "DefaultAllow = true, backend unreachable, bad image",
  747. pod: goodPod("bad"),
  748. defaultAllow: true,
  749. wantAllowed: true,
  750. wantFailOpen: true,
  751. },
  752. {
  753. test: "DefaultAllow = true, backend unreachable, good image",
  754. pod: goodPod("good"),
  755. defaultAllow: true,
  756. wantAllowed: true,
  757. wantFailOpen: true,
  758. },
  759. {
  760. test: "DefaultAllow = false, backend unreachable, good image",
  761. pod: goodPod("good"),
  762. defaultAllow: false,
  763. wantAllowed: false,
  764. wantErr: true,
  765. wantFailOpen: false,
  766. },
  767. {
  768. test: "DefaultAllow = false, backend unreachable, bad image",
  769. pod: goodPod("bad"),
  770. defaultAllow: false,
  771. wantAllowed: false,
  772. wantErr: true,
  773. wantFailOpen: false,
  774. },
  775. }
  776. for _, tt := range tests {
  777. // Use a closure so defer statements trigger between loop iterations.
  778. t.Run(tt.test, func(t *testing.T) {
  779. service := new(mockService)
  780. service.statusCode = 500
  781. server, err := NewTestServer(service, serverCert, serverKey, caCert)
  782. if err != nil {
  783. t.Errorf("%s: failed to create server: %v", tt.test, err)
  784. return
  785. }
  786. defer server.Close()
  787. wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, tt.defaultAllow)
  788. if err != nil {
  789. t.Errorf("%s: failed to create client: %v", tt.test, err)
  790. return
  791. }
  792. attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  793. annotations := make(map[string]string)
  794. attr = &fakeAttributes{attr, annotations}
  795. err = wh.Validate(attr, nil)
  796. if tt.wantAllowed {
  797. if err != nil {
  798. t.Errorf("expected successful admission")
  799. }
  800. } else {
  801. if err == nil {
  802. t.Errorf("expected failed admission")
  803. }
  804. }
  805. if tt.wantErr {
  806. if err == nil {
  807. t.Errorf("expected error making admission request: %v", err)
  808. }
  809. return
  810. }
  811. if err != nil {
  812. t.Errorf("%s: failed to admit: %v", tt.test, err)
  813. return
  814. }
  815. podAnnotations := tt.pod.GetAnnotations()
  816. if tt.wantFailOpen {
  817. if podAnnotations == nil || podAnnotations[api.ImagePolicyFailedOpenKey] != "true" {
  818. t.Errorf("missing expected fail open pod annotation")
  819. }
  820. if annotations[AuditKeyPrefix+ImagePolicyFailedOpenKeySuffix] != "true" {
  821. t.Errorf("missing expected fail open attributes annotation")
  822. }
  823. } else {
  824. if podAnnotations != nil && podAnnotations[api.ImagePolicyFailedOpenKey] == "true" {
  825. t.Errorf("found unexpected fail open pod annotation")
  826. }
  827. if annotations[AuditKeyPrefix+ImagePolicyFailedOpenKeySuffix] == "true" {
  828. t.Errorf("found unexpected fail open attributes annotation")
  829. }
  830. }
  831. })
  832. }
  833. }
  834. // A service that can record annotations sent to it
  835. type annotationService struct {
  836. annotations map[string]string
  837. }
  838. func (a *annotationService) Review(r *v1alpha1.ImageReview) {
  839. a.annotations = make(map[string]string)
  840. for k, v := range r.Spec.Annotations {
  841. a.annotations[k] = v
  842. }
  843. r.Status.Allowed = true
  844. }
  845. func (a *annotationService) HTTPStatusCode() int { return 200 }
  846. func (a *annotationService) Annotations() map[string]string { return a.annotations }
  847. func TestAnnotationFiltering(t *testing.T) {
  848. tests := []struct {
  849. test string
  850. annotations map[string]string
  851. outAnnotations map[string]string
  852. }{
  853. {
  854. test: "all annotations filtered out",
  855. annotations: map[string]string{
  856. "test": "test",
  857. "another": "annotation",
  858. "": "",
  859. },
  860. outAnnotations: map[string]string{},
  861. },
  862. {
  863. test: "image-policy annotations allowed",
  864. annotations: map[string]string{
  865. "my.image-policy.k8s.io/test": "test",
  866. "other.image-policy.k8s.io/test2": "annotation",
  867. "test": "test",
  868. "another": "another",
  869. "": "",
  870. },
  871. outAnnotations: map[string]string{
  872. "my.image-policy.k8s.io/test": "test",
  873. "other.image-policy.k8s.io/test2": "annotation",
  874. },
  875. },
  876. }
  877. for _, tt := range tests {
  878. // Use a closure so defer statements trigger between loop iterations.
  879. t.Run(tt.test, func(t *testing.T) {
  880. service := new(annotationService)
  881. server, err := NewTestServer(service, serverCert, serverKey, caCert)
  882. if err != nil {
  883. t.Errorf("%s: failed to create server: %v", tt.test, err)
  884. return
  885. }
  886. defer server.Close()
  887. wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, true)
  888. if err != nil {
  889. t.Errorf("%s: failed to create client: %v", tt.test, err)
  890. return
  891. }
  892. pod := goodPod("test")
  893. pod.Annotations = tt.annotations
  894. attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  895. err = wh.Validate(attr, nil)
  896. if err != nil {
  897. t.Errorf("expected successful admission")
  898. }
  899. if !reflect.DeepEqual(tt.outAnnotations, service.Annotations()) {
  900. t.Errorf("expected annotations sent to webhook: %v to match expected: %v", service.Annotations(), tt.outAnnotations)
  901. }
  902. })
  903. }
  904. }
  905. func TestReturnedAnnotationAdd(t *testing.T) {
  906. tests := []struct {
  907. test string
  908. pod *api.Pod
  909. verifierAnnotations map[string]string
  910. expectedAnnotations map[string]string
  911. }{
  912. {
  913. test: "Add valid response annotations",
  914. pod: goodPod("good"),
  915. verifierAnnotations: map[string]string{
  916. "foo-test": "true",
  917. "bar-test": "false",
  918. },
  919. expectedAnnotations: map[string]string{
  920. "imagepolicywebhook.image-policy.k8s.io/foo-test": "true",
  921. "imagepolicywebhook.image-policy.k8s.io/bar-test": "false",
  922. },
  923. },
  924. {
  925. test: "No returned annotations are ignored",
  926. pod: goodPod("good"),
  927. verifierAnnotations: map[string]string{},
  928. expectedAnnotations: map[string]string{},
  929. },
  930. {
  931. test: "Handles nil annotations",
  932. pod: goodPod("good"),
  933. verifierAnnotations: nil,
  934. expectedAnnotations: map[string]string{},
  935. },
  936. {
  937. test: "Adds annotations for bad request",
  938. pod: &api.Pod{
  939. Spec: api.PodSpec{
  940. ServiceAccountName: "default",
  941. SecurityContext: &api.PodSecurityContext{},
  942. Containers: []api.Container{
  943. {
  944. Image: "bad",
  945. SecurityContext: &api.SecurityContext{},
  946. },
  947. },
  948. },
  949. },
  950. verifierAnnotations: map[string]string{
  951. "foo-test": "false",
  952. },
  953. expectedAnnotations: map[string]string{
  954. "imagepolicywebhook.image-policy.k8s.io/foo-test": "false",
  955. },
  956. },
  957. }
  958. for _, tt := range tests {
  959. // Use a closure so defer statements trigger between loop iterations.
  960. t.Run(tt.test, func(t *testing.T) {
  961. service := new(mockService)
  962. service.statusCode = 200
  963. service.outAnnotations = tt.verifierAnnotations
  964. server, err := NewTestServer(service, serverCert, serverKey, caCert)
  965. if err != nil {
  966. t.Errorf("%s: failed to create server: %v", tt.test, err)
  967. return
  968. }
  969. defer server.Close()
  970. wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, true)
  971. if err != nil {
  972. t.Errorf("%s: failed to create client: %v", tt.test, err)
  973. return
  974. }
  975. pod := tt.pod
  976. attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  977. annotations := make(map[string]string)
  978. attr = &fakeAttributes{attr, annotations}
  979. err = wh.Validate(attr, nil)
  980. if !reflect.DeepEqual(annotations, tt.expectedAnnotations) {
  981. t.Errorf("got audit annotations: %v; want: %v", annotations, tt.expectedAnnotations)
  982. }
  983. })
  984. }
  985. }
  986. func goodPod(containerID string) *api.Pod {
  987. return &api.Pod{
  988. Spec: api.PodSpec{
  989. ServiceAccountName: "default",
  990. SecurityContext: &api.PodSecurityContext{},
  991. Containers: []api.Container{
  992. {
  993. Image: containerID,
  994. SecurityContext: &api.SecurityContext{},
  995. },
  996. },
  997. },
  998. }
  999. }