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 ae0c33cc..0ca4beae 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 @@ -55,10 +55,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)) + } } } @@ -66,6 +72,17 @@ 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.StringConvResponse: + if value != nil { + res.ActionArgs = append(res.ActionArgs, + SerializableStringConvResponse{value.Style(), + value.Response()}) + } else { + res.ActionArgs = append(res.ActionArgs, nil) + } case pam.ReturnType: 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 e2879cc0..730e92b2 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 @@ -662,6 +662,198 @@ func Test_Moduler_IntegrationTesterModule(t *testing.T) { }, }, }, + "start-conv-no-conv-set": { + expectedStatus: pam.ConvErr, + checkedRequests: []checkedRequest{ + { + r: NewRequest("StartConv", SerializableStringConvRequest{ + pam.TextInfo, + "hello PAM!", + }), + exp: []interface{}{nil, pam.ConvErr}, + }, + { + r: NewRequest("StartStringConv", pam.TextInfo, "hello PAM!"), + exp: []interface{}{nil, pam.ConvErr}, + }, + }, + }, + "start-conv-prompt-text-info": { + expectedStatus: pam.Success, + 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": { + expectedStatus: pam.Success, + 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": { + expectedStatus: pam.Success, + 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": { + expectedStatus: pam.Success, + 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": { + expectedStatus: pam.ConvErr, + 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.ConvErr}, + }, + { + r: NewRequest("StartStringConv", pam.TextInfo, + "This should have been an info message, but is not", + ), + exp: []interface{}{nil, pam.ConvErr}, + }, + }, + }, + "start-conv-text-info-handle-failure-style-mismatch": { + expectedStatus: pam.ConvErr, + 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.ConvErr}, + }, + { + r: NewRequest("StartStringConv", pam.TextInfo, + "This is an info message", + ), + exp: []interface{}{nil, pam.ConvErr}, + }, + }, + }, } for name, tc := range tests { @@ -669,6 +861,7 @@ func Test_Moduler_IntegrationTesterModule(t *testing.T) { name := name t.Run(name, func(t *testing.T) { t.Parallel() + socketPath := filepath.Join(ts.WorkDir(), name+".socket") ts.CreateService(name, []utils.ServiceLine{ {utils.Auth, utils.Requisite, modulePath, []string{socketPath}}, @@ -948,6 +1141,23 @@ func Test_Moduler_IntegrationTesterModule_Authenticate(t *testing.T) { }, }, }, + "StartConv": { + expectedStatus: pam.SystemErr, + checkedRequests: []checkedRequest{{ + r: NewRequest("StartConv", SerializableStringConvRequest{ + pam.TextInfo, + "hello PAM!", + }), + exp: []interface{}{nil, pam.SystemErr}, + }}, + }, + "StartStringConv": { + expectedStatus: pam.SystemErr, + checkedRequests: []checkedRequest{{ + r: NewRequest("StartStringConv", pam.TextInfo, "hello PAM!"), + exp: []interface{}{nil, pam.SystemErr}, + }}, + }, } 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 3ada4a89..1faefe46 100644 --- a/cmd/pam-moduler/tests/integration-tester-module/serialization.go +++ b/cmd/pam-moduler/tests/integration-tester-module/serialization.go @@ -19,12 +19,28 @@ func (e *SerializableTransactionError) Error() string { return e.Status().Error() } +type SerializableStringConvRequest struct { + Style pam.Style + Request string +} + +type SerializableStringConvResponse struct { + Style pam.Style + Response string +} + func init() { gob.Register(map[string]string{}) gob.Register(Request{}) gob.Register(pam.Item(pam.Abort)) + gob.Register(pam.Style(pam.TextInfo)) gob.Register(pam.ReturnType(pam.Success)) + gob.Register([]pam.ConvResponse{}) gob.RegisterName("main.SerializableTransactionError", SerializableTransactionError{}) + 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 a5413d0a..d7a39962 100644 --- a/cmd/pam-moduler/tests/internal/utils/test-utils.go +++ b/cmd/pam-moduler/tests/internal/utils/test-utils.go @@ -98,6 +98,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 @@ -126,9 +130,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 "", pam.NewTransactionError( diff --git a/module-transaction.go b/module-transaction.go index 87414245..630e6462 100644 --- a/module-transaction.go +++ b/module-transaction.go @@ -11,11 +11,14 @@ package pam int set_data(pam_handle_t *pamh, const char *name, uintptr_t handle); int get_data(pam_handle_t *pamh, const char *name, uintptr_t *out_handle); +int start_pam_conv(struct pam_conv *pc, int num_msgs, const struct pam_message **msgs, struct pam_response **out_resp); +struct pam_message **alloc_pam_messages(size_t num); */ import "C" import ( "errors" + "fmt" "runtime/cgo" "sync/atomic" "unsafe" @@ -141,3 +144,190 @@ func (m *ModuleTransaction) GetData(key string) (any, error) { atomic.StoreInt32(&m.status, int32(NoModuleData)) return nil, NoModuleData } + +// 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 + + ret := C.pam_get_item(m.handle, C.PAM_CONV, &convPtr) + atomic.StoreInt32(&m.status, int32(ret)) + if ret != Success.toC() { + return nil, ReturnType(ret) + } + + 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 +} + +// StringConvResponse is a ConvResponse 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) { + switch style { + case BinaryPrompt: + return nil, NewTransactionError(errors.New("Binary style is not supported"), + ConvErr) + } + + res, err := m.StartConv(NewStringConvRequest(style, prompt)) + if err != nil { + return nil, err + } + + stringRes := res.(StringConvResponse) + return &stringRes, nil +} + +// StartsStringConvf 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) { + if resp, err := m.StartConvMulti([]ConvRequest{req}); err != nil { + return nil, err + } else if len(resp) != 1 { + return nil, NewTransactionError(errors.New("not enough values returned"), + ConvErr) + } else { + return resp[0], nil + } +} + +// StartConv initiates a PAM conversation with multiple ConvRequest's +func (m *ModuleTransaction) StartConvMulti(requests []ConvRequest) ( + []ConvResponse, error) { + if len(requests) == 0 { + return nil, NewTransactionError(errors.New("no requests defined"), + ConvErr) + } + if len(requests) > C.PAM_MAX_NUM_MSG { + return nil, NewTransactionError(errors.New("too many requests"), + ConvErr) + } + + conv, err := m.getConv() + if err != nil { + return nil, err + } + + if conv == nil || conv.conv == nil { + atomic.StoreInt32(&m.status, int32(ConvErr)) + return nil, NewTransactionError( + errors.New("impossible to find conv handler"), + ConvErr) + } + + // FIXME: Just use make([]C.struct_pam_message, 0, len(requests)) + // and append, when it's possible to use runtime.Pinner + cMessages := C.alloc_pam_messages(C.size_t(len(requests))) + 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(unsafe.Pointer(cBytes)) + default: + return nil, NewTransactionError( + fmt.Errorf( + "unsupported conversation type %v", r), + ConvErr) + } + + goMsgs[i] = &C.struct_pam_message{ + msg_style: C.int(req.Style()), + msg: (*C.char)(cBytes), + } + } + + var cResponses *C.struct_pam_response + ret := C.start_pam_conv(conv, C.int(len(requests)), cMessages, &cResponses) + atomic.StoreInt32(&m.status, int32(ret)) + if ret != Success.toC() { + return nil, ReturnType(ret) + } + + responses := unsafe.Slice(cResponses, len(requests)) + defer func() { + for _, resp := range responses { + C.free(unsafe.Pointer(resp.resp)) + } + C.free(unsafe.Pointer(cResponses)) + }() + + goReplies := make([]ConvResponse, 0, len(requests)) + for i, resp := range responses { + msgStyle := requests[i].Style() + switch msgStyle { + case PromptEchoOff: + fallthrough + case PromptEchoOn: + fallthrough + case ErrorMsg: + fallthrough + case TextInfo: + goReplies = append(goReplies, StringConvResponse{ + style: msgStyle, + response: C.GoString(resp.resp), + }) + default: + atomic.StoreInt32(&m.status, int32(ConvErr)) + return nil, NewTransactionError( + fmt.Errorf( + "unsupported conversation type %v", msgStyle), + ConvErr) + } + } + + return goReplies, nil +} diff --git a/module-transaction_test.go b/module-transaction_test.go index 4e85941b..6f0dc244 100644 --- a/module-transaction_test.go +++ b/module-transaction_test.go @@ -81,6 +81,24 @@ func Test_NewNullModuleTransaction(t *testing.T) { return nil, mt.SetData("foo", nil) }, }, + "StartConv-StringConv": { + testFunc: func(t *testing.T) (any, error) { + return mt.StartConv(NewStringConvRequest(TextInfo, "a prompt")) + }, + }, + "StartStringConv": { + testFunc: func(t *testing.T) (any, error) { + return mt.StartStringConv(TextInfo, "a prompt") + }, + }, + "StartConvMulti": { + testFunc: func(t *testing.T) (any, error) { + return mt.StartConvMulti([]ConvRequest{ + NewStringConvRequest(TextInfo, "a prompt"), + NewStringConvRequest(ErrorMsg, "another prompt"), + }) + }, + }, } for name, tc := range tests { diff --git a/transaction.c b/transaction.c index c6e59fd8..9b352807 100644 --- a/transaction.c +++ b/transaction.c @@ -47,6 +47,16 @@ void init_pam_conv(struct pam_conv *conv, uintptr_t appdata) conv->appdata_ptr = (void *)appdata; } +struct pam_message **alloc_pam_messages(size_t num) +{ + return calloc(num, sizeof(struct pam_message *)); +} + +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,