python-pam 2.0.2__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pam/__init__.py CHANGED
@@ -1,9 +1,9 @@
1
- import sys as __sys
1
+ """PAM (Pluggable Authentication Modules) interface for Python.
2
2
 
3
- if __sys.version_info < (3, ): # pragma: no cover
4
- print('WARNING, Python 2 is EOL and therefore py2 support in this '
5
- "package is deprecated. It won't be actively checked for"
6
- 'correctness')
3
+ This module provides a Python interface to Linux-PAM, allowing authentication
4
+ against system PAM services.
5
+ """
6
+ import sys as __sys
7
7
 
8
8
  # list all the constants and export them
9
9
  from .__internals import PAM_ACCT_EXPIRED
@@ -58,6 +58,9 @@ from .__internals import PAM_USER_UNKNOWN
58
58
  from .__internals import PAM_XDISPLAY
59
59
  from .__internals import PamAuthenticator
60
60
 
61
+ if __sys.version_info < (3, ): # pragma: no cover
62
+ print('WARNING, Python 2 is EOL not supported')
63
+
61
64
  __all__ = [
62
65
  'authenticate',
63
66
  'pam',
@@ -113,25 +116,45 @@ __all__ = [
113
116
  'PAM_XDISPLAY',
114
117
  ]
115
118
 
116
- __PA = None
117
119
 
120
+ def authenticate(
121
+ username: str | bytes,
122
+ password: str | bytes,
123
+ service: str | bytes = 'login',
124
+ env: dict[str, str] | None = None,
125
+ call_end: bool = True,
126
+ encoding: str = 'utf-8',
127
+ resetcreds: bool = True,
128
+ print_failure_messages: bool = False,
129
+ ) -> bool:
130
+ """Authenticate a user against PAM.
131
+
132
+ Creates a fresh ``PamAuthenticator`` for each call so concurrent use from
133
+ multiple threads is safe. libpam ctypes bindings are loaded once and shared.
134
+
135
+ For result codes after auth, use ``PamAuthenticator`` directly::
118
136
 
119
- def authenticate(username,
120
- password,
121
- service='login',
122
- env=None,
123
- call_end=True,
124
- encoding='utf-8',
125
- resetcreds=True,
126
- print_failure_messages=False):
127
- global __PA
137
+ pa = pam.pam()
138
+ ok = pa.authenticate(user, password)
139
+ print(pa.code, pa.reason)
128
140
 
129
- if __PA is None: # pragma: no branch
130
- __PA = PamAuthenticator()
141
+ Args:
142
+ username: Username to authenticate
143
+ password: Password in plain text
144
+ service: PAM service to authenticate against (default: 'login')
145
+ env: Dictionary of environment variables to set
146
+ call_end: Whether to call pam_end() after authentication (default: True)
147
+ encoding: Encoding to use for string conversions (default: 'utf-8')
148
+ resetcreds: Whether to reset credentials after authentication (default: True)
149
+ print_failure_messages: Whether to print failure messages (default: False)
131
150
 
132
- return __PA.authenticate(username, password, service, env, call_end, encoding, resetcreds, print_failure_messages)
151
+ Returns:
152
+ bool: True if authentication succeeded, False otherwise
153
+ """
154
+ return PamAuthenticator().authenticate(
155
+ username, password, service, env, call_end, encoding, resetcreds, print_failure_messages,
156
+ )
133
157
 
134
158
 
135
159
  # legacy implementations used pam.pam()
136
- pam = PamAuthenticator
137
- authenticate.__doc__ = PamAuthenticator.authenticate.__doc__
160
+ pam = PamAuthenticator # noqa: N816, C0103
pam/__internals.py CHANGED
@@ -1,23 +1,30 @@
1
+ """Internal PAM implementation using ctypes.
2
+
3
+ This module provides the low-level interface to Linux-PAM using ctypes,
4
+ including structure definitions, constants, and the PamAuthenticator class.
5
+ """
1
6
  import os
2
- import six
3
7
  import sys
4
- from ctypes import cdll
5
- from ctypes import CFUNCTYPE
6
- from ctypes import CDLL
7
- from ctypes import POINTER
8
- from ctypes import Structure
9
- from ctypes import byref
10
- from ctypes import cast
11
- from ctypes import sizeof
12
- from ctypes import py_object
13
- from ctypes import c_char
14
- from ctypes import c_char_p
15
- from ctypes import c_int
16
- from ctypes import c_size_t
17
- from ctypes import c_void_p
18
- from ctypes import memmove
8
+ import threading
9
+ from ctypes import (
10
+ CDLL,
11
+ CFUNCTYPE,
12
+ POINTER,
13
+ Structure,
14
+ byref,
15
+ c_char,
16
+ c_char_p,
17
+ c_int,
18
+ c_size_t,
19
+ c_void_p,
20
+ cast,
21
+ cdll,
22
+ memmove,
23
+ py_object,
24
+ sizeof,
25
+ )
19
26
  from ctypes.util import find_library
20
- from typing import Union
27
+ from typing import Any
21
28
 
22
29
  PAM_ABORT = 26
23
30
  PAM_ACCT_EXPIRED = 13
@@ -107,7 +114,7 @@ class PamMessage(Structure):
107
114
  _fields_ = [("msg_style", c_int), ("msg", c_char_p)]
108
115
 
109
116
  def __repr__(self):
110
- return "<PamMessage style: %i, content: %s >" % (self.msg_style, self.msg)
117
+ return f"<PamMessage style: {self.msg_style}, content: {self.msg} >"
111
118
 
112
119
 
113
120
  class PamResponse(Structure):
@@ -115,7 +122,7 @@ class PamResponse(Structure):
115
122
  _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]
116
123
 
117
124
  def __repr__(self):
118
- return "<PamResponse code: %i, content: %s >" % (self.resp_retcode, self.resp)
125
+ return f"<PamResponse code: {self.resp_retcode}, content: {self.resp} >"
119
126
 
120
127
 
