Create working demo (#5)
* Add docs example and update demo source code * Fail with debug * Fix tests * Update sql scripts * Update docs * Add checking for the password * Update documentation with launch/usage example * Launch flake8 instead of pep8 and pyflakes
This commit is contained in:
parent
fec22f971c
commit
820dcc8d93
|
@ -9,8 +9,7 @@ install:
|
||||||
- pip install coveralls
|
- pip install coveralls
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- pyflakes aiohttp_security tests
|
- flake8 aiohttp_security tests
|
||||||
- pep8 aiohttp_security tests
|
|
||||||
- coverage run --source=aiohttp_security setup.py test
|
- coverage run --source=aiohttp_security setup.py test
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
|
|
|
@ -9,6 +9,14 @@ __ aiohttp_web_
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
To install type ``pip install aiohttp_security``.
|
||||||
|
Launch ``make doc`` and see examples or look under **demo** directory for a
|
||||||
|
sample project.
|
||||||
|
|
||||||
|
Develop
|
||||||
|
-------
|
||||||
|
|
||||||
|
``pip install -r requirements-dev``
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Identity polocy for storing info directly into HTTP cookie.
|
"""Identity policy for storing info directly into HTTP cookie.
|
||||||
|
|
||||||
Use mostly for demonstration purposes, SessionIdentityPolicy is much
|
Use mostly for demonstration purposes, SessionIdentityPolicy is much
|
||||||
more handy.
|
more handy.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Identity policy for storing info into aiohttp_session session.
|
"""Identity policy for storing info into aiohttp_session session.
|
||||||
|
|
||||||
aiohttp_session.setup() should be called on application initialization
|
aiohttp_session.setup() should be called on application initialization
|
||||||
to conffigure aiohttp_session properly.
|
to configure aiohttp_session properly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
|
@ -9,7 +9,6 @@ users = sa.Table(
|
||||||
sa.Column('id', sa.Integer, nullable=False),
|
sa.Column('id', sa.Integer, nullable=False),
|
||||||
sa.Column('login', sa.String(256), nullable=False),
|
sa.Column('login', sa.String(256), nullable=False),
|
||||||
sa.Column('passwd', sa.String(256), nullable=False),
|
sa.Column('passwd', sa.String(256), nullable=False),
|
||||||
sa.Column('salt', sa.String(256), nullable=False),
|
|
||||||
sa.Column('is_superuser', sa.Boolean, nullable=False,
|
sa.Column('is_superuser', sa.Boolean, nullable=False,
|
||||||
server_default='FALSE'),
|
server_default='FALSE'),
|
||||||
sa.Column('disabled', sa.Boolean, nullable=False,
|
sa.Column('disabled', sa.Boolean, nullable=False,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
||||||
|
from passlib.hash import sha256_crypt
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
|
|
||||||
|
@ -11,11 +13,11 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
self.dbengine = dbengine
|
self.dbengine = dbengine
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def authorized_user_id(self, identity):
|
def authorized_userid(self, identity):
|
||||||
with (yield from self.dbengine) as conn:
|
with (yield from self.dbengine) as conn:
|
||||||
where = [db.users.c.login == identity,
|
where = sa.and_(db.users.c.login == identity,
|
||||||
not db.users.c.disabled]
|
sa.not_(db.users.c.disabled))
|
||||||
query = db.users.count().where(sa.and_(*where))
|
query = db.users.count().where(where)
|
||||||
ret = yield from conn.scalar(query)
|
ret = yield from conn.scalar(query)
|
||||||
if ret:
|
if ret:
|
||||||
return identity
|
return identity
|
||||||
|
@ -24,12 +26,42 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def permits(self, identity, permission, context=None):
|
def permits(self, identity, permission, context=None):
|
||||||
|
if identity is None:
|
||||||
|
return False
|
||||||
|
|
||||||
with (yield from self.dbengine) as conn:
|
with (yield from self.dbengine) as conn:
|
||||||
where = [db.users.c.login == identity,
|
where = sa.and_(db.users.c.login == identity,
|
||||||
not db.users.c.disabled]
|
sa.not_(db.users.c.disabled))
|
||||||
record = self.data.get(identity)
|
query = db.users.select().where(where)
|
||||||
if record is not None:
|
ret = yield from conn.execute(query)
|
||||||
# TODO: implement actual permission checker
|
user = yield from ret.fetchone()
|
||||||
if permission in record:
|
if user is not None:
|
||||||
return True
|
user_id = user[0]
|
||||||
return False
|
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()
|
||||||
|
if ret is not None:
|
||||||
|
for record in result:
|
||||||
|
if record.perm_name == permission:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def check_credentials(db_engine, username, password):
|
||||||
|
with (yield from 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()
|
||||||
|
if user is not None:
|
||||||
|
hash = user[2]
|
||||||
|
return sha256_crypt.verify(password, hash)
|
||||||
|
return False
|
||||||
|
|
|
@ -3,53 +3,93 @@ import functools
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
|
||||||
from aiohttp_security import remember, forget, authorized_userid, permits
|
from aiohttp_security import remember, forget, authorized_userid, permits
|
||||||
|
|
||||||
|
from .db_auth import check_credentials
|
||||||
|
|
||||||
|
|
||||||
def require(permission):
|
def require(permission):
|
||||||
def wrapper(f):
|
def wrapper(f):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def wrapped(self, request):
|
def wrapped(self, request):
|
||||||
has_perm = yield from permits(request)
|
has_perm = yield from permits(request, permission)
|
||||||
if not has_perm:
|
if not has_perm:
|
||||||
raise web.HTTPForbidden()
|
message = 'User has no permission {}'.format(permission)
|
||||||
|
raise web.HTTPForbidden(body=message.encode())
|
||||||
return (yield from f(self, request))
|
return (yield from f(self, request))
|
||||||
return wrapped
|
return wrapped
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class Web:
|
class Web(object):
|
||||||
@require('public')
|
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>
|
||||||
|
"""
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def index(self, request):
|
def index(self, request):
|
||||||
pass
|
username = yield from authorized_userid(request)
|
||||||
|
if username:
|
||||||
|
template = self.index_template.format(
|
||||||
|
message='Hello, {username}!'.format(username=username))
|
||||||
|
else:
|
||||||
|
template = self.index_template.format(message='You need to login')
|
||||||
|
response = web.Response(body=template.encode())
|
||||||
|
return response
|
||||||
|
|
||||||
@require('public')
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def login(self, request):
|
def login(self, request):
|
||||||
pass
|
response = web.HTTPFound('/')
|
||||||
|
form = yield from 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)
|
||||||
|
return response
|
||||||
|
|
||||||
@require('protected')
|
return web.HTTPUnauthorized(
|
||||||
@asyncio.coroutine
|
body=b'Invalid username/password combination')
|
||||||
def logout(self, request):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@require('public')
|
@require('public')
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def public(self, request):
|
def logout(self, request):
|
||||||
pass
|
response = web.Response(body=b'You have been logged out')
|
||||||
|
yield from forget(request, response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@require('public')
|
||||||
|
@asyncio.coroutine
|
||||||
|
def internal_page(self, request):
|
||||||
|
response = web.Response(
|
||||||
|
body=b'This page is visible for all registered users')
|
||||||
|
return response
|
||||||
|
|
||||||
@require('protected')
|
@require('protected')
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def protected(self, request):
|
def protected_page(self, request):
|
||||||
pass
|
response = web.Response(body=b'You are on protected page')
|
||||||
|
return response
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def configure(self, app):
|
def configure(self, app):
|
||||||
app.add_route('GET', '/', self.index, name='index')
|
router = app.router
|
||||||
app.add_route('POST', '/login', self.login, name='login')
|
router.add_route('GET', '/', self.index, name='index')
|
||||||
app.add_route('POST', '/logout', self.logout, name='logout')
|
router.add_route('POST', '/login', self.login, name='login')
|
||||||
app.add_route('GET', '/public', self.public, name='public')
|
router.add_route('GET', '/logout', self.logout, name='logout')
|
||||||
app.add_route('GET', '/protected', self.protected, name='protected')
|
router.add_route('GET', '/public', self.internal_page, name='public')
|
||||||
|
router.add_route('GET', '/protected', self.protected_page,
|
||||||
|
name='protected')
|
||||||
|
|
19
demo/main.py
19
demo/main.py
|
@ -16,22 +16,23 @@ from demo.handlers import Web
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def init(loop):
|
def init(loop):
|
||||||
redis_pool = yield from create_pool(('localhost', 6379))
|
redis_pool = yield from create_pool(('localhost', 6379))
|
||||||
dbengine = yield from create_engine(user='aiohttp_security',
|
db_engine = yield from create_engine(user='aiohttp_security',
|
||||||
password='aiohttp_security',
|
password='aiohttp_security',
|
||||||
database='aiohttp_security',
|
database='aiohttp_security',
|
||||||
host='127.0.0.1')
|
host='127.0.0.1')
|
||||||
app = web.Application(loop=loop)
|
app = web.Application(loop=loop)
|
||||||
|
app.db_engine = db_engine
|
||||||
setup_session(app, RedisStorage(redis_pool))
|
setup_session(app, RedisStorage(redis_pool))
|
||||||
setup_security(app,
|
setup_security(app,
|
||||||
SessionIdentityPolicy(),
|
SessionIdentityPolicy(),
|
||||||
DBAuthorizationPolicy(dbengine))
|
DBAuthorizationPolicy(db_engine))
|
||||||
|
|
||||||
web_handlers = Web()
|
web_handlers = Web()
|
||||||
yield from web_handlers.configure(app)
|
web_handlers.configure(app)
|
||||||
|
|
||||||
handler = app.make_handler()
|
handler = app.make_handler()
|
||||||
srv = yield from loop.create_server(handler, '127.0.0.1', 8080)
|
srv = yield from loop.create_server(handler, '127.0.0.1', 8080)
|
||||||
print("Server started at http://127.0.0.1:8080")
|
print('Server started at http://127.0.0.1:8080')
|
||||||
return srv, app, handler
|
return srv, app, handler
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,3 +55,7 @@ def main():
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
loop.run_until_complete((finalize(srv, app, handler)))
|
loop.run_until_complete((finalize(srv, app, handler)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE USER aiohttp_security WITH PASSWORD 'aiohttp_security';
|
||||||
|
DROP DATABASE IF EXISTS aiohttp_security;
|
||||||
|
CREATE DATABASE aiohttp_security;
|
||||||
|
ALTER DATABASE aiohttp_security OWNER TO aiohttp_security;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE aiohttp_security TO aiohttp_security;
|
|
@ -0,0 +1,38 @@
|
||||||
|
-- create users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users
|
||||||
|
(
|
||||||
|
id integer NOT NULL,
|
||||||
|
login character varying(256) NOT NULL,
|
||||||
|
passwd character varying(256) NOT NULL,
|
||||||
|
is_superuser boolean NOT NULL DEFAULT false,
|
||||||
|
disabled boolean NOT NULL DEFAULT false,
|
||||||
|
CONSTRAINT user_pkey PRIMARY KEY (id),
|
||||||
|
CONSTRAINT user_login_key UNIQUE (login)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- and permissions for them
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions
|
||||||
|
(
|
||||||
|
id integer NOT NULL,
|
||||||
|
user_id integer NOT NULL,
|
||||||
|
perm_name character varying(64) NOT NULL,
|
||||||
|
CONSTRAINT permission_pkey PRIMARY KEY (id),
|
||||||
|
CONSTRAINT user_permission_fkey FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users (id) MATCH SIMPLE
|
||||||
|
ON UPDATE NO ACTION ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- insert some data
|
||||||
|
INSERT INTO users(id, login, passwd, is_superuser, disabled)
|
||||||
|
VALUES (1, 'admin', '$5$rounds=535000$2kqN9fxCY6Xt5/pi$tVnh0xX87g/IsnOSuorZG608CZDFbWIWBr58ay6S4pD', TRUE, FALSE);
|
||||||
|
INSERT INTO users(id, login, passwd, is_superuser, disabled)
|
||||||
|
VALUES (2, 'moderator', '$5$rounds=535000$2kqN9fxCY6Xt5/pi$tVnh0xX87g/IsnOSuorZG608CZDFbWIWBr58ay6S4pD', FALSE, FALSE);
|
||||||
|
INSERT INTO users(id, login, passwd, is_superuser, disabled)
|
||||||
|
VALUES (3, 'user', '$5$rounds=535000$2kqN9fxCY6Xt5/pi$tVnh0xX87g/IsnOSuorZG608CZDFbWIWBr58ay6S4pD', FALSE, FALSE);
|
||||||
|
|
||||||
|
INSERT INTO permissions(id, user_id, perm_name)
|
||||||
|
VALUES (1, 2, 'protected');
|
||||||
|
INSERT INTO permissions(id, user_id, perm_name)
|
||||||
|
VALUES (2, 2, 'public');
|
||||||
|
INSERT INTO permissions(id, user_id, perm_name)
|
||||||
|
VALUES (3, 3, 'public');
|
|
@ -1,8 +1,11 @@
|
||||||
|
.. _aiohttp-security-example:
|
||||||
|
|
||||||
|
===============================================
|
||||||
How to Make a Simple Server With Authorization
|
How to Make a Simple Server With Authorization
|
||||||
==============================================
|
===============================================
|
||||||
|
|
||||||
|
|
||||||
.. code::python
|
Simple example::
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
@ -13,7 +16,7 @@ How to Make a Simple Server With Authorization
|
||||||
return web.Response(body=text.encode('utf-8'))
|
return web.Response(body=text.encode('utf-8'))
|
||||||
|
|
||||||
# option 2: auth at a higher level?
|
# option 2: auth at a higher level?
|
||||||
# set user_id and allowed in the wsgo handler
|
# set user_id and allowed in the wsgi handler
|
||||||
@protect('view_user')
|
@protect('view_user')
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def user_handler(request):
|
def user_handler(request):
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
.. _aiohttp-security-example-db-auth:
|
||||||
|
|
||||||
|
===========================================
|
||||||
|
Permissions with PostgreSQL-based storage
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
Make sure that you have PostgreSQL and Redis servers up and running.
|
||||||
|
If you want the full source code in advance or for comparison, check out
|
||||||
|
the `demo source`_.
|
||||||
|
|
||||||
|
.. _demo source:
|
||||||
|
https://github.com/aio-libs/aiohttp_security/tree/master/demo
|
||||||
|
|
||||||
|
.. _passlib:
|
||||||
|
https://pythonhosted.org/passlib/
|
||||||
|
|
||||||
|
Database
|
||||||
|
--------
|
||||||
|
|
||||||
|
Launch these sql scripts to init database and fill it with sample data:
|
||||||
|
|
||||||
|
``psql template1 < demo/sql/init_db.sql``
|
||||||
|
|
||||||
|
and then
|
||||||
|
|
||||||
|
``psql template1 < demo/sql/sample_data.sql``
|
||||||
|
|
||||||
|
|
||||||
|
You will have two tables for storing users and their permissions
|
||||||
|
|
||||||
|
+--------------+
|
||||||
|
| users |
|
||||||
|
+==============+
|
||||||
|
| id |
|
||||||
|
+--------------+
|
||||||
|
| login |
|
||||||
|
+--------------+
|
||||||
|
| passwd |
|
||||||
|
+--------------+
|
||||||
|
| is_superuser |
|
||||||
|
+--------------+
|
||||||
|
| disabled |
|
||||||
|
+--------------+
|
||||||
|
|
||||||
|
and second table is permissions table:
|
||||||
|
|
||||||
|
+-----------------+
|
||||||
|
| permissions |
|
||||||
|
+=================+
|
||||||
|
| id |
|
||||||
|
+-----------------+
|
||||||
|
| user_id |
|
||||||
|
+-----------------+
|
||||||
|
| permission_name |
|
||||||
|
+-----------------+
|
||||||
|
|
||||||
|
|
||||||
|
Writing policies
|
||||||
|
----------------
|
||||||
|
|
||||||
|
You need to implement two entities: *IdentityPolicy* and *AuthorizationPolicy*.
|
||||||
|
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
|
||||||
|
this identity::
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def authorized_userid(self, identity):
|
||||||
|
with (yield from 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)
|
||||||
|
if ret:
|
||||||
|
return identity
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
For permission check 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):
|
||||||
|
if identity is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
with (yield from 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()
|
||||||
|
if user is not None:
|
||||||
|
user_id = user[0]
|
||||||
|
is_superuser = user[4]
|
||||||
|
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()
|
||||||
|
if ret is not None:
|
||||||
|
for record in result:
|
||||||
|
if record.perm_name == permission:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
Setup
|
||||||
|
-----
|
||||||
|
|
||||||
|
Once we have all the code in place we can install it for our application::
|
||||||
|
|
||||||
|
from aiohttp_session.redis_storage import RedisStorage
|
||||||
|
from aiohttp_security import setup as setup_security
|
||||||
|
from aiohttp_security import SessionIdentityPolicy
|
||||||
|
from aiopg.sa import create_engine
|
||||||
|
from aioredis import create_pool
|
||||||
|
|
||||||
|
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')
|
||||||
|
app = web.Application(loop=loop)
|
||||||
|
setup_session(app, RedisStorage(redis_pool))
|
||||||
|
setup_security(app,
|
||||||
|
SessionIdentityPolicy(),
|
||||||
|
DBAuthorizationPolicy(dbengine))
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
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::
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
For each view you need to protect just apply the decorator on it::
|
||||||
|
|
||||||
|
class Web:
|
||||||
|
@require('protected')
|
||||||
|
@asyncio.coroutine
|
||||||
|
def protected_page(self, request):
|
||||||
|
response = web.Response(body=b'You are on protected page')
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
If someone will try to access this protected page he will see::
|
||||||
|
|
||||||
|
403, User has no permission "protected"
|
||||||
|
|
||||||
|
|
||||||
|
The best part about it is that you can implement any logic you want until it
|
||||||
|
follows the API conventions.
|
||||||
|
|
||||||
|
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::
|
||||||
|
|
||||||
|
from passlib.hash import sha256_crypt
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def check_credentials(db_engine, username, password):
|
||||||
|
with (yield from 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()
|
||||||
|
if user is not None:
|
||||||
|
hash = user[2]
|
||||||
|
return sha256_crypt.verify(password, hash)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
Final step is to launch your application::
|
||||||
|
|
||||||
|
python demo/main.py
|
||||||
|
|
||||||
|
|
||||||
|
Try to login with admin/moderator/user accounts (with *password* password)
|
||||||
|
and access **/public** or **/protected** endpoints.
|
|
@ -20,6 +20,7 @@ Contents:
|
||||||
usage
|
usage
|
||||||
reference
|
reference
|
||||||
example
|
example
|
||||||
|
example_db_auth
|
||||||
glossary
|
glossary
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,6 @@ alabaster>=0.6.2
|
||||||
pep257
|
pep257
|
||||||
aiohttp_session>=0.4.0
|
aiohttp_session>=0.4.0
|
||||||
aiopg[sa]
|
aiopg[sa]
|
||||||
|
aioredis==0.2.8
|
||||||
|
hiredis==0.2.0
|
||||||
|
passlib==1.6.5
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import aiohttp
|
|
||||||
import asyncio
|
|
||||||
import gc
|
import gc
|
||||||
import pytest
|
|
||||||
import socket
|
import socket
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,7 +81,8 @@ class Client:
|
||||||
while path.startswith('/'):
|
while path.startswith('/'):
|
||||||
path = path[1:]
|
path = path[1:]
|
||||||
url = self._url + path
|
url = self._url + path
|
||||||
return self._session.get(url, **kwargs)
|
resp = self._session.get(url, **kwargs)
|
||||||
|
return resp
|
||||||
|
|
||||||
def post(self, path, **kwargs):
|
def post(self, path, **kwargs):
|
||||||
while path.startswith('/'):
|
while path.startswith('/'):
|
||||||
|
@ -97,6 +100,7 @@ class Client:
|
||||||
@pytest.yield_fixture
|
@pytest.yield_fixture
|
||||||
def create_app_and_client(create_server, loop):
|
def create_app_and_client(create_server, loop):
|
||||||
client = None
|
client = None
|
||||||
|
cookie_jar = aiohttp.CookieJar(loop=loop, unsafe=True)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def maker(*, server_params=None, client_params=None):
|
def maker(*, server_params=None, client_params=None):
|
||||||
|
@ -108,7 +112,11 @@ def create_app_and_client(create_server, loop):
|
||||||
app, url = yield from create_server(**server_params)
|
app, url = yield from create_server(**server_params)
|
||||||
if client_params is None:
|
if client_params is None:
|
||||||
client_params = {}
|
client_params = {}
|
||||||
client = Client(aiohttp.ClientSession(loop=loop, **client_params), url)
|
|
||||||
|
client = Client(
|
||||||
|
aiohttp.ClientSession(loop=loop, cookie_jar=cookie_jar),
|
||||||
|
url
|
||||||
|
)
|
||||||
return app, client
|
return app, client
|
||||||
|
|
||||||
yield maker
|
yield maker
|
||||||
|
|
|
@ -34,7 +34,7 @@ def test_remember(create_app_and_client):
|
||||||
app.router.add_route('GET', '/', handler)
|
app.router.add_route('GET', '/', handler)
|
||||||
resp = yield from client.get('/')
|
resp = yield from client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert 'Andrew' == client.cookies['AIOHTTP_SECURITY'].value
|
assert 'Andrew' == resp.cookies['AIOHTTP_SECURITY'].value
|
||||||
yield from resp.release()
|
yield from resp.release()
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,5 +98,6 @@ def test_forget(create_app_and_client):
|
||||||
resp = yield from client.post('/logout')
|
resp = yield from client.post('/logout')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert resp.url.endswith('/')
|
assert resp.url.endswith('/')
|
||||||
assert '' == client.cookies['AIOHTTP_SECURITY'].value
|
with pytest.raises(KeyError):
|
||||||
|
_ = client.cookies['AIOHTTP_SECURITY'] # noqa
|
||||||
yield from resp.release()
|
yield from resp.release()
|
||||||
|
|
|
@ -7,8 +7,8 @@ from aiohttp_security import (remember, forget,
|
||||||
from aiohttp_security import setup as setup_security
|
from aiohttp_security import setup as setup_security
|
||||||
from aiohttp_security.session_identity import SessionIdentityPolicy
|
from aiohttp_security.session_identity import SessionIdentityPolicy
|
||||||
from aiohttp_security.api import IDENTITY_KEY
|
from aiohttp_security.api import IDENTITY_KEY
|
||||||
from aiohttp_session import (SimpleCookieStorage, session_middleware,
|
from aiohttp_session import SimpleCookieStorage, get_session
|
||||||
get_session)
|
from aiohttp_session import setup as setup_session
|
||||||
|
|
||||||
|
|
||||||
class Autz(AbstractAuthorizationPolicy):
|
class Autz(AbstractAuthorizationPolicy):
|
||||||
|
@ -27,7 +27,7 @@ def create_app_and_client2(create_app_and_client):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def maker(*args, **kwargs):
|
def maker(*args, **kwargs):
|
||||||
app, client = yield from create_app_and_client(*args, **kwargs)
|
app, client = yield from create_app_and_client(*args, **kwargs)
|
||||||
app.middlewares.append(session_middleware(SimpleCookieStorage()))
|
setup_session(app, SimpleCookieStorage())
|
||||||
setup_security(app, SessionIdentityPolicy(), Autz())
|
setup_security(app, SessionIdentityPolicy(), Autz())
|
||||||
return app, client
|
return app, client
|
||||||
return maker
|
return maker
|
||||||
|
@ -82,6 +82,7 @@ def test_identify(create_app_and_client2):
|
||||||
resp = yield from client.post('/')
|
resp = yield from client.post('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
yield from resp.release()
|
||||||
|
|
||||||
resp = yield from client.get('/')
|
resp = yield from client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
yield from resp.release()
|
yield from resp.release()
|
||||||
|
@ -103,7 +104,7 @@ def test_forget(create_app_and_client2):
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def logout(request):
|
def logout(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound('/')
|
||||||
yield from forget(request, response)
|
yield from forget(request, response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -111,12 +112,14 @@ def test_forget(create_app_and_client2):
|
||||||
app.router.add_route('GET', '/', index)
|
app.router.add_route('GET', '/', index)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
app.router.add_route('POST', '/logout', logout)
|
app.router.add_route('POST', '/logout', logout)
|
||||||
|
|
||||||
resp = yield from client.post('/login')
|
resp = yield from client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert resp.url.endswith('/')
|
assert resp.url.endswith('/')
|
||||||
txt = yield from resp.text()
|
txt = yield from resp.text()
|
||||||
assert 'Andrew' == txt
|
assert 'Andrew' == txt
|
||||||
yield from resp.release()
|
yield from resp.release()
|
||||||
|
|
||||||
resp = yield from client.post('/logout')
|
resp = yield from client.post('/logout')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert resp.url.endswith('/')
|
assert resp.url.endswith('/')
|
||||||
|
|
Loading…
Reference in New Issue