Modules have the ability to start PAM conversations, so while the transaction code can handle them we did not have a way to init them. Yet. So add some APIs allowing this, making it easier from the go side to handle the conversations. In this commit we only support text-based conversations, but code is designed with the idea of supporting binary cases too. Added the integration tests using the module that is now able to both start conversation and handle them using Go only.
416 lines
12 KiB
Go
416 lines
12 KiB
Go
// Package pam provides a wrapper for the PAM application API.
|
|
package pam
|
|
|
|
/*
|
|
#include "transaction.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"runtime/cgo"
|
|
"unsafe"
|
|
)
|
|
|
|
const maxNumMsg = C.PAM_MAX_NUM_MSG
|
|
|
|
// ModuleTransaction is an interface that a pam module transaction
|
|
// should implement.
|
|
type ModuleTransaction interface {
|
|
SetItem(Item, string) error
|
|
GetItem(Item) (string, error)
|
|
PutEnv(nameVal string) error
|
|
GetEnv(name string) string
|
|
GetEnvList() (map[string]string, error)
|
|
GetUser(prompt string) (string, error)
|
|
SetData(key string, data any) error
|
|
GetData(key string) (any, error)
|
|
StartStringConv(style Style, prompt string) (StringConvResponse, error)
|
|
StartStringConvf(style Style, format string, args ...interface{}) (
|
|
StringConvResponse, error)
|
|
StartConv(ConvRequest) (ConvResponse, error)
|
|
StartConvMulti([]ConvRequest) ([]ConvResponse, error)
|
|
}
|
|
|
|
// ModuleHandlerFunc is a function type used by the ModuleHandler.
|
|
type ModuleHandlerFunc func(ModuleTransaction, Flags, []string) error
|
|
|
|
// ModuleTransaction is the module-side handle for a PAM transaction.
|
|
type moduleTransaction struct {
|
|
transactionBase
|
|
}
|
|
|
|
// ModuleHandler is an interface for objects that can be used to create
|
|
// PAM modules from go.
|
|
type ModuleHandler interface {
|
|
AcctMgmt(ModuleTransaction, Flags, []string) error
|
|
Authenticate(ModuleTransaction, Flags, []string) error
|
|
ChangeAuthTok(ModuleTransaction, Flags, []string) error
|
|
CloseSession(ModuleTransaction, Flags, []string) error
|
|
OpenSession(ModuleTransaction, Flags, []string) error
|
|
SetCred(ModuleTransaction, Flags, []string) error
|
|
}
|
|
|
|
// ModuleTransactionInvoker is an interface that a pam module transaction
|
|
// should implement to redirect requests from C handlers to go,
|
|
type ModuleTransactionInvoker interface {
|
|
ModuleTransaction
|
|
InvokeHandler(handler ModuleHandlerFunc, flags Flags, args []string) error
|
|
}
|
|
|
|
// NewModuleTransactionInvoker allows initializing a transaction invoker from
|
|
// the module side.
|
|
func NewModuleTransactionInvoker(handle NativeHandle) ModuleTransactionInvoker {
|
|
return &moduleTransaction{transactionBase{handle: handle}}
|
|
}
|
|
|
|
func (m *moduleTransaction) InvokeHandler(handler ModuleHandlerFunc,
|
|
flags Flags, args []string) error {
|
|
invoker := func() error {
|
|
if handler == nil {
|
|
return ErrIgnore
|
|
}
|
|
err := handler(m, flags, args)
|
|
if err != nil {
|
|
service, _ := m.GetItem(Service)
|
|
|
|
var pamErr Error
|
|
if !errors.As(err, &pamErr) {
|
|
err = ErrSystem
|
|
}
|
|
|
|
if pamErr == ErrIgnore || service == "" {
|
|
return err
|
|
}
|
|
|
|
return fmt.Errorf("%s failed: %w", service, err)
|
|
}
|
|
return nil
|
|
}
|
|
err := invoker()
|
|
if errors.Is(err, Error(0)) {
|
|
err = nil
|
|
}
|
|
var status int32
|
|
if err != nil {
|
|
status = int32(ErrSystem)
|
|
|
|
var pamErr Error
|
|
if errors.As(err, &pamErr) {
|
|
status = int32(pamErr)
|
|
}
|
|
}
|
|
m.lastStatus.Store(status)
|
|
return err
|
|
}
|
|
|
|
type moduleTransactionIface interface {
|
|
getUser(outUser **C.char, prompt *C.char) C.int
|
|
setData(key *C.char, handle C.uintptr_t) C.int
|
|
getData(key *C.char, outHandle *C.uintptr_t) C.int
|
|
getConv() (*C.struct_pam_conv, error)
|
|
startConv(conv *C.struct_pam_conv, nMsg C.int,
|
|
messages **C.struct_pam_message,
|
|
outResponses **C.struct_pam_response) C.int
|
|
}
|
|
|
|
func (m *moduleTransaction) getUser(outUser **C.char, prompt *C.char) C.int {
|
|
return C.pam_get_user(m.handle, outUser, prompt)
|
|
}
|
|
|
|
// getUserImpl is the default implementation for GetUser, but kept as private so
|
|
// that can be used to test the pam package
|
|
func (m *moduleTransaction) getUserImpl(iface moduleTransactionIface,
|
|
prompt string) (string, error) {
|
|
var user *C.char
|
|
var cPrompt = C.CString(prompt)
|
|
defer C.free(unsafe.Pointer(cPrompt))
|
|
err := m.handlePamStatus(iface.getUser(&user, cPrompt))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return C.GoString(user), nil
|
|
}
|
|
|
|
// GetUser is similar to GetItem(User), but it would start a conversation if
|
|
// no user is currently set in PAM.
|
|
func (m *moduleTransaction) GetUser(prompt string) (string, error) {
|
|
return m.getUserImpl(m, prompt)
|
|
}
|
|
|
|
// SetData allows to save any value in the module data that is preserved
|
|
// during the whole time the module is loaded.
|
|
func (m *moduleTransaction) SetData(key string, data any) error {
|
|
return m.setDataImpl(m, key, data)
|
|
}
|
|
|
|
func (m *moduleTransaction) setData(key *C.char, handle C.uintptr_t) C.int {
|
|
return C.set_data(m.handle, key, handle)
|
|
}
|
|
|
|
// setDataImpl is the implementation for SetData for testing purposes.
|
|
func (m *moduleTransaction) setDataImpl(iface moduleTransactionIface,
|
|
key string, data any) error {
|
|
var cKey = C.CString(key)
|
|
defer C.free(unsafe.Pointer(cKey))
|
|
var handle cgo.Handle
|
|
if data != nil {
|
|
handle = cgo.NewHandle(data)
|
|
}
|
|
return m.handlePamStatus(iface.setData(cKey, C.uintptr_t(handle)))
|
|
}
|
|
|
|
//export _go_pam_data_cleanup
|
|
func _go_pam_data_cleanup(h NativeHandle, handle C.uintptr_t, status C.int) {
|
|
cgo.Handle(handle).Delete()
|
|
}
|
|
|
|
// GetData allows to get any value from the module data saved using SetData
|
|
// that is preserved across the whole time the module is loaded.
|
|
func (m *moduleTransaction) GetData(key string) (any, error) {
|
|
return m.getDataImpl(m, key)
|
|
}
|
|
|
|
func (m *moduleTransaction) getData(key *C.char, outHandle *C.uintptr_t) C.int {
|
|
return C.get_data(m.handle, key, outHandle)
|
|
}
|
|
|
|
// getDataImpl is the implementation for GetData for testing purposes.
|
|
func (m *moduleTransaction) getDataImpl(iface moduleTransactionIface,
|
|
key string) (any, error) {
|
|
var cKey = C.CString(key)
|
|
defer C.free(unsafe.Pointer(cKey))
|
|
var handle C.uintptr_t
|
|
if err := m.handlePamStatus(iface.getData(cKey, &handle)); err != nil {
|
|
return nil, err
|
|
}
|
|
if goHandle := cgo.Handle(handle); goHandle != cgo.Handle(0) {
|
|
return goHandle.Value(), nil
|
|
}
|
|
|
|
return nil, m.handlePamStatus(C.int(ErrNoModuleData))
|
|
}
|
|
|
|
// getConv is a private function to get the conversation pointer to be used
|
|
// with C.do_conv() to initiate conversations.
|
|
func (m *moduleTransaction) getConv() (*C.struct_pam_conv, error) {
|
|
var convPtr unsafe.Pointer
|
|
|
|
if err := m.handlePamStatus(
|
|
C.pam_get_item(m.handle, C.PAM_CONV, &convPtr)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return (*C.struct_pam_conv)(convPtr), nil
|
|
}
|
|
|
|
// ConvRequest is an interface that all the Conversation requests should
|
|
// implement.
|
|
type ConvRequest interface {
|
|
Style() Style
|
|
}
|
|
|
|
// ConvResponse is an interface that all the Conversation responses should
|
|
// implement.
|
|
type ConvResponse interface {
|
|
Style() Style
|
|
}
|
|
|
|
// StringConvRequest is a ConvRequest for performing text-based conversations.
|
|
type StringConvRequest struct {
|
|
style Style
|
|
prompt string
|
|
}
|
|
|
|
// NewStringConvRequest creates a new StringConvRequest.
|
|
func NewStringConvRequest(style Style, prompt string) StringConvRequest {
|
|
return StringConvRequest{style, prompt}
|
|
}
|
|
|
|
// Style returns the conversation style of the StringConvRequest.
|
|
func (s StringConvRequest) Style() Style {
|
|
return s.style
|
|
}
|
|
|
|
// Prompt returns the conversation style of the StringConvRequest.
|
|
func (s StringConvRequest) Prompt() string {
|
|
return s.prompt
|
|
}
|
|
|
|
// StringConvResponse is an interface that string Conversation responses implements.
|
|
type StringConvResponse interface {
|
|
ConvResponse
|
|
Response() string
|
|
}
|
|
|
|
// stringConvResponse is a StringConvResponse implementation used for text-based
|
|
// conversation responses.
|
|
type stringConvResponse struct {
|
|
style Style
|
|
response string
|
|
}
|
|
|
|
// Style returns the conversation style of the StringConvResponse.
|
|
func (s stringConvResponse) Style() Style {
|
|
return s.style
|
|
}
|
|
|
|
// Response returns the string response of the conversation.
|
|
func (s stringConvResponse) Response() string {
|
|
return s.response
|
|
}
|
|
|
|
// StartStringConv starts a text-based conversation using the provided style
|
|
// and prompt.
|
|
func (m *moduleTransaction) StartStringConv(style Style, prompt string) (
|
|
StringConvResponse, error) {
|
|
return m.startStringConvImpl(m, style, prompt)
|
|
}
|
|
|
|
func (m *moduleTransaction) startStringConvImpl(iface moduleTransactionIface,
|
|
style Style, prompt string) (
|
|
StringConvResponse, error) {
|
|
switch style {
|
|
case BinaryPrompt:
|
|
return nil, fmt.Errorf("%w: binary style is not supported", ErrConv)
|
|
}
|
|
|
|
res, err := m.startConvImpl(iface, NewStringConvRequest(style, prompt))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stringRes, _ := res.(stringConvResponse)
|
|
return stringRes, nil
|
|
}
|
|
|
|
// StartStringConvf allows to start string conversation with formatting support.
|
|
func (m *moduleTransaction) StartStringConvf(style Style, format string, args ...interface{}) (
|
|
StringConvResponse, error) {
|
|
return m.StartStringConv(style, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// StartConv initiates a PAM conversation using the provided ConvRequest.
|
|
func (m *moduleTransaction) StartConv(req ConvRequest) (
|
|
ConvResponse, error) {
|
|
return m.startConvImpl(m, req)
|
|
}
|
|
|
|
func (m *moduleTransaction) startConvImpl(iface moduleTransactionIface, req ConvRequest) (
|
|
ConvResponse, error) {
|
|
resp, err := m.startConvMultiImpl(iface, []ConvRequest{req})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(resp) != 1 {
|
|
return nil, fmt.Errorf("%w: not enough values returned", ErrConv)
|
|
}
|
|
return resp[0], nil
|
|
}
|
|
|
|
func (m *moduleTransaction) startConv(conv *C.struct_pam_conv, nMsg C.int,
|
|
messages **C.struct_pam_message, outResponses **C.struct_pam_response) C.int {
|
|
return C.start_pam_conv(conv, nMsg, messages, outResponses)
|
|
}
|
|
|
|
// startConvMultiImpl is the implementation for GetData for testing purposes.
|
|
func (m *moduleTransaction) startConvMultiImpl(iface moduleTransactionIface,
|
|
requests []ConvRequest) (responses []ConvResponse, err error) {
|
|
defer func() {
|
|
if err == nil {
|
|
_ = m.handlePamStatus(success)
|
|
return
|
|
}
|
|
var pamErr Error
|
|
if !errors.As(err, &pamErr) {
|
|
err = errors.Join(ErrConv, err)
|
|
pamErr = ErrConv
|
|
}
|
|
_ = m.handlePamStatus(C.int(pamErr))
|
|
}()
|
|
|
|
if len(requests) == 0 {
|
|
return nil, errors.New("no requests defined")
|
|
}
|
|
if len(requests) > maxNumMsg {
|
|
return nil, errors.New("too many requests")
|
|
}
|
|
|
|
conv, err := iface.getConv()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if conv == nil || conv.conv == nil {
|
|
return nil, errors.New("impossible to find conv handler")
|
|
}
|
|
|
|
// FIXME: Just use make([]C.struct_pam_message, 0, len(requests))
|
|
// and append, when it's possible to use runtime.Pinner
|
|
var cMessagePtr *C.struct_pam_message
|
|
cMessages := (**C.struct_pam_message)(C.calloc(C.size_t(len(requests)),
|
|
(C.size_t)(unsafe.Sizeof(cMessagePtr))))
|
|
defer C.free(unsafe.Pointer(cMessages))
|
|
goMsgs := unsafe.Slice(cMessages, len(requests))
|
|
|
|
for i, req := range requests {
|
|
var cBytes unsafe.Pointer
|
|
switch r := req.(type) {
|
|
case StringConvRequest:
|
|
cBytes = unsafe.Pointer(C.CString(r.Prompt()))
|
|
defer C.free(cBytes)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported conversation type %#v", r)
|
|
}
|
|
|
|
goMsgs[i] = &C.struct_pam_message{
|
|
msg_style: C.int(req.Style()),
|
|
msg: (*C.char)(cBytes),
|
|
}
|
|
}
|
|
|
|
var cResponses *C.struct_pam_response
|
|
ret := iface.startConv(conv, C.int(len(requests)), cMessages, &cResponses)
|
|
if ret != success {
|
|
return nil, Error(ret)
|
|
}
|
|
|
|
goResponses := unsafe.Slice(cResponses, len(requests))
|
|
defer func() {
|
|
for _, resp := range goResponses {
|
|
C.free(unsafe.Pointer(resp.resp))
|
|
}
|
|
C.free(unsafe.Pointer(cResponses))
|
|
}()
|
|
|
|
responses = make([]ConvResponse, 0, len(requests))
|
|
for i, resp := range goResponses {
|
|
msgStyle := requests[i].Style()
|
|
switch msgStyle {
|
|
case PromptEchoOff:
|
|
fallthrough
|
|
case PromptEchoOn:
|
|
fallthrough
|
|
case ErrorMsg:
|
|
fallthrough
|
|
case TextInfo:
|
|
responses = append(responses, stringConvResponse{
|
|
style: msgStyle,
|
|
response: C.GoString(resp.resp),
|
|
})
|
|
default:
|
|
return nil,
|
|
fmt.Errorf("unsupported conversation type %v", msgStyle)
|
|
}
|
|
}
|
|
|
|
return responses, nil
|
|
}
|
|
|
|
// StartConvMulti initiates a PAM conversation with multiple ConvRequest's.
|
|
func (m *moduleTransaction) StartConvMulti(requests []ConvRequest) (
|
|
[]ConvResponse, error) {
|
|
return m.startConvMultiImpl(m, requests)
|
|
}
|