121
128
  conv_func = CFUNCTYPE(c_int,
@@ -125,22 +132,31 @@ conv_func = CFUNCTYPE(c_int,
125
132
  c_void_p)
126
133
 
127
134
 
128
- def my_conv(n_messages, messages, p_response, libc, msg_list: list, password: bytes, encoding: str):
135
+ def my_conv(
136
+ n_messages: int,
137
+ messages: Any, # POINTER(POINTER(PamMessage)) - ctypes types not supported by mypy
138
+ p_response: Any, # POINTER(POINTER(PamResponse)) - ctypes types not supported by mypy
139
+ libc: Any,
140
+ msg_list: list[str],
141
+ password: bytes,
142
+ encoding: str,
143
+ ) -> int:
129
144
  """Simple conversation function that responds to any
130
145
  prompt where the echo is off with the supplied password"""
131
- # Create an array of n_messages response objects
132
- calloc = libc.calloc
133
- calloc.restype = c_void_p
134
- calloc.argtypes = [c_size_t, c_size_t]
146
+ # Prefer class-level calloc (configured once); fall back for unit tests that
147
+ # pass a raw libc.
148
+ calloc = getattr(PamAuthenticator, 'calloc', None)
149
+ if calloc is None: # pragma: no cover
150
+ calloc = libc.calloc
151
+ calloc.restype = c_void_p
152
+ calloc.argtypes = [c_size_t, c_size_t]
135
153
 
136
154
  cpassword = c_char_p(password)
137
155
 
138
- '''
139
- PAM_PROMPT_ECHO_OFF = 1
140
- PAM_PROMPT_ECHO_ON = 2
141
- PAM_ERROR_MSG = 3
142
- PAM_TEXT_INFO = 4
143
- '''
156
+ # PAM_PROMPT_ECHO_OFF = 1
157
+ # PAM_PROMPT_ECHO_ON = 2
158
+ # PAM_ERROR_MSG = 3
159
+ # PAM_TEXT_INFO = 4
144
160
 
145
161
  addr = calloc(n_messages, sizeof(PamResponse))
146
162
  response = cast(addr, POINTER(PamResponse))
@@ -173,95 +189,155 @@ class PamConv(Structure):
173
189
 
174
190
 
175
191
  class PamAuthenticator:
176
- code = 0
177
- reason = None # type: Union[str, bytes, None]
192
+ """PAM authenticator class.
193
+
194
+ Provides methods to authenticate users against Linux-PAM, manage PAM
195
+ sessions, and handle PAM environment variables.
196
+
197
+ Thread safety:
198
+ - ``pam.authenticate()`` creates a fresh instance per call and is safe
199
+ for concurrent use (libs are loaded once and shared).
200
+ - A single ``PamAuthenticator`` instance must not be used from multiple
201
+ threads at once. For high-QPS auth, prefer ``pam.authenticate()`` or
202
+ one instance per concurrent caller (default ``call_end=True``).
203
+ - With ``call_end=False``, the instance owns a live PAM handle for
204
+ sessions/env; treat that object as single-threaded.
205
+ """
206
+
207
+ # Shared ctypes bindings (immutable after _ensure_libs). Loaded once.
208
+ _lib_lock = threading.Lock()
209
+ _libs_ready = False
210
+ libc: Any = None
211
+ calloc: Any = None
212
+ pam_end: Any = None
213
+ pam_start: Any = None
214
+ pam_acct_mgmt: Any = None
215
+ pam_set_item: Any = None
216
+ pam_setcred: Any = None
217
+ pam_strerror: Any = None
218
+ pam_authenticate: Any = None
219
+ pam_open_session: Any = None
220
+ pam_close_session: Any = None
221
+ pam_putenv: Any = None
222
+ pam_misc_setenv: Any = None
223
+ pam_getenv: Any = None
224
+ pam_getenvlist: Any = None
225
+
226
+ @classmethod
227
+ def _ensure_libs(cls) -> None:
228
+ """Load libpam/libc symbols once for all instances (thread-safe)."""
229
+ if cls._libs_ready:
230
+ return
178
231
 
179
- def __init__(self):
180
- # use a trick of dlopen(), this effectively becomes
181
- # dlopen("", ...) which opens our own executable. since 'python' has
182
- # a libc dependency, this means libc symbols are already available
183
- # to us
232
+ with cls._lib_lock:
233
+ if cls._libs_ready:
234
+ return
184
235
 
185
- # libc = CDLL(find_library("c"))
186
- libc = cdll.LoadLibrary(None)
187
- self.libc = libc
236
+ # dlopen("", ...) — python already links libc, so symbols are available
237
+ libc = cdll.LoadLibrary(None) # type: ignore[arg-type]
238
+ libpam = CDLL(find_library("pam"))
239
+ libpam_misc = CDLL(find_library("pam_misc"))
188
240
 
189
- libpam = CDLL(find_library("pam"))
190
- libpam_misc = CDLL(find_library("pam_misc"))
241
+ cls.libc = libc
242
+ cls.calloc = libc.calloc
243
+ cls.calloc.restype = c_void_p
244
+ cls.calloc.argtypes = [c_size_t, c_size_t]
191
245
 
192
- self.handle = None
193
- self.messages = []
246
+ # bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this
247
+ if hasattr(libpam, 'pam_end'): # pragma: no branch
248
+ cls.pam_end = libpam.pam_end
249
+ cls.pam_end.restype = c_int
250
+ cls.pam_end.argtypes = [PamHandle, c_int]
251
+
252
+ cls.pam_start = libpam.pam_start
253
+ cls.pam_start.restype = c_int
254
+ cls.pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv),
255
+ POINTER(PamHandle)]
256
+
257
+ cls.pam_acct_mgmt = libpam.pam_acct_mgmt
258
+ cls.pam_acct_mgmt.restype = c_int
259
+ cls.pam_acct_mgmt.argtypes = [PamHandle, c_int]
260
+
261
+ cls.pam_set_item = libpam.pam_set_item
262
+ cls.pam_set_item.restype = c_int
263
+ cls.pam_set_item.argtypes = [PamHandle, c_int, c_void_p]
264
+
265
+ cls.pam_setcred = libpam.pam_setcred
266
+
267
+ cls.pam_strerror = libpam.pam_strerror
268
+ cls.pam_strerror.restype = c_char_p
269
+ cls.pam_strerror.argtypes = [PamHandle, c_int]
194
270
 
