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:
parent
f9628b0ac1
commit
1679f6713b
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue