From d2d7966c6db61bddf33cfdd9e77a1df29c62620a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bompard?= Date: Fri, 24 Sep 2021 11:45:18 +0200 Subject: [PATCH 01/17] Add missing import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Bompard --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0e048d2..bba3e9a 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import os from setuptools import setup +from setuptools import find_packages def read(fname): From 2c1450193b6177829dee496a503a64a3c4f9463a Mon Sep 17 00:00:00 2001 From: David Ford Date: Fri, 3 Dec 2021 16:44:04 -0500 Subject: [PATCH 02/17] make the verbosity print branch optional --- pam/__internals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pam/__internals.py b/pam/__internals.py index 4e1ec96..5b9e34a 100644 --- a/pam/__internals.py +++ b/pam/__internals.py @@ -364,7 +364,7 @@ def my_conv(n_messages, messages, p_response, app_data): self.pam_end(self.handle, auth_success) self.handle = None - if print_failure_messages and self.code != PAM_SUCCESS: + if print_failure_messages and self.code != PAM_SUCCESS: # pragma: no cover print(f"Failure: {self.reason}") return auth_success From a7008dd6f43b5650e193ada6c975e702ec4c0f19 Mon Sep 17 00:00:00 2001 From: David Ford Date: Thu, 10 Mar 2022 23:18:53 -0500 Subject: [PATCH 03/17] Switch out things from Makefile rules, start putting everything in pyproject.toml and use tox --- .coveragerc | 21 ----------- CONTRIBUTING.md | 3 ++ Makefile | 27 +++++++------- pam/conftest.py | 1 - pyproject.toml | 64 +++++++++++++++++++++++++++++++++ requirements.txt | 1 + setup.cfg | 44 +++++++++++++++++++++++ setup.py | 40 --------------------- {pam => src/pam}/__init__.py | 0 {pam => src/pam}/__internals.py | 0 {pam => src/pam}/pam.py | 0 src/pam/version.py | 3 ++ tests/test_internals.py | 12 +++---- version.py | 3 -- 14 files changed, 133 insertions(+), 86 deletions(-) delete mode 100644 .coveragerc delete mode 100644 pam/conftest.py create mode 100644 pyproject.toml create mode 100644 setup.cfg delete mode 100644 setup.py rename {pam => src/pam}/__init__.py (100%) rename {pam => src/pam}/__internals.py (100%) rename {pam => src/pam}/pam.py (100%) create mode 100644 src/pam/version.py delete mode 100644 version.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index afcd4c6..0000000 --- a/.coveragerc +++ /dev/null @@ -1,21 +0,0 @@ -[run] -branch = True -source = - pam -omit = - pam/conftest.py - pam/pam.py - venv/ - -[tool:pytest] -addopts = --cov=pam --cov-report=html - -[report] -skip_empty = True -fail_under = 100 -exclude_lines = - pragma: no cover - if __name__ == .__main__.: - -[html] -directory = htmlcov diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6e3ce2..e5cf7eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,3 +3,6 @@ submitted under the existing project license. This project is switching to a git-flow style, please make pull requests against the `develop` branch. + +Good reading +* https://packaging.python.org/en/latest/tutorials/packaging-projects/ diff --git a/Makefile b/Makefile index 22693ba..0fe55fa 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ VIRTUALENV = $(shell which virtualenv) PYTHONEXEC = python -VERSION = `grep VERSION version.py | cut -d \' -f2` +VERSION = `grep VERSION src/pam/version.py | cut -d \' -f2` -bandit: pydeps - . venv/bin/activate; bandit -r pam/ +build: pydeps + python -m build clean: rm -rf *.egg-info/ @@ -34,28 +34,25 @@ deps: . venv/bin/activate; python -m pip install --upgrade -qr requirements.txt install: clean venv deps - . venv/bin/activate; python setup.py install + . venv/bin/activate; pip install --use-pep517 --progress-bar emoji inspectortiger: pydeps - . venv/bin/activate; inspectortiger pam/ + . venv/bin/activate; inspectortiger src/pam/ lint: pydeps - . venv/bin/activate; python -m flake8 pam/ --max-line-length=120 + . venv/bin/activate; python -m flake8 src/pam/ --max-line-length=120 -preflight: bandit coverage test +preflight: bandit test pydeps: - . venv/bin/activate; pip install --upgrade -q pip; \ - pip install --upgrade -q pip flake8 bandit \ - pyre-check coverage pytest pytest-mock pytest-cov pytest-runner \ - mock minimock faker responses - -test: pydeps deps venv lint . venv/bin/activate; \ - pytest tests -r w --capture=sys -vvv --cov; \ - coverage html + pip install --upgrade -q pip && \ + pip install --upgrade -q pip build + +test: tox tox: + rm -fr .tox . venv/bin/activate; tox venv: diff --git a/pam/conftest.py b/pam/conftest.py deleted file mode 100644 index ac07420..0000000 --- a/pam/conftest.py +++ /dev/null @@ -1 +0,0 @@ -# trigger pytest diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d8ef361 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[build-system] +requires = [ + 'setuptools>=44', + 'wheel>=0.30.0', + 'six', +] +build-backend = 'setuptools.build_meta' + +# ignore the tox documentation, it IS NOT supported yet +# https://github.com/tox-dev/tox/issues/2148 +#[tox] +#isolated_build = true + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py310 +isolated_build = true +#skipsdist = true + +[testenv] +basepython = python3.10 +passenv = * +deps = + bandit + flake8 + mypy + types-six + coverage + pytest-cov + pytest + -rrequirements.txt + +commands = + flake8 src/pam/ + mypy + bandit -r src -c "pyproject.toml" + pytest --cov -r w --capture=sys -vvv --cov-report=html +""" + +[tool.bandit] +exclude_dirs = ["./venv", "./test", ] +recursive = true + +[tool.mypy] +files = ["src/pam/__init__.py", "src/pam/__internals.py"] +ignore_missing_imports = true + +[tool.pytest] +python_files = "test_*.py" +norecursedirs = ".tox" + +[tool.coverage.run] +branch = true +# awkward how I can include "pam" but I have to be incredibly specific when omitting +source = ["pam"] +omit = ["*/pam/pam.py", "*/pam/version.py",] + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.report] +skip_empty = true +fail_under = 100 diff --git a/requirements.txt b/requirements.txt index ffe2fce..b2b4ddd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ six +toml diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8514037 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,44 @@ +[metadata] +name = python-pam +version = attr: pam.version.VERSION +author = David Ford +author_email = david@blue-labs.org +description = Python PAM module using ctypes, py3 +long_description = file: README.md +long_description_content_type = text/markdown +license = License :: OSI Approved :: MIT License +url = https://github.com/FirefighterBlu3/python-pam +project_urls = + Bug Tracker = https://github.com/FirefighterBlu3/python-pam/issues +classifiers = + Development Status :: 6 - Mature + Environment :: Plugins + Intended Audience :: Developers + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: MIT License + Operating System :: POSIX + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + Topic :: Security + Topic :: System :: Systems Administration :: Authentication/Directory + +[options] +packages = find: +package_dir = + = src + +[options.packages.find] +where = src + +[sdist] +keep_temp = 1 + +[build_ext] +debug = 1 + +[flake8] +max-line-length = 120 + diff --git a/setup.py b/setup.py deleted file mode 100644 index 0e048d2..0000000 --- a/setup.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -from setuptools import setup - - -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - - -sdesc = 'Python PAM module using ctypes, py3/py2' - -setup(name = 'python-pam', - description = sdesc, - long_description = read('README.md'), - long_description_content_type='text/markdown', - packages = find_packages(exclude=['tests']), - version = '2.0.0rc1', - author = 'David Ford', - author_email = 'david@blue-labs.org', - maintainer = 'David Ford', - maintainer_email = 'david@blue-labs.org', - url = 'https://github.com/FirefighterBlu3/python-pam', - download_url = 'https://github.com/FirefighterBlu3/python-pam', - license = 'License :: OSI Approved :: MIT License', - platforms = ['i686', 'x86_64'], - classifiers = [ - 'Development Status :: 6 - Mature', - 'Environment :: Plugins', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Topic :: Security', - 'Topic :: System :: Systems Administration :: Authentication/Directory', - ], - ) diff --git a/pam/__init__.py b/src/pam/__init__.py similarity index 100% rename from pam/__init__.py rename to src/pam/__init__.py diff --git a/pam/__internals.py b/src/pam/__internals.py similarity index 100% rename from pam/__internals.py rename to src/pam/__internals.py diff --git a/pam/pam.py b/src/pam/pam.py similarity index 100% rename from pam/pam.py rename to src/pam/pam.py diff --git a/src/pam/version.py b/src/pam/version.py new file mode 100644 index 0000000..0dce6ae --- /dev/null +++ b/src/pam/version.py @@ -0,0 +1,3 @@ +VERSION = '2.0.0' +AUTHOR = 'David Ford ' +RELEASED = '2022 March 10' diff --git a/tests/test_internals.py b/tests/test_internals.py index cffd0b8..a3cf0c1 100644 --- a/tests/test_internals.py +++ b/tests/test_internals.py @@ -16,8 +16,8 @@ # In order to run some tests, we need a working user/pass combo # you can specify these on the command line -TEST_USERNAME = os.getenv('USERNAME', '') -TEST_PASSWORD = os.getenv('PASSWORD', '') +TEST_USERNAME = os.getenv('TEST_USERNAME', '') +TEST_PASSWORD = os.getenv('TEST_PASSWORD', '') @pytest.fixture @@ -76,7 +76,7 @@ def test_PamAuthenticator__requires_service_no_nulls(pam_obj): # TEST_* require a valid account def test_PamAuthenticator__normal_success(pam_obj): - if not (TEST_USERNAME and TEST_PASSWORD): + if not (TEST_USERNAME and TEST_PASSWORD): pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") rv = pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD) @@ -84,7 +84,7 @@ def test_PamAuthenticator__normal_success(pam_obj): def test_PamAuthenticator__normal_password_failure(pam_obj): - if not (TEST_USERNAME and TEST_PASSWORD): + if not (TEST_USERNAME and TEST_PASSWORD): pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") rv = pam_obj.authenticate(TEST_USERNAME, 'not-valid') @@ -93,7 +93,7 @@ def test_PamAuthenticator__normal_password_failure(pam_obj): def test_PamAuthenticator__normal_unknown_username(pam_obj): rv = pam_obj.authenticate('bad_user_name', '') - assert PAM_AUTH_ERR == rv + assert rv in (PAM_AUTH_ERR, PAM_USER_UNKNOWN) def test_PamAuthenticator__unset_DISPLAY(pam_obj): @@ -129,7 +129,7 @@ def test_PamAuthenticator__env_set(pam_obj): # yes, this is intentional. this lets us run code coverage on the # affected area even though we know the assert would have failed - if not (TEST_USERNAME and TEST_PASSWORD): + if not (TEST_USERNAME and TEST_PASSWORD): pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") assert PAM_SUCCESS == rv diff --git a/version.py b/version.py deleted file mode 100644 index 5b51f06..0000000 --- a/version.py +++ /dev/null @@ -1,3 +0,0 @@ -VERSION = '2.0.0rc1' -AUTHOR = 'David Ford ' -RELEASED = '2021 December 3' From 72f4509b4abd9a9aa2eeeeaf1e75130d271bd8fa Mon Sep 17 00:00:00 2001 From: David Ford Date: Sun, 13 Mar 2022 20:40:16 -0400 Subject: [PATCH 04/17] tidy up pam.py a bit. six.input moved to six.moves.input (a while ago) and don't check junk env stuff --- src/pam/pam.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pam/pam.py b/src/pam/pam.py index ad0d869..f16bf0c 100644 --- a/src/pam/pam.py +++ b/src/pam/pam.py @@ -43,7 +43,7 @@ def hook(): readline.redisplay() readline.set_pre_input_hook(hook) - result = six.input(prompt) # nosec (bandit; python2) + result = six.moves.input(prompt) # nosec (bandit; python2) readline.set_pre_input_hook() @@ -68,10 +68,6 @@ def hook(): value = __pam.getenv(key) print("Pam Environment item: {}={}".format(key, value)) - key = "asdf" - value = __pam.getenv(key) - print("Missing Pam Environment item: {}={}".format(key, value)) - if __pam.code == __internals.PAM_SUCCESS: result = __pam.open_session() print('Open session: {} ({})'.format(__pam.reason, __pam.code)) From 0307e0b2b6e3f982290fc65e52dc34037783dc79 Mon Sep 17 00:00:00 2001 From: David Ford Date: Sun, 13 Mar 2022 20:43:22 -0400 Subject: [PATCH 05/17] reposition the conversation function (my_conv) so we're doing less libc object generation test reliance, pass in app data rather than rely on global or local context scoping. --- src/pam/__internals.py | 89 ++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/src/pam/__internals.py b/src/pam/__internals.py index 5b9e34a..9f3adb0 100644 --- a/src/pam/__internals.py +++ b/src/pam/__internals.py @@ -1,7 +1,7 @@ import os import six import sys -import ctypes +from ctypes import cdll from ctypes import CFUNCTYPE from ctypes import CDLL from ctypes import POINTER @@ -9,6 +9,7 @@ from ctypes import byref from ctypes import cast from ctypes import sizeof +from ctypes import py_object from ctypes import c_char from ctypes import c_char_p from ctypes import c_int @@ -104,7 +105,7 @@ class PamMessage(Structure): _fields_ = [("msg_style", c_int), ("msg", c_char_p)] def __repr__(self): - return "" % (self.msg_style, self.msg) + return "" % (self.msg_style, self.msg) class PamResponse(Structure): @@ -112,7 +113,7 @@ class PamResponse(Structure): _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)] def __repr__(self): - return "" % (self.resp_retcode, self.resp) + return "" % (self.resp_retcode, self.resp) conv_func = CFUNCTYPE(c_int, @@ -122,6 +123,49 @@ def __repr__(self): c_void_p) +def my_conv(n_messages, messages, p_response, libc, msg_list: list, password: bytes, encoding: str): + """Simple conversation function that responds to any + prompt where the echo is off with the supplied password""" + # Create an array of n_messages response objects + + calloc = libc.calloc + calloc.restype = c_void_p + calloc.argtypes = [c_size_t, c_size_t] + + cpassword = c_char_p(password) + + ''' + PAM_PROMPT_ECHO_OFF = 1 + PAM_PROMPT_ECHO_ON = 2 + PAM_ERROR_MSG = 3 + PAM_TEXT_INFO = 4 + ''' + + addr = calloc(n_messages, sizeof(PamResponse)) + response = cast(addr, POINTER(PamResponse)) + p_response[0] = response + + for i in range(n_messages): + message = messages[i].contents.msg + if sys.version_info >= (3,): # pragma: no branch + message = message.decode(encoding) + + msg_list.append(message) + + if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF: + if i == 0: + dst = calloc(len(password)+1, sizeof(c_char)) + memmove(dst, cpassword, len(password)) + response[i].resp = dst + else: + # void out the message + response[i].resp = None + + response[i].resp_retcode = 0 + + return PAM_SUCCESS + + class PamConv(Structure): """wrapper class for pam_conv structure""" _fields_ = [("conv", conv_func), ("appdata_ptr", c_void_p)] @@ -136,8 +180,11 @@ def __init__(self): # dlopen("", ...) which opens our own executable. since 'python' has # a libc dependency, this means libc symbols are already available # to us - libc = ctypes.cdll.LoadLibrary(None) + # libc = CDLL(find_library("c")) + libc = cdll.LoadLibrary(None) + self.libc = libc + libpam = CDLL(find_library("pam")) libpam_misc = CDLL(find_library("pam_misc")) @@ -247,32 +294,14 @@ def authenticate( """ @conv_func - def my_conv(n_messages, messages, p_response, app_data): - """Simple conversation function that responds to any - prompt where the echo is off with the supplied password""" - # Create an array of n_messages response objects - addr = self.calloc(n_messages, sizeof(PamResponse)) - response = cast(addr, POINTER(PamResponse)) - p_response[0] = response - - for i in range(n_messages): - message = messages[i].contents.msg - if sys.version_info >= (3,): # pragma: no branch - message = message.decode(encoding) - - self.messages.append(message) - - if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF: # pragma: no branch - if i == 0: # pragma: no branch - dst = self.calloc(len(password)+1, sizeof(c_char)) - memmove(dst, cpassword, len(password)) - response[i].resp = dst - else: # pragma: no cover - response[i].resp = None + def __conv(n_messages, messages, p_response, app_data): + pyob = cast(app_data, py_object).value - response[i].resp_retcode = 0 + msg_list = pyob.get('msgs') + password = pyob.get('password') + encoding = pyob.get('encoding') - return PAM_SUCCESS + return my_conv(n_messages, messages, p_response, self.libc, msg_list, password, encoding) if isinstance(username, six.text_type): username = username.encode(encoding) @@ -289,10 +318,10 @@ def my_conv(n_messages, messages, p_response, app_data): # do this up front so we can safely throw an exception if there's # anything wrong with it - cpassword = c_char_p(password) + app_data = {'msgs': self.messages, 'password': password, 'encoding': encoding} + conv = PamConv(__conv, c_void_p.from_buffer(py_object(app_data))) self.handle = PamHandle() - conv = PamConv(my_conv, 0) retval = self.pam_start(service, username, byref(conv), byref(self.handle)) From 414ae394f08f781b24982b5266557c886d2ab7dc Mon Sep 17 00:00:00 2001 From: David Ford Date: Sun, 13 Mar 2022 20:44:22 -0400 Subject: [PATCH 06/17] and make the appropriate test_* units so we actually test branching in my_conv based on the msg_style --- tests/test_internals.py | 149 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 2 deletions(-) diff --git a/tests/test_internals.py b/tests/test_internals.py index a3cf0c1..4c92b0d 100644 --- a/tests/test_internals.py +++ b/tests/test_internals.py @@ -1,18 +1,23 @@ import os import pytest +from ctypes import cdll from ctypes import c_void_p +from ctypes import pointer from pam.__internals import PAM_SYSTEM_ERR from pam.__internals import PAM_SUCCESS from pam.__internals import PAM_SESSION_ERR from pam.__internals import PAM_AUTH_ERR from pam.__internals import PAM_USER_UNKNOWN +from pam.__internals import PAM_PROMPT_ECHO_OFF +from pam.__internals import PAM_PROMPT_ECHO_ON from pam.__internals import PamConv from pam.__internals import PamHandle from pam.__internals import PamMessage from pam.__internals import PamResponse from pam.__internals import PamAuthenticator +from pam.__internals import my_conv # In order to run some tests, we need a working user/pass combo # you can specify these on the command line @@ -41,12 +46,12 @@ def test_PamMessage__repr(): x.msg_style = 1 x.msg = b'1' str(x) - assert "" == repr(x) + assert "" == repr(x) def test_PamResponse__repr(): x = PamResponse() - assert "" == repr(x) + assert "" == repr(x) def test_PamAuthenticator__setup(): @@ -284,3 +289,143 @@ def test_PamAuthenticator__close_session_unauthenticated(pam_obj): pam_obj.pam_start(b'', b'', pam_conv, pam_obj.handle) rv = pam_obj.close_session() assert PAM_SESSION_ERR == rv + + +def test_PamAuthenticator__conversation_callback_prompt_echo_off(pam_obj): + '''Verify that the password is stuffed into the pp_response structure and the + response code is set to zero + ''' + n_messages = 1 + + messages = PamMessage(PAM_PROMPT_ECHO_OFF, b'Password: ') + pp_messages = pointer(pointer(messages)) + + response = PamResponse(b'overwrite', -1) + pp_response = pointer(pointer(response)) + + encoding = 'utf-8' + password = b'blank' + msg_list = [] + + libc = cdll.LoadLibrary(None) + + rv = my_conv(n_messages, + pp_messages, + pp_response, + libc, + msg_list, + password, + encoding) + + assert b'blank' == pp_response.contents.contents.resp + assert 0 == pp_response.contents.contents.resp_retcode + assert PAM_SUCCESS == rv + + +def test_PamAuthenticator__conversation_callback_prompt_echo_on(pam_obj): + '''Verify that the stuffed PamResponse "overwrite" is copied into the output + and the resp_retcode is set to zero + ''' + n_messages = 1 + + messages = PamMessage(PAM_PROMPT_ECHO_ON, b'Password: ') + pp_messages = pointer(pointer(messages)) + + response = PamResponse(b'overwrite', -1) + pp_response = pointer(pointer(response)) + + encoding = 'utf-8' + password = b'blank' + msg_list = [] + + libc = cdll.LoadLibrary(None) + + rv = my_conv(n_messages, + pp_messages, + pp_response, + libc, + msg_list, + password, + encoding) + + assert None is pp_response.contents.contents.resp + assert 0 == pp_response.contents.contents.resp_retcode + assert PAM_SUCCESS == rv + + +def test_PamAuthenticator__conversation_callback_multimessage_OFF_ON(pam_obj): + '''Verify that the stuffed PamResponse "overwrite" is copied into the output + and the resp_retcode is set to zero + ''' + n_messages = 2 + + msg1 = PamMessage(PAM_PROMPT_ECHO_OFF, b'overwrite with PAM_PROMPT_ECHO_OFF') + msg2 = PamMessage(PAM_PROMPT_ECHO_ON, b'overwrite with PAM_PROMPT_ECHO_ON') + + ptr1 = pointer(msg1) + ptr2 = pointer(msg2) + + ptrs = pointer(ptr1) + ptrs[1] = ptr2 + + pp_messages = pointer(ptrs[0]) + + response = PamResponse(b'overwrite', -1) + pp_response = pointer(pointer(response)) + + encoding = 'utf-8' + password = b'blank' + msg_list = [] + + libc = cdll.LoadLibrary(None) + + rv = my_conv(n_messages, + pp_messages, + pp_response, + libc, + msg_list, + password, + encoding) + + assert b'blank' == pp_response.contents.contents.resp + assert 0 == pp_response.contents.contents.resp_retcode + assert PAM_SUCCESS == rv + + +def test_PamAuthenticator__conversation_callback_multimessage_ON_OFF(pam_obj): + '''Verify that the stuffed PamResponse "overwrite" is copied into the output + and the resp_retcode is set to zero + ''' + n_messages = 2 + + msg1 = PamMessage(PAM_PROMPT_ECHO_ON, b'overwrite with PAM_PROMPT_ECHO_ON') + msg2 = PamMessage(PAM_PROMPT_ECHO_OFF, b'overwrite with PAM_PROMPT_ECHO_OFF') + + ptr1 = pointer(msg1) + ptr2 = pointer(msg2) + + ptrs = pointer(ptr1) + ptrs[1] = ptr2 + + pp_messages = pointer(ptrs[0]) + + response = PamResponse(b'overwrite', -1) + pp_response = pointer(pointer(response)) + + encoding = 'utf-8' + password = b'blank' + msg_list = [] + + libc = cdll.LoadLibrary(None) + + rv = my_conv(n_messages, + pp_messages, + pp_response, + libc, + msg_list, + password, + encoding) + + assert None is pp_response.contents.contents.resp + assert 0 == pp_response.contents.contents.resp_retcode + assert PAM_SUCCESS == rv From de17aa2dc1a9f56eb8ea4877a2d62ca5f80952f3 Mon Sep 17 00:00:00 2001 From: David Ford Date: Mon, 14 Mar 2022 22:42:36 -0400 Subject: [PATCH 07/17] Fixes #31 by restoring the boolean return value and redeclares the docstring for the legacy implementation as well as the py2 workaround for typehinting --- src/pam/__init__.py | 14 ++++++++--- src/pam/__internals.py | 55 ++++++++++++++++++----------------------- tests/test_internals.py | 12 +++++---- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/pam/__init__.py b/src/pam/__init__.py index 374ef9d..0c6f9e0 100644 --- a/src/pam/__init__.py +++ b/src/pam/__init__.py @@ -116,14 +116,22 @@ __PA = None -def authenticate(*args, **kvargs): +def authenticate(username, + password, + service='login', + env=None, + call_end=True, + encoding='utf-8', + resetcreds=True, + print_failure_messages=False): global __PA if __PA is None: # pragma: no branch __PA = PamAuthenticator() - return __PA.authenticate(*args, **kvargs) + return __PA.authenticate(username, password, service, env, call_end, encoding, resetcreds, print_failure_messages) # legacy implementations used pam.pam() -pam = authenticate +pam = PamAuthenticator +authenticate.__doc__ = PamAuthenticator.authenticate.__doc__ diff --git a/src/pam/__internals.py b/src/pam/__internals.py index 9f3adb0..bc02e82 100644 --- a/src/pam/__internals.py +++ b/src/pam/__internals.py @@ -17,6 +17,7 @@ from ctypes import c_void_p from ctypes import memmove from ctypes.util import find_library +from typing import Union PAM_ABORT = 26 PAM_ACCT_EXPIRED = 13 @@ -173,7 +174,7 @@ class PamConv(Structure): class PamAuthenticator: code = 0 - reason = None + reason = None # type: Union[str, bytes, None] def __init__(self): # use a trick of dlopen(), this effectively becomes @@ -252,23 +253,15 @@ def __init__(self): def authenticate( self, - username, - password, - service='login', - env=None, - call_end=True, - encoding='utf-8', - resetcreds=True, - print_failure_messages=False): - self.pam_authenticate.__annotations = {'username': str, - 'password': str, - 'service': str, - 'env': dict, - 'call_end': bool, - 'encoding': str, - 'resetcreds': bool, - 'return': bool, - 'print_failure_messages': bool} + username, # type: Union[str, bytes] + password, # type: Union[str, bytes] + service='login', # type: Union[str, bytes] + env=None, # type: dict + call_end=True, # type: bool + encoding='utf-8', # type: str + resetcreds=True, # type: bool + print_failure_messages=False # type: bool + ): # type: (...) -> bool """username and password authentication for the given service. Returns True for success, or False for failure. @@ -281,15 +274,15 @@ def authenticate( necessary conversions using the supplied encoding. Args: - username: username to authenticate - password: password in plain text - service: PAM service to authenticate against, defaults to 'login' - env: Pam environment variables - call_end: call the pam_end() function after (default true) - print_failure_messages: Print messages on failure + username (str): username to authenticate + password (str): password in plain text + service (str): PAM service to authenticate against, defaults to 'login' + env (dict): Pam environment variables + call_end (bool): call the pam_end() function after (default true) + print_failure_messages (bool): Print messages on failure Returns: - success: True + success: PAM_SUCCESS failure: False """ @@ -331,7 +324,7 @@ def __conv(n_messages, messages, p_response, app_data): self.code = retval self.reason = ("pam_start() failed: %s" % self.pam_strerror(self.handle, retval)) - return retval + return False # set the TTY, required when pam_securetty is used and the username # root is used note: this is only needed WHEN the pam_securetty.so @@ -353,10 +346,10 @@ def __conv(n_messages, messages, p_response, app_data): # ctty can be invalid if no tty is being used if ctty: # pragma: no branch - ctty = c_char_p(ctty.encode(encoding)) + ctty_p = c_char_p(ctty.encode(encoding)) - retval = self.pam_set_item(self.handle, PAM_TTY, ctty) - retval = self.pam_set_item(self.handle, PAM_XDISPLAY, ctty) + retval = self.pam_set_item(self.handle, PAM_TTY, ctty_p) + retval = self.pam_set_item(self.handle, PAM_XDISPLAY, ctty_p) # Set the environment variables if they were supplied if env: @@ -387,7 +380,7 @@ def __conv(n_messages, messages, p_response, app_data): self.reason = self.pam_strerror(self.handle, auth_success) if sys.version_info >= (3,): # pragma: no branch - self.reason = self.reason.decode(encoding) + self.reason = self.reason.decode(encoding) # type: ignore if call_end and hasattr(self, 'pam_end'): # pragma: no branch self.pam_end(self.handle, auth_success) @@ -396,7 +389,7 @@ def __conv(n_messages, messages, p_response, app_data): if print_failure_messages and self.code != PAM_SUCCESS: # pragma: no cover print(f"Failure: {self.reason}") - return auth_success + return auth_success == PAM_SUCCESS def end(self): """A direct call to pam_end() diff --git a/tests/test_internals.py b/tests/test_internals.py index 4c92b0d..de3e131 100644 --- a/tests/test_internals.py +++ b/tests/test_internals.py @@ -85,7 +85,7 @@ def test_PamAuthenticator__normal_success(pam_obj): pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") rv = pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD) - assert PAM_SUCCESS == rv + assert True is rv def test_PamAuthenticator__normal_password_failure(pam_obj): @@ -93,12 +93,14 @@ def test_PamAuthenticator__normal_password_failure(pam_obj): pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") rv = pam_obj.authenticate(TEST_USERNAME, 'not-valid') - assert PAM_AUTH_ERR == rv + assert False is rv + assert PAM_AUTH_ERR == pam_obj.code def test_PamAuthenticator__normal_unknown_username(pam_obj): rv = pam_obj.authenticate('bad_user_name', '') - assert rv in (PAM_AUTH_ERR, PAM_USER_UNKNOWN) + assert False is rv + assert pam_obj.code in (PAM_AUTH_ERR, PAM_USER_UNKNOWN) def test_PamAuthenticator__unset_DISPLAY(pam_obj): @@ -111,7 +113,7 @@ def test_PamAuthenticator__unset_DISPLAY(pam_obj): if not (TEST_USERNAME and TEST_PASSWORD): pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - assert PAM_SUCCESS == rv + assert True is rv def test_PamAuthenticator__env_requires_dict(pam_obj): @@ -137,7 +139,7 @@ def test_PamAuthenticator__env_set(pam_obj): if not (TEST_USERNAME and TEST_PASSWORD): pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - assert PAM_SUCCESS == rv + assert True == rv def test_PamAuthenticator__putenv_incomplete_setup(pam_obj): From 4a8e1423dc2550d9ea4cf06994fc6ee83e379a74 Mon Sep 17 00:00:00 2001 From: David Ford Date: Mon, 14 Mar 2022 23:02:34 -0400 Subject: [PATCH 08/17] Create main.yml --- .github/workflows/main.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..36657a0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + tox From d9a3bc2049e7312de71f6d64f4f800a6becb5fd6 Mon Sep 17 00:00:00 2001 From: David Ford Date: Mon, 14 Mar 2022 23:06:03 -0400 Subject: [PATCH 09/17] install reqs --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 36657a0..ebe6919 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,5 +17,9 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Tox test run: | tox From fb0ffd218e93e5ef3c74752c1ed4fd4a52ee847f Mon Sep 17 00:00:00 2001 From: David Ford Date: Mon, 14 Mar 2022 23:12:27 -0400 Subject: [PATCH 10/17] use only py310 for tox --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ebe6919..d900631 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.10"] steps: - uses: actions/checkout@v2 @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox + pip install tox tox-gh-actions - name: Tox test run: | tox From 5225964c99b60d4079397edc9df59b9bd4076208 Mon Sep 17 00:00:00 2001 From: David Ford Date: Tue, 15 Mar 2022 19:22:00 -0400 Subject: [PATCH 11/17] debugging tox in gh-actions --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d900631..eccb36b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,4 +22,4 @@ jobs: pip install tox tox-gh-actions - name: Tox test run: | - tox + tox -vv From f150e61de305870082f26fcdfba9e687a22c72a1 Mon Sep 17 00:00:00 2001 From: David Ford Date: Tue, 15 Mar 2022 19:29:54 -0400 Subject: [PATCH 12/17] debugging tox in gh-actions --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d8ef361..83d8bee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,13 @@ envlist = py310 isolated_build = true #skipsdist = true +[gh-actions] +python = + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + [testenv] basepython = python3.10 passenv = * From 002e04a321201e25d636d98be780e2655f6bb5cd Mon Sep 17 00:00:00 2001 From: David Ford Date: Thu, 17 Mar 2022 19:32:30 -0400 Subject: [PATCH 13/17] Mock a few of the libpam interfaces so we can start fully testing all branches in PamAuthenticate --- src/pam/__internals.py | 20 +++++----- tests/test_internals.py | 86 ++++++++++++++++++++++++++--------------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/pam/__internals.py b/src/pam/__internals.py index 9f3adb0..3683a9f 100644 --- a/src/pam/__internals.py +++ b/src/pam/__internals.py @@ -69,6 +69,7 @@ PAM_USER_UNKNOWN = 10 PAM_XDISPLAY = 11 + __all__ = ('PAM_ABORT', 'PAM_ACCT_EXPIRED', 'PAM_AUTHINFO_UNAVAIL', 'PAM_AUTHTOK_DISABLE_AGING', 'PAM_AUTHTOK_ERR', 'PAM_AUTHTOK_EXPIRED', 'PAM_AUTHTOK_LOCK_BUSY', @@ -127,7 +128,6 @@ def my_conv(n_messages, messages, p_response, libc, msg_list: list, password: by """Simple conversation function that responds to any prompt where the echo is off with the supplied password""" # Create an array of n_messages response objects - calloc = libc.calloc calloc.restype = c_void_p calloc.argtypes = [c_size_t, c_size_t] @@ -260,15 +260,15 @@ def authenticate( encoding='utf-8', resetcreds=True, print_failure_messages=False): - self.pam_authenticate.__annotations = {'username': str, - 'password': str, - 'service': str, - 'env': dict, - 'call_end': bool, - 'encoding': str, - 'resetcreds': bool, - 'return': bool, - 'print_failure_messages': bool} + # self.pam_authenticate.__annotations = {'username': str, + # 'password': str, + # 'service': str, + # 'env': dict, + # 'call_end': bool, + # 'encoding': str, + # 'resetcreds': bool, + # 'return': bool, + # 'print_failure_messages': bool} """username and password authentication for the given service. Returns True for success, or False for failure. diff --git a/tests/test_internals.py b/tests/test_internals.py index 4c92b0d..15e83f7 100644 --- a/tests/test_internals.py +++ b/tests/test_internals.py @@ -1,5 +1,6 @@ import os import pytest +# from pytest import monkeypatch from ctypes import cdll from ctypes import c_void_p @@ -19,15 +20,54 @@ from pam.__internals import PamAuthenticator from pam.__internals import my_conv -# In order to run some tests, we need a working user/pass combo -# you can specify these on the command line -TEST_USERNAME = os.getenv('TEST_USERNAME', '') -TEST_PASSWORD = os.getenv('TEST_PASSWORD', '') + +class MockPam: + def __init__(self, og): + self.og = og + self.og_pam_start = og.pam_start + self.PA_authenticate = og.authenticate + self.username = None + self.password = None + + def authenticate(self, *args, **kwargs): + if len(args) > 0: + self.username = args[0] + if len(args) > 1: + self.password = args[1] + self.service = kwargs.get('service') + return self.PA_authenticate(*args, **kwargs) + + def pam_start(self, service, username, conv, handle): + rv = self.og_pam_start(service, username, conv, handle) + return rv + + def pam_authenticate(self, handle, flags): + if isinstance(self.username, str): + self.username = self.username.encode() + if isinstance(self.password, str): + self.password = self.password.encode() + + if self.username == b'good_username' and self.password == b'good_password': + return PAM_SUCCESS + + if self.username == b'unknown_username': + return PAM_USER_UNKNOWN + + return PAM_AUTH_ERR + + def pam_acct_mgmt(self, handle, flags): + # we don't test anything here (yet) + return PAM_SUCCESS @pytest.fixture -def pam_obj(request): +def pam_obj(request, monkeypatch): obj = PamAuthenticator() + MP = MockPam(obj) + monkeypatch.setattr(obj, 'authenticate', MP.authenticate) + monkeypatch.setattr(obj, 'pam_start', MP.pam_start) + monkeypatch.setattr(obj, 'pam_authenticate', MP.pam_authenticate) + monkeypatch.setattr(obj, 'pam_acct_mgmt', MP.pam_acct_mgmt) yield obj @@ -81,62 +121,44 @@ def test_PamAuthenticator__requires_service_no_nulls(pam_obj): # TEST_* require a valid account def test_PamAuthenticator__normal_success(pam_obj): - if not (TEST_USERNAME and TEST_PASSWORD): - pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - - rv = pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD) + rv = pam_obj.authenticate('good_username', 'good_password') assert PAM_SUCCESS == rv def test_PamAuthenticator__normal_password_failure(pam_obj): - if not (TEST_USERNAME and TEST_PASSWORD): - pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - - rv = pam_obj.authenticate(TEST_USERNAME, 'not-valid') + rv = pam_obj.authenticate('good_username', 'bad_password') assert PAM_AUTH_ERR == rv def test_PamAuthenticator__normal_unknown_username(pam_obj): - rv = pam_obj.authenticate('bad_user_name', '') - assert rv in (PAM_AUTH_ERR, PAM_USER_UNKNOWN) + rv = pam_obj.authenticate('unknown_username', '') + assert PAM_USER_UNKNOWN == rv def test_PamAuthenticator__unset_DISPLAY(pam_obj): os.environ['DISPLAY'] = '' - rv = pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD) - - # yes, this is intentional. this lets us run code coverage on the - # affected area even though we know the assert would have failed - if not (TEST_USERNAME and TEST_PASSWORD): - pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - + rv = pam_obj.authenticate('good_username', 'good_password') assert PAM_SUCCESS == rv def test_PamAuthenticator__env_requires_dict(pam_obj): with pytest.raises(TypeError): - pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD, env='value') + pam_obj.authenticate('good_username', 'good_password', env='value') def test_PamAuthenticator__env_requires_key_no_nulls(pam_obj): with pytest.raises(ValueError): - pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD, env={b'\x00invalid_key': b'value'}) + pam_obj.authenticate('good_username', 'good_password', env={b'\x00invalid_key': b'value'}) def test_PamAuthenticator__env_requires_value_no_nulls(pam_obj): with pytest.raises(ValueError): - pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD, env={b'key': b'\x00invalid_value'}) + pam_obj.authenticate('good_username', 'good_password', env={b'key': b'\x00invalid_value'}) def test_PamAuthenticator__env_set(pam_obj): - rv = pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD, env={'key': b'value'}) - - # yes, this is intentional. this lets us run code coverage on the - # affected area even though we know the assert would have failed - if not (TEST_USERNAME and TEST_PASSWORD): - pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - + rv = pam_obj.authenticate('good_username', 'good_password', env={'key': b'value'}) assert PAM_SUCCESS == rv From af708cf1bc777094ce2003f25e922ce1a3cb5265 Mon Sep 17 00:00:00 2001 From: David Ford Date: Thu, 17 Mar 2022 20:05:51 -0400 Subject: [PATCH 14/17] merge the issue-31 changes --- src/pam/__internals.py | 12 +++--- tests/test_internals.py | 90 ++++++++++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/src/pam/__internals.py b/src/pam/__internals.py index bc02e82..99a0397 100644 --- a/src/pam/__internals.py +++ b/src/pam/__internals.py @@ -70,6 +70,7 @@ PAM_USER_UNKNOWN = 10 PAM_XDISPLAY = 11 + __all__ = ('PAM_ABORT', 'PAM_ACCT_EXPIRED', 'PAM_AUTHINFO_UNAVAIL', 'PAM_AUTHTOK_DISABLE_AGING', 'PAM_AUTHTOK_ERR', 'PAM_AUTHTOK_EXPIRED', 'PAM_AUTHTOK_LOCK_BUSY', @@ -128,7 +129,6 @@ def my_conv(n_messages, messages, p_response, libc, msg_list: list, password: by """Simple conversation function that responds to any prompt where the echo is off with the supplied password""" # Create an array of n_messages response objects - calloc = libc.calloc calloc.restype = c_void_p calloc.argtypes = [c_size_t, c_size_t] @@ -345,7 +345,7 @@ def __conv(n_messages, messages, p_response, app_data): ctty = os.ttyname(0) # ctty can be invalid if no tty is being used - if ctty: # pragma: no branch + if ctty: # pragma: no branch (we don't test a void tty yet) ctty_p = c_char_p(ctty.encode(encoding)) retval = self.pam_set_item(self.handle, PAM_TTY, ctty_p) @@ -367,19 +367,17 @@ def __conv(n_messages, messages, p_response, app_data): auth_success = self.pam_authenticate(self.handle, 0) - # skip code coverage, this can only succeed when TEST_* is supplied - # in the environment. we ostensibly know it will work if auth_success == PAM_SUCCESS: - auth_success = self.pam_acct_mgmt(self.handle, 0) # pragma: no cover + auth_success = self.pam_acct_mgmt(self.handle, 0) if auth_success == PAM_SUCCESS and resetcreds: - auth_success = self.pam_setcred(self.handle, PAM_REINITIALIZE_CRED) # pragma: no cover + auth_success = self.pam_setcred(self.handle, PAM_REINITIALIZE_CRED) # store information to inform the caller why we failed self.code = auth_success self.reason = self.pam_strerror(self.handle, auth_success) - if sys.version_info >= (3,): # pragma: no branch + if sys.version_info >= (3,): # pragma: no branch (we don't test non-py3 versions) self.reason = self.reason.decode(encoding) # type: ignore if call_end and hasattr(self, 'pam_end'): # pragma: no branch diff --git a/tests/test_internals.py b/tests/test_internals.py index de3e131..271e5d9 100644 --- a/tests/test_internals.py +++ b/tests/test_internals.py @@ -1,5 +1,6 @@ import os import pytest +# from pytest import monkeypatch from ctypes import cdll from ctypes import c_void_p @@ -19,15 +20,54 @@ from pam.__internals import PamAuthenticator from pam.__internals import my_conv -# In order to run some tests, we need a working user/pass combo -# you can specify these on the command line -TEST_USERNAME = os.getenv('TEST_USERNAME', '') -TEST_PASSWORD = os.getenv('TEST_PASSWORD', '') + +class MockPam: + def __init__(self, og): + self.og = og + self.og_pam_start = og.pam_start + self.PA_authenticate = og.authenticate + self.username = None + self.password = None + + def authenticate(self, *args, **kwargs): + if len(args) > 0: + self.username = args[0] + if len(args) > 1: + self.password = args[1] + self.service = kwargs.get('service') + return self.PA_authenticate(*args, **kwargs) + + def pam_start(self, service, username, conv, handle): + rv = self.og_pam_start(service, username, conv, handle) + return rv + + def pam_authenticate(self, handle, flags): + if isinstance(self.username, str): + self.username = self.username.encode() + if isinstance(self.password, str): + self.password = self.password.encode() + + if self.username == b'good_username' and self.password == b'good_password': + return PAM_SUCCESS + + if self.username == b'unknown_username': + return PAM_USER_UNKNOWN + + return PAM_AUTH_ERR + + def pam_acct_mgmt(self, handle, flags): + # we don't test anything here (yet) + return PAM_SUCCESS @pytest.fixture -def pam_obj(request): +def pam_obj(request, monkeypatch): obj = PamAuthenticator() + MP = MockPam(obj) + monkeypatch.setattr(obj, 'authenticate', MP.authenticate) + monkeypatch.setattr(obj, 'pam_start', MP.pam_start) + monkeypatch.setattr(obj, 'pam_authenticate', MP.pam_authenticate) + monkeypatch.setattr(obj, 'pam_acct_mgmt', MP.pam_acct_mgmt) yield obj @@ -81,65 +121,49 @@ def test_PamAuthenticator__requires_service_no_nulls(pam_obj): # TEST_* require a valid account def test_PamAuthenticator__normal_success(pam_obj): - if not (TEST_USERNAME and TEST_PASSWORD): - pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - - rv = pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD) + rv = pam_obj.authenticate('good_username', 'good_password') assert True is rv def test_PamAuthenticator__normal_password_failure(pam_obj): - if not (TEST_USERNAME and TEST_PASSWORD): - pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - - rv = pam_obj.authenticate(TEST_USERNAME, 'not-valid') + rv = pam_obj.authenticate('good_username', 'bad_password') assert False is rv assert PAM_AUTH_ERR == pam_obj.code def test_PamAuthenticator__normal_unknown_username(pam_obj): - rv = pam_obj.authenticate('bad_user_name', '') + rv = pam_obj.authenticate('unknown_username', '') assert False is rv - assert pam_obj.code in (PAM_AUTH_ERR, PAM_USER_UNKNOWN) + assert PAM_USER_UNKNOWN == pam_obj.code def test_PamAuthenticator__unset_DISPLAY(pam_obj): os.environ['DISPLAY'] = '' - rv = pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD) - - # yes, this is intentional. this lets us run code coverage on the - # affected area even though we know the assert would have failed - if not (TEST_USERNAME and TEST_PASSWORD): - pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - + rv = pam_obj.authenticate('good_username', 'good_password') assert True is rv + assert PAM_SUCCESS == pam_obj.code def test_PamAuthenticator__env_requires_dict(pam_obj): with pytest.raises(TypeError): - pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD, env='value') + pam_obj.authenticate('good_username', 'good_password', env='value') def test_PamAuthenticator__env_requires_key_no_nulls(pam_obj): with pytest.raises(ValueError): - pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD, env={b'\x00invalid_key': b'value'}) + pam_obj.authenticate('good_username', 'good_password', env={b'\x00invalid_key': b'value'}) def test_PamAuthenticator__env_requires_value_no_nulls(pam_obj): with pytest.raises(ValueError): - pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD, env={b'key': b'\x00invalid_value'}) + pam_obj.authenticate('good_username', 'good_password', env={b'key': b'\x00invalid_value'}) def test_PamAuthenticator__env_set(pam_obj): - rv = pam_obj.authenticate(TEST_USERNAME, TEST_PASSWORD, env={'key': b'value'}) - - # yes, this is intentional. this lets us run code coverage on the - # affected area even though we know the assert would have failed - if not (TEST_USERNAME and TEST_PASSWORD): - pytest.skip("test requires valid TEST_USERNAME and TEST_PASSWORD set in environment") - - assert True == rv + rv = pam_obj.authenticate('good_username', 'good_password', env={'key': b'value'}) + assert True is rv + assert PAM_SUCCESS == pam_obj.code def test_PamAuthenticator__putenv_incomplete_setup(pam_obj): From aa64482c9de78672b67e09adefe346e0e250d7af Mon Sep 17 00:00:00 2001 From: David Ford Date: Thu, 17 Mar 2022 20:23:50 -0400 Subject: [PATCH 15/17] update the version to 2.0.2 --- src/pam/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pam/version.py b/src/pam/version.py index 0dce6ae..b74741f 100644 --- a/src/pam/version.py +++ b/src/pam/version.py @@ -1,3 +1,3 @@ -VERSION = '2.0.0' +VERSION = '2.0.2' AUTHOR = 'David Ford ' -RELEASED = '2022 March 10' +RELEASED = '2022 March 17' From 2408c2eb8ada2bf5e649959679abe202d9ea7ac9 Mon Sep 17 00:00:00 2001 From: David Ford Date: Thu, 17 Mar 2022 20:32:52 -0400 Subject: [PATCH 16/17] add the pypi publish steps to the Makefile --- Makefile | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Makefile b/Makefile index 0fe55fa..11142b5 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,16 @@ lint: pydeps preflight: bandit test +publish-pypi-test: clean venv build + . venv/bin/activate; \ + python3 -m pip install --upgrade twine && \ + python3 -m twine upload --repository testpypi dist/* + +publish-pypi: clean venv build + . venv/bin/activate; \ + python3 -m pip install --upgrade twine && \ + python3 -m twine upload --repository pypi dist/* + pydeps: . venv/bin/activate; \ pip install --upgrade -q pip && \ From 388d67ea99908a42063574a71b4b6a97180fe42c Mon Sep 17 00:00:00 2001 From: Matti Kaupenjohann Date: Tue, 28 Nov 2023 10:09:17 +0100 Subject: [PATCH 17/17] Update gitignore exclude .venv folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 65ac6bb..3e15450 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Distribution / packaging .Python env/ +.venv/ bin/ build/ develop-eggs/