195
- self.calloc = libc.calloc
196
- self.calloc.restype = c_void_p
197
- self.calloc.argtypes = [c_size_t, c_size_t]
198
-
199
- # bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this
200
- # function
201
- if hasattr(libpam, 'pam_end'): # pragma: no branch
202
- self.pam_end = libpam.pam_end
203
- self.pam_end.restype = c_int
204
- self.pam_end.argtypes = [PamHandle, c_int]
205
-
206
- self.pam_start = libpam.pam_start
207
- self.pam_start.restype = c_int
208
- self.pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv),
209
- POINTER(PamHandle)]
210
-
211
- self.pam_acct_mgmt = libpam.pam_acct_mgmt
212
- self.pam_acct_mgmt.restype = c_int
213
- self.pam_acct_mgmt.argtypes = [PamHandle, c_int]
214
-
215
- self.pam_set_item = libpam.pam_set_item
216
- self.pam_set_item.restype = c_int
217
- self.pam_set_item.argtypes = [PamHandle, c_int, c_void_p]
218
-
219
- self.pam_setcred = libpam.pam_setcred
220
- self.pam_strerror = libpam.pam_strerror
221
- self.pam_strerror.restype = c_char_p
222
- self.pam_strerror.argtypes = [PamHandle, c_int]
223
-
224
- self.pam_authenticate = libpam.pam_authenticate
225
- self.pam_authenticate.restype = c_int
226
- self.pam_authenticate.argtypes = [PamHandle, c_int]
227
-
228
- self.pam_open_session = libpam.pam_open_session
229
- self.pam_open_session.restype = c_int
230
- self.pam_open_session.argtypes = [PamHandle, c_int]
231
-
232
- self.pam_close_session = libpam.pam_close_session
233
- self.pam_close_session.restype = c_int
234
- self.pam_close_session.argtypes = [PamHandle, c_int]
235
-
236
- self.pam_putenv = libpam.pam_putenv
237
- self.pam_putenv.restype = c_int
238
- self.pam_putenv.argtypes = [PamHandle, c_char_p]
239
-
240
- if libpam_misc._name: # pragma: no branch
241
- self.pam_misc_setenv = libpam_misc.pam_misc_setenv
242
- self.pam_misc_setenv.restype = c_int
243
- self.pam_misc_setenv.argtypes = [PamHandle, c_char_p, c_char_p,
244
- c_int]
245
-
246
- self.pam_getenv = libpam.pam_getenv
247
- self.pam_getenv.restype = c_char_p
248
- self.pam_getenv.argtypes = [PamHandle, c_char_p]
249
-
250
- self.pam_getenvlist = libpam.pam_getenvlist
251
- self.pam_getenvlist.restype = POINTER(c_char_p)
252
- self.pam_getenvlist.argtypes = [PamHandle]
271
+ cls.pam_authenticate = libpam.pam_authenticate
272
+ cls.pam_authenticate.restype = c_int
273
+ cls.pam_authenticate.argtypes = [PamHandle, c_int]
274
+
275
+ cls.pam_open_session = libpam.pam_open_session
276
+ cls.pam_open_session.restype = c_int
277
+ cls.pam_open_session.argtypes = [PamHandle, c_int]
278
+
279
+ cls.pam_close_session = libpam.pam_close_session
280
+ cls.pam_close_session.restype = c_int
281
+ cls.pam_close_session.argtypes = [PamHandle, c_int]
282
+
283
+ cls.pam_putenv = libpam.pam_putenv
284
+ cls.pam_putenv.restype = c_int
285
+ cls.pam_putenv.argtypes = [PamHandle, c_char_p]
286
+
287
+ # CDLL._name is the loaded library path (empty if unavailable)
288
+ if getattr(libpam_misc, '_name', None): # pragma: no branch
289
+ cls.pam_misc_setenv = libpam_misc.pam_misc_setenv
290
+ cls.pam_misc_setenv.restype = c_int
291
+ cls.pam_misc_setenv.argtypes = [PamHandle, c_char_p, c_char_p,
292
+ c_int]
293
+
294
+ cls.pam_getenv = libpam.pam_getenv
295
+ cls.pam_getenv.restype = c_char_p
296
+ cls.pam_getenv.argtypes = [PamHandle, c_char_p]
297
+
298
+ cls.pam_getenvlist = libpam.pam_getenvlist
299
+ cls.pam_getenvlist.restype = POINTER(c_char_p)
300
+ cls.pam_getenvlist.argtypes = [PamHandle]
301
+
302
+ cls._libs_ready = True
303
+
304
+ def __init__(self):
305
+ self._ensure_libs()
306
+ # Cheap instance aliases to shared class bindings so callers/tests can
307
+ # patch per-object and static analyzers see callables on self.
308
+ cls = self.__class__
309
+ self.libc = cls.libc
310
+ self.calloc = cls.calloc
311
+ self.pam_end = cls.pam_end
312
+ self.pam_start = cls.pam_start
313
+ self.pam_acct_mgmt = cls.pam_acct_mgmt
314
+ self.pam_set_item = cls.pam_set_item
315
+ self.pam_setcred = cls.pam_setcred
316
+ self.pam_strerror = cls.pam_strerror
317
+ self.pam_authenticate = cls.pam_authenticate
318
+ self.pam_open_session = cls.pam_open_session
319
+ self.pam_close_session = cls.pam_close_session
320
+ self.pam_putenv = cls.pam_putenv
321
+ self.pam_misc_setenv = cls.pam_misc_setenv
322
+ self.pam_getenv = cls.pam_getenv
323
+ self.pam_getenvlist = cls.pam_getenvlist
324
+
325
+ self.handle: PamHandle | None = None
326
+ self.messages: list[str] = []
327
+ self.code: int = 0
328
+ self.reason: str | bytes | None = None
253
329
 
