python-pam 2.0.0__py3-none-any.whl → 2.1.0__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,17 +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(*args, **kvargs):
120
- global __PA
137
+ pa = pam.pam()
138
+ ok = pa.authenticate(user, password)
139
+ print(pa.code, pa.reason)
121
140
 
122
- if __PA is None: # pragma: no branch
123
- __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)
124
150
 
125
- return __PA.authenticate(*args, **kvargs)
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
+ )
126
157
 
127
158
 
128
159
  # legacy implementations used pam.pam()
129
- pam = authenticate
160
+ pam = PamAuthenticator # noqa: N816, C0103
pam/__internals.py CHANGED
@@ -1,22 +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
27
+ from typing import Any
20
28
 
21
29
  PAM_ABORT = 26
22
30
  PAM_ACCT_EXPIRED = 13
@@ -69,6 +77,7 @@ PAM_USER_PROMPT = 9
69
77
  PAM_USER_UNKNOWN = 10
70
78
  PAM_XDISPLAY = 11
71
79
 
80
+
72
81
  __all__ = ('PAM_ABORT', 'PAM_ACCT_EXPIRED', 'PAM_AUTHINFO_UNAVAIL',
73
82
  'PAM_AUTHTOK_DISABLE_AGING', 'PAM_AUTHTOK_ERR',
74
83
  'PAM_AUTHTOK_EXPIRED', 'PAM_AUTHTOK_LOCK_BUSY',
@@ -105,7 +114,7 @@ class PamMessage(Structure):
105
114
  _fields_ = [("msg_style", c_int), ("msg", c_char_p)]
106
115
 
107
116
  def __repr__(self):
108
- return "<PamMessage style: %i, content: %s >" % (self.msg_style, self.msg)
117
+ return f"<PamMessage style: {self.msg_style}, content: {self.msg} >"
109
118
 
110
119
 
111
120
  class PamResponse(Structure):
@@ -113,7 +122,7 @@ class PamResponse(Structure):
113
122
  _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]
114
123
 
115
124
  def __repr__(self):
116
- return "<PamResponse code: %i, content: %s >" % (self.resp_retcode, self.resp)
125
+ return f"<PamResponse code: {self.resp_retcode}, content: {self.resp} >"
117
126
 
118
127
 
119
128
  conv_func = CFUNCTYPE(c_int,
@@ -123,23 +132,31 @@ conv_func = CFUNCTYPE(c_int,
123
132
  c_void_p)
124
133
 
125
134
 
126
- 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:
127
144
  """Simple conversation function that responds to any
128
145
  prompt where the echo is off with the supplied password"""
129
- # Create an array of n_messages response objects
130
-
131
- calloc = libc.calloc
132
- calloc.restype = c_void_p
133
- 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]
134
153
 
135
154
  cpassword = c_char_p(password)
136
155
 
137
- '''
138
- PAM_PROMPT_ECHO_OFF = 1
139
- PAM_PROMPT_ECHO_ON = 2
140
- PAM_ERROR_MSG = 3
141
- PAM_TEXT_INFO = 4
142
- '''
156
+ # PAM_PROMPT_ECHO_OFF = 1
157
+ # PAM_PROMPT_ECHO_ON = 2
158
+ # PAM_ERROR_MSG = 3
159
+ # PAM_TEXT_INFO = 4
143
160
 
144
161
  addr = calloc(n_messages, sizeof(PamResponse))
145
162
  response = cast(addr, POINTER(PamResponse))
@@ -172,103 +189,155 @@ class PamConv(Structure):
172
189
 
173
190
 
174
191
  class PamAuthenticator:
175
- code = 0
176
- reason = 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
177
231
 
178
- def __init__(self):
179
- # use a trick of dlopen(), this effectively becomes
180
- # dlopen("", ...) which opens our own executable. since 'python' has
181
- # a libc dependency, this means libc symbols are already available
182
- # to us
232
+ with cls._lib_lock:
233
+ if cls._libs_ready:
234
+ return
183
235
 
184
- # libc = CDLL(find_library("c"))
185
- libc = cdll.LoadLibrary(None)
186
- 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"))
187
240
 
