From 1a9ab6424ee09f8c028876668edd0225e37ee9e2 Mon Sep 17 00:00:00 2001
From: Devin Fee <devin@devinfee.com>
Date: Tue, 19 Sep 2017 01:54:37 -0700
Subject: [PATCH] added simplistic dictionary_auth example (#105)

---
 demo/{ => database_auth}/db.py               |   0
 demo/{ => database_auth}/db_auth.py          |   0
 demo/{ => database_auth}/handlers.py         |   0
 demo/{ => database_auth}/main.py             |   0
 demo/{ => database_auth}/sql/init_db.sql     |   0
 demo/{ => database_auth}/sql/sample_data.sql |   0
 demo/dictionary_auth/authz.py                |  34 ++++++
 demo/dictionary_auth/handlers.py             | 107 +++++++++++++++++++
 demo/dictionary_auth/main.py                 |  33 ++++++
 demo/dictionary_auth/users.py                |  10 ++
 10 files changed, 184 insertions(+)
 rename demo/{ => database_auth}/db.py (100%)
 rename demo/{ => database_auth}/db_auth.py (100%)
 rename demo/{ => database_auth}/handlers.py (100%)
 rename demo/{ => database_auth}/main.py (100%)
 rename demo/{ => database_auth}/sql/init_db.sql (100%)
 rename demo/{ => database_auth}/sql/sample_data.sql (100%)
 create mode 100644 demo/dictionary_auth/authz.py
 create mode 100644 demo/dictionary_auth/handlers.py
 create mode 100644 demo/dictionary_auth/main.py
 create mode 100644 demo/dictionary_auth/users.py

diff --git a/demo/db.py b/demo/database_auth/db.py
similarity index 100%
rename from demo/db.py
rename to demo/database_auth/db.py
diff --git a/demo/db_auth.py b/demo/database_auth/db_auth.py
similarity index 100%
rename from demo/db_auth.py
rename to demo/database_auth/db_auth.py
diff --git a/demo/handlers.py b/demo/database_auth/handlers.py
similarity index 100%
rename from demo/handlers.py
rename to demo/database_auth/handlers.py
diff --git a/demo/main.py b/demo/database_auth/main.py
similarity index 100%
rename from demo/main.py
rename to demo/database_auth/main.py
diff --git a/demo/sql/init_db.sql b/demo/database_auth/sql/init_db.sql
similarity index 100%
rename from demo/sql/init_db.sql
rename to demo/database_auth/sql/init_db.sql
diff --git a/demo/sql/sample_data.sql b/demo/database_auth/sql/sample_data.sql
similarity index 100%
rename from demo/sql/sample_data.sql
rename to demo/database_auth/sql/sample_data.sql
diff --git a/demo/dictionary_auth/authz.py b/demo/dictionary_auth/authz.py
new file mode 100644
index 0000000..0f9baae
--- /dev/null
+++ b/demo/dictionary_auth/authz.py
@@ -0,0 +1,34 @@
+from aiohttp_security.abc import AbstractAuthorizationPolicy
+
+
+class DictionaryAuthorizationPolicy(AbstractAuthorizationPolicy):
+    def __init__(self, user_map):
+        super().__init__()
+        self.user_map = user_map
+
+    async def authorized_userid(self, identity):
+        """Retrieve authorized user id.
+        Return the user_id of the user identified by the identity
+        or 'None' if no user exists related to the identity.
+        """
+        if identity in self.user_map:
+            return identity
+
+    async def permits(self, identity, permission, context=None):
+        """Check user permissions.
+        Return True if the identity is allowed the permission in the
+        current context, else return False.
+        """
+        # pylint: disable=unused-argument
+        user = self.user_map.get(identity)
+        if not user:
+            return False
+        return permission in user.permissions
+
+
+async def check_credentials(user_map, username, password):
+    user = user_map.get(username)
+    if not user:
+        return False
+
+    return user.password == password
diff --git a/demo/dictionary_auth/handlers.py b/demo/dictionary_auth/handlers.py
new file mode 100644
index 0000000..2dffe55
--- /dev/null
+++ b/demo/dictionary_auth/handlers.py
@@ -0,0 +1,107 @@
+import asyncio
+import functools
+from textwrap import dedent
+
+from aiohttp import web
+
+from aiohttp_security import remember, forget, authorized_userid, permits
+
+from .authz import check_credentials
+
+
+def require(permission):
+    def wrapper(f):
+        @asyncio.coroutine
+        @functools.wraps(f)
+        def wrapped(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(request))
+        return wrapped
+    return wrapper
+
+
+index_template = dedent("""
+    <!doctype html>
+    <head>
+    </head>
+    <body>
+    <p>{message}</p>
+    <form action="/login" method="post">
+      Login:
+      <input type="text" name="username">
+      Password:
+      <input type="password" name="password">
+      <input type="submit" value="Login">
+    </form>
+    <a href="/logout">Logout</a>
+    </body>
+    """)
+
+
+async def index(request):
+    username = await authorized_userid(request)
+    if username:
+        template = index_template.format(
+            message='Hello, {username}!'.format(username=username))
+    else:
+        template = index_template.format(message='You need to login')
+    return web.Response(
+        text=template,
+        content_type='text/html',
+    )
+
+
+async def login(request):
+    response = web.HTTPFound('/')
+    form = await request.post()
+    username = form.get('username')
+    password = form.get('password')
+
+    verified = await check_credentials(request.app.user_map, username, password)
+    if verified:
+        await remember(request, response, username)
+        return response
+
+    return web.HTTPUnauthorized(body='Invalid username / password combination')
+
+
+@require('public')
+async def logout(request):
+    response = web.Response(
+        text='You have been logged out',
+        content_type='text/html',
+    )
+    await forget(request, response)
+    return response
+
+
+@require('public')
+async def internal_page(request):
+    # pylint: disable=unused-argument
+    response = web.Response(
+        text='This page is visible for all registered users',
+        content_type='text/html',
+    )
+    return response
+
+
+@require('protected')
+async def protected_page(request):
+    # pylint: disable=unused-argument
+    response = web.Response(
+        text='You are on protected page',
+        content_type='text/html',
+    )
+    return response
+
+
+def configure_handlers(app):
+    router = app.router
+    router.add_get('/', index, name='index')
+    router.add_post('/login', login, name='login')
+    router.add_get('/logout', logout, name='logout')
+    router.add_get('/public', internal_page, name='public')
+    router.add_get('/protected', protected_page, name='protected')
diff --git a/demo/dictionary_auth/main.py b/demo/dictionary_auth/main.py
new file mode 100644
index 0000000..b017ed5
--- /dev/null
+++ b/demo/dictionary_auth/main.py
@@ -0,0 +1,33 @@
+import base64
+from cryptography import fernet
+from aiohttp import web
+from aiohttp_session import setup as setup_session
+from aiohttp_session.cookie_storage import EncryptedCookieStorage
+from aiohttp_security import setup as setup_security
+from aiohttp_security import SessionIdentityPolicy
+
+from .authz import DictionaryAuthorizationPolicy
+from .handlers import configure_handlers
+from .users import user_map
+
+
+def make_app():
+    app = web.Application()
+    app.user_map = user_map
+    configure_handlers(app)
+
+    # secret_key must be 32 url-safe base64-encoded bytes
+    fernet_key = fernet.Fernet.generate_key()
+    secret_key = base64.urlsafe_b64decode(fernet_key)
+
+    storage = EncryptedCookieStorage(secret_key, cookie_name='API_SESSION')
+    setup_session(app, storage)
+
+    policy = SessionIdentityPolicy()
+    setup_security(app, policy, DictionaryAuthorizationPolicy(user_map))
+
+    return app
+
+
+if __name__ == '__main__':
+    web.run_app(make_app(), port=9000)
diff --git a/demo/dictionary_auth/users.py b/demo/dictionary_auth/users.py
new file mode 100644
index 0000000..967b2bb
--- /dev/null
+++ b/demo/dictionary_auth/users.py
@@ -0,0 +1,10 @@
+from collections import namedtuple
+
+User = namedtuple('User', ['username', 'password', 'permissions'])
+
+user_map = {
+    user.username: user for user in [
+        User('devin', 'password', ('public',)),
+        User('jack', 'password', ('public', 'protected',)),
+    ]
+}