From 2371a9574b1efad7a74723ffbd40a8af507cac4d Mon Sep 17 00:00:00 2001 From: Vincent Maillol Date: Thu, 19 Apr 2018 07:42:51 +0200 Subject: [PATCH] Add class_has_permission decorator This decorator adds permission for each method of `aiohttp.web.View` class --- aiohttp_security/__init__.py | 5 +- aiohttp_security/api.py | 36 +++++++- tests/test_class_has_permission.py | 138 +++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 tests/test_class_has_permission.py diff --git a/aiohttp_security/__init__.py b/aiohttp_security/__init__.py index 24d812b..bf9488a 100644 --- a/aiohttp_security/__init__.py +++ b/aiohttp_security/__init__.py @@ -1,5 +1,6 @@ from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy -from .api import (authorized_userid, forget, has_permission, is_anonymous, +from .api import (authorized_userid, forget, has_permission, + class_has_permission, is_anonymous, login_required, permits, remember, setup) from .cookies_identity import CookiesIdentityPolicy from .session_identity import SessionIdentityPolicy @@ -11,4 +12,4 @@ __all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy', 'CookiesIdentityPolicy', 'SessionIdentityPolicy', 'remember', 'forget', 'authorized_userid', 'permits', 'setup', 'is_anonymous', - 'login_required', 'has_permission') + 'login_required', 'has_permission', 'class_has_permission') diff --git a/aiohttp_security/api.py b/aiohttp_security/api.py index ba80a75..eadd957 100644 --- a/aiohttp_security/api.py +++ b/aiohttp_security/api.py @@ -145,7 +145,6 @@ def has_permission( userid = await authorized_userid(request) if userid is None: raise web.HTTPUnauthorized - allowed = await permits(request, permission, context) if not allowed: raise web.HTTPForbidden @@ -157,6 +156,41 @@ def has_permission( return wrapper +def class_has_permission(permission_prefix, context=None): + """Decorator that restrict access only for authorized users + with correct permissions for each method of a `aiohttp.web.View` + class. + + The needed permission to perform: + - POST request is `.create` prefixed by `prefix` + - GET request is `.read` prefixed by `prefix` + - PATCH or PUT request is `.update` prefixed by `prefix` + - DELETE request is `.delete` prefixed by `prefix` + + If user is not authorized - raises HTTPUnauthorized, + if user is authorized and does not have permission - + raises HTTPForbidden. + """ + + def decorator(cls): + methods = {'post': 'create', + 'get': 'read', + 'put': 'update', + 'patch': 'update', + 'delete': 'delete'} + + for method_name, permission in methods.items(): + method = getattr(cls, method_name, None) + if method is not None: + decorator = has_permission( + '{}.{}'.format(permission_prefix, permission), + context) + setattr(cls, method_name, decorator(method)) + + return cls + return decorator + + def setup(app, identity_policy, autz_policy): assert isinstance(identity_policy, AbstractIdentityPolicy), identity_policy assert isinstance(autz_policy, AbstractAuthorizationPolicy), autz_policy diff --git a/tests/test_class_has_permission.py b/tests/test_class_has_permission.py new file mode 100644 index 0000000..14b1fb8 --- /dev/null +++ b/tests/test_class_has_permission.py @@ -0,0 +1,138 @@ +from aiohttp import web +from aiohttp_security import setup as _setup +from aiohttp_security import (AbstractAuthorizationPolicy, + forget, class_has_permission, + remember) +from aiohttp_security.cookies_identity import CookiesIdentityPolicy + + +class Autz(AbstractAuthorizationPolicy): + + user_permission_map = { + 'user_1': {'bike.read'}, + 'user_2': {'bike.create'}, + 'user_3': {'bike.update'}, + 'user_4': {'bike.delete'} + } + + async def permits(self, identity, permission, context=None): + if identity in self.user_permission_map: + return permission in self.user_permission_map[identity] + else: + return False + + async def authorized_userid(self, identity): + if identity in self.user_permission_map: + return identity + else: + return None + + +async def test_class_has_permission(loop, test_client): + + @class_has_permission('bike') + class BikeView(web.View): + + async def get(self): + return web.HTTPOk() + + async def post(self): + return web.HTTPOk() + + async def put(self): + return web.HTTPOk() + + async def patch(self): + return web.HTTPOk() + + async def delete(self): + return web.HTTPOk() + + class SessionView(web.View): + async def post(self): + user = self.request.match_info.get('user') + response = web.HTTPFound(location='/') + await remember(self.request, response, user) + return response + + async def delete(self): + response = web.HTTPFound(location='/') + await forget(self.request, response) + return response + + app = web.Application(loop=loop) + _setup(app, CookiesIdentityPolicy(), Autz()) + + app.router.add_route( + '*', '/permission', BikeView) + app.router.add_route( + '*', '/session/{user}', SessionView) + + client = await test_client(app) + + resp = await client.get('/permission') + assert web.HTTPUnauthorized.status_code == resp.status + resp = await client.post('/permission') + assert web.HTTPUnauthorized.status_code == resp.status + resp = await client.delete('/permission') + assert web.HTTPUnauthorized.status_code == resp.status + + await client.post('/session/user_1') + resp = await client.get('/permission') + assert web.HTTPOk.status_code == resp.status + resp = await client.post('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.put('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.patch('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.delete('/permission') + assert web.HTTPForbidden.status_code == resp.status + + await client.post('/session/user_2') + resp = await client.get('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.post('/permission') + assert web.HTTPOk.status_code == resp.status + resp = await client.put('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.patch('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.delete('/permission') + assert web.HTTPForbidden.status_code == resp.status + + await client.post('/session/user_3') + resp = await client.get('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.post('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.put('/permission') + assert web.HTTPOk.status_code == resp.status + resp = await client.patch('/permission') + assert web.HTTPOk.status_code == resp.status + resp = await client.delete('/permission') + assert web.HTTPForbidden.status_code == resp.status + + await client.post('/session/user_4') + resp = await client.get('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.post('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.put('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.patch('/permission') + assert web.HTTPForbidden.status_code == resp.status + resp = await client.delete('/permission') + assert web.HTTPOk.status_code == resp.status + + await client.delete('/session/user_4') + resp = await client.get('/permission') + assert web.HTTPUnauthorized.status_code == resp.status + resp = await client.post('/permission') + assert web.HTTPUnauthorized.status_code == resp.status + resp = await client.put('/permission') + assert web.HTTPUnauthorized.status_code == resp.status + resp = await client.patch('/permission') + assert web.HTTPUnauthorized.status_code == resp.status + resp = await client.delete('/permission') + assert web.HTTPUnauthorized.status_code == resp.status