X-Git-Url: https://ruderich.org/simon/gitweb/?p=nsscash%2Fnsscash.git;a=blobdiff_plain;f=main_test.go;h=b5fc2ea92c57068ba9dc6016d39dfaa8af7b328c;hp=20158171e2441685fbc6efb794016ae2155b4f1d;hb=HEAD;hpb=17d6cb591586b6a4ce79c9f9251a5bb86fe963a1 diff --git a/main_test.go b/main_test.go index 2015817..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 @@ -25,6 +25,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "reflect" "runtime" "strings" @@ -34,7 +35,7 @@ import ( const ( configPath = "testdata/config.toml" - statePath = "testdata/state.json" + statePath = "testdata/var/state.json" passwdPath = "testdata/passwd.nsscash" plainPath = "testdata/plain" groupPath = "testdata/group.nsscash" @@ -66,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) { @@ -74,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) } @@ -152,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) @@ -162,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) } } @@ -178,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) } } @@ -194,6 +196,7 @@ func TestMainFetch(t *testing.T) { // Perform most tests with passwd for simplicity fetchPasswdCacheFileDoesNotExist, fetchPasswd404, + fetchPasswdUnexpected304, fetchPasswdEmpty, fetchPasswdInvalid, fetchPasswdLimits, @@ -212,6 +215,8 @@ func TestMainFetch(t *testing.T) { fetchStateCannotWrite, fetchCannotDeploy, fetchSecondFetchFails, + fetchBasicAuth, + // TODO: fetchCannotDeployMultiple, } // HTTP tests @@ -265,6 +270,13 @@ func runMainTest(t *testing.T, f func(args), tls *tls.Config) { // 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) @@ -314,7 +326,25 @@ func fetchPasswd404(a args) { "status code 404") mustNotExist(t, statePath, plainPath, groupPath) - mustBeOld(a.t, passwdPath) + 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) { @@ -426,6 +456,7 @@ func fetchPasswd(a args) { mustMakeOld(t, passwdPath, statePath) lastChange := time.Now() + change := false *a.handler = func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/passwd" { return @@ -438,16 +469,19 @@ func fetchPasswd(a args) { t.Fatalf("invalid If-Modified-Since %v", modified) } - if !x.Before(lastChange) { + if !x.Before(lastChange.Truncate(time.Second)) { w.WriteHeader(http.StatusNotModified) return } } w.Header().Add("Last-Modified", - lastChange.Format(http.TimeFormat)) + 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) @@ -487,6 +521,22 @@ func fetchPasswd(a args) { 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) { @@ -664,6 +714,7 @@ func fetchStateCannotRead(a args) { statePath+": permission denied") mustNotExist(t, passwdPath, plainPath, groupPath) + mustBeOld(t, statePath) } func fetchStateInvalid(a args) { @@ -686,23 +737,27 @@ func fetchStateCannotWrite(a args) { mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709") *a.handler = func(w http.ResponseWriter, r *http.Request) { - // To prevent mainFetch() from trying to update groupPath - // which will also fail - w.WriteHeader(http.StatusNotModified) + 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) + err := os.Chmod(filepath.Dir(statePath), 0500) if err != nil { t.Fatal(err) } - defer os.Chmod("testdata", 0755) + defer os.Chmod(filepath.Dir(statePath), 0755) err = mainFetch(configPath) mustBeErrorWithSubstring(t, err, "permission denied") mustNotExist(t, statePath, passwdPath, plainPath) - mustBeOld(t, groupPath) + mustBeNew(t, groupPath) + mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd") } func fetchCannotDeploy(a args) { @@ -775,6 +830,87 @@ ca = "%[5]s" 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 @@ -831,3 +967,52 @@ ca = "%[4]s" 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) +} +*/