254
330
  def authenticate(
255
- self,
256
- username, # type: Union[str, bytes]
257
- password, # type: Union[str, bytes]
258
- service='login', # type: Union[str, bytes]
259
- env=None, # type: dict
260
- call_end=True, # type: bool
261
- encoding='utf-8', # type: str
262
- resetcreds=True, # type: bool
263
- print_failure_messages=False # type: bool
264
- ): # type: (...) -> bool
331
+ self,
332
+ username: str | bytes,
333
+ password: str | bytes,
334
+ service: str | bytes = 'login',
335
+ env: dict[str, str] | None = None,
336
+ call_end: bool = True,
337
+ encoding: str = 'utf-8',
338
+ resetcreds: bool = True,
339
+ print_failure_messages: bool = False,
340
+ ) -> bool:
265
341
  """username and password authentication for the given service.
266
342
 
267
343
  Returns True for success, or False for failure.
@@ -284,7 +360,12 @@ class PamAuthenticator:
284
360
  Returns:
285
361
  success: PAM_SUCCESS
286
362
  failure: False
363
+
364
+ Note:
365
+ Do not call authenticate() on the same instance from multiple threads
366
+ concurrently. Use ``pam.authenticate()`` for concurrent high-QPS auth.
287
367
  """
368
+ self.messages = []
288
369
 
289
370
  @conv_func
290
371
  def __conv(n_messages, messages, p_response, app_data):
@@ -296,11 +377,11 @@ class PamAuthenticator:
296
377
 
297
378
  return my_conv(n_messages, messages, p_response, self.libc, msg_list, password, encoding)
298
379
 
299
- if isinstance(username, six.text_type):
380
+ if isinstance(username, str):
300
381
  username = username.encode(encoding)
301
- if isinstance(password, six.text_type):
382
+ if isinstance(password, str):
302
383
  password = password.encode(encoding)
303
- if isinstance(service, six.text_type):
384
+ if isinstance(service, str):
304
385
  service = service.encode(encoding)
305
386
 
306
387
  if b'\x00' in username or b'\x00' in password or b'\x00' in service:
@@ -322,8 +403,15 @@ class PamAuthenticator:
322
403
  # This is not an authentication error, something has gone wrong
323
404
  # starting up PAM
324
405
  self.code = retval
325
- self.reason = ("pam_start() failed: %s" %
326
- self.pam_strerror(self.handle, retval))
406
+ self.reason = f"pam_start() failed: {self.pam_strerror(self.handle, retval)}"
407
+ self.handle = None
408
+ return False
409
+
410
+ # Verify handle was properly initialized by pam_start
411
+ if self.handle is None or self.handle.handle == 0:
412
+ self.code = PAM_SYSTEM_ERR
413
+ self.reason = "pam_start() succeeded but handle was not properly initialized"
414
+ self.handle = None
327
415
  return False
328
416
 
329
417
  # set the TTY, required when pam_securetty is used and the username
@@ -356,53 +444,81 @@ class PamAuthenticator:
356
444
  if not isinstance(env, dict):
357
445
  raise TypeError('"env" must be a dict')
358
446
 
447
+ # Ensure handle is still valid before setting environment variables
448
+ if self.handle is None or self.handle.handle == 0:
449
+ self.code = PAM_SYSTEM_ERR
450
+ self.reason = "PAM handle became invalid before setting environment variables"
451
+ return False
452
+
359
453
  for key, value in env.items():
360
454
  if isinstance(key, bytes) and b'\x00' in key:
361
455
  raise ValueError('"env{}" key cannot contain NULLs')
362
456
  if isinstance(value, bytes) and b'\x00' in value:
363
457
  raise ValueError('"env{}" value cannot contain NULLs')
364
458
 
365
- name_value = "{}={}".format(key, value)
459
+ name_value = f"{key}={value}"
366
460
  retval = self.putenv(name_value, encoding)
367
461
 
462
+ # Ensure handle is still valid before authentication
463
+ if self.handle is None or self.handle.handle == 0:
464
+ self.code = PAM_SYSTEM_ERR
465
+ self.reason = "PAM handle became invalid before authentication"
466
+ return False
467
+
368
468
  auth_success = self.pam_authenticate(self.handle, 0)
369
469
 
370
470
  if auth_success == PAM_SUCCESS:
471
+ # Ensure handle is still valid before account management
472
+ if self.handle is None or self.handle.handle == 0:
473
+ self.code = PAM_SYSTEM_ERR
474
+ self.reason = "PAM handle became invalid before account management"
475
+ return False
371
476
  auth_success = self.pam_acct_mgmt(self.handle, 0)
372
477
 
373
478
  if auth_success == PAM_SUCCESS and resetcreds:
479
+ # Ensure handle is still valid before setting credentials
480
+ if self.handle is None or self.handle.handle == 0:
481
+ self.code = PAM_SYSTEM_ERR
482
+ self.reason = "PAM handle became invalid before setting credentials"
483
+ return False
374
484
  auth_success = self.pam_setcred(self.handle, PAM_REINITIALIZE_CRED)
375
485
 
376
486
  # store information to inform the caller why we failed
377
487
  self.code = auth_success
378
- self.reason = self.pam_strerror(self.handle, auth_success)
379
-
380
- if sys.version_info >= (3,): # pragma: no branch (we don't test non-py3 versions)
381
- self.reason = self.reason.decode(encoding) # type: ignore
488
+ # Ensure handle is still valid before getting error message
489
+ if self.handle is not None and self.handle.handle != 0:
490
+ reason_bytes = self.pam_strerror(self.handle, auth_success)
491
+ if sys.version_info >= (3,): # pragma: no branch (we don't test non-py3 versions)
492
+ self.reason = reason_bytes.decode(encoding)
493
+ else:
494
+ self.reason = reason_bytes # type: ignore[assignment]
495
+ else:
496
+ self.reason = f"PAM error {auth_success} (handle invalid)"
382
497
 
