Skip to content

Commit 92e6fec

Browse files
litwishaasvetlov
authored andcommitted
is_anonymous, login_required, has_permission helpers (#114)
* add is_anonymous helper function and login_required, has_permission decorators * add docs for `is_anonymous`, `login_required` and `has_permission` functions * permission can be `enum.Enum` object; cover with tests * isort fix
1 parent 810312b commit 92e6fec

File tree

4 files changed

+283
-13
lines changed

4 files changed

+283
-13
lines changed

aiohttp_security/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
from .abc import AbstractIdentityPolicy, AbstractAuthorizationPolicy
2-
from .api import remember, forget, setup, authorized_userid, permits
1+
from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy
2+
from .api import (authorized_userid, forget, has_permission, is_anonymous,
3+
login_required, permits, remember, setup)
34
from .cookies_identity import CookiesIdentityPolicy
45
from .session_identity import SessionIdentityPolicy
56

6-
77
__version__ = '0.1.2'
88

99

1010
__all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
1111
'CookiesIdentityPolicy', 'SessionIdentityPolicy',
1212
'remember', 'forget', 'authorized_userid',
13-
'permits', 'setup')
13+
'permits', 'setup', 'is_anonymous',
14+
'login_required', 'has_permission')

aiohttp_security/api.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import asyncio
2+
import enum
23
from aiohttp import web
34
from aiohttp_security.abc import (AbstractIdentityPolicy,
45
AbstractAuthorizationPolicy)
6+
from functools import wraps
57

68
IDENTITY_KEY = 'aiohttp_security_identity_policy'
79
AUTZ_KEY = 'aiohttp_security_autz_policy'
@@ -62,7 +64,7 @@ def authorized_userid(request):
6264

6365
@asyncio.coroutine
6466
def permits(request, permission, context=None):
65-
assert isinstance(permission, str), permission
67+
assert isinstance(permission, (str, enum.Enum)), permission
6668
assert permission
6769
identity_policy = request.app.get(IDENTITY_KEY)
6870
autz_policy = request.app.get(AUTZ_KEY)
@@ -74,6 +76,85 @@ def permits(request, permission, context=None):
7476
return access
7577

7678

79+
@asyncio.coroutine
80+
def is_anonymous(request):
81+
"""Check if user is anonymous.
82+
83+
User is considered anonymous if there is not identity
84+
in request.
85+
"""
86+
identity_policy = request.app.get(IDENTITY_KEY)
87+
if identity_policy is None:
88+
return True
89+
identity = yield from identity_policy.identify(request)
90+
if identity is None:
91+
return True
92+
return False
93+
94+
95+
def login_required(fn):
96+
"""Decorator that restrict access only for authorized users.
97+
98+
User is considered authorized if authorized_userid
99+
returns some value.
100+
"""
101+
@asyncio.coroutine
102+
@wraps(fn)
103+
def wrapped(*args, **kwargs):
104+
request = args[-1]
105+
if not isinstance(request, web.BaseRequest):
106+
msg = ("Incorrect decorator usage. "
107+
"Expecting `def handler(request)` "
108+
"or `def handler(self, request)`.")
109+
raise RuntimeError(msg)
110+
111+
userid = yield from authorized_userid(request)
112+
if userid is None:
113+
raise web.HTTPUnauthorized
114+
115+
ret = yield from fn(*args, **kwargs)
116+
return ret
117+
118+
return wrapped
119+
120+
121+
def has_permission(
122+
permission,
123+
context=None,
124+
):
125+
"""Decorator that restrict access only for authorized users
126+
with correct permissions.
127+
128+
If user is not authorized - raises HTTPUnauthorized,
129+
if user is authorized and does not have permission -
130+
raises HTTPForbidden.
131+
"""
132+
def wrapper(fn):
133+
@asyncio.coroutine
134+
@wraps(fn)
135+
def wrapped(*args, **kwargs):
136+
request = args[-1]
137+
if not isinstance(request, web.BaseRequest):
138+
msg = ("Incorrect decorator usage. "
139+
"Expecting `def handler(request)` "
140+
"or `def handler(self, request)`.")
141+
raise RuntimeError(msg)
142+
143+
userid = yield from authorized_userid(request)
144+
if userid is None:
145+
raise web.HTTPUnauthorized
146+
147+
allowed = yield from permits(request, permission, context)
148+
if not allowed:
149+
raise web.HTTPForbidden
150+
ret = yield from fn(*args, **kwargs)
151+
return ret
152+
153+
return wrapped
154+
155+
return wrapper
156+
157+
77158
def setup(app, identity_policy, autz_policy):
78159
assert isinstance(identity_policy, AbstractIdentityPolicy), identity_policy
79160
assert isinstance(autz_policy, AbstractAuthorizationPolicy), autz_policy

