From 26912000afcd88990d9817744bab7d5f2a24c2db Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Tue, 20 Feb 2024 17:39:01 +0100 Subject: [PATCH] feat: allow running review app to validate PR from external contribution * configure authentication layer on review application --- apps/reviewapp.py | 78 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/apps/reviewapp.py b/apps/reviewapp.py index 50da8489d..86e7a9a07 100644 --- a/apps/reviewapp.py +++ b/apps/reviewapp.py @@ -1,11 +1,26 @@ +""" +This is a simple application to help our team to review contribution. + +>>> python apps/reviewapp.py + +Runs this application in public and requires authentication. + +>>> export HOST=0.0.0.0; export BASICAUTH=admin:admin; python apps/reviewapp.py +""" +import base64 import os +import time import uvicorn from fastapi.responses import HTMLResponse -from fastapi import FastAPI, Response +from fastapi import FastAPI, status import streamsync.serve +HOST = os.getenv('HOST', 'localhost') +PORT = int(os.getenv('PORT', '8000')) +print(f"listen on {HOST}:{PORT}") + def app_path(app_name: str) -> str: return os.path.join(os.path.dirname(__file__), app_name) @@ -36,8 +51,65 @@ async def init(): """, status_code=200) + + +@root_asgi_app.middleware("http") +async def valid_authentication(request, call_next): + """ + Secures access to the review application using basic auth + + The username and password is stored in the BASICAUTH environment variable. + The authentication process is sequential and when it's wrong it take one second to try again. This protection + is sufficient to limit brute force attack. + """ + if HOST == 'localhost': + """ + Locally, you can launch the review application without needing to authenticate. + + The application bypass the authentication middleware. + """ + return await call_next(request) + + _auth = request.headers.get('Authorization') + if not check_permission(_auth): + return HTMLResponse("", status.HTTP_401_UNAUTHORIZED, {"WWW-Authenticate": "Basic"}) + return await call_next(request) + + +def check_permission(auth) -> bool: + """ + Secures access to the review application using basic auth + + >>> is_valid_token = check_permission('Basic dXNlcm5hbWU6cGFzc3dvcmQ=') + """ + if auth is None: + return False + + scheme, data = (auth or ' ').split(' ', 1) + if scheme != 'Basic': + return False + + username, password = base64.b64decode(data).decode().split(':', 1) + basicauth = os.getenv('BASICAUTH') + if auth is None: + raise ValueError('BASICAUTH environment variable is not set') + + basicauth_part = basicauth.split(':') + if len(basicauth_part) != 2: + raise ValueError('BASICAUTH environment variable is not set') + + basicauth_username, basicauth_password = basicauth_part + + time.sleep(1) + if username == basicauth_username and password == basicauth_password: + return True + else: + time.sleep(1) + return False + + uvicorn.run(root_asgi_app, - host="0.0.0.0", - port=os.getenv('PORT', 8000), + host=HOST, + port=PORT, log_level="warning", ws_max_size=streamsync.serve.MAX_WEBSOCKET_MESSAGE_SIZE) \ No newline at end of file