188
- libpam = CDLL(find_library("pam"))
189
- 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]
190
245
 
191
- self.handle = None
192
- 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
193
266
 
194
- self.calloc = libc.calloc
195
- self.calloc.restype = c_void_p
196
- self.calloc.argtypes = [c_size_t, c_size_t]
197
-
198
- # bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this
199
- # function
200
- if hasattr(libpam, 'pam_end'): # pragma: no branch
201
- self.pam_end = libpam.pam_end
202
- self.pam_end.restype = c_int
203
- self.pam_end.argtypes = [PamHandle, c_int]
204
-
205
- self.pam_start = libpam.pam_start
206
- self.pam_start.restype = c_int
207
- self.pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv),
208
- POINTER(PamHandle)]
209
-
210
- self.pam_acct_mgmt = libpam.pam_acct_mgmt
211
- self.pam_acct_mgmt.restype = c_int
212
- self.pam_acct_mgmt.argtypes = [PamHandle, c_int]
213
-
214
- self.pam_set_item = libpam.pam_set_item
215
- self.pam_set_item.restype = c_int
216
- self.pam_set_item.argtypes = [PamHandle, c_int, c_void_p]
217
-
218
- self.pam_setcred = libpam.pam_setcred
219
- self.pam_strerror = libpam.pam_strerror
220
- self.pam_strerror.restype = c_char_p
221
- self.pam_strerror.argtypes = [PamHandle, c_int]
222
-
223
- self.pam_authenticate = libpam.pam_authenticate
224
- self.pam_authenticate.restype = c_int
225
- self.pam_authenticate.argtypes = [PamHandle, c_int]
226
-
227
- self.pam_open_session = libpam.pam_open_session
228
- self.pam_open_session.restype = c_int
229
- self.pam_open_session.argtypes = [PamHandle, c_int]
230
-
231
- self.pam_close_session = libpam.pam_close_session
232
- self.pam_close_session.restype = c_int
233
- self.pam_close_session.argtypes = [PamHandle, c_int]
234
-
235
- self.pam_putenv = libpam.pam_putenv
236
- self.pam_putenv.restype = c_int
237
- self.pam_putenv.argtypes = [PamHandle, c_char_p]
238
-
239
- if libpam_misc._name: # pragma: no branch
240
- self.pam_misc_setenv = libpam_misc.pam_misc_setenv
241
- self.pam_misc_setenv.restype = c_int
242
- self.pam_misc_setenv.argtypes = [PamHandle, c_char_p, c_char_p,
243
- c_int]
244
-
245
- self.pam_getenv = libpam.pam_getenv
246
- self.pam_getenv.restype = c_char_p
247
- self.pam_getenv.argtypes = [PamHandle, c_char_p]
248
-
249
- self.pam_getenvlist = libpam.pam_getenvlist
250
- self.pam_getenvlist.restype = POINTER(c_char_p)
251
- self.pam_getenvlist.argtypes = [PamHandle]
267
+ cls.pam_strerror = libpam.pam_strerror
268
+ cls.pam_strerror.restype = c_char_p
269
+ cls.pam_strerror.argtypes = [PamHandle, c_int]
270
+
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
252
329
 