383
- if call_end and hasattr(self, 'pam_end'): # pragma: no branch
498
+ if call_end and hasattr(self, 'pam_end') and self.pam_end is not None: # pragma: no branch
384
499
  self.pam_end(self.handle, auth_success)
385
500
  self.handle = None
386
501
 
387
502
  if print_failure_messages and self.code != PAM_SUCCESS: # pragma: no cover
388
- print(f"Failure: {self.reason}")
503
+ reason_str = self.reason if isinstance(self.reason, str) else str(self.reason)
504
+ print(f"Failure: {reason_str}")
389
505
 
390
- return auth_success == PAM_SUCCESS
506
+ return bool(auth_success == PAM_SUCCESS)
391
507
 
392
- def end(self):
508
+ def end(self) -> int:
393
509
  """A direct call to pam_end()
394
510
  Returns:
395
511
  Linux-PAM return value as int
396
512
  """
397
- if not self.handle or not hasattr(self, 'pam_end'):
513
+ if not self.handle or self.pam_end is None:
398
514
  return PAM_SYSTEM_ERR
399
515
 
400
516
  retval = self.pam_end(self.handle, self.code)
401
517
  self.handle = None
402
518
 
403
- return retval
519
+ return int(retval)
404
520
 
405
- def open_session(self, encoding='utf-8'):
521
+ def open_session(self, encoding: str = 'utf-8') -> int:
406
522
  """Call pam_open_session as required by the pam_api
407
523
  Returns:
408
524
  Linux-PAM return value as int
@@ -412,14 +528,15 @@ class PamAuthenticator:
412
528
 
413
529
  retval = self.pam_open_session(self.handle, 0)
414
530
  self.code = retval
415
- self.reason = self.pam_strerror(self.handle, retval)
416
-
531
+ reason_bytes = self.pam_strerror(self.handle, retval)
417
532
  if sys.version_info >= (3,): # pragma: no branch
418
- self.reason = self.reason.decode(encoding)
533
+ self.reason = reason_bytes.decode(encoding)
534
+ else:
535
+ self.reason = reason_bytes # type: ignore[assignment]
419
536
 
420
- return retval
537
+ return int(retval)
421
538
 
422
- def close_session(self, encoding='utf-8'):
539
+ def close_session(self, encoding: str = 'utf-8') -> int:
423
540
  """Call pam_close_session as required by the pam_api
424
541
  Returns:
425
542
  Linux-PAM return value as int
@@ -429,14 +546,15 @@ class PamAuthenticator:
429
546
 
430
547
  retval = self.pam_close_session(self.handle, 0)
431
548
  self.code = retval
432
- self.reason = self.pam_strerror(self.handle, retval)
433
-
549
+ reason_bytes = self.pam_strerror(self.handle, retval)
434
550
  if sys.version_info >= (3,): # pragma: no branch
435
- self.reason = self.reason.decode(encoding)
551
+ self.reason = reason_bytes.decode(encoding)
552
+ else:
553
+ self.reason = reason_bytes # type: ignore[assignment]
436
554
 
437
- return retval
555
+ return int(retval)
438
556
 
439
- def misc_setenv(self, name, value, readonly, encoding='utf-8'):
557
+ def misc_setenv(self, name: str, value: str, readonly: int, encoding: str = 'utf-8') -> int:
440
558
  """A wrapper for the pam_misc_setenv function
441
559
  Args:
442
560
  name: key name of the environment variable
@@ -444,15 +562,16 @@ class PamAuthenticator:
444
562
  Returns:
445
563
  Linux-PAM return value as int
446
564
  """
447
- if not self.handle or not hasattr(self, "pam_misc_setenv"):
565
+ if not self.handle or self.pam_misc_setenv is None:
448
566
  return PAM_SYSTEM_ERR
449
567
 
450
- return self.pam_misc_setenv(self.handle,
451
- name.encode(encoding),
452
- value.encode(encoding),
453
- readonly)
568
+ retval = self.pam_misc_setenv(self.handle,
569
+ name.encode(encoding),
570
+ value.encode(encoding),
571
+ readonly)
572
+ return int(retval)
454
573
 
455
- def putenv(self, name_value, encoding='utf-8'):
574
+ def putenv(self, name_value: str, encoding: str = 'utf-8') -> int:
456
575
  """A wrapper for the pam_putenv function
457
576
  Args:
458
577
  name_value: environment variable in the format KEY=VALUE
@@ -463,46 +582,61 @@ class PamAuthenticator:
463
582
  if not self.handle:
464
583
  return PAM_SYSTEM_ERR
465
584
 
466
- name_value = name_value.encode(encoding)
585
+ name_value_bytes = name_value.encode(encoding)
467
586
 
468
- retval = self.pam_putenv(self.handle, name_value)
587
+ retval = self.pam_putenv(self.handle, name_value_bytes)
469
588
  if retval != PAM_SUCCESS:
470
- raise Exception(self.pam_strerror(self.handle, retval))
589
+ error_msg_bytes = self.pam_strerror(self.handle, retval)
590
+ if sys.version_info >= (3,): # pragma: no branch
591
+ error_msg = error_msg_bytes.decode(encoding)
592
+ else:
593
+ error_msg = error_msg_bytes # type: ignore[assignment]
594
+ raise RuntimeError(error_msg)
471
595
 
472
- return retval
596
+ return int(retval)
473
597
 
474
- def getenv(self, key, encoding='utf-8'):
598
+ def getenv(self, key: str | bytes, encoding: str = 'utf-8') -> str | None | int:
475
599
  """A wrapper for the pam_getenv function
476
600
  Args:
477
601
  key name of the environment variable
478
602
  Returns:
