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:
committed by
Andrew Svetlov
parent
fec22f971c
commit
820dcc8d93
@@ -1,8 +1,11 @@
|
||||
.. _aiohttp-security-example:
|
||||
|
||||
===============================================
|
||||
How to Make a Simple Server With Authorization
|
||||
==============================================
|
||||
===============================================
|
||||
|
||||
|
||||
.. code::python
|
||||
Simple example::
|
||||
|
||||
import asyncio
|
||||
from aiohttp import web
|
||||
@@ -13,7 +16,7 @@ How to Make a Simple Server With Authorization
|
||||
return web.Response(body=text.encode('utf-8'))
|
||||
|
||||
# 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')
|
||||
@asyncio.coroutine
|
||||
def user_handler(request):
|
||||
|
210
docs/example_db_auth.rst
Normal file
210
docs/example_db_auth.rst
Normal file
@@ -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
|
||||
reference
|
||||
example
|
||||
example_db_auth
|
||||
glossary
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user