253
330
  def authenticate(
254
- self,
255
- username,
256
- password,
257
- service='login',
258
- env=None,
259
- call_end=True,
260
- encoding='utf-8',
261
- resetcreds=True,
262
- print_failure_messages=False):
263
- self.pam_authenticate.__annotations = {'username': str,
264
- 'password': str,
265
- 'service': str,
266
- 'env': dict,
267
- 'call_end': bool,
268
- 'encoding': str,
269
- 'resetcreds': bool,
270
- 'return': bool,
271
- 'print_failure_messages': 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:
272
341
  """username and password authentication for the given service.
273
342
 
274
343
  Returns True for success, or False for failure.
@@ -281,17 +350,22 @@ class PamAuthenticator:
281
350
  necessary conversions using the supplied encoding.
282
351
 
283
352
  Args:
284
- username: username to authenticate
285
- password: password in plain text
286
- service: PAM service to authenticate against, defaults to 'login'
287
- env: Pam environment variables
288
- call_end: call the pam_end() function after (default true)
289
- print_failure_messages: Print messages on failure
353
+ username (str): username to authenticate
354
+ password (str): password in plain text
355
+ service (str): PAM service to authenticate against, defaults to 'login'
356
+ env (dict): Pam environment variables
357
+ call_end (bool): call the pam_end() function after (default true)
358
+ print_failure_messages (bool): Print messages on failure
290
359
 
291
360
  Returns:
292
- success: True
361
+ success: PAM_SUCCESS
293
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.
294
367
  """
368
+ self.messages = []
295
369
 
296
370
  @conv_func
297
371
  def __conv(n_messages, messages, p_response, app_data):
@@ -303,11 +377,11 @@ class PamAuthenticator:
303
377
 
304
378
  return my_conv(n_messages, messages, p_response, self.libc, msg_list, password, encoding)
305
379
 
306
- if isinstance(username, six.text_type):
380
+ if isinstance(username, str):
307
381
  username = username.encode(encoding)
308
- if isinstance(password, six.text_type):
382
+ if isinstance(password, str):
309
383
  password = password.encode(encoding)
310
- if isinstance(service, six.text_type):
384
+ if isinstance(service, str):
311
385
  service = service.encode(encoding)
312
386
 
313
387
  if b'\x00' in username or b'\x00' in password or b'\x00' in service:
@@ -329,9 +403,16 @@ class PamAuthenticator:
329
403
  # This is not an authentication error, something has gone wrong
330
404
  # starting up PAM
331
405
  self.code = retval
332
- self.reason = ("pam_start() failed: %s" %
333
- self.pam_strerror(self.handle, retval))
334
- return 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
415
+ return False
335
416
 
336
417
  # set the TTY, required when pam_securetty is used and the username
337
418
  # root is used note: this is only needed WHEN the pam_securetty.so
@@ -352,66 +433,92 @@ class PamAuthenticator:
352
433
  ctty = os.ttyname(0)
353
434
 
354
435
  # ctty can be invalid if no tty is being used
355
- if ctty: # pragma: no branch
356
- ctty = c_char_p(ctty.encode(encoding))
436
+ if ctty: # pragma: no branch (we don't test a void tty yet)
437
+ ctty_p = c_char_p(ctty.encode(encoding))
357
438
 
358
- retval = self.pam_set_item(self.handle, PAM_TTY, ctty)
359
- retval = self.pam_set_item(self.handle, PAM_XDISPLAY, ctty)
439
+ retval = self.pam_set_item(self.handle, PAM_TTY, ctty_p)
440
+ retval = self.pam_set_item(self.handle, PAM_XDISPLAY, ctty_p)
360
441
 
361
442
  # Set the environment variables if they were supplied
362
443
  if env:
363
444
  if not isinstance(env, dict):
364
445
  raise TypeError('"env" must be a dict')
365
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
+
366
453
  for key, value in env.items():
367
454
  if isinstance(key, bytes) and b'\x00' in key:
368
455
  raise ValueError('"env{}" key cannot contain NULLs')
369
456
  if isinstance(value, bytes) and b'\x00' in value:
370
457
  raise ValueError('"env{}" value cannot contain NULLs')
371
458
 
372
- name_value = "{}={}".format(key, value)
459
+ name_value = f"{key}={value}"
373
460
  retval = self.putenv(name_value, encoding)
374
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
+
375
468
  auth_success = self.pam_authenticate(self.handle, 0)
376
469
 
377
- # skip code coverage, this can only succeed when TEST_* is supplied
378
- # in the environment. we ostensibly know it will work
379
470
  if auth_success == PAM_SUCCESS:
380
- auth_success = self.pam_acct_mgmt(self.handle, 0) # pragma: no cover
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
476
+ auth_success = self.pam_acct_mgmt(self.handle, 0)
381
477
 
382
478
  if auth_success == PAM_SUCCESS and resetcreds:
383
- auth_success = self.pam_setcred(self.handle, PAM_REINITIALIZE_CRED) # pragma: no cover
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
484
+ auth_success = self.pam_setcred(self.handle, PAM_REINITIALIZE_CRED)
384
485
 
385
486
  # store information to inform the caller why we failed
386
487
  self.code = auth_success
387
- self.reason = self.pam_strerror(self.handle, auth_success)
388
-
389
- if sys.version_info >= (3,): # pragma: no branch
390
- self.reason = self.reason.decode(encoding)
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)"
391
497
 
