1 // Copyright (C) 2019 Simon Ruderich
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.
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.
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/>.
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"
50 handler *func(http.ResponseWriter, *http.Request)
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 {
58 if !os.IsNotExist(err) {
59 t.Errorf("path %q: unexpected error: %v",
63 t.Errorf("path %q exists", p)
69 // mustHaveHash checks if the given path content has the given SHA-1 string
71 func mustHaveHash(t *testing.T, path string, hash string) {
72 x, err := ioutil.ReadFile(path)
79 y := hex.EncodeToString(h.Sum(nil))
82 t.Errorf("%q has unexpected hash %q", path, y)
86 // mustBeErrorWithSubstring checks if the given error, represented as string,
87 // contains the given substring. This is somewhat ugly but the simplest way to
88 // check for proper errors.
89 func mustBeErrorWithSubstring(t *testing.T, err error, substring string) {
91 t.Errorf("err is nil")
92 } else if !strings.Contains(err.Error(), substring) {
93 t.Errorf("err %q does not contain string %q", err, substring)
97 func mustWriteConfig(t *testing.T, config string) {
98 err := ioutil.WriteFile(configPath, []byte(config), 0644)
104 func mustWritePasswdConfig(t *testing.T, url string) {
105 mustWriteConfig(t, fmt.Sprintf(`
113 `, statePath, url, passwdPath, tlsCAPath))
116 func mustWriteGroupConfig(t *testing.T, url string) {
117 mustWriteConfig(t, fmt.Sprintf(`
125 `, statePath, url, groupPath, tlsCAPath))
128 // mustCreate creates a file, truncating it if it exists. It then changes the
129 // modification to be in the past.
130 func mustCreate(t *testing.T, path string) {
131 f, err := os.Create(path)
140 // Change modification time to the past to detect updates to the file
144 // mustMakeOld change the modification time of all paths to be in the past.
145 func mustMakeOld(t *testing.T, paths ...string) {
146 old := time.Now().Add(-2 * time.Hour)
147 for _, p := range paths {
148 err := os.Chtimes(p, old, old)
155 // mustMakeOld verifies that all paths have a modification time in the past,
156 // as set by mustMakeOld().
157 func mustBeOld(t *testing.T, paths ...string) {
158 for _, p := range paths {
166 if now.Sub(mtime) < time.Hour {
167 t.Errorf("%q was recently modified", p)
172 // mustBeNew verifies that all paths have a modification time in the present.
173 func mustBeNew(t *testing.T, paths ...string) {
174 for _, p := range paths {
182 if now.Sub(mtime) > time.Hour {
183 t.Errorf("%q was not recently modified", p)
188 func TestMainFetch(t *testing.T) {
189 // Suppress log messages
190 log.SetOutput(ioutil.Discard)
191 defer log.SetOutput(os.Stderr)
193 tests := []func(args){
194 // Perform most tests with passwd for simplicity
195 fetchPasswdCacheFileDoesNotExist,
201 // Tests for plain and group
210 fetchStateCannotRead,
212 fetchStateCannotWrite,
214 fetchSecondFetchFails,
219 for _, f := range tests {
220 runMainTest(t, f, nil)
225 tests = append(tests, fetchInvalidCA)
227 cert, err := tls.LoadX509KeyPair(tlsCertPath, tlsKeyPath)
232 Certificates: []tls.Certificate{cert},
235 for _, f := range tests {
236 runMainTest(t, f, tls)
240 func runMainTest(t *testing.T, f func(args), tls *tls.Config) {
249 // NOTE: This is not guaranteed to work according to reflect's
250 // documentation but seems to work reliable for normal functions.
251 fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
253 name = name[strings.LastIndex(name, ".")+1:]
258 t.Run(name, func(t *testing.T) {
259 // Preparation & cleanup
260 for _, p := range cleanup {
262 if err != nil && !os.IsNotExist(err) {
265 // Remove the file at the end of this test run, if it
270 var handler func(http.ResponseWriter, *http.Request)
271 ts := httptest.NewUnstartedServer(http.HandlerFunc(
272 func(w http.ResponseWriter, r *http.Request) {
291 func fetchPasswdCacheFileDoesNotExist(a args) {
293 mustWritePasswdConfig(t, a.url)
295 err := mainFetch(configPath)
296 mustBeErrorWithSubstring(t, err,
297 "file.path \""+passwdPath+"\" must exist")
299 mustNotExist(t, statePath, passwdPath, plainPath, groupPath)
302 func fetchPasswd404(a args) {
304 mustWritePasswdConfig(t, a.url)
305 mustCreate(t, passwdPath)
307 *a.handler = func(w http.ResponseWriter, r *http.Request) {
309 w.WriteHeader(http.StatusNotFound)
312 err := mainFetch(configPath)
313 mustBeErrorWithSubstring(t, err,
316 mustNotExist(t, statePath, plainPath, groupPath)
317 mustBeOld(a.t, passwdPath)
320 func fetchPasswdEmpty(a args) {
322 mustWritePasswdConfig(t, a.url)
323 mustCreate(t, passwdPath)
325 *a.handler = func(w http.ResponseWriter, r *http.Request) {
329 err := mainFetch(configPath)
330 mustBeErrorWithSubstring(t, err,
331 "refusing to use empty passwd file")
333 mustNotExist(t, statePath, plainPath, groupPath)
334 mustBeOld(t, passwdPath)
337 func fetchPasswdInvalid(a args) {
339 mustWritePasswdConfig(t, a.url)
340 mustCreate(t, passwdPath)
342 *a.handler = func(w http.ResponseWriter, r *http.Request) {
343 if r.URL.Path != "/passwd" {
347 fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash")
350 err := mainFetch(configPath)
351 mustBeErrorWithSubstring(t, err,
352 "invalid uid in line")
354 mustNotExist(t, statePath, plainPath, groupPath)
355 mustBeOld(t, passwdPath)
358 func fetchPasswdLimits(a args) {
360 mustWritePasswdConfig(t, a.url)
361 mustCreate(t, passwdPath)
363 *a.handler = func(w http.ResponseWriter, r *http.Request) {
364 if r.URL.Path != "/passwd" {
368 fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash")
369 for i := 0; i < 65536; i++ {
375 err := mainFetch(configPath)
376 mustBeErrorWithSubstring(t, err,
377 "passwd too large to serialize")
379 mustNotExist(t, statePath, plainPath, groupPath)
380 mustBeOld(t, passwdPath)
383 func fetchPasswd(a args) {
385 mustWritePasswdConfig(t, a.url)
386 mustCreate(t, passwdPath)
387 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
389 t.Log("First fetch, write files")
391 *a.handler = func(w http.ResponseWriter, r *http.Request) {
392 if r.URL.Path != "/passwd" {
396 // No "Last-Modified" header
397 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
398 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
401 err := mainFetch(configPath)
406 mustNotExist(t, plainPath, groupPath)
407 mustBeNew(t, passwdPath, statePath)
408 // The actual content of passwdPath is verified by the NSS tests
409 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
411 t.Log("Fetch again, no support for Last-Modified")
413 mustMakeOld(t, passwdPath, statePath)
415 err = mainFetch(configPath)
420 mustNotExist(t, plainPath, groupPath)
421 mustBeNew(t, passwdPath, statePath)
422 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
424 t.Log("Fetch again, support for Last-Modified, but not retrieved yet")
426 mustMakeOld(t, passwdPath, statePath)
428 lastChange := time.Now()
429 *a.handler = func(w http.ResponseWriter, r *http.Request) {
430 if r.URL.Path != "/passwd" {
434 modified := r.Header.Get("If-Modified-Since")
436 x, err := http.ParseTime(modified)
438 t.Fatalf("invalid If-Modified-Since %v",
441 if !x.Before(lastChange) {
442 w.WriteHeader(http.StatusNotModified)
447 w.Header().Add("Last-Modified",
448 lastChange.Format(http.TimeFormat))
449 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
450 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
453 err = mainFetch(configPath)
458 mustNotExist(t, plainPath, groupPath)
459 mustBeNew(t, passwdPath, statePath)
460 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
462 t.Log("Fetch again, support for Last-Modified")
464 mustMakeOld(t, passwdPath, statePath)
466 err = mainFetch(configPath)
471 mustNotExist(t, plainPath, groupPath)
472 mustBeOld(t, passwdPath)
473 mustBeNew(t, statePath)
474 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
476 t.Log("Corrupt local passwd cache, fetched again")
478 os.Chmod(passwdPath, 0644) // make writable again
479 mustCreate(t, passwdPath)
480 mustMakeOld(t, passwdPath, statePath)
482 err = mainFetch(configPath)
487 mustNotExist(t, plainPath, groupPath)
488 mustBeNew(t, passwdPath, statePath)
489 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
492 func fetchPlainEmpty(a args) {
494 mustWriteConfig(t, fmt.Sprintf(`
502 `, statePath, a.url, plainPath, tlsCAPath))
503 mustCreate(t, plainPath)
505 *a.handler = func(w http.ResponseWriter, r *http.Request) {
509 err := mainFetch(configPath)
510 mustBeErrorWithSubstring(t, err,
511 "refusing to use empty response")
513 mustNotExist(t, statePath, passwdPath, groupPath)
514 mustBeOld(t, plainPath)
517 func fetchPlain(a args) {
519 mustWriteConfig(t, fmt.Sprintf(`
527 `, statePath, a.url, plainPath, tlsCAPath))
528 mustCreate(t, plainPath)
529 mustHaveHash(t, plainPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
531 *a.handler = func(w http.ResponseWriter, r *http.Request) {
532 if r.URL.Path != "/plain" {
536 fmt.Fprintln(w, "some file")
539 err := mainFetch(configPath)
544 mustNotExist(t, passwdPath, groupPath)
545 mustBeNew(t, plainPath, statePath)
546 mustHaveHash(t, plainPath, "0e08b5e8c10abc3e455b75286ba4a1fbd56e18a5")
548 // Remaining functionality already tested in fetchPasswd()
551 func fetchGroupEmpty(a args) {
553 mustWriteGroupConfig(t, a.url)
554 mustCreate(t, groupPath)
556 *a.handler = func(w http.ResponseWriter, r *http.Request) {
560 err := mainFetch(configPath)
561 mustBeErrorWithSubstring(t, err,
562 "refusing to use empty group file")
564 mustNotExist(t, statePath, passwdPath, plainPath)
565 mustBeOld(t, groupPath)
568 func fetchGroupInvalid(a args) {
570 mustWriteGroupConfig(t, a.url)
571 mustCreate(t, groupPath)
573 *a.handler = func(w http.ResponseWriter, r *http.Request) {
574 if r.URL.Path != "/group" {
578 fmt.Fprintln(w, "root:x::")
581 err := mainFetch(configPath)
582 mustBeErrorWithSubstring(t, err,
583 "invalid gid in line")
585 mustNotExist(t, statePath, passwdPath, plainPath)
586 mustBeOld(t, groupPath)
589 func fetchGroupLimits(a args) {
591 mustWriteGroupConfig(t, a.url)
592 mustCreate(t, groupPath)
594 *a.handler = func(w http.ResponseWriter, r *http.Request) {
595 if r.URL.Path != "/group" {
599 fmt.Fprint(w, "root:x:0:")
600 for i := 0; i < 65536; i++ {
606 err := mainFetch(configPath)
607 mustBeErrorWithSubstring(t, err,
608 "group too large to serialize")
610 mustNotExist(t, statePath, passwdPath, plainPath)
611 mustBeOld(t, groupPath)
614 func fetchGroup(a args) {
616 mustWriteGroupConfig(t, a.url)
617 mustCreate(t, groupPath)
618 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
620 *a.handler = func(w http.ResponseWriter, r *http.Request) {
621 if r.URL.Path != "/group" {
625 fmt.Fprintln(w, "root:x:0:")
626 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
629 err := mainFetch(configPath)
634 mustNotExist(t, passwdPath, plainPath)
635 mustBeNew(t, groupPath, statePath)
636 // The actual content of groupPath is verified by the NSS tests
637 mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
639 // Remaining functionality already tested in fetchPasswd()
642 func fetchNoConfig(a args) {
645 err := mainFetch(configPath)
646 mustBeErrorWithSubstring(t, err,
647 configPath+": no such file or directory")
649 mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath)
652 func fetchStateCannotRead(a args) {
654 mustWritePasswdConfig(t, a.url)
656 mustCreate(t, statePath)
657 err := os.Chmod(statePath, 0000)
662 err = mainFetch(configPath)
663 mustBeErrorWithSubstring(t, err,
664 statePath+": permission denied")
666 mustNotExist(t, passwdPath, plainPath, groupPath)
669 func fetchStateInvalid(a args) {
671 mustWriteGroupConfig(t, a.url)
672 mustCreate(t, statePath)
674 err := mainFetch(configPath)
675 mustBeErrorWithSubstring(t, err,
676 "unexpected end of JSON input")
678 mustNotExist(t, groupPath, passwdPath, plainPath)
679 mustBeOld(t, statePath)
682 func fetchStateCannotWrite(a args) {
684 mustWriteGroupConfig(t, a.url)
685 mustCreate(t, groupPath)
686 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
688 *a.handler = func(w http.ResponseWriter, r *http.Request) {
689 // To prevent mainFetch() from trying to update groupPath
690 // which will also fail
691 w.WriteHeader(http.StatusNotModified)
694 err := os.Chmod("testdata", 0500)
698 defer os.Chmod("testdata", 0755)
700 err = mainFetch(configPath)
701 mustBeErrorWithSubstring(t, err,
704 mustNotExist(t, statePath, passwdPath, plainPath)
705 mustBeOld(t, groupPath)
708 func fetchCannotDeploy(a args) {
710 mustWriteGroupConfig(t, a.url)
711 mustCreate(t, groupPath)
712 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
714 *a.handler = func(w http.ResponseWriter, r *http.Request) {
715 if r.URL.Path != "/group" {
719 fmt.Fprintln(w, "root:x:0:")
720 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
723 err := os.Chmod("testdata", 0500)
727 defer os.Chmod("testdata", 0755)
729 err = mainFetch(configPath)
730 mustBeErrorWithSubstring(t, err,
733 mustNotExist(t, statePath, passwdPath, plainPath)
734 mustBeOld(t, groupPath)
737 func fetchSecondFetchFails(a args) {
739 mustWriteConfig(t, fmt.Sprintf(`
753 `, statePath, a.url, passwdPath, groupPath, tlsCAPath))
754 mustCreate(t, passwdPath)
755 mustCreate(t, groupPath)
756 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
757 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
759 *a.handler = func(w http.ResponseWriter, r *http.Request) {
760 if r.URL.Path == "/passwd" {
761 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
763 if r.URL.Path == "/group" {
764 w.WriteHeader(http.StatusNotFound)
768 err := mainFetch(configPath)
769 mustBeErrorWithSubstring(t, err,
772 mustNotExist(t, statePath, plainPath)
773 // Even though passwd was successfully fetched, no files were modified
774 // because the second fetch failed
775 mustBeOld(t, passwdPath, groupPath)
778 func fetchInvalidCA(a args) {
783 mustWriteConfig(t, fmt.Sprintf(`
790 `, statePath, a.url, passwdPath))
791 mustCreate(t, passwdPath)
792 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
794 *a.handler = func(w http.ResponseWriter, r *http.Request) {
795 if r.URL.Path == "/passwd" {
796 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
800 err := mainFetch(configPath)
801 mustBeErrorWithSubstring(t, err,
802 "x509: certificate signed by unknown authority")
804 mustNotExist(t, statePath, plainPath, groupPath)
805 mustBeOld(t, passwdPath)
809 mustWriteConfig(t, fmt.Sprintf(`
817 `, statePath, a.url, passwdPath, tlsCA2Path))
818 mustCreate(t, passwdPath)
819 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
821 *a.handler = func(w http.ResponseWriter, r *http.Request) {
822 if r.URL.Path == "/passwd" {
823 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
827 err = mainFetch(configPath)
828 mustBeErrorWithSubstring(t, err,
829 "x509: certificate signed by unknown authority")
831 mustNotExist(t, statePath, plainPath, groupPath)
832 mustBeOld(t, passwdPath)