diff --git a/.gitignore b/.gitignore index 8af43e7..5adbcf0 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,5 @@ docs/_build/ # PyBuilder target/ -coverage \ No newline at end of file +coverage +.pytest_cache \ No newline at end of file diff --git a/CHANGES.txt b/CHANGES.txt index 42ac4f0..6c7d59a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,10 +1,23 @@ 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) ------------------ -- Add `is_anonymous`, `login_required`, `has_permission` helpers (#114) +- Add ``is_anonymous``, ``login_required``, ``has_permission`` helpers (#114) 0.1.2 (2017-10-17) ------------------ diff --git a/aiohttp_security/__init__.py b/aiohttp_security/__init__.py index 937d858..e9d5297 100644 --- a/aiohttp_security/__init__.py +++ b/aiohttp_security/__init__.py @@ -1,11 +1,12 @@ from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy -from .api import (authorized_userid, forget, has_permission, is_anonymous, - login_required, permits, remember, setup) +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 .session_identity import SessionIdentityPolicy from .jwt_identity import JWTIdentityPolicy -__version__ = '0.2.0' +__version__ = '0.3.0' __all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy', @@ -13,4 +14,5 @@ __all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy', 'JWTIdentityPolicy', 'remember', 'forget', 'authorized_userid', 'permits', 'setup', 'is_anonymous', - 'login_required', 'has_permission') + 'login_required', 'has_permission', + 'check_authorized', 'check_permission') diff --git a/aiohttp_security/api.py b/aiohttp_security/api.py index d414459..d92a73e 100644 --- a/aiohttp_security/api.py +++ b/aiohttp_security/api.py @@ -1,4 +1,5 @@ import enum +import warnings from aiohttp import web from aiohttp_security.abc import (AbstractIdentityPolicy, AbstractAuthorizationPolicy) @@ -86,6 +87,15 @@ async def is_anonymous(request): 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): """Decorator that restrict access only for authorized users. @@ -101,21 +111,34 @@ def login_required(fn): "or `def handler(self, request)`.") raise RuntimeError(msg) - userid = await authorized_userid(request) - if userid is None: - raise web.HTTPUnauthorized - - ret = await fn(*args, **kwargs) - return ret + await check_authorized(request) + return await fn(*args, **kwargs) + warnings.warn("login_required decorator is deprecated, " + "use check_authorized instead", + DeprecationWarning) 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( permission, context=None, ): - """Decorator that restrict access only for authorized users + """Decorator that restricts access only for authorized users with correct permissions. If user is not authorized - raises HTTPUnauthorized, @@ -132,18 +155,14 @@ def has_permission( "or `def handler(self, request)`.") raise RuntimeError(msg) - userid = await authorized_userid(request) - if userid is None: - raise web.HTTPUnauthorized - - allowed = await permits(request, permission, context) - if not allowed: - raise web.HTTPForbidden - ret = await fn(*args, **kwargs) - return ret + 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 diff --git a/aiohttp_security/jwt_identity.py b/aiohttp_security/jwt_identity.py index 1df01df..bd6ffe7 100644 --- a/aiohttp_security/jwt_identity.py +++ b/aiohttp_security/jwt_identity.py @@ -35,7 +35,7 @@ class JWTIdentityPolicy(AbstractIdentityPolicy): identity = jwt.decode(token, self.secret, - algorithm=self.algorithm) + algorithms=[self.algorithm]) return identity async def remember(self, *args, **kwargs): # pragma: no cover diff --git a/demo/database_auth/handlers.py b/demo/database_auth/handlers.py index 3d10332..0cc9484 100644 --- a/demo/database_auth/handlers.py +++ b/demo/database_auth/handlers.py @@ -4,7 +4,7 @@ from aiohttp import web from aiohttp_security import ( remember, forget, authorized_userid, - has_permission, login_required, + check_permission, check_authorized, ) from .db_auth import check_credentials @@ -45,25 +45,25 @@ class Web(object): db_engine = request.app.db_engine if await check_credentials(db_engine, login, password): await remember(request, response, login) - return response + raise response - return web.HTTPUnauthorized( + raise web.HTTPUnauthorized( body=b'Invalid username/password combination') - @login_required 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 - @has_permission('public') async def internal_page(self, request): + await check_permission(request, 'public') response = web.Response( body=b'This page is visible for all registered users') return response - @has_permission('protected') async def protected_page(self, request): + await check_permission(request, 'protected') response = web.Response(body=b'You are on protected page') return response diff --git a/demo/database_auth/main.py b/demo/database_auth/main.py index 43da500..822df44 100644 --- a/demo/database_auth/main.py +++ b/demo/database_auth/main.py @@ -19,7 +19,7 @@ async def init(loop): password='aiohttp_security', database='aiohttp_security', host='127.0.0.1') - app = web.Application(loop=loop) + app = web.Application() app.db_engine = db_engine setup_session(app, RedisStorage(redis_pool)) setup_security(app, diff --git a/demo/dictionary_auth/handlers.py b/demo/dictionary_auth/handlers.py index 5816e8d..6c19bab 100644 --- a/demo/dictionary_auth/handlers.py +++ b/demo/dictionary_auth/handlers.py @@ -4,7 +4,7 @@ from aiohttp import web from aiohttp_security import ( remember, forget, authorized_userid, - has_permission, login_required, + check_permission, check_authorized, ) from .authz import check_credentials @@ -55,8 +55,8 @@ async def login(request): return web.HTTPUnauthorized(body='Invalid username / password combination') -@login_required async def logout(request): + await check_authorized(request) response = web.Response( text='You have been logged out', content_type='text/html', @@ -65,9 +65,8 @@ async def logout(request): return response -@has_permission('public') async def internal_page(request): - # pylint: disable=unused-argument + await check_permission(request, 'public') response = web.Response( text='This page is visible for all registered users', content_type='text/html', @@ -75,9 +74,8 @@ async def internal_page(request): return response -@has_permission('protected') async def protected_page(request): - # pylint: disable=unused-argument + await check_permission(request, 'protected') response = web.Response( text='You are on protected page', content_type='text/html', diff --git a/demo/simple_example_auth.py b/demo/simple_example_auth.py index d1da45f..5b4b82d 100644 --- a/demo/simple_example_auth.py +++ b/demo/simple_example_auth.py @@ -1,6 +1,6 @@ from aiohttp import web from aiohttp_session import SimpleCookieStorage, session_middleware -from aiohttp_security import has_permission, \ +from aiohttp_security import check_permission, \ is_anonymous, remember, forget, \ setup as setup_security, SessionIdentityPolicy from aiohttp_security.abc import AbstractAuthorizationPolicy @@ -54,13 +54,13 @@ async def handler_logout(request): raise redirect_response -@has_permission('listen') async def handler_listen(request): + await check_permission(request, 'listen') return web.Response(body="I can listen!") -@has_permission('speak') async def handler_speak(request): + await check_permission(request, 'speak') return web.Response(body="I can speak!") diff --git a/docs/example.rst b/docs/example.rst index 39c839b..945d386 100644 --- a/docs/example.rst +++ b/docs/example.rst @@ -9,7 +9,7 @@ Simple example:: from aiohttp import web from aiohttp_session import SimpleCookieStorage, session_middleware - from aiohttp_security import has_permission, \ + from aiohttp_security import check_permission, \ is_anonymous, remember, forget, \ setup as setup_security, SessionIdentityPolicy from aiohttp_security.abc import AbstractAuthorizationPolicy @@ -63,13 +63,13 @@ Simple example:: raise redirect_response - @has_permission('listen') async def handler_listen(request): + await check_permission(request, 'listen') return web.Response(body="I can listen!") - @has_permission('speak') async def handler_speak(request): + await check_permission(request, 'speak') return web.Response(body="I can speak!") @@ -85,11 +85,12 @@ Simple example:: 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) + app.add_routes([ + web.get('/', handler_root), + web.get('/login', handler_login_jack), + web.get('/logout', handler_logout), + web.get('/listen', handler_listen), + web.get('/speak', handler_speak)]) # set up policies policy = SessionIdentityPolicy() diff --git a/docs/example_db_auth.rst b/docs/example_db_auth.rst index 07d44ef..5e9463a 100644 --- a/docs/example_db_auth.rst +++ b/docs/example_db_auth.rst @@ -133,7 +133,7 @@ Once we have all the code in place we can install it for our application:: password='aiohttp_security', database='aiohttp_security', host='127.0.0.1') - app = web.Application(loop=loop) + app = web.Application() setup_session(app, RedisStorage(redis_pool)) setup_security(app, 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 -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:: class Web: - @has_permission('protected') async def protected_page(self, request): + await check_permission(request, 'protected') response = web.Response(body=b'You are on protected page') return response or:: class Web: - @login_required 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 diff --git a/docs/index.rst b/docs/index.rst index f2ff487..1759104 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,6 +3,7 @@ aiohttp_security The library provides security for :ref:`aiohttp.web`. +The current version is |version| Contents -------- diff --git a/docs/reference.rst b/docs/reference.rst index 788e372..f3297e0 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -13,6 +13,19 @@ 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) Remember *identity* in *response*, e.g. by storing a cookie or @@ -50,6 +63,41 @@ Public API functions 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) Retrieve :term:`userid`. @@ -78,7 +126,8 @@ Public API functions :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 :meth:`AbstractAuthorizationPolicy.permission` @@ -92,7 +141,8 @@ Public API functions 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. @@ -103,29 +153,27 @@ Public API functions 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. + 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 -.. 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. + Use :func:`check_authorized` async function. Abstract policies diff --git a/docs/usage.rst b/docs/usage.rst index 9fe28c5..a772a4b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -11,22 +11,30 @@ 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. -Assets are secured using authentication and authorization as explained below. *aiohttp_security* is part of the *aio_libs* project which takes advantage of asynchronous -processing using Python's asyncio library. +*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. + +Assets are secured using authentication and authorization as explained +below. *aiohttp-security* is part of the +`aio-libs `_ project which takes advantage +of asynchronous processing using Python's asyncio library. 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)). +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 +(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: @@ -34,52 +42,100 @@ 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. +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 **@aiohttp_security.has_permission(**\ *permission*\ **)** decorator. This should return True if permission is granted. +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:`@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 :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 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 -corresponds to a user's id in the underlying system. +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). - *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 -============== +=============== -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 ============== -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. -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 diff --git a/requirements-dev.txt b/requirements-dev.txt index fdf3dc3..0eef8a3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ -e . flake8==3.5.0 +async-timeout==3.0 pytest==3.7.4 pytest-cov==2.5.1 pytest-mock==1.10.0 @@ -14,4 +15,4 @@ passlib==1.7.1 cryptography==2.3.1 aiohttp==3.4.2 pytest-aiohttp==0.3.0 -pyjwt==1.6.4 \ No newline at end of file +pyjwt==1.6.4 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8d6446c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[tool:pytest] +testpaths = tests +filterwarnings= + error diff --git a/setup.py b/setup.py index fa92657..2217951 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def read(f): 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'] extras_require = {'session': 'aiohttp-session'} diff --git a/tests/test_cookies_identity.py b/tests/test_cookies_identity.py index 8d739e6..a003690 100644 --- a/tests/test_cookies_identity.py +++ b/tests/test_cookies_identity.py @@ -15,23 +15,23 @@ class Autz(AbstractAuthorizationPolicy): pass -async def test_remember(loop, test_client): +async def test_remember(loop, aiohttp_client): async def handler(request): response = web.Response() await remember(request, response, 'Andrew') return response - app = web.Application(loop=loop) + app = web.Application() _setup(app, CookiesIdentityPolicy(), Autz()) app.router.add_route('GET', '/', handler) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') assert 200 == resp.status 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): response = web.Response() @@ -44,11 +44,11 @@ async def test_identify(loop, test_client): assert 'Andrew' == user_id return web.Response() - app = web.Application(loop=loop) + app = web.Application() _setup(app, CookiesIdentityPolicy(), Autz()) app.router.add_route('GET', '/', check) app.router.add_route('POST', '/', create) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.post('/') assert 200 == resp.status await resp.release() @@ -56,7 +56,7 @@ async def test_identify(loop, test_client): assert 200 == resp.status -async def test_forget(loop, test_client): +async def test_forget(loop, aiohttp_client): async def index(request): return web.Response() @@ -64,19 +64,19 @@ async def test_forget(loop, test_client): async def login(request): response = web.HTTPFound(location='/') await remember(request, response, 'Andrew') - return response + raise response async def logout(request): response = web.HTTPFound(location='/') await forget(request, response) - return response + raise response - app = web.Application(loop=loop) + 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 test_client(app) + client = await aiohttp_client(app) resp = await client.post('/login') assert 200 == resp.status assert str(resp.url).endswith('/') diff --git a/tests/test_dict_autz.py b/tests/test_dict_autz.py index 9151ce0..a828aa5 100644 --- a/tests/test_dict_autz.py +++ b/tests/test_dict_autz.py @@ -1,10 +1,12 @@ import enum +import pytest from aiohttp import web from aiohttp_security import setup as _setup from aiohttp_security import (AbstractAuthorizationPolicy, authorized_userid, forget, has_permission, is_anonymous, - login_required, permits, remember) + login_required, permits, remember, + check_authorized, check_permission) from aiohttp_security.cookies_identity import CookiesIdentityPolicy @@ -23,23 +25,23 @@ class Autz(AbstractAuthorizationPolicy): return None -async def test_authorized_userid(loop, test_client): +async def test_authorized_userid(loop, aiohttp_client): async def login(request): response = web.HTTPFound(location='/') await remember(request, response, 'UserID') - return response + raise response async def check(request): userid = await authorized_userid(request) assert 'Andrew' == userid return web.Response(text=userid) - app = web.Application(loop=loop) + app = web.Application() _setup(app, CookiesIdentityPolicy(), Autz()) app.router.add_route('GET', '/', check) app.router.add_route('POST', '/login', login) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.post('/login') assert 200 == resp.status @@ -47,23 +49,23 @@ async def test_authorized_userid(loop, test_client): 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): userid = await authorized_userid(request) assert userid is None return web.Response() - app = web.Application(loop=loop) + app = web.Application() _setup(app, CookiesIdentityPolicy(), Autz()) app.router.add_route('GET', '/', check) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') 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): READ = '101' WRITE = '102' @@ -86,7 +88,7 @@ async def test_permits_enum_permission(loop, test_client): async def login(request): response = web.HTTPFound(location='/') await remember(request, response, 'UserID') - return response + raise response async def check(request): ret = await permits(request, Permission.READ) @@ -97,16 +99,16 @@ async def test_permits_enum_permission(loop, test_client): assert not ret return web.Response() - app = web.Application(loop=loop) + app = web.Application() _setup(app, CookiesIdentityPolicy(), Autz()) app.router.add_route('GET', '/', check) app.router.add_route('POST', '/login', login) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.post('/login') assert 200 == resp.status -async def test_permits_unauthorized(loop, test_client): +async def test_permits_unauthorized(loop, aiohttp_client): async def check(request): ret = await permits(request, 'read') @@ -117,38 +119,38 @@ async def test_permits_unauthorized(loop, test_client): assert not ret return web.Response() - app = web.Application(loop=loop) + app = web.Application() _setup(app, CookiesIdentityPolicy(), Autz()) app.router.add_route('GET', '/', check) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') assert 200 == resp.status -async def test_is_anonymous(loop, test_client): +async def test_is_anonymous(loop, aiohttp_client): async def index(request): is_anon = await is_anonymous(request) if is_anon: - return web.HTTPUnauthorized() - return web.HTTPOk() + raise web.HTTPUnauthorized() + return web.Response() async def login(request): response = web.HTTPFound(location='/') await remember(request, response, 'UserID') - return response + raise response async def logout(request): response = web.HTTPFound(location='/') await forget(request, response) - return response + raise response - app = web.Application(loop=loop) + 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 test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') 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 -async def test_login_required(loop, test_client): - @login_required +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): - return web.HTTPOk() + await check_authorized(request) + return web.Response() async def login(request): response = web.HTTPFound(location='/') await remember(request, response, 'UserID') - return response + raise response async def logout(request): response = web.HTTPFound(location='/') await forget(request, response) - return response + raise response - app = web.Application(loop=loop) + 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 test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') 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 -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): - return web.HTTPOk() + await check_permission(request, 'read') + return web.Response() - @has_permission('write') async def index_write(request): - return web.HTTPOk() + await check_permission(request, 'write') + return web.Response() - @has_permission('forbid') async def index_forbid(request): - return web.HTTPOk() + await check_permission(request, 'forbid') + return web.Response() async def login(request): response = web.HTTPFound(location='/') await remember(request, response, 'UserID') - return response + raise response async def logout(request): response = web.HTTPFound(location='/') await forget(request, response) - return response + raise response - app = web.Application(loop=loop) + 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 test_client(app) + client = await aiohttp_client(app) resp = await client.get('/permission/read') assert web.HTTPUnauthorized.status_code == resp.status diff --git a/tests/test_jwt_identity.py b/tests/test_jwt_identity.py index bb5dd02..813deac 100644 --- a/tests/test_jwt_identity.py +++ b/tests/test_jwt_identity.py @@ -35,7 +35,7 @@ async def test_no_pyjwt_installed(mocker): JWTIdentityPolicy('secret') -async def test_identify(loop, make_token, test_client): +async def test_identify(loop, make_token, aiohttp_client): kwt_secret_key = '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'] return web.Response() - app = web.Application(loop=loop) + app = web.Application() _setup(app, JWTIdentityPolicy(kwt_secret_key), Autz()) app.router.add_route('GET', '/', check) - client = await test_client(app) + 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, test_client): +async def test_identify_broken_scheme(loop, make_token, aiohttp_client): kwt_secret_key = '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() - app = web.Application(loop=loop) + app = web.Application() _setup(app, JWTIdentityPolicy(kwt_secret_key), Autz()) app.router.add_route('GET', '/', check) - client = await test_client(app) + client = await aiohttp_client(app) headers = {'Authorization': 'Token {}'.format(token.decode('utf-8'))} resp = await client.get('/', headers=headers) assert 400 == resp.status diff --git a/tests/test_no_auth.py b/tests/test_no_auth.py index 55051df..c995da4 100644 --- a/tests/test_no_auth.py +++ b/tests/test_no_auth.py @@ -2,21 +2,21 @@ from aiohttp import web 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): userid = await authorized_userid(request) assert userid is None return web.Response() - app = web.Application(loop=loop) + app = web.Application() app.router.add_route('GET', '/', check) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') assert 200 == resp.status -async def test_permits(loop, test_client): +async def test_permits(loop, aiohttp_client): async def check(request): ret = await permits(request, 'read') @@ -27,8 +27,8 @@ async def test_permits(loop, test_client): assert ret return web.Response() - app = web.Application(loop=loop) + app = web.Application() app.router.add_route('GET', '/', check) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') assert 200 == resp.status diff --git a/tests/test_no_identity.py b/tests/test_no_identity.py index 208521b..c86eb48 100644 --- a/tests/test_no_identity.py +++ b/tests/test_no_identity.py @@ -2,15 +2,15 @@ from aiohttp import web 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): response = web.Response() await remember(request, response, 'Andrew') - app = web.Application(loop=loop) + app = web.Application() app.router.add_route('POST', '/', do_remember) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.post('/') assert 500 == resp.status assert (('Security subsystem is not initialized, ' @@ -18,15 +18,15 @@ async def test_remember(loop, test_client): resp.reason) -async def test_forget(loop, test_client): +async def test_forget(loop, aiohttp_client): async def do_forget(request): response = web.Response() await forget(request, response) - app = web.Application(loop=loop) + app = web.Application() app.router.add_route('POST', '/', do_forget) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.post('/') assert 500 == resp.status assert (('Security subsystem is not initialized, ' diff --git a/tests/test_session_identity.py b/tests/test_session_identity.py index d6377a8..b697f93 100644 --- a/tests/test_session_identity.py +++ b/tests/test_session_identity.py @@ -20,14 +20,14 @@ class Autz(AbstractAuthorizationPolicy): @pytest.fixture -def make_app(loop): - app = web.Application(loop=loop) +def make_app(): + app = web.Application() setup_session(app, SimpleCookieStorage()) setup_security(app, SessionIdentityPolicy(), Autz()) return app -async def test_remember(make_app, test_client): +async def test_remember(make_app, aiohttp_client): async def handler(request): response = web.Response() @@ -37,12 +37,12 @@ async def test_remember(make_app, test_client): async def check(request): session = await get_session(request) assert session['AIOHTTP_SECURITY'] == 'Andrew' - return web.HTTPOk() + return web.Response() app = make_app() app.router.add_route('GET', '/', handler) app.router.add_route('GET', '/check', check) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') assert 200 == resp.status @@ -50,7 +50,7 @@ async def test_remember(make_app, test_client): assert 200 == resp.status -async def test_identify(make_app, test_client): +async def test_identify(make_app, aiohttp_client): async def create(request): response = web.Response() @@ -66,7 +66,7 @@ async def test_identify(make_app, test_client): app = make_app() app.router.add_route('GET', '/', check) app.router.add_route('POST', '/', create) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.post('/') assert 200 == resp.status @@ -74,28 +74,28 @@ async def test_identify(make_app, test_client): assert 200 == resp.status -async def test_forget(make_app, test_client): +async def test_forget(make_app, aiohttp_client): async def index(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): response = web.HTTPFound(location='/') await remember(request, response, 'Andrew') - return response + raise response async def logout(request): response = web.HTTPFound('/') await forget(request, response) - return response + raise response app = make_app() app.router.add_route('GET', '/', index) app.router.add_route('POST', '/login', login) app.router.add_route('POST', '/logout', logout) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.post('/login') assert 200 == resp.status