From 371fdc9c77ff4d360175f3d9f7ac648318626ca7 Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:27:45 +0000 Subject: [PATCH 01/15] feat(Consent): Implement consent management service and update NGINX configuration --- docker/nginx.conf | 44 +++++++++++ examples/scripts/consent_service.lua | 104 +++++++++++++++++++++++++ examples/templates/webservice%v01.conf | 6 ++ 3 files changed, 154 insertions(+) create mode 100644 examples/scripts/consent_service.lua diff --git a/docker/nginx.conf b/docker/nginx.conf index 8764ff7a..3dc8955b 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -355,6 +355,50 @@ init_by_lua_block { + location = /consent { + # Session configuration identical to backends to share SHM storage + 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; + + content_by_lua_block { + local consent = require("consent_service") + consent.render_consent_page() + } + } + + location = /consent/callback { + # Session configuration identical to backends to share SHM storage + 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; + + content_by_lua_block { + local consent_service = require("consent_service") + consent_service.handle_consent_post() + } + } + include {{ FORC_BACKEND_PATH }}/*.conf; diff --git a/examples/scripts/consent_service.lua b/examples/scripts/consent_service.lua new file mode 100644 index 00000000..9ac4d3a2 --- /dev/null +++ b/examples/scripts/consent_service.lua @@ -0,0 +1,104 @@ +local session = require("resty.session") + +local _M = {} + +-- Valid return_to URLs: must start with '/', no '//', no '\', no control characters, +-- and not be a system control endpoint. +local function is_valid_return_to(url) + if not url or type(url) ~= "string" then return false end + if url == "/" then return true end + if not url:find("^/") then return false end + if url:find("//") or url:find("\\") then return false end + if url:find("[%z-\x1f\x7f]") then return false end + + local forbidden = { "/consent", "/consent/callback", "/redirect_uri" } + for _, path in ipairs(forbidden) do + if url == path then return false end + end + return true +end + +function _M.check_consent() + local sess = session.new() + if not sess then + ngx.log(ngx.ERR, "Failed to initialize session in check_consent") + return + end + + local now = os.time() + local consent_given = sess.data.consent_given + local consent_at = sess.data.consent_at or 0 + + -- Consent is valid if given and not older than 86400 seconds (24 hours) + if not consent_given or (now - consent_at > 86400) then + local return_to = ngx.var.request_uri or "/" + ngx.redirect("/consent?return_to=" .. ngx.escape_uri(return_to), true) + ngx.exit(ngx.HTTP_MOVED_TEMPORARILY) + end +end + +function _M.render_consent_page() + local sess = session.new() + local args = ngx.req.get_uri_args() + local return_to = args["return_to"] + + if not is_valid_return_to(return_to) then + return_to = "/" + end + + sess.data.showing_consent = true + sess.data.consent_return_to = return_to + sess:commit() + + ngx.header.content_type = "text/html; charset=utf-8" + ngx.say([[ + + + + Consent Required + + + +
+

Terms of Service

+

By proceeding, you agree to our terms of service and privacy policy. You acknowledge that your identity will be verified via OIDC.

+
+ +
+
+ + + ]]) +end + +function _M.handle_consent_post() + local sess = session.new() + + if not sess.data.showing_consent then + ngx.status = ngx.HTTP_FORBIDDEN + ngx.say("Forbidden: No consent flow active") + ngx.exit(ngx.HTTP_FORBIDDEN) + end + + sess.data.consent_given = true + sess.data.consent_at = os.time() + + local return_to = sess.data.consent_return_to or "/" + + -- Cleanup temporary flow data + sess.data.showing_consent = nil + sess.data.consent_return_to = nil + sess:commit() + + ngx.redirect(return_to, true) -- 303 See Other + ngx.exit(ngx.HTTP_SEE_OTHER) +end + +return _M diff --git a/examples/templates/webservice%v01.conf b/examples/templates/webservice%v01.conf index acdc2007..3149a8aa 100755 --- a/examples/templates/webservice%v01.conf +++ b/examples/templates/webservice%v01.conf @@ -20,6 +20,12 @@ 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 { local user_service = require("user_service") + + {% if auth_enabled %} + local consent_service = require("consent_service") + consent_service.check_consent() + {% endif %} + -- Start actual openid authentication procedure local res, err = require("resty.openidc").authenticate(opts2) -- If it fails for some reason, escape via HTTP 500 From bb931ab858ebc495a6b1eb729d4e8a57a31e7d38 Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:06:46 +0000 Subject: [PATCH 02/15] minor correction, for testing --- docker/nginx.conf | 10 ++++++++-- examples/scripts/consent_service.lua | 13 +++++++------ examples/templates/webservice%v01.conf | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 3dc8955b..8f8ee825 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -372,12 +372,15 @@ init_by_lua_block { set $session_shm_lock_max_step 0.5; content_by_lua_block { - local consent = require("consent_service") - consent.render_consent_page() + local consent_service = require("consent_service") + consent_service.render_consent_page() } } location = /consent/callback { + + add_header X-Consent-Callback "reached" always; + # Session configuration identical to backends to share SHM storage set $session_cipher none; set $session_storage shm; @@ -394,6 +397,9 @@ init_by_lua_block { set $session_shm_lock_max_step 0.5; content_by_lua_block { + + ngx.log(ngx.ERR, "CONSENT CALLBACK CONTENT HANDLER REACHED") + local consent_service = require("consent_service") consent_service.handle_consent_post() } diff --git a/examples/scripts/consent_service.lua b/examples/scripts/consent_service.lua index 9ac4d3a2..34457d06 100644 --- a/examples/scripts/consent_service.lua +++ b/examples/scripts/consent_service.lua @@ -25,15 +25,16 @@ function _M.check_consent() return end - local now = os.time() + local now = ngx.time() local consent_given = sess.data.consent_given local consent_at = sess.data.consent_at or 0 -- Consent is valid if given and not older than 86400 seconds (24 hours) if not consent_given or (now - consent_at > 86400) then local return_to = ngx.var.request_uri or "/" - ngx.redirect("/consent?return_to=" .. ngx.escape_uri(return_to), true) - ngx.exit(ngx.HTTP_MOVED_TEMPORARILY) + local query = ngx.encode_args({ return_to = return_to }) + ngx.redirect("/consent?" .. query) + return end end @@ -88,7 +89,7 @@ function _M.handle_consent_post() end sess.data.consent_given = true - sess.data.consent_at = os.time() + sess.data.consent_at = ngx.time() local return_to = sess.data.consent_return_to or "/" @@ -97,8 +98,8 @@ function _M.handle_consent_post() sess.data.consent_return_to = nil sess:commit() - ngx.redirect(return_to, true) -- 303 See Other - ngx.exit(ngx.HTTP_SEE_OTHER) + ngx.redirect(return_to, ngx.HTTP_SEE_OTHER) -- 303 See Other + return end return _M diff --git a/examples/templates/webservice%v01.conf b/examples/templates/webservice%v01.conf index 3149a8aa..54db1ca1 100755 --- a/examples/templates/webservice%v01.conf +++ b/examples/templates/webservice%v01.conf @@ -21,7 +21,7 @@ location /{{ key_url }}/ { access_by_lua_block { local user_service = require("user_service") - {% if auth_enabled %} + {% if not auth_enabled %} local consent_service = require("consent_service") consent_service.check_consent() {% endif %} From 257699932b6942e97271e0632c91128347afb292 Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:43:21 +0000 Subject: [PATCH 03/15] feat(Consent): Refactor consent handling and improve session management --- docker/nginx.conf | 6 -- examples/scripts/consent_service.lua | 103 +++++++++++++++++++-------- 2 files changed, 72 insertions(+), 37 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 8f8ee825..1411938c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -378,9 +378,6 @@ init_by_lua_block { } location = /consent/callback { - - add_header X-Consent-Callback "reached" always; - # Session configuration identical to backends to share SHM storage set $session_cipher none; set $session_storage shm; @@ -397,9 +394,6 @@ init_by_lua_block { set $session_shm_lock_max_step 0.5; content_by_lua_block { - - ngx.log(ngx.ERR, "CONSENT CALLBACK CONTENT HANDLER REACHED") - local consent_service = require("consent_service") consent_service.handle_consent_post() } diff --git a/examples/scripts/consent_service.lua b/examples/scripts/consent_service.lua index 34457d06..7ec7d610 100644 --- a/examples/scripts/consent_service.lua +++ b/examples/scripts/consent_service.lua @@ -2,54 +2,83 @@ local session = require("resty.session") local _M = {} +local CONSENT_TTL = 86400 + -- Valid return_to URLs: must start with '/', no '//', no '\', no control characters, -- and not be a system control endpoint. local function is_valid_return_to(url) if not url or type(url) ~= "string" then return false end if url == "/" then return true end if not url:find("^/") then return false end - if url:find("//") or url:find("\\") then return false end + if url:sub(1, 2) == "//" then return false end + if url:find("\\", 1, true) then return false end if url:find("[%z-\x1f\x7f]") then return false end + local path_without_query = url:match("^([^?]*)") local forbidden = { "/consent", "/consent/callback", "/redirect_uri" } for _, path in ipairs(forbidden) do - if url == path then return false end + if path_without_query == path then return false end end return true end + function _M.check_consent() - local sess = session.new() + local sess, err, exists = session.open() + if not sess then - ngx.log(ngx.ERR, "Failed to initialize session in check_consent") - return + ngx.log(ngx.ERR, "Failed to initialize session: ", err or "unknown") + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end + local consent_given = sess:get("consent_given") + local consent_at = sess:get("consent_at") local now = ngx.time() - local consent_given = sess.data.consent_given - local consent_at = sess.data.consent_at or 0 - - -- Consent is valid if given and not older than 86400 seconds (24 hours) - if not consent_given or (now - consent_at > 86400) then - local return_to = ngx.var.request_uri or "/" - local query = ngx.encode_args({ return_to = return_to }) - ngx.redirect("/consent?" .. query) - return + + local consent_valid = + exists + and consent_given == true + and type(consent_at) == "number" + and now - consent_at <= CONSENT_TTL + + if consent_valid then + return true end + + local return_to = ngx.var.request_uri or "/" + local query = ngx.encode_args({ return_to = return_to }) + ngx.redirect("/consent?" .. query, ngx.HTTP_FOUND) -- 302 Found end + function _M.render_consent_page() - local sess = session.new() + if ngx.req.get_method() ~= "GET" then + return ngx.exit(ngx.HTTP_NOT_ALLOWED) + end + + -- Read and validate return_to BEFORE storing it. local args = ngx.req.get_uri_args() local return_to = args["return_to"] if not is_valid_return_to(return_to) then - return_to = "/" + return ngx.exit(ngx.HTTP_BAD_REQUEST) + end + + local sess, err = session.start() + + if not sess then + ngx.log(ngx.ERR, "Failed to start session: ", err or "unknown") + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - sess.data.showing_consent = true - sess.data.consent_return_to = return_to - sess:commit() + sess:set("showing_consent", true) + sess:set("consent_return_to", return_to) + + local ok, save_err = sess:save() + if not ok then + ngx.log(ngx.ERR, "Failed to save session: ", save_err or "unknown") + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end ngx.header.content_type = "text/html; charset=utf-8" ngx.say([[ @@ -79,27 +108,39 @@ function _M.render_consent_page() ]]) end + function _M.handle_consent_post() - local sess = session.new() + local sess, err, exists = session.start() - if not sess.data.showing_consent then + if not sess then + ngx.log(ngx.ERR, "Starting session failed: ", err or "unknown") + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + + if not exists or sess:get("showing_consent") ~= true then ngx.status = ngx.HTTP_FORBIDDEN ngx.say("Forbidden: No consent flow active") - ngx.exit(ngx.HTTP_FORBIDDEN) + return ngx.exit(ngx.HTTP_FORBIDDEN) end - sess.data.consent_given = true - sess.data.consent_at = ngx.time() + local return_to = sess:get("consent_return_to") + if not is_valid_return_to(return_to) then + return ngx.exit(ngx.HTTP_BAD_REQUEST) + end + + sess:set("consent_given", true) + sess:set("consent_at", ngx.time())2 + sess:set("showing_consent", nil) + sess:set("consent_return_to", nil) - local return_to = sess.data.consent_return_to or "/" + local ok, save_err = sess:save() - -- Cleanup temporary flow data - sess.data.showing_consent = nil - sess.data.consent_return_to = nil - sess:commit() + if not ok then + ngx.log(ngx.ERR, "Failed to save consent: ", save_err or "unknown") + return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end - ngx.redirect(return_to, ngx.HTTP_SEE_OTHER) -- 303 See Other - return + return ngx.redirect(return_to, ngx.HTTP_SEE_OTHER) -- 303 See Other end return _M From 93deab0ad9e5b7bfe2d1e739b8e10f53506da2be Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:28:08 +0000 Subject: [PATCH 04/15] feat(Consent): Remove redundant session configuration and enhance logging in consent service --- docker/nginx.conf | 30 ---------------------------- examples/scripts/consent_service.lua | 24 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 1411938c..3c983c28 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -356,21 +356,6 @@ init_by_lua_block { location = /consent { - # Session configuration identical to backends to share SHM storage - 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; - content_by_lua_block { local consent_service = require("consent_service") consent_service.render_consent_page() @@ -378,21 +363,6 @@ init_by_lua_block { } location = /consent/callback { - # Session configuration identical to backends to share SHM storage - 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; - content_by_lua_block { local consent_service = require("consent_service") consent_service.handle_consent_post() diff --git a/examples/scripts/consent_service.lua b/examples/scripts/consent_service.lua index 7ec7d610..5a005f72 100644 --- a/examples/scripts/consent_service.lua +++ b/examples/scripts/consent_service.lua @@ -26,6 +26,23 @@ end function _M.check_consent() local sess, err, exists = session.open() + ngx.log( + ngx.ERR, + "CONSENT CHECK: sess=", + tostring(sess), + " exists=", + tostring(exists), + " err=", + tostring(err) + ) + ngx.log( + ngx.ERR, + "CONSENT VALUES: given=", + tostring(consent_given), + " at=", + tostring(consent_at) + ) + if not sess then ngx.log(ngx.ERR, "Failed to initialize session: ", err or "unknown") return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) @@ -33,13 +50,12 @@ function _M.check_consent() local consent_given = sess:get("consent_given") local consent_at = sess:get("consent_at") - local now = ngx.time() local consent_valid = exists and consent_given == true and type(consent_at) == "number" - and now - consent_at <= CONSENT_TTL + and ngx.time() - consent_at <= CONSENT_TTL if consent_valid then return true @@ -47,7 +63,7 @@ function _M.check_consent() local return_to = ngx.var.request_uri or "/" local query = ngx.encode_args({ return_to = return_to }) - ngx.redirect("/consent?" .. query, ngx.HTTP_FOUND) -- 302 Found + return ngx.redirect("/consent?" .. query, ngx.HTTP_FOUND) -- 302 Found end @@ -129,7 +145,7 @@ function _M.handle_consent_post() end sess:set("consent_given", true) - sess:set("consent_at", ngx.time())2 + sess:set("consent_at", ngx.time()) sess:set("showing_consent", nil) sess:set("consent_return_to", nil) From 67c14febd447f3ffc1b41a218aabdeaf2fae86db Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Tue, 8 Sep 2026 01:15:24 +0000 Subject: [PATCH 05/15] fix: removed LifeScience headers from webservice.conf --- examples/templates/webservice%v01.conf | 9 --------- 1 file changed, 9 deletions(-) diff --git a/examples/templates/webservice%v01.conf b/examples/templates/webservice%v01.conf index 54db1ca1..09c3b40e 100755 --- a/examples/templates/webservice%v01.conf +++ b/examples/templates/webservice%v01.conf @@ -43,15 +43,6 @@ location /{{ key_url }}/ { {% 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. From c142cac52cf66f541e58172bebdc846f60b62c58 Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Tue, 8 Sep 2026 01:15:47 +0000 Subject: [PATCH 06/15] first working version --- examples/scripts/consent_service.lua | 39 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/examples/scripts/consent_service.lua b/examples/scripts/consent_service.lua index 5a005f72..860c8f17 100644 --- a/examples/scripts/consent_service.lua +++ b/examples/scripts/consent_service.lua @@ -26,31 +26,38 @@ end function _M.check_consent() local sess, err, exists = session.open() - ngx.log( - ngx.ERR, - "CONSENT CHECK: sess=", - tostring(sess), - " exists=", - tostring(exists), - " err=", - tostring(err) - ) ngx.log( ngx.ERR, - "CONSENT VALUES: given=", - tostring(consent_given), - " at=", - tostring(consent_at) + "CONSENT CHECK: sess=", + tostring(sess), + " exists=", + tostring(exists), + " err=", + tostring(err) ) - + + if not exists then + local return_to = ngx.var.request_uri or "/" + local query = ngx.encode_args({ return_to = return_to }) + return ngx.redirect("/consent?" .. query, 302) + end + if not sess then ngx.log(ngx.ERR, "Failed to initialize session: ", err or "unknown") return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - + local consent_given = sess:get("consent_given") local consent_at = sess:get("consent_at") + ngx.log( + ngx.ERR, + "CONSENT VALUES: given=", + tostring(consent_given), + " at=", + tostring(consent_at) + ) + local consent_valid = exists and consent_given == true @@ -63,7 +70,7 @@ function _M.check_consent() local return_to = ngx.var.request_uri or "/" local query = ngx.encode_args({ return_to = return_to }) - return ngx.redirect("/consent?" .. query, ngx.HTTP_FOUND) -- 302 Found + return ngx.redirect("/consent?" .. query, 302) end From 4f59bbefc5f603cde1ce573ee73d5d7edb501462 Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Thu, 10 Sep 2026 08:08:03 +0000 Subject: [PATCH 07/15] removed deprecated cookie settings --- examples/templates/webservice%v01.conf | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/examples/templates/webservice%v01.conf b/examples/templates/webservice%v01.conf index 09c3b40e..7025b038 100755 --- a/examples/templates/webservice%v01.conf +++ b/examples/templates/webservice%v01.conf @@ -1,20 +1,5 @@ # 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 c86d2259e001af94208a56738f503fa87582fb2b Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Thu, 10 Sep 2026 08:09:13 +0000 Subject: [PATCH 08/15] update docker --- docker/Dockerfile | 1 + docker/docker-compose.yml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 95994f58..056c4ada 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -66,6 +66,7 @@ COPY docker/html /usr/local/openresty/nginx/html 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/scripts ${FORC_BACKEND_PATH}/scripts EXPOSE 5000 CMD ["./launch.sh"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index cd84be59..a7eedab7 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: forc: - image: forc + image: oci.bi.denbi.de/simplevm/forc:dev container_name: forc env_file: - ../FastapiOpenRestyConfigurator/.env # envs for openresty @@ -9,6 +9,8 @@ services: #- /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 + - ../examples/scripts/:/var/forc/backend_path/scripts/:r + - ../examples/templates/:/var/forc/template_path/:r ports: - 0.0.0.0:5000:5000 - 0.0.0.0:80:80 From bf963d84322155a7c3860ad89ef44c509af8d66c Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Thu, 10 Sep 2026 08:09:54 +0000 Subject: [PATCH 09/15] add claude.md --- CLAUDE.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..869c2a5f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Development +- **Install Dependencies**: + ```bash + pip install -r FastapiOpenRestyConfigurator/requirements.txt + ``` +- **Run Application (Development)**: + ```bash + # From project root + export PYTHONPATH=$PYTHONPATH:$(pwd)/FastapiOpenRestyConfigurator + uvicorn FastapiOpenRestyConfigurator.main:app --reload + ``` +- **Run Application (Production)**: + ```bash + # Using gunicorn with provided config + gunicorn -c FastapiOpenRestyConfigurator/gunicorn_conf.py FastapiOpenRestyConfigurator.main:app + ``` + +### Testing +- **Run All Tests**: + ```bash + # From project root + export PYTHONPATH=$PYTHONPATH:$(pwd)/FastapiOpenRestyConfigurator + pytest FastapiOpenRestyConfigurator/tests + ``` +- **Run Single Test File**: + ```bash + export PYTHONPATH=$PYTHONPATH:$(pwd)/FastapiOpenRestyConfigurator + pytest FastapiOpenRestyConfigurator/tests/test_specific_file.py + ``` + +## Architecture + +The project (Flask OpenResty Configurator - FORC) is a FastAPI-based service that dynamically generates NGINX configuration snippets for an OpenResty web server. + +### High-Level Flow +1. **Request**: A REST API request is received by a `view`. +2. **Logic**: The `view` calls a `service` to perform business logic (e.g., creating a new backend). +3. **Templating**: The `service` uses Jinja2 templates to generate a configuration snippet. +4. **Persistence**: The snippet is written to the filesystem (`FORC_BACKEND_PATH`). +5. **Activation**: OpenResty is reloaded to apply the new configuration. + +### Project Structure (`FastapiOpenRestyConfigurator/`) +- `main.py`: Entry point; initializes the FastAPI app and includes routers. +- `app/main/views/`: API endpoints (Controllers). +- `app/main/service/`: Core business logic. + - `backend.py`: Manages backend configurations. + - `openresty.py`: Handles OpenResty interactions (e.g., reloading). + - `template.py`: Manages Jinja2 template rendering. + - `user.py`: User management logic. +- `app/main/model/`: Data models and Pydantic serializers. +- `app/main/util/`: Shared utilities for authentication, logging, and templating. +- `tests/`: Integration and unit tests. + +### Configuration +The application is configured via environment variables: +- `FORC_SECRET_KEY`: Encryption key for the service. +- `FORC_API_KEY`: API key for `X-API-KEY` authentication. +- `FORC_BACKEND_PATH`: Filesystem path where NGINX config snippets are stored. +- `FORC_TEMPLATE_PATH`: Filesystem path where Jinja2 templates are located. From 2be7ff6231d9de00c34f813fb9ce6c517e46543a Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Fri, 11 Sep 2026 08:55:42 +0000 Subject: [PATCH 10/15] individual consent service with key_url parameter and validation --- .gitignore | 3 +- examples/scripts/consent_service.lua | 54 +++++++++++++++----------- examples/templates/webservice%v01.conf | 2 +- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index d06e8a59..4085b356 100755 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ ansible/test.json FastapiOpenRestyConfigurator/.env template_path backend_path -*/plans \ No newline at end of file +*/plans +.vscode/settings.json diff --git a/examples/scripts/consent_service.lua b/examples/scripts/consent_service.lua index 860c8f17..196656e9 100644 --- a/examples/scripts/consent_service.lua +++ b/examples/scripts/consent_service.lua @@ -22,8 +22,15 @@ local function is_valid_return_to(url) return true end +local function is_valid_service(key_url, return_to) + if type(key_url) ~= "string" then return false end + if type(return_to) ~= "string" then return false end + local service_path = "/" .. key_url .. "/" + return return_to:sub(1, #service_path) == service_path +end + -function _M.check_consent() +function _M.check_consent(key_url) local sess, err, exists = session.open() ngx.log( @@ -35,33 +42,25 @@ function _M.check_consent() " err=", tostring(err) ) - - if not exists then - local return_to = ngx.var.request_uri or "/" - local query = ngx.encode_args({ return_to = return_to }) - return ngx.redirect("/consent?" .. query, 302) - end - + if not sess then ngx.log(ngx.ERR, "Failed to initialize session: ", err or "unknown") return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - - local consent_given = sess:get("consent_given") - local consent_at = sess:get("consent_at") + + local consent_key = "consent:" .. key_url + local consent_at = exists and sess:get(consent_key) or nil ngx.log( ngx.ERR, "CONSENT VALUES: given=", - tostring(consent_given), + tostring(consent_key), " at=", tostring(consent_at) ) local consent_valid = - exists - and consent_given == true - and type(consent_at) == "number" + type(consent_at) == "number" and ngx.time() - consent_at <= CONSENT_TTL if consent_valid then @@ -69,7 +68,10 @@ function _M.check_consent() end local return_to = ngx.var.request_uri or "/" - local query = ngx.encode_args({ return_to = return_to }) + local query = ngx.encode_args({ + return_to = return_to, + key_url = key_url + }) return ngx.redirect("/consent?" .. query, 302) end @@ -82,8 +84,9 @@ function _M.render_consent_page() -- Read and validate return_to BEFORE storing it. local args = ngx.req.get_uri_args() local return_to = args["return_to"] + local key_url = args["key_url"] - if not is_valid_return_to(return_to) then + if not is_valid_return_to(return_to) or not is_valid_service(key_url, return_to) then return ngx.exit(ngx.HTTP_BAD_REQUEST) end @@ -96,6 +99,7 @@ function _M.render_consent_page() sess:set("showing_consent", true) sess:set("consent_return_to", return_to) + sess:set("consent_key_url", key_url) local ok, save_err = sess:save() if not ok then @@ -147,14 +151,20 @@ function _M.handle_consent_post() end local return_to = sess:get("consent_return_to") - if not is_valid_return_to(return_to) then - return ngx.exit(ngx.HTTP_BAD_REQUEST) - end + local key_url = sess:get("consent_key_url") + if not is_valid_return_to(return_to) then + return ngx.exit(ngx.HTTP_BAD_REQUEST) + end + if not is_valid_service(key_url, return_to) then + return ngx.exit(ngx.HTTP_BAD_REQUEST) + end + + local consent_key = "consent:" .. key_url + sess:set(consent_key, ngx.time()) - sess:set("consent_given", true) - sess:set("consent_at", ngx.time()) sess:set("showing_consent", nil) sess:set("consent_return_to", nil) + sess:set("consent_key_url", nil) local ok, save_err = sess:save() diff --git a/examples/templates/webservice%v01.conf b/examples/templates/webservice%v01.conf index 7025b038..1ed26cc5 100755 --- a/examples/templates/webservice%v01.conf +++ b/examples/templates/webservice%v01.conf @@ -8,7 +8,7 @@ location /{{ key_url }}/ { {% if not auth_enabled %} local consent_service = require("consent_service") - consent_service.check_consent() + consent_service.check_consent("{{ key_url }}") {% endif %} -- Start actual openid authentication procedure From bad2826859e33af125998c3308350f66539d5652 Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:44:35 +0000 Subject: [PATCH 11/15] additional logging and validation check --- examples/scripts/consent_service.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/examples/scripts/consent_service.lua b/examples/scripts/consent_service.lua index 196656e9..9e1a079a 100644 --- a/examples/scripts/consent_service.lua +++ b/examples/scripts/consent_service.lua @@ -7,6 +7,7 @@ local CONSENT_TTL = 86400 -- Valid return_to URLs: must start with '/', no '//', no '\', no control characters, -- and not be a system control endpoint. local function is_valid_return_to(url) + ngx.log(ngx.DEBUG, "Validating return_to URL: ", url) if not url or type(url) ~= "string" then return false end if url == "/" then return true end if not url:find("^/") then return false end @@ -19,18 +20,22 @@ local function is_valid_return_to(url) for _, path in ipairs(forbidden) do if path_without_query == path then return false end end + ngx.log(ngx.DEBUG, "Valid return_to URL: ", url) return true end local function is_valid_service(key_url, return_to) + ngx.log(ngx.DEBUG, "Validating service: key_url=", key_url, " return_to=", return_to) if type(key_url) ~= "string" then return false end if type(return_to) ~= "string" then return false end local service_path = "/" .. key_url .. "/" + ngx.log(ngx.DEBUG, "Validating service: service_path=", service_path) return return_to:sub(1, #service_path) == service_path end function _M.check_consent(key_url) + ngx.log(ngx.DEBUG, "Checking consent for key_url: ", key_url) local sess, err, exists = session.open() ngx.log( @@ -43,6 +48,12 @@ function _M.check_consent(key_url) tostring(err) ) + if not exists then + local return_to = ngx.var.request_uri or "/" + local query = ngx.encode_args({ return_to = return_to }) + return ngx.redirect("/consent?" .. query, 302) + end + if not sess then ngx.log(ngx.ERR, "Failed to initialize session: ", err or "unknown") return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) @@ -77,6 +88,7 @@ end function _M.render_consent_page() + ngx.log(ngx.DEBUG, "Rendering consent page") if ngx.req.get_method() ~= "GET" then return ngx.exit(ngx.HTTP_NOT_ALLOWED) end @@ -86,6 +98,8 @@ function _M.render_consent_page() local return_to = args["return_to"] local key_url = args["key_url"] + ngx.log(ngx.DEBUG, "Validating: return_to=" .. return_to .. " | key_url=" .. key_url) + if not is_valid_return_to(return_to) or not is_valid_service(key_url, return_to) then return ngx.exit(ngx.HTTP_BAD_REQUEST) end @@ -137,6 +151,7 @@ end function _M.handle_consent_post() + ngx.log(ngx.DEBUG, "Handling consent POST") local sess, err, exists = session.start() if not sess then From 41dc3abccdd14fc69c8ab6065df78f545d102c3e Mon Sep 17 00:00:00 2001 From: milo39 <206898141+milo39@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:04:45 +0000 Subject: [PATCH 12/15] extracted page rendering from service --- examples/scripts/consent_page.lua | 171 +++++++++++++++++++++++++++ examples/scripts/consent_service.lua | 35 +----- 2 files changed, 174 insertions(+), 32 deletions(-) create mode 100644 examples/scripts/consent_page.lua diff --git a/examples/scripts/consent_page.lua b/examples/scripts/consent_page.lua new file mode 100644 index 00000000..35e17dac --- /dev/null +++ b/examples/scripts/consent_page.lua @@ -0,0 +1,171 @@ +-- examples/scripts/consent_html.lua +local _M = {} + +function _M.render() + return[[ + + + + + + Consent Required + + + +
+
+ + + SimpleVM Web Services + +
+
+

+ The service you are about to access is provided by its users. + Neither SimpleVM nor de.NBI Cloud is responsible for the content + provided through this service. +

+ By continuing, you agree to the + + Terms of Service and Privacy Policy + . +

+ +
+ +
+ +
+
+ + + ]] +end + +return _M \ No newline at end of file diff --git a/examples/scripts/consent_service.lua b/examples/scripts/consent_service.lua index 9e1a079a..b3e4242c 100644 --- a/examples/scripts/consent_service.lua +++ b/examples/scripts/consent_service.lua @@ -1,4 +1,5 @@ local session = require("resty.session") +local consent_page = require("consent_page") local _M = {} @@ -48,12 +49,6 @@ function _M.check_consent(key_url) tostring(err) ) - if not exists then - local return_to = ngx.var.request_uri or "/" - local query = ngx.encode_args({ return_to = return_to }) - return ngx.redirect("/consent?" .. query, 302) - end - if not sess then ngx.log(ngx.ERR, "Failed to initialize session: ", err or "unknown") return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) @@ -93,7 +88,7 @@ function _M.render_consent_page() return ngx.exit(ngx.HTTP_NOT_ALLOWED) end - -- Read and validate return_to BEFORE storing it. + -- Read and validate return_to before storing it. local args = ngx.req.get_uri_args() local return_to = args["return_to"] local key_url = args["key_url"] @@ -122,31 +117,7 @@ function _M.render_consent_page() end ngx.header.content_type = "text/html; charset=utf-8" - ngx.say([[ - - - - Consent Required - - - -
-

Terms of Service

-

By proceeding, you agree to our terms of service and privacy policy. You acknowledge that your identity will be verified via OIDC.

-
- -
-
- - - ]]) + ngx.say(consent_page.render()) end From 83df70b31d38a4c5b5415011cad147078fe85f0f Mon Sep 17 00:00:00 2001 From: dweinholz Date: Tue, 15 Sep 2026 13:13:45 +0000 Subject: [PATCH 13/15] fix(Scripts):mount scripts dir to own path --- docker/Dockerfile | 2 +- docker/nginx.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 056c4ada..181d3efe 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -66,7 +66,7 @@ COPY docker/html /usr/local/openresty/nginx/html 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/scripts ${FORC_BACKEND_PATH}/scripts +COPY examples/scripts /var/forc/scripts/ EXPOSE 5000 CMD ["./launch.sh"] diff --git a/docker/nginx.conf b/docker/nginx.conf index 3c983c28..05eb9f4c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -11,7 +11,7 @@ events { http { include mime.types; - lua_package_path "{{ FORC_BACKEND_PATH }}/scripts/?.lua;;"; + lua_package_path "/var/forc/scripts/?.lua;"; default_type application/octet-stream; From 8e2a490319544c228ad2f522bf0c9f13d67ce15e Mon Sep 17 00:00:00 2001 From: dweinholz Date: Tue, 15 Sep 2026 15:20:07 +0200 Subject: [PATCH 14/15] Update nginx.conf --- docker/nginx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 05eb9f4c..d72fc7c1 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -11,7 +11,7 @@ events { http { include mime.types; - lua_package_path "/var/forc/scripts/?.lua;"; + lua_package_path "/var/forc/scripts/?.lua;;"; default_type application/octet-stream; @@ -393,4 +393,4 @@ init_by_lua_block { root html; } } -} \ No newline at end of file +} From ea197f287961463f35f98235db152744ab0e143a Mon Sep 17 00:00:00 2001 From: dweinholz Date: Tue, 15 Sep 2026 15:47:37 +0200 Subject: [PATCH 15/15] Update docker-compose.yml --- docker/docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a7eedab7..c6f7e0d1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,8 +9,7 @@ services: #- /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 - - ../examples/scripts/:/var/forc/backend_path/scripts/:r - - ../examples/templates/:/var/forc/template_path/:r + ports: - 0.0.0.0:5000:5000 - 0.0.0.0:80:80