479
- value of the environment variable or None on error
603
+ value of the environment variable, None on error, or PAM_SYSTEM_ERR if handle is invalid
480
604
  """
481
605
  if not self.handle:
482
606
  return PAM_SYSTEM_ERR
483
607
 
484
608
  # can't happen unless someone is using internals directly
485
- if sys.version_info >= (3, ): # pragma: no branch
486
- if isinstance(key, six.text_type): # pragma: no branch
487
- key = key.encode(encoding)
609
+ if isinstance(key, str): # pragma: no branch
610
+ key = key.encode(encoding)
488
611
 
489
612
  value = self.pam_getenv(self.handle, key)
490
613
 
491
614
  if isinstance(value, type(None)):
492
- return
615
+ return None
493
616
 
494
617
  if isinstance(value, int): # pragma: no cover
495
- raise Exception(self.pam_strerror(self.handle, value))
618
+ error_msg = self.pam_strerror(self.handle, value)
619
+ if sys.version_info >= (3,): # pragma: no branch
620
+ error_msg = error_msg.decode(encoding)
621
+ raise RuntimeError(error_msg)
496
622
 
497
623
  if sys.version_info >= (3,): # pragma: no branch
498
- value = value.decode(encoding)
499
-
500
- return value
501
-
502
- def getenvlist(self, encoding='utf-8'):
624
+ if isinstance(value, bytes):
625
+ return value.decode(encoding)
626
+ # value is c_char_p which is bytes-like, but mypy doesn't know
627
+ # c_char_p can be None, bytes, or int, but in practice it's bytes here
628
+ # We've already checked for None and bytes, so this should be safe
629
+ # At this point value should be bytes (c_char_p), but mypy sees it as Any
630
+ # Since we've checked for None and bytes, the remaining case is safe
631
+ return str(value)
632
+ # Python 2 path (not tested) - value is bytes in py2
633
+ # This path is never executed in Python 3, so mypy doesn't see the error
634
+ return str(value)
635
+
636
+ def getenvlist(self, encoding: str = 'utf-8') -> dict[str, str] | int:
503
637
  """A wrapper for the pam_getenvlist function
504
638
  Returns:
