Update to 0.3.0

This commit is contained in:
Andrew Svetlov 2018-09-06 13:06:55 +03:00
parent 097f7ecc43
commit 9b1d08c661
23 changed files with 418 additions and 177 deletions

3
.gitignore vendored
View File

@ -55,4 +55,5 @@ docs/_build/
# PyBuilder # PyBuilder
target/ target/
coverage coverage
.pytest_cache

View File

@ -1,10 +1,23 @@
Changes Changes
======= =======
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) 0.2.0 (2017-11-17)
------------------ ------------------
- Add `is_anonymous`, `login_required`, `has_permission` helpers (#114) - Add ``is_anonymous``, ``login_required``, ``has_permission`` helpers (#114)
0.1.2 (2017-10-17) 0.1.2 (2017-10-17)
------------------ ------------------

View File

@ -1,11 +1,12 @@
from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy
from .api import (authorized_userid, forget, has_permission, is_anonymous, from .api import (authorized_userid, forget, has_permission,
login_required, permits, remember, setup) 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 from .jwt_identity import JWTIdentityPolicy
__version__ = '0.2.0' __version__ = '0.3.0'
__all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy', __all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
@ -13,4 +14,5 @@ __all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
'JWTIdentityPolicy', 'JWTIdentityPolicy',
'remember', 'forget', 'authorized_userid', 'remember', 'forget', 'authorized_userid',
'permits', 'setup', 'is_anonymous', 'permits', 'setup', 'is_anonymous',
'login_required', 'has_permission') 'login_required', 'has_permission',
'check_authorized', 'check_permission')

View File

