diff --git a/cmd/pam-moduler/tests/integration-tester-module/integration-tester-module.go b/cmd/pam-moduler/tests/integration-tester-module/integration-tester-module.go index 76cebe8..7437534 100644 --- a/cmd/pam-moduler/tests/integration-tester-module/integration-tester-module.go +++ b/cmd/pam-moduler/tests/integration-tester-module/integration-tester-module.go @@ -56,10 +56,16 @@ func (m *integrationTesterModule) handleRequest(authReq *authRequest, r *Request var args []reflect.Value for i, arg := range r.ActionArgs { - if arg == nil { - args = append(args, reflect.Zero(method.Type().In(i))) - } else { - args = append(args, reflect.ValueOf(arg)) + 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)) + } } } @@ -67,6 +73,9 @@ func (m *integrationTesterModule) handleRequest(authReq *authRequest, r *Request 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) diff --git a/cmd/pam-moduler/tests/integration-tester-module/integration-tester-module_test.go b/cmd/pam-moduler/tests/integration-tester-module/integration-tester-module_test.go index d17ba51..a8b9bb8 100644 --- a/cmd/pam-moduler/tests/integration-tester-module/integration-tester-module_test.go +++ b/cmd/pam-moduler/tests/integration-tester-module/integration-tester-module_test.go @@ -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 { diff --git a/cmd/pam-moduler/tests/integration-tester-module/serialization.go b/cmd/pam-moduler/tests/integration-tester-module/serialization.go index 33b26a7..2eae17b 100644 --- a/cmd/pam-moduler/tests/integration-tester-module/serialization.go +++ b/cmd/pam-moduler/tests/integration-tester-module/serialization.go @@ -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{}) } diff --git a/cmd/pam-moduler/tests/internal/utils/test-utils.go b/cmd/pam-moduler/tests/internal/utils/test-utils.go index 095994b..ce2281b 100644 --- a/cmd/pam-moduler/tests/internal/utils/test-utils.go +++ b/cmd/pam-moduler/tests/internal/utils/test-utils.go @@ -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,9 +149,19 @@ func (c Credentials) RespondPAM(s pam.Style, msg string) (string, error) { switch s { case pam.PromptEchoOn: - return c.User, nil + if c.User != "" { + return c.User, nil + } + return c.EchoOn, nil case pam.PromptEchoOff: - return c.Password, nil + 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, diff --git a/module-transaction-mock.go b/module-transaction-mock.go index 968026a..a789b1e 100644 --- a/module-transaction-mock.go +++ b/module-transaction-mock.go @@ -7,6 +7,8 @@ package pam #include #include #include + +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 - ExpectedMessage string - CheckEmptyMessage bool - ExpectedStyle Style - CheckZeroStyle bool + 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,7 +160,20 @@ func (c mockConversationHandler) RespondPAM(s Style, msg string) (string, error) switch s { case PromptEchoOn: - return c.User, nil + 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) diff --git a/module-transaction.go b/module-transaction.go index 71419e0..bbb1e13 100644 --- a/module-transaction.go +++ b/module-transaction.go @@ -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) +} diff --git a/module-transaction_test.go b/module-transaction_test.go index fa4c1be..9c4da20 100644 --- a/module-transaction_test.go +++ b/module-transaction_test.go @@ -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 { diff --git a/transaction.h b/transaction.h index b19ce3e..4c9f000 100644 --- a/transaction.h +++ b/transaction.h @@ -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,