module-transaction: Add support for initiating PAM Conversations

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.
This commit is contained in:
Marco Trevisan (Treviño)
2023-10-04 23:34:20 +02:00
parent 7a073f5ba0
commit 883dc86533
8 changed files with 790 additions and 14 deletions

View File

@@ -13,6 +13,8 @@ import (
"unsafe"
)
const maxNumMsg = C.PAM_MAX_NUM_MSG
// ModuleTransaction is an interface that a pam module transaction
// should implement.
type ModuleTransaction interface {
@@ -24,6 +26,11 @@ type ModuleTransaction interface {
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.
@@ -102,6 +109,10 @@ 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 {
@@ -180,3 +191,225 @@ func (m *moduleTransaction) getDataImpl(iface moduleTransactionIface,
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)
}