python-pam 2.0.2__tar.gz → 2.1.0__tar.gz

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.
@@ -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
+
@@ -1,32 +1,57 @@
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
1
  # python-pam
28
2
 
29
- Python pam module supporting py3 (and py2) for Linux type systems (!windows)
3
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/FirefighterBlu3/python-pam/badge)](https://scorecard.dev/viewer/?uri=github.com/FirefighterBlu3/python-pam)
4
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13794/badge)](https://www.bestpractices.dev/projects/13794)
5
+
6
+ Python pam module supporting py3 for Linux type systems (!windows)
7
+
8
+ ## Security
9
+
10
+ See [SECURITY.md](SECURITY.md) for supported versions and how to report vulnerabilities.
11
+
12
+ ## Threading and concurrency
13
+
14
+ `pam.authenticate()` is safe to call from many threads at once. Each call uses
15
+ its own PAM handle; libpam ctypes bindings are loaded once and shared (no global
16
+ lock on the auth path).
17
+
18
+ Do **not** share a single `PamAuthenticator` / `pam.pam()` instance across threads
19
+ without external synchronization. That object owns mutable PAM session state
20
+ (`handle`, `code`, `reason`, `messages`). For sessions (`call_end=False`), keep
21
+ one instance per thread (or serialize access).
22
+
23
+ High-QPS login APIs should use:
24
+
25
+ ```python
26
+ import pam
27
+
28
+ if pam.authenticate(username, password, service='myapp'):
29
+ ...
30
+ ```
31
+
32
+ ## Credentials (`resetcreds`)
33
+
34
+ After a successful `pam_authenticate` + `pam_acct_mgmt`, `authenticate()` calls
35
+ `pam_setcred(..., PAM_REINITIALIZE_CRED)` when `resetcreds=True` (the default).
36
+
37
+ **Keep the default (`True`)** when this process is acting like a login / credential
38
+ handoff: modules may establish or refresh credentials (e.g. Kerberos), and you care
39
+ about that step succeeding as part of auth.
40
+
41
+ **Set `resetcreds=False`** when you only need to verify a username/password (typical
42
+ web/API “is this password valid?” checks). You are not assuming the user’s identity
43
+ or opening a session; skipping setcred avoids extra module work and avoids treating a
44
+ setcred failure as an authentication failure.
45
+
46
+ ```python
47
+ # Password check only
48
+ pam.authenticate(user, password, service='myapp', resetcreds=False)
49
+
50
+ # Login-style / credential-aware stack (default)
51
+ pam.authenticate(user, password, service='login')
52
+ ```
53
+
54
+ ## Examples
30
55
 
31
56
  Commandline example:
32
57
 
@@ -45,12 +70,10 @@ Close session: Success (0)
45
70
  Inline examples:
46
71
 
47
72
  ```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
73
  >>> import pam
53
- >>> p = pam.authenticate()
74
+ >>> pam.authenticate('david', 'correctpassword')
75
+ True
76
+ >>> p = pam.pam()
54
77
  >>> p.authenticate('david', 'correctpassword')
55
78
  True
56
79
  >>> p.authenticate('david', 'badpassword')
@@ -96,5 +119,3 @@ Replace `good` with the correct password, replace `david` with your appropriate
96
119
  ~$ (echo -ne 'good\0' > /tmp/myfifo & /usr/bin/unix_chkpwd someotheruser nullok < /tmp/myfifo ) ; echo $?
97
120
  9
98
121
  ```
99
-
100
-
@@ -0,0 +1,92 @@
1
+ [project]
2
+ name = "python-pam"
3
+ version = "2.1.0"
4
+ description = ""
5
+ authors = [{name = "David Ford", email = "david@blue-labs.org"}]
6
+ readme = "README.md"
7
+ requires-python = ">=3.11"
8
+ dependencies = []
9
+
10
+ #[project.scripts]
11
+ #lint = "pylint"
12
+ #test = "pytest"
13
+
14
+ [tool.poetry]
15
+ packages = [{include = "pam", from = "python-pam"}]
16
+
17
+ [tool.poetry.group.dev.dependencies]
18
+ pytest = "^9.1.1"
19
+ pytest-cov = "^7.1.0"
20
+ pylint = "^4.0.8"
21
+ tox = "^4.61.4"
22
+ bandit = "*"
23
+ mypy = ">=2.3.1,<3"
24
+
25
+ [tool.poetry.group.test.dependencies]
26
+ pytest = "^9.1.1"
27
+ pytest-cov = "^7.1.0"
28
+ pytest-env = "^1.7.1"
29
+ tox = ">=4.61.4,<5"
30
+ flake8 = "*"
31
+ pylint = ">=4.0.8,<5"
32
+
33
+ [tool.coverage.run]
34
+ source = ["pam"]
35
+ omit = ["*/tests/*", "*/test_*.py"]
36
+
37
+ [tool.coverage.report]
38
+ precision = 2
39
+ show_missing = true
40
+ skip_covered = false
41
+ fail_under = 85.0
42
+
43
+ [tool.bandit]
44
+ exclude_dirs = ["./venv", "./test", ]
45
+ recursive = true
46
+
47
+ [tool.pylint.main]
48
+ jobs = 0
49
+
50
+ [tool.pylint.format]
51
+ max-line-length = 120
52
+
53
+ [tool.pylint."messages control"]
54
+ disable = [
55
+ "too-many-arguments",
56
+ "too-many-positional-arguments",
57
+ "too-many-branches",
58
+ "too-many-statements",
59
+ "too-many-return-statements",
60
+ "too-many-locals",
61
+ "too-many-instance-attributes",
62
+ "too-few-public-methods",
63
+ "wrong-import-position", # Imports are intentionally after version check
64
+ # ctypes symbols are assigned at runtime in PamAuthenticator._ensure_libs
65
+ "not-callable",
66
+ ]
67
+
68
+ [tool.mypy]
69
+ python_version = "3.12"
70
+ warn_return_any = true
71
+ warn_unused_configs = true
72
+ disallow_untyped_defs = false
73
+ disallow_incomplete_defs = false
74
+ check_untyped_defs = true
75
+ no_implicit_optional = true
76
+ warn_redundant_casts = true
77
+ warn_unused_ignores = true
78
+ warn_no_return = true
79
+ strict_equality = true
80
+ ignore_missing_imports = true
81
+ files = ["python-pam/pam"]
82
+
83
+ [[tool.mypy.overrides]]
84
+ module = [
85
+ "ctypes.*",
86
+ "ctypes.util.*",
87
+ ]
88
+ ignore_missing_imports = true
89
+
90
+ [build-system]
91
+ requires = ["poetry-core"]
92
+ build-backend = "poetry.core.masonry.api"
@@ -1,9 +1,9 @@
1
- import sys as __sys
1
+ """PAM (Pluggable Authentication Modules) interface for Python.
2
2
 
3
- if __sys.version_info < (3, ): # pragma: no cover
4
- print('WARNING, Python 2 is EOL and therefore py2 support in this '
5
- "package is deprecated. It won't be actively checked for"
6
- 'correctness')
3
+ This module provides a Python interface to Linux-PAM, allowing authentication
4
+ against system PAM services.
5
+ """
6
+ import sys as __sys
7
7
 
8
8
  # list all the constants and export them
9
9
  from .__internals import PAM_ACCT_EXPIRED
@@ -58,6 +58,9 @@ from .__internals import PAM_USER_UNKNOWN
58
58
  from .__internals import PAM_XDISPLAY
59
59
  from .__internals import PamAuthenticator
60
60
 
61
+ if __sys.version_info < (3, ): # pragma: no cover
62
+ print('WARNING, Python 2 is EOL not supported')
63
+
61
64
  __all__ = [
62
65
  'authenticate',
63
66
  'pam',
@@ -113,25 +116,45 @@ __all__ = [
113
116
  'PAM_XDISPLAY',
114
117
  ]
115
118
 
116
- __PA = None
117
119
 
120
+ def authenticate(
121
+ username: str | bytes,
122
+ password: str | bytes,
123
+ service: str | bytes = 'login',
124
+ env: dict[str, str] | None = None,
125
+ call_end: bool = True,
126
+ encoding: str = 'utf-8',
127
+ resetcreds: bool = True,
128
+ print_failure_messages: bool = False,
129
+ ) -> bool:
130
+ """Authenticate a user against PAM.
131
+
132
+ Creates a fresh ``PamAuthenticator`` for each call so concurrent use from
133
+ multiple threads is safe. libpam ctypes bindings are loaded once and shared.
134
+
135
+ For result codes after auth, use ``PamAuthenticator`` directly::
118
136
 
119
- def authenticate(username,
120
- password,
121
- service='login',
122
- env=None,
123
- call_end=True,
124
- encoding='utf-8',
125
- resetcreds=True,
126
- print_failure_messages=False):
127
- global __PA
137
+ pa = pam.pam()
138
+ ok = pa.authenticate(user, password)
139
+ print(pa.code, pa.reason)
128
140
 
129
- if __PA is None: # pragma: no branch
130
- __PA = PamAuthenticator()
141
+ Args:
142
+ username: Username to authenticate
143
+ password: Password in plain text
144
+ service: PAM service to authenticate against (default: 'login')
145
+ env: Dictionary of environment variables to set
146
+ call_end: Whether to call pam_end() after authentication (default: True)
147
+ encoding: Encoding to use for string conversions (default: 'utf-8')
148
+ resetcreds: Whether to reset credentials after authentication (default: True)
149
+ print_failure_messages: Whether to print failure messages (default: False)
131
150
 
132
- return __PA.authenticate(username, password, service, env, call_end, encoding, resetcreds, print_failure_messages)
151
+ Returns:
152
+ bool: True if authentication succeeded, False otherwise
153
+ """
154
+ return PamAuthenticator().authenticate(
155
+ username, password, service, env, call_end, encoding, resetcreds, print_failure_messages,
156
+ )
133
157
 
134
158
 
135
159
  # legacy implementations used pam.pam()
136
- pam = PamAuthenticator
137
- authenticate.__doc__ = PamAuthenticator.authenticate.__doc__
160
+ pam = PamAuthenticator # noqa: N816, C0103