mirror of
https://github.com/PiBrewing/craftbeerpi4.git
synced 2024-12-23 14:04:58 +01:00
229 lines
9.3 KiB
ReStructuredText
229 lines
9.3 KiB
ReStructuredText
aiohttp_auth
|
|
============
|
|
|
|
This library provides authorization and authentication middleware plugins for
|
|
aiohttp servers.
|
|
|
|
These plugins are designed to be lightweight, simple, and extensible, allowing
|
|
the library to be reused regardless of the backend authentication mechanism.
|
|
This provides a familiar framework across projects.
|
|
|
|
There are two middleware plugins provided by the library. The auth_middleware
|
|
plugin provides a simple system for authenticating a users credentials, and
|
|
ensuring that the user is who they say they are.
|
|
|
|
The acl_middleware plugin provides a simple access control list authorization
|
|
mechanism, where users are provided access to different view handlers depending
|
|
on what groups the user is a member of.
|
|
|
|
|
|
auth_middleware Usage
|
|
---------------------
|
|
|
|
The auth_middleware plugin provides a simple abstraction for remembering and
|
|
retrieving the authentication details for a user across http requests.
|
|
Typically, an application would retrieve the login details for a user, and call
|
|
the remember function to store the details. These details can then be recalled
|
|
in future requests. A simplistic example of users stored in a python dict would
|
|
be::
|
|
|
|
from aiohttp_auth import auth
|
|
from aiohttp import web
|
|
|
|
# Simplistic name/password map
|
|
db = {'user': 'password',
|
|
'super_user': 'super_password'}
|
|
|
|
|
|
async def login_view(request):
|
|
params = await request.post()
|
|
user = params.get('username', None)
|
|
if (user in db and
|
|
params.get('password', None) == db[user]):
|
|
|
|
# User is in our database, remember their login details
|
|
await auth.remember(request, user)
|
|
return web.Response(body='OK'.encode('utf-8'))
|
|
|
|
raise web.HTTPForbidden()
|
|
|
|
User data can be verified in later requests by checking that their username is
|
|
valid explicity, or by using the auth_required decorator::
|
|
|
|
async def check_explicitly_view(request):
|
|
user = await get_auth(request)
|
|
if user is None:
|
|
# Show login page
|
|
return web.Response(body='Not authenticated'.encode('utf-8'))
|
|
|
|
return web.Response(body='OK'.encode('utf-8'))
|
|
|
|
@auth.auth_required
|
|
async def check_implicitly_view(request):
|
|
# HTTPForbidden is raised by the decorator if user is not valid
|
|
return web.Response(body='OK'.encode('utf-8'))
|
|
|
|
To end the session, the user data can be forgotten by using the forget
|
|
function::
|
|
|
|
@auth.auth_required
|
|
async def logout_view(request):
|
|
await auth.forget(request)
|
|
return web.Response(body='OK'.encode('utf-8'))
|
|
|
|
The actual mechanisms for storing the authentication credentials are passed as
|
|
a policy to the session manager middleware. New policies can be implemented
|
|
quite simply by overriding the AbstractAuthentication class. The aiohttp_auth
|
|
package currently provides two authentication policies, a cookie based policy
|
|
based loosely on mod_auth_tkt (Apache ticket module), and a second policy that
|
|
uses the aiohttp_session class to store authentication tickets.
|
|
|
|
The cookie based policy (CookieTktAuthentication) is a simple mechanism for
|
|
storing the username of the authenticated user in a cookie, along with a hash
|
|
value known only to the server. The cookie contains the maximum age allowed
|
|
before the ticket expires, and can also use the IP address (v4 or v6) of the
|
|
user to link the cookie to that address. The cookies data is not encryptedd,
|
|
but only holds the username of the user and the cookies expiration time, along
|
|
with its security hash::
|
|
|
|
def init(loop):
|
|
# Create a auth ticket mechanism that expires after 1 minute (60
|
|
# seconds), and has a randomly generated secret. Also includes the
|
|
# optional inclusion of the users IP address in the hash
|
|
policy = auth.CookieTktAuthentication(urandom(32), 60,
|
|
include_ip=True))
|
|
|
|
app = web.Application(loop=loop,
|
|
middlewares=[auth.auth_middleware(policy)])
|
|
app = web.Application()
|
|
app.router.add_route('POST', '/login', login_view)
|
|
app.router.add_route('GET', '/logout', logout_view)
|
|
app.router.add_route('GET', '/test0', check_explicitly_view)
|
|
app.router.add_route('GET', '/test1', check_implicitly_view)
|
|
|
|
return app
|
|
|
|
The SessionTktAuthentication policy provides many of the same features, but
|
|
stores the same ticket credentials in a aiohttp_session object, allowing
|
|
different storage mechanisms such as Redis storage, and
|
|
EncryptedCookieStorage::
|
|
|
|
from aiohttp_session import get_session, session_middleware
|
|
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
|
|
|
def init(loop):
|
|
|
|
# Create a auth ticket mechanism that expires after 1 minute (60
|
|
# seconds), and has a randomly generated secret. Also includes the
|
|
# optional inclusion of the users IP address in the hash
|
|
policy = auth.SessionTktAuthentication(urandom(32), 60,
|
|
include_ip=True))
|
|
|
|
middlewares = [session_middleware(EncryptedCookieStorage(urandom(32))),
|
|
auth.auth_middleware(policy)]
|
|
|
|
app = web.Application(loop=loop, middlewares=middlewares)
|
|
|
|
...
|
|
|
|
|
|
acl_middleware Usage
|
|
---------------------
|
|
|
|
The acl_middleware plugin (provided by the aiohttp_auth library), is layered
|
|
on top of the auth_middleware plugin, and provides a access control list (ACL)
|
|
system similar to that used by the Pyramid WSGI module.
|
|
|
|
Each user in the system is assigned a series of groups. Each group in the
|
|
system can then be assigned permissions that they are allowed (or not allowed)
|
|
to access. Groups and permissions are user defined, and need only be immutable
|
|
objects, so they can be strings, numbers, enumerations, or other immutable
|
|
objects.
|
|
|
|
To specify what groups a user is a member of, a function is passed to the
|
|
acl_middleware factory which taks a user_id (as returned from the
|
|
auth.get_auth function) as a parameter, and expects a sequence of permitted ACL
|
|
groups to be returned. This can be a empty tuple to represent no explicit
|
|
permissions, or None to explicitly forbid this particular user_id. Note that
|
|
the user_id passed may be None if no authenticated user exists. Building apon
|
|
our example, a function may be defined as::
|
|
|
|
from aiohttp_auth import acl
|
|
|
|
group_map = {'user': (,),
|
|
'super_user': ('edit_group',),}
|
|
|
|
async def acl_group_callback(user_id):
|
|
# The user_id could be None if the user is not authenticated, but in
|
|
# our example, we allow unauthenticated users access to some things, so
|
|
# we return an empty tuple.
|
|
return group_map.get(user_id, tuple())
|
|
|
|
def init(loop):
|
|
...
|
|
|
|
middlewares = [session_middleware(EncryptedCookieStorage(urandom(32))),
|
|
auth.auth_middleware(policy),
|
|
acl.acl_middleware(acl_group_callback)]
|
|
|
|
app = web.Application(loop=loop, middlewares=middlewares)
|
|
...
|
|
|
|
|
|
Note that the ACL groups returned by the function will be modified by the
|
|
acl_middleware to also include the Group.Everyone group (if the value returned
|
|
is not None), and also the Group.AuthenticatedUser and user_id if the user_id
|
|
is not None.
|
|
|
|
With the groups defined, a ACL context can be specified for looking up what
|
|
permissions each group is allowed to access. A context is a sequence of ACL
|
|
tuples which consist of a Allow/Deny action, a group, and a sequence of
|
|
permissions for that ACL group. For example::
|
|
|
|
from aiohttp_auth.permissions import Group, Permission
|
|
|
|
context = [(Permission.Allow, Group.Everyone, ('view',)),
|
|
(Permission.Allow, Group.AuthenticatedUser, ('view', 'view_extra')),
|
|
(Permission.Allow, 'edit_group', ('view', 'view_extra', 'edit')),]
|
|
|
|
Views can then be defined using the acl_required decorator, allowing only
|
|
specific users access to a particular view. The acl_required decorator
|
|
specifies a permission required to access the view, and a context to check
|
|
against::
|
|
|
|
@acl_required('view', context)
|
|
async def view_view(request):
|
|
return web.Response(body='OK'.encode('utf-8'))
|
|
|
|
@acl_required('view_extra', context)
|
|
async def view_extra_view(request):
|
|
return web.Response(body='OK'.encode('utf-8'))
|
|
|
|
@acl_required('edit', context)
|
|
async def edit_view(request):
|
|
return web.Response(body='OK'.encode('utf-8'))
|
|
|
|
In our example, non-logged in users will have access to the view_view, 'user'
|
|
will have access to both the view_view and view_extra_view, and 'super_user'
|
|
will have access to all three views. If no ACL group of the user matches the
|
|
ACL permission requested by the view, the decorator raises HTTPForbidden.
|
|
|
|
ACL tuple sequences are checked in order, with the first tuple that matches the
|
|
group the user is a member of, AND includes the permission passed to the
|
|
function, declared to be the matching ACL group. This means that if the ACL
|
|
context was modified to::
|
|
|
|
context = [(Permission.Allow, Group.Everyone, ('view',)),
|
|
(Permission.Deny, 'super_user', ('view_extra')),
|
|
(Permission.Allow, Group.AuthenticatedUser, ('view', 'view_extra')),
|
|
(Permission.Allow, 'edit_group', ('view', 'view_extra', 'edit')),]
|
|
|
|
In this example the 'super_user' would be denied access to the view_extra_view
|
|
even though they are an AuthenticatedUser and in the edit_group.
|
|
|
|
License
|
|
-------
|
|
|
|
The library is licensed under a MIT license.
|
|
|
|
|