diff --git a/setup.cfg b/.bumpversion.cfg similarity index 60% rename from setup.cfg rename to .bumpversion.cfg index c5b91fcc..ff2694a6 100644 --- a/setup.cfg +++ b/.bumpversion.cfg @@ -4,10 +4,3 @@ commit = True tag = False [bumpversion:file:mozilla_django_oidc/__init__.py] - -[wheel] -universal = 1 - -[flake8] -max-line-length = 99 -exclude = docs,build diff --git a/.circleci/config.yml b/.circleci/config.yml index 94cf4efd..fdfa614d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,7 +13,7 @@ common_steps: &common_steps command: echo 127.0.0.1 testprovider | tee -a /etc/hosts - run: name: Install latest lib build - command: . /testrp_env/bin/activate && pip install /tmp/workspace/mozilla-django-oidc-dev.tar.gz + command: . /testrp_env/bin/activate && pip install /tmp/workspace/mozilla-django-oidc-dev.tar.gz[test] - run: name: Override django version command: . /testrp_env/bin/activate && pip install $DJANGO_VERSION @@ -45,8 +45,9 @@ jobs: steps: - checkout - run: mkdir workspace + - run: pip install '.[dev]' - run: make sdist - - run: mv dist/mozilla-django-oidc-* workspace/mozilla-django-oidc-dev.tar.gz + - run: mv dist/mozilla_django_oidc-* workspace/mozilla-django-oidc-dev.tar.gz - persist_to_workspace: root: workspace paths: diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..3f14f8ff --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 99 +exclude = docs,build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8cac75b..434a4c57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,9 @@ jobs: name: Python ${{ matrix.python_version }} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - name: Install dependencies diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index a59694af..1c1232e0 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -7,15 +7,15 @@ jobs: name: Codecov steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: pip install codecov tox tox-gh-actions - name: Run tox run: tox - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v5 with: verbose: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cc6dedc..147daeb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,9 +10,9 @@ jobs: name: Mozilla Django OIDC Release steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install dependencies @@ -28,7 +28,7 @@ jobs: --outdir dist/ . - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@v1.4.2 + uses: pypa/gh-action-pypi-publish@v1 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') with: user: __token__ diff --git a/AUTHORS.rst b/AUTHORS.rst index de5759ee..57f43ec2 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -29,3 +29,4 @@ Contributors * Antti Palola (`@anttipalola `_) * Bono de Visser (`@kerrermanisNL `_) * Chris Brantley (`@chrisbrantley `_) +* Ryan Johnson (`@escattone `_) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 3b73a21a..4366fa96 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -62,11 +62,14 @@ Ready to contribute? Here's how to set up `mozilla-django-oidc` for local develo $ git clone git@github.com:your_name_here/mozilla-django-oidc.git -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: +3. Create a virtual environment and install the package with its + development dependencies. This command will install all the tools + needed for testing and linting:: - $ mkvirtualenv mozilla-django-oidc $ cd mozilla-django-oidc/ - $ python setup.py develop + $ python -m venv .venv + $ source .venv/bin/activate # On Windows use `.venv\Scripts\activate` + $ pip install -e .[dev] 4. Create a branch for local development:: @@ -77,12 +80,10 @@ Ready to contribute? Here's how to set up `mozilla-django-oidc` for local develo 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: - $ flake8 mozilla_django_oidc tests - $ python setup.py test + $ make lint + $ make test $ tox - To get flake8 and tox, just pip install them into your virtualenv. - 6. Make sure you update ``HISTORY.rst`` with your changes in the following categories * Backwards-incompatible changes @@ -106,9 +107,9 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 3.6+ and for PyPy. Check - ``_ - and make sure that the tests pass for all supported Python versions. +3. The pull request should work for Python 3.8+. Check + ``_ and make sure + that the tests pass for all supported Python versions. Tips ---- @@ -120,14 +121,14 @@ We use tox to run tests:: To run a specific environment, use the ``-e`` argument:: - $ tox -e py27-django111 + $ tox -e py312-django420 You can also run the tests in a virtual environment without tox:: - $ DJANGO_SETTINGS_MODULE=tests.settings django-admin test + $ make test You can specify test modules to run rather than the whole suite:: - $ DJANGO_SETTINGS_MODULE=tests.settings django-admin test tests.test_views + $ DJANGO_SETTINGS_MODULE=tests.settings python -m django test tests.test_views diff --git a/MANIFEST.in b/MANIFEST.in index e4762095..1962fcfb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,8 @@ include CONTRIBUTING.rst include HISTORY.rst include LICENSE include README.rst -recursive-include mozilla_django_oidc *.html *.png *.gif *js *.css *jpg *jpeg *svg *py + +include .flake8 +include .bumpversion.cfg +recursive-include docs * +recursive-include integration_tests * diff --git a/Makefile b/Makefile index fdb466ce..6e8a1a72 100644 --- a/Makefile +++ b/Makefile @@ -15,18 +15,19 @@ clean-pyc: ## remove Python file artifacts find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -rf {} + lint: ## check style with flake8 flake8 mozilla_django_oidc tests test: ## run tests quickly with the default Python - DJANGO_SETTINGS_MODULE=tests.settings django-admin test + DJANGO_SETTINGS_MODULE=tests.settings python -m django test test-all: ## run tests on every Python version with tox tox coverage: ## check code coverage quickly with the default Python - DJANGO_SETTINGS_MODULE=tests.settings coverage run --source mozilla_django_oidc `which django-admin` test + coverage run --source mozilla_django_oidc -m django test --settings=tests.settings coverage report -m coverage html open htmlcov/index.html @@ -37,10 +38,13 @@ docs: ## generate Sphinx HTML documentation, including API docs $(MAKE) -C docs clean $(MAKE) -C docs html -release: clean ## package and upload a release - python setup.py sdist upload - python setup.py bdist_wheel upload +build: clean ## build the sdist and wheel + python -m build . --sdist --wheel + ls -l dist + +release: build ##package and upload a release + twine upload dist/* sdist: clean ## package - python setup.py sdist + python -m build . --sdist ls -l dist diff --git a/README.rst b/README.rst index 3c17168a..19fe5902 100644 --- a/README.rst +++ b/README.rst @@ -55,20 +55,16 @@ Django of your choice. Here is such an example: .. code-block:: shell - $ virtualenv -p /path/to/bin/python3.8 venv - $ source venv - (venv) $ pip install -r requirements/requirements_dev.txt - (venv) $ DJANGO_SETTINGS_MODULE=tests.settings django-admin test + $ python -m venv venv + $ source ./venv/bin/activate + (venv) $ pip install '.[dev]' + (venv) $ make test Measuring code coverage, continuing the steps above: .. code-block:: shell - (venv) $ pip install coverage - (venv) $ DJANGO_SETTINGS_MODULE=tests.settings coverage run --source mozilla_django_oidc `which django-admin` test - (venv) $ coverage report - (venv) $ coverage html - (venv) $ open htmlcov/index.html + (venv) $ make coverage Local development ----------------- diff --git a/docker-compose.yml b/docker-compose.yml index 7134bb24..bfa7117f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ # Based in the `docker-test-mozilla-django-oidc` images # https://github.com/mozilla/docker-test-mozilla-django-oidc -version: '3' services: testprovider: stdin_open: true @@ -36,7 +35,8 @@ services: - testrp - testprovider command: >- - bash -c "cd /integration_tests && + bash -c "pip install 'splinter[selenium]' && + cd /integration_tests && wait-for-it -t 60 -p 8080 -h testprovider && wait-for-it -t 60 -p 8081 -h testrp && python integration_tests.py" diff --git a/integration_tests/integration_tests.py b/integration_tests/integration_tests.py index 7af239f7..48ad7e9c 100644 --- a/integration_tests/integration_tests.py +++ b/integration_tests/integration_tests.py @@ -1,6 +1,6 @@ import unittest -from splinter import Browser +from splinter import Browser, Config class IntegrationTest(unittest.TestCase): @@ -13,10 +13,11 @@ def __init__(self, *args, **kwargs): "password": "example_p@ssw0rd", "email": "example@example.com", } + self.splinter_config = Config(headless=True) def setUp(self): """Create test account in `testprovider` instance""" - with Browser(self.webdriver, headless=True) as browser: + with Browser(self.webdriver, config=self.splinter_config) as browser: browser.visit("http://testprovider:8080/account/signup") browser.find_by_css("#id_username").fill(self.account["username"]) browser.find_by_css("#id_password").fill(self.account["password"]) @@ -26,7 +27,7 @@ def setUp(self): def tearDown(self): """Remove test account from `testprovider` instance""" - with Browser(self.webdriver, headless=True) as browser: + with Browser(self.webdriver, config=self.splinter_config) as browser: self.perform_login(browser) browser.visit("http://testprovider:8080/account/delete") browser.find_by_css(".btn-danger").click() @@ -46,7 +47,7 @@ def perform_logout(self, browser): def test_login(self): """Test logging in `testrp` using OIDC""" - browser = Browser(self.webdriver, headless=True) + browser = Browser(self.webdriver, config=self.splinter_config) # Check that user is not logged in browser.visit("http://testrp:8081") @@ -63,7 +64,7 @@ def test_login(self): def test_logout(self): """Test logout functionality of OIDC lib""" - browser = Browser(self.webdriver, headless=True) + browser = Browser(self.webdriver, config=self.splinter_config) # Check that user is not logged in browser.visit("http://testrp:8081") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..916bc635 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,90 @@ +[build-system] +requires = [ + "setuptools>=61.0.0", + "wheel" +] +build-backend = "setuptools.build_meta" + +[project] +name = "mozilla-django-oidc" +dynamic = ["version", "readme"] +description = "A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services." +authors = [ + { name = "Tasos Katsoulas", email = "akatsoulas@mozilla.com" }, + { name = "John Giannelos", email = "jgiannelos@mozilla.com" } +] +license = { text = "MPL-2.0" } +keywords = ["mozilla-django-oidc"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Intended Audience :: Developers", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "Django >= 3.2", + "pyjwt", + "requests", + "cryptography", +] + +[project.urls] +Homepage = "https://github.com/mozilla/mozilla-django-oidc" +Documentation = "https://mozilla-django-oidc.readthedocs.io" +Changelog = "https://github.com/mozilla/mozilla-django-oidc/releases" + +[project.optional-dependencies] +drf = [ + "djangorestframework>=3.14", +] +test = [ + "bumpversion", + "ipython", + "ipdb", + "djangorestframework>=3.14", + "pre-commit", + "responses", + "importlib-metadata; python_version < '3.10'", + "splinter[selenium]>=0.21.0", + "coverage", + "tox", +] +lint = [ + "flake8", +] +build = [ + "build", +] +docs = [ + "sphinx", + "sphinx_rtd_theme", + "readthedocs-sphinx-search", +] +dev = [ + "mozilla-django-oidc[test,docs,drf,lint,build]", +] + +[tool.setuptools] +include-package-data = true +zip-safe = false + +[tool.setuptools.dynamic] +version = { attr = "mozilla_django_oidc.__version__" } +readme = { file = ["README.rst"], "content-type" = "text/x-rst" } + +[tool.setuptools.packages.find] +include = ["mozilla_django_oidc", "mozilla_django_oidc.*"] +exclude = ["build*", "dist*", "*.egg-info*"] diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt deleted file mode 100644 index c1471b21..00000000 --- a/requirements/requirements_dev.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Install mozilla-django-oidc --e . - -bumpversion -wheel -ipython -ipdb -mock -tox -djangorestframework -pre-commit -flake8 -sphinx -sphinx_rtd_theme -readthedocs-sphinx-search -importlib-metadata diff --git a/setup.py b/setup.py deleted file mode 100755 index 071cb96a..00000000 --- a/setup.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import os -import sys - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -from mozilla_django_oidc import __version__ as VERSION - -if sys.argv[-1] == "publish": - try: - import wheel - - print("Wheel version: ", wheel.__version__) - except ImportError: - print('Wheel library missing. Please run "pip install wheel"') - sys.exit() - os.system("python setup.py sdist upload") - os.system("python setup.py bdist_wheel upload") - sys.exit() - -if sys.argv[-1] == "tag": - print("Tagging the version on git:") - os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION)) - os.system("git push --tags") - sys.exit() - -readme = open("README.rst").read() -history = open("HISTORY.rst").read().replace(".. :changelog:", "") - -install_requirements = [ - "Django >= 3.2", - "pyjwt", - "requests", - "cryptography", -] - -setup( - name="mozilla-django-oidc", - version=VERSION, - description="""A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services.""", # noqa - long_description=readme + "\n\n" + history, - author="Tasos Katsoulas, John Giannelos", - author_email="akatsoulas@mozilla.com, jgiannelos@mozilla.com", - url="https://github.com/mozilla/mozilla-django-oidc", - packages=["mozilla_django_oidc"], - include_package_data=True, - install_requires=install_requirements, - license="MPL 2.0", - zip_safe=False, - keywords="mozilla-django-oidc", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Framework :: Django", - "Framework :: Django :: 3.2", - "Framework :: Django :: 4.2", - "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", - "Intended Audience :: Developers", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - "Natural Language :: English", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], -) diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_auth.py b/tests/test_auth.py index a78dab48..f7ce1708 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -849,10 +849,12 @@ def test_jwt_verify_sign_key(self, request_mock): key = rsa.generate_private_key(public_exponent=65537, key_size=2048) # Make the public key available through the JWKS response - public_key = smart_str(key.public_key().public_bytes( - serialization.Encoding.PEM, - serialization.PublicFormat.PKCS1, - )) + public_key = smart_str( + key.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.PKCS1, + ) + ) with override_settings(OIDC_RP_IDP_SIGN_KEY=public_key): backend = OIDCAuthenticationBackend() @@ -966,7 +968,9 @@ def test_retrieve_matching_jwk(self, mock_requests): h.update(force_bytes(msg)) signature = base64_url_encode(h.finalize()) - token = "{}.{}.{}".format(base64_url_encode(header), base64_url_encode(payload), signature) + token = "{}.{}.{}".format( + base64_url_encode(header), base64_url_encode(payload), signature + ) jwk_key = self.backend.retrieve_matching_jwk(token) self.assertEqual(jwk_key._jwk_data, get_json_mock.json.return_value["keys"][0]) @@ -1013,7 +1017,9 @@ def test_retrieve_matching_jwk_same_kid(self, mock_requests): h.update(force_bytes(msg)) signature = base64_url_encode(h.finalize()) - token = "{}.{}.{}".format(base64_url_encode(header), base64_url_encode(payload), signature) + token = "{}.{}.{}".format( + base64_url_encode(header), base64_url_encode(payload), signature + ) jwk_key = self.backend.retrieve_matching_jwk(token) self.assertEqual(jwk_key._jwk_data, get_json_mock.json.return_value["keys"][2]) @@ -1046,7 +1052,9 @@ def test_retrieve_mismatcing_jwk_alg(self, mock_requests): h.update(force_bytes(msg)) signature = base64_url_encode(h.finalize()) - token = "{}.{}.{}".format(base64_url_encode(header), base64_url_encode(payload), signature) + token = "{}.{}.{}".format( + base64_url_encode(header), base64_url_encode(payload), signature + ) with self.assertRaises(SuspiciousOperation) as ctx: self.backend.retrieve_matching_jwk(token) @@ -1081,7 +1089,9 @@ def test_retrieve_mismatcing_jwk_kid(self, mock_requests): h.update(force_bytes(msg)) signature = base64_url_encode(h.finalize()) - token = "{}.{}.{}".format(base64_url_encode(header), base64_url_encode(payload), signature) + token = "{}.{}.{}".format( + base64_url_encode(header), base64_url_encode(payload), signature + ) with self.assertRaises(SuspiciousOperation) as ctx: self.backend.retrieve_matching_jwk(token) @@ -1115,7 +1125,9 @@ def test_retrieve_jwk_optional_alg(self, mock_requests): h.update(force_bytes(msg)) signature = base64_url_encode(h.finalize()) - token = "{}.{}.{}".format(base64_url_encode(header), base64_url_encode(payload), signature) + token = "{}.{}.{}".format( + base64_url_encode(header), base64_url_encode(payload), signature + ) jwk_key = self.backend.retrieve_matching_jwk(token) self.assertEqual(jwk_key._jwk_data, get_json_mock.json.return_value["keys"][0]) @@ -1148,7 +1160,9 @@ def test_retrieve_not_existing_jwk(self, mock_requests): h.update(force_bytes(msg)) signature = base64_url_encode(h.finalize()) - token = "{}.{}.{}".format(base64_url_encode(header), base64_url_encode(payload), signature) + token = "{}.{}.{}".format( + base64_url_encode(header), base64_url_encode(payload), signature + ) with self.assertRaises(SuspiciousOperation) as ctx: self.backend.retrieve_matching_jwk(token) @@ -1245,7 +1259,9 @@ def test_es256_alg_verification(self, mock_requests): "kid": "eckid", } data = {"name": "John Doe", "test": "test_es256_alg_verification"} - token = jwt.encode(payload=data, key=private_key, algorithm="ES256", headers=header) + token = jwt.encode( + payload=data, key=private_key, algorithm="ES256", headers=header + ) # Verify the token created with the private key by using the JWKS endpoint, # where the public numbers are. diff --git a/tox.ini b/tox.ini index a9ede934..ad908be7 100644 --- a/tox.ini +++ b/tox.ini @@ -13,30 +13,26 @@ python = 3.12: py312, coverage, lint [testenv] -commands = django-admin test +commands = python -m django test setenv = DJANGO_SETTINGS_MODULE=tests.settings PYTHONPATH={toxinidir} PYTHONWARNINGS=default deps = - -r{toxinidir}/tests/requirements.txt + .[test] django320: Django>=3.2.0,<4.0 - django320: djangorestframework>=3.14 django420: Django>=4.2,<5.0 - django420: djangorestframework>=3.14 [testenv:coverage] commands = - coverage run --source mozilla_django_oidc {envbindir}/django-admin test + coverage run --source mozilla_django_oidc {envbindir}/python -m django test deps = - coverage - -r{toxinidir}/tests/requirements.txt + .[test] Django>=4.2 - djangorestframework>=3.14 [testenv:lint] deps = - flake8 + .[lint] commands = flake8 {toxinidir}/tests flake8 {toxinidir}/mozilla_django_oidc