Add session store and login endpoint and auth middleware

This commit is contained in:
sfigato 2022-02-03 00:46:45 +01:00
parent 714f3221f9
commit ef08fe5997
Signed by: blallo
GPG Key ID: 0CBE577C9B72DC3F
10 changed files with 164 additions and 32 deletions

View File

@ -3,6 +3,8 @@ core:
listen: listen:
host: 127.0.0.1 host: 127.0.0.1
port: 8080 port: 8080
# generated with: openssl rand -hex 16
cookiestore_secret: "e41133b5cfdd8660815b8d5cc2c74843"
ldap: ldap:
@ -14,11 +16,12 @@ ldap:
validate: False # Can either be True or False. Default: False validate: False # Can either be True or False. Default: False
ca_certs: openldap/cert.pem ca_certs: openldap/cert.pem
username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org # username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org
password: phi # password: phi
# username: cn=root,dc=unit,dc=macaomilano,dc=org
# password: root
base_dn: dc=unit,dc=macaomilano,dc=org base_dn: dc=unit,dc=macaomilano,dc=org
attribute_id: uid
logging: logging:

View File

@ -2,24 +2,19 @@
from aiohttp import web from aiohttp import web
from phi.logging import get_logger 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.api.routes import api_routes
from phi.auth_middleware import authenticated
log = get_logger(__name__) log = get_logger(__name__)
def api_app(config): def api_app(store):
log.info("Initializing API sub-app.") log.info("Initializing API sub-app.")
app = web.Application() app = web.Application(middlewares=[authenticated])
ldap_client = AsyncClient(**config.get("ldap", {})) app["store"] = store
app["ldap_client"] = ldap_client
app["users"] = Hackers(ldap_client)
app["services"] = Robots(ldap_client)
app["groups"] = Congregations(ldap_client)
app["log"] = log app["log"] = log
app.router.add_routes(api_routes) app.router.add_routes(api_routes)

View File

@ -9,8 +9,8 @@ from aiohttp.web import (
) )
from phi.logging import get_logger from phi.logging import get_logger
from phi.api.utils import serialize # from phi.api.utils import serialize
from phi.async_ldap.model import Hackers, User from phi.async_ldap.model import User
from phi.exceptions import ( from phi.exceptions import (
PhiEntryDoesNotExist, PhiEntryDoesNotExist,
PhiUnexpectedRuntimeValue, PhiUnexpectedRuntimeValue,

View File

@ -1,9 +1,9 @@
from aiohttp.web import route from aiohttp.web import view
from phi.api.rest import UserView from phi.api.rest import UserView
api_routes = [ api_routes = [
route("*", "/user", UserView), view("/user", UserView),
route("*", "/user/", UserView), view("/user/", UserView),
route("*", "/user/{uid}", UserView), view("/user/{uid}", UserView),
] ]

View File

@ -1,23 +1,34 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
import pkg_resources
from aiohttp import web from aiohttp import web
from aiohttp_session import setup
from aiohttp_session.cookie_storage import EncryptedCookieStorage
import click import click
from pprint import pformat as pp from pprint import pformat as pp
import yaml 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.logging import setup_logging, get_logger
from phi.api.app import api_app from phi.api.app import api_app
from phi.client_store import ClientStore
from phi.login import login
log = get_logger(__name__) log = get_logger(__name__)
LOGIN_ROUTE = "/login"
def setup_app(config): def setup_app(config):
app = web.Application() 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) app.add_subapp("/api", api)
return app return app
@ -109,7 +120,11 @@ def run_app(app):
help="Path to a yaml configuration for the logger.", help="Path to a yaml configuration for the logger.",
) )
@click.option( @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( def cli(
host, host,
@ -141,6 +156,7 @@ def cli(
debug, debug,
) )
config_file, file_config = get_config(config_path) config_file, file_config = get_config(config_path)
log.debug(f"FILE: {pp(file_config)}")
config = merge_config(cli_config, file_config) config = merge_config(cli_config, file_config)
if debug: if debug:
set_to_debug(config) set_to_debug(config)

View File

@ -86,7 +86,7 @@ class AsyncClient(LDAPClient):
host=None, host=None,
port=None, port=None,
encryption=None, encryption=None,
cyphers=None, ciphers=None,
validate=False, validate=False,
ca_cert=None, ca_cert=None,
username=None, username=None,
@ -130,3 +130,6 @@ class AsyncClient(LDAPClient):
self.set_auto_page_acquire(True) self.set_auto_page_acquire(True)
self.set_credentials(self.method, user=self.username, password=self.password) self.set_credentials(self.method, user=self.username, password=self.password)
def __repr__(self):
return f"AsyncClient[{self.full_uri}]<{self.username}>"

View File

@ -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

35
src/phi/client_store.py Normal file
View File

@ -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

View File

@ -5,7 +5,7 @@ import yaml
NAME = "phi" NAME = "phi"
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"core": {"listen": {"host": "localhost", "port": 8080}}, "core": {"listen": {"host": "localhost", "port": 8080}, "cookiestore_secret": None},
"ldap": { "ldap": {
"host": "localhost", "host": "localhost",
"port": 389, "port": 389,
@ -13,10 +13,7 @@ DEFAULT_CONFIG = {
"ciphers": "HIGH", "ciphers": "HIGH",
"validate": True, "validate": True,
"ca_certs": pkg_resources.resource_filename(NAME, "openldap/cert.pem"), "ca_certs": pkg_resources.resource_filename(NAME, "openldap/cert.pem"),
"username": None,
"password": None,
"base_dn": None, "base_dn": None,
"attribute_id": "uid",
"attribute_mail": "mail", "attribute_mail": "mail",
}, },
"logging": { "logging": {
@ -35,9 +32,18 @@ DEFAULT_CONFIG = {
}, },
}, },
"loggers": { "loggers": {
"phi": {"level": "INFO", "handlers": ["console", "file"],}, "phi": {
"aiohttp": {"level": "INFO", "handlers": ["console", "file"],}, "level": "INFO",
"bonsai": {"level": "WARNING", "handlers": ["console", "file"],}, "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) _recursive_merge(main_config, aux_config, DEFAULT_CONFIG, "ROOT", _config)
return _config["ROOT"] 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")

38
src/phi/login.py Normal file
View File

@ -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