@ -1,4 +1,5 @@
import enum import enum
import warnings
from aiohttp import web from aiohttp import web
from aiohttp_security.abc import (AbstractIdentityPolicy, from aiohttp_security.abc import (AbstractIdentityPolicy,
AbstractAuthorizationPolicy) AbstractAuthorizationPolicy)
@ -86,6 +87,15 @@ async def is_anonymous(request):
return False return False
async def check_authorized(request):
"""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): def login_required(fn):
"""Decorator that restrict access only for authorized users. """Decorator that restrict access only for authorized users.
@ -101,21 +111,34 @@ def login_required(fn):
"or `def handler(self, request)`.") "or `def handler(self, request)`.")
raise RuntimeError(msg) raise RuntimeError(msg)
userid = await authorized_userid(request) await check_authorized(request)
if userid is None: return await fn(*args, **kwargs)
raise web.HTTPUnauthorized
ret = await fn(*args, **kwargs)
return ret
warnings.warn("login_required decorator is deprecated, "
"use check_authorized instead",
DeprecationWarning)
return wrapped return wrapped
async def check_permission(request, permission, context=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( def has_permission(
permission, permission,
context=None, context=None,
): ):
"""Decorator that restrict access only for authorized users """Decorator that restricts access only for authorized users
with correct permissions. with correct permissions.
If user is not authorized - raises HTTPUnauthorized, If user is not authorized - raises HTTPUnauthorized,
@ -132,18 +155,14 @@ def has_permission(
"or `def handler(self, request)`.") "or `def handler(self, request)`.")
raise RuntimeError(msg) raise RuntimeError(msg)
userid = await authorized_userid(request) await check_permission(request, permission, context)
if userid is None: return await fn(*args, **kwargs)
raise web.HTTPUnauthorized
allowed = await permits(request, permission, context)
if not allowed:
raise web.HTTPForbidden
ret = await fn(*args, **kwargs)
return ret
return wrapped return wrapped
warnings.warn("has_permission decorator is deprecated, "
"use check_permission instead",
DeprecationWarning)
return wrapper return wrapper

View File

@ -35,7 +35,7 @@ class JWTIdentityPolicy(AbstractIdentityPolicy):
identity = jwt.decode(token, identity = jwt.decode(token,
self.secret, self.secret,
algorithm=self.algorithm) algorithms=[self.algorithm])
return identity return identity
async def remember(self, *args, **kwargs): # pragma: no cover async def remember(self, *args, **kwargs): # pragma: no cover

View File

@ -4,7 +4,7 @@ from aiohttp import web
from aiohttp_security import ( from aiohttp_security import (
remember, forget, authorized_userid, remember, forget, authorized_userid,
has_permission, login_required, check_permission, check_authorized,
) )
from .db_auth import check_credentials from .db_auth import check_credentials
@ -45,25 +45,25 @@ class Web(object):
db_engine = request.app.db_engine db_engine = request.app.db_engine
if await check_credentials(db_engine, login, password): if await check_credentials(db_engine, login, password):
await remember(request, response, login) await remember(request, response, login)
return response raise response
return web.HTTPUnauthorized( raise web.HTTPUnauthorized(
body=b'Invalid username/password combination') body=b'Invalid username/password combination')
@login_required
async def logout(self, request): async def logout(self, request):
await check_authorized(request)
response = web.Response(body=b'You have been logged out') response = web.Response(body=b'You have been logged out')
await forget(request, response) await forget(request, response)
return response return response
@has_permission('public')
async def internal_page(self, request): async def internal_page(self, request):
await check_permission(request, 'public')
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
@has_permission('protected')
async def protected_page(self, request): async def protected_page(self, request):
await check_permission(request, 'protected')
response = web.Response(body=b'You are on protected page') response = web.Response(body=b'You are on protected page')
return response return response

View File

@ -19,7 +19,7 @@ async def init(loop):
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(loop=loop) app = web.Application()
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,

View File

@ -4,7 +4,7 @@ from aiohttp import web
from aiohttp_security import ( from aiohttp_security import (
remember, forget, authorized_userid, remember, forget, authorized_userid,
has_permission, login_required, check_permission, check_authorized,
) )
from .authz import check_credentials from .authz import check_credentials
@ -55,8 +55,8 @@ async def login(request):
return web.HTTPUnauthorized(body='Invalid username / password combination') return web.HTTPUnauthorized(body='Invalid username / password combination')
@login_required
async def logout(request): 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',
@ -65,9 +65,8 @@ async def logout(request):
return response return response
@has_permission('public')
async def internal_page(request): async def internal_page(request):
# pylint: disable=unused-argument await check_permission(request, 'public')
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',
@ -75,9 +74,8 @@ async def internal_page(request):
return response return response
@has_permission('protected')
async def protected_page(request): async def protected_page(request):
# pylint: disable=unused-argument await check_permission(request, 'protected')
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',

View File

@ -1,6 +1,6 @@
from aiohttp import web from aiohttp import web
from aiohttp_session import SimpleCookieStorage, session_middleware from aiohttp_session import SimpleCookieStorage, session_middleware
from aiohttp_security import has_permission, \ from aiohttp_security import check_permission, \
is_anonymous, remember, forget, \ is_anonymous, remember, forget, \
setup as setup_security, SessionIdentityPolicy setup as setup_security, SessionIdentityPolicy
from aiohttp_security.abc import AbstractAuthorizationPolicy from aiohttp_security.abc import AbstractAuthorizationPolicy
@ -54,13 +54,13 @@ async def handler_logout(request):
raise redirect_response raise redirect_response
@has_permission('listen')
async def handler_listen(request): async def handler_listen(request):
await check_permission(request, 'listen')
return web.Response(body="I can listen!") return web.Response(body="I can listen!")
@has_permission('speak')
async def handler_speak(request): async def handler_speak(request):
await check_permission(request, 'speak')
return web.Response(body="I can speak!") return web.Response(body="I can speak!")

View File

@ -9,7 +9,7 @@ Simple example::
from aiohttp import web from aiohttp import web
from aiohttp_session import SimpleCookieStorage, session_middleware from aiohttp_session import SimpleCookieStorage, session_middleware
from aiohttp_security import has_permission, \ from aiohttp_security import check_permission, \
is_anonymous, remember, forget, \ is_anonymous, remember, forget, \
setup as setup_security, SessionIdentityPolicy setup as setup_security, SessionIdentityPolicy
from aiohttp_security.abc import AbstractAuthorizationPolicy from aiohttp_security.abc import AbstractAuthorizationPolicy
@ -63,13 +63,13 @@ Simple example::
raise redirect_response raise redirect_response
@has_permission('listen')
async def handler_listen(request): async def handler_listen(request):
await check_permission(request, 'listen')
return web.Response(body="I can listen!") return web.Response(body="I can listen!")
@has_permission('speak')
async def handler_speak(request): async def handler_speak(request):
await check_permission(request, 'speak')
return web.Response(body="I can speak!") return web.Response(body="I can speak!")
@ -85,11 +85,12 @@ Simple example::
app = web.Application(middlewares=[middleware]) app = web.Application(middlewares=[middleware])
# add the routes # add the routes
app.router.add_route('GET', '/', handler_root) app.add_routes([
app.router.add_route('GET', '/login', handler_login_jack) web.get('/', handler_root),
app.router.add_route('GET', '/logout', handler_logout) web.get('/login', handler_login_jack),
app.router.add_route('GET', '/listen', handler_listen) web.get('/logout', handler_logout),
app.router.add_route('GET', '/speak', handler_speak) web.get('/listen', handler_listen),
web.get('/speak', handler_speak)])
# set up policies # set up policies
policy = SessionIdentityPolicy() policy = SessionIdentityPolicy()

View File

@ -133,7 +133,7 @@ Once we have all the code in place we can install it for our application::
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(loop=loop) app = web.Application()
setup_session(app, RedisStorage(redis_pool)) setup_session(app, RedisStorage(redis_pool))
setup_security(app, setup_security(app,
SessionIdentityPolicy(), SessionIdentityPolicy(),
@ -142,23 +142,23 @@ 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. There are already implemented two decorators:: based on permissions. There are already implemented two helpers::
from aiohttp_security import has_permission, login_required from aiohttp_security import check_authorized, check_permission
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:
@has_permission('protected')
async def protected_page(self, request): async def protected_page(self, request):
await check_permission(request, 'protected')
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:: or::
class Web: class Web:
@login_required
async def logout(self, request): async def logout(self, request):
await check_authorized(request)
response = web.Response(body=b'You have been logged out') response = web.Response(body=b'You have been logged out')
await forget(request, response) await forget(request, response)
return response return response

View File

@ -3,6 +3,7 @@ aiohttp_security
The library provides security for :ref:`aiohttp.web<aiohttp-web>`. The library provides security for :ref:`aiohttp.web<aiohttp-web>`.
The current version is |version|
Contents Contents
-------- --------

View File

@ -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
@ -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 permission: Requested :term:`permission`. :class:`str` or :class:`enum.Enum` object. :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`
@ -92,7 +141,8 @@ Public API functions
Checks if user is anonymous user. Checks if user is anonymous user.
Return ``True`` if user is not remembered in request, otherwise returns ``False``. Return ``True`` if user is not remembered in request, otherwise
returns ``False``.
:param request: :class:`aiohttp.web.Request` object. :param request: :class:`aiohttp.web.Request` object.
@ -103,29 +153,27 @@ Public API functions
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not 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:: has_permission(permission)
Decorator for handlers that checks if user is authorized Decorator for handlers that checks if user is authorized
and has correct permission. and has correct permission.
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized. Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not
Raises :class:`aiohttp.web.HTTPForbidden` if user is authorized but has no access rights. authorized.
Raises :class:`aiohttp.web.HTTPForbidden` if user is
authorized but has no access rights.
:param str permission: requested :term:`permission`. :param str permission: requested :term:`permission`.
.. deprecated:: 0.3
.. function:: setup(app, identity_policy, autz_policy) Use :func:`check_authorized` async function.
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.
Abstract policies Abstract policies

View File

@ -11,22 +11,30 @@
First of all, what is *aiohttp_security* about? First of all, what is *aiohttp_security* about?
*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. *aiohttp-security* is a set of public API functions as well as a
Assets are secured using authentication and authorization as explained below. *aiohttp_security* is part of the *aio_libs* project which takes advantage of asynchronous reference standard for implementation details for securing access to
processing using Python's asyncio library. assets served by a wsgi server.
Assets are secured using authentication and authorization as explained
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.
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 The API is agnostic to the low level implementation details such that
code directly (see explanation below)). 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: Via the API an application can:
(i) remember a user in a local session (:func:`remember`), (i) remember a user in a local session (:func:`remember`),
(ii) forget a user in a local session (:func:`forget`), (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 (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`). (iv) check the :term:`permission` of a remembered user (:func:`permits`).
The library internals are built on top of two concepts: The library internals are built on top of two concepts:
@ -34,52 +42,100 @@ The library internals are built on top of two concepts:
1) :term:`authentication`, and 1) :term:`authentication`, and
2) :term:`authorization`. 2) :term:`authorization`.
There are abstract base classes for both types as well as several pre-built implementations There are abstract base classes for both types as well as several
that are shipped with the library. However, the end user is free to build their own implementations. pre-built implementations that are shipped with the library. However,
The library comes with two pre-built identity policies; one that uses cookies, and one that uses sessions [#f1]_. the end user is free to build their own implementations.
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 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: The workflow is as follows:
1) User is authenticated. This has to be implemented by the developer. 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. 2) Once user is authenticated an identity string has to be created for
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* . that user. This has to be implemented by the developer.
4) If the user tries to access a restricted asset the :func:`permits` method is called. Usually assets are protected using the **@aiohttp_security.has_permission(**\ *permission*\ **)** decorator. This should return True if permission is granted. 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. The :func:`permits` method is implemented by the developer as part of
In addition a :func:`@aiohttp_security.login_required decorator` 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). 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
============== ==============
Authentication is the process where a user's identity is verified. It confirms who the user is. This is traditionally done using a user name and password (note: this is not the only way).
A authenticated user has no access rights, rather an authenticated user merely confirms that the user exists and that the user is who they say they are.
In *aiohttp_security* the developer is responsible for their own authentication mechanism. *aiohttp_security* only requires that the authentication result in a identity string which Authentication is the process where a user's identity is verified. It
corresponds to a user's id in the underlying system. confirms who the user is. This is traditionally done using a user name
and password (note: this is not the only way).
*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. A authenticated user has no access rights, rather an authenticated
user merely confirms that the user exists and that the user is who
they say they are.
In *aiohttp_security* the developer is responsible for their own
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 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. 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. :term:`CookiesIdentityPolicy` that uses cookies and :term:`SessionIdentityPolicy` that uses sessions via :term:`aiohttp.session` library. *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 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. 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. 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. 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. 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 .. rubric:: Footnotes

View File

@ -1,5 +1,6 @@
-e . -e .
flake8==3.5.0 flake8==3.5.0
async-timeout==3.0
pytest==3.7.4 pytest==3.7.4
pytest-cov==2.5.1 pytest-cov==2.5.1
pytest-mock==1.10.0 pytest-mock==1.10.0
@ -14,4 +15,4 @@ passlib==1.7.1
cryptography==2.3.1 cryptography==2.3.1
aiohttp==3.4.2 aiohttp==3.4.2
pytest-aiohttp==0.3.0 pytest-aiohttp==0.3.0
pyjwt==1.6.4 pyjwt==1.6.4

4
setup.cfg Normal file
View File

@ -0,0 +1,4 @@
[tool:pytest]
testpaths = tests
filterwarnings=
error

View File

@ -27,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.0.0']
tests_require = install_requires + ['pytest'] tests_require = install_requires + ['pytest']
extras_require = {'session': 'aiohttp-session'} extras_require = {'session': 'aiohttp-session'}

View File

@ -15,23 +15,23 @@ class Autz(AbstractAuthorizationPolicy):
pass pass
async def test_remember(loop, test_client): async def test_remember(loop, aiohttp_client):
async def handler(request): async def handler(request):
response = web.Response() response = web.Response()
await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await 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
async def test_identify(loop, test_client): async def test_identify(loop, aiohttp_client):
async def create(request): async def create(request):
response = web.Response() response = web.Response()
@ -44,11 +44,11 @@ async def test_identify(loop, test_client):
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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.post('/') resp = await client.post('/')
assert 200 == resp.status assert 200 == resp.status
await resp.release() await resp.release()
@ -56,7 +56,7 @@ async def test_identify(loop, test_client):
assert 200 == resp.status assert 200 == resp.status
async def test_forget(loop, test_client): async def test_forget(loop, aiohttp_client):
async def index(request): async def index(request):
return web.Response() return web.Response()
@ -64,19 +64,19 @@ async def test_forget(loop, test_client):
async def login(request): async def login(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await remember(request, response, 'Andrew') await remember(request, response, 'Andrew')
return response raise response
async def logout(request): async def logout(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await 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('/')

View File

@ -1,10 +1,12 @@
import enum import enum
import pytest
from aiohttp import web from aiohttp import web
from aiohttp_security import setup as _setup from aiohttp_security import setup as _setup
from aiohttp_security import (AbstractAuthorizationPolicy, authorized_userid, from aiohttp_security import (AbstractAuthorizationPolicy, authorized_userid,
forget, has_permission, is_anonymous, forget, has_permission, is_anonymous,
login_required, permits, remember) login_required, permits, remember,
check_authorized, check_permission)
from aiohttp_security.cookies_identity import CookiesIdentityPolicy from aiohttp_security.cookies_identity import CookiesIdentityPolicy
@ -23,23 +25,23 @@ class Autz(AbstractAuthorizationPolicy):
return None return None
async def test_authorized_userid(loop, test_client): async def test_authorized_userid(loop, aiohttp_client):
async def login(request): async def login(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await remember(request, response, 'UserID') await remember(request, response, 'UserID')
return response raise response
async def check(request): async def check(request):
userid = await authorized_userid(request) userid = await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.post('/login') resp = await client.post('/login')
assert 200 == resp.status assert 200 == resp.status
@ -47,23 +49,23 @@ async def test_authorized_userid(loop, test_client):
assert 'Andrew' == txt assert 'Andrew' == txt
async def test_authorized_userid_not_authorized(loop, test_client): async def test_authorized_userid_not_authorized(loop, aiohttp_client):
async def check(request): async def check(request):
userid = await authorized_userid(request) userid = await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.get('/') resp = await client.get('/')
assert 200 == resp.status assert 200 == resp.status
async def test_permits_enum_permission(loop, test_client): async def test_permits_enum_permission(loop, aiohttp_client):
class Permission(enum.Enum): class Permission(enum.Enum):
READ = '101' READ = '101'
WRITE = '102' WRITE = '102'
@ -86,7 +88,7 @@ async def test_permits_enum_permission(loop, test_client):
async def login(request): async def login(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await remember(request, response, 'UserID') await remember(request, response, 'UserID')
return response raise response
async def check(request): async def check(request):
ret = await permits(request, Permission.READ) ret = await permits(request, Permission.READ)
@ -97,16 +99,16 @@ async def test_permits_enum_permission(loop, test_client):
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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.post('/login') resp = await client.post('/login')
assert 200 == resp.status assert 200 == resp.status
async def test_permits_unauthorized(loop, test_client): async def test_permits_unauthorized(loop, aiohttp_client):
async def check(request): async def check(request):
ret = await permits(request, 'read') ret = await permits(request, 'read')
@ -117,38 +119,38 @@ async def test_permits_unauthorized(loop, test_client):
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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.get('/') resp = await client.get('/')
assert 200 == resp.status assert 200 == resp.status
async def test_is_anonymous(loop, test_client): async def test_is_anonymous(loop, aiohttp_client):
async def index(request): async def index(request):
is_anon = await is_anonymous(request) is_anon = await is_anonymous(request)
if is_anon: if is_anon:
return web.HTTPUnauthorized() raise web.HTTPUnauthorized()
return web.HTTPOk() return web.Response()
async def login(request): async def login(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await remember(request, response, 'UserID') await remember(request, response, 'UserID')
return response raise response
async def logout(request): async def logout(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.get('/') resp = await client.get('/')
assert web.HTTPUnauthorized.status_code == resp.status assert web.HTTPUnauthorized.status_code == resp.status
@ -161,27 +163,63 @@ async def test_is_anonymous(loop, test_client):
assert web.HTTPUnauthorized.status_code == resp.status assert web.HTTPUnauthorized.status_code == resp.status
async def test_login_required(loop, test_client): async def test_login_required(loop, aiohttp_client):
@login_required 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): async def index(request):
return web.HTTPOk() await check_authorized(request)
return web.Response()
async def login(request): async def login(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await remember(request, response, 'UserID') await remember(request, response, 'UserID')
return response raise response
async def logout(request): async def logout(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.get('/') resp = await client.get('/')
assert web.HTTPUnauthorized.status_code == resp.status assert web.HTTPUnauthorized.status_code == resp.status
@ -194,38 +232,97 @@ async def test_login_required(loop, test_client):
assert web.HTTPUnauthorized.status_code == resp.status assert web.HTTPUnauthorized.status_code == resp.status
async def test_has_permission(loop, test_client): 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):
@has_permission('read')
async def index_read(request): async def index_read(request):
return web.HTTPOk() await check_permission(request, 'read')
return web.Response()
@has_permission('write')
async def index_write(request): async def index_write(request):
return web.HTTPOk() await check_permission(request, 'write')
return web.Response()
@has_permission('forbid')
async def index_forbid(request): async def index_forbid(request):
return web.HTTPOk() await check_permission(request, 'forbid')
return web.Response()
async def login(request): async def login(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await remember(request, response, 'UserID') await remember(request, response, 'UserID')
return response raise response
async def logout(request): async def logout(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await 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', '/permission/read', index_read) app.router.add_route('GET', '/permission/read', index_read)
app.router.add_route('GET', '/permission/write', index_write) app.router.add_route('GET', '/permission/write', index_write)
app.router.add_route('GET', '/permission/forbid', index_forbid) app.router.add_route('GET', '/permission/forbid', index_forbid)
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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.get('/permission/read') resp = await client.get('/permission/read')
assert web.HTTPUnauthorized.status_code == resp.status assert web.HTTPUnauthorized.status_code == resp.status

View File

@ -35,7 +35,7 @@ async def test_no_pyjwt_installed(mocker):
JWTIdentityPolicy('secret') JWTIdentityPolicy('secret')
async def test_identify(loop, make_token, test_client): async def test_identify(loop, make_token, aiohttp_client):
kwt_secret_key = 'Key' kwt_secret_key = 'Key'
token = make_token({'login': 'Andrew'}, kwt_secret_key) token = make_token({'login': 'Andrew'}, kwt_secret_key)
@ -46,17 +46,17 @@ async def test_identify(loop, make_token, test_client):
assert 'Andrew' == identity['login'] assert 'Andrew' == identity['login']
return web.Response() return web.Response()
app = web.Application(loop=loop) app = web.Application()
_setup(app, JWTIdentityPolicy(kwt_secret_key), Autz()) _setup(app, JWTIdentityPolicy(kwt_secret_key), Autz())
app.router.add_route('GET', '/', check) app.router.add_route('GET', '/', check)
client = await test_client(app) client = await aiohttp_client(app)
headers = {'Authorization': 'Bearer {}'.format(token.decode('utf-8'))} headers = {'Authorization': 'Bearer {}'.format(token.decode('utf-8'))}
resp = await client.get('/', headers=headers) resp = await client.get('/', headers=headers)
assert 200 == resp.status assert 200 == resp.status
async def test_identify_broken_scheme(loop, make_token, test_client): async def test_identify_broken_scheme(loop, make_token, aiohttp_client):
kwt_secret_key = 'Key' kwt_secret_key = 'Key'
token = make_token({'login': 'Andrew'}, kwt_secret_key) token = make_token({'login': 'Andrew'}, kwt_secret_key)
@ -71,11 +71,11 @@ async def test_identify_broken_scheme(loop, make_token, test_client):
return web.Response() return web.Response()
app = web.Application(loop=loop) app = web.Application()
_setup(app, JWTIdentityPolicy(kwt_secret_key), Autz()) _setup(app, JWTIdentityPolicy(kwt_secret_key), Autz())
app.router.add_route('GET', '/', check) app.router.add_route('GET', '/', check)
client = await test_client(app) client = await aiohttp_client(app)
headers = {'Authorization': 'Token {}'.format(token.decode('utf-8'))} headers = {'Authorization': 'Token {}'.format(token.decode('utf-8'))}
resp = await client.get('/', headers=headers) resp = await client.get('/', headers=headers)
assert 400 == resp.status assert 400 == resp.status

View File

@ -2,21 +2,21 @@ from aiohttp import web
from aiohttp_security import authorized_userid, permits from aiohttp_security import authorized_userid, permits
async def test_authorized_userid(loop, test_client): async def test_authorized_userid(loop, aiohttp_client):
async def check(request): async def check(request):
userid = await authorized_userid(request) userid = await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.get('/') resp = await client.get('/')
assert 200 == resp.status assert 200 == resp.status
async def test_permits(loop, test_client): async def test_permits(loop, aiohttp_client):
async def check(request): async def check(request):
ret = await permits(request, 'read') ret = await permits(request, 'read')
@ -27,8 +27,8 @@ async def test_permits(loop, test_client):
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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.get('/') resp = await client.get('/')
assert 200 == resp.status assert 200 == resp.status

View File

@ -2,15 +2,15 @@ from aiohttp import web
from aiohttp_security import remember, forget from aiohttp_security import remember, forget
async def test_remember(loop, test_client): async def test_remember(loop, aiohttp_client):
async def do_remember(request): async def do_remember(request):
response = web.Response() response = web.Response()
await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await 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, '
@ -18,15 +18,15 @@ async def test_remember(loop, test_client):
resp.reason) resp.reason)
async def test_forget(loop, test_client): async def test_forget(loop, aiohttp_client):
async def do_forget(request): async def do_forget(request):
response = web.Response() response = web.Response()
await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await 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, '

View File

@ -20,14 +20,14 @@ class Autz(AbstractAuthorizationPolicy):
@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
async def test_remember(make_app, test_client): async def test_remember(make_app, aiohttp_client):
async def handler(request): async def handler(request):
response = web.Response() response = web.Response()
@ -37,12 +37,12 @@ async def test_remember(make_app, test_client):
async def check(request): async def check(request):
session = await get_session(request) session = await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.get('/') resp = await client.get('/')
assert 200 == resp.status assert 200 == resp.status
@ -50,7 +50,7 @@ async def test_remember(make_app, test_client):
assert 200 == resp.status assert 200 == resp.status
async def test_identify(make_app, test_client): async def test_identify(make_app, aiohttp_client):
async def create(request): async def create(request):
response = web.Response() response = web.Response()
@ -66,7 +66,7 @@ async def test_identify(make_app, test_client):
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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.post('/') resp = await client.post('/')
assert 200 == resp.status assert 200 == resp.status
@ -74,28 +74,28 @@ async def test_identify(make_app, test_client):
assert 200 == resp.status assert 200 == resp.status
async def test_forget(make_app, test_client): async def test_forget(make_app, aiohttp_client):
async def index(request): async def index(request):
session = await get_session(request) session = await get_session(request)
return web.HTTPOk(text=session.get('AIOHTTP_SECURITY', '')) return web.Response(text=session.get('AIOHTTP_SECURITY', ''))
async def login(request): async def login(request):
response = web.HTTPFound(location='/') response = web.HTTPFound(location='/')
await remember(request, response, 'Andrew') await remember(request, response, 'Andrew')
return response raise response
async def logout(request): async def logout(request):
response = web.HTTPFound('/') response = web.HTTPFound('/')
await 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 = await test_client(app) client = await aiohttp_client(app)
resp = await client.post('/login') resp = await client.post('/login')
assert 200 == resp.status assert 200 == resp.status