Skip to content

Commit 0435964

Browse files
committed
pam: Add PamModuleTransaction interface definition
This is a base interface that the PamModule transaction should implement and so use and pass this interface instead of relying on the actual module transaction implementation, so that we can mock it. As per this introduce a new dummy implementation for it that can be used in local tests
1 parent a520835 commit 0435964

1 file changed

Lines changed: 295 additions & 0 deletions

File tree

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
package main
2+
3+
import "C"
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"strings"
9+
"unsafe"
10+
11+
"github.com/msteinert/pam"
12+
)
13+
14+
// PamModuleTransactionDummy is an implementation of PamModuleTransaction for
15+
// testing purposes.
16+
type PamModuleTransactionDummy struct {
17+
Items map[pam.Item]string
18+
Env map[string]string
19+
Data map[string]any
20+
convHandler pam.ConversationHandler
21+
}
22+
23+
// NewPamModuleTransactionDummy returns a new PamModuleTransactionDummy.
24+
func NewPamModuleTransactionDummy(convHandler pam.ConversationHandler) pam.ModuleTransaction {
25+
return &PamModuleTransactionDummy{
26+
convHandler: convHandler,
27+
}
28+
}
29+
30+
// InvokeHandler is called by the C code to invoke the proper handler.
31+
func (m *PamModuleTransactionDummy) InvokeHandler(handler pam.ModuleHandlerFunc,
32+
flags pam.Flags, args []string) (pam.Status, error) {
33+
return pam.Abort, pam.Abort
34+
}
35+
36+
// SetItem sets a PAM information item.
37+
func (m *PamModuleTransactionDummy) SetItem(item pam.Item, value string) error {
38+
if m.Items == nil {
39+
m.Items = make(map[pam.Item]string)
40+
}
41+
42+
m.Items[item] = value
43+
return nil
44+
}
45+
46+
// GetItem retrieves a PAM information item.
47+
func (m *PamModuleTransactionDummy) GetItem(item pam.Item) (string, error) {
48+
if m.Items == nil {
49+
return "", nil
50+
}
51+
return m.Items[item], nil
52+
}
53+
54+
// PutEnv adds or changes the value of PAM environment variables.
55+
//
56+
// NAME=value will set a variable to a value.
57+
// NAME= will set a variable to an empty value.
58+
// NAME (without an "=") will delete a variable.
59+
func (m *PamModuleTransactionDummy) PutEnv(nameVal string) error {
60+
if m.Env == nil {
61+
m.Env = make(map[string]string)
62+
}
63+
64+
env, value, found := strings.Cut(nameVal, "=")
65+
if !found {
66+
delete(m.Env, env)
67+
return nil
68+
}
69+
if env == "" {
70+
return pam.BadItem
71+
}
72+
m.Env[env] = value
73+
return nil
74+
}
75+
76+
// GetEnv is used to retrieve a PAM environment variable.
77+
func (m *PamModuleTransactionDummy) GetEnv(name string) string {
78+
if m.Env == nil {
79+
return ""
80+
}
81+
82+
return m.Env[name]
83+
}
84+
85+
// GetEnvList returns a copy of the PAM environment as a map.
86+
func (m *PamModuleTransactionDummy) GetEnvList() (map[string]string, error) {
87+
if m.Env == nil {
88+
m.Env = make(map[string]string)
89+
}
90+
return m.Env, nil
91+
}
92+
93+
// GetUser is similar to GetItem(User), but it would start a conversation if
94+
// no user is currently set in PAM.
95+
func (m *PamModuleTransactionDummy) GetUser(prompt string) (string, error) {
96+
if user, err := m.GetItem(pam.User); err != nil {
97+
return "", err
98+
} else if user != "" {
99+
return user, nil
100+
}
101+
102+
resp, err := m.StartStringConv(pam.PromptEchoOn, prompt)
103+
if err != nil {
104+
return "", err
105+
}
106+
107+
return resp.Response(), nil
108+
}
109+
110+
// SetData allows to save any value in the module data that is preserved
111+
// during the whole time the module is loaded.
112+
func (m *PamModuleTransactionDummy) SetData(key string, data any) error {
113+
if m.Data == nil {
114+
m.Data = make(map[string]any)
115+
}
116+
117+
m.Data[key] = data
118+
return nil
119+
}
120+
121+
// GetData allows to get any value from the module data saved using SetData
122+
// that is preserved across the whole time the module is loaded.
123+
func (m *PamModuleTransactionDummy) GetData(key string) (any, error) {
124+
if m.Data == nil {
125+
return nil, pam.NoModuleData
126+
}
127+
128+
data, found := m.Data[key]
129+
if found {
130+
return data, nil
131+
}
132+
return nil, pam.NoModuleData
133+
}
134+
135+
// DummyStringResponse is a simple implementation of pam.StringConvResponse.
136+
type DummyStringResponse struct {
137+
style pam.Style
138+
content string
139+
}
140+
141+
// Style returns the conversation style of the DummyStringResponse.
142+
func (s DummyStringResponse) Style() pam.Style {
143+
return s.style
144+
}
145+
146+
// Response returns the string response of the DummyStringResponse.
147+
func (s DummyStringResponse) Response() string {
148+
return s.content
149+
}
150+
151+
// DummyBinaryResponse is an implementation of pam.BinaryConvResponse.
152+
type DummyBinaryResponse struct {
153+
ptr pam.BinaryPointer
154+
}
155+
156+
// Style returns the response style for the response, so always BinaryPrompt.
157+
func (b DummyBinaryResponse) Style() pam.Style {
158+
return pam.BinaryPrompt
159+
}
160+
161+
// Data returns the response native pointer, it's up to the protocol to parse
162+
// it accordingly.
163+
func (b DummyBinaryResponse) Data() pam.BinaryPointer {
164+
return b.ptr
165+
}
166+
167+
// Decode decodes the binary data using the provided decoder function.
168+
func (b DummyBinaryResponse) Decode(decoder pam.BinaryDecoder) (
169+
[]byte, error) {
170+
if decoder == nil {
171+
return nil, errors.New("nil decoder provided")
172+
}
173+
return decoder(b.ptr)
174+
}
175+
176+
// StartStringConv starts a text-based conversation using the provided style
177+
// and prompt.
178+
func (m *PamModuleTransactionDummy) StartStringConv(style pam.Style, prompt string) (
179+
pam.StringConvResponse, error) {
180+
switch style {
181+
case pam.BinaryPrompt:
182+
return nil, pam.NewTransactionError(errors.New("Binary style is not supported"),
183+
pam.ConvErr)
184+
}
185+
186+
res, err := m.StartConv(pam.NewStringConvRequest(style, prompt))
187+
if err != nil {
188+
return nil, err
189+
}
190+
191+
if stringRes, ok := res.(pam.StringConvResponse); ok {
192+
return stringRes, nil
193+
}
194+
195+
return nil, pam.NewTransactionError(
196+
errors.New("Can't convert to pam.StringConvResponse"), pam.ConvErr)
197+
}
198+
199+
// StartStringConvf allows to start string conversation with formatting support.
200+
func (m *PamModuleTransactionDummy) StartStringConvf(style pam.Style, format string, args ...interface{}) (
201+
pam.StringConvResponse, error) {
202+
return m.StartStringConv(style, fmt.Sprintf(format, args...))
203+
}
204+
205+
// StartBinaryConv starts a binary conversation using the provided bytes.
206+
func (m *PamModuleTransactionDummy) StartBinaryConv(bytes []byte) (
207+
pam.BinaryConvResponse, error) {
208+
res, err := m.StartConv(pam.NewBinaryConvRequestFromBytes(bytes))
209+
if err != nil {
210+
return nil, err
211+
}
212+
213+
if binaryRes, ok := res.(pam.BinaryConvResponse); ok {
214+
return binaryRes, nil
215+
}
216+
217+
return nil, pam.NewTransactionError(
218+
errors.New("Can't convert to pam.BinaryConvResponse"), pam.ConvErr)
219+
}
220+
221+
// StartConv initiates a PAM conversation using the provided ConvRequest.
222+
func (m *PamModuleTransactionDummy) StartConv(req pam.ConvRequest) (
223+
pam.ConvResponse, error) {
224+
if resp, err := m.StartConvMulti([]pam.ConvRequest{req}); err != nil {
225+
return nil, err
226+
} else if len(resp) != 1 {
227+
return nil, pam.NewTransactionError(errors.New("not enough values returned"),
228+
pam.ConvErr)
229+
} else {
230+
return resp[0], nil
231+
}
232+
}
233+
234+
// StartConvMulti initiates a PAM conversation with multiple ConvRequest's.
235+
func (m *PamModuleTransactionDummy) StartConvMulti(requests []pam.ConvRequest) (
236+
[]pam.ConvResponse, error) {
237+
if len(requests) == 0 {
238+
return nil, pam.NewTransactionError(errors.New("no requests defined"),
239+
pam.ConvErr)
240+
}
241+
242+
goReplies := make([]pam.ConvResponse, 0, len(requests))
243+
for _, req := range requests {
244+
msgStyle := req.Style()
245+
switch msgStyle {
246+
case pam.PromptEchoOff:
247+
fallthrough
248+
case pam.PromptEchoOn:
249+
fallthrough
250+
case pam.ErrorMsg:
251+
fallthrough
252+
case pam.TextInfo:
253+
if m.convHandler == nil {
254+
return nil, pam.NewTransactionError(
255+
fmt.Errorf(
256+
"no conversation handler provided for style %v", msgStyle),
257+
pam.ConvErr)
258+
}
259+
reply, err := m.convHandler.RespondPAM(msgStyle,
260+
req.(pam.StringConvRequest).Prompt())
261+
if err != nil {
262+
return nil, err
263+
}
264+
goReplies = append(goReplies, DummyStringResponse{
265+
msgStyle,
266+
reply,
267+
})
268+
case pam.BinaryPrompt:
269+
handler, ok := m.convHandler.(pam.BinaryConversationHandler)
270+
if handler == nil || !ok {
271+
return nil, pam.NewTransactionError(
272+
errors.New("no binary handler provided"),
273+
pam.ConvErr)
274+
}
275+
reply, err := handler.RespondPAMBinary(
276+
req.(pam.BinaryConvRequest).Pointer())
277+
if err != nil {
278+
return nil, err
279+
}
280+
goReplies = append(goReplies,
281+
DummyBinaryResponse{pam.BinaryPointer(&reply)})
282+
default:
283+
return nil, pam.NewTransactionError(
284+
fmt.Errorf(
285+
"unsupported conversation type %v", msgStyle),
286+
pam.ConvErr)
287+
}
288+
}
289+
290+
return goReplies, nil
291+
}
292+
293+
func getBinaryFromPointer(ptr pam.BinaryPointer, length int) []byte {
294+
return C.GoBytes(unsafe.Pointer(ptr), C.int(length))
295+
}

0 commit comments

Comments
 (0)