docs/reference.rst

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Public API functions
7878

7979
:param request: :class:`aiohttp.web.Request` object.
8080

81-
:param str permission: requested :term:`permission`.
81+
:param permission: Requested :term:`permission`. :class:`str` or :class:`enum.Enum` object.
8282

8383
:param context: additional object may be passed into
8484
:meth:`AbstractAuthorizationPolicy.permission`
@@ -88,6 +88,33 @@ Public API functions
8888
``False`` otherwise.
8989

9090

91+
.. coroutinefunction:: is_anonymous(request)
92+
93+
Checks if user is anonymous user.
94+
95+
Return ``True`` if user is not remembered in request, otherwise returns ``False``.
96+
97+
:param request: :class:`aiohttp.web.Request` object.
98+
99+
100+
.. decorator:: login_required
101+
102+
Decorator for handlers that checks if user is authorized.
103+
104+
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized.
105+
106+
107+
.. decorator:: has_permission(permission)
108+
109+
Decorator for handlers that checks if user is authorized
110+
and has correct permission.
111+
112+
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized.
113+
Raises :class:`aiohttp.web.HTTPForbidden` if user is authorized but has no access rights.
114+
115+
:param str permission: requested :term:`permission`.
116+
117+
91118
.. function:: setup(app, identity_policy, autz_policy)
92119

93120
Setup :mod:`aiohttp` application with security policies.

tests/test_dict_autz.py

Lines changed: 168 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import asyncio
2+
import enum
23

34
from aiohttp import web
4-
from aiohttp_security import (remember,
5-
authorized_userid, permits,
6-
AbstractAuthorizationPolicy)
75
from aiohttp_security import setup as _setup
6+
from aiohttp_security import (AbstractAuthorizationPolicy, authorized_userid,
7+
forget, has_permission, is_anonymous,
8+
login_required, permits, remember)
89
from aiohttp_security.cookies_identity import CookiesIdentityPolicy
910

1011

@@ -73,7 +74,27 @@ def check(request):
7374

7475

7576
@asyncio.coroutine
76-
def test_permits(loop, test_client):
77+
def test_permits_enum_permission(loop, test_client):
78+
class Permission(enum.Enum):
79+
READ = '101'
80+
WRITE = '102'
81+
UNKNOWN = '103'
82+
83+
class Autz(AbstractAuthorizationPolicy):
84+
85+
@asyncio.coroutine
86+
def permits(self, identity, permission, context=None):
87+
if identity == 'UserID':
88+
return permission in {Permission.READ, Permission.WRITE}
89+
else:
90+
return False
91+
92+
@asyncio.coroutine
93+
def authorized_userid(self, identity):
94+
if identity == 'UserID':
95+
return 'Andrew'
96+
else:
97+
return None
7798

7899
@asyncio.coroutine
79100
def login(request):
@@ -83,11 +104,11 @@ def login(request):
83104

84105
@asyncio.coroutine
85106
def check(request):
86-
ret = yield from permits(request, 'read')
107+
ret = yield from permits(request, Permission.READ)
87108
assert ret
88-
ret = yield from permits(request, 'write')
109+
ret = yield from permits(request, Permission.WRITE)
89110
assert ret
90-
ret = yield from permits(request, 'unknown')
111+
ret = yield from permits(request, Permission.UNKNOWN)
91112
assert not ret
92113
return web.Response()
93114

