14 |
15 |
16 | .. _intro:
17 |
18 | Introduction
19 | ============
20 |
21 | The |pam_python| PAM module runs the Python source file (aka Python PAM
22 | module) it is given in the Python interpreter, making the PAM module API
23 | available to it. This document describes the how the PAM Module API is exposed
24 | to the Python PAM module. It does not describe how to use the API. You must read
25 | the |PMWG|_ to learn how to do that. To re-iterate: this
26 | document does not tell you how to write PAM modules, it only tells you how to
27 | access the PAM module API from Python.
28 |
29 | Writing PAM modules from Python incurs a large performance penalty and requires
30 | Python to be installed, so it is not the best option for writing modules that
31 | will be used widely. On the other hand memory allocation / corruption problems
32 | can not be caused by bad Python code, and a Python module is generally shorter
33 | and easier to write than its C equivalent. This makes it ideal for the system
34 | administrator who just wants to make use of the the PAM API for his own ends
35 | while minimising the risk of introducing memory corruption problems into every
36 | program using PAM.
37 |
38 |
39 | .. _configuring:
40 |
41 | Configuring PAM
42 | ===============
43 |
44 | Tell PAM to use a Python PAM module in the usual way: add a rule to your PAM
45 | configuration. The PAM administrators manual gives the syntax of a rule as::
46 |
47 | service type control module-path module-arguments
48 |
49 | The first three parameters are the same for all PAM modules and so aren't any
50 | different for |pam_python|. The *module-path* is the path to pam_python.so.
51 | Like all paths PAM modules it is relative to the default PAM module directory so
52 | is usually just the string ``pam_python.so``. The first *module-argument* is the
53 | path to the Python PAM module. If it doesn't start with a / it is relative to
54 | the ``/lib/security``. All *module-arguments*, including the path name to the
55 | Python PAM module are passed to it.
56 |
57 |
58 | .. _module:
59 |
60 | Python PAM modules
61 | ==================
62 |
63 | When a PAM handle created by the applications call to PAM's :samp:`pam_start()`
64 | function first uses a Python PAM module, |pam_python| invokes it using Python's
65 | ``execfile`` function. The following variables are passed to the invoked
66 | module in its global namespace:
67 |
68 |
69 | .. data:: __builtins__
70 |
71 | The usual Python ``__builtins__``.
72 |
73 |
74 | .. data:: __file__
75 |
76 | The absolute path name to the Python PAM module.
77 |
78 | As described in the |PMWG|, PAM interacts with your module by calling methods
79 | you provide in it. Each ``type`` in the PAM configuration rules results in one
80 | or more methods being called. The Python PAM module must define the methods that
81 | will be called by each rule ``type`` it can be used with. Those methods are:
82 |
83 |
84 | .. function:: pam_sm_acct_mgmt(pamh, flags, args)
85 |
86 | The service module's implementation of PAM's :manpage:`pam_acct_mgmt(3)` interface.
87 |
88 |
89 | .. function:: pam_sm_authenticate(pamh, flags, args)
90 |
91 | The service module's implementation of PAM's :manpage:`pam_authenticate(3)`
92 | interface.
93 |
94 |
95 | .. function:: pam_sm_close_session(pamh, flags, args)
96 |
97 | The service module's implementation of PAM's :manpage:`pam_close_session(3)`
98 | interface.
99 |
100 |
101 | .. function:: pam_sm_chauthtok(pamh, flags, args)
102 |
103 | The service module's implementation of PAM's :manpage:`pam_chauthtok(3)` interface.
104 |
105 |
106 | .. function:: pam_sm_open_session(pamh, flags, args)
107 |
108 | The service module's implementation of PAM's :manpage:`pam_open_session(3)`
109 | interface.
110 |
111 |
112 | .. function:: pam_sm_setcred(pamh, flags, args)
113 |
114 | The service module's implementation of PAM's :manpage:`pam_setcred(3)` interface.
115 |
116 | The arguments and return value of all these methods are the same. The *pamh*
117 | parameter is an instance of the :class:`PamHandle` class. It is used to interact
118 | with PAM and is described in the next section. The remaining arguments are as
119 | described in the |PMWG|. All functions must return an integer,
120 | eg :const:`pamh.PAM_SUCCESS`. The valid return codes for each function are
121 | defined |PMWG|. If the Python method isn't present
122 | |pam_python| will return :const:`pamh.PAM_SYMBOL_ERR` to PAM; if the method
123 | doesn't return an integer or throws an exception :const:`pamh.PAM_SERVICE_ERR`
124 | is returned.
125 |
126 | There is one other method that in the Python PAM module
127 | that may be called by |pam_python|.
128 | It is optional:
129 |
130 |
131 | .. function:: pam_sm_end(pamh)
132 |
133 | If present this will be called when the application calls PAM's
134 | :manpage:`pam_end(3)` function.
135 | If not present nothing happens.
136 | The parameter *pamh* is the :class:`PamHandle` object.
137 | The return value is ignored.
138 |
139 |
140 | .. _pamhandle:
141 |
142 | The PamHandle Class
143 | ===================
144 |
145 | An instance of this class is automatically created for a Python PAM module when
146 | it is first referenced, (ie when it is ``execfile``'ed). It is the first
147 | argument to every Python method called by PAM. It is destroyed automatically
148 | when PAM's :c:func:`pam_end` is called, right after the ``execfile``'ed
149 | module is destroyed. If any method fails, or any access to a member fails a
150 | :exc:`PamHandle.exception` exception will be thrown. It contains the following
151 | members:
152 |
153 |
154 | .. data:: PAM_???
155 |
156 | All the :const:`PAM_???` constants defined in the PAM include files
157 | version 1.1.1 are available. They are all read-only :class:`int`'s.
158 |
159 |
160 | .. data:: authtok
161 |
162 | The :const:`PAM_AUTHTOK` PAM item. Reading this results in a call
163 | to the |pam-lib-func| :samp:`pam_get_item(PAM_AUTHTOK)`, writing it
164 | results in a call :samp:`pam_set_item(PAM_AUTHTOK, value)`. Its
165 | value will be either a :class:`string` or :const:`None` for the C
166 | value :c:macro:`NULL`.
167 |
168 |
169 | .. data:: authtok_type
170 |
171 | The :const:`PAM_AUTHTOK_TYPE` PAM item. Reading this results in a call
172 | to the |pam-lib-func| :samp:`pam_get_item(PAM_AUTHTOK_TYPE)`, writing it
173 | results in a call :samp:`pam_set_item(PAM_AUTHTOK_TYPE, value)`. Its
174 | value will be either a :class:`string` or :const:`None` for the C
175 | value :c:macro:`NULL`.
176 | New in version 1.0.0.
177 | Only present if the version of PAM |pam_python| is compiled with supports it.
178 |
179 |
180 | .. data:: env
181 |
182 | This is a mapping representing the PAM environment. |pam_python| implements
183 | accesses and changes to it via the |pam-lib-func| :samp:`pam_getenv()`,
184 | :samp:`pam_putenv()` and :samp:`pam_getenvlist()`. The PAM environment
185 | only supports :class:`string` keys and values, and the keys may not be
186 | blank nor contain '='.
187 |
188 |
189 | .. data:: exception
190 |
191 | The exception raised by methods defined here if they fail. It is a
192 | subclass of :class:`StandardError`. Instances contain the member
193 | :const:`pam_result`, which is the error code returned by PAM. The
194 | description is the PAM error message.
195 |
196 |
197 | .. data:: libpam_version
198 |
199 | The version of PAM |pam_python| was compiled with. This is a
200 | :class:`string`. In version 0.1.0 of |pam_python| and prior this was an
201 | :class:`int` holding the version of PAM library loaded. Newer versions of
202 | PAM no longer export that value.
203 |
204 |
205 | .. data:: pamh
206 |
207 | The PAM handle, as read-only :class:`int`. Possibly useful during debugging.
208 |
209 |
210 | .. data:: py_initialized
211 |
212 | A read-only :class:`int`.
213 | If the Python interpreter was initialised
214 | before the |pam_python| module was created this is 0.
215 | Otherwise it is 1, meaning |pam_python| has called :c:func:`Py_Initialize`
216 | and will call :c:func:`Py_Finalize`
217 | when the last |pam_python| module is destroyed.
218 |
219 |
220 | .. data:: oldauthtok
221 |
222 | The :const:`PAM_OLDAUTHTOK` PAM item. Reading this results in a call
223 | to the |pam-lib-func| :samp:`pam_get_item(PAM_OLDAUTHTOK)`,
224 | writing it results in a call :samp:`pam_set_item(PAM_OLDAUTHTOK, value)`.
225 | Its value will be either a :class:`string` or :const:`None` for the
226 | C value :c:macro:`NULL`.
227 |
228 |
229 | .. data:: rhost
230 |
231 | The :const:`PAM_RHOST` PAM item. Reading this results in a call
232 | to the |pam-lib-func| :samp:`pam_get_item(PAM_RHOST)`,
233 | writing it results in a call :samp:`pam_set_item(PAM_RHOST, value)`.
234 | Its value will be either a :class:`string`
235 | or :const:`None` for the C value :c:macro:`NULL`.
236 |
237 |
238 | .. data:: ruser
239 |
240 | The :const:`PAM_RUSER` PAM item. Reading this results in a call
241 | to the |pam-lib-func| :samp:`pam_get_item(PAM_RUSER)`,
242 | writing it results in a call :samp:`pam_set_item(PAM_RUSER, value)`.
243 | Its value will be either a :class:`string`
244 | or :const:`None` for the C value :c:macro:`NULL`.
245 |
246 |
247 | .. data:: service
248 |
249 | The :const:`PAM_SERVICE` PAM item. Reading this results in a call
250 | to the |pam-lib-func| :samp:`pam_get_item(PAM_SERVICE)`,
251 | writing it results in a call :samp:`pam_set_item(PAM_SERVICE, value)`.
252 | Its value will be either a :class:`string`
253 | or :const:`None` for the C value :c:macro:`NULL`.
254 |
255 |
256 | .. data:: tty
257 |
258 | The :const:`PAM_TTY` PAM item. Reading this results in a call
259 | to the |pam-lib-func| :samp:`pam_get_item(PAM_TTY)`,
260 | writing it results in a call :samp:`pam_set_item(PAM_TTY, value)`.
261 | Its value will be either a :class:`string`
262 | or :const:`None` for the C value :c:macro:`NULL`.
263 |
264 |
265 | .. data:: user
266 |
267 | The :const:`PAM_USER` PAM item. Reading this results in a call
268 | to the |pam-lib-func| :samp:`pam_get_item(PAM_USER)`,
269 | writing it results in a call :samp:`pam_set_item(PAM_USER, value)`.
270 | Its value will be either a :class:`string`
271 | or :const:`None` for the C value :c:macro:`NULL`.
272 |
273 |
274 | .. data:: user_prompt
275 |
276 | The :const:`PAM_USER_PROMPT` PAM item. Reading this results in a call
277 | to the |pam-lib-func| :samp:`pam_get_item(PAM_USER_PROMPT)`,
278 | writing it results in a call :samp:`pam_set_item(PAM_USER_PROMPT, value)`.
279 | Its value will be either a :class:`string`
280 | or :const:`None` for the C value :c:macro:`NULL`.
281 |
282 |
283 | .. data:: xauthdata
284 |
285 | The :const:`PAM_XAUTHDATA` PAM item. Reading this results in a call
286 | to the |pam-lib-func| :samp:`pam_get_item(PAM_XAUTHDATA)`,
287 | writing it results in a call :samp:`pam_set_item(PAM_XAUTHDATA, value)`.
288 | Its value is a :class:`XAuthData` instance. When setting its value you
289 | don't have to use an actual :class:`XAuthData` instance,
290 | any class that contains a :class:`string` member :attr:`name`
291 | and a :class:`string` member :attr:`data` will do.
292 | New in version 1.0.0.
293 | Only present if the version of PAM |pam_python| is compiled with supports it.
294 |
295 |
296 | .. data:: xdisplay
297 |
298 | The :const:`PAM_XDISPLAY` PAM item. Reading this results in a call
299 | to the |pam-lib-func| :samp:`pam_get_item(PAM_XDISPLAY)`,
300 | writing it results in a call :samp:`pam_set_item(PAM_XDISPLAY, value)`.
301 | Its value will be either a :class:`string`
302 | or :const:`None` for the C value :c:macro:`NULL`.
303 | New in version 1.0.0.
304 | Only present if the version of PAM |pam_python| is compiled with supports it.
305 |
306 | The following methods are available:
307 |
308 |
309 | .. method:: PamHandle.Message(msg_style,msg)
310 |
311 | Creates an instance of the :class:`Message` class.
312 | The arguments become the instance members of the same name.
313 | This class is used to represent the C API's ``struct pam_message`` type.
314 | An instance has two members corresponding
315 | to the C structure members of the same name:
316 | :attr:`msg_style` an :class:`int`
317 | and :attr:`data` a :class:`string`.
318 | Instances are immutable.
319 | Instances of this class can be passed to the :meth:`conversation` method.
320 |
321 |
322 | .. method:: PamHandle.Response(resp,ret_code)
323 |
324 | Creates an instance of the :class:`Response` class.
325 | The arguments become the instance members of the same name.
326 | This class is used to represent the C API's ``struct pam_response`` type.
327 | An instance has two members
328 | corresponding to the C structure members of the same name:
329 | :attr:`resp` a :class:`string`
330 | and :attr:`ret_code` an :class:`int`.
331 | Instances are immutable.
332 | Instances of this class are returned by the :meth:`conversation` method.
333 |
334 |
335 | .. method:: PamHandle.XAuthData(name,data)
336 |
337 | Creates an instance of the :class:`XAuthData` class.
338 | The arguments become the instance members of the same name.
339 | This class is used to represent the C API's ``struct pam_xauth_data`` type.
340 | An instance has two members
341 | corresponding to the C structure members of the same name:
342 | :attr:`name` a :class:`string` and :attr:`data` also a :class:`string`.
343 | Instances are immutable.
344 | The :data:`xauthdata` member returns instances of this class and
345 | can be set to an instance of this class.
346 |
347 |
348 | .. method:: PamHandle.conversation(prompts)
349 |
350 | Calls the function defined by the PAM :c:macro:`PAM_CONV` item.
351 | The *prompts* argument is a :class:`Message` object
352 | or a :class:`list` of them.
353 | You don't have to pass an actual :class:`Message` object,
354 | any class that contains a :class:`string` member :attr:`msg`
355 | and a :class:`int` member :attr:`msg_style` will do.
356 | These members are used to initialise the ``struct pam_message``
357 | members of the same name. It returns either a single :class:`Response`
358 | object if a single :class:`Message` was passed,
359 | or a :class:`list` of them of the same length as the :class:`list` passed.
360 | These :class:`Response` objects contain the data the user entered.
361 |
362 |
363 | .. method:: PamHandle.fail_delay(delay)
364 |
365 | This results in a call to the |pam-lib-func| :samp:`pam_fail_delay()`,
366 | which sets the maximum random delay after an authentication failure
367 | to *delay* milliseconds.
368 |
369 |
370 | .. method:: PamHandle.get_user([prompt])
371 |
372 | This results in a call to the |pam-lib-func| :samp:`pam_get_user()`,
373 | which returns the current user name (a :class:`string`)
374 | or :const:`None` if :samp:`pam_get_user()` returns :c:macro:`NULL`.
375 | If not known it asks the PAM application for the user name,
376 | giving it the :class:`string` *prompt* parameter
377 | to prompt the user to enter it.
378 |
379 |
380 | .. method:: PamHandle.strerror(errnum)
381 |
382 | This results in a call to the |pam-lib-func| :samp:`pam_strerror()`,
383 | which returns a :class:`string` description of the :class:`int`
384 | PAM return value *errnum*.
385 |
386 | There is no interface provided for the |pam-lib-func|\s :samp:`pam_get_data()`
387 | and :samp:`pam_set_data()`. There are two reasons for this.
388 | Firstly those two methods are provided so C code can have private storage
389 | local to the PAM handle. A Python PAM Module can use own module name space
390 | to do the same job, and it's easier to do so. But more importantly it's
391 | safer because there is no type-safe way of providing access to the facility
392 | from Python.
393 |
394 |
395 | .. _diagnostics:
396 |
397 | Diagnostics, Debugging, Bugs
398 | ============================
399 |
400 | The way |pam_python| operates will be foreign to most Python programmers.
401 | It embeds Python into existing programs, primarily ones written in C.
402 | This means things like debugging and diagnostics
403 | are done differently to a normal Python program.
404 |
405 |
406 | .. _return-values:
407 |
408 | Diagnostics
409 | -----------
410 |
411 | If |pam_python| returns something other than :const:`PAM_SUCCESS` to PAM a
412 | message will be written to the ``syslog`` ``LOG_AUTHPRIV`` facility. The only
413 | exception to this is when |pam_python| is passing on the return value from
414 | a Python :meth:`pam_sm_...` entry point - nothing is logged in that case.
415 | So, if your Python PAM Module is failing in mysterious ways
416 | check the log file your system is configured to write
417 | ``LOG_AUTHPRIV`` entries to.
418 | Usually this is :file:`/var/log/syslog` or :file:`/var/log/auth.log`.
419 | The diagnostic or traceback Python would normally print to :attr:`sys.stderr`
420 | will be in there.
421 |
422 | The PAM result codes returned directly by |pam_python| are:
423 |
424 |
425 | .. data:: PAM_BUF_ERR
426 |
427 | Memory allocation failed.
428 |
429 |
430 | .. data:: PAM_MODULE_UNKNOWN
431 |
432 | The Python PAM module name wasn't supplied.
433 |
434 |
435 | .. data:: PAM_OPEN_ERR
436 |
437 | The Python PAM module could not be opened.
438 |
439 |
440 | .. data:: PAM_SERVICE_ERR
441 |
442 | A Python exception was thrown, unless it was because of a memory allocation
443 | failure.
444 |
445 |
446 | .. data:: PAM_SYMBOL_ERR
447 |
448 | A :meth:`pam_sm_...` called by PAM wasn't defined by the Python PAM module.
449 |
450 |
451 | .. _debugging:
452 |
453 | Debugging
454 | ---------
455 |
456 | If you have Python bindings for the PAM Application library then you can write
457 | test units in Python and use Pythons :mod:`pdb` module debug a Python PAM
458 | module. This is how |pam_python| was developed.
459 |
460 | I used `PyPAM `_ for the Python Application
461 | library bindings. Distributions often package it as ``python-pam``. To set
462 | breakpoints in :mod:`pdb` either wait until PAM has loaded your module, or
463 | :keyword:`import` it before you start debugging.
464 |
465 |
466 | .. _bugs:
467 |
468 | Bugs
469 | ----
470 |
471 | There are several design decisions you may stumble across when using
472 | |pam_python|. One is that the Python PAM module is isolated from the rest
473 | of the Python environment. This differs from a :keyword:`import`'ed Python module,
474 | where regardless of how many times a module is imported there is only one copy
475 | that shares the one global name space.
476 | For example, if you :keyword:`import` your Python PAM module
477 | and then debug it as suggested above then there will be 2
478 | copies of your Python PAM module in memory -
479 | the imported one and the one PAM is using.
480 | If the PAM module sets a global variable you won't see it in the
481 | :keyword:`import`'ed one. Indeed, obtaining any sort of handle to the module
482 | PAM is using is near impossible. This means the debugger can inspect variables
483 | in the module only when a breakpoint has one of the modules functions in its
484 | backtrace.
485 |
486 | There are a few of reasons for this. Firstly, the |PMWG| says
487 | this is the way it should be, so |pam_python| encourages it. Secondly, if a
488 | PAM application is using a Python PAM Module it's important the PAM module
489 | remains as near to invisible as possible to avoid conflicts. Finally, and most
490 | importantly, references to objects constructed by the Python PAM module must
491 | never leak. This is because the destructors to those objects are C functions
492 | that live in |pam_python|, and those destructors are called when all
493 | references to the objects are gone. When the application calls |pam-lib-func|
494 | :samp:`pam_end()` function |pam_python| is unloaded, and with it goes the
495 | destructor code. Should a reference to an object defined by |pam_python| exist
496 | after :samp:`pam_end()` returns the call to destructor
497 | will result in a jump to a non-existent address causing a ``SIGSEGV``.
498 |
499 | Another potential trap is the initialisation and finalisation of the Python
500 | interpreter itself. Calling the interpreter's finalisation routine while it is
501 | in use would I imagine be a big no-no. If |pam_python| has to initialise
502 | the interpreter (by calling :c:func:`Py_Initialize`) then it will call its
503 | finaliser :c:func:`Py_Finalize` when the last Python PAM module is destroyed.
504 | This is heuristic works in most scenarios. One example where is won't work is a
505 | sequence like::
506 |
507 | start-python-pam-module;
508 | application-initialises-interpreter;
509 | stop-python-pam-module;
510 | application-stops-interpreter.
511 |
512 | The above is doomed to fail.
513 |
514 |
515 | .. _example:
516 |
517 | An example
518 | ==========
519 |
520 | This is one of the examples provided by the package:
521 |
522 |
523 | .. include:: pam_permit.py
524 | :literal:
525 |
526 | Assuming it and ``pam_python.so`` are in the directory ``/lib/security`` adding
527 | these rules to ``/etc/pam.conf`` would run it::
528 |
529 | login account requisite pam_python.so pam_accept.py
530 | login auth requisite pam_python.so pam_accept.py
531 | login password requisite pam_python.so pam_accept.py
532 | login session requisite pam_python.so pam_accept.py
533 |
534 | .. |PMWG| replace:: PAM Module Writers Guide
535 |
536 | .. _PMWG: http://www.linux-pam.org/Linux-PAM-html/
537 |
538 | .. |pam_python| replace:: `pam_python`
539 |
540 | .. |pam-lib-func| replace:: PAM library function
541 |
--------------------------------------------------------------------------------
/examples/pam_deny.py:
--------------------------------------------------------------------------------
1 | #
2 | # Duplicates pam_deny.c
3 | #
4 | def pam_sm_authenticate(pamh, flags, argv):
5 | return pamh.PAM_AUTH_ERR
6 |
7 | def pam_sm_setcred(pamh, flags, argv):
8 | return pamh.PAM_CRED_UNAVAIL
9 |
10 | def pam_sm_acct_mgmt(pamh, flags, argv):
11 | return pamh.PAM_ACCT_EXPIRED
12 |
13 | def pam_sm_chauthtok(pamh, flags, argv):
14 | return pamh.PAM_AUTHTOK_ERR
15 |
16 | def pam_sm_open_session(pamh, flags, argv):
17 | return pamh.PAM_SYSTEM_ERR
18 |
19 | def pam_sm_close_session(pamh, flags, argv):
20 | return pamh.PAM_SYSTEM_ERR
21 |
--------------------------------------------------------------------------------
/examples/pam_nologin.py:
--------------------------------------------------------------------------------
1 | #
2 | # Emulate what pam_nologin.c does.
3 | #
4 | import pwd
5 |
6 | #
7 | # Parse our command line.
8 | #
9 | def parse_args(pamh, argv):
10 | #
11 | # Parse the arguments.
12 | #
13 | nologin_file = "/etc/nologin"
14 | retval_when_nofile = pamh.PAM_IGNORE
15 | for arg in argv[1:]:
16 | if arg.starts_with("file="):
17 | nologin_file = arg[5:]
18 | elif arg == "successok":
19 | retval_when_nofile = pamh.PAM_SUCCESS
20 | return nologin_file, retval_when_nofile
21 |
22 | #
23 | # Check the /etc/nologin file.
24 | #
25 | def check_nologin(pamh, nologin_file, retval_when_nofile):
26 | #
27 | # Get the user name.
28 | #
29 | try:
30 | username = pamh.get_user()
31 | except pamh.exception:
32 | username = None
33 | if username == None:
34 | return pamh.PAM_USER_UNKNOWN
35 | #
36 | # Can we open the file?
37 | #
38 | try:
39 | handle = file(nologin_file, "r")
40 | except EnvironmentError:
41 | return retval_when_nofile
42 | #
43 | # Print the message.
44 | #
45 | try:
46 | try:
47 | msg = handle.read()
48 | except EnvironmentError:
49 | return pamh.PAM_SYSTEM_ERR
50 | finally:
51 | handle.close()
52 | #
53 | # Read the user's password entry so we can check if he is root.
54 | # Root can login regardless.
55 | #
56 | try:
57 | pwent = pwd.getpwnam(username)
58 | except KeyError:
59 | retval = pamh.PAM_USER_UNKNOWN
60 | msg_style = pamh.PAM_ERROR_MSG
61 | else:
62 | if pwent[2] == 0: # Is this root?
63 | retval = pamh.PAM_SUCCESS
64 | msg_style = pamh.PAM_TEXT_INFO
65 | else:
66 | retval = pamh.PAM_AUTH_ERR
67 | msg_style = pamh.PAM_ERROR_MSG
68 | #
69 | # Display the message
70 | #
71 | try:
72 | pamh.conversation(pamh.Message(msg_style, msg))
73 | except pamh.exception:
74 | return pamh.PAM_SYSTEM_ERR
75 | return retval
76 |
77 | #
78 | # Entry points we handle.
79 | #
80 | def pam_sm_authenticate(pamh, flags, argv):
81 | nologin_file, retval_when_nofile = parse_args(pamh, argv)
82 | return check_nologin(pamh, nologin_file, retval_when_nofile)
83 |
84 | def pam_sm_setcred(pamh, flags, argv):
85 | nologin_file, retval_when_nofile = parse_args(pamh, argv)
86 | return retval_when_nofile
87 |
88 | def pam_sm_acct_mgmt(pamh, flags, argv):
89 | nologin_file, retval_when_nofile = parse_args(pamh, argv)
90 | return check_nologin(pamh, nologin_file, retval_when_nofile)
91 |
--------------------------------------------------------------------------------
/examples/pam_permit.py:
--------------------------------------------------------------------------------
1 | #
2 | # Duplicates pam_permit.c
3 | #
4 | DEFAULT_USER = "nobody"
5 |
6 | def pam_sm_authenticate(pamh, flags, argv):
7 | try:
8 | user = pamh.get_user(None)
9 | except pamh.exception, e:
10 | return e.pam_result
11 | if user == None:
12 | pamh.user = DEFAULT_USER
13 | return pamh.PAM_SUCCESS
14 |
15 | def pam_sm_setcred(pamh, flags, argv):
16 | return pamh.PAM_SUCCESS
17 |
18 | def pam_sm_acct_mgmt(pamh, flags, argv):
19 | return pamh.PAM_SUCCESS
20 |
21 | def pam_sm_open_session(pamh, flags, argv):
22 | return pamh.PAM_SUCCESS
23 |
24 | def pam_sm_close_session(pamh, flags, argv):
25 | return pamh.PAM_SUCCESS
26 |
27 | def pam_sm_chauthtok(pamh, flags, argv):
28 | return pamh.PAM_SUCCESS
29 |
--------------------------------------------------------------------------------
/pam-python.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 | pam-python - write PAM modules in Python
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Pam-python
24 | Write PAM modules in Python
25 |
26 |
27 |
28 | Pam-python is a PAM Module that runs the Python interpreter,
29 | thus allowing PAM Modules to be written in Python.
30 |
31 |
32 | Documentation
33 |
34 |
35 | There is a
36 | documentation page,
37 | some examples, a
38 | change log and a
39 | README.txt.
40 | The documentation page must be read in conjunction with the
41 | PAM Module Writers Guide.
42 |
43 |
44 | Copyright and License
45 |
46 |
47 | Pam-python is copyright © 2007-2012,2014,2016,2019 Russell Stuart.
48 | It is licensed under the GNU Affero General Public License.
49 |
50 |
51 |
52 | This program is free software: you can redistribute it and/or modify it
53 | under the terms of the GNU Affero General Public License as published by
54 | the Free Software Foundation, either version 3 of the License, or (at your
55 | option) any later version.
56 |
57 |
58 |
59 | The copyright holders grant you an additional permission under Section 7
60 | of the GNU Affero General Public License, version 3, exempting you from
61 | the requirement in Section 6 of the GNU General Public License, version 3,
62 | to accompany Corresponding Source with Installation Information for the
63 | Program or any work based on the Program. You are still required to
64 | comply with all other Section 6 requirements to provide Corresponding
65 | Source.
66 |
67 |
68 |
69 | This program is distributed in the hope that it will be useful,
70 | but WITHOUT ANY WARRANTY; without even the implied warranty of
71 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
72 | GNU Affero General Public License for more details.
73 |
74 |
75 | Downloading, Feedback & Contributing
76 |
77 |
78 | Development for pam-python is hosted on
79 | Source forge:
80 |
81 |
82 |
83 | -
84 | Download area,
85 | (.tar.gz, .deb).
86 |
87 | -
88 | Issue tracker,
89 | bugs, features or just questions.
90 |
91 | -
92 | Source repository.
93 |
94 | -
95 | Pam-python is part of Debian.
96 | Most Debian derived distribution can install using apt-get.
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Russell Stuart, 2014-May-29.
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/pam-python.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Makefile:
--------------------------------------------------------------------------------
1 | all: ctest pam_python.so test-pam_python.pam
2 |
3 | WARNINGS=-Wall -Wextra -Wundef -Wshadow -Wpointer-arith -Wbad-function-cast -Wsign-compare -Waggregate-return -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -Werror
4 | #WARNINGS=-Wunreachable-code # Gcc 4.1 .. 4.4 are too buggy to make this useful
5 |
6 | LIBDIR ?= /lib/security
7 |
8 | pam_python.so: pam_python.c setup.py Makefile
9 | @rm -f "$@"
10 | @[ ! -e build -o build/lib.*/$@ -nt setup.py -a build/lib.*/$@ -nt Makefile ] || rm -r build
11 | CFLAGS="$(WARNINGS) -I/usr/local/lib/ " ./setup.py build
12 | @#CFLAGS="-O0 $(WARNINGS)" ./setup.py build --debug
13 | @#CFLAGS="-O0 $(WARNINGS)" Py_DEBUG=1 ./setup.py build --debug
14 | ln -sf build/lib.*/$@ .
15 |
16 | .PHONY: install install-lib
17 | install: install-lib
18 | install-lib:
19 | mkdir -p $(DESTDIR)$(LIBDIR)
20 | cp build/lib.*/pam_python.so $(DESTDIR)$(LIBDIR)
21 |
22 | .PHONY: clean
23 | clean:
24 | rm -rf build ctest pam_python.so test-pam_python.pam test.pyc core
25 | [ ! -e /etc/pam.d/test-pam_python.pam ] || { s=$$([ $$(id -u) = 0 ] || echo sudo); $$s rm -f /etc/pam.d/test-pam_python.pam; }
26 | [ ! -e /etc/pam.d/test-pam_python-installed.pam ] || { s=$$([ $$(id -u) = 0 ] || echo sudo); $$s rm -f /etc/pam.d/test-pam_python-installed.pam; }
27 |
28 | .PHONY: ctest
29 | ctest: ctest.c Makefile
30 | gcc -O0 $(WARNINGS) -g -o $@ ctest.c -lpam
31 |
32 | test-pam_python.pam: test-pam_python.pam.in Makefile
33 | sed "s,\\\$$PWD,$$(pwd),g" "$@.in" >"$@.tmp"
34 | mv $@.tmp $@
35 |
36 | /etc/pam.d/test-pam_python.pam: test-pam_python.pam
37 | s=$$([ $$(id -u) = 0 ] || echo sudo); $$s ln -sf $$(pwd)/test-pam_python.pam /etc/pam.d
38 |
39 | .PHONY: test
40 | test: pam_python.so ctest /etc/pam.d/test-pam_python.pam
41 | python test.py
42 | ./ctest
43 |
44 | test-pam_python-installed.pam: test-pam_python.pam.in Makefile
45 | sed "s,\\\$$PWD/pam-python.so,pam-python.so,;s,\\\$$PWD,$$(pwd),g" "$@.in" >"$@.tmp"
46 | mv $@.tmp $@
47 |
48 | /etc/pam.d/test-pam_python-installed.pam: test-pam_python-installed.pam
49 | s=$$([ $$(id -u) = 0 ] || echo sudo); $$s ln -sf $$(pwd)/test-pam_python-installed.pam /etc/pam.d
50 |
51 | .PHONY: installed-test
52 | installed-test: ctest /etc/pam.d/test-pam_python-installed.pam
53 | python test.py
54 | ./ctest
55 |
--------------------------------------------------------------------------------
/src/ctest.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Best compiled & run using the Makefile target "test". To compile and run
3 | * manually:
4 | * gcc -O0 -g -Wall -o test -lpam test.c
5 | * sudo ln -s $PWD/test-pam_python.pam /etc/pam.d
6 | * ./ctest
7 | * sudo rm /etc/pam.d/test-pam_python.pam
8 | */
9 | #define _GNU_SOURCE
10 |
11 | #ifdef __APPLE__
12 | #include
13 | #else
14 | #include
15 | #endif
16 | #include
17 | #include
18 | #include
19 | #include
20 | #include
21 |
22 | struct walk_info {
23 | int libpam_python_seen;
24 | int python_seen;
25 | };
26 |
27 | static int conv(
28 | int num_msg, const struct pam_message** msg, struct pam_response** resp, void *appdata_ptr)
29 | {
30 | int i;
31 |
32 | (void)appdata_ptr;
33 | *resp = malloc(num_msg * sizeof(**resp));
34 | for (i = 0; i < num_msg; i += 1)
35 | {
36 | (*resp)[i].resp = strdup((*msg)[i].msg);
37 | (*resp)[i].resp_retcode = (*msg)[i].msg_style;
38 | }
39 | return 0;
40 | }
41 |
42 | static void call_pam(
43 | int* exit_status, const char* who, pam_handle_t* pamh,
44 | int (*func)(pam_handle_t*, int))
45 | {
46 | int pam_result = (*func)(pamh, 0);
47 |
48 | if (pam_result == PAM_SUCCESS)
49 | return;
50 | fprintf(
51 | stderr, "%s failed: %d %s\n",
52 | who, pam_result, pam_strerror(pamh, pam_result));
53 | *exit_status = 1;
54 | }
55 |
56 | #ifdef __APPLE__
57 | static void walk_dlls(struct walk_info* walk_info)
58 | {
59 | int image_index;
60 | walk_info->libpam_python_seen = 0;
61 | walk_info->python_seen = 0;
62 | for (image_index = 0; image_index < _dyld_image_count(); image_index += 1) {
63 | const char* image_name = _dyld_get_image_name(image_index);
64 | if (strstr(image_name, "/pam_python.so") != 0)
65 | walk_info->libpam_python_seen = 1;
66 | if (strstr(image_name, "/libpython") != 0)
67 | walk_info->python_seen = 1;
68 | }
69 | }
70 | #else
71 | static int dl_walk(struct dl_phdr_info* info, size_t size, void* data)
72 | {
73 | struct walk_info* walk_info = data;
74 |
75 | (void)size;
76 | if (strstr(info->dlpi_name, "/pam_python.so") != 0)
77 | walk_info->libpam_python_seen = 1;
78 | if (strstr(info->dlpi_name, "/libpython") != 0)
79 | walk_info->python_seen = 1;
80 | return 0;
81 | }
82 |
83 | static void walk_dlls(struct walk_info* walk_info)
84 | {
85 | walk_info->libpam_python_seen = 0;
86 | walk_info->python_seen = 0;
87 | dl_iterate_phdr(dl_walk, walk_info);
88 | }
89 | #endif
90 |
91 | int main(int argc, char **argv)
92 | {
93 | int exit_status;
94 | struct pam_conv convstruct;
95 | pam_handle_t* pamh;
96 | struct walk_info walk_info_before;
97 | struct walk_info walk_info_after;
98 |
99 | (void)argc;
100 | (void)argv;
101 | if (access("/etc/pam.d/test-pam_python.pam", 0) != 0)
102 | {
103 | fprintf(
104 | stderr,
105 | "**WARNING**\n"
106 | " This test requires ./test-pam_python.pam configuration to be\n"
107 | " available to PAM But it doesn't appear to be in /etc/pam.d.\n"
108 | );
109 | }
110 | printf("Testing calls from C");
111 | fflush(stdout);
112 | convstruct.conv = conv;
113 | convstruct.appdata_ptr = 0;
114 | if (pam_start("test-pam_python.pam", "", &convstruct, &pamh) == -1)
115 | {
116 | fprintf(stderr, "pam_start failed\n");
117 | exit(1);
118 | }
119 | exit_status = 0;
120 | call_pam(&exit_status, "pam_authenticate", pamh, pam_authenticate);
121 | call_pam(&exit_status, "pam_chauthtok", pamh, pam_chauthtok);
122 | call_pam(&exit_status, "pam_acct_mgmt", pamh, pam_acct_mgmt);
123 | call_pam(&exit_status, "pam_open_session", pamh, pam_open_session);
124 | call_pam(&exit_status, "pam_close_session", pamh, pam_close_session);
125 | walk_dlls(&walk_info_before);
126 | call_pam(&exit_status, "pam_end", pamh, pam_end);
127 | if (exit_status == 0)
128 | printf(" OK\n");
129 | walk_dlls(&walk_info_after);
130 | printf("Testing dll load/unload ");
131 | if (!walk_info_before.libpam_python_seen)
132 | {
133 | fprintf(stderr, "It looks like pam_python.so wasn't loaded!\n");
134 | exit_status = 1;
135 | }
136 | else if (!walk_info_before.python_seen)
137 | {
138 | fprintf(stderr, "It looks like libpythonX.Y.so wasn't loaded!\n");
139 | exit_status = 1;
140 | }
141 | else if (walk_info_after.libpam_python_seen)
142 | {
143 | fprintf(stderr, "pam_python.so wasn't unloaded.\n");
144 | exit_status = 1;
145 | }
146 | else if (walk_info_after.python_seen)
147 | {
148 | fprintf(stderr, "libpythonX.Y.so wasn't uloaded.\n");
149 | exit_status = 1;
150 | }
151 | else
152 | printf("OK\n");
153 | return exit_status;
154 | }
155 |
--------------------------------------------------------------------------------
/src/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python -W default
2 | import warnings; warnings.simplefilter('default')
3 |
4 | import distutils.sysconfig
5 | import os
6 | import sys
7 |
8 | try:
9 | from setuptools import setup, Extension
10 | except ImportError:
11 | from distutils.core import setup, Extension
12 |
13 | long_description = """\
14 | Embeds the Python interpreter into PAM \
15 | so PAM modules can be written in Python"""
16 |
17 | classifiers = [
18 | "Development Status :: 4 - Beta",
19 | "Intended Audience :: Developers",
20 | "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
21 | "Natural Language :: English",
22 | "Operating System :: Unix",
23 | "Programming Language :: C",
24 | "Programming Language :: Python",
25 | "Topic :: Software Development :: Libraries :: Python Modules",
26 | "Topic :: System :: Systems Administration :: Authentication/Directory"]
27 |
28 | if not os.environ.has_key("Py_DEBUG"):
29 | Py_DEBUG = []
30 | else:
31 | Py_DEBUG = [('Py_DEBUG',1)]
32 |
33 | libpython_so = distutils.sysconfig.get_config_var('INSTSONAME')
34 | ext_modules = [
35 | Extension(
36 | "pam_python",
37 | sources=["pam_python.c"],
38 | include_dirs = [],
39 | library_dirs=[],
40 | define_macros=[('LIBPYTHON_SO','"'+libpython_so+'"')] + Py_DEBUG,
41 | libraries=["pam","python%d.%d" % sys.version_info[:2]],
42 | ), ]
43 |
44 | setup(
45 | name="pam_python",
46 | version="1.0.7",
47 | description="Enabled PAM Modules to be written in Python",
48 | keywords="pam,embed,authentication,security",
49 | platforms="Unix",
50 | long_description=long_description,
51 | author="Russell Stuart",
52 | author_email="russell-pampython@stuart.id.au",
53 | url="http://pam-python.sourceforge.net/",
54 | license="AGPL-3.0",
55 | classifiers=classifiers,
56 | ext_modules=ext_modules,
57 | )
58 |
--------------------------------------------------------------------------------
/src/test-pam_python.pam.in:
--------------------------------------------------------------------------------
1 | auth required $PWD/pam_python.so $PWD/test.py
2 | account required $PWD/pam_python.so $PWD/test.py arg1 arg2
3 | password required $PWD/pam_python.so $PWD/test.py
4 | session required $PWD/pam_python.so $PWD/test.py
5 |
--------------------------------------------------------------------------------
/src/test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python -W default
2 | #
3 | # This is the test script for libpython-pam. There aren't many stones
4 | # left unturned.
5 | #
6 | # Best run from the Makefile using the target 'test'. To run manually:
7 | # sudo ln -s $PWD/test-pam_python.pam /etc/pam.d
8 | # python test.py
9 | # sudo rm /etc/pam.d/test-pam_python.pam
10 | #
11 | import warnings; warnings.simplefilter('default')
12 | import os
13 | import sys
14 |
15 | TEST_PAM_MODULE = "test-pam_python.pam"
16 | TEST_PAM_USER = "root"
17 |
18 | #
19 | # A Fairly straight forward test harness.
20 | #
21 | def pam_sm_end(pamh):
22 | return test(pam_sm_end, pamh, None, None)
23 | def pam_sm_authenticate(pamh, flags, argv):
24 | return test(pam_sm_authenticate, pamh, flags, argv)
25 | def pam_sm_setcred(pamh, flags, argv):
26 | return test(pam_sm_setcred, pamh, flags, argv)
27 | def pam_sm_acct_mgmt(pamh, flags, argv):
28 | return test(pam_sm_acct_mgmt, pamh, flags, argv)
29 | def pam_sm_open_session(pamh, flags, argv):
30 | return test(pam_sm_open_session, pamh, flags, argv)
31 | def pam_sm_close_session(pamh, flags, argv):
32 | return test(pam_sm_close_session, pamh, flags, argv)
33 | def pam_sm_chauthtok(pamh, flags, argv):
34 | return test(pam_sm_chauthtok, pamh, flags, argv)
35 |
36 | def test(who, pamh, flags, argv):
37 | import test
38 | if not hasattr(test, "test_function"):# only true if not called via "main"
39 | return pamh.PAM_SUCCESS # normally happens only if run by ctest
40 | test_function = globals()[test.test_function.__name__]
41 | return test_function(test.test_results, who, pamh, flags, argv)
42 |
43 | def run_test(caller):
44 | import test
45 | test_name = caller.__name__[4:]
46 | sys.stdout.write("Testing " + test_name + " ")
47 | sys.stdout.flush()
48 | test.test_results = []
49 | test.test_function = globals()["test_" + test_name]
50 | caller(test.test_results)
51 | sys.stdout.write("OK\n")
52 |
53 | def pam_conv(auth, query_list, userData=None):
54 | return query_list
55 |
56 | #
57 | # Verify the results match.
58 | #
59 | def assert_results(expected_results, results):
60 | for i in range(min(len(expected_results), len(results))):
61 | assert expected_results[i] == results[i], (i, expected_results[i], results[i])
62 | if len(expected_results) < len(results):
63 | assert len(expected_results) == len(results), (i, results[len(expected_results)])
64 | else:
65 | assert len(expected_results) == len(results), (i, expected_results[len(results)])
66 |
67 | #
68 | # Test all the calls happen.
69 | #
70 | def test_basic_calls(results, who, pamh, flags, argv):
71 | results.append((who.func_name, flags, argv))
72 | return pamh.PAM_SUCCESS
73 |
74 | def run_basic_calls(results):
75 | pam = PAM.pam()
76 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
77 | pam.authenticate(0)
78 | pam.acct_mgmt()
79 | pam.chauthtok()
80 | pam.open_session()
81 | pam.close_session()
82 | del pam
83 | me = os.path.join(os.getcwd(), __file__)
84 | expected_results = [
85 | (pam_sm_authenticate.func_name, 0, [me]),
86 | (pam_sm_acct_mgmt.func_name, 0, [me, 'arg1', 'arg2']),
87 | (pam_sm_chauthtok.func_name, 16384, [me]),
88 | (pam_sm_chauthtok.func_name, 8192, [me]),
89 | (pam_sm_open_session.func_name, 0, [me]),
90 | (pam_sm_close_session.func_name, 0, [me]),
91 | (pam_sm_end.func_name, None, None)]
92 | assert_results(expected_results, results)
93 |
94 | #
95 | # Test all the constants are defined.
96 | #
97 | PAM_CONSTANTS = {
98 | #
99 | # Constants defined in _pam_types.h. The item constants are omitted.
100 | #
101 | "PAM_SUCCESS": 0,
102 | "PAM_OPEN_ERR": 1,
103 | "PAM_SYMBOL_ERR": 2,
104 | "PAM_SERVICE_ERR": 3,
105 | "PAM_SYSTEM_ERR": 4,
106 | "PAM_BUF_ERR": 5,
107 | "PAM_PERM_DENIED": 6,
108 | "PAM_AUTH_ERR": 7,
109 | "PAM_CRED_INSUFFICIENT": 8,
110 | "PAM_AUTHINFO_UNAVAIL": 9,
111 | "PAM_USER_UNKNOWN": 10,
112 | "PAM_MAXTRIES": 11,
113 | "PAM_NEW_AUTHTOK_REQD": 12,
114 | "PAM_ACCT_EXPIRED": 13,
115 | "PAM_SESSION_ERR": 14,
116 | "PAM_CRED_UNAVAIL": 15,
117 | "PAM_CRED_EXPIRED": 16,
118 | "PAM_CRED_ERR": 17,
119 | "PAM_NO_MODULE_DATA": 18,
120 | "PAM_CONV_ERR": 19,
121 | "PAM_AUTHTOK_ERR": 20,
122 | "PAM_AUTHTOK_RECOVER_ERR": 21,
123 | "PAM_AUTHTOK_RECOVERY_ERR": 21,
124 | "PAM_AUTHTOK_LOCK_BUSY": 22,
125 | "PAM_AUTHTOK_DISABLE_AGING": 23,
126 | "PAM_TRY_AGAIN": 24,
127 | "PAM_IGNORE": 25,
128 | "PAM_ABORT": 26,
129 | "PAM_AUTHTOK_EXPIRED": 27,
130 | "PAM_MODULE_UNKNOWN": 28,
131 | "PAM_BAD_ITEM": 29,
132 | "PAM_CONV_AGAIN": 30,
133 | "PAM_INCOMPLETE": 31,
134 | "PAM_SERVICE": 1,
135 | "PAM_USER": 2,
136 | "PAM_TTY": 3,
137 | "PAM_RHOST": 4,
138 | "PAM_CONV": 5,
139 | "PAM_AUTHTOK": 6,
140 | "PAM_OLDAUTHTOK": 7,
141 | "PAM_RUSER": 8,
142 | "PAM_USER_PROMPT": 9,
143 | "PAM_FAIL_DELAY": 10,
144 | "PAM_XDISPLAY": 11,
145 | "PAM_XAUTHDATA": 12,
146 | "PAM_AUTHTOK_TYPE": 13,
147 | "PAM_SILENT": 0x8000,
148 | "PAM_DISALLOW_NULL_AUTHTOK": 0x0001,
149 | "PAM_ESTABLISH_CRED": 0x0002,
150 | "PAM_DELETE_CRED": 0x0004,
151 | "PAM_REINITIALIZE_CRED": 0x0008,
152 | "PAM_REFRESH_CRED": 0x0010,
153 | "PAM_CHANGE_EXPIRED_AUTHTOK": 0x0020,
154 | "PAM_DATA_SILENT": 0x40000000,
155 | "PAM_PROMPT_ECHO_OFF": 1,
156 | "PAM_PROMPT_ECHO_ON": 2,
157 | "PAM_ERROR_MSG": 3,
158 | "PAM_TEXT_INFO": 4,
159 | "PAM_RADIO_TYPE": 5,
160 | "PAM_BINARY_PROMPT": 7,
161 | "PAM_MAX_NUM_MSG": 32,
162 | "PAM_MAX_MSG_SIZE": 512,
163 | "PAM_MAX_RESP_SIZE": 512,
164 | "_PAM_RETURN_VALUES": 32,
165 | #
166 | # Constants defined in pam_modules.h. The item constants are omitted.
167 | #
168 | "PAM_PRELIM_CHECK": 0x4000,
169 | "PAM_UPDATE_AUTHTOK": 0x2000,
170 | "PAM_DATA_REPLACE": 0x20000000,
171 | }
172 | def test_constants(results, who, pamh, flags, argv):
173 | results.append(who.func_name)
174 | if who != pam_sm_authenticate:
175 | return pamh.PAM_SUCCESS
176 | pam_constants = dict([
177 | (var, getattr(pamh,var))
178 | for var in dir(pamh)
179 | if var.startswith("PAM_") or var.startswith("_PAM_")])
180 | results.append(pam_constants)
181 | try:
182 | pamh.PAM_SUCCESS = 1
183 | results.append("Opps, pamh.PAM_SUCCESS = 1 worked!")
184 | except StandardError, e:
185 | results.append("except: %s" % e)
186 | return pamh.PAM_SUCCESS
187 |
188 | def run_constants(results):
189 | pam = PAM.pam()
190 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
191 | pam.authenticate(0)
192 | pam.close_session()
193 | del pam
194 | assert results[0] == pam_sm_authenticate.func_name, (results[0], pam_sm_authenticate.func_name)
195 | assert results[2] == "except: attribute 'PAM_SUCCESS' of 'PamHandle_type' objects is not writable", results[2]
196 | assert results[3] == pam_sm_close_session.func_name, (results[3], pam_sm_close_session.func_name)
197 | assert results[4] == pam_sm_end.func_name, (results[4], pam_sm_end.func_name)
198 | consts = results[1]
199 | for var in PAM_CONSTANTS.keys():
200 | assert consts.has_key(var), var
201 | assert consts[var] == PAM_CONSTANTS[var], (var, consts[var], PAM_CONSTANTS[var])
202 | for var in consts.keys():
203 | assert PAM_CONSTANTS.has_key(var), var
204 | assert PAM_CONSTANTS[var] == consts[var], (var, PAM_CONSTANTS[var], consts[var])
205 | assert len(results) == 5, len(results)
206 |
207 | #
208 | # Test the environment calls.
209 | #
210 | def test_environment(results, who, pamh, flags, argv):
211 | results.append(who.func_name)
212 | if who != pam_sm_acct_mgmt:
213 | return pamh.PAM_SUCCESS
214 | def test_exception(func):
215 | try:
216 | func()
217 | return str(None)
218 | except Exception, e:
219 | return e.__class__.__name__ + ": " + str(e)
220 | #
221 | # A few things to test here. First that PamEnv_as_mapping works.
222 | #
223 | results.append(len(pamh.env))
224 | results.append(pamh.env["x1"])
225 | pamh.env["yy"] = "y"
226 | results.append(pamh.env["yy"])
227 | pamh.env["yy"] = "z"
228 | results.append(pamh.env["yy"])
229 | def t(): pamh.env["yy"] = 1
230 | results.append(test_exception(t))
231 | del pamh.env["yy"]
232 | results.append(test_exception(lambda: pamh.env["yy"]))
233 | results.append(test_exception(lambda: pamh.env[1]))
234 | results.append(test_exception(lambda: pamh.env['a=']))
235 | results.append(test_exception(lambda: pamh.env['']))
236 | #
237 | # Now the dict functions.
238 | #
239 | pamh.env["xx"] = "x"
240 | results.append("not in" in pamh.env)
241 | results.append("xx" in pamh.env)
242 | results.append(pamh.env.has_key("not in"))
243 | results.append(pamh.env.has_key("xx"))
244 | results.append(test_exception(lambda: pamh.env.__getitem__("not in")))
245 | results.append(pamh.env.get("not in"))
246 | results.append(pamh.env.get("not in", "default"))
247 | results.append(pamh.env.get("xx"))
248 | results.append(pamh.env.get("xx", "default"))
249 | del pamh.env["x1"]
250 | results.append(pamh.env.items())
251 | results.append(pamh.env.keys())
252 | results.append(pamh.env.values())
253 | return pamh.PAM_SUCCESS
254 |
255 | def run_environment(results):
256 | pam = PAM.pam()
257 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
258 | pam.authenticate(0)
259 | pam.putenv("x1=1")
260 | pam.putenv("x2=2")
261 | pam.putenv("x3=3")
262 | pam.acct_mgmt()
263 | pam.close_session()
264 | del pam
265 | expected_results = [
266 | pam_sm_authenticate.func_name, pam_sm_acct_mgmt.func_name,
267 | 3, '1', 'y', 'z',
268 | 'TypeError: PAM environment value must be a string',
269 | "KeyError: 'yy'",
270 | 'TypeError: PAM environment key must be a string',
271 | "ValueError: PAM environment key can't contain '='",
272 | "ValueError: PAM environment key mustn't be 0 length",
273 | False, True, False, True,
274 | "KeyError: 'not in'",
275 | None, 'default', 'x', 'x',
276 | [('x2', '2'), ('x3', '3'), ('xx', 'x')],
277 | ['x2', 'x3', 'xx'],
278 | ['2', '3', 'x'],
279 | pam_sm_close_session.func_name, pam_sm_end.func_name]
280 | assert_results(expected_results, results)
281 |
282 | #
283 | # Test strerror().
284 | #
285 | def test_strerror(results, who, pamh, flags, argv):
286 | results.append(who.func_name)
287 | if who != pam_sm_authenticate:
288 | return pamh.PAM_SUCCESS
289 | results.extend([(e, pamh.strerror(e).lower()) for e in (0, 1, 30, 31)])
290 | return pamh.PAM_SUCCESS
291 |
292 | def run_strerror(results):
293 | pam = PAM.pam()
294 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
295 | pam.authenticate(0)
296 | del pam
297 | expected_results = [
298 | pam_sm_authenticate.func_name,
299 | ( 0, 'success'),
300 | ( 1, 'failed to load module'),
301 | (30, 'conversation is waiting for event'),
302 | (31, 'application needs to call libpam again'),
303 | pam_sm_end.func_name]
304 | assert_results(expected_results, results)
305 |
306 | #
307 | # Test items.
308 | #
309 | def test_items(results, who, pamh, flags, argv):
310 | results.append(who.func_name)
311 | if not who in (pam_sm_open_session, pam_sm_close_session):
312 | return pamh.PAM_SUCCESS
313 | items = {
314 | "authtok": "authtok-module",
315 | "authtok_type": "authtok_type-module",
316 | "oldauthtok": "oldauthtok-module",
317 | "rhost": "rhost-module",
318 | "ruser": "ruser-module",
319 | "tty": "tty-module",
320 | "user_prompt": "user_prompt-module",
321 | "user": "user-module",
322 | "xdisplay": "xdisplay-module",
323 | }
324 | keys = items.keys()
325 | keys.sort()
326 | for key in keys:
327 | results.append((key, getattr(pamh, key)))
328 | value = items[key]
329 | if value != None:
330 | setattr(pamh, key, value)
331 | try:
332 | setattr(pamh, "tty", 1)
333 | results.append("%r = %r" % (key, value))
334 | except StandardError, e:
335 | results.append("except: %s" % e)
336 | results.append(pamh.get_user("a prompt"))
337 | return pamh.PAM_SUCCESS
338 |
339 | def run_items(results):
340 | pam = PAM.pam()
341 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
342 | pam.authenticate(0)
343 | items = {
344 | 2: "user",
345 | 3: "tty",
346 | 4: "rhost",
347 | 8: "ruser",
348 | 9: "user_prompt",
349 | 11: "xdisplay",
350 | 13: "authtok_type"}
351 | items_list = items.keys()
352 | items_list.sort()
353 | for item in items_list:
354 | pam.set_item(item, items[item])
355 | pam.open_session()
356 | pam.close_session()
357 | del pam
358 | expected_results = [
359 | pam_sm_authenticate.func_name, pam_sm_open_session.func_name,
360 | ('authtok', None),
361 | ('authtok_type', 'authtok_type'),
362 | ('oldauthtok', None),
363 | ('rhost', 'rhost'),
364 | ('ruser', 'ruser'),
365 | ('tty', 'tty'),
366 | ('user', 'user'),
367 | ('user_prompt', 'user_prompt'),
368 | ('xdisplay', 'xdisplay'),
369 | 'except: PAM item PAM_TTY must be set to a string',
370 | 'user-module',
371 | pam_sm_close_session.func_name,
372 | ('authtok', 'authtok-module'),
373 | ('authtok_type', 'authtok_type-module'),
374 | ('oldauthtok', 'oldauthtok-module'),
375 | ('rhost', 'rhost-module'),
376 | ('ruser', 'ruser-module'),
377 | ('tty', 'tty-module'),
378 | ('user', 'user-module'),
379 | ('user_prompt', 'user_prompt-module'),
380 | ('xdisplay', 'xdisplay-module'),
381 | 'except: PAM item PAM_TTY must be set to a string',
382 | 'user-module',
383 | pam_sm_end.func_name]
384 | assert_results(expected_results, results)
385 |
386 | #
387 | # Test the xauthdata item.
388 | #
389 | def test_xauthdata(results, who, pamh, flags, argv):
390 | results.append(who.func_name)
391 | if not who in (pam_sm_open_session, pam_sm_close_session):
392 | return pamh.PAM_SUCCESS
393 | xauthdata0 = pamh.XAuthData("name-module", "data-module")
394 | pamh.xauthdata = xauthdata0
395 | xauthdata1 = pamh.xauthdata
396 | results.append('name=%r, data=%r' % (xauthdata1.name, xauthdata1.data))
397 | try:
398 | xauthdata2 = pamh.XAuthData(None, "x")
399 | results.append('pamh.XAuthData(%r, %r)' % (xauthdata2.name, xauthdata2.data))
400 | except TypeError, e:
401 | results.append('except: %s' % e)
402 | try:
403 | xauthdata2 = pamh.XAuthData("x", 1)
404 | results.append('pamh.XAuthData(%r, %r)' % (xauthdata2.name, xauthdata2.data))
405 | except TypeError, e:
406 | results.append('except: %s' % e)
407 | class XA: pass
408 | XA.name = "name-XA"
409 | XA.data = "data-XA"
410 | pamh.xauthdata = XA
411 | xauthdata2 = pamh.xauthdata
412 | results.append('name=%r, data=%r' % (xauthdata2.name, xauthdata2.data))
413 | xa = XA()
414 | xa.name = "name-xa"
415 | xa.data = "data-xa"
416 | pamh.xauthdata = xa
417 | xauthdata4 = pamh.xauthdata
418 | results.append('name=%r, data=%r' % (xauthdata4.name, xauthdata4.data))
419 | return pamh.PAM_SUCCESS
420 |
421 | def run_xauthdata(results):
422 | pam = PAM.pam()
423 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
424 | pam.authenticate(0)
425 | #
426 | # The PAM module doesn't support XAUTHDATA, so check what we can from the
427 | # module only.
428 | #
429 | pam.open_session()
430 | pam.close_session()
431 | del pam
432 | expected_results = [
433 | pam_sm_authenticate.func_name, pam_sm_open_session.func_name,
434 | ("name='name-module', data='data-module'"),
435 | 'except: XAuthData() argument 1 must be string, not None',
436 | 'except: XAuthData() argument 2 must be string, not int',
437 | ("name='name-XA', data='data-XA'"),
438 | ("name='name-xa', data='data-xa'"),
439 | pam_sm_close_session.func_name,
440 | ("name='name-module', data='data-module'"),
441 | 'except: XAuthData() argument 1 must be string, not None',
442 | 'except: XAuthData() argument 2 must be string, not int',
443 | ("name='name-XA', data='data-XA'"),
444 | ("name='name-xa', data='data-xa'"),
445 | pam_sm_end.func_name]
446 | assert_results(expected_results, results)
447 |
448 | #
449 | # Test having no pam_sm_end.
450 | #
451 | def test_no_sm_end(results, who, pamh, flags, argv):
452 | results.append(who.func_name)
453 | global pam_sm_end
454 | del pam_sm_end
455 | return pamh.PAM_SUCCESS
456 |
457 | def run_no_sm_end(results):
458 | pam = PAM.pam()
459 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
460 | pam.authenticate(0)
461 | del pam
462 | expected_results = [pam_sm_authenticate.func_name]
463 | assert_results(expected_results, results)
464 |
465 | #
466 | # Test the conversation mechanism.
467 | #
468 | def test_conv(results, who, pamh, flags, argv):
469 | results.append(who.func_name)
470 | if who == pam_sm_end:
471 | return
472 | #
473 | # We must get rid of all references to pamh.Response objects. This instance
474 | # of the test.py module is running inside of libpam_python. That shared
475 | # library will be unloaded soon. Should a pamh.Response instance be
476 | # dealloc'ed after it is unloaded the now non-existant dealloc function will
477 | # be called, and a SIGSEGV will result. Normally instances would not leak,
478 | # but with the trickery we are performing with fake import's here they will
479 | # leak via the results variable unless we take special action.
480 | #
481 | def conv(convs):
482 | responses = pamh.conversation(convs)
483 | if type(responses) != type(()):
484 | return (responses.resp, responses.resp_retcode)
485 | return [(r.resp, r.resp_retcode) for r in responses]
486 | if who == pam_sm_authenticate:
487 | convs = [
488 | pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Prompt_echo_off"),
489 | pamh.Message(pamh.PAM_PROMPT_ECHO_ON, "Prompt_echo_on"),
490 | pamh.Message(pamh.PAM_ERROR_MSG, "Error_msg"),
491 | pamh.Message(pamh.PAM_TEXT_INFO, "Text_info")]
492 | if who == pam_sm_acct_mgmt:
493 | convs = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "single")
494 | results.append(conv(convs))
495 | return pamh.PAM_SUCCESS
496 |
497 | def run_conv(results):
498 | pam = PAM.pam()
499 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
500 | pam.authenticate(0)
501 | pam.acct_mgmt()
502 | del pam
503 | expected_results = [
504 | pam_sm_authenticate.func_name,
505 | [('Prompt_echo_off', 1), ('Prompt_echo_on', 2), ('Error_msg', 3), ('Text_info', 4)],
506 | pam_sm_acct_mgmt.func_name,
507 | ('single', 1),
508 | pam_sm_end.func_name]
509 | assert_results(expected_results, results)
510 |
511 | #
512 | # Test pam error returns.
513 | #
514 | def test_pamerr(results, who, pamh, flags, argv):
515 | return results[-1]
516 |
517 | def run_pamerr(results):
518 | pam = PAM.pam()
519 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
520 | for err in range(0, PAM._PAM_RETURN_VALUES):
521 | results.append(err)
522 | try:
523 | pam.authenticate(0)
524 | except PAM.error, e:
525 | results[-1] = -e.args[1]
526 | del pam
527 | expected_results = [-r for r in range(PAM._PAM_RETURN_VALUES)]
528 | expected_results[25] = -6
529 | assert_results(expected_results, results)
530 |
531 | #
532 | # Test fail_delay.
533 | #
534 | def test_fail_delay(results, who, pamh, flags, argv):
535 | pamh.fail_delay(10)
536 | return pamh.PAM_SUCCESS
537 |
538 | def run_fail_delay(results):
539 | pam = PAM.pam()
540 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
541 | pam.authenticate(0)
542 | del pam
543 |
544 | #
545 | # Test raising an exception.
546 | #
547 | def test_exceptions(results, who, pamh, flags, argv):
548 | if who != pam_sm_end:
549 | return pamh.PAM_SUCCESS
550 | #
551 | # Here we have use of a backdoor put into pam_python.c specifically
552 | # for testing raising exceptions. Oddly, normally PAM should never
553 | # return anything other than PAM_SUCCESS to anything pam_python.c
554 | # calls.
555 | #
556 | debug_magic = 0x4567abcd
557 | results.append(pamh._PAM_RETURN_VALUES)
558 | for err in range(pamh._PAM_RETURN_VALUES):
559 | try:
560 | pamh.strerror(debug_magic + err)
561 | results.append(err)
562 | except pamh.exception, e:
563 | results.append((-e.pam_result,))
564 | return pamh.PAM_SUCCESS
565 |
566 | def run_exceptions(results):
567 | pam = PAM.pam()
568 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
569 | pam.authenticate(0)
570 | del pam
571 | expected_results = [results[0], 0]
572 | expected_results += [(-r,) for r in range(1, results[0])]
573 | assert_results(expected_results, results)
574 |
575 | #
576 | # Test absent entry point.
577 | #
578 | def test_absent(results, who, pamh, flags, argv):
579 | results.append(who.func_name)
580 | if who != pam_sm_authenticate:
581 | return pamh.PAM_SUCCESS
582 | global pam_sm_acct_mgmt; del pam_sm_acct_mgmt
583 | global pam_sm_setcred; del pam_sm_setcred
584 | global pam_sm_open_session; del pam_sm_open_session
585 | global pam_sm_close_session; del pam_sm_close_session
586 | global pam_sm_chauthtok; del pam_sm_chauthtok
587 | return pamh.PAM_SUCCESS
588 |
589 | def run_absent(results):
590 | pam = PAM.pam()
591 | pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
592 | pam.authenticate(0)
593 | funcs = (
594 | pam.acct_mgmt,
595 | pam.setcred,
596 | pam.open_session,
597 | pam.close_session,
598 | pam.chauthtok
599 | )
600 | for func in funcs:
601 | try:
602 | func(0)
603 | exception = None
604 | except Exception, e:
605 | exception = e
606 | results.append((exception.__class__.__name__, str(exception)))
607 | del pam
608 | expected_results = [
609 | 'pam_sm_authenticate',
610 | ('error', "('Symbol not found', 2)"),
611 | ('error', "('Symbol not found', 2)"),
612 | ('error', "('Symbol not found', 2)"),
613 | ('error', "('Symbol not found', 2)"),
614 | ('error', "('Symbol not found', 2)"),
615 | ]
616 | assert_results(expected_results, results)
617 |
618 | #
619 | # Entry point.
620 | #
621 | def main(argv):
622 | run_test(run_basic_calls)
623 | run_test(run_constants)
624 | run_test(run_environment)
625 | run_test(run_strerror)
626 | run_test(run_items)
627 | run_test(run_xauthdata)
628 | run_test(run_no_sm_end)
629 | run_test(run_conv)
630 | run_test(run_pamerr)
631 | run_test(run_fail_delay)
632 | run_test(run_exceptions)
633 | run_test(run_absent)
634 |
635 | #
636 | # If run from Python run the test suite. Otherwse we are being used
637 | # as a real PAM module presumable from ctest, so just make every call
638 | # return success.
639 | #
640 | if __name__ == "__main__":
641 | import PAM
642 | main(sys.argv)
643 |
--------------------------------------------------------------------------------