From ef08fe5997053630a87de66daf125436a780f1da Mon Sep 17 00:00:00 2001 From: Blallo Date: Thu, 3 Feb 2022 00:46:45 +0100 Subject: [PATCH] Add session store and login endpoint and auth middleware --- config.yml | 9 ++++++--- src/phi/api/app.py | 13 ++++-------- src/phi/api/rest.py | 4 ++-- src/phi/api/routes.py | 8 ++++---- src/phi/app.py | 28 ++++++++++++++++++++------ src/phi/async_ldap/client.py | 5 ++++- src/phi/auth_middleware.py | 23 ++++++++++++++++++++++ src/phi/client_store.py | 35 +++++++++++++++++++++++++++++++++ src/phi/config.py | 33 ++++++++++++++++++++++++------- src/phi/login.py | 38 ++++++++++++++++++++++++++++++++++++ 10 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 src/phi/auth_middleware.py create mode 100644 src/phi/client_store.py create mode 100644 src/phi/login.py diff --git a/config.yml b/config.yml index 1162b95..173907c 100644 --- a/config.yml +++ b/config.yml @@ -3,6 +3,8 @@ core: listen: host: 127.0.0.1 port: 8080 + # generated with: openssl rand -hex 16 + cookiestore_secret: "e41133b5cfdd8660815b8d5cc2c74843" ldap: @@ -14,11 +16,12 @@ ldap: validate: False # Can either be True or False. Default: False ca_certs: openldap/cert.pem - username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org - password: phi + # username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org + # password: phi + # username: cn=root,dc=unit,dc=macaomilano,dc=org + # password: root base_dn: dc=unit,dc=macaomilano,dc=org - attribute_id: uid logging: diff --git a/src/phi/api/app.py b/src/phi/api/app.py index 4f0af82..a33bc4d 100644 --- a/src/phi/api/app.py +++ b/src/phi/api/app.py @@ -2,24 +2,19 @@ from aiohttp import web from phi.logging import get_logger -from phi.async_ldap.client import AsyncClient -from phi.async_ldap.model import Hackers, Robots, Congregations from phi.api.routes import api_routes +from phi.auth_middleware import authenticated log = get_logger(__name__) -def api_app(config): +def api_app(store): log.info("Initializing API sub-app.") - app = web.Application() + app = web.Application(middlewares=[authenticated]) - ldap_client = AsyncClient(**config.get("ldap", {})) - app["ldap_client"] = ldap_client - app["users"] = Hackers(ldap_client) - app["services"] = Robots(ldap_client) - app["groups"] = Congregations(ldap_client) + app["store"] = store app["log"] = log app.router.add_routes(api_routes) diff --git a/src/phi/api/rest.py b/src/phi/api/rest.py index 2cee299..4a6652a 100644 --- a/src/phi/api/rest.py +++ b/src/phi/api/rest.py @@ -9,8 +9,8 @@ from aiohttp.web import ( ) from phi.logging import get_logger -from phi.api.utils import serialize -from phi.async_ldap.model import Hackers, User +# from phi.api.utils import serialize +from phi.async_ldap.model import User from phi.exceptions import ( PhiEntryDoesNotExist, PhiUnexpectedRuntimeValue, diff --git a/src/phi/api/routes.py b/src/phi/api/routes.py index 9d092b0..39654c0 100644 --- a/src/phi/api/routes.py +++ b/src/phi/api/routes.py @@ -1,9 +1,9 @@ -from aiohttp.web import route +from aiohttp.web import view from phi.api.rest import UserView api_routes = [ - route("*", "/user", UserView), - route("*", "/user/", UserView), - route("*", "/user/{uid}", UserView), + view("/user", UserView), + view("/user/", UserView), + view("/user/{uid}", UserView), ] diff --git a/src/phi/app.py b/src/phi/app.py index 2e95c08..6a8d84c 100644 --- a/src/phi/app.py +++ b/src/phi/app.py @@ -1,23 +1,34 @@ # -*- encoding: utf-8 -*- -import pkg_resources - from aiohttp import web +from aiohttp_session import setup +from aiohttp_session.cookie_storage import EncryptedCookieStorage import click from pprint import pformat as pp import yaml -from phi.config import get_config, merge_config +from phi.config import get_config, merge_config, extract_secret from phi.logging import setup_logging, get_logger from phi.api.app import api_app +from phi.client_store import ClientStore +from phi.login import login log = get_logger(__name__) +LOGIN_ROUTE = "/login" + + def setup_app(config): app = web.Application() - app["config"] = config + setup(app, EncryptedCookieStorage(extract_secret(config))) - api = api_app(config) + app["config"] = config + app["store"] = ClientStore(LOGIN_ROUTE) + app["log"] = log + + app.add_routes([web.post(LOGIN_ROUTE, login)]) + + api = api_app(app["store"]) app.add_subapp("/api", api) return app @@ -109,7 +120,11 @@ def run_app(app): help="Path to a yaml configuration for the logger.", ) @click.option( - "--debug", "debug", is_flag=True, default=False, help="Set the log level to debug.", + "--debug", + "debug", + is_flag=True, + default=False, + help="Set the log level to debug.", ) def cli( host, @@ -141,6 +156,7 @@ def cli( debug, ) config_file, file_config = get_config(config_path) + log.debug(f"FILE: {pp(file_config)}") config = merge_config(cli_config, file_config) if debug: set_to_debug(config) diff --git a/src/phi/async_ldap/client.py b/src/phi/async_ldap/client.py index 219d42d..ac63b03 100644 --- a/src/phi/async_ldap/client.py +++ b/src/phi/async_ldap/client.py @@ -86,7 +86,7 @@ class AsyncClient(LDAPClient): host=None, port=None, encryption=None, - cyphers=None, + ciphers=None, validate=False, ca_cert=None, username=None, @@ -130,3 +130,6 @@ class AsyncClient(LDAPClient): self.set_auto_page_acquire(True) self.set_credentials(self.method, user=self.username, password=self.password) + + def __repr__(self): + return f"AsyncClient[{self.full_uri}]<{self.username}>" diff --git a/src/phi/auth_middleware.py b/src/phi/auth_middleware.py new file mode 100644 index 0000000..c57ba4c --- /dev/null +++ b/src/phi/auth_middleware.py @@ -0,0 +1,23 @@ +# -*- encoding: utf-8 -*- +from aiohttp.web import middleware, HTTPFound + +from phi import app +from phi.async_ldap.model import Hackers, Robots, Congregations + + +@middleware +async def authenticated(request, handler): + try: + store = request.app["store"] + except KeyError: + raise HTTPFound(app.LOGIN_ROUTE) + + client = await store.get_client(request) + + request.app["ldap_client"] = client + request.app["users"] = Hackers(client) + request.app["services"] = Robots(client) + request.app["groups"] = Congregations(client) + resp = await handler(request) + + return resp diff --git a/src/phi/client_store.py b/src/phi/client_store.py new file mode 100644 index 0000000..a3c072b --- /dev/null +++ b/src/phi/client_store.py @@ -0,0 +1,35 @@ +# -*- encoding: utf-8 -*- +import secrets + +from aiohttp.web import HTTPFound +from aiohttp_session import get_session + + +class ClientStore(dict): + """ + This class is responsible to hold the clients used by the active connections. + """ + + def __init__(self, login_route): + self.login_route = login_route + self.store = dict() + + async def get_client(self, request): + session = await get_session(request) + client_id = session.get("client_id") + if client_id is None: + raise HTTPFound(self.login_route) + + client = self.store.get(client_id) + if client is None: + raise HTTPFound(self.login_route) + + return client + + async def set_client(self, request, client): + session = await get_session(request) + + client_id = secrets.token_hex(16) + + self.store[client_id] = client + session["client_id"] = client_id diff --git a/src/phi/config.py b/src/phi/config.py index 269d38e..01dead5 100644 --- a/src/phi/config.py +++ b/src/phi/config.py @@ -5,7 +5,7 @@ import yaml NAME = "phi" DEFAULT_CONFIG = { - "core": {"listen": {"host": "localhost", "port": 8080}}, + "core": {"listen": {"host": "localhost", "port": 8080}, "cookiestore_secret": None}, "ldap": { "host": "localhost", "port": 389, @@ -13,10 +13,7 @@ DEFAULT_CONFIG = { "ciphers": "HIGH", "validate": True, "ca_certs": pkg_resources.resource_filename(NAME, "openldap/cert.pem"), - "username": None, - "password": None, "base_dn": None, - "attribute_id": "uid", "attribute_mail": "mail", }, "logging": { @@ -35,9 +32,18 @@ DEFAULT_CONFIG = { }, }, "loggers": { - "phi": {"level": "INFO", "handlers": ["console", "file"],}, - "aiohttp": {"level": "INFO", "handlers": ["console", "file"],}, - "bonsai": {"level": "WARNING", "handlers": ["console", "file"],}, + "phi": { + "level": "INFO", + "handlers": ["console", "file"], + }, + "aiohttp": { + "level": "INFO", + "handlers": ["console", "file"], + }, + "bonsai": { + "level": "WARNING", + "handlers": ["console", "file"], + }, }, }, } @@ -123,3 +129,16 @@ def recursive_merge(main_config, aux_config): _recursive_merge(main_config, aux_config, DEFAULT_CONFIG, "ROOT", _config) return _config["ROOT"] + + +def extract_secret(config): + try: + secret = config["core"]["cookiestore_secret"].encode("utf-8") + if len(secret) != 32: + raise ValueError( + "The provided core.cookiestore_secret must be 32 bytes long" + ) + + return secret + except KeyError: + raise RuntimeError("You must provide a core.cookiestore_secret") diff --git a/src/phi/login.py b/src/phi/login.py new file mode 100644 index 0000000..961e22a --- /dev/null +++ b/src/phi/login.py @@ -0,0 +1,38 @@ +# -*- encoding: utf-8 -*- +from aiohttp.web import HTTPBadRequest, HTTPOk + +from phi.async_ldap.client import AsyncClient + + +async def login(request): + log = request.app["log"] + log.debug("login") + + store = request.app["store"] + config = request.app["config"] + + body = await request.json() + + tag = body.get("tag", "uid") + + ou = body.get("ou") + if ou is not None: + config["ldap"]["ou"] = ou + + try: + user = body["user"] + password = body["password"] + except KeyError as e: + text = f"Missing key: {e}" + log.warn(text) + raise HTTPBadRequest(text=text) + + client = AsyncClient( + attribute_id=tag, username=user, password=password, **config.get("ldap") + ) + + log.debug(f"Client: {client}") + + await store.set_client(request, client) + + raise HTTPOk