392
- 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
393
499
  self.pam_end(self.handle, auth_success)
394
500
  self.handle = None
395
501
 
396
502
  if print_failure_messages and self.code != PAM_SUCCESS: # pragma: no cover
397
- 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}")
398
505
 
399
- return auth_success
506
+ return bool(auth_success == PAM_SUCCESS)
400
507
 
401
- def end(self):
508
+ def end(self) -> int:
402
509
  """A direct call to pam_end()
403
510
  Returns:
404
511
  Linux-PAM return value as int
405
512
  """
406
- if not self.handle or not hasattr(self, 'pam_end'):
513
+ if not self.handle or self.pam_end is None:
407
514
  return PAM_SYSTEM_ERR
408
515
 
409
516
  retval = self.pam_end(self.handle, self.code)
410
517
  self.handle = None
411
518
 
412
- return retval
519
+ return int(retval)
413
520
 
414
- def open_session(self, encoding='utf-8'):
521
+ def open_session(self, encoding: str = 'utf-8') -> int:
415
522
  """Call pam_open_session as required by the pam_api
416
523
  Returns:
417
524
  Linux-PAM return value as int
@@ -421,14 +528,15 @@ class PamAuthenticator:
421
528
 
422
529
  retval = self.pam_open_session(self.handle, 0)
423
530
  self.code = retval
424
- self.reason = self.pam_strerror(self.handle, retval)
425
-
531
+ reason_bytes = self.pam_strerror(self.handle, retval)
426
532
  if sys.version_info >= (3,): # pragma: no branch
427
- self.reason = self.reason.decode(encoding)
533
+ self.reason = reason_bytes.decode(encoding)
534
+ else:
535
+ self.reason = reason_bytes # type: ignore[assignment]
428
536
 
429
- return retval
537
+ return int(retval)
430
538
 
431
- def close_session(self, encoding='utf-8'):
539
+ def close_session(self, encoding: str = 'utf-8') -> int:
432
540
  """Call pam_close_session as required by the pam_api
433
541
  Returns:
434
542
  Linux-PAM return value as int
@@ -438,14 +546,15 @@ class PamAuthenticator:
438
546
 
439
547
  retval = self.pam_close_session(self.handle, 0)
440
548
  self.code = retval
441
- self.reason = self.pam_strerror(self.handle, retval)
442
-
549
+ reason_bytes = self.pam_strerror(self.handle, retval)
443
550
  if sys.version_info >= (3,): # pragma: no branch
444
- self.reason = self.reason.decode(encoding)
551
+ self.reason = reason_bytes.decode(encoding)
552
+ else:
553
+ self.reason = reason_bytes # type: ignore[assignment]
445
554
 
446
- return retval
555
+ return int(retval)
447
556
 
448
- 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:
449
558
  """A wrapper for the pam_misc_setenv function
450
559
  Args:
451
560
  name: key name of the environment variable
@@ -453,15 +562,16 @@ class PamAuthenticator:
453
562
  Returns:
454
563
  Linux-PAM return value as int
455
564
  """
456
- if not self.handle or not hasattr(self, "pam_misc_setenv"):
565
+ if not self.handle or self.pam_misc_setenv is None:
457
566
  return PAM_SYSTEM_ERR
458
567
 
459
- return self.pam_misc_setenv(self.handle,
460
- name.encode(encoding),
461
- value.encode(encoding),
462
- readonly)
568
+ retval = self.pam_misc_setenv(self.handle,
569
+ name.encode(encoding),
570
+ value.encode(encoding),
571
+ readonly)
572
+ return int(retval)
463
573
 
