pam-moduler: Add test that generates a new debug module and verify it works
We mimic what pam_debug.so does by default, by implementing a similar module fully in go, generated using pam-moduler. This requires various utilities to generate the module and run the tests that are in a separate internal modules so that it can be shared between multiple implementations
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
coverage.out
|
coverage.out
|
||||||
example-module/*.so
|
example-module/*.so
|
||||||
example-module/*.h
|
example-module/*.h
|
||||||
|
cmd/pam-moduler/tests/*/*.so
|
||||||
|
cmd/pam-moduler/tests/*/*.h
|
||||||
|
|||||||
119
cmd/pam-moduler/tests/debug-module/debug-module.go
Normal file
119
cmd/pam-moduler/tests/debug-module/debug-module.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
//go:generate go run github.com/msteinert/pam/v2/cmd/pam-moduler -libname "pam_godebug.so"
|
||||||
|
//go:generate go generate --skip="pam_module.go"
|
||||||
|
|
||||||
|
// This is a similar implementation of pam_debug.so
|
||||||
|
|
||||||
|
// Package main is the package for the debug PAM module library
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pamModuleHandler pam.ModuleHandler = &DebugModule{}
|
||||||
|
var _ = pamModuleHandler
|
||||||
|
|
||||||
|
var moduleArgsRetTypes = map[string]error{
|
||||||
|
"success": nil,
|
||||||
|
"open_err": pam.ErrOpen,
|
||||||
|
"symbol_err": pam.ErrSymbol,
|
||||||
|
"service_err": pam.ErrService,
|
||||||
|
"system_err": pam.ErrSystem,
|
||||||
|
"buf_err": pam.ErrBuf,
|
||||||
|
"perm_denied": pam.ErrPermDenied,
|
||||||
|
"auth_err": pam.ErrAuth,
|
||||||
|
"cred_insufficient": pam.ErrCredInsufficient,
|
||||||
|
"authinfo_unavail": pam.ErrAuthinfoUnavail,
|
||||||
|
"user_unknown": pam.ErrUserUnknown,
|
||||||
|
"maxtries": pam.ErrMaxtries,
|
||||||
|
"new_authtok_reqd": pam.ErrNewAuthtokReqd,
|
||||||
|
"acct_expired": pam.ErrAcctExpired,
|
||||||
|
"session_err": pam.ErrSession,
|
||||||
|
"cred_unavail": pam.ErrCredUnavail,
|
||||||
|
"cred_expired": pam.ErrCredExpired,
|
||||||
|
"cred_err": pam.ErrCred,
|
||||||
|
"no_module_data": pam.ErrNoModuleData,
|
||||||
|
"conv_err": pam.ErrConv,
|
||||||
|
"authtok_err": pam.ErrAuthtok,
|
||||||
|
"authtok_recover_err": pam.ErrAuthtokRecovery,
|
||||||
|
"authtok_lock_busy": pam.ErrAuthtokLockBusy,
|
||||||
|
"authtok_disable_aging": pam.ErrAuthtokDisableAging,
|
||||||
|
"try_again": pam.ErrTryAgain,
|
||||||
|
"ignore": pam.ErrIgnore,
|
||||||
|
"abort": pam.ErrAbort,
|
||||||
|
"authtok_expired": pam.ErrAuthtokExpired,
|
||||||
|
"module_unknown": pam.ErrModuleUnknown,
|
||||||
|
"bad_item": pam.ErrBadItem,
|
||||||
|
"conv_again": pam.ErrConvAgain,
|
||||||
|
"incomplete": pam.ErrIncomplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugModuleArgs = []string{"auth", "cred", "acct", "prechauthtok",
|
||||||
|
"chauthtok", "open_session", "close_session"}
|
||||||
|
|
||||||
|
// DebugModule is the PAM module structure.
|
||||||
|
type DebugModule struct {
|
||||||
|
utils.BaseModule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DebugModule) getReturnType(args []string, key string) error {
|
||||||
|
var value string
|
||||||
|
for _, a := range args {
|
||||||
|
v, found := strings.CutPrefix(a, key+"=")
|
||||||
|
if found {
|
||||||
|
value = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
return fmt.Errorf("Value not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret, found := moduleArgsRetTypes[value]; found {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Parameter %s not known", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DebugModule) handleCall(args []string, action string) error {
|
||||||
|
err := dm.getReturnType(args, action)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("error %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcctMgmt is a PAM handler.
|
||||||
|
func (dm *DebugModule) AcctMgmt(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "acct")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate is a PAM handler.
|
||||||
|
func (dm *DebugModule) Authenticate(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeAuthTok is a PAM handler.
|
||||||
|
func (dm *DebugModule) ChangeAuthTok(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "chauthtok")
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSession is a PAM handler.
|
||||||
|
func (dm *DebugModule) OpenSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "open_session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseSession is a PAM handler.
|
||||||
|
func (dm *DebugModule) CloseSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "close_session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCred is a PAM handler.
|
||||||
|
func (dm *DebugModule) SetCred(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "cred")
|
||||||
|
}
|
||||||
120
cmd/pam-moduler/tests/debug-module/debug-module_test.go
Normal file
120
cmd/pam-moduler/tests/debug-module/debug-module_test.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_DebugModule_ActionStatus(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
module := DebugModule{}
|
||||||
|
|
||||||
|
for ret, expected := range moduleArgsRetTypes {
|
||||||
|
ret := ret
|
||||||
|
expected := expected
|
||||||
|
for actionName, action := range utils.Actions {
|
||||||
|
actionName := actionName
|
||||||
|
action := action
|
||||||
|
t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
moduleArgs := make([]string, 0)
|
||||||
|
for _, a := range debugModuleArgs {
|
||||||
|
moduleArgs = append(moduleArgs, fmt.Sprintf("%s=%s", a, ret))
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := pam.ModuleTransactionInvoker(nil)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case utils.Account:
|
||||||
|
err = module.AcctMgmt(mt, 0, moduleArgs)
|
||||||
|
case utils.Auth:
|
||||||
|
err = module.Authenticate(mt, 0, moduleArgs)
|
||||||
|
case utils.Password:
|
||||||
|
err = module.ChangeAuthTok(mt, 0, moduleArgs)
|
||||||
|
case utils.Session:
|
||||||
|
err = module.OpenSession(mt, 0, moduleArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, expected) {
|
||||||
|
t.Fatalf("error #unexpected %#v vs %#v", expected, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_DebugModuleTransaction_ActionStatus(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if !pam.CheckPamHasStartConfdir() {
|
||||||
|
t.Skip("this requires PAM with Conf dir support")
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := utils.NewTestSetup(t, utils.WithWorkDir())
|
||||||
|
modulePath := ts.GenerateModule(".", "pam_godebug.so")
|
||||||
|
|
||||||
|
for ret, expected := range moduleArgsRetTypes {
|
||||||
|
ret := ret
|
||||||
|
expected := expected
|
||||||
|
for actionName, action := range utils.Actions {
|
||||||
|
ret := ret
|
||||||
|
expected := expected
|
||||||
|
actionName := actionName
|
||||||
|
action := action
|
||||||
|
t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
serviceName := ret + "-" + actionName
|
||||||
|
moduleArgs := make([]string, 0)
|
||||||
|
for _, a := range debugModuleArgs {
|
||||||
|
moduleArgs = append(moduleArgs, fmt.Sprintf("%s=%s", a, ret))
|
||||||
|
}
|
||||||
|
control := utils.Requisite
|
||||||
|
fallbackModule := utils.Permit
|
||||||
|
if ret == "success" {
|
||||||
|
fallbackModule = utils.Deny
|
||||||
|
control = utils.Sufficient
|
||||||
|
}
|
||||||
|
ts.CreateService(serviceName, []utils.ServiceLine{
|
||||||
|
{Action: action, Control: control, Module: modulePath, Args: moduleArgs},
|
||||||
|
{Action: action, Control: control, Module: fallbackModule.String(), Args: []string{}},
|
||||||
|
})
|
||||||
|
|
||||||
|
tx, err := pam.StartConfDir(serviceName, "user", nil, ts.WorkDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("start #error: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := tx.End()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("end #error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case utils.Account:
|
||||||
|
err = tx.AcctMgmt(pam.Silent)
|
||||||
|
case utils.Auth:
|
||||||
|
err = tx.Authenticate(pam.Silent)
|
||||||
|
case utils.Password:
|
||||||
|
err = tx.ChangeAuthTok(pam.Silent)
|
||||||
|
case utils.Session:
|
||||||
|
err = tx.OpenSession(pam.Silent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(expected, pam.ErrIgnore) {
|
||||||
|
// Ignore can't be returned
|
||||||
|
expected = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, expected) {
|
||||||
|
t.Fatalf("error #unexpected %#v vs %#v", expected, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
cmd/pam-moduler/tests/debug-module/pam_module.go
Normal file
96
cmd/pam-moduler/tests/debug-module/pam_module.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Code generated by "pam-moduler -libname pam_godebug.so"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:generate go build "-ldflags=-extldflags -Wl,-soname,pam_godebug.so" -buildmode=c-shared -o pam_godebug.so -tags go_pam_module
|
||||||
|
|
||||||
|
// Package main is the package for the PAM module library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -lpam -fPIC
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
|
||||||
|
typedef const char _const_char_t;
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do a typecheck at compile time
|
||||||
|
var _ pam.ModuleHandler = pamModuleHandler
|
||||||
|
|
||||||
|
// sliceFromArgv returns a slice of strings given to the PAM module.
|
||||||
|
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
|
||||||
|
r := make([]string, 0, argc)
|
||||||
|
for _, s := range unsafe.Slice(argv, argc) {
|
||||||
|
r = append(r, C.GoString(s))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePamCall is the function that translates C pam requests to Go.
|
||||||
|
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
|
||||||
|
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
|
||||||
|
if pamModuleHandler == nil {
|
||||||
|
return C.int(pam.ErrNoModuleData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if moduleFunc == nil {
|
||||||
|
return C.int(pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := pam.NewModuleTransactionInvoker(pam.NativeHandle(pamh))
|
||||||
|
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
|
||||||
|
sliceFromArgv(argc, argv))
|
||||||
|
if err == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pam.Flags(flags)&pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
|
||||||
|
fmt.Fprintf(os.Stderr, "module returned error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pamErr pam.Error
|
||||||
|
if errors.As(err, &pamErr) {
|
||||||
|
return C.int(pamErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.int(pam.ErrSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_authenticate
|
||||||
|
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.Authenticate)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_setcred
|
||||||
|
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.SetCred)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_acct_mgmt
|
||||||
|
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.AcctMgmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_open_session
|
||||||
|
func pam_sm_open_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.OpenSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_close_session
|
||||||
|
func pam_sm_close_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.CloseSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_chauthtok
|
||||||
|
func pam_sm_chauthtok(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.ChangeAuthTok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {}
|
||||||
38
cmd/pam-moduler/tests/internal/utils/base-module.go
Normal file
38
cmd/pam-moduler/tests/internal/utils/base-module.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "github.com/msteinert/pam/v2"
|
||||||
|
|
||||||
|
// BaseModule is the type for a base PAM module.
|
||||||
|
type BaseModule struct{}
|
||||||
|
|
||||||
|
// AcctMgmt is the handler function for PAM AcctMgmt.
|
||||||
|
func (h *BaseModule) AcctMgmt(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate is the handler function for PAM Authenticate.
|
||||||
|
func (h *BaseModule) Authenticate(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeAuthTok is the handler function for PAM ChangeAuthTok.
|
||||||
|
func (h *BaseModule) ChangeAuthTok(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSession is the handler function for PAM OpenSession.
|
||||||
|
func (h *BaseModule) OpenSession(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseSession is the handler function for PAM CloseSession.
|
||||||
|
func (h *BaseModule) CloseSession(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCred is the handler function for PAM SetCred.
|
||||||
|
func (h *BaseModule) SetCred(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ pam.ModuleHandler = &BaseModule{}
|
||||||
35
cmd/pam-moduler/tests/internal/utils/base-module_test.go
Normal file
35
cmd/pam-moduler/tests/internal/utils/base-module_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(t *testing.T) {
|
||||||
|
bm := BaseModule{}
|
||||||
|
|
||||||
|
if bm.AcctMgmt(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.Authenticate(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.ChangeAuthTok(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.OpenSession(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.CloseSession(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.SetCred(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
}
|
||||||
135
cmd/pam-moduler/tests/internal/utils/test-setup.go
Normal file
135
cmd/pam-moduler/tests/internal/utils/test-setup.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Package utils contains the internal test utils
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSetup is an utility type for having a playground for test PAM modules.
|
||||||
|
type TestSetup struct {
|
||||||
|
t *testing.T
|
||||||
|
workDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
type withWorkDir struct{}
|
||||||
|
|
||||||
|
//nolint:revive
|
||||||
|
func WithWorkDir() withWorkDir {
|
||||||
|
return withWorkDir{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestSetup creates a new TestSetup.
|
||||||
|
func NewTestSetup(t *testing.T, args ...interface{}) *TestSetup {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ts := &TestSetup{t: t}
|
||||||
|
for _, arg := range args {
|
||||||
|
switch argType := arg.(type) {
|
||||||
|
case withWorkDir:
|
||||||
|
ts.ensureWorkDir()
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unknown parameter of type %v", argType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTemporaryDir creates a temporary directory with provided basename.
|
||||||
|
func (ts *TestSetup) CreateTemporaryDir(basename string) string {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), basename)
|
||||||
|
if err != nil {
|
||||||
|
ts.t.Fatalf("can't create service path %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.t.Cleanup(func() { os.RemoveAll(tmpDir) })
|
||||||
|
return tmpDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TestSetup) ensureWorkDir() string {
|
||||||
|
if ts.workDir != "" {
|
||||||
|
return ts.workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.workDir = ts.CreateTemporaryDir("go-pam-*")
|
||||||
|
return ts.workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkDir returns the test setup work directory.
|
||||||
|
func (ts TestSetup) WorkDir() string {
|
||||||
|
return ts.workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateModule generates a PAM module for the provided path and name.
|
||||||
|
func (ts *TestSetup) GenerateModule(testModulePath string, moduleName string) string {
|
||||||
|
cmd := exec.Command("go", "generate", "-C", testModulePath)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
ts.t.Fatalf("can't build pam module %v: %s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
builtFile := filepath.Join(cmd.Dir, testModulePath, moduleName)
|
||||||
|
modulePath := filepath.Join(ts.ensureWorkDir(), filepath.Base(builtFile))
|
||||||
|
if err = os.Rename(builtFile, modulePath); err != nil {
|
||||||
|
ts.t.Fatalf("can't move module: %v", err)
|
||||||
|
os.Remove(builtFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return modulePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts TestSetup) currentFile(skip int) string {
|
||||||
|
_, currentFile, _, ok := runtime.Caller(skip)
|
||||||
|
if !ok {
|
||||||
|
ts.t.Fatalf("can't get current binary path")
|
||||||
|
}
|
||||||
|
return currentFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentFile returns the current file path.
|
||||||
|
func (ts TestSetup) GetCurrentFile() string {
|
||||||
|
// This is a library so we care about the caller location
|
||||||
|
return ts.currentFile(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentFileDir returns the current file directory.
|
||||||
|
func (ts TestSetup) GetCurrentFileDir() string {
|
||||||
|
return filepath.Dir(ts.currentFile(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateModuleDefault generates a default module.
|
||||||
|
func (ts *TestSetup) GenerateModuleDefault(testModulePath string) string {
|
||||||
|
return ts.GenerateModule(testModulePath, "pam_go.so")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateService creates a service file.
|
||||||
|
func (ts *TestSetup) CreateService(serviceName string, services []ServiceLine) string {
|
||||||
|
if !pam.CheckPamHasStartConfdir() {
|
||||||
|
ts.t.Skip("PAM has no support for custom service paths")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName = strings.ToLower(serviceName)
|
||||||
|
serviceFile := filepath.Join(ts.ensureWorkDir(), serviceName)
|
||||||
|
var contents = []string{}
|
||||||
|
|
||||||
|
for _, s := range services {
|
||||||
|
contents = append(contents, strings.TrimRight(strings.Join([]string{
|
||||||
|
s.Action.String(), s.Control.String(), s.Module, strings.Join(s.Args, " "),
|
||||||
|
}, "\t"), "\t"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(serviceFile,
|
||||||
|
[]byte(strings.Join(contents, "\n")), 0600); err != nil {
|
||||||
|
ts.t.Fatalf("can't create service file %v: %v", serviceFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceFile
|
||||||
|
}
|
||||||
180
cmd/pam-moduler/tests/internal/utils/test-setup_test.go
Normal file
180
cmd/pam-moduler/tests/internal/utils/test-setup_test.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isDir(t *testing.T, path string) bool {
|
||||||
|
t.Helper()
|
||||||
|
if file, err := os.Open(path); err == nil {
|
||||||
|
if fileInfo, err := file.Stat(); err == nil {
|
||||||
|
return fileInfo.IsDir()
|
||||||
|
}
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CreateTemporaryDir(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
dir := ts.CreateTemporaryDir("")
|
||||||
|
if !isDir(t, dir) {
|
||||||
|
t.Fatalf("%s not a directory", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = ts.CreateTemporaryDir("foo-prefix-*")
|
||||||
|
if !isDir(t, dir) {
|
||||||
|
t.Fatalf("%s not a directory", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_TestSetupWithWorkDir(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ts := NewTestSetup(t, WithWorkDir())
|
||||||
|
if !isDir(t, ts.WorkDir()) {
|
||||||
|
t.Fatalf("%s not a directory", ts.WorkDir())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CreateService(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
services []ServiceLine
|
||||||
|
expectedContent string
|
||||||
|
}{
|
||||||
|
"empty": {},
|
||||||
|
"CApital-Empty": {},
|
||||||
|
"auth-sufficient-permit": {
|
||||||
|
services: []ServiceLine{
|
||||||
|
{Auth, Sufficient, Permit.String(), []string{}},
|
||||||
|
},
|
||||||
|
expectedContent: "auth sufficient pam_permit.so",
|
||||||
|
},
|
||||||
|
"auth-sufficient-permit-args": {
|
||||||
|
services: []ServiceLine{
|
||||||
|
{Auth, Required, Deny.String(), []string{"a b c [d e]"}},
|
||||||
|
},
|
||||||
|
expectedContent: "auth required pam_deny.so a b c [d e]",
|
||||||
|
},
|
||||||
|
"complete-custom": {
|
||||||
|
services: []ServiceLine{
|
||||||
|
{Account, Required, "pam_account_module.so", []string{"a", "b", "c", "[d e]"}},
|
||||||
|
{Account, Required, Deny.String(), []string{}},
|
||||||
|
{Auth, Requisite, "pam_auth_module.so", []string{}},
|
||||||
|
{Auth, Requisite, Deny.String(), []string{}},
|
||||||
|
{Password, Sufficient, "pam_password_module.so", []string{"arg"}},
|
||||||
|
{Password, Sufficient, Deny.String(), []string{}},
|
||||||
|
{Session, Optional, "pam_session_module.so", []string{""}},
|
||||||
|
{Session, Optional, Deny.String(), []string{}},
|
||||||
|
},
|
||||||
|
expectedContent: `account required pam_account_module.so a b c [d e]
|
||||||
|
account required pam_deny.so
|
||||||
|
auth requisite pam_auth_module.so
|
||||||
|
auth requisite pam_deny.so
|
||||||
|
password sufficient pam_password_module.so arg
|
||||||
|
password sufficient pam_deny.so
|
||||||
|
session optional pam_session_module.so
|
||||||
|
session optional pam_deny.so`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
name := name
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
service := ts.CreateService(name, tc.services)
|
||||||
|
|
||||||
|
if filepath.Base(service) != strings.ToLower(name) {
|
||||||
|
t.Fatalf("Invalid service name %s", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes, err := os.ReadFile(service); err != nil {
|
||||||
|
t.Fatalf("Failed reading %s: %v", service, err)
|
||||||
|
} else {
|
||||||
|
if string(bytes) != tc.expectedContent {
|
||||||
|
t.Fatalf("Unexpected file content:\n%s\n---\n%s",
|
||||||
|
tc.expectedContent, string(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GenerateModule(t *testing.T) {
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
dir := ts.CreateTemporaryDir("")
|
||||||
|
if !isDir(t, dir) {
|
||||||
|
t.Fatalf("%s not a directory", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(filepath.Join(dir, "test-generate.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't create file %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
randomName := ""
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
// #nosec:G404 - it's a test, we don't care.
|
||||||
|
randomName += string(byte('a' + rand.Intn('z'-'a')))
|
||||||
|
}
|
||||||
|
|
||||||
|
wantFile := randomName + ".so"
|
||||||
|
fmt.Fprintf(f, `//go:generate touch %s
|
||||||
|
package generate_file
|
||||||
|
`, wantFile)
|
||||||
|
|
||||||
|
mod, err := os.Create(filepath.Join(dir, "go.mod"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't create file %v", err)
|
||||||
|
}
|
||||||
|
defer mod.Close()
|
||||||
|
|
||||||
|
fmt.Fprintf(mod, `module example.com/greetings
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
`)
|
||||||
|
|
||||||
|
fakeModule := ts.GenerateModule(dir, wantFile)
|
||||||
|
if _, err := os.Stat(fakeModule); err != nil {
|
||||||
|
t.Fatalf("module not generated %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(f, `//go:generate touch pam_go.so
|
||||||
|
package generate_file
|
||||||
|
`, wantFile)
|
||||||
|
|
||||||
|
fakeModule = ts.GenerateModuleDefault(dir)
|
||||||
|
if _, err := os.Stat(fakeModule); err != nil {
|
||||||
|
t.Fatalf("module not generated %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetCurrentFileDir(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
if !strings.HasSuffix(ts.GetCurrentFileDir(), filepath.Join("internal", "utils")) {
|
||||||
|
t.Fatalf("unexpected file %v", ts.GetCurrentFileDir())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetCurrentFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
if !strings.HasSuffix(ts.GetCurrentFile(), filepath.Join("utils", "test-setup_test.go")) {
|
||||||
|
t.Fatalf("unexpected file %v", ts.GetCurrentFile())
|
||||||
|
}
|
||||||
|
}
|
||||||
99
cmd/pam-moduler/tests/internal/utils/test-utils.go
Normal file
99
cmd/pam-moduler/tests/internal/utils/test-utils.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Package utils contains the internal test utils
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// Action represents a PAM action to perform.
|
||||||
|
type Action int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Account is the account.
|
||||||
|
Account Action = iota + 1
|
||||||
|
// Auth is the auth.
|
||||||
|
Auth
|
||||||
|
// Password is the password.
|
||||||
|
Password
|
||||||
|
// Session is the session.
|
||||||
|
Session
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a Action) String() string {
|
||||||
|
switch a {
|
||||||
|
case Account:
|
||||||
|
return "account"
|
||||||
|
case Auth:
|
||||||
|
return "auth"
|
||||||
|
case Password:
|
||||||
|
return "password"
|
||||||
|
case Session:
|
||||||
|
return "session"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions is a map with all the available Actions by their name.
|
||||||
|
var Actions = map[string]Action{
|
||||||
|
Account.String(): Account,
|
||||||
|
Auth.String(): Auth,
|
||||||
|
Password.String(): Password,
|
||||||
|
Session.String(): Session,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control represents how a PAM module should controlled in PAM service file.
|
||||||
|
type Control int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Required implies that the module is required.
|
||||||
|
Required Control = iota + 1
|
||||||
|
// Requisite implies that the module is requisite.
|
||||||
|
Requisite
|
||||||
|
// Sufficient implies that the module is sufficient.
|
||||||
|
Sufficient
|
||||||
|
// Optional implies that the module is optional.
|
||||||
|
Optional
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Control) String() string {
|
||||||
|
switch c {
|
||||||
|
case Required:
|
||||||
|
return "required"
|
||||||
|
case Requisite:
|
||||||
|
return "requisite"
|
||||||
|
case Sufficient:
|
||||||
|
return "sufficient"
|
||||||
|
case Optional:
|
||||||
|
return "optional"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceLine is the representation of a PAM module service file line.
|
||||||
|
type ServiceLine struct {
|
||||||
|
Action Action
|
||||||
|
Control Control
|
||||||
|
Module string
|
||||||
|
Args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FallBackModule is a type to represent the module that should be used as fallback.
|
||||||
|
type FallBackModule int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NoFallback add no fallback module.
|
||||||
|
NoFallback FallBackModule = iota + 1
|
||||||
|
// Permit uses a module that always permits.
|
||||||
|
Permit
|
||||||
|
// Deny uses a module that always denys.
|
||||||
|
Deny
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a FallBackModule) String() string {
|
||||||
|
switch a {
|
||||||
|
case Permit:
|
||||||
|
return "pam_permit.so"
|
||||||
|
case Deny:
|
||||||
|
return "pam_deny.so"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user