]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blob - main_test.go
nsscash: main_test: create state file in sub-directory
[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 // mustMakeOld verifies that all paths have a modification time in the past,
160 // as set by mustMakeOld().
161 func mustBeOld(t *testing.T, paths ...string) {
162         for _, p := range paths {
163                 i, err := os.Stat(p)
164                 if err != nil {
165                         t.Fatal(err)
166                 }
167
168                 mtime := i.ModTime()
169                 now := time.Now()
170                 if now.Sub(mtime) < time.Hour {
171                         t.Errorf("%q was recently modified", p)
172                 }
173         }
174 }
175
176 // mustBeNew verifies that all paths have a modification time in the present.
177 func mustBeNew(t *testing.T, paths ...string) {
178         for _, p := range paths {
179                 i, err := os.Stat(p)
180                 if err != nil {
181                         t.Fatal(err)
182                 }
183
184                 mtime := i.ModTime()
185                 now := time.Now()
186                 if now.Sub(mtime) > time.Hour {
187                         t.Errorf("%q was not recently modified", p)
188                 }
189         }
190 }
191
192 func TestMainFetch(t *testing.T) {
193         // Suppress log messages
194         log.SetOutput(ioutil.Discard)
195         defer log.SetOutput(os.Stderr)
196
197         tests := []func(args){
198                 // Perform most tests with passwd for simplicity
199                 fetchPasswdCacheFileDoesNotExist,
200                 fetchPasswd404,
201                 fetchPasswdEmpty,
202                 fetchPasswdInvalid,
203                 fetchPasswdLimits,
204                 fetchPasswd,
205                 // Tests for plain and group
206                 fetchPlainEmpty,
207                 fetchPlain,
208                 fetchGroupEmpty,
209                 fetchGroupInvalid,
210                 fetchGroupLimits,
211                 fetchGroup,
212                 // Special tests
213                 fetchNoConfig,
214                 fetchStateCannotRead,
215                 fetchStateInvalid,
216                 fetchStateCannotWrite,
217                 fetchCannotDeploy,
218                 fetchSecondFetchFails,
219                 fetchBasicAuth,
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(a.t, passwdPath)
330 }
331
332 func fetchPasswdEmpty(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                 // Empty response
339         }
340
341         err := mainFetch(configPath)
342         mustBeErrorWithSubstring(t, err,
343                 "refusing to use empty passwd file")
344
345         mustNotExist(t, statePath, plainPath, groupPath)
346         mustBeOld(t, passwdPath)
347 }
348
349 func fetchPasswdInvalid(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                 if r.URL.Path != "/passwd" {
356                         return
357                 }
358
359                 fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash")
360         }
361
362         err := mainFetch(configPath)
363         mustBeErrorWithSubstring(t, err,
364                 "invalid uid in line")
365
366         mustNotExist(t, statePath, plainPath, groupPath)
367         mustBeOld(t, passwdPath)
368 }
369
370 func fetchPasswdLimits(a args) {
371         t := a.t
372         mustWritePasswdConfig(t, a.url)
373         mustCreate(t, passwdPath)
374
375         *a.handler = func(w http.ResponseWriter, r *http.Request) {
376                 if r.URL.Path != "/passwd" {
377                         return
378                 }
379
380                 fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash")
381                 for i := 0; i < 65536; i++ {
382                         fmt.Fprint(w, "x")
383                 }
384                 fmt.Fprint(w, "\n")
385         }
386
387         err := mainFetch(configPath)
388         mustBeErrorWithSubstring(t, err,
389                 "passwd too large to serialize")
390
391         mustNotExist(t, statePath, plainPath, groupPath)
392         mustBeOld(t, passwdPath)
393 }
394
395 func fetchPasswd(a args) {
396         t := a.t
397         mustWritePasswdConfig(t, a.url)
398         mustCreate(t, passwdPath)
399         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
400
401         t.Log("First fetch, write files")
402
403         *a.handler = func(w http.ResponseWriter, r *http.Request) {
404                 if r.URL.Path != "/passwd" {
405                         return
406                 }
407
408                 // No "Last-Modified" header
409                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
410                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
411         }
412
413         err := mainFetch(configPath)
414         if err != nil {
415                 t.Error(err)
416         }
417
418         mustNotExist(t, plainPath, groupPath)
419         mustBeNew(t, passwdPath, statePath)
420         // The actual content of passwdPath is verified by the NSS tests
421         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
422
423         t.Log("Fetch again, no support for Last-Modified")
424
425         mustMakeOld(t, passwdPath, statePath)
426
427         err = mainFetch(configPath)
428         if err != nil {
429                 t.Error(err)
430         }
431
432         mustNotExist(t, plainPath, groupPath)
433         mustBeNew(t, passwdPath, statePath)
434         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
435
436         t.Log("Fetch again, support for Last-Modified, but not retrieved yet")
437
438         mustMakeOld(t, passwdPath, statePath)
439
440         lastChange := time.Now()
441         change := false
442         *a.handler = func(w http.ResponseWriter, r *http.Request) {
443                 if r.URL.Path != "/passwd" {
444                         return
445                 }
446
447                 modified := r.Header.Get("If-Modified-Since")
448                 if modified != "" {
449                         x, err := http.ParseTime(modified)
450                         if err != nil {
451                                 t.Fatalf("invalid If-Modified-Since %v",
452                                         modified)
453                         }
454                         if !x.Before(lastChange.Truncate(time.Second)) {
455                                 w.WriteHeader(http.StatusNotModified)
456                                 return
457                         }
458                 }
459
460                 w.Header().Add("Last-Modified",
461                         lastChange.UTC().Format(http.TimeFormat))
462                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
463                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
464                 if change {
465                         fmt.Fprintln(w, "bin:x:2:2:bin:/bin:/usr/sbin/nologin")
466                 }
467         }
468
469         err = mainFetch(configPath)
470         if err != nil {
471                 t.Error(err)
472         }
473
474         mustNotExist(t, plainPath, groupPath)
475         mustBeNew(t, passwdPath, statePath)
476         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
477
478         t.Log("Fetch again, support for Last-Modified")
479
480         mustMakeOld(t, passwdPath, statePath)
481
482         err = mainFetch(configPath)
483         if err != nil {
484                 t.Error(err)
485         }
486
487         mustNotExist(t, plainPath, groupPath)
488         mustBeOld(t, passwdPath)
489         mustBeNew(t, statePath)
490         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
491
492         t.Log("Corrupt local passwd cache, fetched again")
493
494         os.Chmod(passwdPath, 0644) // make writable again
495         mustCreate(t, passwdPath)
496         mustMakeOld(t, passwdPath, statePath)
497
498         err = mainFetch(configPath)
499         if err != nil {
500                 t.Error(err)
501         }
502
503         mustNotExist(t, plainPath, groupPath)
504         mustBeNew(t, passwdPath, statePath)
505         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
506
507         t.Log("Fetch again with newer server response")
508
509         change = true
510         lastChange = time.Now().Add(time.Second)
511
512         mustMakeOld(t, passwdPath, statePath)
513
514         err = mainFetch(configPath)
515         if err != nil {
516                 t.Error(err)
517         }
518
519         mustNotExist(t, plainPath, groupPath)
520         mustBeNew(t, passwdPath, statePath)
521         mustHaveHash(t, passwdPath, "ca9c7477cb425667fc9ecbd79e8e1c2ad0e84423")
522 }
523
524 func fetchPlainEmpty(a args) {
525         t := a.t
526         mustWriteConfig(t, fmt.Sprintf(`
527 statepath = "%[1]s"
528
529 [[file]]
530 type = "plain"
531 url = "%[2]s/plain"
532 path = "%[3]s"
533 ca = "%[4]s"
534 `, statePath, a.url, plainPath, tlsCAPath))
535         mustCreate(t, plainPath)
536
537         *a.handler = func(w http.ResponseWriter, r *http.Request) {
538                 // Empty response
539         }
540
541         err := mainFetch(configPath)
542         mustBeErrorWithSubstring(t, err,
543                 "refusing to use empty response")
544
545         mustNotExist(t, statePath, passwdPath, groupPath)
546         mustBeOld(t, plainPath)
547 }
548
549 func fetchPlain(a args) {
550         t := a.t
551         mustWriteConfig(t, fmt.Sprintf(`
552 statepath = "%[1]s"
553
554 [[file]]
555 type = "plain"
556 url = "%[2]s/plain"
557 path = "%[3]s"
558 ca = "%[4]s"
559 `, statePath, a.url, plainPath, tlsCAPath))
560         mustCreate(t, plainPath)
561         mustHaveHash(t, plainPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
562
563         *a.handler = func(w http.ResponseWriter, r *http.Request) {
564                 if r.URL.Path != "/plain" {
565                         return
566                 }
567
568                 fmt.Fprintln(w, "some file")
569         }
570
571         err := mainFetch(configPath)
572         if err != nil {
573                 t.Error(err)
574         }
575
576         mustNotExist(t, passwdPath, groupPath)
577         mustBeNew(t, plainPath, statePath)
578         mustHaveHash(t, plainPath, "0e08b5e8c10abc3e455b75286ba4a1fbd56e18a5")
579
580         // Remaining functionality already tested in fetchPasswd()
581 }
582
583 func fetchGroupEmpty(a args) {
584         t := a.t
585         mustWriteGroupConfig(t, a.url)
586         mustCreate(t, groupPath)
587
588         *a.handler = func(w http.ResponseWriter, r *http.Request) {
589                 // Empty response
590         }
591
592         err := mainFetch(configPath)
593         mustBeErrorWithSubstring(t, err,
594                 "refusing to use empty group file")
595
596         mustNotExist(t, statePath, passwdPath, plainPath)
597         mustBeOld(t, groupPath)
598 }
599
600 func fetchGroupInvalid(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                 if r.URL.Path != "/group" {
607                         return
608                 }
609
610                 fmt.Fprintln(w, "root:x::")
611         }
612
613         err := mainFetch(configPath)
614         mustBeErrorWithSubstring(t, err,
615                 "invalid gid in line")
616
617         mustNotExist(t, statePath, passwdPath, plainPath)
618         mustBeOld(t, groupPath)
619 }
620
621 func fetchGroupLimits(a args) {
622         t := a.t
623         mustWriteGroupConfig(t, a.url)
624         mustCreate(t, groupPath)
625
626         *a.handler = func(w http.ResponseWriter, r *http.Request) {
627                 if r.URL.Path != "/group" {
628                         return
629                 }
630
631                 fmt.Fprint(w, "root:x:0:")
632                 for i := 0; i < 65536; i++ {
633                         fmt.Fprint(w, "x")
634                 }
635                 fmt.Fprint(w, "\n")
636         }
637
638         err := mainFetch(configPath)
639         mustBeErrorWithSubstring(t, err,
640                 "group too large to serialize")
641
642         mustNotExist(t, statePath, passwdPath, plainPath)
643         mustBeOld(t, groupPath)
644 }
645
646 func fetchGroup(a args) {
647         t := a.t
648         mustWriteGroupConfig(t, a.url)
649         mustCreate(t, groupPath)
650         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
651
652         *a.handler = func(w http.ResponseWriter, r *http.Request) {
653                 if r.URL.Path != "/group" {
654                         return
655                 }
656
657                 fmt.Fprintln(w, "root:x:0:")
658                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
659         }
660
661         err := mainFetch(configPath)
662         if err != nil {
663                 t.Error(err)
664         }
665
666         mustNotExist(t, passwdPath, plainPath)
667         mustBeNew(t, groupPath, statePath)
668         // The actual content of groupPath is verified by the NSS tests
669         mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
670
671         // Remaining functionality already tested in fetchPasswd()
672 }
673
674 func fetchNoConfig(a args) {
675         t := a.t
676
677         err := mainFetch(configPath)
678         mustBeErrorWithSubstring(t, err,
679                 configPath+": no such file or directory")
680
681         mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath)
682 }
683
684 func fetchStateCannotRead(a args) {
685         t := a.t
686         mustWritePasswdConfig(t, a.url)
687
688         mustCreate(t, statePath)
689         err := os.Chmod(statePath, 0000)
690         if err != nil {
691                 t.Fatal(err)
692         }
693
694         err = mainFetch(configPath)
695         mustBeErrorWithSubstring(t, err,
696                 statePath+": permission denied")
697
698         mustNotExist(t, passwdPath, plainPath, groupPath)
699 }
700
701 func fetchStateInvalid(a args) {
702         t := a.t
703         mustWriteGroupConfig(t, a.url)
704         mustCreate(t, statePath)
705
706         err := mainFetch(configPath)
707         mustBeErrorWithSubstring(t, err,
708                 "unexpected end of JSON input")
709
710         mustNotExist(t, groupPath, passwdPath, plainPath)
711         mustBeOld(t, statePath)
712 }
713
714 func fetchStateCannotWrite(a args) {
715         t := a.t
716         mustWriteGroupConfig(t, a.url)
717         mustCreate(t, groupPath)
718         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
719
720         *a.handler = func(w http.ResponseWriter, r *http.Request) {
721                 if r.URL.Path != "/group" {
722                         return
723                 }
724
725                 fmt.Fprintln(w, "root:x:0:")
726                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
727         }
728
729         err := os.Chmod(filepath.Dir(statePath), 0500)
730         if err != nil {
731                 t.Fatal(err)
732         }
733         defer os.Chmod(filepath.Dir(statePath), 0755)
734
735         err = mainFetch(configPath)
736         mustBeErrorWithSubstring(t, err,
737                 "permission denied")
738
739         mustNotExist(t, statePath, passwdPath, plainPath)
740         mustBeNew(t, groupPath)
741         mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
742 }
743
744 func fetchCannotDeploy(a args) {
745         t := a.t
746         mustWriteGroupConfig(t, a.url)
747         mustCreate(t, groupPath)
748         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
749
750         *a.handler = func(w http.ResponseWriter, r *http.Request) {
751                 if r.URL.Path != "/group" {
752                         return
753                 }
754
755                 fmt.Fprintln(w, "root:x:0:")
756                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
757         }
758
759         err := os.Chmod("testdata", 0500)
760         if err != nil {
761                 t.Fatal(err)
762         }
763         defer os.Chmod("testdata", 0755)
764
765         err = mainFetch(configPath)
766         mustBeErrorWithSubstring(t, err,
767                 "permission denied")
768
769         mustNotExist(t, statePath, passwdPath, plainPath)
770         mustBeOld(t, groupPath)
771 }
772
773 func fetchSecondFetchFails(a args) {
774         t := a.t
775         mustWriteConfig(t, fmt.Sprintf(`
776 statepath = "%[1]s"
777
778 [[file]]
779 type = "passwd"
780 url = "%[2]s/passwd"
781 path = "%[3]s"
782 ca = "%[5]s"
783
784 [[file]]
785 type = "group"
786 url = "%[2]s/group"
787 path = "%[4]s"
788 ca = "%[5]s"
789 `, statePath, a.url, passwdPath, groupPath, tlsCAPath))
790         mustCreate(t, passwdPath)
791         mustCreate(t, groupPath)
792         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
793         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
794
795         *a.handler = func(w http.ResponseWriter, r *http.Request) {
796                 if r.URL.Path == "/passwd" {
797                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
798                 }
799                 if r.URL.Path == "/group" {
800                         w.WriteHeader(http.StatusNotFound)
801                 }
802         }
803
804         err := mainFetch(configPath)
805         mustBeErrorWithSubstring(t, err,
806                 "status code 404")
807
808         mustNotExist(t, statePath, plainPath)
809         // Even though passwd was successfully fetched, no files were modified
810         // because the second fetch failed
811         mustBeOld(t, passwdPath, groupPath)
812 }
813
814 func fetchBasicAuth(a args) {
815         t := a.t
816         mustWritePasswdConfig(t, a.url)
817         mustCreate(t, passwdPath)
818         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
819
820         validUser := "username"
821         validPass := "password"
822
823         *a.handler = func(w http.ResponseWriter, r *http.Request) {
824                 if r.URL.Path != "/passwd" {
825                         return
826                 }
827
828                 user, pass, ok := r.BasicAuth()
829                 // NOTE: Do not use this in production because it permits
830                 // attackers to determine the length of user/pass. Instead use
831                 // hashes and subtle.ConstantTimeCompare().
832                 if !ok || user != validUser || pass != validPass {
833                         w.Header().Set("WWW-Authenticate", `Basic realm="Test"`)
834                         w.WriteHeader(http.StatusUnauthorized)
835                         return
836                 }
837
838                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
839                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
840         }
841
842         t.Log("Missing authentication")
843
844         err := mainFetch(configPath)
845         mustBeErrorWithSubstring(t, err,
846                 "status code 401")
847
848         mustNotExist(t, statePath, groupPath, plainPath)
849         mustBeOld(t, passwdPath)
850
851         t.Log("Unsafe config permissions")
852
853         mustWriteConfig(t, fmt.Sprintf(`
854 statepath = "%[1]s"
855
856 [[file]]
857 type = "passwd"
858 url = "%[2]s/passwd"
859 path = "%[3]s"
860 ca = "%[4]s"
861 username = "%[5]s"
862 password = "%[6]s"
863 `, statePath, a.url, passwdPath, tlsCAPath, validUser, validPass))
864
865         err = os.Chmod(configPath, 0644)
866         if err != nil {
867                 t.Fatal(err)
868         }
869
870         err = mainFetch(configPath)
871         mustBeErrorWithSubstring(t, err,
872                 "file[0].username/passsword in use and unsafe permissions "+
873                         "-rw-r--r-- on \"testdata/config.toml\"")
874
875         mustNotExist(t, statePath, groupPath, plainPath)
876         mustBeOld(t, passwdPath)
877
878         t.Log("Working authentication")
879
880         err = os.Chmod(configPath, 0600)
881         if err != nil {
882                 t.Fatal(err)
883         }
884
885         err = mainFetch(configPath)
886         if err != nil {
887                 t.Error(err)
888         }
889
890         mustNotExist(t, plainPath, groupPath)
891         mustBeNew(t, passwdPath, statePath)
892         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
893 }
894
895 func fetchInvalidCA(a args) {
896         t := a.t
897
898         // System CA
899
900         mustWriteConfig(t, fmt.Sprintf(`
901 statepath = "%[1]s"
902
903 [[file]]
904 type = "passwd"
905 url = "%[2]s/passwd"
906 path = "%[3]s"
907 `, statePath, a.url, passwdPath))
908         mustCreate(t, passwdPath)
909         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
910
911         *a.handler = func(w http.ResponseWriter, r *http.Request) {
912                 if r.URL.Path == "/passwd" {
913                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
914                 }
915         }
916
917         err := mainFetch(configPath)
918         mustBeErrorWithSubstring(t, err,
919                 "x509: certificate signed by unknown authority")
920
921         mustNotExist(t, statePath, plainPath, groupPath)
922         mustBeOld(t, passwdPath)
923
924         // Invalid CA
925
926         mustWriteConfig(t, fmt.Sprintf(`
927 statepath = "%[1]s"
928
929 [[file]]
930 type = "passwd"
931 url = "%[2]s/passwd"
932 path = "%[3]s"
933 ca = "%[4]s"
934 `, statePath, a.url, passwdPath, tlsCA2Path))
935         mustCreate(t, passwdPath)
936         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
937
938         *a.handler = func(w http.ResponseWriter, r *http.Request) {
939                 if r.URL.Path == "/passwd" {
940                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
941                 }
942         }
943
944         err = mainFetch(configPath)
945         mustBeErrorWithSubstring(t, err,
946                 "x509: certificate signed by unknown authority")
947
948         mustNotExist(t, statePath, plainPath, groupPath)
949         mustBeOld(t, passwdPath)
950 }