├── .github └── workflows │ └── main.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ChangeLog ├── LICENSE ├── MANIFEST.in ├── Makefile ├── PKGBUILD ├── README.md ├── pyproject.toml ├── python-pam.sublime-project ├── requirements.txt ├── setup.cfg ├── src └── pam │ ├── __init__.py │ ├── __internals.py │ ├── pam.py │ └── version.py └── tests ├── __init__.py ├── test_internals.py └── test_pam.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.10"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install tox tox-gh-actions 23 | - name: Tox test 24 | run: | 25 | tox -vv 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # workspace files are user-specific 56 | *.sublime-workspace 57 | 58 | # project files should be checked into the repository, unless a significant 59 | # proportion of contributors will probably not be using SublimeText 60 | # *.sublime-project 61 | 62 | #sftp configuration file 63 | sftp-config.json 64 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at david@blue-labs.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please adhere to PEP 8 as best appropriate. Code committed must be 2 | submitted under the existing project license. 3 | 4 | This project is switching to a git-flow style, please make pull requests 5 | against the `develop` branch. 6 | 7 | Good reading 8 | * https://packaging.python.org/en/latest/tutorials/packaging-projects/ 9 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Start of ChangeLog 2 | 3 | 2019-11-12 v1.8.5 4 | use pam_set_item() to set PAM_TTY for pam_securetty module 5 | add a bunch of tools for code quality 6 | refactored the class slightly so the module can be imported and passive at 7 | runtime until authentication is actually needed 8 | 9 | 2018-6-15 v1.8.4 10 | include LICENSE file as some distributions rely on the presence of it 11 | rather than extracting from setup.py 12 | 13 | 2018-3-22 v1.8.3 14 | add a test for the existence libpam.pam_end function 15 | 16 | 2014-11-17 v1.8.2 17 | add MANIFEST.in so README.md gets included for pypi (pip installs) 18 | 19 | 2014-8-4 v1.8.1 20 | adapt, add files, package up for PyPi 21 | adapt, add files, package up for github 22 | adapt, add files, package up for ArchLinux 23 | 24 | Start of forked copy from Chris AtLee 2011-Dec 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 David Ford 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VIRTUALENV = $(shell which virtualenv) 2 | PYTHONEXEC = python 3 | 4 | VERSION = `grep VERSION src/pam/version.py | cut -d \' -f2` 5 | 6 | build: pydeps 7 | python -m build 8 | 9 | clean: 10 | rm -rf *.egg-info/ 11 | rm -rf .cache/ 12 | rm -rf .tox/ 13 | rm -rf .coverage 14 | rm -rf build 15 | rm -rf dist 16 | rm -rf htmlcov 17 | rm -rf venv 18 | find . -type d -name '__pycache__' | xargs rm -rf 19 | find . -name "*.pyc" -type f -print0 | xargs -0 /bin/rm -rf 20 | 21 | compile: 22 | . venv/bin/activate; python setup.py build install 23 | 24 | console: 25 | . venv/bin/activate; python 26 | 27 | coverage: 28 | . venv/bin/activate; coverage html 29 | 30 | current: 31 | @echo $(VERSION) 32 | 33 | deps: 34 | . venv/bin/activate; python -m pip install --upgrade -qr requirements.txt 35 | 36 | install: clean venv deps 37 | . venv/bin/activate; pip install --use-pep517 --progress-bar emoji 38 | 39 | inspectortiger: pydeps 40 | . venv/bin/activate; inspectortiger src/pam/ 41 | 42 | lint: pydeps 43 | . venv/bin/activate; python -m flake8 src/pam/ --max-line-length=120 44 | 45 | preflight: bandit test 46 | 47 | publish-pypi-test: clean venv build 48 | . venv/bin/activate; \ 49 | python3 -m pip install --upgrade twine && \ 50 | python3 -m twine upload --repository testpypi dist/* 51 | 52 | publish-pypi: clean venv build 53 | . venv/bin/activate; \ 54 | python3 -m pip install --upgrade twine && \ 55 | python3 -m twine upload --repository pypi dist/* 56 | 57 | pydeps: 58 | . venv/bin/activate; \ 59 | pip install --upgrade -q pip && \ 60 | pip install --upgrade -q pip build 61 | 62 | test: tox 63 | 64 | tox: 65 | rm -fr .tox 66 | . venv/bin/activate; tox 67 | 68 | venv: 69 | $(VIRTUALENV) -p $(PYTHONEXEC) venv 70 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: David Ford 2 | pkgname=python-pam 3 | pkgver=2.0.0rc1 4 | pkgrel=2 5 | pkgdesc="Linux, FreeBSD, etc (any system that uses PAM) PAM module that provides an 6 | authenticate function given a username, password, and other optional keywords." 7 | arch=('any') 8 | url="https://github.com/FirefighterBlu3/python-pam" 9 | license=('MIT') 10 | depends=('python' 'pam') 11 | makedepends=('python-setuptools') 12 | options=(!emptydirs) 13 | changelog=(ChangeLog) 14 | source=(https://pypi.python.org/packages/source/p/${pkgname}/${pkgname}-${pkgver}.tar.gz) 15 | md5sums=(db71b6b999246fb05d78ecfbe166629d) 16 | 17 | package() { 18 | cd "$pkgname-$pkgver" 19 | python setup.py install --root="$pkgdir/" --optimize=1 20 | } 21 | 22 | # vim:set ts=2 sw=2 et: 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-pam 2 | 3 | Python pam module supporting py3 (and py2) for Linux type systems (!windows) 4 | 5 | Commandline example: 6 | 7 | ```bash 8 | [david@Scott python-pam]$ python pam/pam.py 9 | Username: david 10 | Password: 11 | Auth result: Success (0) 12 | Pam Environment List item: XDG_SEAT=seat0 13 | Pam Environment item: XDG_SEAT=seat0 14 | Missing Pam Environment item: asdf=None 15 | Open session: Success (0) 16 | Close session: Success (0) 17 | ``` 18 | 19 | Inline examples: 20 | 21 | ```python 22 | [david@Scott python-pam]$ python 23 | Python 3.9.7 (default, Oct 10 2021, 15:13:22) 24 | [GCC 11.1.0] on linux 25 | Type "help", "copyright", "credits" or "license" for more information. 26 | >>> import pam 27 | >>> p = pam.authenticate() 28 | >>> p.authenticate('david', 'correctpassword') 29 | True 30 | >>> p.authenticate('david', 'badpassword') 31 | False 32 | >>> p.authenticate('david', 'correctpassword', service='login') 33 | True 34 | >>> p.authenticate('david', 'correctpassword', service='unknownservice') 35 | False 36 | >>> p.authenticate('david', 'correctpassword', service='login', resetcreds=True) 37 | True 38 | >>> p.authenticate('david', 'correctpassword', encoding='latin-1') 39 | True 40 | >>> print('{} {}'.format(p.code, p.reason)) 41 | 0 Success 42 | >>> p.authenticate('david', 'badpassword') 43 | False 44 | >>> print('{} {}'.format(p.code, p.reason)) 45 | 7 Authentication failure 46 | >>> 47 | ``` 48 | 49 | ## Authentication and privileges 50 | Please note, python-pam and *all* tools that do authentication follow two rules: 51 | 52 | * You have root (or privileged access): you can check any account's password for validity 53 | * You don't have root: you can only check the validity of the username running the tool 54 | 55 | 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. 56 | 57 | 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: 58 | 59 | Replace `good` with the correct password, replace `david` with your appropriate username. 60 | 61 | ``` 62 | ~$ mkfifo /tmp/myfifo 63 | 64 | ~$ (echo -ne 'good\0' > /tmp/myfifo & /usr/bin/unix_chkpwd david nullok < /tmp/myfifo ) ; echo $? 65 | 0 66 | 67 | ~$ (echo -ne 'bad\0' > /tmp/myfifo & /usr/bin/unix_chkpwd david nullok < /tmp/myfifo ) ; echo $? 68 | 7 69 | 70 | ~$ (echo -ne 'good\0' > /tmp/myfifo & /usr/bin/unix_chkpwd someotheruser nullok < /tmp/myfifo ) ; echo $? 71 | 9 72 | ``` 73 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | 'setuptools>=44', 4 | 'wheel>=0.30.0', 5 | 'six', 6 | ] 7 | build-backend = 'setuptools.build_meta' 8 | 9 | # ignore the tox documentation, it IS NOT supported yet 10 | # https://github.com/tox-dev/tox/issues/2148 11 | #[tox] 12 | #isolated_build = true 13 | 14 | [tool.tox] 15 | legacy_tox_ini = """ 16 | [tox] 17 | envlist = py310 18 | isolated_build = true 19 | #skipsdist = true 20 | 21 | [gh-actions] 22 | python = 23 | 3.7: py37 24 | 3.8: py38 25 | 3.9: py39 26 | 3.10: py310 27 | 28 | [testenv] 29 | basepython = python3.10 30 | passenv = * 31 | deps = 32 | bandit 33 | flake8 34 | mypy 35 | types-six 36 | coverage 37 | pytest-cov 38 | pytest 39 | -rrequirements.txt 40 | 41 | commands = 42 | flake8 src/pam/ 43 | mypy 44 | bandit -r src -c "pyproject.toml" 45 | pytest --cov -r w --capture=sys -vvv --cov-report=html 46 | """ 47 | 48 | [tool.bandit] 49 | exclude_dirs = ["./venv", "./test", ] 50 | recursive = true 51 | 52 | [tool.mypy] 53 | files = ["src/pam/__init__.py", "src/pam/__internals.py"] 54 | ignore_missing_imports = true 55 | 56 | [tool.pytest] 57 | python_files = "test_*.py" 58 | norecursedirs = ".tox" 59 | 60 | [tool.coverage.run] 61 | branch = true 62 | # awkward how I can include "pam" but I have to be incredibly specific when omitting 63 | source = ["pam"] 64 | omit = ["*/pam/pam.py", "*/pam/version.py",] 65 | 66 | [tool.coverage.html] 67 | directory = "htmlcov" 68 | 69 | [tool.coverage.report] 70 | skip_empty = true 71 | fail_under = 100 72 | -------------------------------------------------------------------------------- /python-pam.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": "." 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | toml 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = python-pam 3 | version = attr: pam.version.VERSION 4 | author = David Ford 5 | author_email = david@blue-labs.org 6 | description = Python PAM module using ctypes, py3 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license = License :: OSI Approved :: MIT License 10 | url = https://github.com/FirefighterBlu3/python-pam 11 | project_urls = 12 | Bug Tracker = https://github.com/FirefighterBlu3/python-pam/issues 13 | classifiers = 14 | Development Status :: 6 - Mature 15 | Environment :: Plugins 16 | Intended Audience :: Developers 17 | Intended Audience :: Information Technology 18 | Intended Audience :: System Administrators 19 | License :: OSI Approved :: MIT License 20 | Operating System :: POSIX 21 | Operating System :: POSIX :: Linux 22 | Programming Language :: Python 23 | Programming Language :: Python :: 2 24 | Programming Language :: Python :: 3 25 | Topic :: Security 26 | Topic :: System :: Systems Administration :: Authentication/Directory 27 | 28 | [options] 29 | packages = find: 30 | package_dir = 31 | = src 32 | 33 | [options.packages.find] 34 | where = src 35 | 36 | [sdist] 37 | keep_temp = 1 38 | 39 | [build_ext] 40 | debug = 1 41 | 42 | [flake8] 43 | max-line-length = 120 44 | 45 | -------------------------------------------------------------------------------- /src/pam/__init__.py: -------------------------------------------------------------------------------- 1 | import sys as __sys 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') 7 | 8 | # list all the constants and export them 9 | from .__internals import PAM_ACCT_EXPIRED 10 | from .__internals import PAM_AUTHINFO_UNAVAIL 11 | from .__internals import PAM_AUTHTOK_DISABLE_AGING 12 | from .__internals import PAM_AUTHTOK_ERR 13 | from .__internals import PAM_ABORT 14 | from .__internals import PAM_AUTHTOK_EXPIRED 15 | from .__internals import PAM_AUTHTOK_LOCK_BUSY 16 | from .__internals import PAM_AUTHTOK_RECOVER_ERR 17 | from .__internals import PAM_AUTH_ERR 18 | from .__internals import PAM_BAD_ITEM 19 | from .__internals import PAM_BUF_ERR 20 | from .__internals import PAM_CHANGE_EXPIRED_AUTHTOK 21 | from .__internals import PAM_CONV 22 | from .__internals import PAM_CONV_ERR 23 | from .__internals import PAM_CRED_ERR 24 | from .__internals import PAM_CRED_EXPIRED 25 | from .__internals import PAM_CRED_INSUFFICIENT 26 | from .__internals import PAM_CRED_UNAVAIL 27 | from .__internals import PAM_DATA_SILENT 28 | from .__internals import PAM_DELETE_CRED 29 | from .__internals import PAM_DISALLOW_NULL_AUTHTOK 30 | from .__internals import PAM_ERROR_MSG 31 | from .__internals import PAM_ESTABLISH_CRED 32 | from .__internals import PAM_IGNORE 33 | from .__internals import PAM_MAXTRIES 34 | from .__internals import PAM_MODULE_UNKNOWN 35 | from .__internals import PAM_NEW_AUTHTOK_REQD 36 | from .__internals import PAM_NO_MODULE_DATA 37 | from .__internals import PAM_OPEN_ERR 38 | from .__internals import PAM_PERM_DENIED 39 | from .__internals import PAM_PROMPT_ECHO_OFF 40 | from .__internals import PAM_PROMPT_ECHO_ON 41 | from .__internals import PAM_REFRESH_CRED 42 | from .__internals import PAM_REINITIALIZE_CRED 43 | from .__internals import PAM_RHOST 44 | from .__internals import PAM_RUSER 45 | from .__internals import PAM_SERVICE 46 | from .__internals import PAM_SERVICE_ERR 47 | from .__internals import PAM_SESSION_ERR 48 | from .__internals import PAM_SILENT 49 | from .__internals import PAM_SUCCESS 50 | from .__internals import PAM_SYMBOL_ERR 51 | from .__internals import PAM_SYSTEM_ERR 52 | from .__internals import PAM_TEXT_INFO 53 | from .__internals import PAM_TRY_AGAIN 54 | from .__internals import PAM_TTY 55 | from .__internals import PAM_USER 56 | from .__internals import PAM_USER_PROMPT 57 | from .__internals import PAM_USER_UNKNOWN 58 | from .__internals import PAM_XDISPLAY 59 | from .__internals import PamAuthenticator 60 | 61 | __all__ = [ 62 | 'authenticate', 63 | 'pam', 64 | 'PAM_ACCT_EXPIRED', 65 | 'PAM_AUTHINFO_UNAVAIL', 66 | 'PAM_AUTHTOK_DISABLE_AGING', 67 | 'PAM_AUTHTOK_ERR', 68 | 'PAM_ABORT', 69 | 'PAM_AUTHTOK_EXPIRED', 70 | 'PAM_AUTHTOK_LOCK_BUSY', 71 | 'PAM_AUTHTOK_RECOVER_ERR', 72 | 'PAM_AUTH_ERR', 73 | 'PAM_BAD_ITEM', 74 | 'PAM_BUF_ERR', 75 | 'PAM_CHANGE_EXPIRED_AUTHTOK', 76 | 'PAM_CONV', 77 | 'PAM_CONV_ERR', 78 | 'PAM_CRED_ERR', 79 | 'PAM_CRED_EXPIRED', 80 | 'PAM_CRED_INSUFFICIENT', 81 | 'PAM_CRED_UNAVAIL', 82 | 'PAM_DATA_SILENT', 83 | 'PAM_DELETE_CRED', 84 | 'PAM_DISALLOW_NULL_AUTHTOK', 85 | 'PAM_ERROR_MSG', 86 | 'PAM_ESTABLISH_CRED', 87 | 'PAM_IGNORE', 88 | 'PAM_MAXTRIES', 89 | 'PAM_MODULE_UNKNOWN', 90 | 'PAM_NEW_AUTHTOK_REQD', 91 | 'PAM_NO_MODULE_DATA', 92 | 'PAM_OPEN_ERR', 93 | 'PAM_PERM_DENIED', 94 | 'PAM_PROMPT_ECHO_OFF', 95 | 'PAM_PROMPT_ECHO_ON', 96 | 'PAM_REFRESH_CRED', 97 | 'PAM_REINITIALIZE_CRED', 98 | 'PAM_RHOST', 99 | 'PAM_RUSER', 100 | 'PAM_SERVICE', 101 | 'PAM_SERVICE_ERR', 102 | 'PAM_SESSION_ERR', 103 | 'PAM_SILENT', 104 | 'PAM_SUCCESS', 105 | 'PAM_SYMBOL_ERR', 106 | 'PAM_SYSTEM_ERR', 107 | 'PAM_TEXT_INFO', 108 | 'PAM_TRY_AGAIN', 109 | 'PAM_TTY', 110 | 'PAM_USER', 111 | 'PAM_USER_PROMPT', 112 | 'PAM_USER_UNKNOWN', 113 | 'PAM_XDISPLAY', 114 | ] 115 | 116 | __PA = None 117 | 118 | 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 128 | 129 | if __PA is None: # pragma: no branch 130 | __PA = PamAuthenticator() 131 | 132 | return __PA.authenticate(username, password, service, env, call_end, encoding, resetcreds, print_failure_messages) 133 | 134 | 135 | # legacy implementations used pam.pam() 136 | pam = PamAuthenticator 137 | authenticate.__doc__ = PamAuthenticator.authenticate.__doc__ 138 | -------------------------------------------------------------------------------- /src/pam/__internals.py: -------------------------------------------------------------------------------- 1 | import os 2 | import six 3 | import sys 4 | from ctypes import cdll 5 | from ctypes import CFUNCTYPE 6 | from ctypes import CDLL 7 | from ctypes import POINTER 8 | from ctypes import Structure 9 | from ctypes import byref 10 | from ctypes import cast 11 | from ctypes import sizeof 12 | from ctypes import py_object 13 | from ctypes import c_char 14 | from ctypes import c_char_p 15 | from ctypes import c_int 16 | from ctypes import c_size_t 17 | from ctypes import c_void_p 18 | from ctypes import memmove 19 | from ctypes.util import find_library 20 | from typing import Union 21 | 22 | PAM_ABORT = 26 23 | PAM_ACCT_EXPIRED = 13 24 | PAM_AUTHINFO_UNAVAIL = 9 25 | PAM_AUTHTOK_DISABLE_AGING = 23 26 | PAM_AUTHTOK_ERR = 20 27 | PAM_AUTHTOK_EXPIRED = 27 28 | PAM_AUTHTOK_LOCK_BUSY = 22 29 | PAM_AUTHTOK_RECOVER_ERR = 21 30 | PAM_AUTH_ERR = 7 31 | PAM_BAD_ITEM = 29 32 | PAM_BUF_ERR = 5 33 | PAM_CHANGE_EXPIRED_AUTHTOK = 32 34 | PAM_CONV = 5 35 | PAM_CONV_ERR = 19 36 | PAM_CRED_ERR = 17 37 | PAM_CRED_EXPIRED = 16 38 | PAM_CRED_INSUFFICIENT = 8 39 | PAM_CRED_UNAVAIL = 15 40 | PAM_DATA_SILENT = 1073741824 41 | PAM_DELETE_CRED = 4 42 | PAM_DISALLOW_NULL_AUTHTOK = 1 43 | PAM_ERROR_MSG = 3 44 | PAM_ESTABLISH_CRED = 2 45 | PAM_IGNORE = 25 46 | PAM_MAXTRIES = 11 47 | PAM_MODULE_UNKNOWN = 28 48 | PAM_NEW_AUTHTOK_REQD = 12 49 | PAM_NO_MODULE_DATA = 18 50 | PAM_OPEN_ERR = 1 51 | PAM_PERM_DENIED = 6 52 | PAM_PROMPT_ECHO_OFF = 1 53 | PAM_PROMPT_ECHO_ON = 2 54 | PAM_REFRESH_CRED = 16 55 | PAM_REINITIALIZE_CRED = 8 56 | PAM_RHOST = 4 57 | PAM_RUSER = 8 58 | PAM_SERVICE = 1 59 | PAM_SERVICE_ERR = 3 60 | PAM_SESSION_ERR = 14 61 | PAM_SILENT = 32768 62 | PAM_SUCCESS = 0 63 | PAM_SYMBOL_ERR = 2 64 | PAM_SYSTEM_ERR = 4 65 | PAM_TEXT_INFO = 4 66 | PAM_TRY_AGAIN = 24 67 | PAM_TTY = 3 68 | PAM_USER = 2 69 | PAM_USER_PROMPT = 9 70 | PAM_USER_UNKNOWN = 10 71 | PAM_XDISPLAY = 11 72 | 73 | 74 | __all__ = ('PAM_ABORT', 'PAM_ACCT_EXPIRED', 'PAM_AUTHINFO_UNAVAIL', 75 | 'PAM_AUTHTOK_DISABLE_AGING', 'PAM_AUTHTOK_ERR', 76 | 'PAM_AUTHTOK_EXPIRED', 'PAM_AUTHTOK_LOCK_BUSY', 77 | 'PAM_AUTHTOK_RECOVER_ERR', 'PAM_AUTH_ERR', 'PAM_BAD_ITEM', 78 | 'PAM_BUF_ERR', 'PAM_CHANGE_EXPIRED_AUTHTOK', 'PAM_CONV', 79 | 'PAM_CONV_ERR', 'PAM_CRED_ERR', 'PAM_CRED_EXPIRED', 80 | 'PAM_CRED_INSUFFICIENT', 'PAM_CRED_UNAVAIL', 'PAM_DATA_SILENT', 81 | 'PAM_DELETE_CRED', 'PAM_DISALLOW_NULL_AUTHTOK', 'PAM_ERROR_MSG', 82 | 'PAM_ESTABLISH_CRED', 'PAM_IGNORE', 'PAM_MAXTRIES', 83 | 'PAM_MODULE_UNKNOWN', 'PAM_NEW_AUTHTOK_REQD', 'PAM_NO_MODULE_DATA', 84 | 'PAM_OPEN_ERR', 'PAM_PERM_DENIED', 'PAM_PROMPT_ECHO_OFF', 85 | 'PAM_PROMPT_ECHO_ON', 'PAM_REFRESH_CRED', 'PAM_REINITIALIZE_CRED', 86 | 'PAM_RHOST', 'PAM_RUSER', 'PAM_SERVICE', 'PAM_SERVICE_ERR', 87 | 'PAM_SESSION_ERR', 'PAM_SILENT', 'PAM_SUCCESS', 'PAM_SYMBOL_ERR', 88 | 'PAM_SYSTEM_ERR', 'PAM_TEXT_INFO', 'PAM_TRY_AGAIN', 'PAM_TTY', 89 | 'PAM_USER', 'PAM_USER_PROMPT', 'PAM_USER_UNKNOWN', 90 | 'PamAuthenticator') 91 | 92 | 93 | class PamHandle(Structure): 94 | """wrapper class for pam_handle_t pointer""" 95 | _fields_ = [("handle", c_void_p)] 96 | 97 | def __init__(self): 98 | super().__init__() 99 | self.handle = 0 100 | 101 | def __repr__(self): 102 | return f"" 103 | 104 | 105 | class PamMessage(Structure): 106 | """wrapper class for pam_message structure""" 107 | _fields_ = [("msg_style", c_int), ("msg", c_char_p)] 108 | 109 | def __repr__(self): 110 | return "" % (self.msg_style, self.msg) 111 | 112 | 113 | class PamResponse(Structure): 114 | """wrapper class for pam_response structure""" 115 | _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)] 116 | 117 | def __repr__(self): 118 | return "" % (self.resp_retcode, self.resp) 119 | 120 | 121 | conv_func = CFUNCTYPE(c_int, 122 | c_int, 123 | POINTER(POINTER(PamMessage)), 124 | POINTER(POINTER(PamResponse)), 125 | c_void_p) 126 | 127 | 128 | def my_conv(n_messages, messages, p_response, libc, msg_list: list, password: bytes, encoding: str): 129 | """Simple conversation function that responds to any 130 | prompt where the echo is off with the supplied password""" 131 | # Create an array of n_messages response objects 132 | calloc = libc.calloc 133 | calloc.restype = c_void_p 134 | calloc.argtypes = [c_size_t, c_size_t] 135 | 136 | cpassword = c_char_p(password) 137 | 138 | ''' 139 | PAM_PROMPT_ECHO_OFF = 1 140 | PAM_PROMPT_ECHO_ON = 2 141 | PAM_ERROR_MSG = 3 142 | PAM_TEXT_INFO = 4 143 | ''' 144 | 145 | addr = calloc(n_messages, sizeof(PamResponse)) 146 | response = cast(addr, POINTER(PamResponse)) 147 | p_response[0] = response 148 | 149 | for i in range(n_messages): 150 | message = messages[i].contents.msg 151 | if sys.version_info >= (3,): # pragma: no branch 152 | message = message.decode(encoding) 153 | 154 | msg_list.append(message) 155 | 156 | if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF: 157 | if i == 0: 158 | dst = calloc(len(password)+1, sizeof(c_char)) 159 | memmove(dst, cpassword, len(password)) 160 | response[i].resp = dst 161 | else: 162 | # void out the message 163 | response[i].resp = None 164 | 165 | response[i].resp_retcode = 0 166 | 167 | return PAM_SUCCESS 168 | 169 | 170 | class PamConv(Structure): 171 | """wrapper class for pam_conv structure""" 172 | _fields_ = [("conv", conv_func), ("appdata_ptr", c_void_p)] 173 | 174 | 175 | class PamAuthenticator: 176 | code = 0 177 | reason = None # type: Union[str, bytes, None] 178 | 179 | def __init__(self): 180 | # use a trick of dlopen(), this effectively becomes 181 | # dlopen("", ...) which opens our own executable. since 'python' has 182 | # a libc dependency, this means libc symbols are already available 183 | # to us 184 | 185 | # libc = CDLL(find_library("c")) 186 | libc = cdll.LoadLibrary(None) 187 | self.libc = libc 188 | 189 | libpam = CDLL(find_library("pam")) 190 | libpam_misc = CDLL(find_library("pam_misc")) 191 | 192 | self.handle = None 193 | self.messages = [] 194 | 195 | self.calloc = libc.calloc 196 | self.calloc.restype = c_void_p 197 | self.calloc.argtypes = [c_size_t, c_size_t] 198 | 199 | # bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this 200 | # function 201 | if hasattr(libpam, 'pam_end'): # pragma: no branch 202 | self.pam_end = libpam.pam_end 203 | self.pam_end.restype = c_int 204 | self.pam_end.argtypes = [PamHandle, c_int] 205 | 206 | self.pam_start = libpam.pam_start 207 | self.pam_start.restype = c_int 208 | self.pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), 209 | POINTER(PamHandle)] 210 | 211 | self.pam_acct_mgmt = libpam.pam_acct_mgmt 212 | self.pam_acct_mgmt.restype = c_int 213 | self.pam_acct_mgmt.argtypes = [PamHandle, c_int] 214 | 215 | self.pam_set_item = libpam.pam_set_item 216 | self.pam_set_item.restype = c_int 217 | self.pam_set_item.argtypes = [PamHandle, c_int, c_void_p] 218 | 219 | self.pam_setcred = libpam.pam_setcred 220 | self.pam_strerror = libpam.pam_strerror 221 | self.pam_strerror.restype = c_char_p 222 | self.pam_strerror.argtypes = [PamHandle, c_int] 223 | 224 | self.pam_authenticate = libpam.pam_authenticate 225 | self.pam_authenticate.restype = c_int 226 | self.pam_authenticate.argtypes = [PamHandle, c_int] 227 | 228 | self.pam_open_session = libpam.pam_open_session 229 | self.pam_open_session.restype = c_int 230 | self.pam_open_session.argtypes = [PamHandle, c_int] 231 | 232 | self.pam_close_session = libpam.pam_close_session 233 | self.pam_close_session.restype = c_int 234 | self.pam_close_session.argtypes = [PamHandle, c_int] 235 | 236 | self.pam_putenv = libpam.pam_putenv 237 | self.pam_putenv.restype = c_int 238 | self.pam_putenv.argtypes = [PamHandle, c_char_p] 239 | 240 | if libpam_misc._name: # pragma: no branch 241 | self.pam_misc_setenv = libpam_misc.pam_misc_setenv 242 | self.pam_misc_setenv.restype = c_int 243 | self.pam_misc_setenv.argtypes = [PamHandle, c_char_p, c_char_p, 244 | c_int] 245 | 246 | self.pam_getenv = libpam.pam_getenv 247 | self.pam_getenv.restype = c_char_p 248 | self.pam_getenv.argtypes = [PamHandle, c_char_p] 249 | 250 | self.pam_getenvlist = libpam.pam_getenvlist 251 | self.pam_getenvlist.restype = POINTER(c_char_p) 252 | self.pam_getenvlist.argtypes = [PamHandle] 253 | 254 | def authenticate( 255 | self, 256 | username, # type: Union[str, bytes] 257 | password, # type: Union[str, bytes] 258 | service='login', # type: Union[str, bytes] 259 | env=None, # type: dict 260 | call_end=True, # type: bool 261 | encoding='utf-8', # type: str 262 | resetcreds=True, # type: bool 263 | print_failure_messages=False # type: bool 264 | ): # type: (...) -> bool 265 | """username and password authentication for the given service. 266 | 267 | Returns True for success, or False for failure. 268 | 269 | self.code (integer) and self.reason (string) are always stored and may 270 | be referenced for the reason why authentication failed. 0/'Success' 271 | will be stored for success. 272 | 273 | Python3 expects bytes() for ctypes inputs. This function will make 274 | necessary conversions using the supplied encoding. 275 | 276 | Args: 277 | username (str): username to authenticate 278 | password (str): password in plain text 279 | service (str): PAM service to authenticate against, defaults to 'login' 280 | env (dict): Pam environment variables 281 | call_end (bool): call the pam_end() function after (default true) 282 | print_failure_messages (bool): Print messages on failure 283 | 284 | Returns: 285 | success: PAM_SUCCESS 286 | failure: False 287 | """ 288 | 289 | @conv_func 290 | def __conv(n_messages, messages, p_response, app_data): 291 | pyob = cast(app_data, py_object).value 292 | 293 | msg_list = pyob.get('msgs') 294 | password = pyob.get('password') 295 | encoding = pyob.get('encoding') 296 | 297 | return my_conv(n_messages, messages, p_response, self.libc, msg_list, password, encoding) 298 | 299 | if isinstance(username, six.text_type): 300 | username = username.encode(encoding) 301 | if isinstance(password, six.text_type): 302 | password = password.encode(encoding) 303 | if isinstance(service, six.text_type): 304 | service = service.encode(encoding) 305 | 306 | if b'\x00' in username or b'\x00' in password or b'\x00' in service: 307 | self.code = PAM_SYSTEM_ERR 308 | self.reason = ('none of username, password, or service may contain' 309 | ' NUL') 310 | raise ValueError(self.reason) 311 | 312 | # do this up front so we can safely throw an exception if there's 313 | # anything wrong with it 314 | app_data = {'msgs': self.messages, 'password': password, 'encoding': encoding} 315 | conv = PamConv(__conv, c_void_p.from_buffer(py_object(app_data))) 316 | 317 | self.handle = PamHandle() 318 | retval = self.pam_start(service, username, byref(conv), 319 | byref(self.handle)) 320 | 321 | if retval != PAM_SUCCESS: # pragma: no cover 322 | # This is not an authentication error, something has gone wrong 323 | # starting up PAM 324 | self.code = retval 325 | self.reason = ("pam_start() failed: %s" % 326 | self.pam_strerror(self.handle, retval)) 327 | return False 328 | 329 | # set the TTY, required when pam_securetty is used and the username 330 | # root is used note: this is only needed WHEN the pam_securetty.so 331 | # module is used; for checking /etc/securetty for allowing root 332 | # logins. if your application doesn't use a TTY or your pam setup 333 | # doesn't involve pam_securetty for this auth path, don't worry 334 | # about it 335 | # 336 | # if your app isn't authenticating root with the right password, you 337 | # may not have the appropriate list of TTYs in /etc/securetty and/or 338 | # the correct configuration in /etc/pam.d/* 339 | # 340 | # if X $DISPLAY is set, use it - otherwise if we have a STDIN tty, 341 | # get it 342 | 343 | ctty = os.environ.get('DISPLAY') 344 | if not ctty and os.isatty(0): 345 | ctty = os.ttyname(0) 346 | 347 | # ctty can be invalid if no tty is being used 348 | if ctty: # pragma: no branch (we don't test a void tty yet) 349 | ctty_p = c_char_p(ctty.encode(encoding)) 350 | 351 | retval = self.pam_set_item(self.handle, PAM_TTY, ctty_p) 352 | retval = self.pam_set_item(self.handle, PAM_XDISPLAY, ctty_p) 353 | 354 | # Set the environment variables if they were supplied 355 | if env: 356 | if not isinstance(env, dict): 357 | raise TypeError('"env" must be a dict') 358 | 359 | for key, value in env.items(): 360 | if isinstance(key, bytes) and b'\x00' in key: 361 | raise ValueError('"env{}" key cannot contain NULLs') 362 | if isinstance(value, bytes) and b'\x00' in value: 363 | raise ValueError('"env{}" value cannot contain NULLs') 364 | 365 | name_value = "{}={}".format(key, value) 366 | retval = self.putenv(name_value, encoding) 367 | 368 | auth_success = self.pam_authenticate(self.handle, 0) 369 | 370 | if auth_success == PAM_SUCCESS: 371 | auth_success = self.pam_acct_mgmt(self.handle, 0) 372 | 373 | if auth_success == PAM_SUCCESS and resetcreds: 374 | auth_success = self.pam_setcred(self.handle, PAM_REINITIALIZE_CRED) 375 | 376 | # store information to inform the caller why we failed 377 | self.code = auth_success 378 | self.reason = self.pam_strerror(self.handle, auth_success) 379 | 380 | if sys.version_info >= (3,): # pragma: no branch (we don't test non-py3 versions) 381 | self.reason = self.reason.decode(encoding) # type: ignore 382 | 383 | if call_end and hasattr(self, 'pam_end'): # pragma: no branch 384 | self.pam_end(self.handle, auth_success) 385 | self.handle = None 386 | 387 | if print_failure_messages and self.code != PAM_SUCCESS: # pragma: no cover 388 | print(f"Failure: {self.reason}") 389 | 390 | return auth_success == PAM_SUCCESS 391 | 392 | def end(self): 393 | """A direct call to pam_end() 394 | Returns: 395 | Linux-PAM return value as int 396 | """ 397 | if not self.handle or not hasattr(self, 'pam_end'): 398 | return PAM_SYSTEM_ERR 399 | 400 | retval = self.pam_end(self.handle, self.code) 401 | self.handle = None 402 | 403 | return retval 404 | 405 | def open_session(self, encoding='utf-8'): 406 | """Call pam_open_session as required by the pam_api 407 | Returns: 408 | Linux-PAM return value as int 409 | """ 410 | if not self.handle: 411 | return PAM_SYSTEM_ERR 412 | 413 | retval = self.pam_open_session(self.handle, 0) 414 | self.code = retval 415 | self.reason = self.pam_strerror(self.handle, retval) 416 | 417 | if sys.version_info >= (3,): # pragma: no branch 418 | self.reason = self.reason.decode(encoding) 419 | 420 | return retval 421 | 422 | def close_session(self, encoding='utf-8'): 423 | """Call pam_close_session as required by the pam_api 424 | Returns: 425 | Linux-PAM return value as int 426 | """ 427 | if not self.handle: 428 | return PAM_SYSTEM_ERR 429 | 430 | retval = self.pam_close_session(self.handle, 0) 431 | self.code = retval 432 | self.reason = self.pam_strerror(self.handle, retval) 433 | 434 | if sys.version_info >= (3,): # pragma: no branch 435 | self.reason = self.reason.decode(encoding) 436 | 437 | return retval 438 | 439 | def misc_setenv(self, name, value, readonly, encoding='utf-8'): 440 | """A wrapper for the pam_misc_setenv function 441 | Args: 442 | name: key name of the environment variable 443 | value: the value of the environment variable 444 | Returns: 445 | Linux-PAM return value as int 446 | """ 447 | if not self.handle or not hasattr(self, "pam_misc_setenv"): 448 | return PAM_SYSTEM_ERR 449 | 450 | return self.pam_misc_setenv(self.handle, 451 | name.encode(encoding), 452 | value.encode(encoding), 453 | readonly) 454 | 455 | def putenv(self, name_value, encoding='utf-8'): 456 | """A wrapper for the pam_putenv function 457 | Args: 458 | name_value: environment variable in the format KEY=VALUE 459 | Without an '=' delete the corresponding variable 460 | Returns: 461 | Linux-PAM return value as int 462 | """ 463 | if not self.handle: 464 | return PAM_SYSTEM_ERR 465 | 466 | name_value = name_value.encode(encoding) 467 | 468 | retval = self.pam_putenv(self.handle, name_value) 469 | if retval != PAM_SUCCESS: 470 | raise Exception(self.pam_strerror(self.handle, retval)) 471 | 472 | return retval 473 | 474 | def getenv(self, key, encoding='utf-8'): 475 | """A wrapper for the pam_getenv function 476 | Args: 477 | key name of the environment variable 478 | Returns: 479 | value of the environment variable or None on error 480 | """ 481 | if not self.handle: 482 | return PAM_SYSTEM_ERR 483 | 484 | # can't happen unless someone is using internals directly 485 | if sys.version_info >= (3, ): # pragma: no branch 486 | if isinstance(key, six.text_type): # pragma: no branch 487 | key = key.encode(encoding) 488 | 489 | value = self.pam_getenv(self.handle, key) 490 | 491 | if isinstance(value, type(None)): 492 | return 493 | 494 | if isinstance(value, int): # pragma: no cover 495 | raise Exception(self.pam_strerror(self.handle, value)) 496 | 497 | if sys.version_info >= (3,): # pragma: no branch 498 | value = value.decode(encoding) 499 | 500 | return value 501 | 502 | def getenvlist(self, encoding='utf-8'): 503 | """A wrapper for the pam_getenvlist function 504 | Returns: 505 | environment as python dictionary 506 | """ 507 | if not self.handle: 508 | return PAM_SYSTEM_ERR 509 | 510 | env_list = self.pam_getenvlist(self.handle) 511 | 512 | env_count = 0 513 | pam_env_items = {} 514 | 515 | while True: 516 | try: 517 | item = env_list[env_count] 518 | except IndexError: # pragma: no cover 519 | break 520 | 521 | if not item: 522 | # end of the list 523 | break 524 | 525 | env_item = item 526 | if sys.version_info >= (3,): # pragma: no branch 527 | env_item = env_item.decode(encoding) 528 | 529 | try: 530 | pam_key, pam_value = env_item.split("=", 1) 531 | except ValueError: # pragma: no cover 532 | # Incorrectly formatted envlist item 533 | pass 534 | else: 535 | pam_env_items[pam_key] = pam_value 536 | 537 | env_count += 1 538 | 539 | return pam_env_items 540 | -------------------------------------------------------------------------------- /src/pam/pam.py: -------------------------------------------------------------------------------- 1 | # Now owned and maintained by David Ford, 2 | # 3 | # (c) 2007 Chris AtLee 4 | # Licensed under the MIT license: 5 | # http://www.opensource.org/licenses/mit-license.php 6 | # 7 | # Original author: Chris AtLee 8 | # 9 | # Modified by David Ford, 2011-12-6 10 | # added py3 support and encoding 11 | # added pam_end 12 | # added pam_setcred to reset credentials after seeing Leon Walker's remarks 13 | # added byref as well 14 | # use readline to prestuff the getuser input 15 | # 16 | # Modified by Laurie Reeves, 2020-02-14 17 | # added opening and closing the pam session 18 | # added setting and reading the pam environment variables 19 | # added setting the "misc" pam environment 20 | # added saving the messages passed back in the conversation function 21 | 22 | ''' 23 | PAM module for python 24 | 25 | This is a legacy file, it is not used. Here for example. 26 | 27 | Provides an authenticate function that will allow the caller to authenticate 28 | a user against the Pluggable Authentication Modules (PAM) on the system. 29 | 30 | Implemented using ctypes, so no compilation is necessary. 31 | ''' 32 | 33 | import six 34 | import __internals 35 | 36 | if __name__ == "__main__": # pragma: no cover 37 | import readline 38 | import getpass 39 | 40 | def input_with_prefill(prompt, text): 41 | def hook(): 42 | readline.insert_text(text) 43 | readline.redisplay() 44 | 45 | readline.set_pre_input_hook(hook) 46 | result = six.moves.input(prompt) # nosec (bandit; python2) 47 | 48 | readline.set_pre_input_hook() 49 | 50 | return result 51 | 52 | __pam = __internals.PamAuthenticator() 53 | 54 | username = input_with_prefill('Username: ', getpass.getuser()) 55 | 56 | # enter a valid username and an invalid/valid password, to verify both 57 | # failure and success 58 | result = __pam.authenticate(username, getpass.getpass(), 59 | env={"XDG_SEAT": "seat0"}, 60 | call_end=False) 61 | print('Auth result: {} ({})'.format(__pam.reason, __pam.code)) 62 | 63 | env_list = __pam.getenvlist() 64 | for key, value in env_list.items(): 65 | print("Pam Environment List item: {}={}".format(key, value)) 66 | 67 | key = "XDG_SEAT" 68 | value = __pam.getenv(key) 69 | print("Pam Environment item: {}={}".format(key, value)) 70 | 71 | if __pam.code == __internals.PAM_SUCCESS: 72 | result = __pam.open_session() 73 | print('Open session: {} ({})'.format(__pam.reason, __pam.code)) 74 | 75 | if __pam.code == __internals.PAM_SUCCESS: 76 | result = __pam.close_session() 77 | print('Close session: {} ({})'.format(__pam.reason, __pam.code)) 78 | 79 | else: 80 | __pam.end() 81 | else: 82 | __pam.end() 83 | -------------------------------------------------------------------------------- /src/pam/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '2.0.2' 2 | AUTHOR = 'David Ford ' 3 | RELEASED = '2022 March 17' 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirefighterBlu3/python-pam/2408c2eb8ada2bf5e649959679abe202d9ea7ac9/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_internals.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | # from pytest import monkeypatch 4 | 5 | from ctypes import cdll 6 | from ctypes import c_void_p 7 | from ctypes import pointer 8 | 9 | from pam.__internals import PAM_SYSTEM_ERR 10 | from pam.__internals import PAM_SUCCESS 11 | from pam.__internals import PAM_SESSION_ERR 12 | from pam.__internals import PAM_AUTH_ERR 13 | from pam.__internals import PAM_USER_UNKNOWN 14 | from pam.__internals import PAM_PROMPT_ECHO_OFF 15 | from pam.__internals import PAM_PROMPT_ECHO_ON 16 | from pam.__internals import PamConv 17 | from pam.__internals import PamHandle 18 | from pam.__internals import PamMessage 19 | from pam.__internals import PamResponse 20 | from pam.__internals import PamAuthenticator 21 | from pam.__internals import my_conv 22 | 23 | 24 | class MockPam: 25 | def __init__(self, og): 26 | self.og = og 27 | self.og_pam_start = og.pam_start 28 | self.PA_authenticate = og.authenticate 29 | self.username = None 30 | self.password = None 31 | 32 | def authenticate(self, *args, **kwargs): 33 | if len(args) > 0: 34 | self.username = args[0] 35 | if len(args) > 1: 36 | self.password = args[1] 37 | self.service = kwargs.get('service') 38 | return self.PA_authenticate(*args, **kwargs) 39 | 40 | def pam_start(self, service, username, conv, handle): 41 | rv = self.og_pam_start(service, username, conv, handle) 42 | return rv 43 | 44 | def pam_authenticate(self, handle, flags): 45 | if isinstance(self.username, str): 46 | self.username = self.username.encode() 47 | if isinstance(self.password, str): 48 | self.password = self.password.encode() 49 | 50 | if self.username == b'good_username' and self.password == b'good_password': 51 | return PAM_SUCCESS 52 | 53 | if self.username == b'unknown_username': 54 | return PAM_USER_UNKNOWN 55 | 56 | return PAM_AUTH_ERR 57 | 58 | def pam_acct_mgmt(self, handle, flags): 59 | # we don't test anything here (yet) 60 | return PAM_SUCCESS 61 | 62 | 63 | @pytest.fixture 64 | def pam_obj(request, monkeypatch): 65 | obj = PamAuthenticator() 66 | MP = MockPam(obj) 67 | monkeypatch.setattr(obj, 'authenticate', MP.authenticate) 68 | monkeypatch.setattr(obj, 'pam_start', MP.pam_start) 69 | monkeypatch.setattr(obj, 'pam_authenticate', MP.pam_authenticate) 70 | monkeypatch.setattr(obj, 'pam_acct_mgmt', MP.pam_acct_mgmt) 71 | yield obj 72 | 73 | 74 | def test_PamHandle__void0(): 75 | x = PamHandle() 76 | assert x.handle == c_void_p(0).value 77 | 78 | 79 | def test_PamHandle__repr(): 80 | x = PamHandle() 81 | assert '' == repr(x) 82 | 83 | 84 | def test_PamMessage__repr(): 85 | x = PamMessage() 86 | x.msg_style = 1 87 | x.msg = b'1' 88 | str(x) 89 | assert "" == repr(x) 90 | 91 | 92 | def test_PamResponse__repr(): 93 | x = PamResponse() 94 | assert "" == repr(x) 95 | 96 | 97 | def test_PamAuthenticator__setup(): 98 | x = PamAuthenticator() 99 | assert hasattr(x, 'reason') 100 | 101 | 102 | def test_PamAuthenticator__requires_username_password(pam_obj): 103 | with pytest.raises(TypeError): 104 | pam_obj.authenticate() 105 | 106 | 107 | def test_PamAuthenticator__requires_username_no_nulls(pam_obj): 108 | with pytest.raises(ValueError): 109 | pam_obj.authenticate(b'username\x00', b'password') 110 | 111 | 112 | def test_PamAuthenticator__requires_password_no_nulls(pam_obj): 113 | with pytest.raises(ValueError): 114 | pam_obj.authenticate(b'username', b'password\x00') 115 | 116 | 117 | def test_PamAuthenticator__requires_service_no_nulls(pam_obj): 118 | with pytest.raises(ValueError): 119 | pam_obj.authenticate(b'username', b'password', b'service\x00') 120 | 121 | 122 | # TEST_* require a valid account 123 | def test_PamAuthenticator__normal_success(pam_obj): 124 | rv = pam_obj.authenticate('good_username', 'good_password') 125 | assert True is rv 126 | 127 | 128 | def test_PamAuthenticator__normal_password_failure(pam_obj): 129 | rv = pam_obj.authenticate('good_username', 'bad_password') 130 | assert False is rv 131 | assert PAM_AUTH_ERR == pam_obj.code 132 | 133 | 134 | def test_PamAuthenticator__normal_unknown_username(pam_obj): 135 | rv = pam_obj.authenticate('unknown_username', '') 136 | assert False is rv 137 | assert PAM_USER_UNKNOWN == pam_obj.code 138 | 139 | 140 | def test_PamAuthenticator__unset_DISPLAY(pam_obj): 141 | os.environ['DISPLAY'] = '' 142 | 143 | rv = pam_obj.authenticate('good_username', 'good_password') 144 | assert True is rv 145 | assert PAM_SUCCESS == pam_obj.code 146 | 147 | 148 | def test_PamAuthenticator__env_requires_dict(pam_obj): 149 | with pytest.raises(TypeError): 150 | pam_obj.authenticate('good_username', 'good_password', env='value') 151 | 152 | 153 | def test_PamAuthenticator__env_requires_key_no_nulls(pam_obj): 154 | with pytest.raises(ValueError): 155 | pam_obj.authenticate('good_username', 'good_password', env={b'\x00invalid_key': b'value'}) 156 | 157 | 158 | def test_PamAuthenticator__env_requires_value_no_nulls(pam_obj): 159 | with pytest.raises(ValueError): 160 | pam_obj.authenticate('good_username', 'good_password', env={b'key': b'\x00invalid_value'}) 161 | 162 | 163 | def test_PamAuthenticator__env_set(pam_obj): 164 | rv = pam_obj.authenticate('good_username', 'good_password', env={'key': b'value'}) 165 | assert True is rv 166 | assert PAM_SUCCESS == pam_obj.code 167 | 168 | 169 | def test_PamAuthenticator__putenv_incomplete_setup(pam_obj): 170 | pam_obj.handle = None 171 | pam_obj.putenv('NAME=SomeValue') 172 | rv = pam_obj.getenv('NAME') 173 | assert PAM_SYSTEM_ERR == rv 174 | 175 | 176 | def test_PamAuthenticator__putenv(pam_obj): 177 | pam_obj.handle = PamHandle() 178 | pam_conv = PamConv() 179 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 180 | pam_obj.putenv('NAME=SomeValue') 181 | rv = pam_obj.getenv('NAME') 182 | assert 'SomeValue' == rv 183 | 184 | 185 | def test_PamAuthenticator__putenv_bad_key(pam_obj): 186 | pam_obj.handle = PamHandle() 187 | pam_conv = PamConv() 188 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 189 | with pytest.raises(Exception): 190 | pam_obj.putenv('NAME\0=SomeValue') 191 | 192 | 193 | def test_PamAuthenticator__putenv_missing_key_delete(pam_obj): 194 | pam_obj.handle = PamHandle() 195 | pam_conv = PamConv() 196 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 197 | with pytest.raises(Exception): 198 | pam_obj.putenv('NAME') 199 | 200 | 201 | def test_PamAuthenticator__getenv_missing_key(pam_obj): 202 | pam_obj.handle = PamHandle() 203 | pam_conv = PamConv() 204 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 205 | pam_obj.putenv('NAME=Foo') 206 | pam_obj.putenv('NAME') 207 | rv = pam_obj.getenv('NAME') 208 | assert rv is None 209 | 210 | 211 | def test_PamAuthenticator__getenv_missing_value(pam_obj): 212 | pam_obj.handle = PamHandle() 213 | pam_conv = PamConv() 214 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 215 | pam_obj.putenv('NAME=') 216 | rv = pam_obj.getenv('NAME') 217 | assert '' == rv 218 | 219 | 220 | def test_PamAuthenticator__getenv(pam_obj): 221 | pam_obj.handle = PamHandle() 222 | pam_conv = PamConv() 223 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 224 | pam_obj.putenv('NAME=foo') 225 | rv = pam_obj.getenv('NAME') 226 | assert 'foo' == rv 227 | 228 | 229 | def test_PamAuthenticator__getenv_stutter(pam_obj): 230 | pam_obj.handle = PamHandle() 231 | pam_conv = PamConv() 232 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 233 | pam_obj.putenv('NAME=NAME=foo') 234 | rv = pam_obj.getenv('NAME') 235 | assert 'NAME=foo' == rv 236 | 237 | 238 | def test_PamAuthenticator__getenvlist_incomplete_setup(pam_obj): 239 | pam_obj.handle = None 240 | rv = pam_obj.getenvlist() 241 | assert PAM_SYSTEM_ERR == rv 242 | 243 | 244 | def test_PamAuthenticator__getenvlist(pam_obj): 245 | pam_obj.handle = PamHandle() 246 | pam_conv = PamConv() 247 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 248 | pam_obj.putenv('A=b') 249 | pam_obj.putenv('C=d') 250 | rv = pam_obj.getenvlist() 251 | assert {'A': 'b', 'C': 'd'} == rv 252 | 253 | 254 | def test_PamAuthenticator__getenvlist_missing_value(pam_obj): 255 | pam_obj.handle = PamHandle() 256 | pam_conv = PamConv() 257 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 258 | pam_obj.putenv('A=b') 259 | pam_obj.putenv('C=') 260 | rv = pam_obj.getenvlist() 261 | assert {'A': 'b', 'C': ''} == rv 262 | 263 | 264 | def test_PamAuthenticator__misc_setenv_incomplete_setup(pam_obj): 265 | pam_obj.handle = None 266 | rv = pam_obj.misc_setenv('NAME', 'SomeValue', False) 267 | assert PAM_SYSTEM_ERR == rv 268 | 269 | 270 | def test_PamAuthenticator__misc_setenv(pam_obj): 271 | pam_obj.handle = PamHandle() 272 | pam_conv = PamConv() 273 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 274 | rv = pam_obj.misc_setenv('NAME', 'SomeValue', False) 275 | assert PAM_SUCCESS == rv 276 | 277 | 278 | def test_PamAuthenticator__pam_end_incomplete_setup(pam_obj): 279 | pam_obj.handle = None 280 | rv = pam_obj.end() 281 | assert PAM_SYSTEM_ERR == rv 282 | 283 | 284 | def test_PamAuthenticator__pam_end(pam_obj): 285 | pam_obj.handle = PamHandle() 286 | pam_conv = PamConv() 287 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 288 | rv = pam_obj.end() 289 | assert PAM_SUCCESS == rv 290 | 291 | 292 | def test_PamAuthenticator__open_session_incomplete_setup(pam_obj): 293 | pam_obj.handle = None 294 | rv = pam_obj.open_session() 295 | assert PAM_SYSTEM_ERR == rv 296 | 297 | 298 | def test_PamAuthenticator__open_session_unauthenticated(pam_obj): 299 | pam_obj.handle = PamHandle() 300 | pam_conv = PamConv() 301 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 302 | rv = pam_obj.open_session() 303 | assert PAM_SESSION_ERR == rv 304 | 305 | 306 | def test_PamAuthenticator__close_session_incomplete_setup(pam_obj): 307 | pam_obj.handle = None 308 | rv = pam_obj.close_session() 309 | assert PAM_SYSTEM_ERR == rv 310 | 311 | 312 | def test_PamAuthenticator__close_session_unauthenticated(pam_obj): 313 | pam_obj.handle = PamHandle() 314 | pam_conv = PamConv() 315 | pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) 316 | rv = pam_obj.close_session() 317 | assert PAM_SESSION_ERR == rv 318 | 319 | 320 | def test_PamAuthenticator__conversation_callback_prompt_echo_off(pam_obj): 321 | '''Verify that the password is stuffed into the pp_response structure and the 322 | response code is set to zero 323 | ''' 324 | n_messages = 1 325 | 326 | messages = PamMessage(PAM_PROMPT_ECHO_OFF, b'Password: ') 327 | pp_messages = pointer(pointer(messages)) 328 | 329 | response = PamResponse(b'overwrite', -1) 330 | pp_response = pointer(pointer(response)) 331 | 332 | encoding = 'utf-8' 333 | password = b'blank' 334 | msg_list = [] 335 | 336 | libc = cdll.LoadLibrary(None) 337 | 338 | rv = my_conv(n_messages, 339 | pp_messages, 340 | pp_response, 341 | libc, 342 | msg_list, 343 | password, 344 | encoding) 345 | 346 | assert b'blank' == pp_response.contents.contents.resp 347 | assert 0 == pp_response.contents.contents.resp_retcode 348 | assert PAM_SUCCESS == rv 349 | 350 | 351 | def test_PamAuthenticator__conversation_callback_prompt_echo_on(pam_obj): 352 | '''Verify that the stuffed PamResponse "overwrite" is copied into the output 353 | and the resp_retcode is set to zero 354 | ''' 355 | n_messages = 1 356 | 357 | messages = PamMessage(PAM_PROMPT_ECHO_ON, b'Password: ') 358 | pp_messages = pointer(pointer(messages)) 359 | 360 | response = PamResponse(b'overwrite', -1) 361 | pp_response = pointer(pointer(response)) 362 | 363 | encoding = 'utf-8' 364 | password = b'blank' 365 | msg_list = [] 366 | 367 | libc = cdll.LoadLibrary(None) 368 | 369 | rv = my_conv(n_messages, 370 | pp_messages, 371 | pp_response, 372 | libc, 373 | msg_list, 374 | password, 375 | encoding) 376 | 377 | assert None is pp_response.contents.contents.resp 378 | assert 0 == pp_response.contents.contents.resp_retcode 379 | assert PAM_SUCCESS == rv 380 | 381 | 382 | def test_PamAuthenticator__conversation_callback_multimessage_OFF_ON(pam_obj): 383 | '''Verify that the stuffed PamResponse "overwrite" is copied into the output 384 | and the resp_retcode is set to zero 385 | ''' 386 | n_messages = 2 387 | 388 | msg1 = PamMessage(PAM_PROMPT_ECHO_OFF, b'overwrite with PAM_PROMPT_ECHO_OFF') 389 | msg2 = PamMessage(PAM_PROMPT_ECHO_ON, b'overwrite with PAM_PROMPT_ECHO_ON') 390 | 391 | ptr1 = pointer(msg1) 392 | ptr2 = pointer(msg2) 393 | 394 | ptrs = pointer(ptr1) 395 | ptrs[1] = ptr2 396 | 397 | pp_messages = pointer(ptrs[0]) 398 | 399 | response = PamResponse(b'overwrite', -1) 400 | pp_response = pointer(pointer(response)) 401 | 402 | encoding = 'utf-8' 403 | password = b'blank' 404 | msg_list = [] 405 | 406 | libc = cdll.LoadLibrary(None) 407 | 408 | rv = my_conv(n_messages, 409 | pp_messages, 410 | pp_response, 411 | libc, 412 | msg_list, 413 | password, 414 | encoding) 415 | 416 | assert b'blank' == pp_response.contents.contents.resp 417 | assert 0 == pp_response.contents.contents.resp_retcode 418 | assert PAM_SUCCESS == rv 419 | 420 | 421 | def test_PamAuthenticator__conversation_callback_multimessage_ON_OFF(pam_obj): 422 | '''Verify that the stuffed PamResponse "overwrite" is copied into the output 423 | and the resp_retcode is set to zero 424 | ''' 425 | n_messages = 2 426 | 427 | msg1 = PamMessage(PAM_PROMPT_ECHO_ON, b'overwrite with PAM_PROMPT_ECHO_ON') 428 | msg2 = PamMessage(PAM_PROMPT_ECHO_OFF, b'overwrite with PAM_PROMPT_ECHO_OFF') 429 | 430 | ptr1 = pointer(msg1) 431 | ptr2 = pointer(msg2) 432 | 433 | ptrs = pointer(ptr1) 434 | ptrs[1] = ptr2 435 | 436 | pp_messages = pointer(ptrs[0]) 437 | 438 | response = PamResponse(b'overwrite', -1) 439 | pp_response = pointer(pointer(response)) 440 | 441 | encoding = 'utf-8' 442 | password = b'blank' 443 | msg_list = [] 444 | 445 | libc = cdll.LoadLibrary(None) 446 | 447 | rv = my_conv(n_messages, 448 | pp_messages, 449 | pp_response, 450 | libc, 451 | msg_list, 452 | password, 453 | encoding) 454 | 455 | assert None is pp_response.contents.contents.resp 456 | assert 0 == pp_response.contents.contents.resp_retcode 457 | assert PAM_SUCCESS == rv 458 | -------------------------------------------------------------------------------- /tests/test_pam.py: -------------------------------------------------------------------------------- 1 | from pam import authenticate 2 | 3 | 4 | def test_PamAuthenticator(): 5 | authenticate('a', 'b') 6 | --------------------------------------------------------------------------------