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:
Marco Trevisan (Treviño)
2023-09-25 23:13:34 +02:00
parent 6f3af6e9b2
commit 8c30b5946a
9 changed files with 824 additions and 0 deletions

View 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{}

View 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")
}
}

View 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
}

View 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())
}
}

View 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 ""
}
}