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 +43 -12
- pam/__internals.py +311 -186
- pam/pam.py +20 -13
- pam/version.py +3 -2
- python_pam-2.1.0.dist-info/METADATA +137 -0
- python_pam-2.1.0.dist-info/RECORD +8 -0
- {python_pam-2.0.0.dist-info → python_pam-2.1.0.dist-info}/WHEEL +1 -2
- python_pam-2.0.0.dist-info/METADATA +0 -100
- python_pam-2.0.0.dist-info/RECORD +0 -9
- python_pam-2.0.0.dist-info/top_level.txt +0 -1
- {python_pam-2.0.0.dist-info → python_pam-2.1.0.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,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
|
-
|
|
120
|
-
|
|
137
|
+
pa = pam.pam()
|
|
138
|
+
ok = pa.authenticate(user, password)
|
|
139
|
+
print(pa.code, pa.reason)
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
#
|
|
130
|
-
|
|
131
|
-
calloc =
|
|
132
|
-
calloc
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
self.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
self.
|
|
237
|
-
self.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
self.
|
|
246
|
-
self.
|
|
247
|
-
self.
|
|
248
|
-
|
|
249
|
-
self.
|
|
250
|
-
self.pam_getenvlist
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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:
|
|
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,
|
|
380
|
+
if isinstance(username, str):
|
|
307
381
|
username = username.encode(encoding)
|
|
308
|
-
if isinstance(password,
|
|
382
|
+
if isinstance(password, str):
|
|
309
383
|
password = password.encode(encoding)
|
|
310
|
-
if isinstance(service,
|
|
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 =
|
|
333
|
-
|
|
334
|
-
return
|
|
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
|
-
|
|
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,
|
|
359
|
-
retval = self.pam_set_item(self.handle, PAM_XDISPLAY,
|
|
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 = "{}={}"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
425
|
-
|
|
531
|
+
reason_bytes = self.pam_strerror(self.handle, retval)
|
|
426
532
|
if sys.version_info >= (3,): # pragma: no branch
|
|
427
|
-
self.reason =
|
|
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
|
-
|
|
442
|
-
|
|
549
|
+
reason_bytes = self.pam_strerror(self.handle, retval)
|
|
443
550
|
if sys.version_info >= (3,): # pragma: no branch
|
|
444
|
-
self.reason =
|
|
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
|
|
565
|
+
if not self.handle or self.pam_misc_setenv is None:
|
|
457
566
|
return PAM_SYSTEM_ERR
|
|
458
567
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
585
|
+
name_value_bytes = name_value.encode(encoding)
|
|
476
586
|
|
|
477
|
-
retval = self.pam_putenv(self.handle,
|
|
587
|
+
retval = self.pam_putenv(self.handle, name_value_bytes)
|
|
478
588
|
if retval != PAM_SUCCESS:
|
|
479
|
-
|
|
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
|
|
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
|
|
495
|
-
|
|
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
|
-
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
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.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
|
+
[](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=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,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
|
|
File without changes
|