Add class_has_permission decorator

This decorator adds permission for each
method of `aiohttp.web.View` class
This commit is contained in:
Vincent Maillol 2018-04-19 07:42:51 +02:00
parent cb1ca8a671
commit 2371a9574b
3 changed files with 176 additions and 3 deletions

View File

@ -1,5 +1,6 @@
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,
class_has_permission, is_anonymous,
login_required, permits, remember, setup) login_required, permits, remember, setup)
from .cookies_identity import CookiesIdentityPolicy from .cookies_identity import CookiesIdentityPolicy
from .session_identity import SessionIdentityPolicy from .session_identity import SessionIdentityPolicy
@ -11,4 +12,4 @@ __all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
'CookiesIdentityPolicy', 'SessionIdentityPolicy', 'CookiesIdentityPolicy', 'SessionIdentityPolicy',
'remember', 'forget', 'authorized_userid', 'remember', 'forget', 'authorized_userid',
'permits', 'setup', 'is_anonymous', 'permits', 'setup', 'is_anonymous',
'login_required', 'has_permission') 'login_required', 'has_permission', 'class_has_permission')

View File

@ -145,7 +145,6 @@ def has_permission(
userid = await authorized_userid(request) userid = await authorized_userid(request)
if userid is None: if userid is None:
raise web.HTTPUnauthorized raise web.HTTPUnauthorized
allowed = await permits(request, permission, context) allowed = await permits(request, permission, context)
if not allowed: if not allowed:
raise web.HTTPForbidden raise web.HTTPForbidden
@ -157,6 +156,41 @@ def has_permission(
return wrapper 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): def setup(app, identity_policy, autz_policy):
assert isinstance(identity_policy, AbstractIdentityPolicy), identity_policy assert isinstance(identity_policy, AbstractIdentityPolicy), identity_policy
assert isinstance(autz_policy, AbstractAuthorizationPolicy), autz_policy assert isinstance(autz_policy, AbstractAuthorizationPolicy), autz_policy

View File

@ -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