package pam import ( "errors" "fmt" "os" "os/user" "path/filepath" "runtime" "sync/atomic" "testing" "time" "unsafe" ) func maybeEndTransaction(t *testing.T, tx *Transaction) { t.Helper() if tx == nil { return } err := tx.End() if err != nil { t.Fatalf("end #error: %v", err) } } func ensureTransactionEnds(t *testing.T, tx *Transaction) { t.Helper() runtime.SetFinalizer(tx, func(tx *Transaction) { // #nosec:G103 - the pointer conversion is checked. handle := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&tx.handle))) if handle == nil { return } t.Fatalf("transaction has not been finalized") }) } func TestPAM_001(t *testing.T) { u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") } p := "secret" tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) { return p, nil }) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } err = tx.Authenticate(0) if err != nil { t.Fatalf("authenticate #error: %v", err) } err = tx.AcctMgmt(Silent) if err != nil { t.Fatalf("acct_mgmt #error: %v", err) } err = tx.SetCred(Silent | EstablishCred) if err != nil { t.Fatalf("setcred #error: %v", err) } } func TestPAM_002(t *testing.T) { u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") } tx, err := StartFunc("", "", func(s Style, msg string) (string, error) { switch s { case PromptEchoOn: return "test", nil case PromptEchoOff: return "secret", nil } return "", errors.New("unexpected") }) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } err = tx.Authenticate(0) if err != nil { t.Fatalf("authenticate #error: %v", err) } } type Credentials struct { User string Password string } func (c Credentials) RespondPAM(s Style, msg string) (string, error) { switch s { case PromptEchoOn: return c.User, nil case PromptEchoOff: return c.Password, nil } return "", errors.New("unexpected") } func TestPAM_003(t *testing.T) { u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") } c := Credentials{ User: "test", Password: "secret", } tx, err := Start("", "", c) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } err = tx.Authenticate(0) if err != nil { t.Fatalf("authenticate #error: %v", err) } } func TestPAM_004(t *testing.T) { u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") } c := Credentials{ Password: "secret", } tx, err := Start("", "test", c) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } err = tx.Authenticate(0) if err != nil { t.Fatalf("authenticate #error: %v", err) } } func TestPAM_005(t *testing.T) { u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") } tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { return "secret", nil }) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } service, err := tx.GetItem(Service) if err != nil { t.Fatalf("GetItem #error: %v", err) } if service != "passwd" { t.Fatalf("Unexpected service: %v", service) } err = tx.ChangeAuthTok(Silent) if err != nil { t.Fatalf("chauthtok #error: %v", err) } } func TestPAM_006(t *testing.T) { u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") } tx, err := StartFunc("passwd", u.Username, func(s Style, msg string) (string, error) { return "secret", nil }) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } err = tx.OpenSession(Silent) if err != nil { t.Fatalf("open_session #error: %v", err) } err = tx.CloseSession(Silent) if err != nil { t.Fatalf("close_session #error: %v", err) } } func TestPAM_007(t *testing.T) { u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") } tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) { return "", errors.New("Sorry, it didn't work") }) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } err = tx.Authenticate(0) if err == nil { t.Fatalf("authenticate #expected an error") } s := err.Error() if len(s) == 0 { t.Fatalf("error #expected an error message") } if !errors.Is(err, ErrAuth) { t.Fatalf("error #unexpected error %v", err) } } func TestPAM_ConfDir(t *testing.T) { u, _ := user.Current() c := Credentials{ // the custom service always permits even with wrong password. Password: "wrongsecret", } tx, err := StartConfDir("permit-service", u.Username, c, "test-services") defer func() { if tx != nil { _ = tx.End() } }() if !CheckPamHasStartConfdir() { if err == nil { t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) } // nothing else we do, we don't support it. return } service, err := tx.GetItem(Service) if err != nil { t.Fatalf("GetItem #error: %v", err) } if service != "permit-service" { t.Fatalf("Unexpected service: %v", service) } if err != nil { t.Fatalf("start #error: %v", err) } err = tx.Authenticate(0) if err != nil { t.Fatalf("authenticate #error: %v", err) } } func TestPAM_ConfDir_FailNoServiceOrUnsupported(t *testing.T) { if !CheckPamHasStartConfdir() { t.Skip("this requires PAM with Conf dir support") } u, _ := user.Current() c := Credentials{ Password: "secret", } tx, err := StartConfDir("does-not-exists", u.Username, c, ".") if err == nil { t.Fatalf("authenticate #expected an error") } if tx != nil { t.Fatalf("authenticate #unexpected transaction") } s := err.Error() if len(s) == 0 { t.Fatalf("error #expected an error message") } var pamErr Error if !errors.As(err, &pamErr) { t.Fatalf("error #unexpected type: %#v", err) } if pamErr != ErrAbort { t.Fatalf("error #unexpected status: %v", pamErr) } } func TestPAM_ConfDir_InfoMessage(t *testing.T) { u, _ := user.Current() var infoText string tx, err := StartConfDir("echo-service", u.Username, ConversationFunc(func(s Style, msg string) (string, error) { switch s { case TextInfo: infoText = msg return "", nil } return "", errors.New("unexpected") }), "test-services") ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } service, err := tx.GetItem(Service) if err != nil { t.Fatalf("GetItem #error: %v", err) } if service != "echo-service" { t.Fatalf("Unexpected service: %v", service) } err = tx.Authenticate(0) if err != nil { t.Fatalf("authenticate #error: %v", err) } if infoText != "This is an info message for user "+u.Username+" on echo-service" { t.Fatalf("Unexpected info message: %v", infoText) } } func TestPAM_ConfDir_Deny(t *testing.T) { if !CheckPamHasStartConfdir() { t.Skip("this requires PAM with Conf dir support") } u, _ := user.Current() tx, err := StartConfDir("deny-service", u.Username, Credentials{}, "test-services") ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } service, err := tx.GetItem(Service) if err != nil { t.Fatalf("GetItem #error: %v", err) } if service != "deny-service" { t.Fatalf("Unexpected service: %v", service) } err = tx.Authenticate(0) if err == nil { t.Fatalf("authenticate #expected an error") } s := err.Error() if len(s) == 0 { t.Fatalf("error #expected an error message") } if !errors.Is(err, ErrAuth) { t.Fatalf("error #unexpected error %v", err) } } func TestPAM_ConfDir_PromptForUserName(t *testing.T) { c := Credentials{ User: "testuser", // the custom service only cares about correct user name. Password: "wrongsecret", } tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services") ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if !CheckPamHasStartConfdir() { if err == nil { t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) } // nothing else we do, we don't support it. return } if err != nil { t.Fatalf("start #error: %v", err) } err = tx.Authenticate(0) if err != nil { t.Fatalf("authenticate #error: %v", err) } } func TestPAM_ConfDir_WrongUserName(t *testing.T) { c := Credentials{ User: "wronguser", Password: "wrongsecret", } tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services") ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if !CheckPamHasStartConfdir() { if err == nil { t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) } // nothing else we do, we don't support it. return } err = tx.Authenticate(0) if err == nil { t.Fatalf("authenticate #expected an error") } s := err.Error() if len(s) == 0 { t.Fatalf("error #expected an error message") } if !errors.Is(err, ErrAuth) { t.Fatalf("error #unexpected error %v", err) } } func TestItem(t *testing.T) { tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { return "", nil }) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } s, err := tx.GetItem(Service) if err != nil { t.Fatalf("getitem #error: %v", err) } if s != "passwd" { t.Fatalf("getitem #error: expected passwd, got %v", s) } s, err = tx.GetItem(User) if err != nil { t.Fatalf("getitem #error: %v", err) } if s != "test" { t.Fatalf("getitem #error: expected test, got %v", s) } err = tx.SetItem(User, "root") if err != nil { t.Fatalf("setitem #error: %v", err) } s, err = tx.GetItem(User) if err != nil { t.Fatalf("getitem #error: %v", err) } if s != "root" { t.Fatalf("getitem #error: expected root, got %v", s) } } func TestEnv(t *testing.T) { tx, err := StartFunc("", "", func(s Style, msg string) (string, error) { return "", nil }) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } m, err := tx.GetEnvList() if err != nil { t.Fatalf("getenvlist #error: %v", err) } n := len(m) if n != 0 { t.Fatalf("putenv #error: expected 0 items, got %v", n) } vals := []string{ "VAL1=1", "VAL2=2", "VAL3=3", } for _, s := range vals { err = tx.PutEnv(s) if err != nil { t.Fatalf("putenv #error: %v", err) } } s := tx.GetEnv("VAL0") if s != "" { t.Fatalf("getenv #error: expected \"\", got %v", s) } s = tx.GetEnv("VAL1") if s != "1" { t.Fatalf("getenv #error: expected 1, got %v", s) } s = tx.GetEnv("VAL2") if s != "2" { t.Fatalf("getenv #error: expected 2, got %v", s) } s = tx.GetEnv("VAL3") if s != "3" { t.Fatalf("getenv #error: expected 3, got %v", s) } m, err = tx.GetEnvList() if err != nil { t.Fatalf("getenvlist #error: %v", err) } n = len(m) if n != 3 { t.Fatalf("getenvlist #error: expected 3 items, got %v", n) } if m["VAL1"] != "1" { t.Fatalf("getenvlist #error: expected 1, got %v", m["VAL1"]) } if m["VAL2"] != "2" { t.Fatalf("getenvlist #error: expected 2, got %v", m["VAL1"]) } if m["VAL3"] != "3" { t.Fatalf("getenvlist #error: expected 3, got %v", m["VAL1"]) } } func Test_Error(t *testing.T) { t.Parallel() if !CheckPamHasStartConfdir() { t.Skip("this requires PAM with Conf dir support") } statuses := map[string]error{ "success": nil, "open_err": ErrOpen, "symbol_err": ErrSymbol, "service_err": ErrService, "system_err": ErrSystem, "buf_err": ErrBuf, "perm_denied": ErrPermDenied, "auth_err": ErrAuth, "cred_insufficient": ErrCredInsufficient, "authinfo_unavail": ErrAuthinfoUnavail, "user_unknown": ErrUserUnknown, "maxtries": ErrMaxtries, "new_authtok_reqd": ErrNewAuthtokReqd, "acct_expired": ErrAcctExpired, "session_err": ErrSession, "cred_unavail": ErrCredUnavail, "cred_expired": ErrCredExpired, "cred_err": ErrCred, "no_module_data": ErrNoModuleData, "conv_err": ErrConv, "authtok_err": ErrAuthtok, "authtok_recover_err": ErrAuthtokRecovery, "authtok_lock_busy": ErrAuthtokLockBusy, "authtok_disable_aging": ErrAuthtokDisableAging, "try_again": ErrTryAgain, "ignore": nil, /* Ignore can't be returned */ "abort": ErrAbort, "authtok_expired": ErrAuthtokExpired, "module_unknown": ErrModuleUnknown, "bad_item": ErrBadItem, "conv_again": ErrConvAgain, "incomplete": ErrIncomplete, } type Action int const ( account Action = iota + 1 auth password session ) actions := map[string]Action{ "account": account, "auth": auth, "password": password, "session": session, } c := Credentials{} servicePath := t.TempDir() for ret, expected := range statuses { ret := ret expected := expected for actionName, action := range actions { actionName := actionName action := action t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) { t.Parallel() serviceName := ret + "-" + actionName serviceFile := filepath.Join(servicePath, serviceName) contents := fmt.Sprintf("%[1]s requisite pam_debug.so "+ "auth=%[2]s cred=%[2]s acct=%[2]s prechauthtok=%[2]s "+ "chauthtok=%[2]s open_session=%[2]s close_session=%[2]s\n"+ "%[1]s requisite pam_permit.so\n", actionName, ret) if err := os.WriteFile(serviceFile, []byte(contents), 0600); err != nil { t.Fatalf("can't create service file %v: %v", serviceFile, err) } tx, err := StartConfDir(serviceName, "user", c, servicePath) ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } switch action { case account: err = tx.AcctMgmt(0) case auth: err = tx.Authenticate(0) case password: err = tx.ChangeAuthTok(0) case session: err = tx.OpenSession(0) } if !errors.Is(err, expected) { t.Fatalf("error #unexpected status %#v vs %#v", err, expected) } if err != nil { var status Error if !errors.As(err, &status) || err.Error() != status.Error() { t.Fatalf("error #unexpected status %#v vs %#v", err.Error(), status.Error()) } } }) } } } func Test_Finalizer(t *testing.T) { if !CheckPamHasStartConfdir() { t.Skip("this requires PAM with Conf dir support") } func() { tx, err := StartConfDir("permit-service", "", nil, "test-services") ensureTransactionEnds(t, tx) defer maybeEndTransaction(t, tx) if err != nil { t.Fatalf("start #error: %v", err) } }() runtime.GC() // sleep to switch to finalizer goroutine time.Sleep(5 * time.Millisecond) } func TestFailure_001(t *testing.T) { tx := Transaction{} _, err := tx.GetEnvList() if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_002(t *testing.T) { tx := Transaction{} err := tx.PutEnv("") if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_003(t *testing.T) { tx := Transaction{} err := tx.CloseSession(0) if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_004(t *testing.T) { tx := Transaction{} err := tx.OpenSession(0) if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_005(t *testing.T) { tx := Transaction{} err := tx.ChangeAuthTok(0) if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_006(t *testing.T) { tx := Transaction{} err := tx.AcctMgmt(0) if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_007(t *testing.T) { tx := Transaction{} err := tx.SetCred(0) if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_008(t *testing.T) { tx := Transaction{} err := tx.SetItem(User, "test") if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_009(t *testing.T) { tx := Transaction{} _, err := tx.GetItem(User) if err == nil { t.Fatalf("getenvlist #expected an error") } } func TestFailure_010(t *testing.T) { tx := Transaction{} err := tx.End() if err != nil { t.Fatalf("end #unexpected error %v", err) } }