Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# ChangeLog

## 2.1.0
December 14, 2025
- switch to poetry
- merge outstanding PRs
- make handle checks and make them more robust
- update documentation with better example
- remove py2 support
- tox test suite set to py312 and py313
- fix #40: `pam.authenticate()` no longer reuses a process-global
`PamAuthenticator` (thread-safe concurrent auth); libpam ctypes bindings
are loaded once and shared for performance
- document threading model (do not share one `PamAuthenticator` across threads)

## 2.0.2 Latest
March 17, 2022
Expand Down
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,34 @@
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/FirefighterBlu3/python-pam/badge)](https://scorecard.dev/viewer/?uri=github.com/FirefighterBlu3/python-pam)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13794/badge)](https://www.bestpractices.dev/projects/13794)

Python pam module supporting py3 for Linux type systems (!windows, !py2)
Python pam module supporting py3 for Linux type systems (!windows)

## Security

See [SECURITY.md](SECURITY.md) for supported versions and how to report vulnerabilities.

## Threading and concurrency

`pam.authenticate()` is safe to call from many threads at once. Each call uses
its own PAM handle; libpam ctypes bindings are loaded once and shared (no global
lock on the auth path).

Do **not** share a single `PamAuthenticator` / `pam.pam()` instance across threads
without external synchronization. That object owns mutable PAM session state
(`handle`, `code`, `reason`, `messages`). For sessions (`call_end=False`), keep
one instance per thread (or serialize access).

High-QPS login APIs should use:

```python
import pam

if pam.authenticate(username, password, service='myapp'):
...
```

## Examples

Commandline example:

```bash
Expand All @@ -26,11 +48,9 @@ Close session: Success (0)
Inline examples:

```python
[david@Scott python-pam]$ python
Python 3.9.7 (default, Oct 10 2021, 15:13:22)
[GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pam
>>> pam.authenticate('david', 'correctpassword')
True
>>> p = pam.pam()
>>> p.authenticate('david', 'correctpassword')
True
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,13 @@ disable = [
"too-many-positional-arguments",
"too-many-branches",
"too-many-statements",
"too-many-return-statements",
"too-many-locals",
"too-many-instance-attributes",
"too-few-public-methods",
"wrong-import-position", # Imports are intentionally after version check
# ctypes symbols are assigned at runtime in PamAuthenticator._ensure_libs
"not-callable",
]

[tool.mypy]
Expand Down
22 changes: 11 additions & 11 deletions python-pam/pam/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@
'PAM_XDISPLAY',
]

__PA: PamAuthenticator | None = None


def authenticate(
username: str | bytes,
Expand All @@ -131,8 +129,14 @@ def authenticate(
) -> bool:
"""Authenticate a user against PAM.

This is a convenience function that creates a PamAuthenticator instance
(reusing a global instance if available) and calls its authenticate method.
Creates a fresh ``PamAuthenticator`` for each call so concurrent use from
multiple threads is safe. libpam ctypes bindings are loaded once and shared.

For result codes after auth, use ``PamAuthenticator`` directly::

pa = pam.pam()
ok = pa.authenticate(user, password)
print(pa.code, pa.reason)

Args:
username: Username to authenticate
Expand All @@ -147,14 +151,10 @@ def authenticate(
Returns:
bool: True if authentication succeeded, False otherwise
"""
global __PA # noqa: W0603, PLW0603

if __PA is None: # pragma: no branch
__PA = PamAuthenticator()

return __PA.authenticate(username, password, service, env, call_end, encoding, resetcreds, print_failure_messages)
return PamAuthenticator().authenticate(
username, password, service, env, call_end, encoding, resetcreds, print_failure_messages,
)


# legacy implementations used pam.pam()
pam = PamAuthenticator # noqa: N816, C0103
authenticate.__doc__ = PamAuthenticator.authenticate.__doc__
Loading