summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Steinert <[email protected]>2023-11-29 20:08:15 -0600
committerGitHub <[email protected]>2023-11-29 20:08:15 -0600
commit4ce1d8a7e0b557e7092b3e759037d5cbcb3f9380 (patch)
treeb4e32467d5e0256f2d4a8854735b011b3430fa1f
parenta85a609bbe4f52a2f19a24bcfbfcdb3c23decd45 (diff)
parent067f634acb780246805728d068f2bfcedcc4a4ea (diff)
Merge pull request #15 from 3v1n0/safer-transaction
Safer transaction: add End() method and don't use as error
-rw-r--r--.github/workflows/lint.yaml22
-rw-r--r--.golangci.yaml61
-rw-r--r--errors.go94
-rw-r--r--transaction.go205
-rw-r--r--transaction_test.go264
5 files changed, 549 insertions, 97 deletions
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
new file mode 100644
index 0000000..771e735
--- /dev/null
+++ b/.github/workflows/lint.yaml
@@ -0,0 +1,22 @@
+on: [push, pull_request]
+name: Lint
+
+permissions:
+ contents: read
+
+jobs:
+ golangci:
+ name: lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v4
+ with:
+ go-version: '1.21'
+ cache: false
+ - name: Install PAM
+ run: sudo apt install -y libpam-dev
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v3
+ with:
+ version: v1.54
diff --git a/.golangci.yaml b/.golangci.yaml
new file mode 100644
index 0000000..bbfa6b4
--- /dev/null
+++ b/.golangci.yaml
@@ -0,0 +1,61 @@
+# This is for linting. To run it, please use:
+# golangci-lint run ${MODULE}/... [--fix]
+
+linters:
+ # linters to run in addition to default ones
+ enable:
+ - dupl
+ - durationcheck
+ - errname
+ - errorlint
+ - exportloopref
+ - forbidigo
+ - forcetypeassert
+ - gci
+ - godot
+ - gofmt
+ - gosec
+ - misspell
+ - nakedret
+ - nolintlint
+ - revive
+ - thelper
+ - tparallel
+ - unconvert
+ - unparam
+ - whitespace
+
+run:
+ timeout: 5m
+
+# Get all linter issues, even if duplicated
+issues:
+ exclude-use-default: false
+ max-issues-per-linter: 0
+ max-same-issues: 0
+ fix: false # we don’t want this in CI
+ exclude:
+ # EXC0001 errcheck: most errors are in defer calls, which are safe to ignore and idiomatic Go (would be good to only ignore defer ones though)
+ - 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv|w\.Stop). is not checked'
+ # EXC0008 gosec: duplicated of errcheck
+ - (G104|G307)
+ # EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)'
+ - Potential file inclusion via variable
+ # We want named parameters even if unused, as they help better document the function
+ - unused-parameter
+ # Sometimes it is more readable it do a `if err:=a(); err != nil` tha simpy `return a()`
+ - if-return
+
+nolintlint:
+ require-explanation: true
+ require-specific: true
+
+linters-settings:
+ # Forbid the usage of deprecated ioutil and debug prints
+ forbidigo:
+ forbid:
+ - ioutil\.
+ - ^print.*$
+ # Never have naked return ever
+ nakedret:
+ max-func-lines: 1
diff --git a/errors.go b/errors.go
new file mode 100644
index 0000000..2f81a9a
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,94 @@
+package pam
+
+/*
+#include <security/pam_appl.h>
+*/
+import "C"
+
+// Error is the Type for PAM Return types
+type Error int
+
+// Pam Return types
+const (
+ // OpenErr indicates a dlopen() failure when dynamically loading a
+ // service module.
+ ErrOpen Error = C.PAM_OPEN_ERR
+ // ErrSymbol indicates a symbol not found.
+ ErrSymbol Error = C.PAM_SYMBOL_ERR
+ // ErrService indicates a error in service module.
+ ErrService Error = C.PAM_SERVICE_ERR
+ // ErrSystem indicates a system error.
+ ErrSystem Error = C.PAM_SYSTEM_ERR
+ // ErrBuf indicates a memory buffer error.
+ ErrBuf Error = C.PAM_BUF_ERR
+ // ErrPermDenied indicates a permission denied.
+ ErrPermDenied Error = C.PAM_PERM_DENIED
+ // ErrAuth indicates a authentication failure.
+ ErrAuth Error = C.PAM_AUTH_ERR
+ // ErrCredInsufficient indicates a can not access authentication data due to
+ // insufficient credentials.
+ ErrCredInsufficient Error = C.PAM_CRED_INSUFFICIENT
+ // ErrAuthinfoUnavail indicates that the underlying authentication service
+ // can not retrieve authentication information.
+ ErrAuthinfoUnavail Error = C.PAM_AUTHINFO_UNAVAIL
+ // ErrUserUnknown indicates a user not known to the underlying authentication
+ // module.
+ ErrUserUnknown Error = C.PAM_USER_UNKNOWN
+ // ErrMaxtries indicates that an authentication service has maintained a retry
+ // count which has been reached. No further retries should be attempted.
+ ErrMaxtries Error = C.PAM_MAXTRIES
+ // ErrNewAuthtokReqd indicates a new authentication token required. This is
+ // normally returned if the machine security policies require that the
+ // password should be changed because the password is nil or it has aged.
+ ErrNewAuthtokReqd Error = C.PAM_NEW_AUTHTOK_REQD
+ // ErrAcctExpired indicates that an user account has expired.
+ ErrAcctExpired Error = C.PAM_ACCT_EXPIRED
+ // ErrSession indicates a can not make/remove an entry for the
+ // specified session.
+ ErrSession Error = C.PAM_SESSION_ERR
+ // ErrCredUnavail indicates that an underlying authentication service can not
+ // retrieve user credentials.
+ ErrCredUnavail Error = C.PAM_CRED_UNAVAIL
+ // ErrCredExpired indicates that an user credentials expired.
+ ErrCredExpired Error = C.PAM_CRED_EXPIRED
+ // ErrCred indicates a failure setting user credentials.
+ ErrCred Error = C.PAM_CRED_ERR
+ // ErrNoModuleData indicates a no module specific data is present.
+ ErrNoModuleData Error = C.PAM_NO_MODULE_DATA
+ // ErrConv indicates a conversation error.
+ ErrConv Error = C.PAM_CONV_ERR
+ // ErrAuthtokErr indicates an authentication token manipulation error.
+ ErrAuthtok Error = C.PAM_AUTHTOK_ERR
+ // ErrAuthtokRecoveryErr indicates an authentication information cannot
+ // be recovered.
+ ErrAuthtokRecovery Error = C.PAM_AUTHTOK_RECOVERY_ERR
+ // ErrAuthtokLockBusy indicates am authentication token lock busy.
+ ErrAuthtokLockBusy Error = C.PAM_AUTHTOK_LOCK_BUSY
+ // ErrAuthtokDisableAging indicates an authentication token aging disabled.
+ ErrAuthtokDisableAging Error = C.PAM_AUTHTOK_DISABLE_AGING
+ // ErrTryAgain indicates a preliminary check by password service.
+ ErrTryAgain Error = C.PAM_TRY_AGAIN
+ // ErrIgnore indicates to ignore underlying account module regardless of
+ // whether the control flag is required, optional, or sufficient.
+ ErrIgnore Error = C.PAM_IGNORE
+ // ErrAbort indicates a critical error (module fail now request).
+ ErrAbort Error = C.PAM_ABORT
+ // ErrAuthtokExpired indicates an user's authentication token has expired.
+ ErrAuthtokExpired Error = C.PAM_AUTHTOK_EXPIRED
+ // ErrModuleUnknown indicates a module is not known.
+ ErrModuleUnknown Error = C.PAM_MODULE_UNKNOWN
+ // ErrBadItem indicates a bad item passed to pam_*_item().
+ ErrBadItem Error = C.PAM_BAD_ITEM
+ // ErrConvAgain indicates a conversation function is event driven and data
+ // is not available yet.
+ ErrConvAgain Error = C.PAM_CONV_AGAIN
+ // ErrIncomplete indicates to please call this function again to complete
+ // authentication stack. Before calling again, verify that conversation
+ // is completed.
+ ErrIncomplete Error = C.PAM_INCOMPLETE
+)
+
+// Error returns the error message for the given status.
+func (status Error) Error() string {
+ return C.GoString(C.pam_strerror(nil, C.int(status)))
+}
diff --git a/transaction.go b/transaction.go
index 96bff63..9fee575 100644
--- a/transaction.go
+++ b/transaction.go
@@ -22,13 +22,16 @@ package pam
import "C"
import (
- "errors"
- "runtime"
+ "fmt"
"runtime/cgo"
"strings"
+ "sync/atomic"
"unsafe"
)
+// success indicates a successful function return.
+const success = C.PAM_SUCCESS
+
// Style is the type of message that the conversation handler should display.
type Style int
@@ -39,16 +42,16 @@ const (
PromptEchoOff Style = C.PAM_PROMPT_ECHO_OFF
// PromptEchoOn indicates the conversation handler should obtain a
// string while echoing text.
- PromptEchoOn = C.PAM_PROMPT_ECHO_ON
+ PromptEchoOn Style = C.PAM_PROMPT_ECHO_ON
// ErrorMsg indicates the conversation handler should display an
// error message.
- ErrorMsg = C.PAM_ERROR_MSG
+ ErrorMsg Style = C.PAM_ERROR_MSG
// TextInfo indicates the conversation handler should display some
// text.
- TextInfo = C.PAM_TEXT_INFO
+ TextInfo Style = C.PAM_TEXT_INFO
// BinaryPrompt indicates the conversation handler that should implement
// the private binary protocol
- BinaryPrompt = C.PAM_BINARY_PROMPT
+ BinaryPrompt Style = C.PAM_BINARY_PROMPT
)
// ConversationHandler is an interface for objects that can be used as
@@ -94,42 +97,61 @@ func cbPAMConv(s C.int, msg *C.char, c C.uintptr_t) (*C.char, C.int) {
var err error
v := cgo.Handle(c).Value()
style := Style(s)
+ var handler ConversationHandler
switch cb := v.(type) {
case BinaryConversationHandler:
if style == BinaryPrompt {
bytes, err := cb.RespondPAMBinary(BinaryPointer(msg))
if err != nil {
- return nil, C.PAM_CONV_ERR
+ return nil, C.int(ErrConv)
}
- return (*C.char)(C.CBytes(bytes)), C.PAM_SUCCESS
- } else {
- r, err = cb.RespondPAM(style, C.GoString(msg))
+ return (*C.char)(C.CBytes(bytes)), success
}
+ handler = cb
case ConversationHandler:
if style == BinaryPrompt {
- return nil, C.PAM_AUTHINFO_UNAVAIL
+ return nil, C.int(ErrConv)
}
- r, err = cb.RespondPAM(style, C.GoString(msg))
+ handler = cb
+ }
+ if handler == nil {
+ return nil, C.int(ErrConv)
}
+ r, err = handler.RespondPAM(style, C.GoString(msg))
if err != nil {
- return nil, C.PAM_CONV_ERR
+ return nil, C.int(ErrConv)
}
- return C.CString(r), C.PAM_SUCCESS
+ return C.CString(r), success
}
// Transaction is the application's handle for a PAM transaction.
type Transaction struct {
- handle *C.pam_handle_t
- conv *C.struct_pam_conv
- status C.int
- c cgo.Handle
+ handle *C.pam_handle_t
+ conv *C.struct_pam_conv
+ lastStatus atomic.Int32
+ c cgo.Handle
}
-// transactionFinalizer cleans up the PAM handle and deletes the callback
-// function.
-func transactionFinalizer(t *Transaction) {
- C.pam_end(t.handle, t.status)
- t.c.Delete()
+// End cleans up the PAM handle and deletes the callback function.
+// It must be called when done with the transaction.
+func (t *Transaction) End() error {
+ handle := atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&t.handle)), nil)
+ if handle == nil {
+ return nil
+ }
+
+ defer t.c.Delete()
+ return t.handlePamStatus(C.pam_end((*C.pam_handle_t)(handle),
+ C.int(t.lastStatus.Load())))
+}
+
+// Allows to call pam functions managing return status
+func (t *Transaction) handlePamStatus(cStatus C.int) error {
+ t.lastStatus.Store(int32(cStatus))
+ if status := Error(cStatus); status != success {
+ return status
+ }
+ return nil
}
// Start initiates a new PAM transaction. Service is treated identically to
@@ -137,13 +159,21 @@ func transactionFinalizer(t *Transaction) {
//
// All application calls to PAM begin with Start*. The returned
// transaction provides an interface to the remainder of the API.
+//
+// It's responsibility of the Transaction owner to release all the resources
+// allocated underneath by PAM by calling End() once done.
+//
+// It's not advised to End the transaction using a runtime.SetFinalizer unless
+// you're absolutely sure that your stack is multi-thread friendly (normally it
+// is not!) and using a LockOSThread/UnlockOSThread pair.
func Start(service, user string, handler ConversationHandler) (*Transaction, error) {
return start(service, user, handler, "")
}
-// StartFunc registers the handler func as a conversation handler.
+// StartFunc registers the handler func as a conversation handler and starts
+// the transaction (see Start() documentation).
func StartFunc(service, user string, handler func(Style, string) (string, error)) (*Transaction, error) {
- return Start(service, user, ConversationFunc(handler))
+ return start(service, user, ConversationFunc(handler), "")
}
// StartConfDir initiates a new PAM transaction. Service is treated identically to
@@ -153,9 +183,18 @@ func StartFunc(service, user string, handler func(Style, string) (string, error)
//
// All application calls to PAM begin with Start*. The returned
// transaction provides an interface to the remainder of the API.
+//
+// It's responsibility of the Transaction owner to release all the resources
+// allocated underneath by PAM by calling End() once done.
+//
+// It's not advised to End the transaction using a runtime.SetFinalizer unless
+// you're absolutely sure that your stack is multi-thread friendly (normally it
+// is not!) and using a LockOSThread/UnlockOSThread pair.
func StartConfDir(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
if !CheckPamHasStartConfdir() {
- return nil, errors.New("StartConfDir() was used, but the pam version on the system is not recent enough")
+ return nil, fmt.Errorf(
+ "%w: StartConfDir was used, but the pam version on the system is not recent enough",
+ ErrSystem)
}
return start(service, user, handler, confDir)
@@ -165,15 +204,16 @@ func start(service, user string, handler ConversationHandler, confDir string) (*
switch handler.(type) {
case BinaryConversationHandler:
if !CheckPamHasBinaryProtocol() {
- return nil, errors.New("BinaryConversationHandler() was used, but it is not supported by this platform")
+ return nil, fmt.Errorf("%w: BinaryConversationHandler was used, but it is not supported by this platform",
+ ErrSystem)
}
}
t := &Transaction{
conv: &C.struct_pam_conv{},
c: cgo.NewHandle(handler),
}
+
C.init_pam_conv(t.conv, C.uintptr_t(t.c))
- runtime.SetFinalizer(t, transactionFinalizer)
s := C.CString(service)
defer C.free(unsafe.Pointer(s))
var u *C.char
@@ -181,23 +221,21 @@ func start(service, user string, handler ConversationHandler, confDir string) (*
u = C.CString(user)
defer C.free(unsafe.Pointer(u))
}
+ var err error
if confDir == "" {
- t.status = C.pam_start(s, u, t.conv, &t.handle)
+ err = t.handlePamStatus(C.pam_start(s, u, t.conv, &t.handle))
} else {
c := C.CString(confDir)
defer C.free(unsafe.Pointer(c))
- t.status = C.pam_start_confdir(s, u, t.conv, c, &t.handle)
+ err = t.handlePamStatus(C.pam_start_confdir(s, u, t.conv, c, &t.handle))
}
- if t.status != C.PAM_SUCCESS {
- return nil, t
+ if err != nil {
+ var _ = t.End()
+ return nil, err
}
return t, nil
}
-func (t *Transaction) Error() string {
- return C.GoString(C.pam_strerror(t.handle, C.int(t.status)))
-}
-
// Item is a an PAM information type.
type Item int
@@ -206,38 +244,42 @@ const (
// Service is the name which identifies the PAM stack.
Service Item = C.PAM_SERVICE
// User identifies the username identity used by a service.
- User = C.PAM_USER
+ User Item = C.PAM_USER
// Tty is the terminal name.
- Tty = C.PAM_TTY
+ Tty Item = C.PAM_TTY
// Rhost is the requesting host name.
- Rhost = C.PAM_RHOST
+ Rhost Item = C.PAM_RHOST
// Authtok is the currently active authentication token.
- Authtok = C.PAM_AUTHTOK
+ Authtok Item = C.PAM_AUTHTOK
// Oldauthtok is the old authentication token.
- Oldauthtok = C.PAM_OLDAUTHTOK
+ Oldauthtok Item = C.PAM_OLDAUTHTOK
// Ruser is the requesting user name.
- Ruser = C.PAM_RUSER
+ Ruser Item = C.PAM_RUSER
// UserPrompt is the string use to prompt for a username.
- UserPrompt = C.PAM_USER_PROMPT
+ UserPrompt Item = C.PAM_USER_PROMPT
+ // FailDelay is the app supplied function to override failure delays.
+ FailDelay Item = C.PAM_FAIL_DELAY
+ // Xdisplay is the X display name
+ Xdisplay Item = C.PAM_XDISPLAY
+ // Xauthdata is the X server authentication data.
+ Xauthdata Item = C.PAM_XAUTHDATA
+ // AuthtokType is the type for pam_get_authtok
+ AuthtokType Item = C.PAM_AUTHTOK_TYPE
)
// SetItem sets a PAM information item.
func (t *Transaction) SetItem(i Item, item string) error {
cs := unsafe.Pointer(C.CString(item))
defer C.free(cs)
- t.status = C.pam_set_item(t.handle, C.int(i), cs)
- if t.status != C.PAM_SUCCESS {
- return t
- }
- return nil
+ return t.handlePamStatus(C.pam_set_item(t.handle, C.int(i), cs))
}
// GetItem retrieves a PAM information item.
func (t *Transaction) GetItem(i Item) (string, error) {
var s unsafe.Pointer
- t.status = C.pam_get_item(t.handle, C.int(i), &s)
- if t.status != C.PAM_SUCCESS {
- return "", t
+ err := t.handlePamStatus(C.pam_get_item(t.handle, C.int(i), &s))
+ if err != nil {
+ return "", err
}
return C.GoString((*C.char)(s)), nil
}
@@ -253,32 +295,28 @@ const (
Silent Flags = C.PAM_SILENT
// DisallowNullAuthtok indicates that authorization should fail
// if the user does not have a registered authentication token.
- DisallowNullAuthtok = C.PAM_DISALLOW_NULL_AUTHTOK
+ DisallowNullAuthtok Flags = C.PAM_DISALLOW_NULL_AUTHTOK
// EstablishCred indicates that credentials should be established
// for the user.
- EstablishCred = C.PAM_ESTABLISH_CRED
- // DeleteCred inidicates that credentials should be deleted.
- DeleteCred = C.PAM_DELETE_CRED
+ EstablishCred Flags = C.PAM_ESTABLISH_CRED
+ // DeleteCred indicates that credentials should be deleted.
+ DeleteCred Flags = C.PAM_DELETE_CRED
// ReinitializeCred indicates that credentials should be fully
// reinitialized.
- ReinitializeCred = C.PAM_REINITIALIZE_CRED
+ ReinitializeCred Flags = C.PAM_REINITIALIZE_CRED
// RefreshCred indicates that the lifetime of existing credentials
// should be extended.
- RefreshCred = C.PAM_REFRESH_CRED
+ RefreshCred Flags = C.PAM_REFRESH_CRED
// ChangeExpiredAuthtok indicates that the authentication token
// should be changed if it has expired.
- ChangeExpiredAuthtok = C.PAM_CHANGE_EXPIRED_AUTHTOK
+ ChangeExpiredAuthtok Flags = C.PAM_CHANGE_EXPIRED_AUTHTOK
)
// Authenticate is used to authenticate the user.
//
// Valid flags: Silent, DisallowNullAuthtok
func (t *Transaction) Authenticate(f Flags) error {
- t.status = C.pam_authenticate(t.handle, C.int(f))
- if t.status != C.PAM_SUCCESS {
- return t
- }
- return nil
+ return t.handlePamStatus(C.pam_authenticate(t.handle, C.int(f)))
}
// SetCred is used to establish, maintain and delete the credentials of a
@@ -286,55 +324,35 @@ func (t *Transaction) Authenticate(f Flags) error {
//
// Valid flags: EstablishCred, DeleteCred, ReinitializeCred, RefreshCred
func (t *Transaction) SetCred(f Flags) error {
- t.status = C.pam_setcred(t.handle, C.int(f))
- if t.status != C.PAM_SUCCESS {
- return t
- }
- return nil
+ return t.handlePamStatus(C.pam_setcred(t.handle, C.int(f)))
}
// AcctMgmt is used to determine if the user's account is valid.
//
// Valid flags: Silent, DisallowNullAuthtok
func (t *Transaction) AcctMgmt(f Flags) error {
- t.status = C.pam_acct_mgmt(t.handle, C.int(f))
- if t.status != C.PAM_SUCCESS {
- return t
- }
- return nil
+ return t.handlePamStatus(C.pam_acct_mgmt(t.handle, C.int(f)))
}
// ChangeAuthTok is used to change the authentication token.
//
// Valid flags: Silent, ChangeExpiredAuthtok
func (t *Transaction) ChangeAuthTok(f Flags) error {
- t.status = C.pam_chauthtok(t.handle, C.int(f))
- if t.status != C.PAM_SUCCESS {
- return t
- }
- return nil
+ return t.handlePamStatus(C.pam_chauthtok(t.handle, C.int(f)))
}
// OpenSession sets up a user session for an authenticated user.
//
// Valid flags: Slient
func (t *Transaction) OpenSession(f Flags) error {
- t.status = C.pam_open_session(t.handle, C.int(f))
- if t.status != C.PAM_SUCCESS {
- return t
- }
- return nil
+ return t.handlePamStatus(C.pam_open_session(t.handle, C.int(f)))
}
// CloseSession closes a previously opened session.
//
// Valid flags: Silent
func (t *Transaction) CloseSession(f Flags) error {
- t.status = C.pam_close_session(t.handle, C.int(f))
- if t.status != C.PAM_SUCCESS {
- return t
- }
- return nil
+ return t.handlePamStatus(C.pam_close_session(t.handle, C.int(f)))
}
// PutEnv adds or changes the value of PAM environment variables.
@@ -345,11 +363,7 @@ func (t *Transaction) CloseSession(f Flags) error {
func (t *Transaction) PutEnv(nameval string) error {
cs := C.CString(nameval)
defer C.free(unsafe.Pointer(cs))
- t.status = C.pam_putenv(t.handle, cs)
- if t.status != C.PAM_SUCCESS {
- return t
- }
- return nil
+ return t.handlePamStatus(C.pam_putenv(t.handle, cs))
}
// GetEnv is used to retrieve a PAM environment variable.
@@ -372,9 +386,10 @@ func (t *Transaction) GetEnvList() (map[string]string, error) {
env := make(map[string]string)
p := C.pam_getenvlist(t.handle)
if p == nil {
- t.status = C.PAM_BUF_ERR
- return nil, t
+ t.lastStatus.Store(int32(ErrBuf))
+ return nil, ErrBuf
}
+ t.lastStatus.Store(success)
for q := p; *q != nil; q = next(q) {
chunks := strings.SplitN(C.GoString(*q), "=", 2)
if len(chunks) == 2 {
diff --git a/transaction_test.go b/transaction_test.go
index 94aa9c1..d358b0e 100644
--- a/transaction_test.go
+++ b/transaction_test.go
@@ -2,10 +2,42 @@ 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" {
@@ -15,6 +47,8 @@ func TestPAM_001(t *testing.T) {
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)
}
@@ -46,6 +80,8 @@ func TestPAM_002(t *testing.T) {
}
return "", errors.New("unexpected")
})
+ ensureTransactionEnds(t, tx)
+ defer maybeEndTransaction(t, tx)
if err != nil {
t.Fatalf("start #error: %v", err)
}
@@ -80,6 +116,8 @@ func TestPAM_003(t *testing.T) {
Password: "secret",
}
tx, err := Start("", "", c)
+ ensureTransactionEnds(t, tx)
+ defer maybeEndTransaction(t, tx)
if err != nil {
t.Fatalf("start #error: %v", err)
}
@@ -98,6 +136,8 @@ func TestPAM_004(t *testing.T) {
Password: "secret",
}
tx, err := Start("", "test", c)
+ ensureTransactionEnds(t, tx)
+ defer maybeEndTransaction(t, tx)
if err != nil {
t.Fatalf("start #error: %v", err)
}
@@ -115,9 +155,18 @@ func TestPAM_005(t *testing.T) {
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)
@@ -132,6 +181,8 @@ func TestPAM_006(t *testing.T) {
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)
}
@@ -153,6 +204,8 @@ func TestPAM_007(t *testing.T) {
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)
}
@@ -164,6 +217,9 @@ func TestPAM_007(t *testing.T) {
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) {
@@ -173,6 +229,11 @@ func TestPAM_ConfDir(t *testing.T) {
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)
@@ -180,6 +241,13 @@ func TestPAM_ConfDir(t *testing.T) {
// 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)
}
@@ -190,18 +258,31 @@ func TestPAM_ConfDir(t *testing.T) {
}
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",
}
- _, err := StartConfDir("does-not-exists", u.Username, c, ".")
+ 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) {
@@ -216,9 +297,18 @@ func TestPAM_ConfDir_InfoMessage(t *testing.T) {
}
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)
@@ -229,11 +319,23 @@ func TestPAM_ConfDir_InfoMessage(t *testing.T) {
}
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")
@@ -242,6 +344,9 @@ func TestPAM_ConfDir_Deny(t *testing.T) {
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) {
@@ -251,6 +356,8 @@ func TestPAM_ConfDir_PromptForUserName(t *testing.T) {
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)
@@ -273,6 +380,8 @@ func TestPAM_ConfDir_WrongUserName(t *testing.T) {
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)
@@ -288,12 +397,20 @@ func TestPAM_ConfDir_WrongUserName(t *testing.T) {
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, _ := StartFunc("passwd", "test", func(s Style, msg string) (string, error) {
+ 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 {
@@ -328,6 +445,8 @@ 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)
}
@@ -390,6 +509,139 @@ func TestEnv(t *testing.T) {
}
}
+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()
@@ -461,3 +713,11 @@ func TestFailure_009(t *testing.T) {
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)
+ }
+}