464
- def putenv(self, name_value, encoding='utf-8'):
574
+ def putenv(self, name_value: str, encoding: str = 'utf-8') -> int:
465
575
  """A wrapper for the pam_putenv function
466
576
  Args:
467
577
  name_value: environment variable in the format KEY=VALUE
@@ -472,46 +582,61 @@ class PamAuthenticator:
472
582
  if not self.handle:
473
583
  return PAM_SYSTEM_ERR
474
584
 
475
- name_value = name_value.encode(encoding)
585
+ name_value_bytes = name_value.encode(encoding)
476
586
 
477
- retval = self.pam_putenv(self.handle, name_value)
587
+ retval = self.pam_putenv(self.handle, name_value_bytes)
478
588
  if retval != PAM_SUCCESS:
479
- 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)
480
595
 
481
- return retval
596
+ return int(retval)
482
597
 
483
- def getenv(self, key, encoding='utf-8'):
598
+ def getenv(self, key: str | bytes, encoding: str = 'utf-8') -> str | None | int:
484
599
  """A wrapper for the pam_getenv function
485
600
  Args:
486
601
  key name of the environment variable
487
602
  Returns:
488
- 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
489
604
  """
490
605
  if not self.handle:
491
606
  return PAM_SYSTEM_ERR
492
607
 
493
608
  # can't happen unless someone is using internals directly
494
- if sys.version_info >= (3, ): # pragma: no branch
495
- if isinstance(key, six.text_type): # pragma: no branch
496
- key = key.encode(encoding)
609
+ if isinstance(key, str): # pragma: no branch
610
+ key = key.encode(encoding)
497
611
 
498
612
  value = self.pam_getenv(self.handle, key)
499
613
 
500
614
  if isinstance(value, type(None)):
501
- return
615
+ return None
502
616
 
503
617
  if isinstance(value, int): # pragma: no cover
504
- 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)
505
622
 
506
623
  if sys.version_info >= (3,): # pragma: no branch
507
- value = value.decode(encoding)
508
-
509
- return value
510
-
511
- 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:
512
637
  """A wrapper for the pam_getenvlist function
513
638
  Returns:
514
- environment as python dictionary
639
+ environment as python dictionary, or PAM_SYSTEM_ERR if handle is invalid
515
640
  """
516
641
  if not self.handle:
517
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.0'
1
+ """Version information for python-pam package."""
2
+ VERSION = '2.1.0'
2
3
  AUTHOR = 'David Ford <david.ford@blue-labs.org>'
3
- RELEASED = '2022 March 10'
4
+ RELEASED = '2026 September 16'
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-pam
3
+ Version: 2.1.0
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=XCnpkS4zsVVwvnbPaAebfpXmy69grDiOLK8EVWFGZa8,148
5
+ python_pam-2.1.0.dist-info/METADATA,sha256=ZXCERT3F0X6N2FJbkA_-mCj0YxIoeQyli15r9iByQtw,4699
6
+ python_pam-2.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
7
+ python_pam-2.1.0.dist-info/licenses/LICENSE,sha256=-B9Dx4t8RlyTldHuUNiV6WYgeXvO0LBO7-C5u9p-HW8,1077
8
+ python_pam-2.1.0.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.0
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=iZGiYU7xldTb15b1P94TbAb3wgUY0SXZBp9Olac8UW4,3824
2
- pam/__internals.py,sha256=Ay6iGazs1m_aio_wMzDezrzaJ8MbLmLLCw6GGEnyJNY,18791
3
- pam/pam.py,sha256=nV_3zY6C58u4xLZ4RqrTNZGvUd-HjJ9Kik3CCY_I8K0,2544
4
- pam/version.py,sha256=o9vrEmrkcCr3Tsbg3NlSajeyTQY9hVEcxpanvPksQwo,94
5
- python_pam-2.0.0.dist-info/LICENSE,sha256=-B9Dx4t8RlyTldHuUNiV6WYgeXvO0LBO7-C5u9p-HW8,1077
6
- python_pam-2.0.0.dist-info/METADATA,sha256=VHjCQV_jZO_SlmQkuqW4WVkgpd2EE9MzFJHkFVDWlfI,3454
7
- python_pam-2.0.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
8
- python_pam-2.0.0.dist-info/top_level.txt,sha256=0EOjbyc3hQyzjhn6iyMgsEseqA66Xz0p27iBN7G7W1w,4
9
- python_pam-2.0.0.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- pam