]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blob - main_test.go
145abe91dfe5b8a0c53989e90133856feeeea528
[nsscash/nsscash.git] / main_test.go
1 // Copyright (C) 2019  Simon Ruderich
2 //
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU Affero General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU Affero General Public License for more details.
12 //
13 // You should have received a copy of the GNU Affero General Public License
14 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16 package main
17
18 import (
19         "crypto/sha1"
20         "crypto/tls"
21         "encoding/hex"
22         "fmt"
23         "io/ioutil"
24         "log"
25         "net/http"
26         "net/http/httptest"
27         "os"
28         "path/filepath"
29         "reflect"
30         "runtime"
31         "strings"
32         "testing"
33         "time"
34 )
35
36 const (
37         configPath  = "testdata/config.toml"
38         statePath   = "testdata/var/state.json"
39         passwdPath  = "testdata/passwd.nsscash"
40         plainPath   = "testdata/plain"
41         groupPath   = "testdata/group.nsscash"
42         tlsCAPath   = "testdata/ca.crt"
43         tlsCertPath = "testdata/server.crt"
44         tlsKeyPath  = "testdata/server.key"
45         tlsCA2Path  = "testdata/ca2.crt"
46 )
47
48 type args struct {
49         t       *testing.T
50         url     string
51         handler *func(http.ResponseWriter, *http.Request)
52 }
53
54 // mustNotExist verifies that all given paths don't exist in the file system.
55 func mustNotExist(t *testing.T, paths ...string) {
56         for _, p := range paths {
57                 f, err := os.Open(p)
58                 if err != nil {
59                         if !os.IsNotExist(err) {
60                                 t.Errorf("path %q: unexpected error: %v",
61                                         p, err)
62                         }
63                 } else {
64                         t.Errorf("path %q exists", p)
65                         f.Close()
66                 }
67         }
68 }
69
70 func hashAsHex(x []byte) string {
71         h := sha1.New()
72         h.Write(x)
73         return hex.EncodeToString(h.Sum(nil))
74 }
75
76 // mustHaveHash checks if the given path content has the given SHA-1 string
77 // (in hex).
78 func mustHaveHash(t *testing.T, path string, hash string) {
79         x, err := ioutil.ReadFile(path)
80         if err != nil {
81                 t.Fatal(err)
82         }
83
84         y := hashAsHex(x)
85         if y != hash {
86                 t.Errorf("%q has unexpected hash %q", path, y)
87         }
88 }
89
90 // mustBeErrorWithSubstring checks if the given error, represented as string,
91 // contains the given substring. This is somewhat ugly but the simplest way to
92 // check for proper errors.
93 func mustBeErrorWithSubstring(t *testing.T, err error, substring string) {
94         if err == nil {
95                 t.Errorf("err is nil")
96         } else if !strings.Contains(err.Error(), substring) {
97                 t.Errorf("err %q does not contain string %q", err, substring)
98         }
99 }
100
101 func mustWriteConfig(t *testing.T, config string) {
102         err := ioutil.WriteFile(configPath, []byte(config), 0644)
103         if err != nil {
104                 t.Fatal(err)
105         }
106 }
107
108 func mustWritePasswdConfig(t *testing.T, url string) {
109         mustWriteConfig(t, fmt.Sprintf(`
110 statepath = "%[1]s"
111
112 [[file]]
113 type = "passwd"
114 url = "%[2]s/passwd"
115 path = "%[3]s"
116 ca = "%[4]s"
117 `, statePath, url, passwdPath, tlsCAPath))
118 }
119
120 func mustWriteGroupConfig(t *testing.T, url string) {
121         mustWriteConfig(t, fmt.Sprintf(`
122 statepath = "%[1]s"
123
124 [[file]]
125 type = "group"
126 url = "%[2]s/group"
127 path = "%[3]s"
128 ca = "%[4]s"
129 `, statePath, url, groupPath, tlsCAPath))
130 }
131
132 // mustCreate creates a file, truncating it if it exists. It then changes the
133 // modification to be in the past.
134 func mustCreate(t *testing.T, path string) {
135         f, err := os.Create(path)
136         if err != nil {
137                 t.Fatal(err)
138         }
139         err = f.Close()
140         if err != nil {
141                 t.Fatal(err)
142         }
143
144         // Change modification time to the past to detect updates to the file
145         mustMakeOld(t, path)
146 }
147
148 // mustMakeOld change the modification time of all paths to be in the past.
149 func mustMakeOld(t *testing.T, paths ...string) {
150         old := time.Now().Add(-2 * time.Hour)
151         for _, p := range paths {
152                 err := os.Chtimes(p, old, old)
153                 if err != nil {
154                         t.Fatal(err)
155                 }
156         }
157 }
158
159 // mustBeOld verifies that all paths have a modification time in the past, as
160 // set by mustMakeOld.
161 func mustBeOld(t *testing.T, paths ...string) {
162         for _, p := range paths {
163                 i, err := os.Stat(p)
164                 if err != nil {
165                         t.Fatal(err)
166                 }
167
168                 mtime := i.ModTime()
169                 now := time.Now()
170                 if now.Sub(mtime) < time.Hour {
171                         t.Errorf("%q was recently modified", p)
172                 }
173         }
174 }
175
176 // mustBeNew verifies that all paths have a modification time in the present.
177 func mustBeNew(t *testing.T, paths ...string) {
178         for _, p := range paths {
179                 i, err := os.Stat(p)
180                 if err != nil {
181                         t.Fatal(err)
182                 }
183
184                 mtime := i.ModTime()
185                 now := time.Now()
186                 if now.Sub(mtime) > time.Hour {
187                         t.Errorf("%q was not recently modified", p)
188                 }
189         }
190 }
191
192 func TestMainFetch(t *testing.T) {
193         // Suppress log messages
194         log.SetOutput(ioutil.Discard)
195         defer log.SetOutput(os.Stderr)
196
197         tests := []func(args){
198                 // Perform most tests with passwd for simplicity
199                 fetchPasswdCacheFileDoesNotExist,
200                 fetchPasswd404,
201                 fetchPasswdUnexpected304,
202                 fetchPasswdEmpty,
203                 fetchPasswdInvalid,
204                 fetchPasswdLimits,
205                 fetchPasswd,
206                 // Tests for plain and group
207                 fetchPlainEmpty,
208                 fetchPlain,
209                 fetchGroupEmpty,
210                 fetchGroupInvalid,
211                 fetchGroupLimits,
212                 fetchGroup,
213                 // Special tests
214                 fetchNoConfig,
215                 fetchStateCannotRead,
216                 fetchStateInvalid,
217                 fetchStateCannotWrite,
218                 fetchCannotDeploy,
219                 fetchSecondFetchFails,
220                 fetchBasicAuth,
221         }
222
223         // HTTP tests
224
225         for _, f := range tests {
226                 runMainTest(t, f, nil)
227         }
228
229         // HTTPS tests
230
231         tests = append(tests, fetchInvalidCA)
232
233         cert, err := tls.LoadX509KeyPair(tlsCertPath, tlsKeyPath)
234         if err != nil {
235                 t.Fatal(err)
236         }
237         tls := &tls.Config{
238                 Certificates: []tls.Certificate{cert},
239         }
240
241         for _, f := range tests {
242                 runMainTest(t, f, tls)
243         }
244 }
245
246 func runMainTest(t *testing.T, f func(args), tls *tls.Config) {
247         cleanup := []string{
248                 configPath,
249                 statePath,
250                 passwdPath,
251                 plainPath,
252                 groupPath,
253         }
254
255         // NOTE: This is not guaranteed to work according to reflect's
256         // documentation but seems to work reliable for normal functions.
257         fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
258         name := fn.Name()
259         name = name[strings.LastIndex(name, ".")+1:]
260         if tls != nil {
261                 name = "tls" + name
262         }
263
264         t.Run(name, func(t *testing.T) {
265                 // Preparation & cleanup
266                 for _, p := range cleanup {
267                         err := os.Remove(p)
268                         if err != nil && !os.IsNotExist(err) {
269                                 t.Fatal(err)
270                         }
271                         // Remove the file at the end of this test run, if it
272                         // was created
273                         defer os.Remove(p)
274
275                         dir := filepath.Dir(p)
276                         err = os.MkdirAll(dir, 0755)
277                         if err != nil {
278                                 t.Fatal(err)
279                         }
280                         defer os.Remove(dir) // remove empty directories
281                 }
282
283                 var handler func(http.ResponseWriter, *http.Request)
284                 ts := httptest.NewUnstartedServer(http.HandlerFunc(
285                         func(w http.ResponseWriter, r *http.Request) {
286                                 handler(w, r)
287                         }))
288                 if tls == nil {
289                         ts.Start()
290                 } else {
291                         ts.TLS = tls
292                         ts.StartTLS()
293                 }
294                 defer ts.Close()
295
296                 f(args{
297                         t:       t,
298                         url:     ts.URL,
299                         handler: &handler,
300                 })
301         })
302 }
303
304 func fetchPasswdCacheFileDoesNotExist(a args) {
305         t := a.t
306         mustWritePasswdConfig(t, a.url)
307
308         err := mainFetch(configPath)
309         mustBeErrorWithSubstring(t, err,
310                 "file.path \""+passwdPath+"\" must exist")
311
312         mustNotExist(t, statePath, passwdPath, plainPath, groupPath)
313 }
314
315 func fetchPasswd404(a args) {
316         t := a.t
317         mustWritePasswdConfig(t, a.url)
318         mustCreate(t, passwdPath)
319
320         *a.handler = func(w http.ResponseWriter, r *http.Request) {
321                 // 404
322                 w.WriteHeader(http.StatusNotFound)
323         }
324
325         err := mainFetch(configPath)
326         mustBeErrorWithSubstring(t, err,
327                 "status code 404")
328
329         mustNotExist(t, statePath, plainPath, groupPath)
330         mustBeOld(t, passwdPath)
331 }
332
333 func fetchPasswdUnexpected304(a args) {
334         t := a.t
335         mustWritePasswdConfig(t, a.url)
336         mustCreate(t, passwdPath)
337
338         *a.handler = func(w http.ResponseWriter, r *http.Request) {
339                 // 304
340                 w.WriteHeader(http.StatusNotModified)
341         }
342
343         err := mainFetch(configPath)
344         mustBeErrorWithSubstring(t, err,
345                 "status code 304 but did not send If-Modified-Since")
346
347         mustNotExist(t, statePath, plainPath, groupPath)
348         mustBeOld(t, passwdPath)
349 }
350
351 func fetchPasswdEmpty(a args) {
352         t := a.t
353         mustWritePasswdConfig(t, a.url)
354         mustCreate(t, passwdPath)
355
356         *a.handler = func(w http.ResponseWriter, r *http.Request) {
357                 // Empty response
358         }
359
360         err := mainFetch(configPath)
361         mustBeErrorWithSubstring(t, err,
362                 "refusing to use empty passwd file")
363
364         mustNotExist(t, statePath, plainPath, groupPath)
365         mustBeOld(t, passwdPath)
366 }
367
368 func fetchPasswdInvalid(a args) {
369         t := a.t
370         mustWritePasswdConfig(t, a.url)
371         mustCreate(t, passwdPath)
372
373         *a.handler = func(w http.ResponseWriter, r *http.Request) {
374                 if r.URL.Path != "/passwd" {
375                         return
376                 }
377
378                 fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash")
379         }
380
381         err := mainFetch(configPath)
382         mustBeErrorWithSubstring(t, err,
383                 "invalid uid in line")
384
385         mustNotExist(t, statePath, plainPath, groupPath)
386         mustBeOld(t, passwdPath)
387 }
388
389 func fetchPasswdLimits(a args) {
390         t := a.t
391         mustWritePasswdConfig(t, a.url)
392         mustCreate(t, passwdPath)
393
394         *a.handler = func(w http.ResponseWriter, r *http.Request) {
395                 if r.URL.Path != "/passwd" {
396                         return
397                 }
398
399                 fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash")
400                 for i := 0; i < 65536; i++ {
401                         fmt.Fprint(w, "x")
402                 }
403                 fmt.Fprint(w, "\n")
404         }
405
406         err := mainFetch(configPath)
407         mustBeErrorWithSubstring(t, err,
408                 "passwd too large to serialize")
409
410         mustNotExist(t, statePath, plainPath, groupPath)
411         mustBeOld(t, passwdPath)
412 }
413
414 func fetchPasswd(a args) {
415         t := a.t
416         mustWritePasswdConfig(t, a.url)
417         mustCreate(t, passwdPath)
418         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
419
420         t.Log("First fetch, write files")
421
422         *a.handler = func(w http.ResponseWriter, r *http.Request) {
423                 if r.URL.Path != "/passwd" {
424                         return
425                 }
426
427                 // No "Last-Modified" header
428                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
429                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
430         }
431
432         err := mainFetch(configPath)
433         if err != nil {
434                 t.Error(err)
435         }
436
437         mustNotExist(t, plainPath, groupPath)
438         mustBeNew(t, passwdPath, statePath)
439         // The actual content of passwdPath is verified by the NSS tests
440         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
441
442         t.Log("Fetch again, no support for Last-Modified")
443
444         mustMakeOld(t, passwdPath, statePath)
445
446         err = mainFetch(configPath)
447         if err != nil {
448                 t.Error(err)
449         }
450
451         mustNotExist(t, plainPath, groupPath)
452         mustBeNew(t, passwdPath, statePath)
453         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
454
455         t.Log("Fetch again, support for Last-Modified, but not retrieved yet")
456
457         mustMakeOld(t, passwdPath, statePath)
458
459         lastChange := time.Now()
460         change := false
461         *a.handler = func(w http.ResponseWriter, r *http.Request) {
462                 if r.URL.Path != "/passwd" {
463                         return
464                 }
465
466                 modified := r.Header.Get("If-Modified-Since")
467                 if modified != "" {
468                         x, err := http.ParseTime(modified)
469                         if err != nil {
470                                 t.Fatalf("invalid If-Modified-Since %v",
471                                         modified)
472                         }
473                         if !x.Before(lastChange.Truncate(time.Second)) {
474                                 w.WriteHeader(http.StatusNotModified)
475                                 return
476                         }
477                 }
478
479                 w.Header().Add("Last-Modified",
480                         lastChange.UTC().Format(http.TimeFormat))
481                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
482                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
483                 if change {
484                         fmt.Fprintln(w, "bin:x:2:2:bin:/bin:/usr/sbin/nologin")
485                 }
486         }
487
488         err = mainFetch(configPath)
489         if err != nil {
490                 t.Error(err)
491         }
492
493         mustNotExist(t, plainPath, groupPath)
494         mustBeNew(t, passwdPath, statePath)
495         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
496
497         t.Log("Fetch again, support for Last-Modified")
498
499         mustMakeOld(t, passwdPath, statePath)
500
501         err = mainFetch(configPath)
502         if err != nil {
503                 t.Error(err)
504         }
505
506         mustNotExist(t, plainPath, groupPath)
507         mustBeOld(t, passwdPath)
508         mustBeNew(t, statePath)
509         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
510
511         t.Log("Corrupt local passwd cache, fetched again")
512
513         os.Chmod(passwdPath, 0644) // make writable again
514         mustCreate(t, passwdPath)
515         mustMakeOld(t, passwdPath, statePath)
516
517         err = mainFetch(configPath)
518         if err != nil {
519                 t.Error(err)
520         }
521
522         mustNotExist(t, plainPath, groupPath)
523         mustBeNew(t, passwdPath, statePath)
524         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
525
526         t.Log("Fetch again with newer server response")
527
528         change = true
529         lastChange = time.Now().Add(time.Second)
530
531         mustMakeOld(t, passwdPath, statePath)
532
533         err = mainFetch(configPath)
534         if err != nil {
535                 t.Error(err)
536         }
537
538         mustNotExist(t, plainPath, groupPath)
539         mustBeNew(t, passwdPath, statePath)
540         mustHaveHash(t, passwdPath, "ca9c7477cb425667fc9ecbd79e8e1c2ad0e84423")
541 }
542
543 func fetchPlainEmpty(a args) {
544         t := a.t
545         mustWriteConfig(t, fmt.Sprintf(`
546 statepath = "%[1]s"
547
548 [[file]]
549 type = "plain"
550 url = "%[2]s/plain"
551 path = "%[3]s"
552 ca = "%[4]s"
553 `, statePath, a.url, plainPath, tlsCAPath))
554         mustCreate(t, plainPath)
555
556         *a.handler = func(w http.ResponseWriter, r *http.Request) {
557                 // Empty response
558         }
559
560         err := mainFetch(configPath)
561         mustBeErrorWithSubstring(t, err,
562                 "refusing to use empty response")
563
564         mustNotExist(t, statePath, passwdPath, groupPath)
565         mustBeOld(t, plainPath)
566 }
567
568 func fetchPlain(a args) {
569         t := a.t
570         mustWriteConfig(t, fmt.Sprintf(`
571 statepath = "%[1]s"
572
573 [[file]]
574 type = "plain"
575 url = "%[2]s/plain"
576 path = "%[3]s"
577 ca = "%[4]s"
578 `, statePath, a.url, plainPath, tlsCAPath))
579         mustCreate(t, plainPath)
580         mustHaveHash(t, plainPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
581
582         *a.handler = func(w http.ResponseWriter, r *http.Request) {
583                 if r.URL.Path != "/plain" {
584                         return
585                 }
586
587                 fmt.Fprintln(w, "some file")
588         }
589
590         err := mainFetch(configPath)
591         if err != nil {
592                 t.Error(err)
593         }
594
595         mustNotExist(t, passwdPath, groupPath)
596         mustBeNew(t, plainPath, statePath)
597         mustHaveHash(t, plainPath, "0e08b5e8c10abc3e455b75286ba4a1fbd56e18a5")
598
599         // Remaining functionality already tested in fetchPasswd()
600 }
601
602 func fetchGroupEmpty(a args) {
603         t := a.t
604         mustWriteGroupConfig(t, a.url)
605         mustCreate(t, groupPath)
606
607         *a.handler = func(w http.ResponseWriter, r *http.Request) {
608                 // Empty response
609         }
610
611         err := mainFetch(configPath)
612         mustBeErrorWithSubstring(t, err,
613                 "refusing to use empty group file")
614
615         mustNotExist(t, statePath, passwdPath, plainPath)
616         mustBeOld(t, groupPath)
617 }
618
619 func fetchGroupInvalid(a args) {
620         t := a.t
621         mustWriteGroupConfig(t, a.url)
622         mustCreate(t, groupPath)
623
624         *a.handler = func(w http.ResponseWriter, r *http.Request) {
625                 if r.URL.Path != "/group" {
626                         return
627                 }
628
629                 fmt.Fprintln(w, "root:x::")
630         }
631
632         err := mainFetch(configPath)
633         mustBeErrorWithSubstring(t, err,
634                 "invalid gid in line")
635
636         mustNotExist(t, statePath, passwdPath, plainPath)
637         mustBeOld(t, groupPath)
638 }
639
640 func fetchGroupLimits(a args) {
641         t := a.t
642         mustWriteGroupConfig(t, a.url)
643         mustCreate(t, groupPath)
644
645         *a.handler = func(w http.ResponseWriter, r *http.Request) {
646                 if r.URL.Path != "/group" {
647                         return
648                 }
649
650                 fmt.Fprint(w, "root:x:0:")
651                 for i := 0; i < 65536; i++ {
652                         fmt.Fprint(w, "x")
653                 }
654                 fmt.Fprint(w, "\n")
655         }
656
657         err := mainFetch(configPath)
658         mustBeErrorWithSubstring(t, err,
659                 "group too large to serialize")
660
661         mustNotExist(t, statePath, passwdPath, plainPath)
662         mustBeOld(t, groupPath)
663 }
664
665 func fetchGroup(a args) {
666         t := a.t
667         mustWriteGroupConfig(t, a.url)
668         mustCreate(t, groupPath)
669         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
670
671         *a.handler = func(w http.ResponseWriter, r *http.Request) {
672                 if r.URL.Path != "/group" {
673                         return
674                 }
675
676                 fmt.Fprintln(w, "root:x:0:")
677                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
678         }
679
680         err := mainFetch(configPath)
681         if err != nil {
682                 t.Error(err)
683         }
684
685         mustNotExist(t, passwdPath, plainPath)
686         mustBeNew(t, groupPath, statePath)
687         // The actual content of groupPath is verified by the NSS tests
688         mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
689
690         // Remaining functionality already tested in fetchPasswd()
691 }
692
693 func fetchNoConfig(a args) {
694         t := a.t
695
696         err := mainFetch(configPath)
697         mustBeErrorWithSubstring(t, err,
698                 configPath+": no such file or directory")
699
700         mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath)
701 }
702
703 func fetchStateCannotRead(a args) {
704         t := a.t
705         mustWritePasswdConfig(t, a.url)
706
707         mustCreate(t, statePath)
708         err := os.Chmod(statePath, 0000)
709         if err != nil {
710                 t.Fatal(err)
711         }
712
713         err = mainFetch(configPath)
714         mustBeErrorWithSubstring(t, err,
715                 statePath+": permission denied")
716
717         mustNotExist(t, passwdPath, plainPath, groupPath)
718         mustBeOld(t, statePath)
719 }
720
721 func fetchStateInvalid(a args) {
722         t := a.t
723         mustWriteGroupConfig(t, a.url)
724         mustCreate(t, statePath)
725
726         err := mainFetch(configPath)
727         mustBeErrorWithSubstring(t, err,
728                 "unexpected end of JSON input")
729
730         mustNotExist(t, groupPath, passwdPath, plainPath)
731         mustBeOld(t, statePath)
732 }
733
734 func fetchStateCannotWrite(a args) {
735         t := a.t
736         mustWriteGroupConfig(t, a.url)
737         mustCreate(t, groupPath)
738         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
739
740         *a.handler = func(w http.ResponseWriter, r *http.Request) {
741                 if r.URL.Path != "/group" {
742                         return
743                 }
744
745                 fmt.Fprintln(w, "root:x:0:")
746                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
747         }
748
749         err := os.Chmod(filepath.Dir(statePath), 0500)
750         if err != nil {
751                 t.Fatal(err)
752         }
753         defer os.Chmod(filepath.Dir(statePath), 0755)
754
755         err = mainFetch(configPath)
756         mustBeErrorWithSubstring(t, err,
757                 "permission denied")
758
759         mustNotExist(t, statePath, passwdPath, plainPath)
760         mustBeNew(t, groupPath)
761         mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
762 }
763
764 func fetchCannotDeploy(a args) {
765         t := a.t
766         mustWriteGroupConfig(t, a.url)
767         mustCreate(t, groupPath)
768         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
769
770         *a.handler = func(w http.ResponseWriter, r *http.Request) {
771                 if r.URL.Path != "/group" {
772                         return
773                 }
774
775                 fmt.Fprintln(w, "root:x:0:")
776                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
777         }
778
779         err := os.Chmod("testdata", 0500)
780         if err != nil {
781                 t.Fatal(err)
782         }
783         defer os.Chmod("testdata", 0755)
784
785         err = mainFetch(configPath)
786         mustBeErrorWithSubstring(t, err,
787                 "permission denied")
788
789         mustNotExist(t, statePath, passwdPath, plainPath)
790         mustBeOld(t, groupPath)
791 }
792
793 func fetchSecondFetchFails(a args) {
794         t := a.t
795         mustWriteConfig(t, fmt.Sprintf(`
796 statepath = "%[1]s"
797
798 [[file]]
799 type = "passwd"
800 url = "%[2]s/passwd"
801 path = "%[3]s"
802 ca = "%[5]s"
803
804 [[file]]
805 type = "group"
806 url = "%[2]s/group"
807 path = "%[4]s"
808 ca = "%[5]s"
809 `, statePath, a.url, passwdPath, groupPath, tlsCAPath))
810         mustCreate(t, passwdPath)
811         mustCreate(t, groupPath)
812         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
813         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
814
815         *a.handler = func(w http.ResponseWriter, r *http.Request) {
816                 if r.URL.Path == "/passwd" {
817                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
818                 }
819                 if r.URL.Path == "/group" {
820                         w.WriteHeader(http.StatusNotFound)
821                 }
822         }
823
824         err := mainFetch(configPath)
825         mustBeErrorWithSubstring(t, err,
826                 "status code 404")
827
828         mustNotExist(t, statePath, plainPath)
829         // Even though passwd was successfully fetched, no files were modified
830         // because the second fetch failed
831         mustBeOld(t, passwdPath, groupPath)
832 }
833
834 func fetchBasicAuth(a args) {
835         t := a.t
836         mustWritePasswdConfig(t, a.url)
837         mustCreate(t, passwdPath)
838         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
839
840         validUser := "username"
841         validPass := "password"
842
843         *a.handler = func(w http.ResponseWriter, r *http.Request) {
844                 if r.URL.Path != "/passwd" {
845                         return
846                 }
847
848                 user, pass, ok := r.BasicAuth()
849                 // NOTE: Do not use this in production because it permits
850                 // attackers to determine the length of user/pass. Instead use
851                 // hashes and subtle.ConstantTimeCompare().
852                 if !ok || user != validUser || pass != validPass {
853                         w.Header().Set("WWW-Authenticate", `Basic realm="Test"`)
854                         w.WriteHeader(http.StatusUnauthorized)
855                         return
856                 }
857
858                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
859                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
860         }
861
862         t.Log("Missing authentication")
863
864         err := mainFetch(configPath)
865         mustBeErrorWithSubstring(t, err,
866                 "status code 401")
867
868         mustNotExist(t, statePath, groupPath, plainPath)
869         mustBeOld(t, passwdPath)
870
871         t.Log("Unsafe config permissions")
872
873         mustWriteConfig(t, fmt.Sprintf(`
874 statepath = "%[1]s"
875
876 [[file]]
877 type = "passwd"
878 url = "%[2]s/passwd"
879 path = "%[3]s"
880 ca = "%[4]s"
881 username = "%[5]s"
882 password = "%[6]s"
883 `, statePath, a.url, passwdPath, tlsCAPath, validUser, validPass))
884
885         err = os.Chmod(configPath, 0644)
886         if err != nil {
887                 t.Fatal(err)
888         }
889
890         err = mainFetch(configPath)
891         mustBeErrorWithSubstring(t, err,
892                 "file[0].username/passsword in use and unsafe permissions "+
893                         "-rw-r--r-- on \"testdata/config.toml\"")
894
895         mustNotExist(t, statePath, groupPath, plainPath)
896         mustBeOld(t, passwdPath)
897
898         t.Log("Working authentication")
899
900         err = os.Chmod(configPath, 0600)
901         if err != nil {
902                 t.Fatal(err)
903         }
904
905         err = mainFetch(configPath)
906         if err != nil {
907                 t.Error(err)
908         }
909
910         mustNotExist(t, plainPath, groupPath)
911         mustBeNew(t, passwdPath, statePath)
912         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
913 }
914
915 func fetchInvalidCA(a args) {
916         t := a.t
917
918         // System CA
919
920         mustWriteConfig(t, fmt.Sprintf(`
921 statepath = "%[1]s"
922
923 [[file]]
924 type = "passwd"
925 url = "%[2]s/passwd"
926 path = "%[3]s"
927 `, statePath, a.url, passwdPath))
928         mustCreate(t, passwdPath)
929         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
930
931         *a.handler = func(w http.ResponseWriter, r *http.Request) {
932                 if r.URL.Path == "/passwd" {
933                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
934                 }
935         }
936
937         err := mainFetch(configPath)
938         mustBeErrorWithSubstring(t, err,
939                 "x509: certificate signed by unknown authority")
940
941         mustNotExist(t, statePath, plainPath, groupPath)
942         mustBeOld(t, passwdPath)
943
944         // Invalid CA
945
946         mustWriteConfig(t, fmt.Sprintf(`
947 statepath = "%[1]s"
948
949 [[file]]
950 type = "passwd"
951 url = "%[2]s/passwd"
952 path = "%[3]s"
953 ca = "%[4]s"
954 `, statePath, a.url, passwdPath, tlsCA2Path))
955         mustCreate(t, passwdPath)
956         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
957
958         *a.handler = func(w http.ResponseWriter, r *http.Request) {
959                 if r.URL.Path == "/passwd" {
960                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
961                 }
962         }
963
964         err = mainFetch(configPath)
965         mustBeErrorWithSubstring(t, err,
966                 "x509: certificate signed by unknown authority")
967
968         mustNotExist(t, statePath, plainPath, groupPath)
969         mustBeOld(t, passwdPath)
970 }