diff --git a/auth.go b/auth.go index c4f329e..f62b328 100644 --- a/auth.go +++ b/auth.go @@ -30,6 +30,9 @@ import ( // Verify a password against system standard shadow file // Note auxilliary fields for expiry policy are *not* inspected. func VerifyPass(reader func(string) ([]byte, error), user, password string) (bool, error) { + if reader == nil { + reader = ioutil.ReadFile // dependency injection hides that this is required + } passlib.UseDefaults(passlib.Defaults20180601) pwFileData, e := reader("/etc/shadow") if e != nil { @@ -70,8 +73,11 @@ func VerifyPass(reader func(string) ([]byte, error), user, password string) (boo // This checks /etc/xs.passwd for auth info, and system /etc/passwd // to cross-check the user actually exists. // nolint: gocyclo -func AuthUserByPasswd(username string, auth string, fname string) (valid bool, allowedCmds string) { - b, e := ioutil.ReadFile(fname) // nolint: gosec +func AuthUserByPasswd(reader func(string) ([]byte, error), userlookup func(string) (*user.User, error), username string, auth string, fname string) (valid bool, allowedCmds string) { + if reader == nil { + reader = ioutil.ReadFile // dependency injection hides that this is required + } + b, e := reader(fname) // nolint: gosec if e != nil { valid = false log.Printf("ERROR: Cannot read %s!\n", fname) @@ -115,7 +121,8 @@ func AuthUserByPasswd(username string, auth string, fname string) (valid bool, a r = nil runtime.GC() - if !userExistsOnSystem(username) { + _, userErr := userlookup(username) + if userErr != nil { valid = false } return @@ -123,24 +130,26 @@ func AuthUserByPasswd(username string, auth string, fname string) (valid bool, a // ------------- End xs-local passwd auth routine(s) ----------- -func userExistsOnSystem(who string) bool { - _, userErr := user.Lookup(who) - return userErr == nil -} - // AuthUserByToken checks user login information against an auth token. // Auth tokens are stored in each user's $HOME/.xs_id and are requested // via the -g option. // The function also check system /etc/passwd to cross-check the user // actually exists. -func AuthUserByToken(username string, connhostname string, auth string) (valid bool) { +func AuthUserByToken(reader func(string) ([]byte, error), userlookup func(string) (*user.User, error), username string, connhostname string, auth string) (valid bool) { + if reader == nil { + reader = ioutil.ReadFile // dependency injection hides that this is required + } + if userlookup == nil { + userlookup = user.Lookup // again for dependency injection as dep is now hidden + } + auth = strings.TrimSpace(auth) - u, ue := user.Lookup(username) + u, ue := userlookup(username) if ue != nil { return false } - b, e := ioutil.ReadFile(fmt.Sprintf("%s/.xs_id", u.HomeDir)) + b, e := reader(fmt.Sprintf("%s/.xs_id", u.HomeDir)) if e != nil { log.Printf("INFO: Cannot read %s/.xs_id\n", u.HomeDir) return false @@ -167,7 +176,8 @@ func AuthUserByToken(username string, connhostname string, auth string) (valid b break } } - if !userExistsOnSystem(username) { + _, userErr := userlookup(username) + if userErr != nil { valid = false } return diff --git a/auth_test.go b/auth_test.go index 6d23c49..2d90e33 100644 --- a/auth_test.go +++ b/auth_test.go @@ -2,6 +2,9 @@ package xs import ( "errors" + "fmt" + "os/user" + "strings" "testing" ) @@ -16,15 +19,39 @@ var ( joebloggs:$6$F.0IXOrb0w0VJHG1$3O4PYyng7F3hlh42mbroEdQZvslybY5etPPiLMQJ1xosjABY.Q4xqAfyIfe03Du61ZjGQIt3nL0j12P9k1fsK/:18310:0:99999:7::: disableduser:!:18310::::::` + dummyAuthTokenFile = "hostA:abcdefg\nhostB:wxyz\n" + testGoodUsers = []userVerifs{ {"johndoe", "testpass", true}, {"joebloggs", "testpass2", true}, {"johndoe", "badpass", false}, } + + userlookup_arg_u string + readfile_arg_f string ) +func _mock_user_Lookup(username string) (*user.User, error) { + username = userlookup_arg_u + if username == "baduser" { + return &user.User{}, errors.New("bad user") + } + urec := &user.User{Uid: "1000", Gid: "1000", Username: username, Name: "Full Name", HomeDir: "/home/user"} + fmt.Printf(" [mock user rec:%v]\n", urec) + return urec, nil +} + func _mock_ioutil_ReadFile(f string) ([]byte, error) { - return []byte(dummyShadowA), nil + f = readfile_arg_f + if f == "/etc/shadow" { + fmt.Println(" [mocking ReadFile(\"/etc/shadow\")]") + return []byte(dummyShadowA), nil + } + if strings.Contains(f, "/.xs_id") { + fmt.Println(" [mocking ReadFile(\".xs_id\")]") + return []byte(dummyAuthTokenFile), nil + } + return []byte{}, errors.New("no readfile_arg_f supplied") } func _mock_ioutil_ReadFileEmpty(f string) ([]byte, error) { @@ -36,6 +63,7 @@ func _mock_ioutil_ReadFileHasError(f string) ([]byte, error) { } func TestVerifyPass(t *testing.T) { + readfile_arg_f = "/etc/shadow" for idx, rec := range testGoodUsers { stat, e := VerifyPass(_mock_ioutil_ReadFile, rec.user, rec.passwd) if rec.good && (!stat || e != nil) { @@ -64,3 +92,43 @@ func TestVerifyPassFailsOnDisabledEntry(t *testing.T) { t.Fatal("failed to fail on disabled user entry") } } + +//// + +func TestAuthUserByTokenFailsOnMissingEntryForHost(t *testing.T) { + stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, "johndoe", "hostZ", "abcdefg") + if stat { + t.Fatal("failed to fail on missing/mismatched host entry") + } +} + +func TestAuthUserByTokenFailsOnMissingEntryForUser(t *testing.T) { + stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, "unkuser", "hostA", "abcdefg") + if stat { + t.Fatal("failed to fail on wrong user") + } +} + +func TestAuthUserByTokenFailsOnUserLookupFailure(t *testing.T) { + userlookup_arg_u = "baduser" + stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, "johndoe", "hostA", "abcdefg") + if stat { + t.Fatal("failed to fail with bad return from user.Lookup()") + } +} + +func TestAuthUserByTokenFailsOnMismatchedTokenForUser(t *testing.T) { + stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, "johndoe", "hostA", "badtoken") + if stat { + t.Fatal("failed to fail with valid user, bad token") + } +} + +func TestAuthUserByTokenSucceedsWithMatchedUserAndToken(t *testing.T) { + userlookup_arg_u = "johndoe" + readfile_arg_f = "/.xs_id" + stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, userlookup_arg_u, "hostA", "hostA:abcdefg") + if !stat { + t.Fatal("failed with valid user and token") + } +} diff --git a/xsd/xsd.go b/xsd/xsd.go index 567e02e..528d094 100755 --- a/xsd/xsd.go +++ b/xsd/xsd.go @@ -708,14 +708,14 @@ func main() { var valid bool var allowedCmds string // Currently unused - if xs.AuthUserByToken(string(rec.Who()), string(rec.ConnHost()), string(rec.AuthCookie(true))) { + if xs.AuthUserByToken(ioutil.ReadFile, user.Lookup, string(rec.Who()), string(rec.ConnHost()), string(rec.AuthCookie(true))) { valid = true } else { if useSystemPasswd { //var passErr error valid, _ /*passErr*/ = xs.VerifyPass(ioutil.ReadFile, string(rec.Who()), string(rec.AuthCookie(true))) } else { - valid, allowedCmds = xs.AuthUserByPasswd(string(rec.Who()), string(rec.AuthCookie(true)), "/etc/xs.passwd") + valid, allowedCmds = xs.AuthUserByPasswd(ioutil.ReadFile, user.Lookup, string(rec.Who()), string(rec.AuthCookie(true)), "/etc/xs.passwd") } }