From 64b1704015cc9dc96ab85d980b4d609aaf9119d0 Mon Sep 17 00:00:00 2001 From: Teemu Kataja Date: Fri, 5 Jan 2024 14:28:00 +0200 Subject: [PATCH] refactor for python 3.12 --- Dockerfile | 6 +- README.md | 3 +- main.py | 144 +++++++++++++++++++++++-------------------- requirements.dev.txt | 3 - requirements.txt | 19 +----- tests/test_main.py | 2 +- tox.ini | 13 +++- 7 files changed, 95 insertions(+), 95 deletions(-) delete mode 100644 requirements.dev.txt diff --git a/Dockerfile b/Dockerfile index bd1d87f..3ea7136 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-alpine3.13 as BUILD +FROM python:3.12.1-alpine3.19 as BUILD RUN apk add --update \ && apk add --no-cache --virtual build-base libressl-dev libffi-dev gcc musl-dev python3-dev \ @@ -9,11 +9,11 @@ COPY requirements.txt /root/requirements.txt RUN pip install --upgrade pip && \ pip install -r /root/requirements.txt -FROM python:3.8-alpine3.13 +FROM python:3.12.1-alpine3.19 RUN apk add --no-cache --update bash -COPY --from=BUILD /usr/local/lib/python3.8/ /usr/local/lib/python3.8/ +COPY --from=BUILD /usr/local/lib/python3.12/ /usr/local/lib/python3.12/ COPY --from=BUILD /usr/local/bin/uvicorn /usr/local/bin/ diff --git a/README.md b/README.md index 1004243..bd9437c 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,11 @@ Tiny RP is a small OpenID Connect Relying Party client that authenticates the user at the configured OpenID provider and saves the user's `id_token` and `access_token` to cookies. ## Installation -- Developed with `python 3.8.5` +- Developed with `python 3.12.1` ``` pip install --upgrade pip pip install -r requirements.txt ``` -Dependencies are listed in `requirements.dev.txt`, while versions are set for production in `requirements.txt`. ## Configuration Configuration variables are set in [config.json](config.json), which resides at the root of the directory. diff --git a/main.py b/main.py index d77931a..42c5d27 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,6 @@ import logging from urllib.parse import urlencode -from distutils.util import strtobool from typing import Tuple import httpx @@ -17,12 +16,30 @@ from fastapi.responses import PlainTextResponse, RedirectResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware -# the web app -app = FastAPI() + +# distutils.util.strtobool was deprecated in python 3.12 here is the source code for the simple function +# https://github.com/pypa/distutils/blob/94942032878d431cee55adaab12a8bd83549a833/distutils/util.py#L340-L353 +def strtobool(val): + """Convert a string representation of truth to true (1) or false (0). + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return 1 + elif val in ("n", "no", "f", "false", "off", "0"): + return 0 + else: + raise ValueError("invalid truth value {!r}".format(val)) + # logging -formatting = '[%(asctime)s][%(name)s][%(process)d %(processName)s][%(levelname)-8s] (L:%(lineno)s) %(module)s | %(funcName)s: %(message)s' -logging.basicConfig(level=logging.DEBUG if bool(strtobool(os.environ.get('DEBUG', 'False'))) else logging.INFO, format=formatting) +formatting = "[%(asctime)s][%(name)s][%(process)d %(processName)s][%(levelname)-8s] (L:%(lineno)s) %(module)s | %(funcName)s: %(message)s" +logging.basicConfig( + level=logging.DEBUG if bool(strtobool(os.environ.get("DEBUG", "False"))) else logging.INFO, format=formatting +) LOG = logging.getLogger("tiny-rp") # configuration @@ -39,22 +56,11 @@ sys.exit(e) -@app.on_event("startup") -async def startup_event(): +def get_configs(): """Request OpenID configuration from OpenID provider.""" - # add CORS middleware - app.add_middleware( - CORSMiddleware, - allow_origins=CONFIG["cors_domains"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - # get missing OIDC configurations - async with httpx.AsyncClient(verify=False) as client: - # request OpenID provider endpoints from their configuration + with httpx.Client(verify=False) as client: LOG.debug(f"requesting OpenID configuration from {CONFIG['url_oidc']}") - response = await client.get(CONFIG["url_oidc"]) + response = client.get(CONFIG["url_oidc"]) if response.status_code == 200: # store URLs for later use LOG.debug("OpenID configuration received") @@ -70,6 +76,20 @@ async def startup_event(): sys.exit(f"failed to retrieve OIDC configuration: {response.status_code}") +get_configs() + +# the web app +app = FastAPI() +# add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=CONFIG["cors_domains"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + @app.get("/") async def index_endpoint(): """Index can be used as a health check endpoint.""" @@ -91,7 +111,7 @@ async def login_endpoint(): "response_type": "code", "state": state, "redirect_uri": CONFIG["url_callback"], - "scope": CONFIG["scope"] + "scope": CONFIG["scope"], } # prepare the redirection response @@ -100,12 +120,9 @@ async def login_endpoint(): response = RedirectResponse(url) # store state cookie for callback verification - response.set_cookie(key="oidc_state", - value=state, - max_age=300, - httponly=True, - secure=True, - domain=CONFIG.get("cookie_domain", None)) + response.set_cookie( + key="oidc_state", value=state, max_age=300, httponly=True, secure=True, domain=CONFIG.get("cookie_domain", None) + ) # redirect user to sign in at OpenID provider LOG.debug("redirecting to OpenID provider") @@ -158,24 +175,30 @@ async def callback_endpoint(oidc_state: str = Cookie(""), state: str = "", code: response = RedirectResponse(CONFIG["url_redirect"]) # store tokens to cookies - response.set_cookie(key="id_token", - value=id_token, - max_age=3600, - httponly=True, - secure=True, - domain=CONFIG.get("cookie_domain", None)) - response.set_cookie(key="access_token", - value=access_token, - max_age=3600, - httponly=True, - secure=True, - domain=CONFIG.get("cookie_domain", None)) - response.set_cookie(key="logged_in", - value="True", - max_age=3600, - httponly=False, - secure=True, - domain=CONFIG.get("cookie_domain", None)) + response.set_cookie( + key="id_token", + value=id_token, + max_age=3600, + httponly=True, + secure=True, + domain=CONFIG.get("cookie_domain", None), + ) + response.set_cookie( + key="access_token", + value=access_token, + max_age=3600, + httponly=True, + secure=True, + domain=CONFIG.get("cookie_domain", None), + ) + response.set_cookie( + key="logged_in", + value="True", + max_age=3600, + httponly=False, + secure=True, + domain=CONFIG.get("cookie_domain", None), + ) # redirect user LOG.debug(f"redirecting to {CONFIG['url_redirect']}") @@ -194,24 +217,15 @@ async def logout_endpoint(id_token: str = Cookie(""), access_token: str = Cookie response = RedirectResponse(CONFIG["url_redirect"]) # overwrite cookies with instantly expiring ones - response.set_cookie(key="id_token", - value="", - max_age=0, - httponly=True, - secure=True, - domain=CONFIG.get("cookie_domain", None)) - response.set_cookie(key="access_token", - value="", - max_age=0, - httponly=True, - secure=True, - domain=CONFIG.get("cookie_domain", None)) - response.set_cookie(key="logged_in", - value="", - max_age=0, - httponly=False, - secure=True, - domain=CONFIG.get("cookie_domain", None)) + response.set_cookie( + key="id_token", value="", max_age=0, httponly=True, secure=True, domain=CONFIG.get("cookie_domain", None) + ) + response.set_cookie( + key="access_token", value="", max_age=0, httponly=True, secure=True, domain=CONFIG.get("cookie_domain", None) + ) + response.set_cookie( + key="logged_in", value="", max_age=0, httponly=False, secure=True, domain=CONFIG.get("cookie_domain", None) + ) # redirect user LOG.debug(f"redirecting to {CONFIG['url_redirect']}") @@ -225,11 +239,7 @@ async def request_tokens(code: str) -> Tuple[str, str]: # set up basic auth and payload auth = httpx.BasicAuth(username=CONFIG["client_id"], password=CONFIG["client_secret"]) LOG.debug("basic auth is set") - data = { - "grant_type": "authorization_code", - "code": code, - "redirect_uri": CONFIG["url_callback"] - } + data = {"grant_type": "authorization_code", "code": code, "redirect_uri": CONFIG["url_callback"]} LOG.debug(f"post payload: {data}") async with httpx.AsyncClient(auth=auth, verify=False) as client: diff --git a/requirements.dev.txt b/requirements.dev.txt deleted file mode 100644 index c937678..0000000 --- a/requirements.dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -fastapi -httpx -uvicorn diff --git a/requirements.txt b/requirements.txt index 04cec73..c937678 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,3 @@ -anyio==3.5.0 -asgiref==3.5.0 -certifi==2021.10.8 -charset-normalizer==2.0.12 -click==8.1.2 -fastapi==0.75.1 -h11==0.12.0 -httpcore==0.14.7 -httpx==0.22.0 -idna==3.3 -pydantic==1.9.0 -rfc3986==1.5.0 -sniffio==1.2.0 -starlette==0.17.1 -typing_extensions==4.1.1 -uvicorn==0.17.6 +fastapi +httpx +uvicorn diff --git a/tests/test_main.py b/tests/test_main.py index 718f601..9ebe759 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -14,7 +14,7 @@ "url_callback": "http://localhost:8080/callback", "url_redirect": "", "scope": "openid", - "cookie_domain": "" + "cookie_domain": "", } diff --git a/tox.ini b/tox.ini index 11846db..f542006 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,15 @@ [tox] -envlist = flake8,mypy,bandit,unit_tests +envlist = black,flake8,mypy,bandit,unit_tests skipsdist = True -; python code style validation +; format validation +[testenv:black] +skip_install = true +deps = + black +commands = black . -l 120 --check + +; style validation [flake8] skip_install = true deps = @@ -32,4 +39,4 @@ commands = pytest tests [gh-actions] python = - 3.8: flake8,mypy,bandit,unit_tests + 3.12: black,flake8,mypy,bandit,unit_tests