In order to properly test the interaction of a module transaction from the application point of view, we need to perform operation in the module and ensure that the expected values are returned and handled In order to do this, without using the PAM apis that we want to test, use a simple trick: - Create an application that works as server using an unix socket - Create a module that connects to it - Pass the socket to the module via the module service file arguments - Add some basic protocol that allows the application to send a request and to the module to reply to that. - Use reflection and serialization to automatically call module methods and return the values to the application where we do the check
306 lines
7.9 KiB
Go
306 lines
7.9 KiB
Go
// pam-moduler is a tool to automate the creation of PAM Modules in go
|
|
//
|
|
// The file is created in the same package and directory as the package that
|
|
// creates the module
|
|
//
|
|
// The module implementation should define a pamModuleHandler object that
|
|
// implements the pam.ModuleHandler type and that will be used for each callback
|
|
//
|
|
// Otherwise it's possible to provide a typename from command line that will
|
|
// be used for this purpose
|
|
//
|
|
// For example:
|
|
//
|
|
// //go:generate go run github.com/msteinert/pam/v2/pam-moduler
|
|
// //go:generate go generate --skip="pam_module"
|
|
// package main
|
|
//
|
|
// import "github.com/msteinert/pam/v2"
|
|
//
|
|
// type ExampleHandler struct{}
|
|
// var pamModuleHandler pam.ModuleHandler = &ExampleHandler{}
|
|
//
|
|
// func (h *ExampleHandler) AcctMgmt(pam.ModuleTransaction, pam.Flags, []string) error {
|
|
// return nil
|
|
// }
|
|
//
|
|
// func (h *ExampleHandler) Authenticate(pam.ModuleTransaction, pam.Flags, []string) error {
|
|
// return nil
|
|
// }
|
|
//
|
|
// func (h *ExampleHandler) ChangeAuthTok(pam.ModuleTransaction, pam.Flags, []string) error {
|
|
// return nil
|
|
// }
|
|
//
|
|
// func (h *ExampleHandler) OpenSession(pam.ModuleTransaction, pam.Flags, []string) error {
|
|
// return nil
|
|
// }
|
|
//
|
|
// func (h *ExampleHandler) CloseSession(pam.ModuleTransaction, pam.Flags, []string) error {
|
|
// return nil
|
|
// }
|
|
//
|
|
// func (h *ExampleHandler) SetCred(pam.ModuleTransaction, pam.Flags, []string) error {
|
|
// return nil
|
|
// }
|
|
|
|
// Package main provides the module shared library.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/format"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const toolName = "pam-moduler"
|
|
|
|
var (
|
|
output = flag.String("output", "", "output file name; default srcdir/pam_module.go")
|
|
libName = flag.String("libname", "", "output library name; default pam_go.so")
|
|
typeName = flag.String("type", "", "type name to be used as pam.ModuleHandler")
|
|
buildTags = flag.String("tags", "", "build tags expression to append to use in the go:build directive")
|
|
skipGenerator = flag.Bool("no-generator", false, "whether to add go:generator directives to the generated source")
|
|
moduleBuildFlags = flag.String("build-flags", "", "comma-separated list of go build flags to use when generating the module")
|
|
moduleBuildTags = flag.String("build-tags", "", "comma-separated list of build tags to use when generating the module")
|
|
noMain = flag.Bool("no-main", false, "whether to add an empty main to generated file")
|
|
)
|
|
|
|
// Usage is a replacement usage function for the flags package.
|
|
func Usage() {
|
|
fmt.Fprintf(os.Stderr, "Usage of %s:\n", toolName)
|
|
fmt.Fprintf(os.Stderr, "\t%s [flags] [-output O] [-libname pam_go] [-type N]\n", toolName)
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
func main() {
|
|
log.SetFlags(0)
|
|
log.SetPrefix(toolName + ": ")
|
|
flag.Usage = Usage
|
|
flag.Parse()
|
|
|
|
if *skipGenerator {
|
|
if *libName != "" {
|
|
fmt.Fprintf(os.Stderr,
|
|
"Generator directives disabled, libname will have no effect\n")
|
|
}
|
|
if *moduleBuildTags != "" {
|
|
fmt.Fprintf(os.Stderr,
|
|
"Generator directives disabled, build-tags will have no effect\n")
|
|
}
|
|
if *moduleBuildFlags != "" {
|
|
fmt.Fprintf(os.Stderr,
|
|
"Generator directives disabled, build-flags will have no effect\n")
|
|
}
|
|
}
|
|
|
|
lib := *libName
|
|
if lib == "" {
|
|
lib = "pam_go"
|
|
} else {
|
|
lib, _ = strings.CutSuffix(lib, ".so")
|
|
lib, _ = strings.CutPrefix(lib, "lib")
|
|
}
|
|
|
|
outputName, _ := strings.CutSuffix(*output, ".go")
|
|
if outputName == "" {
|
|
baseName := "pam_module"
|
|
outputName = filepath.Join(".", strings.ToLower(baseName))
|
|
}
|
|
outputName = outputName + ".go"
|
|
|
|
var tags string
|
|
if *buildTags != "" {
|
|
tags = *buildTags
|
|
}
|
|
|
|
generateTags := []string{"go_pam_module"}
|
|
if len(*moduleBuildTags) > 0 {
|
|
generateTags = append(generateTags, strings.Split(*moduleBuildTags, ",")...)
|
|
}
|
|
|
|
var buildFlags []string
|
|
if *moduleBuildFlags != "" {
|
|
buildFlags = strings.Split(*moduleBuildFlags, ",")
|
|
}
|
|
|
|
g := Generator{
|
|
outputName: outputName,
|
|
libName: lib,
|
|
tags: tags,
|
|
buildFlags: buildFlags,
|
|
generateTags: generateTags,
|
|
noMain: *noMain,
|
|
typeName: *typeName,
|
|
}
|
|
|
|
// Print the header and package clause.
|
|
g.printf("// Code generated by \"%s %s\"; DO NOT EDIT.\n",
|
|
toolName, strings.Join(os.Args[1:], " "))
|
|
g.printf("\n")
|
|
|
|
// Generate the code
|
|
g.generate()
|
|
|
|
// Format the output.
|
|
src := g.format()
|
|
|
|
// Write to file.
|
|
err := os.WriteFile(outputName, src, 0600)
|
|
if err != nil {
|
|
log.Fatalf("writing output: %s", err)
|
|
}
|
|
}
|
|
|
|
// Generator holds the state of the analysis. Primarily used to buffer
|
|
// the output for format.Source.
|
|
type Generator struct {
|
|
buf bytes.Buffer // Accumulated output.
|
|
|
|
libName string
|
|
outputName string
|
|
typeName string
|
|
tags string
|
|
generateTags []string
|
|
buildFlags []string
|
|
noMain bool
|
|
}
|
|
|
|
func (g *Generator) printf(format string, args ...interface{}) {
|
|
fmt.Fprintf(&g.buf, format, args...)
|
|
}
|
|
|
|
// generate produces the String method for the named type.
|
|
func (g *Generator) generate() {
|
|
if g.tags != "" {
|
|
g.printf("//go:build %s\n", g.tags)
|
|
}
|
|
|
|
var buildTagsArg string
|
|
if len(g.generateTags) > 0 {
|
|
buildTagsArg = fmt.Sprintf("-tags %s", strings.Join(g.generateTags, ","))
|
|
}
|
|
|
|
// We use a slice since we want to keep order, for reproducible builds.
|
|
vFuncs := []struct {
|
|
cName string
|
|
goName string
|
|
}{
|
|
{"authenticate", "Authenticate"},
|
|
{"setcred", "SetCred"},
|
|
{"acct_mgmt", "AcctMgmt"},
|
|
{"open_session", "OpenSession"},
|
|
{"close_session", "CloseSession"},
|
|
{"chauthtok", "ChangeAuthTok"},
|
|
}
|
|
|
|
g.printf(`//go:generate go build "-ldflags=-extldflags -Wl,-soname,%[2]s.so" `+
|
|
`-buildmode=c-shared -o %[2]s.so %[3]s %[4]s
|
|
`,
|
|
g.outputName, g.libName, buildTagsArg, strings.Join(g.buildFlags, " "))
|
|
|
|
g.printf(`
|
|
// 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"
|
|
"os"
|
|
"unsafe"
|
|
"github.com/msteinert/pam/v2"
|
|
)
|
|
`)
|
|
|
|
if g.typeName != "" {
|
|
g.printf(`
|
|
var pamModuleHandler pam.ModuleHandler = &%[1]s{}
|
|
`, g.typeName)
|
|
} else {
|
|
g.printf(`
|
|
// Do a typecheck at compile time
|
|
var _ pam.ModuleHandler = pamModuleHandler;
|
|
`)
|
|
}
|
|
|
|
g.printf(`
|
|
// 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)
|
|
}
|
|
`)
|
|
|
|
for _, f := range vFuncs {
|
|
g.printf(`
|
|
//export pam_sm_%[1]s
|
|
func pam_sm_%[1]s(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.%[2]s)
|
|
}
|
|
`, f.cName, f.goName)
|
|
}
|
|
|
|
if !g.noMain {
|
|
g.printf("\nfunc main() {}\n")
|
|
}
|
|
}
|
|
|
|
// format returns the gofmt-ed contents of the Generator's buffer.
|
|
func (g *Generator) format() []byte {
|
|
src, err := format.Source(g.buf.Bytes())
|
|
if err != nil {
|
|
// Should never happen, but can arise when developing this code.
|
|
// The user can compile the output to see the error.
|
|
log.Printf("warning: internal error: invalid Go generated: %s", err)
|
|
log.Printf("warning: compile the package to analyze the error")
|
|
return g.buf.Bytes()
|
|
}
|
|
return src
|
|
}
|