commit 7c702e5df910075d945bc924e58e74eb569e28d5 Author: Andrew Svetlov Date: Wed Jul 8 20:30:24 2015 +0300 Initial commit diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..5c2d373 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = aiohttp_security, tests +omit = site-packages + +[html] +directory = coverage \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8af43e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +include/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +coverage \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b75ea72 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +language: python +python: + - 3.3 + - 3.4 + +install: +- pip install --upgrade setuptools +- pip install pyflakes +- pip install pep8 +- pip install coveralls --use-mirrors +- pip install aiohttp +- python setup.py develop + +script: +- pyflakes aiohttp_security tests +- pep8 aiohttp_security tests +- coverage run --source=aiohttp_security setup.py test + +after_success: +- coveralls + +env: + matrix: + - PYTHONASYNCIODEBUG=1 + - PYTHONASYNCIODEBUG=0 diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..9f43a99 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,2 @@ +Changes +======= diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e06d208 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a782364 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +# Some simple testing tasks (sorry, UNIX only). + +FLAGS= + + +flake: + flake8 aiohttp_security tests + + +test: flake + py.test -s $(FLAGS) ./tests/ + +vtest: flake develop + py.test -s -v $(FLAGS) ./tests/ + +cov cover coverage: flake + @coverage erase + @coverage run -m py.test -s $(FLAGS) tests + @coverage report + @coverage html + @echo "open file://`pwd`/coverage/index.html" + +clean: + rm -rf `find . -name __pycache__` + rm -f `find . -type f -name '*.py[co]' ` + rm -f `find . -type f -name '*~' ` + rm -f `find . -type f -name '.*~' ` + rm -f `find . -type f -name '@*' ` + rm -f `find . -type f -name '#*#' ` + rm -f `find . -type f -name '*.orig' ` + rm -f `find . -type f -name '*.rej' ` + rm -f .coverage + rm -rf coverage + rm -rf build + rm -rf cover + # make -C docs clean + python setup.py clean + +doc: + make -C docs html + @echo "open file://`pwd`/docs/_build/html/index.html" + +.PHONY: all build venv flake test vtest testloop cov clean doc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..fa83e86 --- /dev/null +++ b/README.rst @@ -0,0 +1,17 @@ +aiohttp_security +================ + +The library provides identity and autorization for `aiohttp.web`__. + +.. _aiohttp_web: http://aiohttp.readthedocs.org/en/latest/web.html + +__ aiohttp_web_ + +Usage +----- + + +License +------- + +``aiohttp_security`` is offered under the Apache 2 license. diff --git a/aiohttp_security/__init__.py b/aiohttp_security/__init__.py new file mode 100644 index 0000000..5218b0d --- /dev/null +++ b/aiohttp_security/__init__.py @@ -0,0 +1,10 @@ +__version__ = '0.1.0' + + +from .abc import AbstractIdentityPolicy, AbstractAuthorizationPolicy +from .api import remember, forget, setup, authorized_userid, permits + + +__all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy', + 'remember', 'forget', 'authorized_userid', + 'permits', 'setup') diff --git a/aiohttp_security/abc.py b/aiohttp_security/abc.py new file mode 100644 index 0000000..df064b8 --- /dev/null +++ b/aiohttp_security/abc.py @@ -0,0 +1,50 @@ +import abc +import asyncio + +# see http://plope.com/pyramid_auth_design_api_postmortem + + +class AbstractIdentityPolicy(metaclass=abc.ABCMeta): + + @asyncio.coroutine + @abc.abstractmethod + def identify(self, request): + """ Return the claimed identity of the user associated request or + ``None`` if no identity can be found associated with the request.""" + pass + + @asyncio.coroutine + @abc.abstractmethod + def remember(self, request, identity, **kwargs): + """Remember identity. + + Return MultiDict with headers on this request's response. + + An individual identity policy and its consumers can decide on + the composition and meaning of **kw. + """ + pass + + @asyncio.coroutine + @abc.abstractmethod + def forget(self, request): + """ Modify request.response which can be used to 'forget' the + current identity on subsequent requests.""" + pass + + +class AbstractAuthorizationPolicy(metaclass=abc.ABCMeta): + + @asyncio.coroutine + @abc.abstractmethod + def permits(self, identity, permission, context=None): + """ Return True if the identity is allowed the permission in the + current context, else return False""" + pass + + @asyncio.coroutine + @abc.abstractmethod + def authorized_userid(self, identity): + """ Return the user_id of the user identified by the identity + or 'None' if no user exists related to the identity """ + pass diff --git a/aiohttp_security/api.py b/aiohttp_security/api.py new file mode 100644 index 0000000..b7b8e49 --- /dev/null +++ b/aiohttp_security/api.py @@ -0,0 +1,46 @@ +import asyncio +from aiohttp_security.abc import (AbstractIdentityPolicy, + AbstractAuthorizationPolicy) + +IDENTITY_KEY = 'aiohttp_security_identity_policy' +AUTZ_KEY = 'aiohttp_security_autz_policy' + + +@asyncio.coroutine +def remember(request, identity, **kwargs): + identity_policy = request.app[IDENTITY_KEY] + headers = yield from identity_policy.remember(request, identity, **kwargs) + return headers + + +@asyncio.coroutine +def forget(request): + identity_policy = request.app[IDENTITY_KEY] + headers = yield from identity_policy.forget(request) + return headers + + +@asyncio.coroutine +def authorized_userid(request): + identity_policy = request.app[IDENTITY_KEY] + autz_policy = request.app[AUTZ_KEY] + identity = yield from identity_policy.identify(request) + user_id = yield from autz_policy.authorized_userid(identity) + return user_id + + +@asyncio.coroutine +def permits(request, permission, context=None): + identity_policy = request.app[IDENTITY_KEY] + autz_policy = request.app[AUTZ_KEY] + identity = yield from identity_policy.identify(request) + access = yield from autz_policy.permits(identity, permission, context) + return access + + +def setup(app, identity_policy, auth_policy): + assert isinstance(identity_policy, AbstractIdentityPolicy), identity_policy + assert isinstance(auth_policy, AbstractAuthorizationPolicy), auth_policy + + app[IDENTITY_KEY] = identity_policy + app[AUTZ_KEY] = auth_policy diff --git a/aiohttp_security/cookies_identity.py b/aiohttp_security/cookies_identity.py new file mode 100644 index 0000000..cf829f6 --- /dev/null +++ b/aiohttp_security/cookies_identity.py @@ -0,0 +1,40 @@ +import asyncio +import http.cookies + +from aiohttp import hdrs, CIMultiDict + +from .abc import AbstractIdentityPolicy + + +class CookiesIdentityPolicy(AbstractIdentityPolicy): + + def __init__(self): + self._cookie_name = 'AIOHTTP_SECURITY' + self._max_age = 30 * 24 * 3600 + + @asyncio.coroutine + def identify(self, request): + identity = request.cookies.get(self._cookie_name) + return identity + + @asyncio.coroutine + def remember(self, request, identity, **kwargs): + cookies = http.cookies.SimpleCookie() + max_age = kwargs.pop('max_age', self._max_age) + cookies[self._cookie_name] = identity + cookie = cookies[self._cookie_name] + cookie['max-age'] = max_age + cookie.update(kwargs) + + value = cookie.output(header='')[1:] + result = CIMultiDict({hdrs.SET_COOKIE: value}) + return result + + @asyncio.coroutine + def forget(self, request): + cookies = http.cookies.SimpleCookie() + cookies[self._cookie_name] = '' + cookie = cookies[self._cookie_name] + value = cookie.output(header='')[1:] + result = CIMultiDict({hdrs.SET_COOKIE: value}) + return result diff --git a/aiohttp_security/examples/__init__.py b/aiohttp_security/examples/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/aiohttp_security/examples/__init__.py @@ -0,0 +1 @@ +# diff --git a/aiohttp_security/examples/dict_auth.py b/aiohttp_security/examples/dict_auth.py new file mode 100644 index 0000000..b28dacd --- /dev/null +++ b/aiohttp_security/examples/dict_auth.py @@ -0,0 +1,21 @@ +import asyncio + +from aiohttp_security.authorization import AbstractAuthorizationPolicy + + +class DictionaryAuthorizationPolicy(AbstractAuthorizationPolicy): + def __init__(self, data): + self.data = data + + @asyncio.coroutine + def permits(self, identity, permission, context=None): + record = self.data.get(identity) + if record is not None: + # TODO: implement actual permission checker + if permission in record: + return True + return False + + @asyncio.coroutine + def authorized_user_id(self, identity): + return identity if identity in self.data else None diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..c7a36b9 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,192 @@ +# 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 coverage 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 " applehelp to make an Apple Help Book" + @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)" + @echo " coverage to run coverage check of 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/aiohttp_security.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiohttp_security.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/aiohttp_security" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiohttp_security" + @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." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.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." diff --git a/docs/_static/aiohttp-icon-128x128.png b/docs/_static/aiohttp-icon-128x128.png new file mode 100644 index 0000000..1a3c949 Binary files /dev/null and b/docs/_static/aiohttp-icon-128x128.png differ diff --git a/docs/aiohttp-icon.ico b/docs/aiohttp-icon.ico new file mode 100644 index 0000000..56b6e56 Binary files /dev/null and b/docs/aiohttp-icon.ico differ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..b4cfef5 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# aiohttp_security documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 14 11:54:09 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import codecs +import re + +_docs_path = os.path.dirname(__file__) +_version_path = os.path.abspath(os.path.join(_docs_path, + '..', + 'aiohttp_security', + '__init__.py')) +with codecs.open(_version_path, 'r', 'latin1') as fp: + try: + _version_info = re.search(r"^__version__ = '" + r"(?P\d+)" + r"\.(?P\d+)" + r"\.(?P\d+)" + r"(?P.*)?'$", + fp.read(), re.M).groupdict() + except IndexError: + raise RuntimeError('Unable to determine version.') + + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) + +import alabaster + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'alabaster', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'aiohttp_security' +copyright = '' +author = 'eleddy' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '{major}.{minor}'.format(**_version_info) +# The full version, including alpha/beta/rc tags. +release = '{major}.{minor}.{patch}-{tag}'.format(**_version_info) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + 'logo': 'aiohttp-icon-128x128.png', + 'description': 'Authorization and identity for aoihttp', + 'github_user': 'aio-libs', + 'github_repo': 'aiohttp_security', + 'github_button': True, + 'github_banner': True, + 'travis_button': True, + 'pre_bg': '#FFF6E5', + 'note_bg': '#E5ECD1', + 'note_border': '#BFCF8C', + 'body_text': '#482C0A', + 'sidebar_text': '#49443E', + 'sidebar_header': '#4B4032', +} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = [alabaster.get_path()] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = 'aiohttp-icon.ico' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +html_sidebars = { + '**': [ + 'about.html', 'navigation.html', 'searchbox.html', + ] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'aiohttp_securitydoc' + +# -- Options for LaTeX output --------------------------------------------- + +# The paper size ('letterpaper' or 'a4paper'). +# 'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +# 'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +# 'preamble': '', + +# Latex figure (float) alignment +# 'figure_align': 'htbp', +latex_elements = { +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'aiohttp_security.tex', 'aiohttp\\_security Documentation', + 'Andrew Svetlov', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'aiohttp_security', 'aiohttp_security Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'aiohttp_security', 'aiohttp_security Documentation', + author, 'aiohttp_security', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/3': None, + 'http://aiohttp.readthedocs.org/en/stable': None} diff --git a/docs/example.rst b/docs/example.rst new file mode 100644 index 0000000..eb88231 --- /dev/null +++ b/docs/example.rst @@ -0,0 +1,71 @@ +How to Make a Simple Server With Authorization +============================================== + + +.. code::python + + import asyncio + from aiohttp import web + + @asyncio.coroutine + def root_handler(request): + text = "Alive and kicking!" + return web.Response(body=text.encode('utf-8')) + + # option 2: auth at a higher level? + # set user_id and allowed in the wsgo handler + @protect('view_user') + @asyncio.coroutine + def user_handler(request): + name = request.match_info.get('name', "Anonymous") + text = "Hello, " + name + return web.Response(body=text.encode('utf-8')) + + + # option 3: super low + # wsgi doesn't do anything + @asyncio.coroutine + def user_update_handler(request): + # identity, asked_permission + user_id = yield from identity_policy.identify(request) + identity = yield from auth_policy.authorized_user_id(user_id) + allowed = yield from request.auth_policy.permits( + identity, asked_permission + ) + if not allowed: + # how is this pluggable as well? + # ? return NotAllowedStream() + raise NotAllowedResponse() + + update_user() + + @asyncio.coroutine + def init(loop): + # set up identity and auth + auth_policy = DictionaryAuthorizationPolicy({'me': ('view_user',), + 'you': ('view_user', + 'edit_user',)}) + identity_policy = CookieIdentityPolicy() + auth = authorization_middleware(auth_policy, identity_policy) + + # wsgi app + app = web.Application(loop=loop, middlewares=*auth) + + # add the routes + app.router.add_route('GET', '/', root_handler) + app.router.add_route('GET', '/{user}', user_handler) + app.router.add_route('GET', '/{user}/edit', user_update_handler) + + # get it started + srv = yield from loop.create_server(app.make_handler(), + '127.0.0.1', 8080) + print("Server started at http://127.0.0.1:8080") + return srv + + + loop = asyncio.get_event_loop() + loop.run_until_complete(init(loop)) + try: + loop.run_forever() + except KeyboardInterrupt: + pass # TODO put handler cleanup here diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 0000000..0019f90 --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,16 @@ +.. _aiohttp-security-glossary: + +========== + Glossary +========== + +.. if you add new entries, keep the alphabetical sorting! + +.. glossary:: + + + session + + A namespace that is valid for some period of continual activity + that can be used to represent a user's interaction with a web + application. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..a008f6c --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,30 @@ +aiohttp_security +================ + +The library provides security for :ref:`aiohttp.web`. + +Usage +----- + + +License +------- + +``aiohttp_security`` is offered under the Apache 2 license. + +Contents: + +.. toctree:: + :maxdepth: 2 + + reference + glossary + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..ea08229 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :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. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over 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 + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 2> nul +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aiohttp_security.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aiohttp_security.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 0000000..4e0cc7f --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,16 @@ +.. _aiohttp-security-reference: + + +=========== + Reference +=========== + +.. module:: aiohttp_security +.. currentmodule:: aiohttp_security +.. highlight:: python + + +Public functions +================ + + diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..de46db5 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,8 @@ +. +flake8 +pytest +coverage +sphinx +alabaster>=0.6.2 +pep257 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..735c2a1 --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +import codecs +from setuptools import setup, find_packages +import os +import re + + +with codecs.open(os.path.join(os.path.abspath(os.path.dirname( + __file__)), 'aiohttp_security', '__init__.py'), 'r', 'latin1') as fp: + try: + version = re.findall(r"^__version__ = '([^']+)'$", fp.read(), re.M)[0] + except IndexError: + raise RuntimeError('Unable to determine version.') + + +def read(f): + return open(os.path.join(os.path.dirname(__file__), f)).read().strip() + +install_requires = ['aiohttp>=0.14'] +tests_require = install_requires + ['nose'] +extras_require = {} + +setup(name='aiohttp_security', + version=version, + description=("security for aiohttp.web"), + long_description='\n\n'.join((read('README.rst'), read('CHANGES.txt'))), + classifiers=[ + 'License :: OSI Approved :: Apache Software License', + 'Intended Audience :: Developers', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Topic :: Internet :: WWW/HTTP'], + author='eleddy', + author_email='', + url='https://github.com/aio-libs/aiohttp_security/', + license='Apache 2', + packages=find_packages(), + install_requires=install_requires, + tests_require=tests_require, + test_suite='nose.collector', + include_package_data=True, + extras_require=extras_require) diff --git a/tests/test_cookies_identity.py b/tests/test_cookies_identity.py new file mode 100644 index 0000000..6845760 --- /dev/null +++ b/tests/test_cookies_identity.py @@ -0,0 +1,148 @@ +import asyncio +import socket +import unittest + +import aiohttp +from aiohttp import web +from aiohttp_security import (remember, setup, forget, + AbstractAuthorizationPolicy) +from aiohttp_security.cookies_identity import CookiesIdentityPolicy +from aiohttp_security.api import IDENTITY_KEY + + +class Autz(AbstractAuthorizationPolicy): + + @asyncio.coroutine + def permits(self, identity, permission, context=None): + pass + + @asyncio.coroutine + def authorized_userid(self, identity): + pass + + +class TestCookiesIdentity(unittest.TestCase): + + def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + self.client = aiohttp.ClientSession(loop=self.loop) + + def tearDown(self): + self.client.close() + self.loop.run_until_complete(self.handler.finish_connections()) + self.srv.close() + self.loop.run_until_complete(self.srv.wait_closed()) + self.loop.close() + + def find_unused_port(self): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('127.0.0.1', 0)) + port = s.getsockname()[1] + s.close() + return port + + @asyncio.coroutine + def create_server(self): + app = web.Application(loop=self.loop) + setup(app, CookiesIdentityPolicy(), Autz()) + + port = self.find_unused_port() + self.handler = app.make_handler( + debug=True, keep_alive_on=False) + srv = yield from self.loop.create_server( + self.handler, '127.0.0.1', port) + url = "http://127.0.0.1:{}".format(port) + self.srv = srv + return app, srv, url + + def test_remember(self): + + @asyncio.coroutine + def handler(request): + response = web.Response() + hdrs = yield from remember(request, 'Andrew') + response.headers.extend(hdrs) + return response + + @asyncio.coroutine + def go(): + app, srv, url = yield from self.create_server() + app.router.add_route('GET', '/', handler) + resp = yield from self.client.get(url+'/') + self.assertEqual(200, resp.status) + self.assertEqual('Andrew', + self.client.cookies['AIOHTTP_SECURITY'].value) + yield from resp.release() + + self.loop.run_until_complete(go()) + + def test_identify(self): + + @asyncio.coroutine + def create(request): + response = web.Response() + hdrs = yield from remember(request, 'Andrew') + response.headers.extend(hdrs) + return response + + @asyncio.coroutine + def check(request): + policy = request.app[IDENTITY_KEY] + user_id = yield from policy.identify(request) + self.assertEqual('Andrew', user_id) + return web.Response() + + @asyncio.coroutine + def go(): + app, srv, url = yield from self.create_server() + app.router.add_route('GET', '/', check) + app.router.add_route('POST', '/', create) + resp = yield from self.client.post(url+'/') + self.assertEqual(200, resp.status) + yield from resp.release() + resp = yield from self.client.get(url+'/') + self.assertEqual(200, resp.status) + yield from resp.release() + + self.loop.run_until_complete(go()) + + def test_forget(self): + + @asyncio.coroutine + def index(request): + return web.Response() + + @asyncio.coroutine + def login(request): + response = web.HTTPFound(location='/') + hdrs = yield from remember(request, 'Andrew') + response.headers.extend(hdrs) + return response + + @asyncio.coroutine + def logout(request): + response = web.HTTPFound(location='/') + hdrs = yield from forget(request) + response.headers.extend(hdrs) + return response + + @asyncio.coroutine + def go(): + app, srv, url = yield from self.create_server() + app.router.add_route('GET', '/', index) + app.router.add_route('POST', '/login', login) + app.router.add_route('POST', '/logout', logout) + resp = yield from self.client.post(url+'/login') + self.assertEqual(200, resp.status) + self.assertEqual(url+'/', resp.url) + self.assertEqual('Andrew', + self.client.cookies['AIOHTTP_SECURITY'].value) + yield from resp.release() + resp = yield from self.client.post(url+'/logout') + self.assertEqual(200, resp.status) + self.assertEqual(url+'/', resp.url) + self.assertEqual('', self.client.cookies['AIOHTTP_SECURITY'].value) + yield from resp.release() + + self.loop.run_until_complete(go()) diff --git a/tests/test_dict_autz.py b/tests/test_dict_autz.py new file mode 100644 index 0000000..6c689ca --- /dev/null +++ b/tests/test_dict_autz.py @@ -0,0 +1,161 @@ +import asyncio +import socket +import unittest + +import aiohttp +from aiohttp import web +from aiohttp_security import (remember, setup, + authorized_userid, permits, + AbstractAuthorizationPolicy) +from aiohttp_security.cookies_identity import CookiesIdentityPolicy + + +class Autz(AbstractAuthorizationPolicy): + + @asyncio.coroutine + def permits(self, identity, permission, context=None): + if identity == 'UserID': + return permission in {'read', 'write'} + else: + return False + + @asyncio.coroutine + def authorized_userid(self, identity): + if identity == 'UserID': + return 'Andrew' + else: + return None + + +class TestCookiesIdentity(unittest.TestCase): + + def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + self.client = aiohttp.ClientSession(loop=self.loop) + + def tearDown(self): + self.client.close() + self.loop.run_until_complete(self.handler.finish_connections()) + self.srv.close() + self.loop.run_until_complete(self.srv.wait_closed()) + self.loop.close() + + def find_unused_port(self): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('127.0.0.1', 0)) + port = s.getsockname()[1] + s.close() + return port + + @asyncio.coroutine + def create_server(self): + app = web.Application(loop=self.loop) + setup(app, CookiesIdentityPolicy(), Autz()) + + port = self.find_unused_port() + self.handler = app.make_handler( + debug=True, keep_alive_on=False) + srv = yield from self.loop.create_server( + self.handler, '127.0.0.1', port) + url = "http://127.0.0.1:{}".format(port) + self.srv = srv + return app, srv, url + + def test_authorized_userid(self): + + @asyncio.coroutine + def login(request): + response = web.HTTPFound(location='/') + hdrs = yield from remember(request, 'UserID') + response.headers.extend(hdrs) + return response + + @asyncio.coroutine + def check(request): + userid = yield from authorized_userid(request) + self.assertEqual('Andrew', userid) + return web.Response(text=userid) + + @asyncio.coroutine + def go(): + app, srv, url = yield from self.create_server() + app.router.add_route('GET', '/', check) + app.router.add_route('POST', '/login', login) + resp = yield from self.client.post(url+'/login') + self.assertEqual(200, resp.status) + txt = yield from resp.text() + self.assertEqual('Andrew', txt) + yield from resp.release() + + self.loop.run_until_complete(go()) + + def test_authorized_userid_not_authorized(self): + + @asyncio.coroutine + def check(request): + userid = yield from authorized_userid(request) + self.assertIsNone(userid) + return web.Response() + + @asyncio.coroutine + def go(): + app, srv, url = yield from self.create_server() + app.router.add_route('GET', '/', check) + resp = yield from self.client.get(url+'/') + self.assertEqual(200, resp.status) + yield from resp.release() + + self.loop.run_until_complete(go()) + + def test_permits(self): + + @asyncio.coroutine + def login(request): + response = web.HTTPFound(location='/') + hdrs = yield from remember(request, 'UserID') + response.headers.extend(hdrs) + return response + + @asyncio.coroutine + def check(request): + ret = yield from permits(request, 'read') + self.assertTrue(ret) + ret = yield from permits(request, 'write') + self.assertTrue(ret) + ret = yield from permits(request, 'unknown') + self.assertFalse(ret) + return web.Response() + + @asyncio.coroutine + def go(): + app, srv, url = yield from self.create_server() + app.router.add_route('GET', '/', check) + app.router.add_route('POST', '/login', login) + resp = yield from self.client.post(url+'/login') + self.assertEqual(200, resp.status) + yield from resp.release() + + self.loop.run_until_complete(go()) + + def test_permits_unauthorized(self): + + @asyncio.coroutine + def check(request): + ret = yield from permits(request, 'read') + self.assertFalse(ret) + ret = yield from permits(request, 'write') + self.assertFalse(ret) + ret = yield from permits(request, 'unknown') + self.assertFalse(ret) + return web.Response() + + @asyncio.coroutine + def go(): + app, srv, url = yield from self.create_server() + app.router.add_route('GET', '/', check) + resp = yield from self.client.get(url+'/') + self.assertEqual(200, resp.status) + yield from resp.release() + + self.loop.run_until_complete(go())