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