macaroonbakery-1.3.1/0000755000175000017500000000000013616470550016106 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/docs/0000755000175000017500000000000013616470550017036 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/docs/readme.rst0000644000175000017500000000003313466233337021024 0ustar frankbanfrankban00000000000000.. include:: ../README.rst macaroonbakery-1.3.1/docs/Makefile0000644000175000017500000001523213466233337020504 0ustar frankbanfrankban00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/py-macaroon-bakery.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/py-macaroon-bakery.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/py-macaroon-bakery" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/py-macaroon-bakery" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." macaroonbakery-1.3.1/docs/packaging.rst0000644000175000017500000000003713466233337021517 0ustar frankbanfrankban00000000000000========= Packaging ========= macaroonbakery-1.3.1/docs/contributing.rst0000644000175000017500000000004113466233337022275 0ustar frankbanfrankban00000000000000.. include:: ../CONTRIBUTING.rst macaroonbakery-1.3.1/docs/authors.rst0000644000175000017500000000003413466233337021255 0ustar frankbanfrankban00000000000000.. include:: ../AUTHORS.rst macaroonbakery-1.3.1/docs/usage.rst0000644000175000017500000000021413466233337020674 0ustar frankbanfrankban00000000000000======== Usage ======== To use the macaroon bakery Python library for handling macaroon higher level function:: import macaroonbakery macaroonbakery-1.3.1/docs/index.rst0000644000175000017500000000102213466233337020675 0ustar frankbanfrankban00000000000000.. bakery documentation master file, created by sphinx-quickstart on Tue Jul 9 22:26:36 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Macaroon Bakery Library's documentation! =================================================== Contents: .. toctree:: :maxdepth: 2 readme installation usage contributing packaging authors Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` macaroonbakery-1.3.1/docs/installation.rst0000644000175000017500000000014013466233337022267 0ustar frankbanfrankban00000000000000============ Installation ============ At the command line:: $ pip install macaroonbakery macaroonbakery-1.3.1/macaroonbakery.egg-info/0000755000175000017500000000000013616470550022575 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery.egg-info/requires.txt0000644000175000017500000000041413616470550025174 0ustar frankbanfrankban00000000000000PyNaCl<2.0,>=1.1.2 protobuf<4.0,>=3.0.0 pyRFC3339<2.0,>=1.0 pymacaroons<1.0,>=0.12.0 requests<3.0,>=2.18.1 six<2.0,>=1.11.0 [:python_full_version < "2.7.9"] cryptography==1.3.2 ndg_httpsclient==0.3.3 pyOpenSSL==16.0.0 pyasn1==0.1.9 [:python_version < "3"] ipaddress macaroonbakery-1.3.1/macaroonbakery.egg-info/not-zip-safe0000644000175000017500000000000113616470502025020 0ustar frankbanfrankban00000000000000 macaroonbakery-1.3.1/macaroonbakery.egg-info/top_level.txt0000644000175000017500000000001713616470550025325 0ustar frankbanfrankban00000000000000macaroonbakery macaroonbakery-1.3.1/macaroonbakery.egg-info/PKG-INFO0000644000175000017500000000377613616470550023707 0ustar frankbanfrankban00000000000000Metadata-Version: 1.1 Name: macaroonbakery Version: 1.3.1 Summary: A Python library port for bakery, higher level operation to work with macaroons Home-page: https://github.com/go-macaroon-bakery/py-macaroon-bakery Author: Juju UI Team Author-email: juju-gui@lists.ubuntu.com License: LGPL3 Description: =============== Macaroon Bakery =============== A Python library for working with macaroons. Installation ------------ The easiest way to install macaroonbakery is via pip:: $ pip install macaroonbakery macaroonbakery was developed around pymacaroons. On ubuntu, you can get libsodium from a ppa:: $ sudo add-apt-repository ppa:yellow/ppa -y $ apt-get install libsodium13 Usage ----- Interacting with a protected url, you can use the BakeryAuth provided to deal with the macaroon bakery >>> from macaroonbakery import httpbakery >>> jar = requests.cookies.RequestsCookieJar() >>> resp = requests.get('some protected url', cookies=jar, auth=httpbakery.BakeryAuth(cookies=jar)) >>> resp.raise_for_status() You can use any cookie storage you'd like so next subsequent calls the macaroon saved in the cookie jar will be directly used and will not require any other authentication (for example, cookielib.FileCookieJar). Keywords: macaroon cookie Platform: UNKNOWN Classifier: Development Status :: 2 - Pre-Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 macaroonbakery-1.3.1/macaroonbakery.egg-info/dependency_links.txt0000644000175000017500000000000113616470550026643 0ustar frankbanfrankban00000000000000 macaroonbakery-1.3.1/macaroonbakery.egg-info/SOURCES.txt0000644000175000017500000000466313616470550024472 0ustar frankbanfrankban00000000000000AUTHORS.rst CONTRIBUTING.rst LICENSE MANIFEST.in README.rst setup.cfg setup.py docs/Makefile docs/authors.rst docs/contributing.rst docs/index.rst docs/installation.rst docs/packaging.rst docs/readme.rst docs/usage.rst macaroonbakery/__init__.py macaroonbakery.egg-info/PKG-INFO macaroonbakery.egg-info/SOURCES.txt macaroonbakery.egg-info/dependency_links.txt macaroonbakery.egg-info/not-zip-safe macaroonbakery.egg-info/requires.txt macaroonbakery.egg-info/top_level.txt macaroonbakery/_utils/__init__.py macaroonbakery/bakery/__init__.py macaroonbakery/bakery/_authorizer.py macaroonbakery/bakery/_bakery.py macaroonbakery/bakery/_checker.py macaroonbakery/bakery/_codec.py macaroonbakery/bakery/_discharge.py macaroonbakery/bakery/_error.py macaroonbakery/bakery/_identity.py macaroonbakery/bakery/_keys.py macaroonbakery/bakery/_macaroon.py macaroonbakery/bakery/_oven.py macaroonbakery/bakery/_store.py macaroonbakery/bakery/_third_party.py macaroonbakery/bakery/_versions.py macaroonbakery/bakery/_internal/__init__.py macaroonbakery/bakery/_internal/id_pb2.py macaroonbakery/checkers/__init__.py macaroonbakery/checkers/_auth_context.py macaroonbakery/checkers/_caveat.py macaroonbakery/checkers/_checkers.py macaroonbakery/checkers/_conditions.py macaroonbakery/checkers/_declared.py macaroonbakery/checkers/_namespace.py macaroonbakery/checkers/_operation.py macaroonbakery/checkers/_time.py macaroonbakery/checkers/_utils.py macaroonbakery/httpbakery/__init__.py macaroonbakery/httpbakery/_browser.py macaroonbakery/httpbakery/_client.py macaroonbakery/httpbakery/_discharge.py macaroonbakery/httpbakery/_error.py macaroonbakery/httpbakery/_interactor.py macaroonbakery/httpbakery/_keyring.py macaroonbakery/httpbakery/agent/__init__.py macaroonbakery/httpbakery/agent/_agent.py macaroonbakery/tests/__init__.py macaroonbakery/tests/common.py macaroonbakery/tests/test_agent.py macaroonbakery/tests/test_authorizer.py macaroonbakery/tests/test_bakery.py macaroonbakery/tests/test_checker.py macaroonbakery/tests/test_checkers.py macaroonbakery/tests/test_client.py macaroonbakery/tests/test_codec.py macaroonbakery/tests/test_discharge.py macaroonbakery/tests/test_discharge_all.py macaroonbakery/tests/test_httpbakery.py macaroonbakery/tests/test_keyring.py macaroonbakery/tests/test_macaroon.py macaroonbakery/tests/test_namespace.py macaroonbakery/tests/test_oven.py macaroonbakery/tests/test_store.py macaroonbakery/tests/test_time.py macaroonbakery/tests/test_utils.pymacaroonbakery-1.3.1/setup.cfg0000644000175000017500000000007513616470550017731 0ustar frankbanfrankban00000000000000[wheel] universal = 1 [egg_info] tag_build = tag_date = 0 macaroonbakery-1.3.1/README.rst0000644000175000017500000000175413466233337017607 0ustar frankbanfrankban00000000000000=============== Macaroon Bakery =============== A Python library for working with macaroons. Installation ------------ The easiest way to install macaroonbakery is via pip:: $ pip install macaroonbakery macaroonbakery was developed around pymacaroons. On ubuntu, you can get libsodium from a ppa:: $ sudo add-apt-repository ppa:yellow/ppa -y $ apt-get install libsodium13 Usage ----- Interacting with a protected url, you can use the BakeryAuth provided to deal with the macaroon bakery >>> from macaroonbakery import httpbakery >>> jar = requests.cookies.RequestsCookieJar() >>> resp = requests.get('some protected url', cookies=jar, auth=httpbakery.BakeryAuth(cookies=jar)) >>> resp.raise_for_status() You can use any cookie storage you'd like so next subsequent calls the macaroon saved in the cookie jar will be directly used and will not require any other authentication (for example, cookielib.FileCookieJar). macaroonbakery-1.3.1/PKG-INFO0000644000175000017500000000377613616470550017220 0ustar frankbanfrankban00000000000000Metadata-Version: 1.1 Name: macaroonbakery Version: 1.3.1 Summary: A Python library port for bakery, higher level operation to work with macaroons Home-page: https://github.com/go-macaroon-bakery/py-macaroon-bakery Author: Juju UI Team Author-email: juju-gui@lists.ubuntu.com License: LGPL3 Description: =============== Macaroon Bakery =============== A Python library for working with macaroons. Installation ------------ The easiest way to install macaroonbakery is via pip:: $ pip install macaroonbakery macaroonbakery was developed around pymacaroons. On ubuntu, you can get libsodium from a ppa:: $ sudo add-apt-repository ppa:yellow/ppa -y $ apt-get install libsodium13 Usage ----- Interacting with a protected url, you can use the BakeryAuth provided to deal with the macaroon bakery >>> from macaroonbakery import httpbakery >>> jar = requests.cookies.RequestsCookieJar() >>> resp = requests.get('some protected url', cookies=jar, auth=httpbakery.BakeryAuth(cookies=jar)) >>> resp.raise_for_status() You can use any cookie storage you'd like so next subsequent calls the macaroon saved in the cookie jar will be directly used and will not require any other authentication (for example, cookielib.FileCookieJar). Keywords: macaroon cookie Platform: UNKNOWN Classifier: Development Status :: 2 - Pre-Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 macaroonbakery-1.3.1/AUTHORS.rst0000644000175000017500000000014613466233337017771 0ustar frankbanfrankban00000000000000======= Credits ======= Development Lead ---------------- * Juju UI Team macaroonbakery-1.3.1/MANIFEST.in0000644000175000017500000000026513466233337017652 0ustar frankbanfrankban00000000000000include AUTHORS.rst include CONTRIBUTING.rst include LICENSE include README.rst recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-include docs *.rst Makefile macaroonbakery-1.3.1/CONTRIBUTING.rst0000644000175000017500000000525613466233337020562 0ustar frankbanfrankban00000000000000============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/go-macaroon-bakery/py-macaroon-bakery/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ The macaroon bakery could always use more documentation, whether as part of the official macaroon bakery docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/go-macaroon-bakery/py-macaroon-bakery/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. Get Started! ------------ Ready to contribute? Here's how to set up `py-macaroon-bakery` for local development. 1. Fork the `py-macaroon-bakery` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/py-macaroon-bakery.git 3. Prepare your development environment:: $ make devenv 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: $ make check 6. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 2.7, 3+ and for PyPy. Tips ---- To run a subset of tests:: $ devenv/bin/nosetests macaroonbakery/tests/... macaroonbakery-1.3.1/setup.py0000755000175000017500000000364113616470421017624 0ustar frankbanfrankban00000000000000#!/usr/bin/env python # Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from setuptools import ( find_packages, setup, ) PROJECT_NAME = 'macaroonbakery' # version 1.3.1 VERSION = (1, 3, 1) def get_version(): '''Return the macaroon bakery version as a string.''' return '.'.join(map(str, VERSION)) with open('README.rst') as readme_file: readme = readme_file.read() requirements = [ 'requests>=2.18.1,<3.0', 'PyNaCl>=1.1.2,<2.0', 'pymacaroons>=0.12.0,<1.0', 'six>=1.11.0,<2.0', 'protobuf>=3.0.0,<4.0', 'pyRFC3339>=1.0,<2.0', 'ipaddress;python_version<"3"', 'cryptography==1.3.2;python_full_version<"2.7.9"', 'ndg_httpsclient==0.3.3;python_full_version<"2.7.9"', 'pyasn1==0.1.9;python_full_version<"2.7.9"', 'pyOpenSSL==16.0.0;python_full_version<"2.7.9"', ] test_requirements = [ 'tox', 'fixtures', 'httmock==1.2.5', ] setup( name=PROJECT_NAME, version=get_version(), description='A Python library port for bakery, higher level operation ' 'to work with macaroons', long_description=readme, author="Juju UI Team", author_email='juju-gui@lists.ubuntu.com', url='https://github.com/go-macaroon-bakery/py-macaroon-bakery', packages=find_packages(), include_package_data=True, install_requires=requirements, license="LGPL3", zip_safe=False, keywords='macaroon cookie', classifiers=[ 'Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 'Natural Language :: English', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', ], test_suite='tests', tests_require=test_requirements, ) macaroonbakery-1.3.1/LICENSE0000644000175000017500000001674413466233337017132 0ustar frankbanfrankban00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. macaroonbakery-1.3.1/macaroonbakery/0000755000175000017500000000000013616470550021103 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/bakery/0000755000175000017500000000000013616470550022360 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/bakery/_codec.py0000644000175000017500000002432113466233337024153 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 import json from ._versions import (VERSION_1, VERSION_2, VERSION_3) from ._third_party import legacy_namespace, ThirdPartyCaveatInfo from ._keys import PublicKey from ._error import VerificationError import macaroonbakery.checkers as checkers import nacl.public import six _PUBLIC_KEY_PREFIX_LEN = 4 _KEY_LEN = 32 # version3CaveatMinLen holds an underestimate of the # minimum length of a version 3 caveat. _VERSION3_CAVEAT_MIN_LEN = 1 + 4 + 32 + 24 + 16 + 1 def encode_caveat(condition, root_key, third_party_info, key, ns): '''Encrypt a third-party caveat. The third_party_info key holds information about the third party we're encrypting the caveat for; the key is the public/private key pair of the party that's adding the caveat. The caveat will be encoded according to the version information found in third_party_info. @param condition string @param root_key bytes @param third_party_info object @param key nacl key @param ns not used yet @return bytes ''' if third_party_info.version == VERSION_1: return _encode_caveat_v1(condition, root_key, third_party_info.public_key, key) if (third_party_info.version == VERSION_2 or third_party_info.version == VERSION_3): return _encode_caveat_v2_v3(third_party_info.version, condition, root_key, third_party_info.public_key, key, ns) raise NotImplementedError('only bakery v1, v2, v3 supported') def _encode_caveat_v1(condition, root_key, third_party_pub_key, key): '''Create a JSON-encoded third-party caveat. The third_party_pub_key key represents the PublicKey of the third party we're encrypting the caveat for; the key is the public/private key pair of the party that's adding the caveat. @param condition string @param root_key bytes @param third_party_pub_key (PublicKey) @param key (PrivateKey) @return a base64 encoded bytes ''' plain_data = json.dumps({ 'RootKey': base64.b64encode(root_key).decode('ascii'), 'Condition': condition }) box = nacl.public.Box(key.key, third_party_pub_key.key) encrypted = box.encrypt(six.b(plain_data)) nonce = encrypted[0:nacl.public.Box.NONCE_SIZE] encrypted = encrypted[nacl.public.Box.NONCE_SIZE:] return base64.b64encode(six.b(json.dumps({ 'ThirdPartyPublicKey': str(third_party_pub_key), 'FirstPartyPublicKey': str(key.public_key), 'Nonce': base64.b64encode(nonce).decode('ascii'), 'Id': base64.b64encode(encrypted).decode('ascii') }))) def _encode_caveat_v2_v3(version, condition, root_key, third_party_pub_key, key, ns): '''Create a version 2 or version 3 third-party caveat. The format has the following packed binary fields (note that all fields up to and including the nonce are the same as the v2 format): version 2 or 3 [1 byte] first 4 bytes of third-party Curve25519 public key [4 bytes] first-party Curve25519 public key [32 bytes] nonce [24 bytes] encrypted secret part [rest of message] The encrypted part encrypts the following fields with box.Seal: version 2 or 3 [1 byte] length of root key [n: uvarint] root key [n bytes] length of encoded namespace [n: uvarint] (Version 3 only) encoded namespace [n bytes] (Version 3 only) condition [rest of encrypted part] ''' ns_data = bytearray() if version >= VERSION_3: ns_data = ns.serialize_text() data = bytearray() data.append(version) data.extend(third_party_pub_key.serialize(raw=True)[:_PUBLIC_KEY_PREFIX_LEN]) data.extend(key.public_key.serialize(raw=True)[:]) secret = _encode_secret_part_v2_v3(version, condition, root_key, ns_data) box = nacl.public.Box(key.key, third_party_pub_key.key) encrypted = box.encrypt(secret) nonce = encrypted[0:nacl.public.Box.NONCE_SIZE] encrypted = encrypted[nacl.public.Box.NONCE_SIZE:] data.extend(nonce[:]) data.extend(encrypted) return bytes(data) def _encode_secret_part_v2_v3(version, condition, root_key, ns): '''Creates a version 2 or version 3 secret part of the third party caveat. The returned data is not encrypted. The format has the following packed binary fields: version 2 or 3 [1 byte] root key length [n: uvarint] root key [n bytes] namespace length [n: uvarint] (v3 only) namespace [n bytes] (v3 only) predicate [rest of message] ''' data = bytearray() data.append(version) encode_uvarint(len(root_key), data) data.extend(root_key) if version >= VERSION_3: encode_uvarint(len(ns), data) data.extend(ns) data.extend(condition.encode('utf-8')) return bytes(data) def decode_caveat(key, caveat): '''Decode caveat by decrypting the encrypted part using key. @param key the nacl private key to decode. @param caveat bytes. @return ThirdPartyCaveatInfo ''' if len(caveat) == 0: raise VerificationError('empty third party caveat') first = caveat[:1] if first == b'e': # 'e' will be the first byte if the caveatid is a base64 # encoded JSON object. return _decode_caveat_v1(key, caveat) first_as_int = six.byte2int(first) if (first_as_int == VERSION_2 or first_as_int == VERSION_3): if (len(caveat) < _VERSION3_CAVEAT_MIN_LEN and first_as_int == VERSION_3): # If it has the version 3 caveat tag and it's too short, it's # almost certainly an id, not an encrypted payload. raise VerificationError( 'caveat id payload not provided for caveat id {}'.format( caveat)) return _decode_caveat_v2_v3(first_as_int, key, caveat) raise VerificationError('unknown version for caveat') def _decode_caveat_v1(key, caveat): '''Decode a base64 encoded JSON id. @param key the nacl private key to decode. @param caveat a base64 encoded JSON string. ''' data = base64.b64decode(caveat).decode('utf-8') wrapper = json.loads(data) tp_public_key = nacl.public.PublicKey( base64.b64decode(wrapper['ThirdPartyPublicKey'])) if key.public_key.key != tp_public_key: raise Exception('public key mismatch') # TODO if wrapper.get('FirstPartyPublicKey', None) is None: raise Exception('target service public key not specified') # The encrypted string is base64 encoded in the JSON representation. secret = base64.b64decode(wrapper.get('Id')) nonce = base64.b64decode(wrapper.get('Nonce')) fp_public_key = nacl.public.PublicKey(base64.b64decode( wrapper.get('FirstPartyPublicKey'))) box = nacl.public.Box(key.key, fp_public_key) c = box.decrypt(secret, nonce) record = json.loads(c.decode('utf-8')) fp_key = nacl.public.PublicKey( base64.b64decode(wrapper.get('FirstPartyPublicKey'))) return ThirdPartyCaveatInfo( condition=record.get('Condition'), first_party_public_key=PublicKey(fp_key), third_party_key_pair=key, root_key=base64.b64decode(record.get('RootKey')), caveat=caveat, id=None, version=VERSION_1, namespace=legacy_namespace() ) def _decode_caveat_v2_v3(version, key, caveat): '''Decodes a version 2 or version 3 caveat. ''' if (len(caveat) < 1 + _PUBLIC_KEY_PREFIX_LEN + _KEY_LEN + nacl.public.Box.NONCE_SIZE + 16): raise VerificationError('caveat id too short') original_caveat = caveat caveat = caveat[1:] # skip version (already checked) pk_prefix = caveat[:_PUBLIC_KEY_PREFIX_LEN] caveat = caveat[_PUBLIC_KEY_PREFIX_LEN:] if key.public_key.serialize(raw=True)[:_PUBLIC_KEY_PREFIX_LEN] != pk_prefix: raise VerificationError('public key mismatch') first_party_pub = caveat[:_KEY_LEN] caveat = caveat[_KEY_LEN:] nonce = caveat[:nacl.public.Box.NONCE_SIZE] caveat = caveat[nacl.public.Box.NONCE_SIZE:] fp_public_key = nacl.public.PublicKey(first_party_pub) box = nacl.public.Box(key.key, fp_public_key) data = box.decrypt(caveat, nonce) root_key, condition, ns = _decode_secret_part_v2_v3(version, data) return ThirdPartyCaveatInfo( condition=condition.decode('utf-8'), first_party_public_key=PublicKey(fp_public_key), third_party_key_pair=key, root_key=root_key, caveat=original_caveat, version=version, id=None, namespace=ns ) def _decode_secret_part_v2_v3(version, data): if len(data) < 1: raise VerificationError('secret part too short') got_version = six.byte2int(data[:1]) data = data[1:] if version != got_version: raise VerificationError( 'unexpected secret part version, got {} want {}'.format( got_version, version)) root_key_length, read = decode_uvarint(data) data = data[read:] root_key = data[:root_key_length] data = data[root_key_length:] if version >= VERSION_3: namespace_length, read = decode_uvarint(data) data = data[read:] ns_data = data[:namespace_length] data = data[namespace_length:] ns = checkers.deserialize_namespace(ns_data) else: ns = legacy_namespace() return root_key, data, ns def encode_uvarint(n, data): '''encodes integer into variable-length format into data.''' if n < 0: raise ValueError('only support positive integer') while True: this_byte = n & 127 n >>= 7 if n == 0: data.append(this_byte) break data.append(this_byte | 128) def decode_uvarint(data): '''Decode a variable-length integer. Reads a sequence of unsigned integer byte and decodes them into an integer in variable-length format and returns it and the length read. ''' n = 0 shift = 0 length = 0 for b in data: if not isinstance(b, int): b = six.byte2int(b) n |= (b & 0x7f) << shift length += 1 if (b & 0x80) == 0: break shift += 7 return n, length macaroonbakery-1.3.1/macaroonbakery/bakery/_oven.py0000644000175000017500000002461513466233337024053 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 import hashlib import itertools import os import google from ._checker import (Op, LOGIN_OP) from ._store import MemoryKeyStore from ._error import VerificationError from ._versions import ( VERSION_2, VERSION_3, LATEST_VERSION, ) from ._macaroon import ( Macaroon, macaroon_version, ) import macaroonbakery.checkers as checkers import six from macaroonbakery._utils import ( raw_urlsafe_b64encode, b64decode, ) from ._internal import id_pb2 from pymacaroons import MACAROON_V2, Verifier class Oven: ''' Oven bakes macaroons. They emerge sweet and delicious and ready for use in a Checker. All macaroons are associated with one or more operations (see the Op type) which define the capabilities of the macaroon. There is one special operation, "login" (defined by LOGIN_OP) which grants the capability to speak for a particular user. The login capability will never be mixed with other capabilities. It is up to the caller to decide on semantics for other operations. ''' def __init__(self, key=None, location=None, locator=None, namespace=None, root_keystore_for_ops=None, ops_store=None): ''' @param namespace holds the namespace to use when adding first party caveats. @param root_keystore_for_ops a function that will give the macaroon storage to be used for root keys associated with macaroons created with macaroon. @param ops_store object is used to persistently store the association of multi-op entities with their associated operations when macaroon is called with multiple operations. When this is in use, operation entities with the prefix "multi-" are reserved - a "multi-"-prefixed entity represents a set of operations stored in the OpsStore. @param key holds the private nacl key pair used to encrypt third party caveats. If it is None, no third party caveats can be created. @param location string holds the location that will be associated with new macaroons (as returned by Macaroon.Location). @param locator is used to find out information on third parties when adding third party caveats. If this is None, no non-local third party caveats can be added. ''' self.key = key self.location = location self.locator = locator if namespace is None: namespace = checkers.Checker().namespace() self.namespace = namespace self.ops_store = ops_store self.root_keystore_for_ops = root_keystore_for_ops if root_keystore_for_ops is None: my_store = MemoryKeyStore() self.root_keystore_for_ops = lambda x: my_store def macaroon(self, version, expiry, caveats, ops): ''' Takes a macaroon with the given version from the oven, associates it with the given operations and attaches the given caveats. There must be at least one operation specified. The macaroon will expire at the given time - a time_before first party caveat will be added with that time. @return: a new Macaroon object. ''' if len(ops) == 0: raise ValueError('cannot mint a macaroon associated ' 'with no operations') ops = canonical_ops(ops) root_key, storage_id = self.root_keystore_for_ops(ops).root_key() id = self._new_macaroon_id(storage_id, expiry, ops) id_bytes = six.int2byte(LATEST_VERSION) + \ id.SerializeToString() if macaroon_version(version) < MACAROON_V2: # The old macaroon format required valid text for the macaroon id, # so base64-encode it. id_bytes = raw_urlsafe_b64encode(id_bytes) m = Macaroon( root_key, id_bytes, self.location, version, self.namespace, ) m.add_caveat(checkers.time_before_caveat(expiry), self.key, self.locator) m.add_caveats(caveats, self.key, self.locator) return m def _new_macaroon_id(self, storage_id, expiry, ops): nonce = os.urandom(16) if len(ops) == 1 or self.ops_store is None: return id_pb2.MacaroonId( nonce=nonce, storageId=storage_id, ops=_macaroon_id_ops(ops)) # We've got several operations and a multi-op store, so use the store. # TODO use the store only if the encoded macaroon id exceeds some size? entity = self.ops_entity(ops) self.ops_store.put_ops(entity, expiry, ops) return id_pb2.MacaroonId( nonce=nonce, storageId=storage_id, ops=[id_pb2.Op(entity=entity, actions=['*'])]) def ops_entity(self, ops): ''' Returns a new multi-op entity name string that represents all the given operations and caveats. It returns the same value regardless of the ordering of the operations. It assumes that the operations have been canonicalized and that there's at least one operation. :param ops: :return: string that represents all the given operations and caveats. ''' # Hash the operations, removing duplicates as we go. hash_entity = hashlib.sha256() for op in ops: hash_entity.update('{}\n{}\n'.format( op.action, op.entity).encode()) hash_encoded = base64.urlsafe_b64encode(hash_entity.digest()) return 'multi-' + hash_encoded.decode('utf-8').rstrip('=') def macaroon_ops(self, macaroons): ''' This method makes the oven satisfy the MacaroonOpStore protocol required by the Checker class. For macaroons minted with previous bakery versions, it always returns a single LoginOp operation. :param macaroons: :return: ''' if len(macaroons) == 0: raise ValueError('no macaroons provided') storage_id, ops = _decode_macaroon_id(macaroons[0].identifier_bytes) root_key = self.root_keystore_for_ops(ops).get(storage_id) if root_key is None: raise VerificationError( 'macaroon key not found in storage') v = Verifier() conditions = [] def validator(condition): # Verify the macaroon's signature only. Don't check any of the # caveats yet but save them so that we can return them. conditions.append(condition) return True v.satisfy_general(validator) try: v.verify(macaroons[0], root_key, macaroons[1:]) except Exception as exc: # Unfortunately pymacaroons doesn't control # the set of exceptions that can be raised here. # Possible candidates are: # pymacaroons.exceptions.MacaroonUnmetCaveatException # pymacaroons.exceptions.MacaroonInvalidSignatureException # ValueError # nacl.exceptions.CryptoError # # There may be others too, so just catch everything. raise six.raise_from( VerificationError('verification failed: {}'.format(str(exc))), exc, ) if (self.ops_store is not None and len(ops) == 1 and ops[0].entity.startswith('multi-')): # It's a multi-op entity, so retrieve the actual operations # it's associated with. ops = self.ops_store.get_ops(ops[0].entity) return ops, conditions def _decode_macaroon_id(id): storage_id = b'' base64_decoded = False first = id[:1] if first == b'A': # The first byte is not a version number and it's 'A', which is the # base64 encoding of the top 6 bits (all zero) of the version number 2 # or 3, so we assume that it's the base64 encoding of a new-style # macaroon id, so we base64 decode it. # # Note that old-style ids always start with an ASCII character >= 4 # (> 32 in fact) so this logic won't be triggered for those. try: dec = b64decode(id.decode('utf-8')) # Set the id only on success. id = dec base64_decoded = True except: # if it's a bad encoding, we'll get an error which is fine pass # Trim any extraneous information from the id before retrieving # it from storage, including the UUID that's added when # creating macaroons to make all macaroons unique even if # they're using the same root key. first = six.byte2int(id[:1]) if first == VERSION_2: # Skip the UUID at the start of the id. storage_id = id[1 + 16:] if first == VERSION_3: try: id1 = id_pb2.MacaroonId.FromString(id[1:]) except google.protobuf.message.DecodeError: raise VerificationError( 'no operations found in macaroon') if len(id1.ops) == 0 or len(id1.ops[0].actions) == 0: raise VerificationError( 'no operations found in macaroon') ops = [] for op in id1.ops: for action in op.actions: ops.append(Op(op.entity, action)) return id1.storageId, ops if not base64_decoded and _is_lower_case_hex_char(first): # It's an old-style id, probably with a hyphenated UUID. # so trim that off. last = id.rfind(b'-') if last >= 0: storage_id = id[0:last] return storage_id, [LOGIN_OP] def _is_lower_case_hex_char(b): if ord('0') <= b <= ord('9'): return True if ord('a') <= b <= ord('f'): return True return False def canonical_ops(ops): ''' Returns the given operations array sorted with duplicates removed. @param ops checker.Ops @return: checker.Ops ''' new_ops = sorted(set(ops), key=lambda x: (x.entity, x.action)) return new_ops def _macaroon_id_ops(ops): '''Return operations suitable for serializing as part of a MacaroonId. It assumes that ops has been canonicalized and that there's at least one operation. ''' id_ops = [] for entity, entity_ops in itertools.groupby(ops, lambda x: x.entity): actions = map(lambda x: x.action, entity_ops) id_ops.append(id_pb2.Op(entity=entity, actions=actions)) return id_ops macaroonbakery-1.3.1/macaroonbakery/bakery/_discharge.py0000644000175000017500000002223713466233337025033 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import abc from collections import namedtuple from ._error import ( ThirdPartyCaveatCheckFailed, CaveatNotRecognizedError, VerificationError, ) from ._codec import decode_caveat from ._macaroon import ( Macaroon, ThirdPartyLocator, ) from ._versions import VERSION_2 from ._third_party import ThirdPartyCaveatInfo import macaroonbakery.checkers as checkers emptyContext = checkers.AuthContext() def discharge_all(m, get_discharge, local_key=None): '''Gathers discharge macaroons for all the third party caveats in m (and any subsequent caveats required by those) using get_discharge to acquire each discharge macaroon. The local_key parameter may optionally hold the key of the client, in which case it will be used to discharge any third party caveats with the special location "local". In this case, the caveat itself must be "true". This can be used be a server to ask a client to prove ownership of the private key. It returns a list of macaroon with m as the first element, followed by all the discharge macaroons. All the discharge macaroons will be bound to the primary macaroon. The get_discharge function is passed a context (AuthContext), the caveat(pymacaroons.Caveat) to be discharged and encrypted_caveat (bytes) will be passed the external caveat payload found in m, if any. It should return a bakery.Macaroon object holding the discharge macaroon for the third party caveat. ''' primary = m.macaroon discharges = [primary] # cav holds the macaroon caveat that needs discharge. # encrypted_caveat (bytes) holds encrypted caveat if it was held # externally. _NeedCaveat = namedtuple('_NeedCaveat', 'cav encrypted_caveat') need = [] def add_caveats(m): for cav in m.macaroon.caveats: if cav.location is None or cav.location == '': continue encrypted_caveat = m.caveat_data.get(cav.caveat_id, None) need.append( _NeedCaveat(cav=cav, encrypted_caveat=encrypted_caveat)) add_caveats(m) while len(need) > 0: cav = need[0] need = need[1:] if cav.cav.location == 'local': if local_key is None: raise ThirdPartyCaveatCheckFailed( 'found local third party caveat but no private key provided', ) # TODO use a small caveat id. dm = discharge(ctx=emptyContext, key=local_key, checker=_LocalDischargeChecker(), caveat=cav.encrypted_caveat, id=cav.cav.caveat_id_bytes, locator=_EmptyLocator()) else: dm = get_discharge(cav.cav, cav.encrypted_caveat) # It doesn't matter that we're invalidating dm here because we're # about to throw it away. discharge_m = dm.macaroon m = primary.prepare_for_request(discharge_m) discharges.append(m) add_caveats(dm) return discharges class ThirdPartyCaveatChecker(object): ''' Defines an abstract class that's used to check third party caveats. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def check_third_party_caveat(self, ctx, info): ''' If the caveat is valid, it returns optionally a slice of extra caveats that will be added to the discharge macaroon. If the caveat kind was not recognised, the checker should raise a CaveatNotRecognized exception; if the check failed, it should raise a ThirdPartyCaveatCheckFailed exception. :param ctx (AuthContext) :param info (ThirdPartyCaveatInfo) holds the information decoded from a third party caveat id :return: An array of extra caveats to be added to the discharge macaroon. ''' raise NotImplementedError('check_third_party_caveat method must be ' 'defined in subclass') class _LocalDischargeChecker(ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): if info.condition != 'true': raise CaveatNotRecognizedError() return [] def discharge(ctx, id, caveat, key, checker, locator): ''' Creates a macaroon to discharge a third party caveat. The given parameters specify the caveat and how it should be checked. The condition implicit in the caveat is checked for validity using checker. If it is valid, a new macaroon is returned which discharges the caveat. The macaroon is created with a version derived from the version that was used to encode the id. :param id: (bytes) holds the id to give to the discharge macaroon. If Caveat is empty, then the id also holds the encrypted third party caveat. :param caveat: (bytes) holds the encrypted third party caveat. If this is None, id will be used. :param key: holds the key to use to decrypt the third party caveat information and to encrypt any additional third party caveats returned by the caveat checker. :param checker: used to check the third party caveat, and may also return further caveats to be added to the discharge macaroon. :param locator: used to information on third parties referred to by third party caveats returned by the Checker. ''' caveat_id_prefix = [] if caveat is None: # The caveat information is encoded in the id itself. caveat = id else: # We've been given an explicit id, so when extra third party # caveats are added, use that id as the prefix # for any more ids. caveat_id_prefix = id cav_info = decode_caveat(key, caveat) cav_info = ThirdPartyCaveatInfo( condition=cav_info.condition, first_party_public_key=cav_info.first_party_public_key, third_party_key_pair=cav_info.third_party_key_pair, root_key=cav_info.root_key, caveat=cav_info.caveat, version=cav_info.version, id=id, namespace=cav_info.namespace ) # Note that we don't check the error - we allow the # third party checker to see even caveats that we can't # understand. try: cond, arg = checkers.parse_caveat(cav_info.condition) except ValueError as exc: raise VerificationError(exc.args[0]) if cond == checkers.COND_NEED_DECLARED: cav_info = cav_info._replace(condition=arg) caveats = _check_need_declared(ctx, cav_info, checker) else: caveats = checker.check_third_party_caveat(ctx, cav_info) # Note that the discharge macaroon does not need to # be stored persistently. Indeed, it would be a problem if # we did, because then the macaroon could potentially be used # for normal authorization with the third party. m = Macaroon( cav_info.root_key, id, '', cav_info.version, cav_info.namespace, ) m._caveat_id_prefix = caveat_id_prefix if caveats is not None: for cav in caveats: m.add_caveat(cav, key, locator) return m def _check_need_declared(ctx, cav_info, checker): arg = cav_info.condition i = arg.find(' ') if i <= 0: raise VerificationError( 'need-declared caveat requires an argument, got %q'.format(arg), ) need_declared = arg[0:i].split(',') for d in need_declared: if d == '': raise VerificationError('need-declared caveat with empty required attribute') if len(need_declared) == 0: raise VerificationError('need-declared caveat with no required attributes') cav_info = cav_info._replace(condition=arg[i + 1:]) caveats = checker.check_third_party_caveat(ctx, cav_info) declared = {} for cav in caveats: if cav.location is not None and cav.location != '': continue # Note that we ignore the error. We allow the service to # generate caveats that we don't understand here. try: cond, arg = checkers.parse_caveat(cav.condition) except ValueError: continue if cond != checkers.COND_DECLARED: continue parts = arg.split() if len(parts) != 2: raise VerificationError('declared caveat has no value') declared[parts[0]] = True # Add empty declarations for everything mentioned in need-declared # that was not actually declared. for d in need_declared: if not declared.get(d, False): caveats.append(checkers.declared_caveat(d, '')) return caveats class _EmptyLocator(ThirdPartyLocator): def third_party_info(self, loc): return None def local_third_party_caveat(key, version): ''' Returns a third-party caveat that, when added to a macaroon with add_caveat, results in a caveat with the location "local", encrypted with the given PublicKey. This can be automatically discharged by discharge_all passing a local key. ''' if version >= VERSION_2: loc = 'local {} {}'.format(version, key) else: loc = 'local {}'.format(key) return checkers.Caveat(location=loc, condition='') macaroonbakery-1.3.1/macaroonbakery/bakery/_versions.py0000644000175000017500000000026013466233337024742 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. VERSION_0 = 0 VERSION_1 = 1 VERSION_2 = 2 VERSION_3 = 3 LATEST_VERSION = VERSION_3 macaroonbakery-1.3.1/macaroonbakery/bakery/_third_party.py0000644000175000017500000000401513466233337025425 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from collections import namedtuple import macaroonbakery.checkers as checkers def legacy_namespace(): ''' Standard namespace for pre-version3 macaroons. ''' ns = checkers.Namespace(None) ns.register(checkers.STD_NAMESPACE, '') return ns class ThirdPartyCaveatInfo(namedtuple( 'ThirdPartyCaveatInfo', 'condition, first_party_public_key, third_party_key_pair, root_key, ' 'caveat, version, id, namespace')): '''ThirdPartyCaveatInfo holds the information decoded from a third party caveat id. @param condition holds the third party condition to be discharged. This is the only field that most third party dischargers will need to consider. {str} @param first_party_public_key holds the public key of the party that created the third party caveat. {PublicKey} @param third_party_key_pair holds the nacl private used to decrypt the caveat - the key pair of the discharging service. {PrivateKey} @param root_key holds the secret root key encoded by the caveat. {bytes} @param caveat holds the full caveat id from which all the other fields are derived. {bytes} @param version holds the version that was used to encode the caveat id. {number} @param id holds the id of the third party caveat (the id that the discharge macaroon should be given). This will differ from Caveat when the caveat information is encoded separately. {bytes} @param namespace object that holds the namespace of the first party that created the macaroon, as encoded by the party that added the third party caveat. {checkers.Namespace} ''' class ThirdPartyInfo(namedtuple('ThirdPartyInfo', 'version, public_key')): ''' ThirdPartyInfo holds information on a given third party discharge service. @param version The latest bakery protocol version supported by the discharger {number} @param public_key Public key of the third party {PublicKey} ''' macaroonbakery-1.3.1/macaroonbakery/bakery/_internal/0000755000175000017500000000000013616470550024333 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/bakery/_internal/id_pb2.py0000644000175000017500000001010013466233337026037 0ustar frankbanfrankban00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: macaroonbakery/internal/id.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='macaroonbakery/internal/id.proto', package='', syntax='proto3', serialized_pb=_b('\n macaroonbakery/internal/id.proto\"@\n\nMacaroonId\x12\r\n\x05nonce\x18\x01 \x01(\x0c\x12\x11\n\tstorageId\x18\x02 \x01(\x0c\x12\x10\n\x03ops\x18\x03 \x03(\x0b\x32\x03.Op\"%\n\x02Op\x12\x0e\n\x06\x65ntity\x18\x01 \x01(\t\x12\x0f\n\x07\x61\x63tions\x18\x02 \x03(\tB\x0cZ\nmacaroonpbb\x06proto3') ) _MACAROONID = _descriptor.Descriptor( name='MacaroonId', full_name='MacaroonId', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='nonce', full_name='MacaroonId.nonce', index=0, number=1, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='storageId', full_name='MacaroonId.storageId', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='ops', full_name='MacaroonId.ops', index=2, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=36, serialized_end=100, ) _OP = _descriptor.Descriptor( name='Op', full_name='Op', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='entity', full_name='Op.entity', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='actions', full_name='Op.actions', index=1, number=2, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=102, serialized_end=139, ) _MACAROONID.fields_by_name['ops'].message_type = _OP DESCRIPTOR.message_types_by_name['MacaroonId'] = _MACAROONID DESCRIPTOR.message_types_by_name['Op'] = _OP _sym_db.RegisterFileDescriptor(DESCRIPTOR) MacaroonId = _reflection.GeneratedProtocolMessageType('MacaroonId', (_message.Message,), dict( DESCRIPTOR = _MACAROONID, __module__ = 'macaroonbakery.internal.id_pb2' # @@protoc_insertion_point(class_scope:MacaroonId) )) _sym_db.RegisterMessage(MacaroonId) Op = _reflection.GeneratedProtocolMessageType('Op', (_message.Message,), dict( DESCRIPTOR = _OP, __module__ = 'macaroonbakery.internal.id_pb2' # @@protoc_insertion_point(class_scope:Op) )) _sym_db.RegisterMessage(Op) DESCRIPTOR.has_options = True DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('Z\nmacaroonpb')) # @@protoc_insertion_point(module_scope) macaroonbakery-1.3.1/macaroonbakery/bakery/_internal/__init__.py0000644000175000017500000000000013466233337026435 0ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/bakery/_checker.py0000644000175000017500000004052713466233337024510 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from collections import namedtuple from threading import Lock from ._authorizer import ClosedAuthorizer from ._identity import NoIdentities from ._error import ( AuthInitError, VerificationError, IdentityError, DischargeRequiredError, PermissionDenied, ) import macaroonbakery.checkers as checkers import pyrfc3339 class Op(namedtuple('Op', 'entity, action')): ''' Op holds an entity and action to be authorized on that entity. entity string holds the name of the entity to be authorized. @param entity should not contain spaces and should not start with the prefix "login" or "multi-" (conventionally, entity names will be prefixed with the entity type followed by a hyphen. @param action string holds the action to perform on the entity, such as "read" or "delete". It is up to the service using a checker to define a set of operations and keep them consistent over time. ''' # LOGIN_OP represents a login (authentication) operation. # A macaroon that is associated with this operation generally # carries authentication information with it. LOGIN_OP = Op(entity='login', action='login') class Checker(object): '''Checker implements an authentication and authorization checker. It uses macaroons as authorization tokens but it is not itself responsible for creating the macaroons See the Oven type (TODO) for one way of doing that. ''' def __init__(self, checker=checkers.Checker(), authorizer=ClosedAuthorizer(), identity_client=None, macaroon_opstore=None): ''' :param checker: a first party checker implementing a :param authorizer (Authorizer): used to check whether an authenticated user is allowed to perform operations. The identity parameter passed to authorizer.allow will always have been obtained from a call to identity_client.declared_identity. :param identity_client (IdentityClient) used for interactions with the external identity service used for authentication. If this is None, no authentication will be possible. :param macaroon_opstore (object with new_macaroon and macaroon_ops method): used to retrieve macaroon root keys and other associated information. ''' self._first_party_caveat_checker = checker self._authorizer = authorizer if identity_client is None: identity_client = NoIdentities() self._identity_client = identity_client self._macaroon_opstore = macaroon_opstore def auth(self, mss): ''' Returns a new AuthChecker instance using the given macaroons to inform authorization decisions. @param mss: a list of macaroon lists. ''' return AuthChecker(parent=self, macaroons=mss) def namespace(self): ''' Returns the namespace of the first party checker. ''' return self._first_party_caveat_checker.namespace() class AuthChecker(object): '''Authorizes operations with respect to a user's request. The identity is authenticated only once, the first time any method of the AuthChecker is called, using the context passed in then. To find out any declared identity without requiring a login, use allow(ctx); to require authentication but no additional operations, use allow(ctx, LOGIN_OP). ''' def __init__(self, parent, macaroons): ''' :param parent (Checker): used to check first party caveats. :param macaroons: a list of py macaroons ''' self._macaroons = macaroons self._init_errors = [] self._executed = False self._identity = None self._identity_caveats = [] self.parent = parent self._conditions = None self._mutex = Lock() def _init(self, ctx): with self._mutex: if not self._executed: self._init_once(ctx) self._executed = True def _init_once(self, ctx): self._auth_indexes = {} self._conditions = [None] * len(self._macaroons) for i, ms in enumerate(self._macaroons): try: ops, conditions = self.parent._macaroon_opstore.macaroon_ops(ms) except VerificationError as e: self._init_errors.append(str(e)) continue except Exception as exc: raise AuthInitError(str(exc)) # It's a valid macaroon (in principle - we haven't checked first # party caveats). self._conditions[i] = conditions is_login = False for op in ops: if op == LOGIN_OP: # Don't associate the macaroon with the login operation # until we've verified that it is valid below is_login = True else: if op not in self._auth_indexes: self._auth_indexes[op] = [] self._auth_indexes[op].append(i) if not is_login: continue # It's a login macaroon. Check the conditions now - # all calls want to see the same authentication # information so that callers have a consistent idea of # the client's identity. # # If the conditions fail, we won't use the macaroon for # identity, but we can still potentially use it for its # other operations if the conditions succeed for those. declared, err = self._check_conditions(ctx, LOGIN_OP, conditions) if err is not None: self._init_errors.append('cannot authorize login macaroon: ' + err) continue if self._identity is not None: # We've already found a login macaroon so ignore this one # for the purposes of identity. continue try: identity = self.parent._identity_client.declared_identity( ctx, declared) except IdentityError as exc: self._init_errors.append( 'cannot decode declared identity: {}'.format(exc.args[0])) continue if LOGIN_OP not in self._auth_indexes: self._auth_indexes[LOGIN_OP] = [] self._auth_indexes[LOGIN_OP].append(i) self._identity = identity if self._identity is None: # No identity yet, so try to get one based on the context. try: identity, cavs = self.parent.\ _identity_client.identity_from_context(ctx) except IdentityError: self._init_errors.append('could not determine identity') if cavs is None: cavs = [] self._identity, self._identity_caveats = identity, cavs return None def allow(self, ctx, ops): ''' Checks that the authorizer's request is authorized to perform all the given operations. Note that allow does not check first party caveats - if there is more than one macaroon that may authorize the request, it will choose the first one that does regardless. If all the operations are allowed, an AuthInfo is returned holding details of the decision and any first party caveats that must be checked before actually executing any operation. If operations include LOGIN_OP, the request should contain an authentication macaroon proving the client's identity. Once an authentication macaroon is chosen, it will be used for all other authorization requests. If an operation was not allowed, an exception will be raised which may be: - DischargeRequiredError holding the operations that remain to be authorized in order to allow authorization to proceed - PermissionDenied when no operations can be authorized and there's no third party to discharge macaroons for. @param ctx AuthContext @param ops an array of Op :return: an AuthInfo object. ''' auth_info, _ = self.allow_any(ctx, ops) return auth_info def allow_any(self, ctx, ops): ''' like allow except that it will authorize as many of the operations as possible without requiring any to be authorized. If all the operations succeeded, the array will be nil. If any the operations failed, the returned error will be the same that allow would return and each element in the returned slice will hold whether its respective operation was allowed. If all the operations succeeded, the returned slice will be None. The returned AuthInfo will always be non-None. The LOGIN_OP operation is treated specially - it is always required if present in ops. @param ctx AuthContext @param ops an array of Op :return: an AuthInfo object and the auth used as an array of int. ''' authed, used = self._allow_any(ctx, ops) return self._new_auth_info(used), authed def _new_auth_info(self, used): info = AuthInfo(identity=self._identity, macaroons=[]) for i, is_used in enumerate(used): if is_used: info.macaroons.append(self._macaroons[i]) return info def _allow_any(self, ctx, ops): self._init(ctx) used = [False] * len(self._macaroons) authed = [False] * len(ops) num_authed = 0 errors = [] for i, op in enumerate(ops): for mindex in self._auth_indexes.get(op, []): _, err = self._check_conditions(ctx, op, self._conditions[mindex]) if err is not None: errors.append(err) continue authed[i] = True num_authed += 1 used[mindex] = True # Use the first authorized macaroon only. break if op == LOGIN_OP and not authed[i] and self._identity is not None: # Allow LOGIN_OP when there's an authenticated user even # when there's no macaroon that specifically authorizes it. authed[i] = True if self._identity is not None: # We've authenticated as a user, so even if the operations didn't # specifically require it, we add the login macaroon # to the macaroons used. # Note that the LOGIN_OP conditions have already been checked # successfully in initOnceFunc so no need to check again. # Note also that there may not be any macaroons if the # identity client decided on an identity even with no # macaroons. for i in self._auth_indexes.get(LOGIN_OP, []): used[i] = True if num_authed == len(ops): # All operations allowed. return authed, used # There are some unauthorized operations. need = [] need_index = [0] * (len(ops) - num_authed) for i, ok in enumerate(authed): if not ok: need_index[len(need)] = i need.append(ops[i]) # Try to authorize the operations # even if we haven't got an authenticated user. oks, caveats = self.parent._authorizer.authorize( ctx, self._identity, need) still_need = [] for i, _ in enumerate(need): if i < len(oks) and oks[i]: authed[need_index[i]] = True else: still_need.append(ops[need_index[i]]) if len(still_need) == 0 and len(caveats) == 0: # No more ops need to be authenticated and # no caveats to be discharged. return authed, used if self._identity is None and len(self._identity_caveats) > 0: raise DischargeRequiredError( msg='authentication required', ops=[LOGIN_OP], cavs=self._identity_caveats) if caveats is None or len(caveats) == 0: all_errors = [] all_errors.extend(self._init_errors) all_errors.extend(errors) err = '' if len(all_errors) > 0: err = all_errors[0] raise PermissionDenied(err) raise DischargeRequiredError( msg='some operations have extra caveats', ops=ops, cavs=caveats) def allow_capability(self, ctx, ops): '''Checks that the user is allowed to perform all the given operations. If not, a discharge error will be raised. If allow_capability succeeds, it returns a list of first party caveat conditions that must be applied to any macaroon granting capability to execute the operations. Those caveat conditions will not include any declarations contained in login macaroons - the caller must be careful not to mint a macaroon associated with the LOGIN_OP operation unless they add the expected declaration caveat too - in general, clients should not create capabilities that grant LOGIN_OP rights. The operations must include at least one non-LOGIN_OP operation. ''' nops = 0 for op in ops: if op != LOGIN_OP: nops += 1 if nops == 0: raise ValueError('no non-login operations required in capability') _, used = self._allow_any(ctx, ops) squasher = _CaveatSquasher() for i, is_used in enumerate(used): if not is_used: continue for cond in self._conditions[i]: squasher.add(cond) return squasher.final() def _check_conditions(self, ctx, op, conds): declared = checkers.infer_declared_from_conditions( conds, self.parent.namespace()) ctx = checkers.context_with_operations(ctx, [op.action]) ctx = checkers.context_with_declared(ctx, declared) for cond in conds: err = self.parent._first_party_caveat_checker.\ check_first_party_caveat(ctx, cond) if err is not None: return None, err return declared, None class AuthInfo(namedtuple('AuthInfo', 'identity macaroons')): '''AuthInfo information about an authorization decision. @param identity: holds information on the authenticated user as returned identity_client. It may be None after a successful authorization if LOGIN_OP access was not required. @param macaroons: holds all the macaroons that were used for the authorization. Macaroons that were invalid or unnecessary are not included. ''' class _CaveatSquasher(object): ''' Rationalizes first party caveats created for a capability by: - including only the earliest time-before caveat. - excluding allow and deny caveats (operations are checked by virtue of the operations associated with the macaroon). - removing declared caveats. - removing duplicates. ''' def __init__(self, expiry=None, conds=None): self._expiry = expiry if conds is None: conds = [] self._conds = conds def add(self, cond): if self._add(cond): self._conds.append(cond) def _add(self, cond): try: cond, args = checkers.parse_caveat(cond) except ValueError: # Be safe - if we can't parse the caveat, just leave it there. return True if cond == checkers.COND_TIME_BEFORE: try: et = pyrfc3339.parse(args, utc=True).replace(tzinfo=None) except ValueError: # Again, if it doesn't seem valid, leave it alone. return True if self._expiry is None or et <= self._expiry: self._expiry = et return False elif cond in [checkers.COND_ALLOW, checkers.COND_DENY, checkers.COND_DECLARED]: return False return True def final(self): if self._expiry is not None: self._conds.append( checkers.time_before_caveat(self._expiry).condition) # Make deterministic and eliminate duplicates. return sorted(set(self._conds)) macaroonbakery-1.3.1/macaroonbakery/bakery/_macaroon.py0000644000175000017500000003627113616470421024676 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import abc import base64 import json import logging import os import macaroonbakery.checkers as checkers import pymacaroons from macaroonbakery._utils import b64decode from pymacaroons.serializers import json_serializer from ._versions import ( LATEST_VERSION, VERSION_0, VERSION_1, VERSION_2, VERSION_3, ) from ._error import ( ThirdPartyInfoNotFound, ) from ._codec import ( encode_uvarint, encode_caveat, ) from ._keys import PublicKey from ._third_party import ( legacy_namespace, ThirdPartyInfo, ) log = logging.getLogger(__name__) class Macaroon(object): '''Represent an undischarged macaroon along with its first party caveat namespace and associated third party caveat information which should be passed to the third party when discharging a caveat. ''' def __init__(self, root_key, id, location=None, version=LATEST_VERSION, namespace=None): '''Creates a new macaroon with the given root key, id and location. If the version is more than the latest known version, the latest known version will be used. The namespace should hold the namespace of the service that is creating the macaroon. @param root_key bytes or string @param id bytes or string @param location bytes or string @param version the bakery version. @param namespace is that of the service creating it ''' if version > LATEST_VERSION: log.info('use last known version:{} instead of: {}'.format( LATEST_VERSION, version )) version = LATEST_VERSION # m holds the underlying macaroon. self._macaroon = pymacaroons.Macaroon( location=location, key=root_key, identifier=id, version=macaroon_version(version)) # version holds the version of the macaroon. self._version = version self._caveat_data = {} if namespace is None: namespace = checkers.Namespace() self._namespace = namespace self._caveat_id_prefix = bytearray() @property def macaroon(self): ''' Return the underlying macaroon. ''' return self._macaroon @property def version(self): return self._version @property def namespace(self): return self._namespace @property def caveat_data(self): return self._caveat_data def add_caveat(self, cav, key=None, loc=None): '''Add a caveat to the macaroon. It encrypts it using the given key pair and by looking up the location using the given locator. As a special case, if the caveat's Location field has the prefix "local " the caveat is added as a client self-discharge caveat using the public key base64-encoded in the rest of the location. In this case, the Condition field must be empty. The resulting third-party caveat will encode the condition "true" encrypted with that public key. @param cav the checkers.Caveat to be added. @param key the public key to encrypt third party caveat. @param loc locator to find information on third parties when adding third party caveats. It is expected to have a third_party_info method that will be called with a location string and should return a ThirdPartyInfo instance holding the requested information. ''' if cav.location is None: self._macaroon.add_first_party_caveat( self.namespace.resolve_caveat(cav).condition) return if key is None: raise ValueError( 'no private key to encrypt third party caveat') local_info = _parse_local_location(cav.location) if local_info is not None: if cav.condition: raise ValueError( 'cannot specify caveat condition in ' 'local third-party caveat') info = local_info cav = checkers.Caveat(location='local', condition='true') else: if loc is None: raise ValueError( 'no locator when adding third party caveat') info = loc.third_party_info(cav.location) root_key = os.urandom(24) # Use the least supported version to encode the caveat. if self._version < info.version: info = ThirdPartyInfo( version=self._version, public_key=info.public_key, ) caveat_info = encode_caveat( cav.condition, root_key, info, key, self._namespace) if info.version < VERSION_3: # We're encoding for an earlier client or third party which does # not understand bundled caveat info, so use the encoded # caveat information as the caveat id. id = caveat_info else: id = self._new_caveat_id(self._caveat_id_prefix) self._caveat_data[id] = caveat_info self._macaroon.add_third_party_caveat(cav.location, root_key, id) def add_caveats(self, cavs, key, loc): '''Add an array of caveats to the macaroon. This method does not mutate the current object. @param cavs arrary of caveats. @param key the PublicKey to encrypt third party caveat. @param loc locator to find the location object that has a method third_party_info. ''' if cavs is None: return for cav in cavs: self.add_caveat(cav, key, loc) def serialize_json(self): '''Return a string holding the macaroon data in JSON format. @return a string holding the macaroon data in JSON format ''' return json.dumps(self.to_dict()) def to_dict(self): '''Return a dict representation of the macaroon data in JSON format. @return a dict ''' if self.version < VERSION_3: if len(self._caveat_data) > 0: raise ValueError('cannot serialize pre-version3 macaroon with ' 'external caveat data') return json.loads(self._macaroon.serialize( json_serializer.JsonSerializer())) serialized = { 'm': json.loads(self._macaroon.serialize( json_serializer.JsonSerializer())), 'v': self._version, } if self._namespace is not None: serialized['ns'] = self._namespace.serialize_text().decode('utf-8') caveat_data = {} for id in self._caveat_data: key = base64.b64encode(id).decode('utf-8') value = base64.b64encode(self._caveat_data[id]).decode('utf-8') caveat_data[key] = value if len(caveat_data) > 0: serialized['cdata'] = caveat_data return serialized @classmethod def from_dict(cls, json_dict): '''Return a macaroon obtained from the given dictionary as deserialized from JSON. @param json_dict The deserialized JSON object. ''' json_macaroon = json_dict.get('m') if json_macaroon is None: # Try the v1 format if we don't have a macaroon field. m = pymacaroons.Macaroon.deserialize( json.dumps(json_dict), json_serializer.JsonSerializer()) macaroon = Macaroon(root_key=None, id=None, namespace=legacy_namespace(), version=_bakery_version(m.version)) macaroon._macaroon = m return macaroon version = json_dict.get('v', None) if version is None: raise ValueError('no version specified') if (version < VERSION_3 or version > LATEST_VERSION): raise ValueError('unknown bakery version {}'.format(version)) m = pymacaroons.Macaroon.deserialize(json.dumps(json_macaroon), json_serializer.JsonSerializer()) if m.version != macaroon_version(version): raise ValueError( 'underlying macaroon has inconsistent version; ' 'got {} want {}'.format(m.version, macaroon_version(version))) namespace = checkers.deserialize_namespace(json_dict.get('ns')) cdata = json_dict.get('cdata', {}) caveat_data = {} for id64 in cdata: id = b64decode(id64) data = b64decode(cdata[id64]) caveat_data[id] = data macaroon = Macaroon(root_key=None, id=None, namespace=namespace, version=version) macaroon._caveat_data = caveat_data macaroon._macaroon = m return macaroon @classmethod def deserialize_json(cls, serialized_json): '''Return a macaroon deserialized from a string @param serialized_json The string to decode {str} @return {Macaroon} ''' serialized = json.loads(serialized_json) return Macaroon.from_dict(serialized) def _new_caveat_id(self, base): '''Return a third party caveat id This does not duplicate any third party caveat ids already inside macaroon. If base is non-empty, it is used as the id prefix. @param base bytes @return bytes ''' id = bytearray() if len(base) > 0: id.extend(base) else: # Add a version byte to the caveat id. Technically # this is unnecessary as the caveat-decoding logic # that looks at versions should never see this id, # but if the caveat payload isn't provided with the # payload, having this version gives a strong indication # that the payload has been omitted so we can produce # a better error for the user. id.append(VERSION_3) # Iterate through integers looking for one that isn't already used, # starting from n so that if everyone is using this same algorithm, # we'll only perform one iteration. i = len(self._caveat_data) caveats = self._macaroon.caveats while True: # We append a varint to the end of the id and assume that # any client that's created the id that we're using as a base # is using similar conventions - in the worst case they might # end up with a duplicate third party caveat id and thus create # a macaroon that cannot be discharged. temp = id[:] encode_uvarint(i, temp) found = False for cav in caveats: if (cav.verification_key_id is not None and cav.caveat_id == temp): found = True break if not found: return bytes(temp) i += 1 def first_party_caveats(self): '''Return the first party caveats from this macaroon. @return the first party caveats from this macaroon as pymacaroons caveats. ''' return self._macaroon.first_party_caveats() def third_party_caveats(self): '''Return the third party caveats. @return the third party caveats as pymacaroons caveats. ''' return self._macaroon.third_party_caveats() def copy(self): ''' Returns a copy of the macaroon. Note that the the new macaroon's namespace still points to the same underlying Namespace - copying the macaroon does not make a copy of the namespace. :return a Macaroon ''' m1 = Macaroon(None, None, version=self._version, namespace=self._namespace) m1._macaroon = self._macaroon.copy() m1._caveat_data = self._caveat_data.copy() return m1 def macaroon_version(bakery_version): '''Return the macaroon version given the bakery version. @param bakery_version the bakery version @return macaroon_version the derived macaroon version ''' if bakery_version in [VERSION_0, VERSION_1]: return pymacaroons.MACAROON_V1 return pymacaroons.MACAROON_V2 class ThirdPartyLocator(object): '''Used to find information on third party discharge services. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def third_party_info(self, loc): '''Return information on the third party at the given location. @param loc string @return: a ThirdPartyInfo @raise: ThirdPartyInfoNotFound ''' raise NotImplementedError('third_party_info method must be defined in ' 'subclass') class ThirdPartyStore(ThirdPartyLocator): ''' Implements a simple in memory ThirdPartyLocator. ''' def __init__(self): self._store = {} def third_party_info(self, loc): info = self._store.get(loc.rstrip('/')) if info is None: raise ThirdPartyInfoNotFound( 'cannot retrieve the info for location {}'.format(loc)) return info def add_info(self, loc, info): '''Associates the given information with the given location. It will ignore any trailing slash. @param loc the location as string @param info (ThirdPartyInfo) to store for this location. ''' self._store[loc.rstrip('/')] = info def _parse_local_location(loc): '''Parse a local caveat location as generated by LocalThirdPartyCaveat. This is of the form: local where is the bakery version of the client that we're adding the local caveat for. It returns None if the location does not represent a local caveat location. @return a ThirdPartyInfo. ''' if not (loc.startswith('local ')): return None v = VERSION_1 fields = loc.split() fields = fields[1:] # Skip 'local' if len(fields) == 2: try: v = int(fields[0]) except ValueError: return None fields = fields[1:] if len(fields) == 1: key = PublicKey.deserialize(fields[0]) return ThirdPartyInfo(public_key=key, version=v) return None def _bakery_version(v): # bakery_version returns a bakery version that corresponds to # the macaroon version v. It is necessarily approximate because # several bakery versions can correspond to a single macaroon # version, so it's only of use when decoding legacy formats # # It will raise a ValueError if it doesn't recognize the version. if v == pymacaroons.MACAROON_V1: # Use version 1 because we don't know of any existing # version 0 clients. return VERSION_1 elif v == pymacaroons.MACAROON_V2: # Note that this could also correspond to Version 3, but # this logic is explicitly for legacy versions. return VERSION_2 else: raise ValueError('unknown macaroon version when deserializing legacy ' 'bakery macaroon; got {}'.format(v)) class MacaroonJSONEncoder(json.JSONEncoder): def encode(self, m): return m.serialize_json() class MacaroonJSONDecoder(json.JSONDecoder): def decode(self, s, _w=json.decoder.WHITESPACE.match): return Macaroon.deserialize_json(s) macaroonbakery-1.3.1/macaroonbakery/bakery/_identity.py0000644000175000017500000001006313466233337024725 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import abc from ._error import IdentityError class Identity(object): ''' Holds identity information declared in a first party caveat added when discharging a third party caveat. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def id(self): ''' Returns the id of the user. May be an opaque blob with no human meaning. An id is only considered to be unique with a given domain. :return string ''' raise NotImplementedError('id method must be defined in subclass') @abc.abstractmethod def domain(self): '''Return the domain of the user. This will be empty if the user was authenticated directly with the identity provider. :return string ''' raise NotImplementedError('domain method must be defined in subclass') class ACLIdentity(Identity): ''' ACLIdentity may be implemented by Identity implementations to report group membership information. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def allow(self, ctx, acls): ''' reports whether the user should be allowed to access any of the users or groups in the given acl list. :param ctx(AuthContext) is the context of the authorization request. :param acls array of string acl :return boolean ''' raise NotImplementedError('allow method must be defined in subclass') class SimpleIdentity(ACLIdentity): ''' A simple form of identity where the user is represented by a string. ''' def __init__(self, user): self._identity = user def domain(self): ''' A simple identity has no domain. ''' return '' def id(self): '''Return the user name as the id. ''' return self._identity def allow(self, ctx, acls): '''Allow access to any ACL members that was equal to the user name. That is, some user u is considered a member of group u and no other. ''' for acl in acls: if self._identity == acl: return True return False class IdentityClient(object): ''' Represents an abstract identity manager. User identities can be based on local informaton (for example HTTP basic auth) or by reference to an external trusted third party (an identity manager). ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def identity_from_context(self, ctx): ''' Returns the identity based on information in the context. If it cannot determine the identity based on the context, then it should return a set of caveats containing a third party caveat that, when discharged, can be used to obtain the identity with declared_identity. It should only raise an error if it cannot check the identity (for example because of a database access error) - it's OK to return all zero values when there's no identity found and no third party to address caveats to. @param ctx an AuthContext :return: an Identity and array of caveats ''' raise NotImplementedError('identity_from_context method must be ' 'defined in subclass') @abc.abstractmethod def declared_identity(self, ctx, declared): '''Parses the identity declaration from the given declared attributes. TODO take the set of first party caveat conditions instead? @param ctx (AuthContext) @param declared (dict of string/string) :return: an Identity ''' raise NotImplementedError('declared_identity method must be ' 'defined in subclass') class NoIdentities(IdentityClient): ''' Defines the null identity provider - it never returns any identities. ''' def identity_from_context(self, ctx): return None, None def declared_identity(self, ctx, declared): raise IdentityError('no identity declared or possible') macaroonbakery-1.3.1/macaroonbakery/bakery/_authorizer.py0000644000175000017500000001001713466233337025267 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import abc from ._identity import ACLIdentity # EVERYONE is recognized by ACLAuthorizer as the name of a # group that has everyone in it. EVERYONE = 'everyone' class Authorizer(object): ''' Used to check whether a given user is allowed to perform a set of operations. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def authorize(self, ctx, id, ops): ''' Checks whether the given identity (which will be None when there is no authenticated user) is allowed to perform the given operations. It should raise an exception only when the authorization cannot be determined, not when the user has been denied access. On success, each element of allowed holds whether the respective element of ops has been allowed, and caveats holds any additional third party caveats that apply. If allowed is shorter then ops, the additional elements are assumed to be False. ctx(AuthContext) is the context of the authorization request. :return: a list of boolean and a list of caveats ''' raise NotImplementedError('authorize method must be defined in ' 'subclass') class AuthorizerFunc(Authorizer): ''' Implements a simplified version of Authorizer that operates on a single operation at a time. ''' def __init__(self, f): ''' :param f: a function that takes an identity that operates on a single operation at a time. Will return if this op is allowed as a boolean and and a list of caveat that holds any additional third party caveats that apply. ''' self._f = f def authorize(self, ctx, identity, ops): '''Implements Authorizer.authorize by calling f with the given identity for each operation. ''' allowed = [] caveats = [] for op in ops: ok, fcaveats = self._f(ctx, identity, op) allowed.append(ok) if fcaveats is not None: caveats.extend(fcaveats) return allowed, caveats class ACLAuthorizer(Authorizer): ''' ACLAuthorizer is an Authorizer implementation that will check access control list (ACL) membership of users. It uses get_acl to find out the ACLs that apply to the requested operations and will authorize an operation if an ACL contains the group "everyone" or if the identity is an instance of ACLIdentity and its allow method returns True for the ACL. ''' def __init__(self, get_acl, allow_public=False): ''' :param get_acl get_acl will be called with an auth context and an Op. It should return the ACL that applies (an array of string ids). If an entity cannot be found or the action is not recognised, get_acl should return an empty list but no error. :param allow_public: boolean, If True and an ACL contains "everyone", then authorization will be granted even if there is no logged in user. ''' self._allow_public = allow_public self._get_acl = get_acl def authorize(self, ctx, identity, ops): '''Implements Authorizer.authorize by calling identity.allow to determine whether the identity is a member of the ACLs associated with the given operations. ''' if len(ops) == 0: # Anyone is allowed to do nothing. return [], [] allowed = [False] * len(ops) has_allow = isinstance(identity, ACLIdentity) for i, op in enumerate(ops): acl = self._get_acl(ctx, op) if has_allow: allowed[i] = identity.allow(ctx, acl) else: allowed[i] = self._allow_public and EVERYONE in acl return allowed, [] class ClosedAuthorizer(Authorizer): ''' An Authorizer implementation that will never authorize anything. ''' def authorize(self, ctx, id, ops): return [False] * len(ops), [] macaroonbakery-1.3.1/macaroonbakery/bakery/_error.py0000644000175000017500000000441713466233337024233 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. class DischargeRequiredError(Exception): ''' Raised by checker when authorization has failed and a discharged macaroon might fix it. A caller should grant the user the ability to authorize by minting a macaroon associated with Ops (see MacaroonStore.MacaroonIdInfo for how the associated operations are retrieved) and adding Caveats. If the user succeeds in discharging the caveats, the authorization will be granted. ''' def __init__(self, msg, ops, cavs): ''' :param msg: holds some reason why the authorization was denied. :param ops: holds all the operations that were not authorized. If ops contains a single LOGIN_OP member, the macaroon should be treated as an login token. Login tokens (also known as authentication macaroons) usually have a longer life span than other macaroons. :param cavs: holds the caveats that must be added to macaroons that authorize the above operations. ''' super(DischargeRequiredError, self).__init__(msg) self._ops = ops self._cavs = cavs def ops(self): return self._ops def cavs(self): return self._cavs class PermissionDenied(Exception): '''Raised from AuthChecker when permission has been denied. ''' pass class CaveatNotRecognizedError(Exception): '''Containing the cause of errors returned from caveat checkers when the caveat was not recognized. ''' pass class VerificationError(Exception): '''Raised to signify that an error is because of a verification failure rather than because verification could not be done.''' pass class AuthInitError(Exception): '''Raised if AuthChecker cannot be initialized properly.''' pass class IdentityError(Exception): ''' Raised from IdentityClient.declared_identity when an error occurs. ''' pass class ThirdPartyCaveatCheckFailed(Exception): ''' Raised from ThirdPartyCaveatChecker.check_third_party when check fails. ''' pass class ThirdPartyInfoNotFound(Exception): ''' Raised from implementation of ThirdPartyLocator.third_party_info when the info cannot be found. ''' pass macaroonbakery-1.3.1/macaroonbakery/bakery/_keys.py0000644000175000017500000000556713466233337024064 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import nacl.public class PrivateKey(object): ''' A private key used by the bakery to encrypt and decrypt third party caveats. Internally, it is a 256-bit Ed25519 private key. ''' def __init__(self, key): self._key = key @property def key(self): ''' Internal nacl key representation. ''' return self._key @property def public_key(self): ''' :return: the PublicKey associated with the private key. ''' return PublicKey(self._key.public_key) @classmethod def deserialize(cls, serialized): ''' Create a PrivateKey from a base64 encoded bytes. :return: a PrivateKey ''' return PrivateKey( nacl.public.PrivateKey(serialized, encoder=nacl.encoding.Base64Encoder)) def serialize(self, raw=False): '''Encode the private part of the key in a base64 format by default, but when raw is True it will return hex encoded bytes. @return: bytes ''' if raw: return self._key.encode() return self._key.encode(nacl.encoding.Base64Encoder) def __str__(self): '''Return the private part of the key key as a base64-encoded string''' return self.serialize().decode('utf-8') def __eq__(self, other): return self.key == other.key class PublicKey(object): ''' A public key used by the bakery to encrypt third party caveats. Every discharger is associated with a public key which is used to encrypt third party caveat ids addressed to that discharger. Internally, it is a 256 bit Ed25519 public key. ''' def __init__(self, key): self._key = key @property def key(self): ''' Internal nacl key representation. ''' return self._key def serialize(self, raw=False): '''Encode the private part of the key in a base64 format by default, but when raw is True it will return hex encoded bytes. @return: bytes ''' if raw: return self._key.encode() return self._key.encode(nacl.encoding.Base64Encoder) def __str__(self): '''Return the key as a base64-encoded string''' return self.serialize().decode('utf-8') @classmethod def deserialize(cls, serialized): ''' Create a PublicKey from a base64 encoded bytes. :return: a PublicKey ''' return PublicKey( nacl.public.PublicKey(serialized, encoder=nacl.encoding.Base64Encoder)) def __eq__(self, other): return self.key == other.key def generate_key(): '''GenerateKey generates a new PrivateKey. :return: a PrivateKey ''' return PrivateKey(nacl.public.PrivateKey.generate()) macaroonbakery-1.3.1/macaroonbakery/bakery/__init__.py0000644000175000017500000000517413466233337024503 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from ._versions import ( VERSION_0, VERSION_1, VERSION_2, VERSION_3, LATEST_VERSION, ) from ._authorizer import ( ACLAuthorizer, Authorizer, AuthorizerFunc, ClosedAuthorizer, EVERYONE, ) from ._codec import ( decode_caveat, encode_caveat, encode_uvarint, ) from ._checker import ( AuthChecker, AuthInfo, Checker, LOGIN_OP, Op, ) from ._error import ( AuthInitError, CaveatNotRecognizedError, DischargeRequiredError, IdentityError, PermissionDenied, ThirdPartyCaveatCheckFailed, ThirdPartyInfoNotFound, VerificationError, ) from ._identity import ( ACLIdentity, Identity, IdentityClient, NoIdentities, SimpleIdentity, ) from ._keys import ( generate_key, PrivateKey, PublicKey, ) from ._store import ( MemoryOpsStore, MemoryKeyStore, ) from ._third_party import ( ThirdPartyCaveatInfo, ThirdPartyInfo, legacy_namespace, ) from ._macaroon import ( Macaroon, MacaroonJSONDecoder, MacaroonJSONEncoder, ThirdPartyLocator, ThirdPartyStore, macaroon_version, ) from ._discharge import ( ThirdPartyCaveatChecker, discharge, discharge_all, local_third_party_caveat, ) from ._oven import ( Oven, canonical_ops, ) from ._bakery import Bakery from macaroonbakery._utils import ( b64decode, macaroon_to_dict, ) __all__ = [ 'ACLAuthorizer', 'ACLIdentity', 'AuthChecker', 'AuthInfo', 'AuthInitError', 'Authorizer', 'AuthorizerFunc', 'VERSION_0', 'VERSION_1', 'VERSION_2', 'VERSION_3', 'Bakery', 'CaveatNotRecognizedError', 'Checker', 'ClosedAuthorizer', 'DischargeRequiredError', 'EVERYONE', 'Identity', 'IdentityClient', 'IdentityError', 'LATEST_VERSION', 'LOGIN_OP', 'Macaroon', 'MacaroonJSONDecoder', 'MacaroonJSONEncoder', 'MemoryKeyStore', 'MemoryOpsStore', 'NoIdentities', 'Op', 'Oven', 'PermissionDenied', 'PrivateKey', 'PublicKey', 'SimpleIdentity', 'ThirdPartyCaveatCheckFailed', 'ThirdPartyCaveatChecker', 'ThirdPartyCaveatInfo', 'ThirdPartyInfo', 'ThirdPartyInfoNotFound', 'ThirdPartyLocator', 'ThirdPartyStore', 'VERSION', 'VerificationError', 'b64decode', 'canonical_ops', 'decode_caveat', 'discharge', 'discharge_all', 'encode_caveat', 'encode_uvarint', 'generate_key', 'legacy_namespace', 'local_third_party_caveat', 'macaroon_to_dict', 'macaroon_version', ] macaroonbakery-1.3.1/macaroonbakery/bakery/_store.py0000644000175000017500000000436613466233337024241 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import abc import os class MemoryOpsStore: ''' A multi-op store that stores the operations in memory. ''' def __init__(self): self._store = {} def put_ops(self, key, time, ops): ''' Put an ops only if not already there, otherwise it's a no op. ''' if self._store.get(key) is None: self._store[key] = ops def get_ops(self, key): ''' Returns ops from the key if found otherwise raises a KeyError. ''' ops = self._store.get(key) if ops is None: raise KeyError( 'cannot get operations for {}'.format(key)) return ops class RootKeyStore(object): ''' Defines a store for macaroon root keys. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def get(self, id): ''' Returns the root key for the given id. If the item is not there, it returns None. @param id: bytes @return: bytes ''' raise NotImplementedError('get method must be defined in ' 'subclass') @abc.abstractmethod def root_key(self): ''' Returns the root key to be used for making a new macaroon, and an id that can be used to look it up later with the get method. Note that the root keys should remain available for as long as the macaroons using them are valid. Note that there is no need for it to return a new root key for every call - keys may be reused, although some key cycling is over time is advisable. @return: bytes ''' class MemoryKeyStore(RootKeyStore): ''' MemoryKeyStore returns an implementation of Store that generates a single key and always returns that from root_key. The same id ("0") is always used. ''' def __init__(self, key=None): ''' If the key is not specified a random key will be generated. @param key: bytes ''' if key is None: key = os.urandom(24) self._key = key def get(self, id): if id != b'0': return None return self._key def root_key(self): return self._key, b'0' macaroonbakery-1.3.1/macaroonbakery/bakery/_bakery.py0000644000175000017500000000620513466233337024354 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from ._authorizer import ClosedAuthorizer from ._checker import Checker import macaroonbakery.checkers as checkers from ._oven import Oven class Bakery(object): '''Convenience class that contains both an Oven and a Checker. ''' def __init__(self, location=None, locator=None, ops_store=None, key=None, identity_client=None, checker=None, root_key_store=None, authorizer=ClosedAuthorizer()): '''Returns a new Bakery instance which combines an Oven with a Checker for the convenience of callers that wish to use both together. @param checker holds the checker used to check first party caveats. If this is None, it will use checkers.Checker(None). @param root_key_store holds the root key store to use. If you need to use a different root key store for different operations, you'll need to pass a root_key_store_for_ops value to Oven directly. @param root_key_store If this is None, it will use MemoryKeyStore(). Note that that is almost certain insufficient for production services that are spread across multiple instances or that need to persist keys across restarts. @param locator is used to find out information on third parties when adding third party caveats. If this is None, no non-local third party caveats can be added. @param key holds the private key of the oven. If this is None, no third party caveats may be added. @param identity_client holds the identity implementation to use for authentication. If this is None, no authentication will be possible. @param authorizer is used to check whether an authenticated user is allowed to perform operations. If it is None, it will use a ClosedAuthorizer. The identity parameter passed to authorizer.allow will always have been obtained from a call to IdentityClient.declared_identity. @param ops_store used to persistently store the association of multi-op entities with their associated operations when oven.macaroon is called with multiple operations. @param location holds the location to use when creating new macaroons. ''' if checker is None: checker = checkers.Checker() root_keystore_for_ops = None if root_key_store is not None: def root_keystore_for_ops(ops): return root_key_store oven = Oven(key=key, location=location, locator=locator, namespace=checker.namespace(), root_keystore_for_ops=root_keystore_for_ops, ops_store=ops_store) self._oven = oven self._checker = Checker(checker=checker, authorizer=authorizer, identity_client=identity_client, macaroon_opstore=oven) @property def oven(self): return self._oven @property def checker(self): return self._checker macaroonbakery-1.3.1/macaroonbakery/checkers/0000755000175000017500000000000013616470550022672 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/checkers/_checkers.py0000644000175000017500000002002213466233337025171 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import abc from collections import namedtuple from datetime import datetime import pyrfc3339 from ._caveat import parse_caveat from ._conditions import ( COND_ALLOW, COND_DECLARED, COND_DENY, COND_ERROR, COND_TIME_BEFORE, STD_NAMESPACE, ) from ._declared import DECLARED_KEY from ._namespace import Namespace from ._operation import OP_KEY from ._time import TIME_KEY from ._utils import condition_with_prefix class RegisterError(Exception): '''Raised when a condition cannot be registered with a Checker.''' pass class FirstPartyCaveatChecker(object): '''Used to check first party caveats for validity with respect to information in the provided context. If the caveat kind was not recognised, the checker should return ErrCaveatNotRecognized. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def check_first_party_caveat(self, ctx, caveat): ''' Checks that the given caveat condition is valid with respect to the given context information. :param ctx: an Auth context :param caveat a string ''' raise NotImplementedError('check_first_party_caveat method must be ' 'defined in subclass') def namespace(self): ''' Returns the namespace associated with the caveat checker. ''' raise NotImplementedError('namespace method must be ' 'defined in subclass') class Checker(FirstPartyCaveatChecker): ''' Holds a set of checkers for first party caveats. ''' def __init__(self, namespace=None, include_std_checkers=True): if namespace is None: namespace = Namespace() self._namespace = namespace self._checkers = {} if include_std_checkers: self.register_std() def check_first_party_caveat(self, ctx, cav): ''' Checks the caveat against all registered caveat conditions. :return: error message string if any or None ''' try: cond, arg = parse_caveat(cav) except ValueError as ex: # If we can't parse it, perhaps it's in some other format, # return a not-recognised error. return 'cannot parse caveat "{}": {}'.format(cav, ex.args[0]) checker = self._checkers.get(cond) if checker is None: return 'caveat "{}" not satisfied: caveat not recognized'.format( cav) err = checker.check(ctx, cond, arg) if err is not None: return 'caveat "{}" not satisfied: {}'.format(cav, err) def namespace(self): ''' Returns the namespace associated with the Checker. ''' return self._namespace def info(self): ''' Returns information on all the registered checkers. Sorted by namespace and then name :returns a list of CheckerInfo ''' return sorted(self._checkers.values(), key=lambda x: (x.ns, x.name)) def register(self, cond, uri, check): ''' Registers the given condition(string) in the given namespace uri (string) to be checked with the given check function. The check function checks a caveat by passing an auth context, a cond parameter(string) that holds the caveat condition including any namespace prefix and an arg parameter(string) that hold any additional caveat argument text. It will return any error as string otherwise None. It will raise a ValueError if the namespace is not registered or if the condition has already been registered. ''' if check is None: raise RegisterError( 'no check function registered for namespace {} when ' 'registering condition {}'.format(uri, cond)) prefix = self._namespace.resolve(uri) if prefix is None: raise RegisterError('no prefix registered for namespace {} when ' 'registering condition {}'.format(uri, cond)) if prefix == '' and cond.find(':') >= 0: raise RegisterError( 'caveat condition {} in namespace {} contains a colon but its' ' prefix is empty'.format(cond, uri)) full_cond = condition_with_prefix(prefix, cond) info = self._checkers.get(full_cond) if info is not None: raise RegisterError( 'checker for {} (namespace {}) already registered in ' 'namespace {}'.format(full_cond, uri, info.ns)) self._checkers[full_cond] = CheckerInfo( check=check, ns=uri, name=cond, prefix=prefix) def register_std(self): ''' Registers all the standard checkers in the given checker. If not present already, the standard checkers schema (STD_NAMESPACE) is added to the checker's namespace with an empty prefix. ''' self._namespace.register(STD_NAMESPACE, '') for cond in _ALL_CHECKERS: self.register(cond, STD_NAMESPACE, _ALL_CHECKERS[cond]) class CheckerInfo(namedtuple('CheckInfo', 'prefix name ns check')): '''CheckerInfo holds information on a registered checker. ''' __slots__ = () def __new__(cls, prefix, name, ns, check=None): ''' :param check holds the actual checker function which takes an auth context and a condition and arg string as arguments. :param prefix holds the prefix for the checker condition as string. :param name holds the name of the checker condition as string. :param ns holds the namespace URI for the checker's schema as Namespace. ''' return super(CheckerInfo, cls).__new__(cls, prefix, name, ns, check) def _check_time_before(ctx, cond, arg): clock = ctx.get(TIME_KEY) if clock is None: now = datetime.utcnow() else: now = clock.utcnow() try: # Note: pyrfc3339 returns a datetime with a timezone, which # we need to remove before we can compare it with the naive # datetime object returned by datetime.utcnow. expiry = pyrfc3339.parse(arg, utc=True).replace(tzinfo=None) if now >= expiry: return 'macaroon has expired' except ValueError: return 'cannot parse "{}" as RFC 3339'.format(arg) return None def _check_declared(ctx, cond, arg): parts = arg.split(' ', 1) if len(parts) != 2: return 'declared caveat has no value' attrs = ctx.get(DECLARED_KEY, {}) val = attrs.get(parts[0]) if val is None: return 'got {}=null, expected "{}"'.format(parts[0], parts[1]) if val != parts[1]: return 'got {}="{}", expected "{}"'.format(parts[0], val, parts[1]) return None def _check_error(ctx, cond, arg): return 'bad caveat' def _check_allow(ctx, cond, arg): return _check_operations(ctx, True, arg) def _check_deny(ctx, cond, arg): return _check_operations(ctx, False, arg) def _check_operations(ctx, need_ops, arg): ''' Checks an allow or a deny caveat. The need_ops parameter specifies whether we require all the operations in the caveat to be declared in the context. ''' ctx_ops = ctx.get(OP_KEY, []) if len(ctx_ops) == 0: if need_ops: f = arg.split() if len(f) == 0: return 'no operations allowed' return '{} not allowed'.format(f[0]) return None fields = arg.split() for op in ctx_ops: err = _check_op(op, need_ops, fields) if err is not None: return err return None def _check_op(ctx_op, need_op, fields): found = False for op in fields: if op == ctx_op: found = True break if found != need_op: return '{} not allowed'.format(ctx_op) return None _ALL_CHECKERS = { COND_TIME_BEFORE: _check_time_before, COND_DECLARED: _check_declared, COND_ERROR: _check_error, COND_ALLOW: _check_allow, COND_DENY: _check_deny, } macaroonbakery-1.3.1/macaroonbakery/checkers/_conditions.py0000644000175000017500000000100313466233337025551 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. # StdNamespace holds the URI of the standard checkers schema. STD_NAMESPACE = 'std' # Constants for all the standard caveat conditions. # First and third party caveat conditions are both defined here, # even though notionally they exist in separate name spaces. COND_DECLARED = 'declared' COND_TIME_BEFORE = 'time-before' COND_ERROR = 'error' COND_ALLOW = 'allow' COND_DENY = 'deny' COND_NEED_DECLARED = 'need-declared' macaroonbakery-1.3.1/macaroonbakery/checkers/_utils.py0000644000175000017500000000057113466233337024551 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. def condition_with_prefix(prefix, condition): '''Returns the given string prefixed by the given prefix. If the prefix is non-empty, a colon is used to separate them. ''' if prefix == '' or prefix is None: return condition return prefix + ':' + condition macaroonbakery-1.3.1/macaroonbakery/checkers/_namespace.py0000644000175000017500000001252513466233337025347 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import collections from ._caveat import error_caveat from ._utils import condition_with_prefix class Namespace: '''Holds maps from schema URIs to prefixes. prefixes that are used to encode them in first party caveats. Several different URIs may map to the same prefix - this is usual when several different backwardly compatible schema versions are registered. ''' def __init__(self, uri_to_prefix=None): self._uri_to_prefix = {} if uri_to_prefix is not None: for k in uri_to_prefix: self.register(k, uri_to_prefix[k]) def __str__(self): '''Returns the namespace representation as returned by serialize :return: str ''' return self.serialize_text().decode('utf-8') def __eq__(self, other): return self._uri_to_prefix == other._uri_to_prefix def serialize_text(self): '''Returns a serialized form of the Namepace. All the elements in the namespace are sorted by URI, joined to the associated prefix with a colon and separated with spaces. :return: bytes ''' if self._uri_to_prefix is None or len(self._uri_to_prefix) == 0: return b'' od = collections.OrderedDict(sorted(self._uri_to_prefix.items())) data = [] for uri in od: data.append(uri + ':' + od[uri]) return ' '.join(data).encode('utf-8') def register(self, uri, prefix): '''Registers the given URI and associates it with the given prefix. If the URI has already been registered, this is a no-op. :param uri: string :param prefix: string ''' if not is_valid_schema_uri(uri): raise KeyError( 'cannot register invalid URI {} (prefix {})'.format( uri, prefix)) if not is_valid_prefix(prefix): raise ValueError( 'cannot register invalid prefix %q for URI %q'.format( prefix, uri)) if self._uri_to_prefix.get(uri) is None: self._uri_to_prefix[uri] = prefix def resolve(self, uri): ''' Returns the prefix associated to the uri. returns None if not found. :param uri: string :return: string ''' return self._uri_to_prefix.get(uri) def resolve_caveat(self, cav): ''' Resolves the given caveat(string) by using resolve to map from its schema namespace to the appropriate prefix. If there is no registered prefix for the namespace, it returns an error caveat. If cav.namespace is empty or cav.location is non-empty, it returns cav unchanged. It does not mutate ns and may be called concurrently with other non-mutating Namespace methods. :return: Caveat object ''' # TODO: If a namespace isn't registered, try to resolve it by # resolving it to the latest compatible version that is # registered. if cav.namespace == '' or cav.location != '': return cav prefix = self.resolve(cav.namespace) if prefix is None: err_cav = error_caveat( 'caveat {} in unregistered namespace {}'.format( cav.condition, cav.namespace)) if err_cav.namespace != cav.namespace: prefix = self.resolve(err_cav.namespace) if prefix is None: prefix = '' cav = err_cav if prefix != '': cav.condition = condition_with_prefix(prefix, cav.condition) cav.namespace = '' return cav def is_valid_schema_uri(uri): '''Reports if uri is suitable for use as a namespace schema URI. It must be non-empty and it must not contain white space. :param uri string :return bool ''' if len(uri) <= 0: return False return uri.find(' ') == -1 def is_valid_prefix(prefix): '''Reports if prefix is valid. It must not contain white space or semi-colon. :param prefix string :return bool ''' return prefix.find(' ') == -1 and prefix.find(':') == -1 def deserialize_namespace(data): ''' Deserialize a Namespace object. :param data: bytes or str :return: namespace ''' if isinstance(data, bytes): data = data.decode('utf-8') kvs = data.split() uri_to_prefix = {} for kv in kvs: i = kv.rfind(':') if i == -1: raise ValueError('no colon in namespace ' 'field {}'.format(repr(kv))) uri, prefix = kv[0:i], kv[i + 1:] if not is_valid_schema_uri(uri): # Currently this can't happen because the only invalid URIs # are those which contain a space raise ValueError( 'invalid URI {} in namespace ' 'field {}'.format(repr(uri), repr(kv))) if not is_valid_prefix(prefix): raise ValueError( 'invalid prefix {} in namespace field' ' {}'.format(repr(prefix), repr(kv))) if uri in uri_to_prefix: raise ValueError( 'duplicate URI {} in ' 'namespace {}'.format(repr(uri), repr(data))) uri_to_prefix[uri] = prefix return Namespace(uri_to_prefix) macaroonbakery-1.3.1/macaroonbakery/checkers/_auth_context.py0000644000175000017500000000333313613544502026106 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. try: from collections.abc import Mapping except ImportError: from collections import Mapping class AuthContext(Mapping): ''' Holds a set of keys and values relevant to authorization. It is passed as an argument to authorization checkers, so that the checkers can access information about the context of the authorization request. It is immutable - values can only be added by copying the whole thing. ''' def __init__(self, somedict=None): if somedict is None: somedict = {} self._dict = dict(somedict) self._hash = None def with_value(self, key, val): ''' Return a copy of the AuthContext object with the given key and value added. ''' new_dict = dict(self._dict) new_dict[key] = val return AuthContext(new_dict) def __getitem__(self, key): return self._dict[key] def __len__(self): return len(self._dict) def __iter__(self): return iter(self._dict) def __hash__(self): if self._hash is None: self._hash = hash(frozenset(self._dict.items())) return self._hash def __eq__(self, other): return self._dict == other._dict class ContextKey(object): '''Provides a unique key suitable for use as a key into AuthContext.''' def __init__(self, name): '''Creates a context key using the given name. The name is only for informational purposes. ''' self._name = name def __str__(self): return '%s#%#x' % (self._name, id(self)) def __repr__(self): return 'context_key(%r, %#x)' % (self._name, id(self)) macaroonbakery-1.3.1/macaroonbakery/checkers/_caveat.py0000644000175000017500000001016213466233337024651 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import collections import pyrfc3339 from ._conditions import ( COND_ALLOW, COND_DECLARED, COND_DENY, COND_ERROR, COND_TIME_BEFORE, STD_NAMESPACE, ) class Caveat(collections.namedtuple('Caveat', 'condition location namespace')): '''Represents a condition that must be true for a check to complete successfully. If location is provided, the caveat must be discharged by a third party at the given location (a URL string). The namespace parameter holds the namespace URI string of the condition - if it is provided, it will be converted to a namespace prefix before adding to the macaroon. ''' __slots__ = () def __new__(cls, condition, location=None, namespace=None): return super(Caveat, cls).__new__(cls, condition, location, namespace) def declared_caveat(key, value): '''Returns a "declared" caveat asserting that the given key is set to the given value. If a macaroon has exactly one first party caveat asserting the value of a particular key, then infer_declared will be able to infer the value, and then the check will allow the declared value if it has the value specified here. If the key is empty or contains a space, it will return an error caveat. ''' if key.find(' ') >= 0 or key == '': return error_caveat('invalid caveat \'declared\' key "{}"'.format(key)) return _first_party(COND_DECLARED, key + ' ' + value) def error_caveat(f): '''Returns a caveat that will never be satisfied, holding f as the text of the caveat. This should only be used for highly unusual conditions that are never expected to happen in practice, such as a malformed key that is conventionally passed as a constant. It's not a panic but you should only use it in cases where a panic might possibly be appropriate. This mechanism means that caveats can be created without error checking and a later systematic check at a higher level (in the bakery package) can produce an error instead. ''' return _first_party(COND_ERROR, f) def allow_caveat(ops): ''' Returns a caveat that will deny attempts to use the macaroon to perform any operation other than those listed. Operations must not contain a space. ''' if ops is None or len(ops) == 0: return error_caveat('no operations allowed') return _operation_caveat(COND_ALLOW, ops) def deny_caveat(ops): '''Returns a caveat that will deny attempts to use the macaroon to perform any of the listed operations. Operations must not contain a space. ''' return _operation_caveat(COND_DENY, ops) def _operation_caveat(cond, ops): ''' Helper for allow_caveat and deny_caveat. It checks that all operation names are valid before creating the caveat. ''' for op in ops: if op.find(' ') != -1: return error_caveat('invalid operation name "{}"'.format(op)) return _first_party(cond, ' '.join(ops)) def time_before_caveat(t): '''Return a caveat that specifies that the time that it is checked at should be before t. :param t is a a UTC date in - use datetime.utcnow, not datetime.now ''' return _first_party(COND_TIME_BEFORE, pyrfc3339.generate(t, accept_naive=True, microseconds=True)) def parse_caveat(cav): ''' Parses a caveat into an identifier, identifying the checker that should be used, and the argument to the checker (the rest of the string). The identifier is taken from all the characters before the first space character. :return two string, identifier and arg ''' if cav == '': raise ValueError('empty caveat') try: i = cav.index(' ') except ValueError: return cav, '' if i == 0: raise ValueError('caveat starts with space character') return cav[0:i], cav[i + 1:] def _first_party(name, arg): condition = name if arg != '': condition += ' ' + arg return Caveat(condition=condition, namespace=STD_NAMESPACE) macaroonbakery-1.3.1/macaroonbakery/checkers/_time.py0000644000175000017500000000420313466233337024343 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import pyrfc3339 from ._auth_context import ContextKey from ._caveat import parse_caveat from ._conditions import COND_TIME_BEFORE, STD_NAMESPACE from ._utils import condition_with_prefix TIME_KEY = ContextKey('time-key') def context_with_clock(ctx, clock): ''' Returns a copy of ctx with a key added that associates it with the given clock implementation, which will be used by the time-before checker to determine the current time. The clock should have a utcnow method that returns the current time as a datetime value in UTC. ''' if clock is None: return ctx return ctx.with_value(TIME_KEY, clock) def macaroons_expiry_time(ns, ms): ''' Returns the minimum time of any time-before caveats found in the given macaroons or None if no such caveats were found. :param ns: a Namespace, used to resolve caveats. :param ms: a list of pymacaroons.Macaroon :return: datetime.DateTime or None. ''' t = None for m in ms: et = expiry_time(ns, m.caveats) if et is not None and (t is None or et < t): t = et return t def expiry_time(ns, cavs): ''' Returns the minimum time of any time-before caveats found in the given list or None if no such caveats were found. The ns parameter is :param ns: used to determine the standard namespace prefix - if the standard namespace is not found, the empty prefix is assumed. :param cavs: a list of pymacaroons.Caveat :return: datetime.DateTime or None. ''' prefix = ns.resolve(STD_NAMESPACE) time_before_cond = condition_with_prefix( prefix, COND_TIME_BEFORE) t = None for cav in cavs: if not cav.first_party(): continue cav = cav.caveat_id_bytes.decode('utf-8') name, rest = parse_caveat(cav) if name != time_before_cond: continue try: et = pyrfc3339.parse(rest, utc=True).replace(tzinfo=None) if t is None or et < t: t = et except ValueError: continue return t macaroonbakery-1.3.1/macaroonbakery/checkers/_declared.py0000644000175000017500000000544313466233337025157 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from ._auth_context import ContextKey from ._caveat import Caveat, error_caveat, parse_caveat from ._conditions import ( COND_DECLARED, COND_NEED_DECLARED, STD_NAMESPACE, ) from ._namespace import Namespace DECLARED_KEY = ContextKey('declared-key') def infer_declared(ms, namespace=None): '''Retrieves any declared information from the given macaroons and returns it as a key-value map. Information is declared with a first party caveat as created by declared_caveat. If there are two caveats that declare the same key with different values, the information is omitted from the map. When the caveats are later checked, this will cause the check to fail. namespace is the Namespace used to retrieve the prefix associated to the uri, if None it will use the STD_NAMESPACE only. ''' conditions = [] for m in ms: for cav in m.caveats: if cav.location is None or cav.location == '': conditions.append(cav.caveat_id_bytes.decode('utf-8')) return infer_declared_from_conditions(conditions, namespace) def infer_declared_from_conditions(conds, namespace=None): ''' like infer_declared except that it is passed a set of first party caveat conditions as a list of string rather than a set of macaroons. ''' conflicts = [] # If we can't resolve that standard namespace, then we'll look for # just bare "declared" caveats which will work OK for legacy # macaroons with no namespace. if namespace is None: namespace = Namespace() prefix = namespace.resolve(STD_NAMESPACE) if prefix is None: prefix = '' declared_cond = prefix + COND_DECLARED info = {} for cond in conds: try: name, rest = parse_caveat(cond) except ValueError: name, rest = '', '' if name != declared_cond: continue parts = rest.split(' ', 1) if len(parts) != 2: continue key, val = parts[0], parts[1] old_val = info.get(key) if old_val is not None and old_val != val: conflicts.append(key) continue info[key] = val for key in set(conflicts): del info[key] return info def context_with_declared(ctx, declared): ''' Returns a context with attached declared information, as returned from infer_declared. ''' return ctx.with_value(DECLARED_KEY, declared) def need_declared_caveat(cav, keys): if cav.location == '': return error_caveat('need-declared caveat is not third-party') return Caveat(location=cav.location, condition=(COND_NEED_DECLARED + ' ' + ','.join(keys) + ' ' + cav.condition)) macaroonbakery-1.3.1/macaroonbakery/checkers/__init__.py0000644000175000017500000000310213466233337025002 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from ._conditions import ( STD_NAMESPACE, COND_DECLARED, COND_TIME_BEFORE, COND_ERROR, COND_ALLOW, COND_DENY, COND_NEED_DECLARED, ) from ._caveat import ( allow_caveat, deny_caveat, declared_caveat, parse_caveat, time_before_caveat, Caveat, ) from ._declared import ( context_with_declared, infer_declared, infer_declared_from_conditions, need_declared_caveat, ) from ._operation import ( context_with_operations, ) from ._namespace import ( Namespace, deserialize_namespace ) from ._time import ( context_with_clock, expiry_time, macaroons_expiry_time, ) from ._checkers import ( Checker, CheckerInfo, RegisterError, ) from ._auth_context import ( AuthContext, ContextKey, ) from ._utils import ( condition_with_prefix, ) __all__ = [ 'AuthContext', 'Caveat', 'Checker', 'CheckerInfo', 'COND_ALLOW', 'COND_DECLARED', 'COND_DENY', 'COND_ERROR', 'COND_NEED_DECLARED', 'COND_TIME_BEFORE', 'ContextKey', 'STD_NAMESPACE', 'Namespace', 'RegisterError', 'allow_caveat', 'condition_with_prefix', 'context_with_declared', 'context_with_operations', 'context_with_clock', 'declared_caveat', 'deny_caveat', 'deserialize_namespace', 'expiry_time', 'infer_declared', 'infer_declared_from_conditions', 'macaroons_expiry_time', 'need_declared_caveat', 'parse_caveat', 'time_before_caveat', ] macaroonbakery-1.3.1/macaroonbakery/checkers/_operation.py0000644000175000017500000000111213466233337025401 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from ._auth_context import ContextKey OP_KEY = ContextKey('op-key') def context_with_operations(ctx, ops): ''' Returns a context(AuthContext) which is associated with all the given operations (list of string). It will be based on the auth context passed in as ctx. An allow caveat will succeed only if one of the allowed operations is in ops; a deny caveat will succeed only if none of the denied operations are in ops. ''' return ctx.with_value(OP_KEY, ops) macaroonbakery-1.3.1/macaroonbakery/tests/0000755000175000017500000000000013616470550022245 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/tests/test_checkers.py0000644000175000017500000003644313466233337025462 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from datetime import datetime, timedelta from unittest import TestCase import macaroonbakery.checkers as checkers import six from pymacaroons import MACAROON_V2, Macaroon # A frozen time for the tests. NOW = datetime( year=2006, month=1, day=2, hour=15, minute=4, second=5, microsecond=123) class TestClock(): def utcnow(self): return NOW class TestCheckers(TestCase): def test_checkers(self): tests = [ ('nothing in context, no extra checkers', [ ('something', 'caveat "something" not satisfied: caveat not recognized'), ('', 'cannot parse caveat "": empty caveat'), (' hello', 'cannot parse caveat " hello": caveat starts with' ' space character'), ], None), ('one failed caveat', [ ('t:a aval', None), ('t:b bval', None), ('t:a wrong', 'caveat "t:a wrong" not satisfied: wrong arg'), ], None), ('time from clock', [ (checkers.time_before_caveat( datetime.utcnow() + timedelta(0, 1)).condition, None), (checkers.time_before_caveat(NOW).condition, 'caveat "time-before 2006-01-02T15:04:05.000123Z" ' 'not satisfied: macaroon has expired'), (checkers.time_before_caveat(NOW - timedelta(0, 1)).condition, 'caveat "time-before 2006-01-02T15:04:04.000123Z" ' 'not satisfied: macaroon has expired'), ('time-before bad-date', 'caveat "time-before bad-date" not satisfied: ' 'cannot parse "bad-date" as RFC 3339'), (checkers.time_before_caveat(NOW).condition + " ", 'caveat "time-before 2006-01-02T15:04:05.000123Z " ' 'not satisfied: ' 'cannot parse "2006-01-02T15:04:05.000123Z " as RFC 3339'), ], lambda x: checkers.context_with_clock(ctx, TestClock())), ('real time', [ (checkers.time_before_caveat(datetime( year=2010, month=1, day=1)).condition, 'caveat "time-before 2010-01-01T00:00:00.000000Z" not ' 'satisfied: macaroon has expired'), (checkers.time_before_caveat(datetime( year=3000, month=1, day=1)).condition, None), ], None), ('declared, no entries', [ (checkers.declared_caveat('a', 'aval').condition, 'caveat "declared a aval" not satisfied: got a=null, ' 'expected "aval"'), (checkers.COND_DECLARED, 'caveat "declared" not satisfied: ' 'declared caveat has no value'), ], None), ('declared, some entries', [ (checkers.declared_caveat('a', 'aval').condition, None), (checkers.declared_caveat('b', 'bval').condition, None), (checkers.declared_caveat('spc', ' a b').condition, None), (checkers.declared_caveat('a', 'bval').condition, 'caveat "declared a bval" not satisfied: ' 'got a="aval", expected "bval"'), (checkers.declared_caveat('a', ' aval').condition, 'caveat "declared a aval" not satisfied: ' 'got a="aval", expected " aval"'), (checkers.declared_caveat('spc', 'a b').condition, 'caveat "declared spc a b" not satisfied: ' 'got spc=" a b", expected "a b"'), (checkers.declared_caveat('', 'a b').condition, 'caveat "error invalid caveat \'declared\' key """ ' 'not satisfied: bad caveat'), (checkers.declared_caveat('a b', 'a b').condition, 'caveat "error invalid caveat \'declared\' key "a b"" ' 'not satisfied: bad caveat'), ], lambda x: checkers.context_with_declared(x, { 'a': 'aval', 'b': 'bval', 'spc': ' a b'})), ] checker = checkers.Checker() checker.namespace().register('testns', 't') checker.register('a', 'testns', arg_checker(self, 't:a', 'aval')) checker.register('b', 'testns', arg_checker(self, 't:b', 'bval')) ctx = checkers.AuthContext() for test in tests: print(test[0]) if test[2] is not None: ctx1 = test[2](ctx) else: ctx1 = ctx for check in test[1]: err = checker.check_first_party_caveat(ctx1, check[0]) if check[1] is not None: self.assertEqual(err, check[1]) else: self.assertIsNone(err) def test_infer_declared(self): tests = [ ('no macaroons', [], {}, None), ('single macaroon with one declaration', [ [checkers.Caveat(condition='declared foo bar')] ], {'foo': 'bar'}, None), ('only one argument to declared', [ [checkers.Caveat(condition='declared foo')] ], {}, None), ('spaces in value', [ [checkers.Caveat(condition='declared foo bar bloggs')] ], {'foo': 'bar bloggs'}, None), ('attribute with declared prefix', [ [checkers.Caveat(condition='declaredccf foo')] ], {}, None), ('several macaroons with different declares', [ [ checkers.declared_caveat('a', 'aval'), checkers.declared_caveat('b', 'bval') ], [ checkers.declared_caveat('c', 'cval'), checkers.declared_caveat('d', 'dval') ] ], {'a': 'aval', 'b': 'bval', 'c': 'cval', 'd': 'dval'}, None), ('duplicate values', [ [ checkers.declared_caveat('a', 'aval'), checkers.declared_caveat('a', 'aval'), checkers.declared_caveat('b', 'bval') ], [ checkers.declared_caveat('a', 'aval'), checkers.declared_caveat('b', 'bval'), checkers.declared_caveat('c', 'cval'), checkers.declared_caveat('d', 'dval') ] ], {'a': 'aval', 'b': 'bval', 'c': 'cval', 'd': 'dval'}, None), ('conflicting values', [ [ checkers.declared_caveat('a', 'aval'), checkers.declared_caveat('a', 'conflict'), checkers.declared_caveat('b', 'bval') ], [ checkers.declared_caveat('a', 'conflict'), checkers.declared_caveat('b', 'another conflict'), checkers.declared_caveat('c', 'cval'), checkers.declared_caveat('d', 'dval') ] ], {'c': 'cval', 'd': 'dval'}, None), ('third party caveats ignored', [ [checkers.Caveat(condition='declared a no conflict', location='location')], [checkers.declared_caveat('a', 'aval')] ], {'a': 'aval'}, None), ('unparseable caveats ignored', [ [checkers.Caveat(condition=' bad')], [checkers.declared_caveat('a', 'aval')] ], {'a': 'aval'}, None), ('infer with namespace', [ [ checkers.declared_caveat('a', 'aval'), caveat_with_ns(checkers.declared_caveat('a', 'aval'), 'testns'), ] ], {'a': 'aval'}, None), ] for test in tests: uri_to_prefix = test[3] if uri_to_prefix is None: uri_to_prefix = {checkers.STD_NAMESPACE: ''} ns = checkers.Namespace(uri_to_prefix) print(test[0]) ms = [] for i, caveats in enumerate(test[1]): m = Macaroon(key=None, identifier=six.int2byte(i), location='', version=MACAROON_V2) for cav in caveats: cav = ns.resolve_caveat(cav) if cav.location == '': m.add_first_party_caveat(cav.condition) else: m.add_third_party_caveat(cav.location, None, cav.condition) ms.append(m) self.assertEqual(checkers.infer_declared(ms), test[2]) def test_operations_checker(self): tests = [ ('all allowed', checkers.allow_caveat( ['op1', 'op2', 'op4', 'op3']), ['op1', 'op3', 'op2'], None), ('none denied', checkers.deny_caveat(['op1', 'op2']), ['op3', 'op4'], None), ('one not allowed', checkers.allow_caveat(['op1', 'op2']), ['op1', 'op3'], 'caveat "allow op1 op2" not satisfied: op3 not allowed'), ('one not denied', checkers.deny_caveat(['op1', 'op2']), ['op4', 'op5', 'op2'], 'caveat "deny op1 op2" not satisfied: op2 not allowed'), ('no operations, allow caveat', checkers.allow_caveat(['op1']), [], 'caveat "allow op1" not satisfied: op1 not allowed'), ('no operations, deny caveat', checkers.deny_caveat(['op1']), [], None), ('no operations, empty allow caveat', checkers.Caveat( condition=checkers.COND_ALLOW), [], 'caveat "allow" not satisfied: no operations allowed'), ] checker = checkers.Checker() for test in tests: print(test[0]) ctx = checkers.context_with_operations(checkers.AuthContext(), test[2]) err = checker.check_first_party_caveat(ctx, test[1].condition) if test[3] is None: self.assertIsNone(err) continue self.assertEqual(err, test[3]) def test_operation_error_caveat(self): tests = [ ('empty allow', checkers.allow_caveat(None), 'error no operations allowed'), ('allow: invalid operation name', checkers.allow_caveat(['op1', 'operation number 2']), 'error invalid operation name "operation number 2"'), ('deny: invalid operation name', checkers.deny_caveat(['op1', 'operation number 2']), 'error invalid operation name "operation number 2"') ] for test in tests: print(test[0]) self.assertEqual(test[1].condition, test[2]) def test_register_none_func_raise_exception(self): checker = checkers.Checker() with self.assertRaises(checkers.RegisterError) as ctx: checker.register('x', checkers.STD_NAMESPACE, None) self.assertEqual(ctx.exception.args[0], 'no check function registered for namespace std when ' 'registering condition x') def test_register_no_registered_ns_exception(self): checker = checkers.Checker() with self.assertRaises(checkers.RegisterError) as ctx: checker.register('x', 'testns', lambda x: None) self.assertEqual(ctx.exception.args[0], 'no prefix registered for namespace testns when ' 'registering condition x') def test_register_empty_prefix_condition_with_colon(self): checker = checkers.Checker() checker.namespace().register('testns', '') with self.assertRaises(checkers.RegisterError) as ctx: checker.register('x:y', 'testns', lambda x: None) self.assertEqual(ctx.exception.args[0], 'caveat condition x:y in namespace testns contains a ' 'colon but its prefix is empty') def test_register_twice_same_namespace(self): checker = checkers.Checker() checker.namespace().register('testns', '') checker.register('x', 'testns', lambda x: None) with self.assertRaises(checkers.RegisterError) as ctx: checker.register('x', 'testns', lambda x: None) self.assertEqual(ctx.exception.args[0], 'checker for x (namespace testns) already registered' ' in namespace testns') def test_register_twice_different_namespace(self): checker = checkers.Checker() checker.namespace().register('testns', '') checker.namespace().register('otherns', '') checker.register('x', 'testns', lambda x: None) with self.assertRaises(checkers.RegisterError) as ctx: checker.register('x', 'otherns', lambda x: None) self.assertEqual(ctx.exception.args[0], 'checker for x (namespace otherns) already registered' ' in namespace testns') def test_checker_info(self): checker = checkers.Checker(include_std_checkers=False) checker.namespace().register('one', 't') checker.namespace().register('two', 't') checker.namespace().register('three', '') checker.namespace().register('four', 's') class Called(object): val = '' def register(name, ns): def func(ctx, cond, arg): Called.val = name + ' ' + ns return None checker.register(name, ns, func) register('x', 'one') register('y', 'one') register('z', 'two') register('a', 'two') register('something', 'three') register('other', 'three') register('xxx', 'four') expect = [ checkers.CheckerInfo(ns='four', name='xxx', prefix='s'), checkers.CheckerInfo(ns='one', name='x', prefix='t'), checkers.CheckerInfo(ns='one', name='y', prefix='t'), checkers.CheckerInfo(ns='three', name='other', prefix=''), checkers.CheckerInfo(ns='three', name='something', prefix=''), checkers.CheckerInfo(ns='two', name='a', prefix='t'), checkers.CheckerInfo(ns='two', name='z', prefix='t'), ] infos = checker.info() self.assertEqual(len(infos), len(expect)) new_infos = [] for i, info in enumerate(infos): Called.val = '' info.check(None, '', '') self.assertEqual(Called.val, expect[i].name + ' ' + expect[i].ns) new_infos.append(checkers.CheckerInfo(ns=info.ns, name=info.name, prefix=info.prefix)) self.assertEqual(new_infos, expect) def caveat_with_ns(cav, ns): return checkers.Caveat(location=cav.location, condition=cav.condition, namespace=ns) def arg_checker(test, expect_cond, check_arg): ''' Returns a checker function that checks that the caveat condition is check_arg. ''' def func(ctx, cond, arg): test.assertEqual(cond, expect_cond) if arg != check_arg: return 'wrong arg' return None return func macaroonbakery-1.3.1/macaroonbakery/tests/test_codec.py0000644000175000017500000001710213466233337024737 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 from unittest import TestCase import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers import nacl.public import six from macaroonbakery.bakery import _codec as codec class TestCodec(TestCase): def setUp(self): self.fp_key = bakery.generate_key() self.tp_key = bakery.generate_key() def test_v1_round_trip(self): tp_info = bakery.ThirdPartyInfo( version=bakery.VERSION_1, public_key=self.tp_key.public_key) cid = bakery.encode_caveat( 'is-authenticated-user', b'a random string', tp_info, self.fp_key, None) res = bakery.decode_caveat(self.tp_key, cid) self.assertEquals(res, bakery.ThirdPartyCaveatInfo( first_party_public_key=self.fp_key.public_key, root_key=b'a random string', condition='is-authenticated-user', caveat=cid, third_party_key_pair=self.tp_key, version=bakery.VERSION_1, id=None, namespace=bakery.legacy_namespace() )) def test_v2_round_trip(self): tp_info = bakery.ThirdPartyInfo( version=bakery.VERSION_2, public_key=self.tp_key.public_key) cid = bakery.encode_caveat( 'is-authenticated-user', b'a random string', tp_info, self.fp_key, None) res = bakery.decode_caveat(self.tp_key, cid) self.assertEquals(res, bakery.ThirdPartyCaveatInfo( first_party_public_key=self.fp_key.public_key, root_key=b'a random string', condition='is-authenticated-user', caveat=cid, third_party_key_pair=self.tp_key, version=bakery.VERSION_2, id=None, namespace=bakery.legacy_namespace() )) def test_v3_round_trip(self): tp_info = bakery.ThirdPartyInfo( version=bakery.VERSION_3, public_key=self.tp_key.public_key) ns = checkers.Namespace() ns.register('testns', 'x') cid = bakery.encode_caveat( 'is-authenticated-user', b'a random string', tp_info, self.fp_key, ns) res = bakery.decode_caveat(self.tp_key, cid) self.assertEquals(res, bakery.ThirdPartyCaveatInfo( first_party_public_key=self.fp_key.public_key, root_key=b'a random string', condition='is-authenticated-user', caveat=cid, third_party_key_pair=self.tp_key, version=bakery.VERSION_3, id=None, namespace=ns )) def test_empty_caveat_id(self): with self.assertRaises(bakery.VerificationError) as context: bakery.decode_caveat(self.tp_key, b'') self.assertTrue('empty third party caveat' in str(context.exception)) def test_decode_caveat_v1_from_go(self): tp_key = bakery.PrivateKey( nacl.public.PrivateKey(base64.b64decode( 'TSpvLpQkRj+T3JXnsW2n43n5zP/0X4zn0RvDiWC3IJ0='))) fp_key = bakery.PrivateKey( nacl.public.PrivateKey(base64.b64decode( 'KXpsoJ9ujZYi/O2Cca6kaWh65MSawzy79LWkrjOfzcs='))) root_key = base64.b64decode('vDxEmWZEkgiNEFlJ+8ruXe3qDSLf1H+o') # This caveat has been generated from the go code # to check the compatibilty encrypted_cav = six.b( 'eyJUaGlyZFBhcnR5UHVibGljS2V5IjoiOFA3R1ZZc3BlWlN4c' '3hFdmJsSVFFSTFqdTBTSWl0WlIrRFdhWE40cmxocz0iLCJGaX' 'JzdFBhcnR5UHVibGljS2V5IjoiSDlqSFJqSUxidXppa1VKd2o' '5VGtDWk9qeW5oVmtTdHVsaUFRT2d6Y0NoZz0iLCJOb25jZSI6' 'Ii9lWTRTTWR6TGFxbDlsRFc3bHUyZTZuSzJnVG9veVl0IiwiS' 'WQiOiJra0ZuOGJEaEt4RUxtUjd0NkJxTU0vdHhMMFVqaEZjR1' 'BORldUUExGdjVla1dWUjA4Uk1sbGJhc3c4VGdFbkhzM0laeVo' '0V2lEOHhRUWdjU3ljOHY4eUt4dEhxejVEczJOYmh1ZDJhUFdt' 'UTVMcVlNWitmZ2FNaTAxdE9DIn0=') cav = bakery.decode_caveat(tp_key, encrypted_cav) self.assertEquals(cav, bakery.ThirdPartyCaveatInfo( condition='caveat condition', first_party_public_key=fp_key.public_key, third_party_key_pair=tp_key, root_key=root_key, caveat=encrypted_cav, version=bakery.VERSION_1, id=None, namespace=bakery.legacy_namespace() )) def test_decode_caveat_v2_from_go(self): tp_key = bakery.PrivateKey(nacl.public.PrivateKey( base64.b64decode( 'TSpvLpQkRj+T3JXnsW2n43n5zP/0X4zn0RvDiWC3IJ0='))) fp_key = bakery.PrivateKey( nacl.public.PrivateKey(base64.b64decode( 'KXpsoJ9ujZYi/O2Cca6kaWh65MSawzy79LWkrjOfzcs='))) root_key = base64.b64decode('wh0HSM65wWHOIxoGjgJJOFvQKn2jJFhC') # This caveat has been generated from the go code # to check the compatibilty encrypted_cav = bakery.b64decode( 'AvD-xlUf2MdGMgtu7OKRQnCP1OQJk6PKeFWRK26WIBA6DNwKGIHq9xGcHS9IZ' 'Lh0cL6D9qpeKI0mXmCPfnwRQDuVYC8y5gVWd-oCGZaj5TGtk3byp2Vnw6ojmt' 'sULDhY59YA_J_Y0ATkERO5T9ajoRWBxU2OXBoX6bImXA', ) cav = bakery.decode_caveat(tp_key, encrypted_cav) self.assertEqual(cav, bakery.ThirdPartyCaveatInfo( condition='third party condition', first_party_public_key=fp_key.public_key, third_party_key_pair=tp_key, root_key=root_key, caveat=encrypted_cav, version=bakery.VERSION_2, id=None, namespace=bakery.legacy_namespace() )) def test_decode_caveat_v3_from_go(self): tp_key = bakery.PrivateKey( nacl.public.PrivateKey(base64.b64decode( 'TSpvLpQkRj+T3JXnsW2n43n5zP/0X4zn0RvDiWC3IJ0='))) fp_key = bakery.PrivateKey(nacl.public.PrivateKey( base64.b64decode( 'KXpsoJ9ujZYi/O2Cca6kaWh65MSawzy79LWkrjOfzcs='))) root_key = base64.b64decode(b'oqOXI3/Mz/pKjCuFOt2eYxb7ndLq66GY') # This caveat has been generated from the go code # to check the compatibilty encrypted_cav = bakery.b64decode( 'A_D-xlUf2MdGMgtu7OKRQnCP1OQJk6PKeFWRK26WIBA6DNwKGNLeFSkD2M-8A' 'EYvmgVH95GWu7T7caKxKhhOQFcEKgnXKJvYXxz1zin4cZc4Q6C7gVqA-J4_j3' '1LX4VKxymqG62UGPo78wOv0_fKjr3OI6PPJOYOQgBMclemlRF2', ) cav = bakery.decode_caveat(tp_key, encrypted_cav) self.assertEquals(cav, bakery.ThirdPartyCaveatInfo( condition='third party condition', first_party_public_key=fp_key.public_key, third_party_key_pair=tp_key, root_key=root_key, caveat=encrypted_cav, version=bakery.VERSION_3, id=None, namespace=bakery.legacy_namespace() )) def test_encode_decode_varint(self): tests = [ (12, [12]), (127, [127]), (128, [128, 1]), (129, [129, 1]), (1234567, [135, 173, 75]), (12131231231312, [208, 218, 233, 173, 136, 225, 2]) ] for test in tests: data = bytearray() expected = bytearray() bakery.encode_uvarint(test[0], data) for v in test[1]: expected.append(v) self.assertEquals(data, expected) val = codec.decode_uvarint(bytes(data)) self.assertEquals(test[0], val[0]) self.assertEquals(len(test[1]), val[1]) macaroonbakery-1.3.1/macaroonbakery/tests/test_store.py0000644000175000017500000000104213466233337025012 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from unittest import TestCase import macaroonbakery.bakery as bakery class TestOven(TestCase): def test_mem_store(self): st = bakery.MemoryKeyStore() key, id = st.root_key() self.assertEqual(len(key), 24) self.assertEqual(id.decode('utf-8'), '0') key1, id1 = st.root_key() self.assertEqual(key1, key) self.assertEqual(id1, id) key2 = st.get(id) self.assertEqual(key2, key) macaroonbakery-1.3.1/macaroonbakery/tests/test_keyring.py0000644000175000017500000000671113466233337025336 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import unittest import macaroonbakery.bakery as bakery import macaroonbakery.httpbakery as httpbakery from httmock import HTTMock, urlmatch class TestKeyRing(unittest.TestCase): def test_cache_fetch(self): key = bakery.generate_key() @urlmatch(path='.*/discharge/info') def discharge_info(url, request): return { 'status_code': 200, 'content': { 'Version': bakery.LATEST_VERSION, 'PublicKey': str(key.public_key), } } expectInfo = bakery.ThirdPartyInfo( public_key=key.public_key, version=bakery.LATEST_VERSION ) kr = httpbakery.ThirdPartyLocator(allow_insecure=True) with HTTMock(discharge_info): info = kr.third_party_info('http://0.1.2.3/') self.assertEqual(info, expectInfo) def test_cache_norefetch(self): key = bakery.generate_key() @urlmatch(path='.*/discharge/info') def discharge_info(url, request): return { 'status_code': 200, 'content': { 'Version': bakery.LATEST_VERSION, 'PublicKey': str(key.public_key), } } expectInfo = bakery.ThirdPartyInfo( public_key=key.public_key, version=bakery.LATEST_VERSION ) kr = httpbakery.ThirdPartyLocator(allow_insecure=True) with HTTMock(discharge_info): info = kr.third_party_info('http://0.1.2.3/') self.assertEqual(info, expectInfo) info = kr.third_party_info('http://0.1.2.3/') self.assertEqual(info, expectInfo) def test_cache_fetch_no_version(self): key = bakery.generate_key() @urlmatch(path='.*/discharge/info') def discharge_info(url, request): return { 'status_code': 200, 'content': { 'PublicKey': str(key.public_key), } } expectInfo = bakery.ThirdPartyInfo( public_key=key.public_key, version=bakery.VERSION_1 ) kr = httpbakery.ThirdPartyLocator(allow_insecure=True) with HTTMock(discharge_info): info = kr.third_party_info('http://0.1.2.3/') self.assertEqual(info, expectInfo) def test_allow_insecure(self): kr = httpbakery.ThirdPartyLocator() with self.assertRaises(bakery.ThirdPartyInfoNotFound): kr.third_party_info('http://0.1.2.3/') def test_fallback(self): key = bakery.generate_key() @urlmatch(path='.*/discharge/info') def discharge_info(url, request): return { 'status_code': 404, } @urlmatch(path='.*/publickey') def public_key(url, request): return { 'status_code': 200, 'content': { 'PublicKey': str(key.public_key), } } expectInfo = bakery.ThirdPartyInfo( public_key=key.public_key, version=bakery.VERSION_1 ) kr = httpbakery.ThirdPartyLocator(allow_insecure=True) with HTTMock(discharge_info): with HTTMock(public_key): info = kr.third_party_info('http://0.1.2.3/') self.assertEqual(info, expectInfo) macaroonbakery-1.3.1/macaroonbakery/tests/test_client.py0000644000175000017500000006240513613544502025137 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 import datetime import json import threading import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers import macaroonbakery.httpbakery as httpbakery import pymacaroons import requests import macaroonbakery._utils as utils from fixtures import ( EnvironmentVariable, TestWithFixtures, ) from httmock import HTTMock, urlmatch from six.moves.urllib.parse import parse_qs from six.moves.urllib.request import Request try: from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler except ImportError: from http.server import HTTPServer, BaseHTTPRequestHandler AGES = datetime.datetime.utcnow() + datetime.timedelta(days=1) TEST_OP = bakery.Op(entity='test', action='test') class TestClient(TestWithFixtures): def setUp(self): super(TestClient, self).setUp() # http_proxy would cause requests to talk to the proxy, which is # unlikely to know how to talk to the test server. self.useFixture(EnvironmentVariable('http_proxy')) self.useFixture(EnvironmentVariable('HTTP_PROXY')) def test_single_service_first_party(self): b = new_bakery('loc', None, None) def handler(*args): GetHandler(b, None, None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() srv_macaroon = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=AGES, caveats=None, ops=[TEST_OP]) self.assertEquals(srv_macaroon.macaroon.location, 'loc') client = httpbakery.Client() client.cookies.set_cookie(requests.cookies.create_cookie( 'macaroon-test', base64.b64encode(json.dumps([ srv_macaroon.to_dict().get('m') ]).encode('utf-8')).decode('utf-8') )) resp = requests.get( url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) resp.raise_for_status() self.assertEquals(resp.text, 'done') finally: httpd.shutdown() def test_single_service_third_party(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): self.assertEqual(url.path, '/discharge') qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) server_url = 'http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): resp = requests.get( url=server_url, cookies=client.cookies, auth=client.auth()) resp.raise_for_status() self.assertEquals(resp.text, 'done') finally: httpd.shutdown() def test_single_service_third_party_with_path(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4/some/path': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): self.assertEqual(url.path, '/some/path/discharge') qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } def handler(*args): GetHandler(b, 'http://1.2.3.4/some/path', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) server_url = 'http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): resp = requests.get( url=server_url, cookies=client.cookies, auth=client.auth()) resp.raise_for_status() self.assertEquals(resp.text, 'done') finally: httpd.shutdown() def test_single_service_third_party_version_1_caveat(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.VERSION_1, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) server_url = 'http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): resp = requests.get( url=server_url, cookies=client.cookies, auth=client.auth()) resp.raise_for_status() self.assertEquals(resp.text, 'done') finally: httpd.shutdown() def test_cookie_domain_host_not_fqdn(self): # See # https://github.com/go-macaroon-bakery/py-macaroon-bakery/issues/53 b = new_bakery('loc', None, None) def handler(*args): GetHandler(b, None, None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() srv_macaroon = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=AGES, caveats=None, ops=[TEST_OP]) self.assertEquals(srv_macaroon.macaroon.location, 'loc') client = httpbakery.Client() # Note: by using "localhost" instead of the presumably numeric address held # in httpd.server_address, we're triggering the no-FQDN logic in the cookie # code. resp = requests.get( url='http://localhost:' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) resp.raise_for_status() self.assertEquals(resp.text, 'done') except httpbakery.BakeryException: pass # interacion required exception is expected finally: httpd.shutdown() # the cookie has the .local domain appended [cookie] = client.cookies self.assertEqual(cookie.name, 'macaroon-test') self.assertEqual(cookie.domain, 'localhost.local') def test_single_party_with_header(self): b = new_bakery('loc', None, None) def handler(*args): GetHandler(b, None, None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() srv_macaroon = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=AGES, caveats=None, ops=[TEST_OP]) self.assertEquals(srv_macaroon.macaroon.location, 'loc') headers = { 'Macaroons': base64.b64encode(json.dumps([ srv_macaroon.to_dict().get('m') ]).encode('utf-8')) } resp = requests.get( url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), headers=headers) resp.raise_for_status() self.assertEquals(resp.text, 'done') finally: httpd.shutdown() def test_expiry_cookie_is_set(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } ages = datetime.datetime.utcnow() + datetime.timedelta(days=1) def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, ages, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): resp = requests.get( url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) resp.raise_for_status() m = bakery.Macaroon.from_dict(json.loads( base64.b64decode(client.cookies.get('macaroon-test')).decode('utf-8'))[0]) t = checkers.macaroons_expiry_time( checkers.Namespace(), [m.macaroon]) self.assertEquals(ages, t) self.assertEquals(resp.text, 'done') finally: httpd.shutdown() def test_expiry_cookie_set_in_past(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } ages = datetime.datetime.utcnow() - datetime.timedelta(days=1) def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, ages, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): with self.assertRaises(httpbakery.BakeryException) as ctx: requests.get( url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) self.assertEqual(ctx.exception.args[0], 'too many (3) discharge requests') finally: httpd.shutdown() def test_too_many_discharge(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): wrong_macaroon = bakery.Macaroon( root_key=b'some key', id=b'xxx', location='some other location', version=bakery.VERSION_0) return { 'status_code': 200, 'content': { 'Macaroon': wrong_macaroon.to_dict() } } def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): with self.assertRaises(httpbakery.BakeryException) as ctx: requests.get( url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) self.assertEqual(ctx.exception.args[0], 'too many (3) discharge requests') finally: httpd.shutdown() def test_third_party_discharge_refused(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) def check(cond, arg): raise bakery.ThirdPartyCaveatCheckFailed('boo! cond' + cond) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} httpbakery.discharge(checkers.AuthContext(), content, d.key, d, ThirdPartyCaveatCheckerF(check)) def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): with self.assertRaises(bakery.ThirdPartyCaveatCheckFailed): requests.get( url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) finally: httpd.shutdown() def test_discharge_with_interaction_required_error(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): return { 'status_code': 401, 'content': { 'Code': httpbakery.ERR_INTERACTION_REQUIRED, 'Message': 'interaction required', 'Info': { 'WaitURL': 'http://0.1.2.3/', 'VisitURL': 'http://0.1.2.3/', }, } } def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() class MyInteractor(httpbakery.LegacyInteractor): def legacy_interact(self, ctx, location, visit_url): raise httpbakery.InteractionError('cannot visit') def interact(self, ctx, location, interaction_required_err): pass def kind(self): return httpbakery.WEB_BROWSER_INTERACTION_KIND client = httpbakery.Client(interaction_methods=[MyInteractor()]) with HTTMock(discharge): with self.assertRaises(httpbakery.InteractionError): requests.get( 'http://' + httpd.server_address[0] + ':' + str( httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) finally: httpd.shutdown() def test_extract_macaroons_from_request(self): def encode_macaroon(m): macaroons = '[' + utils.macaroon_to_json_string(m) + ']' return base64.urlsafe_b64encode(utils.to_bytes(macaroons)).decode('ascii') req = Request('http://example.com') m1 = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2, identifier='one') req.add_header('Macaroons', encode_macaroon(m1)) m2 = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2, identifier='two') jar = requests.cookies.RequestsCookieJar() jar.set_cookie(utils.cookie( name='macaroon-auth', value=encode_macaroon(m2), url='http://example.com', )) jar.set_cookie(utils.cookie( name='macaroon-empty', value='', url='http://example.com', )) jar.add_cookie_header(req) macaroons = httpbakery.extract_macaroons(req) self.assertEquals(len(macaroons), 2) macaroons.sort(key=lambda ms: ms[0].identifier) self.assertEquals(macaroons[0][0].identifier, m1.identifier) self.assertEquals(macaroons[1][0].identifier, m2.identifier) def test_handle_error_cookie_path(self): macaroon = bakery.Macaroon( root_key=b'some key', id=b'xxx', location='some location', version=bakery.VERSION_0) info = { 'Macaroon': macaroon.to_dict(), 'MacaroonPath': '.', 'CookieNameSuffix': 'test' } error = httpbakery.Error( code=407, message='error', version=bakery.LATEST_VERSION, info=httpbakery.ErrorInfo.from_dict(info)) client = httpbakery.Client() client.handle_error(error, 'http://example.com/some/path') [cookie] = client.cookies self.assertEqual(cookie.path, "/some/") class GetHandler(BaseHTTPRequestHandler): '''A mock HTTP server that serves a GET request''' def __init__(self, bakery, auth_location, mutate_error, caveats, version, expiry, *args): ''' @param bakery used to check incoming requests and macaroons for discharge-required errors. @param auth_location holds the location of any 3rd party authorizer. If this is not None, a 3rd party caveat will be added addressed to this location. @param mutate_error if non None, will be called with any discharge-required error before responding to the client. @param caveats called to get caveats to add to the returned macaroon. @param version holds the version of the bakery that the server will purport to serve. @param expiry holds the expiry for the macaroon that will be created in _write_discharge_error ''' self._bakery = bakery self._auth_location = auth_location self._mutate_error = mutate_error self._caveats = caveats self._server_version = version self._expiry = expiry BaseHTTPRequestHandler.__init__(self, *args) def do_GET(self): '''do_GET implements a handler for the HTTP GET method''' ctx = checkers.AuthContext() auth_checker = self._bakery.checker.auth( httpbakery.extract_macaroons(self.headers)) try: auth_checker.allow(ctx, [TEST_OP]) except (bakery.PermissionDenied, bakery.VerificationError) as exc: return self._write_discharge_error(exc) self.send_response(200) self.end_headers() content_len = int(self.headers.get('content-length', 0)) content = 'done' if self.path != '/no-body'and content_len > 0: body = self.rfile.read(content_len) content = content + ' ' + body self.wfile.write(content.encode('utf-8')) return def _write_discharge_error(self, exc): version = httpbakery.request_version(self.headers) if version < bakery.LATEST_VERSION: self._server_version = version caveats = [] if self._auth_location != '': caveats = [ checkers.Caveat(location=self._auth_location, condition='is-ok') ] if self._caveats is not None: caveats.extend(self._caveats) m = self._bakery.oven.macaroon( version=bakery.LATEST_VERSION, expiry=self._expiry, caveats=caveats, ops=[TEST_OP]) content, headers = httpbakery.discharge_required_response( m, '/', 'test', exc.args[0]) self.send_response(401) for h in headers: self.send_header(h, headers[h]) self.send_header('Connection', 'close') self.end_headers() self.wfile.write(content) def new_bakery(location, locator, checker): '''Return a new bakery instance. @param location Location of the bakery {str}. @param locator Locator for third parties {ThirdPartyLocator or None} @param checker Caveat checker {FirstPartyCaveatChecker or None} @return {Bakery} ''' if checker is None: c = checkers.Checker() c.namespace().register('testns', '') c.register('is', 'testns', check_is_something) checker = c key = bakery.generate_key() return bakery.Bakery( location=location, locator=locator, key=key, checker=checker, ) def is_something_caveat(): return checkers.Caveat(condition='is something', namespace='testns') def check_is_something(ctx, cond, arg): if arg != 'something': return '{} doesn\'t match "something"'.format(arg) return None class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker): def __init__(self, check): self._check = check def check_third_party_caveat(self, ctx, info): cond, arg = checkers.parse_caveat(info.condition) return self._check(cond, arg) alwaysOK3rd = ThirdPartyCaveatCheckerF(lambda cond, arg: []) macaroonbakery-1.3.1/macaroonbakery/tests/test_checker.py0000644000175000017500000010737513466233337025302 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 import json from collections import namedtuple from datetime import timedelta from unittest import TestCase import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers import pymacaroons from macaroonbakery.tests.common import epoch, test_checker, test_context from pymacaroons.verifier import FirstPartyCaveatVerifierDelegate, Verifier class TestChecker(TestCase): def setUp(self): self._discharges = [] def test_authorize_with_open_access_and_no_macaroons(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer( {bakery.Op(entity='something', action='read'): {bakery.EVERYONE}}) ts = _Service('myservice', auth, ids, locator) client = _Client(locator) auth_info = client.do(test_context, ts, [ bakery.Op(entity='something', action='read'), ]) self.assertEqual(len(self._discharges), 0) self.assertIsNotNone(auth_info) self.assertIsNone(auth_info.identity) self.assertEqual(len(auth_info.macaroons), 0) def test_authorization_denied(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = bakery.ClosedAuthorizer() ts = _Service('myservice', auth, ids, locator) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') with self.assertRaises(bakery.PermissionDenied): client.do(ctx, ts, [bakery.Op(entity='something', action='read')]) def test_authorize_with_authentication_required(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer( {bakery.Op(entity='something', action='read'): {'bob'}}) ts = _Service('myservice', auth, ids, locator) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') auth_info = client.do(ctx, ts, [bakery.Op(entity='something', action='read')]) self.assertEqual(self._discharges, [_DischargeRecord(location='ids', user='bob')]) self.assertIsNotNone(auth_info) self.assertEqual(auth_info.identity.id(), 'bob') self.assertEqual(len(auth_info.macaroons), 1) def test_authorize_multiple_ops(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer( { bakery.Op(entity='something', action='read'): {'bob'}, bakery.Op(entity='otherthing', action='read'): {'bob'} } ) ts = _Service('myservice', auth, ids, locator) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') client.do(ctx, ts, [ bakery.Op(entity='something', action='read'), bakery.Op(entity='otherthing', action='read') ]) self.assertEqual(self._discharges, [_DischargeRecord(location='ids', user='bob')]) def test_capability(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer( {bakery.Op(entity='something', action='read'): {'bob'}}) ts = _Service('myservice', auth, ids, locator) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') m = client.discharged_capability( ctx, ts, [bakery.Op(entity='something', action='read')]) # Check that we can exercise the capability directly on the service # with no discharging required. auth_info = ts.do(test_context, [m], [ bakery.Op(entity='something', action='read'), ]) self.assertIsNotNone(auth_info) self.assertIsNone(auth_info.identity) self.assertEqual(len(auth_info.macaroons), 1) self.assertEqual(auth_info.macaroons[0][0].identifier_bytes, m[0].identifier_bytes) def test_capability_multiple_entities(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'bob'}, bakery.Op(entity='e2', action='read'): {'bob'}, bakery.Op(entity='e3', action='read'): {'bob'}, }) ts = _Service('myservice', auth, ids, locator) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') m = client.discharged_capability(ctx, ts, [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), bakery.Op(entity='e3', action='read'), ]) self.assertEqual(self._discharges, [_DischargeRecord(location='ids', user='bob')]) # Check that we can exercise the capability directly on the service # with no discharging required. ts.do(test_context, [m], [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), bakery.Op(entity='e3', action='read'), ]) # Check that we can exercise the capability to act on a subset of # the operations. ts.do(test_context, [m], [ bakery.Op(entity='e2', action='read'), bakery.Op(entity='e3', action='read'), ]) ts.do(test_context, [m], [bakery.Op(entity='e3', action='read')]) def test_multiple_capabilities(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'alice'}, bakery.Op(entity='e2', action='read'): {'bob'}, }) ts = _Service('myservice', auth, ids, locator) # Acquire two capabilities as different users and check # that we can combine them together to do both operations # at once. ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice') m1 = _Client(locator).discharged_capability(ctx, ts, [ bakery.Op(entity='e1', action='read'), ]) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') m2 = _Client(locator).discharged_capability(ctx, ts, [bakery.Op( entity='e2', action='read')]) self.assertEqual(self._discharges, [ _DischargeRecord(location='ids', user='alice'), _DischargeRecord(location='ids', user='bob'), ]) auth_info = ts.do(test_context, [m1, m2], [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), ]) self.assertIsNotNone(auth_info) self.assertIsNone(auth_info.identity) self.assertEqual(len(auth_info.macaroons), 2) self.assertEqual(auth_info.macaroons[0][0].identifier_bytes, m1[0].identifier_bytes) self.assertEqual(auth_info.macaroons[1][0].identifier_bytes, m2[0].identifier_bytes) def test_combine_capabilities(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'alice'}, bakery.Op(entity='e2', action='read'): {'bob'}, bakery.Op(entity='e3', action='read'): {'bob', 'alice'}, }) ts = _Service('myservice', auth, ids, locator) # Acquire two capabilities as different users and check # that we can combine them together into a single capability # capable of both operations. ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice') m1 = _Client(locator).discharged_capability(ctx, ts, [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e3', action='read'), ]) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') m2 = _Client(locator).discharged_capability( ctx, ts, [bakery.Op(entity='e2', action='read')]) m = ts.capability(test_context, [m1, m2], [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), bakery.Op(entity='e3', action='read'), ]) ts.do(test_context, [[m.macaroon]], [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), bakery.Op(entity='e3', action='read'), ]) def test_partially_authorized_request(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'alice'}, bakery.Op(entity='e2', action='read'): {'bob'}, }) ts = _Service('myservice', auth, ids, locator) # Acquire a capability for e1 but rely on authentication to # authorize e2. ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice') m = _Client(locator).discharged_capability(ctx, ts, [ bakery.Op(entity='e1', action='read'), ]) client = _Client(locator) client.add_macaroon(ts, 'authz', m) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') client.discharged_capability(ctx, ts, [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), ]) def test_auth_with_third_party_caveats(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) # We make an authorizer that requires a third party discharge # when authorizing. def authorize_with_tp_discharge(ctx, id, op): if (id is not None and id.id() == 'bob' and op == bakery.Op(entity='something', action='read')): return True, [checkers.Caveat(condition='question', location='other third party')] return False, None auth = bakery.AuthorizerFunc(authorize_with_tp_discharge) ts = _Service('myservice', auth, ids, locator) class _LocalDischargeChecker(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(_, ctx, info): if info.condition != 'question': raise ValueError('third party condition not recognized') self._discharges.append(_DischargeRecord( location='other third party', user=ctx.get(_DISCHARGE_USER_KEY) )) return [] locator['other third party'] = _Discharger( key=bakery.generate_key(), checker=_LocalDischargeChecker(), locator=locator, ) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') client.do(ctx, ts, [bakery.Op(entity='something', action='read')]) self.assertEqual(self._discharges, [ _DischargeRecord(location='ids', user='bob'), _DischargeRecord(location='other third party', user='bob') ]) def test_capability_combines_first_party_caveats(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'alice'}, bakery.Op(entity='e2', action='read'): {'bob'}, }) ts = _Service('myservice', auth, ids, locator) # Acquire two capabilities as different users, add some first party # caveats that we can combine them together into a single capability # capable of both operations. ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice') m1 = _Client(locator).capability( ctx, ts, [bakery.Op(entity='e1', action='read')]) m1.macaroon.add_first_party_caveat('true 1') m1.macaroon.add_first_party_caveat('true 2') ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') m2 = _Client(locator).capability( ctx, ts, [bakery.Op(entity='e2', action='read')]) m2.macaroon.add_first_party_caveat('true 3') m2.macaroon.add_first_party_caveat('true 4') client = _Client(locator) client.add_macaroon(ts, 'authz1', [m1.macaroon]) client.add_macaroon(ts, 'authz2', [m2.macaroon]) m = client.capability(test_context, ts, [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), ]) self.assertEqual(_macaroon_conditions(m.macaroon.caveats, False), [ 'true 1', 'true 2', 'true 3', 'true 4', ]) def test_first_party_caveat_squashing(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'alice'}, bakery.Op(entity='e2', action='read'): {'alice'}, }) ts = _Service('myservice', auth, ids, locator) tests = [ ('duplicates removed', [ checkers.Caveat(condition='true 1', namespace='testns'), checkers.Caveat(condition='true 2', namespace='testns'), checkers.Caveat(condition='true 1', namespace='testns'), checkers.Caveat(condition='true 1', namespace='testns'), checkers.Caveat(condition='true 3', namespace='testns'), ], [ checkers.Caveat(condition='true 1', namespace='testns'), checkers.Caveat(condition='true 2', namespace='testns'), checkers.Caveat(condition='true 3', namespace='testns'), ]), ('earliest time before', [ checkers.time_before_caveat(epoch + timedelta(days=1)), checkers.Caveat(condition='true 1', namespace='testns'), checkers.time_before_caveat( epoch + timedelta(days=0, hours=1)), checkers.time_before_caveat(epoch + timedelta( days=0, hours=0, minutes=5)), ], [ checkers.time_before_caveat(epoch + timedelta( days=0, hours=0, minutes=5)), checkers.Caveat(condition='true 1', namespace='testns'), ]), ('operations and declared caveats removed', [ checkers.deny_caveat(['foo']), checkers.allow_caveat(['read', 'write']), checkers.declared_caveat('username', 'bob'), checkers.Caveat(condition='true 1', namespace='testns'), ], [ checkers.Caveat(condition='true 1', namespace='testns'), ]) ] for test in tests: print(test[0]) # Make a first macaroon with all the required first party caveats. ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice') m1 = _Client(locator).capability( ctx, ts, [bakery.Op(entity='e1', action='read')]) m1.add_caveats(test[1], None, None) # Make a second macaroon that's not used to check that it's # caveats are not added. m2 = _Client(locator).capability( ctx, ts, [bakery.Op(entity='e1', action='read')]) m2.add_caveat(checkers.Caveat( condition='true notused', namespace='testns'), None, None) client = _Client(locator) client.add_macaroon(ts, 'authz1', [m1.macaroon]) client.add_macaroon(ts, 'authz2', [m2.macaroon]) m3 = client.capability( test_context, ts, [bakery.Op(entity='e1', action='read')]) self.assertEqual( _macaroon_conditions(m3.macaroon.caveats, False), _resolve_caveats(m3.namespace, test[2])) def test_login_only(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = bakery.ClosedAuthorizer() ts = _Service('myservice', auth, ids, locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') auth_info = _Client(locator).do(ctx, ts, [bakery.LOGIN_OP]) self.assertIsNotNone(auth_info) self.assertEqual(auth_info.identity.id(), 'bob') def test_allow_any(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer( { bakery.Op(entity='e1', action='read'): {'alice'}, bakery.Op(entity='e2', action='read'): {'bob'}, }) ts = _Service('myservice', auth, ids, locator) # Acquire a capability for e1 but rely on authentication to # authorize e2. ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice') m = _Client(locator).discharged_capability(ctx, ts, [ bakery.Op(entity='e1', action='read'), ]) client = _Client(locator) client.add_macaroon(ts, 'authz', m) self._discharges = [] ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') with self.assertRaises(_DischargeRequiredError): client.do_any( ctx, ts, [ bakery.LOGIN_OP, bakery.Op(entity='e1', action='read'), bakery.Op(entity='e1', action='read') ] ) self.assertEqual(len(self._discharges), 0) # Log in as bob. _, err = client.do(ctx, ts, [bakery.LOGIN_OP]) # All the previous actions should now be allowed. auth_info, allowed = client.do_any(ctx, ts, [ bakery.LOGIN_OP, bakery.Op(entity='e1', action='read'), bakery.Op(entity='e1', action='read'), ]) self.assertEqual(auth_info.identity.id(), 'bob') self.assertEqual(len(auth_info.macaroons), 2) self.assertEqual(allowed, [True, True, True]) def test_auth_with_identity_from_context(self): locator = _DischargerLocator() ids = _BasicAuthIdService() auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'sherlock'}, bakery.Op(entity='e2', action='read'): {'bob'}, }) ts = _Service('myservice', auth, ids, locator) # Check that we can perform the ops with basic auth in the # context. ctx = _context_with_basic_auth(test_context, 'sherlock', 'holmes') auth_info = _Client(locator).do( ctx, ts, [bakery.Op(entity='e1', action='read')]) self.assertEqual(auth_info.identity.id(), 'sherlock') self.assertEqual(len(auth_info.macaroons), 0) def test_auth_login_op_with_identity_from_context(self): locator = _DischargerLocator() ids = _BasicAuthIdService() ts = _Service('myservice', bakery.ClosedAuthorizer(), ids, locator) # Check that we can use LoginOp # when auth isn't granted through macaroons. ctx = _context_with_basic_auth(test_context, 'sherlock', 'holmes') auth_info = _Client(locator).do(ctx, ts, [bakery.LOGIN_OP]) self.assertEqual(auth_info.identity.id(), 'sherlock') self.assertEqual(len(auth_info.macaroons), 0) def test_operation_allow_caveat(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'bob'}, bakery.Op(entity='e1', action='write'): {'bob'}, bakery.Op(entity='e2', action='read'): {'bob'}, }) ts = _Service('myservice', auth, ids, locator) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') m = client.capability(ctx, ts, [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e1', action='write'), bakery.Op(entity='e2', action='read'), ]) # Sanity check that we can do a write. ts.do(test_context, [[m.macaroon]], [bakery.Op(entity='e1', action='write')]) m.add_caveat(checkers.allow_caveat(['read']), None, None) # A read operation should work. ts.do(test_context, [[m.macaroon]], [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), ]) # A write operation should fail # even though the original macaroon allowed it. with self.assertRaises(_DischargeRequiredError): ts.do(test_context, [[m.macaroon]], [ bakery.Op(entity='e1', action='write'), ]) def test_operation_deny_caveat(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = _OpAuthorizer({ bakery.Op(entity='e1', action='read'): {'bob'}, bakery.Op(entity='e1', action='write'): {'bob'}, bakery.Op(entity='e2', action='read'): {'bob'}, }) ts = _Service('myservice', auth, ids, locator) client = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') m = client.capability(ctx, ts, [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e1', action='write'), bakery.Op(entity='e2', action='read'), ]) # Sanity check that we can do a write. ts.do(test_context, [[m.macaroon]], [ bakery.Op(entity='e1', action='write')]) m.add_caveat(checkers.deny_caveat(['write']), None, None) # A read operation should work. ts.do(test_context, [[m.macaroon]], [ bakery.Op(entity='e1', action='read'), bakery.Op(entity='e2', action='read'), ]) # A write operation should fail # even though the original macaroon allowed it. with self.assertRaises(_DischargeRequiredError): ts.do(test_context, [[m.macaroon]], [ bakery.Op(entity='e1', action='write')]) def test_duplicate_login_macaroons(self): locator = _DischargerLocator() ids = _IdService('ids', locator, self) auth = bakery.ClosedAuthorizer() ts = _Service('myservice', auth, ids, locator) # Acquire a login macaroon for bob. client1 = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob') auth_info = client1.do(ctx, ts, [bakery.LOGIN_OP]) self.assertEqual(auth_info.identity.id(), 'bob') # Acquire a login macaroon for alice. client2 = _Client(locator) ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice') auth_info = client2.do(ctx, ts, [bakery.LOGIN_OP]) self.assertEqual(auth_info.identity.id(), 'alice') # Combine the two login macaroons into one client. client3 = _Client(locator) client3.add_macaroon(ts, '1.bob', client1._macaroons[ts.name()]['authn']) client3.add_macaroon(ts, '2.alice', client2._macaroons[ts.name()]['authn']) # We should authenticate as bob (because macaroons are presented # ordered by "cookie" name) auth_info = client3.do(test_context, ts, [bakery.LOGIN_OP]) self.assertEqual(auth_info.identity.id(), 'bob') self.assertEqual(len(auth_info.macaroons), 1) # Try them the other way around and we should authenticate as alice. client3 = _Client(locator) client3.add_macaroon(ts, '1.alice', client2._macaroons[ts.name()]['authn']) client3.add_macaroon(ts, '2.bob', client1._macaroons[ts.name()]['authn']) auth_info = client3.do(test_context, ts, [bakery.LOGIN_OP]) self.assertEqual(auth_info.identity.id(), 'alice') self.assertEqual(len(auth_info.macaroons), 1) def test_macaroon_ops_fatal_error(self): # When we get a non-VerificationError error from the # opstore, we don't do any more verification. checker = bakery.Checker( macaroon_opstore=_MacaroonStoreWithError()) m = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2) with self.assertRaises(bakery.AuthInitError): checker.auth([m]).allow(test_context, [bakery.LOGIN_OP]) class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self, dischargers=None): if dischargers is None: dischargers = {} self._dischargers = dischargers def third_party_info(self, loc): d = self._dischargers.get(loc) if d is None: return None return bakery.ThirdPartyInfo( public_key=d._key.public_key, version=bakery.LATEST_VERSION, ) def __setitem__(self, key, item): self._dischargers[key] = item def __getitem__(self, key): return self._dischargers[key] def get(self, key): return self._dischargers.get(key) class _IdService(bakery.IdentityClient, bakery.ThirdPartyCaveatChecker): def __init__(self, location, locator, test_class): self._location = location self._test = test_class key = bakery.generate_key() self._discharger = _Discharger(key=key, checker=self, locator=locator) locator[location] = self._discharger def check_third_party_caveat(self, ctx, info): if info.condition != 'is-authenticated-user': raise bakery.CaveatNotRecognizedError( 'third party condition not recognized') username = ctx.get(_DISCHARGE_USER_KEY, '') if username == '': raise bakery.ThirdPartyCaveatCheckFailed('no current user') self._test._discharges.append( _DischargeRecord(location=self._location, user=username)) return [checkers.declared_caveat('username', username)] def identity_from_context(self, ctx): return None, [checkers.Caveat(location=self._location, condition='is-authenticated-user')] def declared_identity(self, ctx, declared): user = declared.get('username') if user is None: raise bakery.IdentityError('no username declared') return bakery.SimpleIdentity(user) _DISCHARGE_USER_KEY = checkers.ContextKey('user-key') _DischargeRecord = namedtuple('_DISCHARGE_RECORD', ['location', 'user']) class _Discharger(object): ''' utility class that has a discharge function with the same signature of get_discharge for discharge_all. ''' def __init__(self, key, locator, checker): self._key = key self._locator = locator self._checker = checker def discharge(self, ctx, cav, payload): return bakery.discharge( ctx, key=self._key, id=cav.caveat_id, caveat=payload, checker=self._checker, locator=self._locator, ) class _OpAuthorizer(bakery.Authorizer): '''Implements bakery.Authorizer by looking the operation up in the given map. If the username is in the associated list or the list contains "everyone", authorization is granted. ''' def __init__(self, auth=None): if auth is None: auth = {} self._auth = auth def authorize(self, ctx, id, ops): return bakery.ACLAuthorizer( allow_public=True, get_acl=lambda ctx, op: self._auth.get(op, [])).authorize( ctx, id, ops) class _MacaroonStore(object): ''' Stores root keys in memory and puts all operations in the macaroon id. ''' def __init__(self, key, locator): self._root_key_store = bakery.MemoryKeyStore() self._key = key self._locator = locator def new_macaroon(self, caveats, namespace, ops): root_key, id = self._root_key_store.root_key() m_id = {'id': base64.urlsafe_b64encode(id).decode('utf-8'), 'ops': ops} data = json.dumps(m_id) m = bakery.Macaroon( root_key=root_key, id=data, location='', version=bakery.LATEST_VERSION, namespace=namespace) m.add_caveats(caveats, self._key, self._locator) return m def macaroon_ops(self, ms): if len(ms) == 0: raise ValueError('no macaroons provided') m_id = json.loads(ms[0].identifier_bytes.decode('utf-8')) root_key = self._root_key_store.get( base64.urlsafe_b64decode(m_id['id'].encode('utf-8'))) v = Verifier() class NoValidationOnFirstPartyCaveat(FirstPartyCaveatVerifierDelegate): def verify_first_party_caveat(self, verifier, caveat, signature): return True v.first_party_caveat_verifier_delegate = \ NoValidationOnFirstPartyCaveat() ok = v.verify(macaroon=ms[0], key=root_key, discharge_macaroons=ms[1:]) if not ok: raise bakery.VerificationError('invalid signature') conditions = [] for m in ms: cavs = m.first_party_caveats() for cav in cavs: conditions.append(cav.caveat_id_bytes.decode('utf-8')) ops = [] for op in m_id['ops']: ops.append(bakery.Op(entity=op[0], action=op[1])) return ops, conditions class _Service(object): '''Represents a service that requires authorization. Clients can make requests to the service to perform operations and may receive a macaroon to discharge if the authorization process requires it. ''' def __init__(self, name, auth, idm, locator): self._name = name self._store = _MacaroonStore(bakery.generate_key(), locator) self._checker = bakery.Checker( checker=test_checker(), authorizer=auth, identity_client=idm, macaroon_opstore=self._store) def name(self): return self._name def do(self, ctx, ms, ops): try: authInfo = self._checker.auth(ms).allow(ctx, ops) except bakery.DischargeRequiredError as exc: self._discharge_required_error(exc) return authInfo def do_any(self, ctx, ms, ops): # makes a request to the service to perform any of the given # operations. It reports which operations have succeeded. try: authInfo, allowed = self._checker.auth(ms).allow_any(ctx, ops) return authInfo, allowed except bakery.DischargeRequiredError as exc: self._discharge_required_error(exc) def capability(self, ctx, ms, ops): try: conds = self._checker.auth(ms).allow_capability(ctx, ops) except bakery.DischargeRequiredError as exc: self._discharge_required_error(exc) m = self._store.new_macaroon(None, self._checker.namespace(), ops) for cond in conds: m.macaroon.add_first_party_caveat(cond) return m def _discharge_required_error(self, err): m = self._store.new_macaroon(err.cavs(), self._checker.namespace(), err.ops()) name = 'authz' if len(err.ops()) == 1 and err.ops()[0] == bakery.LOGIN_OP: name = 'authn' raise _DischargeRequiredError(name=name, m=m) class _DischargeRequiredError(Exception): def __init__(self, name, m): Exception.__init__(self, 'discharge required') self._name = name self._m = m def m(self): return self._m def name(self): return self._name class _Client(object): max_retries = 3 def __init__(self, dischargers): self._key = bakery.generate_key() self._macaroons = {} self._dischargers = dischargers def do(self, ctx, svc, ops): class _AuthInfo: authInfo = None def svc_do(ms): _AuthInfo.authInfo = svc.do(ctx, ms, ops) self._do_func(ctx, svc, svc_do) return _AuthInfo.authInfo def do_any(self, ctx, svc, ops): return svc.do_any(ctx, self._request_macaroons(svc), ops) def capability(self, ctx, svc, ops): # capability returns a capability macaroon for the given operations. class _M: m = None def svc_capability(ms): _M.m = svc.capability(ctx, ms, ops) return self._do_func(ctx, svc, svc_capability) return _M.m def discharged_capability(self, ctx, svc, ops): m = self.capability(ctx, svc, ops) return self._discharge_all(ctx, m) def _do_func(self, ctx, svc, f): for i in range(0, self.max_retries): try: f(self._request_macaroons(svc)) return except _DischargeRequiredError as exc: ms = self._discharge_all(ctx, exc.m()) self.add_macaroon(svc, exc.name(), ms) raise ValueError('discharge failed too many times') def _clear_macaroons(self, svc): if svc is None: self._macaroons = {} return if svc.name() in self._macaroons: del self._macaroons[svc.name()] def add_macaroon(self, svc, name, m): if svc.name() not in self._macaroons: self._macaroons[svc.name()] = {} self._macaroons[svc.name()][name] = m def _request_macaroons(self, svc): mmap = self._macaroons.get(svc.name(), []) # Put all the macaroons in the slice ordered by key # so that we have deterministic behaviour in the tests. names = [] for name in mmap: names.append(name) names = sorted(names) ms = [None] * len(names) for i, name in enumerate(names): ms[i] = mmap[name] return ms def _discharge_all(self, ctx, m): def get_discharge(cav, payload): d = self._dischargers.get(cav.location) if d is None: raise ValueError('third party discharger ' '{} not found'.format(cav.location)) return d.discharge(ctx, cav, payload) return bakery.discharge_all(m, get_discharge) class _BasicAuthIdService(bakery.IdentityClient): def identity_from_context(self, ctx): user, pwd = _basic_auth_from_context(ctx) if user != 'sherlock' or pwd != 'holmes': return None, None return bakery.SimpleIdentity(user), None def declared_identity(self, ctx, declared): raise bakery.IdentityError('no identity declarations in basic auth' ' id service') _BASIC_AUTH_KEY = checkers.ContextKey('user-key') class _BasicAuth(object): def __init__(self, user, password): self.user = user self.password = password def _context_with_basic_auth(ctx, user, password): return ctx.with_value(_BASIC_AUTH_KEY, _BasicAuth(user, password)) def _basic_auth_from_context(ctx): auth = ctx.get(_BASIC_AUTH_KEY, _BasicAuth('', '')) return auth.user, auth.password def _macaroon_conditions(caveats, allow_third): conds = [''] * len(caveats) for i, cav in enumerate(caveats): if cav.location is not None and cav.location != '': if not allow_third: raise ValueError('found unexpected third party caveat:' ' {}'.format(cav.location)) continue conds[i] = cav.caveat_id.decode('utf-8') return conds def _resolve_caveats(ns, caveats): conds = [''] * len(caveats) for i, cav in enumerate(caveats): if cav.location is not None and cav.location != '': raise ValueError('found unexpected third party caveat') conds[i] = ns.resolve_caveat(cav).condition return conds class _MacaroonStoreWithError(object): def new_macaroon(self, caveats, ns, ops): raise ValueError('some error') def macaroon_ops(self, ms): raise ValueError('some error') macaroonbakery-1.3.1/macaroonbakery/tests/test_agent.py0000644000175000017500000003652413466233337024771 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import json import logging import os import tempfile from datetime import datetime, timedelta from unittest import TestCase import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers import macaroonbakery.httpbakery as httpbakery import macaroonbakery.httpbakery.agent as agent import requests.cookies from httmock import HTTMock, response, urlmatch from six.moves.urllib.parse import parse_qs, urlparse log = logging.getLogger(__name__) PRIVATE_KEY = 'CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=' PUBLIC_KEY = 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=' class TestAgents(TestCase): def setUp(self): fd, filename = tempfile.mkstemp() with os.fdopen(fd, 'w') as f: f.write(agent_file) self.agent_filename = filename fd, filename = tempfile.mkstemp() with os.fdopen(fd, 'w') as f: f.write(bad_key_agent_file) self.bad_key_agent_filename = filename fd, filename = tempfile.mkstemp() with os.fdopen(fd, 'w') as f: f.write(no_username_agent_file) self.no_username_agent_filename = filename def tearDown(self): os.remove(self.agent_filename) os.remove(self.bad_key_agent_filename) os.remove(self.no_username_agent_filename) def test_load_auth_info(self): auth_info = agent.load_auth_info(self.agent_filename) self.assertEqual(str(auth_info.key), PRIVATE_KEY) self.assertEqual(str(auth_info.key.public_key), PUBLIC_KEY) self.assertEqual(auth_info.agents, [ agent.Agent(url='https://1.example.com/', username='user-1'), agent.Agent(url='https://2.example.com/discharger', username='user-2'), agent.Agent(url='http://0.3.2.1', username='test-user'), ]) def test_invalid_agent_json(self): with self.assertRaises(agent.AgentFileFormatError): agent.read_auth_info('}') def test_invalid_read_auth_info_arg(self): with self.assertRaises(agent.AgentFileFormatError): agent.read_auth_info(0) def test_load_auth_info_with_bad_key(self): with self.assertRaises(agent.AgentFileFormatError): agent.load_auth_info(self.bad_key_agent_filename) def test_load_auth_info_with_no_username(self): with self.assertRaises(agent.AgentFileFormatError): agent.load_auth_info(self.no_username_agent_filename) def test_agent_login(self): discharge_key = bakery.generate_key() class _DischargerLocator(bakery.ThirdPartyLocator): def third_party_info(self, loc): if loc == 'http://0.3.2.1': return bakery.ThirdPartyInfo( public_key=discharge_key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() server_key = bakery.generate_key() server_bakery = bakery.Bakery(key=server_key, locator=d) @urlmatch(path='.*/here') def server_get(url, request): ctx = checkers.AuthContext() test_ops = [bakery.Op(entity='test-op', action='read')] auth_checker = server_bakery.checker.auth( httpbakery.extract_macaroons(request.headers)) try: auth_checker.allow(ctx, test_ops) resp = response(status_code=200, content='done') except bakery.PermissionDenied: caveats = [ checkers.Caveat(location='http://0.3.2.1', condition='is-ok') ] m = server_bakery.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=caveats, ops=test_ops) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response(status_code=401, content=content, headers=headers) return request.hooks['response'][0](resp) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) if qs.get('token64') is None: return response( status_code=401, content={ 'Code': httpbakery.ERR_INTERACTION_REQUIRED, 'Message': 'interaction required', 'Info': { 'InteractionMethods': { 'agent': {'login-url': '/login'}, }, }, }, headers={'Content-Type': 'application/json'}) else: qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, discharge_key, None, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } auth_info = agent.load_auth_info(self.agent_filename) @urlmatch(path='.*/login') def login(url, request): qs = parse_qs(urlparse(request.url).query) self.assertEqual(request.method, 'GET') self.assertEqual( qs, {'username': ['test-user'], 'public-key': [PUBLIC_KEY]}) b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[bakery.local_third_party_caveat( PUBLIC_KEY, version=httpbakery.request_version(request.headers))], ops=[bakery.Op(entity='agent', action='login')]) return { 'status_code': 200, 'content': { 'macaroon': m.to_dict() } } with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(login): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor(auth_info), ]) resp = requests.get( 'http://0.1.2.3/here', cookies=client.cookies, auth=client.auth()) self.assertEqual(resp.content, b'done') def test_agent_legacy(self): discharge_key = bakery.generate_key() class _DischargerLocator(bakery.ThirdPartyLocator): def third_party_info(self, loc): if loc == 'http://0.3.2.1': return bakery.ThirdPartyInfo( public_key=discharge_key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() server_key = bakery.generate_key() server_bakery = bakery.Bakery(key=server_key, locator=d) @urlmatch(path='.*/here') def server_get(url, request): ctx = checkers.AuthContext() test_ops = [bakery.Op(entity='test-op', action='read')] auth_checker = server_bakery.checker.auth( httpbakery.extract_macaroons(request.headers)) try: auth_checker.allow(ctx, test_ops) resp = response(status_code=200, content='done') except bakery.PermissionDenied: caveats = [ checkers.Caveat(location='http://0.3.2.1', condition='is-ok') ] m = server_bakery.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=caveats, ops=test_ops) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response( status_code=401, content=content, headers=headers, ) return request.hooks['response'][0](resp) class InfoStorage: info = None @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) if qs.get('caveat64') is not None: content = {q: qs[q][0] for q in qs} class InteractionRequiredError(Exception): def __init__(self, error): self.error = error class CheckerInError(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): InfoStorage.info = info raise InteractionRequiredError( httpbakery.Error( code=httpbakery.ERR_INTERACTION_REQUIRED, version=httpbakery.request_version( request.headers), message='interaction required', info=httpbakery.ErrorInfo( wait_url='http://0.3.2.1/wait?' 'dischargeid=1', visit_url='http://0.3.2.1/visit?' 'dischargeid=1' ), ), ) try: httpbakery.discharge( checkers.AuthContext(), content, discharge_key, None, CheckerInError()) except InteractionRequiredError as exc: return response( status_code=401, content={ 'Code': exc.error.code, 'Message': exc.error.message, 'Info': { 'WaitURL': exc.error.info.wait_url, 'VisitURL': exc.error.info.visit_url, }, }, headers={'Content-Type': 'application/json'}) key = bakery.generate_key() @urlmatch(path='.*/visit') def visit(url, request): if request.headers.get('Accept') == 'application/json': return { 'status_code': 200, 'content': { 'agent': '/agent-visit', } } raise Exception('unexpected call to visit without Accept header') @urlmatch(path='.*/agent-visit') def agent_visit(url, request): if request.method != "POST": raise Exception('unexpected method') log.info('agent_visit url {}'.format(url)) body = json.loads(request.body.decode('utf-8')) if body['username'] != 'test-user': raise Exception('unexpected username in body {!r}'.format(request.body)) public_key = bakery.PublicKey.deserialize(body['public_key']) ms = httpbakery.extract_macaroons(request.headers) if len(ms) == 0: b = bakery.Bakery(key=discharge_key) m = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=datetime.utcnow() + timedelta(days=1), caveats=[bakery.local_third_party_caveat( public_key, version=httpbakery.request_version(request.headers))], ops=[bakery.Op(entity='agent', action='login')]) content, headers = httpbakery.discharge_required_response( m, '/', 'test', 'message') resp = response(status_code=401, content=content, headers=headers) return request.hooks['response'][0](resp) return { 'status_code': 200, 'content': { 'agent_login': True } } @urlmatch(path='.*/wait$') def wait(url, request): class EmptyChecker(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): return [] if InfoStorage.info is None: self.fail('visit url has not been visited') m = bakery.discharge( checkers.AuthContext(), InfoStorage.info.id, InfoStorage.info.caveat, discharge_key, EmptyChecker(), _DischargerLocator(), ) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } with HTTMock(server_get), \ HTTMock(discharge), \ HTTMock(visit), \ HTTMock(wait), \ HTTMock(agent_visit): client = httpbakery.Client(interaction_methods=[ agent.AgentInteractor( agent.AuthInfo( key=key, agents=[agent.Agent(username='test-user', url=u'http://0.3.2.1')], ), ), ]) resp = requests.get( 'http://0.1.2.3/here', cookies=client.cookies, auth=client.auth(), ) self.assertEqual(resp.content, b'done') agent_file = ''' { "key": { "public": "YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=", "private": "CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=" }, "agents": [{ "url": "https://1.example.com/", "username": "user-1" }, { "url": "https://2.example.com/discharger", "username": "user-2" }, { "url": "http://0.3.2.1", "username": "test-user" }] } ''' bad_key_agent_file = ''' { "key": { "public": "YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=", "private": "CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJE==" }, "agents": [{ "url": "https://1.example.com/", "username": "user-1" }, { "url": "https://2.example.com/discharger", "username": "user-2" }] } ''' no_username_agent_file = ''' { "key": { "public": "YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=", "private": "CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=" }, "agents": [{ "url": "https://1.example.com/" }, { "url": "https://2.example.com/discharger", "username": "user-2" }] } ''' class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker): def __init__(self, check): self._check = check def check_third_party_caveat(self, ctx, info): cond, arg = checkers.parse_caveat(info.condition) return self._check(cond, arg) alwaysOK3rd = ThirdPartyCaveatCheckerF(lambda cond, arg: []) macaroonbakery-1.3.1/macaroonbakery/tests/test_discharge.py0000644000175000017500000005210113466233337025611 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import os import unittest import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers from macaroonbakery.tests import common from pymacaroons import MACAROON_V1, Macaroon class TestDischarge(unittest.TestCase): def test_single_service_first_party(self): ''' Creates a single service with a macaroon with one first party caveat. It creates a request with this macaroon and checks that the service can verify this macaroon as valid. ''' oc = common.new_bakery('bakerytest') primary = oc.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) self.assertEqual(primary.macaroon.location, 'bakerytest') primary.add_caveat(checkers.Caveat(condition='str something', namespace='testns'), oc.oven.key, oc.oven.locator) oc.checker.auth([[primary.macaroon]]).allow( common.str_context('something'), [bakery.LOGIN_OP]) def test_macaroon_paper_fig6(self): ''' Implements an example flow as described in the macaroons paper: http://theory.stanford.edu/~ataly/Papers/macaroons.pdf There are three services, ts, fs, bs: ts is a store service which has deligated authority to a forum service fs. The forum service wants to require its users to be logged into to an authentication service bs. The client obtains a macaroon from fs (minted by ts, with a third party caveat addressed to bs). The client obtains a discharge macaroon from bs to satisfy this caveat. The target service verifies the original macaroon it delegated to fs No direct contact between bs and ts is required ''' locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) ts = common.new_bakery('ts-loc', locator) fs = common.new_bakery('fs-loc', locator) # ts creates a macaroon. ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) # ts somehow sends the macaroon to fs which adds a third party caveat # to be discharged by bs. ts_macaroon.add_caveat(checkers.Caveat(location='bs-loc', condition='user==bob'), fs.oven.key, fs.oven.locator) # client asks for a discharge macaroon for each third party caveat def get_discharge(cav, payload): self.assertEqual(cav.location, 'bs-loc') return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, bs.oven.key, common.ThirdPartyStrcmpChecker('user==bob'), bs.oven.locator, ) d = bakery.discharge_all(ts_macaroon, get_discharge) ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) def test_discharge_with_version1_macaroon(self): locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) ts = common.new_bakery('ts-loc', locator) # ts creates a old-version macaroon. ts_macaroon = ts.oven.macaroon(bakery.VERSION_1, common.ages, None, [bakery.LOGIN_OP]) ts_macaroon.add_caveat(checkers.Caveat(condition='something', location='bs-loc'), ts.oven.key, ts.oven.locator) # client asks for a discharge macaroon for each third party caveat def get_discharge(cav, payload): # Make sure that the caveat id really is old-style. try: cav.caveat_id_bytes.decode('utf-8') except UnicodeDecodeError: self.fail('caveat id is not utf-8') return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, bs.oven.key, common.ThirdPartyStrcmpChecker('something'), bs.oven.locator, ) d = bakery.discharge_all(ts_macaroon, get_discharge) ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) for m in d: self.assertEqual(m.version, MACAROON_V1) def test_version1_macaroon_id(self): # In the version 1 bakery, macaroon ids were hex-encoded with a # hyphenated UUID suffix. root_key_store = bakery.MemoryKeyStore() b = bakery.Bakery( root_key_store=root_key_store, identity_client=common.OneIdentity(), ) key, id = root_key_store.root_key() root_key_store.get(id) m = Macaroon(key=key, version=MACAROON_V1, location='', identifier=id + b'-deadl00f') b.checker.auth([[m]]).allow(common.test_context, [bakery.LOGIN_OP]) def test_macaroon_paper_fig6_fails_without_discharges(self): ''' Runs a similar test as test_macaroon_paper_fig6 without the client discharging the third party caveats. ''' locator = bakery.ThirdPartyStore() ts = common.new_bakery('ts-loc', locator) fs = common.new_bakery('fs-loc', locator) common.new_bakery('as-loc', locator) # ts creates a macaroon. ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) # ts somehow sends the macaroon to fs which adds a third party # caveat to be discharged by as. ts_macaroon.add_caveat(checkers.Caveat(location='as-loc', condition='user==bob'), fs.oven.key, fs.oven.locator) # client makes request to ts try: ts.checker.auth([[ts_macaroon.macaroon]]).allow( common.test_context, bakery.LOGIN_OP ) self.fail('macaroon unmet should be raised') except bakery.PermissionDenied: pass def test_macaroon_paper_fig6_fails_with_binding_on_tampered_sig(self): ''' Runs a similar test as test_macaroon_paper_fig6 with the discharge macaroon binding being done on a tampered signature. ''' locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) ts = common.new_bakery('ts-loc', locator) # ts creates a macaroon. ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) # ts somehow sends the macaroon to fs which adds a third party caveat # to be discharged by as. ts_macaroon.add_caveat(checkers.Caveat(condition='user==bob', location='bs-loc'), ts.oven.key, ts.oven.locator) # client asks for a discharge macaroon for each third party caveat def get_discharge(cav, payload): self.assertEqual(cav.location, 'bs-loc') return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, bs.oven.key, common.ThirdPartyStrcmpChecker('user==bob'), bs.oven.locator, ) d = bakery.discharge_all(ts_macaroon, get_discharge) # client has all the discharge macaroons. For each discharge macaroon # bind it to our ts_macaroon and add it to our request. tampered_macaroon = Macaroon() for i, dm in enumerate(d[1:]): d[i + 1] = tampered_macaroon.prepare_for_request(dm) # client makes request to ts. with self.assertRaises(bakery.PermissionDenied) as exc: ts.checker.auth([d]).allow(common.test_context, bakery.LOGIN_OP) self.assertEqual('verification failed: Signatures do not match', exc.exception.args[0]) def test_need_declared(self): locator = bakery.ThirdPartyStore() first_party = common.new_bakery('first', locator) third_party = common.new_bakery('third', locator) # firstParty mints a macaroon with a third-party caveat addressed # to thirdParty with a need-declared caveat. m = first_party.oven.macaroon( bakery.LATEST_VERSION, common.ages, [ checkers.need_declared_caveat( checkers.Caveat(location='third', condition='something'), ['foo', 'bar'] ) ], [bakery.LOGIN_OP]) # The client asks for a discharge macaroon for each third party caveat. def get_discharge(cav, payload): return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, third_party.oven.key, common.ThirdPartyStrcmpChecker('something'), third_party.oven.locator, ) d = bakery.discharge_all(m, get_discharge) # The required declared attributes should have been added # to the discharge macaroons. declared = checkers.infer_declared(d, first_party.checker.namespace()) self.assertEqual(declared, { 'foo': '', 'bar': '', }) # Make sure the macaroons actually check out correctly # when provided with the declared checker. ctx = checkers.context_with_declared(common.test_context, declared) first_party.checker.auth([d]).allow(ctx, [bakery.LOGIN_OP]) # Try again when the third party does add a required declaration. # The client asks for a discharge macaroon for each third party caveat. def get_discharge(cav, payload): checker = common.ThirdPartyCheckerWithCaveats([ checkers.declared_caveat('foo', 'a'), checkers.declared_caveat('arble', 'b') ]) return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, third_party.oven.key, checker, third_party.oven.locator, ) d = bakery.discharge_all(m, get_discharge) # One attribute should have been added, the other was already there. declared = checkers.infer_declared(d, first_party.checker.namespace()) self.assertEqual(declared, { 'foo': 'a', 'bar': '', 'arble': 'b', }) ctx = checkers.context_with_declared(common.test_context, declared) first_party.checker.auth([d]).allow(ctx, [bakery.LOGIN_OP]) # Try again, but this time pretend a client is sneakily trying # to add another 'declared' attribute to alter the declarations. def get_discharge(cav, payload): checker = common.ThirdPartyCheckerWithCaveats([ checkers.declared_caveat('foo', 'a'), checkers.declared_caveat('arble', 'b'), ]) # Sneaky client adds a first party caveat. m = bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, third_party.oven.key, checker, third_party.oven.locator, ) m.add_caveat(checkers.declared_caveat('foo', 'c'), None, None) return m d = bakery.discharge_all(m, get_discharge) declared = checkers.infer_declared(d, first_party.checker.namespace()) self.assertEqual(declared, { 'bar': '', 'arble': 'b', }) with self.assertRaises(bakery.PermissionDenied) as exc: first_party.checker.auth([d]).allow(common.test_context, bakery.LOGIN_OP) self.assertEqual('cannot authorize login macaroon: caveat ' '"declared foo a" not satisfied: got foo=null, ' 'expected "a"', exc.exception.args[0]) def test_discharge_two_need_declared(self): locator = bakery.ThirdPartyStore() first_party = common.new_bakery('first', locator) third_party = common.new_bakery('third', locator) # first_party mints a macaroon with two third party caveats # with overlapping attributes. m = first_party.oven.macaroon( bakery.LATEST_VERSION, common.ages, [ checkers.need_declared_caveat( checkers.Caveat(location='third', condition='x'), ['foo', 'bar']), checkers.need_declared_caveat( checkers.Caveat(location='third', condition='y'), ['bar', 'baz']), ], [bakery.LOGIN_OP]) # The client asks for a discharge macaroon for each third party caveat. # Since no declarations are added by the discharger, def get_discharge(cav, payload): return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, third_party.oven.key, common.ThirdPartyCaveatCheckerEmpty(), third_party.oven.locator, ) d = bakery.discharge_all(m, get_discharge) declared = checkers.infer_declared(d, first_party.checker.namespace()) self.assertEqual(declared, { 'foo': '', 'bar': '', 'baz': '', }) ctx = checkers.context_with_declared(common.test_context, declared) first_party.checker.auth([d]).allow(ctx, [bakery.LOGIN_OP]) # If they return conflicting values, the discharge fails. # The client asks for a discharge macaroon for each third party caveat. # Since no declarations are added by the discharger, class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, cav_info): if cav_info.condition == 'x': return [checkers.declared_caveat('foo', 'fooval1')] if cav_info.condition == 'y': return [ checkers.declared_caveat('foo', 'fooval2'), checkers.declared_caveat('baz', 'bazval') ] raise bakery.ThirdPartyCaveatCheckFailed('not matched') def get_discharge(cav, payload): return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, third_party.oven.key, ThirdPartyCaveatCheckerF(), third_party.oven.locator, ) d = bakery.discharge_all(m, get_discharge) declared = checkers.infer_declared(d, first_party.checker.namespace()) self.assertEqual(declared, { 'bar': '', 'baz': 'bazval', }) with self.assertRaises(bakery.PermissionDenied) as exc: first_party.checker.auth([d]).allow(common.test_context, bakery.LOGIN_OP) self.assertEqual('cannot authorize login macaroon: caveat "declared ' 'foo fooval1" not satisfied: got foo=null, expected ' '"fooval1"', exc.exception.args[0]) def test_discharge_macaroon_cannot_be_used_as_normal_macaroon(self): locator = bakery.ThirdPartyStore() first_party = common.new_bakery('first', locator) third_party = common.new_bakery('third', locator) # First party mints a macaroon with a 3rd party caveat. m = first_party.oven.macaroon(bakery.LATEST_VERSION, common.ages, [ checkers.Caveat(location='third', condition='true')], [bakery.LOGIN_OP]) # Acquire the discharge macaroon, but don't bind it to the original. class M: unbound = None def get_discharge(cav, payload): m = bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, third_party.oven.key, common.ThirdPartyStrcmpChecker('true'), third_party.oven.locator, ) M.unbound = m.macaroon.copy() return m bakery.discharge_all(m, get_discharge) self.assertIsNotNone(M.unbound) # Make sure it cannot be used as a normal macaroon in the third party. with self.assertRaises(bakery.PermissionDenied) as exc: third_party.checker.auth([[M.unbound]]).allow( common.test_context, [bakery.LOGIN_OP]) self.assertEqual('no operations found in macaroon', exc.exception.args[0]) def test_third_party_discharge_macaroon_ids_are_small(self): locator = bakery.ThirdPartyStore() bakeries = { 'ts-loc': common.new_bakery('ts-loc', locator), 'as1-loc': common.new_bakery('as1-loc', locator), 'as2-loc': common.new_bakery('as2-loc', locator), } ts = bakeries['ts-loc'] ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) ts_macaroon.add_caveat(checkers.Caveat(condition='something', location='as1-loc'), ts.oven.key, ts.oven.locator) class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker): def __init__(self, loc): self._loc = loc def check_third_party_caveat(self, ctx, info): if self._loc == 'as1-loc': return [checkers.Caveat(condition='something', location='as2-loc')] if self._loc == 'as2-loc': return [] raise bakery.ThirdPartyCaveatCheckFailed( 'unknown location {}'.format(self._loc)) def get_discharge(cav, payload): oven = bakeries[cav.location].oven return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, oven.key, ThirdPartyCaveatCheckerF(cav.location), oven.locator, ) d = bakery.discharge_all(ts_macaroon, get_discharge) ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) for i, m in enumerate(d): for j, cav in enumerate(m.caveats): if (cav.verification_key_id is not None and len(cav.caveat_id) > 3): self.fail('caveat id on caveat {} of macaroon {} ' 'is too big ({})'.format(j, i, cav.id)) def test_third_party_discharge_macaroon_wrong_root_key_and_third_party_caveat(self): root_keys = bakery.MemoryKeyStore() ts = bakery.Bakery( key=bakery.generate_key(), checker=common.test_checker(), root_key_store=root_keys, identity_client=common.OneIdentity(), ) locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) # ts creates a macaroon with a third party caveat addressed to bs. ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, common.ages, None, [bakery.LOGIN_OP]) ts_macaroon.add_caveat( checkers.Caveat(location='bs-loc', condition='true'), ts.oven.key, locator, ) def get_discharge(cav, payload): return bakery.discharge( common.test_context, cav.caveat_id_bytes, payload, bs.oven.key, common.ThirdPartyStrcmpChecker('true'), bs.oven.locator, ) d = bakery.discharge_all(ts_macaroon, get_discharge) # The authorization should succeed at first. ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) # Corrupt the root key and try again. # We should get a DischargeRequiredError because the verification has failed. root_keys._key = os.urandom(24) with self.assertRaises(bakery.PermissionDenied) as err: ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) self.assertEqual(str(err.exception), 'verification failed: Decryption failed. Ciphertext failed verification') macaroonbakery-1.3.1/macaroonbakery/tests/test_time.py0000644000175000017500000001123613466233337024622 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from collections import namedtuple from datetime import timedelta from unittest import TestCase import macaroonbakery.checkers as checkers import pymacaroons import pyrfc3339 from pymacaroons import Macaroon t1 = pyrfc3339.parse('2017-10-26T16:19:47.441402074Z', produce_naive=True) t2 = t1 + timedelta(hours=1) t3 = t2 + timedelta(hours=1) def fpcaveat(s): return pymacaroons.Caveat(caveat_id=s.encode('utf-8')) class TestExpireTime(TestCase): def test_expire_time(self): ExpireTest = namedtuple('ExpireTest', 'about caveats expectTime') tests = [ ExpireTest( about='no caveats', caveats=[], expectTime=None, ), ExpireTest( about='single time-before caveat', caveats=[ fpcaveat(checkers.time_before_caveat(t1).condition), ], expectTime=t1, ), ExpireTest( about='multiple time-before caveat', caveats=[ fpcaveat(checkers.time_before_caveat(t2).condition), fpcaveat(checkers.time_before_caveat(t1).condition), ], expectTime=t1, ), ExpireTest( about='mixed caveats', caveats=[ fpcaveat(checkers.time_before_caveat(t1).condition), fpcaveat('allow bar'), fpcaveat(checkers.time_before_caveat(t2).condition), fpcaveat('deny foo'), ], expectTime=t1, ), ExpireTest( about='mixed caveats', caveats=[ fpcaveat(checkers.COND_TIME_BEFORE + ' tomorrow'), ], expectTime=None, ), ] for test in tests: print('test ', test.about) t = checkers.expiry_time(checkers.Namespace(), test.caveats) self.assertEqual(t, test.expectTime) def test_macaroons_expire_time(self): ExpireTest = namedtuple('ExpireTest', 'about macaroons expectTime') tests = [ ExpireTest( about='no macaroons', macaroons=[newMacaroon()], expectTime=None, ), ExpireTest( about='single macaroon without caveats', macaroons=[newMacaroon()], expectTime=None, ), ExpireTest( about='multiple macaroon without caveats', macaroons=[newMacaroon()], expectTime=None, ), ExpireTest( about='single macaroon with time-before caveat', macaroons=[ newMacaroon([checkers.time_before_caveat(t1).condition]), ], expectTime=t1, ), ExpireTest( about='single macaroon with multiple time-before caveats', macaroons=[ newMacaroon([ checkers.time_before_caveat(t2).condition, checkers.time_before_caveat(t1).condition, ]), ], expectTime=t1, ), ExpireTest( about='multiple macaroons with multiple time-before caveats', macaroons=[ newMacaroon([ checkers.time_before_caveat(t3).condition, checkers.time_before_caveat(t1).condition, ]), newMacaroon([ checkers.time_before_caveat(t3).condition, checkers.time_before_caveat(t1).condition, ]), ], expectTime=t1, ), ] for test in tests: print('test ', test.about) t = checkers.macaroons_expiry_time(checkers.Namespace(), test.macaroons) self.assertEqual(t, test.expectTime) def test_macaroons_expire_time_skips_third_party(self): m1 = newMacaroon([checkers.time_before_caveat(t1).condition]) m2 = newMacaroon() m2.add_third_party_caveat('https://example.com', 'a-key', '123') t = checkers.macaroons_expiry_time(checkers.Namespace(), [m1, m2]) self.assertEqual(t1, t) def newMacaroon(conds=[]): m = Macaroon(key='key', version=2) for cond in conds: m.add_first_party_caveat(cond) return m macaroonbakery-1.3.1/macaroonbakery/tests/test_httpbakery.py0000644000175000017500000000241413466233337026037 0ustar frankbanfrankban00000000000000from unittest import TestCase import macaroonbakery.httpbakery as httpbakery import macaroonbakery.bakery as bakery class TestWebBrowserInteractionInfo(TestCase): def test_from_dict(self): info_dict = { 'VisitURL': 'https://example.com/visit', 'WaitTokenURL': 'https://example.com/wait'} interaction_info = httpbakery.WebBrowserInteractionInfo.from_dict(info_dict) self.assertEqual( interaction_info.visit_url, 'https://example.com/visit') self.assertEqual( interaction_info.wait_token_url, 'https://example.com/wait') class TestError(TestCase): def test_from_dict_upper_case_fields(self): err = httpbakery.Error.from_dict({ 'Message': 'm', 'Code': 'c', }) self.assertEqual(err, httpbakery.Error( code='c', message='m', info=None, version=bakery.LATEST_VERSION, )) def test_from_dict_lower_case_fields(self): err = httpbakery.Error.from_dict({ 'message': 'm', 'code': 'c', }) self.assertEqual(err, httpbakery.Error( code='c', message='m', info=None, version=bakery.LATEST_VERSION, )) macaroonbakery-1.3.1/macaroonbakery/tests/test_namespace.py0000644000175000017500000000357713466233337025631 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from unittest import TestCase import macaroonbakery.checkers as checkers class TestNamespace(TestCase): def test_serialize(self): tests = [ ('empty namespace', None, b''), ('standard namespace', {'std': ''}, b'std:'), ('several elements', { 'std': '', 'http://blah.blah': 'blah', 'one': 'two', 'foo.com/x.v0.1': 'z', }, b'foo.com/x.v0.1:z http://blah.blah:blah one:two std:'), ('sort by URI not by field', { 'a': 'one', 'a1': 'two', }, b'a:one a1:two') ] for test in tests: ns = checkers.Namespace(test[1]) data = ns.serialize_text() self.assertEquals(data, test[2]) self.assertEquals(str(ns), test[2].decode('utf-8')) # Check that it can be deserialize to the same thing: ns1 = checkers.deserialize_namespace(data) self.assertEquals(ns1, ns) # TODO(rogpeppe) add resolve tests def test_register(self): ns = checkers.Namespace(None) ns.register('testns', 't') prefix = ns.resolve('testns') self.assertEquals(prefix, 't') ns.register('other', 'o') prefix = ns.resolve('other') self.assertEquals(prefix, 'o') # If we re-register the same URL, it does nothing. ns.register('other', 'p') prefix = ns.resolve('other') self.assertEquals(prefix, 'o') def test_register_bad_uri(self): ns = checkers.Namespace(None) with self.assertRaises(KeyError): ns.register('', 'x') def test_register_bad_prefix(self): ns = checkers.Namespace(None) with self.assertRaises(ValueError): ns.register('std', 'x:1') macaroonbakery-1.3.1/macaroonbakery/tests/test_macaroon.py0000644000175000017500000002035613466233337025466 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import json from unittest import TestCase import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers import pymacaroons import six from macaroonbakery.tests import common from pymacaroons import serializers class TestMacaroon(TestCase): def test_new_macaroon(self): m = bakery.Macaroon( b'rootkey', b'some id', 'here', bakery.LATEST_VERSION) self.assertIsNotNone(m) self.assertEquals(m._macaroon.identifier, b'some id') self.assertEquals(m._macaroon.location, 'here') self.assertEquals(m.version, bakery.LATEST_VERSION) def test_add_first_party_caveat(self): m = bakery.Macaroon('rootkey', 'some id', 'here', bakery.LATEST_VERSION) m.add_caveat(checkers.Caveat('test_condition')) caveats = m.first_party_caveats() self.assertEquals(len(caveats), 1) self.assertEquals(caveats[0].caveat_id, b'test_condition') def test_add_third_party_caveat(self): locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) lbv = six.int2byte(bakery.LATEST_VERSION) tests = [ ('no existing id', b'', [], lbv + six.int2byte(0)), ('several existing ids', b'', [ lbv + six.int2byte(0), lbv + six.int2byte(1), lbv + six.int2byte(2) ], lbv + six.int2byte(3)), ('with base id', lbv + six.int2byte(0), [lbv + six.int2byte(0)], lbv + six.int2byte(0) + six.int2byte(0)), ('with base id and existing id', lbv + six.int2byte(0), [ lbv + six.int2byte(0) + six.int2byte(0) ], lbv + six.int2byte(0) + six.int2byte(1)) ] for test in tests: print('test ', test[0]) m = bakery.Macaroon( root_key=b'root key', id=b'id', location='location', version=bakery.LATEST_VERSION) for id in test[2]: m.macaroon.add_third_party_caveat(key=None, key_id=id, location='') m._caveat_id_prefix = test[1] m.add_caveat(checkers.Caveat(location='bs-loc', condition='something'), bs.oven.key, locator) self.assertEqual(m.macaroon.caveats[len(test[2])].caveat_id, test[3]) def test_marshal_json_latest_version(self): locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) ns = checkers.Namespace({ 'testns': 'x', 'otherns': 'y', }) m = bakery.Macaroon( root_key=b'root key', id=b'id', location='location', version=bakery.LATEST_VERSION, namespace=ns) m.add_caveat(checkers.Caveat(location='bs-loc', condition='something'), bs.oven.key, locator) data = m.serialize_json() m1 = bakery.Macaroon.deserialize_json(data) # Just check the signature and version - we're not interested in fully # checking the macaroon marshaling here. self.assertEqual(m1.macaroon.signature, m.macaroon.signature) self.assertEqual(m1.macaroon.version, m.macaroon.version) self.assertEqual(len(m1.macaroon.caveats), 1) self.assertEqual(m1.namespace, m.namespace) self.assertEqual(m1._caveat_data, m._caveat_data) # test with the encoder, decoder data = json.dumps(m, cls=bakery.MacaroonJSONEncoder) m1 = json.loads(data, cls=bakery.MacaroonJSONDecoder) self.assertEqual(m1.macaroon.signature, m.macaroon.signature) self.assertEqual(m1.macaroon.version, m.macaroon.version) self.assertEqual(len(m1.macaroon.caveats), 1) self.assertEqual(m1.namespace, m.namespace) self.assertEqual(m1._caveat_data, m._caveat_data) def test_json_version1(self): self._test_json_with_version(bakery.VERSION_1) def test_json_version2(self): self._test_json_with_version(bakery.VERSION_2) def _test_json_with_version(self, version): locator = bakery.ThirdPartyStore() bs = common.new_bakery('bs-loc', locator) ns = checkers.Namespace({ 'testns': 'x', }) m = bakery.Macaroon( root_key=b'root key', id=b'id', location='location', version=version, namespace=ns) m.add_caveat(checkers.Caveat(location='bs-loc', condition='something'), bs.oven.key, locator) # Sanity check that no external caveat data has been added. self.assertEqual(len(m._caveat_data), 0) data = json.dumps(m, cls=bakery.MacaroonJSONEncoder) m1 = json.loads(data, cls=bakery.MacaroonJSONDecoder) # Just check the signature and version - we're not interested in fully # checking the macaroon marshaling here. self.assertEqual(m1.macaroon.signature, m.macaroon.signature) self.assertEqual(m1.macaroon.version, bakery.macaroon_version(version)) self.assertEqual(len(m1.macaroon.caveats), 1) # Namespace information has been thrown away. self.assertEqual(m1.namespace, bakery.legacy_namespace()) self.assertEqual(len(m1._caveat_data), 0) def test_json_unknown_version(self): m = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2) with self.assertRaises(ValueError) as exc: json.loads(json.dumps({ 'm': m.serialize(serializer=serializers.JsonSerializer()), 'v': bakery.LATEST_VERSION + 1 }), cls=bakery.MacaroonJSONDecoder) self.assertEqual('unknown bakery version 4', exc.exception.args[0]) def test_json_inconsistent_version(self): m = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V1) with self.assertRaises(ValueError) as exc: json.loads(json.dumps({ 'm': json.loads(m.serialize( serializer=serializers.JsonSerializer())), 'v': bakery.LATEST_VERSION }), cls=bakery.MacaroonJSONDecoder) self.assertEqual('underlying macaroon has inconsistent version; ' 'got 1 want 2', exc.exception.args[0]) def test_clone(self): locator = bakery.ThirdPartyStore() bs = common.new_bakery("bs-loc", locator) ns = checkers.Namespace({ "testns": "x", }) m = bakery.Macaroon( root_key=b'root key', id=b'id', location='location', version=bakery.LATEST_VERSION, namespace=ns) m.add_caveat(checkers.Caveat(location='bs-loc', condition='something'), bs.oven.key, locator) m1 = m.copy() self.assertEqual(len(m.macaroon.caveats), 1) self.assertEqual(len(m1.macaroon.caveats), 1) self.assertEqual(m._caveat_data, m1._caveat_data) m.add_caveat(checkers.Caveat(location='bs-loc', condition='something'), bs.oven.key, locator) self.assertEqual(len(m.macaroon.caveats), 2) self.assertEqual(len(m1.macaroon.caveats), 1) self.assertNotEqual(m._caveat_data, m1._caveat_data) def test_json_deserialize_from_go(self): ns = checkers.Namespace() ns.register("someuri", "x") m = bakery.Macaroon( root_key=b'rootkey', id=b'some id', location='here', version=bakery.LATEST_VERSION, namespace=ns) m.add_caveat(checkers.Caveat(condition='something', namespace='someuri')) data = '{"m":{"c":[{"i":"x:something"}],"l":"here","i":"some id",' \ '"s64":"c8edRIupArSrY-WZfa62pgZFD8VjDgqho9U2PlADe-E"},"v":3,' \ '"ns":"someuri:x"}' m_go = bakery.Macaroon.deserialize_json(data) self.assertEqual(m.macaroon.signature_bytes, m_go.macaroon.signature_bytes) self.assertEqual(m.macaroon.version, m_go.macaroon.version) self.assertEqual(len(m_go.macaroon.caveats), 1) self.assertEqual(m.namespace, m_go.namespace) macaroonbakery-1.3.1/macaroonbakery/tests/test_utils.py0000644000175000017500000000656213466233337025032 0ustar frankbanfrankban00000000000000# -*- coding: utf-8 -*- # Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import json from datetime import datetime from unittest import TestCase import macaroonbakery.bakery as bakery import pymacaroons from macaroonbakery._utils import cookie from pymacaroons.serializers import json_serializer class CookieTest(TestCase): def test_cookie_expires_naive(self): timestamp = datetime.utcnow() c = cookie('http://example.com', 'test', 'value', expires=timestamp) self.assertEqual( c.expires, int((timestamp - datetime(1970, 1, 1)).total_seconds())) def test_cookie_expires_with_timezone(self): from datetime import tzinfo timestamp = datetime.utcnow().replace(tzinfo=tzinfo()) self.assertRaises( ValueError, cookie, 'http://example.com', 'test', 'value', expires=timestamp) def test_cookie_with_hostname_not_fqdn(self): c = cookie('http://myhost', 'test', 'value') self.assertEqual(c.domain, 'myhost.local') def test_cookie_with_hostname_ipv4(self): c = cookie('http://1.2.3.4', 'test', 'value') self.assertEqual(c.domain, '1.2.3.4') def test_cookie_with_hostname_ipv6(self): c = cookie('http://[dead::beef]', 'test', 'value') self.assertEqual(c.domain, 'dead::beef') def test_cookie_with_hostname_like_ipv4(self): c = cookie('http://1.2.3.4.com', 'test', 'value') self.assertEqual(c.domain, '1.2.3.4.com') def test_cookie_with_hostname_not_ascii(self): c = cookie('http://κουλουράκι', 'test', 'value') self.assertEqual(c.domain, 'κουλουράκι.local') class TestB64Decode(TestCase): def test_decode(self): test_cases = [{ 'about': 'empty string', 'input': '', 'expect': '', }, { 'about': 'standard encoding, padded', 'input': 'Z29+IQ==', 'expect': 'go~!', }, { 'about': 'URL encoding, padded', 'input': 'Z29-IQ==', 'expect': 'go~!', }, { 'about': 'standard encoding, not padded', 'input': 'Z29+IQ', 'expect': 'go~!', }, { 'about': 'URL encoding, not padded', 'input': 'Z29-IQ', 'expect': 'go~!', }, { 'about': 'standard encoding, not enough much padding', 'input': 'Z29+IQ=', 'expect_error': 'illegal base64 data at input byte 8', }] for test in test_cases: if test.get('expect_error'): with self.assertRaises(ValueError, msg=test['about']) as e: bakery.b64decode(test['input']) self.assertEqual(str(e.exception), 'Incorrect padding') else: self.assertEqual(bakery.b64decode(test['input']), test['expect'].encode('utf-8'), msg=test['about']) class MacaroonToDictTest(TestCase): def test_macaroon_to_dict(self): m = pymacaroons.Macaroon( key=b'rootkey', identifier=b'some id', location='here', version=2) as_dict = bakery.macaroon_to_dict(m) data = json.dumps(as_dict) m1 = pymacaroons.Macaroon.deserialize(data, json_serializer.JsonSerializer()) self.assertEqual(m1.signature, m.signature) pymacaroons.Verifier().verify(m1, b'rootkey') macaroonbakery-1.3.1/macaroonbakery/tests/test_oven.py0000644000175000017500000001203413466233337024630 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import copy from datetime import datetime, timedelta from unittest import TestCase import macaroonbakery.bakery as bakery EPOCH = datetime(1900, 11, 17, 19, 00, 13, 0, None) AGES = EPOCH + timedelta(days=10) class TestOven(TestCase): def test_canonical_ops(self): canonical_ops_tests = ( ('empty array', [], []), ('one element', [bakery.Op('a', 'a')], [bakery.Op('a', 'a')]), ('all in order', [bakery.Op('a', 'a'), bakery.Op('a', 'b'), bakery.Op('c', 'c')], [bakery.Op('a', 'a'), bakery.Op('a', 'b'), bakery.Op('c', 'c')]), ('out of order', [bakery.Op('c', 'c'), bakery.Op('a', 'b'), bakery.Op('a', 'a')], [bakery.Op('a', 'a'), bakery.Op('a', 'b'), bakery.Op('c', 'c')]), ('with duplicates', [bakery.Op('c', 'c'), bakery.Op('a', 'b'), bakery.Op('a', 'a'), bakery.Op('c', 'a'), bakery.Op('c', 'b'), bakery.Op('c', 'c'), bakery.Op('a', 'a')], [bakery.Op('a', 'a'), bakery.Op('a', 'b'), bakery.Op('c', 'a'), bakery.Op('c', 'b'), bakery.Op('c', 'c')]), ('make sure we\'ve got the fields right', [bakery.Op(entity='read', action='two'), bakery.Op(entity='read', action='one'), bakery.Op(entity='write', action='one')], [bakery.Op(entity='read', action='one'), bakery.Op(entity='read', action='two'), bakery.Op(entity='write', action='one')]) ) for about, ops, expected in canonical_ops_tests: new_ops = copy.copy(ops) canonical_ops = bakery.canonical_ops(new_ops) self.assertEquals(canonical_ops, expected) # Verify that the original array isn't changed. self.assertEquals(new_ops, ops) def test_multiple_ops(self): test_oven = bakery.Oven( ops_store=bakery.MemoryOpsStore()) ops = [bakery.Op('one', 'read'), bakery.Op('one', 'write'), bakery.Op('two', 'read')] m = test_oven.macaroon(bakery.LATEST_VERSION, AGES, None, ops) got_ops, conds = test_oven.macaroon_ops([m.macaroon]) self.assertEquals(len(conds), 1) # time-before caveat. self.assertEquals(bakery.canonical_ops(got_ops), ops) def test_multiple_ops_in_id(self): test_oven = bakery.Oven() ops = [bakery.Op('one', 'read'), bakery.Op('one', 'write'), bakery.Op('two', 'read')] m = test_oven.macaroon(bakery.LATEST_VERSION, AGES, None, ops) got_ops, conds = test_oven.macaroon_ops([m.macaroon]) self.assertEquals(len(conds), 1) # time-before caveat. self.assertEquals(bakery.canonical_ops(got_ops), ops) def test_multiple_ops_in_id_with_version1(self): test_oven = bakery.Oven() ops = [bakery.Op('one', 'read'), bakery.Op('one', 'write'), bakery.Op('two', 'read')] m = test_oven.macaroon(bakery.VERSION_1, AGES, None, ops) got_ops, conds = test_oven.macaroon_ops([m.macaroon]) self.assertEquals(len(conds), 1) # time-before caveat. self.assertEquals(bakery.canonical_ops(got_ops), ops) def test_huge_number_of_ops_gives_small_macaroon(self): test_oven = bakery.Oven( ops_store=bakery.MemoryOpsStore()) ops = [] for i in range(30000): ops.append(bakery.Op(entity='entity' + str(i), action='action' + str(i))) m = test_oven.macaroon(bakery.LATEST_VERSION, AGES, None, ops) got_ops, conds = test_oven.macaroon_ops([m.macaroon]) self.assertEquals(len(conds), 1) # time-before caveat. self.assertEquals(bakery.canonical_ops(got_ops), bakery.canonical_ops(ops)) data = m.serialize_json() self.assertLess(len(data), 300) def test_ops_stored_only_once(self): st = bakery.MemoryOpsStore() test_oven = bakery.Oven(ops_store=st) ops = [bakery.Op('one', 'read'), bakery.Op('one', 'write'), bakery.Op('two', 'read')] m = test_oven.macaroon(bakery.LATEST_VERSION, AGES, None, ops) got_ops, conds = test_oven.macaroon_ops([m.macaroon]) self.assertEquals(bakery.canonical_ops(got_ops), bakery.canonical_ops(ops)) # Make another macaroon containing the same ops in a different order. ops = [bakery.Op('one', 'write'), bakery.Op('one', 'read'), bakery.Op('one', 'read'), bakery.Op('two', 'read')] test_oven.macaroon(bakery.LATEST_VERSION, AGES, None, ops) self.assertEquals(len(st._store), 1) macaroonbakery-1.3.1/macaroonbakery/tests/common.py0000644000175000017500000000631413466233337024116 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from datetime import datetime, timedelta import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers class _StoppedClock(object): def __init__(self, t): self.t = t def utcnow(self): return self.t epoch = datetime(year=1900, month=11, day=17, hour=19, minute=00, second=13) ages = epoch + timedelta(days=1) test_context = checkers.context_with_clock(checkers.AuthContext(), _StoppedClock(epoch)) def test_checker(): c = checkers.Checker() c.namespace().register('testns', '') c.register('str', 'testns', str_check) c.register('true', 'testns', true_check) return c _str_key = checkers.ContextKey('str_check') def str_context(s): return test_context.with_value(_str_key, s) def str_check(ctx, cond, args): expect = ctx[_str_key] if args != expect: return '{} doesn\'t match {}'.format(cond, expect) return None def true_check(ctx, cond, args): # Always succeeds. return None class OneIdentity(bakery.IdentityClient): '''An IdentityClient implementation that always returns a single identity from declared_identity, allowing allow(LOGIN_OP) to work even when there are no declaration caveats (this is mostly to support the legacy tests which do their own checking of declaration caveats). ''' def identity_from_context(self, ctx): return None, None def declared_identity(self, ctx, declared): return _NoOne() class _NoOne(object): def id(self): return 'noone' def domain(self): return '' class ThirdPartyStrcmpChecker(bakery.ThirdPartyCaveatChecker): def __init__(self, str): self.str = str def check_third_party_caveat(self, ctx, cav_info): condition = cav_info.condition if isinstance(cav_info.condition, bytes): condition = cav_info.condition.decode('utf-8') if condition != self.str: raise bakery.ThirdPartyCaveatCheckFailed( '{} doesn\'t match {}'.format(repr(condition), repr(self.str))) return [] class ThirdPartyCheckerWithCaveats(bakery.ThirdPartyCaveatChecker): def __init__(self, cavs=None): if cavs is None: cavs = [] self.cavs = cavs def check_third_party_caveat(self, ctx, cav_info): return self.cavs class ThirdPartyCaveatCheckerEmpty(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, cav_info): return [] def new_bakery(location, locator=None): # Returns a new Bakery instance using a new # key pair, and registers the key with the given locator if provided. # # It uses test_checker to check first party caveats. key = bakery.generate_key() if locator is not None: locator.add_info(location, bakery.ThirdPartyInfo( public_key=key.public_key, version=bakery.LATEST_VERSION)) return bakery.Bakery( key=key, checker=test_checker(), location=location, identity_client=OneIdentity(), locator=locator, ) macaroonbakery-1.3.1/macaroonbakery/tests/test_discharge_all.py0000644000175000017500000001334313466233337026446 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import unittest import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers from macaroonbakery.tests import common from pymacaroons.verifier import Verifier def always_ok(predicate): return True class TestDischargeAll(unittest.TestCase): def test_discharge_all_no_discharges(self): root_key = b'root key' m = bakery.Macaroon( root_key=root_key, id=b'id0', location='loc0', version=bakery.LATEST_VERSION, namespace=common.test_checker().namespace()) ms = bakery.discharge_all(m, no_discharge(self)) self.assertEqual(len(ms), 1) self.assertEqual(ms[0], m.macaroon) v = Verifier() v.satisfy_general(always_ok) v.verify(m.macaroon, root_key, None) def test_discharge_all_many_discharges(self): root_key = b'root key' m0 = bakery.Macaroon( root_key=root_key, id=b'id0', location='loc0', version=bakery.LATEST_VERSION) class State(object): total_required = 40 id = 1 def add_caveats(m): for i in range(0, 1): if State.total_required == 0: break cid = 'id{}'.format(State.id) m.macaroon.add_third_party_caveat( location='somewhere', key='root key {}'.format(cid).encode('utf-8'), key_id=cid.encode('utf-8')) State.id += 1 State.total_required -= 1 add_caveats(m0) def get_discharge(cav, payload): self.assertEqual(payload, None) m = bakery.Macaroon( root_key='root key {}'.format( cav.caveat_id.decode('utf-8')).encode('utf-8'), id=cav.caveat_id, location='', version=bakery.LATEST_VERSION) add_caveats(m) return m ms = bakery.discharge_all(m0, get_discharge) self.assertEqual(len(ms), 41) v = Verifier() v.satisfy_general(always_ok) v.verify(ms[0], root_key, ms[1:]) def test_discharge_all_many_discharges_with_real_third_party_caveats(self): # This is the same flow as TestDischargeAllManyDischarges except that # we're using actual third party caveats as added by # Macaroon.add_caveat and we use a larger number of caveats # so that caveat ids will need to get larger. locator = bakery.ThirdPartyStore() bakeries = {} total_discharges_required = 40 class M: bakery_id = 0 still_required = total_discharges_required def add_bakery(): M.bakery_id += 1 loc = 'loc{}'.format(M.bakery_id) bakeries[loc] = common.new_bakery(loc, locator) return loc ts = common.new_bakery('ts-loc', locator) def checker(_, ci): caveats = [] if ci.condition != 'something': self.fail('unexpected condition') for i in range(0, 2): if M.still_required <= 0: break caveats.append(checkers.Caveat(location=add_bakery(), condition='something')) M.still_required -= 1 return caveats root_key = b'root key' m0 = bakery.Macaroon( root_key=root_key, id=b'id0', location='ts-loc', version=bakery.LATEST_VERSION) m0.add_caveat(checkers. Caveat(location=add_bakery(), condition='something'), ts.oven.key, locator) # We've added a caveat (the first) so one less caveat is required. M.still_required -= 1 class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, info): return checker(ctx, info) def get_discharge(cav, payload): return bakery.discharge( common.test_context, cav.caveat_id, payload, bakeries[cav.location].oven.key, ThirdPartyCaveatCheckerF(), locator) ms = bakery.discharge_all(m0, get_discharge) self.assertEqual(len(ms), total_discharges_required + 1) v = Verifier() v.satisfy_general(always_ok) v.verify(ms[0], root_key, ms[1:]) def test_discharge_all_local_discharge(self): oc = common.new_bakery('ts', None) client_key = bakery.generate_key() m = oc.oven.macaroon(bakery.LATEST_VERSION, common.ages, [ bakery.local_third_party_caveat( client_key.public_key, bakery.LATEST_VERSION) ], [bakery.LOGIN_OP]) ms = bakery.discharge_all(m, no_discharge(self), client_key) oc.checker.auth([ms]).allow(common.test_context, [bakery.LOGIN_OP]) def test_discharge_all_local_discharge_version1(self): oc = common.new_bakery('ts', None) client_key = bakery.generate_key() m = oc.oven.macaroon(bakery.VERSION_1, common.ages, [ bakery.local_third_party_caveat( client_key.public_key, bakery.VERSION_1) ], [bakery.LOGIN_OP]) ms = bakery.discharge_all(m, no_discharge(self), client_key) oc.checker.auth([ms]).allow(common.test_context, [bakery.LOGIN_OP]) def no_discharge(test): def get_discharge(cav, payload): test.fail("get_discharge called unexpectedly") return get_discharge macaroonbakery-1.3.1/macaroonbakery/tests/test_bakery.py0000644000175000017500000002277513613544502025144 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from unittest import TestCase import macaroonbakery.httpbakery as httpbakery import requests from mock import patch from httmock import HTTMock, response, urlmatch ID_PATH = 'http://example.com/someprotecteurl' json_macaroon = { u'identifier': u'macaroon-identifier', u'caveats': [ { u'cl': u'http://example.com/identity/v1/discharger', u'vid': u'zgtQa88oS9UF45DlJniRaAUT4qqHhLxQzCeUU9N2O1Uu-' u'yhFulgGbSA0zDGdkrq8YNQAxGiARA_-AGxyoh25kiTycb8u47pD', u'cid': u'eyJUaGlyZFBhcnR5UHV' }, { u'cid': u'allow read-no-terms write' }, { u'cid': u'time-before 2158-07-19T14:29:14.312669464Z' }], u'location': u'charmstore', u'signature': u'52d17cb11f5c84d58441bc0ffd7cc396' u'5115374ce2fa473ecf06265b5d4d9e81' } discharge_token = [{ u'identifier': u'token-identifier===', u'caveats': [{ u'cid': u'declared username someone' }, { u'cid': u'time-before 2158-08-15T15:55:52.428319076Z' }, { u'cid': u'origin ' }], u'location': u'https://example.com/identity', u'signature': u'5ae0e7a2abf806bdd92f510fcd3' u'198f520691259abe76ffae5623dae048769ef' }] discharged_macaroon = { u'identifier': u'discharged-identifier=', u'caveats': [{ u'cid': u'declared uuid a1130b10-3deb-59b7-baf0-c2a3f83e7382' }, { u'cid': u'declared username someone' }, { u'cid': u'time-before 2158-07-19T15:55:52.432439055Z' }], u'location': u'', u'signature': u'3513db5503ab17f9576760cd28' u'ce658ce8bf6b43038255969fc3c1cd8b172345' } @urlmatch(path='.*/someprotecteurl') def first_407_then_200(url, request): if request.headers.get('cookie', '').startswith('macaroon-'): return { 'status_code': 200, 'content': { 'Value': 'some value' } } else: resp = response(status_code=407, content={ 'Info': { 'Macaroon': json_macaroon, 'MacaroonPath': '/', 'CookieNameSuffix': 'test' }, 'Message': 'verification failed: no macaroon ' 'cookies in request', 'Code': 'macaroon discharge required' }, headers={'Content-Type': 'application/json'}) return request.hooks['response'][0](resp) @urlmatch(netloc='example.com:8000', path='.*/someprotecteurl') def first_407_then_200_with_port(url, request): if request.headers.get('cookie', '').startswith('macaroon-'): return { 'status_code': 200, 'content': { 'Value': 'some value' } } else: resp = response(status_code=407, content={ 'Info': { 'Macaroon': json_macaroon, 'MacaroonPath': '/', 'CookieNameSuffix': 'test' }, 'Message': 'verification failed: no macaroon ' 'cookies in request', 'Code': 'macaroon discharge required' }, headers={'Content-Type': 'application/json'}, request=request) return request.hooks['response'][0](resp) @urlmatch(path='.*/someprotecteurl') def valid_200(url, request): return { 'status_code': 200, 'content': { 'Value': 'some value' } } @urlmatch(path='.*/discharge') def discharge_200(url, request): return { 'status_code': 200, 'content': { 'Macaroon': discharged_macaroon } } @urlmatch(path='.*/discharge') def discharge_401(url, request): return { 'status_code': 401, 'content': { 'Code': 'interaction required', 'Info': { 'VisitURL': 'http://example.com/visit', 'WaitURL': 'http://example.com/wait' } }, 'headers': { 'WWW-Authenticate': 'Macaroon' } } @urlmatch(path='.*/visit') def visit_200(url, request): return { 'status_code': 200, 'content': { 'interactive': '/visit' } } @urlmatch(path='.*/wait') def wait_after_401(url, request): if request.url != 'http://example.com/wait': return {'status_code': 500} return { 'status_code': 200, 'content': { 'DischargeToken': discharge_token, 'Macaroon': discharged_macaroon } } @urlmatch(path='.*/wait') def wait_on_error(url, request): return { 'status_code': 500, 'content': { 'DischargeToken': discharge_token, 'Macaroon': discharged_macaroon } } class TestBakery(TestCase): def assert_cookie_security(self, cookies, name, secure): for cookie in cookies: if cookie.name == name: assert cookie.secure == secure break else: assert False, 'no cookie named {} found in jar'.format(name) def test_discharge(self): client = httpbakery.Client() with HTTMock(first_407_then_200), HTTMock(discharge_200): resp = requests.get(ID_PATH, cookies=client.cookies, auth=client.auth()) resp.raise_for_status() assert 'macaroon-test' in client.cookies.keys() self.assert_cookie_security(client.cookies, 'macaroon-test', secure=False) @patch('webbrowser.open') def test_407_then_401_on_discharge(self, mock_open): client = httpbakery.Client() with HTTMock(first_407_then_200), HTTMock(discharge_401), \ HTTMock(wait_after_401): resp = requests.get( ID_PATH, cookies=client.cookies, auth=client.auth(), ) resp.raise_for_status() mock_open.assert_called_once_with(u'http://example.com/visit', new=1) assert 'macaroon-test' in client.cookies.keys() @patch('webbrowser.open') def test_407_then_error_on_wait(self, mock_open): client = httpbakery.Client() with HTTMock(first_407_then_200), HTTMock(discharge_401),\ HTTMock(wait_on_error): with self.assertRaises(httpbakery.InteractionError) as exc: requests.get( ID_PATH, cookies=client.cookies, auth=client.auth(), ) self.assertEqual(str(exc.exception), 'cannot start interactive session: cannot get ' 'http://example.com/wait') mock_open.assert_called_once_with(u'http://example.com/visit', new=1) def test_407_then_no_interaction_methods(self): client = httpbakery.Client(interaction_methods=[]) with HTTMock(first_407_then_200), HTTMock(discharge_401): with self.assertRaises(httpbakery.InteractionError) as exc: requests.get( ID_PATH, cookies=client.cookies, auth=client.auth(), ) self.assertEqual(str(exc.exception), 'cannot start interactive session: interaction ' 'required but not possible') def test_407_then_unknown_interaction_methods(self): class UnknownInteractor(httpbakery.Interactor): def kind(self): return 'unknown' client = httpbakery.Client(interaction_methods=[UnknownInteractor()]) with HTTMock(first_407_then_200), HTTMock(discharge_401),\ HTTMock(visit_200): with self.assertRaises(httpbakery.InteractionError) as exc: requests.get( ID_PATH, cookies=client.cookies, auth=client.auth(), ) self.assertEqual( str(exc.exception), 'cannot start interactive session: no methods supported; ' 'supported [unknown]; provided [interactive]' ) def test_cookie_with_port(self): client = httpbakery.Client() with HTTMock(first_407_then_200_with_port): with HTTMock(discharge_200): resp = requests.get('http://example.com:8000/someprotecteurl', cookies=client.cookies, auth=client.auth()) resp.raise_for_status() assert 'macaroon-test' in client.cookies.keys() def test_secure_cookie_for_https(self): client = httpbakery.Client() with HTTMock(first_407_then_200_with_port), HTTMock(discharge_200): resp = requests.get( 'https://example.com:8000/someprotecteurl', cookies=client.cookies, auth=client.auth()) resp.raise_for_status() assert 'macaroon-test' in client.cookies.keys() self.assert_cookie_security(client.cookies, 'macaroon-test', secure=True) macaroonbakery-1.3.1/macaroonbakery/tests/__init__.py0000644000175000017500000000013313466233337024356 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. macaroonbakery-1.3.1/macaroonbakery/tests/test_authorizer.py0000644000175000017500000001145213466233337026060 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from unittest import TestCase import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers class TestAuthorizer(TestCase): def test_authorize_func(self): def f(ctx, identity, op): self.assertEqual(identity.id(), 'bob') if op.entity == 'a': return False, None elif op.entity == 'b': return True, None elif op.entity == 'c': return True, [checkers.Caveat(location='somewhere', condition='c')] elif op.entity == 'd': return True, [checkers.Caveat(location='somewhere', condition='d')] else: self.fail('unexpected entity: ' + op.Entity) ops = [bakery.Op('a', 'x'), bakery.Op('b', 'x'), bakery.Op('c', 'x'), bakery.Op('d', 'x')] allowed, caveats = bakery.AuthorizerFunc(f).authorize( checkers.AuthContext(), bakery.SimpleIdentity('bob'), ops ) self.assertEqual(allowed, [False, True, True, True]) self.assertEqual(caveats, [ checkers.Caveat(location='somewhere', condition='c'), checkers.Caveat(location='somewhere', condition='d') ]) def test_acl_authorizer(self): ctx = checkers.AuthContext() tests = [ ('no ops, no problem', bakery.ACLAuthorizer(allow_public=True, get_acl=lambda x, y: []), None, [], []), ('identity that does not implement ACLIdentity; ' 'user should be denied except for everyone group', bakery.ACLAuthorizer( allow_public=True, get_acl=lambda ctx, op: [bakery.EVERYONE] if op.entity == 'a' else ['alice'], ), SimplestIdentity('bob'), [bakery.Op(entity='a', action='a'), bakery.Op(entity='b', action='b')], [True, False]), ('identity that does not implement ACLIdentity with user == Id; ' 'user should be denied except for everyone group', bakery.ACLAuthorizer( allow_public=True, get_acl=lambda ctx, op: [bakery.EVERYONE] if op.entity == 'a' else ['bob'], ), SimplestIdentity('bob'), [bakery.Op(entity='a', action='a'), bakery.Op(entity='b', action='b')], [True, False]), ('permission denied for everyone without AllowPublic', bakery.ACLAuthorizer( allow_public=False, get_acl=lambda x, y: [bakery.EVERYONE], ), SimplestIdentity('bob'), [bakery.Op(entity='a', action='a')], [False]), ('permission granted to anyone with no identity with AllowPublic', bakery.ACLAuthorizer( allow_public=True, get_acl=lambda x, y: [bakery.EVERYONE], ), None, [bakery.Op(entity='a', action='a')], [True]) ] for test in tests: allowed, caveats = test[1].authorize(ctx, test[2], test[3]) self.assertEqual(len(caveats), 0) self.assertEqual(allowed, test[4]) def test_context_wired_properly(self): ctx = checkers.AuthContext({'a': 'aval'}) class Visited: in_f = False in_allow = False in_get_acl = False def f(ctx, identity, op): self.assertEqual(ctx.get('a'), 'aval') Visited.in_f = True return False, None bakery.AuthorizerFunc(f).authorize( ctx, bakery.SimpleIdentity('bob'), ['op1'] ) self.assertTrue(Visited.in_f) class TestIdentity(SimplestIdentity, bakery.ACLIdentity): def allow(other, ctx, acls): self.assertEqual(ctx.get('a'), 'aval') Visited.in_allow = True return False def get_acl(ctx, acl): self.assertEqual(ctx.get('a'), 'aval') Visited.in_get_acl = True return [] bakery.ACLAuthorizer( allow_public=False, get_acl=get_acl, ).authorize(ctx, TestIdentity('bob'), ['op1']) self.assertTrue(Visited.in_get_acl) self.assertTrue(Visited.in_allow) class SimplestIdentity(bakery.Identity): # SimplestIdentity implements Identity for a string. Unlike # SimpleIdentity, it does not implement ACLIdentity. def __init__(self, user): self._identity = user def domain(self): return '' def id(self): return self._identity macaroonbakery-1.3.1/macaroonbakery/httpbakery/0000755000175000017500000000000013616470550023260 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/httpbakery/_discharge.py0000644000175000017500000000221213466233337025722 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import macaroonbakery.bakery as bakery import macaroonbakery._utils as utils def discharge(ctx, content, key, locator, checker): '''Handles a discharge request as received by the /discharge endpoint. @param ctx The context passed to the checker {checkers.AuthContext} @param content URL and form parameters {dict} @param locator Locator used to add third party caveats returned by the checker {macaroonbakery.ThirdPartyLocator} @param checker {macaroonbakery.ThirdPartyCaveatChecker} Used to check third party caveats. @return The discharge macaroon {macaroonbakery.Macaroon} ''' id = content.get('id') if id is not None: id = id.encode('utf-8') else: id = content.get('id64') if id is not None: id = utils.b64decode(id) caveat = content.get('caveat64') if caveat is not None: caveat = utils.b64decode(caveat) return bakery.discharge( ctx, id=id, caveat=caveat, key=key, checker=checker, locator=locator, ) macaroonbakery-1.3.1/macaroonbakery/httpbakery/_interactor.py0000644000175000017500000000552313466233337026153 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import abc from collections import namedtuple WEB_BROWSER_INTERACTION_KIND = 'browser-window' class Interactor(object): ''' Represents a way of persuading a discharger that it should grant a discharge macaroon. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def kind(self): '''Returns the interaction method name. This corresponds to the key in the Error.interaction_methods type. @return {str} ''' raise NotImplementedError('kind method must be defined in subclass') def interact(self, client, location, interaction_required_err): ''' Performs the interaction, and returns a token that can be used to acquire the discharge macaroon. The location provides the third party caveat location to make it possible to use relative URLs. The client holds the client being used to do the current request. If the given interaction isn't supported by the client for the given location, it may raise an InteractionMethodNotFound which will cause the interactor to be ignored that time. @param client The client being used for the current request {Client} @param location Third party caveat location {str} @param interaction_required_err The error causing the interaction to take place {Error} @return {DischargeToken} The discharge token. ''' raise NotImplementedError('interact method must be defined in subclass') class LegacyInteractor(object): ''' May optionally be implemented by Interactor implementations that implement the legacy interaction-required error protocols. ''' __metaclass__ = abc.ABCMeta @abc.abstractmethod def legacy_interact(self, client, location, visit_url): ''' Implements the "visit" half of a legacy discharge interaction. The "wait" half will be implemented by httpbakery. The location is the location specified by the third party caveat. The client holds the client being used to do the current request. @param client The client being used for the current request {Client} @param location Third party caveat location {str} @param visit_url The visit_url field from the error {str} @return None ''' raise NotImplementedError('legacy_interact method must be defined in subclass') class DischargeToken(namedtuple('DischargeToken', 'kind, value')): ''' Holds a token that is intended to persuade a discharger to discharge a third party caveat. @param kind holds the kind of the token. By convention this matches the name of the interaction method used to obtain the token, but that's not required {str} @param value holds the token data. {bytes} ''' macaroonbakery-1.3.1/macaroonbakery/httpbakery/_browser.py0000644000175000017500000000675313466233337025472 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 from collections import namedtuple import requests from ._error import InteractionError from ._interactor import ( WEB_BROWSER_INTERACTION_KIND, DischargeToken, Interactor, LegacyInteractor, ) from macaroonbakery._utils import visit_page_with_browser from six.moves.urllib.parse import urljoin class WebBrowserInteractor(Interactor, LegacyInteractor): ''' Handles web-browser-based interaction-required errors by opening a web browser to allow the user to prove their credentials interactively. ''' def __init__(self, open=visit_page_with_browser): '''Create a WebBrowserInteractor that uses the given function to open a browser window. The open function is expected to take a single argument of string type, the URL to open. ''' self._open_web_browser = open def kind(self): return WEB_BROWSER_INTERACTION_KIND def legacy_interact(self, ctx, location, visit_url): '''Implement LegacyInteractor.legacy_interact by opening the web browser window''' self._open_web_browser(visit_url) def interact(self, ctx, location, ir_err): '''Implement Interactor.interact by opening the browser window and waiting for the discharge token''' p = ir_err.interaction_method(self.kind(), WebBrowserInteractionInfo) if not location.endswith('/'): location += '/' visit_url = urljoin(location, p.visit_url) wait_token_url = urljoin(location, p.wait_token_url) self._open_web_browser(visit_url) return self._wait_for_token(ctx, wait_token_url) def _wait_for_token(self, ctx, wait_token_url): ''' Returns a token from a the wait token URL @param wait_token_url URL to wait for (string) :return DischargeToken ''' resp = requests.get(wait_token_url) if resp.status_code != 200: raise InteractionError('cannot get {}'.format(wait_token_url)) json_resp = resp.json() kind = json_resp.get('kind') if kind is None: raise InteractionError( 'cannot get kind token from {}'.format(wait_token_url)) token_val = json_resp.get('token') if token_val is None: token_val = json_resp.get('token64') if token_val is None: raise InteractionError( 'cannot get token from {}'.format(wait_token_url)) token_val = base64.b64decode(token_val) return DischargeToken(kind=kind, value=token_val) class WebBrowserInteractionInfo(namedtuple('WebBrowserInteractionInfo', 'visit_url, wait_token_url')): ''' holds the information expected in the browser-window interaction entry in an interaction-required error. :param visit_url holds the URL to be visited in a web browser. :param wait_token_url holds a URL that will block on GET until the browser interaction has completed. ''' @classmethod def from_dict(cls, info_dict): '''Create a new instance of WebBrowserInteractionInfo, as expected by the Error.interaction_method method. @param info_dict The deserialized JSON object @return a new WebBrowserInteractionInfo object. ''' return WebBrowserInteractionInfo( visit_url=info_dict.get('VisitURL'), wait_token_url=info_dict.get('WaitTokenURL')) macaroonbakery-1.3.1/macaroonbakery/httpbakery/_keyring.py0000644000175000017500000000437413466233337025454 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import macaroonbakery.bakery as bakery import requests from ._error import BAKERY_PROTOCOL_HEADER from six.moves.urllib.parse import urlparse class ThirdPartyLocator(bakery.ThirdPartyLocator): ''' Implements macaroonbakery.ThirdPartyLocator by first looking in the backing cache and, if that fails, making an HTTP request to find the information associated with the given discharge location. ''' def __init__(self, allow_insecure=False): ''' @param url: the url to retrieve public_key @param allow_insecure: By default it refuses to use insecure URLs. ''' self._allow_insecure = allow_insecure self._cache = {} def third_party_info(self, loc): u = urlparse(loc) if u.scheme != 'https' and not self._allow_insecure: raise bakery.ThirdPartyInfoNotFound( 'untrusted discharge URL {}'.format(loc)) loc = loc.rstrip('/') info = self._cache.get(loc) if info is not None: return info url_endpoint = '/discharge/info' headers = { BAKERY_PROTOCOL_HEADER: str(bakery.LATEST_VERSION) } resp = requests.get(url=loc + url_endpoint, headers=headers) status_code = resp.status_code if status_code == 404: url_endpoint = '/publickey' resp = requests.get(url=loc + url_endpoint, headers=headers) status_code = resp.status_code if status_code != 200: raise bakery.ThirdPartyInfoNotFound( 'unable to get info from {}'.format(url_endpoint)) json_resp = resp.json() if json_resp is None: raise bakery.ThirdPartyInfoNotFound( 'no response from /discharge/info') pk = json_resp.get('PublicKey') if pk is None: raise bakery.ThirdPartyInfoNotFound( 'no public key found in /discharge/info') idm_pk = bakery.PublicKey.deserialize(pk) version = json_resp.get('Version', bakery.VERSION_1) self._cache[loc] = bakery.ThirdPartyInfo( version=version, public_key=idm_pk ) return self._cache.get(loc) macaroonbakery-1.3.1/macaroonbakery/httpbakery/agent/0000755000175000017500000000000013616470550024356 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/httpbakery/agent/_agent.py0000644000175000017500000001550113466233337026172 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import copy import json import logging from collections import namedtuple import macaroonbakery.bakery as bakery import macaroonbakery.httpbakery as httpbakery import macaroonbakery._utils as utils import requests.cookies from six.moves.urllib.parse import urljoin log = logging.getLogger(__name__) class AgentFileFormatError(Exception): ''' AgentFileFormatError is the exception raised when an agent file has a bad structure. ''' pass def load_auth_info(filename): '''Loads agent authentication information from the specified file. The returned information is suitable for passing as an argument to the AgentInteractor constructor. @param filename The name of the file to open (str) @return AuthInfo The authentication information @raises AgentFileFormatError when the file format is bad. ''' with open(filename) as f: return read_auth_info(f.read()) def read_auth_info(agent_file_content): '''Loads agent authentication information from the specified content string, as read from an agents file. The returned information is suitable for passing as an argument to the AgentInteractor constructor. @param agent_file_content The agent file content (str) @return AuthInfo The authentication information @raises AgentFileFormatError when the file format is bad. ''' try: data = json.loads(agent_file_content) return AuthInfo( key=bakery.PrivateKey.deserialize(data['key']['private']), agents=list( Agent(url=a['url'], username=a['username']) for a in data.get('agents', []) ), ) except ( KeyError, ValueError, TypeError, ) as e: raise AgentFileFormatError('invalid agent file', e) class InteractionInfo(object): '''Holds the information expected in the agent interaction entry in an interaction-required error. ''' def __init__(self, login_url): self._login_url = login_url @property def login_url(self): ''' Return the URL from which to acquire a macaroon that can be used to complete the agent login. To acquire the macaroon, make a POST request to the URL with user and public-key parameters. :return string ''' return self._login_url @classmethod def from_dict(cls, json_dict): '''Return an InteractionInfo obtained from the given dictionary as deserialized from JSON. @param json_dict The deserialized JSON object. ''' return InteractionInfo(json_dict.get('login-url')) class AgentInteractor(httpbakery.Interactor, httpbakery.LegacyInteractor): ''' Interactor that performs interaction using the agent login protocol. ''' def __init__(self, auth_info): self._auth_info = auth_info def kind(self): '''Implement Interactor.kind by returning the agent kind''' return 'agent' def interact(self, client, location, interaction_required_err): '''Implement Interactor.interact by obtaining obtaining a macaroon from the discharger, discharging it with the local private key using the discharged macaroon as a discharge token''' p = interaction_required_err.interaction_method('agent', InteractionInfo) if p.login_url is None or p.login_url == '': raise httpbakery.InteractionError( 'no login-url field found in agent interaction method') agent = self._find_agent(location) if not location.endswith('/'): location += '/' login_url = urljoin(location, p.login_url) resp = requests.get( login_url, params={ 'username': agent.username, 'public-key': str(self._auth_info.key.public_key)}, auth=client.auth()) if resp.status_code != 200: raise httpbakery.InteractionError( 'cannot acquire agent macaroon: {} {}'.format( resp.status_code, resp.text) ) m = resp.json().get('macaroon') if m is None: raise httpbakery.InteractionError('no macaroon in response') m = bakery.Macaroon.from_dict(m) ms = bakery.discharge_all(m, None, self._auth_info.key) b = bytearray() for m in ms: b.extend(utils.b64decode(m.serialize())) return httpbakery.DischargeToken(kind='agent', value=bytes(b)) def _find_agent(self, location): ''' Finds an appropriate agent entry for the given location. :return Agent ''' for a in self._auth_info.agents: # Don't worry about trailing slashes if a.url.rstrip('/') == location.rstrip('/'): return a raise httpbakery.InteractionMethodNotFound( 'cannot find username for discharge location {}'.format(location)) def legacy_interact(self, client, location, visit_url): '''Implement LegacyInteractor.legacy_interact by obtaining the discharge macaroon using the client's private key ''' agent = self._find_agent(location) # Shallow-copy the client so that we don't unexpectedly side-effect # it by changing the key. Another possibility might be to # set up agent authentication differently, in such a way that # we're sure that client.key is the same as self._auth_info.key. client = copy.copy(client) client.key = self._auth_info.key resp = client.request( method='POST', url=visit_url, json={ 'username': agent.username, 'public_key': str(self._auth_info.key.public_key), }, ) if resp.status_code != 200: raise httpbakery.InteractionError( 'cannot acquire agent macaroon from {}: {} (response body: {!r})'.format(visit_url, resp.status_code, resp.text)) if not resp.json().get('agent_login', False): raise httpbakery.InteractionError('agent login failed') class Agent(namedtuple('Agent', 'url, username')): ''' Represents an agent that can be used for agent authentication. @param url(string) holds the URL of the discharger that knows about the agent. @param username holds the username agent (string). ''' class AuthInfo(namedtuple('AuthInfo', 'key, agents')): ''' Holds the agent information required to set up agent authentication information. It holds the agent's private key and information about the username associated with each known agent-authentication server. @param key the agent's private key (bakery.PrivateKey). @param agents information about the known agents (list of Agent). ''' macaroonbakery-1.3.1/macaroonbakery/httpbakery/agent/__init__.py0000644000175000017500000000055613466233337026500 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from ._agent import ( load_auth_info, read_auth_info, Agent, AgentInteractor, AgentFileFormatError, AuthInfo, ) __all__ = [ 'Agent', 'AgentFileFormatError', 'AgentInteractor', 'AuthInfo', 'load_auth_info', 'read_auth_info', ] macaroonbakery-1.3.1/macaroonbakery/httpbakery/_client.py0000644000175000017500000003620413466233337025257 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 import json import logging import macaroonbakery.bakery as bakery import macaroonbakery.checkers as checkers import macaroonbakery._utils as utils from ._browser import WebBrowserInteractor from ._error import ( BAKERY_PROTOCOL_HEADER, ERR_DISCHARGE_REQUIRED, ERR_INTERACTION_REQUIRED, DischargeError, Error, InteractionError, InteractionMethodNotFound, ) from ._interactor import ( WEB_BROWSER_INTERACTION_KIND, LegacyInteractor, ) import requests from six.moves.http_cookies import SimpleCookie from six.moves.urllib.parse import urljoin TIME_OUT = 30 MAX_DISCHARGE_RETRIES = 3 log = logging.getLogger('httpbakery') class BakeryException(requests.RequestException): '''Raised when some errors happen using the httpbakery authorizer''' class Client: '''Client holds the context for making HTTP requests with macaroons. To make a request, use the auth method to obtain an HTTP authorizer suitable for passing as the auth parameter to a requests method. Note that the same cookie jar should be passed to requests as is used to initialize the client. For example: import macaroonbakery.httpbakery client = httpbakery.Client() resp = requests.get('some protected url', cookies=client.cookies, auth=client.auth()) @param interaction_methods A list of Interactor implementations. @param key The private key of the client {bakery.PrivateKey} @param cookies storage for the cookies {CookieJar}. It should be the same as in the requests cookies. If not provided, one will be created. ''' def __init__(self, interaction_methods=None, key=None, cookies=None): if interaction_methods is None: interaction_methods = [WebBrowserInteractor()] if cookies is None: cookies = requests.cookies.RequestsCookieJar() self._interaction_methods = interaction_methods self.key = key self.cookies = cookies def auth(self): '''Return an authorizer object suitable for passing to requests methods that accept one. If a request returns a discharge-required error, the authorizer will acquire discharge macaroons and retry the request. ''' return _BakeryAuth(self) def request(self, method, url, **kwargs): '''Use the requests library to make a request. Using this method is like doing: requests.request(method, url, auth=client.auth()) ''' # TODO should we raise an exception if auth or cookies are explicitly # mentioned in kwargs? kwargs['auth'] = self.auth() kwargs['cookies'] = self.cookies return requests.request(method=method, url=url, **kwargs) def handle_error(self, error, url): '''Try to resolve the given error, which should be a response to the given URL, by discharging any macaroon contained in it. That is, if error.code is ERR_DISCHARGE_REQUIRED then it will try to discharge err.info.macaroon. If the discharge succeeds, the discharged macaroon will be saved to the client's cookie jar, otherwise an exception will be raised. ''' if error.info is None or error.info.macaroon is None: raise BakeryException('unable to read info in discharge error ' 'response') discharges = bakery.discharge_all( error.info.macaroon, self.acquire_discharge, self.key, ) macaroons = '[' + ','.join(map(utils.macaroon_to_json_string, discharges)) + ']' all_macaroons = base64.urlsafe_b64encode(utils.to_bytes(macaroons)) full_path = urljoin(url, error.info.macaroon_path) if error.info.cookie_name_suffix is not None: name = 'macaroon-' + error.info.cookie_name_suffix else: name = 'macaroon-auth' expires = checkers.macaroons_expiry_time(checkers.Namespace(), discharges) self.cookies.set_cookie(utils.cookie( name=name, value=all_macaroons.decode('ascii'), url=full_path, expires=expires, )) def acquire_discharge(self, cav, payload): ''' Request a discharge macaroon from the caveat location as an HTTP URL. @param cav Third party {pymacaroons.Caveat} to be discharged. @param payload External caveat data {bytes}. @return The acquired macaroon {macaroonbakery.Macaroon} ''' resp = self._acquire_discharge_with_token(cav, payload, None) # TODO Fabrice what is the other http response possible ?? if resp.status_code == 200: return bakery.Macaroon.from_dict(resp.json().get('Macaroon')) cause = Error.from_dict(resp.json()) if cause.code != ERR_INTERACTION_REQUIRED: raise DischargeError(cause.message) if cause.info is None: raise DischargeError( 'interaction-required response with no info: {}'.format( resp.json()) ) loc = cav.location if not loc.endswith('/'): loc = loc + '/' token, m = self._interact(loc, cause, payload) if m is not None: # We've acquired the macaroon directly via legacy interaction. return m # Try to acquire the discharge again, but this time with # the token acquired by the interaction method. resp = self._acquire_discharge_with_token(cav, payload, token) if resp.status_code == 200: return bakery.Macaroon.from_dict(resp.json().get('Macaroon')) else: raise DischargeError( 'discharge failed with code {}'.format(resp.status_code)) def _acquire_discharge_with_token(self, cav, payload, token): req = {} _add_json_binary_field(cav.caveat_id_bytes, req, 'id') if token is not None: _add_json_binary_field(token.value, req, 'token') req['token-kind'] = token.kind if payload is not None: req['caveat64'] = base64.urlsafe_b64encode(payload).rstrip( b'=').decode('utf-8') loc = cav.location if not loc.endswith('/'): loc += '/' target = urljoin(loc, 'discharge') headers = { BAKERY_PROTOCOL_HEADER: str(bakery.LATEST_VERSION) } return self.request('POST', target, data=req, headers=headers) def _interact(self, location, error_info, payload): '''Gathers a macaroon by directing the user to interact with a web page. The error_info argument holds the interaction-required error response. @return DischargeToken, bakery.Macaroon ''' if (self._interaction_methods is None or len(self._interaction_methods) == 0): raise InteractionError('interaction required but not possible') # TODO(rogpeppe) make the robust against a wider range of error info. if error_info.info.interaction_methods is None and \ error_info.info.visit_url is not None: # It's an old-style error; deal with it differently. return None, self._legacy_interact(location, error_info) for interactor in self._interaction_methods: found = error_info.info.interaction_methods.get(interactor.kind()) if found is None: continue try: token = interactor.interact(self, location, error_info) except InteractionMethodNotFound: continue if token is None: raise InteractionError('interaction method returned an empty ' 'token') return token, None raise InteractionError('no supported interaction method') def _legacy_interact(self, location, error_info): visit_url = urljoin(location, error_info.info.visit_url) wait_url = urljoin(location, error_info.info.wait_url) method_urls = { "interactive": visit_url } if (len(self._interaction_methods) > 1 or self._interaction_methods[0].kind() != WEB_BROWSER_INTERACTION_KIND): # We have several possible methods or we only support a non-window # method, so we need to fetch the possible methods supported by # the discharger. method_urls = _legacy_get_interaction_methods(visit_url) for interactor in self._interaction_methods: kind = interactor.kind() if kind == WEB_BROWSER_INTERACTION_KIND: # This is the old name for browser-window interaction. kind = "interactive" if not isinstance(interactor, LegacyInteractor): # Legacy interaction mode isn't supported. continue visit_url = method_urls.get(kind) if visit_url is None: continue visit_url = urljoin(location, visit_url) interactor.legacy_interact(self, location, visit_url) return _wait_for_macaroon(wait_url) raise InteractionError('no methods supported; supported [{}]; provided [{}]'.format( ' '.join([x.kind() for x in self._interaction_methods]), ' '.join(method_urls.keys()), )) class _BakeryAuth: '''_BakeryAuth implements an authorizer as required by the requests HTTP client. ''' def __init__(self, client): ''' @param interaction_methods A list of Interactor implementations. @param key The private key of the client (macaroonbakery.PrivateKey) @param cookies storage for the cookies {CookieJar}. It should be the same as in the requests cookies. ''' self._client = client def __call__(self, req): req.headers[BAKERY_PROTOCOL_HEADER] = str(bakery.LATEST_VERSION) hook = _prepare_discharge_hook(req.copy(), self._client) req.register_hook(event='response', hook=hook) return req def _prepare_discharge_hook(req, client): ''' Return the hook function (called when the response is received.) This allows us to intercept the response and do any necessary macaroon discharge before returning. ''' class Retry: # Define a local class so that we can use its class variable as # mutable state accessed by the closures below. count = 0 def hook(response, *args, **kwargs): ''' Requests hooks system, this is the hook for the response. ''' status_code = response.status_code if status_code != 407 and status_code != 401: return response if (status_code == 401 and response.headers.get('WWW-Authenticate') != 'Macaroon'): return response if response.headers.get('Content-Type') != 'application/json': return response errorJSON = response.json() if errorJSON.get('Code') != ERR_DISCHARGE_REQUIRED: return response error = Error.from_dict(errorJSON) Retry.count += 1 if Retry.count >= MAX_DISCHARGE_RETRIES: raise BakeryException('too many ({}) discharge requests'.format( Retry.count) ) client.handle_error(error, req.url) req.headers.pop('Cookie', None) req.prepare_cookies(client.cookies) req.headers[BAKERY_PROTOCOL_HEADER] = \ str(bakery.LATEST_VERSION) with requests.Session() as s: settings = s.merge_environment_settings( req.url, {}, None, None, None) return s.send(req, **settings) return hook def extract_macaroons(headers_or_request): ''' Returns an array of any macaroons found in the given slice of cookies. If the argument implements a get_header method, that will be used instead of the get method to retrieve headers. @param headers_or_request: dict of headers or a urllib.request.Request-like object. @return: A list of list of mpy macaroons ''' def get_header(key, default=None): try: return headers_or_request.get_header(key, default) except AttributeError: return headers_or_request.get(key, default) mss = [] def add_macaroon(data): try: data = utils.b64decode(data) data_as_objs = json.loads(data.decode('utf-8')) except ValueError: return ms = [utils.macaroon_from_dict(x) for x in data_as_objs] mss.append(ms) cookie_header = get_header('Cookie') if cookie_header is not None: cs = SimpleCookie() # The cookie might be a unicode object, so convert it # to ASCII. This may cause an exception under Python 2. # TODO is that a problem? cs.load(str(cookie_header)) for c in cs: if c.startswith('macaroon-'): add_macaroon(cs[c].value) # Python doesn't make it easy to have multiple values for a # key, so split the header instead, which is necessary # for HTTP1.1 compatibility anyway (see RFC 7230, section 3.2.2) macaroon_header = get_header('Macaroons') if macaroon_header is not None: for h in macaroon_header.split(','): add_macaroon(h) return mss def _add_json_binary_field(b, serialized, field): '''' Set the given field to the given val (bytes) in the serialized dictionary. If the value isn't valid utf-8, we base64 encode it and use field+"64" as the field name. ''' try: val = b.decode('utf-8') serialized[field] = val except UnicodeDecodeError: val = base64.b64encode(b).decode('utf-8') serialized[field + '64'] = val def _wait_for_macaroon(wait_url): ''' Returns a macaroon from a legacy wait endpoint. ''' headers = { BAKERY_PROTOCOL_HEADER: str(bakery.LATEST_VERSION) } resp = requests.get(url=wait_url, headers=headers) if resp.status_code != 200: raise InteractionError('cannot get {}'.format(wait_url)) return bakery.Macaroon.from_dict(resp.json().get('Macaroon')) def _legacy_get_interaction_methods(u): ''' Queries a URL as found in an ErrInteractionRequired VisitURL field to find available interaction methods. It does this by sending a GET request to the URL with the Accept header set to "application/json" and parsing the resulting response as a dict. ''' headers = { BAKERY_PROTOCOL_HEADER: str(bakery.LATEST_VERSION), 'Accept': 'application/json' } resp = requests.get(url=u, headers=headers) method_urls = {} if resp.status_code == 200: json_resp = resp.json() for m in json_resp: method_urls[m] = urljoin(u, json_resp[m]) if method_urls.get('interactive') is None: # There's no "interactive" method returned, but we know # the server does actually support it, because all dischargers # are required to, so fill it in with the original URL. method_urls['interactive'] = u return method_urls macaroonbakery-1.3.1/macaroonbakery/httpbakery/_error.py0000644000175000017500000002002713466233337025126 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import json from collections import namedtuple import macaroonbakery.bakery as bakery ERR_INTERACTION_REQUIRED = 'interaction required' ERR_DISCHARGE_REQUIRED = 'macaroon discharge required' class InteractionMethodNotFound(Exception): '''This is thrown by client-side interaction methods when they find that a given interaction isn't supported by the client for a location''' pass class DischargeError(Exception): '''This is thrown by Client when a third party has refused a discharge''' def __init__(self, msg): super(DischargeError, self).__init__( 'third party refused dischargex: {}'.format(msg)) class InteractionError(Exception): '''This is thrown by Client when it fails to deal with an interaction-required error ''' def __init__(self, msg): super(InteractionError, self).__init__( 'cannot start interactive session: {}'.format(msg)) def discharge_required_response(macaroon, path, cookie_suffix_name, message=None): ''' Get response content and headers from a discharge macaroons error. @param macaroon may hold a macaroon that, when discharged, may allow access to a service. @param path holds the URL path to be associated with the macaroon. The macaroon is potentially valid for all URLs under the given path. @param cookie_suffix_name holds the desired cookie name suffix to be associated with the macaroon. The actual name used will be ("macaroon-" + CookieName). Clients may ignore this field - older clients will always use ("macaroon-" + macaroon.signature() in hex) @return content(bytes) and the headers to set on the response(dict). ''' if message is None: message = 'discharge required' content = json.dumps( { 'Code': 'macaroon discharge required', 'Message': message, 'Info': { 'Macaroon': macaroon.to_dict(), 'MacaroonPath': path, 'CookieNameSuffix': cookie_suffix_name }, } ).encode('utf-8') return content, { 'WWW-Authenticate': 'Macaroon', 'Content-Type': 'application/json' } # BAKERY_PROTOCOL_HEADER is the header that HTTP clients should set # to determine the bakery protocol version. If it is 0 or missing, # a discharge-required error response will be returned with HTTP status 407; # if it is greater than 0, the response will have status 401 with the # WWW-Authenticate header set to "Macaroon". BAKERY_PROTOCOL_HEADER = 'Bakery-Protocol-Version' def request_version(req_headers): ''' Determines the bakery protocol version from a client request. If the protocol cannot be determined, or is invalid, the original version of the protocol is used. If a later version is found, the latest known version is used, which is OK because versions are backwardly compatible. @param req_headers: the request headers as a dict. @return: bakery protocol version (for example macaroonbakery.VERSION_1) ''' vs = req_headers.get(BAKERY_PROTOCOL_HEADER) if vs is None: # No header - use backward compatibility mode. return bakery.VERSION_1 try: x = int(vs) except ValueError: # Badly formed header - use backward compatibility mode. return bakery.VERSION_1 if x > bakery.LATEST_VERSION: # Later version than we know about - use the # latest version that we can. return bakery.LATEST_VERSION return x class Error(namedtuple('Error', 'code, message, version, info')): '''This class defines an error value as returned from an httpbakery API. ''' @classmethod def from_dict(cls, serialized): '''Create an error from a JSON-deserialized object @param serialized the object holding the serialized error {dict} ''' # Some servers return lower case field names for message and code. # The Go client is tolerant of this, so be similarly tolerant here. def field(name): return serialized.get(name) or serialized.get(name.lower()) return Error( code=field('Code'), message=field('Message'), info=ErrorInfo.from_dict(field('Info')), version=bakery.LATEST_VERSION, ) def interaction_method(self, kind, x): ''' Checks whether the error is an InteractionRequired error that implements the method with the given name, and JSON-unmarshals the method-specific data into x by calling its from_dict method with the deserialized JSON object. @param kind The interaction method kind (string). @param x A class with a class method from_dict that returns a new instance of the interaction info for the given kind. @return The result of x.from_dict. ''' if self.info is None or self.code != ERR_INTERACTION_REQUIRED: raise InteractionError( 'not an interaction-required error (code {})'.format( self.code) ) entry = self.info.interaction_methods.get(kind) if entry is None: raise InteractionMethodNotFound( 'interaction method {} not found'.format(kind) ) return x.from_dict(entry) class ErrorInfo( namedtuple('ErrorInfo', 'macaroon, macaroon_path, cookie_name_suffix, ' 'interaction_methods, visit_url, wait_url')): ''' Holds additional information provided by an error. @param macaroon may hold a macaroon that, when discharged, may allow access to a service. This field is associated with the ERR_DISCHARGE_REQUIRED error code. @param macaroon_path holds the URL path to be associated with the macaroon. The macaroon is potentially valid for all URLs under the given path. If it is empty, the macaroon will be associated with the original URL from which the error was returned. @param cookie_name_suffix holds the desired cookie name suffix to be associated with the macaroon. The actual name used will be ("macaroon-" + cookie_name_suffix). Clients may ignore this field - older clients will always use ("macaroon-" + macaroon.signature() in hex). @param visit_url holds a URL that the client should visit in a web browser to authenticate themselves. @param wait_url holds a URL that the client should visit to acquire the discharge macaroon. A GET on this URL will block until the client has authenticated, and then it will return the discharge macaroon. ''' __slots__ = () @classmethod def from_dict(cls, serialized): '''Create a new ErrorInfo object from a JSON deserialized dictionary @param serialized The JSON object {dict} @return ErrorInfo object ''' if serialized is None: return None macaroon = serialized.get('Macaroon') if macaroon is not None: macaroon = bakery.Macaroon.from_dict(macaroon) path = serialized.get('MacaroonPath') cookie_name_suffix = serialized.get('CookieNameSuffix') visit_url = serialized.get('VisitURL') wait_url = serialized.get('WaitURL') interaction_methods = serialized.get('InteractionMethods') return ErrorInfo(macaroon=macaroon, macaroon_path=path, cookie_name_suffix=cookie_name_suffix, visit_url=visit_url, wait_url=wait_url, interaction_methods=interaction_methods) def __new__(cls, macaroon=None, macaroon_path=None, cookie_name_suffix=None, interaction_methods=None, visit_url=None, wait_url=None): '''Override the __new__ method so that we can have optional arguments, which namedtuple doesn't allow''' return super(ErrorInfo, cls).__new__( cls, macaroon, macaroon_path, cookie_name_suffix, interaction_methods, visit_url, wait_url) macaroonbakery-1.3.1/macaroonbakery/httpbakery/__init__.py0000644000175000017500000000234413466233337025377 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. from ._client import ( BakeryException, Client, extract_macaroons, ) from ._error import ( BAKERY_PROTOCOL_HEADER, DischargeError, ERR_DISCHARGE_REQUIRED, ERR_INTERACTION_REQUIRED, Error, ErrorInfo, InteractionError, InteractionMethodNotFound, discharge_required_response, request_version, ) from ._keyring import ThirdPartyLocator from ._interactor import ( DischargeToken, Interactor, LegacyInteractor, WEB_BROWSER_INTERACTION_KIND, ) from ._browser import ( WebBrowserInteractionInfo, WebBrowserInteractor, ) from ._discharge import discharge __all__ = [ 'BAKERY_PROTOCOL_HEADER', 'BakeryException', 'Client', 'DischargeError', 'DischargeToken', 'ERR_DISCHARGE_REQUIRED', 'ERR_INTERACTION_REQUIRED', 'Error', 'ErrorInfo', 'InteractionError', 'InteractionMethodNotFound', 'Interactor', 'LegacyInteractor', 'ThirdPartyLocator', 'WEB_BROWSER_INTERACTION_KIND', 'WebBrowserInteractionInfo', 'WebBrowserInteractor', 'discharge', 'discharge_required_response', 'extract_macaroons', 'request_version', ] macaroonbakery-1.3.1/macaroonbakery/_utils/0000755000175000017500000000000013616470550022402 5ustar frankbanfrankban00000000000000macaroonbakery-1.3.1/macaroonbakery/_utils/__init__.py0000644000175000017500000001156013466233337024521 0ustar frankbanfrankban00000000000000# Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. import base64 import binascii import ipaddress import json import webbrowser from datetime import datetime import six from pymacaroons import Macaroon from pymacaroons.serializers import json_serializer import six.moves.http_cookiejar as http_cookiejar from six.moves.urllib.parse import urlparse def to_bytes(s): '''Return s as a bytes type, using utf-8 encoding if necessary. @param s string or bytes @return bytes ''' if isinstance(s, six.binary_type): return s if isinstance(s, six.string_types): return s.encode('utf-8') raise TypeError('want string or bytes, got {}', type(s)) def macaroon_from_dict(json_macaroon): '''Return a pymacaroons.Macaroon object from the given JSON-deserialized dict. @param JSON-encoded macaroon as dict @return the deserialized macaroon object. ''' return Macaroon.deserialize(json.dumps(json_macaroon), json_serializer.JsonSerializer()) def macaroon_to_dict(macaroon): '''Turn macaroon into JSON-serializable dict object @param pymacaroons.Macaroon. ''' return json.loads(macaroon.serialize(json_serializer.JsonSerializer())) def macaroon_to_json_string(macaroon): '''Serialize macaroon object to a JSON-encoded string. @param macaroon object to be serialized. @return a string serialization form of the macaroon. ''' return macaroon.serialize(json_serializer.JsonSerializer()) def _add_base64_padding(b): '''Add padding to base64 encoded bytes. pymacaroons does not give padded base64 bytes from serialization. @param bytes b to be padded. @return a padded bytes. ''' return b + b'=' * (-len(b) % 4) def _remove_base64_padding(b): '''Remove padding from base64 encoded bytes. pymacaroons does not give padded base64 bytes from serialization. @param bytes b to be padded. @return a padded bytes. ''' return b.rstrip(b'=') def b64decode(s): '''Base64 decodes a base64-encoded string in URL-safe or normal format, with or without padding. The argument may be string or bytes. @param s bytes decode @return bytes decoded @raises ValueError on failure ''' # add padding if necessary. s = to_bytes(s) if not s.endswith(b'='): s = s + b'=' * (-len(s) % 4) try: if '_' or '-' in s: return base64.urlsafe_b64decode(s) else: return base64.b64decode(s) except (TypeError, binascii.Error) as e: raise ValueError(str(e)) def raw_urlsafe_b64encode(b): '''Base64 encode using URL-safe encoding with padding removed. @param b bytes to decode @return bytes decoded ''' b = to_bytes(b) b = base64.urlsafe_b64encode(b) b = b.rstrip(b'=') # strip padding return b def visit_page_with_browser(visit_url): '''Open a browser so the user can validate its identity. @param visit_url: where to prove your identity. ''' webbrowser.open(visit_url, new=1) print('Opening an authorization web page in your browser.') print('If it does not open, please open this URL:\n', visit_url, '\n') def cookie( url, name, value, expires=None): '''Return a new Cookie using a slightly more friendly API than that provided by six.moves.http_cookiejar @param name The cookie name {str} @param value The cookie value {str} @param url The URL path of the cookie {str} @param expires The expiry time of the cookie {datetime}. If provided, it must be a naive timestamp in UTC. ''' u = urlparse(url) domain = u.hostname if '.' not in domain and not _is_ip_addr(domain): domain += ".local" port = str(u.port) if u.port is not None else None secure = u.scheme == 'https' if expires is not None: if expires.tzinfo is not None: raise ValueError('Cookie expiration must be a naive datetime') expires = (expires - datetime(1970, 1, 1)).total_seconds() return http_cookiejar.Cookie( version=0, name=name, value=value, port=port, port_specified=port is not None, domain=domain, domain_specified=True, domain_initial_dot=False, path=u.path, path_specified=True, secure=secure, expires=expires, discard=False, comment=None, comment_url=None, rest=None, rfc2109=False, ) def _is_ip_addr(h): if six.PY2: # the python2.7 backport of ipaddr needs a bytestring passed in try: h = h.decode('ascii') except UnicodeDecodeError: # If there are non-ascii chars it's not an address anyway return False try: ipaddress.ip_address(h) except ValueError: return False return True macaroonbakery-1.3.1/macaroonbakery/__init__.py0000644000175000017500000000000013466233337023205 0ustar frankbanfrankban00000000000000