pax_global_header00006660000000000000000000000064145613474120014521gustar00rootroot0000000000000052 comment=985461c806bc0653a59f562001aef3abb00df7ba mozilla-services-pyramid_multiauth-985461c/000077500000000000000000000000001456134741200210355ustar00rootroot00000000000000mozilla-services-pyramid_multiauth-985461c/.github/000077500000000000000000000000001456134741200223755ustar00rootroot00000000000000mozilla-services-pyramid_multiauth-985461c/.github/CODE_OF_CONDUCT.md000066400000000000000000000012631456134741200251760ustar00rootroot00000000000000# Community Participation Guidelines This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details, please read the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). ## How to Report For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. mozilla-services-pyramid_multiauth-985461c/.github/CONTRIBUTING.md000066400000000000000000000026031456134741200246270ustar00rootroot00000000000000How to contribute ================= Thanks for your interest in contributing! ## Reporting Bugs Report bugs at https://github.com/mozilla-services/pyramid_multiauth/issues/new If you are reporting a bug, please include: - Any details about your local setup that might be helpful in troubleshooting. - Detailed steps to reproduce the bug or even a PR with a failing tests if you can. ## Ready to contribute? ### Getting Started - Fork the repo on GitHub and clone locally: ```bash git clone git@github.com:mozilla-services/pyramid_multiauth.git git remote add {your_name} git@github.com:{your_name}/pyramid_multiauth.git ``` ## Testing - `make test` to run all the tests ## Submitting Changes ```bash git checkout main git pull origin main git checkout -b issue_number-bug-title git commit # Your changes git push -u {your_name} issue_number-bug-title ``` Then you can create a Pull-Request. Please create your pull-request as soon as you have at least one commit even if it has only failing tests. This will allow us to help and give guidance. You will be able to update your pull-request by pushing commits to your branch. ## Releasing 1. Create a release on Github on https://github.com/mozilla-services/pyramid_multiauth/releases/new 2. Create a new tag `X.Y.Z` (*This tag will be created from the target when you publish this release.*) 3. Generate release notes 4. Publish release mozilla-services-pyramid_multiauth-985461c/.github/dependabot.yml000066400000000000000000000006251456134741200252300ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: weekly open-pull-requests-limit: 99 groups: all-dependencies: update-types: ["major", "minor", "patch"] - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly open-pull-requests-limit: 99 groups: all-dependencies: update-types: ["major", "minor", "patch"] mozilla-services-pyramid_multiauth-985461c/.github/release.yml000066400000000000000000000007021456134741200245370ustar00rootroot00000000000000changelog: exclude: authors: - dependabot categories: - title: Breaking Changes labels: - "breaking-change" - title: Bug Fixes labels: - "bug" - title: New Features labels: - "enhancement" - title: Documentation labels: - "documentation" - title: Dependency Updates labels: - "dependencies" - title: Other Changes labels: - "*" mozilla-services-pyramid_multiauth-985461c/.github/workflows/000077500000000000000000000000001456134741200244325ustar00rootroot00000000000000mozilla-services-pyramid_multiauth-985461c/.github/workflows/labels.yaml000066400000000000000000000005461456134741200265650ustar00rootroot00000000000000name: Force pull-requests label(s) on: pull_request: types: [opened, labeled, unlabeled] jobs: pr-has-label: name: Will be skipped if labelled runs-on: ubuntu-latest if: ${{ join(github.event.pull_request.labels.*.name, ', ') == '' }} steps: - run: | echo 'Pull-request must have at least one label' exit 1 mozilla-services-pyramid_multiauth-985461c/.github/workflows/publish.yml000066400000000000000000000024721456134741200266300ustar00rootroot00000000000000name: Publish Python 🐍 distribution 📦 to PyPI on: push: tags: - '*' jobs: build: name: Build distribution 📦 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.x" - name: Print environment run: | python --version - name: Install pypa/build run: python3 -m pip install build - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ publish-to-pypi: name: Publish Python 🐍 distribution 📦 to PyPI if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes needs: - build runs-on: ubuntu-latest environment: name: release url: https://pypi.org/p/pyramid_multiauth permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@release/v1 mozilla-services-pyramid_multiauth-985461c/.github/workflows/test.yml000066400000000000000000000015261456134741200261400ustar00rootroot00000000000000on: pull_request name: Tests jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - name: Run linting and formatting checks run: make lint unit-tests: name: Unit Tests needs: lint runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - name: Run unit tests run: make test - name: Coveralls env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | pip install tomli coveralls coveralls --service=github mozilla-services-pyramid_multiauth-985461c/.gitignore000066400000000000000000000001101456134741200230150ustar00rootroot00000000000000.hg* *.pyc *.egg-info *.swp \.coverage *~ dist build htmlcov .tox .venv mozilla-services-pyramid_multiauth-985461c/CHANGES.txt000066400000000000000000000046571456134741200226620ustar00rootroot00000000000000> 1.0.2 ======= Since version 1.0.2, we use `Github releases `_ and autogenerated changelogs. 1.0.1 (2021-10-28) ================== **Bug Fixes** - Fix the `ConfigurationError` about authentication and authorization conflicting with the default security when loading various policies via their module name. **Internal Changes** - Migrate CI from CircleCI to Github Actions - Tox: add py3.7 and py3.9 support - Remove code for Pyramid < 1.3 - Use ``assertEqual()`` in tests - Drop support of Python 2.7 1.0.0 (2021-10-21) ================== **Breaking Changes** - Drop support for Pyramid 1.X (#27) 0.9.0 (2016-11-07) ================== - Drop support for python 2.6 0.8.0 (2016-02-11) ================== - Provide ``userid`` attribute in ``MultiAuthPolicySelected`` event. - Always notify event when user is identified with authenticated_userid() (i.e. through ``effective_principals()`` with group finder callback). 0.7.0 (2016-02-09) ================== - Add ``get_policies()`` method to retrieve the list of contained authentication policies and their respective names. 0.6.0 (2016-01-27) ================== - Provide the policy name used in settings in the ``MultiAuthPolicySelected`` event. 0.5.0 - 2015-05-19 ================== - Read authorization policy from settings if present. 0.4.0 - 2014-01-02 ================== - Make authenticated_userid None when groupfinder returns None. 0.3.2 - 2013-05-29 ================== - Fix some merge bustage; this should contain all the things that were *claimed* to be contained in the 0.3.1 release, but in fact were not. 0.3.1 - 2013-05-15 ================== - MultiAuthPolicySelected events now include the request object, so you can e.g. access the registry from the handler function. - Fixed some edge-cases in merging effective_principals with the output of the groupfinder callback. 0.3.0 - 2012-11-27 ================== - Support for Python3 via source-level compatibility. - Fire a MultiAuthPolicySelected event when a policy is successfully used for authentication. 0.2.0 - 2012-10-04 ================== - Add get_policy() method, which can be used to look up the loaded sub-policies at runtime. 0.1.2 - 2012-01-30 ================== - Update license to MPL 2.0. 0.1.1 - 2011-12-20 ================== - Compatability with Pyramid 1.3. 0.1.0 - 2011-11-11 ================== - Initial release. mozilla-services-pyramid_multiauth-985461c/CONTRIBUTORS.txt000066400000000000000000000003221456134741200235300ustar00rootroot00000000000000List of contributors: * Ryan Kelly * John Anderson * Laurence Rowe * Mathieu Leplatre * Rémy Hubscher mozilla-services-pyramid_multiauth-985461c/LICENSE000066400000000000000000000405261456134741200220510ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. mozilla-services-pyramid_multiauth-985461c/Makefile000066400000000000000000000017341456134741200225020ustar00rootroot00000000000000VENV := $(shell echo $${VIRTUAL_ENV-.venv}) PYTHON = $(VENV)/bin/python INSTALL_STAMP = $(VENV)/.install.stamp .PHONY: all all: install install: $(INSTALL_STAMP) $(INSTALL_STAMP): $(PYTHON) pyproject.toml requirements.txt $(VENV)/bin/pip install -U pip $(VENV)/bin/pip install -r requirements.txt $(VENV)/bin/pip install -e ".[dev]" touch $(INSTALL_STAMP) $(PYTHON): python3 -m venv $(VENV) requirements.txt: requirements.in pip-compile .PHONY: test test: install $(VENV)/bin/pytest --cov-report term-missing --cov-fail-under 95 --cov pyramid_multiauth .PHONY: lint lint: install $(VENV)/bin/ruff check src tests $(VENV)/bin/ruff format --check src tests .PHONY: format format: install $(VENV)/bin/ruff check --fix src tests $(VENV)/bin/ruff format src tests .IGNORE: clean clean: find src -name '__pycache__' -type d -exec rm -fr {} \; find tests -name '__pycache__' -type d -exec rm -fr {} \; rm -rf .venv .coverage *.egg-info .pytest_cache .ruff_cache build dist mozilla-services-pyramid_multiauth-985461c/README.rst000066400000000000000000000102371456134741200225270ustar00rootroot00000000000000================= pyramid_multiauth ================= |pypi| |ci| |coverage| .. |pypi| image:: https://img.shields.io/pypi/v/pyramid_multiauth.svg :target: https://pypi.python.org/pypi/pyramid_multiauth .. |ci| image:: https://github.com/mozilla-services/pyramid_multiauth/actions/workflows/test.yml/badge.svg :target: https://github.com/mozilla-services/pyramid_multiauth/actions .. |coverage| image:: https://coveralls.io/repos/github/mozilla-services/pyramid_multiauth/badge.svg?branch=main :target: https://coveralls.io/github/mozilla-services/pyramid_multiauth?branch=main An authentication policy for Pyramid that proxies to a stack of other authentication policies. Overview ======== MultiAuthenticationPolicy is a Pyramid authentication policy that proxies to a stack of *other* IAuthenticationPolicy objects, to provide a combined auth solution from individual pieces. Simply pass it a list of policies that should be tried in order:: policies = [ IPAuthenticationPolicy("127.0.*.*", principals=["local"]) IPAuthenticationPolicy("192.168.*.*", principals=["trusted"]) ] authn_policy = MultiAuthenticationPolicy(policies) config.set_authentication_policy(authn_policy) This example uses the pyramid_ipauth module to assign effective principals based on originating IP address of the request. It combines two such policies so that requests originating from "127.0.*.*" will have principal "local" while requests originating from "192.168.*.*" will have principal "trusted". In general, the results from the stacked authentication policies are combined as follows: * authenticated_userid: return userid from first successful policy * unauthenticated_userid: return userid from first successful policy * effective_principals: return union of principals from all policies * remember: return headers from all policies * forget: return headers from all policies Deployment Settings =================== It is also possible to specify the authentication policies as part of your paste deployment settings. Consider the following example:: [app:pyramidapp] use = egg:mypyramidapp multiauth.policies = ipauth1 ipauth2 pyramid_browserid multiauth.policy.ipauth1.use = pyramid_ipauth.IPAuthentictionPolicy multiauth.policy.ipauth1.ipaddrs = 127.0.*.* multiauth.policy.ipauth1.principals = local multiauth.policy.ipauth2.use = pyramid_ipauth.IPAuthentictionPolicy multiauth.policy.ipauth2.ipaddrs = 192.168.*.* multiauth.policy.ipauth2.principals = trusted To configure authentication from these settings, simply include the multiauth module into your configurator:: config.include("pyramid_multiauth") In this example you would get a MultiAuthenticationPolicy with three stacked auth policies. The first two, ipauth1 and ipauth2, are defined as the name of of a callable along with a set of keyword arguments. The third is defined as the name of a module, pyramid_browserid, which will be processed via the standard config.include() mechanism. The end result would be a system that authenticates users via BrowserID, and assigns additional principal identifiers based on the originating IP address of the request. If necessary, the *group finder function* and the *authorization policy* can also be specified from configuration:: [app:pyramidapp] use = egg:mypyramidapp multiauth.authorization_policy = mypyramidapp.acl.Custom multiauth.groupfinder = mypyramidapp.acl.groupfinder ... MultiAuthPolicySelected Event ============================= An event is triggered when one of the multiple policies configured is selected. :: from pyramid_multiauth import MultiAuthPolicySelected # Track policy used, for prefixing user_id and for logging. def on_policy_selected(event): print("%s (%s) authenticated %s for request %s" % (event.policy_name, event.policy, event.userid, event.request)) config.add_subscriber(on_policy_selected, MultiAuthPolicySelected) mozilla-services-pyramid_multiauth-985461c/pyproject.toml000066400000000000000000000032111456134741200237460ustar00rootroot00000000000000[project] dynamic = ["version", "dependencies", "readme"] name = "pyramid_multiauth" description = "An authentication policy for Pyramid that proxies to a stack of other authentication policies" license = {file = "LICENSE"} classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Framework :: Pylons", "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", ] keywords = ["web pyramid pylons authentication"] authors = [ {name = "Mozilla Services", email = "services-dev@mozilla.org"}, ] [project.urls] Repository = "https://github.com/mozilla-services/pyramid_multiauth" [tool.setuptools_scm] # can be empty if no extra settings are needed, presence enables setuptools_scm [tool.setuptools.dynamic] dependencies = { file = ["requirements.in"] } readme = {file = ["README.rst", "CONTRIBUTORS.rst"]} [build-system] requires = ["setuptools>=64", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project.optional-dependencies] dev = [ "ruff", "pytest", "pytest-cache", "pytest-cov", ] [tool.pip-tools] generate-hashes = true [tool.coverage.run] relative_files = true [tool.ruff] line-length = 99 extend-exclude = [ "__pycache__", ".venv/", ] [tool.ruff.lint] select = [ # pycodestyle "E", "W", # flake8 "F", # isort "I", ] ignore = [ # `format` will wrap lines. "E501", ] [tool.ruff.lint.isort] lines-after-imports = 2 mozilla-services-pyramid_multiauth-985461c/requirements.in000066400000000000000000000000161456134741200241050ustar00rootroot00000000000000pyramid>=2,<3 mozilla-services-pyramid_multiauth-985461c/requirements.txt000066400000000000000000000124271456134741200243270ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # pip-compile --generate-hashes # hupper==1.12.1 \ --hash=sha256:06bf54170ff4ecf4c84ad5f188dee3901173ab449c2608ad05b9bfd6b13e32eb \ --hash=sha256:e872b959f09d90be5fb615bd2e62de89a0b57efc037bdf9637fb09cdf8552b19 # via pyramid pastedeploy==3.1.0 \ --hash=sha256:76388ad53a661448d436df28c798063108f70e994ddc749540d733cdbd1b38cf \ --hash=sha256:9ddbaf152f8095438a9fe81f82c78a6714b92ae8e066bed418b6a7ff6a095a95 # via plaster-pastedeploy plaster==1.1.2 \ --hash=sha256:42992ab1f4865f1278e2ad740e8ad145683bb4022e03534265528f0c23c0df2d \ --hash=sha256:f8befc54bf8c1147c10ab40297ec84c2676fa2d4ea5d6f524d9436a80074ef98 # via # plaster-pastedeploy # pyramid plaster-pastedeploy==1.0.1 \ --hash=sha256:ad3550cc744648969ed3b810f33c9344f515ee8d8a8cec18e8f2c4a643c2181f \ --hash=sha256:be262e6d2e41a7264875daa2fe2850cbb0615728bcdc92828fdc72736e381412 # via pyramid pyramid==2.0.2 \ --hash=sha256:2e6585ac55c147f0a51bc00dadf72075b3bdd9a871b332ff9e5e04117ccd76fa \ --hash=sha256:372138a738e4216535cc76dcce6eddd5a1aaca95130f2354fb834264c06f18de # via -r requirements.in translationstring==1.4 \ --hash=sha256:5f4dc4d939573db851c8d840551e1a0fb27b946afe3b95aafc22577eed2d6262 \ --hash=sha256:bf947538d76e69ba12ab17283b10355a9ecfbc078e6123443f43f2107f6376f3 # via pyramid venusian==3.1.0 \ --hash=sha256:d1fb1e49927f42573f6c9b7c4fcf61c892af8fdcaa2314daa01d9a560b23488d \ --hash=sha256:eb72cdca6f3139a15dc80f9c95d3c10f8a54a0ba881eeef8e2ec5b42d3ee3a95 # via pyramid webob==1.8.7 \ --hash=sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b \ --hash=sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323 # via pyramid zope-deprecation==5.0 \ --hash=sha256:28c2ee983812efb4676d33c7a8c6ade0df191c1c6d652bbbfe6e2eeee067b2d4 \ --hash=sha256:b7c32d3392036b2145c40b3103e7322db68662ab09b7267afe1532a9d93f640f # via pyramid zope-interface==6.1 \ --hash=sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff \ --hash=sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c \ --hash=sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac \ --hash=sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f \ --hash=sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d \ --hash=sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309 \ --hash=sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736 \ --hash=sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179 \ --hash=sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb \ --hash=sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941 \ --hash=sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d \ --hash=sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92 \ --hash=sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b \ --hash=sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41 \ --hash=sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f \ --hash=sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3 \ --hash=sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d \ --hash=sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8 \ --hash=sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3 \ --hash=sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1 \ --hash=sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1 \ --hash=sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40 \ --hash=sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d \ --hash=sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1 \ --hash=sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605 \ --hash=sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7 \ --hash=sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd \ --hash=sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43 \ --hash=sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0 \ --hash=sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b \ --hash=sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379 \ --hash=sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a \ --hash=sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83 \ --hash=sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56 \ --hash=sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9 \ --hash=sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de # via pyramid # WARNING: The following packages were not pinned, but pip requires them to be # pinned when the requirements file includes hashes and the requirement is not # satisfied by a package already installed. Consider using the --allow-unsafe flag. # setuptools mozilla-services-pyramid_multiauth-985461c/src/000077500000000000000000000000001456134741200216245ustar00rootroot00000000000000mozilla-services-pyramid_multiauth-985461c/src/pyramid_multiauth/000077500000000000000000000000001456134741200253655ustar00rootroot00000000000000mozilla-services-pyramid_multiauth-985461c/src/pyramid_multiauth/__init__.py000066400000000000000000000363521456134741200275070ustar00rootroot00000000000000# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. """ Pyramid authn policy that ties together multiple backends. """ import sys from pyramid.authorization import Authenticated, Everyone from pyramid.interfaces import PHASE2_CONFIG, IAuthenticationPolicy, ISecurityPolicy from pyramid.security import LegacySecurityPolicy from zope.interface import implementer __ver_major__ = 0 __ver_minor__ = 9 __ver_patch__ = 0 __ver_sub__ = "" __ver_tuple__ = (__ver_major__, __ver_minor__, __ver_patch__, __ver_sub__) __version__ = "%d.%d.%d%s" % __ver_tuple__ if sys.version_info > (3,): # pragma: nocover basestring = str class MultiAuthPolicySelected(object): """Event for tracking which authentication policy was used. This event is fired whenever a particular backend policy is successfully used for authentication. It can be used by other parts of the code in order to act based on the selected policy:: from pyramid.events import subscriber @subscriber(MultiAuthPolicySelected) def track_policy(event): print("We selected policy %s" % event.policy) """ def __init__(self, policy, request, userid=None): self.policy = policy self.policy_name = getattr(policy, "_pyramid_multiauth_name", None) self.request = request self.userid = userid @implementer(IAuthenticationPolicy) class MultiAuthenticationPolicy(object): """Pyramid authentication policy for stacked authentication. This is a pyramid authentication policy that stitches together other authentication policies into a flexible auth stack. You give it a list of IAuthenticationPolicy objects, and it will try each one in turn until it obtains a usable response: * authenticated_userid: return userid from first successful policy * unauthenticated_userid: return userid from first successful policy * effective_principals: return union of principals from all policies * remember: return headers from all policies * forget: return headers from all policies """ def __init__(self, policies, callback=None): self._policies = policies self._callback = callback def authenticated_userid(self, request): """Find the authenticated userid for this request. This method delegates to each authn policy in turn, taking the userid from the first one that doesn't return None. If a groupfinder callback is configured, it is also used to validate the userid before returning. """ userid = None for policy in self._policies: userid = policy.authenticated_userid(request) if userid is not None: request.registry.notify(MultiAuthPolicySelected(policy, request, userid)) if self._callback is None: break if self._callback(userid, request) is not None: break else: userid = None return userid def unauthenticated_userid(self, request): """Find the unauthenticated userid for this request. This method delegates to each authn policy in turn, taking the userid from the first one that doesn't return None. """ userid = None for policy in self._policies: userid = policy.unauthenticated_userid(request) if userid is not None: break return userid def effective_principals(self, request): """Get the list of effective principals for this request. This method returns the union of the principals returned by each authn policy. If a groupfinder callback is registered, its output is also added to the list. """ principals = set((Everyone,)) for policy in self._policies: principals.update(policy.effective_principals(request)) if self._callback is not None: principals.discard(Authenticated) groups = None for policy in self._policies: userid = policy.authenticated_userid(request) if userid is None: continue request.registry.notify(MultiAuthPolicySelected(policy, request, userid)) groups = self._callback(userid, request) if groups is not None: break if groups is not None: principals.add(userid) principals.add(Authenticated) principals.update(groups) return list(principals) def remember(self, request, principal, **kw): """Remember the authenticated userid. This method returns the concatenation of the headers returned by each authn policy. """ headers = [] for policy in self._policies: headers.extend(policy.remember(request, principal, **kw)) return headers def forget(self, request): """Forget a previously remembered userid. This method returns the concatenation of the headers returned by each authn policy. """ headers = [] for policy in self._policies: headers.extend(policy.forget(request)) return headers def get_policies(self): """Get the list of contained authentication policies, as tuple of name and instances. This may be useful to introspect the configured policies, and their respective name defined in configuration. """ return [ (getattr(policy, "_pyramid_multiauth_name", None), policy) for policy in self._policies ] def get_policy(self, name_or_class): """Get one of the contained authentication policies, by name or class. This method can be used to obtain one of the subpolicies loaded by this policy object. The policy can be looked up either by the name given to it in the config settings, or or by its class. If no policy is found matching the given query, None is returned. This may be useful if you need to access non-standard methods or properties on one of the loaded policy objects. """ for policy in self._policies: if isinstance(name_or_class, basestring): policy_name = getattr(policy, "_pyramid_multiauth_name", None) if policy_name == name_or_class: return policy else: if isinstance(policy, name_or_class): return policy return None def includeme(config): """Include pyramid_multiauth into a pyramid configurator. This function provides a hook for pyramid to include the default settings for auth via pyramid_multiauth. Activate it like so: config.include("pyramid_multiauth") This will pull the list of registered authn policies from the deployment settings, and configure and install each policy in order. The policies to use can be specified in one of two ways: * as the name of a module to be included. * as the name of a callable along with a set of parameters. Here's an example suite of settings: multiauth.policies = ipauth1 ipauth2 pyramid_browserid multiauth.policy.ipauth1.use = pyramid_ipauth.IPAuthentictionPolicy multiauth.policy.ipauth1.ipaddrs = 123.123.0.0/16 multiauth.policy.ipauth1.userid = local1 multiauth.policy.ipauth2.use = pyramid_ipauth.IPAuthentictionPolicy multiauth.policy.ipauth2.ipaddrs = 124.124.0.0/16 multiauth.policy.ipauth2.userid = local2 This will configure a MultiAuthenticationPolicy with three policy objects. The first two will be IPAuthenticationPolicy objects created by passing in the specified keyword arguments. The third will be a BrowserID authentication policy just like you would get from executing: config.include("pyramid_browserid") As a side-effect, the configuration will also get the additional views that pyramid_browserid sets up by default. The *group finder function* and the *authorization policy* are also read from configuration if specified: multiauth.authorization_policy = mypyramidapp.acl.Custom multiauth.groupfinder = mypyramidapp.acl.groupfinder """ # Grab the pyramid-wide settings, to look for any auth config. settings = config.get_settings() # Hook up a default AuthorizationPolicy. # Get the authorization policy from config if present. # Default ACLAuthorizationPolicy is usually what you want. authz_class = settings.get( "multiauth.authorization_policy", "pyramid.authorization.ACLAuthorizationPolicy" ) authz_policy = config.maybe_dotted(authz_class)() # If the app configures one explicitly then this will get overridden. # In autocommit mode this needs to be done before setting the authn policy. config.set_authorization_policy(authz_policy) # Get the groupfinder from config if present. groupfinder = settings.get("multiauth.groupfinder", None) groupfinder = config.maybe_dotted(groupfinder) # Look for callable policy definitions. # Suck them all out at once and store them in a dict for later use. policy_definitions = get_policy_definitions(settings) # Read and process the list of policies to load. # We build up a list of callables which can be executed at config commit # time to obtain the final list of policies. # Yeah, it's complicated. But we want to be able to inherit any default # views or other config added by the sub-policies when they're included. # Process policies in reverse order so that things at the front of the # list can override things at the back of the list. policy_factories = [] policy_names = settings.get("multiauth.policies", "").split() for policy_name in reversed(policy_names): if policy_name in policy_definitions: # It's a policy defined using a callable. # Just append it straight to the list. definition = policy_definitions[policy_name] factory = config.maybe_dotted(definition.pop("use")) policy_factories.append((factory, policy_name, definition)) else: # It's a module to be directly included. try: factory = policy_factory_from_module(config, policy_name) except ImportError: err = "pyramid_multiauth: policy %r has no settings " "and is not importable" % ( policy_name, ) raise ValueError(err) policy_factories.append((factory, policy_name, {})) # OK. We now have a list of callbacks which need to be called at # commit time, and will return the policies in reverse order. # Register a special action to pull them into our list of policies. policies = [] def grab_policies(): for factory, name, kwds in policy_factories: policy = factory(**kwds) if policy: policy._pyramid_multiauth_name = name if not policies or policy is not policies[0]: # Remember, they're being processed in reverse order. # So each new policy needs to go at the front. policies.insert(0, policy) config.action(None, grab_policies, order=PHASE2_CONFIG) authn_policy = MultiAuthenticationPolicy(policies, groupfinder) config.set_authentication_policy(authn_policy) def policy_factory_from_module(config, module): """Create a policy factory that works by config.include()'ing a module. This function does some trickery with the Pyramid config system. Loosely, it does config.include(module), and then sucks out information about the authn policy that was registered. It's complicated by pyramid's delayed- commit system, which means we have to do the work via callbacks. """ # Remember the policy that's active before including the module, if any. orig_policy = config.registry.queryUtility(IAuthenticationPolicy) # Include the module, so we get any default views etc. config.include(module) # That might have registered and commited a new policy object. policy = config.registry.queryUtility(IAuthenticationPolicy) if policy is not None and policy is not orig_policy: return lambda: policy # Or it might have set up a pending action to register one later. # Find the most recent IAuthenticationPolicy action, and grab # out the registering function so we can call it ourselves. for action in reversed(config.action_state.actions): # Extract the discriminator and callable. discriminator = action["discriminator"] callable = action["callable"] # If it's not setting the authn policy, keep looking. if discriminator is not IAuthenticationPolicy: continue # Otherwise, wrap it up so we can extract the registered object. def grab_policy(register=callable): # In Pyramid 2.0, a default security policy is registered when # none is found: # https://github.com/Pylons/pyramid/blob/8061fce/src/pyramid/config/security.py#L100-L101 # When including various policies, this can result in # `ConfigurationError`s since we're not supposed to set # authentication once a security policy is already in place. # Clean-up this side-effect manually here. security = config.registry.queryUtility(ISecurityPolicy) if isinstance(security, LegacySecurityPolicy): config.registry.registerUtility(None, ISecurityPolicy) old_policy = config.registry.queryUtility(IAuthenticationPolicy) register() new_policy = config.registry.queryUtility(IAuthenticationPolicy) config.registry.registerUtility(old_policy, IAuthenticationPolicy) # Clean-up the side-effect of the default security policy # here too, after executing the actions via `register()`. security = config.registry.queryUtility(ISecurityPolicy) if isinstance(security, LegacySecurityPolicy): config.registry.registerUtility(None, ISecurityPolicy) return new_policy return grab_policy # Or it might not have done *anything*. # So return a null policy factory. return lambda: None def get_policy_definitions(settings): """Find all multiauth policy definitions from the settings dict. This function processes the paster deployment settings looking for items that start with "multiauth.policy..". It pulls them all out into a dict indexed by the policy name. """ policy_definitions = {} for name in settings: if not name.startswith("multiauth.policy."): continue value = settings[name] name = name[len("multiauth.policy.") :] policy_name, setting_name = name.split(".", 1) if policy_name not in policy_definitions: policy_definitions[policy_name] = {} policy_definitions[policy_name][setting_name] = value return policy_definitions mozilla-services-pyramid_multiauth-985461c/tests/000077500000000000000000000000001456134741200221775ustar00rootroot00000000000000mozilla-services-pyramid_multiauth-985461c/tests/__init__.py000066400000000000000000000000001456134741200242760ustar00rootroot00000000000000mozilla-services-pyramid_multiauth-985461c/tests/test_base.py000066400000000000000000000437401456134741200245320ustar00rootroot00000000000000# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import unittest import pyramid.testing from pyramid.authorization import ACLAuthorizationPolicy, Authenticated, Everyone from pyramid.exceptions import Forbidden from pyramid.interfaces import IAuthenticationPolicy, IAuthorizationPolicy, ISecurityPolicy from pyramid.security import LegacySecurityPolicy from pyramid.testing import DummyRequest from pyramid_multiauth import MultiAuthenticationPolicy from zope.interface import implementer # Here begins various helper classes and functions for the tests. @implementer(IAuthenticationPolicy) class BaseAuthnPolicy(object): """A do-nothing base class for authn policies.""" def __init__(self, **kwds): self.__dict__.update(kwds) def authenticated_userid(self, request): return self.unauthenticated_userid(request) def unauthenticated_userid(self, request): return None def effective_principals(self, request): principals = [Everyone] userid = self.authenticated_userid(request) if userid is not None: principals.append(Authenticated) principals.append(userid) return principals def remember(self, request, principal): return [] def forget(self, request): return [] @implementer(IAuthenticationPolicy) class TestAuthnPolicy1(BaseAuthnPolicy): """An authn policy that adds "test1" to the principals.""" def effective_principals(self, request): return [Everyone, "test1"] def remember(self, request, principal): return [("X-Remember", principal)] def forget(self, request): return [("X-Forget", "foo")] @implementer(IAuthenticationPolicy) class TestAuthnPolicy2(BaseAuthnPolicy): """An authn policy that sets "test2" as the username.""" def unauthenticated_userid(self, request): return "test2" def remember(self, request, principal): return [("X-Remember-2", principal)] def forget(self, request): return [("X-Forget", "bar")] @implementer(IAuthenticationPolicy) class TestAuthnPolicy3(BaseAuthnPolicy): """Authn policy that sets "test3" as the username "test4" in principals.""" def unauthenticated_userid(self, request): return "test3" def effective_principals(self, request): return [Everyone, Authenticated, "test3", "test4"] @implementer(IAuthenticationPolicy) class TestAuthnPolicyUnauthOnly(BaseAuthnPolicy): """An authn policy that returns an unauthenticated userid but not an authenticated userid, similar to the basic auth policy. """ def authenticated_userid(self, request): return None def unauthenticated_userid(self, request): return "test3" def effective_principals(self, request): return [Everyone] @implementer(IAuthorizationPolicy) class TestAuthzPolicyCustom(object): def permits(self, context, principals, permission): return True def principals_allowed_by_permission(self, context, permission): raise NotImplementedError() # pragma: nocover def includeme1(config): """Config include that sets up a TestAuthnPolicy1 and a forbidden view.""" config.set_authentication_policy(TestAuthnPolicy1()) def forbidden_view(request): return "FORBIDDEN ONE" config.add_view(forbidden_view, renderer="json", context="pyramid.exceptions.Forbidden") def includeme2(config): """Config include that sets up a TestAuthnPolicy2.""" config.set_authentication_policy(TestAuthnPolicy2()) def includemenull(config): """Config include that doesn't do anything.""" pass def includeme3(config): """Config include that adds a TestAuthPolicy3 and commits it.""" config.set_authentication_policy(TestAuthnPolicy3()) config.commit() def raiseforbidden(request): """View that always just raises Forbidden.""" raise Forbidden() def customgroupfinder(userid, request): """A test groupfinder that only recognizes user "test3".""" if userid != "test3": return None return ["group"] # Here begins the actual test cases class MultiAuthPolicyTests(unittest.TestCase): """Testcases for MultiAuthenticationPolicy and related hooks.""" def setUp(self): self.config = pyramid.testing.setUp(autocommit=False) def tearDown(self): pyramid.testing.tearDown() def test_basic_stacking(self): policies = [TestAuthnPolicy1(), TestAuthnPolicy2()] policy = MultiAuthenticationPolicy(policies) request = DummyRequest() self.assertEqual(policy.authenticated_userid(request), "test2") self.assertEqual( sorted(policy.effective_principals(request)), [Authenticated, Everyone, "test1", "test2"], ) def test_policy_selected_event(self): from pyramid.testing import testConfig from pyramid_multiauth import MultiAuthPolicySelected policies = [TestAuthnPolicy2(), TestAuthnPolicy3()] policy = MultiAuthenticationPolicy(policies) # Simulate loading from config: policies[0]._pyramid_multiauth_name = "name" with testConfig() as config: request = DummyRequest() selected_policy = [] def track_policy(event): selected_policy.append(event) config.add_subscriber(track_policy, MultiAuthPolicySelected) self.assertEqual(policy.authenticated_userid(request), "test2") self.assertEqual(selected_policy[0].policy, policies[0]) self.assertEqual(selected_policy[0].policy_name, "name") self.assertEqual(selected_policy[0].userid, "test2") self.assertEqual(selected_policy[0].request, request) self.assertEqual(len(selected_policy), 1) # Effective principals also triggers an event when groupfinder # is provided. policy_with_group = MultiAuthenticationPolicy(policies, lambda u, r: ["foo"]) policy_with_group.effective_principals(request) self.assertEqual(len(selected_policy), 2) def test_stacking_of_unauthenticated_userid(self): policies = [TestAuthnPolicy2(), TestAuthnPolicy3()] policy = MultiAuthenticationPolicy(policies) request = DummyRequest() self.assertEqual(policy.unauthenticated_userid(request), "test2") policies.reverse() self.assertEqual(policy.unauthenticated_userid(request), "test3") def test_stacking_of_authenticated_userid(self): policies = [TestAuthnPolicy2(), TestAuthnPolicy3()] policy = MultiAuthenticationPolicy(policies) request = DummyRequest() self.assertEqual(policy.authenticated_userid(request), "test2") policies.reverse() self.assertEqual(policy.authenticated_userid(request), "test3") def test_stacking_of_authenticated_userid_with_groupdfinder(self): policies = [TestAuthnPolicy2(), TestAuthnPolicy3()] policy = MultiAuthenticationPolicy(policies, customgroupfinder) request = DummyRequest() self.assertEqual(policy.authenticated_userid(request), "test3") policies.reverse() self.assertEqual(policy.unauthenticated_userid(request), "test3") def test_only_unauthenticated_userid_with_groupfinder(self): policies = [TestAuthnPolicyUnauthOnly()] policy = MultiAuthenticationPolicy(policies, customgroupfinder) request = DummyRequest() self.assertEqual(policy.unauthenticated_userid(request), "test3") self.assertEqual(policy.authenticated_userid(request), None) self.assertEqual(policy.effective_principals(request), [Everyone]) def test_authenticated_userid_unauthenticated_with_groupfinder(self): policies = [TestAuthnPolicy2()] policy = MultiAuthenticationPolicy(policies, customgroupfinder) request = DummyRequest() self.assertEqual(policy.authenticated_userid(request), None) self.assertEqual(sorted(policy.effective_principals(request)), [Everyone, "test2"]) def test_stacking_of_effective_principals(self): policies = [TestAuthnPolicy2(), TestAuthnPolicy3()] policy = MultiAuthenticationPolicy(policies) request = DummyRequest() self.assertEqual( sorted(policy.effective_principals(request)), [Authenticated, Everyone, "test2", "test3", "test4"], ) policies.reverse() self.assertEqual( sorted(policy.effective_principals(request)), [Authenticated, Everyone, "test2", "test3", "test4"], ) policies.append(TestAuthnPolicy1()) self.assertEqual( sorted(policy.effective_principals(request)), [Authenticated, Everyone, "test1", "test2", "test3", "test4"], ) def test_stacking_of_effective_principals_with_groupfinder(self): policies = [TestAuthnPolicy2(), TestAuthnPolicy3()] policy = MultiAuthenticationPolicy(policies, customgroupfinder) request = DummyRequest() self.assertEqual( sorted(policy.effective_principals(request)), ["group", Authenticated, Everyone, "test2", "test3", "test4"], ) policies.reverse() self.assertEqual( sorted(policy.effective_principals(request)), ["group", Authenticated, Everyone, "test2", "test3", "test4"], ) policies.append(TestAuthnPolicy1()) self.assertEqual( sorted(policy.effective_principals(request)), ["group", Authenticated, Everyone, "test1", "test2", "test3", "test4"], ) def test_stacking_of_remember_and_forget(self): policies = [TestAuthnPolicy1(), TestAuthnPolicy2(), TestAuthnPolicy3()] policy = MultiAuthenticationPolicy(policies) request = DummyRequest() self.assertEqual( policy.remember(request, "ha"), [("X-Remember", "ha"), ("X-Remember-2", "ha")] ) self.assertEqual(policy.forget(request), [("X-Forget", "foo"), ("X-Forget", "bar")]) policies.reverse() self.assertEqual( policy.remember(request, "ha"), [("X-Remember-2", "ha"), ("X-Remember", "ha")] ) self.assertEqual(policy.forget(request), [("X-Forget", "bar"), ("X-Forget", "foo")]) def test_includeme_uses_acl_authorization_by_default(self): self.config.include("pyramid_multiauth") self.config.commit() policy = self.config.registry.getUtility(IAuthorizationPolicy) expected = ACLAuthorizationPolicy self.assertTrue(isinstance(policy, expected)) def test_includeme_reads_authorization_from_settings(self): self.config.add_settings( {"multiauth.authorization_policy": "tests.test_base.TestAuthzPolicyCustom"} ) self.config.include("pyramid_multiauth") self.config.commit() policy = self.config.registry.getUtility(IAuthorizationPolicy) self.assertTrue(isinstance(policy, TestAuthzPolicyCustom)) def test_includeme_by_module(self): self.config.add_settings( { "multiauth.groupfinder": "tests.test_base.customgroupfinder", "multiauth.policies": "tests.test_base.includeme1 " "tests.test_base.includeme2 " "tests.test_base.includemenull " "tests.test_base.includeme3 ", } ) self.config.include("pyramid_multiauth") self.config.commit() policy = self.config.registry.getUtility(IAuthenticationPolicy) self.assertEqual(policy._callback, customgroupfinder) self.assertEqual(len(policy._policies), 3) # Check that they stack correctly. request = DummyRequest() self.assertEqual(policy.unauthenticated_userid(request), "test2") self.assertEqual(policy.authenticated_userid(request), "test3") # Check that the forbidden view gets invoked. self.config.add_route("index", path="/") self.config.add_view(raiseforbidden, route_name="index") app = self.config.make_wsgi_app() environ = {"PATH_INFO": "/", "REQUEST_METHOD": "GET"} def start_response(*args): pass result = b"".join(app(environ, start_response)) self.assertEqual(result, b'"FORBIDDEN ONE"') def test_includeme_by_callable(self): self.config.add_settings( { "multiauth.groupfinder": "tests.test_base.customgroupfinder", "multiauth.policies": "tests.test_base.includeme1 policy1 policy2", "multiauth.policy.policy1.use": "tests.test_base.TestAuthnPolicy2", "multiauth.policy.policy1.foo": "bar", "multiauth.policy.policy2.use": "tests.test_base.TestAuthnPolicy3", } ) self.config.include("pyramid_multiauth") self.config.commit() policy = self.config.registry.getUtility(IAuthenticationPolicy) self.assertEqual(policy._callback, customgroupfinder) self.assertEqual(len(policy._policies), 3) self.assertEqual(policy._policies[1].foo, "bar") # Check that they stack correctly. request = DummyRequest() self.assertEqual(policy.unauthenticated_userid(request), "test2") self.assertEqual(policy.authenticated_userid(request), "test3") # Check that the forbidden view gets invoked. self.config.add_route("index", path="/") self.config.add_view(raiseforbidden, route_name="index") app = self.config.make_wsgi_app() environ = {"PATH_INFO": "/", "REQUEST_METHOD": "GET"} def start_response(*args): pass result = b"".join(app(environ, start_response)) self.assertEqual(result, b'"FORBIDDEN ONE"') def test_includeme_with_unconfigured_policy(self): self.config.add_settings( { "multiauth.groupfinder": "tests.test_base.customgroupfinder", "multiauth.policies": "tests.test_base.includeme1 policy1 policy2", "multiauth.policy.policy1.use": "tests.test_base.TestAuthnPolicy2", "multiauth.policy.policy1.foo": "bar", } ) self.assertRaises(ValueError, self.config.include, "pyramid_multiauth") def test_get_policy(self): self.config.add_settings( { "multiauth.policies": "tests.test_base.includeme1 policy1 policy2", "multiauth.policy.policy1.use": "tests.test_base.TestAuthnPolicy2", "multiauth.policy.policy1.foo": "bar", "multiauth.policy.policy2.use": "tests.test_base.TestAuthnPolicy3", } ) self.config.include("pyramid_multiauth") self.config.commit() policy = self.config.registry.getUtility(IAuthenticationPolicy) # Test getting policies by name. self.assertTrue(isinstance(policy.get_policy("policy1"), TestAuthnPolicy2)) self.assertTrue(isinstance(policy.get_policy("policy2"), TestAuthnPolicy3)) self.assertEqual(policy.get_policy("policy3"), None) # Test getting policies by class. self.assertTrue(isinstance(policy.get_policy(TestAuthnPolicy1), TestAuthnPolicy1)) self.assertTrue(isinstance(policy.get_policy(TestAuthnPolicy2), TestAuthnPolicy2)) self.assertTrue(isinstance(policy.get_policy(TestAuthnPolicy3), TestAuthnPolicy3)) self.assertEqual(policy.get_policy(MultiAuthPolicyTests), None) def test_get_policies(self): self.config.add_settings( { "multiauth.policies": "tests.test_base.includeme1 policy1 policy2", "multiauth.policy.policy1.use": "tests.test_base.TestAuthnPolicy2", "multiauth.policy.policy2.use": "tests.test_base.TestAuthnPolicy3", } ) self.config.include("pyramid_multiauth") self.config.commit() policy = self.config.registry.getUtility(IAuthenticationPolicy) policies = policy.get_policies() expected_result = [ ("tests.test_base.includeme1", TestAuthnPolicy1), ("policy1", TestAuthnPolicy2), ("policy2", TestAuthnPolicy3), ] for obtained, expected in zip(policies, expected_result): self.assertEqual(obtained[0], expected[0]) self.assertTrue(isinstance(obtained[1], expected[1])) def test_default_security(self): self.config.add_settings({"multiauth.policies": "tests.test_base.includeme1"}) self.config.include("pyramid_multiauth") self.config.commit() authn = self.config.registry.getUtility(IAuthenticationPolicy) self.assertTrue(isinstance(authn, MultiAuthenticationPolicy), authn) authz = self.config.registry.getUtility(IAuthorizationPolicy) self.assertTrue(isinstance(authz, ACLAuthorizationPolicy), authz) security = self.config.registry.getUtility(ISecurityPolicy) self.assertTrue(isinstance(security, LegacySecurityPolicy), security) def test_custom_security(self): class CustomSecurity: # Fake security class, didn't bother to implement interface. pass # Use an authentication from module. self.config.add_settings({"multiauth.policies": "tests.test_base.includeme1"}) # Will grab the authentication policy setup during include. self.config.include("pyramid_multiauth") # Set custom security (will override LegacySecurityPolicy). self.config.set_security_policy(CustomSecurity()) self.config.commit() # Check that registered authentication and security are appropriate. authn = self.config.registry.getUtility(IAuthenticationPolicy) self.assertTrue(isinstance(authn, MultiAuthenticationPolicy)) authz = self.config.registry.getUtility(IAuthorizationPolicy) self.assertTrue(isinstance(authz, ACLAuthorizationPolicy), authz) security = self.config.registry.getUtility(ISecurityPolicy) self.assertTrue(isinstance(security, CustomSecurity))