From 6e9d272ee59194e3defbb55832af3f311f3ee687 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 31 Oct 2025 13:42:20 +0000 Subject: [PATCH 001/118] initial commit, added API call and dynamically generated .conf for research environments --- .clog.toml | 0 .github/workflows/dummy-renovate.yml | 0 .gitignore | 0 FastapiOpenRestyConfigurator/.env.in | 0 .../.requirements.txt.kate-swp | Bin FastapiOpenRestyConfigurator/app/__init__.py | 0 .../app/main/__init__.py | 0 .../app/main/config.py | 4 +- .../app/main/model/__init__.py | 0 .../app/main/model/serializers.py | 7 ++ .../app/main/service/__init__.py | 0 .../app/main/service/backend.py | 76 +++++++++++++++++- .../app/main/service/openresty.py | 0 .../app/main/service/template.py | 0 .../app/main/service/user.py | 0 .../app/main/util/__init__.py | 0 .../app/main/util/auth.py | 0 .../app/main/util/logging.py | 0 .../app/main/util/templating.py | 11 ++- .../app/main/views/__init__.py | 0 .../app/main/views/backend.py | 15 ++++ .../app/main/views/template.py | 0 .../app/main/views/user.py | 0 .../app/main/views/utils.py | 0 FastapiOpenRestyConfigurator/gunicorn_conf.py | 11 +++ FastapiOpenRestyConfigurator/main.py | 0 LICENSE | 0 README.md | 0 docker/Dockerfile | 0 docker/README.md | 0 docker/launch.sh | 0 docker/uwsgi.ini | 0 examples/openresty_configuration.md | 0 examples/scripts/user_service.lua | 0 examples/templates/cwlab%v01.conf | 8 +- examples/templates/cwlab%v02.conf | 8 +- examples/templates/emgb%v01.conf | 9 ++- examples/templates/guacamole%v01.conf | 8 +- examples/templates/guacamole%v02.conf | 8 +- examples/templates/guacamole%v03.conf | 9 ++- examples/templates/jupyterlab%v01.conf | 8 +- examples/templates/jupyterlab%v02.conf | 8 +- examples/templates/jupyterlab%v03.conf | 8 +- examples/templates/rstudio%v01.conf | 8 +- examples/templates/rstudio%v02.conf | 10 ++- examples/templates/rstudio%v03.conf | 10 ++- examples/templates/rstudio%v04.conf | 8 +- examples/templates/theiaide%v01.conf | 8 +- examples/templates/theiaide%v02.conf | 8 +- examples/templates/theiaide%v03.conf | 54 ++++++++----- examples/templates/vscode%v03.conf | 8 +- examples/templating_guide.md | 28 ++++--- gfx/forc_overview.drawio | 0 gfx/forc_overview.png | Bin renovate.json | 0 55 files changed, 250 insertions(+), 90 deletions(-) mode change 100644 => 100755 .clog.toml mode change 100644 => 100755 .github/workflows/dummy-renovate.yml mode change 100644 => 100755 .gitignore mode change 100644 => 100755 FastapiOpenRestyConfigurator/.env.in mode change 100644 => 100755 FastapiOpenRestyConfigurator/.requirements.txt.kate-swp mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/__init__.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/__init__.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/config.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/model/__init__.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/model/serializers.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/service/__init__.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/service/backend.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/service/openresty.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/service/template.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/service/user.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/util/__init__.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/util/auth.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/util/logging.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/util/templating.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/views/__init__.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/views/backend.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/views/template.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/views/user.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/app/main/views/utils.py create mode 100755 FastapiOpenRestyConfigurator/gunicorn_conf.py mode change 100644 => 100755 FastapiOpenRestyConfigurator/main.py mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md mode change 100644 => 100755 docker/Dockerfile mode change 100644 => 100755 docker/README.md mode change 100644 => 100755 docker/launch.sh mode change 100644 => 100755 docker/uwsgi.ini mode change 100644 => 100755 examples/openresty_configuration.md mode change 100644 => 100755 examples/scripts/user_service.lua mode change 100644 => 100755 examples/templates/cwlab%v01.conf mode change 100644 => 100755 examples/templates/cwlab%v02.conf mode change 100644 => 100755 examples/templates/emgb%v01.conf mode change 100644 => 100755 examples/templates/guacamole%v01.conf mode change 100644 => 100755 examples/templates/guacamole%v02.conf mode change 100644 => 100755 examples/templates/guacamole%v03.conf mode change 100644 => 100755 examples/templates/jupyterlab%v01.conf mode change 100644 => 100755 examples/templates/jupyterlab%v02.conf mode change 100644 => 100755 examples/templates/jupyterlab%v03.conf mode change 100644 => 100755 examples/templates/rstudio%v01.conf mode change 100644 => 100755 examples/templates/rstudio%v02.conf mode change 100644 => 100755 examples/templates/rstudio%v03.conf mode change 100644 => 100755 examples/templates/rstudio%v04.conf mode change 100644 => 100755 examples/templates/theiaide%v01.conf mode change 100644 => 100755 examples/templates/theiaide%v02.conf mode change 100644 => 100755 examples/templates/theiaide%v03.conf mode change 100644 => 100755 examples/templates/vscode%v03.conf mode change 100644 => 100755 examples/templating_guide.md mode change 100644 => 100755 gfx/forc_overview.drawio mode change 100644 => 100755 gfx/forc_overview.png mode change 100644 => 100755 renovate.json diff --git a/.clog.toml b/.clog.toml old mode 100644 new mode 100755 diff --git a/.github/workflows/dummy-renovate.yml b/.github/workflows/dummy-renovate.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/.env.in b/FastapiOpenRestyConfigurator/.env.in old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/.requirements.txt.kate-swp b/FastapiOpenRestyConfigurator/.requirements.txt.kate-swp old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/__init__.py b/FastapiOpenRestyConfigurator/app/__init__.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/__init__.py b/FastapiOpenRestyConfigurator/app/main/__init__.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/config.py b/FastapiOpenRestyConfigurator/app/main/config.py old mode 100644 new mode 100755 index 2c861703..964dd770 --- a/FastapiOpenRestyConfigurator/app/main/config.py +++ b/FastapiOpenRestyConfigurator/app/main/config.py @@ -12,8 +12,8 @@ class Settings(BaseSettings): Reads settings from .env file. """ FORC_VERSION: str = '0.2' - DEBUG: bool = False - LOG_LEVEL: str = "INFO" + DEBUG: bool = True #temporary !!!!!!!!! + LOG_LEVEL: str = "DEBUG" #temporary !!!!!!!!! FORC_API_KEY: SecretStr FORC_SECRET_KEY: SecretStr = 'my_precious_secret_key' FORC_BACKEND_PATH: DirectoryPath diff --git a/FastapiOpenRestyConfigurator/app/main/model/__init__.py b/FastapiOpenRestyConfigurator/app/main/model/__init__.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/model/serializers.py b/FastapiOpenRestyConfigurator/app/main/model/serializers.py old mode 100644 new mode 100755 index 8afbb404..0bbdd696 --- a/FastapiOpenRestyConfigurator/app/main/model/serializers.py +++ b/FastapiOpenRestyConfigurator/app/main/model/serializers.py @@ -84,6 +84,12 @@ class BackendIn(BackendBase): description="Inject the full url (with protocol) for the real location of the backend service in the template.", example="http://192.168.0.1:8787/" ) + only_allow_owner: bool = Field( + True, + title="Authorization for the research environment", + description="If set to true, only the owner of the backend is allowed to access it.", + example=False + ) @validator("user_key_url") def user_key_url_validation(cls, v): @@ -143,6 +149,7 @@ class BackendTemp(BackendIn, BackendOut): template_version: str = None user_key_url: str = None upstream_url: str = None + only_allow_owner: bool = None class Template(BaseModel): diff --git a/FastapiOpenRestyConfigurator/app/main/service/__init__.py b/FastapiOpenRestyConfigurator/app/main/service/__init__.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py old mode 100644 new mode 100755 index 51e425f5..9291c6a4 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -38,10 +38,12 @@ async def get_backends() -> List[BackendOut]: for file in backend_path_files: match = re.fullmatch(file_regex, file) if not match: + if file == "users" or file == "scripts": + continue logger.warning("Found a backend file with wrong naming, skipping it: " + str(file)) continue backend: BackendOut = BackendOut( - id=match.group(1), + id=match.group(1), owner=match.group(2), location_url=match.group(3), template=match.group(4), @@ -98,11 +100,14 @@ async def generate_suffix_number(user_key_url): return str(highest_id + 1) -async def create_backend(payload: BackendIn): +async def create_backend(payload: BackendIn, **kwargs): payload: BackendTemp = BackendTemp(**payload.dict()) suffix_number = await generate_suffix_number(payload.user_key_url) payload.id = str(await random_with_n_digits(10)) + if 'id' in kwargs: # override id and suffix if provided + payload = payload.copy(update={'id': str(kwargs.get('id'))}) + suffix_number = str(kwargs.get('location_url')).split("_")[1] backend_file_contents = await generate_backend_by_template(payload, suffix_number) if not backend_file_contents: @@ -151,3 +156,70 @@ async def delete_backend(backend_id) -> bool: logger.warning(f"Was not able to delete backend with id: {backend_id} ERROR: {e}") raise InternalServerError("Server was not able to delete this backend. Contact the admin.") raise NotFound("Backend was not found.") + + +async def update_backend_authorization(backend_id: int, auth_enable: bool) -> bool: + # look up backend by id + backends = await get_backends() + target = next((b for b in backends if int(b.id) == int(backend_id)), None) + if not target: + logger.warning(f"Backend {backend_id} not found for auth-switch.") + return False + + # extract upstream_url from file + upstream_url = extract_proxy_pass(target.file_path) + if not upstream_url: + logger.error(f"Could not extract proxy_pass from {target.file_path}") + return False + + # get existing location_url and user_key_url + try: + base_key, suffix = target.location_url.rsplit("_", 1) + except ValueError: + logger.error(f"location_url has no suffix pattern: {target.location_url}") + return False + logger.info(f"Base key: {base_key}, Suffix: {suffix}") + + # build new temp payload + try: + temp_payload = BackendIn( + #id=str(backend_id), + owner=target.owner, + #location_url=target.location_url, + template=target.template, + template_version=target.template_version, + user_key_url=base_key, + upstream_url=upstream_url, + only_allow_owner=auth_enable, # set new auth flag + ) + except Exception as e: + logger.error(f"Error building temp payload for backend update: {e}") + return False + logger.info(f"temp_payload: {temp_payload}") + + # generate new backend contents + new_contents = await create_backend(temp_payload, id=str(backend_id), location_url=target.location_url) + logger.info(f"New contents: {new_contents}") + if not new_contents: + logger.error("Templating returned empty result.") + return False + return True + + """ safely write new contents to file + tmp_path = f"{target.file_path}.tmp" + try: + with open(tmp_path, "w") as f: + f.write(new_contents) + os.replace(tmp_path, target.file_path) # atomic replace, no downtime + except OSError as e: + logger.exception(f"Failed to write/replace backend file: {e}") + try: + if os.path.exists(tmp_path): + os.remove(tmp_path) + except OSError: + pass + return False + + # reload openResty + await reload_openresty() + return True""" diff --git a/FastapiOpenRestyConfigurator/app/main/service/openresty.py b/FastapiOpenRestyConfigurator/app/main/service/openresty.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/service/template.py b/FastapiOpenRestyConfigurator/app/main/service/template.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/service/user.py b/FastapiOpenRestyConfigurator/app/main/service/user.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/util/__init__.py b/FastapiOpenRestyConfigurator/app/main/util/__init__.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/util/auth.py b/FastapiOpenRestyConfigurator/app/main/util/auth.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/util/logging.py b/FastapiOpenRestyConfigurator/app/main/util/logging.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/util/templating.py b/FastapiOpenRestyConfigurator/app/main/util/templating.py old mode 100644 new mode 100755 index 34d143c5..8d855f8f --- a/FastapiOpenRestyConfigurator/app/main/util/templating.py +++ b/FastapiOpenRestyConfigurator/app/main/util/templating.py @@ -30,11 +30,20 @@ async def generate_backend_by_template(backend_temp: BackendTemp, suffix_number) return None template = templateEnv.get_template(assembled_template_file_name) + logger.info({ + "event": "templating_vars", + "only_allow_owner_value": backend_temp.only_allow_owner, + "only_allow_owner_type": type(backend_temp.only_allow_owner).__name__, + "template": assembled_template_file_name, + }) + logger.info(f"template: {template}") + rendered_backend = template.render( key_url=f"{backend_temp.user_key_url}_{suffix_number}", owner=backend_temp.owner, backend_id=backend_temp.id, forc_backend_path=settings.FORC_BACKEND_PATH, - location_url=backend_temp.upstream_url + location_url=backend_temp.upstream_url, + only_allow_owner=backend_temp.only_allow_owner ) return rendered_backend diff --git a/FastapiOpenRestyConfigurator/app/main/views/__init__.py b/FastapiOpenRestyConfigurator/app/main/views/__init__.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/views/backend.py b/FastapiOpenRestyConfigurator/app/main/views/backend.py old mode 100644 new mode 100755 index 662064b0..30dd000c --- a/FastapiOpenRestyConfigurator/app/main/views/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/views/backend.py @@ -46,6 +46,21 @@ async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_ap raise HTTPException(status_code=400) +@router.post( + "/backends/{backend_id}/auth/{enable_auth}", + tags=["Backends"], + summary="Set owner authorization to true/false for an existing backend." +) +async def backend_update_auth(backend_id: int, enable_auth: bool, api_key: APIKey = Depends(get_api_key)): + backend_id = int(secure_filename(str(backend_id))) + logger.info(f"Updating backend authorization for backend id: ${backend_id}") + ok = await backend_service.update_backend_authorization(backend_id, enable_auth) + if not ok: + raise HTTPException(status_code=404, detail="Backend not found or update failed.") + return {"error": "Backend not found or update failed."} + return {"auth": f"{str(enable_auth).lower()}"} + + @router.get( "/backends/{backend_id}", response_model=BackendOut, diff --git a/FastapiOpenRestyConfigurator/app/main/views/template.py b/FastapiOpenRestyConfigurator/app/main/views/template.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/views/user.py b/FastapiOpenRestyConfigurator/app/main/views/user.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/app/main/views/utils.py b/FastapiOpenRestyConfigurator/app/main/views/utils.py old mode 100644 new mode 100755 diff --git a/FastapiOpenRestyConfigurator/gunicorn_conf.py b/FastapiOpenRestyConfigurator/gunicorn_conf.py new file mode 100755 index 00000000..d3db1294 --- /dev/null +++ b/FastapiOpenRestyConfigurator/gunicorn_conf.py @@ -0,0 +1,11 @@ +# Socket Path +bind = "unix:/var/run/forc.sock" + +# Worker Options +workers = 5 +worker_class = "uvicorn.workers.UvicornWorker" + +# Logging Options +loglevel = "info" +accesslog = "/var/log/forc.access.log" +errorlog = "/var/log/forc.error.log" diff --git a/FastapiOpenRestyConfigurator/main.py b/FastapiOpenRestyConfigurator/main.py old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/docker/Dockerfile b/docker/Dockerfile old mode 100644 new mode 100755 diff --git a/docker/README.md b/docker/README.md old mode 100644 new mode 100755 diff --git a/docker/launch.sh b/docker/launch.sh old mode 100644 new mode 100755 diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini old mode 100644 new mode 100755 diff --git a/examples/openresty_configuration.md b/examples/openresty_configuration.md old mode 100644 new mode 100755 diff --git a/examples/scripts/user_service.lua b/examples/scripts/user_service.lua old mode 100644 new mode 100755 diff --git a/examples/templates/cwlab%v01.conf b/examples/templates/cwlab%v01.conf old mode 100644 new mode 100755 index f46e9337..374631e2 --- a/examples/templates/cwlab%v01.conf +++ b/examples/templates/cwlab%v01.conf @@ -14,9 +14,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } rewrite /{{ key_url }}/(.*) /$1 break; diff --git a/examples/templates/cwlab%v02.conf b/examples/templates/cwlab%v02.conf old mode 100644 new mode 100755 index 69f72438..4f3a3f16 --- a/examples/templates/cwlab%v02.conf +++ b/examples/templates/cwlab%v02.conf @@ -31,9 +31,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) ngx.req.set_header("X-Auth-Email", res.id_token.email) diff --git a/examples/templates/emgb%v01.conf b/examples/templates/emgb%v01.conf old mode 100644 new mode 100755 index e70ea0eb..61412b62 --- a/examples/templates/emgb%v01.conf +++ b/examples/templates/emgb%v01.conf @@ -30,10 +30,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) ngx.req.set_header("X-Auth-Email", res.id_token.email) diff --git a/examples/templates/guacamole%v01.conf b/examples/templates/guacamole%v01.conf old mode 100644 new mode 100755 index 87b8bc30..2e734eae --- a/examples/templates/guacamole%v01.conf +++ b/examples/templates/guacamole%v01.conf @@ -14,9 +14,11 @@ location /{{ key_url }}/ { end -- Protect this location and allow only one specific ELIXIR User - if res.id_token.sub ~= "{{ owner }}" then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } diff --git a/examples/templates/guacamole%v02.conf b/examples/templates/guacamole%v02.conf old mode 100644 new mode 100755 index a6322a79..4a28b1e1 --- a/examples/templates/guacamole%v02.conf +++ b/examples/templates/guacamole%v02.conf @@ -15,9 +15,11 @@ location /{{ key_url }}/ { end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } diff --git a/examples/templates/guacamole%v03.conf b/examples/templates/guacamole%v03.conf old mode 100644 new mode 100755 index 34d8286d..75f1ab22 --- a/examples/templates/guacamole%v03.conf +++ b/examples/templates/guacamole%v03.conf @@ -29,9 +29,12 @@ end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) ngx.req.set_header("X-Auth-Email", res.id_token.email) ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) diff --git a/examples/templates/jupyterlab%v01.conf b/examples/templates/jupyterlab%v01.conf old mode 100644 new mode 100755 index 152bc08e..6517135b --- a/examples/templates/jupyterlab%v01.conf +++ b/examples/templates/jupyterlab%v01.conf @@ -13,9 +13,11 @@ location /{{ key_url }} { end -- Protect this location and allow only one specific ELIXIR User - if res.id_token.sub ~= "{{ owner }}" then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } proxy_pass {{ location_url }}; diff --git a/examples/templates/jupyterlab%v02.conf b/examples/templates/jupyterlab%v02.conf old mode 100644 new mode 100755 index 709c0548..874267c2 --- a/examples/templates/jupyterlab%v02.conf +++ b/examples/templates/jupyterlab%v02.conf @@ -14,9 +14,11 @@ location /{{ key_url }} { end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. diff --git a/examples/templates/jupyterlab%v03.conf b/examples/templates/jupyterlab%v03.conf old mode 100644 new mode 100755 index bd6a4288..9684fcd8 --- a/examples/templates/jupyterlab%v03.conf +++ b/examples/templates/jupyterlab%v03.conf @@ -29,9 +29,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) ngx.req.set_header("X-Auth-Email", res.id_token.email) diff --git a/examples/templates/rstudio%v01.conf b/examples/templates/rstudio%v01.conf old mode 100644 new mode 100755 index ccd0f964..a939e9f8 --- a/examples/templates/rstudio%v01.conf +++ b/examples/templates/rstudio%v01.conf @@ -11,9 +11,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if res.id_token.sub ~= "{{ owner }}" then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } diff --git a/examples/templates/rstudio%v02.conf b/examples/templates/rstudio%v02.conf old mode 100644 new mode 100755 index c4de14e8..85d09ad6 --- a/examples/templates/rstudio%v02.conf +++ b/examples/templates/rstudio%v02.conf @@ -10,10 +10,12 @@ ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - -- Protect this location and allow only one specific ELIXIR User - if res.id_token.sub ~= "{{ owner }}" then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + -- Protect this location and allow only one specific ELIXIR User + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } diff --git a/examples/templates/rstudio%v03.conf b/examples/templates/rstudio%v03.conf old mode 100644 new mode 100755 index ab106682..7975047c --- a/examples/templates/rstudio%v03.conf +++ b/examples/templates/rstudio%v03.conf @@ -13,10 +13,12 @@ ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + -- Protect this location and allow only one specific ELIXIR User + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } diff --git a/examples/templates/rstudio%v04.conf b/examples/templates/rstudio%v04.conf old mode 100644 new mode 100755 index b2adfcea..25f4cfcd --- a/examples/templates/rstudio%v04.conf +++ b/examples/templates/rstudio%v04.conf @@ -29,9 +29,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) ngx.req.set_header("X-Auth-Email", res.id_token.email) diff --git a/examples/templates/theiaide%v01.conf b/examples/templates/theiaide%v01.conf old mode 100644 new mode 100755 index 814e0e22..3be18f79 --- a/examples/templates/theiaide%v01.conf +++ b/examples/templates/theiaide%v01.conf @@ -12,9 +12,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if res.id_token.sub ~= "{{ owner }}" then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. diff --git a/examples/templates/theiaide%v02.conf b/examples/templates/theiaide%v02.conf old mode 100644 new mode 100755 index 3e8492d8..be886177 --- a/examples/templates/theiaide%v02.conf +++ b/examples/templates/theiaide%v02.conf @@ -14,9 +14,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. diff --git a/examples/templates/theiaide%v03.conf b/examples/templates/theiaide%v03.conf old mode 100644 new mode 100755 index 7e079a11..2f1d2ed3 --- a/examples/templates/theiaide%v03.conf +++ b/examples/templates/theiaide%v03.conf @@ -1,5 +1,4 @@ - # PROTECT FIRST THEIA CONTAINER - location /{{ key_url }}/ { + location /{{ key_url }} { set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier set $session_storage shm; # use shared memory set $session_cookie_persistent on; # persist cookie between browser sessions @@ -19,6 +18,11 @@ set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) access_by_lua_block { + # temporary logging + local ONLY_ALLOW_OWNER = {{ 'true' if only_allow_owner else 'false' }} + ngx.log(ngx.NOTICE, "ONLY_ALLOW_OWNER(runtime)=", tostring(ONLY_ALLOW_OWNER), type(ONLY_ALLOW_OWNER)) + ngx.log(ngx.NOTICE, "only_allow_owner(runtime)=", tostring(only_allow_owner), type(only_allow_owner)) + local user_service = require("user_service") -- Start actual openid authentication procedure local res, err = require("resty.openidc").authenticate(opts2) @@ -29,10 +33,15 @@ ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end + # JINJA_ONLY_ALLOW_OWNER={{ only_allow_owner | tojson }} -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner is true %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN + {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) ngx.req.set_header("X-Auth-Email", res.id_token.email) @@ -44,19 +53,22 @@ ngx.req.set_header("X-Auth-Locale", res.id_token.locale) } - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - rewrite /{{ key_url }}/(.*) /$1 break; - proxy_pass {{ location_url }}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; - access_log logs/code.access.log; - error_log logs/code.error.log; - } \ No newline at end of file + # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. + + proxy_pass {{ location_url }}; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 20d; + proxy_set_header X-Scheme $scheme; + + client_max_body_size 0; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Referrer-Policy "same-origin" always; + + } diff --git a/examples/templates/vscode%v03.conf b/examples/templates/vscode%v03.conf old mode 100644 new mode 100755 index aae021be..61412b62 --- a/examples/templates/vscode%v03.conf +++ b/examples/templates/vscode%v03.conf @@ -30,9 +30,11 @@ end -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) ngx.req.set_header("X-Auth-Email", res.id_token.email) diff --git a/examples/templating_guide.md b/examples/templating_guide.md old mode 100644 new mode 100755 index 2911786c..647c8a36 --- a/examples/templating_guide.md +++ b/examples/templating_guide.md @@ -29,19 +29,21 @@ This is an example Template for the research environment [RStudio](https://rstud location /{{ key_url }}/ { # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if res.id_token.sub ~= "{{ owner }}" then - ngx.exit(ngx.HTTP_FORBIDDEN) - end + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + + -- Protect this location and allow only one specific ELIXIR User + {% if only_allow_owner %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif %} } diff --git a/gfx/forc_overview.drawio b/gfx/forc_overview.drawio old mode 100644 new mode 100755 diff --git a/gfx/forc_overview.png b/gfx/forc_overview.png old mode 100644 new mode 100755 diff --git a/renovate.json b/renovate.json old mode 100644 new mode 100755 From bad4cb7d93a4eb04c1a43561bf4f8777055b66a3 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 4 Nov 2025 14:46:37 +0000 Subject: [PATCH 002/118] fixed issue with theiaide and rstudio, should be working now --- examples/templates/rstudio%v02.conf | 2 +- examples/templates/theiaide%v03.conf | 36 ++++++++++++---------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/examples/templates/rstudio%v02.conf b/examples/templates/rstudio%v02.conf index 85d09ad6..8e2d0b65 100755 --- a/examples/templates/rstudio%v02.conf +++ b/examples/templates/rstudio%v02.conf @@ -22,7 +22,7 @@ rewrite ^/{{ key_url }}/(.*)$ /$1 break; proxy_pass {{ location_url }}; proxy_redirect {{ location_url }} $scheme://$http_host/{{ key_url }}/; - proxy_http_version 1.1; + proxy_http_version 1.1; @ reviewer: we have the same here. is this necessary? proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 20d; diff --git a/examples/templates/theiaide%v03.conf b/examples/templates/theiaide%v03.conf index 2f1d2ed3..8d24ae93 100755 --- a/examples/templates/theiaide%v03.conf +++ b/examples/templates/theiaide%v03.conf @@ -18,11 +18,6 @@ set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) access_by_lua_block { - # temporary logging - local ONLY_ALLOW_OWNER = {{ 'true' if only_allow_owner else 'false' }} - ngx.log(ngx.NOTICE, "ONLY_ALLOW_OWNER(runtime)=", tostring(ONLY_ALLOW_OWNER), type(ONLY_ALLOW_OWNER)) - ngx.log(ngx.NOTICE, "only_allow_owner(runtime)=", tostring(only_allow_owner), type(only_allow_owner)) - local user_service = require("user_service") -- Start actual openid authentication procedure local res, err = require("resty.openidc").authenticate(opts2) @@ -33,7 +28,6 @@ ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - # JINJA_ONLY_ALLOW_OWNER={{ only_allow_owner | tojson }} -- Protect this location and allow only one specific ELIXIR User {% if only_allow_owner is true %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then @@ -53,22 +47,22 @@ ngx.req.set_header("X-Auth-Locale", res.id_token.locale) } - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - - proxy_pass {{ location_url }}; - proxy_http_version 1.1; + # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. + rewrite /{{ key_url }}/(.*) /$1 break; + proxy_pass {{ location_url }}; + proxy_http_version 1.1; # @reviewer: is this needed? - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; - proxy_set_header X-Scheme $scheme; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 20d; + proxy_set_header X-Scheme $scheme; - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; + client_max_body_size 0; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Referrer-Policy "same-origin" always; } From 1d604ff8932b634953ec5dc5c29e1882122be3d4 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Wed, 5 Nov 2025 18:19:08 +0000 Subject: [PATCH 003/118] added get_backends_by_id(int id) and implemented it in update_backend_authorization --- .../app/main/service/backend.py | 54 +++++++------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 9291c6a4..e3fadd4e 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -54,6 +54,14 @@ async def get_backends() -> List[BackendOut]: return valid_backends +async def get_backends_by_id(backend_id: int) -> BackendOut: + valid_backends: List[BackendOut] = await get_backends() + for backend in valid_backends: + if int(backend.id) == int(backend_id): + return backend + raise NotFound(f"Backend {backend_id} was not found.") + + async def get_backends_upstream_urls() -> Dict[str, List[BackendOut]]: valid_backends: List[BackendOut] = await get_backends() upstream_urls = {} @@ -159,35 +167,28 @@ async def delete_backend(backend_id) -> bool: async def update_backend_authorization(backend_id: int, auth_enable: bool) -> bool: - # look up backend by id - backends = await get_backends() - target = next((b for b in backends if int(b.id) == int(backend_id)), None) - if not target: - logger.warning(f"Backend {backend_id} not found for auth-switch.") - return False + backend = await get_backends_by_id(backend_id) # extract upstream_url from file - upstream_url = extract_proxy_pass(target.file_path) + upstream_url = extract_proxy_pass(backend.file_path) if not upstream_url: - logger.error(f"Could not extract proxy_pass from {target.file_path}") + logger.error(f"Could not extract proxy_pass from {backend.file_path}") return False # get existing location_url and user_key_url try: - base_key, suffix = target.location_url.rsplit("_", 1) + base_key, suffix = backend.location_url.rsplit("_", 1) except ValueError: - logger.error(f"location_url has no suffix pattern: {target.location_url}") + logger.error(f"location_url has no suffix pattern: {backend.location_url}") return False logger.info(f"Base key: {base_key}, Suffix: {suffix}") # build new temp payload try: temp_payload = BackendIn( - #id=str(backend_id), - owner=target.owner, - #location_url=target.location_url, - template=target.template, - template_version=target.template_version, + owner=backend.owner, + template=backend.template, + template_version=backend.template_version, user_key_url=base_key, upstream_url=upstream_url, only_allow_owner=auth_enable, # set new auth flag @@ -197,29 +198,10 @@ async def update_backend_authorization(backend_id: int, auth_enable: bool) -> bo return False logger.info(f"temp_payload: {temp_payload}") - # generate new backend contents - new_contents = await create_backend(temp_payload, id=str(backend_id), location_url=target.location_url) + # generate new backend contents with additional kwargs to persist backend_id and location_url + new_contents = await create_backend(temp_payload, id=str(backend_id), location_url=backend.location_url) logger.info(f"New contents: {new_contents}") if not new_contents: logger.error("Templating returned empty result.") return False return True - - """ safely write new contents to file - tmp_path = f"{target.file_path}.tmp" - try: - with open(tmp_path, "w") as f: - f.write(new_contents) - os.replace(tmp_path, target.file_path) # atomic replace, no downtime - except OSError as e: - logger.exception(f"Failed to write/replace backend file: {e}") - try: - if os.path.exists(tmp_path): - os.remove(tmp_path) - except OSError: - pass - return False - - # reload openResty - await reload_openresty() - return True""" From ee09ea54d9a925f912161f8e0b677649698c88d1 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Wed, 5 Nov 2025 18:39:38 +0000 Subject: [PATCH 004/118] backend_update_auth now has different exception responses (404/500) for different edge cases --- .../app/main/views/backend.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/views/backend.py b/FastapiOpenRestyConfigurator/app/main/views/backend.py index 30dd000c..6d475fb4 100755 --- a/FastapiOpenRestyConfigurator/app/main/views/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/views/backend.py @@ -53,11 +53,13 @@ async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_ap ) async def backend_update_auth(backend_id: int, enable_auth: bool, api_key: APIKey = Depends(get_api_key)): backend_id = int(secure_filename(str(backend_id))) - logger.info(f"Updating backend authorization for backend id: ${backend_id}") - ok = await backend_service.update_backend_authorization(backend_id, enable_auth) - if not ok: - raise HTTPException(status_code=404, detail="Backend not found or update failed.") - return {"error": "Backend not found or update failed."} + logger.info(f"Updating backend authorization for backend id: {backend_id}") + try: + await backend_service.update_backend_authorization(backend_id, enable_auth) + except NotFound: + raise HTTPException(status_code=404, detail="Backend not found.") + except InternalServerError: + raise HTTPException(status_code=500, detail="Internal server error.") return {"auth": f"{str(enable_auth).lower()}"} From b4344725b98d4fc22a7fe932d475f8af789ac464 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Thu, 13 Nov 2025 09:29:44 +0000 Subject: [PATCH 005/118] minor corrections --- FastapiOpenRestyConfigurator/app/main/config.py | 4 ++-- .../app/main/model/serializers.py | 14 +++++++------- .../app/main/service/backend.py | 14 ++++++++------ .../app/main/util/templating.py | 6 +++--- .../app/main/views/backend.py | 17 +++++++++++------ 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/config.py b/FastapiOpenRestyConfigurator/app/main/config.py index 964dd770..2c861703 100755 --- a/FastapiOpenRestyConfigurator/app/main/config.py +++ b/FastapiOpenRestyConfigurator/app/main/config.py @@ -12,8 +12,8 @@ class Settings(BaseSettings): Reads settings from .env file. """ FORC_VERSION: str = '0.2' - DEBUG: bool = True #temporary !!!!!!!!! - LOG_LEVEL: str = "DEBUG" #temporary !!!!!!!!! + DEBUG: bool = False + LOG_LEVEL: str = "INFO" FORC_API_KEY: SecretStr FORC_SECRET_KEY: SecretStr = 'my_precious_secret_key' FORC_BACKEND_PATH: DirectoryPath diff --git a/FastapiOpenRestyConfigurator/app/main/model/serializers.py b/FastapiOpenRestyConfigurator/app/main/model/serializers.py index 0bbdd696..370dc352 100755 --- a/FastapiOpenRestyConfigurator/app/main/model/serializers.py +++ b/FastapiOpenRestyConfigurator/app/main/model/serializers.py @@ -53,6 +53,12 @@ class BackendBase(BaseModel): description="Version of the template the backend refers to.", example="v04" ) + auth_enabled: bool = Field( + True, + title="Authorization for the research environment", + description="If set to true, only the owner of the backend is allowed to access it.", + example=False + ) @validator("owner") def owner_validation(cls, owner): @@ -84,12 +90,6 @@ class BackendIn(BackendBase): description="Inject the full url (with protocol) for the real location of the backend service in the template.", example="http://192.168.0.1:8787/" ) - only_allow_owner: bool = Field( - True, - title="Authorization for the research environment", - description="If set to true, only the owner of the backend is allowed to access it.", - example=False - ) @validator("user_key_url") def user_key_url_validation(cls, v): @@ -149,7 +149,7 @@ class BackendTemp(BackendIn, BackendOut): template_version: str = None user_key_url: str = None upstream_url: str = None - only_allow_owner: bool = None + auth_enabled: bool = None class Template(BaseModel): diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index e3fadd4e..a8c9b6c9 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -54,7 +54,7 @@ async def get_backends() -> List[BackendOut]: return valid_backends -async def get_backends_by_id(backend_id: int) -> BackendOut: +async def get_backend_by_id(backend_id: int) -> BackendOut: valid_backends: List[BackendOut] = await get_backends() for backend in valid_backends: if int(backend.id) == int(backend_id): @@ -151,6 +151,8 @@ async def delete_backend(backend_id) -> bool: for file in backend_path_files: match = re.fullmatch(file_regex, file) if not match: + if file == "users" or file == "scripts": + continue logger.warning(f"Found a backend file with wrong naming, skipping it: {file}") continue if int(match.group(1)) == int(backend_id): @@ -163,11 +165,11 @@ async def delete_backend(backend_id) -> bool: except OSError as e: logger.warning(f"Was not able to delete backend with id: {backend_id} ERROR: {e}") raise InternalServerError("Server was not able to delete this backend. Contact the admin.") - raise NotFound("Backend was not found.") + raise NotFound(f"Backend {backend_id} was not found.") -async def update_backend_authorization(backend_id: int, auth_enable: bool) -> bool: - backend = await get_backends_by_id(backend_id) +async def update_backend_authorization(backend_id: int, auth_enable: bool): + backend = await get_backend_by_id(backend_id) # extract upstream_url from file upstream_url = extract_proxy_pass(backend.file_path) @@ -191,7 +193,7 @@ async def update_backend_authorization(backend_id: int, auth_enable: bool) -> bo template_version=backend.template_version, user_key_url=base_key, upstream_url=upstream_url, - only_allow_owner=auth_enable, # set new auth flag + auth_enabled=auth_enable, # set new auth flag ) except Exception as e: logger.error(f"Error building temp payload for backend update: {e}") @@ -204,4 +206,4 @@ async def update_backend_authorization(backend_id: int, auth_enable: bool) -> bo if not new_contents: logger.error("Templating returned empty result.") return False - return True + return new_contents diff --git a/FastapiOpenRestyConfigurator/app/main/util/templating.py b/FastapiOpenRestyConfigurator/app/main/util/templating.py index 8d855f8f..5d3d8220 100755 --- a/FastapiOpenRestyConfigurator/app/main/util/templating.py +++ b/FastapiOpenRestyConfigurator/app/main/util/templating.py @@ -32,8 +32,8 @@ async def generate_backend_by_template(backend_temp: BackendTemp, suffix_number) logger.info({ "event": "templating_vars", - "only_allow_owner_value": backend_temp.only_allow_owner, - "only_allow_owner_type": type(backend_temp.only_allow_owner).__name__, + "auth_enabled_value": backend_temp.auth_enabled, + "auth_enabled_type": type(backend_temp.auth_enabled).__name__, "template": assembled_template_file_name, }) logger.info(f"template: {template}") @@ -44,6 +44,6 @@ async def generate_backend_by_template(backend_temp: BackendTemp, suffix_number) backend_id=backend_temp.id, forc_backend_path=settings.FORC_BACKEND_PATH, location_url=backend_temp.upstream_url, - only_allow_owner=backend_temp.only_allow_owner + auth_enabled=backend_temp.auth_enabled ) return rendered_backend diff --git a/FastapiOpenRestyConfigurator/app/main/views/backend.py b/FastapiOpenRestyConfigurator/app/main/views/backend.py index 6d475fb4..15dac9b6 100755 --- a/FastapiOpenRestyConfigurator/app/main/views/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/views/backend.py @@ -4,7 +4,7 @@ import logging from typing import List -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Body from fastapi.responses import JSONResponse from fastapi.openapi.models import APIKey from werkzeug.exceptions import NotFound, InternalServerError @@ -47,20 +47,25 @@ async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_ap @router.post( - "/backends/{backend_id}/auth/{enable_auth}", + "/backends/{backend_id}/auth/", + response_model=BackendOut, tags=["Backends"], summary="Set owner authorization to true/false for an existing backend." ) -async def backend_update_auth(backend_id: int, enable_auth: bool, api_key: APIKey = Depends(get_api_key)): +async def backend_update_auth(backend_id: int, body: dict = Body(...), api_key: APIKey = Depends(get_api_key)): backend_id = int(secure_filename(str(backend_id))) logger.info(f"Updating backend authorization for backend id: {backend_id}") + enable_auth = body.get("auth_enabled", None) + if enable_auth is None or not isinstance(enable_auth, bool): + logger.error(f"auth_enabled is required and must be a boolean, backend_id: {backend_id}, auth_enabled: {enable_auth}, {type(enable_auth)}") + raise HTTPException(status_code=422, + detail=f"auth_enabled is required and must be a boolean, backend_id: {backend_id}, auth_enabled: {enable_auth}, {type(enable_auth)}") try: - await backend_service.update_backend_authorization(backend_id, enable_auth) + return await backend_service.update_backend_authorization(backend_id, enable_auth) except NotFound: - raise HTTPException(status_code=404, detail="Backend not found.") + raise HTTPException(status_code=404, detail=f"Backend with id {backend_id} not found.") except InternalServerError: raise HTTPException(status_code=500, detail="Internal server error.") - return {"auth": f"{str(enable_auth).lower()}"} @router.get( From 1601e3bff43cc9191e458cd9defbb76927f849b1 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Thu, 27 Nov 2025 00:15:47 +0000 Subject: [PATCH 006/118] implemented and fixed switch function with auth status output. minor refactors for readability, changes to upstream_url_regex --- .../app/main/model/serializers.py | 4 +- .../app/main/service/backend.py | 68 +++++++++++-------- .../app/main/util/templating.py | 31 +++++---- .../app/main/views/backend.py | 4 +- 4 files changed, 61 insertions(+), 46 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/model/serializers.py b/FastapiOpenRestyConfigurator/app/main/model/serializers.py index 370dc352..717f089f 100755 --- a/FastapiOpenRestyConfigurator/app/main/model/serializers.py +++ b/FastapiOpenRestyConfigurator/app/main/model/serializers.py @@ -28,7 +28,7 @@ owner_regex = r'^[a-zA-Z0-9@.-]{30,}$' user_key_url_regex = r"^[a-zA-Z0-9_-]{3,25}$" -upstream_url_regex = r"^(https?)://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{1,5})$" +upstream_url_regex = r"^(https?)://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{1,5})(/[a-zA-Z0-9_-]+/)?$" class BackendBase(BaseModel): @@ -88,7 +88,7 @@ class BackendIn(BackendBase): ..., title="Upstream URL", description="Inject the full url (with protocol) for the real location of the backend service in the template.", - example="http://192.168.0.1:8787/" + example="http://192.168.0.1:8787/guacamole/" ) @validator("user_key_url") diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index a8c9b6c9..4c03030c 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -17,7 +17,9 @@ logger = logging.getLogger("service") settings = get_settings() -file_regex = r"(\d*)%([a-z0-9\-\@.]*?)%([^%]*)%([^%]*)%([^%]*)\.conf" +# format of filename saves information of BackendOut by this schema: +# {id}%{owner}%{location_url}%{template}%{template_version}%{auth_enabled}.conf +filename_regex = r"(\d*)%([a-z0-9\-\@.]*?)%([^%]*)%([^%]*)%([^%]*)%([01])\.conf" async def random_with_n_digits(n): @@ -33,23 +35,25 @@ async def get_backends() -> List[BackendOut]: if len(os.listdir(settings.FORC_BACKEND_PATH)) == 0: return [] backend_path_files = os.listdir(settings.FORC_BACKEND_PATH) - logger.info(backend_path_files) + logger.info(f"Files in backend_path: {backend_path_files}") valid_backends = [] for file in backend_path_files: - match = re.fullmatch(file_regex, file) + match = re.fullmatch(filename_regex, file) if not match: if file == "users" or file == "scripts": continue logger.warning("Found a backend file with wrong naming, skipping it: " + str(file)) continue backend: BackendOut = BackendOut( - id=match.group(1), - owner=match.group(2), - location_url=match.group(3), - template=match.group(4), - template_version=match.group(5), - file_path=os.path.join(settings.FORC_BACKEND_PATH, file) + id = match.group(1), + owner = match.group(2), + location_url = match.group(3), + template = match.group(4), + template_version = match.group(5), + auth_enabled = bool(int(match.group(6))), + file_path = os.path.join(settings.FORC_BACKEND_PATH, file) ) + logger.debug(f"Discovered backend: {backend}") valid_backends.append(backend) return valid_backends @@ -108,30 +112,34 @@ async def generate_suffix_number(user_key_url): return str(highest_id + 1) -async def create_backend(payload: BackendIn, **kwargs): +async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: + logger.debug(f"Creating backend for owner: {payload.owner} with template: {payload.template} version: {payload.template_version}") + # build payload as BackendTemp for generate_backend_by_template() payload: BackendTemp = BackendTemp(**payload.dict()) - suffix_number = await generate_suffix_number(payload.user_key_url) - payload.id = str(await random_with_n_digits(10)) - if 'id' in kwargs: # override id and suffix if provided + if 'id' in kwargs: # override id and suffix if provided from update_backend_authorization() payload = payload.copy(update={'id': str(kwargs.get('id'))}) suffix_number = str(kwargs.get('location_url')).split("_")[1] + else: + suffix_number = await generate_suffix_number(payload.user_key_url) + payload.id = str(await random_with_n_digits(10)) # TODO: should we refactor id: int? see delete_backend() + # generate backend and location_url backend_file_contents = await generate_backend_by_template(payload, suffix_number) if not backend_file_contents: raise InternalServerError("Server was not able to template a new backend.") payload.location_url = f"{payload.user_key_url}_{suffix_number}" - # check for duplicated in ip and port: + # check for duplicated in ip and port upstream_urls: Dict[str, List[BackendOut]] = await get_backends_upstream_urls() matching_urls_backends: List[BackendOut] = upstream_urls.get(payload.upstream_url, []) for backend in matching_urls_backends: logger.info(f"Deleting existing Backend with same Upstream Url - {payload.upstream_url}") await delete_backend(backend.id) - # create backend file in filesystem - filename = f"{payload.id}%{payload.owner}%{payload.location_url}%{payload.template}%{payload.template_version}.conf" + # create backend file in filesystem, save BackendOut info in filename + filename = f"{payload.id}%{payload.owner}%{payload.location_url}%{payload.template}%{payload.template_version}%{str(int(payload.auth_enabled))}.conf" with open(f"{settings.FORC_BACKEND_PATH}/{filename}", 'w') as backend_file: backend_file.write(backend_file_contents) @@ -149,7 +157,7 @@ async def delete_backend(backend_id) -> bool: return False backend_path_files = os.listdir(settings.FORC_BACKEND_PATH) for file in backend_path_files: - match = re.fullmatch(file_regex, file) + match = re.fullmatch(filename_regex, file) if not match: if file == "users" or file == "scripts": continue @@ -168,9 +176,14 @@ async def delete_backend(backend_id) -> bool: raise NotFound(f"Backend {backend_id} was not found.") -async def update_backend_authorization(backend_id: int, auth_enable: bool): - backend = await get_backend_by_id(backend_id) +async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> BackendTemp | bool: + try: + backend: BackendOut = await get_backend_by_id(backend_id) + except NotFound as e: + logger.error(f"Backend with id {backend_id} not found for authorization update.") + raise e + # build temp payload as BackendIn for create_backend() # extract upstream_url from file upstream_url = extract_proxy_pass(backend.file_path) if not upstream_url: @@ -183,27 +196,28 @@ async def update_backend_authorization(backend_id: int, auth_enable: bool): except ValueError: logger.error(f"location_url has no suffix pattern: {backend.location_url}") return False - logger.info(f"Base key: {base_key}, Suffix: {suffix}") + logger.debug(f"Backend id: {backend.id}, Base key: {base_key}, Suffix: {suffix}") # build new temp payload try: temp_payload = BackendIn( - owner=backend.owner, - template=backend.template, - template_version=backend.template_version, - user_key_url=base_key, - upstream_url=upstream_url, - auth_enabled=auth_enable, # set new auth flag + owner = backend.owner, + template = backend.template, + template_version = backend.template_version, + user_key_url = base_key, + upstream_url = upstream_url, + auth_enabled = auth_enabled, # set new auth flag ) except Exception as e: logger.error(f"Error building temp payload for backend update: {e}") return False logger.info(f"temp_payload: {temp_payload}") - # generate new backend contents with additional kwargs to persist backend_id and location_url + # generate new backend contents with temp_payload and additional kwargs to persist backend_id and location_url new_contents = await create_backend(temp_payload, id=str(backend_id), location_url=backend.location_url) logger.info(f"New contents: {new_contents}") if not new_contents: logger.error("Templating returned empty result.") return False + logger.info(f"Updated backend authorization to {temp_payload.auth_enabled} for backend id: {backend_id}") return new_contents diff --git a/FastapiOpenRestyConfigurator/app/main/util/templating.py b/FastapiOpenRestyConfigurator/app/main/util/templating.py index 5d3d8220..9ebb6187 100755 --- a/FastapiOpenRestyConfigurator/app/main/util/templating.py +++ b/FastapiOpenRestyConfigurator/app/main/util/templating.py @@ -20,30 +20,31 @@ logger.error("Was not able to load template engine. Adjust the templates_path in the config.") -async def generate_backend_by_template(backend_temp: BackendTemp, suffix_number): +async def generate_backend_by_template(backend_temp: BackendTemp, suffix_number) -> str | None: + logger.debug(f"Generating backend from template: {backend_temp.template} with version: {backend_temp.template_version}") if not templateLoader or not templateEnv: logger.error("The template engine is not loaded. Can't generate backend.") return None - assembled_template_file_name = f"{backend_temp.template}%{backend_temp.template_version}.conf" - if not os.path.isfile(f"{settings.FORC_TEMPLATE_PATH}/{assembled_template_file_name}"): - logger.error(f"Not able to find {settings.FORC_TEMPLATE_PATH}/{assembled_template_file_name}") + assembled_template_filename = f"{backend_temp.template}%{backend_temp.template_version}.conf" + if not os.path.isfile(f"{settings.FORC_TEMPLATE_PATH}/{assembled_template_filename}"): + logger.error(f"Not able to find {settings.FORC_TEMPLATE_PATH}/{assembled_template_filename}") return None - template = templateEnv.get_template(assembled_template_file_name) + template = templateEnv.get_template(assembled_template_filename) logger.info({ "event": "templating_vars", - "auth_enabled_value": backend_temp.auth_enabled, - "auth_enabled_type": type(backend_temp.auth_enabled).__name__, - "template": assembled_template_file_name, + "auth_enabled": backend_temp.auth_enabled, + "assembled template filename": assembled_template_filename, + "template": template }) - logger.info(f"template: {template}") rendered_backend = template.render( - key_url=f"{backend_temp.user_key_url}_{suffix_number}", - owner=backend_temp.owner, - backend_id=backend_temp.id, - forc_backend_path=settings.FORC_BACKEND_PATH, - location_url=backend_temp.upstream_url, - auth_enabled=backend_temp.auth_enabled + key_url = f"{backend_temp.user_key_url}_{suffix_number}", + owner = backend_temp.owner, + backend_id = backend_temp.id, + forc_backend_path = settings.FORC_BACKEND_PATH, + location_url = backend_temp.upstream_url, + auth_enabled = backend_temp.auth_enabled ) + logger.debug(f"Rendered backend: {rendered_backend}") return rendered_backend diff --git a/FastapiOpenRestyConfigurator/app/main/views/backend.py b/FastapiOpenRestyConfigurator/app/main/views/backend.py index 15dac9b6..a93eda79 100755 --- a/FastapiOpenRestyConfigurator/app/main/views/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/views/backend.py @@ -53,9 +53,9 @@ async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_ap summary="Set owner authorization to true/false for an existing backend." ) async def backend_update_auth(backend_id: int, body: dict = Body(...), api_key: APIKey = Depends(get_api_key)): - backend_id = int(secure_filename(str(backend_id))) - logger.info(f"Updating backend authorization for backend id: {backend_id}") + backend_id = int(secure_filename(str(backend_id))) # TODO: do we need secure_filename for int? enable_auth = body.get("auth_enabled", None) + logger.debug(f"Updating backend authorization to {enable_auth} for backend id: {backend_id}") if enable_auth is None or not isinstance(enable_auth, bool): logger.error(f"auth_enabled is required and must be a boolean, backend_id: {backend_id}, auth_enabled: {enable_auth}, {type(enable_auth)}") raise HTTPException(status_code=422, From 60251d8103c074baf5f30b91cee8a3cb3a3bdb32 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Thu, 4 Dec 2025 03:53:52 +0000 Subject: [PATCH 007/118] refactored update backend authorization flow, changed return types, improved logging --- .../app/main/service/backend.py | 48 ++++++++++++++----- .../app/main/service/openresty.py | 2 +- .../app/main/util/templating.py | 2 +- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 4c03030c..edcb0147 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -114,8 +114,9 @@ async def generate_suffix_number(user_key_url): async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: logger.debug(f"Creating backend for owner: {payload.owner} with template: {payload.template} version: {payload.template_version}") - # build payload as BackendTemp for generate_backend_by_template() - payload: BackendTemp = BackendTemp(**payload.dict()) + + # overwrite payload as BackendTemp for generate_backend_by_template() + payload: BackendTemp = BackendTemp(**payload.dict()) # TODO: is this cast necessary? if 'id' in kwargs: # override id and suffix if provided from update_backend_authorization() payload = payload.copy(update={'id': str(kwargs.get('id'))}) @@ -144,7 +145,7 @@ async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: with open(f"{settings.FORC_BACKEND_PATH}/{filename}", 'w') as backend_file: backend_file.write(backend_file_contents) - # attempt to reload openrest + # attempt to reload openresty await reload_openresty() return payload @@ -176,7 +177,7 @@ async def delete_backend(backend_id) -> bool: raise NotFound(f"Backend {backend_id} was not found.") -async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> BackendTemp | bool: +async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> BackendOut | bool: try: backend: BackendOut = await get_backend_by_id(backend_id) except NotFound as e: @@ -198,26 +199,49 @@ async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> B return False logger.debug(f"Backend id: {backend.id}, Base key: {base_key}, Suffix: {suffix}") - # build new temp payload + # build new temp payload as BackendIn try: - temp_payload = BackendIn( + payload = BackendIn( owner = backend.owner, template = backend.template, template_version = backend.template_version, user_key_url = base_key, - upstream_url = upstream_url, + upstream_url = "/".join(upstream_url.split("/", 3)[:3]), # remove potential trailing path, see guacamole template auth_enabled = auth_enabled, # set new auth flag ) except Exception as e: logger.error(f"Error building temp payload for backend update: {e}") return False - logger.info(f"temp_payload: {temp_payload}") + # logger.debug(f"payload: {payload}") # generate new backend contents with temp_payload and additional kwargs to persist backend_id and location_url - new_contents = await create_backend(temp_payload, id=str(backend_id), location_url=backend.location_url) - logger.info(f"New contents: {new_contents}") + new_contents = await create_backend(payload, id=str(backend_id), location_url=backend.location_url) + logger.debug(f"New contents: {new_contents}") if not new_contents: logger.error("Templating returned empty result.") return False - logger.info(f"Updated backend authorization to {temp_payload.auth_enabled} for backend id: {backend_id}") - return new_contents + + logger.info(f"Updated backend authorization to {payload.auth_enabled} for backend id: {backend_id}") + + # convert BackendTemp to BackendOut for returning + returning_backend: BackendOut = BackendOut( + id = new_contents.id, + owner = new_contents.owner, + location_url = new_contents.location_url, + template = new_contents.template, + template_version = new_contents.template_version, + auth_enabled = new_contents.auth_enabled, + file_path = await get_file_path_by_id(new_contents.id) + ) + + return returning_backend + + +async def get_file_path_by_id(backend_id: int) -> str: + backends: List[BackendOut] = await get_backends() + logger.debug(f"Searching file path for backend id: {backend_id} in backends: {backends}") + for backend in backends: + if int(backend.id) == int(backend_id): + logger.debug(f"Returning found file path for backend id: {backend_id}: {backend.file_path}") + return backend.file_path + raise NotFound(f"Backend with id {backend_id} not found.") diff --git a/FastapiOpenRestyConfigurator/app/main/service/openresty.py b/FastapiOpenRestyConfigurator/app/main/service/openresty.py index e5053f4d..67d13e41 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/openresty.py +++ b/FastapiOpenRestyConfigurator/app/main/service/openresty.py @@ -12,6 +12,6 @@ async def reload_openresty(): logger.info("Reloading openresty config after backend change.") try: os.popen("sudo openresty -s reload") - logger.info("Reload succesful.") + logger.info("Reload successful.") except OSError as e: logger.exception(f"Was not able to reload OpenResty: {e}") diff --git a/FastapiOpenRestyConfigurator/app/main/util/templating.py b/FastapiOpenRestyConfigurator/app/main/util/templating.py index 9ebb6187..d7ecee7d 100755 --- a/FastapiOpenRestyConfigurator/app/main/util/templating.py +++ b/FastapiOpenRestyConfigurator/app/main/util/templating.py @@ -46,5 +46,5 @@ async def generate_backend_by_template(backend_temp: BackendTemp, suffix_number) location_url = backend_temp.upstream_url, auth_enabled = backend_temp.auth_enabled ) - logger.debug(f"Rendered backend: {rendered_backend}") + # logger.debug(f"Rendered backend: {rendered_backend}") return rendered_backend From 99f93fa83996fa333f39cbef7bbde93c3b9594c3 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Wed, 17 Dec 2025 08:12:13 +0000 Subject: [PATCH 008/118] started adding unit tests for views/backend.py and service/backend.py --- .../app/main/tests/test_backend.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 FastapiOpenRestyConfigurator/app/main/tests/test_backend.py diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_backend.py new file mode 100644 index 00000000..f6d7e173 --- /dev/null +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_backend.py @@ -0,0 +1,107 @@ +import pytest +from unittest.mock import patch + +from app.main.model.serializers import BackendOut + +from app.main.views import backend as backend_views +from app.main.service import backend as backend_service + +# views/backend.py + +@pytest.mark.asyncio +async def test_backend_update_auth_activate(): + + with patch( + "app.main.views.backend.backend_service.update_backend_authorization", + return_value = BackendOut( + id = 1, + auth_enabled = True, + ) + ) as mock_update_backend_authorization: + + res: BackendOut = await backend_views.backend_update_auth( + backend_id = 1, + body = {"auth_enabled": True}, + api_key = "test" + ) + + assert res.id == 1 + assert res.auth_enabled is True + mock_update_backend_authorization.assert_awaited_once_with(backend_id = 1, auth_enabled = True) + +@pytest.mark.asyncio +async def test_backend_update_auth_deactivate(): + + with patch( + "app.main.views.backend.backend_service.update_backend_authorization", + return_value = BackendOut( + id = 1, + auth_enabled = False, + ) + ) as mock_update_backend_authorization: + + res: BackendOut = await backend_views.backend_update_auth( + backend_id = 1, + body = {"auth_enabled": False}, + api_key = "test" + ) + + assert res.id == 1 + assert res.auth_enabled is False + mock_update_backend_authorization.assert_awaited_once_with(backend_id = 1, auth_enabled = False) + + +# service/backend.py + +@pytest.mark.asyncio +async def test_update_backend_authorization_activate(): + + with patch( + "app.main.service.backend.get_backend_by_id", + return_value = BackendOut( + id = 1234567890, + auth_enabled = False, + owner = "testuser", + template = "testtemplate", + template_version = "v01", + location_url = "animal_100", + file_path = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" + ) + ) as mock_get_backend_by_id, patch( + "app.main.service.backend.create_backend", + return_value = BackendOut( + id = 1234567890, + auth_enabled = True, + owner = "testuser", + template = "testtemplate", + template_version = "v01", + location_url = "animal_100", + file_path = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%1.conf" + ) + ) as mock_create_backend, patch( + "app.main.service.backend.extract_proxy_pass", + return_value = "https://example.com:1000/" + ) as mock_extract_proxy_pass, patch( + "app.main.service.backend.get_file_path_by_id", + return_value = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" + ) as mock_get_file_path_by_id: + + res: BackendOut = await backend_service.update_backend_authorization( + backend_id = 1234567890, + auth_enabled = True, + ) + + assert res.id == 1234567890 + assert res.auth_enabled is True + + mock_get_backend_by_id.assert_awaited_once_with(backend_id = 1234567890) + mock_get_file_path_by_id.assert_called_once_with(backend_id = 1234567890) + mock_extract_proxy_pass.assert_called_once() + mock_create_backend.assert_awaited_once() + +def test_get_backend_by_id(): + patch( + "app.main.service.backend.get_backends", + return_value = [BackendOut(id = 1)] + ): + assert await backend_service.get_backend_by_id(1).id == 1 \ No newline at end of file From eee4076f6c09d08ddc50ec6ed9f56cf4922ddf33 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Thu, 8 Jan 2026 15:51:49 +0000 Subject: [PATCH 009/118] unfinished, wrote unit tests for parts of service/backend.py and views/backend.py --- .../app/main/service/backend.py | 25 ++-- .../app/main/tests/test_backend.py | 107 ------------------ .../app/main/tests/test_service_backend.py | 87 ++++++++++++++ .../app/main/tests/test_views_backend.py | 72 ++++++++++++ 4 files changed, 172 insertions(+), 119 deletions(-) delete mode 100644 FastapiOpenRestyConfigurator/app/main/tests/test_backend.py create mode 100644 FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py create mode 100644 FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index edcb0147..6295972a 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -66,6 +66,17 @@ async def get_backend_by_id(backend_id: int) -> BackendOut: raise NotFound(f"Backend {backend_id} was not found.") +async def get_file_path_by_id(backend_id: int) -> str: + backends: List[BackendOut] = await get_backends() + logger.debug(f"Searching file path for backend id: {backend_id} in backends: {backends}") + for backend in backends: + if int(backend.id) == int(backend_id): + logger.debug(f"Returning found file path for backend id: {backend_id}: {backend.file_path}") + return backend.file_path + raise NotFound(f"Backend with id {backend_id} not found.") + + + async def get_backends_upstream_urls() -> Dict[str, List[BackendOut]]: valid_backends: List[BackendOut] = await get_backends() upstream_urls = {} @@ -116,7 +127,7 @@ async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: logger.debug(f"Creating backend for owner: {payload.owner} with template: {payload.template} version: {payload.template_version}") # overwrite payload as BackendTemp for generate_backend_by_template() - payload: BackendTemp = BackendTemp(**payload.dict()) # TODO: is this cast necessary? + payload: BackendTemp = BackendTemp(**payload.dict()) if 'id' in kwargs: # override id and suffix if provided from update_backend_authorization() payload = payload.copy(update={'id': str(kwargs.get('id'))}) @@ -139,7 +150,7 @@ async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: logger.info(f"Deleting existing Backend with same Upstream Url - {payload.upstream_url}") await delete_backend(backend.id) - # create backend file in filesystem, save BackendOut info in filename + # save BackendOut info in filename, create backend file in filesystem filename = f"{payload.id}%{payload.owner}%{payload.location_url}%{payload.template}%{payload.template_version}%{str(int(payload.auth_enabled))}.conf" with open(f"{settings.FORC_BACKEND_PATH}/{filename}", 'w') as backend_file: @@ -235,13 +246,3 @@ async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> B ) return returning_backend - - -async def get_file_path_by_id(backend_id: int) -> str: - backends: List[BackendOut] = await get_backends() - logger.debug(f"Searching file path for backend id: {backend_id} in backends: {backends}") - for backend in backends: - if int(backend.id) == int(backend_id): - logger.debug(f"Returning found file path for backend id: {backend_id}: {backend.file_path}") - return backend.file_path - raise NotFound(f"Backend with id {backend_id} not found.") diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_backend.py deleted file mode 100644 index f6d7e173..00000000 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_backend.py +++ /dev/null @@ -1,107 +0,0 @@ -import pytest -from unittest.mock import patch - -from app.main.model.serializers import BackendOut - -from app.main.views import backend as backend_views -from app.main.service import backend as backend_service - -# views/backend.py - -@pytest.mark.asyncio -async def test_backend_update_auth_activate(): - - with patch( - "app.main.views.backend.backend_service.update_backend_authorization", - return_value = BackendOut( - id = 1, - auth_enabled = True, - ) - ) as mock_update_backend_authorization: - - res: BackendOut = await backend_views.backend_update_auth( - backend_id = 1, - body = {"auth_enabled": True}, - api_key = "test" - ) - - assert res.id == 1 - assert res.auth_enabled is True - mock_update_backend_authorization.assert_awaited_once_with(backend_id = 1, auth_enabled = True) - -@pytest.mark.asyncio -async def test_backend_update_auth_deactivate(): - - with patch( - "app.main.views.backend.backend_service.update_backend_authorization", - return_value = BackendOut( - id = 1, - auth_enabled = False, - ) - ) as mock_update_backend_authorization: - - res: BackendOut = await backend_views.backend_update_auth( - backend_id = 1, - body = {"auth_enabled": False}, - api_key = "test" - ) - - assert res.id == 1 - assert res.auth_enabled is False - mock_update_backend_authorization.assert_awaited_once_with(backend_id = 1, auth_enabled = False) - - -# service/backend.py - -@pytest.mark.asyncio -async def test_update_backend_authorization_activate(): - - with patch( - "app.main.service.backend.get_backend_by_id", - return_value = BackendOut( - id = 1234567890, - auth_enabled = False, - owner = "testuser", - template = "testtemplate", - template_version = "v01", - location_url = "animal_100", - file_path = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" - ) - ) as mock_get_backend_by_id, patch( - "app.main.service.backend.create_backend", - return_value = BackendOut( - id = 1234567890, - auth_enabled = True, - owner = "testuser", - template = "testtemplate", - template_version = "v01", - location_url = "animal_100", - file_path = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%1.conf" - ) - ) as mock_create_backend, patch( - "app.main.service.backend.extract_proxy_pass", - return_value = "https://example.com:1000/" - ) as mock_extract_proxy_pass, patch( - "app.main.service.backend.get_file_path_by_id", - return_value = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" - ) as mock_get_file_path_by_id: - - res: BackendOut = await backend_service.update_backend_authorization( - backend_id = 1234567890, - auth_enabled = True, - ) - - assert res.id == 1234567890 - assert res.auth_enabled is True - - mock_get_backend_by_id.assert_awaited_once_with(backend_id = 1234567890) - mock_get_file_path_by_id.assert_called_once_with(backend_id = 1234567890) - mock_extract_proxy_pass.assert_called_once() - mock_create_backend.assert_awaited_once() - -def test_get_backend_by_id(): - patch( - "app.main.service.backend.get_backends", - return_value = [BackendOut(id = 1)] - ): - assert await backend_service.get_backend_by_id(1).id == 1 \ No newline at end of file diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py new file mode 100644 index 00000000..834a105f --- /dev/null +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -0,0 +1,87 @@ +import pytest +from unittest.mock import patch + +from app.main.model.serializers import BackendOut + +from app.main.service import backend as backend_service + +from werkzeug.exceptions import NotFound + +file_path_example_1 = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" +file_path_example_2 = "/var/forc/backend_path/9876543210%otheruser%cat_200%othertemplate%v02%1.conf" + + +@pytest.mark.asyncio +async def test_get_backend_by_id(): + + with patch( + "app.main.service.backend.get_backends", + return_value = [BackendOut(backend_id = 123), BackendOut(backend_id = 456)] + ) as mock_get_backends: + + response: BackendOut = await backend_service.get_backend_by_id(123) + + assert response.backend_id == 123 + mock_get_backends.assert_awaited_once() + +@pytest.mark.asyncio +async def test_get_backend_by_id_not_found(): + + with pytest.raises(NotFound): + with patch( + "app.main.service.backend.get_backends", + return_value = [BackendOut(backend_id = 123), BackendOut(backend_id = 456)] + ) as mock_get_backends: + + await backend_service.get_backend_by_id(789) + mock_get_backends.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_get_filepath_by_id(): + + with patch( + "app.main.service.backend.get_backends", + return_value = [ + BackendOut( + backend_id = 1234567890, + file_path = file_path_example_1), + BackendOut( + backend_id = 9876543210, + file_path = file_path_example_2)] + ) as mock_get_backends: + + response: str = await backend_service.get_file_path_by_id(1234567890) + + assert response == file_path_example_1 + mock_get_backends.assert_awaited_once() + +@pytest.mark.asyncio +async def test_get_filepath_by_id_not_found(): + + with pytest.raises(NotFound): + with patch( + "app.main.service.backend.get_backends", + return_value = BackendOut(backend_id = 1234567890) + ) as mock_get_backends: + await backend_service.get_file_path_by_id(9876543210) + mock_get_backends.assert_awaited_once() + +""" +@pytest.mark.asyncio +async def test_create_backend(): # check kwargs payload id and suffix number at the end + + with patch( + "app.main.service.backend.generate_suffix_number", + return_value = 111 + ) as mock_generate_suffix_number, patch( + "app.main.service.backend.random_with_n_digits", + return_value = 9876543210 + ) as mock_random_with_n_digits, patch( + "app.main.service.backend.generate_backend_by_template", + + +@pytest.mark.asyncio +async def test_update_backend_authorization_activate(): + +""" diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py new file mode 100644 index 00000000..2660b4db --- /dev/null +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py @@ -0,0 +1,72 @@ +import pytest +from unittest.mock import patch + +from app.main.model.serializers import BackendOut + +from app.main.views import backend as backend_views + +from fastapi import HTTPException + + +@pytest.mark.asyncio +async def test_backend_update_auth_activate(): + + with patch( + "app.main.views.backend.update_backend_authorization", + return_value = BackendOut( + backend_id = 123, + auth_enabled = True, + ) + ) as mock_update_backend_authorization: + + response: BackendOut = await backend_views.backend_update_auth( + backend_id = 123, + body = {"auth_enabled": True}, + api_key = "test" + ) + + assert response.backend_id == 123 + assert response.auth_enabled is True + mock_update_backend_authorization.assert_awaited_once_with(backend_id = 123, auth_enabled = True) + + +@pytest.mark.asyncio +async def test_backend_update_auth_deactivate(): + + with patch( + "app.main.views.backend.update_backend_authorization", + return_value = BackendOut( + backend_id = 123, + auth_enabled = False, + ) + ) as mock_update_backend_authorization: + + response: BackendOut = await backend_views.backend_update_auth( + backend_id = 123, + body = {"auth_enabled": False}, + api_key = "test" + ) + + assert response.backend_id == 123 + assert response.auth_enabled is False + mock_update_backend_authorization.assert_awaited_once_with(backend_id = 123, auth_enabled = False) + + +@pytest.mark.asyncio +async def test_backend_update_auth_invalid_body(): + + with pytest.raises(HTTPException) as not_boolean_exception: + await backend_views.backend_update_auth( + backend_id = 123, + body = {"auth_enabled": "not a boolean"}, + api_key = "test" + ) + assert not_boolean_exception.value.status_code == 422 + + with pytest.raises(HTTPException) as empty_body_exception: + await backend_views.backend_update_auth( + backend_id = 123, + body = {}, + api_key = "test" + ) + assert empty_body_exception.value.status_code == 422 From e2d0972a583c8ea6e610ef25fc4344321c332e99 Mon Sep 17 00:00:00 2001 From: vktrrdk Date: Thu, 8 Jan 2026 17:27:57 +0000 Subject: [PATCH 010/118] test --- FastapiOpenRestyConfigurator/pytest.ini | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 FastapiOpenRestyConfigurator/pytest.ini diff --git a/FastapiOpenRestyConfigurator/pytest.ini b/FastapiOpenRestyConfigurator/pytest.ini new file mode 100644 index 00000000..f7ab32e3 --- /dev/null +++ b/FastapiOpenRestyConfigurator/pytest.ini @@ -0,0 +1,3 @@ +# pytest.ini +[pytest] +pythonpath = /home/ubuntu/code/simpleVMWebGateway/FastapiOpenRestyConfigurator From 9e8c631dc1ba284f8f017a24f441063a74c726f6 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Tue, 13 Jan 2026 19:30:40 +0000 Subject: [PATCH 011/118] Added helper functions and refactored backend authorization update logic, also organized the functions --- .../app/main/service/backend.py | 194 +++++++++++------- 1 file changed, 118 insertions(+), 76 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 6295972a..91036d46 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -22,11 +22,36 @@ filename_regex = r"(\d*)%([a-z0-9\-\@.]*?)%([^%]*)%([^%]*)%([^%]*)%([01])\.conf" + +# HELPER FUNCTIONS + async def random_with_n_digits(n): range_start = 10 ** (n - 1) range_end = (10 ** n) - 1 return randint(range_start, range_end) +async def generate_suffix_number(user_key_url): + current_backends: List[BackendOut] = await get_backends() + same_name_backend_ids = [] + + for backend in current_backends: + if backend.location_url.split("_")[0] == user_key_url: + same_name_backend_ids.append(int(backend.location_url.split("_")[1])) + + if not same_name_backend_ids: + return "100" + + same_name_backend_ids.sort() + highest_id = same_name_backend_ids[-1] + if highest_id == 999: + logger.warning("Reached max index number for requested user_key_url: " + user_key_url) + raise InternalServerError("Reached max index number for requested user_key_url (limit=999).") + + return str(highest_id + 1) + + + +# CORE GETTER FUNCTIONS async def get_backends() -> List[BackendOut]: if not os.path.exists(settings.FORC_BACKEND_PATH) and not os.access(settings.FORC_BACKEND_PATH, os.W_OK): @@ -63,18 +88,7 @@ async def get_backend_by_id(backend_id: int) -> BackendOut: for backend in valid_backends: if int(backend.id) == int(backend_id): return backend - raise NotFound(f"Backend {backend_id} was not found.") - - -async def get_file_path_by_id(backend_id: int) -> str: - backends: List[BackendOut] = await get_backends() - logger.debug(f"Searching file path for backend id: {backend_id} in backends: {backends}") - for backend in backends: - if int(backend.id) == int(backend_id): - logger.debug(f"Returning found file path for backend id: {backend_id}: {backend.file_path}") - return backend.file_path - raise NotFound(f"Backend with id {backend_id} not found.") - + raise NotFound(f"Backend with id {backend_id} was not found.") async def get_backends_upstream_urls() -> Dict[str, List[BackendOut]]: @@ -91,7 +105,21 @@ async def get_backends_upstream_urls() -> Dict[str, List[BackendOut]]: return upstream_urls -def extract_proxy_pass(file_path): +async def get_file_path_by_id(backend_id: int) -> str: + backends: List[BackendOut] = await get_backends() + logger.debug(f"Searching file path for backend id: {backend_id} in backends: {backends}") + for backend in backends: + if int(backend.id) == int(backend_id): + logger.debug(f"Returning found file path for backend id: {backend_id}: {backend.file_path}") + return backend.file_path + raise NotFound(f"Backend with id {backend_id} not found.") + + + +# EXTENDED GETTER FUNCTIONS + +def extract_proxy_pass(file_path) -> str | None: + # proxy_pass consists of upstream_url with potential trailing path, see guacamole template with open(file_path, 'r') as file: content = file.read() @@ -100,41 +128,46 @@ def extract_proxy_pass(file_path): if match: return match.group(1) else: + logger.error(f"Could not extract proxy_pass from {file_path}") return None -async def generate_suffix_number(user_key_url): - current_backends: List[BackendOut] = await get_backends() - same_name_backend_ids = [] +def get_upstream_url(file_path) -> str | None: + # extracts upstream_url from proxy_pass by removing trailing path, see guacamole template + proxy_pass = extract_proxy_pass(file_path) + if proxy_pass is None: + return None - for backend in current_backends: - if backend.location_url.split("_")[0] == user_key_url: - same_name_backend_ids.append(int(backend.location_url.split("_")[1])) + # split and rejoin to remove trailing path to remove potential trailing path, unaffected if no trailing path + upstream_url = "/".join(proxy_pass.split("/", 3)[:3]) + return upstream_url - if not same_name_backend_ids: - return "100" - same_name_backend_ids.sort() - highest_id = same_name_backend_ids[-1] - if highest_id == 999: - logger.warning("Reached max index number for requested user_key_url: " + user_key_url) - raise InternalServerError("Reached max index number for requested user_key_url (limit=999).") +def get_basekey_from_backend(backend: BackendOut) -> str | None: + try: + base_key = backend.location_url.rsplit("_", 1)[0] + logger.debug(f"Backend id: {backend.id}, Base key: {base_key}") + return base_key + except ValueError: + logger.error(f"location_url has no suffix pattern: {backend.location_url}") + return None + - return str(highest_id + 1) +# CORE MUTATOR AND SERVICE FUNCTIONS async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: logger.debug(f"Creating backend for owner: {payload.owner} with template: {payload.template} version: {payload.template_version}") # overwrite payload as BackendTemp for generate_backend_by_template() - payload: BackendTemp = BackendTemp(**payload.dict()) + payload: BackendTemp = BackendTemp(**payload.model_dump()) if 'id' in kwargs: # override id and suffix if provided from update_backend_authorization() - payload = payload.copy(update={'id': str(kwargs.get('id'))}) + payload = payload.model_copy(update={'id': str(kwargs.get('id'))}) suffix_number = str(kwargs.get('location_url')).split("_")[1] else: suffix_number = await generate_suffix_number(payload.user_key_url) - payload.id = str(await random_with_n_digits(10)) # TODO: should we refactor id: int? see delete_backend() + payload.id = str(await random_with_n_digits(10)) # TODO: should we refactor id: int? see delete_backend() maybe it has something to do with int beginning with 0 # generate backend and location_url backend_file_contents = await generate_backend_by_template(payload, suffix_number) @@ -188,61 +221,70 @@ async def delete_backend(backend_id) -> bool: raise NotFound(f"Backend {backend_id} was not found.") -async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> BackendOut | bool: - try: - backend: BackendOut = await get_backend_by_id(backend_id) - except NotFound as e: - logger.error(f"Backend with id {backend_id} not found for authorization update.") - raise e - - # build temp payload as BackendIn for create_backend() - # extract upstream_url from file - upstream_url = extract_proxy_pass(backend.file_path) - if not upstream_url: - logger.error(f"Could not extract proxy_pass from {backend.file_path}") - return False +async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> BackendOut | None: + backend = await get_backend_by_id(backend_id) + if not backend: + return None + + # build temporary payload as BackendIn for create_backend() + temp_payload = build_payload_for_auth_update(backend, auth_enabled) + if not temp_payload: + return None + + # generate new backend contents with temp_payload, additional kwargs to persist backend_id and location_url + new_contents = await create_backend(temp_payload, id=str(backend_id), location_url=backend.location_url) + logger.debug(f"New contents: {new_contents}") + if not new_contents: + logger.error("Templating returned empty result.") + return None + + logger.info(f"Updated backend authorization to {temp_payload.auth_enabled} for backend id: {backend_id}") + + # convert BackendTemp to BackendOut for returning + returning_backend = await convert_backend_temp_to_out(new_contents) + + return returning_backend - # get existing location_url and user_key_url - try: - base_key, suffix = backend.location_url.rsplit("_", 1) - except ValueError: - logger.error(f"location_url has no suffix pattern: {backend.location_url}") - return False - logger.debug(f"Backend id: {backend.id}, Base key: {base_key}, Suffix: {suffix}") - # build new temp payload as BackendIn + +# HELPER FUNCTIONS FOR MUTATORS + +def build_payload_for_auth_update(backend: BackendOut, auth_enabled: bool) -> BackendIn | None: + # fetch necessary info from existing BackendOut and build BackendIn payload for create_backend(), see update_backend_authorization() + upstream_url = get_upstream_url(backend.file_path) + base_key = get_basekey_from_backend(backend) + if not upstream_url or not base_key: + logger.error(f"Could not extract necessary info (upstream_url and base_key) from backend id: {backend.id} for updating authorization") + return None + + # build new temporary payload as BackendIn try: - payload = BackendIn( + temp_payload = BackendIn( owner = backend.owner, template = backend.template, template_version = backend.template_version, user_key_url = base_key, - upstream_url = "/".join(upstream_url.split("/", 3)[:3]), # remove potential trailing path, see guacamole template + upstream_url = upstream_url, auth_enabled = auth_enabled, # set new auth flag ) + return temp_payload except Exception as e: logger.error(f"Error building temp payload for backend update: {e}") - return False - # logger.debug(f"payload: {payload}") - - # generate new backend contents with temp_payload and additional kwargs to persist backend_id and location_url - new_contents = await create_backend(payload, id=str(backend_id), location_url=backend.location_url) - logger.debug(f"New contents: {new_contents}") - if not new_contents: - logger.error("Templating returned empty result.") - return False - - logger.info(f"Updated backend authorization to {payload.auth_enabled} for backend id: {backend_id}") - - # convert BackendTemp to BackendOut for returning - returning_backend: BackendOut = BackendOut( - id = new_contents.id, - owner = new_contents.owner, - location_url = new_contents.location_url, - template = new_contents.template, - template_version = new_contents.template_version, - auth_enabled = new_contents.auth_enabled, - file_path = await get_file_path_by_id(new_contents.id) - ) + return None - return returning_backend +async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | None: + # needed for returning backends to client + try: + backend_out = BackendOut( + id = backend_temp.id, + owner = backend_temp.owner, + location_url = backend_temp.location_url, + template = backend_temp.template, + template_version = backend_temp.template_version, + auth_enabled = backend_temp.auth_enabled, + file_path = await get_file_path_by_id(backend_temp.id) + ) + return backend_out + except Exception as e: + logger.error(f"Error converting BackendTemp to BackendOut: {e}") + return None From e92f18ff9a5eef57ef773c79bb20f85fb42973fd Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Tue, 13 Jan 2026 21:15:55 +0000 Subject: [PATCH 012/118] backend service: refactored and debloated create_backend() by adding helper functions --- .../app/main/service/backend.py | 84 ++++++++++++------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 91036d46..01309a49 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -26,6 +26,7 @@ # HELPER FUNCTIONS async def random_with_n_digits(n): + # used for backend id generation, never starts with 0 range_start = 10 ** (n - 1) range_end = (10 ** n) - 1 return randint(range_start, range_end) @@ -49,6 +50,10 @@ async def generate_suffix_number(user_key_url): return str(highest_id + 1) +def generate_backend_filename(backend: BackendOut) -> str: + filename = f"{backend.id}%{backend.owner}%{backend.location_url}%{backend.template}%{backend.template_version}%{str(int(backend.auth_enabled))}.conf" + return filename + # CORE GETTER FUNCTIONS @@ -116,7 +121,7 @@ async def get_file_path_by_id(backend_id: int) -> str: -# EXTENDED GETTER FUNCTIONS +# FURTHER GETTER FUNCTIONS def extract_proxy_pass(file_path) -> str | None: # proxy_pass consists of upstream_url with potential trailing path, see guacamole template @@ -162,12 +167,8 @@ async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: # overwrite payload as BackendTemp for generate_backend_by_template() payload: BackendTemp = BackendTemp(**payload.model_dump()) - if 'id' in kwargs: # override id and suffix if provided from update_backend_authorization() - payload = payload.model_copy(update={'id': str(kwargs.get('id'))}) - suffix_number = str(kwargs.get('location_url')).split("_")[1] - else: - suffix_number = await generate_suffix_number(payload.user_key_url) - payload.id = str(await random_with_n_digits(10)) # TODO: should we refactor id: int? see delete_backend() maybe it has something to do with int beginning with 0 + # generate or reuse backend id and suffix number + payload, suffix_number = await set_backend_id_and_suffix_for(payload, **kwargs) # generate backend and location_url backend_file_contents = await generate_backend_by_template(payload, suffix_number) @@ -176,15 +177,12 @@ async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: payload.location_url = f"{payload.user_key_url}_{suffix_number}" - # check for duplicated in ip and port - upstream_urls: Dict[str, List[BackendOut]] = await get_backends_upstream_urls() - matching_urls_backends: List[BackendOut] = upstream_urls.get(payload.upstream_url, []) - for backend in matching_urls_backends: - logger.info(f"Deleting existing Backend with same Upstream Url - {payload.upstream_url}") - await delete_backend(backend.id) + # check for duplicates and delete them before creating new backend + if not await delete_duplicate_backends(payload.upstream_url): + raise InternalServerError("Server was not able to delete duplicate backends before creating a new one.") # save BackendOut info in filename, create backend file in filesystem - filename = f"{payload.id}%{payload.owner}%{payload.location_url}%{payload.template}%{payload.template_version}%{str(int(payload.auth_enabled))}.conf" + filename = generate_backend_filename(payload) with open(f"{settings.FORC_BACKEND_PATH}/{filename}", 'w') as backend_file: backend_file.write(backend_file_contents) @@ -249,6 +247,47 @@ async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> B # HELPER FUNCTIONS FOR MUTATORS +async def set_backend_id_and_suffix_for(payload: BackendTemp, **kwargs) -> tuple[BackendTemp, str]: + if 'id' in kwargs: # override id and suffix if provided from update_backend_authorization() + payload = payload.model_copy(update={'id': str(kwargs.get('id'))}) + suffix_number = str(kwargs.get('location_url')).split("_")[1] + else: + payload.id = str(await random_with_n_digits(10)) # TODO: should we refactor id: int? see delete_backend(). maybe it has something to do with int beginning with 0 + suffix_number = await generate_suffix_number(payload.user_key_url) + logger.debug(f"Set backend id: {payload.id} with suffix number: {suffix_number}") + return payload, suffix_number + + +async def delete_duplicate_backends(upstream_url: str) -> bool: + # check for duplicates in ip and port and delete them + upstream_urls: Dict[str, List[BackendOut]] = await get_backends_upstream_urls() + matching_urls_backends: List[BackendOut] = upstream_urls.get(upstream_url, []) + success: bool = True + for backend in matching_urls_backends: + logger.info(f"Deleting existing Backend with same Upstream Url - {upstream_url}") + if not await delete_backend(backend.id): + success = False + return success + + +async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | None: + # needed for returning backends to client + try: + backend_out = BackendOut( + id = backend_temp.id, + owner = backend_temp.owner, + location_url = backend_temp.location_url, + template = backend_temp.template, + template_version = backend_temp.template_version, + auth_enabled = backend_temp.auth_enabled, + file_path = await get_file_path_by_id(backend_temp.id) + ) + return backend_out + except Exception as e: + logger.error(f"Error converting BackendTemp to BackendOut: {e}") + return None + + def build_payload_for_auth_update(backend: BackendOut, auth_enabled: bool) -> BackendIn | None: # fetch necessary info from existing BackendOut and build BackendIn payload for create_backend(), see update_backend_authorization() upstream_url = get_upstream_url(backend.file_path) @@ -271,20 +310,3 @@ def build_payload_for_auth_update(backend: BackendOut, auth_enabled: bool) -> Ba except Exception as e: logger.error(f"Error building temp payload for backend update: {e}") return None - -async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | None: - # needed for returning backends to client - try: - backend_out = BackendOut( - id = backend_temp.id, - owner = backend_temp.owner, - location_url = backend_temp.location_url, - template = backend_temp.template, - template_version = backend_temp.template_version, - auth_enabled = backend_temp.auth_enabled, - file_path = await get_file_path_by_id(backend_temp.id) - ) - return backend_out - except Exception as e: - logger.error(f"Error converting BackendTemp to BackendOut: {e}") - return None From 0ad366aca8f66b1ddc35b375ea86e54091c401bf Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Thu, 15 Jan 2026 18:31:55 +0000 Subject: [PATCH 013/118] refactored delete_backend() by splitting into sub functions --- .../app/main/service/backend.py | 121 +++++++++++++----- 1 file changed, 89 insertions(+), 32 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 01309a49..480936aa 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -26,7 +26,7 @@ # HELPER FUNCTIONS async def random_with_n_digits(n): - # used for backend id generation, never starts with 0 + # used for backend id generation (n=10), never starts with 0 range_start = 10 ** (n - 1) range_end = (10 ** n) - 1 return randint(range_start, range_end) @@ -64,15 +64,15 @@ async def get_backends() -> List[BackendOut]: return [] if len(os.listdir(settings.FORC_BACKEND_PATH)) == 0: return [] - backend_path_files = os.listdir(settings.FORC_BACKEND_PATH) - logger.info(f"Files in backend_path: {backend_path_files}") + backend_path_filenames = os.listdir(settings.FORC_BACKEND_PATH) + logger.info(f"Files in backend_path: {backend_path_filenames}") valid_backends = [] - for file in backend_path_files: - match = re.fullmatch(filename_regex, file) + for filename in backend_path_filenames: + match = re.fullmatch(filename_regex, filename) if not match: - if file == "users" or file == "scripts": + if filename == "users" or filename == "scripts": continue - logger.warning("Found a backend file with wrong naming, skipping it: " + str(file)) + logger.warning("Found a backend file with wrong naming, skipping it: " + str(filename)) continue backend: BackendOut = BackendOut( id = match.group(1), @@ -81,7 +81,7 @@ async def get_backends() -> List[BackendOut]: template = match.group(4), template_version = match.group(5), auth_enabled = bool(int(match.group(6))), - file_path = os.path.join(settings.FORC_BACKEND_PATH, file) + file_path = os.path.join(settings.FORC_BACKEND_PATH, filename) ) logger.debug(f"Discovered backend: {backend}") valid_backends.append(backend) @@ -193,30 +193,30 @@ async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: async def delete_backend(backend_id) -> bool: - if not os.path.exists(settings.FORC_BACKEND_PATH) and not os.access(settings.FORC_BACKEND_PATH, os.W_OK): - logger.error("Not able to access configured backend path.") + backend_path_filenames = get_valid_backend_filenames() + if not backend_path_filenames: return False - if len(os.listdir(settings.FORC_BACKEND_PATH)) == 0: - return False - backend_path_files = os.listdir(settings.FORC_BACKEND_PATH) - for file in backend_path_files: - match = re.fullmatch(filename_regex, file) - if not match: - if file == "users" or file == "scripts": - continue - logger.warning(f"Found a backend file with wrong naming, skipping it: {file}") - continue - if int(match.group(1)) == int(backend_id): - logger.info(f"Attempting to delete backend with id: {backend_id} as file: {settings.FORC_BACKEND_PATH}/{file}") - try: - os.remove(f"{settings.FORC_BACKEND_PATH}/{file}") - logger.info(f"Deleted backend with id: {backend_id}") - await reload_openresty() - return True - except OSError as e: - logger.warning(f"Was not able to delete backend with id: {backend_id} ERROR: {e}") - raise InternalServerError("Server was not able to delete this backend. Contact the admin.") - raise NotFound(f"Backend {backend_id} was not found.") + + matching_backend_filenames = filter_backend_filenames_by_id(backend_path_filenames, backend_id) + + amount_of_files = len(matching_backend_filenames) + if amount_of_files == 0: + raise NotFound(f"Backend {backend_id} was not found.") + if amount_of_files > 1: + logger.error(f"Found multiple backend files for backend id: {backend_id}, cannot delete.") + raise InternalServerError("Server found multiple backend files, cannot delete.") + + filename = matching_backend_filenames[0] + + logger.info(f"Attempting to delete backend with id: {backend_id} as file: {settings.FORC_BACKEND_PATH}/{filename}") + try: + os.remove(f"{settings.FORC_BACKEND_PATH}/{filename}") + logger.info(f"Deleted backend with id: {backend_id}") + await reload_openresty() + return True + except OSError as e: + logger.warning(f"Was not able to delete backend with id: {backend_id} ERROR: {e}") + raise InternalServerError("Server was not able to delete this backend. Contact the admin.") async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> BackendOut | None: @@ -248,7 +248,8 @@ async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> B # HELPER FUNCTIONS FOR MUTATORS async def set_backend_id_and_suffix_for(payload: BackendTemp, **kwargs) -> tuple[BackendTemp, str]: - if 'id' in kwargs: # override id and suffix if provided from update_backend_authorization() + # override id and suffix if provided from update_backend_authorization() + if 'id' in kwargs and 'location_url' in kwargs: payload = payload.model_copy(update={'id': str(kwargs.get('id'))}) suffix_number = str(kwargs.get('location_url')).split("_")[1] else: @@ -288,6 +289,62 @@ async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | return None +def check_backend_path_file() -> bool: + # check if backend path exists, is accessible and has files + if not os.path.exists(settings.FORC_BACKEND_PATH) and not os.access(settings.FORC_BACKEND_PATH, os.W_OK): + logger.error("Not able to access configured backend path.") + return False + if len(os.listdir(settings.FORC_BACKEND_PATH)) == 0: + logger.error("No files found in backend path.") + return False + return True + + +def check_backend_path_file_naming(backend_path_filename: str) -> bool | None: + # check for correct naming + match = re.fullmatch(filename_regex, backend_path_filename) + # skip files with wrong naming and log warning + if not match: + # exclude expected files from warning + if backend_path_filename == "users" or backend_path_filename == "scripts": + return None + logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") + return None + # return backend id of the correctly named file + return True + + +def get_backend_path_filenames() -> List[str] | None: + # get list of filenames in backend path + if not check_backend_path_file(): + return None + + return os.listdir(settings.FORC_BACKEND_PATH) + + +def get_valid_backend_filenames() -> List[str] | None: + + # get list of valid backend filenames in backend path + backend_path_filenames = get_backend_path_filenames() + if not backend_path_filenames: + return None + + # check naming, skip invalid filenames + valid_backend_filenames = [] + for filename in backend_path_filenames: + if not check_backend_path_file_naming(filename): + continue + + # add valid filenames to list and return them + valid_backend_filenames.append(filename) + return valid_backend_filenames + + +def filter_backend_filenames_by_id(backend_path_filenames: List[str], backend_id: int) -> List[str]: + # filter a list of backend filenames for matching backend id + return [filename for filename in backend_path_filenames if int(filename.split("%")[0]) == int(backend_id)] + + def build_payload_for_auth_update(backend: BackendOut, auth_enabled: bool) -> BackendIn | None: # fetch necessary info from existing BackendOut and build BackendIn payload for create_backend(), see update_backend_authorization() upstream_url = get_upstream_url(backend.file_path) From bc80914e62703defe435e2d3a46f0f41e08741db Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Sat, 17 Jan 2026 17:15:17 +0000 Subject: [PATCH 014/118] removed unneccessary async calls and commented out existing functions --- .../app/main/service/backend.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 480936aa..977b4408 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -25,31 +25,36 @@ # HELPER FUNCTIONS -async def random_with_n_digits(n): +def random_with_n_digits(n): # used for backend id generation (n=10), never starts with 0 range_start = 10 ** (n - 1) range_end = (10 ** n) - 1 return randint(range_start, range_end) -async def generate_suffix_number(user_key_url): + +# TODO: unlikely but potential error cause, if two users have same randomly generated user_key_url! +async def generate_suffix_number(user_key_url = None) -> str: + if user_key_url is None: + return "100" + + # look for backends with same user_key_url current_backends: List[BackendOut] = await get_backends() same_name_backend_ids = [] - for backend in current_backends: if backend.location_url.split("_")[0] == user_key_url: same_name_backend_ids.append(int(backend.location_url.split("_")[1])) - if not same_name_backend_ids: return "100" + # return highest found suffix number + 1 to iterate same_name_backend_ids.sort() highest_id = same_name_backend_ids[-1] if highest_id == 999: logger.warning("Reached max index number for requested user_key_url: " + user_key_url) raise InternalServerError("Reached max index number for requested user_key_url (limit=999).") - return str(highest_id + 1) + def generate_backend_filename(backend: BackendOut) -> str: filename = f"{backend.id}%{backend.owner}%{backend.location_url}%{backend.template}%{backend.template_version}%{str(int(backend.auth_enabled))}.conf" return filename @@ -253,7 +258,7 @@ async def set_backend_id_and_suffix_for(payload: BackendTemp, **kwargs) -> tuple payload = payload.model_copy(update={'id': str(kwargs.get('id'))}) suffix_number = str(kwargs.get('location_url')).split("_")[1] else: - payload.id = str(await random_with_n_digits(10)) # TODO: should we refactor id: int? see delete_backend(). maybe it has something to do with int beginning with 0 + payload.id = str(random_with_n_digits(10)) # TODO: should we refactor id: int? see delete_backend(). maybe it has something to do with int beginning with 0 suffix_number = await generate_suffix_number(payload.user_key_url) logger.debug(f"Set backend id: {payload.id} with suffix number: {suffix_number}") return payload, suffix_number @@ -359,7 +364,7 @@ def build_payload_for_auth_update(backend: BackendOut, auth_enabled: bool) -> Ba owner = backend.owner, template = backend.template, template_version = backend.template_version, - user_key_url = base_key, + user_key_url = base_key, # add fetched fields upstream_url = upstream_url, auth_enabled = auth_enabled, # set new auth flag ) From 81ef9b5983a8efab766080c8953b5b51b29b784a Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Tue, 20 Jan 2026 09:12:15 +0000 Subject: [PATCH 015/118] testing environment, added some test functions --- .../app/main/tests/test_views_backend.py | 98 +++++++------------ .../app/main/views/backend.py | 34 ++++--- FastapiOpenRestyConfigurator/pytest.ini | 2 +- FastapiOpenRestyConfigurator/requirements.txt | 4 + 4 files changed, 62 insertions(+), 76 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py index 2660b4db..af71b901 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py @@ -1,72 +1,46 @@ import pytest from unittest.mock import patch - -from app.main.model.serializers import BackendOut - -from app.main.views import backend as backend_views - from fastapi import HTTPException - -@pytest.mark.asyncio -async def test_backend_update_auth_activate(): - - with patch( - "app.main.views.backend.update_backend_authorization", - return_value = BackendOut( - backend_id = 123, - auth_enabled = True, - ) - ) as mock_update_backend_authorization: - - response: BackendOut = await backend_views.backend_update_auth( - backend_id = 123, - body = {"auth_enabled": True}, - api_key = "test" - ) - - assert response.backend_id == 123 - assert response.auth_enabled is True - mock_update_backend_authorization.assert_awaited_once_with(backend_id = 123, auth_enabled = True) +from app.main.views import backend as backend_views +@pytest.mark.parametrize( + "exception_expected, backend_id, body", + [ + (False, 123, {"auth_enabled": True}), + # success cases + (False, 123, {"auth_enabled": 1}), + (False, 123, {"auth_enabled": 0}), + (False, 123, {"auth_enabled": False}), + # corrupted backend_id + # TODO: more tests when there is further validation on backend_id + (True, "not an int", {"auth_enabled": True}), + (True, None, {"auth_enabled": True}), + # corrupted body + (True, 123, {"auth_enabled": "not a boolean"}), + (True, 123, {"auth_enabled": ""}), + (True, 123, {"differrent_value": True}), + (True, 123, None), + (True, 123, {}), + ] +) @pytest.mark.asyncio -async def test_backend_update_auth_deactivate(): +async def test_backend_update_auth(exception_expected, backend_id, body): with patch( - "app.main.views.backend.update_backend_authorization", - return_value = BackendOut( - backend_id = 123, - auth_enabled = False, - ) + "app.main.service.backend.update_backend_authorization" ) as mock_update_backend_authorization: - - response: BackendOut = await backend_views.backend_update_auth( - backend_id = 123, - body = {"auth_enabled": False}, - api_key = "test" - ) - - assert response.backend_id == 123 - assert response.auth_enabled is False - mock_update_backend_authorization.assert_awaited_once_with(backend_id = 123, auth_enabled = False) - - -@pytest.mark.asyncio -async def test_backend_update_auth_invalid_body(): - - with pytest.raises(HTTPException) as not_boolean_exception: - await backend_views.backend_update_auth( - backend_id = 123, - body = {"auth_enabled": "not a boolean"}, - api_key = "test" - ) - assert not_boolean_exception.value.status_code == 422 - - with pytest.raises(HTTPException) as empty_body_exception: - await backend_views.backend_update_auth( - backend_id = 123, - body = {}, - api_key = "test" - ) - assert empty_body_exception.value.status_code == 422 + # success case + try: + await backend_views.backend_update_auth( + backend_id = backend_id, + body = body, + api_key = object() # type: ignore[reportArgumentType] + ) + mock_update_backend_authorization.assert_called_once_with(backend_id = 123, auth_enabled = True) + # fail case + except Exception: + if exception_expected: + mock_update_backend_authorization.assert_not_awaited() + #if not exception_expected: diff --git a/FastapiOpenRestyConfigurator/app/main/views/backend.py b/FastapiOpenRestyConfigurator/app/main/views/backend.py index a93eda79..e0218894 100755 --- a/FastapiOpenRestyConfigurator/app/main/views/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/views/backend.py @@ -53,19 +53,27 @@ async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_ap summary="Set owner authorization to true/false for an existing backend." ) async def backend_update_auth(backend_id: int, body: dict = Body(...), api_key: APIKey = Depends(get_api_key)): - backend_id = int(secure_filename(str(backend_id))) # TODO: do we need secure_filename for int? - enable_auth = body.get("auth_enabled", None) - logger.debug(f"Updating backend authorization to {enable_auth} for backend id: {backend_id}") - if enable_auth is None or not isinstance(enable_auth, bool): - logger.error(f"auth_enabled is required and must be a boolean, backend_id: {backend_id}, auth_enabled: {enable_auth}, {type(enable_auth)}") - raise HTTPException(status_code=422, - detail=f"auth_enabled is required and must be a boolean, backend_id: {backend_id}, auth_enabled: {enable_auth}, {type(enable_auth)}") - try: - return await backend_service.update_backend_authorization(backend_id, enable_auth) - except NotFound: - raise HTTPException(status_code=404, detail=f"Backend with id {backend_id} not found.") - except InternalServerError: - raise HTTPException(status_code=500, detail="Internal server error.") + # process inputs TODO: should we validate further? + backend_id = int(secure_filename(str(backend_id))) # TODO: are secure_filename and str necessary? validation? + enable_auth = bool(body.get("auth_enabled", None)) + logger.debug(f"Attempting to update backend authorization to {enable_auth} for backend id: {backend_id}") + + # check inputs and raise error + if backend_id is None or enable_auth is None or not isinstance(enable_auth, bool): + logger.error( + f"Received faulty data. backend_id: {backend_id}, auth_enabled: {enable_auth}, {type(enable_auth)}") + raise HTTPException(status_code=422, detail = + f"auth_enabled is required and must be a boolean, \ + backend_id: {backend_id}, auth_enabled: {enable_auth}, {type(enable_auth)}") + # forward to service layer + else: + try: + return await backend_service.update_backend_authorization(backend_id, enable_auth) + # TODO: the exceptions are raised in the service layer already, do we still need this? + except NotFound: + raise HTTPException(status_code=404, detail=f"Backend with id {backend_id} not found.") + except InternalServerError: + raise HTTPException(status_code=500, detail="Internal server error.") @router.get( diff --git a/FastapiOpenRestyConfigurator/pytest.ini b/FastapiOpenRestyConfigurator/pytest.ini index f7ab32e3..5d8c57f5 100644 --- a/FastapiOpenRestyConfigurator/pytest.ini +++ b/FastapiOpenRestyConfigurator/pytest.ini @@ -1,3 +1,3 @@ # pytest.ini [pytest] -pythonpath = /home/ubuntu/code/simpleVMWebGateway/FastapiOpenRestyConfigurator +pythonpath = /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 9cb12eec..bd1ec3bd 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -5,3 +5,7 @@ Jinja2==3.1.6 python-dotenv==1.2.1 gunicorn==23.0.0 pydantic-settings + +# testing +pytest==8.4.2 +pytest-asyncio # TODO: @reviewer: which version? From d7ff5500d0254593385cd093763bc23bcf938a71 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 21 Jan 2026 03:06:18 +0100 Subject: [PATCH 016/118] feat(Tests):updated tests (#535) and also some changes in templating --- .../.requirements.txt.kate-swp | Bin 126 -> 0 bytes .../app/main/service/backend.py | 7 +- .../app/main/service/user.py | 5 +- .../app/main/tests/test_service_backend.py | 87 ------------------ .../app/main/tests/test_views_backend.py | 46 --------- .../app/main/util/auth.py | 2 +- .../app/main/util/templating.py | 76 +++++++++------ FastapiOpenRestyConfigurator/pytest.ini | 3 +- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 9 files changed, 60 insertions(+), 168 deletions(-) delete mode 100755 FastapiOpenRestyConfigurator/.requirements.txt.kate-swp delete mode 100644 FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py delete mode 100644 FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py diff --git a/FastapiOpenRestyConfigurator/.requirements.txt.kate-swp b/FastapiOpenRestyConfigurator/.requirements.txt.kate-swp deleted file mode 100755 index 76928433583b54389d2bec593c232d8d1247e1b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126 zcmZQzU=Z?7EJ;-eE>A2_aLdd|RWQ;sU|?VnId&n{P4ne3|EiO|4`W&tS6Q>&Ulkk# ql;Hqk5D)-y*uhL79SUS{xCVnLc2}@ER;UyYNQ@0422;)lQ4RnNtrcqk diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 977b4408..335b46f9 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -15,7 +15,7 @@ from ..config import get_settings logger = logging.getLogger("service") -settings = get_settings() + # format of filename saves information of BackendOut by this schema: # {id}%{owner}%{location_url}%{template}%{template_version}%{auth_enabled}.conf @@ -64,6 +64,7 @@ def generate_backend_filename(backend: BackendOut) -> str: # CORE GETTER FUNCTIONS async def get_backends() -> List[BackendOut]: + settings = get_settings() if not os.path.exists(settings.FORC_BACKEND_PATH) and not os.access(settings.FORC_BACKEND_PATH, os.W_OK): logger.error("Not able to access configured backend path.") return [] @@ -167,6 +168,7 @@ def get_basekey_from_backend(backend: BackendOut) -> str | None: # CORE MUTATOR AND SERVICE FUNCTIONS async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: + settings = get_settings() logger.debug(f"Creating backend for owner: {payload.owner} with template: {payload.template} version: {payload.template_version}") # overwrite payload as BackendTemp for generate_backend_by_template() @@ -198,6 +200,7 @@ async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: async def delete_backend(backend_id) -> bool: + settings = get_settings() backend_path_filenames = get_valid_backend_filenames() if not backend_path_filenames: return False @@ -295,6 +298,7 @@ async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | def check_backend_path_file() -> bool: + settings = get_settings() # check if backend path exists, is accessible and has files if not os.path.exists(settings.FORC_BACKEND_PATH) and not os.access(settings.FORC_BACKEND_PATH, os.W_OK): logger.error("Not able to access configured backend path.") @@ -320,6 +324,7 @@ def check_backend_path_file_naming(backend_path_filename: str) -> bool | None: def get_backend_path_filenames() -> List[str] | None: + settings = get_settings() # get list of filenames in backend path if not check_backend_path_file(): return None diff --git a/FastapiOpenRestyConfigurator/app/main/service/user.py b/FastapiOpenRestyConfigurator/app/main/service/user.py index d0da9762..d785bf5f 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/user.py +++ b/FastapiOpenRestyConfigurator/app/main/service/user.py @@ -11,10 +11,10 @@ from ..model.serializers import User logger = logging.getLogger("service") -settings = get_settings() async def get_users(backend_id): + settings = get_settings() backend_id = secure_filename(str(backend_id)) user_id_path = f"{settings.FORC_USER_PATH}/{backend_id}" if not os.path.exists(user_id_path) and not os.access(user_id_path, os.R_OK): @@ -28,6 +28,7 @@ async def get_users(backend_id): async def add_user(backend_id, user_id): + settings = get_settings() backend_id = secure_filename(str(backend_id)) if "@" in user_id: user_id_parts = user_id.split("@") @@ -59,6 +60,7 @@ async def add_user(backend_id, user_id): async def delete_user(backend_id, user_id): + settings = get_settings() backend_id = secure_filename(str(backend_id)) if "@" in user_id: user_id_parts = user_id.split("@") @@ -93,6 +95,7 @@ async def delete_user(backend_id, user_id): async def delete_all(backend_id): + settings = get_settings() backend_id = secure_filename(str(backend_id)) user_id_path = f"{settings.FORC_USER_PATH}/{backend_id}" if not os.path.exists(user_id_path): diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py deleted file mode 100644 index 834a105f..00000000 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ /dev/null @@ -1,87 +0,0 @@ -import pytest -from unittest.mock import patch - -from app.main.model.serializers import BackendOut - -from app.main.service import backend as backend_service - -from werkzeug.exceptions import NotFound - -file_path_example_1 = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" -file_path_example_2 = "/var/forc/backend_path/9876543210%otheruser%cat_200%othertemplate%v02%1.conf" - - -@pytest.mark.asyncio -async def test_get_backend_by_id(): - - with patch( - "app.main.service.backend.get_backends", - return_value = [BackendOut(backend_id = 123), BackendOut(backend_id = 456)] - ) as mock_get_backends: - - response: BackendOut = await backend_service.get_backend_by_id(123) - - assert response.backend_id == 123 - mock_get_backends.assert_awaited_once() - -@pytest.mark.asyncio -async def test_get_backend_by_id_not_found(): - - with pytest.raises(NotFound): - with patch( - "app.main.service.backend.get_backends", - return_value = [BackendOut(backend_id = 123), BackendOut(backend_id = 456)] - ) as mock_get_backends: - - await backend_service.get_backend_by_id(789) - mock_get_backends.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_get_filepath_by_id(): - - with patch( - "app.main.service.backend.get_backends", - return_value = [ - BackendOut( - backend_id = 1234567890, - file_path = file_path_example_1), - BackendOut( - backend_id = 9876543210, - file_path = file_path_example_2)] - ) as mock_get_backends: - - response: str = await backend_service.get_file_path_by_id(1234567890) - - assert response == file_path_example_1 - mock_get_backends.assert_awaited_once() - -@pytest.mark.asyncio -async def test_get_filepath_by_id_not_found(): - - with pytest.raises(NotFound): - with patch( - "app.main.service.backend.get_backends", - return_value = BackendOut(backend_id = 1234567890) - ) as mock_get_backends: - await backend_service.get_file_path_by_id(9876543210) - mock_get_backends.assert_awaited_once() - -""" -@pytest.mark.asyncio -async def test_create_backend(): # check kwargs payload id and suffix number at the end - - with patch( - "app.main.service.backend.generate_suffix_number", - return_value = 111 - ) as mock_generate_suffix_number, patch( - "app.main.service.backend.random_with_n_digits", - return_value = 9876543210 - ) as mock_random_with_n_digits, patch( - "app.main.service.backend.generate_backend_by_template", - - -@pytest.mark.asyncio -async def test_update_backend_authorization_activate(): - -""" diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py deleted file mode 100644 index af71b901..00000000 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest -from unittest.mock import patch -from fastapi import HTTPException - -from app.main.views import backend as backend_views - - -@pytest.mark.parametrize( - "exception_expected, backend_id, body", - [ - (False, 123, {"auth_enabled": True}), - # success cases - (False, 123, {"auth_enabled": 1}), - (False, 123, {"auth_enabled": 0}), - (False, 123, {"auth_enabled": False}), - # corrupted backend_id - # TODO: more tests when there is further validation on backend_id - (True, "not an int", {"auth_enabled": True}), - (True, None, {"auth_enabled": True}), - # corrupted body - (True, 123, {"auth_enabled": "not a boolean"}), - (True, 123, {"auth_enabled": ""}), - (True, 123, {"differrent_value": True}), - (True, 123, None), - (True, 123, {}), - ] -) -@pytest.mark.asyncio -async def test_backend_update_auth(exception_expected, backend_id, body): - - with patch( - "app.main.service.backend.update_backend_authorization" - ) as mock_update_backend_authorization: - # success case - try: - await backend_views.backend_update_auth( - backend_id = backend_id, - body = body, - api_key = object() # type: ignore[reportArgumentType] - ) - mock_update_backend_authorization.assert_called_once_with(backend_id = 123, auth_enabled = True) - # fail case - except Exception: - if exception_expected: - mock_update_backend_authorization.assert_not_awaited() - #if not exception_expected: diff --git a/FastapiOpenRestyConfigurator/app/main/util/auth.py b/FastapiOpenRestyConfigurator/app/main/util/auth.py index b69a5a40..711daba2 100755 --- a/FastapiOpenRestyConfigurator/app/main/util/auth.py +++ b/FastapiOpenRestyConfigurator/app/main/util/auth.py @@ -10,12 +10,12 @@ API_KEY_NAME = "X-API-KEY" api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) -settings = get_settings() async def get_api_key( api_key_header_in: str = Security(api_key_header), ): + settings = get_settings() if api_key_header_in == settings.FORC_API_KEY.get_secret_value(): return api_key_header_in else: diff --git a/FastapiOpenRestyConfigurator/app/main/util/templating.py b/FastapiOpenRestyConfigurator/app/main/util/templating.py index d7ecee7d..7877f8ba 100755 --- a/FastapiOpenRestyConfigurator/app/main/util/templating.py +++ b/FastapiOpenRestyConfigurator/app/main/util/templating.py @@ -4,47 +4,65 @@ import jinja2 import logging import os +from functools import lru_cache from ..model.serializers import BackendTemp from ..config import get_settings logger = logging.getLogger("util") -settings = get_settings() -logger.info("Loading the templating engine.") -try: - templateLoader = jinja2.FileSystemLoader(searchpath=settings.FORC_TEMPLATE_PATH) - templateEnv = jinja2.Environment(loader=templateLoader, autoescape=True) -except jinja2.exceptions.TemplatesNotFound: - logger.error("Was not able to load template engine. Adjust the templates_path in the config.") +@lru_cache +def _template_env(): + """ + Lazily initialize and cache the Jinja environment. + Called only at runtime, never at import time. + """ + settings = get_settings() + logger.info("Loading the templating engine.") -async def generate_backend_by_template(backend_temp: BackendTemp, suffix_number) -> str | None: - logger.debug(f"Generating backend from template: {backend_temp.template} with version: {backend_temp.template_version}") - if not templateLoader or not templateEnv: - logger.error("The template engine is not loaded. Can't generate backend.") - return None - assembled_template_filename = f"{backend_temp.template}%{backend_temp.template_version}.conf" - if not os.path.isfile(f"{settings.FORC_TEMPLATE_PATH}/{assembled_template_filename}"): - logger.error(f"Not able to find {settings.FORC_TEMPLATE_PATH}/{assembled_template_filename}") + loader = jinja2.FileSystemLoader( + searchpath=settings.FORC_TEMPLATE_PATH + ) + + env = jinja2.Environment( + loader=loader, + autoescape=True + ) + + return env, settings + + +async def generate_backend_by_template( + backend_temp: BackendTemp, + suffix_number: int +) -> str | None: + + env, settings = _template_env() + + assembled_template_filename = ( + f"{backend_temp.template}%{backend_temp.template_version}.conf" + ) + + template_path = os.path.join( + settings.FORC_TEMPLATE_PATH, + assembled_template_filename + ) + + if not os.path.isfile(template_path): + logger.error(f"Template not found: {template_path}") return None - template = templateEnv.get_template(assembled_template_filename) - logger.info({ - "event": "templating_vars", - "auth_enabled": backend_temp.auth_enabled, - "assembled template filename": assembled_template_filename, - "template": template - }) + template = env.get_template(assembled_template_filename) rendered_backend = template.render( - key_url = f"{backend_temp.user_key_url}_{suffix_number}", - owner = backend_temp.owner, - backend_id = backend_temp.id, - forc_backend_path = settings.FORC_BACKEND_PATH, - location_url = backend_temp.upstream_url, - auth_enabled = backend_temp.auth_enabled + key_url=f"{backend_temp.user_key_url}_{suffix_number}", + owner=backend_temp.owner, + backend_id=backend_temp.id, + forc_backend_path=settings.FORC_BACKEND_PATH, + location_url=backend_temp.upstream_url, + auth_enabled=backend_temp.auth_enabled, ) - # logger.debug(f"Rendered backend: {rendered_backend}") + return rendered_backend diff --git a/FastapiOpenRestyConfigurator/pytest.ini b/FastapiOpenRestyConfigurator/pytest.ini index 5d8c57f5..5ee64771 100644 --- a/FastapiOpenRestyConfigurator/pytest.ini +++ b/FastapiOpenRestyConfigurator/pytest.ini @@ -1,3 +1,2 @@ -# pytest.ini [pytest] -pythonpath = /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ +testpaths = tests diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index bd1ec3bd..526fcc01 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -5,7 +5,7 @@ Jinja2==3.1.6 python-dotenv==1.2.1 gunicorn==23.0.0 pydantic-settings - +factory-boy==3.3.3 # testing pytest==8.4.2 pytest-asyncio # TODO: @reviewer: which version? From 9c6a722df1f5831f0e85ba67a870b566fc9255c0 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Wed, 21 Jan 2026 04:24:18 +0000 Subject: [PATCH 017/118] finished test_views_backend, started test_service_backend, improvements to service/backend.py WIP --- .../app/main/model/serializers.py | 2 +- .../app/main/service/backend.py | 49 ++-- .../app/main/tests/test_service_backend.py | 253 ++++++++++++++++++ .../app/main/tests/test_views_backend.py | 46 ++++ 4 files changed, 324 insertions(+), 26 deletions(-) create mode 100644 FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py create mode 100644 FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py diff --git a/FastapiOpenRestyConfigurator/app/main/model/serializers.py b/FastapiOpenRestyConfigurator/app/main/model/serializers.py index 717f089f..16c20d5f 100755 --- a/FastapiOpenRestyConfigurator/app/main/model/serializers.py +++ b/FastapiOpenRestyConfigurator/app/main/model/serializers.py @@ -119,7 +119,7 @@ class BackendOut(BackendBase): """ Backend class which holds information needed when returning a backend. """ - id: int = Field( + id: int = Field( # @reviewer: should we rename id to backend_id for consistency? ..., title="ID", description="ID of the backend.", diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 335b46f9..fed20e20 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -33,18 +33,24 @@ def random_with_n_digits(n): # TODO: unlikely but potential error cause, if two users have same randomly generated user_key_url! -async def generate_suffix_number(user_key_url = None) -> str: +async def generate_suffix_number(user_key_url: str = None) -> int: if user_key_url is None: - return "100" + return 100 + + current_suffix_number = int(user_key_url.split("_")[1]) + if current_suffix_number < 100 or current_suffix_number > 999: + logger.error("Invalid user_key_url provided for suffix generation: " + str(user_key_url)) + raise InternalServerError("Invalid user_key_url provided for suffix generation.") # look for backends with same user_key_url current_backends: List[BackendOut] = await get_backends() same_name_backend_ids = [] for backend in current_backends: - if backend.location_url.split("_")[0] == user_key_url: - same_name_backend_ids.append(int(backend.location_url.split("_")[1])) + location_url = backend.location_url.split("_") + if location_url[0] == user_key_url: + same_name_backend_ids.append(int(location_url[1])) if not same_name_backend_ids: - return "100" + return 100 # return highest found suffix number + 1 to iterate same_name_backend_ids.sort() @@ -52,15 +58,14 @@ async def generate_suffix_number(user_key_url = None) -> str: if highest_id == 999: logger.warning("Reached max index number for requested user_key_url: " + user_key_url) raise InternalServerError("Reached max index number for requested user_key_url (limit=999).") - return str(highest_id + 1) + return highest_id + 1 -def generate_backend_filename(backend: BackendOut) -> str: +def generate_backend_filename(backend: BackendOut) -> str | None: + backend = BackendOut.model_validate(backend) # ensure validity of backend filename = f"{backend.id}%{backend.owner}%{backend.location_url}%{backend.template}%{backend.template_version}%{str(int(backend.auth_enabled))}.conf" return filename - - # CORE GETTER FUNCTIONS async def get_backends() -> List[BackendOut]: @@ -97,7 +102,7 @@ async def get_backends() -> List[BackendOut]: async def get_backend_by_id(backend_id: int) -> BackendOut: valid_backends: List[BackendOut] = await get_backends() for backend in valid_backends: - if int(backend.id) == int(backend_id): + if int(backend.id) == int(backend_id): # @reviewer: are we sure that there is only one backend with this backend_id? return backend raise NotFound(f"Backend with id {backend_id} was not found.") @@ -117,13 +122,8 @@ async def get_backends_upstream_urls() -> Dict[str, List[BackendOut]]: async def get_file_path_by_id(backend_id: int) -> str: - backends: List[BackendOut] = await get_backends() - logger.debug(f"Searching file path for backend id: {backend_id} in backends: {backends}") - for backend in backends: - if int(backend.id) == int(backend_id): - logger.debug(f"Returning found file path for backend id: {backend_id}: {backend.file_path}") - return backend.file_path - raise NotFound(f"Backend with id {backend_id} not found.") + backend: BackendOut = await get_backend_by_id(backend_id) + return backend.file_path @@ -131,7 +131,7 @@ async def get_file_path_by_id(backend_id: int) -> str: def extract_proxy_pass(file_path) -> str | None: # proxy_pass consists of upstream_url with potential trailing path, see guacamole template - with open(file_path, 'r') as file: + with open(file_path, 'r') as file: # @reviewer: add error handling, e.g. file_path = None ? content = file.read() match = re.search(r'proxy_pass\s+(http[^\s;]+);', content) @@ -167,15 +167,14 @@ def get_basekey_from_backend(backend: BackendOut) -> str | None: # CORE MUTATOR AND SERVICE FUNCTIONS -async def create_backend(payload: BackendIn, **kwargs) -> BackendTemp: +async def create_backend(payload_input: BackendIn, **kwargs) -> BackendTemp: settings = get_settings() - logger.debug(f"Creating backend for owner: {payload.owner} with template: {payload.template} version: {payload.template_version}") # overwrite payload as BackendTemp for generate_backend_by_template() - payload: BackendTemp = BackendTemp(**payload.model_dump()) + payload: BackendTemp = BackendTemp(**payload_input.model_dump()) # generate or reuse backend id and suffix number - payload, suffix_number = await set_backend_id_and_suffix_for(payload, **kwargs) + payload, suffix_number = await set_backend_id_and_suffix#(payload, **kwargs) # generate backend and location_url backend_file_contents = await generate_backend_by_template(payload, suffix_number) @@ -255,13 +254,13 @@ async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> B # HELPER FUNCTIONS FOR MUTATORS -async def set_backend_id_and_suffix_for(payload: BackendTemp, **kwargs) -> tuple[BackendTemp, str]: +async def set_backend_id_and_suffix(payload: BackendTemp, **kwargs) -> tuple[BackendTemp, int]: # override id and suffix if provided from update_backend_authorization() if 'id' in kwargs and 'location_url' in kwargs: payload = payload.model_copy(update={'id': str(kwargs.get('id'))}) - suffix_number = str(kwargs.get('location_url')).split("_")[1] + suffix_number = int(str(kwargs.get('location_url')).split("_")[1]) else: - payload.id = str(random_with_n_digits(10)) # TODO: should we refactor id: int? see delete_backend(). maybe it has something to do with int beginning with 0 + payload.id = random_with_n_digits(10) suffix_number = await generate_suffix_number(payload.user_key_url) logger.debug(f"Set backend id: {payload.id} with suffix number: {suffix_number}") return payload, suffix_number diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py new file mode 100644 index 00000000..169fd388 --- /dev/null +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -0,0 +1,253 @@ +import pytest +from unittest.mock import patch + +from app.main.model.serializers import BackendOut + +from app.main.service import backend as backend_service + +from werkzeug.exceptions import NotFound + +file_path_example_1 = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" +file_path_example_2 = "/var/forc/backend_path/9876543210%otheruser%cat_200%othertemplate%v02%1.conf" + +# TEST DATA + +# backend test cases: (exception_expected, filename, backend), see test_generate_backend_filename() +test_backends = [ + (False, "123%testuser%dog_100%testtemplate%v01%0.conf", + BackendOut.model_construct( + id = 123, + owner = "testuser", + location_url = "dog_100", + template = "testtemplate", + template_version = "v01", + auth_enabled = False + )), + (False, None, + BackendOut.model_construct( + id = None, + owner = "otheruser", + location_url = "cat_200", + template = "othertemplate", + template_version = "v02", + auth_enabled = True + )), + (False, None, + BackendOut.model_construct( + id = 123, + owner = None, + location_url = "cat_200", + template = "othertemplate", + template_version = "v02", + auth_enabled = True + )), + (False, None, + BackendOut.model_construct( + id = 123, + owner = "otheruser", + location_url = None, + template = "othertemplate", + template_version = "v02", + auth_enabled = True + )), + (False, None, + BackendOut.model_construct( + id = 123, + owner = "otheruser", + location_url = "cat_200", + template = None, + template_version = "v02", + auth_enabled = True + )), + (False, None, + BackendOut.model_construct( + id = 123, + owner = "otheruser", + location_url = "cat_200", + template = "othertemplate", + template_version = None, + auth_enabled = True + )), + (False, None, + BackendOut.model_construct( + id = 123, + owner = "otheruser", + location_url = "cat_200", + template = "othertemplate", + template_version = "v02", + auth_enabled = None + )) + ] + + +# HELPER FUNCTIONS + +@pytest.mark.parametrize( + "exception_expected, user_key_url", + [ + (False, None), + (False, 200), + (True, 999), + (True, -10), + (True, None), + (True, "not an int") + ] +) +@pytest.mark.asyncio +async def test_generate_suffix_number(): + + + +@pytest.mark.parametrize( + "exception_expected, filename, backend", + test_backends +) +@pytest.mark.asyncio +async def test_generate_backend_filename(exception_expected, filename, backend): + ... + assert filename == f"{backend.id}%{backend.owner}%{backend.location_url}%{backend.template}%{backend.template_version}%{str(int(backend.auth_enabled))}.conf" + + + +# CORE GETTER FUNCTIONS + +@pytest.mark.parametrize( + "exception_expected, backend_id", + [ + (False, 123), + (True, None) + ] +) +@pytest.mark.asyncio +async def test_get_backend_by_id(exception_expected, backend_id): + + with patch( + "app.main.service.backend.get_backends", + return_value = [BackendOut.model_construct(backend_id = backend_id), BackendOut.model_construct(backend_id = 456)] + ) as mock_get_backends: + + try: + response: BackendOut = await backend_service.get_backend_by_id(backend_id) + # success case + if not exception_expected: + assert response.id == backend_id + mock_get_backends.assert_awaited_once() + # fail case + except Exception as e: + if not exception_expected: + raise e + + +@pytest.mark.parametrize( + "exception_expected, backend_id", + [ + (False, 123), + (True, None) + ] +) +@pytest.mark.asyncio +async def test_get_filepath_by_id(exception_expected, backend_id): + + with patch( + "app.main.service.backend.get_backend_by_id", + return_value = BackendOut.model_construct(backend_id = backend_id, file_path = "test_path") + ) as mock_get_backend_by_id: + + try: + filepath: str = await backend_service.get_file_path_by_id(backend_id) + # success case + if not exception_expected: + assert filepath == "test_path" + mock_get_backend_by_id.assert_awaited_once() + # fail case + except Exception as e: + if not exception_expected: + raise e + + + +# FURTHER GETTER FUNCTIONS + +@pytest.mark.parametrize( + "exception_expected, proxy_pass", + [ + (False, "http://1.1.1.1:1000/guacamole/"), + (True, None) + ] +) +@pytest.mark.asyncio +async def test_get_upstream_url(exception_expected, proxy_pass): + with patch( + "app.main.service.backend.extract_proxy_pass", + return_value = proxy_pass + ) as mock_extract_proxy_pass: + + upstream_url = backend_service.get_upstream_url("") + # success case + if not exception_expected: + assert upstream_url == "http://1.1.1.1:1000" + mock_extract_proxy_pass.assert_called_once() + # fail case + else: + assert upstream_url is None + + +@pytest.mark.parametrize( + "exception_expected, proxy_pass", + [ + (False, "http://1.1.1.1:1000/guacamole/"), + (True, None) + ] +) +def test_get_basekey_from_backend(): + ... + + + +# CORE MUTATOR AND SERVICE FUNCTIONS + +@pytest.mark.asyncio +async def test_create_backend(): + ... + +@pytest.mark.asyncio +async def test_delete_backend(): + ... + +@pytest.mark.asyncio +async def test_update_backend_authorization(): + ... + + + +# HELPER FUNCTIONS FOR MUTATORS + +@pytest.mark.asyncio +async def test_set_backend_id_and_suffix(): + ... + +@pytest.mark.asyncio +async def test_delete_duplicate_backends(): + ... + +@pytest.mark.asyncio +async def test_convert_backend_temp_to_out(): + ... + +def test_check_backend_path_file(): + ... + +def test_check_backend_path_file_naming(): + ... + +def test_get_backend_path_filenames(): + ... + +def test_get_valid_backend_filenames(): + ... + +def test_filter_backend_filenames_by_id(): + ... + +def test_build_payload_for_auth_update(): + ... \ No newline at end of file diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py new file mode 100644 index 00000000..e3b8c6da --- /dev/null +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py @@ -0,0 +1,46 @@ +import pytest +from unittest.mock import patch +from fastapi import HTTPException + +from app.main.views import backend as backend_views + + +@pytest.mark.parametrize( + "exception_expected, backend_id, body", + [ + (False, 123, {"auth_enabled": True}), + # success cases + (False, 123, {"auth_enabled": 1}), + (False, 123, {"auth_enabled": 0}), + (False, 123, {"auth_enabled": False}), + # corrupted backend_id + # TODO: more tests when there is further validation on backend_id + (True, "not an int", {"auth_enabled": True}), + (True, None, {"auth_enabled": True}), + # corrupted body + (True, 123, {"auth_enabled": "not a boolean"}), + (True, 123, {"auth_enabled": ""}), + (True, 123, {"differrent_value": True}), + (True, 123, None), + (True, 123, {}), + ] +) +@pytest.mark.asyncio +async def test_backend_update_auth(exception_expected, backend_id, body): + + with patch( + "app.main.service.backend.update_backend_authorization" + ) as mock_update_backend_authorization: + try: + await backend_views.backend_update_auth( + backend_id = backend_id, + body = body, + api_key = object() # type: ignore[reportArgumentType] + ) + # success case + if not exception_expected: + mock_update_backend_authorization.assert_called_once_with(backend_id = 123, auth_enabled = True) + # fail case + except Exception: + if exception_expected: + mock_update_backend_authorization.assert_not_awaited() From 0cca71c456efb6205e400fdde115292d5440bd51 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Wed, 21 Jan 2026 08:35:40 +0000 Subject: [PATCH 018/118] added tests to test_service_backend.py, corrections to service/backend.py --- .../app/main/model/serializers.py | 5 +- .../app/main/service/backend.py | 99 +++--- .../app/main/tests/test_service_backend.py | 292 +++++++++++------- .../app/main/tests/test_views_backend.py | 2 +- 4 files changed, 247 insertions(+), 151 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/model/serializers.py b/FastapiOpenRestyConfigurator/app/main/model/serializers.py index 16c20d5f..d3e7d37e 100755 --- a/FastapiOpenRestyConfigurator/app/main/model/serializers.py +++ b/FastapiOpenRestyConfigurator/app/main/model/serializers.py @@ -31,6 +31,8 @@ upstream_url_regex = r"^(https?)://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{1,5})(/[a-zA-Z0-9_-]+/)?$" +# TODO: needs refactoring to comply with python 3.10 and pydantic v2 + class BackendBase(BaseModel): """ Base class for backend. @@ -119,7 +121,7 @@ class BackendOut(BackendBase): """ Backend class which holds information needed when returning a backend. """ - id: int = Field( # @reviewer: should we rename id to backend_id for consistency? + id: int = Field( # TODO: needs refactoring: change type to int and rename to backend_id ..., title="ID", description="ID of the backend.", @@ -150,6 +152,7 @@ class BackendTemp(BackendIn, BackendOut): user_key_url: str = None upstream_url: str = None auth_enabled: bool = None + file_path: str = None class Template(BaseModel): diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index fed20e20..9f0b6447 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -33,38 +33,44 @@ def random_with_n_digits(n): # TODO: unlikely but potential error cause, if two users have same randomly generated user_key_url! -async def generate_suffix_number(user_key_url: str = None) -> int: +async def generate_suffix_number(user_key_url: str | None = None) -> int: if user_key_url is None: return 100 - current_suffix_number = int(user_key_url.split("_")[1]) + # extract current suffix number, check validity + current_suffix_number: int = int(user_key_url.split("_")[1]) if current_suffix_number < 100 or current_suffix_number > 999: - logger.error("Invalid user_key_url provided for suffix generation: " + str(user_key_url)) + logger.error("Invalid user_key_url provided for suffix generation: " + user_key_url) raise InternalServerError("Invalid user_key_url provided for suffix generation.") # look for backends with same user_key_url - current_backends: List[BackendOut] = await get_backends() - same_name_backend_ids = [] - for backend in current_backends: - location_url = backend.location_url.split("_") - if location_url[0] == user_key_url: - same_name_backend_ids.append(int(location_url[1])) - if not same_name_backend_ids: + backends: List[BackendOut] = await get_backends() + same_name_backend_suffixes: List[int] = [] + for backend in backends: + if backend.location_url == user_key_url: + suffix: int = int(backend.location_url.split("_")[1]) + same_name_backend_suffixes.append(suffix) + if not same_name_backend_suffixes: return 100 # return highest found suffix number + 1 to iterate - same_name_backend_ids.sort() - highest_id = same_name_backend_ids[-1] + same_name_backend_suffixes.sort() + highest_id: int = same_name_backend_suffixes[-1] if highest_id == 999: logger.warning("Reached max index number for requested user_key_url: " + user_key_url) raise InternalServerError("Reached max index number for requested user_key_url (limit=999).") return highest_id + 1 -def generate_backend_filename(backend: BackendOut) -> str | None: - backend = BackendOut.model_validate(backend) # ensure validity of backend - filename = f"{backend.id}%{backend.owner}%{backend.location_url}%{backend.template}%{backend.template_version}%{str(int(backend.auth_enabled))}.conf" - return filename +def generate_backend_filename(backend: BackendOut) -> str: + b: BackendOut = backend + if b.id and b.owner and b.location_url and b.template and b.template_version and b.auth_enabled is not None: + return f"{str(b.id)}%{b.owner}%{b.location_url}%{b.template}%{b.template_version}%{str(int(b.auth_enabled))}.conf" + else: + logger.error("Not all necessary backend fields are set for filename generation: " + str(backend)) + raise InternalServerError("Filename generation failed.") + + # CORE GETTER FUNCTIONS @@ -86,7 +92,7 @@ async def get_backends() -> List[BackendOut]: logger.warning("Found a backend file with wrong naming, skipping it: " + str(filename)) continue backend: BackendOut = BackendOut( - id = match.group(1), + id = int(match.group(1)), owner = match.group(2), location_url = match.group(3), template = match.group(4), @@ -121,17 +127,13 @@ async def get_backends_upstream_urls() -> Dict[str, List[BackendOut]]: return upstream_urls -async def get_file_path_by_id(backend_id: int) -> str: - backend: BackendOut = await get_backend_by_id(backend_id) - return backend.file_path - - # FURTHER GETTER FUNCTIONS def extract_proxy_pass(file_path) -> str | None: # proxy_pass consists of upstream_url with potential trailing path, see guacamole template - with open(file_path, 'r') as file: # @reviewer: add error handling, e.g. file_path = None ? + # TODO: add error handling, e.g. file_path = None + with open(file_path, 'r') as file: content = file.read() match = re.search(r'proxy_pass\s+(http[^\s;]+);', content) @@ -144,9 +146,11 @@ def extract_proxy_pass(file_path) -> str | None: def get_upstream_url(file_path) -> str | None: - # extracts upstream_url from proxy_pass by removing trailing path, see guacamole template + """ + Extracts upstream_url from proxy_pass by removing trailing path, see guacamole template + """ proxy_pass = extract_proxy_pass(file_path) - if proxy_pass is None: + if proxy_pass is None or proxy_pass == "": return None # split and rejoin to remove trailing path to remove potential trailing path, unaffected if no trailing path @@ -155,12 +159,21 @@ def get_upstream_url(file_path) -> str | None: def get_basekey_from_backend(backend: BackendOut) -> str | None: + """ + Extracts basekey from location_url by removing suffix pattern. + """ try: - base_key = backend.location_url.rsplit("_", 1)[0] - logger.debug(f"Backend id: {backend.id}, Base key: {base_key}") + if backend is None or backend.location_url is None: + logger.error("backend or backend.location_url is None.") + return None + if "_" not in backend.location_url: + logger.error(f"location_url has no suffix pattern: {backend.location_url}") + return None + # split and return basekey + base_key = backend.location_url.split("_")[0] return base_key except ValueError: - logger.error(f"location_url has no suffix pattern: {backend.location_url}") + logger.error(f"could not get basekey from location_url: {backend.location_url}") return None @@ -254,16 +267,30 @@ async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> B # HELPER FUNCTIONS FOR MUTATORS -async def set_backend_id_and_suffix(payload: BackendTemp, **kwargs) -> tuple[BackendTemp, int]: +async def set_backend_id_and_suffix(backend: BackendTemp, **kwargs) -> tuple[BackendTemp, int]: + """ + Sets backend id and suffix number for a new backend. + If id and location_url are provided in kwargs, they are used to override. + Otherwise, a new id and suffix number are generated. + """ # override id and suffix if provided from update_backend_authorization() if 'id' in kwargs and 'location_url' in kwargs: - payload = payload.model_copy(update={'id': str(kwargs.get('id'))}) - suffix_number = int(str(kwargs.get('location_url')).split("_")[1]) + id: str = str(kwargs.get('id')) + suffix: int = int(str(kwargs.get('location_url')).split("_")[1]) + if not isinstance(id, str) or not isinstance(suffix, int): + logger.error("Provided id or location_url have wrong type.") + raise InternalServerError("Provided id or location_url have wrong type.") + + backend = backend.model_copy(update={'id': id}) + suffix_number = suffix + # if no id provided, generate id and suffix else: - payload.id = random_with_n_digits(10) - suffix_number = await generate_suffix_number(payload.user_key_url) - logger.debug(f"Set backend id: {payload.id} with suffix number: {suffix_number}") - return payload, suffix_number + if kwargs is not None: + logger.warning(f"set_backend_id_and_suffix() received unexpected kwargs: {kwargs}") + raise InternalServerError("Unexpected kwargs provided to set_backend_id_and_suffix().") # @reviewer: should we really error here? + backend = backend.model_copy(update={'id': str(random_with_n_digits(10))}) + suffix_number = await generate_suffix_number(backend.user_key_url) + return backend, suffix_number async def delete_duplicate_backends(upstream_url: str) -> bool: @@ -288,7 +315,7 @@ async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | template = backend_temp.template, template_version = backend_temp.template_version, auth_enabled = backend_temp.auth_enabled, - file_path = await get_file_path_by_id(backend_temp.id) + file_path = (await get_backend_by_id(backend_temp.id)).file_path ) return backend_out except Exception as e: diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 169fd388..f1cf3f5c 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import patch -from app.main.model.serializers import BackendOut +from app.main.model.serializers import BackendOut, BackendTemp from app.main.service import backend as backend_service @@ -15,67 +15,76 @@ # backend test cases: (exception_expected, filename, backend), see test_generate_backend_filename() test_backends = [ (False, "123%testuser%dog_100%testtemplate%v01%0.conf", - BackendOut.model_construct( - id = 123, - owner = "testuser", - location_url = "dog_100", - template = "testtemplate", - template_version = "v01", - auth_enabled = False + BackendOut.model_construct( + id = 123, + owner = "testuser", + location_url = "dog_100", + template = "testtemplate", + template_version = "v01", + auth_enabled = False )), - (False, None, - BackendOut.model_construct( - id = None, - owner = "otheruser", - location_url = "cat_200", - template = "othertemplate", - template_version = "v02", - auth_enabled = True + (False, "456%otheruser%ant_300%anothertemplate%v03%1.conf", + BackendOut.model_construct( + id = 456, + owner = "otheruser", + location_url = "ant_300", + template = "anothertemplate", + template_version = "v03", + auth_enabled = True )), - (False, None, - BackendOut.model_construct( - id = 123, - owner = None, - location_url = "cat_200", - template = "othertemplate", - template_version = "v02", - auth_enabled = True + (True, None, + BackendOut.model_construct( + id = None, + owner = "otheruser", + location_url = "cat_200", + template = "othertemplate", + template_version = "v02", + auth_enabled = True )), - (False, None, - BackendOut.model_construct( - id = 123, - owner = "otheruser", - location_url = None, - template = "othertemplate", - template_version = "v02", - auth_enabled = True + (True, None, + BackendOut.model_construct( + id = 123, + owner = None, + location_url = "cat_200", + template = "othertemplate", + template_version = "v02", + auth_enabled = True )), - (False, None, - BackendOut.model_construct( - id = 123, - owner = "otheruser", - location_url = "cat_200", - template = None, - template_version = "v02", - auth_enabled = True + (True, None, + BackendOut.model_construct( + id = 123, + owner = "otheruser", + location_url = None, + template = "othertemplate", + template_version = "v02", + auth_enabled = True )), - (False, None, - BackendOut.model_construct( - id = 123, - owner = "otheruser", - location_url = "cat_200", - template = "othertemplate", - template_version = None, - auth_enabled = True + (True, None, + BackendOut.model_construct( + id = 123, + owner = "otheruser", + location_url = "cat_200", + template = None, + template_version = "v02", + auth_enabled = True )), - (False, None, - BackendOut.model_construct( - id = 123, - owner = "otheruser", - location_url = "cat_200", - template = "othertemplate", - template_version = "v02", - auth_enabled = None + (True, None, + BackendOut.model_construct( + id = 123, + owner = "otheruser", + location_url = "cat_200", + template = "othertemplate", + template_version = None, + auth_enabled = True + )), + (True, None, + BackendOut.model_construct( + id = 123, + owner = "otheruser", + location_url = "cat_200", + template = "othertemplate", + template_version = "v02", + auth_enabled = None )) ] @@ -83,19 +92,36 @@ # HELPER FUNCTIONS @pytest.mark.parametrize( - "exception_expected, user_key_url", + "exception_expected, expected_suffix, user_key_url", [ - (False, None), - (False, 200), - (True, 999), - (True, -10), - (True, None), - (True, "not an int") + (False, 100, None), + (False, 201, "test_200"), + (True, None, "test_-10"), + (True, None, "test_999"), + (True, None, "test_1000"), + (True, None, "test_150.5"), + (True, None, "test_not-an-int") ] ) @pytest.mark.asyncio -async def test_generate_suffix_number(): - +async def test_generate_suffix_number(exception_expected, expected_suffix, user_key_url): + + with patch( + "app.main.service.backend.get_backends", + return_value = [BackendOut.model_construct(location_url = user_key_url), BackendOut.model_construct(location_url = "animal_100")] + ) as mock_get_backends: + + try: + response_suffix: int = await backend_service.generate_suffix_number(user_key_url) + # success case + if not exception_expected: + assert response_suffix == expected_suffix + if user_key_url is not None: + mock_get_backends.assert_awaited_once() + # fail case + except Exception as e: + if not exception_expected: + raise e @pytest.mark.parametrize( @@ -104,13 +130,23 @@ async def test_generate_suffix_number(): ) @pytest.mark.asyncio async def test_generate_backend_filename(exception_expected, filename, backend): - ... - assert filename == f"{backend.id}%{backend.owner}%{backend.location_url}%{backend.template}%{backend.template_version}%{str(int(backend.auth_enabled))}.conf" + try: + response_filename: str = backend_service.generate_backend_filename(backend) + # success case + if not exception_expected: + assert response_filename == filename + # fail case + except Exception as e: + if not exception_expected: + raise e # CORE GETTER FUNCTIONS +# TODO: async def test_get_backends(): + + @pytest.mark.parametrize( "exception_expected, backend_id", [ @@ -123,14 +159,14 @@ async def test_get_backend_by_id(exception_expected, backend_id): with patch( "app.main.service.backend.get_backends", - return_value = [BackendOut.model_construct(backend_id = backend_id), BackendOut.model_construct(backend_id = 456)] + return_value = [BackendOut.model_construct(id = backend_id), BackendOut.model_construct(id = 456)] ) as mock_get_backends: try: - response: BackendOut = await backend_service.get_backend_by_id(backend_id) + response_backend: BackendOut = await backend_service.get_backend_by_id(backend_id) # success case if not exception_expected: - assert response.id == backend_id + assert response_backend.id == backend_id mock_get_backends.assert_awaited_once() # fail case except Exception as e: @@ -138,72 +174,58 @@ async def test_get_backend_by_id(exception_expected, backend_id): raise e -@pytest.mark.parametrize( - "exception_expected, backend_id", - [ - (False, 123), - (True, None) - ] -) -@pytest.mark.asyncio -async def test_get_filepath_by_id(exception_expected, backend_id): - - with patch( - "app.main.service.backend.get_backend_by_id", - return_value = BackendOut.model_construct(backend_id = backend_id, file_path = "test_path") - ) as mock_get_backend_by_id: - - try: - filepath: str = await backend_service.get_file_path_by_id(backend_id) - # success case - if not exception_expected: - assert filepath == "test_path" - mock_get_backend_by_id.assert_awaited_once() - # fail case - except Exception as e: - if not exception_expected: - raise e - - # FURTHER GETTER FUNCTIONS @pytest.mark.parametrize( - "exception_expected, proxy_pass", + "exception_expected, proxy_pass, expected_upstream_url", [ - (False, "http://1.1.1.1:1000/guacamole/"), - (True, None) + (False, "http://1.1.1.1:1000/guacamole/", "http://1.1.1.1:1000"), + (False, "http://200.100.50.10:4200/other/", "http://200.100.50.10:4200"), + (True, None, None) ] ) @pytest.mark.asyncio -async def test_get_upstream_url(exception_expected, proxy_pass): +async def test_get_upstream_url(exception_expected, proxy_pass, expected_upstream_url): + with patch( "app.main.service.backend.extract_proxy_pass", return_value = proxy_pass ) as mock_extract_proxy_pass: - upstream_url = backend_service.get_upstream_url("") + response_upstream_url = backend_service.get_upstream_url("test_path") + mock_extract_proxy_pass.assert_called_once() + # success case if not exception_expected: - assert upstream_url == "http://1.1.1.1:1000" - mock_extract_proxy_pass.assert_called_once() + assert response_upstream_url == expected_upstream_url # fail case else: - assert upstream_url is None + assert response_upstream_url is None @pytest.mark.parametrize( - "exception_expected, proxy_pass", + "exception_expected, backend, expected_basekey", [ - (False, "http://1.1.1.1:1000/guacamole/"), - (True, None) + (False, BackendOut.model_construct(location_url = "test_100"), "test"), + (False, BackendOut.model_construct(location_url = "example_200"), "example"), + (True, BackendOut.model_construct(location_url = "corrupted"), None), + (True, BackendOut.model_construct(location_url = "123"), None), + (True, None, None) ] ) -def test_get_basekey_from_backend(): - ... +def test_get_basekey_from_backend(exception_expected, backend, expected_basekey): + response_basekey = backend_service.get_basekey_from_backend(backend) + # success case + if not exception_expected: + assert response_basekey == expected_basekey + # fail case + else: + assert response_basekey is None +""" # CORE MUTATOR AND SERVICE FUNCTIONS @pytest.mark.asyncio @@ -217,15 +239,58 @@ async def test_delete_backend(): @pytest.mark.asyncio async def test_update_backend_authorization(): ... +""" # HELPER FUNCTIONS FOR MUTATORS +@pytest.mark.parametrize( + "exception_expected, kwargs", + [ + # SUCESS CASES + (False, None), + (False, {"id": 123, "location_url": "test_200"}), + # FAIL CASES + # one param is missing + (True, {"id": 123}), + (True, {"location_url": "test_200"}), + # required param is None + (True, {"id": None, "location_url": "test_200"}), + (True, {"id": 123, "location_url": None}), + # wrong type param + (True, {"id": "not-an-int", "location_url": "test_200"}), + (True, {"id": 123, "location_url": 111}) + ] +) @pytest.mark.asyncio -async def test_set_backend_id_and_suffix(): - ... +async def test_set_backend_id_and_suffix(exception_expected, kwargs): + with patch( + "app.main.service.backend.generate_suffix_number", + ) as mock_generate_suffix_number: + + try: + backend, suffix_number = await backend_service.set_backend_id_and_suffix(BackendTemp.model_construct(), **kwargs) + # success case + if not exception_expected: + assert backend.id + assert suffix_number + if kwargs is None: + mock_generate_suffix_number.assert_awaited_once() + else: + mock_generate_suffix_number.assert_not_awaited() + assert backend.id == kwargs["id"] + # fail case + except Exception as e: + if not exception_expected: + raise e + + + + + +""" @pytest.mark.asyncio async def test_delete_duplicate_backends(): ... @@ -250,4 +315,5 @@ def test_filter_backend_filenames_by_id(): ... def test_build_payload_for_auth_update(): - ... \ No newline at end of file + ... +""" \ No newline at end of file diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py index e3b8c6da..ba799f8a 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_views_backend.py @@ -8,8 +8,8 @@ @pytest.mark.parametrize( "exception_expected, backend_id, body", [ - (False, 123, {"auth_enabled": True}), # success cases + (False, 123, {"auth_enabled": True}), (False, 123, {"auth_enabled": 1}), (False, 123, {"auth_enabled": 0}), (False, 123, {"auth_enabled": False}), From cd9f4eb480629aa6fac012b4b9e145f455a999aa Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Wed, 21 Jan 2026 15:52:05 +0000 Subject: [PATCH 019/118] refactored some backend functions for clarity and consistency; added tests for duplicate backend deletion --- .../app/main/service/backend.py | 52 ++++++++----- .../app/main/tests/test_service_backend.py | 78 +++++++++++++++++-- 2 files changed, 103 insertions(+), 27 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 9f0b6447..63f7ae70 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -113,18 +113,21 @@ async def get_backend_by_id(backend_id: int) -> BackendOut: raise NotFound(f"Backend with id {backend_id} was not found.") -async def get_backends_upstream_urls() -> Dict[str, List[BackendOut]]: +async def get_backends_proxy_pass() -> Dict[str, List[BackendOut]]: + """ + Returns a dictionary mapping proxy_pass values to lists of BackendOuts that use them. + """ valid_backends: List[BackendOut] = await get_backends() - upstream_urls = {} + proxy_passes = {} for backend in valid_backends: - upstream_url = extract_proxy_pass(backend.file_path) - if upstream_url: - if upstream_url not in upstream_urls: - upstream_urls[upstream_url] = [] - upstream_urls[upstream_url].append(backend) + proxy_pass = extract_proxy_pass(backend.file_path) + if proxy_pass: + if proxy_pass not in proxy_passes: + proxy_passes[proxy_pass] = [] + proxy_passes[proxy_pass].append(backend) - return upstream_urls + return proxy_passes @@ -147,7 +150,7 @@ def extract_proxy_pass(file_path) -> str | None: def get_upstream_url(file_path) -> str | None: """ - Extracts upstream_url from proxy_pass by removing trailing path, see guacamole template + Extracts upstream_url from proxy_pass and removes trailing path, see guacamole template """ proxy_pass = extract_proxy_pass(file_path) if proxy_pass is None or proxy_pass == "": @@ -197,7 +200,7 @@ async def create_backend(payload_input: BackendIn, **kwargs) -> BackendTemp: payload.location_url = f"{payload.user_key_url}_{suffix_number}" # check for duplicates and delete them before creating new backend - if not await delete_duplicate_backends(payload.upstream_url): + if not await delete_duplicate_backends(payload): raise InternalServerError("Server was not able to delete duplicate backends before creating a new one.") # save BackendOut info in filename, create backend file in filesystem @@ -285,7 +288,7 @@ async def set_backend_id_and_suffix(backend: BackendTemp, **kwargs) -> tuple[Bac suffix_number = suffix # if no id provided, generate id and suffix else: - if kwargs is not None: + if kwargs != {}: logger.warning(f"set_backend_id_and_suffix() received unexpected kwargs: {kwargs}") raise InternalServerError("Unexpected kwargs provided to set_backend_id_and_suffix().") # @reviewer: should we really error here? backend = backend.model_copy(update={'id': str(random_with_n_digits(10))}) @@ -293,15 +296,26 @@ async def set_backend_id_and_suffix(backend: BackendTemp, **kwargs) -> tuple[Bac return backend, suffix_number -async def delete_duplicate_backends(upstream_url: str) -> bool: - # check for duplicates in ip and port and delete them - upstream_urls: Dict[str, List[BackendOut]] = await get_backends_upstream_urls() - matching_urls_backends: List[BackendOut] = upstream_urls.get(upstream_url, []) +# TODO: handle cases where same proxy_pass exists with and without trailing path (guacamole) +async def delete_duplicate_backends(backend_with_proxy_pass: BackendIn) -> bool: + """ + Checks for duplicates in proxy_pass (namely upstream_url) for a given backend and deletes all of them. + Returns only False, if a deletion failed. Returns True if no backends were found and thus deleted. + """ + # get proxy_pass from provided backend + proxy_pass = backend_with_proxy_pass.upstream_url + # get all backends linked to the given proxy_pass + backends_proxy_passes: Dict[str, List[BackendOut]] = await get_backends_proxy_pass() + matching_backends: List[BackendOut] = backends_proxy_passes.get(proxy_pass, []) + # delete all matching backends success: bool = True - for backend in matching_urls_backends: - logger.info(f"Deleting existing Backend with same Upstream Url - {upstream_url}") - if not await delete_backend(backend.id): - success = False + if len(matching_backends) == 0: + logger.warning("No backends found for matching, proxy_pass: " + str(proxy_pass)) + else: + for backend in matching_backends: + logger.info(f"Deleting existing backend with same proxy_pass: {proxy_pass}, backend id: {backend.id}") + if not await delete_backend(backend_id = backend.id): + return False return success diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index f1cf3f5c..472266e0 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import patch -from app.main.model.serializers import BackendOut, BackendTemp +from app.main.model.serializers import BackendIn, BackendOut, BackendTemp from app.main.service import backend as backend_service @@ -249,7 +249,7 @@ async def test_update_backend_authorization(): "exception_expected, kwargs", [ # SUCESS CASES - (False, None), + (False, {}), (False, {"id": 123, "location_url": "test_200"}), # FAIL CASES # one param is missing @@ -271,30 +271,92 @@ async def test_set_backend_id_and_suffix(exception_expected, kwargs): try: backend, suffix_number = await backend_service.set_backend_id_and_suffix(BackendTemp.model_construct(), **kwargs) + backend: BackendTemp + suffix_number: int # success case if not exception_expected: assert backend.id assert suffix_number - if kwargs is None: + if kwargs == {}: mock_generate_suffix_number.assert_awaited_once() else: mock_generate_suffix_number.assert_not_awaited() - assert backend.id == kwargs["id"] + assert int(backend.id) == int(kwargs["id"]) # fail case except Exception as e: if not exception_expected: raise e +@pytest.mark.parametrize( + "delete_succeeded, proxy_pass, expected_delete_backend_ids", + [ + (True, "http://192.168.0.1:8787/guacamole/", [12, 34]), + (True, "http://192.168.0.1:8787", [56, 78]), + (True, "http://1.1.1.1:4000", []), + (False, None, []), + (False, "", []), + (False, "not-a-url", []) + ] +) +@pytest.mark.asyncio +async def test_delete_duplicate_backends(delete_succeeded, proxy_pass, expected_delete_backend_ids): + with patch( + "app.main.service.backend.get_backends_proxy_pass", + return_value = { + "http://192.168.0.1:8787/guacamole/": [ + BackendOut.model_construct( + id = 12, + upstream_url = "http://192.168.0.1:8787/guacamole/", + ), + BackendOut.model_construct( + id = 34, + upstream_url = "http://192.168.0.1:8787/guacamole/", + ), + ], + "http://192.168.0.1:8787": [ + BackendOut.model_construct( + id = 56, + upstream_url = "http://192.168.0.1:8787", + ), + BackendOut.model_construct( + id = 78, + upstream_url = "http://192.168.0.1:8787", + ), + ], + "http://1.1.1.1:4000": [ + BackendOut.model_construct( + id = 90, + upstream_url = "http://1.1.1.1:4000/", + ), + ], + } + ) as mock_get_backends_upstream_urls, patch( + "app.main.service.backend.delete_backend", + return_value = delete_succeeded + ) as mock_delete_backend: + + response_success: bool = await backend_service.delete_duplicate_backends(BackendIn.model_construct(upstream_url = proxy_pass)) + mock_get_backends_upstream_urls.assert_awaited_once() + # success case + if delete_succeeded: + assert response_success is True + mock_delete_backend.assert_has_awaits( + [id in expected_backend_ids: call(backend_id=id)] + ) + # fail case + else: + assert response_success is False + mock_delete_backend.assert_awaited() -""" -@pytest.mark.asyncio -async def test_delete_duplicate_backends(): - ... + + + +""" @pytest.mark.asyncio async def test_convert_backend_temp_to_out(): ... From 55ce4da8f865baa7f4bd3d1e19f732e96e2290d8 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Wed, 21 Jan 2026 19:31:36 +0000 Subject: [PATCH 020/118] added tests to service/backend.py --- .../app/main/service/backend.py | 4 +- .../app/main/tests/test_service_backend.py | 41 +++++++++++++------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 63f7ae70..f296b96b 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -190,7 +190,7 @@ async def create_backend(payload_input: BackendIn, **kwargs) -> BackendTemp: payload: BackendTemp = BackendTemp(**payload_input.model_dump()) # generate or reuse backend id and suffix number - payload, suffix_number = await set_backend_id_and_suffix#(payload, **kwargs) + payload, suffix_number = await set_backend_id_and_suffix(payload, **kwargs) # generate backend and location_url backend_file_contents = await generate_backend_by_template(payload, suffix_number) @@ -314,7 +314,7 @@ async def delete_duplicate_backends(backend_with_proxy_pass: BackendIn) -> bool: else: for backend in matching_backends: logger.info(f"Deleting existing backend with same proxy_pass: {proxy_pass}, backend id: {backend.id}") - if not await delete_backend(backend_id = backend.id): + if not delete_backend(backend_id = backend.id): return False return success diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 472266e0..4275e6fe 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import patch +from unittest.mock import call, patch from app.main.model.serializers import BackendIn, BackendOut, BackendTemp @@ -293,11 +293,10 @@ async def test_set_backend_id_and_suffix(exception_expected, kwargs): [ (True, "http://192.168.0.1:8787/guacamole/", [12, 34]), (True, "http://192.168.0.1:8787", [56, 78]), - (True, "http://1.1.1.1:4000", []), - (False, None, []), - (False, "", []), - (False, "not-a-url", []) + (False, "http://192.168.0.1:8787", [56, 78]), + (True, "http://1.1.1.1:4000", []) ] + # not able to test cases like None, "", "not_valid" without a validator, TODO: can we use serializer.BackendIn validator? ) @pytest.mark.asyncio async def test_delete_duplicate_backends(delete_succeeded, proxy_pass, expected_delete_backend_ids): @@ -341,26 +340,42 @@ async def test_delete_duplicate_backends(delete_succeeded, proxy_pass, expected_ # success case if delete_succeeded: assert response_success is True - mock_delete_backend.assert_has_awaits( - [id in expected_backend_ids: call(backend_id=id)] - ) + if len(expected_delete_backend_ids) != 0: + mock_delete_backend.assert_has_awaits( + [call(backend_id=id) for id in expected_delete_backend_ids], + any_order = True + ) # fail case - else: + elif not delete_succeeded: assert response_success is False mock_delete_backend.assert_awaited() +@pytest.mark.parametrize( + "exception_expected, filename, backend", + test_backends +) +@pytest.mark.asyncio +async def test_convert_backend_temp_to_out(exception_expected, backend): + with patch( + "app.main.service.backend.get_backend_by_id", + return_value = BackendOut.model_construct(file_path = "test_path") + ) as mock_get_backend_by_id: + + backend_out: BackendOut = await backend_service.convert_backend_temp_to_out(backend) + + -""" -@pytest.mark.asyncio -async def test_convert_backend_temp_to_out(): - ... + + + +""" def test_check_backend_path_file(): ... From e06af18bc60f0f0ff9b55480ccd1418bf535301d Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Wed, 21 Jan 2026 19:34:43 +0000 Subject: [PATCH 021/118] added further tests --- .../app/main/tests/test_service_backend.py | 150 +++++++++++++++++- 1 file changed, 143 insertions(+), 7 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 4275e6fe..db99d60b 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -5,15 +5,13 @@ from app.main.service import backend as backend_service -from werkzeug.exceptions import NotFound - file_path_example_1 = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" file_path_example_2 = "/var/forc/backend_path/9876543210%otheruser%cat_200%othertemplate%v02%1.conf" # TEST DATA # backend test cases: (exception_expected, filename, backend), see test_generate_backend_filename() -test_backends = [ +test_backends_for_generate_backend_filename = [ (False, "123%testuser%dog_100%testtemplate%v01%0.conf", BackendOut.model_construct( id = 123, @@ -87,6 +85,129 @@ auth_enabled = None )) ] +# backend test cases: (exception_expected, backend), see test_convert_backend_temp_to_out() +test_backends_for_convert_backend_temp_to_out = [ + (False, + BackendTemp.model_construct( + id = 123, + owner = "testuser", + location_url = "dog_100", + template = "testtemplate", + template_version = "v01", + user_key_url = "myRstudio", + upstream_url = "http://192.168.0.1:4000", + auth_enabled = False, + file_path = "file_path" + )), + (False, + BackendOut.model_construct( + id = 456, + owner = "otheruser", + location_url = "ant_300", + template = "anothertemplate", + template_version = "v03", + user_key_url = "yourRstudio", + upstream_url = "http://1.1.1.1:8002", + auth_enabled = True, + file_path = "another_file_path" + )), + (True, + BackendTemp.model_construct( + id = None, + owner = "testuser", + location_url = "dog_100", + template = "testtemplate", + template_version = "v01", + user_key_url = "myRstudio", + upstream_url = "http://192.168.0.1:4000", + auth_enabled = False, + file_path = "file_path" + )), + (True, + BackendTemp.model_construct( + id = 123, + owner = None, + location_url = "dog_100", + template = "testtemplate", + template_version = "v01", + user_key_url = "myRstudio", + upstream_url = "http://192.168.0.1:4000", + auth_enabled = False, + file_path = "file_path" + )), + (True, + BackendTemp.model_construct( + id = 123, + owner = "testuser", + location_url = None, + template = "testtemplate", + template_version = "v01", + user_key_url = "myRstudio", + upstream_url = "http://192.168.0.1:4000", + auth_enabled = False, + file_path = "file_path" + )), + (True, + BackendTemp.model_construct( + id = 123, + owner = "testuser", + location_url = "dog_100", + template = None, + template_version = "v01", + user_key_url = "myRstudio", + upstream_url = "http://192.168.0.1:4000", + auth_enabled = False, + file_path = "file_path" + )), + (True, + BackendTemp.model_construct( + id = 123, + owner = "testuser", + location_url = "dog_100", + template = "testtemplate", + template_version = None, + user_key_url = "myRstudio", + upstream_url = "http://192.168.0.1:4000", + auth_enabled = False, + file_path = "file_path" + )), + (True, + BackendTemp.model_construct( + id = 123, + owner = "testuser", + location_url = "dog_100", + template = "testtemplate", + template_version = "v01", + user_key_url = None, + upstream_url = "http://192.168.0.1:4000", + auth_enabled = False, + file_path = "file_path" + )), + (True, + BackendTemp.model_construct( + id = 123, + owner = "testuser", + location_url = "dog_100", + template = "testtemplate", + template_version = "v01", + user_key_url = "myRstudio", + upstream_url = None, + auth_enabled = False, + file_path = "file_path" + )), + (True, + BackendTemp.model_construct( + id = 123, + owner = "testuser", + location_url = "dog_100", + template = "testtemplate", + template_version = "v01", + user_key_url = "myRstudio", + upstream_url = "http://192.168.0.1:4000", + auth_enabled = None, + file_path = None + )), + ] # HELPER FUNCTIONS @@ -352,17 +473,32 @@ async def test_delete_duplicate_backends(delete_succeeded, proxy_pass, expected_ @pytest.mark.parametrize( - "exception_expected, filename, backend", - test_backends + "exception_expected, backend", + test_backends_for_convert_backend_temp_to_out ) @pytest.mark.asyncio async def test_convert_backend_temp_to_out(exception_expected, backend): with patch( "app.main.service.backend.get_backend_by_id", - return_value = BackendOut.model_construct(file_path = "test_path") + return_value = BackendOut.model_construct(file_path = backend.file_path) ) as mock_get_backend_by_id: + try: + backend_out = await backend_service.convert_backend_temp_to_out(backend) + if not exception_expected: + assert isinstance(backend_out, BackendOut) + mock_get_backend_by_id.assert_once_awaited_with(backend_id = backend.id) + assert backend_out.id == backend.id + assert backend_out.owner == backend.owner + assert backend_out.location_url == backend.location_url + assert backend_out.template == backend.template + assert backend_out.template_version == backend.template_version + assert backend_out.auth_enabled == backend.auth_enabled + assert backend_out.file_path == backend.file_path + except Exception as e: + if not exception_expected: + raise e + - backend_out: BackendOut = await backend_service.convert_backend_temp_to_out(backend) From 8708a230a852e060aa04236ac9e017a8e462a330 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Mon, 26 Jan 2026 13:16:38 +0000 Subject: [PATCH 022/118] finished test_convert_backend_temp_to_out, added conftest.py and test_check_backend_path_file --- .../app/main/model/serializers.py | 2 +- .../app/main/service/backend.py | 35 ++++++---- .../app/main/tests/conftest.py | 16 +++++ .../app/main/tests/test_service_backend.py | 66 +++++++++++++------ 4 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 FastapiOpenRestyConfigurator/app/main/tests/conftest.py diff --git a/FastapiOpenRestyConfigurator/app/main/model/serializers.py b/FastapiOpenRestyConfigurator/app/main/model/serializers.py index d3e7d37e..b359e29c 100755 --- a/FastapiOpenRestyConfigurator/app/main/model/serializers.py +++ b/FastapiOpenRestyConfigurator/app/main/model/serializers.py @@ -144,7 +144,7 @@ class BackendTemp(BackendIn, BackendOut): """ Backend class to temporarily save information. Links BackendIn with BackendOut. """ - id: int = None + id: int = None # TODO: also needs refactoring: change type to int and rename to backend_id owner: str = None location_url: str = None template: str = None diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index f296b96b..1dcbd406 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -299,7 +299,7 @@ async def set_backend_id_and_suffix(backend: BackendTemp, **kwargs) -> tuple[Bac # TODO: handle cases where same proxy_pass exists with and without trailing path (guacamole) async def delete_duplicate_backends(backend_with_proxy_pass: BackendIn) -> bool: """ - Checks for duplicates in proxy_pass (namely upstream_url) for a given backend and deletes all of them. + Checks for duplicates in proxy_pass (namely upstream_url) for a given BackendIn and deletes all of them. Returns only False, if a deletion failed. Returns True if no backends were found and thus deleted. """ # get proxy_pass from provided backend @@ -314,12 +314,15 @@ async def delete_duplicate_backends(backend_with_proxy_pass: BackendIn) -> bool: else: for backend in matching_backends: logger.info(f"Deleting existing backend with same proxy_pass: {proxy_pass}, backend id: {backend.id}") - if not delete_backend(backend_id = backend.id): + if not await delete_backend(backend_id = backend.id): return False return success async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | None: + """ + Converts a given BackendTemp to BackendOut. Returns None otherwise. + """ # needed for returning backends to client try: backend_out = BackendOut( @@ -338,9 +341,11 @@ async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | def check_backend_path_file() -> bool: + """ + Checks whether a backend path exists, is accessible and has files + """ settings = get_settings() - # check if backend path exists, is accessible and has files - if not os.path.exists(settings.FORC_BACKEND_PATH) and not os.access(settings.FORC_BACKEND_PATH, os.W_OK): + if not os.path.exists(settings.FORC_BACKEND_PATH) or not os.access(settings.FORC_BACKEND_PATH, os.W_OK): logger.error("Not able to access configured backend path.") return False if len(os.listdir(settings.FORC_BACKEND_PATH)) == 0: @@ -350,17 +355,19 @@ def check_backend_path_file() -> bool: def check_backend_path_file_naming(backend_path_filename: str) -> bool | None: - # check for correct naming - match = re.fullmatch(filename_regex, backend_path_filename) - # skip files with wrong naming and log warning - if not match: - # exclude expected files from warning - if backend_path_filename == "users" or backend_path_filename == "scripts": - return None - logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") + """ + Checks for correct naming of the file in the backend path + """ + match = re.fullmatch(filename_regex, backend_path_filename) + # skip files with wrong naming and log warning + if not match: + # exclude expected files from warning + if backend_path_filename == "users" or backend_path_filename == "scripts": return None - # return backend id of the correctly named file - return True + logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") + return None + # return backend id of the correctly named file + return True def get_backend_path_filenames() -> List[str] | None: diff --git a/FastapiOpenRestyConfigurator/app/main/tests/conftest.py b/FastapiOpenRestyConfigurator/app/main/tests/conftest.py new file mode 100644 index 00000000..b0c5e1d7 --- /dev/null +++ b/FastapiOpenRestyConfigurator/app/main/tests/conftest.py @@ -0,0 +1,16 @@ +import pytest +import os + +@pytest.fixture(scope="session", autouse=True) +def test_dirs(tmp_path_factory): + """ + See config.get_settings(). Creates the path structure and sets environment variables. + """ + backend = tmp_path_factory.mktemp("backend") + templates = tmp_path_factory.mktemp("templates") + + os.environ["FORC_BACKEND_PATH"] = str(backend) + os.environ["FORC_TEMPLATE_PATH"] = str(templates) + os.environ["FORC_API_KEY"] = "test-api-key" + + yield diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index db99d60b..c858ef67 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -1,5 +1,8 @@ +import os import pytest from unittest.mock import call, patch +from app.main.config import get_settings + from app.main.model.serializers import BackendIn, BackendOut, BackendTemp @@ -90,7 +93,7 @@ (False, BackendTemp.model_construct( id = 123, - owner = "testuser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = "dog_100", template = "testtemplate", template_version = "v01", @@ -100,13 +103,13 @@ file_path = "file_path" )), (False, - BackendOut.model_construct( + BackendTemp.model_construct( id = 456, - owner = "otheruser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = "ant_300", template = "anothertemplate", template_version = "v03", - user_key_url = "yourRstudio", + user_key_url = "myRstudio", upstream_url = "http://1.1.1.1:8002", auth_enabled = True, file_path = "another_file_path" @@ -114,7 +117,7 @@ (True, BackendTemp.model_construct( id = None, - owner = "testuser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = "dog_100", template = "testtemplate", template_version = "v01", @@ -127,7 +130,7 @@ BackendTemp.model_construct( id = 123, owner = None, - location_url = "dog_100", + location_url = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", template = "testtemplate", template_version = "v01", user_key_url = "myRstudio", @@ -138,7 +141,7 @@ (True, BackendTemp.model_construct( id = 123, - owner = "testuser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = None, template = "testtemplate", template_version = "v01", @@ -150,7 +153,7 @@ (True, BackendTemp.model_construct( id = 123, - owner = "testuser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = "dog_100", template = None, template_version = "v01", @@ -162,7 +165,7 @@ (True, BackendTemp.model_construct( id = 123, - owner = "testuser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = "dog_100", template = "testtemplate", template_version = None, @@ -174,7 +177,7 @@ (True, BackendTemp.model_construct( id = 123, - owner = "testuser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = "dog_100", template = "testtemplate", template_version = "v01", @@ -186,7 +189,7 @@ (True, BackendTemp.model_construct( id = 123, - owner = "testuser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = "dog_100", template = "testtemplate", template_version = "v01", @@ -198,7 +201,7 @@ (True, BackendTemp.model_construct( id = 123, - owner = "testuser", + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb", location_url = "dog_100", template = "testtemplate", template_version = "v01", @@ -247,7 +250,7 @@ async def test_generate_suffix_number(exception_expected, expected_suffix, user_ @pytest.mark.parametrize( "exception_expected, filename, backend", - test_backends + test_backends_for_generate_backend_filename ) @pytest.mark.asyncio async def test_generate_backend_filename(exception_expected, filename, backend): @@ -482,11 +485,11 @@ async def test_convert_backend_temp_to_out(exception_expected, backend): "app.main.service.backend.get_backend_by_id", return_value = BackendOut.model_construct(file_path = backend.file_path) ) as mock_get_backend_by_id: - try: + try: # @reviewer: is there an easier way? backend_out = await backend_service.convert_backend_temp_to_out(backend) if not exception_expected: assert isinstance(backend_out, BackendOut) - mock_get_backend_by_id.assert_once_awaited_with(backend_id = backend.id) + mock_get_backend_by_id.assert_awaited_once_with(backend.id) assert backend_out.id == backend.id assert backend_out.owner == backend.owner assert backend_out.location_url == backend.location_url @@ -499,9 +502,24 @@ async def test_convert_backend_temp_to_out(exception_expected, backend): raise e +def test_check_backend_path_file(): + # fail case - backend file missing + assert backend_service.check_backend_path_file() == False + # success case, create backend file for that + settings = get_settings() + backend_file_path = str(settings.FORC_BACKEND_PATH) + "/test_backend" + backend_file = os.open(backend_file_path, os.O_CREAT) + os.close(backend_file) + assert backend_service.check_backend_path_file() == True + # fail case - no (write) access to file, remove access + os.chmod(backend_file_path, 0o555) + assert backend_service.check_backend_path_file() == False + # fail case - environment variable missing + os.environ.pop("FORC_BACKEND_PATH", None) + assert backend_service.check_backend_path_file() == False @@ -512,11 +530,21 @@ async def test_convert_backend_temp_to_out(exception_expected, backend): """ -def test_check_backend_path_file(): - ... - +@pytest.mark.parametrize( + "exception_expected, filename" +) def test_check_backend_path_file_naming(): - ... + + + + + + + + + + + def test_get_backend_path_filenames(): ... From 0bbf613b499400db9d4bbedaa9e88e392bfca43c Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Mon, 26 Jan 2026 13:38:48 +0000 Subject: [PATCH 023/118] finished test_check_backend_path_file() --- .../app/main/tests/test_service_backend.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index c858ef67..9652c7ae 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -508,19 +508,22 @@ def test_check_backend_path_file(): # success case, create backend file for that settings = get_settings() - backend_file_path = str(settings.FORC_BACKEND_PATH) + "/test_backend" + forc_backend_path = str(settings.FORC_BACKEND_PATH) + backend_file_path = forc_backend_path + "/test_backend" backend_file = os.open(backend_file_path, os.O_CREAT) os.close(backend_file) assert backend_service.check_backend_path_file() == True - # fail case - no (write) access to file, remove access - os.chmod(backend_file_path, 0o555) - assert backend_service.check_backend_path_file() == False - - # fail case - environment variable missing + # fail case - environment variable missing, remove and set again for next test os.environ.pop("FORC_BACKEND_PATH", None) + get_settings.cache_clear() assert backend_service.check_backend_path_file() == False + os.environ["FORC_BACKEND_PATH"] = forc_backend_path + get_settings.cache_clear() + # fail case - no (write) access to file, remove access + os.chmod(settings.FORC_BACKEND_PATH, 0o555) + assert backend_service.check_backend_path_file() == False From 9d0c857aaf0a875d8d6079c71cb9fa6e14e1eace Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Mon, 26 Jan 2026 14:14:58 +0000 Subject: [PATCH 024/118] finished test_check_backend_path_file_naming and changed to original function --- .../app/main/service/backend.py | 14 ++++----- .../app/main/tests/test_service_backend.py | 30 ++++++++++++++----- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 1dcbd406..9e4d74ee 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -354,20 +354,20 @@ def check_backend_path_file() -> bool: return True -def check_backend_path_file_naming(backend_path_filename: str) -> bool | None: +def check_backend_path_file_naming(backend_path_filename: str) -> bool: """ Checks for correct naming of the file in the backend path """ match = re.fullmatch(filename_regex, backend_path_filename) # skip files with wrong naming and log warning - if not match: + if match: + return True + else: # exclude expected files from warning if backend_path_filename == "users" or backend_path_filename == "scripts": - return None - logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") - return None - # return backend id of the correctly named file - return True + logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") + return True + return False def get_backend_path_filenames() -> List[str] | None: diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 9652c7ae..7d68009d 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -8,8 +8,8 @@ from app.main.service import backend as backend_service -file_path_example_1 = "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" -file_path_example_2 = "/var/forc/backend_path/9876543210%otheruser%cat_200%othertemplate%v02%1.conf" +file_path_example_1 = "1234567890%testuser%animal_100%testtemplate%v01%0.conf" +file_path_example_2 = "9876543210%otheruser%cat_200%othertemplate%v02%1.conf" # TEST DATA @@ -526,17 +526,30 @@ def test_check_backend_path_file(): assert backend_service.check_backend_path_file() == False +@pytest.mark.parametrize( + "expected, filename", + [ + (True, "users"), + (True, "scripts"), + (True, file_path_example_1), + (True, file_path_example_2), + (False, "obviously_wrong"), + (False, 37) + ] +) +def test_check_backend_path_file_naming(expected, filename): + try: + assert backend_service.check_backend_path_file_naming(filename) is expected + except TypeError as e: + assert not expected + if expected: + raise e -""" -@pytest.mark.parametrize( - "exception_expected, filename" -) -def test_check_backend_path_file_naming(): @@ -549,6 +562,9 @@ def test_check_backend_path_file_naming(): + + +""" def test_get_backend_path_filenames(): ... From a91b76b885c1a8b1a0a5fbab98f535d948b0fea8 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Mon, 26 Jan 2026 14:42:25 +0000 Subject: [PATCH 025/118] added helper_create_backend_file, might need to refactor to mock os functions --- .../app/main/tests/test_service_backend.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 7d68009d..9757c6bd 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -502,16 +502,21 @@ async def test_convert_backend_temp_to_out(exception_expected, backend): raise e +def helper_create_backend_file(filename: str) -> str: + settings = get_settings() + forc_backend_path = str(settings.FORC_BACKEND_PATH) + backend_file_path = forc_backend_path + "/" + filename + backend_file = os.open(backend_file_path, os.O_CREAT) + os.close(backend_file) + return forc_backend_path + +# TODO: mock all os functions def test_check_backend_path_file(): # fail case - backend file missing assert backend_service.check_backend_path_file() == False # success case, create backend file for that - settings = get_settings() - forc_backend_path = str(settings.FORC_BACKEND_PATH) - backend_file_path = forc_backend_path + "/test_backend" - backend_file = os.open(backend_file_path, os.O_CREAT) - os.close(backend_file) + forc_backend_path = helper_create_backend_file("test_backend") assert backend_service.check_backend_path_file() == True # fail case - environment variable missing, remove and set again for next test @@ -522,10 +527,14 @@ def test_check_backend_path_file(): get_settings.cache_clear() # fail case - no (write) access to file, remove access - os.chmod(settings.FORC_BACKEND_PATH, 0o555) +# with patch + os.chmod(get_settings().FORC_BACKEND_PATH, 0o555) + if backend_service.check_backend_path_file() == True: + os.chmod(get_settings().FORC_BACKEND_PATH, 0o755) assert backend_service.check_backend_path_file() == False + @pytest.mark.parametrize( "expected, filename", [ @@ -546,6 +555,15 @@ def test_check_backend_path_file_naming(expected, filename): raise e +def test_get_backend_path_filenames(): + # fail case - no backend files + assert backend_service.get_backend_path_filenames() is None + # success case, create two backend files + helper_create_backend_file("test_backend_1") + helper_create_backend_file("test_backend_2") + backend_path_filenames = backend_service.get_backend_path_filenames() + assert backend_path_filenames + assert len(backend_path_filenames) == 2 @@ -565,8 +583,7 @@ def test_check_backend_path_file_naming(expected, filename): """ -def test_get_backend_path_filenames(): - ... + def test_get_valid_backend_filenames(): ... From bac4dc1accb9965935677c396f04bdeed8686ad2 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Thu, 29 Jan 2026 18:52:32 +0000 Subject: [PATCH 026/118] refactored tests to mock os functions, added test_get_backend_path_filenames, minor changes in service/backend.py --- .../app/main/service/backend.py | 24 +++-- .../app/main/tests/test_service_backend.py | 102 ++++++++++-------- 2 files changed, 71 insertions(+), 55 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 9e4d74ee..70ef5f47 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -340,21 +340,21 @@ async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | return None -def check_backend_path_file() -> bool: +def check_backend_path() -> bool: """ Checks whether a backend path exists, is accessible and has files """ - settings = get_settings() - if not os.path.exists(settings.FORC_BACKEND_PATH) or not os.access(settings.FORC_BACKEND_PATH, os.W_OK): + forc_backend_path = get_settings().FORC_BACKEND_PATH + if not os.path.exists(forc_backend_path) or not os.access(forc_backend_path, os.W_OK): logger.error("Not able to access configured backend path.") return False - if len(os.listdir(settings.FORC_BACKEND_PATH)) == 0: + if len(os.listdir(forc_backend_path)) == 0: logger.error("No files found in backend path.") return False return True -def check_backend_path_file_naming(backend_path_filename: str) -> bool: +def check_backend_file_naming(backend_path_filename: str) -> bool: """ Checks for correct naming of the file in the backend path """ @@ -371,15 +371,19 @@ def check_backend_path_file_naming(backend_path_filename: str) -> bool: def get_backend_path_filenames() -> List[str] | None: - settings = get_settings() - # get list of filenames in backend path - if not check_backend_path_file(): + """ + Returns a list of all the filenames in the backend path, or None if failed. + """ + if not check_backend_path(): return None - return os.listdir(settings.FORC_BACKEND_PATH) + return os.listdir(get_settings().FORC_BACKEND_PATH) def get_valid_backend_filenames() -> List[str] | None: + """ + Returns a list of all valid filenames in the backend path, or None if failed. + """ # get list of valid backend filenames in backend path backend_path_filenames = get_backend_path_filenames() @@ -389,7 +393,7 @@ def get_valid_backend_filenames() -> List[str] | None: # check naming, skip invalid filenames valid_backend_filenames = [] for filename in backend_path_filenames: - if not check_backend_path_file_naming(filename): + if not check_backend_file_naming(filename): continue # add valid filenames to list and return them diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 9757c6bd..09740614 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -1,7 +1,5 @@ -import os import pytest from unittest.mock import call, patch -from app.main.config import get_settings from app.main.model.serializers import BackendIn, BackendOut, BackendTemp @@ -502,37 +500,40 @@ async def test_convert_backend_temp_to_out(exception_expected, backend): raise e -def helper_create_backend_file(filename: str) -> str: - settings = get_settings() - forc_backend_path = str(settings.FORC_BACKEND_PATH) - backend_file_path = forc_backend_path + "/" + filename - backend_file = os.open(backend_file_path, os.O_CREAT) - os.close(backend_file) - return forc_backend_path - -# TODO: mock all os functions -def test_check_backend_path_file(): - # fail case - backend file missing - assert backend_service.check_backend_path_file() == False - - # success case, create backend file for that - forc_backend_path = helper_create_backend_file("test_backend") - assert backend_service.check_backend_path_file() == True - - # fail case - environment variable missing, remove and set again for next test - os.environ.pop("FORC_BACKEND_PATH", None) - get_settings.cache_clear() - assert backend_service.check_backend_path_file() == False - os.environ["FORC_BACKEND_PATH"] = forc_backend_path - get_settings.cache_clear() - - # fail case - no (write) access to file, remove access -# with patch - os.chmod(get_settings().FORC_BACKEND_PATH, 0o555) - if backend_service.check_backend_path_file() == True: - os.chmod(get_settings().FORC_BACKEND_PATH, 0o755) - assert backend_service.check_backend_path_file() == False +@pytest.mark.parametrize( + "expected, path_exists, access, listdir", + [ + (True, True, True, ["first"]), + (True, True, True, ["first", "second"]), + (True, True, True, ["first", "second", "third"]), + (False, False, False, ["first"]), + (False, False, True, ["first"]), + (False, True, False, ["first"]), + (False, True, True, []), + + ] +) +def test_check_backend_path(expected, path_exists, access, listdir): + with patch( + "app.main.service.backend.get_settings" # imported + ) as mock_get_settings, patch( + "os.path.exists", + return_value = path_exists + ) as mock_os_path_exists, patch( + "os.access", + return_value = access + ) as mock_os_access, patch( + "os.listdir", + return_value = listdir + ) as mock_os_listdir: + assert backend_service.check_backend_path() == expected + mock_get_settings.assert_called_once() + mock_os_path_exists.assert_called_once() + if path_exists: + mock_os_access.assert_called_once() + if access: + mock_os_listdir.assert_called_once() @pytest.mark.parametrize( @@ -546,26 +547,37 @@ def test_check_backend_path_file(): (False, 37) ] ) -def test_check_backend_path_file_naming(expected, filename): +def test_check_backend_file_naming(expected, filename): try: - assert backend_service.check_backend_path_file_naming(filename) is expected + assert backend_service.check_backend_file_naming(filename) is expected except TypeError as e: assert not expected if expected: raise e +@pytest.mark.parametrize( + "returning, backend_check", + [ + (["something"], True), + (None, False) + ] +) +def test_get_backend_path_filenames(returning, backend_check): -def test_get_backend_path_filenames(): - # fail case - no backend files - assert backend_service.get_backend_path_filenames() is None - # success case, create two backend files - helper_create_backend_file("test_backend_1") - helper_create_backend_file("test_backend_2") - backend_path_filenames = backend_service.get_backend_path_filenames() - assert backend_path_filenames - assert len(backend_path_filenames) == 2 - - + with patch( + "app.main.service.backend.get_settings" # imported + ) as mock_get_settings, patch( + "app.main.service.backend.check_backend_path", + return_value = backend_check + ) as mock_check_backend_path, patch( + "os.listdir", + return_value = returning + ) as mock_os_listdir: + assert backend_service.get_backend_path_filenames() == returning + mock_check_backend_path.assert_called_once() + if backend_check: + mock_os_listdir.assert_called_once() + mock_get_settings.assert_called_once() From f274bc954253ed83e02637ed42fdc079782546c6 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Thu, 5 Feb 2026 16:44:51 +0000 Subject: [PATCH 027/118] fixed issue with create_instance --- .../app/main/service/backend.py | 16 +++++++--------- .../app/main/tests/test_service_backend.py | 4 +++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 70ef5f47..4f03f5d2 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -33,21 +33,19 @@ def random_with_n_digits(n): # TODO: unlikely but potential error cause, if two users have same randomly generated user_key_url! -async def generate_suffix_number(user_key_url: str | None = None) -> int: - if user_key_url is None: +async def generate_suffix_number(location_url: str | None = None) -> int: + if location_url is None: return 100 - # extract current suffix number, check validity - current_suffix_number: int = int(user_key_url.split("_")[1]) + current_suffix_number: int = int(location_url.split("_")[1]) if current_suffix_number < 100 or current_suffix_number > 999: - logger.error("Invalid user_key_url provided for suffix generation: " + user_key_url) + logger.error("Invalid user_key_url provided for suffix generation: " + location_url) raise InternalServerError("Invalid user_key_url provided for suffix generation.") - # look for backends with same user_key_url backends: List[BackendOut] = await get_backends() same_name_backend_suffixes: List[int] = [] for backend in backends: - if backend.location_url == user_key_url: + if backend.location_url == location_url: suffix: int = int(backend.location_url.split("_")[1]) same_name_backend_suffixes.append(suffix) if not same_name_backend_suffixes: @@ -57,7 +55,7 @@ async def generate_suffix_number(user_key_url: str | None = None) -> int: same_name_backend_suffixes.sort() highest_id: int = same_name_backend_suffixes[-1] if highest_id == 999: - logger.warning("Reached max index number for requested user_key_url: " + user_key_url) + logger.warning("Reached max index number for requested user_key_url: " + location_url) raise InternalServerError("Reached max index number for requested user_key_url (limit=999).") return highest_id + 1 @@ -292,7 +290,7 @@ async def set_backend_id_and_suffix(backend: BackendTemp, **kwargs) -> tuple[Bac logger.warning(f"set_backend_id_and_suffix() received unexpected kwargs: {kwargs}") raise InternalServerError("Unexpected kwargs provided to set_backend_id_and_suffix().") # @reviewer: should we really error here? backend = backend.model_copy(update={'id': str(random_with_n_digits(10))}) - suffix_number = await generate_suffix_number(backend.user_key_url) + suffix_number = await generate_suffix_number(backend.location_url) return backend, suffix_number diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 09740614..02f0958b 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -266,7 +266,9 @@ async def test_generate_backend_filename(exception_expected, filename, backend): # CORE GETTER FUNCTIONS -# TODO: async def test_get_backends(): +""" +async def test_get_backends(): +""" @pytest.mark.parametrize( From 8d79b6b4ac6efd61eb147392dd8f0a73d79936b9 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Fri, 6 Feb 2026 08:52:12 +0000 Subject: [PATCH 028/118] cleaned up some log calls --- FastapiOpenRestyConfigurator/app/main/service/backend.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 4f03f5d2..7f747ed0 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -98,7 +98,6 @@ async def get_backends() -> List[BackendOut]: auth_enabled = bool(int(match.group(6))), file_path = os.path.join(settings.FORC_BACKEND_PATH, filename) ) - logger.debug(f"Discovered backend: {backend}") valid_backends.append(backend) return valid_backends @@ -252,7 +251,6 @@ async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> B # generate new backend contents with temp_payload, additional kwargs to persist backend_id and location_url new_contents = await create_backend(temp_payload, id=str(backend_id), location_url=backend.location_url) - logger.debug(f"New contents: {new_contents}") if not new_contents: logger.error("Templating returned empty result.") return None @@ -363,8 +361,8 @@ def check_backend_file_naming(backend_path_filename: str) -> bool: else: # exclude expected files from warning if backend_path_filename == "users" or backend_path_filename == "scripts": - logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") return True + logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") return False From fbfff3a59ccc707de68315371b27f30455b374ce Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Sun, 1 Mar 2026 04:57:24 +0000 Subject: [PATCH 029/118] Enhance backend service logging and error handling; refactor create_backend and delete_backend functions for improved clarity and robustness. --- .../app/main/service/backend.py | 40 +++-- .../app/main/tests/test_service_backend.py | 167 +++++++++++++++--- .../app/main/views/backend.py | 5 + 3 files changed, 174 insertions(+), 38 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 7f747ed0..e1506ee5 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -181,6 +181,7 @@ def get_basekey_from_backend(backend: BackendOut) -> str | None: # CORE MUTATOR AND SERVICE FUNCTIONS async def create_backend(payload_input: BackendIn, **kwargs) -> BackendTemp: + logger.info(f"Attempting to create backend with user_key_url: {payload_input.user_key_url}") settings = get_settings() # overwrite payload as BackendTemp for generate_backend_by_template() @@ -205,6 +206,7 @@ async def create_backend(payload_input: BackendIn, **kwargs) -> BackendTemp: with open(f"{settings.FORC_BACKEND_PATH}/{filename}", 'w') as backend_file: backend_file.write(backend_file_contents) + backend_file.close() # attempt to reload openresty await reload_openresty() @@ -215,16 +217,21 @@ async def delete_backend(backend_id) -> bool: settings = get_settings() backend_path_filenames = get_valid_backend_filenames() if not backend_path_filenames: - return False + logger.error(f"Backend {backend_id} was not found.") + raise NotFound(f"Backend {backend_id} was not found.") matching_backend_filenames = filter_backend_filenames_by_id(backend_path_filenames, backend_id) amount_of_files = len(matching_backend_filenames) if amount_of_files == 0: + logger.error(f"Backend {backend_id} was not found") raise NotFound(f"Backend {backend_id} was not found.") if amount_of_files > 1: logger.error(f"Found multiple backend files for backend id: {backend_id}, cannot delete.") - raise InternalServerError("Server found multiple backend files, cannot delete.") + raise InternalServerError("Found multiple backend files for backend id: {backend_id}, cannot delete.") + if not amount_of_files == 1: + logger.error(f"Something went wrong. Did not expect multiple files for deletion. Backend id: {backend_id}") + raise InternalServerError(f"Something went wrong. Did not expect multiple files for deletion. Backend id: {backend_id}") filename = matching_backend_filenames[0] @@ -303,16 +310,18 @@ async def delete_duplicate_backends(backend_with_proxy_pass: BackendIn) -> bool: # get all backends linked to the given proxy_pass backends_proxy_passes: Dict[str, List[BackendOut]] = await get_backends_proxy_pass() matching_backends: List[BackendOut] = backends_proxy_passes.get(proxy_pass, []) + logger.info(f"Matching backends: {matching_backends}") # delete all matching backends - success: bool = True if len(matching_backends) == 0: - logger.warning("No backends found for matching, proxy_pass: " + str(proxy_pass)) + logger.warning("No matching backends found with proxy_pass: " + str(proxy_pass)) else: for backend in matching_backends: - logger.info(f"Deleting existing backend with same proxy_pass: {proxy_pass}, backend id: {backend.id}") - if not await delete_backend(backend_id = backend.id): + if await delete_backend(backend_id = backend.id): + logger.info(f"Deleted existing backend with same proxy_pass: {proxy_pass}, backend id: {backend.id}") + else: + logger.error(f"Failed to delete existing backend with same proxy_pass: {proxy_pass}, backend id: {backend.id}") return False - return success + return True async def convert_backend_temp_to_out(backend_temp: BackendTemp) -> BackendOut | None: @@ -360,9 +369,8 @@ def check_backend_file_naming(backend_path_filename: str) -> bool: return True else: # exclude expected files from warning - if backend_path_filename == "users" or backend_path_filename == "scripts": - return True - logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") + if not backend_path_filename == "users" and not backend_path_filename == "scripts": + logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") return False @@ -386,24 +394,28 @@ def get_valid_backend_filenames() -> List[str] | None: if not backend_path_filenames: return None - # check naming, skip invalid filenames valid_backend_filenames = [] for filename in backend_path_filenames: + # check naming, skip invalid filenames if not check_backend_file_naming(filename): continue - # add valid filenames to list and return them + # add valid filenames to list and return them valid_backend_filenames.append(filename) return valid_backend_filenames def filter_backend_filenames_by_id(backend_path_filenames: List[str], backend_id: int) -> List[str]: - # filter a list of backend filenames for matching backend id + """ + Filters and returns a list of backend filenames for matching backend ids. Provided filenames must be checked for correct naming separately before. + """ return [filename for filename in backend_path_filenames if int(filename.split("%")[0]) == int(backend_id)] def build_payload_for_auth_update(backend: BackendOut, auth_enabled: bool) -> BackendIn | None: - # fetch necessary info from existing BackendOut and build BackendIn payload for create_backend(), see update_backend_authorization() + """ + Build BackendIn payload by fetching required information from existing BackendOut. For backend_service.create_backend(), see update_backend_authorization(). + """ upstream_url = get_upstream_url(backend.file_path) base_key = get_basekey_from_backend(backend) if not upstream_url or not base_key: diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 02f0958b..d1f7e6d2 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -349,13 +349,74 @@ def test_get_basekey_from_backend(exception_expected, backend, expected_basekey) assert response_basekey is None -""" + # CORE MUTATOR AND SERVICE FUNCTIONS +@pytest.mark.parametrize( + "backend_file_contents, delete_duplicate_backends", + [ + ("something", True), + ("something", False), + (None, True), + (None, False) + ] +) @pytest.mark.asyncio -async def test_create_backend(): - ... +async def test_create_backend(backend_file_contents, delete_duplicate_backends): + with patch( + "app.main.config.get_settings", + ) as mock_get_settings, patch( + "app.main.service.backend.set_backend_id_and_suffix", + return_value = (BackendTemp.model_construct(user_key_url="olddragon"), 100) + ) as mock_set_backend_id_and_suffix, patch( + "app.main.service.backend.generate_backend_by_template", + return_value = backend_file_contents + ) as mock_generate_backend_by_template, patch( + "app.main.service.backend.delete_duplicate_backends", + return_value = delete_duplicate_backends + ) as mock_delete_duplicate_backends, patch( + "app.main.service.backend.generate_backend_filename" + ) as mock_generate_backend_filename, patch( + "os.open" + ) as mock_os_open, patch( + "os.write" + ) as mock_os_write, patch( + "os.close" + ) as mock_os_close, patch( + "app.main.service.backend.reload_openresty" + ) as mock_reload_openresty: + backend_in = BackendIn( + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb7", + template = "theiaide", + template_version = "v03", + auth_enabled = True, + user_key_url = "olddragon", + upstream_url = "http://192.168.0.1:8787/guacamole/" + ) + try: + result = await backend_service.create_backend(backend_in) + # mock_get_settings.assert_called_once() + mock_set_backend_id_and_suffix.assert_awaited_once() + mock_generate_backend_by_template.assert_awaited_once() + mock_delete_duplicate_backends.assert_awaited_once() + mock_generate_backend_filename.assert_called_once() + # mock_os_open.assert_called_once() + # mock_os_write.assert_called_once() + # mock_os_close.assert_called_once() @ reviewer: these 4 mocks are not called + mock_reload_openresty.assert_awaited_once() + assert result.location_url == "olddragon_100" + except Exception as e: + assert not backend_file_contents or not delete_duplicate_backends + + + + + + + + +""" @pytest.mark.asyncio async def test_delete_backend(): ... @@ -541,10 +602,10 @@ def test_check_backend_path(expected, path_exists, access, listdir): @pytest.mark.parametrize( "expected, filename", [ - (True, "users"), - (True, "scripts"), (True, file_path_example_1), (True, file_path_example_2), + (False, "users"), + (False, "scripts"), (False, "obviously_wrong"), (False, 37) ] @@ -582,29 +643,87 @@ def test_get_backend_path_filenames(returning, backend_check): mock_get_settings.assert_called_once() +@pytest.mark.parametrize( + "filenames, naming_check", + [ + ([file_path_example_1, file_path_example_2], True), + (["invalid_file_1", "invalid_file_2"], False), + (["users", "scripts"], False), + ([], False) + ] +) +def test_get_valid_backend_filenames(filenames, naming_check): + with patch( + "app.main.service.backend.get_backend_path_filenames", + return_value = filenames + ) as mock_get_backend_path_filenames, patch( + "app.main.service.backend.check_backend_file_naming", + return_value = naming_check + ) as mock_check_backend_file_naming: + expected = None + if filenames: + expected = filenames if naming_check else [] + + assert backend_service.get_valid_backend_filenames() == expected + mock_get_backend_path_filenames.assert_called_once() + if filenames: + mock_check_backend_file_naming.assert_called() +@pytest.mark.parametrize( + "backend_id, filenames, expected", + [ + (1234567890, [file_path_example_1, file_path_example_2], [file_path_example_1]), + (9876543210, [file_path_example_1, file_path_example_2], [file_path_example_2]), + (None, [], []) + ] +) +def test_filter_backend_filenames_by_id(backend_id, filenames, expected): + assert backend_service.filter_backend_filenames_by_id(filenames, backend_id) == expected - - - - - - - -""" - - -def test_get_valid_backend_filenames(): - ... - -def test_filter_backend_filenames_by_id(): - ... - -def test_build_payload_for_auth_update(): - ... -""" \ No newline at end of file +@pytest.mark.parametrize( + "upstream_url, base_key, auth_enabled, exception_expected", + [ + ("http://192.168.0.1:8787/guacamole/", "olddragon", True, False), + ("http://192.168.0.1:4000/guacamole/", "youngmonkey", False, False), + (None, "crazydog", True, False), + ("http://192.168.0.1:1000/", None, False, False), + ("http://192.168.0.1:1234/", "test_100", None, True) + ] +) +def test_build_payload_for_auth_update(upstream_url, base_key, auth_enabled, exception_expected): + with patch( + "app.main.service.backend.get_upstream_url", + return_value = upstream_url + ) as mock_get_upstream_url, patch( + "app.main.service.backend.get_basekey_from_backend", + return_value = base_key + ) as mock_get_basekey_from_backend: + backend: BackendOut = BackendOut( + owner = "4d2e5e17-a378-4df0-ba9e-4fb710f0eeb7", + template = "theiaide", + template_version = "v03", + auth_enabled = True, + id = 3872943384, + location_url = "tropicalantelope_100", + file_path = "3872943384%4d2e5e17-a378-4df0-ba9e-4fb710f0eeb7%tropicalantelope_100%theiaide%v03%1.conf" + ) + + result = backend_service.build_payload_for_auth_update(backend, auth_enabled) + mock_get_upstream_url.assert_called_once_with(backend.file_path) + mock_get_basekey_from_backend.assert_called_once_with(backend) + if not upstream_url or not base_key or exception_expected: + assert result is None + return + + assert isinstance(result, BackendIn) + assert result.owner == backend.owner + assert result.template == backend.template + assert result.template_version == backend.template_version + assert result.user_key_url == base_key + assert result.upstream_url == upstream_url + assert result.auth_enabled == auth_enabled \ No newline at end of file diff --git a/FastapiOpenRestyConfigurator/app/main/views/backend.py b/FastapiOpenRestyConfigurator/app/main/views/backend.py index e0218894..b45baa9e 100755 --- a/FastapiOpenRestyConfigurator/app/main/views/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/views/backend.py @@ -39,8 +39,11 @@ async def list_backends(api_key: APIKey = Depends(get_api_key)): summary="Create a new backend." ) async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_api_key)): + logger.info("111") if backend_in: + logger.info("112") backend = await backend_service.create_backend(backend_in) + logger.info(f"113 {backend_in.model_dump()}") return backend else: raise HTTPException(status_code=400) @@ -55,6 +58,7 @@ async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_ap async def backend_update_auth(backend_id: int, body: dict = Body(...), api_key: APIKey = Depends(get_api_key)): # process inputs TODO: should we validate further? backend_id = int(secure_filename(str(backend_id))) # TODO: are secure_filename and str necessary? validation? + assert backend_id is not None, "backend_id is required" # @reviewer: is this okay? enable_auth = bool(body.get("auth_enabled", None)) logger.debug(f"Attempting to update backend authorization to {enable_auth} for backend id: {backend_id}") @@ -105,6 +109,7 @@ async def get_backend(backend_id: int, api_key: APIKey = Depends(get_api_key)): async def delete_backend(backend_id: int, api_key: APIKey = Depends(get_api_key)): try: backend_id = int(secure_filename(str(backend_id))) + assert backend_id is not None, "backend_id is required" # @reviewer: is this okay? await backend_service.delete_backend(backend_id) await user_service.delete_all(backend_id) except NotFound: From b476870766f587c251a43cf9423626ec68a584f2 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Thu, 5 Mar 2026 00:47:42 +0000 Subject: [PATCH 030/118] finished tests --- .../app/main/service/backend.py | 15 +-- .../app/main/tests/test_service_backend.py | 106 ++++++++++++++---- 2 files changed, 94 insertions(+), 27 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index e1506ee5..e187f51a 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -222,14 +222,14 @@ async def delete_backend(backend_id) -> bool: matching_backend_filenames = filter_backend_filenames_by_id(backend_path_filenames, backend_id) - amount_of_files = len(matching_backend_filenames) - if amount_of_files == 0: + number_of_files = len(matching_backend_filenames) + if number_of_files == 0: logger.error(f"Backend {backend_id} was not found") raise NotFound(f"Backend {backend_id} was not found.") - if amount_of_files > 1: + if number_of_files > 1: logger.error(f"Found multiple backend files for backend id: {backend_id}, cannot delete.") raise InternalServerError("Found multiple backend files for backend id: {backend_id}, cannot delete.") - if not amount_of_files == 1: + if not number_of_files == 1: # fallback logger.error(f"Something went wrong. Did not expect multiple files for deletion. Backend id: {backend_id}") raise InternalServerError(f"Something went wrong. Did not expect multiple files for deletion. Backend id: {backend_id}") @@ -246,15 +246,16 @@ async def delete_backend(backend_id) -> bool: raise InternalServerError("Server was not able to delete this backend. Contact the admin.") -async def update_backend_authorization(backend_id: int, auth_enabled: bool) -> BackendOut | None: +async def update_backend_authorization(backend_id: int, auth_enable: bool) -> BackendOut | None: + # get existing BackendOut by id to fetch necessary information for payload building and filename generation backend = await get_backend_by_id(backend_id) if not backend: return None # build temporary payload as BackendIn for create_backend() - temp_payload = build_payload_for_auth_update(backend, auth_enabled) + temp_payload = build_payload_for_auth_update(backend, auth_enable) if not temp_payload: - return None + return None # generate new backend contents with temp_payload, additional kwargs to persist backend_id and location_url new_contents = await create_backend(temp_payload, id=str(backend_id), location_url=backend.location_url) diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index d1f7e6d2..35985fb1 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -1,6 +1,7 @@ import pytest from unittest.mock import call, patch +from werkzeug.exceptions import NotFound, InternalServerError from app.main.model.serializers import BackendIn, BackendOut, BackendTemp @@ -409,22 +410,87 @@ async def test_create_backend(backend_file_contents, delete_duplicate_backends): assert not backend_file_contents or not delete_duplicate_backends +@pytest.mark.parametrize( + "exception_expected, backend_path_filenames, matching_backend_filenames", + [ + (False, [file_path_example_1, file_path_example_2], [file_path_example_1]), + (False, [file_path_example_1, file_path_example_2], [file_path_example_2]), + (True, [file_path_example_1, file_path_example_2], []), + (True, [], []), + (True, [file_path_example_2], []) + ] +) +@pytest.mark.asyncio +async def test_delete_backend(exception_expected, backend_path_filenames, matching_backend_filenames): + with patch( + "app.main.config.get_settings" + ) as mock_get_settings, patch( + "app.main.service.backend.get_valid_backend_filenames", + return_value = backend_path_filenames + ) as mock_get_valid_backend_filenames, patch( + "app.main.service.backend.filter_backend_filenames_by_id", + return_value = matching_backend_filenames + ) as mock_filter_backend_filenames_by_id, patch( + "os.remove" + ) as mock_os_remove, patch( + "app.main.service.backend.reload_openresty" + ) as mock_reload_openresty: + try: + number_of_files = len(matching_backend_filenames) + result = await backend_service.delete_backend(0) # backend_id irrelevant due to mocks + # mock_get_settings.assert_called_once() + mock_get_valid_backend_filenames.assert_called_once() + mock_filter_backend_filenames_by_id.assert_called_once() + mock_os_remove.assert_called_once() + mock_reload_openresty.assert_awaited_once() + assert result is True + except NotFound as e: + assert exception_expected and (not backend_path_filenames or number_of_files == 0) + except InternalServerError as e: + assert exception_expected and backend_path_filenames and number_of_files != 1 + except OSError as e: + assert exception_expected and (not backend_path_filenames or number_of_files == 0) + except Exception as e: + raise e - - - - -""" -@pytest.mark.asyncio -async def test_delete_backend(): - ... - +@pytest.mark.parametrize( + "exception_expected, auth_enable, get_backend, temp_payload, new_contents, returning_backend", + [ + (False, True, True, True, True, True), + (False, False, True, True, True, True), + (True, True, False, True, True, True), + (True, True, True, False, True, True), + (True, True, True, True, False, True), + (True, True, True, True, True, False) + ] +) @pytest.mark.asyncio -async def test_update_backend_authorization(): - ... -""" +async def test_update_backend_authorization(exception_expected, auth_enable, get_backend, temp_payload, new_contents, returning_backend): + with patch( + "app.main.service.backend.get_backend_by_id", + return_value = BackendOut.model_construct(location_url="test_100") if get_backend else None + ) as mock_get_backend_by_id, patch( + "app.main.service.backend.build_payload_for_auth_update", + return_value = BackendIn.model_construct() if temp_payload else None + ) as mock_build_payload_for_auth_update, patch( + "app.main.service.backend.create_backend", + return_value = BackendTemp.model_construct() if new_contents else None + ) as mock_create_backend, patch( + "app.main.service.backend.convert_backend_temp_to_out", + return_value = BackendOut.model_construct() if returning_backend else None + ) as mock_convert_backend_temp_to_out: + + result = await backend_service.update_backend_authorization(0, auth_enable) # backend_id irrelevant due to mocks + if not exception_expected: + mock_get_backend_by_id.assert_awaited_once_with(0) + mock_build_payload_for_auth_update.assert_called_once_with(mock_get_backend_by_id.return_value, auth_enable) + mock_create_backend.assert_awaited_once_with(mock_build_payload_for_auth_update.return_value, id='0', location_url='test_100') + mock_convert_backend_temp_to_out.assert_awaited_once_with(mock_create_backend.return_value) + assert result == mock_convert_backend_temp_to_out.return_value + else: + assert result is None @@ -686,16 +752,16 @@ def test_filter_backend_filenames_by_id(backend_id, filenames, expected): @pytest.mark.parametrize( - "upstream_url, base_key, auth_enabled, exception_expected", + "exception_expected, upstream_url, base_key, auth_enabled", [ - ("http://192.168.0.1:8787/guacamole/", "olddragon", True, False), - ("http://192.168.0.1:4000/guacamole/", "youngmonkey", False, False), - (None, "crazydog", True, False), - ("http://192.168.0.1:1000/", None, False, False), - ("http://192.168.0.1:1234/", "test_100", None, True) + (False, "http://192.168.0.1:8787/guacamole/", "olddragon", True), + (False, "http://192.168.0.1:4000/guacamole/", "youngmonkey", False), + (True, None, "crazydog", True), + (True, "http://192.168.0.1:1000/", None, False), + (True, "http://192.168.0.1:1234/", "test_100", None) ] ) -def test_build_payload_for_auth_update(upstream_url, base_key, auth_enabled, exception_expected): +def test_build_payload_for_auth_update(exception_expected, upstream_url, base_key, auth_enabled): with patch( "app.main.service.backend.get_upstream_url", return_value = upstream_url @@ -726,4 +792,4 @@ def test_build_payload_for_auth_update(upstream_url, base_key, auth_enabled, exc assert result.template_version == backend.template_version assert result.user_key_url == base_key assert result.upstream_url == upstream_url - assert result.auth_enabled == auth_enabled \ No newline at end of file + assert result.auth_enabled == auth_enabled From 40b66a4a029a3dffd44639b1fedaf73d0b021001 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 5 Mar 2026 14:39:06 +0000 Subject: [PATCH 031/118] changed info to debug log --- .../app/main/service/backend.py | 4 +- .../tests/__init__.py | 0 .../tests/conftest.py | 23 +++++ .../tests/factories/__init__.py | 0 .../tests/factories/backendOutFactory.py | 32 ++++++ .../tests/test_service_backend.py | 99 +++++++++++++++++++ .../tests/test_views_backend.py | 63 ++++++++++++ 7 files changed, 219 insertions(+), 2 deletions(-) create mode 100755 FastapiOpenRestyConfigurator/tests/__init__.py create mode 100644 FastapiOpenRestyConfigurator/tests/conftest.py create mode 100755 FastapiOpenRestyConfigurator/tests/factories/__init__.py create mode 100644 FastapiOpenRestyConfigurator/tests/factories/backendOutFactory.py create mode 100644 FastapiOpenRestyConfigurator/tests/test_service_backend.py create mode 100644 FastapiOpenRestyConfigurator/tests/test_views_backend.py diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index e187f51a..8975583e 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -80,14 +80,14 @@ async def get_backends() -> List[BackendOut]: if len(os.listdir(settings.FORC_BACKEND_PATH)) == 0: return [] backend_path_filenames = os.listdir(settings.FORC_BACKEND_PATH) - logger.info(f"Files in backend_path: {backend_path_filenames}") + logger.debug(f"Files in backend_path: {backend_path_filenames}") valid_backends = [] for filename in backend_path_filenames: match = re.fullmatch(filename_regex, filename) if not match: if filename == "users" or filename == "scripts": continue - logger.warning("Found a backend file with wrong naming, skipping it: " + str(filename)) + logger.debug("Found a backend file with wrong naming, skipping it: " + str(filename)) continue backend: BackendOut = BackendOut( id = int(match.group(1)), diff --git a/FastapiOpenRestyConfigurator/tests/__init__.py b/FastapiOpenRestyConfigurator/tests/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/FastapiOpenRestyConfigurator/tests/conftest.py b/FastapiOpenRestyConfigurator/tests/conftest.py new file mode 100644 index 00000000..d647da51 --- /dev/null +++ b/FastapiOpenRestyConfigurator/tests/conftest.py @@ -0,0 +1,23 @@ +import pytest +import os + +from .factories.backendOutFactory import build_backend_out + +@pytest.fixture(scope="session", autouse=True) +def test_dirs(tmp_path_factory): + backend = tmp_path_factory.mktemp("backend") + templates = tmp_path_factory.mktemp("templates") + + os.environ["FORC_BACKEND_PATH"] = str(backend) + os.environ["FORC_TEMPLATE_PATH"] = str(templates) + os.environ["FORC_API_KEY"] = "test-api-key" + + yield + + +@pytest.fixture +def backend_out_factory(): + """ + Returns a callable factory. + """ + return build_backend_out diff --git a/FastapiOpenRestyConfigurator/tests/factories/__init__.py b/FastapiOpenRestyConfigurator/tests/factories/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/FastapiOpenRestyConfigurator/tests/factories/backendOutFactory.py b/FastapiOpenRestyConfigurator/tests/factories/backendOutFactory.py new file mode 100644 index 00000000..5dfd5f42 --- /dev/null +++ b/FastapiOpenRestyConfigurator/tests/factories/backendOutFactory.py @@ -0,0 +1,32 @@ +from app.main.model.serializers import BackendOut +import factory +from faker import Faker +import string +import random +fake = Faker() +def random_owner(): + allowed = string.ascii_letters + string.digits + "@" + # mind. 30 chars + return "".join(random.choices(allowed, k=30)) + +class BackendOutFactory(factory.DictFactory): + id = factory.Sequence(lambda n: n + 1) + + owner = factory.LazyFunction(random_owner) + template = "testtemplate" + template_version = "v01" + location_url = factory.Faker("url") + config_path = factory.LazyAttribute( + lambda o: f"{o.template}%{o.template_version}%0.conf" + ) + + class Meta: + model = dict + +def build_backend_out(**kwargs) -> BackendOut: + """ + Helper that returns a validated BackendOut model. + """ + return BackendOut.model_validate( + BackendOutFactory.build(**kwargs) + ) \ No newline at end of file diff --git a/FastapiOpenRestyConfigurator/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/tests/test_service_backend.py new file mode 100644 index 00000000..573520dd --- /dev/null +++ b/FastapiOpenRestyConfigurator/tests/test_service_backend.py @@ -0,0 +1,99 @@ +import pytest +from unittest.mock import patch + +from app.main.model.serializers import BackendOut + +from app.main.service import backend as backend_service + +from werkzeug.exceptions import NotFound + +file_path_example_1 = ( + "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" +) +file_path_example_2 = ( + "/var/forc/backend_path/9876543210%otheruser%cat_200%othertemplate%v02%1.conf" +) + + +@pytest.mark.asyncio +async def test_get_backend_by_id(backend_out_factory): + backendout1 = backend_out_factory(id=123) + backendout2 = backend_out_factory(id=456) + + + with patch( + "app.main.service.backend.get_backends", + return_value=[backendout1, backendout2], + ) as mock_get_backends: + + response: BackendOut = await backend_service.get_backend_by_id(123) + + assert response.id == 123 + mock_get_backends.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_get_backend_by_id_not_found(backend_out_factory): + backendout1 = backend_out_factory(id=123) + backendout2 = backend_out_factory(id=456) + + with pytest.raises(NotFound): + with patch( + "app.main.service.backend.get_backends", + return_value=[backendout1, backendout2], + ) as mock_get_backends: + + await backend_service.get_backend_by_id(789) + mock_get_backends.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_get_filepath_by_id(backend_out_factory): + backendout1 = backend_out_factory(id=1234567890, file_path=file_path_example_1) + backendout2 = backend_out_factory(id=9876543210, file_path=file_path_example_2) + + with patch( + "app.main.service.backend.get_backends", + return_value=[ + backendout1, + backendout2, + ], + ) as mock_get_backends: + + response: str = await backend_service.get_file_path_by_id(1234567890) + + assert response == file_path_example_1 + mock_get_backends.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_get_filepath_by_id_not_found(backend_out_factory): + backendout1 = backend_out_factory(id=1234567890) + + with pytest.raises(NotFound): + with patch( + "app.main.service.backend.get_backends", + return_value=[backendout1], + ) as mock_get_backends: + await backend_service.get_file_path_by_id(9876543210) + mock_get_backends.assert_awaited_once() + + +""" +@pytest.mark.asyncio +async def test_create_backend(): # check kwargs payload id and suffix number at the end + + with patch( + "app.main.service.backend.generate_suffix_number", + return_value = 111 + ) as mock_generate_suffix_number, patch( + "app.main.service.backend.random_with_n_digits", + return_value = 9876543210 + ) as mock_random_with_n_digits, patch( + "app.main.service.backend.generate_backend_by_template", + + +@pytest.mark.asyncio +async def test_update_backend_authorization_activate(): + +""" diff --git a/FastapiOpenRestyConfigurator/tests/test_views_backend.py b/FastapiOpenRestyConfigurator/tests/test_views_backend.py new file mode 100644 index 00000000..1d50fed4 --- /dev/null +++ b/FastapiOpenRestyConfigurator/tests/test_views_backend.py @@ -0,0 +1,63 @@ +import pytest +from unittest.mock import patch + +from app.main.model.serializers import BackendOut + +from app.main.views import backend as backend_views + +from fastapi import HTTPException + + +@pytest.mark.asyncio +async def test_backend_update_auth_activate(backend_out_factory): + backendOut = backend_out_factory(id=123, auth_enabled=True) + + + with patch( + "app.main.views.backend.update_backend_authorization", + return_value=backendOut, + ) as mock_update_backend_authorization: + + response: BackendOut = await backend_views.backend_update_auth( + backend_id=123, body={"auth_enabled": True}, api_key="test" + ) + + assert response.id == 123 + assert response.auth_enabled is True + mock_update_backend_authorization.assert_awaited_once_with( + backend_id=123, auth_enabled=True + ) + + +@pytest.mark.asyncio +async def test_backend_update_auth_deactivate(backend_out_factory): + backendOut = backend_out_factory(id=123, auth_enabled=False) + + with patch( + "app.main.views.backend.update_backend_authorization", + return_value=backendOut, + ) as mock_update_backend_authorization: + + response: BackendOut = await backend_views.backend_update_auth( + backend_id=123, body={"auth_enabled": False}, api_key="test" + ) + + assert response.id == 123 + assert response.auth_enabled is False + mock_update_backend_authorization.assert_awaited_once_with( + backend_id=123, auth_enabled=False + ) + + +@pytest.mark.asyncio +async def test_backend_update_auth_invalid_body(): + + with pytest.raises(HTTPException) as not_boolean_exception: + await backend_views.backend_update_auth( + backend_id=123, body={"auth_enabled": "not a boolean"}, api_key="test" + ) + assert not_boolean_exception.value.status_code == 422 + + with pytest.raises(HTTPException) as empty_body_exception: + await backend_views.backend_update_auth(backend_id=123, body={}, api_key="test") + assert empty_body_exception.value.status_code == 422 From d9b62dc2093673a391ea66e41e511084527d100a Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 5 Mar 2026 14:42:10 +0000 Subject: [PATCH 032/118] changed info to debug log --- FastapiOpenRestyConfigurator/app/main/service/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 8975583e..0e301904 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -371,7 +371,7 @@ def check_backend_file_naming(backend_path_filename: str) -> bool: else: # exclude expected files from warning if not backend_path_filename == "users" and not backend_path_filename == "scripts": - logger.warning(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") + logger.debug(f"Found a backend file with wrong naming, skipping it: {backend_path_filename}") return False From 7b06957b4cff90fddd535df08621ef1d8db745ee Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Mon, 9 Mar 2026 15:15:14 +0000 Subject: [PATCH 033/118] updated all template example files --- examples/templates/cwlab%v01.conf | 4 +++- examples/templates/cwlab%v02.conf | 4 +++- examples/templates/emgb%v01.conf | 4 +++- examples/templates/guacamole%v01.conf | 4 +++- examples/templates/guacamole%v02.conf | 4 +++- examples/templates/guacamole%v03.conf | 4 +++- examples/templates/jupyterlab%v01.conf | 4 +++- examples/templates/jupyterlab%v02.conf | 4 +++- examples/templates/jupyterlab%v03.conf | 4 +++- examples/templates/rstudio%v01.conf | 4 +++- examples/templates/rstudio%v02.conf | 6 ++++-- examples/templates/rstudio%v03.conf | 4 +++- examples/templates/rstudio%v04.conf | 4 +++- examples/templates/theiaide%v01.conf | 4 +++- examples/templates/theiaide%v02.conf | 4 +++- examples/templates/theiaide%v03.conf | 4 ++-- examples/templates/vscode%v03.conf | 4 +++- 17 files changed, 51 insertions(+), 19 deletions(-) diff --git a/examples/templates/cwlab%v01.conf b/examples/templates/cwlab%v01.conf index 374631e2..17f21f8c 100755 --- a/examples/templates/cwlab%v01.conf +++ b/examples/templates/cwlab%v01.conf @@ -14,10 +14,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/cwlab%v02.conf b/examples/templates/cwlab%v02.conf index 4f3a3f16..31b05a1a 100755 --- a/examples/templates/cwlab%v02.conf +++ b/examples/templates/cwlab%v02.conf @@ -31,10 +31,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) diff --git a/examples/templates/emgb%v01.conf b/examples/templates/emgb%v01.conf index 61412b62..06786a72 100755 --- a/examples/templates/emgb%v01.conf +++ b/examples/templates/emgb%v01.conf @@ -30,10 +30,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) diff --git a/examples/templates/guacamole%v01.conf b/examples/templates/guacamole%v01.conf index 2e734eae..153d444f 100755 --- a/examples/templates/guacamole%v01.conf +++ b/examples/templates/guacamole%v01.conf @@ -14,10 +14,12 @@ location /{{ key_url }}/ { end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/guacamole%v02.conf b/examples/templates/guacamole%v02.conf index 4a28b1e1..c6b4fd78 100755 --- a/examples/templates/guacamole%v02.conf +++ b/examples/templates/guacamole%v02.conf @@ -15,10 +15,12 @@ location /{{ key_url }}/ { end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/guacamole%v03.conf b/examples/templates/guacamole%v03.conf index 75f1ab22..9c3a6360 100755 --- a/examples/templates/guacamole%v03.conf +++ b/examples/templates/guacamole%v03.conf @@ -29,10 +29,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) diff --git a/examples/templates/jupyterlab%v01.conf b/examples/templates/jupyterlab%v01.conf index 6517135b..4f210aea 100755 --- a/examples/templates/jupyterlab%v01.conf +++ b/examples/templates/jupyterlab%v01.conf @@ -13,10 +13,12 @@ location /{{ key_url }} { end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/jupyterlab%v02.conf b/examples/templates/jupyterlab%v02.conf index 874267c2..9e1e4be5 100755 --- a/examples/templates/jupyterlab%v02.conf +++ b/examples/templates/jupyterlab%v02.conf @@ -14,10 +14,12 @@ location /{{ key_url }} { end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/jupyterlab%v03.conf b/examples/templates/jupyterlab%v03.conf index 9684fcd8..06abe3f6 100755 --- a/examples/templates/jupyterlab%v03.conf +++ b/examples/templates/jupyterlab%v03.conf @@ -29,10 +29,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) diff --git a/examples/templates/rstudio%v01.conf b/examples/templates/rstudio%v01.conf index a939e9f8..c63e253d 100755 --- a/examples/templates/rstudio%v01.conf +++ b/examples/templates/rstudio%v01.conf @@ -11,10 +11,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/rstudio%v02.conf b/examples/templates/rstudio%v02.conf index 8e2d0b65..e0ac6fb6 100755 --- a/examples/templates/rstudio%v02.conf +++ b/examples/templates/rstudio%v02.conf @@ -11,10 +11,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } @@ -22,7 +24,7 @@ rewrite ^/{{ key_url }}/(.*)$ /$1 break; proxy_pass {{ location_url }}; proxy_redirect {{ location_url }} $scheme://$http_host/{{ key_url }}/; - proxy_http_version 1.1; @ reviewer: we have the same here. is this necessary? + proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 20d; diff --git a/examples/templates/rstudio%v03.conf b/examples/templates/rstudio%v03.conf index 7975047c..40de41f3 100755 --- a/examples/templates/rstudio%v03.conf +++ b/examples/templates/rstudio%v03.conf @@ -14,10 +14,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/rstudio%v04.conf b/examples/templates/rstudio%v04.conf index 25f4cfcd..61224e5c 100755 --- a/examples/templates/rstudio%v04.conf +++ b/examples/templates/rstudio%v04.conf @@ -29,10 +29,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) diff --git a/examples/templates/theiaide%v01.conf b/examples/templates/theiaide%v01.conf index 3be18f79..3cab167d 100755 --- a/examples/templates/theiaide%v01.conf +++ b/examples/templates/theiaide%v01.conf @@ -12,10 +12,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/theiaide%v02.conf b/examples/templates/theiaide%v02.conf index be886177..0b2a7e70 100755 --- a/examples/templates/theiaide%v02.conf +++ b/examples/templates/theiaide%v02.conf @@ -14,10 +14,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} } diff --git a/examples/templates/theiaide%v03.conf b/examples/templates/theiaide%v03.conf index 8d24ae93..ac912ad0 100755 --- a/examples/templates/theiaide%v03.conf +++ b/examples/templates/theiaide%v03.conf @@ -29,7 +29,7 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner is true %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end @@ -50,7 +50,7 @@ # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. rewrite /{{ key_url }}/(.*) /$1 break; proxy_pass {{ location_url }}; - proxy_http_version 1.1; # @reviewer: is this needed? + proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/examples/templates/vscode%v03.conf b/examples/templates/vscode%v03.conf index 61412b62..06786a72 100755 --- a/examples/templates/vscode%v03.conf +++ b/examples/templates/vscode%v03.conf @@ -30,10 +30,12 @@ end -- Protect this location and allow only one specific ELIXIR User - {% if only_allow_owner %} + {% if auth_enabled %} if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN {% endif %} ngx.req.set_header("X-Auth-Audience", res.id_token.aud) From 7676244347341500bf2c2efb7c93ce62f94da490 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Mon, 9 Mar 2026 16:12:19 +0000 Subject: [PATCH 034/118] removed auth-toggle from all example templates except for emgb and deleted cwlab template --- examples/templates/cwlab%v01.conf | 40 ------------- examples/templates/cwlab%v02.conf | 66 ---------------------- examples/templates/guacamole%v01.conf | 60 +++++++++----------- examples/templates/guacamole%v02.conf | 52 ++++++++--------- examples/templates/guacamole%v03.conf | 46 +++++++-------- examples/templates/jupyterlab%v01.conf | 75 ++++++++++++------------- examples/templates/jupyterlab%v02.conf | 68 +++++++++++----------- examples/templates/jupyterlab%v03.conf | 78 ++++++++++++-------------- examples/templates/rstudio%v01.conf | 46 +++++++-------- examples/templates/rstudio%v02.conf | 36 ++++++------ examples/templates/rstudio%v03.conf | 53 ++++++++--------- examples/templates/rstudio%v04.conf | 49 ++++++++-------- examples/templates/theiaide%v01.conf | 69 +++++++++++------------ examples/templates/theiaide%v02.conf | 73 ++++++++++++------------ examples/templates/theiaide%v03.conf | 45 +++++++-------- examples/templates/vscode%v03.conf | 47 ++++++++-------- 16 files changed, 371 insertions(+), 532 deletions(-) delete mode 100755 examples/templates/cwlab%v01.conf delete mode 100755 examples/templates/cwlab%v02.conf diff --git a/examples/templates/cwlab%v01.conf b/examples/templates/cwlab%v01.conf deleted file mode 100755 index 17f21f8c..00000000 --- a/examples/templates/cwlab%v01.conf +++ /dev/null @@ -1,40 +0,0 @@ - location /{{ key_url }}/ { - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } - - rewrite /{{ key_url }}/(.*) /$1 break; - proxy_pass {{ location_url }}; - sub_filter '"/' '"/{{ key_url }}/'; - sub_filter_once off; - - proxy_http_version 1.1; - proxy_set_header Host $host:$server_port; - proxy_set_header X-Forwarded-Host $server_name; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - proxy_set_header Origin ''; - } \ No newline at end of file diff --git a/examples/templates/cwlab%v02.conf b/examples/templates/cwlab%v02.conf deleted file mode 100755 index 31b05a1a..00000000 --- a/examples/templates/cwlab%v02.conf +++ /dev/null @@ -1,66 +0,0 @@ - location /{{ key_url }}/ { - - set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier - set $session_storage shm; # use shared memory - set $session_cookie_persistent on; # persist cookie between browser sessions - set $session_cookie_renew 3500; # new cookie every hour - set $session_cookie_lifetime 86400; # lifetime for persistent cookies - set $session_name sess_auth; # name of the cookie to store the session identifier in - - set $session_shm_store sessions; # name of the dict to store sessions in - # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; - - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - - ngx.req.set_header("X-Auth-Audience", res.id_token.aud) - ngx.req.set_header("X-Auth-Email", res.id_token.email) - ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) - ngx.req.set_header("X-Auth-Name", res.id_token.name) - ngx.req.set_header("X-Auth-Subject", res.id_token.sub) - ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Locale", res.id_token.locale) - } - - rewrite /{{ key_url }}/(.*) /$1 break; - proxy_pass {{ location_url }}; - sub_filter '"/' '"/{{ key_url }}/'; - sub_filter_once off; - - proxy_http_version 1.1; - proxy_set_header Host $host:$server_port; - proxy_set_header X-Forwarded-Host $server_name; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - proxy_set_header Origin ''; - } \ No newline at end of file diff --git a/examples/templates/guacamole%v01.conf b/examples/templates/guacamole%v01.conf index 153d444f..3ecaf822 100755 --- a/examples/templates/guacamole%v01.conf +++ b/examples/templates/guacamole%v01.conf @@ -2,35 +2,31 @@ location /{{ key_url }}/ { - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } - - - - - proxy_pass {{ location_url }}/guacamole/; - proxy_buffering off; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - access_log off; -} \ No newline at end of file + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } + + + + + proxy_pass {{ location_url }}/guacamole/; + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + access_log off; + } \ No newline at end of file diff --git a/examples/templates/guacamole%v02.conf b/examples/templates/guacamole%v02.conf index c6b4fd78..4268d531 100755 --- a/examples/templates/guacamole%v02.conf +++ b/examples/templates/guacamole%v02.conf @@ -1,37 +1,33 @@ location /{{ key_url }}/ { - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end + set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } - proxy_pass {{ location_url }}/guacamole/; - proxy_buffering off; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - access_log off; + proxy_pass {{ location_url }}/guacamole/; + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + access_log off; } \ No newline at end of file diff --git a/examples/templates/guacamole%v03.conf b/examples/templates/guacamole%v03.conf index 9c3a6360..8bca83ac 100755 --- a/examples/templates/guacamole%v03.conf +++ b/examples/templates/guacamole%v03.conf @@ -1,4 +1,4 @@ - location /{{ key_url }}/ { +location /{{ key_url }}/ { set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier set $session_storage shm; # use shared memory set $session_cookie_persistent on; # persist cookie between browser sessions @@ -18,33 +18,29 @@ set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then ngx.status = 500 ngx.say(err) ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - - ngx.req.set_header("X-Auth-Audience", res.id_token.aud) - ngx.req.set_header("X-Auth-Email", res.id_token.email) - ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) - ngx.req.set_header("X-Auth-Name", res.id_token.name) - ngx.req.set_header("X-Auth-Subject", res.id_token.sub) - ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + end + + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) + ngx.req.set_header("X-Auth-Email", res.id_token.email) + ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) + ngx.req.set_header("X-Auth-Name", res.id_token.name) + ngx.req.set_header("X-Auth-Subject", res.id_token.sub) + ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Locale", res.id_token.locale) } diff --git a/examples/templates/jupyterlab%v01.conf b/examples/templates/jupyterlab%v01.conf index 4f210aea..43a001a7 100755 --- a/examples/templates/jupyterlab%v01.conf +++ b/examples/templates/jupyterlab%v01.conf @@ -1,42 +1,37 @@ location /{{ key_url }} { - - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } - - proxy_pass {{ location_url }}; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; - proxy_set_header X-Scheme $scheme; - - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; - - - } + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } + + proxy_pass {{ location_url }}; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 20d; + proxy_set_header X-Scheme $scheme; + + client_max_body_size 0; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Referrer-Policy "same-origin" always; + + +} diff --git a/examples/templates/jupyterlab%v02.conf b/examples/templates/jupyterlab%v02.conf index 9e1e4be5..9597b469 100755 --- a/examples/templates/jupyterlab%v02.conf +++ b/examples/templates/jupyterlab%v02.conf @@ -1,44 +1,40 @@ location /{{ key_url }} { - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end + set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - proxy_pass {{ location_url }}; - proxy_http_version 1.1; + # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. + proxy_pass {{ location_url }}; + proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; - proxy_set_header X-Scheme $scheme; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 20d; + proxy_set_header X-Scheme $scheme; - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; + client_max_body_size 0; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Referrer-Policy "same-origin" always; - } +} diff --git a/examples/templates/jupyterlab%v03.conf b/examples/templates/jupyterlab%v03.conf index 06abe3f6..26d0da25 100755 --- a/examples/templates/jupyterlab%v03.conf +++ b/examples/templates/jupyterlab%v03.conf @@ -1,4 +1,4 @@ - location /{{ key_url }} { +location /{{ key_url }} { set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier set $session_storage shm; # use shared memory set $session_cookie_persistent on; # persist cookie between browser sessions @@ -18,51 +18,47 @@ set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end - ngx.req.set_header("X-Auth-Audience", res.id_token.aud) - ngx.req.set_header("X-Auth-Email", res.id_token.email) - ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) - ngx.req.set_header("X-Auth-Name", res.id_token.name) - ngx.req.set_header("X-Auth-Subject", res.id_token.sub) - ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) + ngx.req.set_header("X-Auth-Email", res.id_token.email) + ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) + ngx.req.set_header("X-Auth-Name", res.id_token.name) + ngx.req.set_header("X-Auth-Subject", res.id_token.sub) + ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Locale", res.id_token.locale) } - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. + # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - proxy_pass {{ location_url }}; - proxy_http_version 1.1; + proxy_pass {{ location_url }}; + proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; - proxy_set_header X-Scheme $scheme; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 20d; + proxy_set_header X-Scheme $scheme; - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; + client_max_body_size 0; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Referrer-Policy "same-origin" always; - } +} diff --git a/examples/templates/rstudio%v01.conf b/examples/templates/rstudio%v01.conf index c63e253d..957c1116 100755 --- a/examples/templates/rstudio%v01.conf +++ b/examples/templates/rstudio%v01.conf @@ -1,31 +1,27 @@ - location /{{ key_url }}/ { - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end +location /{{ key_url }}/ { + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end } - rewrite ^/{{ key_url }}/(.*)$ /$1 break; - proxy_pass {{ location_url }}; - proxy_redirect {{ location_url }} $scheme://$http_host/rstudio/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; + rewrite ^/{{ key_url }}/(.*)$ /$1 break; + proxy_pass {{ location_url }}; + proxy_redirect {{ location_url }} $scheme://$http_host/rstudio/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 20d; } diff --git a/examples/templates/rstudio%v02.conf b/examples/templates/rstudio%v02.conf index e0ac6fb6..501df4b6 100755 --- a/examples/templates/rstudio%v02.conf +++ b/examples/templates/rstudio%v02.conf @@ -1,25 +1,21 @@ - location /{{ key_url }}/ { - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end +location /{{ key_url }}/ { - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } rewrite ^/{{ key_url }}/(.*)$ /$1 break; proxy_pass {{ location_url }}; diff --git a/examples/templates/rstudio%v03.conf b/examples/templates/rstudio%v03.conf index 40de41f3..348abc9e 100755 --- a/examples/templates/rstudio%v03.conf +++ b/examples/templates/rstudio%v03.conf @@ -1,34 +1,29 @@ - location /{{ key_url }}/ { +location /{{ key_url }}/ { - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end + set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } - - rewrite ^/{{ key_url }}/(.*)$ /$1 break; - proxy_pass {{ location_url }}; - proxy_redirect {{ location_url }} $scheme://$http_host/{{ key_url }}/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; + rewrite ^/{{ key_url }}/(.*)$ /$1 break; + proxy_pass {{ location_url }}; + proxy_redirect {{ location_url }} $scheme://$http_host/{{ key_url }}/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 20d; } diff --git a/examples/templates/rstudio%v04.conf b/examples/templates/rstudio%v04.conf index 61224e5c..e240053b 100755 --- a/examples/templates/rstudio%v04.conf +++ b/examples/templates/rstudio%v04.conf @@ -1,4 +1,4 @@ - location /{{ key_url }}/ { +location /{{ key_url }}/ { set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier set $session_storage shm; # use shared memory set $session_cookie_persistent on; # persist cookie between browser sessions @@ -18,36 +18,31 @@ set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) access_by_lua_block { - local user_service = require "user_service" - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end + local user_service = require "user_service" + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end - ngx.req.set_header("X-Auth-Audience", res.id_token.aud) - ngx.req.set_header("X-Auth-Email", res.id_token.email) - ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) - ngx.req.set_header("X-Auth-Name", res.id_token.name) - ngx.req.set_header("X-Auth-Subject", res.id_token.sub) - ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) + ngx.req.set_header("X-Auth-Email", res.id_token.email) + ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) + ngx.req.set_header("X-Auth-Name", res.id_token.name) + ngx.req.set_header("X-Auth-Subject", res.id_token.sub) + ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Locale", res.id_token.locale) } - rewrite ^/{{ key_url }}/(.*)$ /$1 break; proxy_pass {{ location_url }}; proxy_redirect {{ location_url }} $scheme://$http_host/{{ key_url }}/; diff --git a/examples/templates/theiaide%v01.conf b/examples/templates/theiaide%v01.conf index 3cab167d..b922db06 100755 --- a/examples/templates/theiaide%v01.conf +++ b/examples/templates/theiaide%v01.conf @@ -1,39 +1,36 @@ - # PROTECT FIRST THEIA CONTAINER - location /{{ key_url }}/ { - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end +# PROTECT FIRST THEIA CONTAINER +location /{{ key_url }}/ { - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - rewrite /{{ key_url }}/(.*) /$1 break; - proxy_pass {{ location_url }}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; - access_log logs/code.access.log; - error_log logs/code.error.log; - } \ No newline at end of file + # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. + rewrite /{{ key_url }}/(.*) /$1 break; + proxy_pass {{ location_url }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + client_max_body_size 0; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Referrer-Policy "same-origin" always; + access_log logs/code.access.log; + error_log logs/code.error.log; +} \ No newline at end of file diff --git a/examples/templates/theiaide%v02.conf b/examples/templates/theiaide%v02.conf index 0b2a7e70..67ce0246 100755 --- a/examples/templates/theiaide%v02.conf +++ b/examples/templates/theiaide%v02.conf @@ -1,41 +1,38 @@ - # PROTECT FIRST THEIA CONTAINER - location /{{ key_url }}/ { - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end +# PROTECT FIRST THEIA CONTAINER +location /{{ key_url }}/ { - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - } + set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - rewrite /{{ key_url }}/(.*) /$1 break; - proxy_pass {{ location_url }}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + } - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; - access_log logs/code.access.log; - error_log logs/code.error.log; - } \ No newline at end of file + # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. + rewrite /{{ key_url }}/(.*) /$1 break; + proxy_pass {{ location_url }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + client_max_body_size 0; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Referrer-Policy "same-origin" always; + access_log logs/code.access.log; + error_log logs/code.error.log; +} \ No newline at end of file diff --git a/examples/templates/theiaide%v03.conf b/examples/templates/theiaide%v03.conf index ac912ad0..0374bdb6 100755 --- a/examples/templates/theiaide%v03.conf +++ b/examples/templates/theiaide%v03.conf @@ -1,4 +1,5 @@ - location /{{ key_url }} { +location /{{ key_url }} { + set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier set $session_storage shm; # use shared memory set $session_cookie_persistent on; # persist cookie between browser sessions @@ -18,33 +19,29 @@ set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} + -- Protect this location and allow only one specific ELIXIR User if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} - ngx.req.set_header("X-Auth-Audience", res.id_token.aud) - ngx.req.set_header("X-Auth-Email", res.id_token.email) - ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) - ngx.req.set_header("X-Auth-Name", res.id_token.name) - ngx.req.set_header("X-Auth-Subject", res.id_token.sub) - ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) + ngx.req.set_header("X-Auth-Email", res.id_token.email) + ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) + ngx.req.set_header("X-Auth-Name", res.id_token.name) + ngx.req.set_header("X-Auth-Subject", res.id_token.sub) + ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Locale", res.id_token.locale) } # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. @@ -65,4 +62,4 @@ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header Referrer-Policy "same-origin" always; - } +} diff --git a/examples/templates/vscode%v03.conf b/examples/templates/vscode%v03.conf index 06786a72..8386815f 100755 --- a/examples/templates/vscode%v03.conf +++ b/examples/templates/vscode%v03.conf @@ -1,5 +1,6 @@ - # PROTECT FIRST THEIA CONTAINER - location /{{ key_url }}/ { +# PROTECT FIRST THEIA CONTAINER +location /{{ key_url }}/ { + set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier set $session_storage shm; # use shared memory set $session_cookie_persistent on; # persist cookie between browser sessions @@ -19,33 +20,29 @@ set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then ngx.status = 500 ngx.say(err) ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end + end - -- Protect this location and allow only one specific ELIXIR User - {% if auth_enabled %} - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - {% else %} - -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN - {% endif %} + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end - ngx.req.set_header("X-Auth-Audience", res.id_token.aud) - ngx.req.set_header("X-Auth-Email", res.id_token.email) - ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) - ngx.req.set_header("X-Auth-Name", res.id_token.name) - ngx.req.set_header("X-Auth-Subject", res.id_token.sub) - ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) + ngx.req.set_header("X-Auth-Email", res.id_token.email) + ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) + ngx.req.set_header("X-Auth-Name", res.id_token.name) + ngx.req.set_header("X-Auth-Subject", res.id_token.sub) + ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Locale", res.id_token.locale) } # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. @@ -63,4 +60,4 @@ add_header Referrer-Policy "same-origin" always; access_log logs/code.access.log; error_log logs/code.error.log; - } +} From 3611bb1a805b1eb95719594cf45f7bd4eb8c809d Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Mon, 9 Mar 2026 16:14:37 +0000 Subject: [PATCH 035/118] requested changes, refactored generate_suffix_number(), added version to requirements --- .../app/main/service/backend.py | 22 ++++++++++++++----- .../app/main/views/backend.py | 10 ++++----- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 0e301904..70880ff5 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -41,7 +41,20 @@ async def generate_suffix_number(location_url: str | None = None) -> int: if current_suffix_number < 100 or current_suffix_number > 999: logger.error("Invalid user_key_url provided for suffix generation: " + location_url) raise InternalServerError("Invalid user_key_url provided for suffix generation.") - # look for backends with same user_key_url + + # determine suffix number + highest_id = await get_highest_suffix_number(location_url) + + if highest_id == 999: + logger.warning("Reached max index number for requested user_key_url: " + location_url) + raise InternalServerError("Reached max index number for requested user_key_url (limit=999).") + return highest_id + 1 + +async def get_highest_suffix_number(location_url: str) -> int: + """ + Looks for backends with same user_key_url and returns the highest suffix number. See generate_suffix_number(). + """ + # look for backends with same user_key_url and extract suffix numbers, if they exist backends: List[BackendOut] = await get_backends() same_name_backend_suffixes: List[int] = [] for backend in backends: @@ -54,10 +67,7 @@ async def generate_suffix_number(location_url: str | None = None) -> int: # return highest found suffix number + 1 to iterate same_name_backend_suffixes.sort() highest_id: int = same_name_backend_suffixes[-1] - if highest_id == 999: - logger.warning("Reached max index number for requested user_key_url: " + location_url) - raise InternalServerError("Reached max index number for requested user_key_url (limit=999).") - return highest_id + 1 + return highest_id def generate_backend_filename(backend: BackendOut) -> str: @@ -105,7 +115,7 @@ async def get_backends() -> List[BackendOut]: async def get_backend_by_id(backend_id: int) -> BackendOut: valid_backends: List[BackendOut] = await get_backends() for backend in valid_backends: - if int(backend.id) == int(backend_id): # @reviewer: are we sure that there is only one backend with this backend_id? + if int(backend.id) == int(backend_id): return backend raise NotFound(f"Backend with id {backend_id} was not found.") diff --git a/FastapiOpenRestyConfigurator/app/main/views/backend.py b/FastapiOpenRestyConfigurator/app/main/views/backend.py index b45baa9e..0bdd583c 100755 --- a/FastapiOpenRestyConfigurator/app/main/views/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/views/backend.py @@ -39,11 +39,8 @@ async def list_backends(api_key: APIKey = Depends(get_api_key)): summary="Create a new backend." ) async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_api_key)): - logger.info("111") if backend_in: - logger.info("112") backend = await backend_service.create_backend(backend_in) - logger.info(f"113 {backend_in.model_dump()}") return backend else: raise HTTPException(status_code=400) @@ -57,8 +54,10 @@ async def create_backend(backend_in: BackendIn, api_key: APIKey = Depends(get_ap ) async def backend_update_auth(backend_id: int, body: dict = Body(...), api_key: APIKey = Depends(get_api_key)): # process inputs TODO: should we validate further? + logger.debug(f"Received request to update backend authorization with backend_id: {backend_id} and body: {body}") backend_id = int(secure_filename(str(backend_id))) # TODO: are secure_filename and str necessary? validation? - assert backend_id is not None, "backend_id is required" # @reviewer: is this okay? + if backend_id is None: + raise HTTPException(status_code=400, detail="backend_id is required") enable_auth = bool(body.get("auth_enabled", None)) logger.debug(f"Attempting to update backend authorization to {enable_auth} for backend id: {backend_id}") @@ -109,7 +108,8 @@ async def get_backend(backend_id: int, api_key: APIKey = Depends(get_api_key)): async def delete_backend(backend_id: int, api_key: APIKey = Depends(get_api_key)): try: backend_id = int(secure_filename(str(backend_id))) - assert backend_id is not None, "backend_id is required" # @reviewer: is this okay? + if backend_id is None: + raise HTTPException(status_code=400, detail="backend_id is required") await backend_service.delete_backend(backend_id) await user_service.delete_all(backend_id) except NotFound: diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 526fcc01..44dd37c5 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -8,4 +8,4 @@ pydantic-settings factory-boy==3.3.3 # testing pytest==8.4.2 -pytest-asyncio # TODO: @reviewer: which version? +pytest-asyncio==1.3.0 From 06b9071278ab82ead78794cf4cfe9a9147e3f431 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Mon, 9 Mar 2026 16:49:24 +0000 Subject: [PATCH 036/118] added test for get_highest_suffix_number() and deleted old test files --- .../app/main/service/backend.py | 2 +- .../app/main/tests/test_service_backend.py | 33 ++++++- .../tests/test_service_backend.py | 99 ------------------- .../tests/test_views_backend.py | 63 ------------ 4 files changed, 29 insertions(+), 168 deletions(-) delete mode 100644 FastapiOpenRestyConfigurator/tests/test_service_backend.py delete mode 100644 FastapiOpenRestyConfigurator/tests/test_views_backend.py diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 70880ff5..3a132fdd 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -61,7 +61,7 @@ async def get_highest_suffix_number(location_url: str) -> int: if backend.location_url == location_url: suffix: int = int(backend.location_url.split("_")[1]) same_name_backend_suffixes.append(suffix) - if not same_name_backend_suffixes: + if len(same_name_backend_suffixes) == 0: return 100 # return highest found suffix number + 1 to iterate diff --git a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py index 35985fb1..b1eff6db 100644 --- a/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py +++ b/FastapiOpenRestyConfigurator/app/main/tests/test_service_backend.py @@ -230,9 +230,9 @@ async def test_generate_suffix_number(exception_expected, expected_suffix, user_key_url): with patch( - "app.main.service.backend.get_backends", - return_value = [BackendOut.model_construct(location_url = user_key_url), BackendOut.model_construct(location_url = "animal_100")] - ) as mock_get_backends: + "app.main.service.backend.get_highest_suffix_number", + return_value = (expected_suffix - 1) if expected_suffix else None + ) as mock_get_highest_suffix_number: try: response_suffix: int = await backend_service.generate_suffix_number(user_key_url) @@ -240,13 +240,36 @@ async def test_generate_suffix_number(exception_expected, expected_suffix, user_ if not exception_expected: assert response_suffix == expected_suffix if user_key_url is not None: - mock_get_backends.assert_awaited_once() + mock_get_highest_suffix_number.assert_awaited_once() # fail case except Exception as e: if not exception_expected: raise e +@pytest.mark.parametrize( + "expected_suffix, user_key_url, backends", + [ + (100, "unusual_123", []), + (123, "test_123", [BackendOut.model_construct(location_url = "test_123"), BackendOut.model_construct(location_url = "animal_100")]), + (250, "test_250", [BackendOut.model_construct(location_url = "test_250"), BackendOut.model_construct(location_url = "animal_100")]), + (499, "test_499", [BackendOut.model_construct(location_url = "test_499"), BackendOut.model_construct(location_url = "animal_100")]) + ] +) +@pytest.mark.asyncio +async def test_get_highest_suffix_number(expected_suffix, user_key_url, backends): + + with patch( + "app.main.service.backend.get_backends", + return_value = backends + ) as mock_get_backends: + + highest_id = await backend_service.get_highest_suffix_number(user_key_url) + mock_get_backends.assert_awaited_once() + assert highest_id == expected_suffix + + + @pytest.mark.parametrize( "exception_expected, filename, backend", test_backends_for_generate_backend_filename @@ -499,7 +522,7 @@ async def test_update_backend_authorization(exception_expected, auth_enable, get @pytest.mark.parametrize( "exception_expected, kwargs", [ - # SUCESS CASES + # SUCCESS CASES (False, {}), (False, {"id": 123, "location_url": "test_200"}), # FAIL CASES diff --git a/FastapiOpenRestyConfigurator/tests/test_service_backend.py b/FastapiOpenRestyConfigurator/tests/test_service_backend.py deleted file mode 100644 index 573520dd..00000000 --- a/FastapiOpenRestyConfigurator/tests/test_service_backend.py +++ /dev/null @@ -1,99 +0,0 @@ -import pytest -from unittest.mock import patch - -from app.main.model.serializers import BackendOut - -from app.main.service import backend as backend_service - -from werkzeug.exceptions import NotFound - -file_path_example_1 = ( - "/var/forc/backend_path/1234567890%testuser%animal_100%testtemplate%v01%0.conf" -) -file_path_example_2 = ( - "/var/forc/backend_path/9876543210%otheruser%cat_200%othertemplate%v02%1.conf" -) - - -@pytest.mark.asyncio -async def test_get_backend_by_id(backend_out_factory): - backendout1 = backend_out_factory(id=123) - backendout2 = backend_out_factory(id=456) - - - with patch( - "app.main.service.backend.get_backends", - return_value=[backendout1, backendout2], - ) as mock_get_backends: - - response: BackendOut = await backend_service.get_backend_by_id(123) - - assert response.id == 123 - mock_get_backends.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_get_backend_by_id_not_found(backend_out_factory): - backendout1 = backend_out_factory(id=123) - backendout2 = backend_out_factory(id=456) - - with pytest.raises(NotFound): - with patch( - "app.main.service.backend.get_backends", - return_value=[backendout1, backendout2], - ) as mock_get_backends: - - await backend_service.get_backend_by_id(789) - mock_get_backends.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_get_filepath_by_id(backend_out_factory): - backendout1 = backend_out_factory(id=1234567890, file_path=file_path_example_1) - backendout2 = backend_out_factory(id=9876543210, file_path=file_path_example_2) - - with patch( - "app.main.service.backend.get_backends", - return_value=[ - backendout1, - backendout2, - ], - ) as mock_get_backends: - - response: str = await backend_service.get_file_path_by_id(1234567890) - - assert response == file_path_example_1 - mock_get_backends.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_get_filepath_by_id_not_found(backend_out_factory): - backendout1 = backend_out_factory(id=1234567890) - - with pytest.raises(NotFound): - with patch( - "app.main.service.backend.get_backends", - return_value=[backendout1], - ) as mock_get_backends: - await backend_service.get_file_path_by_id(9876543210) - mock_get_backends.assert_awaited_once() - - -""" -@pytest.mark.asyncio -async def test_create_backend(): # check kwargs payload id and suffix number at the end - - with patch( - "app.main.service.backend.generate_suffix_number", - return_value = 111 - ) as mock_generate_suffix_number, patch( - "app.main.service.backend.random_with_n_digits", - return_value = 9876543210 - ) as mock_random_with_n_digits, patch( - "app.main.service.backend.generate_backend_by_template", - - -@pytest.mark.asyncio -async def test_update_backend_authorization_activate(): - -""" diff --git a/FastapiOpenRestyConfigurator/tests/test_views_backend.py b/FastapiOpenRestyConfigurator/tests/test_views_backend.py deleted file mode 100644 index 1d50fed4..00000000 --- a/FastapiOpenRestyConfigurator/tests/test_views_backend.py +++ /dev/null @@ -1,63 +0,0 @@ -import pytest -from unittest.mock import patch - -from app.main.model.serializers import BackendOut - -from app.main.views import backend as backend_views - -from fastapi import HTTPException - - -@pytest.mark.asyncio -async def test_backend_update_auth_activate(backend_out_factory): - backendOut = backend_out_factory(id=123, auth_enabled=True) - - - with patch( - "app.main.views.backend.update_backend_authorization", - return_value=backendOut, - ) as mock_update_backend_authorization: - - response: BackendOut = await backend_views.backend_update_auth( - backend_id=123, body={"auth_enabled": True}, api_key="test" - ) - - assert response.id == 123 - assert response.auth_enabled is True - mock_update_backend_authorization.assert_awaited_once_with( - backend_id=123, auth_enabled=True - ) - - -@pytest.mark.asyncio -async def test_backend_update_auth_deactivate(backend_out_factory): - backendOut = backend_out_factory(id=123, auth_enabled=False) - - with patch( - "app.main.views.backend.update_backend_authorization", - return_value=backendOut, - ) as mock_update_backend_authorization: - - response: BackendOut = await backend_views.backend_update_auth( - backend_id=123, body={"auth_enabled": False}, api_key="test" - ) - - assert response.id == 123 - assert response.auth_enabled is False - mock_update_backend_authorization.assert_awaited_once_with( - backend_id=123, auth_enabled=False - ) - - -@pytest.mark.asyncio -async def test_backend_update_auth_invalid_body(): - - with pytest.raises(HTTPException) as not_boolean_exception: - await backend_views.backend_update_auth( - backend_id=123, body={"auth_enabled": "not a boolean"}, api_key="test" - ) - assert not_boolean_exception.value.status_code == 422 - - with pytest.raises(HTTPException) as empty_body_exception: - await backend_views.backend_update_auth(backend_id=123, body={}, api_key="test") - assert empty_body_exception.value.status_code == 422 From 68fa92700a65052335126a5bf1a2352819b1344b Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Tue, 10 Mar 2026 14:30:35 +0000 Subject: [PATCH 037/118] fixed filename_regex and updated and deleted example templates --- .../app/main/service/backend.py | 2 +- examples/templates/emgb%v01.conf | 6 +- examples/templates/guacamole%v01.conf | 32 --------- examples/templates/guacamole%v02.conf | 33 ---------- examples/templates/jupyterlab%v01.conf | 37 ----------- examples/templates/jupyterlab%v02.conf | 40 ----------- examples/templates/rstudio%v01.conf | 27 -------- examples/templates/rstudio%v02.conf | 27 -------- examples/templates/rstudio%v03.conf | 29 -------- examples/templates/shiny%v01.conf | 66 +++++++++++++++++++ examples/templates/theiaide%v01.conf | 36 ---------- examples/templates/theiaide%v02.conf | 38 ----------- 12 files changed, 70 insertions(+), 303 deletions(-) delete mode 100755 examples/templates/guacamole%v01.conf delete mode 100755 examples/templates/guacamole%v02.conf delete mode 100755 examples/templates/jupyterlab%v01.conf delete mode 100755 examples/templates/jupyterlab%v02.conf delete mode 100755 examples/templates/rstudio%v01.conf delete mode 100755 examples/templates/rstudio%v02.conf delete mode 100755 examples/templates/rstudio%v03.conf create mode 100644 examples/templates/shiny%v01.conf delete mode 100755 examples/templates/theiaide%v01.conf delete mode 100755 examples/templates/theiaide%v02.conf diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 3a132fdd..ebaebe97 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -19,7 +19,7 @@ # format of filename saves information of BackendOut by this schema: # {id}%{owner}%{location_url}%{template}%{template_version}%{auth_enabled}.conf -filename_regex = r"(\d*)%([a-z0-9\-\@.]*?)%([^%]*)%([^%]*)%([^%]*)%([01])\.conf" +filename_regex = r"(\d*)%([a-z0-9\-\@.]*?)%([^%]*)%([^%]*)%([^%]*)(%([01]))?\.conf" diff --git a/examples/templates/emgb%v01.conf b/examples/templates/emgb%v01.conf index 06786a72..876c0066 100755 --- a/examples/templates/emgb%v01.conf +++ b/examples/templates/emgb%v01.conf @@ -1,5 +1,5 @@ - # PROTECT FIRST THEIA CONTAINER - location /{{ key_url }}/ { +# PROTECT FIRST THEIA CONTAINER +location /{{ key_url }}/ { set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier set $session_storage shm; # use shared memory set $session_cookie_persistent on; # persist cookie between browser sessions @@ -29,8 +29,8 @@ ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - -- Protect this location and allow only one specific ELIXIR User {% if auth_enabled %} + -- Protect this location and allow only one specific ELIXIR User if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then ngx.exit(ngx.HTTP_FORBIDDEN) end diff --git a/examples/templates/guacamole%v01.conf b/examples/templates/guacamole%v01.conf deleted file mode 100755 index 3ecaf822..00000000 --- a/examples/templates/guacamole%v01.conf +++ /dev/null @@ -1,32 +0,0 @@ -location /{{ key_url }}/ { - - - - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - - - - proxy_pass {{ location_url }}/guacamole/; - proxy_buffering off; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - access_log off; - } \ No newline at end of file diff --git a/examples/templates/guacamole%v02.conf b/examples/templates/guacamole%v02.conf deleted file mode 100755 index 4268d531..00000000 --- a/examples/templates/guacamole%v02.conf +++ /dev/null @@ -1,33 +0,0 @@ -location /{{ key_url }}/ { - - - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - - - - proxy_pass {{ location_url }}/guacamole/; - proxy_buffering off; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - access_log off; -} \ No newline at end of file diff --git a/examples/templates/jupyterlab%v01.conf b/examples/templates/jupyterlab%v01.conf deleted file mode 100755 index 43a001a7..00000000 --- a/examples/templates/jupyterlab%v01.conf +++ /dev/null @@ -1,37 +0,0 @@ -location /{{ key_url }} { - - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - proxy_pass {{ location_url }}; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; - proxy_set_header X-Scheme $scheme; - - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; - - -} diff --git a/examples/templates/jupyterlab%v02.conf b/examples/templates/jupyterlab%v02.conf deleted file mode 100755 index 9597b469..00000000 --- a/examples/templates/jupyterlab%v02.conf +++ /dev/null @@ -1,40 +0,0 @@ -location /{{ key_url }} { - - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - proxy_pass {{ location_url }}; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; - proxy_set_header X-Scheme $scheme; - - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; - - -} diff --git a/examples/templates/rstudio%v01.conf b/examples/templates/rstudio%v01.conf deleted file mode 100755 index 957c1116..00000000 --- a/examples/templates/rstudio%v01.conf +++ /dev/null @@ -1,27 +0,0 @@ -location /{{ key_url }}/ { - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - - rewrite ^/{{ key_url }}/(.*)$ /$1 break; - proxy_pass {{ location_url }}; - proxy_redirect {{ location_url }} $scheme://$http_host/rstudio/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; -} diff --git a/examples/templates/rstudio%v02.conf b/examples/templates/rstudio%v02.conf deleted file mode 100755 index 501df4b6..00000000 --- a/examples/templates/rstudio%v02.conf +++ /dev/null @@ -1,27 +0,0 @@ -location /{{ key_url }}/ { - - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - rewrite ^/{{ key_url }}/(.*)$ /$1 break; - proxy_pass {{ location_url }}; - proxy_redirect {{ location_url }} $scheme://$http_host/{{ key_url }}/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; -} diff --git a/examples/templates/rstudio%v03.conf b/examples/templates/rstudio%v03.conf deleted file mode 100755 index 348abc9e..00000000 --- a/examples/templates/rstudio%v03.conf +++ /dev/null @@ -1,29 +0,0 @@ -location /{{ key_url }}/ { - - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - rewrite ^/{{ key_url }}/(.*)$ /$1 break; - proxy_pass {{ location_url }}; - proxy_redirect {{ location_url }} $scheme://$http_host/{{ key_url }}/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_read_timeout 20d; -} diff --git a/examples/templates/shiny%v01.conf b/examples/templates/shiny%v01.conf new file mode 100644 index 00000000..eaef4c29 --- /dev/null +++ b/examples/templates/shiny%v01.conf @@ -0,0 +1,66 @@ +# PROTECT FIRST THEIA CONTAINER +location /{{ key_url }}/ { + set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier + set $session_storage shm; # use shared memory + set $session_cookie_persistent on; # persist cookie between browser sessions + set $session_cookie_renew 3500; # new cookie every hour + set $session_cookie_lifetime 86400; # lifetime for persistent cookies + set $session_name sess_auth; # name of the cookie to store the session identifier in + + set $session_shm_store sessions; # name of the dict to store sessions in + # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options + set $session_shm_uselocking off; + set $session_shm_lock_exptime 3; + set $session_shm_lock_timeout 2; + set $session_shm_lock_step 0.001; + set $session_shm_lock_ratio 1; + set $session_shm_lock_max_step 0.5; + + set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + + {% if auth_enabled %} + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% else %} + -- AUTH DISABLED, ALLOW ANY USER WITH A VALID TOKEN + {% endif %} + + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) + ngx.req.set_header("X-Auth-Email", res.id_token.email) + ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) + ngx.req.set_header("X-Auth-Name", res.id_token.name) + ngx.req.set_header("X-Auth-Subject", res.id_token.sub) + ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + } + + # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. + rewrite /{{ key_url }}/(.*) /$1 break; + proxy_pass {{ location_url }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + client_max_body_size 0; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Referrer-Policy "same-origin" always; + access_log logs/code.access.log; + error_log logs/code.error.log; +} diff --git a/examples/templates/theiaide%v01.conf b/examples/templates/theiaide%v01.conf deleted file mode 100755 index b922db06..00000000 --- a/examples/templates/theiaide%v01.conf +++ /dev/null @@ -1,36 +0,0 @@ -# PROTECT FIRST THEIA CONTAINER -location /{{ key_url }}/ { - - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - rewrite /{{ key_url }}/(.*) /$1 break; - proxy_pass {{ location_url }}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; - access_log logs/code.access.log; - error_log logs/code.error.log; -} \ No newline at end of file diff --git a/examples/templates/theiaide%v02.conf b/examples/templates/theiaide%v02.conf deleted file mode 100755 index 67ce0246..00000000 --- a/examples/templates/theiaide%v02.conf +++ /dev/null @@ -1,38 +0,0 @@ -# PROTECT FIRST THEIA CONTAINER -location /{{ key_url }}/ { - - set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; - # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) - access_by_lua_block { - local user_service = require("user_service") - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - -- Protect this location and allow only one specific ELIXIR User - if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then - ngx.exit(ngx.HTTP_FORBIDDEN) - end - } - - # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - rewrite /{{ key_url }}/(.*) /$1 break; - proxy_pass {{ location_url }}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - client_max_body_size 0; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header Referrer-Policy "same-origin" always; - access_log logs/code.access.log; - error_log logs/code.error.log; -} \ No newline at end of file From c14b5414de331e6419b3c7a24aa22bd180f0657c Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Tue, 10 Mar 2026 14:34:47 +0000 Subject: [PATCH 038/118] changed rights of shiny%v01.conf --- examples/templates/shiny%v01.conf | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/templates/shiny%v01.conf diff --git a/examples/templates/shiny%v01.conf b/examples/templates/shiny%v01.conf old mode 100644 new mode 100755 From eb3fc6118d028e775dcec248f02afe0a807280b4 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Tue, 10 Mar 2026 14:49:55 +0000 Subject: [PATCH 039/118] fixed get_backend for backwards compatibility --- FastapiOpenRestyConfigurator/app/main/service/backend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index ebaebe97..55646390 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -99,13 +99,14 @@ async def get_backends() -> List[BackendOut]: continue logger.debug("Found a backend file with wrong naming, skipping it: " + str(filename)) continue + backend: BackendOut = BackendOut( id = int(match.group(1)), owner = match.group(2), location_url = match.group(3), template = match.group(4), template_version = match.group(5), - auth_enabled = bool(int(match.group(6))), + auth_enabled = bool(int(match.group(6))) if match.group(6) else True, file_path = os.path.join(settings.FORC_BACKEND_PATH, filename) ) valid_backends.append(backend) From 6ccec2bc273a612cfd7159d4b4aeacb084a6edbe Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Tue, 10 Mar 2026 14:54:07 +0000 Subject: [PATCH 040/118] fix to fix get_backends() --- FastapiOpenRestyConfigurator/app/main/service/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 55646390..34471f0e 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -106,7 +106,7 @@ async def get_backends() -> List[BackendOut]: location_url = match.group(3), template = match.group(4), template_version = match.group(5), - auth_enabled = bool(int(match.group(6))) if match.group(6) else True, + auth_enabled = bool(int(match.group(6))) if match.group(6) is not None else True, file_path = os.path.join(settings.FORC_BACKEND_PATH, filename) ) valid_backends.append(backend) From 1e415d9be3fc07303c7bcb90eedb71619b4a2fa9 Mon Sep 17 00:00:00 2001 From: Milad Tajdar Date: Tue, 10 Mar 2026 15:09:12 +0000 Subject: [PATCH 041/118] fix 3 to get_backends() --- FastapiOpenRestyConfigurator/app/main/service/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/backend.py b/FastapiOpenRestyConfigurator/app/main/service/backend.py index 34471f0e..488d662d 100755 --- a/FastapiOpenRestyConfigurator/app/main/service/backend.py +++ b/FastapiOpenRestyConfigurator/app/main/service/backend.py @@ -106,7 +106,7 @@ async def get_backends() -> List[BackendOut]: location_url = match.group(3), template = match.group(4), template_version = match.group(5), - auth_enabled = bool(int(match.group(6))) if match.group(6) is not None else True, + auth_enabled = bool(int(match.group(6)[1])) if match.group(6) is not None else True, file_path = os.path.join(settings.FORC_BACKEND_PATH, filename) ) valid_backends.append(backend) From 27862681131aa6daae61589ffc2533a718463be0 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 11 Mar 2026 10:23:18 +0100 Subject: [PATCH 042/118] Rename shiny%v01.conf to webservice%v01.conf --- examples/templates/{shiny%v01.conf => webservice%v01.conf} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/templates/{shiny%v01.conf => webservice%v01.conf} (100%) diff --git a/examples/templates/shiny%v01.conf b/examples/templates/webservice%v01.conf similarity index 100% rename from examples/templates/shiny%v01.conf rename to examples/templates/webservice%v01.conf From 911233b612af22e4d3c2b28ef9efb560a3274976 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 12 Mar 2026 07:45:00 +0100 Subject: [PATCH 043/118] Create guacamole_vnc%v03.conf --- examples/templates/guacamole_vnc%v03.conf | 56 +++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 examples/templates/guacamole_vnc%v03.conf diff --git a/examples/templates/guacamole_vnc%v03.conf b/examples/templates/guacamole_vnc%v03.conf new file mode 100644 index 00000000..c794c595 --- /dev/null +++ b/examples/templates/guacamole_vnc%v03.conf @@ -0,0 +1,56 @@ +location /{{ key_url }}/ { + set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier + set $session_storage shm; # use shared memory + set $session_cookie_persistent on; # persist cookie between browser sessions + set $session_cookie_renew 3500; # new cookie every hour + set $session_cookie_lifetime 86400; # lifetime for persistent cookies + set $session_name sess_auth; # name of the cookie to store the session identifier in + + set $session_shm_store sessions; # name of the dict to store sessions in + # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options + set $session_shm_uselocking off; + set $session_shm_lock_exptime 3; + set $session_shm_lock_timeout 2; + set $session_shm_lock_step 0.001; + set $session_shm_lock_ratio 1; + set $session_shm_lock_max_step 0.5; + + set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; + # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) + access_by_lua_block { + local user_service = require("user_service") + -- Start actual openid authentication procedure + local res, err = require("resty.openidc").authenticate(opts2) + -- If it fails for some reason, escape via HTTP 500 + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + + -- Protect this location and allow only one specific ELIXIR User + if (res.id_token.sub ~= "{{ owner }}" and not user_service.file_exists(ngx.var.user_path .. res.id_token.sub)) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) + ngx.req.set_header("X-Auth-Email", res.id_token.email) + ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) + ngx.req.set_header("X-Auth-Name", res.id_token.name) + ngx.req.set_header("X-Auth-Subject", res.id_token.sub) + ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + } + + + + + proxy_pass {{ location_url }}/guacamole/; + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + access_log off; + } From 94e87d9c534c8ebb84e4709763af62f3a4a1c553 Mon Sep 17 00:00:00 2001 From: vktrrdk Date: Mon, 1 Jun 2026 13:02:30 +0000 Subject: [PATCH 044/118] fix(Renovate): Adjust config to label security fixes as such in PR --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renovate.json b/renovate.json index 604980b7..4ac50b4a 100755 --- a/renovate.json +++ b/renovate.json @@ -20,6 +20,11 @@ "addLabels": ["pin"] } ], + "vulnerabilityAlerts": { + "labels": [ + "security" + ] + }, "baseBranches": ["dev"], "reviewers": ["team:portal-dev"], "labels": ["dependencies"], From 3338963a3e85e590738a1a0989c20b717881525d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 06:50:52 +0000 Subject: [PATCH 045/118] feat(Dependencies): Update dependency fastapi to v0.138.0 | datasource | package | from | to | | ---------- | ------- | ------- | ------- | | pypi | fastapi | 0.136.1 | 0.138.0 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index d1ade30b..8f3ca855 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.136.1 +fastapi==0.138.0 uvicorn==0.47.0 werkzeug==3.1.8 Jinja2==3.1.6 From 512d13d22b877eeec62ac3a29cafaecae53ca71a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 07:32:30 +0000 Subject: [PATCH 046/118] feat(Dependencies): Update python Docker tag to v3.14.6 | datasource | package | from | to | | ---------- | ------- | ------ | ------ | | docker | python | 3.14.5 | 3.14.6 | --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index a6d9ada0..3f0a10fd 100755 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14.5 +3.14.6 From 32ac3c7cbbd92385fa6198fb3e7e83292bb7ca65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 07:32:34 +0000 Subject: [PATCH 047/118] feat(Dependencies): Update dependency pytest-asyncio to v1.4.0 | datasource | package | from | to | | ---------- | -------------- | ----- | ----- | | pypi | pytest-asyncio | 1.3.0 | 1.4.0 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 8f3ca855..6d180a02 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -8,4 +8,4 @@ pydantic-settings factory-boy==3.3.3 # testing pytest==8.4.2 -pytest-asyncio==1.3.0 +pytest-asyncio==1.4.0 From f12ad4337d273a9eb7e69a94861e1c42553c0fa6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 07:32:37 +0000 Subject: [PATCH 048/118] feat(Dependencies): Update dependency uvicorn to v0.49.0 | datasource | package | from | to | | ---------- | ------- | ------ | ------ | | pypi | uvicorn | 0.47.0 | 0.49.0 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 8f3ca855..306635f7 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -1,5 +1,5 @@ fastapi==0.138.0 -uvicorn==0.47.0 +uvicorn==0.49.0 werkzeug==3.1.8 Jinja2==3.1.6 python-dotenv==1.2.2 From 3b884ba6e83b9eba3e34c4c21402c830c1ba29c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 07:32:41 +0000 Subject: [PATCH 049/118] feat(Dependencies): Update actions/checkout action to v7 | datasource | package | from | to | | ----------- | ---------------- | ------ | ------ | | github-tags | actions/checkout | v6.0.3 | v7.0.0 | --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index feabfe5a..2970b397 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL From 0592ae532aeeed7c65b80c54d4bc62eef5c2ec7d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 07:10:46 +0000 Subject: [PATCH 050/118] feat(Dependencies): Update dependency pytest to v9 [SECURITY] | datasource | package | from | to | | ---------- | ------- | ----- | ----- | | pypi | pytest | 8.4.2 | 9.0.3 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 139857e1..5aefc24a 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -7,5 +7,5 @@ gunicorn==26.0.0 pydantic-settings factory-boy==3.3.3 # testing -pytest==8.4.2 +pytest==9.0.3 pytest-asyncio==1.4.0 From a79da74b2e1749830765af81b5bee7818ab7b33d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:43:13 +0000 Subject: [PATCH 051/118] feat(Dependencies): Update dependency fastapi to v0.139.0 | datasource | package | from | to | | ---------- | ------- | ------- | ------- | | pypi | fastapi | 0.138.0 | 0.139.0 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 5aefc24a..9431eb61 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.138.0 +fastapi==0.139.0 uvicorn==0.49.0 werkzeug==3.1.8 Jinja2==3.1.6 From c2fc3bb25cf26c9e8ea396403f68ab3b8cdf63a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:25:50 +0000 Subject: [PATCH 052/118] feat(Dependencies): Pin dependencies --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2970b397..ac261cf7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 with: languages: ${{ matrix.language }} queries: +security-extended, security-and-quality @@ -51,7 +51,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v4 + uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -65,4 +65,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 From 54f1f63e9b4509c2fd69f9939ca6227e8496d700 Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:24:58 +0200 Subject: [PATCH 053/118] Remove rewrite from webservice (trailing path) (#589) --- examples/templates/webservice%v01.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/templates/webservice%v01.conf b/examples/templates/webservice%v01.conf index eaef4c29..acdc2007 100755 --- a/examples/templates/webservice%v01.conf +++ b/examples/templates/webservice%v01.conf @@ -49,7 +49,6 @@ location /{{ key_url }}/ { } # After check via lua-oidc is done, start reverse proxying this backend by configuring a billion headers. - rewrite /{{ key_url }}/(.*) /$1 break; proxy_pass {{ location_url }}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; From 3d14b409e1d2e796815cb56118724d73ee4c97e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:28:29 +0000 Subject: [PATCH 054/118] feat(Dependencies): Update github/codeql-action digest to 99df26d --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ac261cf7..4554ec9b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 with: languages: ${{ matrix.language }} queries: +security-extended, security-and-quality @@ -51,7 +51,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 + uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -65,4 +65,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 From a9cbba34ad72fc1f1d878c11b70a20cf8b4b1baa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:50:08 +0000 Subject: [PATCH 055/118] feat(Dependencies): Update dependency fastapi to v0.139.1 | datasource | package | from | to | | ---------- | ------- | ------- | ------- | | pypi | fastapi | 0.139.0 | 0.139.1 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 9431eb61..bc233eb0 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.139.0 +fastapi==0.139.1 uvicorn==0.49.0 werkzeug==3.1.8 Jinja2==3.1.6 From b042f843e67adb5c4b99ce26009fc098a01e6c13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:33:38 +0000 Subject: [PATCH 056/118] feat(Dependencies): Update dependency fastapi to v0.139.2 | datasource | package | from | to | | ---------- | ------- | ------- | ------- | | pypi | fastapi | 0.139.1 | 0.139.2 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index bc233eb0..1bfa4ef7 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.139.1 +fastapi==0.139.2 uvicorn==0.49.0 werkzeug==3.1.8 Jinja2==3.1.6 From 40cc954f3d809697cc0db4f70711ed9017174eb2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:49:51 +0000 Subject: [PATCH 057/118] feat(Dependencies): Update dependency pytest to v9.1.1 | datasource | package | from | to | | ---------- | ------- | ----- | ----- | | pypi | pytest | 9.0.3 | 9.1.1 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 1bfa4ef7..7d1e71c4 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -7,5 +7,5 @@ gunicorn==26.0.0 pydantic-settings factory-boy==3.3.3 # testing -pytest==9.0.3 +pytest==9.1.1 pytest-asyncio==1.4.0 From 24baf738b6b4b785c24b7d7705e4db4a19df1b66 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:49:55 +0000 Subject: [PATCH 058/118] feat(Dependencies): Update dependency uvicorn to v0.51.0 | datasource | package | from | to | | ---------- | ------- | ------ | ------ | | pypi | uvicorn | 0.49.0 | 0.51.0 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 1bfa4ef7..c555a7ab 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -1,5 +1,5 @@ fastapi==0.139.2 -uvicorn==0.49.0 +uvicorn==0.51.0 werkzeug==3.1.8 Jinja2==3.1.6 python-dotenv==1.2.2 From f76ef0d228d6d925654790450cfea54266d2c204 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:17:34 +0000 Subject: [PATCH 059/118] started reiplenting for docker --- docker/Dockerfile | 52 +++++++--- docker/gunicorn_conf.py | 9 ++ docker/launch.sh | 4 +- docker/nginx.conf | 203 ++++++++++++++++++++++++++++++++++++++++ docker/uwsgi.ini | 11 --- 5 files changed, 253 insertions(+), 26 deletions(-) create mode 100644 docker/gunicorn_conf.py create mode 100644 docker/nginx.conf delete mode 100644 docker/uwsgi.ini diff --git a/docker/Dockerfile b/docker/Dockerfile index 2fbcd102..ef20e8cf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,26 +1,52 @@ FROM ubuntu:24.04 -MAINTAINER awalende - ENV FORC_BACKEND_PATH=/var/forc/backend_path/ ENV FORC_TEMPLATE_PATH=/var/forc/template_path/ -RUN mkdir -p /var/forc/backend_path/; mkdir -p /var/forc/template_path/ +RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} + +RUN apt-get update && apt-get install -y \ + git \ + wget \ + gnupg \ + ca-certificates \ + software-properties-common \ + python3 \ + python3-pip \ + python3-venv \ + lsb-release \ + && rm -rf /var/lib/apt/lists/* -RUN apt-get update; apt-get install -y sudo git wget gnupg ca-certificates software-properties-common python3 python3-pip python-setuptools; pip3 install uwsgi -RUN wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add - -RUN add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" -RUN apt-get update -RUN apt-get -y install openresty +# Install OpenResty +RUN wget -qO- https://openresty.org/package/pubkey.gpg \ + | gpg --dearmor \ + -o /usr/share/keyrings/openresty.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" \ + > /etc/apt/sources.list.d/openresty.list \ + && apt-get update \ + && apt-get install -y openresty \ + && rm -rf /var/lib/apt/lists/* RUN opm install zmartzone/lua-resty-openidc + RUN mkdir -p /opt/simpleVMWebGateway -RUN git clone https://github.com/deNBI/simpleVMWebGateway.git /opt/simpleVMWebGateway/ -RUN pip3 install -r /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/requirements.txt + +RUN git clone \ + https://github.com/deNBI/simpleVMWebGateway.git \ + /opt/simpleVMWebGateway/ WORKDIR /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator -COPY uwsgi.ini uwsgi.ini +RUN python3 -m venv /opt/venv \ + && /opt/venv/bin/pip install --upgrade pip \ + && /opt/venv/bin/pip install -r requirements.txt \ + && /opt/venv/bin/pip install gunicorn uvicorn + +ENV PATH="/opt/venv/bin:$PATH" + +COPY nginx.conf /etc/openresty/nginx.conf +COPY gunicorn_conf.py gunicorn_conf.py COPY launch.sh launch.sh -RUN chmod +x launch.sh -CMD ./launch.sh +RUN chmod +x launch.sh +EXPOSE 5000 +CMD ["./launch.sh"] \ No newline at end of file diff --git a/docker/gunicorn_conf.py b/docker/gunicorn_conf.py new file mode 100644 index 00000000..778e7ebc --- /dev/null +++ b/docker/gunicorn_conf.py @@ -0,0 +1,9 @@ +bind = "0.0.0.0:5000" +# Worker Options +workers = 5 +worker_class = "uvicorn.workers.UvicornWorker" + +# Logging Options +loglevel = "info" +accesslog = "/var/log/forc.access.log" +errorlog = "/var/log/forc.error.log" \ No newline at end of file diff --git a/docker/launch.sh b/docker/launch.sh index 07c87c51..f8f8a8a1 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -1,4 +1,4 @@ #!/bin/bash -/usr/local/openresty/nginx/sbin/nginx -g 'daemon on; master_process on;' -uwsgi --ini uwsgi.ini \ No newline at end of file +#/usr/local/openresty/nginx/sbin/nginx -g 'daemon on; master_process on;' +gunicorn -c gunicorn_conf.py main:app \ No newline at end of file diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 00000000..1cebcfd1 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,203 @@ +worker_processes {{ OPENRESTY_WORKER_PROCESSES }}; + +# /usr/local/openresty/nginx/logs/ +error_log logs/error.log; +error_log logs/error.log notice; +error_log logs/error.log info; +error_log logs/error.log debug; + +events { + worker_connections 1024; +} + +http { + include mime.types; + lua_package_path "{{ FORC_BACKEND_PATH }}/scripts/?.lua;;"; + default_type application/octet-stream; + # Hardcoded resolver + resolver {{ OPENRESTY_DNS_SERVER }}; + sendfile on; + keepalive_timeout 65; + + # LUA shared dicts + lua_shared_dict discovery 1m; + lua_shared_dict jwks 1m; + lua_shared_dict sessions 10m; + lua_code_cache off; + + client_max_body_size 100M; + + # Websocket support + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + # OIDC config + init_by_lua_block { + opts2 = { + redirect_uri = "https://{{ DOMAIN }}/redirect_uri", + discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", + client_id = "{{ FORC_OIDC_CLIENT_ID }}", + client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", + logout_path = "/logout", + ssl_verify = "no", + iat_slack = 600, + scope = "openid email profile offline_access", + renew_access_token_on_expiry = true, + access_token_expires_leeway = 60, + session_contents = {id_token=true, access_token=true} + } + } + + # ------------------------------- + # BLOCKED IP CONFIGURATION + # ------------------------------- + geo $blocked_ip { + default 0; + include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP + } + + # ------------------------------- + # SERVER BLOCKS + # ------------------------------- + + {% if FORC_SERVICE_USE_HTTPS %} + server { + listen {{ FORC_SERVICE_PORT }} ssl http2; + ssl_certificate /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers on; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy 'strict-origin'; + ssl_stapling on; + ssl_stapling_verify on; + + # Block IPs + if ($blocked_ip) { + return 444; + } + + location / { + proxy_pass http://unix:/var/run/forc.sock; + } + } + {% else %} + server { + listen {{ FORC_LOCAL_IP }}:{{ FORC_SERVICE_PORT }}; + + # Block IPs + if ($blocked_ip) { + return 444; + } + + location / { + proxy_pass http://unix:/var/run/forc.sock; + } + } + {% endif %} + + # HTTP redirect to HTTPS + server { + listen 0.0.0.0:80 default_server; + server_name {{ DOMAIN }}; + + # Block IPs + if ($blocked_ip) { + return 444; + } + + return 301 https://$host$request_uri; + } + + # Local nginx status + server { + listen 127.0.0.1:8080; + + location /nginx_status { + stub_status; + allow 127.0.0.1; + deny all; + } + } + + # Main public HTTPS server with Lua OIDC + server { + listen 0.0.0.0:443 ssl http2; + server_name {{ DOMAIN }}; + + ssl_certificate /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers on; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy 'strict-origin'; + ssl_stapling on; + ssl_stapling_verify on; + client_max_body_size 100M; + + # Block IPs + if ($blocked_ip) { + return 444; + } + + # Session config + set $session_secret {{ FORC_SECRET_KEY }}; + set $session_cipher none; + set $session_storage shm; + set $session_cookie_persistent on; + set $session_cookie_renew 3500; + set $session_cookie_lifetime 86400; + set $session_name sess_auth; + set $session_shm_store sessions; + set $session_shm_uselocking off; + set $session_shm_lock_exptime 3; + set $session_shm_lock_timeout 2; + set $session_shm_lock_step 0.001; + set $session_shm_lock_ratio 1; + set $session_shm_lock_max_step 0.5; + + location / { + access_by_lua_block { + local res, err = require("resty.openidc").authenticate(opts2) + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + + ngx.req.set_header("X-Auth-Audience", res.id_token.aud) + ngx.req.set_header("X-Auth-Email", res.id_token.email) + ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) + ngx.req.set_header("X-Auth-Name", res.id_token.name) + ngx.req.set_header("X-Auth-Subject", res.id_token.sub) + ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) + ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + } + } + + # Dynamically included locations + include {{ FORC_BACKEND_PATH }}/*.conf; + + error_page 500 502 503 504 /50x.html; + location = /50x.html { root html; } + + error_page 404 /404.html; + location = /404.html { root html; } + + error_page 403 /403.html; + location = /403.html { root html; } + } +} \ No newline at end of file diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini deleted file mode 100644 index 5eaaae2e..00000000 --- a/docker/uwsgi.ini +++ /dev/null @@ -1,11 +0,0 @@ -[uwsgi] -chdir = /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator -module = manage:app -master = true -processes = 5 -logto = /var/log/forc.log - -socket = /var/run/forc.sock -vacuum = true - -chmod-socket = 776 \ No newline at end of file From 76337e668b6078dc660b16731caeea8d11ae9256 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:30:01 +0000 Subject: [PATCH 060/118] =?UTF-8?q?added=20test=20compose=C2=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/docker-compose.yml | 12 ++++++++++++ docker/gunicorn_conf.py | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 docker/docker-compose.yml diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..565a653b --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,12 @@ +services: + forc: + image: forc + env_file: + - ../FastapiOpenRestyConfigurator/.env + volumes: + - /var/forc/backend_path/:/var/forc/backend_path/:rw + - /var/forc/template_path/:/var/forc/template_path/:rw + ports: + - 0.0.0.0:5000:5000 + - 0.0.0.0:80:80 + - 0.0.0.0:443:443 \ No newline at end of file diff --git a/docker/gunicorn_conf.py b/docker/gunicorn_conf.py index 778e7ebc..d3898e79 100644 --- a/docker/gunicorn_conf.py +++ b/docker/gunicorn_conf.py @@ -1,4 +1,6 @@ -bind = "0.0.0.0:5000" +# Socket Path +bind = "unix:/var/run/forc.sock" + # Worker Options workers = 5 worker_class = "uvicorn.workers.UvicornWorker" From bf3c51cbdd16cf9055c6c4d119122dfc9f45d22c Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:43:58 +0000 Subject: [PATCH 061/118] render nginx config with jinja2 --- docker/Dockerfile | 3 ++- docker/launch.sh | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ef20e8cf..69068c36 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,7 +43,8 @@ RUN python3 -m venv /opt/venv \ ENV PATH="/opt/venv/bin:$PATH" -COPY nginx.conf /etc/openresty/nginx.conf +COPY nginx.conf /etc/openresty/nginx.conf.j2 +COPY render_nginx.py /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ COPY gunicorn_conf.py gunicorn_conf.py COPY launch.sh launch.sh diff --git a/docker/launch.sh b/docker/launch.sh index f8f8a8a1..8259443f 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -1,4 +1,14 @@ #!/bin/bash +set -e -#/usr/local/openresty/nginx/sbin/nginx -g 'daemon on; master_process on;' -gunicorn -c gunicorn_conf.py main:app \ No newline at end of file +echo "Rendering OpenResty configuration..." + +python3 /usr/local/bin/render_nginx.py + +echo "Starting OpenResty..." +openresty + +echo "Starting FastAPI..." +exec gunicorn \ + -c gunicorn_conf.py \ + main:app \ No newline at end of file From e6bc790d6ca2abb830eb7b99305f7f0d9156906d Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:46:38 +0000 Subject: [PATCH 062/118] added container name --- docker/docker-compose.yml | 3 ++- docker/render_nginx.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 docker/render_nginx.py diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 565a653b..c750b9fe 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,7 @@ services: forc: image: forc + container_name: forc env_file: - ../FastapiOpenRestyConfigurator/.env volumes: @@ -9,4 +10,4 @@ services: ports: - 0.0.0.0:5000:5000 - 0.0.0.0:80:80 - - 0.0.0.0:443:443 \ No newline at end of file + - 0.0.0.0:443:443 diff --git a/docker/render_nginx.py b/docker/render_nginx.py new file mode 100644 index 00000000..f1eec1f3 --- /dev/null +++ b/docker/render_nginx.py @@ -0,0 +1,23 @@ +import os +from pathlib import Path +from jinja2 import Environment, FileSystemLoader + +TEMPLATE_DIR = "/opt/simpleVMWebGateway/FastapiOpenRestyConfigurator" +OUTPUT_FILE = "/etc/openresty/nginx.conf" + + +def main(): + env = Environment( + loader=FileSystemLoader(TEMPLATE_DIR), + autoescape=False, + ) + + template = env.get_template("nginx.conf.j2") + + rendered = template.render(**os.environ) + + Path(OUTPUT_FILE).write_text(rendered) + + +if __name__ == "__main__": + main() From 78866f87f4123bdb6edd3b08a8b1be98bc862086 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:47:39 +0000 Subject: [PATCH 063/118] updated script --- docker/launch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/launch.sh b/docker/launch.sh index 8259443f..7a76ff90 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -3,7 +3,7 @@ set -e echo "Rendering OpenResty configuration..." -python3 /usr/local/bin/render_nginx.py +python3 /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/render_nginx.py echo "Starting OpenResty..." openresty From 3b2cc27550db7856a577f283c1dfd1103abddd92 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:48:54 +0000 Subject: [PATCH 064/118] updated script --- docker/render_nginx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/render_nginx.py b/docker/render_nginx.py index f1eec1f3..2527dc88 100644 --- a/docker/render_nginx.py +++ b/docker/render_nginx.py @@ -2,7 +2,7 @@ from pathlib import Path from jinja2 import Environment, FileSystemLoader -TEMPLATE_DIR = "/opt/simpleVMWebGateway/FastapiOpenRestyConfigurator" +TEMPLATE_DIR = "/etc/openresty/" OUTPUT_FILE = "/etc/openresty/nginx.conf" From 0e3601d5c55f9b5a55cfa741d3b7be094d6d123e Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:50:04 +0000 Subject: [PATCH 065/118] uopdated nginx conf --- docker/nginx.conf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 1cebcfd1..18fc9842 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -51,12 +51,12 @@ http { } # ------------------------------- - # BLOCKED IP CONFIGURATION + # BLOCKED IP CONFIGURATION TODO READD # ------------------------------- - geo $blocked_ip { - default 0; - include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP - } + # geo $blocked_ip { + # default 0; + # include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP + #} # ------------------------------- # SERVER BLOCKS From 664e8d3a6ff91c69931a8b40c0a65422f4ce5c2b Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:55:07 +0000 Subject: [PATCH 066/118] uopdated nginx conf --- docker/nginx.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 18fc9842..4d312734 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -80,9 +80,9 @@ http { ssl_stapling_verify on; # Block IPs - if ($blocked_ip) { - return 444; - } + # if ($blocked_ip) { + # return 444; + # } location / { proxy_pass http://unix:/var/run/forc.sock; From 17f9a3c6b225bbac0b5fa4307aa34b9d61d26c9d Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 11:55:58 +0000 Subject: [PATCH 067/118] uopdated nginx conf --- docker/nginx.conf | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 4d312734..61794f89 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -93,9 +93,9 @@ http { listen {{ FORC_LOCAL_IP }}:{{ FORC_SERVICE_PORT }}; # Block IPs - if ($blocked_ip) { - return 444; - } + #if ($blocked_ip) { + # return 444; + #} location / { proxy_pass http://unix:/var/run/forc.sock; @@ -109,9 +109,9 @@ http { server_name {{ DOMAIN }}; # Block IPs - if ($blocked_ip) { - return 444; - } + #if ($blocked_ip) { + # return 444; + #} return 301 https://$host$request_uri; } @@ -148,9 +148,9 @@ http { client_max_body_size 100M; # Block IPs - if ($blocked_ip) { - return 444; - } + # if ($blocked_ip) { + # return 444; + # } # Session config set $session_secret {{ FORC_SECRET_KEY }}; From 6416477cc891fed1b239881a6911dfb55c5c20e4 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 12:57:26 +0000 Subject: [PATCH 068/118] updated dockerifel adn contaienr --- docker/Dockerfile | 2 +- docker/nginx.conf | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 69068c36..48b57158 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,7 +27,7 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ && apt-get install -y openresty \ && rm -rf /var/lib/apt/lists/* RUN opm install zmartzone/lua-resty-openidc - +RUN opm get bungle/lua-resty-session 3.10 RUN mkdir -p /opt/simpleVMWebGateway RUN git clone \ diff --git a/docker/nginx.conf b/docker/nginx.conf index 61794f89..b4d0edcf 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -23,7 +23,7 @@ http { lua_shared_dict discovery 1m; lua_shared_dict jwks 1m; lua_shared_dict sessions 10m; - lua_code_cache off; + lua_code_cache on; client_max_body_size 100M; @@ -159,6 +159,11 @@ http { set $session_cookie_persistent on; set $session_cookie_renew 3500; set $session_cookie_lifetime 86400; + set $session_cookie_secure on; + set $session_cookie_samesite Lax; + set $session_cookie_domain {{ DOMAIN }}; + set $session_cookie_path /; + set $session_name sess_auth; set $session_shm_store sessions; set $session_shm_uselocking off; From 6c51ce37c41802d53730064144bedeffbb0cb3bf Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 13:08:03 +0000 Subject: [PATCH 069/118] updated nginx conf --- docker/nginx.conf | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index b4d0edcf..f8e41747 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -26,6 +26,26 @@ http { lua_code_cache on; client_max_body_size 100M; + # Session config + set $session_secret {{ FORC_SECRET_KEY }}; + set $session_cipher none; + set $session_storage shm; + set $session_cookie_persistent on; + set $session_cookie_renew 3500; + set $session_cookie_lifetime 86400; + set $session_cookie_secure on; + set $session_cookie_samesite Lax; + set $session_cookie_domain {{ DOMAIN }}; + set $session_cookie_path /; + + set $session_name sess_auth; + set $session_shm_store sessions; + set $session_shm_uselocking off; + set $session_shm_lock_exptime 3; + set $session_shm_lock_timeout 2; + set $session_shm_lock_step 0.001; + set $session_shm_lock_ratio 1; + set $session_shm_lock_max_step 0.5; # Websocket support map $http_upgrade $connection_upgrade { @@ -152,27 +172,7 @@ http { # return 444; # } - # Session config - set $session_secret {{ FORC_SECRET_KEY }}; - set $session_cipher none; - set $session_storage shm; - set $session_cookie_persistent on; - set $session_cookie_renew 3500; - set $session_cookie_lifetime 86400; - set $session_cookie_secure on; - set $session_cookie_samesite Lax; - set $session_cookie_domain {{ DOMAIN }}; - set $session_cookie_path /; - - set $session_name sess_auth; - set $session_shm_store sessions; - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; - + location / { access_by_lua_block { local res, err = require("resty.openidc").authenticate(opts2) From e33ea1b39ef0183a50fed0c7e77a6b9aae5902f9 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 13:10:53 +0000 Subject: [PATCH 070/118] rest dockerfile --- docker/nginx.conf | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index f8e41747..b4d0edcf 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -26,26 +26,6 @@ http { lua_code_cache on; client_max_body_size 100M; - # Session config - set $session_secret {{ FORC_SECRET_KEY }}; - set $session_cipher none; - set $session_storage shm; - set $session_cookie_persistent on; - set $session_cookie_renew 3500; - set $session_cookie_lifetime 86400; - set $session_cookie_secure on; - set $session_cookie_samesite Lax; - set $session_cookie_domain {{ DOMAIN }}; - set $session_cookie_path /; - - set $session_name sess_auth; - set $session_shm_store sessions; - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; # Websocket support map $http_upgrade $connection_upgrade { @@ -172,7 +152,27 @@ http { # return 444; # } - + # Session config + set $session_secret {{ FORC_SECRET_KEY }}; + set $session_cipher none; + set $session_storage shm; + set $session_cookie_persistent on; + set $session_cookie_renew 3500; + set $session_cookie_lifetime 86400; + set $session_cookie_secure on; + set $session_cookie_samesite Lax; + set $session_cookie_domain {{ DOMAIN }}; + set $session_cookie_path /; + + set $session_name sess_auth; + set $session_shm_store sessions; + set $session_shm_uselocking off; + set $session_shm_lock_exptime 3; + set $session_shm_lock_timeout 2; + set $session_shm_lock_step 0.001; + set $session_shm_lock_ratio 1; + set $session_shm_lock_max_step 0.5; + location / { access_by_lua_block { local res, err = require("resty.openidc").authenticate(opts2) From 1fc4d7b78b96834690fbc4d91dfe129fe1c8d894 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 13:38:40 +0000 Subject: [PATCH 071/118] Updated nginc --- docker/Dockerfile | 2 +- docker/nginx.conf | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 48b57158..cbc4464d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,7 +27,7 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ && apt-get install -y openresty \ && rm -rf /var/lib/apt/lists/* RUN opm install zmartzone/lua-resty-openidc -RUN opm get bungle/lua-resty-session 3.10 +RUN opm get bungle/lua-resty-session RUN mkdir -p /opt/simpleVMWebGateway RUN git clone \ diff --git a/docker/nginx.conf b/docker/nginx.conf index b4d0edcf..647fe50c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -23,7 +23,7 @@ http { lua_shared_dict discovery 1m; lua_shared_dict jwks 1m; lua_shared_dict sessions 10m; - lua_code_cache on; + lua_code_cache off; client_max_body_size 100M; @@ -150,7 +150,7 @@ http { # Block IPs # if ($blocked_ip) { # return 444; - # } + } # Session config set $session_secret {{ FORC_SECRET_KEY }}; @@ -159,11 +159,6 @@ http { set $session_cookie_persistent on; set $session_cookie_renew 3500; set $session_cookie_lifetime 86400; - set $session_cookie_secure on; - set $session_cookie_samesite Lax; - set $session_cookie_domain {{ DOMAIN }}; - set $session_cookie_path /; - set $session_name sess_auth; set $session_shm_store sessions; set $session_shm_uselocking off; From 8b07dd412e1193610f766344814606a2e255a3f9 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 13:45:16 +0000 Subject: [PATCH 072/118] Updated nginc --- docker/nginx.conf | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 647fe50c..8ae41c07 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -51,12 +51,12 @@ http { } # ------------------------------- - # BLOCKED IP CONFIGURATION TODO READD + # BLOCKED IP CONFIGURATION # ------------------------------- - # geo $blocked_ip { + # geo $blocked_ip { # default 0; - # include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP - #} + # include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP + # } # ------------------------------- # SERVER BLOCKS @@ -80,9 +80,9 @@ http { ssl_stapling_verify on; # Block IPs - # if ($blocked_ip) { - # return 444; - # } + if ($blocked_ip) { + return 444; + } location / { proxy_pass http://unix:/var/run/forc.sock; @@ -93,9 +93,9 @@ http { listen {{ FORC_LOCAL_IP }}:{{ FORC_SERVICE_PORT }}; # Block IPs - #if ($blocked_ip) { - # return 444; - #} + # if ($blocked_ip) { + # return 444; + # } location / { proxy_pass http://unix:/var/run/forc.sock; @@ -109,9 +109,9 @@ http { server_name {{ DOMAIN }}; # Block IPs - #if ($blocked_ip) { - # return 444; - #} + # if ($blocked_ip) { + # return 444; + # } return 301 https://$host$request_uri; } @@ -148,9 +148,9 @@ http { client_max_body_size 100M; # Block IPs - # if ($blocked_ip) { - # return 444; - } + #if ($blocked_ip) { + # return 444; + #} # Session config set $session_secret {{ FORC_SECRET_KEY }}; From a854f8c9efa4893f95aec1fe87db732f1d093a41 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 13:46:02 +0000 Subject: [PATCH 073/118] Updated nginc --- docker/nginx.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 8ae41c07..c64febb4 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -80,9 +80,9 @@ http { ssl_stapling_verify on; # Block IPs - if ($blocked_ip) { - return 444; - } + # if ($blocked_ip) { + # return 444; + #} location / { proxy_pass http://unix:/var/run/forc.sock; From 1ee3d127b8ec74b6b5550f6f69abbb21bfaec01f Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 13:58:08 +0000 Subject: [PATCH 074/118] Updated nginc --- docker/Dockerfile | 11 +++++++-- docker/nginx.conf | 58 ++++++++++++++++++++++++----------------------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index cbc4464d..d8e5ad3a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,8 +26,15 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ && apt-get update \ && apt-get install -y openresty \ && rm -rf /var/lib/apt/lists/* -RUN opm install zmartzone/lua-resty-openidc -RUN opm get bungle/lua-resty-session + +# Install OIDC + session +RUN git clone --depth 1 --branch v1.9.0 \ + https://github.com/zmartzone/lua-resty-openidc.git /tmp/lua-resty-openidc \ + && cp /tmp/lua-resty-openidc/lib/resty/openidc.lua \ + /usr/local/openresty/site/lualib/resty/openidc.lua \ + && rm -rf /tmp/lua-resty-openidc + +RUN opm install bungle/lua-resty-session RUN mkdir -p /opt/simpleVMWebGateway RUN git clone \ diff --git a/docker/nginx.conf b/docker/nginx.conf index c64febb4..da2d3531 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -34,21 +34,38 @@ http { } # OIDC config - init_by_lua_block { - opts2 = { - redirect_uri = "https://{{ DOMAIN }}/redirect_uri", - discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", - client_id = "{{ FORC_OIDC_CLIENT_ID }}", - client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", - logout_path = "/logout", - ssl_verify = "no", - iat_slack = 600, - scope = "openid email profile offline_access", - renew_access_token_on_expiry = true, - access_token_expires_leeway = 60, - session_contents = {id_token=true, access_token=true} +init_by_lua_block { + opts2 = { + redirect_uri = "https://{{ DOMAIN }}/redirect_uri", + discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", + client_id = "{{ FORC_OIDC_CLIENT_ID }}", + client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", + + logout_path = "/logout", + + ssl_verify = "no", + iat_slack = 600, + + scope = "openid email profile offline_access", + + renew_access_token_on_expiry = true, + access_token_expires_leeway = 60, + + session_secret = "{{ FORC_SECRET_KEY }}", + session_name = "sess_auth", + session_storage = "shm", + session_shm_store = "sessions", + + session_cookie_persistent = true, + session_cookie_lifetime = 86400, + session_cookie_renew = 3500, + + session_contents = { + id_token = true, + access_token = true } } +} # ------------------------------- # BLOCKED IP CONFIGURATION @@ -152,21 +169,6 @@ http { # return 444; #} - # Session config - set $session_secret {{ FORC_SECRET_KEY }}; - set $session_cipher none; - set $session_storage shm; - set $session_cookie_persistent on; - set $session_cookie_renew 3500; - set $session_cookie_lifetime 86400; - set $session_name sess_auth; - set $session_shm_store sessions; - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; location / { access_by_lua_block { From 6aab4c7d59b6085dbbf276bd83d178fea1063a36 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 14:10:04 +0000 Subject: [PATCH 075/118] Updated nginc --- docker/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d8e5ad3a..1d8469da 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,15 +27,15 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ && apt-get install -y openresty \ && rm -rf /var/lib/apt/lists/* -# Install OIDC + session +# Install OIDC RUN git clone --depth 1 --branch v1.9.0 \ https://github.com/zmartzone/lua-resty-openidc.git /tmp/lua-resty-openidc \ - && cp /tmp/lua-resty-openidc/lib/resty/openidc.lua \ - /usr/local/openresty/site/lualib/resty/openidc.lua \ + && cp -r /tmp/lua-resty-openidc/lib/resty/* \ + /usr/local/openresty/lualib/resty/ \ && rm -rf /tmp/lua-resty-openidc +# Install session RUN opm install bungle/lua-resty-session -RUN mkdir -p /opt/simpleVMWebGateway RUN git clone \ https://github.com/deNBI/simpleVMWebGateway.git \ From f8d407924ecd652fc22f5555f5bd4951a28d220f Mon Sep 17 00:00:00 2001 From: dweinholz Date: Mon, 27 Jul 2026 14:21:31 +0000 Subject: [PATCH 076/118] use luarocks --- docker/Dockerfile | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1d8469da..e60a0fec 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,6 +15,7 @@ RUN apt-get update && apt-get install -y \ python3-pip \ python3-venv \ lsb-release \ + luarocks \ && rm -rf /var/lib/apt/lists/* # Install OpenResty @@ -27,16 +28,8 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ && apt-get install -y openresty \ && rm -rf /var/lib/apt/lists/* -# Install OIDC -RUN git clone --depth 1 --branch v1.9.0 \ - https://github.com/zmartzone/lua-resty-openidc.git /tmp/lua-resty-openidc \ - && cp -r /tmp/lua-resty-openidc/lib/resty/* \ - /usr/local/openresty/lualib/resty/ \ - && rm -rf /tmp/lua-resty-openidc - -# Install session -RUN opm install bungle/lua-resty-session +RUN luarocks --lua-version=5.1 install lua-resty-openidc && luarocks --lua-version=5.1 install lua-resty-session RUN git clone \ https://github.com/deNBI/simpleVMWebGateway.git \ /opt/simpleVMWebGateway/ From 5a12c3562f8957ed8eb5ded0b9c3bc1659a27a9a Mon Sep 17 00:00:00 2001 From: dweinholz Date: Tue, 28 Jul 2026 08:58:14 +0000 Subject: [PATCH 077/118] updated examples --- examples/templates/emgb%v01.conf | 14 -------------- examples/templates/guacamole%v03.conf | 14 -------------- examples/templates/jupyterlab%v03.conf | 14 -------------- examples/templates/rstudio%v04.conf | 14 -------------- examples/templates/theiaide%v03.conf | 14 -------------- examples/templates/vscode%v03.conf | 14 -------------- 6 files changed, 84 deletions(-) diff --git a/examples/templates/emgb%v01.conf b/examples/templates/emgb%v01.conf index 517dd3e8..33a68e18 100644 --- a/examples/templates/emgb%v01.conf +++ b/examples/templates/emgb%v01.conf @@ -1,20 +1,6 @@ # PROTECT FIRST THEIA CONTAINER location /{{ key_url }}/ { - set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier - set $session_storage shm; # use shared memory - set $session_cookie_persistent on; # persist cookie between browser sessions - set $session_cookie_renew 3500; # new cookie every hour - set $session_cookie_lifetime 86400; # lifetime for persistent cookies - set $session_name sess_auth; # name of the cookie to store the session identifier in - set $session_shm_store sessions; # name of the dict to store sessions in - # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) diff --git a/examples/templates/guacamole%v03.conf b/examples/templates/guacamole%v03.conf index 34d8286d..ca06d016 100644 --- a/examples/templates/guacamole%v03.conf +++ b/examples/templates/guacamole%v03.conf @@ -1,19 +1,5 @@ location /{{ key_url }}/ { - set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier - set $session_storage shm; # use shared memory - set $session_cookie_persistent on; # persist cookie between browser sessions - set $session_cookie_renew 3500; # new cookie every hour - set $session_cookie_lifetime 86400; # lifetime for persistent cookies - set $session_name sess_auth; # name of the cookie to store the session identifier in - set $session_shm_store sessions; # name of the dict to store sessions in - # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) diff --git a/examples/templates/jupyterlab%v03.conf b/examples/templates/jupyterlab%v03.conf index bd6a4288..867b1464 100644 --- a/examples/templates/jupyterlab%v03.conf +++ b/examples/templates/jupyterlab%v03.conf @@ -1,19 +1,5 @@ location /{{ key_url }} { - set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier - set $session_storage shm; # use shared memory - set $session_cookie_persistent on; # persist cookie between browser sessions - set $session_cookie_renew 3500; # new cookie every hour - set $session_cookie_lifetime 86400; # lifetime for persistent cookies - set $session_name sess_auth; # name of the cookie to store the session identifier in - set $session_shm_store sessions; # name of the dict to store sessions in - # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) diff --git a/examples/templates/rstudio%v04.conf b/examples/templates/rstudio%v04.conf index d6ff16c8..ffdff2b2 100644 --- a/examples/templates/rstudio%v04.conf +++ b/examples/templates/rstudio%v04.conf @@ -1,19 +1,5 @@ location /{{ key_url }}/ { - set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier - set $session_storage shm; # use shared memory - set $session_cookie_persistent on; # persist cookie between browser sessions - set $session_cookie_renew 3500; # new cookie every hour - set $session_cookie_lifetime 86400; # lifetime for persistent cookies - set $session_name sess_auth; # name of the cookie to store the session identifier in - set $session_shm_store sessions; # name of the dict to store sessions in - # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) diff --git a/examples/templates/theiaide%v03.conf b/examples/templates/theiaide%v03.conf index bfe3bb10..4f0e04a5 100644 --- a/examples/templates/theiaide%v03.conf +++ b/examples/templates/theiaide%v03.conf @@ -1,20 +1,6 @@ # PROTECT FIRST THEIA CONTAINER location /{{ key_url }}/ { - set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier - set $session_storage shm; # use shared memory - set $session_cookie_persistent on; # persist cookie between browser sessions - set $session_cookie_renew 3500; # new cookie every hour - set $session_cookie_lifetime 86400; # lifetime for persistent cookies - set $session_name sess_auth; # name of the cookie to store the session identifier in - set $session_shm_store sessions; # name of the dict to store sessions in - # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) diff --git a/examples/templates/vscode%v03.conf b/examples/templates/vscode%v03.conf index 5ca4b4df..f04019a3 100644 --- a/examples/templates/vscode%v03.conf +++ b/examples/templates/vscode%v03.conf @@ -1,20 +1,6 @@ # PROTECT FIRST THEIA CONTAINER location /{{ key_url }}/ { - set $session_cipher none; # don't need to encrypt the session content, it's an opaque identifier - set $session_storage shm; # use shared memory - set $session_cookie_persistent on; # persist cookie between browser sessions - set $session_cookie_renew 3500; # new cookie every hour - set $session_cookie_lifetime 86400; # lifetime for persistent cookies - set $session_name sess_auth; # name of the cookie to store the session identifier in - set $session_shm_store sessions; # name of the dict to store sessions in - # See https://github.com/bungle/lua-resty-session#shared-dictionary-storage-adapter for the following options - set $session_shm_uselocking off; - set $session_shm_lock_exptime 3; - set $session_shm_lock_timeout 2; - set $session_shm_lock_step 0.001; - set $session_shm_lock_ratio 1; - set $session_shm_lock_max_step 0.5; set $user_path '{{ forc_backend_path }}/users/{{backend_id}}/'; # Run this lua block, which checks if we are authenticated (again) und filters request by JWT (via id_token.sub) From 2f80009dd238e27074bcab01a8774568125964f0 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Tue, 28 Jul 2026 11:06:11 +0000 Subject: [PATCH 078/118] updated dockerfile --- docker/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e60a0fec..b32a36e2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -31,9 +31,11 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ RUN luarocks --lua-version=5.1 install lua-resty-openidc && luarocks --lua-version=5.1 install lua-resty-session RUN git clone \ + --branch refactor/docker \ + --single-branch \ + --depth 1 \ https://github.com/deNBI/simpleVMWebGateway.git \ /opt/simpleVMWebGateway/ - WORKDIR /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator RUN python3 -m venv /opt/venv \ From 4a1e42c52687e1435b1b2cdb0778cfeb04aba261 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Tue, 28 Jul 2026 11:50:55 +0000 Subject: [PATCH 079/118] using moneypatch --- docker/Dockerfile | 2 +- docker/nginx.conf | 90 +++--- docker/nginx.conf.old | 274 +++++++++++++++++++ docker/plans/graceful-tinkering-jellyfish.md | 41 +++ 4 files changed, 358 insertions(+), 49 deletions(-) create mode 100644 docker/nginx.conf.old create mode 100644 docker/plans/graceful-tinkering-jellyfish.md diff --git a/docker/Dockerfile b/docker/Dockerfile index b32a36e2..e5d7a642 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,7 +29,7 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ && rm -rf /var/lib/apt/lists/* -RUN luarocks --lua-version=5.1 install lua-resty-openidc && luarocks --lua-version=5.1 install lua-resty-session +RUN luarocks --lua-version=5.1 install lua-resty-openidc RUN git clone \ --branch refactor/docker \ --single-branch \ diff --git a/docker/nginx.conf b/docker/nginx.conf index da2d3531..4960a151 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -34,46 +34,39 @@ http { } # OIDC config -init_by_lua_block { - opts2 = { - redirect_uri = "https://{{ DOMAIN }}/redirect_uri", - discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", - client_id = "{{ FORC_OIDC_CLIENT_ID }}", - client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", - - logout_path = "/logout", - - ssl_verify = "no", - iat_slack = 600, - - scope = "openid email profile offline_access", - - renew_access_token_on_expiry = true, - access_token_expires_leeway = 60, - - session_secret = "{{ FORC_SECRET_KEY }}", - session_name = "sess_auth", - session_storage = "shm", - session_shm_store = "sessions", - - session_cookie_persistent = true, - session_cookie_lifetime = 86400, - session_cookie_renew = 3500, - - session_contents = { - id_token = true, - access_token = true + init_by_lua_block { + -- Monkey-Patch for lua-resty-session to force the cookie name + local session = require("resty.session") + local old_new = session.new + session.new = function(opts) + opts = opts or {} + opts.cookie_name = "sess_auth" + opts.secret = "{{ FORC_SECRET_KEY }}" + return old_new(opts) + end + + opts2 = { + redirect_uri = "https://{{ DOMAIN }}/redirect_uri", + discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", + client_id = "{{ FORC_OIDC_CLIENT_ID }}", + client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", + logout_path = "/logout", + ssl_verify = "no", + iat_slack = 600, + scope = "openid email profile offline_access", + renew_access_token_on_expiry = true, + access_token_expires_leeway = 60, + session_contents = {id_token=true, access_token=true} } } -} # ------------------------------- # BLOCKED IP CONFIGURATION # ------------------------------- - # geo $blocked_ip { - # default 0; - # include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP - # } + # geo $blocked_ip { + # default 0; + # include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP + # } # ------------------------------- # SERVER BLOCKS @@ -97,9 +90,9 @@ init_by_lua_block { ssl_stapling_verify on; # Block IPs - # if ($blocked_ip) { - # return 444; - #} + # if ($blocked_ip) { + # return 444; + # } location / { proxy_pass http://unix:/var/run/forc.sock; @@ -110,9 +103,9 @@ init_by_lua_block { listen {{ FORC_LOCAL_IP }}:{{ FORC_SERVICE_PORT }}; # Block IPs - # if ($blocked_ip) { - # return 444; - # } + # if ($blocked_ip) { + # return 444; + # } location / { proxy_pass http://unix:/var/run/forc.sock; @@ -126,9 +119,9 @@ init_by_lua_block { server_name {{ DOMAIN }}; # Block IPs - # if ($blocked_ip) { - # return 444; - # } + # if ($blocked_ip) { + # return 444; + # } return 301 https://$host$request_uri; } @@ -165,14 +158,14 @@ init_by_lua_block { client_max_body_size 100M; # Block IPs - #if ($blocked_ip) { - # return 444; - #} + # if ($blocked_ip) { + # return 444; + # } location / { access_by_lua_block { - local res, err = require("resty.openidc").authenticate(opts2) + local res, err = require("resty.openidc").authenticate(opts2, nil, nil, opts_session) if err then ngx.status = 500 ngx.say(err) @@ -191,6 +184,7 @@ init_by_lua_block { } # Dynamically included locations + include {{ FORC_BACKEND_PATH }}/*.conf; error_page 500 502 503 504 /50x.html; @@ -202,4 +196,4 @@ init_by_lua_block { error_page 403 /403.html; location = /403.html { root html; } } -} \ No newline at end of file +} diff --git a/docker/nginx.conf.old b/docker/nginx.conf.old new file mode 100644 index 00000000..34703bf5 --- /dev/null +++ b/docker/nginx.conf.old @@ -0,0 +1,274 @@ +worker_processes {{ OPENRESTY_WORKER_PROCESSES }}; + +error_log logs/error.log debug; + +events { + worker_connections 1024; +} + +http { + + include mime.types; + + lua_package_path "{{ FORC_BACKEND_PATH }}/scripts/?.lua;;"; + + default_type application/octet-stream; + + resolver {{ OPENRESTY_DNS_SERVER }}; + + sendfile on; + + keepalive_timeout 65; + + + # + # Lua shared memory + # + lua_shared_dict discovery 1m; + lua_shared_dict jwks 1m; + lua_shared_dict sessions 10m; + + + lua_code_cache on; + + + client_max_body_size 100M; + + + # + # Websocket support + # + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + + # + # OIDC configuration + # + init_by_lua_block { + + opts2 = { + + redirect_uri = "https://{{ DOMAIN }}/redirect_uri", + + discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", + + client_id = "{{ FORC_OIDC_CLIENT_ID }}", + + client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", + + + logout_path = "/logout", + + + ssl_verify = "no", + + iat_slack = 600, + + + scope = "openid email profile offline_access", + + + renew_access_token_on_expiry = true, + + access_token_expires_leeway = 60, + + + session_contents = { + id_token = true, + access_token = true + } + } + } + + + + # + # HTTP -> HTTPS + # + server { + + listen 0.0.0.0:80 default_server; + + server_name {{ DOMAIN }}; + + return 301 https://$host$request_uri; + } + + + + # + # Main HTTPS server + # + server { + + listen 0.0.0.0:443 ssl http2; + + server_name {{ DOMAIN }}; + + + ssl_certificate /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; + + ssl_certificate_key /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; + + + ssl_protocols TLSv1.2 TLSv1.3; + + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + + add_header X-Frame-Options SAMEORIGIN; + + add_header X-Content-Type-Options nosniff; + + add_header Referrer-Policy "strict-origin"; + + + ssl_stapling on; + + ssl_stapling_verify on; + + + + location / { + + + access_by_lua_block { + + + local session_opts = { + + secret = "{{ FORC_SECRET_KEY }}", + + + storage = "shm", + + + shm = { + store = "sessions" + }, + + cookie_name = "sess_auth", + + } + + + + local res, err = + require("resty.openidc").authenticate( + opts2, + nil, + nil, + session_opts + ) + + + + if err then + + ngx.status = 500; + + ngx.say(err); + + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR); + + end + + + + ngx.req.set_header( + "X-Auth-Audience", + res.id_token.aud + ) + + + ngx.req.set_header( + "X-Auth-Email", + res.id_token.email + ) + + + ngx.req.set_header( + "X-Auth-ExpiresIn", + res.id_token.exp + ) + + + ngx.req.set_header( + "X-Auth-Name", + res.id_token.name + ) + + + ngx.req.set_header( + "X-Auth-Subject", + res.id_token.sub + ) + + + ngx.req.set_header( + "X-Auth-Userid", + res.id_token.preferred_username + ) + + + ngx.req.set_header( + "X-Auth-Username", + res.id_token.preferred_username + ) + + + ngx.req.set_header( + "X-Auth-Locale", + res.id_token.locale + ) + } + + + + proxy_pass http://unix:/var/run/forc.sock; + + + proxy_set_header Host $host; + + proxy_set_header X-Real-IP $remote_addr; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_set_header X-Forwarded-Proto $scheme; + + + proxy_set_header Upgrade $http_upgrade; + + proxy_set_header Connection $connection_upgrade; + } + + + + include {{ FORC_BACKEND_PATH }}/*.conf; + + + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root html; + } + + + error_page 404 /404.html; + + location = /404.html { + root html; + } + + + error_page 403 /403.html; + + location = /403.html { + root html; + } + } +} \ No newline at end of file diff --git a/docker/plans/graceful-tinkering-jellyfish.md b/docker/plans/graceful-tinkering-jellyfish.md new file mode 100644 index 00000000..fe7d1fb8 --- /dev/null +++ b/docker/plans/graceful-tinkering-jellyfish.md @@ -0,0 +1,41 @@ +# Migration of Session Config in Example Templates + +## Context +The `nginx.conf` template was migrated to use `lua-resty-session` 4.1.5 and `lua-resty-openidc` 1.9.0. In these versions, session configuration is moved from Nginx variables (`set $session_...`) to a structured Lua table passed within the OIDC options. + +The example templates in `examples/templates/` currently contain redundant and obsolete `set $session_...` directives. Since these templates use the global `opts2` configuration defined in the main `nginx.conf`, these local variable definitions are no longer needed and should be removed to ensure consistency and prevent potential conflicts. + +## Implementation Plan + +### 1. Identify Affected Files +The following files in `examples/templates/` contain the obsolete session configuration: +- `emgb%v01.conf` +- `guacamole%v03.conf` +- `jupyterlab%v03.conf` +- `rstudio%v04.conf` +- `theiaide%v03.conf` +- `vscode%v03.conf` + +### 2. Remove Obsolete Configuration +In each of the identified files, remove the block of lines starting with `set $session_`. + +The block to be removed typically looks like: +```nginx +set $session_cipher none; +set $session_storage shm; +set $session_cookie_persistent on; +set $session_cookie_renew 3500; +set $session_cookie_lifetime 86400; +set $session_name sess_auth; +set $session_shm_store sessions; +set $session_shm_uselocking off; +set $session_shm_lock_exptime 3; +set $session_shm_lock_timeout 2; +set $session_shm_lock_step 0.001; +set $session_shm_lock_ratio 1; +set $session_shm_lock_max_step 0.5; +``` + +### 3. Verification +- Run `grep -r "set \$session_" examples/templates/` to ensure no remaining session variable definitions exist in the templates directory. +- Verify that the `access_by_lua_block` still calls `require("resty.openidc").authenticate(opts2)`, which now inherits the correct session configuration from the main `nginx.conf`. From 03fabd9abf81375b8714e187c3cfe86785113595 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Tue, 28 Jul 2026 12:06:16 +0000 Subject: [PATCH 080/118] updated dockerfile --- docker/Dockerfile | 9 + docker/generate_ip_blocklists.sh | 53 ++++++ docker/ip_blocklists.txt | 6 + docker/launch.sh | 3 + docker/nginx.conf | 32 ++-- docker/nginx.conf.old | 274 ------------------------------- 6 files changed, 87 insertions(+), 290 deletions(-) create mode 100644 docker/generate_ip_blocklists.sh create mode 100644 docker/ip_blocklists.txt delete mode 100644 docker/nginx.conf.old diff --git a/docker/Dockerfile b/docker/Dockerfile index e5d7a642..3e7213f5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,6 +8,7 @@ RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} RUN apt-get update && apt-get install -y \ git \ wget \ + curl \ gnupg \ ca-certificates \ software-properties-common \ @@ -16,6 +17,7 @@ RUN apt-get update && apt-get install -y \ python3-venv \ lsb-release \ luarocks \ + cron \ && rm -rf /var/lib/apt/lists/* # Install OpenResty @@ -50,6 +52,13 @@ COPY render_nginx.py /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ COPY gunicorn_conf.py gunicorn_conf.py COPY launch.sh launch.sh +RUN mkdir -p /opt/scripts +COPY generate_ip_blocklists.sh ip_blocklists.txt /opt/scripts/ +RUN chmod +x /opt/scripts/generate_ip_blocklists.sh + +RUN echo "0 */2 * * * root /opt/scripts/generate_ip_blocklists.sh && /usr/sbin/openresty -s reload" > /etc/cron.d/ip-blocklist \ + && chmod 0644 /etc/cron.d/ip-blocklist + RUN chmod +x launch.sh EXPOSE 5000 CMD ["./launch.sh"] \ No newline at end of file diff --git a/docker/generate_ip_blocklists.sh b/docker/generate_ip_blocklists.sh new file mode 100644 index 00000000..6e482da6 --- /dev/null +++ b/docker/generate_ip_blocklists.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Paths relative to script directory +INPUT="$SCRIPT_DIR/ip_blocklists.txt" +OUTPUT="/etc/openresty/block_ips_geo.conf" +TEMP="$SCRIPT_DIR/block_tmp.txt" + +rm -f "$TEMP" "$OUTPUT" + +# Check that list exists +if [[ ! -f $INPUT ]]; then + echo "ERROR: $INPUT does not exist!" + exit 1 +fi + +# Download all blocklists +while IFS= read -r url; do + + # Skip empty or commented lines + [[ -z "$url" ]] && continue + [[ "$url" =~ ^# ]] && continue + + echo "Downloading: $url" + + curl -fsSL "$url" >> "$TEMP" || echo "Failed: $url" + +done < "$INPUT" + +# Check if anything was downloaded +if [[ ! -s "$TEMP" ]]; then + echo "ERROR: download failed — TEMP file is empty!" + exit 1 +fi + +echo "Cleaning downloaded dataâ€Ļ" + +grep -vE '^\s*#|^\s*;' "$TEMP" \ +| sed '/^\s*$/d' \ +| sed 's/[#;].*$//' \ +| awk '{print $1}' \ +| grep -E '^[0-9a-fA-F:.]+(/[0-9]{1,3})?$' \ +| sort -u \ +| awk '{print $1 " 1;"}' \ +> "$OUTPUT" + +rm "$TEMP" + +echo "✔ Done" +echo "Final list: $OUTPUT" +wc -l "$OUTPUT" diff --git a/docker/ip_blocklists.txt b/docker/ip_blocklists.txt new file mode 100644 index 00000000..a3900e56 --- /dev/null +++ b/docker/ip_blocklists.txt @@ -0,0 +1,6 @@ +https://rules.emergingthreats.net/blockrules/compromised-ips.txt +https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level2.netset +https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level3.netset +https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ciarmy.ipset +https://lists.blocklist.de/lists/all.txt +https://www.spamhaus.org/drop/drop.txt diff --git a/docker/launch.sh b/docker/launch.sh index 7a76ff90..e58f464b 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -1,6 +1,9 @@ #!/bin/bash set -e +echo "Starting cron..." +service cron start + echo "Rendering OpenResty configuration..." python3 /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/render_nginx.py diff --git a/docker/nginx.conf b/docker/nginx.conf index 4960a151..97284116 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -63,10 +63,10 @@ http { # ------------------------------- # BLOCKED IP CONFIGURATION # ------------------------------- - # geo $blocked_ip { - # default 0; - # include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP - # } + geo $blocked_ip { + default 0; + include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP + } # ------------------------------- # SERVER BLOCKS @@ -90,9 +90,9 @@ http { ssl_stapling_verify on; # Block IPs - # if ($blocked_ip) { - # return 444; - # } + if ($blocked_ip) { + return 444; + } location / { proxy_pass http://unix:/var/run/forc.sock; @@ -103,9 +103,9 @@ http { listen {{ FORC_LOCAL_IP }}:{{ FORC_SERVICE_PORT }}; # Block IPs - # if ($blocked_ip) { - # return 444; - # } + if ($blocked_ip) { + return 444; + } location / { proxy_pass http://unix:/var/run/forc.sock; @@ -119,9 +119,9 @@ http { server_name {{ DOMAIN }}; # Block IPs - # if ($blocked_ip) { - # return 444; - # } + if ($blocked_ip) { + return 444; + } return 301 https://$host$request_uri; } @@ -158,9 +158,9 @@ http { client_max_body_size 100M; # Block IPs - # if ($blocked_ip) { - # return 444; - # } + if ($blocked_ip) { + return 444; + } location / { diff --git a/docker/nginx.conf.old b/docker/nginx.conf.old deleted file mode 100644 index 34703bf5..00000000 --- a/docker/nginx.conf.old +++ /dev/null @@ -1,274 +0,0 @@ -worker_processes {{ OPENRESTY_WORKER_PROCESSES }}; - -error_log logs/error.log debug; - -events { - worker_connections 1024; -} - -http { - - include mime.types; - - lua_package_path "{{ FORC_BACKEND_PATH }}/scripts/?.lua;;"; - - default_type application/octet-stream; - - resolver {{ OPENRESTY_DNS_SERVER }}; - - sendfile on; - - keepalive_timeout 65; - - - # - # Lua shared memory - # - lua_shared_dict discovery 1m; - lua_shared_dict jwks 1m; - lua_shared_dict sessions 10m; - - - lua_code_cache on; - - - client_max_body_size 100M; - - - # - # Websocket support - # - map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } - - - # - # OIDC configuration - # - init_by_lua_block { - - opts2 = { - - redirect_uri = "https://{{ DOMAIN }}/redirect_uri", - - discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", - - client_id = "{{ FORC_OIDC_CLIENT_ID }}", - - client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", - - - logout_path = "/logout", - - - ssl_verify = "no", - - iat_slack = 600, - - - scope = "openid email profile offline_access", - - - renew_access_token_on_expiry = true, - - access_token_expires_leeway = 60, - - - session_contents = { - id_token = true, - access_token = true - } - } - } - - - - # - # HTTP -> HTTPS - # - server { - - listen 0.0.0.0:80 default_server; - - server_name {{ DOMAIN }}; - - return 301 https://$host$request_uri; - } - - - - # - # Main HTTPS server - # - server { - - listen 0.0.0.0:443 ssl http2; - - server_name {{ DOMAIN }}; - - - ssl_certificate /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; - - ssl_certificate_key /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; - - - ssl_protocols TLSv1.2 TLSv1.3; - - - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; - - add_header X-Frame-Options SAMEORIGIN; - - add_header X-Content-Type-Options nosniff; - - add_header Referrer-Policy "strict-origin"; - - - ssl_stapling on; - - ssl_stapling_verify on; - - - - location / { - - - access_by_lua_block { - - - local session_opts = { - - secret = "{{ FORC_SECRET_KEY }}", - - - storage = "shm", - - - shm = { - store = "sessions" - }, - - cookie_name = "sess_auth", - - } - - - - local res, err = - require("resty.openidc").authenticate( - opts2, - nil, - nil, - session_opts - ) - - - - if err then - - ngx.status = 500; - - ngx.say(err); - - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR); - - end - - - - ngx.req.set_header( - "X-Auth-Audience", - res.id_token.aud - ) - - - ngx.req.set_header( - "X-Auth-Email", - res.id_token.email - ) - - - ngx.req.set_header( - "X-Auth-ExpiresIn", - res.id_token.exp - ) - - - ngx.req.set_header( - "X-Auth-Name", - res.id_token.name - ) - - - ngx.req.set_header( - "X-Auth-Subject", - res.id_token.sub - ) - - - ngx.req.set_header( - "X-Auth-Userid", - res.id_token.preferred_username - ) - - - ngx.req.set_header( - "X-Auth-Username", - res.id_token.preferred_username - ) - - - ngx.req.set_header( - "X-Auth-Locale", - res.id_token.locale - ) - } - - - - proxy_pass http://unix:/var/run/forc.sock; - - - proxy_set_header Host $host; - - proxy_set_header X-Real-IP $remote_addr; - - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_set_header X-Forwarded-Proto $scheme; - - - proxy_set_header Upgrade $http_upgrade; - - proxy_set_header Connection $connection_upgrade; - } - - - - include {{ FORC_BACKEND_PATH }}/*.conf; - - - - error_page 500 502 503 504 /50x.html; - - location = /50x.html { - root html; - } - - - error_page 404 /404.html; - - location = /404.html { - root html; - } - - - error_page 403 /403.html; - - location = /403.html { - root html; - } - } -} \ No newline at end of file From 65cd60eebc1975dc7c78595adfec99ed23c9da72 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 09:02:22 +0000 Subject: [PATCH 081/118] run blocked ips at startup --- docker/launch.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/launch.sh b/docker/launch.sh index e58f464b..697a1295 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -1,6 +1,8 @@ #!/bin/bash set -e +echo "Generating Block IPs for startup.." +./opt/scripts/generate_ip_blocklists.sh echo "Starting cron..." service cron start From 16bbcdf89d2c9944310f0fa341490404784d2a34 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 09:23:44 +0000 Subject: [PATCH 082/118] onyl run blocked ups script if conf is not presetn --- docker/launch.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/launch.sh b/docker/launch.sh index 697a1295..941d0140 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -2,7 +2,10 @@ set -e echo "Generating Block IPs for startup.." -./opt/scripts/generate_ip_blocklists.sh +## only neede if not present +if [ ! -f /etc/openresty/block_ips_geo.conf ]; then + /opt/scripts/generate_ip_blocklists.sh +fi echo "Starting cron..." service cron start From b64627362a29036952093e0209723132daf6365b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:48:17 +0000 Subject: [PATCH 083/118] feat(Dependencies): Update dependency uvicorn to v0.52.0 | datasource | package | from | to | | ---------- | ------- | ------ | ------ | | pypi | uvicorn | 0.51.0 | 0.52.0 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index 3445eb3c..a69d3cf2 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -1,5 +1,5 @@ fastapi==0.139.2 -uvicorn==0.51.0 +uvicorn==0.52.0 werkzeug==3.1.8 Jinja2==3.1.6 python-dotenv==1.2.2 From 21eb1f6607b9291e8cbd3af2dfb93f2b6de624c4 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 11:48:42 +0000 Subject: [PATCH 084/118] added containerized option --- .../app/main/config.py | 12 +++++--- .../app/main/service/openresty.py | 30 ++++++++++++++++--- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/config.py b/FastapiOpenRestyConfigurator/app/main/config.py index 2c861703..6d52fbba 100644 --- a/FastapiOpenRestyConfigurator/app/main/config.py +++ b/FastapiOpenRestyConfigurator/app/main/config.py @@ -3,6 +3,7 @@ from pydantic_settings import BaseSettings from pydantic import SecretStr, validator, DirectoryPath + basedir = os.path.abspath(os.path.dirname(__file__)) @@ -11,16 +12,18 @@ class Settings(BaseSettings): Settings object. Reads settings from .env file. """ - FORC_VERSION: str = '0.2' + + FORC_VERSION: str = "0.2" DEBUG: bool = False LOG_LEVEL: str = "INFO" FORC_API_KEY: SecretStr - FORC_SECRET_KEY: SecretStr = 'my_precious_secret_key' + FORC_SECRET_KEY: SecretStr = "my_precious_secret_key" FORC_BACKEND_PATH: DirectoryPath FORC_TEMPLATE_PATH: DirectoryPath FORC_USER_PATH: str = "users" + CONTAINERZIED: bool = False - @validator('FORC_USER_PATH', pre=True) + @validator("FORC_USER_PATH", pre=True) def apply_backend_path(cls, v, values): """ Validates forc user path, as it depends on forc backend path. @@ -29,7 +32,7 @@ def apply_backend_path(cls, v, values): :return: Updated FORC_USER_PATH. """ # := assigns and compares a value. (if a := b:) == (a = b; if a:) - if FORC_BACKEND_PATH := values.get('FORC_BACKEND_PATH'): + if FORC_BACKEND_PATH := values.get("FORC_BACKEND_PATH"): return f"{FORC_BACKEND_PATH}/{v}" else: # should only happen when there was an error with FORC_BACKEND_PATH @@ -39,6 +42,7 @@ class Config: """ Config for settings object. """ + # Enabled case sensitive for reading variables from .env file case_sensitive = True # Path to .env file diff --git a/FastapiOpenRestyConfigurator/app/main/service/openresty.py b/FastapiOpenRestyConfigurator/app/main/service/openresty.py index e5053f4d..86fd1dc6 100644 --- a/FastapiOpenRestyConfigurator/app/main/service/openresty.py +++ b/FastapiOpenRestyConfigurator/app/main/service/openresty.py @@ -1,17 +1,39 @@ """ Service to reload openresty by starting a process. """ -import os + +import asyncio import logging +from FastapiOpenRestyConfigurator.app.main.config import get_settings + +settings = get_settings() + logger = logging.getLogger("service") async def reload_openresty(): logger.info("Reloading openresty config after backend change.") + try: - os.popen("sudo openresty -s reload") - logger.info("Reload succesful.") - except OSError as e: + if settings.CONTAINERIZED: + cmd = ["openresty", "-s", "reload"] + else: + cmd = ["sudo", "openresty", "-s", "reload"] + + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + stdout, stderr = await process.communicate() + + if process.returncode != 0: + raise RuntimeError(stderr.decode().strip()) + + logger.info("Reload successful.") + + except Exception as e: logger.exception(f"Was not able to reload OpenResty: {e}") From 10ef7227392102991ae2f0df6d6a864aec091726 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 11:50:04 +0000 Subject: [PATCH 085/118] set env in dockerfile --- docker/Dockerfile | 2 +- docker/nginx.conf | 369 +++++++++++++++++++------ docker/plans/ticklish-spinning-moth.md | 32 +++ 3 files changed, 310 insertions(+), 93 deletions(-) create mode 100644 docker/plans/ticklish-spinning-moth.md diff --git a/docker/Dockerfile b/docker/Dockerfile index 3e7213f5..7f0cad06 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,7 +2,7 @@ FROM ubuntu:24.04 ENV FORC_BACKEND_PATH=/var/forc/backend_path/ ENV FORC_TEMPLATE_PATH=/var/forc/template_path/ - +ENV CONTAINERIZED=true RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} RUN apt-get update && apt-get install -y \ diff --git a/docker/nginx.conf b/docker/nginx.conf index 97284116..10c4b435 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,31 +1,34 @@ -worker_processes {{ OPENRESTY_WORKER_PROCESSES }}; +worker_processes {{ OPENRESTY_WORKER_PROCESSES }}; # /usr/local/openresty/nginx/logs/ -error_log logs/error.log; -error_log logs/error.log notice; -error_log logs/error.log info; -error_log logs/error.log debug; +error_log logs/error.log notice; events { - worker_connections 1024; + worker_connections 1024; + multi_accept off; } http { - include mime.types; + include mime.types; + lua_package_path "{{ FORC_BACKEND_PATH }}/scripts/?.lua;;"; - default_type application/octet-stream; - # Hardcoded resolver - resolver {{ OPENRESTY_DNS_SERVER }}; - sendfile on; - keepalive_timeout 65; + + default_type application/octet-stream; + + resolver {{ OPENRESTY_DNS_SERVER }} valid=300s ipv6=off; + + sendfile on; + keepalive_timeout 65; + + client_max_body_size 100M; # LUA shared dicts lua_shared_dict discovery 1m; lua_shared_dict jwks 1m; lua_shared_dict sessions 10m; - lua_code_cache off; - client_max_body_size 100M; + lua_code_cache on; + # Websocket support map $http_upgrade $connection_upgrade { @@ -33,167 +36,349 @@ http { '' close; } + + # SSL defaults + ssl_protocols TLSv1.2 TLSv1.3; + + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256: + ECDHE-RSA-AES128-GCM-SHA256: + ECDHE-ECDSA-AES256-GCM-SHA384: + ECDHE-RSA-AES256-GCM-SHA384: + ECDHE-ECDSA-CHACHA20-POLY1305: + ECDHE-RSA-CHACHA20-POLY1305: + DHE-RSA-AES128-GCM-SHA256: + DHE-RSA-AES256-GCM-SHA384; + + ssl_prefer_server_ciphers off; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_session_tickets off; + + # OIDC config init_by_lua_block { - -- Monkey-Patch for lua-resty-session to force the cookie name - local session = require("resty.session") - local old_new = session.new - session.new = function(opts) - opts = opts or {} - opts.cookie_name = "sess_auth" - opts.secret = "{{ FORC_SECRET_KEY }}" - return old_new(opts) - end + + opts_session = { + cookie_name = "sess_auth", + secret = "{{ FORC_SECRET_KEY }}" + } + opts2 = { redirect_uri = "https://{{ DOMAIN }}/redirect_uri", discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", client_id = "{{ FORC_OIDC_CLIENT_ID }}", client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", + logout_path = "/logout", + ssl_verify = "no", + iat_slack = 600, + scope = "openid email profile offline_access", + renew_access_token_on_expiry = true, access_token_expires_leeway = 60, - session_contents = {id_token=true, access_token=true} + + session_contents = { + id_token = true, + access_token = true + } } } - # ------------------------------- - # BLOCKED IP CONFIGURATION - # ------------------------------- + + # Blocked IPs geo $blocked_ip { - default 0; - include /etc/openresty/block_ips_geo.conf; # IP list with 1 per blocked IP - } + default 0; + include /etc/openresty/block_ips_geo.conf; + } - # ------------------------------- - # SERVER BLOCKS - # ------------------------------- - {% if FORC_SERVICE_USE_HTTPS %} + # + # Internal HTTPS service + # +{% if FORC_SERVICE_USE_HTTPS %} + server { - listen {{ FORC_SERVICE_PORT }} ssl http2; - ssl_certificate /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers on; - - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + listen {{ FORC_SERVICE_PORT }} ssl; + http2 on; + + server_name {{ DOMAIN }}; + + + ssl_certificate + /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; + + ssl_certificate_key + /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; + + ssl_trusted_certificate + /etc/letsencrypt/live/{{ DOMAIN }}/chain.pem; + + + add_header Strict-Transport-Security + "max-age=31536000; includeSubDomains; preload"; + add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - add_header Referrer-Policy 'strict-origin'; + add_header Referrer-Policy "strict-origin"; + + ssl_stapling on; ssl_stapling_verify on; - # Block IPs - if ($blocked_ip) { - return 444; - } + + if ($blocked_ip) { + return 444; + } + location / { + proxy_pass http://unix:/var/run/forc.sock; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_set_header + X-Forwarded-For + $proxy_add_x_forwarded_for; + + proxy_set_header + X-Forwarded-Proto + https; + + + proxy_http_version 1.1; + + proxy_set_header + Upgrade + $http_upgrade; + + proxy_set_header + Connection + $connection_upgrade; } } - {% else %} + +{% else %} + server { + listen {{ FORC_LOCAL_IP }}:{{ FORC_SERVICE_PORT }}; - # Block IPs - if ($blocked_ip) { - return 444; - } + + if ($blocked_ip) { + return 444; + } + location / { + proxy_pass http://unix:/var/run/forc.sock; + + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_set_header + X-Forwarded-For + $proxy_add_x_forwarded_for; + + proxy_set_header + X-Forwarded-Proto + http; } } - {% endif %} - # HTTP redirect to HTTPS +{% endif %} + + + + # + # HTTP redirect + # server { + listen 0.0.0.0:80 default_server; + server_name {{ DOMAIN }}; - # Block IPs - if ($blocked_ip) { - return 444; - } + + if ($blocked_ip) { + return 444; + } + return 301 https://$host$request_uri; } - # Local nginx status + + + # + # nginx status + # server { + listen 127.0.0.1:8080; + location /nginx_status { + stub_status; + allow 127.0.0.1; + deny all; } } - # Main public HTTPS server with Lua OIDC + + + # + # Public HTTPS + OIDC + # server { - listen 0.0.0.0:443 ssl http2; + + listen 0.0.0.0:443 ssl; + + http2 on; + + server_name {{ DOMAIN }}; - ssl_certificate /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers on; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + ssl_certificate + /etc/letsencrypt/live/{{ DOMAIN }}/fullchain.pem; + + ssl_certificate_key + /etc/letsencrypt/live/{{ DOMAIN }}/privkey.pem; + + ssl_trusted_certificate + /etc/letsencrypt/live/{{ DOMAIN }}/chain.pem; + + + + add_header Strict-Transport-Security + "max-age=31536000; includeSubDomains; preload"; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - add_header Referrer-Policy 'strict-origin'; + + add_header Referrer-Policy "strict-origin"; + + + ssl_stapling on; ssl_stapling_verify on; - client_max_body_size 100M; - # Block IPs - if ($blocked_ip) { - return 444; - } + + + if ($blocked_ip) { + return 444; + } + location / { + + access_by_lua_block { - local res, err = require("resty.openidc").authenticate(opts2, nil, nil, opts_session) + + local res, err = + require("resty.openidc") + .authenticate( + opts2, + nil, + nil, + opts_session + ) + + if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + + ngx.status = 500; + + ngx.say(err); + + ngx.exit( + ngx.HTTP_INTERNAL_SERVER_ERROR + ); + end - ngx.req.set_header("X-Auth-Audience", res.id_token.aud) - ngx.req.set_header("X-Auth-Email", res.id_token.email) - ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp) - ngx.req.set_header("X-Auth-Name", res.id_token.name) - ngx.req.set_header("X-Auth-Subject", res.id_token.sub) - ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username) - ngx.req.set_header("X-Auth-Locale", res.id_token.locale) + + + ngx.req.set_header( + "X-Auth-Audience", + res.id_token.aud + ) + + ngx.req.set_header( + "X-Auth-Email", + res.id_token.email + ) + + ngx.req.set_header( + "X-Auth-ExpiresIn", + res.id_token.exp + ) + + ngx.req.set_header( + "X-Auth-Name", + res.id_token.name + ) + + ngx.req.set_header( + "X-Auth-Subject", + res.id_token.sub + ) + + ngx.req.set_header( + "X-Auth-Userid", + res.id_token.preferred_username + ) + + ngx.req.set_header( + "X-Auth-Username", + res.id_token.preferred_username + ) + + ngx.req.set_header( + "X-Auth-Locale", + res.id_token.locale + ) } } - # Dynamically included locations + include {{ FORC_BACKEND_PATH }}/*.conf; + + error_page 500 502 503 504 /50x.html; - location = /50x.html { root html; } + + location = /50x.html { + root html; + } + error_page 404 /404.html; - location = /404.html { root html; } + + location = /404.html { + root html; + } + error_page 403 /403.html; - location = /403.html { root html; } + + location = /403.html { + root html; + } } -} +} \ No newline at end of file diff --git a/docker/plans/ticklish-spinning-moth.md b/docker/plans/ticklish-spinning-moth.md new file mode 100644 index 00000000..35146ef0 --- /dev/null +++ b/docker/plans/ticklish-spinning-moth.md @@ -0,0 +1,32 @@ +# Implementation Plan: IP Blocklist Automation + +## Context +The goal is to automate the update of IP blocklists in the OpenResty gateway. Currently, `generate_ip_blocklists.sh` exists but is not integrated into the container's lifecycle. We need to ensure these lists are updated every two hours and that OpenResty reloads its configuration to apply the new blocks. + +## Proposed Changes + +### 1. Dockerfile Updates +- Update the `apt-get install` list to include `cron` and `curl`. +- Copy `generate_ip_blocklists.sh` and `ip_blocklists.txt` to `/opt/scripts/`. +- Ensure `generate_ip_blocklists.sh` has executable permissions. +- Create a cron job file in `/etc/cron.d/ip-blocklist` with the following schedule: + `0 */2 * * * root /opt/scripts/generate_ip_blocklists.sh && /usr/sbin/openresty -s reload` + +### 2. Launch Script Updates +- Modify `launch.sh` to start the `cron` daemon before starting OpenResty and FastAPI. + +### 3. File Renaming +- `ip_blocklists.sh` has already been renamed to `ip_blocklists.txt` to reflect its content as a list of URLs. + +## Critical Files +- `/home/ubuntu/workspace/denbi/simpleVMWebGateway/docker/Dockerfile` +- `/home/ubuntu/workspace/denbi/simpleVMWebGateway/docker/launch.sh` +- `/home/ubuntu/workspace/denbi/simpleVMWebGateway/docker/generate_ip_blocklists.sh` +- `/home/ubuntu/workspace/denbi/simpleVMWebGateway/docker/ip_blocklists.txt` + +## Verification Plan +1. Build the docker image. +2. Run the container. +3. Manually execute `/opt/scripts/generate_ip_blocklists.sh` and verify that `/etc/openresty/block_ips_geo.conf` is populated. +4. Verify that `cron` is running inside the container (`ps aux | grep cron`). +5. (Optional) Temporarily change the cron schedule to every minute to verify the automatic update and reload. From 1c347001c030169cab62fbb1b19a163c0a902136 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 12:13:02 +0000 Subject: [PATCH 086/118] updated nginx --- docker/generate_ip_blocklists.sh | 2 +- docker/launch.sh | 20 ++++++++++++++------ docker/nginx.conf | 14 +++----------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docker/generate_ip_blocklists.sh b/docker/generate_ip_blocklists.sh index 6e482da6..c1c54dee 100644 --- a/docker/generate_ip_blocklists.sh +++ b/docker/generate_ip_blocklists.sh @@ -25,7 +25,7 @@ while IFS= read -r url; do echo "Downloading: $url" - curl -fsSL "$url" >> "$TEMP" || echo "Failed: $url" + curl -fsSL -m 6 "$url" >> "$TEMP" || echo "Failed: $url" done < "$INPUT" diff --git a/docker/launch.sh b/docker/launch.sh index 941d0140..b0b3942c 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -2,21 +2,29 @@ set -e echo "Generating Block IPs for startup.." -## only neede if not present +## only needed if not present if [ ! -f /etc/openresty/block_ips_geo.conf ]; then /opt/scripts/generate_ip_blocklists.sh fi -echo "Starting cron..." -service cron start echo "Rendering OpenResty configuration..." - python3 /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/render_nginx.py echo "Starting OpenResty..." -openresty +# Start openresty in background with a writable PID file +openresty -g "pid /tmp/openresty.pid" + +echo "Starting Blocklist Updater Loop..." +( + while true; do + sleep 7200 # 2 hours + echo "$(date): Updating blocklists..." + /opt/scripts/generate_ip_blocklists.sh + openresty -g "pid /tmp/openresty.pid" -s reload + done +) & echo "Starting FastAPI..." exec gunicorn \ -c gunicorn_conf.py \ - main:app \ No newline at end of file + main:app diff --git a/docker/nginx.conf b/docker/nginx.conf index 10c4b435..2bfb6af2 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -40,16 +40,8 @@ http { # SSL defaults ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256: - ECDHE-RSA-AES128-GCM-SHA256: - ECDHE-ECDSA-AES256-GCM-SHA384: - ECDHE-RSA-AES256-GCM-SHA384: - ECDHE-ECDSA-CHACHA20-POLY1305: - ECDHE-RSA-CHACHA20-POLY1305: - DHE-RSA-AES128-GCM-SHA256: - DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; @@ -138,7 +130,7 @@ http { location / { - proxy_pass http://unix:/var/run/forc.sock; + proxy_pass http://unix:/tmp/forc.sock; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -178,7 +170,7 @@ http { location / { - proxy_pass http://unix:/var/run/forc.sock; + proxy_pass http://unix:/tmp/forc.sock; proxy_set_header Host $host; From 96c24de1c4b25d855f856ccf3b9dd801d3cc88a6 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 12:16:02 +0000 Subject: [PATCH 087/118] updated Dockerfile --- .gitignore | 3 +- docker/Dockerfile | 101 ++++++++++++++++++++++++++++------------ docker/gunicorn_conf.py | 6 +-- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index e0fea293..d06e8a59 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ ansible/test.json */.python-version FastapiOpenRestyConfigurator/.env template_path -backend_path \ No newline at end of file +backend_path +*/plans \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 7f0cad06..c2cf4c28 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,9 @@ -FROM ubuntu:24.04 +# --- Build Stage --- +FROM ubuntu:24.04 AS build-stage -ENV FORC_BACKEND_PATH=/var/forc/backend_path/ -ENV FORC_TEMPLATE_PATH=/var/forc/template_path/ -ENV CONTAINERIZED=true -RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} +ENV DEBIAN_FRONTEND=noninteractive +# Install build-time dependencies RUN apt-get update && apt-get install -y \ git \ wget \ @@ -12,15 +11,18 @@ RUN apt-get update && apt-get install -y \ gnupg \ ca-certificates \ software-properties-common \ + lsb-release \ python3 \ python3-pip \ python3-venv \ - lsb-release \ luarocks \ - cron \ + gcc \ + make \ && rm -rf /var/lib/apt/lists/* -# Install OpenResty +# Install OpenResty in build stage to prepare artifacts if needed +# However, OpenResty is a system package, we'll install it in runtime too. +# But we need it here for luarocks. RUN wget -qO- https://openresty.org/package/pubkey.gpg \ | gpg --dearmor \ -o /usr/share/keyrings/openresty.gpg \ @@ -30,35 +32,76 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ && apt-get install -y openresty \ && rm -rf /var/lib/apt/lists/* +# Install Lua modules +RUN luarocks --lua-version=5.1 install lua-resty-openidc -RUN luarocks --lua-version=5.1 install lua-resty-openidc -RUN git clone \ - --branch refactor/docker \ - --single-branch \ - --depth 1 \ - https://github.com/deNBI/simpleVMWebGateway.git \ - /opt/simpleVMWebGateway/ -WORKDIR /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator - +# Prepare Python virtual environment +COPY FastapiOpenRestyConfigurator/requirements.txt /tmp/requirements.txt RUN python3 -m venv /opt/venv \ && /opt/venv/bin/pip install --upgrade pip \ - && /opt/venv/bin/pip install -r requirements.txt \ + && /opt/venv/bin/pip install -r /tmp/requirements.txt \ && /opt/venv/bin/pip install gunicorn uvicorn +# --- Runtime Stage --- +FROM ubuntu:24.04 AS runtime-stage + +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV CONTAINERIZED=true +ENV FORC_BACKEND_PATH=/var/forc/backend_path/ +ENV FORC_TEMPLATE_PATH=/var/forc/template_path/ ENV PATH="/opt/venv/bin:$PATH" -COPY nginx.conf /etc/openresty/nginx.conf.j2 -COPY render_nginx.py /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ -COPY gunicorn_conf.py gunicorn_conf.py -COPY launch.sh launch.sh +# Install runtime-only dependencies +RUN apt-get update && apt-get install -y \ + python3 \ + ca-certificates \ + curl \ + libcap2 \ + && rm -rf /var/lib/apt/lists/* + +# Install OpenResty +RUN wget -qO- https://openresty.org/package/pubkey.gpg \ + | gpg --dearmor \ + -o /usr/share/keyrings/openresty.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" \ + > /etc/apt/sources.list.d/openresty.list \ + && apt-get update \ + && apt-get install -y openresty \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN groupadd -r gatewayuser && useradd -r -g gatewayuser -s /sbin/nologin gatewayuser + +# Copy artifacts from build-stage +COPY --from=build-stage /opt/venv /opt/venv +COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 +COPY --from=build-stage /usr/local/lib/lua/5.1 /usr/local/lib/lua/5.1 + +# Copy application code and configurations +WORKDIR /opt/simpleVMWebGateway +COPY FastapiOpenRestyConfigurator /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator + +# Copy Docker-specific files (build context is project root) +COPY docker/nginx.conf /etc/openresty/nginx.conf.j2 +COPY docker/render_nginx.py /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ +COPY docker/gunicorn_conf.py /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ +COPY docker/launch.sh /opt/launch.sh +COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ + +# Setup permissions +RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ + && chown -R gatewayuser:gatewayuser /opt/simpleVMWebGateway \ + && chown -R gatewayuser:gatewayuser /opt/scripts \ + && chown -R gatewayuser:gatewayuser /etc/openresty \ + && chown -R gatewayuser:gatewayuser ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} \ + && chmod +x /opt/launch.sh /opt/scripts/generate_ip_blocklists.sh -RUN mkdir -p /opt/scripts -COPY generate_ip_blocklists.sh ip_blocklists.txt /opt/scripts/ -RUN chmod +x /opt/scripts/generate_ip_blocklists.sh +# Allow non-root user to bind to privileged ports +RUN setcap 'cap_net_bind_service=+ep' /usr/sbin/openresty -RUN echo "0 */2 * * * root /opt/scripts/generate_ip_blocklists.sh && /usr/sbin/openresty -s reload" > /etc/cron.d/ip-blocklist \ - && chmod 0644 /etc/cron.d/ip-blocklist +USER gatewayuser -RUN chmod +x launch.sh EXPOSE 5000 -CMD ["./launch.sh"] \ No newline at end of file +CMD ["/opt/launch.sh"] diff --git a/docker/gunicorn_conf.py b/docker/gunicorn_conf.py index d3898e79..15e6c62d 100644 --- a/docker/gunicorn_conf.py +++ b/docker/gunicorn_conf.py @@ -1,5 +1,5 @@ # Socket Path -bind = "unix:/var/run/forc.sock" +bind = "unix:/tmp/forc.sock" # Worker Options workers = 5 @@ -7,5 +7,5 @@ # Logging Options loglevel = "info" -accesslog = "/var/log/forc.access.log" -errorlog = "/var/log/forc.error.log" \ No newline at end of file +accesslog = "/tmp/forc.access.log" +errorlog = "/tmp/forc.error.log" \ No newline at end of file From b24cc0aea304897f0b8b81637055083967f11b77 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 12:19:03 +0000 Subject: [PATCH 088/118] adde missing dep --- docker/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index c2cf4c28..6b77eef8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -58,6 +58,9 @@ RUN apt-get update && apt-get install -y \ python3 \ ca-certificates \ curl \ + wget \ + gnupg \ + lsb-release \ libcap2 \ && rm -rf /var/lib/apt/lists/* From a3308849be5065f97a0749e10407363331e0df54 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 13:16:28 +0000 Subject: [PATCH 089/118] updated Dockerfile --- docker/Dockerfile | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 6b77eef8..d48bae53 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -61,7 +61,7 @@ RUN apt-get update && apt-get install -y \ wget \ gnupg \ lsb-release \ - libcap2 \ + libcap2-bin \ && rm -rf /var/lib/apt/lists/* # Install OpenResty @@ -80,7 +80,21 @@ RUN groupadd -r gatewayuser && useradd -r -g gatewayuser -s /sbin/nologin gatewa # Copy artifacts from build-stage COPY --from=build-stage /opt/venv /opt/venv COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 -COPY --from=build-stage /usr/local/lib/lua/5.1 /usr/local/lib/lua/5.1 +# Lua modules might only be in share or lib depending on the package; +# we'll use a wildcard or conditional copy if supported, but for standard +# luarocks on Ubuntu, share is the primary. Let's try to copy lib only if it exists +# by using a shell command or just omitting it if it's not there. +# Since COPY fails if the source doesn't exist, we can combine this into a RUN command +# or just rely on the most common path. +# Actually, let's use a more robust way: copy the whole lua directory or check. +# For now, I'll remove the problematic line and add a note. +# Better yet, let's just copy /usr/local/lib/lua if it exists via a shell script in build stage. +# Or simpler: copy /usr/local/lib/lua/5.1 /usr/local/lib/lua/5.1 but wrap it. +# Actually, the most reliable way in Docker is to copy the parent directory if you're unsure. +# Let's just copy /usr/local/share/lua/5.1 as that's where most resty modules go. +# If lib is needed, we can check where luarocks puts them. +# I will remove the failing line and add a check. +COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 # Copy application code and configurations WORKDIR /opt/simpleVMWebGateway @@ -102,7 +116,9 @@ RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ && chmod +x /opt/launch.sh /opt/scripts/generate_ip_blocklists.sh # Allow non-root user to bind to privileged ports -RUN setcap 'cap_net_bind_service=+ep' /usr/sbin/openresty +# OpenResty's /usr/bin/openresty is often a shell script wrapper. +# We need to set capabilities on the actual nginx binary. +RUN setcap 'cap_net_bind_service=+ep' /usr/local/openresty/nginx/sbin/nginx USER gatewayuser From 2d70098185a9666e9317dd8463430ed6a555bf4f Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 13:21:07 +0000 Subject: [PATCH 090/118] updated permissions --- docker/generate_ip_blocklists.sh | 2 +- docker/launch.sh | 2 +- docker/nginx.conf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/generate_ip_blocklists.sh b/docker/generate_ip_blocklists.sh index c1c54dee..9408acf3 100644 --- a/docker/generate_ip_blocklists.sh +++ b/docker/generate_ip_blocklists.sh @@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Paths relative to script directory INPUT="$SCRIPT_DIR/ip_blocklists.txt" -OUTPUT="/etc/openresty/block_ips_geo.conf" +OUTPUT="/tmp/block_ips_geo.conf" TEMP="$SCRIPT_DIR/block_tmp.txt" rm -f "$TEMP" "$OUTPUT" diff --git a/docker/launch.sh b/docker/launch.sh index b0b3942c..19483305 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -3,7 +3,7 @@ set -e echo "Generating Block IPs for startup.." ## only needed if not present -if [ ! -f /etc/openresty/block_ips_geo.conf ]; then +if [ ! -f /tmp/block_ips_geo.conf ]; then /opt/scripts/generate_ip_blocklists.sh fi diff --git a/docker/nginx.conf b/docker/nginx.conf index 2bfb6af2..571ef99f 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -85,7 +85,7 @@ http { # Blocked IPs geo $blocked_ip { default 0; - include /etc/openresty/block_ips_geo.conf; + include /tmp/block_ips_geo.conf; } From b0c7ba5b89b04a2ed8557a4af5c67c2356af4128 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 13:23:11 +0000 Subject: [PATCH 091/118] updated permissions --- docker/launch.sh | 4 ++-- docker/render_nginx.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/launch.sh b/docker/launch.sh index 19483305..f6843a43 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -11,8 +11,8 @@ echo "Rendering OpenResty configuration..." python3 /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/render_nginx.py echo "Starting OpenResty..." -# Start openresty in background with a writable PID file -openresty -g "pid /tmp/openresty.pid" +# Start openresty in background with a writable PID file and config file +openresty -g "pid /tmp/openresty.pid" -c /tmp/nginx.conf echo "Starting Blocklist Updater Loop..." ( diff --git a/docker/render_nginx.py b/docker/render_nginx.py index 2527dc88..46b46988 100644 --- a/docker/render_nginx.py +++ b/docker/render_nginx.py @@ -3,7 +3,7 @@ from jinja2 import Environment, FileSystemLoader TEMPLATE_DIR = "/etc/openresty/" -OUTPUT_FILE = "/etc/openresty/nginx.conf" +OUTPUT_FILE = "/tmp/nginx.conf" def main(): From 74dd905f798dfc85c6aeb95e58c63c6fe8d7ef79 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 13:26:21 +0000 Subject: [PATCH 092/118] updated permissions --- docker/launch.sh | 5 ++--- docker/nginx.conf | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/launch.sh b/docker/launch.sh index f6843a43..220640cd 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -11,8 +11,7 @@ echo "Rendering OpenResty configuration..." python3 /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/render_nginx.py echo "Starting OpenResty..." -# Start openresty in background with a writable PID file and config file -openresty -g "pid /tmp/openresty.pid" -c /tmp/nginx.conf +openresty -c /tmp/nginx.conf echo "Starting Blocklist Updater Loop..." ( @@ -20,7 +19,7 @@ echo "Starting Blocklist Updater Loop..." sleep 7200 # 2 hours echo "$(date): Updating blocklists..." /opt/scripts/generate_ip_blocklists.sh - openresty -g "pid /tmp/openresty.pid" -s reload + openresty -g "pid /tmp/openresty.pid;" -s reload done ) & diff --git a/docker/nginx.conf b/docker/nginx.conf index 571ef99f..ab7bbf2d 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,7 +1,8 @@ worker_processes {{ OPENRESTY_WORKER_PROCESSES }}; -# /usr/local/openresty/nginx/logs/ -error_log logs/error.log notice; +# /tmp/error.log +error_log /tmp/error.log notice; +pid /tmp/openresty.pid; events { worker_connections 1024; From 9941165f560a98429110307abe105340ecb748e5 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 14:02:03 +0000 Subject: [PATCH 093/118] reset to root --- docker/Dockerfile | 15 --------------- docker/generate_ip_blocklists.sh | 2 +- docker/gunicorn_conf.py | 6 +++--- docker/launch.sh | 6 +++--- docker/nginx.conf | 11 +++++------ docker/render_nginx.py | 2 +- 6 files changed, 13 insertions(+), 29 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d48bae53..56805a33 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -61,7 +61,6 @@ RUN apt-get update && apt-get install -y \ wget \ gnupg \ lsb-release \ - libcap2-bin \ && rm -rf /var/lib/apt/lists/* # Install OpenResty @@ -74,9 +73,6 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ && apt-get install -y openresty \ && rm -rf /var/lib/apt/lists/* -# Create non-root user -RUN groupadd -r gatewayuser && useradd -r -g gatewayuser -s /sbin/nologin gatewayuser - # Copy artifacts from build-stage COPY --from=build-stage /opt/venv /opt/venv COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 @@ -109,18 +105,7 @@ COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ # Setup permissions RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ - && chown -R gatewayuser:gatewayuser /opt/simpleVMWebGateway \ - && chown -R gatewayuser:gatewayuser /opt/scripts \ - && chown -R gatewayuser:gatewayuser /etc/openresty \ - && chown -R gatewayuser:gatewayuser ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} \ && chmod +x /opt/launch.sh /opt/scripts/generate_ip_blocklists.sh -# Allow non-root user to bind to privileged ports -# OpenResty's /usr/bin/openresty is often a shell script wrapper. -# We need to set capabilities on the actual nginx binary. -RUN setcap 'cap_net_bind_service=+ep' /usr/local/openresty/nginx/sbin/nginx - -USER gatewayuser - EXPOSE 5000 CMD ["/opt/launch.sh"] diff --git a/docker/generate_ip_blocklists.sh b/docker/generate_ip_blocklists.sh index 9408acf3..c1c54dee 100644 --- a/docker/generate_ip_blocklists.sh +++ b/docker/generate_ip_blocklists.sh @@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Paths relative to script directory INPUT="$SCRIPT_DIR/ip_blocklists.txt" -OUTPUT="/tmp/block_ips_geo.conf" +OUTPUT="/etc/openresty/block_ips_geo.conf" TEMP="$SCRIPT_DIR/block_tmp.txt" rm -f "$TEMP" "$OUTPUT" diff --git a/docker/gunicorn_conf.py b/docker/gunicorn_conf.py index 15e6c62d..d3898e79 100644 --- a/docker/gunicorn_conf.py +++ b/docker/gunicorn_conf.py @@ -1,5 +1,5 @@ # Socket Path -bind = "unix:/tmp/forc.sock" +bind = "unix:/var/run/forc.sock" # Worker Options workers = 5 @@ -7,5 +7,5 @@ # Logging Options loglevel = "info" -accesslog = "/tmp/forc.access.log" -errorlog = "/tmp/forc.error.log" \ No newline at end of file +accesslog = "/var/log/forc.access.log" +errorlog = "/var/log/forc.error.log" \ No newline at end of file diff --git a/docker/launch.sh b/docker/launch.sh index 220640cd..d7aa27cd 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -3,7 +3,7 @@ set -e echo "Generating Block IPs for startup.." ## only needed if not present -if [ ! -f /tmp/block_ips_geo.conf ]; then +if [ ! -f /etc/openresty/block_ips_geo.conf ]; then /opt/scripts/generate_ip_blocklists.sh fi @@ -11,7 +11,7 @@ echo "Rendering OpenResty configuration..." python3 /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/render_nginx.py echo "Starting OpenResty..." -openresty -c /tmp/nginx.conf +openresty echo "Starting Blocklist Updater Loop..." ( @@ -19,7 +19,7 @@ echo "Starting Blocklist Updater Loop..." sleep 7200 # 2 hours echo "$(date): Updating blocklists..." /opt/scripts/generate_ip_blocklists.sh - openresty -g "pid /tmp/openresty.pid;" -s reload + openresty -s reload done ) & diff --git a/docker/nginx.conf b/docker/nginx.conf index ab7bbf2d..ea13ce3a 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,8 +1,7 @@ worker_processes {{ OPENRESTY_WORKER_PROCESSES }}; -# /tmp/error.log -error_log /tmp/error.log notice; -pid /tmp/openresty.pid; +# /usr/local/openresty/nginx/logs/ +error_log logs/error.log notice; events { worker_connections 1024; @@ -86,7 +85,7 @@ http { # Blocked IPs geo $blocked_ip { default 0; - include /tmp/block_ips_geo.conf; + include /etc/openresty/block_ips_geo.conf; } @@ -131,7 +130,7 @@ http { location / { - proxy_pass http://unix:/tmp/forc.sock; + proxy_pass http://unix:/var/run/forc.sock; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -171,7 +170,7 @@ http { location / { - proxy_pass http://unix:/tmp/forc.sock; + proxy_pass http://unix:/var/run/forc.sock; proxy_set_header Host $host; diff --git a/docker/render_nginx.py b/docker/render_nginx.py index 46b46988..2527dc88 100644 --- a/docker/render_nginx.py +++ b/docker/render_nginx.py @@ -3,7 +3,7 @@ from jinja2 import Environment, FileSystemLoader TEMPLATE_DIR = "/etc/openresty/" -OUTPUT_FILE = "/tmp/nginx.conf" +OUTPUT_FILE = "/etc/openresty/nginx.conf" def main(): From fe60a82d04f00719378c5631c2411cf2b7af8c23 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 14:09:03 +0000 Subject: [PATCH 094/118] fixed gunicorn path --- docker/Dockerfile | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 56805a33..2bbb1f42 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -76,24 +76,10 @@ RUN wget -qO- https://openresty.org/package/pubkey.gpg \ # Copy artifacts from build-stage COPY --from=build-stage /opt/venv /opt/venv COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 -# Lua modules might only be in share or lib depending on the package; -# we'll use a wildcard or conditional copy if supported, but for standard -# luarocks on Ubuntu, share is the primary. Let's try to copy lib only if it exists -# by using a shell command or just omitting it if it's not there. -# Since COPY fails if the source doesn't exist, we can combine this into a RUN command -# or just rely on the most common path. -# Actually, let's use a more robust way: copy the whole lua directory or check. -# For now, I'll remove the problematic line and add a note. -# Better yet, let's just copy /usr/local/lib/lua if it exists via a shell script in build stage. -# Or simpler: copy /usr/local/lib/lua/5.1 /usr/local/lib/lua/5.1 but wrap it. -# Actually, the most reliable way in Docker is to copy the parent directory if you're unsure. -# Let's just copy /usr/local/share/lua/5.1 as that's where most resty modules go. -# If lib is needed, we can check where luarocks puts them. -# I will remove the failing line and add a check. -COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 + # Copy application code and configurations -WORKDIR /opt/simpleVMWebGateway +WORKDIR /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator COPY FastapiOpenRestyConfigurator /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator # Copy Docker-specific files (build context is project root) From dc6ce4f556ac7ffcc5a4eae3062a8e177671f82d Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 14:15:21 +0000 Subject: [PATCH 095/118] copy examples --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2bbb1f42..94a84956 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -81,7 +81,6 @@ COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 # Copy application code and configurations WORKDIR /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator COPY FastapiOpenRestyConfigurator /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator - # Copy Docker-specific files (build context is project root) COPY docker/nginx.conf /etc/openresty/nginx.conf.j2 COPY docker/render_nginx.py /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ @@ -92,6 +91,7 @@ COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ # Setup permissions RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ && chmod +x /opt/launch.sh /opt/scripts/generate_ip_blocklists.sh +COPY examples/templates ${FORC_TEMPLATE_PATH} EXPOSE 5000 CMD ["/opt/launch.sh"] From 1564d13a38de1dba003ebaaaf3aae007cef6cf68 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 14:32:45 +0000 Subject: [PATCH 096/118] fixed typo --- .../app/main/config.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/config.py b/FastapiOpenRestyConfigurator/app/main/config.py index 6d52fbba..6053a611 100644 --- a/FastapiOpenRestyConfigurator/app/main/config.py +++ b/FastapiOpenRestyConfigurator/app/main/config.py @@ -2,7 +2,8 @@ from functools import lru_cache from pydantic_settings import BaseSettings -from pydantic import SecretStr, validator, DirectoryPath +from pydantic import SecretStr, field_validator, DirectoryPath +from pydantic import field_validator, ValidationInfo basedir = os.path.abspath(os.path.dirname(__file__)) @@ -21,22 +22,24 @@ class Settings(BaseSettings): FORC_BACKEND_PATH: DirectoryPath FORC_TEMPLATE_PATH: DirectoryPath FORC_USER_PATH: str = "users" - CONTAINERZIED: bool = False + CONTAINERIZED: bool = False - @validator("FORC_USER_PATH", pre=True) - def apply_backend_path(cls, v, values): + @field_validator("FORC_USER_PATH", mode="before") + @classmethod + def apply_backend_path(cls, v, info: ValidationInfo): """ Validates forc user path, as it depends on forc backend path. :param v: Value for forc user path. - :param values: Values already read for settings object. + :param info: Validation context containing already validated fields. :return: Updated FORC_USER_PATH. """ - # := assigns and compares a value. (if a := b:) == (a = b; if a:) - if FORC_BACKEND_PATH := values.get("FORC_BACKEND_PATH"): - return f"{FORC_BACKEND_PATH}/{v}" - else: - # should only happen when there was an error with FORC_BACKEND_PATH - return "/var/forc/backend_path/users" + forc_backend_path = info.data.get("FORC_BACKEND_PATH") + + if forc_backend_path: + return f"{forc_backend_path}/{v}" + + # should only happen when there was an error with FORC_BACKEND_PATH + return "/var/forc/backend_path/users" class Config: """ From 6bde1c841d32c6b27bbae3f15aab98dfffc30ea8 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 14:42:44 +0000 Subject: [PATCH 097/118] updated path --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 94a84956..61cc5375 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -79,7 +79,7 @@ COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 # Copy application code and configurations -WORKDIR /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator +WORKDIR /opt/simpleVMWebGateway/ COPY FastapiOpenRestyConfigurator /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator # Copy Docker-specific files (build context is project root) COPY docker/nginx.conf /etc/openresty/nginx.conf.j2 From 212547415f9191940571320b2aa442d5ddebbf5a Mon Sep 17 00:00:00 2001 From: dweinholz Date: Wed, 29 Jul 2026 14:52:19 +0000 Subject: [PATCH 098/118] updated path --- docker/Dockerfile | 8 ++++---- docker/launch.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 61cc5375..b7d7c91a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -79,12 +79,12 @@ COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 # Copy application code and configurations -WORKDIR /opt/simpleVMWebGateway/ -COPY FastapiOpenRestyConfigurator /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator +WORKDIR /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator +COPY FastapiOpenRestyConfigurator . # Copy Docker-specific files (build context is project root) COPY docker/nginx.conf /etc/openresty/nginx.conf.j2 -COPY docker/render_nginx.py /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ -COPY docker/gunicorn_conf.py /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/ +COPY docker/render_nginx.py . +COPY docker/gunicorn_conf.py . COPY docker/launch.sh /opt/launch.sh COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ diff --git a/docker/launch.sh b/docker/launch.sh index d7aa27cd..02ee91c9 100644 --- a/docker/launch.sh +++ b/docker/launch.sh @@ -8,7 +8,7 @@ if [ ! -f /etc/openresty/block_ips_geo.conf ]; then fi echo "Rendering OpenResty configuration..." -python3 /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/render_nginx.py +python3 render_nginx.py echo "Starting OpenResty..." openresty From 1a10fab0fd9383c26346e37d82bb7553699fd163 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 06:23:25 +0000 Subject: [PATCH 099/118] fixed import bug --- FastapiOpenRestyConfigurator/app/main/service/openresty.py | 2 +- docker/Dockerfile | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FastapiOpenRestyConfigurator/app/main/service/openresty.py b/FastapiOpenRestyConfigurator/app/main/service/openresty.py index 86fd1dc6..b3cf8931 100644 --- a/FastapiOpenRestyConfigurator/app/main/service/openresty.py +++ b/FastapiOpenRestyConfigurator/app/main/service/openresty.py @@ -5,7 +5,7 @@ import asyncio import logging -from FastapiOpenRestyConfigurator.app.main.config import get_settings +from app.main.config import get_settings settings = get_settings() diff --git a/docker/Dockerfile b/docker/Dockerfile index b7d7c91a..d5933d49 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -85,13 +85,13 @@ COPY FastapiOpenRestyConfigurator . COPY docker/nginx.conf /etc/openresty/nginx.conf.j2 COPY docker/render_nginx.py . COPY docker/gunicorn_conf.py . -COPY docker/launch.sh /opt/launch.sh +COPY docker/launch.sh . COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ # Setup permissions RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ - && chmod +x /opt/launch.sh /opt/scripts/generate_ip_blocklists.sh + && chmod +x launch.sh /opt/scripts/generate_ip_blocklists.sh COPY examples/templates ${FORC_TEMPLATE_PATH} EXPOSE 5000 -CMD ["/opt/launch.sh"] +CMD ["./launch.sh"] From 1c224ccfed5edca99143fc74eb7d76576b2088b2 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 07:02:19 +0000 Subject: [PATCH 100/118] readded monkeypatch --- docker/nginx.conf | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index ea13ce3a..82f04b11 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -49,37 +49,43 @@ http { # OIDC config - init_by_lua_block { +init_by_lua_block { + -- Monkey-patch lua-resty-session defaults + local session = require("resty.session") + local old_new = session.new - opts_session = { - cookie_name = "sess_auth", - secret = "{{ FORC_SECRET_KEY }}" - } + session.new = function(opts) + opts = opts or {} + opts.cookie_name = "sess_auth" + opts.secret = "{{ FORC_SECRET_KEY }}" - opts2 = { - redirect_uri = "https://{{ DOMAIN }}/redirect_uri", - discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", - client_id = "{{ FORC_OIDC_CLIENT_ID }}", - client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", + return old_new(opts) + end - logout_path = "/logout", + opts2 = { + redirect_uri = "https://{{ DOMAIN }}/redirect_uri", + discovery = "{{ FORC_OIDC_DISCOVERY_URL }}", + client_id = "{{ FORC_OIDC_CLIENT_ID }}", + client_secret = "{{ FORC_OIDC_CLIENT_SECRET }}", - ssl_verify = "no", + logout_path = "/logout", - iat_slack = 600, + ssl_verify = "no", - scope = "openid email profile offline_access", + iat_slack = 600, - renew_access_token_on_expiry = true, - access_token_expires_leeway = 60, + scope = "openid email profile offline_access", - session_contents = { - id_token = true, - access_token = true - } + renew_access_token_on_expiry = true, + access_token_expires_leeway = 60, + + session_contents = { + id_token = true, + access_token = true } } +} # Blocked IPs From 8722c6370c410772411c2ea719a9d857671ad0ea Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 07:40:49 +0000 Subject: [PATCH 101/118] feat(Actions):added docker build actions --- .github/workflows/build-image.yml | 54 +++++ .github/workflows/publish-docker.yml | 46 ++++ .github/workflows/release_image.yml | 43 ++++ .../app/main/config.py | 19 +- docker/README.md | 218 +++++++++--------- docker/docker-compose.yml | 8 +- 6 files changed, 259 insertions(+), 129 deletions(-) create mode 100644 .github/workflows/build-image.yml create mode 100644 .github/workflows/publish-docker.yml create mode 100644 .github/workflows/release_image.yml diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 00000000..4cf0f835 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,54 @@ +name: build-image +on: + pull_request: + workflow_dispatch: + +jobs: + build-test: + runs-on: [self-hosted, build] + steps: + - name: Workflow run cleanup action + uses: rokroskar/workflow-run-cleanup-action@ee1451b869ba1e381729b3d40489997021f0d562 # v0.3.3 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Build and publish image to OCI + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + with: + file: ./compose/production/django/Dockerfile + push: false + load: true + tags: localbuild/testimage:latest + cache-db: true + - name: Scan image (table) + if: always() + + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + with: + image: "localbuild/testimage:latest" + output-format: table + fail-build: true + severity-cutoff: critical + - name: Scan image (JSON) + if: always() + + id: scan-json + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + with: + image: "localbuild/testimage:latest" + output-format: json + fail-build: false + output-file: scan-results.json + - name: Check scan results + if: always() + run: | + MAX_RISK=$(jq '[.matches[].vulnerability.risk // empty] | if length > 0 then max else 0 end' scan-results.json) + echo "Max vulnerability risk: ${MAX_RISK}%" + CRITICAL=$(jq '[.matches[].vulnerability.severity] | map(select(. == "Critical")) | length' scan-results.json) + echo "Critical vulnerabilities: ${CRITICAL}" + if [ "${CRITICAL}" -gt 0 ] || [ "$(echo "${MAX_RISK} > 50" | bc)" -eq 1 ]; then + echo "::error::Scan failed: ${CRITICAL} critical CVE(s), max risk ${MAX_RISK}%" + exit 1 + fi diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 00000000..de8fb4f0 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,46 @@ +name: Publish Docker +on: + workflow_dispatch: + push: + branches: + - 'staging' + - 'dev' + - 'hotfix/**' +jobs: + build: + runs-on: [self-hosted,bielefeld] + concurrency: api + steps: + - name: Clean up repository directory (with elevated privileges) -.pytest_cache + shell: bash + run: | + sudo rm -rfv ${{ github.workspace }}/.pytest_cache + - name: Clean up repository directory (with elevated privileges) -.pytest_cache + shell: bash + run: | + sudo rm -rfv ${{ github.workspace }}/* + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + + - name: Set tag + run: sed 's/\//-/g' <<< "::set-output name=TAG::${{ steps.extract_branch.outputs.branch }}" + id: tag + - name: Get tag + run: echo "The selected tag is ${{ steps.tag.outputs.TAG }}" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + - name: Login to oci.bi.denbi.de + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4 + with: + registry: oci.bi.denbi.de + username: ${{ secrets.OCI_USERNAME }} + password: ${{ secrets.OCI_TOKEN }} + - name: Build and publish image to OCI + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + with: + file: compose/production/django/Dockerfile + push: true + tags: oci.bi.denbi.de/simplevm/forc:${{ steps.tag.outputs.TAG }} diff --git a/.github/workflows/release_image.yml b/.github/workflows/release_image.yml new file mode 100644 index 00000000..bc15207a --- /dev/null +++ b/.github/workflows/release_image.yml @@ -0,0 +1,43 @@ +name: Create Release Image +on: + push: + tags: + - '*' +jobs: + build_release: + runs-on: [self-hosted, production, build] + + steps: + - name: Clean up repository directory (with elevated privileges) -.pytest_cache + shell: bash + run: | + sudo rm -rfv ${{ github.workspace }}/.pytest_cache + - name: Clean up repository directory (with elevated privileges) -.pytest_cache + shell: bash + run: | + sudo rm -rfv ${{ github.workspace }}/* + - name: Get the version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + - name: Login to oci.bi.denbi.de + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4 + with: + registry: oci.bi.denbi.de + username: ${{ secrets.OCI_USERNAME }} + password: ${{ secrets.OCI_TOKEN }} + - name: Build and publish image to OCI + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + with: + file: compose/production/django/Dockerfile + push: true + tags: oci.bi.denbi.de/simplevm/forc:${{ steps.get_version.outputs.VERSION }} + load: true + - name: Generate SBOM + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 + with: + image: oci.bi.denbi.de/simplevm/forc:${{ steps.get_version.outputs.VERSION }} + format: spdx-json + output-file: oci.bi.denbi.de/simplevm/forc:${{ steps.get_version.outputs.VERSION }} diff --git a/FastapiOpenRestyConfigurator/app/main/config.py b/FastapiOpenRestyConfigurator/app/main/config.py index 6053a611..b047eb52 100644 --- a/FastapiOpenRestyConfigurator/app/main/config.py +++ b/FastapiOpenRestyConfigurator/app/main/config.py @@ -1,7 +1,7 @@ import os from functools import lru_cache -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic import SecretStr, field_validator, DirectoryPath from pydantic import field_validator, ValidationInfo @@ -41,17 +41,12 @@ def apply_backend_path(cls, v, info: ValidationInfo): # should only happen when there was an error with FORC_BACKEND_PATH return "/var/forc/backend_path/users" - class Config: - """ - Config for settings object. - """ - - # Enabled case sensitive for reading variables from .env file - case_sensitive = True - # Path to .env file - env_file = ".env" - # Encoding of .env file - env_file_encoding = "utf-8" + model_config = SettingsConfigDict( + case_sensitive=True, + env_file=".env", + env_file_encoding="utf-8", + extra="ignore" + ) @lru_cache() diff --git a/docker/README.md b/docker/README.md index f27372e5..0216fa92 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,114 +1,104 @@ -## FORC+OpenResty in Docker - -### Guide - -This folder contains the setup for FORC+OpenResty in Docker. - -In order to run this container, you need to provide it with a ready configured `nginx.conf` file. - -A minimal file could look this: - -```` -worker_processes 10; -# /usr/local/openresty/nginx/logs/ -error_log logs/error.log; -error_log logs/error.log notice; -error_log logs/error.log info; -error_log logs/error.log debug; -events { - worker_connections 1024; -} -http { - include mime.types; - default_type application/octet-stream; - #For some reason, nginx wants a hardcoded Name Resolver - resolver 8.8.8.8; - sendfile on; - keepalive_timeout 65; - #LUA caches for various session modules - lua_shared_dict discovery 1m; - lua_shared_dict jwks 1m; - lua_code_cache off; - - #Allow websockets by allowing general connection upgrade requests, theia needs websockets - map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } - - #Create global LUA variable which keeps our ELIXIR AAI Configuration dict - init_by_lua_block { - opts2 = { - redirect_uri = "http://reverseproxy.bibiserv.projects.bi.denbi.de/redirect_uri", - discovery = "https://login.elixir-czech.org/oidc/.well-known/openid-configuration", - client_id = "CLIENTID", - client_secret = "CLIENTSECRET", - logout_path = "/logout", - ssl_verify = "no" - } - } - - - server { - listen 5000; - location / { - include uwsgi_params; - uwsgi_pass unix:/var/run/forc.sock; - } - } - -server { - listen 0.0.0.0:80; - server_name reverseproxy.bibiserv.projects.bi.denbi.de; - set $session_secret fdhtzzu45z34t32g24f43; - - location / { - - access_by_lua_block { - -- Start actual openid authentication procedure - local res, err = require("resty.openidc").authenticate(opts2) - -- If it fails for some reason, escape via HTTP 500 - if err then - ngx.status = 500 - ngx.say(err) - ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) - end - - } - - } - - #Load all dynamicaly created locations. - include /var/forc/backend_path//*.conf; - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root html; - } - } -} - -```` - -In this config file, adjust your oidc-client credentials in the `opts2` field and make changes to `servername`. - -This container pushes port `80` for reverse-proxied webcontent and port `5000` for the forc api. - -Generate a strong API Key for forc. - -Summing up, you can then build the container and run it afterwards: - -```` -docker build -t forc . - -docker run -p 5656:5000 -p 5657:80 -v nginx.conf:/etc/openresty/nginx.conf -e "FORC_API_KEY=somerandompw" forc -```` - -Path for forc backends and templates are the default ones: - -```` -/var/forc/backend_path/ -/var/forc/template_path/ -```` - -For convenience, it is recommended to mount these folders as a volume to the container. +# Docker Deployment for FORC + +This directory contains the configuration and scripts necessary to build and run the FORC (FastAPI OpenResty Configurator) gateway in a Docker container. + +## Overview + +FORC is a hybrid gateway that leverages **OpenResty** (an extended Nginx) for high-performance request handling and **FastAPI** for dynamic configuration and management. + +The container automates the setup of: +- A dynamically rendered Nginx configuration based on environment variables. +- An automated IP blocklist update system. +- A FastAPI backend running via Gunicorn. + +## File Descriptions + +| File | Description | +| :--- | :--- | +| `Dockerfile` | Multi-stage build definition based on Ubuntu 24.04. Installs OpenResty, Python, and necessary Lua modules. | +| `docker-compose.yml` | Service orchestration defining ports, volumes, and environment variable sources. | +| `launch.sh` | Container entrypoint. Orchestrates the startup sequence: blocklist generation $\rightarrow$ Nginx rendering $\rightarrow$ OpenResty start $\rightarrow$ Background blocklist loop $\rightarrow$ FastAPI start. | +| `render_nginx.py` | A utility script that renders the Nginx template (`nginx.conf.j2`) using all current environment variables. | +| `generate_ip_blocklists.sh` | Downloads and processes IP blocklists from sources defined in `ip_blocklists.txt` into a format OpenResty can use. | +| `ip_blocklists.txt` | A list of external URLs providing IP blocklists. | +| `gunicorn_conf.py` | Configuration settings for the Gunicorn WSGI server. | +| `nginx.conf` | The Jinja2 template used by `render_nginx.py` to generate the final Nginx configuration. | + +## Container Internals + +### Architecture +- **OS**: Ubuntu 24.04 +- **Web Server**: OpenResty (Nginx + Lua) +- **Application**: FastAPI (Python 3) +- **WSGI Server**: Gunicorn + +### Key Directories +- `/opt/simpleVMWebGateway/FastapiOpenRestyConfigurator`: The application root. +- `/etc/openresty/`: Contains the generated `nginx.conf` and `block_ips_geo.conf`. +- `/var/forc/backend_path/`: Used for backend persistence. +- `/var/forc/template_path/`: Stores configuration templates. +- `/opt/scripts/`: Contains the blocklist generation scripts. + +### Startup Sequence +1. **Blocklist Generation**: `generate_ip_blocklists.sh` is run to create the initial `/etc/openresty/block_ips_geo.conf`. +2. **Configuration Rendering**: `render_nginx.py` takes all system environment variables and applies them to the Nginx template. +3. **OpenResty Startup**: The OpenResty server is started to handle incoming traffic on ports 80 and 443. +4. **Blocklist Updater**: A background process is spawned that refreshes the IP blocklists every 2 hours and reloads OpenResty. +5. **FastAPI Startup**: The FastAPI application is started via Gunicorn on port 5000. + +## Configuration + +### Environment Variables (`.env`) + +FORC uses a single `.env` file for both the OpenResty configuration and the FastAPI application. + +#### 1. OpenResty / Gateway Settings +These variables are used by `render_nginx.py` to generate the final Nginx configuration. + +| Variable | Description | Example | +| :--- | :--- | :--- | +| `DOMAIN` | The primary domain name for the gateway. | `gateway.example.com` | +| `FORC_SERVICE_PORT` | Port for the internal service. | `443` | +| `FORC_SERVICE_USE_HTTPS` | Whether to use HTTPS for the internal service. | `True` / `False` | +| `FORC_LOCAL_IP` | Local IP to bind to if HTTPS is disabled. | `0.0.0.0` | +| `OPENRESTY_DNS_SERVER` | DNS server for OpenResty resolver. | `8.8.8.8` | +| `OPENRESTY_WORKER_PROCESSES` | Number of Nginx worker processes. | `auto` or `10` | +| `FORC_OIDC_DISCOVERY_URL` | OIDC discovery endpoint. | `https://auth.example.com/...` | +| `FORC_OIDC_CLIENT_ID` | Client ID assigned by OIDC provider. | `my-client-id` | +| `FORC_OIDC_CLIENT_SECRET` | Client secret assigned by OIDC provider. | `secret-string` | +| `FORC_SECRET_KEY` | Secret key for session signing. | `random-long-string` | + +#### 2. FastAPI Application Settings +Required for the management API and backend logic. + +| Variable | Description | Example | +| :--- | :--- | :--- | +| `FORC_API_KEY` | API key for authenticating with the FORC API. | `secure-api-key` | +| `FORC_SECRET_KEY` | Secret key for session signing. | `random-long-string` | +| `FORC_BACKEND_PATH` | Path for backend persistence. | `/var/forc/backend_path` | +| `FORC_TEMPLATE_PATH` | Path for configuration templates. | `/var/forc/template_path` | +| `DEBUG` | Enable debug mode. | `True` / `False` | +| `LOG_LEVEL` | Logging verbosity. | `INFO`, `DEBUG`, `WARNING` | + +### Mounts & Volumes + +The following mounts are configured in `docker-compose.yml`: + +| Host Path | Container Path | Mode | Purpose | +| :--- | :--- | :--- | :--- | +| `/var/forc/backend_path/` | `/var/forc/backend_path/` | `rw` | Persistence for the backend database/files. | +| `/etc/letsencrypt/` | `/etc/letsencrypt/` | `r` | SSL certificates for HTTPS. | +| `.env` | `/opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/.env` | `rw` | Unified environment configuration. | + +## Usage + +1. Create a `.env` file containing all required variables from the tables above. +2. Start the gateway: +```bash +docker-compose up -d +``` + +To view logs: +```bash +docker-compose logs -f +``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c750b9fe..cd84be59 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -3,10 +3,12 @@ services: image: forc container_name: forc env_file: - - ../FastapiOpenRestyConfigurator/.env + - ../FastapiOpenRestyConfigurator/.env # envs for openresty volumes: - - /var/forc/backend_path/:/var/forc/backend_path/:rw - - /var/forc/template_path/:/var/forc/template_path/:rw + - /var/forc/backend_path/:/var/forc/backend_path/:rw # for persistence should be mounted + #- /var/forc/template_path/:/var/forc/template_path/:rw # optional default has the exmaples/templates + - /etc/letsencrypt/:/etc/letsencrypt/:r #needs to provided for cert + - .env.forc:/opt/simpleVMWebGateway/FastapiOpenRestyConfigurator/.env # needs to be mounted for roc ports: - 0.0.0.0:5000:5000 - 0.0.0.0:80:80 From dd6804623ebd6e0b0da0bf8e38e7249b8b371da8 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 07:51:31 +0000 Subject: [PATCH 102/118] added html pages --- docker/Dockerfile | 2 +- docker/html/403.html | 47 ++++++++++++++++++++++++++++++++++++++++++ docker/html/404.html | 40 +++++++++++++++++++++++++++++++++++ docker/html/50x.html | 42 +++++++++++++++++++++++++++++++++++++ docker/html/index.html | 33 +++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 docker/html/403.html create mode 100644 docker/html/404.html create mode 100644 docker/html/50x.html create mode 100644 docker/html/index.html diff --git a/docker/Dockerfile b/docker/Dockerfile index d5933d49..3d8cb63d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -87,7 +87,7 @@ COPY docker/render_nginx.py . COPY docker/gunicorn_conf.py . COPY docker/launch.sh . COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ - +COPY html /usr/local/openresty/nginx # Setup permissions RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ && chmod +x launch.sh /opt/scripts/generate_ip_blocklists.sh diff --git a/docker/html/403.html b/docker/html/403.html new file mode 100644 index 00000000..7fddcaa5 --- /dev/null +++ b/docker/html/403.html @@ -0,0 +1,47 @@ + + + + + + + Access forbidden! + + + +

403: You are not authorized to access this research environment!

+
    +
  • + In case of an error, + please contact support. You may find the appropriate mail address + + on our Support page. + +
  • +
  • + The owner of the virtual machine might need to authorize you first, please have a look at + + our guide in our Wiki + + for more information. +
  • +
  • + On the + + instance overview + + you may find the appropriate URL for your research environment. +
  • +
  • + Please have a look on our + Wiki + to find more information about research environments in general. +
  • +
+ + diff --git a/docker/html/404.html b/docker/html/404.html new file mode 100644 index 00000000..da01ee35 --- /dev/null +++ b/docker/html/404.html @@ -0,0 +1,40 @@ + + + + + + + No research environment found! + + + +

404: No research environment was found at this URL!

+
    +
  • + In case there should be a research environment at this URL, + please contact support. You may find the appropriate mail address + + on our Support page. + +
  • +
  • + On the + + instance overview + + you may find the appropriate URL for your research environment. +
  • +
  • + Please have a look on our + Wiki + to find more information about research environments in general. +
  • +
+ + diff --git a/docker/html/50x.html b/docker/html/50x.html new file mode 100644 index 00000000..97200e19 --- /dev/null +++ b/docker/html/50x.html @@ -0,0 +1,42 @@ + + + + + + + An error occurred! + + + +

50x: An error occurred when trying to access this research environment!

+
    +
  • + Please have a look on the + + instance overview + + if your virtual machine is active and if the research environment is set up correctly.
    + An error may occur if your virtual machine is not active or if the research environment is not + set up correctly. +
  • +
  • + In case of an error, + please contact support. You may find the appropriate mail address + + on our Support page. + +
  • +
  • + Please have a look on our + Wiki + to find more information about research environments in general. +
  • +
+ + diff --git a/docker/html/index.html b/docker/html/index.html new file mode 100644 index 00000000..0b3d5e3e --- /dev/null +++ b/docker/html/index.html @@ -0,0 +1,33 @@ + + + + + + + No research environment entered! + + + +

If you see this page, you have not entered a research environment URL!

+
    +
  • + Please have a look on our + Wiki + to find more information about research environments in general. +
  • +
  • + On the + + instance overview + + you may find the appropriate URL for your research environment. +
  • +
+ + From 65a1a6cb98193a1eb1fb5ecab96442e697172231 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 07:52:04 +0000 Subject: [PATCH 103/118] added html pages --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3d8cb63d..3f5a5a81 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -87,7 +87,7 @@ COPY docker/render_nginx.py . COPY docker/gunicorn_conf.py . COPY docker/launch.sh . COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ -COPY html /usr/local/openresty/nginx +COPY docker/html /usr/local/openresty/nginx # Setup permissions RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ && chmod +x launch.sh /opt/scripts/generate_ip_blocklists.sh From 7fa06c33f2c9f65a65da4550b63eef7f34216275 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 07:54:42 +0000 Subject: [PATCH 104/118] added html pages --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3f5a5a81..04ae3d26 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -87,7 +87,7 @@ COPY docker/render_nginx.py . COPY docker/gunicorn_conf.py . COPY docker/launch.sh . COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ -COPY docker/html /usr/local/openresty/nginx +COPY docker/html /usr/local/openresty/nginx/html # Setup permissions RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ && chmod +x launch.sh /opt/scripts/generate_ip_blocklists.sh From d637d5ee3f317a8ee12032bd5152aa26b400a123 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:07:24 +0000 Subject: [PATCH 105/118] feat(Dependencies): Update github/codeql-action digest to e4fba86 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4554ec9b..fd9b310e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 + uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 with: languages: ${{ matrix.language }} queries: +security-extended, security-and-quality @@ -51,7 +51,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 + uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -65,4 +65,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 + uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 From 313efaaa13e7fda423af0b4cc287c51dc479a40b Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 10:21:56 +0200 Subject: [PATCH 106/118] Update GitHub Actions runner to ubuntu-latest Changed the runner environment from self-hosted to ubuntu-latest. --- .github/workflows/publish-docker.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index de8fb4f0..77b3e2eb 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -8,8 +8,7 @@ on: - 'hotfix/**' jobs: build: - runs-on: [self-hosted,bielefeld] - concurrency: api + runs-on: ubuntu-latest steps: - name: Clean up repository directory (with elevated privileges) -.pytest_cache shell: bash From 9831e2d5ff9aab59b9903e55a6aa034994d06033 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 10:22:04 +0200 Subject: [PATCH 107/118] Update release_image.yml --- .github/workflows/release_image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_image.yml b/.github/workflows/release_image.yml index bc15207a..6a47b53c 100644 --- a/.github/workflows/release_image.yml +++ b/.github/workflows/release_image.yml @@ -5,7 +5,7 @@ on: - '*' jobs: build_release: - runs-on: [self-hosted, production, build] + runs-on: ubuntu-latest steps: - name: Clean up repository directory (with elevated privileges) -.pytest_cache From f9cdbfaae403dcdfdc0443a5ed3cea29fbbe69ee Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 10:22:12 +0200 Subject: [PATCH 108/118] Update build-image.yml --- .github/workflows/build-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 4cf0f835..93400b05 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -5,7 +5,7 @@ on: jobs: build-test: - runs-on: [self-hosted, build] + runs-on: ubuntu-latest steps: - name: Workflow run cleanup action uses: rokroskar/workflow-run-cleanup-action@ee1451b869ba1e381729b3d40489997021f0d562 # v0.3.3 From 28748d445f4d16d085d110c9e8ca684fe51291b9 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 10:39:46 +0200 Subject: [PATCH 109/118] Update Dockerfile path in publish workflow --- .github/workflows/publish-docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 77b3e2eb..083fb0cc 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -40,6 +40,6 @@ jobs: - name: Build and publish image to OCI uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: - file: compose/production/django/Dockerfile + file: docker/Dockerfile push: true tags: oci.bi.denbi.de/simplevm/forc:${{ steps.tag.outputs.TAG }} From a18e4db6db821a4a84760966e764e32437bb187d Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 10:40:00 +0200 Subject: [PATCH 110/118] Update Dockerfile path in release workflow --- .github/workflows/release_image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_image.yml b/.github/workflows/release_image.yml index 6a47b53c..786844ef 100644 --- a/.github/workflows/release_image.yml +++ b/.github/workflows/release_image.yml @@ -31,7 +31,7 @@ jobs: - name: Build and publish image to OCI uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: - file: compose/production/django/Dockerfile + file: docker/Dockerfile push: true tags: oci.bi.denbi.de/simplevm/forc:${{ steps.get_version.outputs.VERSION }} load: true From 19b36c121b01649d55f7298fe19ab99a5aca5538 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 10:40:21 +0200 Subject: [PATCH 111/118] Change Dockerfile path in build-image workflow --- .github/workflows/build-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 93400b05..062ff6f4 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -17,7 +17,7 @@ jobs: id: build uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: - file: ./compose/production/django/Dockerfile + file: docker/Dockerfile push: false load: true tags: localbuild/testimage:latest From 59e1879294add5c51816abce68945453ff2f9cc0 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 09:50:52 +0000 Subject: [PATCH 112/118] testing alpine dockerfile --- docker/Dockerfile | 74 +++++++++++++++-------------------------------- 1 file changed, 24 insertions(+), 50 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 04ae3d26..025bca21 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,39 +1,22 @@ # --- Build Stage --- -FROM ubuntu:24.04 AS build-stage - -ENV DEBIAN_FRONTEND=noninteractive +FROM openresty/openresty:alpine AS build-stage # Install build-time dependencies -RUN apt-get update && apt-get install -y \ - git \ - wget \ - curl \ - gnupg \ - ca-certificates \ - software-properties-common \ - lsb-release \ - python3 \ - python3-pip \ - python3-venv \ - luarocks \ +RUN apk add --no-cache \ + bash \ gcc \ make \ - && rm -rf /var/lib/apt/lists/* - -# Install OpenResty in build stage to prepare artifacts if needed -# However, OpenResty is a system package, we'll install it in runtime too. -# But we need it here for luarocks. -RUN wget -qO- https://openresty.org/package/pubkey.gpg \ - | gpg --dearmor \ - -o /usr/share/keyrings/openresty.gpg \ - && echo "deb [signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" \ - > /etc/apt/sources.list.d/openresty.list \ - && apt-get update \ - && apt-get install -y openresty \ - && rm -rf /var/lib/apt/lists/* + musl-dev \ + lua5.1-dev \ + git \ + python3 \ + python3-dev \ + py3-pip \ + luarocks5.1 -# Install Lua modules -RUN luarocks --lua-version=5.1 install lua-resty-openidc +# Install Lua modules into OpenResty lualib directory +RUN luarocks-5.1 install lua-resty-openidc \ + && cp -r /usr/local/share/lua/5.1/resty/* /usr/local/openresty/lualib/resty/ # Prepare Python virtual environment COPY FastapiOpenRestyConfigurator/requirements.txt /tmp/requirements.txt @@ -43,9 +26,8 @@ RUN python3 -m venv /opt/venv \ && /opt/venv/bin/pip install gunicorn uvicorn # --- Runtime Stage --- -FROM ubuntu:24.04 AS runtime-stage +FROM openresty/openresty:alpine AS runtime-stage -ENV DEBIAN_FRONTEND=noninteractive ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 ENV CONTAINERIZED=true @@ -54,29 +36,20 @@ ENV FORC_TEMPLATE_PATH=/var/forc/template_path/ ENV PATH="/opt/venv/bin:$PATH" # Install runtime-only dependencies -RUN apt-get update && apt-get install -y \ - python3 \ - ca-certificates \ +RUN apk add --no-cache \ + bash \ curl \ wget \ - gnupg \ - lsb-release \ - && rm -rf /var/lib/apt/lists/* + ca-certificates \ + python3 -# Install OpenResty -RUN wget -qO- https://openresty.org/package/pubkey.gpg \ - | gpg --dearmor \ - -o /usr/share/keyrings/openresty.gpg \ - && echo "deb [signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" \ - > /etc/apt/sources.list.d/openresty.list \ - && apt-get update \ - && apt-get install -y openresty \ - && rm -rf /var/lib/apt/lists/* +# Path Mapping: Link /etc/openresty to the default OpenResty config directory +# This maintains compatibility with render_nginx.py and scripts +RUN ln -snf /usr/local/openresty/nginx/conf /etc/openresty # Copy artifacts from build-stage COPY --from=build-stage /opt/venv /opt/venv -COPY --from=build-stage /usr/local/share/lua/5.1 /usr/local/share/lua/5.1 - +COPY --from=build-stage /usr/local/openresty/lualib /usr/local/openresty/lualib # Copy application code and configurations WORKDIR /opt/simpleVMWebGateway/FastapiOpenRestyConfigurator @@ -88,10 +61,11 @@ COPY docker/gunicorn_conf.py . COPY docker/launch.sh . COPY docker/generate_ip_blocklists.sh docker/ip_blocklists.txt /opt/scripts/ COPY docker/html /usr/local/openresty/nginx/html + # Setup permissions RUN mkdir -p ${FORC_BACKEND_PATH} ${FORC_TEMPLATE_PATH} /opt/scripts \ && chmod +x launch.sh /opt/scripts/generate_ip_blocklists.sh -COPY examples/templates ${FORC_TEMPLATE_PATH} +COPY examples/templates ${FORC_TEMPLATE_PATH} EXPOSE 5000 CMD ["./launch.sh"] From b7cfd4ccba28e9059bc35ec2f315ff68028024d4 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 10:31:50 +0000 Subject: [PATCH 113/118] added missing dependencies --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 025bca21..cfdb0900 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -16,7 +16,7 @@ RUN apk add --no-cache \ # Install Lua modules into OpenResty lualib directory RUN luarocks-5.1 install lua-resty-openidc \ - && cp -r /usr/local/share/lua/5.1/resty/* /usr/local/openresty/lualib/resty/ + && cp -r /usr/local/share/lua/5.1/* /usr/local/openresty/lualib/ # Prepare Python virtual environment COPY FastapiOpenRestyConfigurator/requirements.txt /tmp/requirements.txt From 5f2f07c2e70da431a7690a6793fc3aa9cc3c0ead Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 30 Jul 2026 11:17:08 +0000 Subject: [PATCH 114/118] set fixed alpine version --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index cfdb0900..95994f58 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # --- Build Stage --- -FROM openresty/openresty:alpine AS build-stage +FROM openresty/openresty:1.31-alpine-slim AS build-stage # Install build-time dependencies RUN apk add --no-cache \ @@ -26,7 +26,7 @@ RUN python3 -m venv /opt/venv \ && /opt/venv/bin/pip install gunicorn uvicorn # --- Runtime Stage --- -FROM openresty/openresty:alpine AS runtime-stage +FROM openresty/openresty:1.31-alpine-slim AS runtime-stage ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 From 13dd56ff6654240f7fc1d64a0d1f000444ba3c33 Mon Sep 17 00:00:00 2001 From: dweinholz Date: Thu, 6 Aug 2026 09:20:45 +0000 Subject: [PATCH 115/118] set cookie secure --- docker/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 82f04b11..8764ff7a 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -58,6 +58,7 @@ init_by_lua_block { opts = opts or {} opts.cookie_name = "sess_auth" + opts.cookie_secure = true opts.secret = "{{ FORC_SECRET_KEY }}" return old_new(opts) @@ -79,7 +80,6 @@ init_by_lua_block { renew_access_token_on_expiry = true, access_token_expires_leeway = 60, - session_contents = { id_token = true, access_token = true From 08c663eaa582ea333d195d61b5ee12bcd0b249b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:08:11 +0000 Subject: [PATCH 116/118] feat(Dependencies): Pin dependencies --- .github/workflows/pip_audit.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pip_audit.yml b/.github/workflows/pip_audit.yml index 97252033..53a4e5cf 100644 --- a/.github/workflows/pip_audit.yml +++ b/.github/workflows/pip_audit.yml @@ -10,15 +10,15 @@ jobs: runs-on: self-hosted steps: - name: Checkout PR branch - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: 3.14 - - uses: pypa/gh-action-pip-audit@v1.1.0 + - uses: pypa/gh-action-pip-audit@1220774d901786e6f652ae159f7b6bc8fea6d266 # v1.1.0 with: inputs: FastapiOpenRestyConfigurator/requirements.txt From a5e930ffa2f13fda4b94f69ec6fac99f4245978a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:08:20 +0000 Subject: [PATCH 117/118] feat(Dependencies): Update actions/checkout digest to 3d3c42e --- .github/workflows/build-image.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 062ff6f4..de0ad857 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -11,7 +11,7 @@ jobs: uses: rokroskar/workflow-run-cleanup-action@ee1451b869ba1e381729b3d40489997021f0d562 # v0.3.3 env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Build and publish image to OCI id: build diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fd9b310e..8029ad4f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL From 6c12dfc2e3ccd7a6c65c03405aec47a52edc8307 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:08:52 +0000 Subject: [PATCH 118/118] feat(Dependencies): Update dependency fastapi to v0.141.1 | datasource | package | from | to | | ---------- | ------- | ------- | ------- | | pypi | fastapi | 0.139.2 | 0.141.1 | --- FastapiOpenRestyConfigurator/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FastapiOpenRestyConfigurator/requirements.txt b/FastapiOpenRestyConfigurator/requirements.txt index a69d3cf2..3cedfe8b 100644 --- a/FastapiOpenRestyConfigurator/requirements.txt +++ b/FastapiOpenRestyConfigurator/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.139.2 +fastapi==0.141.1 uvicorn==0.52.0 werkzeug==3.1.8 Jinja2==3.1.6