314 lines
8.2 KiB
Go
314 lines
8.2 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 git.cloudyne.io/go/msteinert-go-pam/pam-moduler
|
|
// //go:generate go generate --skip="pam_module"
|
|
// package main
|
|
//
|
|
// import "git.cloudyne.io/go/msteinert-go-pam"
|
|
//
|
|
// 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")
|
|
parallelConv = flag.Bool("parallel-conv", false, "whether to support performing PAM conversations in parallel")
|
|
)
|
|
|
|
// 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,
|
|
parallelConv: *parallelConv,
|
|
}
|
|
|
|
// 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
|
|
parallelConv 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, ","))
|
|
}
|
|
|
|
var transactionCreator = "NewModuleTransactionInvoker"
|
|
if g.parallelConv {
|
|
transactionCreator = "NewModuleTransactionInvokerParallelConv"
|
|
}
|
|
|
|
// 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"
|
|
"git.cloudyne.io/go/msteinert-go-pam"
|
|
)
|
|
`)
|
|
|
|
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.%s(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)
|
|
}
|
|
`, transactionCreator)
|
|
|
|
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
|
|
}
|