New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add has permission class decorator #150
Closed
Maillol
wants to merge
2
commits into
aio-libs:master
from
Maillol:add_class_has_permission_decorator
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,13 +92,18 @@ def login_required(fn): | |
User is considered authorized if authorized_userid | ||
returns some value. | ||
""" | ||
|
||
@wraps(fn) | ||
async def wrapped(*args, **kwargs): | ||
request = args[-1] | ||
if not isinstance(request, web.BaseRequest): | ||
if isinstance(request, web.View): | ||
request = request.request | ||
elif not isinstance(request, web.BaseRequest): | ||
msg = ("Incorrect decorator usage. " | ||
"Expecting `def handler(request)` " | ||
"or `def handler(self, request)`.") | ||
"`def handler(self, request)` or " | ||
"`def handler(self)` if handler is " | ||
"a web.View subclasse method.") | ||
raise RuntimeError(msg) | ||
|
||
userid = await authorized_userid(request) | ||
|
@@ -123,19 +128,23 @@ def has_permission( | |
raises HTTPForbidden. | ||
""" | ||
def wrapper(fn): | ||
|
||
@wraps(fn) | ||
async def wrapped(*args, **kwargs): | ||
request = args[-1] | ||
if not isinstance(request, web.BaseRequest): | ||
if isinstance(request, web.View): | ||
request = request.request | ||
elif not isinstance(request, web.BaseRequest): | ||
msg = ("Incorrect decorator usage. " | ||
"Expecting `def handler(request)` " | ||
"or `def handler(self, request)`.") | ||
"`def handler(self, request)` or " | ||
"`def handler(self)` if handler is " | ||
"a web.View subclasse method.") | ||
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 | ||
|
@@ -147,6 +156,41 @@ async def wrapped(*args, **kwargs): | |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we also check type of |
||
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious, may be name this function
view_has_permission
to indicate that decorator works forweb.View
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we can change the name, but if the
has_permission
decorator is deprecated (#169) I think that we cannot use this API to have set global permission.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thought that not just
has_permission
was deprecated, but the decorator-way to check permissions is deprecated. Wouldn't this one face the same fate?