diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 000000000..28f228da4 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,31 @@ +name: checks + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + poetry-check: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install Poetry + run: pipx install poetry==2.4.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + - name: Validate pyproject.toml + run: poetry check + - name: Validate poetry.lock is in sync + run: poetry check --lock diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml new file mode 100644 index 000000000..b9918a92e --- /dev/null +++ b/.github/workflows/lints.yml @@ -0,0 +1,37 @@ +name: lints + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install Poetry + run: pipx install poetry==2.4.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + cache: poetry + - name: Install dev dependencies + run: poetry install --with dev + - name: black + run: poetry run black --check src/ tests/ example/ + - name: isort + run: poetry run isort --check-only src/ tests/ example/ + - name: flake8 + continue-on-error: true + run: poetry run flake8 src/ diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml new file mode 100644 index 000000000..727e7ebb8 --- /dev/null +++ b/.github/workflows/release-github.yml @@ -0,0 +1,43 @@ +name: Attach artifacts to GitHub release + +on: + release: + types: [published] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + attach: + name: Build, attest, and attach to release + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write # upload assets to the GitHub release + id-token: write # required by attest-build-provenance + attestations: write # write to GitHub Attestations API + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install Poetry + run: pipx install poetry==2.4.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + cache: poetry + - name: Build sdist and wheel + run: poetry build + - name: Generate build provenance attestation + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: 'dist/*' + - name: Upload distributions to GitHub release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" dist/* diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml new file mode 100644 index 000000000..b5fb387a2 --- /dev/null +++ b/.github/workflows/release-pypi.yml @@ -0,0 +1,74 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install Poetry + run: pipx install poetry==2.4.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + cache: poetry + - name: Build sdist and wheel + run: poetry build + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: python-package-distributions + path: dist/ + + publish-to-testpypi: + name: Publish to TestPyPI + if: github.event.release.prerelease == true + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: testpypi + url: https://test.pypi.org/p/pysaml2 + permissions: + id-token: write # Trusted Publishing OIDC + auto-generated PEP 740 attestations + attestations: write # GitHub Attestations API + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-package-distributions + path: dist/ + - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + repository-url: https://test.pypi.org/legacy/ + + publish-to-pypi: + name: Publish to PyPI + if: github.event.release.prerelease == false + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: pypi + url: https://pypi.org/p/pysaml2 + permissions: + id-token: write + attestations: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-package-distributions + path: dist/ + - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..d25ab214f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,41 @@ +name: tests + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install xmlsec1 + run: | + sudo apt-get update + sudo apt-get install -y xmlsec1 + xmlsec1 --version + - name: Install Poetry + run: pipx install poetry==2.4.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ matrix.python-version }} + cache: poetry + - name: Install dependencies + run: poetry install --with test,coverage + - name: Run tests + run: poetry run pytest --import-mode=importlib --cov=saml2 --cov-report=term-missing diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000..86792febd --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,29 @@ +name: zizmor + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + zizmor: + name: Audit GitHub Actions workflows + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install zizmor + run: pipx install zizmor==1.24.1 + - name: Run zizmor + run: zizmor ./.github + env: + GH_TOKEN: ${{ github.token }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b9aae06fc..000000000 --- a/.travis.yml +++ /dev/null @@ -1,102 +0,0 @@ -os: linux -dist: bionic -language: python - -services: - - mongodb - -before_install: - - sudo apt-get install -y xmlsec1 - -install: - - pip install tox - - pip install tox-travis - -script: - - tox - -jobs: - allow_failures: - - python: 3.10-dev - - python: pypy3 - include: - - python: 3.6 - - python: 3.7 - - python: 3.8 - - python: 3.9 - - python: 3.10-dev - - python: pypy3 - - - stage: Expose env-var information - script: | - cat <VERSION - deploy: - - provider: pypi - distributions: sdist bdist_wheel - server: "https://test.pypi.org/legacy/" - skip_cleanup: true - username: "__token__" - password: "$PYPI_PRE_RELEASE_TOKEN" - on: - repo: IdentityPython/pysaml2 - - - stage: Deploy new release on PyPI - if: type = push AND tag IS present - before_install: skip - install: skip - script: skip - deploy: - - provider: pypi - distributions: sdist bdist_wheel - username: "__token__" - password: "$PYPI_RELEASE_TOKEN" - on: - repo: IdentityPython/pysaml2 - tags: true - - - stage: Deploy new release on GitHub - if: type = push AND tag IS present - before_install: skip - install: skip - script: skip - deploy: - - provider: releases - token: "$GITHUB_RELEASE_TOKEN" - on: - repo: IdentityPython/pysaml2 - tags: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef7fed65..432faa13d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased (2026-XX-XX) + +- ci: Migrate from Travis CI to GitHub Actions +- ci: Add `tests` workflow with a Python 3.9–3.14 matrix +- ci: Add `lint` workflow (black, isort, flake8) and `checks` workflow (`poetry check` + lockfile validation) +- ci: Publish to PyPI/TestPyPI via Trusted Publishing with attestations +- ci: Attach build artifacts and provenance attestations to GitHub releases +- ci: Add `zizmor` workflow to audit GitHub Actions for security issues +- docs: Update RELEASE.md with the `gh release create` flow ## v7.5.4 (2025-10-07) diff --git a/RELEASE.md b/RELEASE.md index d2dd6b93b..e82a69cf4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,22 +1,22 @@ -## Release instructions +# Release instructions When releasing a new version, the following steps should be taken: 1. Make sure the package metadata in `pyproject.toml` is up-to-date. - ``` + ```shell poetry check ``` 2. Make sure all automated tests pass: - ``` + ```shell poetry run pytest ``` 3. Bump the version of the package - ``` + ```shell poetry version -- X.Y.Z ``` @@ -24,34 +24,47 @@ When releasing a new version, the following steps should be taken: 5. Commit and sign the changes: - ``` + ```shell git add -u # CHANGELOG.md pyproject.toml git commit -v -s -m "Release version X.Y.Z" ``` 6. Create a signed release [tag]: - ``` + ```shell git tag -a -s vX.Y.Z -m "Version X.Y.Z" ``` 7. Push the changes and the release to Github: - ``` + ```shell git push --follow-tags ``` -8. Publish the release on PyPI: +8. Publish the release. Creating the GitHub release fires the + `Publish to PyPI` and `Attach artifacts to GitHub release` workflows, + which build the distributions, generate PEP 740 attestations, and + upload to PyPI/TestPyPI via Trusted Publishing. + Install the [GitHub CLI] if you don't already have it (`brew install gh` on macOS). + + Pre-release path (publishes to TestPyPI): + + ```shell + gh release create v7.5.5rc1 --prerelease --title "v7.5.5rc1" --notes "Pre-release for CI" --target ``` - poetry publish --build + + Full release path (publishes to PyPI): + + ```shell + gh release create v7.5.5 --title "v7.5.5" --notes "Release" --target master ``` -9. Send an email to the pysaml2 list announcing this release + Or via the UI: Releases -> Draft a new release -> choose/create tag -> + tick (or untick) "Set as a pre-release" -> Publish release. +9. Send an email to the pysaml2 list announcing this release - [VERSION]: https://github.com/IdentityPython/pysaml2/blob/master/VERSION - [CHANGELOG.md]: https://github.com/IdentityPython/pysaml2/blob/master/CHANGELOG.md - [docutils]: http://docutils.sourceforge.net/ - [branch]: https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell - [tag]: https://git-scm.com/book/en/v2/Git-Basics-Tagging#_annotated_tags +[CHANGELOG.md]: https://github.com/IdentityPython/pysaml2/blob/master/CHANGELOG.md +[GitHub CLI]: https://cli.github.com/ +[tag]: https://git-scm.com/book/en/v2/Git-Basics-Tagging#_annotated_tags diff --git a/example/idp2_repoze/modules/login.mako.py b/example/idp2_repoze/modules/login.mako.py index 211fa37f1..dc6d097ef 100644 --- a/example/idp2_repoze/modules/login.mako.py +++ b/example/idp2_repoze/modules/login.mako.py @@ -1,4 +1,6 @@ -from mako import cache, runtime +from mako import cache +from mako import runtime + UNDEFINED = runtime.UNDEFINED __M_dict_builtin = dict diff --git a/example/idp2_repoze/modules/root.mako.py b/example/idp2_repoze/modules/root.mako.py index 411992323..caf7f302c 100644 --- a/example/idp2_repoze/modules/root.mako.py +++ b/example/idp2_repoze/modules/root.mako.py @@ -1,4 +1,7 @@ -from mako import runtime, filters, cache +from mako import cache +from mako import filters +from mako import runtime + UNDEFINED = runtime.UNDEFINED __M_dict_builtin = dict diff --git a/poetry.lock b/poetry.lock index fdae3665a..8c68e1d9d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "alabaster" @@ -1535,25 +1535,25 @@ tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asy [[package]] name = "setuptools" -version = "80.9.0" +version = "81.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"s2repoze\"" +groups = ["main", "dev"] files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, + {file = "setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6"}, + {file = "setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a"}, ] +markers = {main = "extra == \"s2repoze\""} [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] name = "six" @@ -2073,4 +2073,4 @@ s2repoze = ["paste", "repoze.who", "zope.interface"] [metadata] lock-version = "2.1" python-versions = ">= 3.9" -content-hash = "04b0c0e0efb4781e75bd015e82e23592fb454f9bdeb42507a18c9e5a18d30029" +content-hash = "5861a1db5bbd7f1f578f5527c01566dc9bf549911e5c21ceffcbe501d976c433" diff --git a/pyproject.toml b/pyproject.toml index 87068b18d..72d964640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ types-python-dateutil = "^2.8.19.6" types-setuptools = "^67.2.0.1" types-six = "^1.16.21.4" types-requests = "^2.28.11.12" +setuptools = "81.0.0" [tool.poetry.group.test] optional = true diff --git a/src/saml2/__init__.py b/src/saml2/__init__.py index 01067b4be..ae1381d25 100644 --- a/src/saml2/__init__.py +++ b/src/saml2/__init__.py @@ -2,18 +2,18 @@ """Contains base classes representing SAML elements. - These codes were originally written by Jeffrey Scudder for - representing Saml elements. Takashi Matsuo had added some codes, and - changed some. Roland Hedberg rewrote the whole thing from bottom up so - barely anything but the original structures remained. - - Module objective: provide data classes for SAML constructs. These - classes hide the XML-ness of SAML and provide a set of native Python - classes to interact with. - - Conversions to and from XML should only be necessary when the SAML classes - "touch the wire" and are sent over HTTP. For this reason this module - provides methods and functions to convert SAML classes to and from strings. +These codes were originally written by Jeffrey Scudder for +representing Saml elements. Takashi Matsuo had added some codes, and +changed some. Roland Hedberg rewrote the whole thing from bottom up so +barely anything but the original structures remained. + +Module objective: provide data classes for SAML constructs. These +classes hide the XML-ness of SAML and provide a set of native Python +classes to interact with. + +Conversions to and from XML should only be necessary when the SAML classes +"touch the wire" and are sent over HTTP. For this reason this module +provides methods and functions to convert SAML classes to and from strings. """ import logging diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index 4c0ab1511..329d0b3e7 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -343,11 +343,11 @@ def get(self, attribute, sp_entity_id, default=None): restrictions = ( sp_restrictions if sp_restrictions is not None - else ra_restrictions - if ra_restrictions is not None - else default_restrictions - if default_restrictions is not None - else {} + else ( + ra_restrictions + if ra_restrictions is not None + else default_restrictions if default_restrictions is not None else {} + ) ) attribute_restriction = restrictions.get(attribute) diff --git a/src/saml2/cert.py b/src/saml2/cert.py index e90651e44..3eaa18b7f 100644 --- a/src/saml2/cert.py +++ b/src/saml2/cert.py @@ -1,10 +1,10 @@ __author__ = "haho0032" import base64 -from os import remove -from os.path import join from datetime import datetime from datetime import timezone +from os import remove +from os.path import join from OpenSSL import crypto import dateutil.parser @@ -204,7 +204,6 @@ def create_cert_signed_certificate( sn=1, passphrase=None, ): - """ Will sign a certificate request with a give certificate. :param sign_cert_str: This certificate will be used to sign with. diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index d5e797d76..388515bd4 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -391,17 +391,17 @@ def create_authn_request( None # SAML 2.0 errata says AllowCreate MUST NOT be used for transient ids if nameid_policy_format == NAMEID_FORMAT_TRANSIENT - else allow_create - if allow_create - else str(bool(allow_create_config)).lower() + else allow_create if allow_create else str(bool(allow_create_config)).lower() ) name_id_policy = ( kwargs.pop("name_id_policy", None) if "name_id_policy" in kwargs - else None - if not nameid_policy_format - else samlp.NameIDPolicy(allow_create=allow_create, format=nameid_policy_format) + else ( + None + if not nameid_policy_format + else samlp.NameIDPolicy(allow_create=allow_create, format=nameid_policy_format) + ) ) if name_id_policy and vorg: diff --git a/src/saml2/httputil.py b/src/saml2/httputil.py index c7abc6fdd..aa54d160d 100644 --- a/src/saml2/httputil.py +++ b/src/saml2/httputil.py @@ -183,7 +183,7 @@ def extract(environ, empty=False, err=False): """ input_stream = environ["wsgi.input"] content_length = int(environ.get("CONTENT_LENGTH", 0)) - input_data = input_stream.read(content_length).decode('utf-8') + input_data = input_stream.read(content_length).decode("utf-8") formdata = parse_qs(input_data) # Remove single entries from lists for key, value in iter(formdata.items()): diff --git a/src/saml2/mcache.py b/src/saml2/mcache.py index c464cfc29..dad33b398 100644 --- a/src/saml2/mcache.py +++ b/src/saml2/mcache.py @@ -57,7 +57,7 @@ def get_identity(self, subject_id, entities=None): res = {} oldees = [] - for (entity_id, item) in self._cache.get_multi(entities, f"{subject_id}_").items(): + for entity_id, item in self._cache.get_multi(entities, f"{subject_id}_").items(): try: info = self.get_info(item) except TooOld: diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py index 2ea9742f0..202f8bd8a 100644 --- a/src/saml2/mdstore.py +++ b/src/saml2/mdstore.py @@ -207,7 +207,7 @@ def attribute_requirement(entity_descriptor, index=None): if index is not None and acs["index"] != index: continue - for attr in (acs.get("requested_attribute") or []): + for attr in acs.get("requested_attribute") or []: if attr.get("is_required") == "true": res["required"].append(attr) else: @@ -607,9 +607,7 @@ def parse(self, xmlstr): _md_desc = ( f"metadata file: {self.filename}" if isinstance(self, MetaDataFile) - else f"remote metadata: {self.url}" - if isinstance(self, MetaDataExtern) - else "metadata" + else f"remote metadata: {self.url}" if isinstance(self, MetaDataExtern) else "metadata" ) raise SAMLError(f"Failed to parse {_md_desc}") from e @@ -1327,7 +1325,7 @@ def subject_id_requirement(self, entity_id): "name_format": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "friendly_name": "subject-id", "is_required": "true", - } + }, ] elif subject_id_req == "pairwise-id": return [ diff --git a/src/saml2/metadata.py b/src/saml2/metadata.py index 3961c869f..02154bdca 100644 --- a/src/saml2/metadata.py +++ b/src/saml2/metadata.py @@ -493,7 +493,7 @@ def do_spsso_descriptor(conf, cert=None, enc_cert=None): endps = conf.getattr("endpoints", "sp") if endps: - for (endpoint, instlist) in do_endpoints(endps, ENDPOINTS["sp"]).items(): + for endpoint, instlist in do_endpoints(endps, ENDPOINTS["sp"]).items(): setattr(spsso, endpoint, instlist) ext = do_endpoints(endps, ENDPOINT_EXT["sp"]) @@ -547,7 +547,7 @@ def do_idpsso_descriptor(conf, cert=None, enc_cert=None): endps = conf.getattr("endpoints", "idp") if endps: - for (endpoint, instlist) in do_endpoints(endps, ENDPOINTS["idp"]).items(): + for endpoint, instlist in do_endpoints(endps, ENDPOINTS["idp"]).items(): setattr(idpsso, endpoint, instlist) _do_nameid_format(idpsso, conf, "idp") @@ -608,7 +608,7 @@ def do_aa_descriptor(conf, cert=None, enc_cert=None): endps = conf.getattr("endpoints", "aa") if endps: - for (endpoint, instlist) in do_endpoints(endps, ENDPOINTS["aa"]).items(): + for endpoint, instlist in do_endpoints(endps, ENDPOINTS["aa"]).items(): setattr(aad, endpoint, instlist) _do_nameid_format(aad, conf, "aa") @@ -647,7 +647,7 @@ def do_aq_descriptor(conf, cert=None, enc_cert=None): endps = conf.getattr("endpoints", "aq") if endps: - for (endpoint, instlist) in do_endpoints(endps, ENDPOINTS["aq"]).items(): + for endpoint, instlist in do_endpoints(endps, ENDPOINTS["aq"]).items(): setattr(aqs, endpoint, instlist) _do_nameid_format(aqs, conf, "aq") @@ -678,7 +678,7 @@ def do_pdp_descriptor(conf, cert=None, enc_cert=None): endps = conf.getattr("endpoints", "pdp") if endps: - for (endpoint, instlist) in do_endpoints(endps, ENDPOINTS["pdp"]).items(): + for endpoint, instlist in do_endpoints(endps, ENDPOINTS["pdp"]).items(): setattr(pdp, endpoint, instlist) _do_nameid_format(pdp, conf, "pdp") diff --git a/src/saml2/mongo_store.py b/src/saml2/mongo_store.py index 44175c82f..cb198983f 100644 --- a/src/saml2/mongo_store.py +++ b/src/saml2/mongo_store.py @@ -1,7 +1,7 @@ -import logging -from hashlib import sha1 from datetime import datetime from datetime import timezone +from hashlib import sha1 +import logging from pymongo import MongoClient import pymongo.errors diff --git a/src/saml2/pack.py b/src/saml2/pack.py index cee1cf1c8..143ac7a3b 100644 --- a/src/saml2/pack.py +++ b/src/saml2/pack.py @@ -10,7 +10,6 @@ import base64 import html import logging - from urllib.parse import urlencode from urllib.parse import urlparse from xml.etree import ElementTree as ElementTree diff --git a/src/saml2/saml.py b/src/saml2/saml.py index 1c01dc16c..2e496d4a9 100644 --- a/src/saml2/saml.py +++ b/src/saml2/saml.py @@ -314,11 +314,15 @@ def _wrong_type_value(xsd, value): xsd_ns, xsd_type = ( ["", type(None)] if xsd_string is None - else ["", ""] - if xsd_string == "" - else [XSD if xsd_string in xsd_types_props else "", xsd_string] - if ":" not in xsd_string - else xsd_string.split(":", 1) + else ( + ["", ""] + if xsd_string == "" + else ( + [XSD if xsd_string in xsd_types_props else "", xsd_string] + if ":" not in xsd_string + else xsd_string.split(":", 1) + ) + ) ) xsd_type_props = xsd_types_props.get(xsd_type) diff --git a/src/saml2/server.py b/src/saml2/server.py index ca2b312d7..ee733d45b 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -4,11 +4,11 @@ """Contains classes and functions that a SAML2.0 Identity provider (IdP) or attribute authority (AA) may use to conclude its tasks. """ +from dbm import error as DbmError import importlib import logging import shelve import threading -from dbm import error as DbmError from saml2 import BINDING_HTTP_REDIRECT from saml2 import class_name diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index 738ac04b1..565d7a6ac 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -1,16 +1,16 @@ -""" Functions connected to signing and verifying. +"""Functions connected to signing and verifying. Based on the use of xmlsec1 binaries and not the python xmlsec module. """ import base64 +from datetime import datetime +from datetime import timezone +from importlib.resources import files as _resource_files import hashlib import itertools import logging import os import re -from datetime import datetime -from datetime import timezone -from importlib.resources import files as _resource_files from subprocess import PIPE from subprocess import Popen from tempfile import NamedTemporaryFile @@ -317,7 +317,7 @@ def signed_instance_factory(instance, seccont, elements_to_sign=None): if not isinstance(instance, str): signed_xml = instance.to_string() - for (node_name, nodeid) in elements_to_sign: + for node_name, nodeid in elements_to_sign: signed_xml = seccont.sign_statement(signed_xml, node_name=node_name, node_id=nodeid) return signed_xml @@ -476,9 +476,9 @@ def parse_xmlsec_verify_output(output, version=None): raise XmlsecError(output) else: for line in output.splitlines(): - if line == 'Verification status: OK': + if line == "Verification status: OK": return True - elif line == 'Verification status: FAILED': + elif line == "Verification status: FAILED": raise XmlsecError(output) raise XmlsecError(output) @@ -844,7 +844,7 @@ def _run_xmlsec(self, com_list, extra_args): with NamedTemporaryFile(suffix=".xml") as ntf: com_list.extend(["--output", ntf.name]) if self.version_nums >= (1, 3): - com_list.extend(['--lax-key-search']) + com_list.extend(["--lax-key-search"]) com_list += extra_args logger.debug("xmlsec command: %s", " ".join(com_list)) @@ -883,6 +883,7 @@ def __init__(self): def version(self): try: import xmlsec + return xmlsec.__version__ except (ImportError, AttributeError): return "0.0.0" @@ -1723,7 +1724,7 @@ def multiple_signatures(self, statement, to_sign, key=None, key_file=None, sign_ :param key_file: A file that contains the key to be used :return: A possibly multiple signed statement """ - for (item, sid) in to_sign: + for item, sid in to_sign: if not sid: if not item.id: sid = item.id = sid() diff --git a/src/saml2/time_util.py b/src/saml2/time_util.py index 86ad711d8..1a2475daf 100644 --- a/src/saml2/time_util.py +++ b/src/saml2/time_util.py @@ -6,12 +6,12 @@ """ import calendar +from datetime import datetime +from datetime import timedelta +from datetime import timezone import re import sys import time -from datetime import datetime -from datetime import timezone -from datetime import timedelta TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" diff --git a/src/saml2/tools/parse_xsd2.py b/src/saml2/tools/parse_xsd2.py index 5129dd349..db358c260 100644 --- a/src/saml2/tools/parse_xsd2.py +++ b/src/saml2/tools/parse_xsd2.py @@ -1908,7 +1908,7 @@ def out(self): ignore = [p.name for (p, _l, _s) in tups] done = output(objekt, self.target_namespace, eldict, ignore) if done: - for (prop, lines, _) in tups: + for prop, lines, _ in tups: exceptions.extend(lines) block = [] else: diff --git a/src/saml2/validate.py b/src/saml2/validate.py index 51a41b992..5fd7ea5d9 100644 --- a/src/saml2/validate.py +++ b/src/saml2/validate.py @@ -354,7 +354,7 @@ def valid_instance(instance): except NotValid as exc: raise NotValid(f"Class '{class_name}' instance: {exc.args[0]}") - for (name, typ, required) in instclass.c_attributes.values(): + for name, typ, required in instclass.c_attributes.values(): value = getattr(instance, name, "") if required and not value: txt = f"Required value on property '{name}' missing" @@ -375,7 +375,7 @@ def valid_instance(instance): txt = ERROR_TEXT % (value, name, exc.args[0]) raise NotValid(f"Class '{class_name}' instance: {txt}") - for (name, _spec) in instclass.c_children.values(): + for name, _spec in instclass.c_children.values(): value = getattr(instance, name, "") try: diff --git a/tests/attribute_statement_data.py b/tests/attribute_statement_data.py index e15bcdd20..d98314457 100644 --- a/tests/attribute_statement_data.py +++ b/tests/attribute_statement_data.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -"""Testdata for attribute converters """ +"""Testdata for attribute converters""" STATEMENT1 = """ diff --git a/tests/test_37_entity_categories.py b/tests/test_37_entity_categories.py index 894b03cf3..b1e93f2cb 100644 --- a/tests/test_37_entity_categories.py +++ b/tests/test_37_entity_categories.py @@ -1,8 +1,7 @@ from contextlib import closing -import pytest - from pathutils import full_path +import pytest from saml2 import config from saml2 import sigver diff --git a/tests/test_41_response.py b/tests/test_41_response.py index e6f26400a..90c56dd2e 100644 --- a/tests/test_41_response.py +++ b/tests/test_41_response.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -import logging from contextlib import closing from datetime import datetime from datetime import timezone +import logging from unittest.mock import Mock from unittest.mock import patch @@ -147,7 +147,7 @@ def test_false_sign(self, mock_datetime, caplog): assert isinstance(resp, AuthnResponse) with raises(SignatureError): resp.verify() - assert 'The signature on the assertion cannot be verified.' in caplog.text + assert "The signature on the assertion cannot be verified." in caplog.text def test_other_response(self): with open(full_path("attribute_response.xml")) as fp: diff --git a/tests/test_75_mongodb.py b/tests/test_75_mongodb.py index 51ef67c9f..603745758 100644 --- a/tests/test_75_mongodb.py +++ b/tests/test_75_mongodb.py @@ -48,7 +48,7 @@ def test_flow(): }, userid="jeter", authn=AUTHN, - **rinfo + **rinfo, ) # What's stored away is the assertion diff --git a/tests/test_schema_validator.py b/tests/test_schema_validator.py index ee1335832..49ad0a77a 100644 --- a/tests/test_schema_validator.py +++ b/tests/test_schema_validator.py @@ -1,5 +1,4 @@ from pathutils import full_path as expand_full_path - from pytest import mark from pytest import raises