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