X-Git-Url: https://ruderich.org/simon/gitweb/?p=nsscash%2Fnsscash.git;a=blobdiff_plain;f=main_test.go;h=b5fc2ea92c57068ba9dc6016d39dfaa8af7b328c;hp=21dc0463958659df5151cf34edf29c82c26634ce;hb=refs%2Fheads%2Fmaster;hpb=ba614f8f445aa5490484427ecb3f643345b69358 diff --git a/main_test.go b/main_test.go index 21dc046..b5fc2ea 100644 --- a/main_test.go +++ b/main_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019 Simon Ruderich +// Copyright (C) 2019-2021 Simon Ruderich // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -17,6 +17,7 @@ package main import ( "crypto/sha1" + "crypto/tls" "encoding/hex" "fmt" "io/ioutil" @@ -24,6 +25,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "reflect" "runtime" "strings" @@ -32,11 +34,15 @@ import ( ) const ( - configPath = "testdata/config.toml" - statePath = "testdata/state.json" - passwdPath = "testdata/passwd.nsscash" - plainPath = "testdata/plain" - groupPath = "testdata/group.nsscash" + configPath = "testdata/config.toml" + statePath = "testdata/var/state.json" + passwdPath = "testdata/passwd.nsscash" + plainPath = "testdata/plain" + groupPath = "testdata/group.nsscash" + tlsCAPath = "testdata/ca.crt" + tlsCertPath = "testdata/server.crt" + tlsKeyPath = "testdata/server.key" + tlsCA2Path = "testdata/ca2.crt" ) type args struct { @@ -61,6 +67,12 @@ func mustNotExist(t *testing.T, paths ...string) { } } +func hashAsHex(x []byte) string { + h := sha1.New() + h.Write(x) + return hex.EncodeToString(h.Sum(nil)) +} + // mustHaveHash checks if the given path content has the given SHA-1 string // (in hex). func mustHaveHash(t *testing.T, path string, hash string) { @@ -69,10 +81,7 @@ func mustHaveHash(t *testing.T, path string, hash string) { t.Fatal(err) } - h := sha1.New() - h.Write(x) - y := hex.EncodeToString(h.Sum(nil)) - + y := hashAsHex(x) if y != hash { t.Errorf("%q has unexpected hash %q", path, y) } @@ -104,7 +113,8 @@ statepath = "%[1]s" type = "passwd" url = "%[2]s/passwd" path = "%[3]s" -`, statePath, url, passwdPath)) +ca = "%[4]s" +`, statePath, url, passwdPath, tlsCAPath)) } func mustWriteGroupConfig(t *testing.T, url string) { @@ -115,7 +125,8 @@ statepath = "%[1]s" type = "group" url = "%[2]s/group" path = "%[3]s" -`, statePath, url, groupPath)) +ca = "%[4]s" +`, statePath, url, groupPath, tlsCAPath)) } // mustCreate creates a file, truncating it if it exists. It then changes the @@ -145,8 +156,8 @@ func mustMakeOld(t *testing.T, paths ...string) { } } -// mustMakeOld verifies that all paths have a modification time in the past, -// as set by mustMakeOld(). +// mustBeOld verifies that all paths have a modification time in the past, as +// set by mustMakeOld. func mustBeOld(t *testing.T, paths ...string) { for _, p := range paths { i, err := os.Stat(p) @@ -155,8 +166,7 @@ func mustBeOld(t *testing.T, paths ...string) { } mtime := i.ModTime() - now := time.Now() - if now.Sub(mtime) < time.Hour { + if time.Since(mtime) < time.Hour { t.Errorf("%q was recently modified", p) } } @@ -171,8 +181,7 @@ func mustBeNew(t *testing.T, paths ...string) { } mtime := i.ModTime() - now := time.Now() - if now.Sub(mtime) > time.Hour { + if time.Since(mtime) > time.Hour { t.Errorf("%q was not recently modified", p) } } @@ -184,10 +193,56 @@ func TestMainFetch(t *testing.T) { defer log.SetOutput(os.Stderr) tests := []func(args){ + // Perform most tests with passwd for simplicity + fetchPasswdCacheFileDoesNotExist, + fetchPasswd404, + fetchPasswdUnexpected304, + fetchPasswdEmpty, + fetchPasswdInvalid, + fetchPasswdLimits, + fetchPasswd, + // Tests for plain and group + fetchPlainEmpty, + fetchPlain, + fetchGroupEmpty, + fetchGroupInvalid, + fetchGroupLimits, + fetchGroup, // Special tests fetchNoConfig, + fetchStateCannotRead, + fetchStateInvalid, + fetchStateCannotWrite, + fetchCannotDeploy, + fetchSecondFetchFails, + fetchBasicAuth, + // TODO: fetchCannotDeployMultiple, + } + + // HTTP tests + + for _, f := range tests { + runMainTest(t, f, nil) + } + + // HTTPS tests + + tests = append(tests, fetchInvalidCA) + + cert, err := tls.LoadX509KeyPair(tlsCertPath, tlsKeyPath) + if err != nil { + t.Fatal(err) + } + tls := &tls.Config{ + Certificates: []tls.Certificate{cert}, } + for _, f := range tests { + runMainTest(t, f, tls) + } +} + +func runMainTest(t *testing.T, f func(args), tls *tls.Config) { cleanup := []string{ configPath, statePath, @@ -196,39 +251,442 @@ func TestMainFetch(t *testing.T) { groupPath, } - for _, f := range tests { - // NOTE: This is not guaranteed to work according to reflect's - // documentation but seems to work reliable for normal - // functions. - fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer()) - name := fn.Name() - name = name[strings.LastIndex(name, ".")+1:] - - t.Run(name, func(t *testing.T) { - // Preparation & cleanup - for _, p := range cleanup { - err := os.Remove(p) - if err != nil && !os.IsNotExist(err) { - t.Fatal(err) - } - // Remove the file at the end of this test - // run, if it was created - defer os.Remove(p) + // NOTE: This is not guaranteed to work according to reflect's + // documentation but seems to work reliable for normal functions. + fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer()) + name := fn.Name() + name = name[strings.LastIndex(name, ".")+1:] + if tls != nil { + name = "tls" + name + } + + t.Run(name, func(t *testing.T) { + // Preparation & cleanup + for _, p := range cleanup { + err := os.Remove(p) + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + // Remove the file at the end of this test run, if it + // was created + defer os.Remove(p) + + dir := filepath.Dir(p) + err = os.MkdirAll(dir, 0755) + if err != nil { + t.Fatal(err) } + defer os.Remove(dir) // remove empty directories + } - var handler func(http.ResponseWriter, *http.Request) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var handler func(http.ResponseWriter, *http.Request) + ts := httptest.NewUnstartedServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { handler(w, r) })) - defer ts.Close() + if tls == nil { + ts.Start() + } else { + ts.TLS = tls + ts.StartTLS() + } + defer ts.Close() - f(args{ - t: t, - url: ts.URL, - handler: &handler, - }) + f(args{ + t: t, + url: ts.URL, + handler: &handler, }) + }) +} + +func fetchPasswdCacheFileDoesNotExist(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "file.path \""+passwdPath+"\" must exist") + + mustNotExist(t, statePath, passwdPath, plainPath, groupPath) +} + +func fetchPasswd404(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + mustCreate(t, passwdPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + // 404 + w.WriteHeader(http.StatusNotFound) + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "status code 404") + + mustNotExist(t, statePath, plainPath, groupPath) + mustBeOld(t, passwdPath) +} + +func fetchPasswdUnexpected304(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + mustCreate(t, passwdPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + // 304 + w.WriteHeader(http.StatusNotModified) + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "status code 304 but did not send If-Modified-Since") + + mustNotExist(t, statePath, plainPath, groupPath) + mustBeOld(t, passwdPath) +} + +func fetchPasswdEmpty(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + mustCreate(t, passwdPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + // Empty response + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "refusing to use empty passwd file") + + mustNotExist(t, statePath, plainPath, groupPath) + mustBeOld(t, passwdPath) +} + +func fetchPasswdInvalid(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + mustCreate(t, passwdPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/passwd" { + return + } + + fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash") + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "invalid uid in line") + + mustNotExist(t, statePath, plainPath, groupPath) + mustBeOld(t, passwdPath) +} + +func fetchPasswdLimits(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + mustCreate(t, passwdPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/passwd" { + return + } + + fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash") + for i := 0; i < 65536; i++ { + fmt.Fprint(w, "x") + } + fmt.Fprint(w, "\n") + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "passwd too large to serialize") + + mustNotExist(t, statePath, plainPath, groupPath) + mustBeOld(t, passwdPath) +} + +func fetchPasswd(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + mustCreate(t, passwdPath) + mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + t.Log("First fetch, write files") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/passwd" { + return + } + + // No "Last-Modified" header + fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash") + fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin") } + + err := mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, plainPath, groupPath) + mustBeNew(t, passwdPath, statePath) + // The actual content of passwdPath is verified by the NSS tests + mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23") + + t.Log("Fetch again, no support for Last-Modified") + + mustMakeOld(t, passwdPath, statePath) + + err = mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, plainPath, groupPath) + mustBeNew(t, passwdPath, statePath) + mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23") + + t.Log("Fetch again, support for Last-Modified, but not retrieved yet") + + mustMakeOld(t, passwdPath, statePath) + + lastChange := time.Now() + change := false + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/passwd" { + return + } + + modified := r.Header.Get("If-Modified-Since") + if modified != "" { + x, err := http.ParseTime(modified) + if err != nil { + t.Fatalf("invalid If-Modified-Since %v", + modified) + } + if !x.Before(lastChange.Truncate(time.Second)) { + w.WriteHeader(http.StatusNotModified) + return + } + } + + w.Header().Add("Last-Modified", + lastChange.UTC().Format(http.TimeFormat)) + fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash") + fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin") + if change { + fmt.Fprintln(w, "bin:x:2:2:bin:/bin:/usr/sbin/nologin") + } + } + + err = mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, plainPath, groupPath) + mustBeNew(t, passwdPath, statePath) + mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23") + + t.Log("Fetch again, support for Last-Modified") + + mustMakeOld(t, passwdPath, statePath) + + err = mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, plainPath, groupPath) + mustBeOld(t, passwdPath) + mustBeNew(t, statePath) + mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23") + + t.Log("Corrupt local passwd cache, fetched again") + + os.Chmod(passwdPath, 0644) // make writable again + mustCreate(t, passwdPath) + mustMakeOld(t, passwdPath, statePath) + + err = mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, plainPath, groupPath) + mustBeNew(t, passwdPath, statePath) + mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23") + + t.Log("Fetch again with newer server response") + + change = true + lastChange = time.Now().Add(time.Second) + + mustMakeOld(t, passwdPath, statePath) + + err = mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, plainPath, groupPath) + mustBeNew(t, passwdPath, statePath) + mustHaveHash(t, passwdPath, "ca9c7477cb425667fc9ecbd79e8e1c2ad0e84423") +} + +func fetchPlainEmpty(a args) { + t := a.t + mustWriteConfig(t, fmt.Sprintf(` +statepath = "%[1]s" + +[[file]] +type = "plain" +url = "%[2]s/plain" +path = "%[3]s" +ca = "%[4]s" +`, statePath, a.url, plainPath, tlsCAPath)) + mustCreate(t, plainPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + // Empty response + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "refusing to use empty response") + + mustNotExist(t, statePath, passwdPath, groupPath) + mustBeOld(t, plainPath) +} + +func fetchPlain(a args) { + t := a.t + mustWriteConfig(t, fmt.Sprintf(` +statepath = "%[1]s" + +[[file]] +type = "plain" +url = "%[2]s/plain" +path = "%[3]s" +ca = "%[4]s" +`, statePath, a.url, plainPath, tlsCAPath)) + mustCreate(t, plainPath) + mustHaveHash(t, plainPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/plain" { + return + } + + fmt.Fprintln(w, "some file") + } + + err := mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, passwdPath, groupPath) + mustBeNew(t, plainPath, statePath) + mustHaveHash(t, plainPath, "0e08b5e8c10abc3e455b75286ba4a1fbd56e18a5") + + // Remaining functionality already tested in fetchPasswd() +} + +func fetchGroupEmpty(a args) { + t := a.t + mustWriteGroupConfig(t, a.url) + mustCreate(t, groupPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + // Empty response + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "refusing to use empty group file") + + mustNotExist(t, statePath, passwdPath, plainPath) + mustBeOld(t, groupPath) +} + +func fetchGroupInvalid(a args) { + t := a.t + mustWriteGroupConfig(t, a.url) + mustCreate(t, groupPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/group" { + return + } + + fmt.Fprintln(w, "root:x::") + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "invalid gid in line") + + mustNotExist(t, statePath, passwdPath, plainPath) + mustBeOld(t, groupPath) +} + +func fetchGroupLimits(a args) { + t := a.t + mustWriteGroupConfig(t, a.url) + mustCreate(t, groupPath) + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/group" { + return + } + + fmt.Fprint(w, "root:x:0:") + for i := 0; i < 65536; i++ { + fmt.Fprint(w, "x") + } + fmt.Fprint(w, "\n") + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "group too large to serialize") + + mustNotExist(t, statePath, passwdPath, plainPath) + mustBeOld(t, groupPath) +} + +func fetchGroup(a args) { + t := a.t + mustWriteGroupConfig(t, a.url) + mustCreate(t, groupPath) + mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/group" { + return + } + + fmt.Fprintln(w, "root:x:0:") + fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal") + } + + err := mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, passwdPath, plainPath) + mustBeNew(t, groupPath, statePath) + // The actual content of groupPath is verified by the NSS tests + mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd") + + // Remaining functionality already tested in fetchPasswd() } func fetchNoConfig(a args) { @@ -240,3 +698,321 @@ func fetchNoConfig(a args) { mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath) } + +func fetchStateCannotRead(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + + mustCreate(t, statePath) + err := os.Chmod(statePath, 0000) + if err != nil { + t.Fatal(err) + } + + err = mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + statePath+": permission denied") + + mustNotExist(t, passwdPath, plainPath, groupPath) + mustBeOld(t, statePath) +} + +func fetchStateInvalid(a args) { + t := a.t + mustWriteGroupConfig(t, a.url) + mustCreate(t, statePath) + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "unexpected end of JSON input") + + mustNotExist(t, groupPath, passwdPath, plainPath) + mustBeOld(t, statePath) +} + +func fetchStateCannotWrite(a args) { + t := a.t + mustWriteGroupConfig(t, a.url) + mustCreate(t, groupPath) + mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/group" { + return + } + + fmt.Fprintln(w, "root:x:0:") + fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal") + } + + err := os.Chmod(filepath.Dir(statePath), 0500) + if err != nil { + t.Fatal(err) + } + defer os.Chmod(filepath.Dir(statePath), 0755) + + err = mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "permission denied") + + mustNotExist(t, statePath, passwdPath, plainPath) + mustBeNew(t, groupPath) + mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd") +} + +func fetchCannotDeploy(a args) { + t := a.t + mustWriteGroupConfig(t, a.url) + mustCreate(t, groupPath) + mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/group" { + return + } + + fmt.Fprintln(w, "root:x:0:") + fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal") + } + + err := os.Chmod("testdata", 0500) + if err != nil { + t.Fatal(err) + } + defer os.Chmod("testdata", 0755) + + err = mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "permission denied") + + mustNotExist(t, statePath, passwdPath, plainPath) + mustBeOld(t, groupPath) +} + +func fetchSecondFetchFails(a args) { + t := a.t + mustWriteConfig(t, fmt.Sprintf(` +statepath = "%[1]s" + +[[file]] +type = "passwd" +url = "%[2]s/passwd" +path = "%[3]s" +ca = "%[5]s" + +[[file]] +type = "group" +url = "%[2]s/group" +path = "%[4]s" +ca = "%[5]s" +`, statePath, a.url, passwdPath, groupPath, tlsCAPath)) + mustCreate(t, passwdPath) + mustCreate(t, groupPath) + mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/passwd" { + fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash") + } + if r.URL.Path == "/group" { + w.WriteHeader(http.StatusNotFound) + } + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "status code 404") + + mustNotExist(t, statePath, plainPath) + // Even though passwd was successfully fetched, no files were modified + // because the second fetch failed + mustBeOld(t, passwdPath, groupPath) +} + +func fetchBasicAuth(a args) { + t := a.t + mustWritePasswdConfig(t, a.url) + mustCreate(t, passwdPath) + mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + validUser := "username" + validPass := "password" + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/passwd" { + return + } + + user, pass, ok := r.BasicAuth() + // NOTE: Do not use this in production because it permits + // attackers to determine the length of user/pass. Instead use + // hashes and subtle.ConstantTimeCompare(). + if !ok || user != validUser || pass != validPass { + w.Header().Set("WWW-Authenticate", `Basic realm="Test"`) + w.WriteHeader(http.StatusUnauthorized) + return + } + + fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash") + fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin") + } + + t.Log("Missing authentication") + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "status code 401") + + mustNotExist(t, statePath, groupPath, plainPath) + mustBeOld(t, passwdPath) + + t.Log("Unsafe config permissions") + + mustWriteConfig(t, fmt.Sprintf(` +statepath = "%[1]s" + +[[file]] +type = "passwd" +url = "%[2]s/passwd" +path = "%[3]s" +ca = "%[4]s" +username = "%[5]s" +password = "%[6]s" +`, statePath, a.url, passwdPath, tlsCAPath, validUser, validPass)) + + err = os.Chmod(configPath, 0644) + if err != nil { + t.Fatal(err) + } + + err = mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "file[0].username/passsword in use and unsafe permissions "+ + "-rw-r--r-- on \""+configPath+"\"") + + mustNotExist(t, statePath, groupPath, plainPath) + mustBeOld(t, passwdPath) + + t.Log("Working authentication") + + err = os.Chmod(configPath, 0600) + if err != nil { + t.Fatal(err) + } + + err = mainFetch(configPath) + if err != nil { + t.Error(err) + } + + mustNotExist(t, plainPath, groupPath) + mustBeNew(t, passwdPath, statePath) + mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23") +} + +func fetchInvalidCA(a args) { + t := a.t + + // System CA + + mustWriteConfig(t, fmt.Sprintf(` +statepath = "%[1]s" + +[[file]] +type = "passwd" +url = "%[2]s/passwd" +path = "%[3]s" +`, statePath, a.url, passwdPath)) + mustCreate(t, passwdPath) + mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/passwd" { + fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash") + } + } + + err := mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "x509: certificate signed by unknown authority") + + mustNotExist(t, statePath, plainPath, groupPath) + mustBeOld(t, passwdPath) + + // Invalid CA + + mustWriteConfig(t, fmt.Sprintf(` +statepath = "%[1]s" + +[[file]] +type = "passwd" +url = "%[2]s/passwd" +path = "%[3]s" +ca = "%[4]s" +`, statePath, a.url, passwdPath, tlsCA2Path)) + mustCreate(t, passwdPath) + mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/passwd" { + fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash") + } + } + + err = mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "x509: certificate signed by unknown authority") + + mustNotExist(t, statePath, plainPath, groupPath) + mustBeOld(t, passwdPath) +} + +/* +TODO: implement code for this test + +func fetchCannotDeployMultiple(a args) { + t := a.t + newPlainDir := "testdata/x" + newPlainPath := newPlainDir + "/plain" + mustWriteConfig(t, fmt.Sprintf(` +statepath = "%[1]s" + +[[file]] +type = "group" +url = "%[2]s/group" +path = "%[3]s" + +[[file]] +type = "plain" +url = "%[2]s/plain" +path = "%[4]s" +`, statePath, a.url, groupPath, newPlainPath)) + os.Mkdir(newPlainDir, 0755) + defer os.RemoveAll(newPlainDir) + mustCreate(t, groupPath) + mustCreate(t, newPlainPath) + mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + *a.handler = func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/group" { + fmt.Fprintln(w, "root:x:0:") + } + if r.URL.Path == "/plain" { + fmt.Fprintln(w, "some file") + } + } + + err := os.Chmod(newPlainDir, 0500) + if err != nil { + t.Fatal(err) + } + + err = mainFetch(configPath) + mustBeErrorWithSubstring(t, err, + "permission denied") + + mustNotExist(t, statePath, passwdPath, plainPath) + mustBeOld(t, groupPath, newPlainPath) +} +*/