505
- environment as python dictionary
639
+ environment as python dictionary, or PAM_SYSTEM_ERR if handle is invalid
506
640
  """
507
641
  if not self.handle:
508
642
  return PAM_SYSTEM_ERR
pam/pam.py CHANGED
@@ -30,24 +30,24 @@ a user against the Pluggable Authentication Modules (PAM) on the system.
30
30
  Implemented using ctypes, so no compilation is necessary.
31
31
  '''
32
32
 
33
- import six
34
- import __internals
33
+ from . import __internals
35
34
 
36
35
  if __name__ == "__main__": # pragma: no cover
37
36
  import readline
38
37
  import getpass
39
38
 
40
39
  def input_with_prefill(prompt, text):
40
+ """Input function with prefilled text."""
41
41
  def hook():
42
42
  readline.insert_text(text)
43
43
  readline.redisplay()
44
44
 
45
45
  readline.set_pre_input_hook(hook)
46
- result = six.moves.input(prompt) # nosec (bandit; python2)
46
+ user_input = input(prompt) # nosec (bandit; python2)
47
47
 
48
48
  readline.set_pre_input_hook()
49
49
 
50
- return result
50
+ return user_input
51
51
 
52
52
  __pam = __internals.PamAuthenticator()
53
53
 
@@ -58,23 +58,30 @@ if __name__ == "__main__": # pragma: no cover
58
58
  result = __pam.authenticate(username, getpass.getpass(),
59
59
  env={"XDG_SEAT": "seat0"},
60
60
  call_end=False)
61
- print('Auth result: {} ({})'.format(__pam.reason, __pam.code))
61
+ reason_str = __pam.reason if isinstance(__pam.reason, str) else str(__pam.reason)
62
+ print(f'Auth result: {reason_str} ({__pam.code})')
62
63
 
63
64
  env_list = __pam.getenvlist()
64
- for key, value in env_list.items():
65
- print("Pam Environment List item: {}={}".format(key, value))
65
+ if isinstance(env_list, dict):
66
+ for key, value in env_list.items():
67
+ print(f"Pam Environment List item: {key}={value}")
66
68
 
67
69
  key = "XDG_SEAT"
68
- value = __pam.getenv(key)
69
- print("Pam Environment item: {}={}".format(key, value))
70
+ env_value = __pam.getenv(key)
71
+ if env_value is not None:
72
+ print(f"Pam Environment item: {key}={env_value}")
73
+ else:
74
+ print(f"Pam Environment item: {key}=None")
70
75
 
71
76
  if __pam.code == __internals.PAM_SUCCESS:
72
- result = __pam.open_session()
73
- print('Open session: {} ({})'.format(__pam.reason, __pam.code))
77
+ session_result = __pam.open_session()
78
+ session_reason = __pam.reason if isinstance(__pam.reason, str) else str(__pam.reason)
79
+ print(f'Open session: {session_reason} ({__pam.code})')
74
80
 
75
81
  if __pam.code == __internals.PAM_SUCCESS:
76
- result = __pam.close_session()
77
- print('Close session: {} ({})'.format(__pam.reason, __pam.code))
82
+ close_result = __pam.close_session()
83
+ close_reason = __pam.reason if isinstance(__pam.reason, str) else str(__pam.reason)
84
+ print(f'Close session: {close_reason} ({__pam.code})')
78
85
 
79
86
  else:
80
87
  __pam.end()
pam/version.py CHANGED
@@ -1,3 +1,4 @@
1
- VERSION = '2.0.2'
1
+ """Version information for python-pam package."""
2
+ VERSION = '2.1.1'
2
3
  AUTHOR = 'David Ford <david.ford@blue-labs.org>'
3
- RELEASED = '2022 March 17'
4
+ RELEASED = '2026 October 27'
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-pam
3
+ Version: 2.1.1
4
+ Summary:
5
+ License-File: LICENSE
6
+ Author: David Ford
7
+ Author-email: david@blue-labs.org
8
+ Requires-Python: >=3.11
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Description-Content-Type: text/markdown
15
+
16
+ # python-pam
17
+
18
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/FirefighterBlu3/python-pam/badge)](https://scorecard.dev/viewer/?uri=github.com/FirefighterBlu3/python-pam)
19
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13794/badge)](https://www.bestpractices.dev/projects/13794)
20
+
21
+ Python pam module supporting py3 for Linux type systems (!windows)
22
+
23
+ ## Security
24
+
25
+ See [SECURITY.md](SECURITY.md) for supported versions and how to report vulnerabilities.
26
+
27
+ ## Threading and concurrency
28
+
29
+ `pam.authenticate()` is safe to call from many threads at once. Each call uses
30
+ its own PAM handle; libpam ctypes bindings are loaded once and shared (no global
31
+ lock on the auth path).
32
+
33
+ Do **not** share a single `PamAuthenticator` / `pam.pam()` instance across threads
34
+ without external synchronization. That object owns mutable PAM session state
35
+ (`handle`, `code`, `reason`, `messages`). For sessions (`call_end=False`), keep
36
+ one instance per thread (or serialize access).
37
+
38
+ High-QPS login APIs should use:
39
+
40
+ ```python
41
+ import pam
42
+
43
+ if pam.authenticate(username, password, service='myapp'):
44
+ ...
45
+ ```
46
+
47
+ ## Credentials (`resetcreds`)
48
+
49
+ After a successful `pam_authenticate` + `pam_acct_mgmt`, `authenticate()` calls
50
+ `pam_setcred(..., PAM_REINITIALIZE_CRED)` when `resetcreds=True` (the default).
51
+
52
+ **Keep the default (`True`)** when this process is acting like a login / credential
53
+ handoff: modules may establish or refresh credentials (e.g. Kerberos), and you care
54
+ about that step succeeding as part of auth.
55
+
56
+ **Set `resetcreds=False`** when you only need to verify a username/password (typical
57
+ web/API “is this password valid?” checks). You are not assuming the user’s identity
58
+ or opening a session; skipping setcred avoids extra module work and avoids treating a
59
+ setcred failure as an authentication failure.
60
+
61
+ ```python
62
+ # Password check only
63
+ pam.authenticate(user, password, service='myapp', resetcreds=False)
64
+
65
+ # Login-style / credential-aware stack (default)
66
+ pam.authenticate(user, password, service='login')
67
+ ```
68
+
69
+ ## Examples
70
+
71
+ Commandline example:
72
+
73
+ ```bash
74
+ [david@Scott python-pam]$ python pam/pam.py
75
+ Username: david
76
+ Password:
77
+ Auth result: Success (0)
78
+ Pam Environment List item: XDG_SEAT=seat0
79
+ Pam Environment item: XDG_SEAT=seat0
80
+ Missing Pam Environment item: asdf=None
81
+ Open session: Success (0)
82
+ Close session: Success (0)
83
+ ```
84
+
85
+ Inline examples:
86
+
87
+ ```python
88
+ >>> import pam
89
+ >>> pam.authenticate('david', 'correctpassword')
90
+ True
91
+ >>> p = pam.pam()
92
+ >>> p.authenticate('david', 'correctpassword')
93
+ True
94
+ >>> p.authenticate('david', 'badpassword')
95
+ False
96
+ >>> p.authenticate('david', 'correctpassword', service='login')
97
+ True
98
+ >>> p.authenticate('david', 'correctpassword', service='unknownservice')
99
+ False
100
+ >>> p.authenticate('david', 'correctpassword', service='login', resetcreds=True)
101
+ True
102
+ >>> p.authenticate('david', 'correctpassword', encoding='latin-1')
103
+ True
104
+ >>> print('{} {}'.format(p.code, p.reason))
105
+ 0 Success
106
+ >>> p.authenticate('david', 'badpassword')
107
+ False
108
+ >>> print('{} {}'.format(p.code, p.reason))
109
+ 7 Authentication failure
110
+ >>>
111
+ ```
112
+
113
+ ## Authentication and privileges
114
+ Please note, python-pam and *all* tools that do authentication follow two rules:
115
+
116
+ * You have root (or privileged access): you can check any account's password for validity
117
+ * You don't have root: you can only check the validity of the username running the tool
118
+
119
+ If you need to authenticate multiple users, you must use an authentication stack that at some stage has privileged access. On Linux systems one example of doing this is using SSSD.
120
+
121
+ Typical Linux installations check against `/etc/shadow` with `pam_unix.so` which will spawn `/usr/bin/unix_chkpwd` to verify the password. Both of these are intentionally written to meet the above two rules. You can test the functionality of `unix_chkpwd` in the following manner:
122
+
123
+ Replace `good` with the correct password, replace `david` with your appropriate username.
124
+
125
+ ```
126
+ ~$ mkfifo /tmp/myfifo
127
+
128
+ ~$ (echo -ne 'good\0' > /tmp/myfifo & /usr/bin/unix_chkpwd david nullok < /tmp/myfifo ) ; echo $?
129
+ 0
130
+
131
+ ~$ (echo -ne 'bad\0' > /tmp/myfifo & /usr/bin/unix_chkpwd david nullok < /tmp/myfifo ) ; echo $?
132
+ 7
133
+
134
+ ~$ (echo -ne 'good\0' > /tmp/myfifo & /usr/bin/unix_chkpwd someotheruser nullok < /tmp/myfifo ) ; echo $?
135
+ 9
136
+ ```
137
+
@@ -0,0 +1,8 @@
1
+ pam/__init__.py,sha256=6uTNelMCu1yx2m4l0j-SShRRHpgzDUkZ3mYWP5o5guE,5165
2
+ pam/__internals.py,sha256=lsR_9b3EW8EpjywL_mwv58KokeOqMdHux2z_g_Qt734,24596
3
+ pam/pam.py,sha256=_7vWm5YUWSk9-QRdeVSpej8km6h36qYoa_EmpW2aUac,2974
4
+ pam/version.py,sha256=_LnW3DnMRuYnlRsmZ18Y4MPppFxQZQ8IMfCgQfRzcqo,146
5
+ python_pam-2.1.1.dist-info/METADATA,sha256=Bqz0RTSMPYq60Qs-DFaWtp7-gyIuIHV1kij8Bimlbg0,4699
6
+ python_pam-2.1.1.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
7
+ python_pam-2.1.1.dist-info/licenses/LICENSE,sha256=-B9Dx4t8RlyTldHuUNiV6WYgeXvO0LBO7-C5u9p-HW8,1077
8
+ python_pam-2.1.1.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.37.1)
2
+ Generator: poetry-core 2.4.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,100 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: python-pam
3
- Version: 2.0.2
4
- Summary: Python PAM module using ctypes, py3
5
- Home-page: https://github.com/FirefighterBlu3/python-pam
6
- Author: David Ford
7
- Author-email: david@blue-labs.org
8
- License: License :: OSI Approved :: MIT License
9
- Project-URL: Bug Tracker, https://github.com/FirefighterBlu3/python-pam/issues
10
- Platform: UNKNOWN
11
- Classifier: Development Status :: 6 - Mature
12
- Classifier: Environment :: Plugins
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Intended Audience :: Information Technology
15
- Classifier: Intended Audience :: System Administrators
16
- Classifier: License :: OSI Approved :: MIT License
17
- Classifier: Operating System :: POSIX
18
- Classifier: Operating System :: POSIX :: Linux
19
- Classifier: Programming Language :: Python
20
- Classifier: Programming Language :: Python :: 2
21
- Classifier: Programming Language :: Python :: 3
22
- Classifier: Topic :: Security
23
- Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
24
- Description-Content-Type: text/markdown
25
- License-File: LICENSE
26
-
27
- # python-pam
28
-
29
- Python pam module supporting py3 (and py2) for Linux type systems (!windows)
30
-
31
- Commandline example:
32
-
33
- ```bash
34
- [david@Scott python-pam]$ python pam/pam.py
35
- Username: david
36
- Password:
37
- Auth result: Success (0)
38
- Pam Environment List item: XDG_SEAT=seat0
39
- Pam Environment item: XDG_SEAT=seat0
40
- Missing Pam Environment item: asdf=None
41
- Open session: Success (0)
42
- Close session: Success (0)
43
- ```
44
-
45
- Inline examples:
46
-
47
- ```python
48
- [david@Scott python-pam]$ python
49
- Python 3.9.7 (default, Oct 10 2021, 15:13:22)
50
- [GCC 11.1.0] on linux
51
- Type "help", "copyright", "credits" or "license" for more information.
52
- >>> import pam
53
- >>> p = pam.authenticate()
54
- >>> p.authenticate('david', 'correctpassword')
55
- True
56
- >>> p.authenticate('david', 'badpassword')
57
- False
58
- >>> p.authenticate('david', 'correctpassword', service='login')
59
- True
60
- >>> p.authenticate('david', 'correctpassword', service='unknownservice')
61
- False
62
- >>> p.authenticate('david', 'correctpassword', service='login', resetcreds=True)
63
- True
64
- >>> p.authenticate('david', 'correctpassword', encoding='latin-1')
65
- True
66
- >>> print('{} {}'.format(p.code, p.reason))
67
- 0 Success
68
- >>> p.authenticate('david', 'badpassword')
69
- False
70
- >>> print('{} {}'.format(p.code, p.reason))
71
- 7 Authentication failure
72
- >>>
73
- ```
74
-
75
- ## Authentication and privileges
76
- Please note, python-pam and *all* tools that do authentication follow two rules:
77
-
78
- * You have root (or privileged access): you can check any account's password for validity
79
- * You don't have root: you can only check the validity of the username running the tool
80
-
81
- If you need to authenticate multiple users, you must use an authentication stack that at some stage has privileged access. On Linux systems one example of doing this is using SSSD.
82
-
83
- Typical Linux installations check against `/etc/shadow` with `pam_unix.so` which will spawn `/usr/bin/unix_chkpwd` to verify the password. Both of these are intentionally written to meet the above two rules. You can test the functionality of `unix_chkpwd` in the following manner:
84
-
85
- Replace `good` with the correct password, replace `david` with your appropriate username.
86
-
87
- ```
88
- ~$ mkfifo /tmp/myfifo
89
-
90
- ~$ (echo -ne 'good\0' > /tmp/myfifo & /usr/bin/unix_chkpwd david nullok < /tmp/myfifo ) ; echo $?
91
- 0
92
-
93
- ~$ (echo -ne 'bad\0' > /tmp/myfifo & /usr/bin/unix_chkpwd david nullok < /tmp/myfifo ) ; echo $?
94
- 7
95
-
96
- ~$ (echo -ne 'good\0' > /tmp/myfifo & /usr/bin/unix_chkpwd someotheruser nullok < /tmp/myfifo ) ; echo $?
97
- 9
98
- ```
99
-
100
-
@@ -1,9 +0,0 @@
1
- pam/__init__.py,sha256=L85mgord2_NNcPYEjOcziYinm6Q0qrmRxxvjHFMXtsQ,4191
2
- pam/__internals.py,sha256=plZd01XwDSww5_lOWFbIsuwCKN9_uZzkYj0UnWq6P8E,18568
3
- pam/pam.py,sha256=nV_3zY6C58u4xLZ4RqrTNZGvUd-HjJ9Kik3CCY_I8K0,2544
4
- pam/version.py,sha256=JJzlHi8Pd67j0sMrgVQyqsikRqrNHvdu33lwee-tG9c,94
5
- python_pam-2.0.2.dist-info/LICENSE,sha256=-B9Dx4t8RlyTldHuUNiV6WYgeXvO0LBO7-C5u9p-HW8,1077
6
- python_pam-2.0.2.dist-info/METADATA,sha256=Kfo6ONqsqoeMn6VqeiqIqod2iRhtvH1tQ3SnSUrGKrI,3454
7
- python_pam-2.0.2.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
8
- python_pam-2.0.2.dist-info/top_level.txt,sha256=0EOjbyc3hQyzjhn6iyMgsEseqA66Xz0p27iBN7G7W1w,4
9
- python_pam-2.0.2.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- pam