@@ -121,3 +142,143 @@ def check(request):
121142
resp = yield from client.get('/')
122143
assert 200 == resp.status
123144
yield from resp.release()
145+
146+
147+
@asyncio.coroutine
148+
def test_is_anonymous(loop, test_client):
149+
150+
@asyncio.coroutine
151+
def index(request):
152+
is_anon = yield from is_anonymous(request)
153+
if is_anon:
154+
return web.HTTPUnauthorized()
155+
return web.HTTPOk()
156+
157+
@asyncio.coroutine
158+
def login(request):
159+
response = web.HTTPFound(location='/')
160+
yield from remember(request, response, 'UserID')
161+
return response
162+
163+
@asyncio.coroutine
164+
def logout(request):
165+
response = web.HTTPFound(location='/')
166+
yield from forget(request, response)
167+
return response
168+
169+
app = web.Application(loop=loop)
170+
_setup(app, CookiesIdentityPolicy(), Autz())
171+
app.router.add_route('GET', '/', index)
172+
app.router.add_route('POST', '/login', login)
173+
app.router.add_route('POST', '/logout', logout)
174+
client = yield from test_client(app)
175+
resp = yield from client.get('/')
176+
assert web.HTTPUnauthorized.status_code == resp.status
177+
178+
yield from client.post('/login')
179+
resp = yield from client.get('/')
180+
assert web.HTTPOk.status_code == resp.status
181+
182+
yield from client.post('/logout')
183+
resp = yield from client.get('/')
184+
assert web.HTTPUnauthorized.status_code == resp.status
185+
186+
187+
@asyncio.coroutine
188+
def test_login_required(loop, test_client):
189+
@login_required
190+
@asyncio.coroutine
191+
def index(request):
192+
return web.HTTPOk()
193+
194+
@asyncio.coroutine
195+
def login(request):
196+
response = web.HTTPFound(location='/')
197+
yield from remember(request, response, 'UserID')
198+
return response
199+
200+
@asyncio.coroutine
201+
def logout(request):
202+
response = web.HTTPFound(location='/')
203+
yield from forget(request, response)
204+
return response
205+
206+
app = web.Application(loop=loop)
207+
_setup(app, CookiesIdentityPolicy(), Autz())
208+
app.router.add_route('GET', '/', index)
209+
app.router.add_route('POST', '/login', login)
210+
app.router.add_route('POST', '/logout', logout)
211+
client = yield from test_client(app)
212+
resp = yield from client.get('/')
213+
assert web.HTTPUnauthorized.status_code == resp.status
214+
215+
yield from client.post('/login')
216+
resp = yield from client.get('/')
217+
assert web.HTTPOk.status_code == resp.status
218+
219+
yield from client.post('/logout')
220+
resp = yield from client.get('/')
221+
assert web.HTTPUnauthorized.status_code == resp.status
222+
223+
224+
@asyncio.coroutine
225+
def test_has_permission(loop, test_client):
226+
227+
@has_permission('read')
228+
@asyncio.coroutine
229+
def index_read(request):
230+
return web.HTTPOk()
231+
232+
@has_permission('write')
233+
@asyncio.coroutine
234+
def index_write(request):
235+
return web.HTTPOk()
236+
237+
@has_permission('forbid')
238+
@asyncio.coroutine
239+
def index_forbid(request):
240+
return web.HTTPOk()
241+
242+
@asyncio.coroutine
243+
def login(request):
244+
response = web.HTTPFound(location='/')
245+
yield from remember(request, response, 'UserID')
246+
return response
247+
248+
@asyncio.coroutine
249+
def logout(request):
250+
response = web.HTTPFound(location='/')
251+
yield from forget(request, response)
252+
return response
253+
254+
app = web.Application(loop=loop)
255+
_setup(app, CookiesIdentityPolicy(), Autz())
256+
app.router.add_route('GET', '/permission/read', index_read)
257+
app.router.add_route('GET', '/permission/write', index_write)
258+
app.router.add_route('GET', '/permission/forbid', index_forbid)
259+
app.router.add_route('POST', '/login', login)
260+
app.router.add_route('POST', '/logout', logout)
261+
client = yield from test_client(app)
262+
263+
resp = yield from client.get('/permission/read')
264+
assert web.HTTPUnauthorized.status_code == resp.status
265+
resp = yield from client.get('/permission/write')
266+
assert web.HTTPUnauthorized.status_code == resp.status
267+
resp = yield from client.get('/permission/forbid')
268+
assert web.HTTPUnauthorized.status_code == resp.status
269+
270+
yield from client.post('/login')
271+
resp = yield from client.get('/permission/read')
272+
assert web.HTTPOk.status_code == resp.status
273+
resp = yield from client.get('/permission/write')
274+
assert web.HTTPOk.status_code == resp.status
275+
resp = yield from client.get('/permission/forbid')
276+
assert web.HTTPForbidden.status_code == resp.status
277+
278+
yield from client.post('/logout')
279+
resp = yield from client.get('/permission/read')
280+
assert web.HTTPUnauthorized.status_code == resp.status
281+
resp = yield from client.get('/permission/write')
282+
assert web.HTTPUnauthorized.status_code == resp.status
283+
resp = yield from client.get('/permission/forbid')
284+
assert web.HTTPUnauthorized.status_code == resp.status

0 commit comments

Comments
 (0)