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 +43 -20
- pam/__internals.py +296 -162
- pam/pam.py +20 -13
- pam/version.py +3 -2
- python_pam-2.1.1.dist-info/METADATA +137 -0
- python_pam-2.1.1.dist-info/RECORD +8 -0
- {python_pam-2.0.2.dist-info → python_pam-2.1.1.dist-info}/WHEEL +1 -2
- python_pam-2.0.2.dist-info/METADATA +0 -100
- python_pam-2.0.2.dist-info/RECORD +0 -9
- python_pam-2.0.2.dist-info/top_level.txt +0 -1
- {python_pam-2.0.2.dist-info → python_pam-2.1.1.dist-info/licenses}/LICENSE +0 -0
pam/__init__.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
"""PAM (Pluggable Authentication Modules) interface for Python.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
from ctypes import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
#
|
|
132
|
-
|
|
133
|
-
calloc
|
|
134
|
-
calloc
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
self.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
self.
|
|
234
|
-
self.
|
|
235
|
-
|
|
236
|
-
self.
|
|
237
|
-
self.
|
|
238
|
-
self.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
self.pam_getenv =
|
|
247
|
-
self.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
self.
|
|
251
|
-
self.
|
|
252
|
-
self.
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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,
|
|
380
|
+
if isinstance(username, str):
|
|
300
381
|
username = username.encode(encoding)
|
|
301
|
-
if isinstance(password,
|
|
382
|
+
if isinstance(password, str):
|
|
302
383
|
password = password.encode(encoding)
|
|
303
|
-
if isinstance(service,
|
|
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 =
|
|
326
|
-
|
|
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 = "{}={}"
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
416
|
-
|
|
531
|
+
reason_bytes = self.pam_strerror(self.handle, retval)
|
|
417
532
|
if sys.version_info >= (3,): # pragma: no branch
|
|
418
|
-
self.reason =
|
|
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
|
-
|
|
433
|
-
|
|
549
|
+
reason_bytes = self.pam_strerror(self.handle, retval)
|
|
434
550
|
if sys.version_info >= (3,): # pragma: no branch
|
|
435
|
-
self.reason =
|
|
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
|
|
565
|
+
if not self.handle or self.pam_misc_setenv is None:
|
|
448
566
|
return PAM_SYSTEM_ERR
|
|
449
567
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
585
|
+
name_value_bytes = name_value.encode(encoding)
|
|
467
586
|
|
|
468
|
-
retval = self.pam_putenv(self.handle,
|
|
587
|
+
retval = self.pam_putenv(self.handle, name_value_bytes)
|
|
469
588
|
if retval != PAM_SUCCESS:
|
|
470
|
-
|
|
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
|
|
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
|
|
486
|
-
|
|
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
|
-
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
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
|
-
|
|
46
|
+
user_input = input(prompt) # nosec (bandit; python2)
|
|
47
47
|
|
|
48
48
|
readline.set_pre_input_hook()
|
|
49
49
|
|
|
50
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
@@ -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
|
+
[](https://scorecard.dev/viewer/?uri=github.com/FirefighterBlu3/python-pam)
|
|
19
|
+
[](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,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
|
|
File without changes
|