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:
@@ -56,17 +56,26 @@ func (m *integrationTesterModule) handleRequest(authReq *authRequest, r *Request
|
||||
|
||||
var args []reflect.Value
|
||||
for i, arg := range r.ActionArgs {
|
||||
switch v := arg.(type) {
|
||||
case SerializableStringConvRequest:
|
||||
args = append(args, reflect.ValueOf(
|
||||
pam.NewStringConvRequest(v.Style, v.Request)))
|
||||
default:
|
||||
if arg == nil {
|
||||
args = append(args, reflect.Zero(method.Type().In(i)))
|
||||
} else {
|
||||
args = append(args, reflect.ValueOf(arg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = &Result{Action: "return"}
|
||||
for _, ret := range method.Call(args) {
|
||||
iface := ret.Interface()
|
||||
switch value := iface.(type) {
|
||||
case pam.StringConvResponse:
|
||||
res.ActionArgs = append(res.ActionArgs,
|
||||
SerializableStringConvResponse{value.Style(), value.Response()})
|
||||
case pam.Error:
|
||||
authReq.lastError = value
|
||||
res.ActionArgs = append(res.ActionArgs, value)
|
||||
|
||||
@@ -630,6 +630,194 @@ func Test_Moduler_IntegrationTesterModule(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
"start-conv-no-conv-set": {
|
||||
expectedError: pam.ErrConv,
|
||||
checkedRequests: []checkedRequest{
|
||||
{
|
||||
r: NewRequest("StartConv", SerializableStringConvRequest{
|
||||
pam.TextInfo,
|
||||
"hello PAM!",
|
||||
}),
|
||||
exp: []interface{}{nil, pam.ErrConv},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConv", pam.TextInfo, "hello PAM!"),
|
||||
exp: []interface{}{nil, pam.ErrConv},
|
||||
},
|
||||
},
|
||||
},
|
||||
"start-conv-prompt-text-info": {
|
||||
credentials: utils.Credentials{
|
||||
ExpectedMessage: "hello PAM!",
|
||||
ExpectedStyle: pam.TextInfo,
|
||||
TextInfo: "nice to see you, Go!",
|
||||
},
|
||||
checkedRequests: []checkedRequest{
|
||||
{
|
||||
r: NewRequest("StartConv", SerializableStringConvRequest{
|
||||
pam.TextInfo,
|
||||
"hello PAM!",
|
||||
}),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.TextInfo,
|
||||
"nice to see you, Go!",
|
||||
}, nil},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConv", pam.TextInfo, "hello PAM!"),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.TextInfo,
|
||||
"nice to see you, Go!",
|
||||
}, nil},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConvf", pam.TextInfo, "hello %s!", "PAM"),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.TextInfo,
|
||||
"nice to see you, Go!",
|
||||
}, nil},
|
||||
},
|
||||
},
|
||||
},
|
||||
"start-conv-prompt-error-msg": {
|
||||
credentials: utils.Credentials{
|
||||
ExpectedMessage: "This is wrong, PAM!",
|
||||
ExpectedStyle: pam.ErrorMsg,
|
||||
ErrorMsg: "ops, sorry...",
|
||||
},
|
||||
checkedRequests: []checkedRequest{
|
||||
{
|
||||
r: NewRequest("StartConv", SerializableStringConvRequest{
|
||||
pam.ErrorMsg,
|
||||
"This is wrong, PAM!",
|
||||
}),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.ErrorMsg,
|
||||
"ops, sorry...",
|
||||
}, nil},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConv", pam.ErrorMsg,
|
||||
"This is wrong, PAM!",
|
||||
),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.ErrorMsg,
|
||||
"ops, sorry...",
|
||||
}, nil},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConvf", pam.ErrorMsg,
|
||||
"This is wrong, %s!", "PAM",
|
||||
),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.ErrorMsg,
|
||||
"ops, sorry...",
|
||||
}, nil},
|
||||
},
|
||||
},
|
||||
},
|
||||
"start-conv-prompt-echo-on": {
|
||||
credentials: utils.Credentials{
|
||||
ExpectedMessage: "Give me your non-private infos",
|
||||
ExpectedStyle: pam.PromptEchoOn,
|
||||
EchoOn: "here's my public data",
|
||||
},
|
||||
checkedRequests: []checkedRequest{
|
||||
{
|
||||
r: NewRequest("StartConv", SerializableStringConvRequest{
|
||||
pam.PromptEchoOn,
|
||||
"Give me your non-private infos",
|
||||
}),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.PromptEchoOn,
|
||||
"here's my public data",
|
||||
}, nil},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConv", pam.PromptEchoOn,
|
||||
"Give me your non-private infos",
|
||||
),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.PromptEchoOn,
|
||||
"here's my public data",
|
||||
}, nil},
|
||||
},
|
||||
},
|
||||
},
|
||||
"start-conv-prompt-echo-off": {
|
||||
credentials: utils.Credentials{
|
||||
ExpectedMessage: "Give me your super-secret data",
|
||||
ExpectedStyle: pam.PromptEchoOff,
|
||||
EchoOff: "here's my private token",
|
||||
},
|
||||
checkedRequests: []checkedRequest{
|
||||
{
|
||||
r: NewRequest("StartConv", SerializableStringConvRequest{
|
||||
pam.PromptEchoOff,
|
||||
"Give me your super-secret data",
|
||||
}),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.PromptEchoOff,
|
||||
"here's my private token",
|
||||
}, nil},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConv", pam.PromptEchoOff,
|
||||
"Give me your super-secret data",
|
||||
),
|
||||
exp: []interface{}{SerializableStringConvResponse{
|
||||
pam.PromptEchoOff,
|
||||
"here's my private token",
|
||||
}, nil},
|
||||
},
|
||||
},
|
||||
},
|
||||
"start-conv-text-info-handle-failure-message-mismatch": {
|
||||
expectedError: pam.ErrConv,
|
||||
credentials: utils.Credentials{
|
||||
ExpectedMessage: "This is an info message",
|
||||
ExpectedStyle: pam.TextInfo,
|
||||
TextInfo: "And this is what is returned",
|
||||
},
|
||||
checkedRequests: []checkedRequest{
|
||||
{
|
||||
r: NewRequest("StartConv", SerializableStringConvRequest{
|
||||
pam.TextInfo,
|
||||
"This should have been an info message, but is not",
|
||||
}),
|
||||
exp: []interface{}{nil, pam.ErrConv},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConv", pam.TextInfo,
|
||||
"This should have been an info message, but is not",
|
||||
),
|
||||
exp: []interface{}{nil, pam.ErrConv},
|
||||
},
|
||||
},
|
||||
},
|
||||
"start-conv-text-info-handle-failure-style-mismatch": {
|
||||
expectedError: pam.ErrConv,
|
||||
credentials: utils.Credentials{
|
||||
ExpectedMessage: "This is an info message",
|
||||
ExpectedStyle: pam.PromptEchoOff,
|
||||
TextInfo: "And this is what is returned",
|
||||
},
|
||||
checkedRequests: []checkedRequest{
|
||||
{
|
||||
r: NewRequest("StartConv", SerializableStringConvRequest{
|
||||
pam.TextInfo,
|
||||
"This is an info message",
|
||||
}),
|
||||
exp: []interface{}{nil, pam.ErrConv},
|
||||
},
|
||||
{
|
||||
r: NewRequest("StartStringConv", pam.TextInfo,
|
||||
"This is an info message",
|
||||
),
|
||||
exp: []interface{}{nil, pam.ErrConv},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
@@ -880,6 +1068,23 @@ func Test_Moduler_IntegrationTesterModule_Authenticate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
"StartConv": {
|
||||
expectedError: pam.ErrSystem,
|
||||
checkedRequests: []checkedRequest{{
|
||||
r: NewRequest("StartConv", SerializableStringConvRequest{
|
||||
pam.TextInfo,
|
||||
"hello PAM!",
|
||||
}),
|
||||
exp: []interface{}{nil, pam.ErrSystem},
|
||||
}},
|
||||
},
|
||||
"StartStringConv": {
|
||||
expectedError: pam.ErrSystem,
|
||||
checkedRequests: []checkedRequest{{
|
||||
r: NewRequest("StartStringConv", pam.TextInfo, "hello PAM!"),
|
||||
exp: []interface{}{nil, pam.ErrSystem},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
|
||||
@@ -24,12 +24,30 @@ func (e *SerializablePamError) Error() string {
|
||||
return e.RetStatus.Error()
|
||||
}
|
||||
|
||||
// SerializableStringConvRequest is a serializable string request.
|
||||
type SerializableStringConvRequest struct {
|
||||
Style pam.Style
|
||||
Request string
|
||||
}
|
||||
|
||||
// SerializableStringConvResponse is a serializable string response.
|
||||
type SerializableStringConvResponse struct {
|
||||
Style pam.Style
|
||||
Response string
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(map[string]string{})
|
||||
gob.Register(Request{})
|
||||
gob.Register(pam.Item(0))
|
||||
gob.Register(pam.Error(0))
|
||||
gob.Register(pam.Style(0))
|
||||
gob.Register([]pam.ConvResponse{})
|
||||
gob.RegisterName("main.SerializablePamError",
|
||||
SerializablePamError{})
|
||||
gob.RegisterName("main.SerializableStringConvRequest",
|
||||
SerializableStringConvRequest{})
|
||||
gob.RegisterName("main.SerializableStringConvResponse",
|
||||
SerializableStringConvResponse{})
|
||||
gob.Register(utils.SerializableError{})
|
||||
}
|
||||
|
||||
@@ -118,6 +118,10 @@ func (e *SerializableError) Error() string {
|
||||
type Credentials struct {
|
||||
User string
|
||||
Password string
|
||||
EchoOn string
|
||||
EchoOff string
|
||||
TextInfo string
|
||||
ErrorMsg string
|
||||
ExpectedMessage string
|
||||
CheckEmptyMessage bool
|
||||
ExpectedStyle pam.Style
|
||||
@@ -145,10 +149,20 @@ func (c Credentials) RespondPAM(s pam.Style, msg string) (string, error) {
|
||||
|
||||
switch s {
|
||||
case pam.PromptEchoOn:
|
||||
if c.User != "" {
|
||||
return c.User, nil
|
||||
}
|
||||
return c.EchoOn, nil
|
||||
case pam.PromptEchoOff:
|
||||
if c.Password != "" {
|
||||
return c.Password, nil
|
||||
}
|
||||
return c.EchoOff, nil
|
||||
case pam.TextInfo:
|
||||
return c.TextInfo, nil
|
||||
case pam.ErrorMsg:
|
||||
return c.ErrorMsg, nil
|
||||
}
|
||||
|
||||
return "", errors.Join(pam.ErrConv,
|
||||
&SerializableError{fmt.Sprintf("unhandled style: %v", s)})
|
||||
|
||||
@@ -7,6 +7,8 @@ package pam
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <security/pam_modules.h>
|
||||
|
||||
void init_pam_conv(struct pam_conv *conv, uintptr_t appdata);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -110,17 +113,41 @@ func (m *mockModuleTransaction) setData(key *C.char, handle C.uintptr_t) C.int {
|
||||
return C.int(m.RetData.Status)
|
||||
}
|
||||
|
||||
func (m *mockModuleTransaction) getConv() (*C.struct_pam_conv, error) {
|
||||
if m.ConversationHandler != nil {
|
||||
conv := C.struct_pam_conv{}
|
||||
handler := cgo.NewHandle(m.ConversationHandler)
|
||||
C.init_pam_conv(&conv, C.uintptr_t(handler))
|
||||
return &conv, nil
|
||||
}
|
||||
if C.int(m.RetData.Status) != success {
|
||||
return nil, m.RetData.Status
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type mockConversationHandler struct {
|
||||
User string
|
||||
PromptEchoOn string
|
||||
PromptEchoOff string
|
||||
TextInfo string
|
||||
ErrorMsg string
|
||||
ExpectedMessage string
|
||||
ExpectedMessagesByStyle map[Style]string
|
||||
CheckEmptyMessage bool
|
||||
ExpectedStyle Style
|
||||
CheckZeroStyle bool
|
||||
IgnoreUnknownStyle bool
|
||||
}
|
||||
|
||||
func (c mockConversationHandler) RespondPAM(s Style, msg string) (string, error) {
|
||||
if (c.ExpectedMessage != "" || c.CheckEmptyMessage) &&
|
||||
msg != c.ExpectedMessage {
|
||||
var expectedMsg = c.ExpectedMessage
|
||||
if msg, ok := c.ExpectedMessagesByStyle[s]; ok {
|
||||
expectedMsg = msg
|
||||
}
|
||||
|
||||
if (expectedMsg != "" || c.CheckEmptyMessage) &&
|
||||
msg != expectedMsg {
|
||||
return "", fmt.Errorf("%w: unexpected prompt: %s vs %s",
|
||||
ErrConv, msg, c.ExpectedMessage)
|
||||
}
|
||||
@@ -133,8 +160,21 @@ func (c mockConversationHandler) RespondPAM(s Style, msg string) (string, error)
|
||||
|
||||
switch s {
|
||||
case PromptEchoOn:
|
||||
if c.User != "" {
|
||||
return c.User, nil
|
||||
}
|
||||
return c.PromptEchoOn, nil
|
||||
case PromptEchoOff:
|
||||
return c.PromptEchoOff, nil
|
||||
case TextInfo:
|
||||
return c.TextInfo, nil
|
||||
case ErrorMsg:
|
||||
return c.ErrorMsg, nil
|
||||
}
|
||||
|
||||
if c.IgnoreUnknownStyle {
|
||||
return c.ExpectedMessage, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: unhandled style: %v", ErrConv, s)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type customConvRequest int
|
||||
|
||||
func (r customConvRequest) Style() Style {
|
||||
return Style(r)
|
||||
}
|
||||
|
||||
func ensureNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
@@ -93,6 +99,33 @@ func Test_NewNullModuleTransaction(t *testing.T) {
|
||||
return nil, mt.SetData("foo", nil)
|
||||
},
|
||||
},
|
||||
"StartConv-StringConv": {
|
||||
testFunc: func(t *testing.T) (any, error) {
|
||||
t.Helper()
|
||||
return mt.StartConv(NewStringConvRequest(TextInfo, "a prompt"))
|
||||
},
|
||||
},
|
||||
"StartStringConv": {
|
||||
testFunc: func(t *testing.T) (any, error) {
|
||||
t.Helper()
|
||||
return mt.StartStringConv(TextInfo, "a prompt")
|
||||
},
|
||||
},
|
||||
"StartStringConvf": {
|
||||
testFunc: func(t *testing.T) (any, error) {
|
||||
t.Helper()
|
||||
return mt.StartStringConvf(TextInfo, "a prompt %s", "with info")
|
||||
},
|
||||
},
|
||||
"StartConvMulti": {
|
||||
testFunc: func(t *testing.T) (any, error) {
|
||||
t.Helper()
|
||||
return mt.StartConvMulti([]ConvRequest{
|
||||
NewStringConvRequest(TextInfo, "a prompt"),
|
||||
NewStringConvRequest(ErrorMsg, "another prompt"),
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
@@ -395,6 +428,225 @@ func Test_MockModuleTransaction(t *testing.T) {
|
||||
return mt.getDataImpl(mock, "replaced-data")
|
||||
},
|
||||
},
|
||||
"StartConv-no-conv-set": {
|
||||
expectedError: ErrConv,
|
||||
expectedValue: nil,
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvImpl(mock, StringConvRequest{
|
||||
TextInfo,
|
||||
"hello PAM!",
|
||||
})
|
||||
},
|
||||
},
|
||||
"StartConv-text-info": {
|
||||
expectedValue: stringConvResponse{TextInfo, "nice to see you, Go!"},
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: TextInfo,
|
||||
ExpectedMessage: "hello PAM!",
|
||||
TextInfo: "nice to see you, Go!",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvImpl(mock, StringConvRequest{
|
||||
TextInfo,
|
||||
"hello PAM!",
|
||||
})
|
||||
},
|
||||
},
|
||||
"StartConv-error-msg": {
|
||||
expectedValue: stringConvResponse{ErrorMsg, "ops, sorry..."},
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: ErrorMsg,
|
||||
ExpectedMessage: "This is wrong, PAM!",
|
||||
ErrorMsg: "ops, sorry...",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvImpl(mock, StringConvRequest{
|
||||
ErrorMsg,
|
||||
"This is wrong, PAM!",
|
||||
})
|
||||
},
|
||||
},
|
||||
"StartConv-prompt-echo-on": {
|
||||
expectedValue: stringConvResponse{PromptEchoOn, "here's my public data"},
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: PromptEchoOn,
|
||||
ExpectedMessage: "Give me your non-private infos",
|
||||
PromptEchoOn: "here's my public data",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvImpl(mock, StringConvRequest{
|
||||
PromptEchoOn,
|
||||
"Give me your non-private infos",
|
||||
})
|
||||
},
|
||||
},
|
||||
"StartConv-prompt-echo-off": {
|
||||
expectedValue: stringConvResponse{PromptEchoOff, "here's my private data"},
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: PromptEchoOff,
|
||||
ExpectedMessage: "Give me your private secrets",
|
||||
PromptEchoOff: "here's my private data",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvImpl(mock, StringConvRequest{
|
||||
PromptEchoOff,
|
||||
"Give me your private secrets",
|
||||
})
|
||||
},
|
||||
},
|
||||
"StartConv-unknown-style": {
|
||||
expectedError: ErrConv,
|
||||
expectedValue: nil,
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: Style(9999),
|
||||
ExpectedMessage: "hello PAM!",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvImpl(mock, StringConvRequest{
|
||||
Style(9999),
|
||||
"hello PAM!",
|
||||
})
|
||||
},
|
||||
},
|
||||
"StartConv-unknown-style-response": {
|
||||
expectedError: ErrConv,
|
||||
expectedValue: nil,
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: Style(9999),
|
||||
ExpectedMessage: "hello PAM!",
|
||||
IgnoreUnknownStyle: true,
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvImpl(mock, StringConvRequest{
|
||||
Style(9999),
|
||||
"hello PAM!",
|
||||
})
|
||||
},
|
||||
},
|
||||
"StartStringConv-text-info": {
|
||||
expectedValue: stringConvResponse{TextInfo, "nice to see you, Go!"},
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: TextInfo,
|
||||
ExpectedMessage: "hello PAM!",
|
||||
TextInfo: "nice to see you, Go!",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startStringConvImpl(mock, TextInfo,
|
||||
"hello PAM!")
|
||||
},
|
||||
},
|
||||
"StartStringConv-error-msg": {
|
||||
expectedValue: stringConvResponse{ErrorMsg, "ops, sorry..."},
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: ErrorMsg,
|
||||
ExpectedMessage: "This is wrong, PAM!",
|
||||
ErrorMsg: "ops, sorry...",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startStringConvImpl(mock, ErrorMsg,
|
||||
"This is wrong, PAM!")
|
||||
},
|
||||
},
|
||||
"StartStringConv-prompt-echo-on": {
|
||||
expectedValue: stringConvResponse{PromptEchoOn, "here's my public data"},
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: PromptEchoOn,
|
||||
ExpectedMessage: "Give me your non-private infos",
|
||||
PromptEchoOn: "here's my public data",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startStringConvImpl(mock, PromptEchoOn,
|
||||
"Give me your non-private infos")
|
||||
},
|
||||
},
|
||||
"StartStringConv-prompt-echo-off": {
|
||||
expectedValue: stringConvResponse{PromptEchoOff, "here's my private data"},
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: PromptEchoOff,
|
||||
ExpectedMessage: "Give me your private secrets",
|
||||
PromptEchoOff: "here's my private data",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startStringConvImpl(mock, PromptEchoOff,
|
||||
"Give me your private secrets")
|
||||
},
|
||||
},
|
||||
"StartStringConv-binary": {
|
||||
expectedError: ErrConv,
|
||||
expectedValue: nil,
|
||||
conversationHandler: mockConversationHandler{
|
||||
ExpectedStyle: BinaryPrompt,
|
||||
ExpectedMessage: "require binary data",
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startStringConvImpl(mock, PromptEchoOff,
|
||||
"require binary data")
|
||||
},
|
||||
},
|
||||
"StartConvMulti-missing": {
|
||||
expectedError: ErrConv,
|
||||
expectedValue: ([]ConvResponse)(nil),
|
||||
conversationHandler: mockConversationHandler{},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvMultiImpl(mock, nil)
|
||||
},
|
||||
},
|
||||
"StartConvMulti-too-many": {
|
||||
expectedError: ErrConv,
|
||||
expectedValue: ([]ConvResponse)(nil),
|
||||
conversationHandler: mockConversationHandler{},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
reqs := [maxNumMsg + 1]ConvRequest{}
|
||||
return mt.startConvMultiImpl(mock, reqs[:])
|
||||
},
|
||||
},
|
||||
"StartConvMulti-unexpected-style": {
|
||||
expectedError: ErrConv,
|
||||
expectedValue: ([]ConvResponse)(nil),
|
||||
conversationHandler: mockConversationHandler{},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
var req ConvRequest = customConvRequest(0xdeadbeef)
|
||||
return mt.startConvMultiImpl(mock, []ConvRequest{req})
|
||||
},
|
||||
},
|
||||
"StartConvMulti-string-as-binary": {
|
||||
expectedError: ErrConv,
|
||||
expectedValue: ([]ConvResponse)(nil),
|
||||
conversationHandler: mockConversationHandler{},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvMultiImpl(mock, []ConvRequest{
|
||||
NewStringConvRequest(BinaryPrompt, "no binary!"),
|
||||
})
|
||||
},
|
||||
},
|
||||
"StartConvMulti-all-types": {
|
||||
expectedValue: []ConvResponse{
|
||||
stringConvResponse{TextInfo, "nice to see you, Go!"},
|
||||
stringConvResponse{ErrorMsg, "ops, sorry..."},
|
||||
stringConvResponse{PromptEchoOn, "here's my public data"},
|
||||
stringConvResponse{PromptEchoOff, "here's my private data"},
|
||||
},
|
||||
conversationHandler: mockConversationHandler{
|
||||
TextInfo: "nice to see you, Go!",
|
||||
ErrorMsg: "ops, sorry...",
|
||||
PromptEchoOn: "here's my public data",
|
||||
PromptEchoOff: "here's my private data",
|
||||
ExpectedMessagesByStyle: map[Style]string{
|
||||
TextInfo: "hello PAM!",
|
||||
ErrorMsg: "This is wrong, PAM!",
|
||||
PromptEchoOn: "Give me your non-private infos",
|
||||
PromptEchoOff: "Give me your private secrets",
|
||||
},
|
||||
},
|
||||
testFunc: func(mock *mockModuleTransaction) (any, error) {
|
||||
return mt.startConvMultiImpl(mock, []ConvRequest{
|
||||
NewStringConvRequest(TextInfo, "hello PAM!"),
|
||||
NewStringConvRequest(ErrorMsg, "This is wrong, PAM!"),
|
||||
NewStringConvRequest(PromptEchoOn, "Give me your non-private infos"),
|
||||
NewStringConvRequest(PromptEchoOff, "Give me your private secrets"),
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
|
||||
@@ -59,6 +59,11 @@ static inline void init_pam_conv(struct pam_conv *conv, uintptr_t appdata)
|
||||
conv->appdata_ptr = (void *)appdata;
|
||||
}
|
||||
|
||||
static inline int start_pam_conv(struct pam_conv *pc, int num_msgs, const struct pam_message **msgs, struct pam_response **out_resp)
|
||||
{
|
||||
return pc->conv(num_msgs, msgs, out_resp, pc->appdata_ptr);
|
||||
}
|
||||
|
||||
// pam_start_confdir is a recent PAM api to declare a confdir (mostly for
|
||||
// tests) weaken the linking dependency to detect if it’s present.
|
||||
int pam_start_confdir(const char *service_name, const char *user, const struct pam_conv *pam_conversation,
|
||||
|
||||
Reference in New Issue
Block a user