]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blob - main_test.go
.github: update upstream actions to latest version
[nsscash/nsscash.git] / main_test.go
1 // Copyright (C) 2019-2021  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                 if time.Since(mtime) < time.Hour {
170                         t.Errorf("%q was recently modified", p)
171                 }
172         }
173 }
174
175 // mustBeNew verifies that all paths have a modification time in the present.
176 func mustBeNew(t *testing.T, paths ...string) {
177         for _, p := range paths {
178                 i, err := os.Stat(p)
179                 if err != nil {
180                         t.Fatal(err)
181                 }
182
183                 mtime := i.ModTime()
184                 if time.Since(mtime) > time.Hour {
185                         t.Errorf("%q was not recently modified", p)
186                 }
187         }
188 }
189
190 func TestMainFetch(t *testing.T) {
191         // Suppress log messages
192         log.SetOutput(ioutil.Discard)
193         defer log.SetOutput(os.Stderr)
194
195         tests := []func(args){
196                 // Perform most tests with passwd for simplicity
197                 fetchPasswdCacheFileDoesNotExist,
198                 fetchPasswd404,
199                 fetchPasswdUnexpected304,
200                 fetchPasswdEmpty,
201                 fetchPasswdInvalid,
202                 fetchPasswdLimits,
203                 fetchPasswd,
204                 // Tests for plain and group
205                 fetchPlainEmpty,
206                 fetchPlain,
207                 fetchGroupEmpty,
208                 fetchGroupInvalid,
209                 fetchGroupLimits,
210                 fetchGroup,
211                 // Special tests
212                 fetchNoConfig,
213                 fetchStateCannotRead,
214                 fetchStateInvalid,
215                 fetchStateCannotWrite,
216                 fetchCannotDeploy,
217                 fetchSecondFetchFails,
218                 fetchBasicAuth,
219                 // TODO: fetchCannotDeployMultiple,
220         }
221
222         // HTTP tests
223
224         for _, f := range tests {
225                 runMainTest(t, f, nil)
226         }
227
228         // HTTPS tests
229
230         tests = append(tests, fetchInvalidCA)
231
232         cert, err := tls.LoadX509KeyPair(tlsCertPath, tlsKeyPath)
233         if err != nil {
234                 t.Fatal(err)
235         }
236         tls := &tls.Config{
237                 Certificates: []tls.Certificate{cert},
238         }
239
240         for _, f := range tests {
241                 runMainTest(t, f, tls)
242         }
243 }
244
245 func runMainTest(t *testing.T, f func(args), tls *tls.Config) {
246         cleanup := []string{
247                 configPath,
248                 statePath,
249                 passwdPath,
250                 plainPath,
251                 groupPath,
252         }
253
254         // NOTE: This is not guaranteed to work according to reflect's
255         // documentation but seems to work reliable for normal functions.
256         fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
257         name := fn.Name()
258         name = name[strings.LastIndex(name, ".")+1:]
259         if tls != nil {
260                 name = "tls" + name
261         }
262
263         t.Run(name, func(t *testing.T) {
264                 // Preparation & cleanup
265                 for _, p := range cleanup {
266                         err := os.Remove(p)
267                         if err != nil && !os.IsNotExist(err) {
268                                 t.Fatal(err)
269                         }
270                         // Remove the file at the end of this test run, if it
271                         // was created
272                         defer os.Remove(p)
273
274                         dir := filepath.Dir(p)
275                         err = os.MkdirAll(dir, 0755)
276                         if err != nil {
277                                 t.Fatal(err)
278                         }
279                         defer os.Remove(dir) // remove empty directories
280                 }
281
282                 var handler func(http.ResponseWriter, *http.Request)
283                 ts := httptest.NewUnstartedServer(http.HandlerFunc(
284                         func(w http.ResponseWriter, r *http.Request) {
285                                 handler(w, r)
286                         }))
287                 if tls == nil {
288                         ts.Start()
289                 } else {
290                         ts.TLS = tls
291                         ts.StartTLS()
292                 }
293                 defer ts.Close()
294
295                 f(args{
296                         t:       t,
297                         url:     ts.URL,
298                         handler: &handler,
299                 })
300         })
301 }
302
303 func fetchPasswdCacheFileDoesNotExist(a args) {
304         t := a.t
305         mustWritePasswdConfig(t, a.url)
306
307         err := mainFetch(configPath)
308         mustBeErrorWithSubstring(t, err,
309                 "file.path \""+passwdPath+"\" must exist")
310
311         mustNotExist(t, statePath, passwdPath, plainPath, groupPath)
312 }
313
314 func fetchPasswd404(a args) {
315         t := a.t
316         mustWritePasswdConfig(t, a.url)
317         mustCreate(t, passwdPath)
318
319         *a.handler = func(w http.ResponseWriter, r *http.Request) {
320                 // 404
321                 w.WriteHeader(http.StatusNotFound)
322         }
323
324         err := mainFetch(configPath)
325         mustBeErrorWithSubstring(t, err,
326                 "status code 404")
327
328         mustNotExist(t, statePath, plainPath, groupPath)
329         mustBeOld(t, passwdPath)
330 }
331
332 func fetchPasswdUnexpected304(a args) {
333         t := a.t
334         mustWritePasswdConfig(t, a.url)
335         mustCreate(t, passwdPath)
336
337         *a.handler = func(w http.ResponseWriter, r *http.Request) {
338                 // 304
339                 w.WriteHeader(http.StatusNotModified)
340         }
341
342         err := mainFetch(configPath)
343         mustBeErrorWithSubstring(t, err,
344                 "status code 304 but did not send If-Modified-Since")
345
346         mustNotExist(t, statePath, plainPath, groupPath)
347         mustBeOld(t, passwdPath)
348 }
349
350 func fetchPasswdEmpty(a args) {
351         t := a.t
352         mustWritePasswdConfig(t, a.url)
353         mustCreate(t, passwdPath)
354
355         *a.handler = func(w http.ResponseWriter, r *http.Request) {
356                 // Empty response
357         }
358
359         err := mainFetch(configPath)
360         mustBeErrorWithSubstring(t, err,
361                 "refusing to use empty passwd file")
362
363         mustNotExist(t, statePath, plainPath, groupPath)
364         mustBeOld(t, passwdPath)
365 }
366
367 func fetchPasswdInvalid(a args) {
368         t := a.t
369         mustWritePasswdConfig(t, a.url)
370         mustCreate(t, passwdPath)
371
372         *a.handler = func(w http.ResponseWriter, r *http.Request) {
373                 if r.URL.Path != "/passwd" {
374                         return
375                 }
376
377                 fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash")
378         }
379
380         err := mainFetch(configPath)
381         mustBeErrorWithSubstring(t, err,
382                 "invalid uid in line")
383
384         mustNotExist(t, statePath, plainPath, groupPath)
385         mustBeOld(t, passwdPath)
386 }
387
388 func fetchPasswdLimits(a args) {
389         t := a.t
390         mustWritePasswdConfig(t, a.url)
391         mustCreate(t, passwdPath)
392
393         *a.handler = func(w http.ResponseWriter, r *http.Request) {
394                 if r.URL.Path != "/passwd" {
395                         return
396                 }
397
398                 fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash")
399                 for i := 0; i < 65536; i++ {
400                         fmt.Fprint(w, "x")
401                 }
402                 fmt.Fprint(w, "\n")
403         }
404
405         err := mainFetch(configPath)
406         mustBeErrorWithSubstring(t, err,
407                 "passwd too large to serialize")
408
409         mustNotExist(t, statePath, plainPath, groupPath)
410         mustBeOld(t, passwdPath)
411 }
412
413 func fetchPasswd(a args) {
414         t := a.t
415         mustWritePasswdConfig(t, a.url)
416         mustCreate(t, passwdPath)
417         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
418
419         t.Log("First fetch, write files")
420
421         *a.handler = func(w http.ResponseWriter, r *http.Request) {
422                 if r.URL.Path != "/passwd" {
423                         return
424                 }
425
426                 // No "Last-Modified" header
427                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
428                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
429         }
430
431         err := mainFetch(configPath)
432         if err != nil {
433                 t.Error(err)
434         }
435
436         mustNotExist(t, plainPath, groupPath)
437         mustBeNew(t, passwdPath, statePath)
438         // The actual content of passwdPath is verified by the NSS tests
439         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
440
441         t.Log("Fetch again, no support for Last-Modified")
442
443         mustMakeOld(t, passwdPath, statePath)
444
445         err = mainFetch(configPath)
446         if err != nil {
447                 t.Error(err)
448         }
449
450         mustNotExist(t, plainPath, groupPath)
451         mustBeNew(t, passwdPath, statePath)
452         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
453
454         t.Log("Fetch again, support for Last-Modified, but not retrieved yet")
455
456         mustMakeOld(t, passwdPath, statePath)
457
458         lastChange := time.Now()
459         change := false
460         *a.handler = func(w http.ResponseWriter, r *http.Request) {
461                 if r.URL.Path != "/passwd" {
462                         return
463                 }
464
465                 modified := r.Header.Get("If-Modified-Since")
466                 if modified != "" {
467                         x, err := http.ParseTime(modified)
468                         if err != nil {
469                                 t.Fatalf("invalid If-Modified-Since %v",
470                                         modified)
471                         }
472                         if !x.Before(lastChange.Truncate(time.Second)) {
473                                 w.WriteHeader(http.StatusNotModified)
474                                 return
475                         }
476                 }
477
478                 w.Header().Add("Last-Modified",
479                         lastChange.UTC().Format(http.TimeFormat))
480                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
481                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
482                 if change {
483                         fmt.Fprintln(w, "bin:x:2:2:bin:/bin:/usr/sbin/nologin")
484                 }
485         }
486
487         err = mainFetch(configPath)
488         if err != nil {
489                 t.Error(err)
490         }
491
492         mustNotExist(t, plainPath, groupPath)
493         mustBeNew(t, passwdPath, statePath)
494         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
495
496         t.Log("Fetch again, support for Last-Modified")
497
498         mustMakeOld(t, passwdPath, statePath)
499
500         err = mainFetch(configPath)
501         if err != nil {
502                 t.Error(err)
503         }
504
505         mustNotExist(t, plainPath, groupPath)
506         mustBeOld(t, passwdPath)
507         mustBeNew(t, statePath)
508         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
509
510         t.Log("Corrupt local passwd cache, fetched again")
511
512         os.Chmod(passwdPath, 0644) // make writable again
513         mustCreate(t, passwdPath)
514         mustMakeOld(t, passwdPath, statePath)
515
516         err = mainFetch(configPath)
517         if err != nil {
518                 t.Error(err)
519         }
520
521         mustNotExist(t, plainPath, groupPath)
522         mustBeNew(t, passwdPath, statePath)
523         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
524
525         t.Log("Fetch again with newer server response")
526
527         change = true
528         lastChange = time.Now().Add(time.Second)
529
530         mustMakeOld(t, passwdPath, statePath)
531
532         err = mainFetch(configPath)
533         if err != nil {
534                 t.Error(err)
535         }
536
537         mustNotExist(t, plainPath, groupPath)
538         mustBeNew(t, passwdPath, statePath)
539         mustHaveHash(t, passwdPath, "ca9c7477cb425667fc9ecbd79e8e1c2ad0e84423")
540 }
541
542 func fetchPlainEmpty(a args) {
543         t := a.t
544         mustWriteConfig(t, fmt.Sprintf(`
545 statepath = "%[1]s"
546
547 [[file]]
548 type = "plain"
549 url = "%[2]s/plain"
550 path = "%[3]s"
551 ca = "%[4]s"
552 `, statePath, a.url, plainPath, tlsCAPath))
553         mustCreate(t, plainPath)
554
555         *a.handler = func(w http.ResponseWriter, r *http.Request) {
556                 // Empty response
557         }
558
559         err := mainFetch(configPath)
560         mustBeErrorWithSubstring(t, err,
561                 "refusing to use empty response")
562
563         mustNotExist(t, statePath, passwdPath, groupPath)
564         mustBeOld(t, plainPath)
565 }
566
567 func fetchPlain(a args) {
568         t := a.t
569         mustWriteConfig(t, fmt.Sprintf(`
570 statepath = "%[1]s"
571
572 [[file]]
573 type = "plain"
574 url = "%[2]s/plain"
575 path = "%[3]s"
576 ca = "%[4]s"
577 `, statePath, a.url, plainPath, tlsCAPath))
578         mustCreate(t, plainPath)
579         mustHaveHash(t, plainPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
580
581         *a.handler = func(w http.ResponseWriter, r *http.Request) {
582                 if r.URL.Path != "/plain" {
583                         return
584                 }
585
586                 fmt.Fprintln(w, "some file")
587         }
588
589         err := mainFetch(configPath)
590         if err != nil {
591                 t.Error(err)
592         }
593
594         mustNotExist(t, passwdPath, groupPath)
595         mustBeNew(t, plainPath, statePath)
596         mustHaveHash(t, plainPath, "0e08b5e8c10abc3e455b75286ba4a1fbd56e18a5")
597
598         // Remaining functionality already tested in fetchPasswd()
599 }
600
601 func fetchGroupEmpty(a args) {
602         t := a.t
603         mustWriteGroupConfig(t, a.url)
604         mustCreate(t, groupPath)
605
606         *a.handler = func(w http.ResponseWriter, r *http.Request) {
607                 // Empty response
608         }
609
610         err := mainFetch(configPath)
611         mustBeErrorWithSubstring(t, err,
612                 "refusing to use empty group file")
613
614         mustNotExist(t, statePath, passwdPath, plainPath)
615         mustBeOld(t, groupPath)
616 }
617
618 func fetchGroupInvalid(a args) {
619         t := a.t
620         mustWriteGroupConfig(t, a.url)
621         mustCreate(t, groupPath)
622
623         *a.handler = func(w http.ResponseWriter, r *http.Request) {
624                 if r.URL.Path != "/group" {
625                         return
626                 }
627
628                 fmt.Fprintln(w, "root:x::")
629         }
630
631         err := mainFetch(configPath)
632         mustBeErrorWithSubstring(t, err,
633                 "invalid gid in line")
634
635         mustNotExist(t, statePath, passwdPath, plainPath)
636         mustBeOld(t, groupPath)
637 }
638
639 func fetchGroupLimits(a args) {
640         t := a.t
641         mustWriteGroupConfig(t, a.url)
642         mustCreate(t, groupPath)
643
644         *a.handler = func(w http.ResponseWriter, r *http.Request) {
645                 if r.URL.Path != "/group" {
646                         return
647                 }
648
649                 fmt.Fprint(w, "root:x:0:")
650                 for i := 0; i < 65536; i++ {
651                         fmt.Fprint(w, "x")
652                 }
653                 fmt.Fprint(w, "\n")
654         }
655
656         err := mainFetch(configPath)
657         mustBeErrorWithSubstring(t, err,
658                 "group too large to serialize")
659
660         mustNotExist(t, statePath, passwdPath, plainPath)
661         mustBeOld(t, groupPath)
662 }
663
664 func fetchGroup(a args) {
665         t := a.t
666         mustWriteGroupConfig(t, a.url)
667         mustCreate(t, groupPath)
668         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
669
670         *a.handler = func(w http.ResponseWriter, r *http.Request) {
671                 if r.URL.Path != "/group" {
672                         return
673                 }
674
675                 fmt.Fprintln(w, "root:x:0:")
676                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
677         }
678
679         err := mainFetch(configPath)
680         if err != nil {
681                 t.Error(err)
682         }
683
684         mustNotExist(t, passwdPath, plainPath)
685         mustBeNew(t, groupPath, statePath)
686         // The actual content of groupPath is verified by the NSS tests
687         mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
688
689         // Remaining functionality already tested in fetchPasswd()
690 }
691
692 func fetchNoConfig(a args) {
693         t := a.t
694
695         err := mainFetch(configPath)
696         mustBeErrorWithSubstring(t, err,
697                 configPath+": no such file or directory")
698
699         mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath)
700 }
701
702 func fetchStateCannotRead(a args) {
703         t := a.t
704         mustWritePasswdConfig(t, a.url)
705
706         mustCreate(t, statePath)
707         err := os.Chmod(statePath, 0000)
708         if err != nil {
709                 t.Fatal(err)
710         }
711
712         err = mainFetch(configPath)
713         mustBeErrorWithSubstring(t, err,
714                 statePath+": permission denied")
715
716         mustNotExist(t, passwdPath, plainPath, groupPath)
717         mustBeOld(t, statePath)
718 }
719
720 func fetchStateInvalid(a args) {
721         t := a.t
722         mustWriteGroupConfig(t, a.url)
723         mustCreate(t, statePath)
724
725         err := mainFetch(configPath)
726         mustBeErrorWithSubstring(t, err,
727                 "unexpected end of JSON input")
728
729         mustNotExist(t, groupPath, passwdPath, plainPath)
730         mustBeOld(t, statePath)
731 }
732
733 func fetchStateCannotWrite(a args) {
734         t := a.t
735         mustWriteGroupConfig(t, a.url)
736         mustCreate(t, groupPath)
737         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
738
739         *a.handler = func(w http.ResponseWriter, r *http.Request) {
740                 if r.URL.Path != "/group" {
741                         return
742                 }
743
744                 fmt.Fprintln(w, "root:x:0:")
745                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
746         }
747
748         err := os.Chmod(filepath.Dir(statePath), 0500)
749         if err != nil {
750                 t.Fatal(err)
751         }
752         defer os.Chmod(filepath.Dir(statePath), 0755)
753
754         err = mainFetch(configPath)
755         mustBeErrorWithSubstring(t, err,
756                 "permission denied")
757
758         mustNotExist(t, statePath, passwdPath, plainPath)
759         mustBeNew(t, groupPath)
760         mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
761 }
762
763 func fetchCannotDeploy(a args) {
764         t := a.t
765         mustWriteGroupConfig(t, a.url)
766         mustCreate(t, groupPath)
767         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
768
769         *a.handler = func(w http.ResponseWriter, r *http.Request) {
770                 if r.URL.Path != "/group" {
771                         return
772                 }
773
774                 fmt.Fprintln(w, "root:x:0:")
775                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
776         }
777
778         err := os.Chmod("testdata", 0500)
779         if err != nil {
780                 t.Fatal(err)
781         }
782         defer os.Chmod("testdata", 0755)
783
784         err = mainFetch(configPath)
785         mustBeErrorWithSubstring(t, err,
786                 "permission denied")
787
788         mustNotExist(t, statePath, passwdPath, plainPath)
789         mustBeOld(t, groupPath)
790 }
791
792 func fetchSecondFetchFails(a args) {
793         t := a.t
794         mustWriteConfig(t, fmt.Sprintf(`
795 statepath = "%[1]s"
796
797 [[file]]
798 type = "passwd"
799 url = "%[2]s/passwd"
800 path = "%[3]s"
801 ca = "%[5]s"
802
803 [[file]]
804 type = "group"
805 url = "%[2]s/group"
806 path = "%[4]s"
807 ca = "%[5]s"
808 `, statePath, a.url, passwdPath, groupPath, tlsCAPath))
809         mustCreate(t, passwdPath)
810         mustCreate(t, groupPath)
811         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
812         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
813
814         *a.handler = func(w http.ResponseWriter, r *http.Request) {
815                 if r.URL.Path == "/passwd" {
816                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
817                 }
818                 if r.URL.Path == "/group" {
819                         w.WriteHeader(http.StatusNotFound)
820                 }
821         }
822
823         err := mainFetch(configPath)
824         mustBeErrorWithSubstring(t, err,
825                 "status code 404")
826
827         mustNotExist(t, statePath, plainPath)
828         // Even though passwd was successfully fetched, no files were modified
829         // because the second fetch failed
830         mustBeOld(t, passwdPath, groupPath)
831 }
832
833 func fetchBasicAuth(a args) {
834         t := a.t
835         mustWritePasswdConfig(t, a.url)
836         mustCreate(t, passwdPath)
837         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
838
839         validUser := "username"
840         validPass := "password"
841
842         *a.handler = func(w http.ResponseWriter, r *http.Request) {
843                 if r.URL.Path != "/passwd" {
844                         return
845                 }
846
847                 user, pass, ok := r.BasicAuth()
848                 // NOTE: Do not use this in production because it permits
849                 // attackers to determine the length of user/pass. Instead use
850                 // hashes and subtle.ConstantTimeCompare().
851                 if !ok || user != validUser || pass != validPass {
852                         w.Header().Set("WWW-Authenticate", `Basic realm="Test"`)
853                         w.WriteHeader(http.StatusUnauthorized)
854                         return
855                 }
856
857                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
858                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
859         }
860
861         t.Log("Missing authentication")
862
863         err := mainFetch(configPath)
864         mustBeErrorWithSubstring(t, err,
865                 "status code 401")
866
867         mustNotExist(t, statePath, groupPath, plainPath)
868         mustBeOld(t, passwdPath)
869
870         t.Log("Unsafe config permissions")
871
872         mustWriteConfig(t, fmt.Sprintf(`
873 statepath = "%[1]s"
874
875 [[file]]
876 type = "passwd"
877 url = "%[2]s/passwd"
878 path = "%[3]s"
879 ca = "%[4]s"
880 username = "%[5]s"
881 password = "%[6]s"
882 `, statePath, a.url, passwdPath, tlsCAPath, validUser, validPass))
883
884         err = os.Chmod(configPath, 0644)
885         if err != nil {
886                 t.Fatal(err)
887         }
888
889         err = mainFetch(configPath)
890         mustBeErrorWithSubstring(t, err,
891                 "file[0].username/passsword in use and unsafe permissions "+
892                         "-rw-r--r-- on \""+configPath+"\"")
893
894         mustNotExist(t, statePath, groupPath, plainPath)
895         mustBeOld(t, passwdPath)
896
897         t.Log("Working authentication")
898
899         err = os.Chmod(configPath, 0600)
900         if err != nil {
901                 t.Fatal(err)
902         }
903
904         err = mainFetch(configPath)
905         if err != nil {
906                 t.Error(err)
907         }
908
909         mustNotExist(t, plainPath, groupPath)
910         mustBeNew(t, passwdPath, statePath)
911         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
912 }
913
914 func fetchInvalidCA(a args) {
915         t := a.t
916
917         // System CA
918
919         mustWriteConfig(t, fmt.Sprintf(`
920 statepath = "%[1]s"
921
922 [[file]]
923 type = "passwd"
924 url = "%[2]s/passwd"
925 path = "%[3]s"
926 `, statePath, a.url, passwdPath))
927         mustCreate(t, passwdPath)
928         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
929
930         *a.handler = func(w http.ResponseWriter, r *http.Request) {
931                 if r.URL.Path == "/passwd" {
932                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
933                 }
934         }
935
936         err := mainFetch(configPath)
937         mustBeErrorWithSubstring(t, err,
938                 "x509: certificate signed by unknown authority")
939
940         mustNotExist(t, statePath, plainPath, groupPath)
941         mustBeOld(t, passwdPath)
942
943         // Invalid CA
944
945         mustWriteConfig(t, fmt.Sprintf(`
946 statepath = "%[1]s"
947
948 [[file]]
949 type = "passwd"
950 url = "%[2]s/passwd"
951 path = "%[3]s"
952 ca = "%[4]s"
953 `, statePath, a.url, passwdPath, tlsCA2Path))
954         mustCreate(t, passwdPath)
955         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
956
957         *a.handler = func(w http.ResponseWriter, r *http.Request) {
958                 if r.URL.Path == "/passwd" {
959                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
960                 }
961         }
962
963         err = mainFetch(configPath)
964         mustBeErrorWithSubstring(t, err,
965                 "x509: certificate signed by unknown authority")
966
967         mustNotExist(t, statePath, plainPath, groupPath)
968         mustBeOld(t, passwdPath)
969 }
970
971 /*
972 TODO: implement code for this test
973
974 func fetchCannotDeployMultiple(a args) {
975         t := a.t
976         newPlainDir := "testdata/x"
977         newPlainPath := newPlainDir + "/plain"
978         mustWriteConfig(t, fmt.Sprintf(`
979 statepath = "%[1]s"
980
981 [[file]]
982 type = "group"
983 url = "%[2]s/group"
984 path = "%[3]s"
985
986 [[file]]
987 type = "plain"
988 url = "%[2]s/plain"
989 path = "%[4]s"
990 `, statePath, a.url, groupPath, newPlainPath))
991         os.Mkdir(newPlainDir, 0755)
992         defer os.RemoveAll(newPlainDir)
993         mustCreate(t, groupPath)
994         mustCreate(t, newPlainPath)
995         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
996
997         *a.handler = func(w http.ResponseWriter, r *http.Request) {
998                 if r.URL.Path == "/group" {
999                         fmt.Fprintln(w, "root:x:0:")
1000                 }
1001                 if r.URL.Path == "/plain" {
1002                         fmt.Fprintln(w, "some file")
1003                 }
1004         }
1005
1006         err := os.Chmod(newPlainDir, 0500)
1007         if err != nil {
1008                 t.Fatal(err)
1009         }
1010
1011         err = mainFetch(configPath)
1012         mustBeErrorWithSubstring(t, err,
1013                 "permission denied")
1014
1015         mustNotExist(t, statePath, passwdPath, plainPath)
1016         mustBeOld(t, groupPath, newPlainPath)
1017 }
1018 */