Compare commits
20 Commits
v0.1.2
...
pyup-sched
Author | SHA1 | Date | |
---|---|---|---|
|
140804e4b5 | ||
|
6d9f5f03ed | ||
|
363f3b71c0 | ||
|
9da55517b2 | ||
|
d375b22f1b | ||
|
4506c306a7 | ||
|
9b9b848fdd | ||
|
1679f6713b | ||
|
f9628b0ac1 | ||
|
f8940c0696 | ||
|
5d1195b85d | ||
|
8360095011 | ||
|
d89f6b7e3d | ||
|
db7dbd9b07 | ||
|
5b2ff779c3 | ||
|
b9dee120c3 | ||
|
aff9a6a915 | ||
|
92e6fec6f5 | ||
|
810312b508 | ||
|
18a04b879e |
@@ -1,11 +1,15 @@
|
||||
language: python
|
||||
python:
|
||||
- 3.4.3
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7-dev
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: 3.7-dev
|
||||
- python: nightly
|
||||
|
||||
install:
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements-dev.txt
|
||||
|
@@ -1,6 +1,11 @@
|
||||
Changes
|
||||
=======
|
||||
|
||||
0.2.0 (2017-11-17)
|
||||
------------------
|
||||
|
||||
- Add `is_anonymous`, `login_required`, `has_permission` helpers (#114)
|
||||
|
||||
0.1.2 (2017-10-17)
|
||||
------------------
|
||||
|
||||
|
2
LICENSE
2
LICENSE
@@ -186,7 +186,7 @@ Apache License
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015-2016 Andrew Svetlov
|
||||
Copyright 2015-2018 Andrew Svetlov and aio-libs team.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
29
README.rst
29
README.rst
@@ -9,17 +9,34 @@ aiohttp_security
|
||||
.. image:: https://img.shields.io/pypi/v/aiohttp-security.svg
|
||||
:target: https://pypi.python.org/pypi/aiohttp-security
|
||||
|
||||
The library provides identity and autorization for `aiohttp.web`__.
|
||||
The library provides identity and authorization for `aiohttp.web`__.
|
||||
|
||||
.. _aiohttp_web: http://aiohttp.readthedocs.org/en/latest/web.html
|
||||
|
||||
__ aiohttp_web_
|
||||
|
||||
Usage
|
||||
-----
|
||||
To install type ``pip install aiohttp_security``.
|
||||
Launch ``make doc`` and see examples or look under **demo** directory for a
|
||||
sample project.
|
||||
Installation
|
||||
------------
|
||||
Simplest case (authorization via cookies) ::
|
||||
|
||||
$ pip install aiohttp_security
|
||||
|
||||
With `aiohttp-session` support ::
|
||||
|
||||
$ pip install aiohttp_security[session]
|
||||
|
||||
Examples
|
||||
--------
|
||||
Take a look at examples:
|
||||
|
||||
`Basic example`_
|
||||
|
||||
`Example with DB auth`_
|
||||
|
||||
.. _`Basic example`: docs/example.rst
|
||||
.. _`Example with db auth`: docs/example_db_auth.rst
|
||||
|
||||
and demos at **demo** directory.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
@@ -1,13 +1,14 @@
|
||||
from .abc import AbstractIdentityPolicy, AbstractAuthorizationPolicy
|
||||
from .api import remember, forget, setup, authorized_userid, permits
|
||||
from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy
|
||||
from .api import (authorized_userid, forget, has_permission, is_anonymous,
|
||||
login_required, permits, remember, setup)
|
||||
from .cookies_identity import CookiesIdentityPolicy
|
||||
from .session_identity import SessionIdentityPolicy
|
||||
|
||||
|
||||
__version__ = '0.1.2'
|
||||
__version__ = '0.2.0'
|
||||
|
||||
|
||||
__all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
|
||||
'CookiesIdentityPolicy', 'SessionIdentityPolicy',
|
||||
'remember', 'forget', 'authorized_userid',
|
||||
'permits', 'setup')
|
||||
'permits', 'setup', 'is_anonymous',
|
||||
'login_required', 'has_permission')
|
||||
|
@@ -1,21 +1,18 @@
|
||||
import abc
|
||||
import asyncio
|
||||
|
||||
# see http://plope.com/pyramid_auth_design_api_postmortem
|
||||
|
||||
|
||||
class AbstractIdentityPolicy(metaclass=abc.ABCMeta):
|
||||
|
||||
@asyncio.coroutine
|
||||
@abc.abstractmethod
|
||||
def identify(self, request):
|
||||
async def identify(self, request):
|
||||
"""Return the claimed identity of the user associated request or
|
||||
``None`` if no identity can be found associated with the request."""
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
@abc.abstractmethod
|
||||
def remember(self, request, response, identity, **kwargs):
|
||||
async def remember(self, request, response, identity, **kwargs):
|
||||
"""Remember identity.
|
||||
|
||||
Modify response object by filling it's headers with remembered user.
|
||||
@@ -25,9 +22,8 @@ class AbstractIdentityPolicy(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
@abc.abstractmethod
|
||||
def forget(self, request, response):
|
||||
async def forget(self, request, response):
|
||||
""" Modify response which can be used to 'forget' the
|
||||
current identity on subsequent requests."""
|
||||
pass
|
||||
@@ -35,9 +31,8 @@ class AbstractIdentityPolicy(metaclass=abc.ABCMeta):
|
||||
|
||||
class AbstractAuthorizationPolicy(metaclass=abc.ABCMeta):
|
||||
|
||||
@asyncio.coroutine
|
||||
@abc.abstractmethod
|
||||
def permits(self, identity, permission, context=None):
|
||||
async def permits(self, identity, permission, context=None):
|
||||
"""Check user permissions.
|
||||
|
||||
Return True if the identity is allowed the permission in the
|
||||
@@ -45,9 +40,8 @@ class AbstractAuthorizationPolicy(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
@abc.abstractmethod
|
||||
def authorized_userid(self, identity):
|
||||
async def authorized_userid(self, identity):
|
||||
"""Retrieve authorized user id.
|
||||
|
||||
Return the user_id of the user identified by the identity
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import asyncio
|
||||
import enum
|
||||
from aiohttp import web
|
||||
from aiohttp_security.abc import (AbstractIdentityPolicy,
|
||||
AbstractAuthorizationPolicy)
|
||||
from functools import wraps
|
||||
|
||||
IDENTITY_KEY = 'aiohttp_security_identity_policy'
|
||||
AUTZ_KEY = 'aiohttp_security_autz_policy'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def remember(request, response, identity, **kwargs):
|
||||
async def remember(request, response, identity, **kwargs):
|
||||
"""Remember identity into response.
|
||||
|
||||
The action is performed by identity_policy.remember()
|
||||
|
||||
Usually the idenity is stored in user cookies homehow but may be
|
||||
Usually the identity is stored in user cookies somehow but may be
|
||||
pushed into custom header also.
|
||||
"""
|
||||
assert isinstance(identity, str), identity
|
||||
@@ -26,11 +26,10 @@ def remember(request, response, identity, **kwargs):
|
||||
# output and rendered page we add same message to *reason* and
|
||||
# *text* arguments.
|
||||
raise web.HTTPInternalServerError(reason=text, text=text)
|
||||
yield from identity_policy.remember(request, response, identity, **kwargs)
|
||||
await identity_policy.remember(request, response, identity, **kwargs)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def forget(request, response):
|
||||
async def forget(request, response):
|
||||
"""Forget previously remembered identity.
|
||||
|
||||
Usually it clears cookie or server-side storage to forget user
|
||||
@@ -44,36 +43,110 @@ def forget(request, response):
|
||||
# output and rendered page we add same message to *reason* and
|
||||
# *text* arguments.
|
||||
raise web.HTTPInternalServerError(reason=text, text=text)
|
||||
yield from identity_policy.forget(request, response)
|
||||
await identity_policy.forget(request, response)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def authorized_userid(request):
|
||||
async def authorized_userid(request):
|
||||
identity_policy = request.app.get(IDENTITY_KEY)
|
||||
autz_policy = request.app.get(AUTZ_KEY)
|
||||
if identity_policy is None or autz_policy is None:
|
||||
return None
|
||||
identity = yield from identity_policy.identify(request)
|
||||
identity = await identity_policy.identify(request)
|
||||
if identity is None:
|
||||
return None # non-registered user has None user_id
|
||||
user_id = yield from autz_policy.authorized_userid(identity)
|
||||
user_id = await autz_policy.authorized_userid(identity)
|
||||
return user_id
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def permits(request, permission, context=None):
|
||||
assert isinstance(permission, str), permission
|
||||
async def permits(request, permission, context=None):
|
||||
assert isinstance(permission, (str, enum.Enum)), permission
|
||||
assert permission
|
||||
identity_policy = request.app.get(IDENTITY_KEY)
|
||||
autz_policy = request.app.get(AUTZ_KEY)
|
||||
if identity_policy is None or autz_policy is None:
|
||||
return True
|
||||
identity = yield from identity_policy.identify(request)
|
||||
identity = await identity_policy.identify(request)
|
||||
# non-registered user still may has some permissions
|
||||
access = yield from autz_policy.permits(identity, permission, context)
|
||||
access = await autz_policy.permits(identity, permission, context)
|
||||
return access
|
||||
|
||||
|
||||
async def is_anonymous(request):
|
||||
"""Check if user is anonymous.
|
||||
|
||||
User is considered anonymous if there is not identity
|
||||
in request.
|
||||
"""
|
||||
identity_policy = request.app.get(IDENTITY_KEY)
|
||||
if identity_policy is None:
|
||||
return True
|
||||
identity = await identity_policy.identify(request)
|
||||
if identity is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def login_required(fn):
|
||||
"""Decorator that restrict access only for authorized users.
|
||||
|
||||
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):
|
||||
msg = ("Incorrect decorator usage. "
|
||||
"Expecting `def handler(request)` "
|
||||
"or `def handler(self, request)`.")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
userid = await authorized_userid(request)
|
||||
if userid is None:
|
||||
raise web.HTTPUnauthorized
|
||||
|
||||
ret = await fn(*args, **kwargs)
|
||||
return ret
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def has_permission(
|
||||
permission,
|
||||
context=None,
|
||||
):
|
||||
"""Decorator that restrict access only for authorized users
|
||||
with correct permissions.
|
||||
|
||||
If user is not authorized - raises HTTPUnauthorized,
|
||||
if user is authorized and does not have permission -
|
||||
raises HTTPForbidden.
|
||||
"""
|
||||
def wrapper(fn):
|
||||
@wraps(fn)
|
||||
async def wrapped(*args, **kwargs):
|
||||
request = args[-1]
|
||||
if not isinstance(request, web.BaseRequest):
|
||||
msg = ("Incorrect decorator usage. "
|
||||
"Expecting `def handler(request)` "
|
||||
"or `def handler(self, request)`.")
|
||||
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
|
||||
ret = await fn(*args, **kwargs)
|
||||
return ret
|
||||
|
||||
return wrapped
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def setup(app, identity_policy, autz_policy):
|
||||
assert isinstance(identity_policy, AbstractIdentityPolicy), identity_policy
|
||||
assert isinstance(autz_policy, AbstractAuthorizationPolicy), autz_policy
|
||||
|
@@ -5,8 +5,6 @@ more handy.
|
||||
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from .abc import AbstractIdentityPolicy
|
||||
|
||||
|
||||
@@ -19,19 +17,16 @@ class CookiesIdentityPolicy(AbstractIdentityPolicy):
|
||||
self._cookie_name = 'AIOHTTP_SECURITY'
|
||||
self._max_age = 30 * 24 * 3600
|
||||
|
||||
@asyncio.coroutine
|
||||
def identify(self, request):
|
||||
async def identify(self, request):
|
||||
identity = request.cookies.get(self._cookie_name)
|
||||
return identity
|
||||
|
||||
@asyncio.coroutine
|
||||
def remember(self, request, response, identity, max_age=sentinel,
|
||||
**kwargs):
|
||||
async def remember(self, request, response, identity, max_age=sentinel,
|
||||
**kwargs):
|
||||
if max_age is sentinel:
|
||||
max_age = self._max_age
|
||||
response.set_cookie(self._cookie_name, identity,
|
||||
max_age=max_age, **kwargs)
|
||||
|
||||
@asyncio.coroutine
|
||||
def forget(self, request, response):
|
||||
async def forget(self, request, response):
|
||||
response.del_cookie(self._cookie_name)
|
||||
|
@@ -4,8 +4,6 @@ aiohttp_session.setup() should be called on application initialization
|
||||
to configure aiohttp_session properly.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
from aiohttp_session import get_session
|
||||
HAS_AIOHTTP_SESSION = True
|
||||
@@ -24,17 +22,14 @@ class SessionIdentityPolicy(AbstractIdentityPolicy):
|
||||
raise ImportError(
|
||||
'SessionIdentityPolicy requires `aiohttp_session`')
|
||||
|
||||
@asyncio.coroutine
|
||||
def identify(self, request):
|
||||
session = yield from get_session(request)
|
||||
async def identify(self, request):
|
||||
session = await get_session(request)
|
||||
return session.get(self._session_key)
|
||||
|
||||
@asyncio.coroutine
|
||||
def remember(self, request, response, identity, **kwargs):
|
||||
session = yield from get_session(request)
|
||||
async def remember(self, request, response, identity, **kwargs):
|
||||
session = await get_session(request)
|
||||
session[self._session_key] = identity
|
||||
|
||||
@asyncio.coroutine
|
||||
def forget(self, request, response):
|
||||
session = yield from get_session(request)
|
||||
async def forget(self, request, response):
|
||||
session = await get_session(request)
|
||||
session.pop(self._session_key, None)
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import asyncio
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
||||
@@ -12,29 +10,27 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||
def __init__(self, dbengine):
|
||||
self.dbengine = dbengine
|
||||
|
||||
@asyncio.coroutine
|
||||
def authorized_userid(self, identity):
|
||||
with (yield from self.dbengine) as conn:
|
||||
async def authorized_userid(self, identity):
|
||||
async with self.dbengine as conn:
|
||||
where = sa.and_(db.users.c.login == identity,
|
||||
sa.not_(db.users.c.disabled))
|
||||
query = db.users.count().where(where)
|
||||
ret = yield from conn.scalar(query)
|
||||
ret = await conn.scalar(query)
|
||||
if ret:
|
||||
return identity
|
||||
else:
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
def permits(self, identity, permission, context=None):
|
||||
async def permits(self, identity, permission, context=None):
|
||||
if identity is None:
|
||||
return False
|
||||
|
||||
with (yield from self.dbengine) as conn:
|
||||
async with self.dbengine as conn:
|
||||
where = sa.and_(db.users.c.login == identity,
|
||||
sa.not_(db.users.c.disabled))
|
||||
query = db.users.select().where(where)
|
||||
ret = yield from conn.execute(query)
|
||||
user = yield from ret.fetchone()
|
||||
ret = await conn.execute(query)
|
||||
user = await ret.fetchone()
|
||||
if user is not None:
|
||||
user_id = user[0]
|
||||
is_superuser = user[3]
|
||||
@@ -43,8 +39,8 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||
|
||||
where = db.permissions.c.user_id == user_id
|
||||
query = db.permissions.select().where(where)
|
||||
ret = yield from conn.execute(query)
|
||||
result = yield from ret.fetchall()
|
||||
ret = await conn.execute(query)
|
||||
result = await ret.fetchall()
|
||||
if ret is not None:
|
||||
for record in result:
|
||||
if record.perm_name == permission:
|
||||
@@ -53,14 +49,13 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||
return False
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def check_credentials(db_engine, username, password):
|
||||
with (yield from db_engine) as conn:
|
||||
async def check_credentials(db_engine, username, password):
|
||||
async with db_engine as conn:
|
||||
where = sa.and_(db.users.c.login == username,
|
||||
sa.not_(db.users.c.disabled))
|
||||
query = db.users.select().where(where)
|
||||
ret = yield from conn.execute(query)
|
||||
user = yield from ret.fetchone()
|
||||
ret = await conn.execute(query)
|
||||
user = await ret.fetchone()
|
||||
if user is not None:
|
||||
hash = user[2]
|
||||
return sha256_crypt.verify(password, hash)
|
||||
|
@@ -1,48 +1,34 @@
|
||||
import asyncio
|
||||
import functools
|
||||
from textwrap import dedent
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from aiohttp_security import remember, forget, authorized_userid, permits
|
||||
from aiohttp_security import (
|
||||
remember, forget, authorized_userid,
|
||||
has_permission, login_required,
|
||||
)
|
||||
|
||||
from .db_auth import check_credentials
|
||||
|
||||
|
||||
def require(permission):
|
||||
def wrapper(f):
|
||||
@asyncio.coroutine
|
||||
@functools.wraps(f)
|
||||
def wrapped(self, request):
|
||||
has_perm = yield from permits(request, permission)
|
||||
if not has_perm:
|
||||
message = 'User has no permission {}'.format(permission)
|
||||
raise web.HTTPForbidden(body=message.encode())
|
||||
return (yield from f(self, request))
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
class Web(object):
|
||||
index_template = """
|
||||
<!doctype html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<p>{message}</p>
|
||||
<form action="/login" method="post">
|
||||
Login:
|
||||
<input type="text" name="login">
|
||||
Password:
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<a href="/logout">Logout</a>
|
||||
</body>
|
||||
"""
|
||||
index_template = dedent("""
|
||||
<!doctype html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>{message}</p>
|
||||
<form action="/login" method="post">
|
||||
Login:
|
||||
<input type="text" name="login">
|
||||
Password:
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<a href="/logout">Logout</a>
|
||||
</body>
|
||||
""")
|
||||
|
||||
@asyncio.coroutine
|
||||
def index(self, request):
|
||||
username = yield from authorized_userid(request)
|
||||
async def index(self, request):
|
||||
username = await authorized_userid(request)
|
||||
if username:
|
||||
template = self.index_template.format(
|
||||
message='Hello, {username}!'.format(username=username))
|
||||
@@ -51,37 +37,33 @@ class Web(object):
|
||||
response = web.Response(body=template.encode())
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def login(self, request):
|
||||
async def login(self, request):
|
||||
response = web.HTTPFound('/')
|
||||
form = yield from request.post()
|
||||
form = await request.post()
|
||||
login = form.get('login')
|
||||
password = form.get('password')
|
||||
db_engine = request.app.db_engine
|
||||
if (yield from check_credentials(db_engine, login, password)):
|
||||
yield from remember(request, response, login)
|
||||
if await check_credentials(db_engine, login, password):
|
||||
await remember(request, response, login)
|
||||
return response
|
||||
|
||||
return web.HTTPUnauthorized(
|
||||
body=b'Invalid username/password combination')
|
||||
|
||||
@require('public')
|
||||
@asyncio.coroutine
|
||||
def logout(self, request):
|
||||
@login_required
|
||||
async def logout(self, request):
|
||||
response = web.Response(body=b'You have been logged out')
|
||||
yield from forget(request, response)
|
||||
await forget(request, response)
|
||||
return response
|
||||
|
||||
@require('public')
|
||||
@asyncio.coroutine
|
||||
def internal_page(self, request):
|
||||
@has_permission('public')
|
||||
async def internal_page(self, request):
|
||||
response = web.Response(
|
||||
body=b'This page is visible for all registered users')
|
||||
return response
|
||||
|
||||
@require('protected')
|
||||
@asyncio.coroutine
|
||||
def protected_page(self, request):
|
||||
@has_permission('protected')
|
||||
async def protected_page(self, request):
|
||||
response = web.Response(body=b'You are on protected page')
|
||||
return response
|
||||
|
||||
|
@@ -9,17 +9,16 @@ from aiopg.sa import create_engine
|
||||
from aioredis import create_pool
|
||||
|
||||
|
||||
from demo.db_auth import DBAuthorizationPolicy
|
||||
from demo.handlers import Web
|
||||
from demo.database_auth.db_auth import DBAuthorizationPolicy
|
||||
from demo.database_auth.handlers import Web
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def init(loop):
|
||||
redis_pool = yield from create_pool(('localhost', 6379))
|
||||
db_engine = yield from create_engine(user='aiohttp_security',
|
||||
password='aiohttp_security',
|
||||
database='aiohttp_security',
|
||||
host='127.0.0.1')
|
||||
async def init(loop):
|
||||
redis_pool = await create_pool(('localhost', 6379))
|
||||
db_engine = await create_engine(user='aiohttp_security',
|
||||
password='aiohttp_security',
|
||||
database='aiohttp_security',
|
||||
host='127.0.0.1')
|
||||
app = web.Application(loop=loop)
|
||||
app.db_engine = db_engine
|
||||
setup_session(app, RedisStorage(redis_pool))
|
||||
@@ -31,21 +30,20 @@ def init(loop):
|
||||
web_handlers.configure(app)
|
||||
|
||||
handler = app.make_handler()
|
||||
srv = yield from loop.create_server(handler, '127.0.0.1', 8080)
|
||||
srv = await loop.create_server(handler, '127.0.0.1', 8080)
|
||||
print('Server started at http://127.0.0.1:8080')
|
||||
return srv, app, handler
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def finalize(srv, app, handler):
|
||||
async def finalize(srv, app, handler):
|
||||
sock = srv.sockets[0]
|
||||
app.loop.remove_reader(sock.fileno())
|
||||
sock.close()
|
||||
|
||||
yield from handler.finish_connections(1.0)
|
||||
await handler.finish_connections(1.0)
|
||||
srv.close()
|
||||
yield from srv.wait_closed()
|
||||
yield from app.finish()
|
||||
await srv.wait_closed()
|
||||
await app.finish()
|
||||
|
||||
|
||||
def main():
|
||||
|
@@ -1,44 +1,30 @@
|
||||
import asyncio
|
||||
import functools
|
||||
from textwrap import dedent
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from aiohttp_security import remember, forget, authorized_userid, permits
|
||||
from aiohttp_security import (
|
||||
remember, forget, authorized_userid,
|
||||
has_permission, login_required,
|
||||
)
|
||||
|
||||
from .authz import check_credentials
|
||||
|
||||
|
||||
def require(permission):
|
||||
def wrapper(f):
|
||||
@asyncio.coroutine
|
||||
@functools.wraps(f)
|
||||
def wrapped(request):
|
||||
has_perm = yield from permits(request, permission)
|
||||
if not has_perm:
|
||||
message = 'User has no permission {}'.format(permission)
|
||||
raise web.HTTPForbidden(body=message.encode())
|
||||
return (yield from f(request))
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
index_template = dedent("""
|
||||
<!doctype html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<p>{message}</p>
|
||||
<form action="/login" method="post">
|
||||
Login:
|
||||
<input type="text" name="username">
|
||||
Password:
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<a href="/logout">Logout</a>
|
||||
</body>
|
||||
""")
|
||||
<head></head>
|
||||
<body>
|
||||
<p>{message}</p>
|
||||
<form action="/login" method="post">
|
||||
Login:
|
||||
<input type="text" name="username">
|
||||
Password:
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<a href="/logout">Logout</a>
|
||||
</body>
|
||||
""")
|
||||
|
||||
|
||||
async def index(request):
|
||||
@@ -60,7 +46,8 @@ async def login(request):
|
||||
username = form.get('username')
|
||||
password = form.get('password')
|
||||
|
||||
verified = await check_credentials(request.app.user_map, username, password)
|
||||
verified = await check_credentials(
|
||||
request.app.user_map, username, password)
|
||||
if verified:
|
||||
await remember(request, response, username)
|
||||
return response
|
||||
@@ -68,7 +55,7 @@ async def login(request):
|
||||
return web.HTTPUnauthorized(body='Invalid username / password combination')
|
||||
|
||||
|
||||
@require('public')
|
||||
@login_required
|
||||
async def logout(request):
|
||||
response = web.Response(
|
||||
text='You have been logged out',
|
||||
@@ -78,7 +65,7 @@ async def logout(request):
|
||||
return response
|
||||
|
||||
|
||||
@require('public')
|
||||
@has_permission('public')
|
||||
async def internal_page(request):
|
||||
# pylint: disable=unused-argument
|
||||
response = web.Response(
|
||||
@@ -88,7 +75,7 @@ async def internal_page(request):
|
||||
return response
|
||||
|
||||
|
||||
@require('protected')
|
||||
@has_permission('protected')
|
||||
async def protected_page(request):
|
||||
# pylint: disable=unused-argument
|
||||
response = web.Response(
|
||||
|
@@ -6,9 +6,9 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||
from aiohttp_security import setup as setup_security
|
||||
from aiohttp_security import SessionIdentityPolicy
|
||||
|
||||
from .authz import DictionaryAuthorizationPolicy
|
||||
from .handlers import configure_handlers
|
||||
from .users import user_map
|
||||
from demo.dictionary_auth.authz import DictionaryAuthorizationPolicy
|
||||
from demo.dictionary_auth.handlers import configure_handlers
|
||||
from demo.dictionary_auth.users import user_map
|
||||
|
||||
|
||||
def make_app():
|
||||
|
@@ -10,16 +10,14 @@ Simple example::
|
||||
import asyncio
|
||||
from aiohttp import web
|
||||
|
||||
@asyncio.coroutine
|
||||
def root_handler(request):
|
||||
async def root_handler(request):
|
||||
text = "Alive and kicking!"
|
||||
return web.Response(body=text.encode('utf-8'))
|
||||
|
||||
# option 2: auth at a higher level?
|
||||
# set user_id and allowed in the wsgi handler
|
||||
@protect('view_user')
|
||||
@asyncio.coroutine
|
||||
def user_handler(request):
|
||||
async def user_handler(request):
|
||||
name = request.match_info.get('name', "Anonymous")
|
||||
text = "Hello, " + name
|
||||
return web.Response(body=text.encode('utf-8'))
|
||||
@@ -27,14 +25,12 @@ Simple example::
|
||||
|
||||
# option 3: super low
|
||||
# wsgi doesn't do anything
|
||||
@asyncio.coroutine
|
||||
def user_update_handler(request):
|
||||
async def user_update_handler(request):
|
||||
# identity, asked_permission
|
||||
user_id = yield from identity_policy.identify(request)
|
||||
identity = yield from auth_policy.authorized_user_id(user_id)
|
||||
allowed = yield from request.auth_policy.permits(
|
||||
identity, asked_permission
|
||||
)
|
||||
user_id = await identity_policy.identify(request)
|
||||
identity = await auth_policy.authorized_userid(user_id)
|
||||
allowed = await request.auth_policy.permits(
|
||||
identity, asked_permission)
|
||||
if not allowed:
|
||||
# how is this pluggable as well?
|
||||
# ? return NotAllowedStream()
|
||||
@@ -42,8 +38,7 @@ Simple example::
|
||||
|
||||
update_user()
|
||||
|
||||
@asyncio.coroutine
|
||||
def init(loop):
|
||||
async def init(loop):
|
||||
# set up identity and auth
|
||||
auth_policy = DictionaryAuthorizationPolicy({'me': ('view_user',),
|
||||
'you': ('view_user',
|
||||
@@ -60,8 +55,8 @@ Simple example::
|
||||
app.router.add_route('GET', '/{user}/edit', user_update_handler)
|
||||
|
||||
# get it started
|
||||
srv = yield from loop.create_server(app.make_handler(),
|
||||
'127.0.0.1', 8080)
|
||||
srv = await loop.create_server(app.make_handler(),
|
||||
'127.0.0.1', 8080)
|
||||
print("Server started at http://127.0.0.1:8080")
|
||||
return srv
|
||||
|
||||
|
@@ -21,12 +21,14 @@ Launch these sql scripts to init database and fill it with sample data:
|
||||
|
||||
``psql template1 < demo/sql/init_db.sql``
|
||||
|
||||
and then
|
||||
and
|
||||
|
||||
``psql template1 < demo/sql/sample_data.sql``
|
||||
|
||||
|
||||
You will have two tables for storing users and their permissions
|
||||
Now you have two tables:
|
||||
|
||||
- for storing users
|
||||
|
||||
+--------------+
|
||||
| users |
|
||||
@@ -42,7 +44,7 @@ You will have two tables for storing users and their permissions
|
||||
| disabled |
|
||||
+--------------+
|
||||
|
||||
and second table is permissions table:
|
||||
- for storing their permissions
|
||||
|
||||
+-----------------+
|
||||
| permissions |
|
||||
@@ -63,48 +65,46 @@ First one should have these methods: *identify*, *remember* and *forget*.
|
||||
For second one: *authorized_userid* and *permits*. We will use built-in
|
||||
*SessionIdentityPolicy* and write our own database-based authorization policy.
|
||||
|
||||
In our example we will lookup database by user login and if present return
|
||||
In our example we will lookup database by user login and if presents then return
|
||||
this identity::
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def authorized_userid(self, identity):
|
||||
with (yield from self.dbengine) as conn:
|
||||
async def authorized_userid(self, identity):
|
||||
async with self.dbengine as conn:
|
||||
where = sa.and_(db.users.c.login == identity,
|
||||
sa.not_(db.users.c.disabled))
|
||||
query = db.users.count().where(where)
|
||||
ret = yield from conn.scalar(query)
|
||||
ret = await conn.scalar(query)
|
||||
if ret:
|
||||
return identity
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
For permission check we will fetch the user first, check if he is superuser
|
||||
For permission checking we will fetch the user first, check if he is superuser
|
||||
(all permissions are allowed), otherwise check if permission is explicitly set
|
||||
for that user::
|
||||
|
||||
@asyncio.coroutine
|
||||
def permits(self, identity, permission, context=None):
|
||||
async def permits(self, identity, permission, context=None):
|
||||
if identity is None:
|
||||
return False
|
||||
|
||||
with (yield from self.dbengine) as conn:
|
||||
async with self.dbengine as conn:
|
||||
where = sa.and_(db.users.c.login == identity,
|
||||
sa.not_(db.users.c.disabled))
|
||||
query = db.users.select().where(where)
|
||||
ret = yield from conn.execute(query)
|
||||
user = yield from ret.fetchone()
|
||||
ret = await conn.execute(query)
|
||||
user = await ret.fetchone()
|
||||
if user is not None:
|
||||
user_id = user[0]
|
||||
is_superuser = user[4]
|
||||
is_superuser = user[3]
|
||||
if is_superuser:
|
||||
return True
|
||||
|
||||
where = db.permissions.c.user_id == user_id
|
||||
query = db.permissions.select().where(where)
|
||||
ret = yield from conn.execute(query)
|
||||
result = yield from ret.fetchall()
|
||||
ret = await conn.execute(query)
|
||||
result = await ret.fetchall()
|
||||
if ret is not None:
|
||||
for record in result:
|
||||
if record.perm_name == permission:
|
||||
@@ -127,13 +127,12 @@ Once we have all the code in place we can install it for our application::
|
||||
from .db_auth import DBAuthorizationPolicy
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def init(loop):
|
||||
redis_pool = yield from create_pool(('localhost', 6379))
|
||||
dbengine = yield from create_engine(user='aiohttp_security',
|
||||
password='aiohttp_security',
|
||||
database='aiohttp_security',
|
||||
host='127.0.0.1')
|
||||
async def init(loop):
|
||||
redis_pool = await create_pool(('localhost', 6379))
|
||||
dbengine = await create_engine(user='aiohttp_security',
|
||||
password='aiohttp_security',
|
||||
database='aiohttp_security',
|
||||
host='127.0.0.1')
|
||||
app = web.Application(loop=loop)
|
||||
setup_session(app, RedisStorage(redis_pool))
|
||||
setup_security(app,
|
||||
@@ -143,39 +142,33 @@ Once we have all the code in place we can install it for our application::
|
||||
|
||||
|
||||
Now we have authorization and can decorate every other view with access rights
|
||||
based on permissions. This simple decorator (for class-based handlers) will
|
||||
help to do that::
|
||||
based on permissions. There are already implemented two decorators::
|
||||
|
||||
def require(permission):
|
||||
def wrapper(f):
|
||||
@asyncio.coroutine
|
||||
@functools.wraps(f)
|
||||
def wrapped(self, request):
|
||||
has_perm = yield from permits(request, permission)
|
||||
if not has_perm:
|
||||
message = 'User has no permission {}'.format(permission)
|
||||
raise web.HTTPForbidden(body=message.encode())
|
||||
return (yield from f(self, request))
|
||||
return wrapped
|
||||
return wrapper
|
||||
from aiohttp_security import has_permission, login_required
|
||||
|
||||
|
||||
For each view you need to protect just apply the decorator on it::
|
||||
For each view you need to protect - just apply the decorator on it::
|
||||
|
||||
class Web:
|
||||
@require('protected')
|
||||
@asyncio.coroutine
|
||||
def protected_page(self, request):
|
||||
@has_permission('protected')
|
||||
async def protected_page(self, request):
|
||||
response = web.Response(body=b'You are on protected page')
|
||||
return response
|
||||
|
||||
or::
|
||||
|
||||
If someone will try to access this protected page he will see::
|
||||
class Web:
|
||||
@login_required
|
||||
async def logout(self, request):
|
||||
response = web.Response(body=b'You have been logged out')
|
||||
await forget(request, response)
|
||||
return response
|
||||
|
||||
403, User has no permission "protected"
|
||||
If someone try to access that protected page he will see::
|
||||
|
||||
403: Forbidden
|
||||
|
||||
|
||||
The best part about it is that you can implement any logic you want until it
|
||||
The best part of it - you can implement any logic you want until it
|
||||
follows the API conventions.
|
||||
|
||||
Launch application
|
||||
@@ -183,18 +176,17 @@ Launch application
|
||||
|
||||
For working with passwords there is a good library passlib_. Once you've
|
||||
created some users you want to check their credentials on login. Similar
|
||||
function may do what you trying to accomplish::
|
||||
function may do what you are trying to accomplish::
|
||||
|
||||
from passlib.hash import sha256_crypt
|
||||
|
||||
@asyncio.coroutine
|
||||
def check_credentials(db_engine, username, password):
|
||||
with (yield from db_engine) as conn:
|
||||
async def check_credentials(db_engine, username, password):
|
||||
async with db_engine as conn:
|
||||
where = sa.and_(db.users.c.login == username,
|
||||
sa.not_(db.users.c.disabled))
|
||||
query = db.users.select().where(where)
|
||||
ret = yield from conn.execute(query)
|
||||
user = yield from ret.fetchone()
|
||||
ret = await conn.execute(query)
|
||||
user = await ret.fetchone()
|
||||
if user is not None:
|
||||
hash = user[2]
|
||||
return sha256_crypt.verify(password, hash)
|
||||
@@ -203,8 +195,8 @@ function may do what you trying to accomplish::
|
||||
|
||||
Final step is to launch your application::
|
||||
|
||||
python demo/main.py
|
||||
python demo/database_auth/main.py
|
||||
|
||||
|
||||
Try to login with admin/moderator/user accounts (with *password* password)
|
||||
Try to login with admin/moderator/user accounts (with **password** password)
|
||||
and access **/public** or **/protected** endpoints.
|
||||
|
@@ -3,16 +3,9 @@ aiohttp_security
|
||||
|
||||
The library provides security for :ref:`aiohttp.web<aiohttp-web>`.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
``aiohttp_security`` is offered under the Apache 2 license.
|
||||
|
||||
Contents:
|
||||
Contents
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
@@ -23,7 +16,10 @@ Contents:
|
||||
example_db_auth
|
||||
glossary
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
``aiohttp_security`` is offered under the Apache 2 license.
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
@@ -21,7 +21,7 @@ Public API functions
|
||||
The action is performed by registered
|
||||
:meth:`AbstractIdentityPolicy.remember`.
|
||||
|
||||
Usually the *idenity* is stored in user cookies homehow for using by
|
||||
Usually the *identity* is stored in user cookies somehow for using by
|
||||
:func:`authorized_userid` and :func:`permits`.
|
||||
|
||||
:param request: :class:`aiohttp.web.Request` object.
|
||||
@@ -78,7 +78,7 @@ Public API functions
|
||||
|
||||
:param request: :class:`aiohttp.web.Request` object.
|
||||
|
||||
:param str permission: requested :term:`permission`.
|
||||
:param permission: Requested :term:`permission`. :class:`str` or :class:`enum.Enum` object.
|
||||
|
||||
:param context: additional object may be passed into
|
||||
:meth:`AbstractAuthorizationPolicy.permission`
|
||||
@@ -88,6 +88,33 @@ Public API functions
|
||||
``False`` otherwise.
|
||||
|
||||
|
||||
.. coroutinefunction:: is_anonymous(request)
|
||||
|
||||
Checks if user is anonymous user.
|
||||
|
||||
Return ``True`` if user is not remembered in request, otherwise returns ``False``.
|
||||
|
||||
:param request: :class:`aiohttp.web.Request` object.
|
||||
|
||||
|
||||
.. decorator:: login_required
|
||||
|
||||
Decorator for handlers that checks if user is authorized.
|
||||
|
||||
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized.
|
||||
|
||||
|
||||
.. decorator:: has_permission(permission)
|
||||
|
||||
Decorator for handlers that checks if user is authorized
|
||||
and has correct permission.
|
||||
|
||||
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized.
|
||||
Raises :class:`aiohttp.web.HTTPForbidden` if user is authorized but has no access rights.
|
||||
|
||||
:param str permission: requested :term:`permission`.
|
||||
|
||||
|
||||
.. function:: setup(app, identity_policy, autz_policy)
|
||||
|
||||
Setup :mod:`aiohttp` application with security policies.
|
||||
|
@@ -13,6 +13,10 @@ First of all, what is *aiohttp_security* about?
|
||||
|
||||
It is a set of public API functions and standard for implementation details.
|
||||
|
||||
|
||||
Public API
|
||||
==========
|
||||
|
||||
API is implementation agnostic, all client code should not call policy
|
||||
code (see below) directly but use API only.
|
||||
|
||||
@@ -27,9 +31,6 @@ base classes for both concepts as well as several implementations
|
||||
shipped with the library. End user is free to build own implemetations
|
||||
if needed.
|
||||
|
||||
Public API
|
||||
==========
|
||||
|
||||
|
||||
Authentication
|
||||
==============
|
||||
@@ -43,11 +44,6 @@ knowledge is there the user still registered in DB.
|
||||
If :class:`aiohttp.web.Request` has an :term:`identity` it means the user has
|
||||
some ID that should be checked by :term:`authorization` policy.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
identity is a string shared between browser and server.
|
||||
:term:`identity` is a string shared between browser and server.
|
||||
Thus it's not supposed to be database primary key, user login/email etc.
|
||||
Random string like uuid or hash is better choice.
|
||||
|
@@ -1,14 +1,15 @@
|
||||
-e .
|
||||
flake8==3.4.1
|
||||
pytest==3.2.3
|
||||
flake8==3.5.0
|
||||
pytest==3.4.2
|
||||
pytest-cov==2.5.1
|
||||
coverage==4.4.1
|
||||
sphinx==1.6.4
|
||||
coverage==4.5.1
|
||||
sphinx==1.7.1
|
||||
pep257==0.7.0
|
||||
aiohttp-session==1.0.1
|
||||
aiopg[sa]==0.13.1
|
||||
aioredis==0.3.3
|
||||
aiohttp-session==2.3.0
|
||||
aiopg[sa]==0.13.2
|
||||
aioredis==1.1.0
|
||||
hiredis==0.2.0
|
||||
passlib==1.7.1
|
||||
aiohttp==2.2.5
|
||||
pytest-aiohttp==0.1.3
|
||||
cryptography==2.2
|
||||
aiohttp==3.0.9
|
||||
pytest-aiohttp==0.3.0
|
||||
|
1
setup.py
1
setup.py
@@ -42,7 +42,6 @@ setup(name='aiohttp-security',
|
||||
'Intended Audience :: Developers',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import asyncio
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_security import (remember, forget,
|
||||
AbstractAuthorizationPolicy)
|
||||
@@ -10,47 +8,39 @@ from aiohttp_security.api import IDENTITY_KEY
|
||||
|
||||
class Autz(AbstractAuthorizationPolicy):
|
||||
|
||||
@asyncio.coroutine
|
||||
def permits(self, identity, permission, context=None):
|
||||
async def permits(self, identity, permission, context=None):
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
def authorized_userid(self, identity):
|
||||
async def authorized_userid(self, identity):
|
||||
pass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_remember(loop, test_client):
|
||||
async def test_remember(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def handler(request):
|
||||
async def handler(request):
|
||||
response = web.Response()
|
||||
yield from remember(request, response, 'Andrew')
|
||||
await remember(request, response, 'Andrew')
|
||||
return response
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||
app.router.add_route('GET', '/', handler)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.get('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.get('/')
|
||||
assert 200 == resp.status
|
||||
assert 'Andrew' == resp.cookies['AIOHTTP_SECURITY'].value
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_identify(loop, test_client):
|
||||
async def test_identify(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def create(request):
|
||||
async def create(request):
|
||||
response = web.Response()
|
||||
yield from remember(request, response, 'Andrew')
|
||||
await remember(request, response, 'Andrew')
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
async def check(request):
|
||||
policy = request.app[IDENTITY_KEY]
|
||||
user_id = yield from policy.identify(request)
|
||||
user_id = await policy.identify(request)
|
||||
assert 'Andrew' == user_id
|
||||
return web.Response()
|
||||
|
||||
@@ -58,32 +48,27 @@ def test_identify(loop, test_client):
|
||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||
app.router.add_route('GET', '/', check)
|
||||
app.router.add_route('POST', '/', create)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.post('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.post('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
resp = yield from client.get('/')
|
||||
await resp.release()
|
||||
resp = await client.get('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_forget(loop, test_client):
|
||||
async def test_forget(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def index(request):
|
||||
async def index(request):
|
||||
return web.Response()
|
||||
|
||||
@asyncio.coroutine
|
||||
def login(request):
|
||||
async def login(request):
|
||||
response = web.HTTPFound(location='/')
|
||||
yield from remember(request, response, 'Andrew')
|
||||
await remember(request, response, 'Andrew')
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def logout(request):
|
||||
async def logout(request):
|
||||
response = web.HTTPFound(location='/')
|
||||
yield from forget(request, response)
|
||||
await forget(request, response)
|
||||
return response
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
@@ -91,18 +76,17 @@ def test_forget(loop, test_client):
|
||||
app.router.add_route('GET', '/', index)
|
||||
app.router.add_route('POST', '/login', login)
|
||||
app.router.add_route('POST', '/logout', logout)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.post('/login')
|
||||
client = await test_client(app)
|
||||
resp = await client.post('/login')
|
||||
assert 200 == resp.status
|
||||
assert str(resp.url).endswith('/')
|
||||
cookies = client.session.cookie_jar.filter_cookies(
|
||||
client.make_url('/'))
|
||||
assert 'Andrew' == cookies['AIOHTTP_SECURITY'].value
|
||||
yield from resp.release()
|
||||
resp = yield from client.post('/logout')
|
||||
|
||||
resp = await client.post('/logout')
|
||||
assert 200 == resp.status
|
||||
assert str(resp.url).endswith('/')
|
||||
cookies = client.session.cookie_jar.filter_cookies(
|
||||
client.make_url('/'))
|
||||
assert 'AIOHTTP_SECURITY' not in cookies
|
||||
yield from resp.release()
|
||||
|
@@ -1,42 +1,37 @@
|
||||
import asyncio
|
||||
import enum
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_security import (remember,
|
||||
authorized_userid, permits,
|
||||
AbstractAuthorizationPolicy)
|
||||
from aiohttp_security import setup as _setup
|
||||
from aiohttp_security import (AbstractAuthorizationPolicy, authorized_userid,
|
||||
forget, has_permission, is_anonymous,
|
||||
login_required, permits, remember)
|
||||
from aiohttp_security.cookies_identity import CookiesIdentityPolicy
|
||||
|
||||
|
||||
class Autz(AbstractAuthorizationPolicy):
|
||||
|
||||
@asyncio.coroutine
|
||||
def permits(self, identity, permission, context=None):
|
||||
async def permits(self, identity, permission, context=None):
|
||||
if identity == 'UserID':
|
||||
return permission in {'read', 'write'}
|
||||
else:
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def authorized_userid(self, identity):
|
||||
async def authorized_userid(self, identity):
|
||||
if identity == 'UserID':
|
||||
return 'Andrew'
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_authorized_userid(loop, test_client):
|
||||
async def test_authorized_userid(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def login(request):
|
||||
async def login(request):
|
||||
response = web.HTTPFound(location='/')
|
||||
yield from remember(request, response, 'UserID')
|
||||
await remember(request, response, 'UserID')
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
userid = yield from authorized_userid(request)
|
||||
async def check(request):
|
||||
userid = await authorized_userid(request)
|
||||
assert 'Andrew' == userid
|
||||
return web.Response(text=userid)
|
||||
|
||||
@@ -44,50 +39,61 @@ def test_authorized_userid(loop, test_client):
|
||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||
app.router.add_route('GET', '/', check)
|
||||
app.router.add_route('POST', '/login', login)
|
||||
client = yield from test_client(app)
|
||||
client = await test_client(app)
|
||||
|
||||
resp = yield from client.post('/login')
|
||||
resp = await client.post('/login')
|
||||
assert 200 == resp.status
|
||||
txt = yield from resp.text()
|
||||
txt = await resp.text()
|
||||
assert 'Andrew' == txt
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_authorized_userid_not_authorized(loop, test_client):
|
||||
async def test_authorized_userid_not_authorized(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
userid = yield from authorized_userid(request)
|
||||
async def check(request):
|
||||
userid = await authorized_userid(request)
|
||||
assert userid is None
|
||||
return web.Response()
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||
app.router.add_route('GET', '/', check)
|
||||
client = yield from test_client(app)
|
||||
client = await test_client(app)
|
||||
|
||||
resp = yield from client.get('/')
|
||||
resp = await client.get('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_permits(loop, test_client):
|
||||
async def test_permits_enum_permission(loop, test_client):
|
||||
class Permission(enum.Enum):
|
||||
READ = '101'
|
||||
WRITE = '102'
|
||||
UNKNOWN = '103'
|
||||
|
||||
@asyncio.coroutine
|
||||
def login(request):
|
||||
class Autz(AbstractAuthorizationPolicy):
|
||||
|
||||
async def permits(self, identity, permission, context=None):
|
||||
if identity == 'UserID':
|
||||
return permission in {Permission.READ, Permission.WRITE}
|
||||
else:
|
||||
return False
|
||||
|
||||
async def authorized_userid(self, identity):
|
||||
if identity == 'UserID':
|
||||
return 'Andrew'
|
||||
else:
|
||||
return None
|
||||
|
||||
async def login(request):
|
||||
response = web.HTTPFound(location='/')
|
||||
yield from remember(request, response, 'UserID')
|
||||
await remember(request, response, 'UserID')
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
ret = yield from permits(request, 'read')
|
||||
async def check(request):
|
||||
ret = await permits(request, Permission.READ)
|
||||
assert ret
|
||||
ret = yield from permits(request, 'write')
|
||||
ret = await permits(request, Permission.WRITE)
|
||||
assert ret
|
||||
ret = yield from permits(request, 'unknown')
|
||||
ret = await permits(request, Permission.UNKNOWN)
|
||||
assert not ret
|
||||
return web.Response()
|
||||
|
||||
@@ -95,29 +101,151 @@ def test_permits(loop, test_client):
|
||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||
app.router.add_route('GET', '/', check)
|
||||
app.router.add_route('POST', '/login', login)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.post('/login')
|
||||
client = await test_client(app)
|
||||
resp = await client.post('/login')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_permits_unauthorized(loop, test_client):
|
||||
async def test_permits_unauthorized(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
ret = yield from permits(request, 'read')
|
||||
async def check(request):
|
||||
ret = await permits(request, 'read')
|
||||
assert not ret
|
||||
ret = yield from permits(request, 'write')
|
||||
ret = await permits(request, 'write')
|
||||
assert not ret
|
||||
ret = yield from permits(request, 'unknown')
|
||||
ret = await permits(request, 'unknown')
|
||||
assert not ret
|
||||
return web.Response()
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||
app.router.add_route('GET', '/', check)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.get('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.get('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
async def test_is_anonymous(loop, test_client):
|
||||
|
||||
async def index(request):
|
||||
is_anon = await is_anonymous(request)
|
||||
if is_anon:
|
||||
return web.HTTPUnauthorized()
|
||||
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('GET', '/', index)
|
||||
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_login_required(loop, test_client):
|
||||
@login_required
|
||||
async def index(request):
|
||||
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('GET', '/', index)
|
||||
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')
|
||||
async def index_read(request):
|
||||
return web.HTTPOk()
|
||||
|
||||
@has_permission('write')
|
||||
async def index_write(request):
|
||||
return web.HTTPOk()
|
||||
|
||||
@has_permission('forbid')
|
||||
async def index_forbid(request):
|
||||
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('GET', '/permission/read', index_read)
|
||||
app.router.add_route('GET', '/permission/write', index_write)
|
||||
app.router.add_route('GET', '/permission/forbid', index_forbid)
|
||||
app.router.add_route('POST', '/login', login)
|
||||
app.router.add_route('POST', '/logout', logout)
|
||||
client = await test_client(app)
|
||||
|
||||
resp = await client.get('/permission/read')
|
||||
assert web.HTTPUnauthorized.status_code == resp.status
|
||||
resp = await client.get('/permission/write')
|
||||
assert web.HTTPUnauthorized.status_code == resp.status
|
||||
resp = await client.get('/permission/forbid')
|
||||
assert web.HTTPUnauthorized.status_code == resp.status
|
||||
|
||||
await client.post('/login')
|
||||
resp = await client.get('/permission/read')
|
||||
assert web.HTTPOk.status_code == resp.status
|
||||
resp = await client.get('/permission/write')
|
||||
assert web.HTTPOk.status_code == resp.status
|
||||
resp = await client.get('/permission/forbid')
|
||||
assert web.HTTPForbidden.status_code == resp.status
|
||||
|
||||
await client.post('/logout')
|
||||
resp = await client.get('/permission/read')
|
||||
assert web.HTTPUnauthorized.status_code == resp.status
|
||||
resp = await client.get('/permission/write')
|
||||
assert web.HTTPUnauthorized.status_code == resp.status
|
||||
resp = await client.get('/permission/forbid')
|
||||
assert web.HTTPUnauthorized.status_code == resp.status
|
||||
|
@@ -1,42 +1,34 @@
|
||||
import asyncio
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_security import authorized_userid, permits
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_authorized_userid(loop, test_client):
|
||||
async def test_authorized_userid(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
userid = yield from authorized_userid(request)
|
||||
async def check(request):
|
||||
userid = await authorized_userid(request)
|
||||
assert userid is None
|
||||
return web.Response()
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
app.router.add_route('GET', '/', check)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.get('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.get('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_permits(loop, test_client):
|
||||
async def test_permits(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
ret = yield from permits(request, 'read')
|
||||
async def check(request):
|
||||
ret = await permits(request, 'read')
|
||||
assert ret
|
||||
ret = yield from permits(request, 'write')
|
||||
ret = await permits(request, 'write')
|
||||
assert ret
|
||||
ret = yield from permits(request, 'unknown')
|
||||
ret = await permits(request, 'unknown')
|
||||
assert ret
|
||||
return web.Response()
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
app.router.add_route('GET', '/', check)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.get('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.get('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
@@ -1,42 +1,34 @@
|
||||
import asyncio
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_security import remember, forget
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_remember(loop, test_client):
|
||||
async def test_remember(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def do_remember(request):
|
||||
async def do_remember(request):
|
||||
response = web.Response()
|
||||
yield from remember(request, response, 'Andrew')
|
||||
await remember(request, response, 'Andrew')
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
app.router.add_route('POST', '/', do_remember)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.post('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.post('/')
|
||||
assert 500 == resp.status
|
||||
assert (('Security subsystem is not initialized, '
|
||||
'call aiohttp_security.setup(...) first') ==
|
||||
resp.reason)
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_forget(loop, test_client):
|
||||
async def test_forget(loop, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def do_forget(request):
|
||||
async def do_forget(request):
|
||||
response = web.Response()
|
||||
yield from forget(request, response)
|
||||
await forget(request, response)
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
app.router.add_route('POST', '/', do_forget)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.post('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.post('/')
|
||||
assert 500 == resp.status
|
||||
assert (('Security subsystem is not initialized, '
|
||||
'call aiohttp_security.setup(...) first') ==
|
||||
resp.reason)
|
||||
yield from resp.release()
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from aiohttp import web
|
||||
@@ -13,12 +12,10 @@ from aiohttp_session import setup as setup_session
|
||||
|
||||
class Autz(AbstractAuthorizationPolicy):
|
||||
|
||||
@asyncio.coroutine
|
||||
def permits(self, identity, permission, context=None):
|
||||
async def permits(self, identity, permission, context=None):
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
def authorized_userid(self, identity):
|
||||
async def authorized_userid(self, identity):
|
||||
pass
|
||||
|
||||
|
||||
@@ -30,81 +27,67 @@ def make_app(loop):
|
||||
return app
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_remember(make_app, test_client):
|
||||
async def test_remember(make_app, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def handler(request):
|
||||
async def handler(request):
|
||||
response = web.Response()
|
||||
yield from remember(request, response, 'Andrew')
|
||||
await remember(request, response, 'Andrew')
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
session = yield from get_session(request)
|
||||
async def check(request):
|
||||
session = await get_session(request)
|
||||
assert session['AIOHTTP_SECURITY'] == 'Andrew'
|
||||
return web.HTTPOk()
|
||||
|
||||
app = make_app()
|
||||
app.router.add_route('GET', '/', handler)
|
||||
app.router.add_route('GET', '/check', check)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.get('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.get('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
resp = yield from client.get('/check')
|
||||
resp = await client.get('/check')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_identify(make_app, test_client):
|
||||
async def test_identify(make_app, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def create(request):
|
||||
async def create(request):
|
||||
response = web.Response()
|
||||
yield from remember(request, response, 'Andrew')
|
||||
await remember(request, response, 'Andrew')
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def check(request):
|
||||
async def check(request):
|
||||
policy = request.app[IDENTITY_KEY]
|
||||
user_id = yield from policy.identify(request)
|
||||
user_id = await policy.identify(request)
|
||||
assert 'Andrew' == user_id
|
||||
return web.Response()
|
||||
|
||||
app = make_app()
|
||||
app.router.add_route('GET', '/', check)
|
||||
app.router.add_route('POST', '/', create)
|
||||
client = yield from test_client(app)
|
||||
resp = yield from client.post('/')
|
||||
client = await test_client(app)
|
||||
resp = await client.post('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
resp = yield from client.get('/')
|
||||
resp = await client.get('/')
|
||||
assert 200 == resp.status
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_forget(make_app, test_client):
|
||||
async def test_forget(make_app, test_client):
|
||||
|
||||
@asyncio.coroutine
|
||||
def index(request):
|
||||
session = yield from get_session(request)
|
||||
async def index(request):
|
||||
session = await get_session(request)
|
||||
return web.HTTPOk(text=session.get('AIOHTTP_SECURITY', ''))
|
||||
|
||||
@asyncio.coroutine
|
||||
def login(request):
|
||||
async def login(request):
|
||||
response = web.HTTPFound(location='/')
|
||||
yield from remember(request, response, 'Andrew')
|
||||
await remember(request, response, 'Andrew')
|
||||
return response
|
||||
|
||||
@asyncio.coroutine
|
||||
def logout(request):
|
||||
async def logout(request):
|
||||
response = web.HTTPFound('/')
|
||||
yield from forget(request, response)
|
||||
await forget(request, response)
|
||||
return response
|
||||
|
||||
app = make_app()
|
||||
@@ -112,18 +95,16 @@ def test_forget(make_app, test_client):
|
||||
app.router.add_route('POST', '/login', login)
|
||||
app.router.add_route('POST', '/logout', logout)
|
||||
|
||||
client = yield from test_client(app)
|
||||
client = await test_client(app)
|
||||
|
||||
resp = yield from client.post('/login')
|
||||
resp = await client.post('/login')
|
||||
assert 200 == resp.status
|
||||
assert str(resp.url).endswith('/')
|
||||
txt = yield from resp.text()
|
||||
txt = await resp.text()
|
||||
assert 'Andrew' == txt
|
||||
yield from resp.release()
|
||||
|
||||
resp = yield from client.post('/logout')
|
||||
resp = await client.post('/logout')
|
||||
assert 200 == resp.status
|
||||
assert str(resp.url).endswith('/')
|
||||
txt = yield from resp.text()
|
||||
txt = await resp.text()
|
||||
assert '' == txt
|
||||
yield from resp.release()
|
||||
|
Reference in New Issue
Block a user