Update docs and demo with login required, has permission (#128)

* Work on

* Update docs with login_required and has_permission
This commit is contained in:
Eduard Nabokov 2018-01-24 12:29:20 +02:00 committed by Andrew Svetlov
parent f9628b0ac1
commit 1679f6713b
6 changed files with 84 additions and 108 deletions

View File

@ -1,42 +1,31 @@
import functools from textwrap import dedent
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,
has_permission, login_required,
)
from .db_auth import check_credentials from .db_auth import check_credentials
def require(permission):
def wrapper(f):
@functools.wraps(f)
async def wrapped(self, request):
has_perm = await permits(request, permission)
if not has_perm:
message = 'User has no permission {}'.format(permission)
raise web.HTTPForbidden(body=message.encode())
return await f(self, request)
return wrapped
return wrapper
class Web(object): class Web(object):
index_template = """ index_template = dedent("""
<!doctype html> <!doctype html>
<head> <head></head>
</head> <body>
<body> <p>{message}</p>
<p>{message}</p> <form action="/login" method="post">
<form action="/login" method="post"> Login:
Login: <input type="text" name="login">
<input type="text" name="login"> Password:
Password: <input type="password" name="password">
<input type="password" name="password"> <input type="submit" value="Login">
<input type="submit" value="Login"> </form>
</form> <a href="/logout">Logout</a>
<a href="/logout">Logout</a> </body>
</body> """)
"""
async def index(self, request): async def index(self, request):
username = await authorized_userid(request) username = await authorized_userid(request)
@ -61,19 +50,19 @@ class Web(object):
return web.HTTPUnauthorized( return web.HTTPUnauthorized(
body=b'Invalid username/password combination') body=b'Invalid username/password combination')
@require('public') @login_required
async def logout(self, request): async def logout(self, request):
response = web.Response(body=b'You have been logged out') response = web.Response(body=b'You have been logged out')
await forget(request, response) await forget(request, response)
return response return response
@require('public') @has_permission('public')
async def internal_page(self, request): async def internal_page(self, request):
response = web.Response( response = web.Response(
body=b'This page is visible for all registered users') body=b'This page is visible for all registered users')
return response return response
@require('protected') @has_permission('protected')
async def protected_page(self, request): async def protected_page(self, request):
response = web.Response(body=b'You are on protected page') response = web.Response(body=b'You are on protected page')
return response return response

View File

