auth.go: AuthCtx added to structure mockable entities for testing

Signed-off-by: Russ Magee <rmagee@gmail.com>
This commit is contained in:
Russ Magee 2020-02-18 10:53:04 -08:00
parent 733fc46d86
commit 654de563dc
3 changed files with 56 additions and 29 deletions

47
auth.go
View file

@ -26,15 +26,25 @@ import (
passlib "gopkg.in/hlandau/passlib.v1" passlib "gopkg.in/hlandau/passlib.v1"
) )
type AuthCtx struct {
reader func(string) ([]byte, error) // eg. ioutil.ReadFile()
userlookup func(string) (*user.User, error) // eg. os/user.Lookup()
}
func NewAuthCtx(/*reader func(string) ([]byte, error), userlookup func(string) (*user.User, error)*/) (ret *AuthCtx) {
ret = &AuthCtx{ioutil.ReadFile, user.Lookup}
return
}
// --------- System passwd/shadow auth routine(s) -------------- // --------- System passwd/shadow auth routine(s) --------------
// Verify a password against system standard shadow file // Verify a password against system standard shadow file
// Note auxilliary fields for expiry policy are *not* inspected. // Note auxilliary fields for expiry policy are *not* inspected.
func VerifyPass(reader func(string) ([]byte, error), user, password string) (bool, error) { func VerifyPass(ctx *AuthCtx, user, password string) (bool, error) {
if reader == nil { if ctx.reader == nil {
reader = ioutil.ReadFile // dependency injection hides that this is required ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
} }
passlib.UseDefaults(passlib.Defaults20180601) passlib.UseDefaults(passlib.Defaults20180601)
pwFileData, e := reader("/etc/shadow") pwFileData, e := ctx.reader("/etc/shadow")
if e != nil { if e != nil {
return false, e return false, e
} }
@ -73,11 +83,14 @@ func VerifyPass(reader func(string) ([]byte, error), user, password string) (boo
// This checks /etc/xs.passwd for auth info, and system /etc/passwd // This checks /etc/xs.passwd for auth info, and system /etc/passwd
// to cross-check the user actually exists. // to cross-check the user actually exists.
// nolint: gocyclo // nolint: gocyclo
func AuthUserByPasswd(reader func(string) ([]byte, error), userlookup func(string) (*user.User, error), username string, auth string, fname string) (valid bool, allowedCmds string) { func AuthUserByPasswd(ctx *AuthCtx, username string, auth string, fname string) (valid bool, allowedCmds string) {
if reader == nil { if ctx.reader == nil {
reader = ioutil.ReadFile // dependency injection hides that this is required ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
} }
b, e := reader(fname) // nolint: gosec if ctx.userlookup == nil {
ctx.userlookup = user.Lookup // again for dependency injection as dep is now hidden
}
b, e := ctx.reader(fname) // nolint: gosec
if e != nil { if e != nil {
valid = false valid = false
log.Printf("ERROR: Cannot read %s!\n", fname) log.Printf("ERROR: Cannot read %s!\n", fname)
@ -121,7 +134,7 @@ func AuthUserByPasswd(reader func(string) ([]byte, error), userlookup func(strin
r = nil r = nil
runtime.GC() runtime.GC()
_, userErr := userlookup(username) _, userErr := ctx.userlookup(username)
if userErr != nil { if userErr != nil {
valid = false valid = false
} }
@ -135,21 +148,21 @@ func AuthUserByPasswd(reader func(string) ([]byte, error), userlookup func(strin
// via the -g option. // via the -g option.
// The function also check system /etc/passwd to cross-check the user // The function also check system /etc/passwd to cross-check the user
// actually exists. // actually exists.
func AuthUserByToken(reader func(string) ([]byte, error), userlookup func(string) (*user.User, error), username string, connhostname string, auth string) (valid bool) { func AuthUserByToken(ctx *AuthCtx, username string, connhostname string, auth string) (valid bool) {
if reader == nil { if ctx.reader == nil {
reader = ioutil.ReadFile // dependency injection hides that this is required ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
} }
if userlookup == nil { if ctx.userlookup == nil {
userlookup = user.Lookup // again for dependency injection as dep is now hidden ctx.userlookup = user.Lookup // again for dependency injection as dep is now hidden
} }
auth = strings.TrimSpace(auth) auth = strings.TrimSpace(auth)
u, ue := userlookup(username) u, ue := ctx.userlookup(username)
if ue != nil { if ue != nil {
return false return false
} }
b, e := reader(fmt.Sprintf("%s/.xs_id", u.HomeDir)) b, e := ctx.reader(fmt.Sprintf("%s/.xs_id", u.HomeDir))
if e != nil { if e != nil {
log.Printf("INFO: Cannot read %s/.xs_id\n", u.HomeDir) log.Printf("INFO: Cannot read %s/.xs_id\n", u.HomeDir)
return false return false
@ -176,7 +189,7 @@ func AuthUserByToken(reader func(string) ([]byte, error), userlookup func(string
break break
} }
} }
_, userErr := userlookup(username) _, userErr := ctx.userlookup(username)
if userErr != nil { if userErr != nil {
valid = false valid = false
} }

View file

@ -31,6 +31,11 @@ disableduser:!:18310::::::`
readfile_arg_f string readfile_arg_f string
) )
func newMockAuthCtx(reader func(string) ([]byte, error), userlookup func(string) (*user.User, error)) (ret *AuthCtx) {
ret = &AuthCtx{reader, userlookup}
return
}
func _mock_user_Lookup(username string) (*user.User, error) { func _mock_user_Lookup(username string) (*user.User, error) {
username = userlookup_arg_u username = userlookup_arg_u
if username == "baduser" { if username == "baduser" {
@ -64,8 +69,9 @@ func _mock_ioutil_ReadFileHasError(f string) ([]byte, error) {
func TestVerifyPass(t *testing.T) { func TestVerifyPass(t *testing.T) {
readfile_arg_f = "/etc/shadow" readfile_arg_f = "/etc/shadow"
ctx := newMockAuthCtx(_mock_ioutil_ReadFile, nil)
for idx, rec := range testGoodUsers { for idx, rec := range testGoodUsers {
stat, e := VerifyPass(_mock_ioutil_ReadFile, rec.user, rec.passwd) stat, e := VerifyPass(ctx, rec.user, rec.passwd)
if rec.good && (!stat || e != nil) { if rec.good && (!stat || e != nil) {
t.Fatalf("failed %d\n", idx) t.Fatalf("failed %d\n", idx)
} }
@ -73,21 +79,24 @@ func TestVerifyPass(t *testing.T) {
} }
func TestVerifyPassFailsOnEmptyFile(t *testing.T) { func TestVerifyPassFailsOnEmptyFile(t *testing.T) {
stat, e := VerifyPass(_mock_ioutil_ReadFileEmpty, "johndoe", "sompass") ctx := newMockAuthCtx(_mock_ioutil_ReadFileEmpty, nil)
stat, e := VerifyPass(ctx, "johndoe", "somepass")
if stat || (e == nil) { if stat || (e == nil) {
t.Fatal("failed to fail w/empty file") t.Fatal("failed to fail w/empty file")
} }
} }
func TestVerifyPassFailsOnFileError(t *testing.T) { func TestVerifyPassFailsOnFileError(t *testing.T) {
stat, e := VerifyPass(_mock_ioutil_ReadFileEmpty, "johndoe", "somepass") ctx := newMockAuthCtx(_mock_ioutil_ReadFileEmpty, nil)
stat, e := VerifyPass(ctx, "johndoe", "somepass")
if stat || (e == nil) { if stat || (e == nil) {
t.Fatal("failed to fail on ioutil.ReadFile error") t.Fatal("failed to fail on ioutil.ReadFile error")
} }
} }
func TestVerifyPassFailsOnDisabledEntry(t *testing.T) { func TestVerifyPassFailsOnDisabledEntry(t *testing.T) {
stat, e := VerifyPass(_mock_ioutil_ReadFileEmpty, "disableduser", "!") ctx := newMockAuthCtx(_mock_ioutil_ReadFileEmpty, nil)
stat, e := VerifyPass(ctx, "disableduser", "!")
if stat || (e == nil) { if stat || (e == nil) {
t.Fatal("failed to fail on disabled user entry") t.Fatal("failed to fail on disabled user entry")
} }
@ -96,38 +105,43 @@ func TestVerifyPassFailsOnDisabledEntry(t *testing.T) {
//// ////
func TestAuthUserByTokenFailsOnMissingEntryForHost(t *testing.T) { func TestAuthUserByTokenFailsOnMissingEntryForHost(t *testing.T) {
stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, "johndoe", "hostZ", "abcdefg") ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
stat := AuthUserByToken(ctx, "johndoe", "hostZ", "abcdefg")
if stat { if stat {
t.Fatal("failed to fail on missing/mismatched host entry") t.Fatal("failed to fail on missing/mismatched host entry")
} }
} }
func TestAuthUserByTokenFailsOnMissingEntryForUser(t *testing.T) { func TestAuthUserByTokenFailsOnMissingEntryForUser(t *testing.T) {
stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, "unkuser", "hostA", "abcdefg") ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
stat := AuthUserByToken(ctx, "unkuser", "hostA", "abcdefg")
if stat { if stat {
t.Fatal("failed to fail on wrong user") t.Fatal("failed to fail on wrong user")
} }
} }
func TestAuthUserByTokenFailsOnUserLookupFailure(t *testing.T) { func TestAuthUserByTokenFailsOnUserLookupFailure(t *testing.T) {
ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
userlookup_arg_u = "baduser" userlookup_arg_u = "baduser"
stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, "johndoe", "hostA", "abcdefg") stat := AuthUserByToken(ctx, "johndoe", "hostA", "abcdefg")
if stat { if stat {
t.Fatal("failed to fail with bad return from user.Lookup()") t.Fatal("failed to fail with bad return from user.Lookup()")
} }
} }
func TestAuthUserByTokenFailsOnMismatchedTokenForUser(t *testing.T) { func TestAuthUserByTokenFailsOnMismatchedTokenForUser(t *testing.T) {
stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, "johndoe", "hostA", "badtoken") ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
stat := AuthUserByToken(ctx, "johndoe", "hostA", "badtoken")
if stat { if stat {
t.Fatal("failed to fail with valid user, bad token") t.Fatal("failed to fail with valid user, bad token")
} }
} }
func TestAuthUserByTokenSucceedsWithMatchedUserAndToken(t *testing.T) { func TestAuthUserByTokenSucceedsWithMatchedUserAndToken(t *testing.T) {
ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
userlookup_arg_u = "johndoe" userlookup_arg_u = "johndoe"
readfile_arg_f = "/.xs_id" readfile_arg_f = "/.xs_id"
stat := AuthUserByToken(_mock_ioutil_ReadFile, _mock_user_Lookup, userlookup_arg_u, "hostA", "hostA:abcdefg") stat := AuthUserByToken(ctx, userlookup_arg_u, "hostA", "hostA:abcdefg")
if !stat { if !stat {
t.Fatal("failed with valid user and token") t.Fatal("failed with valid user and token")
} }

View file

@ -708,14 +708,14 @@ func main() {
var valid bool var valid bool
var allowedCmds string // Currently unused var allowedCmds string // Currently unused
if xs.AuthUserByToken(ioutil.ReadFile, user.Lookup, string(rec.Who()), string(rec.ConnHost()), string(rec.AuthCookie(true))) { if xs.AuthUserByToken(xs.NewAuthCtx(), string(rec.Who()), string(rec.ConnHost()), string(rec.AuthCookie(true))) {
valid = true valid = true
} else { } else {
if useSystemPasswd { if useSystemPasswd {
//var passErr error //var passErr error
valid, _ /*passErr*/ = xs.VerifyPass(ioutil.ReadFile, string(rec.Who()), string(rec.AuthCookie(true))) valid, _ /*passErr*/ = xs.VerifyPass(xs.NewAuthCtx(), string(rec.Who()), string(rec.AuthCookie(true)))
} else { } else {
valid, allowedCmds = xs.AuthUserByPasswd(ioutil.ReadFile, user.Lookup, string(rec.Who()), string(rec.AuthCookie(true)), "/etc/xs.passwd") valid, allowedCmds = xs.AuthUserByPasswd(xs.NewAuthCtx(), string(rec.Who()), string(rec.AuthCookie(true)), "/etc/xs.passwd")
} }
} }