Compare commits
168 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0346ec8b6d | ||
|
2f6bf14120 | ||
|
aa184593f8 | ||
|
cfa326b502 | ||
|
3836f750bc | ||
|
117eb19e1e | ||
|
b3b5367460 | ||
|
2247eb72f9 | ||
|
75f317fe2d | ||
|
2c17f8eb7a | ||
|
4623aa0cca | ||
|
101c19f8c9 | ||
|
9131976657 | ||
|
ba6d455e8d | ||
|
ae2d4d8d25 | ||
|
57893dc274 | ||
|
15068563cc | ||
|
b78b525864 | ||
|
235330ae1e | ||
|
19c044f6ad | ||
|
8662c2ac8f | ||
|
c85f3b660d | ||
|
9bb8c41786 | ||
|
1751e8bf55 | ||
|
06ef997a7c | ||
|
65c94f624f | ||
|
cb0aa10a25 | ||
|
67816eb354 | ||
|
9aa6c4eea3 | ||
|
e41852d763 | ||
|
80728460b9 | ||
|
f86069281f | ||
|
2b90dc7be2 | ||
|
ea83d1c4e4 | ||
|
05eef37ac6 | ||
|
1b34d7de74 | ||
|
cb242e9c09 | ||
|
d903ffda50 | ||
|
2bc9eb88b1 | ||
|
106c322b1a | ||
|
e3c6d00665 | ||
|
78867ce3c1 | ||
|
0133d23d4a | ||
|
e00a5eeb49 | ||
|
323d28b625 | ||
|
c6a8fd5a0e | ||
|
94ee8c2348 | ||
|
84277f5f2e | ||
|
c88e176c07 | ||
|
2ffed0380f | ||
|
f24a9b2914 | ||
|
52420cd2ba | ||
|
ac4bd3620f | ||
|
46d13348e8 | ||
|
aea0d5ba21 | ||
|
72e9a80fee | ||
|
09f7b7e855 | ||
|
0729ca10c8 | ||
|
173da83582 | ||
|
7494e75fd1 | ||
|
e5bd591a17 | ||
|
50ed7703c1 | ||
|
02dc750bc7 | ||
|
c36a654393 | ||
|
85df85a1c2 | ||
|
859689f82c | ||
|
e4bb720132 | ||
|
ee8b55af0f | ||
|
f6bb8f1564 | ||
|
56cc1b46be | ||
|
6599dd4250 | ||
|
9ea0ae319a | ||
|
14c03dba69 | ||
|
fd0228240a | ||
|
88ecee7ee1 | ||
|
cc1eaf6c92 | ||
|
74281bd7e4 | ||
|
9f9ca363c9 | ||
|
8dfe89594a | ||
|
eedb79b560 | ||
|
139d8d9974 | ||
|
e3eecf1bd0 | ||
|
e5ce21edcf | ||
|
21bba9ec12 | ||
|
1a80a79b69 | ||
|
ce30bd4544 | ||
|
99408d151e | ||
|
11c5b691b7 | ||
|
0ae8668ad2 | ||
|
f58d9b5c7a | ||
|
7e15f918b7 | ||
|
0b16e3a745 | ||
|
c5f4c63892 | ||
|
94c7af6c8d | ||
|
775b1ccffc | ||
|
8e2d1bdf6b | ||
|
be08d06057 | ||
|
1bcf612222 | ||
|
96ee5d2cb3 | ||
|
534043e109 | ||
|
4572e2c23c | ||
|
0cd61422fc | ||
|
5667dfd0e1 | ||
|
cb300bb5ee | ||
|
a25d215b43 | ||
|
74345a25ef | ||
|
3cd857dca8 | ||
|
9007a7ca55 | ||
|
b87e58efe7 | ||
|
ba6fbd0708 | ||
|
342991c21d | ||
|
c3e7a4c8bb | ||
|
c81b89a9e7 | ||
|
30b07a6033 | ||
|
e745334204 | ||
|
05ebfe1142 | ||
|
22a3bd36e1 | ||
|
901cf1a7e0 | ||
|
82cff61379 | ||
|
cb0e17d9c5 | ||
|
27c95969c2 | ||
|
0b2e3cd6c0 | ||
|
dab8b6f248 | ||
|
460ddfd50c | ||
|
354b1a8995 | ||
|
9b428003d0 | ||
|
124c83ee24 | ||
|
74381e8cc2 | ||
|
fdf8a39607 | ||
|
bdb64ed010 | ||
|
9b1d08c661 | ||
|
097f7ecc43 | ||
|
ca38b38a0e | ||
|
bb63715634 | ||
|
79e811ea79 | ||
|
4c824c34c1 | ||
|
42769df454 | ||
|
ff2171d6c5 | ||
|
6638432aac | ||
|
78e752e65f | ||
|
f8dcc4a6de | ||
|
27ffe6dc3c | ||
|
29166f4743 | ||
|
b982e2136c | ||
|
797d892ac1 | ||
|
c5c39ce2b8 | ||
|
46e50fede8 | ||
|
c172da2b42 | ||
|
6e0cedaac7 | ||
|
475b666a8e | ||
|
363f3b71c0 | ||
|
9da55517b2 | ||
|
d375b22f1b | ||
|
4506c306a7 | ||
|
9b9b848fdd | ||
|
1679f6713b | ||
|
f9628b0ac1 | ||
|
f8940c0696 | ||
|
5d1195b85d | ||
|
8360095011 | ||
|
d89f6b7e3d | ||
|
db7dbd9b07 | ||
|
5b2ff779c3 | ||
|
b9dee120c3 | ||
|
aff9a6a915 | ||
|
92e6fec6f5 | ||
|
810312b508 | ||
|
18a04b879e |
38
.github/workflows/ci.yaml
vendored
Normal file
38
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mypy:
|
||||||
|
name: Check annotations with Mypy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- run: pip install aiohttp mypy
|
||||||
|
- run: mypy
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
pip install codecov
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
make coverage
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
run: |
|
||||||
|
codecov
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -55,4 +55,5 @@ docs/_build/
|
|||||||
# PyBuilder
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
|
.pytest_cache
|
37
.mypy.ini
Normal file
37
.mypy.ini
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[mypy]
|
||||||
|
files = aiohttp_security, demo, tests
|
||||||
|
check_untyped_defs = True
|
||||||
|
follow_imports_for_stubs = True
|
||||||
|
disallow_any_decorated = True
|
||||||
|
disallow_any_generics = True
|
||||||
|
disallow_incomplete_defs = True
|
||||||
|
disallow_subclassing_any = True
|
||||||
|
disallow_untyped_calls = True
|
||||||
|
disallow_untyped_decorators = True
|
||||||
|
disallow_untyped_defs = True
|
||||||
|
implicit_reexport = False
|
||||||
|
no_implicit_optional = True
|
||||||
|
show_error_codes = True
|
||||||
|
strict_equality = True
|
||||||
|
warn_incomplete_stub = True
|
||||||
|
warn_redundant_casts = True
|
||||||
|
warn_unreachable = True
|
||||||
|
warn_unused_ignores = True
|
||||||
|
disallow_any_unimported = True
|
||||||
|
warn_return_any = True
|
||||||
|
|
||||||
|
[mypy-aiohttp_security.abc.*]
|
||||||
|
disallow_any_decorated = False
|
||||||
|
|
||||||
|
[mypy-tests.*]
|
||||||
|
disallow_any_decorated = False
|
||||||
|
disallow_untyped_defs = False
|
||||||
|
|
||||||
|
[mypy-aiopg.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-aioredis.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-passlib.*]
|
||||||
|
ignore_missing_imports = True
|
34
.travis.yml
34
.travis.yml
@@ -1,34 +0,0 @@
|
|||||||
language: python
|
|
||||||
python:
|
|
||||||
- 3.4.3
|
|
||||||
- 3.5
|
|
||||||
- 3.6
|
|
||||||
- 3.7-dev
|
|
||||||
- nightly
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install --upgrade pip
|
|
||||||
- pip install -r requirements-dev.txt
|
|
||||||
- pip install codecov
|
|
||||||
|
|
||||||
script:
|
|
||||||
- make coverage
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- codecov
|
|
||||||
|
|
||||||
env:
|
|
||||||
matrix:
|
|
||||||
- PYTHONASYNCIODEBUG=x
|
|
||||||
- PYTHONASYNCIODEBUG=
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
provider: pypi
|
|
||||||
user: andrew.svetlov
|
|
||||||
password:
|
|
||||||
secure: "JdBvuOBA/198ognVDOY/qZpIKGXfCx47725kyJo/SpQ3nP+x0GLZb3PMQkR0jfSWWkx6Sisk3vOCYsoWclPyPzp+o4ZpfM8yAjHNFmtbr+k+XJdUEApEiWb6/Y3g7DCyY2Qa/L8IYlyABPWrrJI/nld2sKm5kmhFpR/z3HfeFtINP6Ivp34dUOkeRP6kOvCi9d6GyWnvTRnhlybAnk/Ngrroh8XrbKHdDv0zkQkshF8+pmxVzwao4C6S5ld5cFXIYZHLBA9lNC3zgvOMuFeGPUEN9vab3q77MvaiMIuTC9QjcgIhfw3gabH2u7knqfFzqqzXMaVptx5z8o1JtsxMyYt5NVBqS4NPIljpZjaoS/CASHJlRxniJiYfjvjOtFEcfGMNtZj8ZYsGR0nuP2jwzgpEHHWIs4qL0Y8h9t7pGirxCuQcnY10sr+Y+JKaZNJsugNLgbqE2aaZUye5gjDcEj9WY8kKNZXucLP7c0McJuwPqplDEO4CQouMttcKSYkA0QoETmpAFqaXCaMs3p/glOoU2ZyHSH9mXWir69yo84ymb2NlGPMTAstXlv/g/oLmLMSq7lbl6cSUnO1/wxBGlyfv5AAq/75YUaqsgYofzN5CjUgA3m6NedvbWxLUJaxVQ7nduYGEQKDvGEBmzCNv6CdVRCjQ9J1xX3XzkVheQGc="
|
|
||||||
distributions: "sdist bdist_wheel"
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
all_branches: true
|
|
||||||
python: 3.6
|
|
26
CHANGES.txt
26
CHANGES.txt
@@ -1,6 +1,32 @@
|
|||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
0.4.0 (2018-09-27)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Bump minimal supported ``aiohttp`` version to 3.2
|
||||||
|
|
||||||
|
- Use ``request.config_dict`` for accessing ``jinja2`` environment. It
|
||||||
|
allows to reuse jinja rendering engine from parent application.
|
||||||
|
|
||||||
|
0.3.0 (2018-09-06)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Deprecate ``login_required`` and ``has_permission`` decorators.
|
||||||
|
Use ``check_authorized`` and ``check_permission`` helper functions instead.
|
||||||
|
|
||||||
|
- Bump supported ``aiohttp`` version to 3.0+
|
||||||
|
|
||||||
|
- Enable strong warnings mode for test suite, clean-up all deprecation
|
||||||
|
warnings.
|
||||||
|
|
||||||
|
- Polish documentation
|
||||||
|
|
||||||
|
0.2.0 (2017-11-17)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Add ``is_anonymous``, ``login_required``, ``has_permission`` helpers (#114)
|
||||||
|
|
||||||
0.1.2 (2017-10-17)
|
0.1.2 (2017-10-17)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
2
LICENSE
2
LICENSE
@@ -186,7 +186,7 @@ Apache License
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2015-2016 Andrew Svetlov
|
Copyright 2015-2018 Andrew Svetlov and aio-libs team.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
35
README.rst
35
README.rst
@@ -1,7 +1,7 @@
|
|||||||
aiohttp_security
|
aiohttp_security
|
||||||
================
|
================
|
||||||
.. image:: https://travis-ci.org/aio-libs/aiohttp-security.svg?branch=master
|
.. image:: https://travis-ci.com/aio-libs/aiohttp-security.svg?branch=master
|
||||||
:target: https://travis-ci.org/aio-libs/aiohttp-security
|
:target: https://travis-ci.com/aio-libs/aiohttp-security
|
||||||
.. image:: https://codecov.io/github/aio-libs/aiohttp-security/coverage.svg?branch=master
|
.. image:: https://codecov.io/github/aio-libs/aiohttp-security/coverage.svg?branch=master
|
||||||
:target: https://codecov.io/github/aio-libs/aiohttp-security
|
:target: https://codecov.io/github/aio-libs/aiohttp-security
|
||||||
.. image:: https://readthedocs.org/projects/aiohttp-security/badge/?version=latest
|
.. image:: https://readthedocs.org/projects/aiohttp-security/badge/?version=latest
|
||||||
@@ -9,17 +9,34 @@ aiohttp_security
|
|||||||
.. image:: https://img.shields.io/pypi/v/aiohttp-security.svg
|
.. image:: https://img.shields.io/pypi/v/aiohttp-security.svg
|
||||||
:target: https://pypi.python.org/pypi/aiohttp-security
|
:target: https://pypi.python.org/pypi/aiohttp-security
|
||||||
|
|
||||||
The library provides identity and autorization for `aiohttp.web`__.
|
The library provides identity and authorization for `aiohttp.web`__.
|
||||||
|
|
||||||
.. _aiohttp_web: http://aiohttp.readthedocs.org/en/latest/web.html
|
.. _aiohttp_web: http://aiohttp.readthedocs.org/en/latest/web.html
|
||||||
|
|
||||||
__ aiohttp_web_
|
__ aiohttp_web_
|
||||||
|
|
||||||
Usage
|
Installation
|
||||||
-----
|
------------
|
||||||
To install type ``pip install aiohttp_security``.
|
Simplest case (authorization via cookies) ::
|
||||||
Launch ``make doc`` and see examples or look under **demo** directory for a
|
|
||||||
sample project.
|
$ pip install aiohttp_security
|
||||||
|
|
||||||
|
With `aiohttp-session` support ::
|
||||||
|
|
||||||
|
$ pip install aiohttp_security[session]
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
Take a look at examples:
|
||||||
|
|
||||||
|
`Basic example`_
|
||||||
|
|
||||||
|
`Example with DB auth`_
|
||||||
|
|
||||||
|
.. _`Basic example`: docs/example.rst
|
||||||
|
.. _`Example with db auth`: docs/example_db_auth.rst
|
||||||
|
|
||||||
|
and demos at **demo** directory.
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
-------------
|
-------------
|
||||||
@@ -29,7 +46,7 @@ https://aiohttp-security.readthedocs.io/
|
|||||||
Develop
|
Develop
|
||||||
-------
|
-------
|
||||||
|
|
||||||
``pip install -r requirements-dev``
|
``pip install -r requirements-dev.txt``
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
|
@@ -1,13 +1,18 @@
|
|||||||
from .abc import AbstractIdentityPolicy, AbstractAuthorizationPolicy
|
from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy
|
||||||
from .api import remember, forget, setup, authorized_userid, permits
|
from .api import (authorized_userid, forget, has_permission,
|
||||||
|
is_anonymous, login_required, permits, remember,
|
||||||
|
setup, check_authorized, check_permission)
|
||||||
from .cookies_identity import CookiesIdentityPolicy
|
from .cookies_identity import CookiesIdentityPolicy
|
||||||
from .session_identity import SessionIdentityPolicy
|
from .session_identity import SessionIdentityPolicy
|
||||||
|
from .jwt_identity import JWTIdentityPolicy
|
||||||
|
|
||||||
|
__version__ = '0.4.0'
|
||||||
__version__ = '0.1.2'
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
|
__all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
|
||||||
'CookiesIdentityPolicy', 'SessionIdentityPolicy',
|
'CookiesIdentityPolicy', 'SessionIdentityPolicy',
|
||||||
|
'JWTIdentityPolicy',
|
||||||
'remember', 'forget', 'authorized_userid',
|
'remember', 'forget', 'authorized_userid',
|
||||||
'permits', 'setup')
|
'permits', 'setup', 'is_anonymous',
|
||||||
|
'login_required', 'has_permission',
|
||||||
|
'check_authorized', 'check_permission')
|
||||||
|
@@ -1,21 +1,23 @@
|
|||||||
import abc
|
import abc
|
||||||
import asyncio
|
from enum import Enum
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
# see http://plope.com/pyramid_auth_design_api_postmortem
|
# see http://plope.com/pyramid_auth_design_api_postmortem
|
||||||
|
|
||||||
|
|
||||||
class AbstractIdentityPolicy(metaclass=abc.ABCMeta):
|
class AbstractIdentityPolicy(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def identify(self, request):
|
async def identify(self, request: web.Request) -> Optional[str]:
|
||||||
"""Return the claimed identity of the user associated request or
|
"""Return the claimed identity of the user associated request or
|
||||||
``None`` if no identity can be found associated with the request."""
|
``None`` if no identity can be found associated with the request."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def remember(self, request, response, identity, **kwargs):
|
async def remember(self, request: web.Request, response: web.StreamResponse,
|
||||||
|
identity: str, **kwargs: Any) -> None:
|
||||||
"""Remember identity.
|
"""Remember identity.
|
||||||
|
|
||||||
Modify response object by filling it's headers with remembered user.
|
Modify response object by filling it's headers with remembered user.
|
||||||
@@ -25,9 +27,8 @@ class AbstractIdentityPolicy(metaclass=abc.ABCMeta):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def forget(self, request, response):
|
async def forget(self, request: web.Request, response: web.StreamResponse) -> None:
|
||||||
""" Modify response which can be used to 'forget' the
|
""" Modify response which can be used to 'forget' the
|
||||||
current identity on subsequent requests."""
|
current identity on subsequent requests."""
|
||||||
pass
|
pass
|
||||||
@@ -35,9 +36,9 @@ class AbstractIdentityPolicy(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
class AbstractAuthorizationPolicy(metaclass=abc.ABCMeta):
|
class AbstractAuthorizationPolicy(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def permits(self, identity, permission, context=None):
|
async def permits(self, identity: str, permission: Union[str, Enum],
|
||||||
|
context: Any = None) -> bool:
|
||||||
"""Check user permissions.
|
"""Check user permissions.
|
||||||
|
|
||||||
Return True if the identity is allowed the permission in the
|
Return True if the identity is allowed the permission in the
|
||||||
@@ -45,9 +46,8 @@ class AbstractAuthorizationPolicy(metaclass=abc.ABCMeta):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def authorized_userid(self, identity):
|
async def authorized_userid(self, identity: str) -> Optional[str]:
|
||||||
"""Retrieve authorized user id.
|
"""Retrieve authorized user id.
|
||||||
|
|
||||||
Return the user_id of the user identified by the identity
|
Return the user_id of the user identified by the identity
|
||||||
|
@@ -1,24 +1,33 @@
|
|||||||
import asyncio
|
import enum
|
||||||
|
import warnings
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Callable, Optional, TypeVar, Union
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_security.abc import (AbstractIdentityPolicy,
|
from aiohttp_security.abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy
|
||||||
AbstractAuthorizationPolicy)
|
|
||||||
|
|
||||||
IDENTITY_KEY = 'aiohttp_security_identity_policy'
|
IDENTITY_KEY = 'aiohttp_security_identity_policy'
|
||||||
AUTZ_KEY = 'aiohttp_security_autz_policy'
|
AUTZ_KEY = 'aiohttp_security_autz_policy'
|
||||||
|
|
||||||
|
# _AIP/_AAP are shorthand for Optional[policy] when we retrieve from request.
|
||||||
|
_AAP = Optional[AbstractAuthorizationPolicy]
|
||||||
|
_AIP = Optional[AbstractIdentityPolicy]
|
||||||
|
_Handler = TypeVar('_Handler', bound=Union[Callable[[web.Request], Any],
|
||||||
|
Callable[[object, web.Request], Any]])
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def remember(request, response, identity, **kwargs):
|
async def remember(request: web.Request, response: web.StreamResponse,
|
||||||
|
identity: str, **kwargs: Any) -> None:
|
||||||
"""Remember identity into response.
|
"""Remember identity into response.
|
||||||
|
|
||||||
The action is performed by identity_policy.remember()
|
The action is performed by identity_policy.remember()
|
||||||
|
|
||||||
Usually the idenity is stored in user cookies homehow but may be
|
Usually the identity is stored in user cookies somehow but may be
|
||||||
pushed into custom header also.
|
pushed into custom header also.
|
||||||
"""
|
"""
|
||||||
assert isinstance(identity, str), identity
|
assert isinstance(identity, str), identity
|
||||||
assert identity
|
assert identity
|
||||||
identity_policy = request.app.get(IDENTITY_KEY)
|
identity_policy = request.config_dict.get(IDENTITY_KEY)
|
||||||
if identity_policy is None:
|
if identity_policy is None:
|
||||||
text = ("Security subsystem is not initialized, "
|
text = ("Security subsystem is not initialized, "
|
||||||
"call aiohttp_security.setup(...) first")
|
"call aiohttp_security.setup(...) first")
|
||||||
@@ -26,17 +35,16 @@ def remember(request, response, identity, **kwargs):
|
|||||||
# output and rendered page we add same message to *reason* and
|
# output and rendered page we add same message to *reason* and
|
||||||
# *text* arguments.
|
# *text* arguments.
|
||||||
raise web.HTTPInternalServerError(reason=text, text=text)
|
raise web.HTTPInternalServerError(reason=text, text=text)
|
||||||
yield from identity_policy.remember(request, response, identity, **kwargs)
|
await identity_policy.remember(request, response, identity, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def forget(request: web.Request, response: web.StreamResponse) -> None:
|
||||||
def forget(request, response):
|
|
||||||
"""Forget previously remembered identity.
|
"""Forget previously remembered identity.
|
||||||
|
|
||||||
Usually it clears cookie or server-side storage to forget user
|
Usually it clears cookie or server-side storage to forget user
|
||||||
session.
|
session.
|
||||||
"""
|
"""
|
||||||
identity_policy = request.app.get(IDENTITY_KEY)
|
identity_policy = request.config_dict.get(IDENTITY_KEY)
|
||||||
if identity_policy is None:
|
if identity_policy is None:
|
||||||
text = ("Security subsystem is not initialized, "
|
text = ("Security subsystem is not initialized, "
|
||||||
"call aiohttp_security.setup(...) first")
|
"call aiohttp_security.setup(...) first")
|
||||||
@@ -44,37 +52,129 @@ def forget(request, response):
|
|||||||
# output and rendered page we add same message to *reason* and
|
# output and rendered page we add same message to *reason* and
|
||||||
# *text* arguments.
|
# *text* arguments.
|
||||||
raise web.HTTPInternalServerError(reason=text, text=text)
|
raise web.HTTPInternalServerError(reason=text, text=text)
|
||||||
yield from identity_policy.forget(request, response)
|
await identity_policy.forget(request, response)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def authorized_userid(request: web.Request) -> Optional[str]:
|
||||||
def authorized_userid(request):
|
identity_policy: _AIP = request.config_dict.get(IDENTITY_KEY)
|
||||||
identity_policy = request.app.get(IDENTITY_KEY)
|
autz_policy: _AAP = request.config_dict.get(AUTZ_KEY)
|
||||||
autz_policy = request.app.get(AUTZ_KEY)
|
|
||||||
if identity_policy is None or autz_policy is None:
|
if identity_policy is None or autz_policy is None:
|
||||||
return None
|
return None
|
||||||
identity = yield from identity_policy.identify(request)
|
identity = await identity_policy.identify(request)
|
||||||
if identity is None:
|
if identity is None:
|
||||||
return None # non-registered user has None user_id
|
return None # non-registered user has None user_id
|
||||||
user_id = yield from autz_policy.authorized_userid(identity)
|
user_id = await autz_policy.authorized_userid(identity)
|
||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def permits(request: web.Request, permission: Union[str, enum.Enum],
|
||||||
def permits(request, permission, context=None):
|
context: Any = None) -> bool:
|
||||||
assert isinstance(permission, str), permission
|
assert isinstance(permission, (str, enum.Enum)), permission
|
||||||
assert permission
|
assert permission
|
||||||
identity_policy = request.app.get(IDENTITY_KEY)
|
identity_policy: _AIP = request.config_dict.get(IDENTITY_KEY)
|
||||||
autz_policy = request.app.get(AUTZ_KEY)
|
autz_policy: _AAP = request.config_dict.get(AUTZ_KEY)
|
||||||
if identity_policy is None or autz_policy is None:
|
if identity_policy is None or autz_policy is None:
|
||||||
return True
|
return True
|
||||||
identity = yield from identity_policy.identify(request)
|
identity = await identity_policy.identify(request)
|
||||||
# non-registered user still may has some permissions
|
# non-registered user still may have some permissions
|
||||||
access = yield from autz_policy.permits(identity, permission, context)
|
access = await autz_policy.permits(identity, permission, context)
|
||||||
return access
|
return access
|
||||||
|
|
||||||
|
|
||||||
def setup(app, identity_policy, autz_policy):
|
async def is_anonymous(request: web.Request) -> bool:
|
||||||
|
"""Check if user is anonymous.
|
||||||
|
|
||||||
|
User is considered anonymous if there is not identity
|
||||||
|
in request.
|
||||||
|
"""
|
||||||
|
identity_policy = request.config_dict.get(IDENTITY_KEY)
|
||||||
|
if identity_policy is None:
|
||||||
|
return True
|
||||||
|
identity = await identity_policy.identify(request)
|
||||||
|
if identity is None:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def check_authorized(request: web.Request) -> str:
|
||||||
|
"""Checker that raises HTTPUnauthorized for anonymous users.
|
||||||
|
"""
|
||||||
|
userid = await authorized_userid(request)
|
||||||
|
if userid is None:
|
||||||
|
raise web.HTTPUnauthorized()
|
||||||
|
return userid
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(fn: _Handler) -> _Handler:
|
||||||
|
"""Decorator that restrict access only for authorized users.
|
||||||
|
|
||||||
|
User is considered authorized if authorized_userid
|
||||||
|
returns some value.
|
||||||
|
"""
|
||||||
|
@wraps(fn)
|
||||||
|
async def wrapped(*args: Union[object, web.Request]) -> Any:
|
||||||
|
request = args[-1]
|
||||||
|
if not isinstance(request, web.Request):
|
||||||
|
msg = ("Incorrect decorator usage. "
|
||||||
|
"Expecting `def handler(request)` "
|
||||||
|
"or `def handler(self, request)`.")
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
await check_authorized(request)
|
||||||
|
return await fn(*args) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
warnings.warn("login_required decorator is deprecated, "
|
||||||
|
"use check_authorized instead",
|
||||||
|
DeprecationWarning)
|
||||||
|
return wrapped # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
|
async def check_permission(request: web.Request, permission: Union[str, enum.Enum],
|
||||||
|
context: Any = None) -> None:
|
||||||
|
"""Checker that passes only to authoraised users with given permission.
|
||||||
|
|
||||||
|
If user is not authorized - raises HTTPUnauthorized,
|
||||||
|
if user is authorized and does not have permission -
|
||||||
|
raises HTTPForbidden.
|
||||||
|
"""
|
||||||
|
|
||||||
|
await check_authorized(request)
|
||||||
|
allowed = await permits(request, permission, context)
|
||||||
|
if not allowed:
|
||||||
|
raise web.HTTPForbidden()
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(permission: Union[str, enum.Enum], context: Any = None): # type: ignore
|
||||||
|
"""Decorator that restricts access only for authorized users
|
||||||
|
with correct permissions.
|
||||||
|
|
||||||
|
If user is not authorized - raises HTTPUnauthorized,
|
||||||
|
if user is authorized and does not have permission -
|
||||||
|
raises HTTPForbidden.
|
||||||
|
"""
|
||||||
|
def wrapper(fn): # type: ignore
|
||||||
|
@wraps(fn)
|
||||||
|
async def wrapped(*args, **kwargs): # type: ignore
|
||||||
|
request = args[-1]
|
||||||
|
if not isinstance(request, web.Request):
|
||||||
|
msg = ("Incorrect decorator usage. "
|
||||||
|
"Expecting `def handler(request)` "
|
||||||
|
"or `def handler(self, request)`.")
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
await check_permission(request, permission, context)
|
||||||
|
return await fn(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
warnings.warn("has_permission decorator is deprecated, "
|
||||||
|
"use check_permission instead",
|
||||||
|
DeprecationWarning)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app: web.Application, identity_policy: AbstractIdentityPolicy,
|
||||||
|
autz_policy: AbstractAuthorizationPolicy) -> None:
|
||||||
assert isinstance(identity_policy, AbstractIdentityPolicy), identity_policy
|
assert isinstance(identity_policy, AbstractIdentityPolicy), identity_policy
|
||||||
assert isinstance(autz_policy, AbstractAuthorizationPolicy), autz_policy
|
assert isinstance(autz_policy, AbstractAuthorizationPolicy), autz_policy
|
||||||
|
|
||||||
|
@@ -5,33 +5,32 @@ more handy.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
from aiohttp import web
|
||||||
|
from typing import Any, NewType, Optional, Union, cast
|
||||||
|
|
||||||
from .abc import AbstractIdentityPolicy
|
from .abc import AbstractIdentityPolicy
|
||||||
|
|
||||||
|
_Sentinel = NewType('_Sentinel', object)
|
||||||
sentinel = object()
|
sentinel = _Sentinel(object())
|
||||||
|
|
||||||
|
|
||||||
class CookiesIdentityPolicy(AbstractIdentityPolicy):
|
class CookiesIdentityPolicy(AbstractIdentityPolicy):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self._cookie_name = 'AIOHTTP_SECURITY'
|
self._cookie_name = 'AIOHTTP_SECURITY'
|
||||||
self._max_age = 30 * 24 * 3600
|
self._max_age = 30 * 24 * 3600
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def identify(self, request: web.Request) -> Optional[str]:
|
||||||
def identify(self, request):
|
return request.cookies.get(self._cookie_name)
|
||||||
identity = request.cookies.get(self._cookie_name)
|
|
||||||
return identity
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def remember(self, request: web.Request, response: web.StreamResponse,
|
||||||
def remember(self, request, response, identity, max_age=sentinel,
|
identity: str, max_age: Union[_Sentinel, Optional[int]] = sentinel,
|
||||||
**kwargs):
|
**kwargs: Any) -> None:
|
||||||
if max_age is sentinel:
|
if max_age is sentinel:
|
||||||
max_age = self._max_age
|
max_age = self._max_age
|
||||||
|
max_age = cast(Optional[int], max_age)
|
||||||
response.set_cookie(self._cookie_name, identity,
|
response.set_cookie(self._cookie_name, identity,
|
||||||
max_age=max_age, **kwargs)
|
max_age=max_age, **kwargs)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def forget(self, request: web.Request, response: web.StreamResponse) -> None:
|
||||||
def forget(self, request, response):
|
|
||||||
response.del_cookie(self._cookie_name)
|
response.del_cookie(self._cookie_name)
|
||||||
|
51
aiohttp_security/jwt_identity.py
Normal file
51
aiohttp_security/jwt_identity.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""Identity policy for storing info in the jwt token.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from .abc import AbstractIdentityPolicy
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jwt
|
||||||
|
HAS_JWT = True
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
HAS_JWT = False
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_HEADER_NAME = 'Authorization'
|
||||||
|
AUTH_SCHEME = 'Bearer '
|
||||||
|
|
||||||
|
|
||||||
|
class JWTIdentityPolicy(AbstractIdentityPolicy):
|
||||||
|
def __init__(self, secret: str, algorithm: str = 'HS256'):
|
||||||
|
if not HAS_JWT:
|
||||||
|
raise RuntimeError('Please install `PyJWT`')
|
||||||
|
self.secret = secret
|
||||||
|
self.algorithm = algorithm
|
||||||
|
|
||||||
|
async def identify(self, request: web.Request) -> Optional[str]:
|
||||||
|
header_identity = request.headers.get(AUTH_HEADER_NAME)
|
||||||
|
|
||||||
|
if header_identity is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not header_identity.startswith(AUTH_SCHEME):
|
||||||
|
raise ValueError('Invalid authorization scheme. ' +
|
||||||
|
'Should be `{}<token>`'.format(AUTH_SCHEME))
|
||||||
|
|
||||||
|
token = header_identity.split(' ')[1].strip()
|
||||||
|
|
||||||
|
identity = jwt.decode(token,
|
||||||
|
self.secret,
|
||||||
|
algorithms=[self.algorithm])
|
||||||
|
return identity
|
||||||
|
|
||||||
|
async def remember(self, request: web.Request, response: web.StreamResponse,
|
||||||
|
identity: str, **kwargs: None) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def forget(self, request: web.Request, response: web.StreamResponse) -> None:
|
||||||
|
pass
|
0
aiohttp_security/py.typed
Normal file
0
aiohttp_security/py.typed
Normal file
@@ -4,8 +4,7 @@ aiohttp_session.setup() should be called on application initialization
|
|||||||
to configure aiohttp_session properly.
|
to configure aiohttp_session properly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
from aiohttp import web
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from aiohttp_session import get_session
|
from aiohttp_session import get_session
|
||||||
HAS_AIOHTTP_SESSION = True
|
HAS_AIOHTTP_SESSION = True
|
||||||
@@ -17,24 +16,22 @@ from .abc import AbstractIdentityPolicy
|
|||||||
|
|
||||||
class SessionIdentityPolicy(AbstractIdentityPolicy):
|
class SessionIdentityPolicy(AbstractIdentityPolicy):
|
||||||
|
|
||||||
def __init__(self, session_key='AIOHTTP_SECURITY'):
|
def __init__(self, session_key: str = 'AIOHTTP_SECURITY'):
|
||||||
self._session_key = session_key
|
self._session_key = session_key
|
||||||
|
|
||||||
if not HAS_AIOHTTP_SESSION: # pragma: no cover
|
if not HAS_AIOHTTP_SESSION: # pragma: no cover
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
'SessionIdentityPolicy requires `aiohttp_session`')
|
'SessionIdentityPolicy requires `aiohttp_session`')
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def identify(self, request: web.Request) -> str:
|
||||||
def identify(self, request):
|
session = await get_session(request)
|
||||||
session = yield from get_session(request)
|
|
||||||
return session.get(self._session_key)
|
return session.get(self._session_key)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def remember(self, request: web.Request, response: web.StreamResponse,
|
||||||
def remember(self, request, response, identity, **kwargs):
|
identity: str, **kwargs: None) -> None:
|
||||||
session = yield from get_session(request)
|
session = await get_session(request)
|
||||||
session[self._session_key] = identity
|
session[self._session_key] = identity
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def forget(self, request: web.Request, response: web.StreamResponse) -> None:
|
||||||
def forget(self, request, response):
|
session = await get_session(request)
|
||||||
session = yield from get_session(request)
|
|
||||||
session.pop(self._session_key, None)
|
session.pop(self._session_key, None)
|
||||||
|
0
demo/__init__.py
Normal file
0
demo/__init__.py
Normal file
0
demo/database_auth/__init__.py
Normal file
0
demo/database_auth/__init__.py
Normal file
@@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
from enum import Enum
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
||||||
from passlib.hash import sha256_crypt
|
from passlib.hash import sha256_crypt
|
||||||
|
|
||||||
@@ -9,32 +9,28 @@ from . import db
|
|||||||
|
|
||||||
|
|
||||||
class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
def __init__(self, dbengine):
|
def __init__(self, dbengine: Any):
|
||||||
self.dbengine = dbengine
|
self.dbengine = dbengine
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def authorized_userid(self, identity: str) -> Optional[str]:
|
||||||
def authorized_userid(self, identity):
|
async with self.dbengine.acquire() as conn:
|
||||||
with (yield from self.dbengine) as conn:
|
|
||||||
where = sa.and_(db.users.c.login == identity,
|
where = sa.and_(db.users.c.login == identity,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled)) # type: ignore[no-untyped-call]
|
||||||
query = db.users.count().where(where)
|
query = db.users.count().where(where)
|
||||||
ret = yield from conn.scalar(query)
|
ret = await conn.scalar(query)
|
||||||
if ret:
|
if ret:
|
||||||
return identity
|
return identity
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def permits(self, identity: str, permission: Union[str, Enum],
|
||||||
def permits(self, identity, permission, context=None):
|
context: None = None) -> bool:
|
||||||
if identity is None:
|
async with self.dbengine.acquire() as conn:
|
||||||
return False
|
|
||||||
|
|
||||||
with (yield from self.dbengine) as conn:
|
|
||||||
where = sa.and_(db.users.c.login == identity,
|
where = sa.and_(db.users.c.login == identity,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled)) # type: ignore[no-untyped-call]
|
||||||
query = db.users.select().where(where)
|
query = db.users.select().where(where)
|
||||||
ret = yield from conn.execute(query)
|
ret = await conn.execute(query)
|
||||||
user = yield from ret.fetchone()
|
user = await ret.fetchone()
|
||||||
if user is not None:
|
if user is not None:
|
||||||
user_id = user[0]
|
user_id = user[0]
|
||||||
is_superuser = user[3]
|
is_superuser = user[3]
|
||||||
@@ -43,8 +39,8 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
|||||||
|
|
||||||
where = db.permissions.c.user_id == user_id
|
where = db.permissions.c.user_id == user_id
|
||||||
query = db.permissions.select().where(where)
|
query = db.permissions.select().where(where)
|
||||||
ret = yield from conn.execute(query)
|
ret = await conn.execute(query)
|
||||||
result = yield from ret.fetchall()
|
result = await ret.fetchall()
|
||||||
if ret is not None:
|
if ret is not None:
|
||||||
for record in result:
|
for record in result:
|
||||||
if record.perm_name == permission:
|
if record.perm_name == permission:
|
||||||
@@ -53,15 +49,14 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check_credentials(db_engine: Any, username: str, password: str) -> bool:
|
||||||
def check_credentials(db_engine, username, password):
|
async with db_engine.acquire() as conn:
|
||||||
with (yield from db_engine) as conn:
|
|
||||||
where = sa.and_(db.users.c.login == username,
|
where = sa.and_(db.users.c.login == username,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled)) # type: ignore[no-untyped-call]
|
||||||
query = db.users.select().where(where)
|
query = db.users.select().where(where)
|
||||||
ret = yield from conn.execute(query)
|
ret = await conn.execute(query)
|
||||||
user = yield from ret.fetchone()
|
user = await ret.fetchone()
|
||||||
if user is not None:
|
if user is not None:
|
||||||
hash = user[2]
|
hashed = user[2]
|
||||||
return sha256_crypt.verify(password, hash)
|
return sha256_crypt.verify(password, hashed) # type: ignore[no-any-return]
|
||||||
return False
|
return False
|
||||||
|
@@ -1,48 +1,35 @@
|
|||||||
import asyncio
|
from textwrap import dedent
|
||||||
import functools
|
from typing import NoReturn
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from aiohttp_security import remember, forget, authorized_userid, permits
|
from aiohttp_security import (
|
||||||
|
remember, forget, authorized_userid,
|
||||||
|
check_permission, check_authorized,
|
||||||
|
)
|
||||||
|
|
||||||
from .db_auth import check_credentials
|
from .db_auth import check_credentials
|
||||||
|
|
||||||
|
|
||||||
def require(permission):
|
|
||||||
def wrapper(f):
|
|
||||||
@asyncio.coroutine
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapped(self, request):
|
|
||||||
has_perm = yield from permits(request, permission)
|
|
||||||
if not has_perm:
|
|
||||||
message = 'User has no permission {}'.format(permission)
|
|
||||||
raise web.HTTPForbidden(body=message.encode())
|
|
||||||
return (yield from f(self, request))
|
|
||||||
return wrapped
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class Web(object):
|
class Web(object):
|
||||||
index_template = """
|
index_template = dedent("""
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<head>
|
<head></head>
|
||||||
</head>
|
<body>
|
||||||
<body>
|
<p>{message}</p>
|
||||||
<p>{message}</p>
|
<form action="/login" method="post">
|
||||||
<form action="/login" method="post">
|
Login:
|
||||||
Login:
|
<input type="text" name="login">
|
||||||
<input type="text" name="login">
|
Password:
|
||||||
Password:
|
<input type="password" name="password">
|
||||||
<input type="password" name="password">
|
<input type="submit" value="Login">
|
||||||
<input type="submit" value="Login">
|
</form>
|
||||||
</form>
|
<a href="/logout">Logout</a>
|
||||||
<a href="/logout">Logout</a>
|
</body>
|
||||||
</body>
|
""")
|
||||||
"""
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def index(self, request: web.Request) -> web.Response:
|
||||||
def index(self, request):
|
username = await authorized_userid(request)
|
||||||
username = yield from authorized_userid(request)
|
|
||||||
if username:
|
if username:
|
||||||
template = self.index_template.format(
|
template = self.index_template.format(
|
||||||
message='Hello, {username}!'.format(username=username))
|
message='Hello, {username}!'.format(username=username))
|
||||||
@@ -51,41 +38,41 @@ class Web(object):
|
|||||||
response = web.Response(body=template.encode())
|
response = web.Response(body=template.encode())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def login(self, request: web.Request) -> NoReturn:
|
||||||
def login(self, request):
|
invalid_resp = web.HTTPUnauthorized(body=b'Invalid username/password combination')
|
||||||
response = web.HTTPFound('/')
|
form = await request.post()
|
||||||
form = yield from request.post()
|
|
||||||
login = form.get('login')
|
login = form.get('login')
|
||||||
password = form.get('password')
|
password = form.get('password')
|
||||||
db_engine = request.app.db_engine
|
db_engine = request.app['db_engine']
|
||||||
if (yield from check_credentials(db_engine, login, password)):
|
|
||||||
yield from remember(request, response, login)
|
|
||||||
return response
|
|
||||||
|
|
||||||
return web.HTTPUnauthorized(
|
if not (isinstance(login, str) and isinstance(password, str)):
|
||||||
body=b'Invalid username/password combination')
|
raise invalid_resp
|
||||||
|
|
||||||
@require('public')
|
if await check_credentials(db_engine, login, password):
|
||||||
@asyncio.coroutine
|
response = web.HTTPFound('/')
|
||||||
def logout(self, request):
|
await remember(request, response, login)
|
||||||
|
raise response
|
||||||
|
|
||||||
|
raise invalid_resp
|
||||||
|
|
||||||
|
async def logout(self, request: web.Request) -> web.Response:
|
||||||
|
await check_authorized(request)
|
||||||
response = web.Response(body=b'You have been logged out')
|
response = web.Response(body=b'You have been logged out')
|
||||||
yield from forget(request, response)
|
await forget(request, response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@require('public')
|
async def internal_page(self, request: web.Request) -> web.Response:
|
||||||
@asyncio.coroutine
|
await check_permission(request, 'public')
|
||||||
def internal_page(self, request):
|
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
body=b'This page is visible for all registered users')
|
body=b'This page is visible for all registered users')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@require('protected')
|
async def protected_page(self, request: web.Request) -> web.Response:
|
||||||
@asyncio.coroutine
|
await check_permission(request, 'protected')
|
||||||
def protected_page(self, request):
|
|
||||||
response = web.Response(body=b'You are on protected page')
|
response = web.Response(body=b'You are on protected page')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def configure(self, app):
|
def configure(self, app: web.Application) -> None:
|
||||||
router = app.router
|
router = app.router
|
||||||
router.add_route('GET', '/', self.index, name='index')
|
router.add_route('GET', '/', self.index, name='index')
|
||||||
router.add_route('POST', '/login', self.login, name='login')
|
router.add_route('POST', '/login', self.login, name='login')
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from typing import Any, Tuple
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_session import setup as setup_session
|
from aiohttp_session import setup as setup_session
|
||||||
@@ -9,19 +10,18 @@ from aiopg.sa import create_engine
|
|||||||
from aioredis import create_pool
|
from aioredis import create_pool
|
||||||
|
|
||||||
|
|
||||||
from demo.db_auth import DBAuthorizationPolicy
|
from demo.database_auth.db_auth import DBAuthorizationPolicy
|
||||||
from demo.handlers import Web
|
from demo.database_auth.handlers import Web
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def init(loop: asyncio.AbstractEventLoop) -> Tuple[Any, ...]:
|
||||||
def init(loop):
|
redis_pool = await create_pool(('localhost', 6379))
|
||||||
redis_pool = yield from create_pool(('localhost', 6379))
|
db_engine = await create_engine(user='aiohttp_security',
|
||||||
db_engine = yield from create_engine(user='aiohttp_security',
|
password='aiohttp_security',
|
||||||
password='aiohttp_security',
|
database='aiohttp_security',
|
||||||
database='aiohttp_security',
|
host='127.0.0.1')
|
||||||
host='127.0.0.1')
|
app = web.Application()
|
||||||
app = web.Application(loop=loop)
|
app['db_engine'] = db_engine
|
||||||
app.db_engine = db_engine
|
|
||||||
setup_session(app, RedisStorage(redis_pool))
|
setup_session(app, RedisStorage(redis_pool))
|
||||||
setup_security(app,
|
setup_security(app,
|
||||||
SessionIdentityPolicy(),
|
SessionIdentityPolicy(),
|
||||||
@@ -31,24 +31,23 @@ def init(loop):
|
|||||||
web_handlers.configure(app)
|
web_handlers.configure(app)
|
||||||
|
|
||||||
handler = app.make_handler()
|
handler = app.make_handler()
|
||||||
srv = yield from loop.create_server(handler, '127.0.0.1', 8080)
|
srv = await loop.create_server(handler, '127.0.0.1', 8080)
|
||||||
print('Server started at http://127.0.0.1:8080')
|
print('Server started at http://127.0.0.1:8080')
|
||||||
return srv, app, handler
|
return srv, app, handler
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def finalize(srv: Any, app: Any, handler: Any) -> None:
|
||||||
def finalize(srv, app, handler):
|
|
||||||
sock = srv.sockets[0]
|
sock = srv.sockets[0]
|
||||||
app.loop.remove_reader(sock.fileno())
|
app.loop.remove_reader(sock.fileno())
|
||||||
sock.close()
|
sock.close()
|
||||||
|
|
||||||
yield from handler.finish_connections(1.0)
|
await handler.finish_connections(1.0)
|
||||||
srv.close()
|
srv.close()
|
||||||
yield from srv.wait_closed()
|
await srv.wait_closed()
|
||||||
yield from app.finish()
|
await app.finish()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
srv, app, handler = loop.run_until_complete(init(loop))
|
srv, app, handler = loop.run_until_complete(init(loop))
|
||||||
try:
|
try:
|
||||||
|
0
demo/dictionary_auth/__init__.py
Normal file
0
demo/dictionary_auth/__init__.py
Normal file
@@ -1,20 +1,25 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Dict, Optional, Union
|
||||||
|
|
||||||
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
||||||
|
|
||||||
|
from .users import User
|
||||||
|
|
||||||
|
|
||||||
class DictionaryAuthorizationPolicy(AbstractAuthorizationPolicy):
|
class DictionaryAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
def __init__(self, user_map):
|
def __init__(self, user_map: Dict[str, User]):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.user_map = user_map
|
self.user_map = user_map
|
||||||
|
|
||||||
async def authorized_userid(self, identity):
|
async def authorized_userid(self, identity: str) -> Optional[str]:
|
||||||
"""Retrieve authorized user id.
|
"""Retrieve authorized user id.
|
||||||
Return the user_id of the user identified by the identity
|
Return the user_id of the user identified by the identity
|
||||||
or 'None' if no user exists related to the identity.
|
or 'None' if no user exists related to the identity.
|
||||||
"""
|
"""
|
||||||
if identity in self.user_map:
|
return identity if identity in self.user_map else None
|
||||||
return identity
|
|
||||||
|
|
||||||
async def permits(self, identity, permission, context=None):
|
async def permits(self, identity: str, permission: Union[str, Enum],
|
||||||
|
context: None = None) -> bool:
|
||||||
"""Check user permissions.
|
"""Check user permissions.
|
||||||
Return True if the identity is allowed the permission in the
|
Return True if the identity is allowed the permission in the
|
||||||
current context, else return False.
|
current context, else return False.
|
||||||
@@ -26,7 +31,7 @@ class DictionaryAuthorizationPolicy(AbstractAuthorizationPolicy):
|
|||||||
return permission in user.permissions
|
return permission in user.permissions
|
||||||
|
|
||||||
|
|
||||||
async def check_credentials(user_map, username, password):
|
async def check_credentials(user_map: Dict[str, User], username: str, password: str) -> bool:
|
||||||
user = user_map.get(username)
|
user = user_map.get(username)
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return False
|
||||||
|
@@ -1,47 +1,35 @@
|
|||||||
import asyncio
|
|
||||||
import functools
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from typing import Dict, NoReturn
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from aiohttp_security import remember, forget, authorized_userid, permits
|
from aiohttp_security import (
|
||||||
|
remember, forget, authorized_userid,
|
||||||
|
check_permission, check_authorized,
|
||||||
|
)
|
||||||
|
|
||||||
from .authz import check_credentials
|
from .authz import check_credentials
|
||||||
|
from .users import User
|
||||||
|
|
||||||
def require(permission):
|
|
||||||
def wrapper(f):
|
|
||||||
@asyncio.coroutine
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapped(request):
|
|
||||||
has_perm = yield from permits(request, permission)
|
|
||||||
if not has_perm:
|
|
||||||
message = 'User has no permission {}'.format(permission)
|
|
||||||
raise web.HTTPForbidden(body=message.encode())
|
|
||||||
return (yield from f(request))
|
|
||||||
return wrapped
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
index_template = dedent("""
|
index_template = dedent("""
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<head>
|
<head></head>
|
||||||
</head>
|
<body>
|
||||||
<body>
|
<p>{message}</p>
|
||||||
<p>{message}</p>
|
<form action="/login" method="post">
|
||||||
<form action="/login" method="post">
|
Login:
|
||||||
Login:
|
<input type="text" name="username">
|
||||||
<input type="text" name="username">
|
Password:
|
||||||
Password:
|
<input type="password" name="password">
|
||||||
<input type="password" name="password">
|
<input type="submit" value="Login">
|
||||||
<input type="submit" value="Login">
|
</form>
|
||||||
</form>
|
<a href="/logout">Logout</a>
|
||||||
<a href="/logout">Logout</a>
|
</body>
|
||||||
</body>
|
""")
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
async def index(request):
|
async def index(request: web.Request) -> web.Response:
|
||||||
username = await authorized_userid(request)
|
username = await authorized_userid(request)
|
||||||
if username:
|
if username:
|
||||||
template = index_template.format(
|
template = index_template.format(
|
||||||
@@ -54,22 +42,27 @@ async def index(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def login(request):
|
async def login(request: web.Request) -> NoReturn:
|
||||||
response = web.HTTPFound('/')
|
user_map: Dict[str, User] = request.app['user_map']
|
||||||
|
invalid_response = web.HTTPUnauthorized(body='Invalid username / password combination')
|
||||||
form = await request.post()
|
form = await request.post()
|
||||||
username = form.get('username')
|
username = form.get('username')
|
||||||
password = form.get('password')
|
password = form.get('password')
|
||||||
|
|
||||||
verified = await check_credentials(request.app.user_map, username, password)
|
if not (isinstance(username, str) and isinstance(password, str)):
|
||||||
|
raise invalid_response
|
||||||
|
|
||||||
|
verified = await check_credentials(user_map, username, password)
|
||||||
if verified:
|
if verified:
|
||||||
|
response = web.HTTPFound('/')
|
||||||
await remember(request, response, username)
|
await remember(request, response, username)
|
||||||
return response
|
raise response
|
||||||
|
|
||||||
return web.HTTPUnauthorized(body='Invalid username / password combination')
|
raise invalid_response
|
||||||
|
|
||||||
|
|
||||||
@require('public')
|
async def logout(request: web.Request) -> web.Response:
|
||||||
async def logout(request):
|
await check_authorized(request)
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
text='You have been logged out',
|
text='You have been logged out',
|
||||||
content_type='text/html',
|
content_type='text/html',
|
||||||
@@ -78,9 +71,8 @@ async def logout(request):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@require('public')
|
async def internal_page(request: web.Request) -> web.Response:
|
||||||
async def internal_page(request):
|
await check_permission(request, 'public')
|
||||||
# pylint: disable=unused-argument
|
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
text='This page is visible for all registered users',
|
text='This page is visible for all registered users',
|
||||||
content_type='text/html',
|
content_type='text/html',
|
||||||
@@ -88,9 +80,8 @@ async def internal_page(request):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@require('protected')
|
async def protected_page(request: web.Request) -> web.Response:
|
||||||
async def protected_page(request):
|
await check_permission(request, 'protected')
|
||||||
# pylint: disable=unused-argument
|
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
text='You are on protected page',
|
text='You are on protected page',
|
||||||
content_type='text/html',
|
content_type='text/html',
|
||||||
@@ -98,7 +89,7 @@ async def protected_page(request):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def configure_handlers(app):
|
def configure_handlers(app: web.Application) -> None:
|
||||||
router = app.router
|
router = app.router
|
||||||
router.add_get('/', index, name='index')
|
router.add_get('/', index, name='index')
|
||||||
router.add_post('/login', login, name='login')
|
router.add_post('/login', login, name='login')
|
||||||
|
@@ -6,14 +6,14 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
|||||||
from aiohttp_security import setup as setup_security
|
from aiohttp_security import setup as setup_security
|
||||||
from aiohttp_security import SessionIdentityPolicy
|
from aiohttp_security import SessionIdentityPolicy
|
||||||
|
|
||||||
from .authz import DictionaryAuthorizationPolicy
|
from demo.dictionary_auth.authz import DictionaryAuthorizationPolicy
|
||||||
from .handlers import configure_handlers
|
from demo.dictionary_auth.handlers import configure_handlers
|
||||||
from .users import user_map
|
from demo.dictionary_auth.users import user_map
|
||||||
|
|
||||||
|
|
||||||
def make_app():
|
def make_app() -> web.Application:
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.user_map = user_map
|
app['user_map'] = user_map
|
||||||
configure_handlers(app)
|
configure_handlers(app)
|
||||||
|
|
||||||
# secret_key must be 32 url-safe base64-encoded bytes
|
# secret_key must be 32 url-safe base64-encoded bytes
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
from collections import namedtuple
|
from typing import NamedTuple, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class User(NamedTuple):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
permissions: Tuple[str, ...]
|
||||||
|
|
||||||
User = namedtuple('User', ['username', 'password', 'permissions'])
|
|
||||||
|
|
||||||
user_map = {
|
user_map = {
|
||||||
user.username: user for user in [
|
user.username: user for user in [
|
||||||
|
96
demo/simple_example_auth.py
Normal file
96
demo/simple_example_auth.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import NoReturn, Optional, Union
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from aiohttp_session import SimpleCookieStorage, session_middleware
|
||||||
|
from aiohttp_security import check_permission, \
|
||||||
|
is_anonymous, remember, forget, \
|
||||||
|
setup as setup_security, SessionIdentityPolicy
|
||||||
|
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
||||||
|
|
||||||
|
|
||||||
|
# Demo authorization policy for only one user.
|
||||||
|
# User 'jack' has only 'listen' permission.
|
||||||
|
# For more complicated authorization policies see examples
|
||||||
|
# in the 'demo' directory.
|
||||||
|
class SimpleJack_AuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
|
async def authorized_userid(self, identity: str) -> Optional[str]:
|
||||||
|
"""Retrieve authorized user id.
|
||||||
|
Return the user_id of the user identified by the identity
|
||||||
|
or 'None' if no user exists related to the identity.
|
||||||
|
"""
|
||||||
|
return identity if identity == 'jack' else None
|
||||||
|
|
||||||
|
async def permits(self, identity: str, permission: Union[str, Enum],
|
||||||
|
context: None = None) -> bool:
|
||||||
|
"""Check user permissions.
|
||||||
|
Return True if the identity is allowed the permission
|
||||||
|
in the current context, else return False.
|
||||||
|
"""
|
||||||
|
return identity == 'jack' and permission in ('listen',)
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_root(request: web.Request) -> web.Response:
|
||||||
|
is_logged = not await is_anonymous(request)
|
||||||
|
return web.Response(text='''<html><head></head><body>
|
||||||
|
Hello, I'm Jack, I'm {logged} logged in.<br /><br />
|
||||||
|
<a href="/login">Log me in</a><br />
|
||||||
|
<a href="/logout">Log me out</a><br /><br />
|
||||||
|
Check my permissions,
|
||||||
|
when i'm logged in and logged out.<br />
|
||||||
|
<a href="/listen">Can I listen?</a><br />
|
||||||
|
<a href="/speak">Can I speak?</a><br />
|
||||||
|
</body></html>'''.format(
|
||||||
|
logged='' if is_logged else 'NOT',
|
||||||
|
), content_type='text/html')
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_login_jack(request: web.Request) -> NoReturn:
|
||||||
|
redirect_response = web.HTTPFound('/')
|
||||||
|
await remember(request, redirect_response, 'jack')
|
||||||
|
raise redirect_response
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_logout(request: web.Request) -> NoReturn:
|
||||||
|
redirect_response = web.HTTPFound('/')
|
||||||
|
await forget(request, redirect_response)
|
||||||
|
raise redirect_response
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_listen(request: web.Request) -> web.Response:
|
||||||
|
await check_permission(request, 'listen')
|
||||||
|
return web.Response(body="I can listen!")
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_speak(request: web.Request) -> web.Response:
|
||||||
|
await check_permission(request, 'speak')
|
||||||
|
return web.Response(body="I can speak!")
|
||||||
|
|
||||||
|
|
||||||
|
async def make_app() -> web.Application:
|
||||||
|
#
|
||||||
|
# WARNING!!!
|
||||||
|
# Never use SimpleCookieStorage on production!!!
|
||||||
|
# It’s highly insecure!!!
|
||||||
|
#
|
||||||
|
|
||||||
|
# make app
|
||||||
|
middleware = session_middleware(SimpleCookieStorage())
|
||||||
|
app = web.Application(middlewares=[middleware])
|
||||||
|
|
||||||
|
# add the routes
|
||||||
|
app.router.add_route('GET', '/', handler_root)
|
||||||
|
app.router.add_route('GET', '/login', handler_login_jack)
|
||||||
|
app.router.add_route('GET', '/logout', handler_logout)
|
||||||
|
app.router.add_route('GET', '/listen', handler_listen)
|
||||||
|
app.router.add_route('GET', '/speak', handler_speak)
|
||||||
|
|
||||||
|
# set up policies
|
||||||
|
policy = SessionIdentityPolicy()
|
||||||
|
setup_security(app, policy, SimpleJack_AuthorizationPolicy())
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
web.run_app(make_app(), port=9000)
|
BIN
docs/_static/aiohttp-icon-128x128.png
vendored
BIN
docs/_static/aiohttp-icon-128x128.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 17 KiB |
137
docs/example.rst
137
docs/example.rst
@@ -7,68 +7,97 @@ How to Make a Simple Server With Authorization
|
|||||||
|
|
||||||
Simple example::
|
Simple example::
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from aiohttp_session import SimpleCookieStorage, session_middleware
|
||||||
@asyncio.coroutine
|
from aiohttp_security import check_permission, \
|
||||||
def root_handler(request):
|
is_anonymous, remember, forget, \
|
||||||
text = "Alive and kicking!"
|
setup as setup_security, SessionIdentityPolicy
|
||||||
return web.Response(body=text.encode('utf-8'))
|
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
||||||
|
|
||||||
# option 2: auth at a higher level?
|
|
||||||
# set user_id and allowed in the wsgi 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
|
# Demo authorization policy for only one user.
|
||||||
# wsgi doesn't do anything
|
# User 'jack' has only 'listen' permission.
|
||||||
@asyncio.coroutine
|
# For more complicated authorization policies see examples
|
||||||
def user_update_handler(request):
|
# in the 'demo' directory.
|
||||||
# identity, asked_permission
|
class SimpleJack_AuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
user_id = yield from identity_policy.identify(request)
|
async def authorized_userid(self, identity):
|
||||||
identity = yield from auth_policy.authorized_user_id(user_id)
|
"""Retrieve authorized user id.
|
||||||
allowed = yield from request.auth_policy.permits(
|
Return the user_id of the user identified by the identity
|
||||||
identity, asked_permission
|
or 'None' if no user exists related to the identity.
|
||||||
)
|
"""
|
||||||
if not allowed:
|
if identity == 'jack':
|
||||||
# how is this pluggable as well?
|
return identity
|
||||||
# ? return NotAllowedStream()
|
|
||||||
raise NotAllowedResponse()
|
|
||||||
|
|
||||||
update_user()
|
async def permits(self, identity, permission, context=None):
|
||||||
|
"""Check user permissions.
|
||||||
|
Return True if the identity is allowed the permission
|
||||||
|
in the current context, else return False.
|
||||||
|
"""
|
||||||
|
return identity == 'jack' and permission in ('listen',)
|
||||||
|
|
||||||
@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
|
async def handler_root(request):
|
||||||
app = web.Application(loop=loop, middlewares=*auth)
|
is_logged = not await is_anonymous(request)
|
||||||
|
return web.Response(text='''<html><head></head><body>
|
||||||
|
Hello, I'm Jack, I'm {logged} logged in.<br /><br />
|
||||||
|
<a href="/login">Log me in</a><br />
|
||||||
|
<a href="/logout">Log me out</a><br /><br />
|
||||||
|
Check my permissions,
|
||||||
|
when i'm logged in and logged out.<br />
|
||||||
|
<a href="/listen">Can I listen?</a><br />
|
||||||
|
<a href="/speak">Can I speak?</a><br />
|
||||||
|
</body></html>'''.format(
|
||||||
|
logged='' if is_logged else 'NOT',
|
||||||
|
), content_type='text/html')
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_login_jack(request):
|
||||||
|
redirect_response = web.HTTPFound('/')
|
||||||
|
await remember(request, redirect_response, 'jack')
|
||||||
|
raise redirect_response
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_logout(request):
|
||||||
|
redirect_response = web.HTTPFound('/')
|
||||||
|
await forget(request, redirect_response)
|
||||||
|
raise redirect_response
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_listen(request):
|
||||||
|
await check_permission(request, 'listen')
|
||||||
|
return web.Response(body="I can listen!")
|
||||||
|
|
||||||
|
|
||||||
|
async def handler_speak(request):
|
||||||
|
await check_permission(request, 'speak')
|
||||||
|
return web.Response(body="I can speak!")
|
||||||
|
|
||||||
|
|
||||||
|
async def make_app():
|
||||||
|
#
|
||||||
|
# WARNING!!!
|
||||||
|
# Never use SimpleCookieStorage on production!!!
|
||||||
|
# It’s highly insecure!!!
|
||||||
|
#
|
||||||
|
|
||||||
|
# make app
|
||||||
|
middleware = session_middleware(SimpleCookieStorage())
|
||||||
|
app = web.Application(middlewares=[middleware])
|
||||||
|
|
||||||
# add the routes
|
# add the routes
|
||||||
app.router.add_route('GET', '/', root_handler)
|
app.add_routes([
|
||||||
app.router.add_route('GET', '/{user}', user_handler)
|
web.get('/', handler_root),
|
||||||
app.router.add_route('GET', '/{user}/edit', user_update_handler)
|
web.get('/login', handler_login_jack),
|
||||||
|
web.get('/logout', handler_logout),
|
||||||
|
web.get('/listen', handler_listen),
|
||||||
|
web.get('/speak', handler_speak)])
|
||||||
|
|
||||||
# get it started
|
# set up policies
|
||||||
srv = yield from loop.create_server(app.make_handler(),
|
policy = SessionIdentityPolicy()
|
||||||
'127.0.0.1', 8080)
|
setup_security(app, policy, SimpleJack_AuthorizationPolicy())
|
||||||
print("Server started at http://127.0.0.1:8080")
|
|
||||||
return srv
|
return app
|
||||||
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
if __name__ == '__main__':
|
||||||
loop.run_until_complete(init(loop))
|
web.run_app(make_app(), port=9000)
|
||||||
try:
|
|
||||||
loop.run_forever()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass # TODO put handler cleanup here
|
|
||||||
|
@@ -21,12 +21,14 @@ Launch these sql scripts to init database and fill it with sample data:
|
|||||||
|
|
||||||
``psql template1 < demo/sql/init_db.sql``
|
``psql template1 < demo/sql/init_db.sql``
|
||||||
|
|
||||||
and then
|
and
|
||||||
|
|
||||||
``psql template1 < demo/sql/sample_data.sql``
|
``psql template1 < demo/sql/sample_data.sql``
|
||||||
|
|
||||||
|
|
||||||
You will have two tables for storing users and their permissions
|
Now you have two tables:
|
||||||
|
|
||||||
|
- for storing users
|
||||||
|
|
||||||
+--------------+
|
+--------------+
|
||||||
| users |
|
| users |
|
||||||
@@ -42,7 +44,7 @@ You will have two tables for storing users and their permissions
|
|||||||
| disabled |
|
| disabled |
|
||||||
+--------------+
|
+--------------+
|
||||||
|
|
||||||
and second table is permissions table:
|
- for storing their permissions
|
||||||
|
|
||||||
+-----------------+
|
+-----------------+
|
||||||
| permissions |
|
| permissions |
|
||||||
@@ -51,7 +53,7 @@ and second table is permissions table:
|
|||||||
+-----------------+
|
+-----------------+
|
||||||
| user_id |
|
| user_id |
|
||||||
+-----------------+
|
+-----------------+
|
||||||
| permission_name |
|
| perm_name |
|
||||||
+-----------------+
|
+-----------------+
|
||||||
|
|
||||||
|
|
||||||
@@ -63,48 +65,46 @@ First one should have these methods: *identify*, *remember* and *forget*.
|
|||||||
For second one: *authorized_userid* and *permits*. We will use built-in
|
For second one: *authorized_userid* and *permits*. We will use built-in
|
||||||
*SessionIdentityPolicy* and write our own database-based authorization policy.
|
*SessionIdentityPolicy* and write our own database-based authorization policy.
|
||||||
|
|
||||||
In our example we will lookup database by user login and if present return
|
In our example we will lookup database by user login and if presents then return
|
||||||
this identity::
|
this identity::
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def authorized_userid(self, identity):
|
||||||
def authorized_userid(self, identity):
|
async with self.dbengine as conn:
|
||||||
with (yield from self.dbengine) as conn:
|
|
||||||
where = sa.and_(db.users.c.login == identity,
|
where = sa.and_(db.users.c.login == identity,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled))
|
||||||
query = db.users.count().where(where)
|
query = db.users.count().where(where)
|
||||||
ret = yield from conn.scalar(query)
|
ret = await conn.scalar(query)
|
||||||
if ret:
|
if ret:
|
||||||
return identity
|
return identity
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
For permission check we will fetch the user first, check if he is superuser
|
For permission checking we will fetch the user first, check if he is superuser
|
||||||
(all permissions are allowed), otherwise check if permission is explicitly set
|
(all permissions are allowed), otherwise check if permission is explicitly set
|
||||||
for that user::
|
for that user::
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def permits(self, identity, permission, context=None):
|
||||||
def permits(self, identity, permission, context=None):
|
|
||||||
if identity is None:
|
if identity is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with (yield from self.dbengine) as conn:
|
async with self.dbengine as conn:
|
||||||
where = sa.and_(db.users.c.login == identity,
|
where = sa.and_(db.users.c.login == identity,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled))
|
||||||
query = db.users.select().where(where)
|
query = db.users.select().where(where)
|
||||||
ret = yield from conn.execute(query)
|
ret = await conn.execute(query)
|
||||||
user = yield from ret.fetchone()
|
user = await ret.fetchone()
|
||||||
if user is not None:
|
if user is not None:
|
||||||
user_id = user[0]
|
user_id = user[0]
|
||||||
is_superuser = user[4]
|
is_superuser = user[3]
|
||||||
if is_superuser:
|
if is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
where = db.permissions.c.user_id == user_id
|
where = db.permissions.c.user_id == user_id
|
||||||
query = db.permissions.select().where(where)
|
query = db.permissions.select().where(where)
|
||||||
ret = yield from conn.execute(query)
|
ret = await conn.execute(query)
|
||||||
result = yield from ret.fetchall()
|
result = await ret.fetchall()
|
||||||
if ret is not None:
|
if ret is not None:
|
||||||
for record in result:
|
for record in result:
|
||||||
if record.perm_name == permission:
|
if record.perm_name == permission:
|
||||||
@@ -127,14 +127,13 @@ Once we have all the code in place we can install it for our application::
|
|||||||
from .db_auth import DBAuthorizationPolicy
|
from .db_auth import DBAuthorizationPolicy
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def init(loop):
|
||||||
def init(loop):
|
redis_pool = await create_pool(('localhost', 6379))
|
||||||
redis_pool = yield from create_pool(('localhost', 6379))
|
dbengine = await create_engine(user='aiohttp_security',
|
||||||
dbengine = yield from create_engine(user='aiohttp_security',
|
password='aiohttp_security',
|
||||||
password='aiohttp_security',
|
database='aiohttp_security',
|
||||||
database='aiohttp_security',
|
host='127.0.0.1')
|
||||||
host='127.0.0.1')
|
app = web.Application()
|
||||||
app = web.Application(loop=loop)
|
|
||||||
setup_session(app, RedisStorage(redis_pool))
|
setup_session(app, RedisStorage(redis_pool))
|
||||||
setup_security(app,
|
setup_security(app,
|
||||||
SessionIdentityPolicy(),
|
SessionIdentityPolicy(),
|
||||||
@@ -143,39 +142,33 @@ Once we have all the code in place we can install it for our application::
|
|||||||
|
|
||||||
|
|
||||||
Now we have authorization and can decorate every other view with access rights
|
Now we have authorization and can decorate every other view with access rights
|
||||||
based on permissions. This simple decorator (for class-based handlers) will
|
based on permissions. There are already implemented two helpers::
|
||||||
help to do that::
|
|
||||||
|
|
||||||
def require(permission):
|
from aiohttp_security import check_authorized, check_permission
|
||||||
def wrapper(f):
|
|
||||||
@asyncio.coroutine
|
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapped(self, request):
|
|
||||||
has_perm = yield from permits(request, permission)
|
|
||||||
if not has_perm:
|
|
||||||
message = 'User has no permission {}'.format(permission)
|
|
||||||
raise web.HTTPForbidden(body=message.encode())
|
|
||||||
return (yield from f(self, request))
|
|
||||||
return wrapped
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
For each view you need to protect - just apply the decorator on it::
|
||||||
For each view you need to protect just apply the decorator on it::
|
|
||||||
|
|
||||||
class Web:
|
class Web:
|
||||||
@require('protected')
|
async def protected_page(self, request):
|
||||||
@asyncio.coroutine
|
await check_permission(request, 'protected')
|
||||||
def protected_page(self, request):
|
|
||||||
response = web.Response(body=b'You are on protected page')
|
response = web.Response(body=b'You are on protected page')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
If someone will try to access this protected page he will see::
|
class Web:
|
||||||
|
async def logout(self, request):
|
||||||
|
await check_authorized(request)
|
||||||
|
response = web.Response(body=b'You have been logged out')
|
||||||
|
await forget(request, response)
|
||||||
|
return response
|
||||||
|
|
||||||
403, User has no permission "protected"
|
If someone try to access that protected page he will see::
|
||||||
|
|
||||||
|
403: Forbidden
|
||||||
|
|
||||||
|
|
||||||
The best part about it is that you can implement any logic you want until it
|
The best part of it - you can implement any logic you want until it
|
||||||
follows the API conventions.
|
follows the API conventions.
|
||||||
|
|
||||||
Launch application
|
Launch application
|
||||||
@@ -183,18 +176,17 @@ Launch application
|
|||||||
|
|
||||||
For working with passwords there is a good library passlib_. Once you've
|
For working with passwords there is a good library passlib_. Once you've
|
||||||
created some users you want to check their credentials on login. Similar
|
created some users you want to check their credentials on login. Similar
|
||||||
function may do what you trying to accomplish::
|
function may do what you are trying to accomplish::
|
||||||
|
|
||||||
from passlib.hash import sha256_crypt
|
from passlib.hash import sha256_crypt
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check_credentials(db_engine, username, password):
|
||||||
def check_credentials(db_engine, username, password):
|
async with db_engine as conn:
|
||||||
with (yield from db_engine) as conn:
|
|
||||||
where = sa.and_(db.users.c.login == username,
|
where = sa.and_(db.users.c.login == username,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled))
|
||||||
query = db.users.select().where(where)
|
query = db.users.select().where(where)
|
||||||
ret = yield from conn.execute(query)
|
ret = await conn.execute(query)
|
||||||
user = yield from ret.fetchone()
|
user = await ret.fetchone()
|
||||||
if user is not None:
|
if user is not None:
|
||||||
hash = user[2]
|
hash = user[2]
|
||||||
return sha256_crypt.verify(password, hash)
|
return sha256_crypt.verify(password, hash)
|
||||||
@@ -203,8 +195,8 @@ function may do what you trying to accomplish::
|
|||||||
|
|
||||||
Final step is to launch your application::
|
Final step is to launch your application::
|
||||||
|
|
||||||
python demo/main.py
|
python demo/database_auth/main.py
|
||||||
|
|
||||||
|
|
||||||
Try to login with admin/moderator/user accounts (with *password* password)
|
Try to login with admin/moderator/user accounts (with **password** password)
|
||||||
and access **/public** or **/protected** endpoints.
|
and access **/public** or **/protected** endpoints.
|
||||||
|
@@ -3,16 +3,10 @@ aiohttp_security
|
|||||||
|
|
||||||
The library provides security for :ref:`aiohttp.web<aiohttp-web>`.
|
The library provides security for :ref:`aiohttp.web<aiohttp-web>`.
|
||||||
|
|
||||||
Usage
|
The current version is |version|
|
||||||
-----
|
|
||||||
|
|
||||||
|
Contents
|
||||||
License
|
--------
|
||||||
-------
|
|
||||||
|
|
||||||
``aiohttp_security`` is offered under the Apache 2 license.
|
|
||||||
|
|
||||||
Contents:
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
@@ -23,7 +17,10 @@ Contents:
|
|||||||
example_db_auth
|
example_db_auth
|
||||||
glossary
|
glossary
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
``aiohttp_security`` is offered under the Apache 2 license.
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
@@ -13,6 +13,19 @@
|
|||||||
Public API functions
|
Public API functions
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
.. function:: setup(app, identity_policy, autz_policy)
|
||||||
|
|
||||||
|
Setup :mod:`aiohttp` application with security policies.
|
||||||
|
|
||||||
|
:param app: aiohttp :class:`aiohttp.web.Application` instance.
|
||||||
|
|
||||||
|
:param identity_policy: indentification policy, an
|
||||||
|
:class:`AbstractIdentityPolicy` instance.
|
||||||
|
|
||||||
|
:param autz_policy: authorization policy, an
|
||||||
|
:class:`AbstractAuthorizationPolicy` instance.
|
||||||
|
|
||||||
|
|
||||||
.. coroutinefunction:: remember(request, response, identity, **kwargs)
|
.. coroutinefunction:: remember(request, response, identity, **kwargs)
|
||||||
|
|
||||||
Remember *identity* in *response*, e.g. by storing a cookie or
|
Remember *identity* in *response*, e.g. by storing a cookie or
|
||||||
@@ -21,7 +34,7 @@ Public API functions
|
|||||||
The action is performed by registered
|
The action is performed by registered
|
||||||
:meth:`AbstractIdentityPolicy.remember`.
|
:meth:`AbstractIdentityPolicy.remember`.
|
||||||
|
|
||||||
Usually the *idenity* is stored in user cookies homehow for using by
|
Usually the *identity* is stored in user cookies somehow for using by
|
||||||
:func:`authorized_userid` and :func:`permits`.
|
:func:`authorized_userid` and :func:`permits`.
|
||||||
|
|
||||||
:param request: :class:`aiohttp.web.Request` object.
|
:param request: :class:`aiohttp.web.Request` object.
|
||||||
@@ -50,6 +63,41 @@ Public API functions
|
|||||||
descendants like :class:`aiohttp.web.Response`.
|
descendants like :class:`aiohttp.web.Response`.
|
||||||
|
|
||||||
|
|
||||||
|
.. coroutinefunction:: check_authorized(request)
|
||||||
|
|
||||||
|
Checker that doesn't pass if user is not authorized by *request*.
|
||||||
|
|
||||||
|
:param request: :class:`aiohttp.web.Request` object.
|
||||||
|
|
||||||
|
:return str: authorized user ID if success
|
||||||
|
|
||||||
|
:raise: :class:`aiohttp.web.HTTPUnauthorized` for anonymous users.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
await check_authorized(request)
|
||||||
|
# this line is never executed for anonymous users
|
||||||
|
|
||||||
|
|
||||||
|
.. coroutinefunction:: check_permission(request, permission)
|
||||||
|
|
||||||
|
Checker that doesn't pass if user has no requested permission.
|
||||||
|
|
||||||
|
:param request: :class:`aiohttp.web.Request` object.
|
||||||
|
|
||||||
|
:raise: :class:`aiohttp.web.HTTPUnauthorized` for anonymous users.
|
||||||
|
|
||||||
|
:raise: :class:`aiohttp.web.HTTPForbidden` if user is
|
||||||
|
authorized but has no access rights.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
await check_permission(request, 'read')
|
||||||
|
# this line is never executed if a user has no read permission
|
||||||
|
|
||||||
|
|
||||||
.. coroutinefunction:: authorized_userid(request)
|
.. coroutinefunction:: authorized_userid(request)
|
||||||
|
|
||||||
Retrieve :term:`userid`.
|
Retrieve :term:`userid`.
|
||||||
@@ -78,7 +126,8 @@ Public API functions
|
|||||||
|
|
||||||
:param request: :class:`aiohttp.web.Request` object.
|
:param request: :class:`aiohttp.web.Request` object.
|
||||||
|
|
||||||
:param str permission: requested :term:`permission`.
|
:param permission: Requested :term:`permission`. :class:`str` or
|
||||||
|
:class:`enum.Enum` object.
|
||||||
|
|
||||||
:param context: additional object may be passed into
|
:param context: additional object may be passed into
|
||||||
:meth:`AbstractAuthorizationPolicy.permission`
|
:meth:`AbstractAuthorizationPolicy.permission`
|
||||||
@@ -88,17 +137,43 @@ Public API functions
|
|||||||
``False`` otherwise.
|
``False`` otherwise.
|
||||||
|
|
||||||
|
|
||||||
.. function:: setup(app, identity_policy, autz_policy)
|
.. coroutinefunction:: is_anonymous(request)
|
||||||
|
|
||||||
Setup :mod:`aiohttp` application with security policies.
|
Checks if user is anonymous user.
|
||||||
|
|
||||||
:param app: aiohttp :class:`aiohttp.web.Application` instance.
|
Return ``True`` if user is not remembered in request, otherwise
|
||||||
|
returns ``False``.
|
||||||
|
|
||||||
:param identity_policy: indentification policy, an
|
:param request: :class:`aiohttp.web.Request` object.
|
||||||
:class:`AbstractIdentityPolicy` instance.
|
|
||||||
|
|
||||||
:param autz_policy: authorization policy, an
|
|
||||||
:class:`AbstractAuthorizationPolicy` instance.
|
.. decorator:: login_required
|
||||||
|
|
||||||
|
Decorator for handlers that checks if user is authorized.
|
||||||
|
|
||||||
|
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized.
|
||||||
|
|
||||||
|
.. deprecated:: 0.3
|
||||||
|
|
||||||
|
Use :func:`check_authorized` async function.
|
||||||
|
|
||||||
|
|
||||||
|
.. decorator:: has_permission(permission)
|
||||||
|
|
||||||
|
Decorator for handlers that checks if user is authorized
|
||||||
|
and has correct permission.
|
||||||
|
|
||||||
|
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not
|
||||||
|
authorized.
|
||||||
|
|
||||||
|
Raises :class:`aiohttp.web.HTTPForbidden` if user is
|
||||||
|
authorized but has no access rights.
|
||||||
|
|
||||||
|
:param str permission: requested :term:`permission`.
|
||||||
|
|
||||||
|
.. deprecated:: 0.3
|
||||||
|
|
||||||
|
Use :func:`check_authorized` async function.
|
||||||
|
|
||||||
|
|
||||||
Abstract policies
|
Abstract policies
|
||||||
|
137
docs/usage.rst
137
docs/usage.rst
@@ -11,43 +11,132 @@
|
|||||||
|
|
||||||
First of all, what is *aiohttp_security* about?
|
First of all, what is *aiohttp_security* about?
|
||||||
|
|
||||||
It is a set of public API functions and standard for implementation details.
|
*aiohttp-security* is a set of public API functions as well as a
|
||||||
|
reference standard for implementation details for securing access to
|
||||||
|
assets served by a wsgi server.
|
||||||
|
|
||||||
API is implementation agnostic, all client code should not call policy
|
Assets are secured using authentication and authorization as explained
|
||||||
code (see below) directly but use API only.
|
below. *aiohttp-security* is part of the
|
||||||
|
`aio-libs <https://github.com/aio-libs>`_ project which takes advantage
|
||||||
|
of asynchronous processing using Python's asyncio library.
|
||||||
|
|
||||||
Via API application can remember/forget user in local session
|
|
||||||
(:func:`remember`/:func:`forget`), retrieve :term:`userid`
|
|
||||||
(:func:`authorized_userid`) and check :term:`permission` for
|
|
||||||
remembered user (:func:`permits`).
|
|
||||||
|
|
||||||
The library internals are built on top of two policies:
|
|
||||||
:term:`authentication` and :term:`authorization`. There are abstract
|
|
||||||
base classes for both concepts as well as several implementations
|
|
||||||
shipped with the library. End user is free to build own implemetations
|
|
||||||
if needed.
|
|
||||||
|
|
||||||
Public API
|
Public API
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
The API is agnostic to the low level implementation details such that
|
||||||
|
all client code only needs to implement the endpoints as provided by
|
||||||
|
the API (instead of calling policy code directly (see explanation
|
||||||
|
below)).
|
||||||
|
|
||||||
|
Via the API an application can:
|
||||||
|
|
||||||
|
(i) remember a user in a local session (:func:`remember`),
|
||||||
|
(ii) forget a user in a local session (:func:`forget`),
|
||||||
|
(iii) retrieve the :term:`userid` (:func:`authorized_userid`) of a
|
||||||
|
remembered user from an :term:`identity` (discussed below), and
|
||||||
|
(iv) check the :term:`permission` of a remembered user (:func:`permits`).
|
||||||
|
|
||||||
|
The library internals are built on top of two concepts:
|
||||||
|
|
||||||
|
1) :term:`authentication`, and
|
||||||
|
2) :term:`authorization`.
|
||||||
|
|
||||||
|
There are abstract base classes for both types as well as several
|
||||||
|
pre-built implementations that are shipped with the library. However,
|
||||||
|
the end user is free to build their own implementations.
|
||||||
|
|
||||||
|
The library comes with two pre-built identity policies; one that uses
|
||||||
|
cookies, and one that uses sessions [#f1]_. It is envisioned that in
|
||||||
|
most use cases developers will use one of the provided identity
|
||||||
|
policies (Cookie or Session) and implement their own authorization
|
||||||
|
policy.
|
||||||
|
|
||||||
|
The workflow is as follows:
|
||||||
|
|
||||||
|
1) User is authenticated. This has to be implemented by the developer.
|
||||||
|
2) Once user is authenticated an identity string has to be created for
|
||||||
|
that user. This has to be implemented by the developer.
|
||||||
|
3) The identity string is passed to the Identity Policy's remember
|
||||||
|
method and the user is now remembered (Cookie or Session if using
|
||||||
|
built-in). *Only once a user is remembered can the other API
|
||||||
|
methods:* :func:`permits`, :func:`forget`, *and*
|
||||||
|
:func:`authorized_userid` *be invoked* .
|
||||||
|
4) If the user tries to access a restricted asset the :func:`permits`
|
||||||
|
method is called. Usually assets are protected using the
|
||||||
|
:func:`check_permission` helper. This should return True if
|
||||||
|
permission is granted.
|
||||||
|
|
||||||
|
The :func:`permits` method is implemented by the developer as part of
|
||||||
|
the :class:`AbstractAuthorizationPolicy` and passed to the
|
||||||
|
application at runtime via setup.
|
||||||
|
|
||||||
|
In addition a :func:`check_authorized` also
|
||||||
|
exists that requires no permissions (i.e. doesn't call :func:`permits`
|
||||||
|
method) but only requires that the user is remembered
|
||||||
|
(i.e. authenticated/logged in).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Authentication
|
Authentication
|
||||||
==============
|
==============
|
||||||
|
|
||||||
Actions related to retrieving, storing and removing user's
|
Authentication is the process where a user's identity is verified. It
|
||||||
:term:`identity`.
|
confirms who the user is. This is traditionally done using a user name
|
||||||
|
and password (note: this is not the only way).
|
||||||
|
|
||||||
Authenticated user has no access rights, the system even has no
|
A authenticated user has no access rights, rather an authenticated
|
||||||
knowledge is there the user still registered in DB.
|
user merely confirms that the user exists and that the user is who
|
||||||
|
they say they are.
|
||||||
|
|
||||||
If :class:`aiohttp.web.Request` has an :term:`identity` it means the user has
|
In *aiohttp_security* the developer is responsible for their own
|
||||||
some ID that should be checked by :term:`authorization` policy.
|
authentication mechanism. *aiohttp_security* only requires that the
|
||||||
|
authentication result in a identity string which corresponds to a
|
||||||
|
user's id in the underlying system.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
:term:`identity` is a string that is shared between the browser and
|
||||||
|
the server. Therefore it is recommended that a random string
|
||||||
|
such as a uuid or hash is used rather than things like a
|
||||||
|
database primary key, user login/email, etc.
|
||||||
|
|
||||||
|
Identity Policy
|
||||||
|
===============
|
||||||
|
|
||||||
|
Once a user is authenticated the *aiohttp_security* API is invoked for
|
||||||
|
storing, retrieving, and removing a user's :term:`identity`. This is
|
||||||
|
accommplished via AbstractIdentityPolicy's :func:`remember`,
|
||||||
|
:func:`identify`, and :func:`forget` methods. The Identity Policy is
|
||||||
|
therefore the mechanism by which a authenticated user is persisted in
|
||||||
|
the system.
|
||||||
|
|
||||||
|
*aiohttp_security* has two built in identity policy's for this
|
||||||
|
purpose. :class:`CookiesIdentityPolicy` that uses cookies and
|
||||||
|
:class:`SessionIdentityPolicy` that uses sessions via
|
||||||
|
``aiohttp-session`` library.
|
||||||
|
|
||||||
|
Authorization
|
||||||
|
==============
|
||||||
|
|
||||||
|
Once a user is authenticated (see above) it means that the user has an
|
||||||
|
:term:`identity`. This :term:`identity` can now be used for checking
|
||||||
|
access rights or :term:`permission` using a :term:`authorization`
|
||||||
|
policy.
|
||||||
|
|
||||||
|
The authorization policy's :func:`permits()` method is used for this purpose.
|
||||||
|
|
||||||
|
|
||||||
|
When :class:`aiohttp.web.Request` has an :term:`identity` it means the
|
||||||
|
user has been authenticated and therefore has an :term:`identity` that
|
||||||
|
can be checked by the :term:`authorization` policy.
|
||||||
|
|
||||||
|
As noted above, :term:`identity` is a string that is shared between
|
||||||
|
the browser and the server. Therefore it is recommended that a
|
||||||
|
random string such as a uuid or hash is used rather than things like
|
||||||
|
a database primary key, user login/email, etc.
|
||||||
|
|
||||||
|
|
||||||
|
.. rubric:: Footnotes
|
||||||
|
.. [#f1] jwt - json web tokens in the works
|
||||||
identity is a string shared between browser and server.
|
|
||||||
Thus it's not supposed to be database primary key, user login/email etc.
|
|
||||||
Random string like uuid or hash is better choice.
|
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
-e .
|
-e .
|
||||||
flake8==3.4.1
|
flake8==3.8.4
|
||||||
pytest==3.2.3
|
async-timeout==3.0.1
|
||||||
pytest-cov==2.5.1
|
pytest==6.1.2
|
||||||
coverage==4.4.1
|
pytest-cov==2.10.1
|
||||||
sphinx==1.6.4
|
pytest-mock==3.3.1
|
||||||
|
coverage==5.3
|
||||||
|
sphinx==3.3.1
|
||||||
pep257==0.7.0
|
pep257==0.7.0
|
||||||
aiohttp-session==1.0.1
|
aiohttp-session==2.9.0
|
||||||
aiopg[sa]==0.13.1
|
aiopg[sa]==1.1.0
|
||||||
aioredis==0.3.3
|
aioredis==1.3.1
|
||||||
hiredis==0.2.0
|
hiredis==1.1.0
|
||||||
passlib==1.7.1
|
passlib==1.7.4
|
||||||
aiohttp==2.2.5
|
cryptography==3.3.1
|
||||||
pytest-aiohttp==0.1.3
|
aiohttp==3.7.3
|
||||||
|
pytest-aiohttp==0.3.0
|
||||||
|
pyjwt==1.7.1
|
||||||
|
4
setup.cfg
Normal file
4
setup.cfg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[tool:pytest]
|
||||||
|
testpaths = tests
|
||||||
|
filterwarnings=
|
||||||
|
error
|
9
setup.py
9
setup.py
@@ -1,4 +1,3 @@
|
|||||||
import codecs
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -16,8 +15,8 @@ class PyTest(TestCommand):
|
|||||||
raise SystemExit(errno)
|
raise SystemExit(errno)
|
||||||
|
|
||||||
|
|
||||||
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
|
with open(os.path.join(os.path.abspath(os.path.dirname(
|
||||||
__file__)), 'aiohttp_security', '__init__.py'), 'r', 'latin1') as fp:
|
__file__)), 'aiohttp_security', '__init__.py'), 'r', encoding='latin1') as fp:
|
||||||
try:
|
try:
|
||||||
version = re.findall(r"^__version__ = '([^']+)'$", fp.read(), re.M)[0]
|
version = re.findall(r"^__version__ = '([^']+)'$", fp.read(), re.M)[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@@ -28,7 +27,7 @@ def read(f):
|
|||||||
return open(os.path.join(os.path.dirname(__file__), f)).read().strip()
|
return open(os.path.join(os.path.dirname(__file__), f)).read().strip()
|
||||||
|
|
||||||
|
|
||||||
install_requires = ['aiohttp>=0.18']
|
install_requires = ['aiohttp>=3.2.0']
|
||||||
tests_require = install_requires + ['pytest']
|
tests_require = install_requires + ['pytest']
|
||||||
extras_require = {'session': 'aiohttp-session'}
|
extras_require = {'session': 'aiohttp-session'}
|
||||||
|
|
||||||
@@ -42,9 +41,9 @@ setup(name='aiohttp-security',
|
|||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
'Topic :: Internet :: WWW/HTTP',
|
'Topic :: Internet :: WWW/HTTP',
|
||||||
'Framework :: AsyncIO',
|
'Framework :: AsyncIO',
|
||||||
],
|
],
|
||||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@@ -1,5 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_security import (remember, forget,
|
from aiohttp_security import (remember, forget,
|
||||||
AbstractAuthorizationPolicy)
|
AbstractAuthorizationPolicy)
|
||||||
@@ -10,99 +8,85 @@ from aiohttp_security.api import IDENTITY_KEY
|
|||||||
|
|
||||||
class Autz(AbstractAuthorizationPolicy):
|
class Autz(AbstractAuthorizationPolicy):
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def permits(self, identity, permission, context=None):
|
||||||
def permits(self, identity, permission, context=None):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def authorized_userid(self, identity):
|
||||||
def authorized_userid(self, identity):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_remember(loop, aiohttp_client):
|
||||||
def test_remember(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def handler(request):
|
||||||
def handler(request):
|
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
yield from remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', handler)
|
app.router.add_route('GET', '/', handler)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert 'Andrew' == resp.cookies['AIOHTTP_SECURITY'].value
|
assert 'Andrew' == resp.cookies['AIOHTTP_SECURITY'].value
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_identify(loop, aiohttp_client):
|
||||||
def test_identify(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def create(request):
|
||||||
def create(request):
|
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
yield from remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
|
||||||
policy = request.app[IDENTITY_KEY]
|
policy = request.app[IDENTITY_KEY]
|
||||||
user_id = yield from policy.identify(request)
|
user_id = await policy.identify(request)
|
||||||
assert 'Andrew' == user_id
|
assert 'Andrew' == user_id
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
app.router.add_route('POST', '/', create)
|
app.router.add_route('POST', '/', create)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.post('/')
|
resp = await client.post('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
await resp.release()
|
||||||
resp = yield from client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_forget(loop, aiohttp_client):
|
||||||
def test_forget(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def index(request):
|
||||||
def index(request):
|
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def login(request):
|
||||||
def login(request):
|
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
yield from remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
return response
|
raise response
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def logout(request):
|
||||||
def logout(request):
|
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
yield from forget(request, response)
|
await forget(request, response)
|
||||||
return response
|
raise response
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', index)
|
app.router.add_route('GET', '/', index)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
app.router.add_route('POST', '/logout', logout)
|
app.router.add_route('POST', '/logout', logout)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.post('/login')
|
resp = await client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert str(resp.url).endswith('/')
|
assert str(resp.url).endswith('/')
|
||||||
cookies = client.session.cookie_jar.filter_cookies(
|
cookies = client.session.cookie_jar.filter_cookies(
|
||||||
client.make_url('/'))
|
client.make_url('/'))
|
||||||
assert 'Andrew' == cookies['AIOHTTP_SECURITY'].value
|
assert 'Andrew' == cookies['AIOHTTP_SECURITY'].value
|
||||||
yield from resp.release()
|
|
||||||
resp = yield from client.post('/logout')
|
resp = await client.post('/logout')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert str(resp.url).endswith('/')
|
assert str(resp.url).endswith('/')
|
||||||
cookies = client.session.cookie_jar.filter_cookies(
|
cookies = client.session.cookie_jar.filter_cookies(
|
||||||
client.make_url('/'))
|
client.make_url('/'))
|
||||||
assert 'AIOHTTP_SECURITY' not in cookies
|
assert 'AIOHTTP_SECURITY' not in cookies
|
||||||
yield from resp.release()
|
|
||||||
|
@@ -1,123 +1,348 @@
|
|||||||
import asyncio
|
import enum
|
||||||
|
import pytest
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_security import (remember,
|
|
||||||
authorized_userid, permits,
|
|
||||||
AbstractAuthorizationPolicy)
|
|
||||||
from aiohttp_security import setup as _setup
|
from aiohttp_security import setup as _setup
|
||||||
|
from aiohttp_security import (AbstractAuthorizationPolicy, authorized_userid,
|
||||||
|
forget, has_permission, is_anonymous,
|
||||||
|
login_required, permits, remember,
|
||||||
|
check_authorized, check_permission)
|
||||||
from aiohttp_security.cookies_identity import CookiesIdentityPolicy
|
from aiohttp_security.cookies_identity import CookiesIdentityPolicy
|
||||||
|
|
||||||
|
|
||||||
class Autz(AbstractAuthorizationPolicy):
|
class Autz(AbstractAuthorizationPolicy):
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def permits(self, identity, permission, context=None):
|
||||||
def permits(self, identity, permission, context=None):
|
|
||||||
if identity == 'UserID':
|
if identity == 'UserID':
|
||||||
return permission in {'read', 'write'}
|
return permission in {'read', 'write'}
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def authorized_userid(self, identity):
|
||||||
def authorized_userid(self, identity):
|
|
||||||
if identity == 'UserID':
|
if identity == 'UserID':
|
||||||
return 'Andrew'
|
return 'Andrew'
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_authorized_userid(loop, aiohttp_client):
|
||||||
def test_authorized_userid(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def login(request):
|
||||||
def login(request):
|
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
yield from remember(request, response, 'UserID')
|
await remember(request, response, 'UserID')
|
||||||
return response
|
raise response
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
userid = await authorized_userid(request)
|
||||||
userid = yield from authorized_userid(request)
|
|
||||||
assert 'Andrew' == userid
|
assert 'Andrew' == userid
|
||||||
return web.Response(text=userid)
|
return web.Response(text=userid)
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = yield from client.post('/login')
|
resp = await client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
txt = yield from resp.text()
|
txt = await resp.text()
|
||||||
assert 'Andrew' == txt
|
assert 'Andrew' == txt
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_authorized_userid_not_authorized(loop, aiohttp_client):
|
||||||
def test_authorized_userid_not_authorized(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
userid = await authorized_userid(request)
|
||||||
userid = yield from authorized_userid(request)
|
|
||||||
assert userid is None
|
assert userid is None
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = yield from client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_permits_enum_permission(loop, aiohttp_client):
|
||||||
def test_permits(loop, test_client):
|
class Permission(enum.Enum):
|
||||||
|
READ = '101'
|
||||||
|
WRITE = '102'
|
||||||
|
UNKNOWN = '103'
|
||||||
|
|
||||||
@asyncio.coroutine
|
class Autz(AbstractAuthorizationPolicy):
|
||||||
def login(request):
|
|
||||||
|
async def permits(self, identity, permission, context=None):
|
||||||
|
if identity == 'UserID':
|
||||||
|
return permission in {Permission.READ, Permission.WRITE}
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def authorized_userid(self, identity):
|
||||||
|
if identity == 'UserID':
|
||||||
|
return 'Andrew'
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def login(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
yield from remember(request, response, 'UserID')
|
await remember(request, response, 'UserID')
|
||||||
return response
|
raise response
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
ret = await permits(request, Permission.READ)
|
||||||
ret = yield from permits(request, 'read')
|
|
||||||
assert ret
|
assert ret
|
||||||
ret = yield from permits(request, 'write')
|
ret = await permits(request, Permission.WRITE)
|
||||||
assert ret
|
assert ret
|
||||||
ret = yield from permits(request, 'unknown')
|
ret = await permits(request, Permission.UNKNOWN)
|
||||||
assert not ret
|
assert not ret
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.post('/login')
|
resp = await client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_permits_unauthorized(loop, aiohttp_client):
|
||||||
def test_permits_unauthorized(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
ret = await permits(request, 'read')
|
||||||
ret = yield from permits(request, 'read')
|
|
||||||
assert not ret
|
assert not ret
|
||||||
ret = yield from permits(request, 'write')
|
ret = await permits(request, 'write')
|
||||||
assert not ret
|
assert not ret
|
||||||
ret = yield from permits(request, 'unknown')
|
ret = await permits(request, 'unknown')
|
||||||
assert not ret
|
assert not ret
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
async def test_is_anonymous(loop, aiohttp_client):
|
||||||
|
|
||||||
|
async def index(request):
|
||||||
|
is_anon = await is_anonymous(request)
|
||||||
|
if is_anon:
|
||||||
|
raise web.HTTPUnauthorized()
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
async def login(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await remember(request, response, 'UserID')
|
||||||
|
raise response
|
||||||
|
|
||||||
|
async def logout(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await forget(request, response)
|
||||||
|
raise response
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
|
app.router.add_route('GET', '/', index)
|
||||||
|
app.router.add_route('POST', '/login', login)
|
||||||
|
app.router.add_route('POST', '/logout', logout)
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/login')
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPOk.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/logout')
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
|
||||||
|
async def test_login_required(loop, aiohttp_client):
|
||||||
|
with pytest.raises(DeprecationWarning):
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
async def index(request):
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
async def login(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await remember(request, response, 'UserID')
|
||||||
|
raise response
|
||||||
|
|
||||||
|
async def logout(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await forget(request, response)
|
||||||
|
raise response
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
|
app.router.add_route('GET', '/', index)
|
||||||
|
app.router.add_route('POST', '/login', login)
|
||||||
|
app.router.add_route('POST', '/logout', logout)
|
||||||
|
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/login')
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPOk.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/logout')
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
|
||||||
|
async def test_check_authorized(loop, aiohttp_client):
|
||||||
|
async def index(request):
|
||||||
|
await check_authorized(request)
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
async def login(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await remember(request, response, 'UserID')
|
||||||
|
raise response
|
||||||
|
|
||||||
|
async def logout(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await forget(request, response)
|
||||||
|
raise response
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
|
app.router.add_route('GET', '/', index)
|
||||||
|
app.router.add_route('POST', '/login', login)
|
||||||
|
app.router.add_route('POST', '/logout', logout)
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/login')
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPOk.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/logout')
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
|
||||||
|
async def test_has_permission(loop, aiohttp_client):
|
||||||
|
|
||||||
|
with pytest.warns(DeprecationWarning):
|
||||||
|
|
||||||
|
@has_permission('read')
|
||||||
|
async def index_read(request):
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
@has_permission('write')
|
||||||
|
async def index_write(request):
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
@has_permission('forbid')
|
||||||
|
async def index_forbid(request):
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
async def login(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await remember(request, response, 'UserID')
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def logout(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await forget(request, response)
|
||||||
|
raise response
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
|
app.router.add_route('GET', '/permission/read', index_read)
|
||||||
|
app.router.add_route('GET', '/permission/write', index_write)
|
||||||
|
app.router.add_route('GET', '/permission/forbid', index_forbid)
|
||||||
|
app.router.add_route('POST', '/login', login)
|
||||||
|
app.router.add_route('POST', '/logout', logout)
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
|
resp = await client.get('/permission/read')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/write')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/forbid')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/login')
|
||||||
|
resp = await client.get('/permission/read')
|
||||||
|
assert web.HTTPOk.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/write')
|
||||||
|
assert web.HTTPOk.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/forbid')
|
||||||
|
assert web.HTTPForbidden.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/logout')
|
||||||
|
resp = await client.get('/permission/read')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/write')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/forbid')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
|
||||||
|
async def test_check_permission(loop, aiohttp_client):
|
||||||
|
|
||||||
|
async def index_read(request):
|
||||||
|
await check_permission(request, 'read')
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
async def index_write(request):
|
||||||
|
await check_permission(request, 'write')
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
async def index_forbid(request):
|
||||||
|
await check_permission(request, 'forbid')
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
async def login(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await remember(request, response, 'UserID')
|
||||||
|
raise response
|
||||||
|
|
||||||
|
async def logout(request):
|
||||||
|
response = web.HTTPFound(location='/')
|
||||||
|
await forget(request, response)
|
||||||
|
raise response
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
|
app.router.add_route('GET', '/permission/read', index_read)
|
||||||
|
app.router.add_route('GET', '/permission/write', index_write)
|
||||||
|
app.router.add_route('GET', '/permission/forbid', index_forbid)
|
||||||
|
app.router.add_route('POST', '/login', login)
|
||||||
|
app.router.add_route('POST', '/logout', logout)
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
|
resp = await client.get('/permission/read')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/write')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/forbid')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/login')
|
||||||
|
resp = await client.get('/permission/read')
|
||||||
|
assert web.HTTPOk.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/write')
|
||||||
|
assert web.HTTPOk.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/forbid')
|
||||||
|
assert web.HTTPForbidden.status_code == resp.status
|
||||||
|
|
||||||
|
await client.post('/logout')
|
||||||
|
resp = await client.get('/permission/read')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/write')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
resp = await client.get('/permission/forbid')
|
||||||
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
82
tests/test_jwt_identity.py
Normal file
82
tests/test_jwt_identity.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import jwt
|
||||||
|
import pytest
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from aiohttp_security import setup as _setup
|
||||||
|
from aiohttp_security import AbstractAuthorizationPolicy
|
||||||
|
from aiohttp_security.api import IDENTITY_KEY
|
||||||
|
from aiohttp_security.jwt_identity import JWTIdentityPolicy
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def make_token():
|
||||||
|
def factory(payload, secret):
|
||||||
|
return jwt.encode(
|
||||||
|
payload,
|
||||||
|
secret,
|
||||||
|
algorithm='HS256',
|
||||||
|
)
|
||||||
|
|
||||||
|
return factory
|
||||||
|
|
||||||
|
|
||||||
|
class Autz(AbstractAuthorizationPolicy):
|
||||||
|
|
||||||
|
async def permits(self, identity, permission, context=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def authorized_userid(self, identity):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_pyjwt_installed(mocker):
|
||||||
|
mocker.patch('aiohttp_security.jwt_identity.jwt', None)
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
JWTIdentityPolicy('secret')
|
||||||
|
|
||||||
|
|
||||||
|
async def test_identify(loop, make_token, aiohttp_client):
|
||||||
|
kwt_secret_key = 'Key'
|
||||||
|
|
||||||
|
token = make_token({'login': 'Andrew'}, kwt_secret_key)
|
||||||
|
|
||||||
|
async def check(request):
|
||||||
|
policy = request.app[IDENTITY_KEY]
|
||||||
|
identity = await policy.identify(request)
|
||||||
|
assert 'Andrew' == identity['login']
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
_setup(app, JWTIdentityPolicy(kwt_secret_key), Autz())
|
||||||
|
app.router.add_route('GET', '/', check)
|
||||||
|
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
headers = {'Authorization': 'Bearer {}'.format(token.decode('utf-8'))}
|
||||||
|
resp = await client.get('/', headers=headers)
|
||||||
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
|
||||||
|
async def test_identify_broken_scheme(loop, make_token, aiohttp_client):
|
||||||
|
kwt_secret_key = 'Key'
|
||||||
|
|
||||||
|
token = make_token({'login': 'Andrew'}, kwt_secret_key)
|
||||||
|
|
||||||
|
async def check(request):
|
||||||
|
policy = request.app[IDENTITY_KEY]
|
||||||
|
|
||||||
|
try:
|
||||||
|
await policy.identify(request)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise web.HTTPBadRequest(reason=str(exc))
|
||||||
|
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
_setup(app, JWTIdentityPolicy(kwt_secret_key), Autz())
|
||||||
|
app.router.add_route('GET', '/', check)
|
||||||
|
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
headers = {'Authorization': 'Token {}'.format(token.decode('utf-8'))}
|
||||||
|
resp = await client.get('/', headers=headers)
|
||||||
|
assert 400 == resp.status
|
||||||
|
assert 'Invalid authorization scheme' in resp.reason
|
@@ -1,42 +1,34 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_security import authorized_userid, permits
|
from aiohttp_security import authorized_userid, permits
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_authorized_userid(loop, aiohttp_client):
|
||||||
def test_authorized_userid(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
userid = await authorized_userid(request)
|
||||||
userid = yield from authorized_userid(request)
|
|
||||||
assert userid is None
|
assert userid is None
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_permits(loop, aiohttp_client):
|
||||||
def test_permits(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
ret = await permits(request, 'read')
|
||||||
ret = yield from permits(request, 'read')
|
|
||||||
assert ret
|
assert ret
|
||||||
ret = yield from permits(request, 'write')
|
ret = await permits(request, 'write')
|
||||||
assert ret
|
assert ret
|
||||||
ret = yield from permits(request, 'unknown')
|
ret = await permits(request, 'unknown')
|
||||||
assert ret
|
assert ret
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
@@ -1,42 +1,34 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_security import remember, forget
|
from aiohttp_security import remember, forget
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_remember(loop, aiohttp_client):
|
||||||
def test_remember(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def do_remember(request):
|
||||||
def do_remember(request):
|
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
yield from remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
app.router.add_route('POST', '/', do_remember)
|
app.router.add_route('POST', '/', do_remember)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.post('/')
|
resp = await client.post('/')
|
||||||
assert 500 == resp.status
|
assert 500 == resp.status
|
||||||
assert (('Security subsystem is not initialized, '
|
assert (('Security subsystem is not initialized, '
|
||||||
'call aiohttp_security.setup(...) first') ==
|
'call aiohttp_security.setup(...) first') ==
|
||||||
resp.reason)
|
resp.reason)
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_forget(loop, aiohttp_client):
|
||||||
def test_forget(loop, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def do_forget(request):
|
||||||
def do_forget(request):
|
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
yield from forget(request, response)
|
await forget(request, response)
|
||||||
|
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
app.router.add_route('POST', '/', do_forget)
|
app.router.add_route('POST', '/', do_forget)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.post('/')
|
resp = await client.post('/')
|
||||||
assert 500 == resp.status
|
assert 500 == resp.status
|
||||||
assert (('Security subsystem is not initialized, '
|
assert (('Security subsystem is not initialized, '
|
||||||
'call aiohttp_security.setup(...) first') ==
|
'call aiohttp_security.setup(...) first') ==
|
||||||
resp.reason)
|
resp.reason)
|
||||||
yield from resp.release()
|
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@@ -13,117 +12,99 @@ from aiohttp_session import setup as setup_session
|
|||||||
|
|
||||||
class Autz(AbstractAuthorizationPolicy):
|
class Autz(AbstractAuthorizationPolicy):
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def permits(self, identity, permission, context=None):
|
||||||
def permits(self, identity, permission, context=None):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def authorized_userid(self, identity):
|
||||||
def authorized_userid(self, identity):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def make_app(loop):
|
def make_app():
|
||||||
app = web.Application(loop=loop)
|
app = web.Application()
|
||||||
setup_session(app, SimpleCookieStorage())
|
setup_session(app, SimpleCookieStorage())
|
||||||
setup_security(app, SessionIdentityPolicy(), Autz())
|
setup_security(app, SessionIdentityPolicy(), Autz())
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_remember(make_app, aiohttp_client):
|
||||||
def test_remember(make_app, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def handler(request):
|
||||||
def handler(request):
|
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
yield from remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
session = await get_session(request)
|
||||||
session = yield from get_session(request)
|
|
||||||
assert session['AIOHTTP_SECURITY'] == 'Andrew'
|
assert session['AIOHTTP_SECURITY'] == 'Andrew'
|
||||||
return web.HTTPOk()
|
return web.Response()
|
||||||
|
|
||||||
app = make_app()
|
app = make_app()
|
||||||
app.router.add_route('GET', '/', handler)
|
app.router.add_route('GET', '/', handler)
|
||||||
app.router.add_route('GET', '/check', check)
|
app.router.add_route('GET', '/check', check)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
resp = yield from client.get('/check')
|
resp = await client.get('/check')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_identify(make_app, aiohttp_client):
|
||||||
def test_identify(make_app, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def create(request):
|
||||||
def create(request):
|
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
yield from remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def check(request):
|
||||||
def check(request):
|
|
||||||
policy = request.app[IDENTITY_KEY]
|
policy = request.app[IDENTITY_KEY]
|
||||||
user_id = yield from policy.identify(request)
|
user_id = await policy.identify(request)
|
||||||
assert 'Andrew' == user_id
|
assert 'Andrew' == user_id
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = make_app()
|
app = make_app()
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
app.router.add_route('POST', '/', create)
|
app.router.add_route('POST', '/', create)
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = yield from client.post('/')
|
resp = await client.post('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
resp = yield from client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_forget(make_app, aiohttp_client):
|
||||||
def test_forget(make_app, test_client):
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def index(request):
|
||||||
def index(request):
|
session = await get_session(request)
|
||||||
session = yield from get_session(request)
|
return web.Response(text=session.get('AIOHTTP_SECURITY', ''))
|
||||||
return web.HTTPOk(text=session.get('AIOHTTP_SECURITY', ''))
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def login(request):
|
||||||
def login(request):
|
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
yield from remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
return response
|
raise response
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def logout(request):
|
||||||
def logout(request):
|
|
||||||
response = web.HTTPFound('/')
|
response = web.HTTPFound('/')
|
||||||
yield from forget(request, response)
|
await forget(request, response)
|
||||||
return response
|
raise response
|
||||||
|
|
||||||
app = make_app()
|
app = make_app()
|
||||||
app.router.add_route('GET', '/', index)
|
app.router.add_route('GET', '/', index)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
app.router.add_route('POST', '/logout', logout)
|
app.router.add_route('POST', '/logout', logout)
|
||||||
|
|
||||||
client = yield from test_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = yield from client.post('/login')
|
resp = await client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert str(resp.url).endswith('/')
|
assert str(resp.url).endswith('/')
|
||||||
txt = yield from resp.text()
|
txt = await resp.text()
|
||||||
assert 'Andrew' == txt
|
assert 'Andrew' == txt
|
||||||
yield from resp.release()
|
|
||||||
|
|
||||||
resp = yield from client.post('/logout')
|
resp = await client.post('/logout')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert str(resp.url).endswith('/')
|
assert str(resp.url).endswith('/')
|
||||||
txt = yield from resp.text()
|
txt = await resp.text()
|
||||||
assert '' == txt
|
assert '' == txt
|
||||||
yield from resp.release()
|
|
||||||
|
Reference in New Issue
Block a user