Skip to content
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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions 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
Expand All @@ -11,4 +12,4 @@
'CookiesIdentityPolicy', 'SessionIdentityPolicy',
'remember', 'forget', 'authorized_userid',
'permits', 'setup', 'is_anonymous',
'login_required', 'has_permission')
'login_required', 'has_permission', 'class_has_permission')
54 changes: 49 additions & 5 deletions aiohttp_security/api.py
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -147,6 +156,41 @@ async def wrapped(*args, **kwargs):
return wrapper


def class_has_permission(permission_prefix, context=None):
Copy link
Member

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 for web.View?

Copy link
Author

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.

Copy link
Member

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?

"""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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also check type of cls object and throw TypeError if clsis not web.Veiw?

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
Expand Down
138 changes: 138 additions & 0 deletions 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
95 changes: 95 additions & 0 deletions tests/test_dict_autz.py
Expand Up @@ -194,6 +194,41 @@ async def logout(request):
assert web.HTTPUnauthorized.status_code == resp.status


async def test_login_required_with_class_view(loop, test_client):

class IndexView(web.View):
@login_required
async def get(self):
return web.HTTPOk()

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)
return response

app = web.Application(loop=loop)
_setup(app, CookiesIdentityPolicy(), Autz())
app.router.add_route('*', '/', IndexView)
app.router.add_route('POST', '/login', login)
app.router.add_route('POST', '/logout', logout)
client = await test_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_has_permission(loop, test_client):

@has_permission('read')
Expand Down Expand Up @@ -249,3 +284,63 @@ async def logout(request):
assert web.HTTPUnauthorized.status_code == resp.status
resp = await client.get('/permission/forbid')
assert web.HTTPUnauthorized.status_code == resp.status


async def test_has_permission_with_class_view(loop, test_client):

class IndexView(web.View):
@has_permission('read')
async def get(self):
return web.HTTPOk()

@has_permission('write')
async def post(self):
return web.HTTPOk()

@has_permission('forbid')
async def delete(self):
return web.HTTPOk()

class SessionView(web.View):
async def post(self):
response = web.HTTPFound(location='/')
await remember(self.request, response, 'UserID')
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', IndexView)
app.router.add_route(
'*', '/session', 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')
resp = await client.get('/permission')
assert web.HTTPOk.status_code == resp.status
resp = await client.post('/permission')
assert web.HTTPOk.status_code == resp.status
resp = await client.delete('/permission')
assert web.HTTPForbidden.status_code == resp.status

await client.delete('/session')
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