Add session store and login endpoint and auth middleware

feat/auth_middleware_and_state
blallo 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:
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:

View File

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

View File

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

View File

@ -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),
]

View File

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

View File

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

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

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"
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")

38
src/phi/login.py 100644
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