@ -9,16 +9,16 @@ from aiopg.sa import create_engine
from aioredis import create_pool from aioredis import create_pool
from demo.db_auth import DBAuthorizationPolicy from demo.database_auth.db_auth import DBAuthorizationPolicy
from demo.handlers import Web from demo.database_auth.handlers import Web
async def init(loop): async def init(loop):
redis_pool = await create_pool(('localhost', 6379)) redis_pool = await create_pool(('localhost', 6379))
db_engine = await create_engine(user='aiohttp_security', db_engine = await 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 app.db_engine = db_engine
setup_session(app, RedisStorage(redis_pool)) setup_session(app, RedisStorage(redis_pool))

View File

@ -1,42 +1,30 @@
import functools
from textwrap import dedent from textwrap import dedent
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,
has_permission, login_required,
)
from .authz import check_credentials from .authz import check_credentials
def require(permission):
def wrapper(f):
@functools.wraps(f)
async def wrapped(request):
has_perm = await permits(request, permission)
if not has_perm:
message = 'User has no permission {}'.format(permission)
raise web.HTTPForbidden(body=message.encode())
return await f(request)
return wrapped
return wrapper
index_template = dedent(""" index_template = dedent("""
<!doctype html> <!doctype html>
<head> <head></head>
</head> <body>
<body> <p>{message}</p>
<p>{message}</p> <form action="/login" method="post">
<form action="/login" method="post"> Login:
Login: <input type="text" name="username">
<input type="text" name="username"> Password:
Password: <input type="password" name="password">
<input type="password" name="password"> <input type="submit" value="Login">
<input type="submit" value="Login"> </form>
</form> <a href="/logout">Logout</a>
<a href="/logout">Logout</a> </body>
</body> """)
""")
async def index(request): async def index(request):
@ -58,7 +46,8 @@ async def login(request):
username = form.get('username') username = form.get('username')
password = form.get('password') 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: if verified:
await remember(request, response, username) await remember(request, response, username)
return response return response
@ -66,7 +55,7 @@ async def login(request):
return web.HTTPUnauthorized(body='Invalid username / password combination') return web.HTTPUnauthorized(body='Invalid username / password combination')
@require('public') @login_required
async def logout(request): async def logout(request):
response = web.Response( response = web.Response(
text='You have been logged out', text='You have been logged out',
@ -76,7 +65,7 @@ async def logout(request):
return response return response
@require('public') @has_permission('public')
async def internal_page(request): async def internal_page(request):
# pylint: disable=unused-argument # pylint: disable=unused-argument
response = web.Response( response = web.Response(
@ -86,7 +75,7 @@ async def internal_page(request):
return response return response
@require('protected') @has_permission('protected')
async def protected_page(request): async def protected_page(request):
# pylint: disable=unused-argument # pylint: disable=unused-argument
response = web.Response( response = web.Response(

View File

@ -6,9 +6,9 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage
from aiohttp_security import setup as setup_security from aiohttp_security import setup as setup_security
from aiohttp_security import SessionIdentityPolicy from aiohttp_security import SessionIdentityPolicy
from .authz import DictionaryAuthorizationPolicy from demo.dictionary_auth.authz import DictionaryAuthorizationPolicy
from .handlers import configure_handlers from demo.dictionary_auth.handlers import configure_handlers
from .users import user_map from demo.dictionary_auth.users import user_map
def make_app(): def make_app():

View File

@ -28,9 +28,9 @@ Simple example::
async def user_update_handler(request): async def user_update_handler(request):
# identity, asked_permission # identity, asked_permission
user_id = await identity_policy.identify(request) user_id = await identity_policy.identify(request)
identity = await auth_policy.authorized_user_id(user_id) identity = await auth_policy.authorized_userid(user_id)
allowed = await request.auth_policy.permits(identity, allowed = await request.auth_policy.permits(
asked_permission) identity, asked_permission)
if not allowed: if not allowed:
# how is this pluggable as well? # how is this pluggable as well?
# ? return NotAllowedStream() # ? return NotAllowedStream()
@ -56,7 +56,7 @@ Simple example::
# get it started # get it started
srv = await loop.create_server(app.make_handler(), srv = await loop.create_server(app.make_handler(),
'127.0.0.1', 8080) '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 return srv

View File

@ -21,12 +21,14 @@ Launch these sql scripts to init database and fill it with sample data:
``psql template1 < demo/sql/init_db.sql`` ``psql template1 < demo/sql/init_db.sql``
and then and
``psql template1 < demo/sql/sample_data.sql`` ``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 | | users |
@ -42,7 +44,7 @@ You will have two tables for storing users and their permissions
| disabled | | disabled |
+--------------+ +--------------+
and second table is permissions table: - for storing their permissions
+-----------------+ +-----------------+
| permissions | | permissions |
@ -63,12 +65,12 @@ First one should have these methods: *identify*, *remember* and *forget*.
For second one: *authorized_userid* and *permits*. We will use built-in For second one: *authorized_userid* and *permits*. We will use built-in
*SessionIdentityPolicy* and write our own database-based authorization policy. *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:: this identity::
async def authorized_userid(self, identity): async def authorized_userid(self, identity):
async with self.dbengine as conn: async with self.dbengine as conn:
where = sa.and_(db.users.c.login == identity, where = sa.and_(db.users.c.login == identity,
sa.not_(db.users.c.disabled)) sa.not_(db.users.c.disabled))
query = db.users.count().where(where) query = db.users.count().where(where)
@ -79,7 +81,7 @@ this identity::
return None 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 (all permissions are allowed), otherwise check if permission is explicitly set
for that user:: for that user::
@ -95,7 +97,7 @@ for that user::
user = await ret.fetchone() user = await ret.fetchone()
if user is not None: if user is not None:
user_id = user[0] user_id = user[0]
is_superuser = user[4] is_superuser = user[3]
if is_superuser: if is_superuser:
return True return True
@ -140,37 +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 Now we have authorization and can decorate every other view with access rights
based on permissions. This simple decorator (for class-based handlers) will based on permissions. There are already implemented two decorators::
help to do that::
def require(permission): from aiohttp_security import has_permission, login_required
def wrapper(f):
@functools.wraps(f)
async def wrapped(self, request):
has_perm = await permits(request, permission)
if not has_perm:
message = 'User has no permission {}'.format(permission)
raise web.HTTPForbidden(body=message.encode())
return await f(self, request)
return wrapped
return wrapper
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: class Web:
@require('protected') @has_permission('protected')
async def protected_page(self, request): async def protected_page(self, request):
response = web.Response(body=b'You are on protected page') response = web.Response(body=b'You are on protected page')
return response 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. follows the API conventions.
Launch application Launch application
@ -178,7 +176,7 @@ Launch application
For working with passwords there is a good library passlib_. Once you've 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 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 from passlib.hash import sha256_crypt
@ -197,8 +195,8 @@ function may do what you trying to accomplish::
Final step is to launch your application:: 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. and access **/public** or **/protected** endpoints.