From 5fde1663f5ae6bfef8140557c395606c78e090ab Mon Sep 17 00:00:00 2001 From: Yves G Date: Mon, 9 Aug 2021 17:43:42 +0200 Subject: [PATCH 1/5] minimum: login, portal, redirects; todo: refactor, quality, security review --- .editorconfig | 16 + .gitignore | 1 + Makefile | 105 ++++++ doc/samples/global.json | 8 + doc/samples/login/login.css | 32 ++ doc/samples/login/login.html | 14 + doc/samples/login/login.js | 1 + doc/samples/portal/portal.css | 50 +++ doc/samples/portal/portal.html | 9 + doc/samples/portal/portal.js | 1 + doc/samples/test_pages/private_restricted.php | 2 + .../test_pages/private_unrestricted.php | 2 + doc/samples/test_pages/private_with_ban.php | 2 + doc/samples/test_pages/public_access.php | 2 + .../test_pages/sites/private_restricted.json | 23 ++ .../sites/private_unrestricted.json | 28 ++ .../test_pages/sites/private_with_ban.json | 31 ++ .../test_pages/sites/public_access.json | 20 ++ src/do_access.lua | 51 +++ src/do_init.lua | 28 ++ src/ssso_auth.lua | 39 +++ src/ssso_base64.lua | 19 ++ src/ssso_config.lua | 38 +++ src/ssso_crypto.lua | 133 ++++++++ src/ssso_log.lua | 14 + src/ssso_login.lua | 104 ++++++ src/ssso_nginx.lua | 156 +++++++++ src/ssso_oauth2.lua | 14 + src/ssso_portal.lua | 55 +++ src/ssso_profile.lua | 63 ++++ src/ssso_sessions.lua | 24 ++ src/ssso_sites.lua | 215 ++++++++++++ src/ssso_util.lua | 18 + test/aes.utest.lua | 23 ++ test/alt/bit.lua | 1 + test/alt/ngx.lua | 135 ++++++++ test/alt/ngx/base64.lua | 18 + test/alt/resty/openssl/cipher.lua | 35 ++ test/alt/resty/random.lua | 9 + test/alt/resty/sha256.lua | 20 ++ test/anonymous1.ctest.lua | 18 + test/anonymous2.ctest.lua | 18 + test/anonymous3.ctest.lua | 18 + test/anonymous4.ctest.lua | 18 + test/anonymous5.ctest.lua | 18 + test/auth.utest.lua | 19 ++ test/config.utest.lua | 16 + test/crypto.utest.lua | 50 +++ test/global.json | 8 + test/login.utest.lua | 203 ++++++++++++ test/login/login.css | 1 + test/login/login.html | 7 + test/login/login.js | 1 + test/login1.ctest.lua | 28 ++ test/login2.ctest.lua | 21 ++ test/login3.ctest.lua | 21 ++ test/login4.ctest.lua | 20 ++ test/login5.ctest.lua | 23 ++ test/login6.ctest.lua | 32 ++ test/login7.ctest.lua | 22 ++ test/nginx.utest.lua | 313 ++++++++++++++++++ test/portal/portal.css | 1 + test/portal/portal.html | 7 + test/portal/portal.js | 1 + test/portal1.ctest.lua | 37 +++ test/portal2.ctest.lua | 25 ++ test/portal3.ctest.lua | 25 ++ test/portal4.ctest.lua | 22 ++ test/portal5.ctest.lua | 19 ++ test/profile.utest.lua | 81 +++++ test/random.utest.lua | 12 + test/sessions.utest.lua | 61 ++++ test/sha256.utest.lua | 13 + test/sites.utest.lua | 289 ++++++++++++++++ test/sites/mixed.json | 56 ++++ test/sites/private.json | 25 ++ test/sites/public.json | 31 ++ test/util.utest.lua | 14 + 78 files changed, 3153 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 doc/samples/global.json create mode 100644 doc/samples/login/login.css create mode 100644 doc/samples/login/login.html create mode 100644 doc/samples/login/login.js create mode 100644 doc/samples/portal/portal.css create mode 100644 doc/samples/portal/portal.html create mode 100644 doc/samples/portal/portal.js create mode 100644 doc/samples/test_pages/private_restricted.php create mode 100644 doc/samples/test_pages/private_unrestricted.php create mode 100644 doc/samples/test_pages/private_with_ban.php create mode 100644 doc/samples/test_pages/public_access.php create mode 100644 doc/samples/test_pages/sites/private_restricted.json create mode 100644 doc/samples/test_pages/sites/private_unrestricted.json create mode 100644 doc/samples/test_pages/sites/private_with_ban.json create mode 100644 doc/samples/test_pages/sites/public_access.json create mode 100644 src/do_access.lua create mode 100644 src/do_init.lua create mode 100644 src/ssso_auth.lua create mode 100644 src/ssso_base64.lua create mode 100644 src/ssso_config.lua create mode 100644 src/ssso_crypto.lua create mode 100644 src/ssso_log.lua create mode 100644 src/ssso_login.lua create mode 100644 src/ssso_nginx.lua create mode 100644 src/ssso_oauth2.lua create mode 100644 src/ssso_portal.lua create mode 100644 src/ssso_profile.lua create mode 100644 src/ssso_sessions.lua create mode 100644 src/ssso_sites.lua create mode 100644 src/ssso_util.lua create mode 100644 test/aes.utest.lua create mode 100644 test/alt/bit.lua create mode 100644 test/alt/ngx.lua create mode 100644 test/alt/ngx/base64.lua create mode 100644 test/alt/resty/openssl/cipher.lua create mode 100644 test/alt/resty/random.lua create mode 100644 test/alt/resty/sha256.lua create mode 100644 test/anonymous1.ctest.lua create mode 100644 test/anonymous2.ctest.lua create mode 100644 test/anonymous3.ctest.lua create mode 100644 test/anonymous4.ctest.lua create mode 100644 test/anonymous5.ctest.lua create mode 100644 test/auth.utest.lua create mode 100644 test/config.utest.lua create mode 100644 test/crypto.utest.lua create mode 100644 test/global.json create mode 100644 test/login.utest.lua create mode 100644 test/login/login.css create mode 100644 test/login/login.html create mode 100644 test/login/login.js create mode 100644 test/login1.ctest.lua create mode 100644 test/login2.ctest.lua create mode 100644 test/login3.ctest.lua create mode 100644 test/login4.ctest.lua create mode 100644 test/login5.ctest.lua create mode 100644 test/login6.ctest.lua create mode 100644 test/login7.ctest.lua create mode 100644 test/nginx.utest.lua create mode 100644 test/portal/portal.css create mode 100644 test/portal/portal.html create mode 100644 test/portal/portal.js create mode 100644 test/portal1.ctest.lua create mode 100644 test/portal2.ctest.lua create mode 100644 test/portal3.ctest.lua create mode 100644 test/portal4.ctest.lua create mode 100644 test/portal5.ctest.lua create mode 100644 test/profile.utest.lua create mode 100644 test/random.utest.lua create mode 100644 test/sessions.utest.lua create mode 100644 test/sha256.utest.lua create mode 100644 test/sites.utest.lua create mode 100644 test/sites/mixed.json create mode 100644 test/sites/private.json create mode 100644 test/sites/public.json create mode 100644 test/util.utest.lua diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b860e67 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[Makefile] +indent_style = tab +tab_width = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c24bd96 --- /dev/null +++ b/Makefile @@ -0,0 +1,105 @@ +# https://stackoverflow.com/a/23324703 +ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) + +lua_family = 5.1 +lua_version = ${lua_family}.5 +lua_suffix = +lua_build_platform = linux +luarocks_version = 3.7.0 + +lua_src = http://www.lua.org/ftp/lua-${lua_version}.tar.gz +luarocks_src = http://luarocks.github.io/luarocks/releases/luarocks-${luarocks_version}.tar.gz + +lua_root = ${ROOT_DIR}/target/dist +lua_mods = ${lua_root}/share/lua/${lua_family} +lua_cmods = ${lua_root}/lib/lua/${lua_family} + +run_test_file = env \ + LUA_PATH="${ROOT_DIR}/test/alt/?.lua;${lua_mods}/?.lua;${ROOT_DIR}/target/dist/etc/nginx/ssso/?.lua" \ + LUA_CPATH="${lua_cmods}/?.so" \ + ${lua_root}/bin/lua${lua_suffix} + +all: test + +test: test-env + ${run_test_file} ${ROOT_DIR}/test/aes.utest.lua + ${run_test_file} ${ROOT_DIR}/test/random.utest.lua + ${run_test_file} ${ROOT_DIR}/test/sha256.utest.lua + ${run_test_file} ${ROOT_DIR}/test/util.utest.lua + ${run_test_file} ${ROOT_DIR}/test/config.utest.lua + ${run_test_file} ${ROOT_DIR}/test/auth.utest.lua + ${run_test_file} ${ROOT_DIR}/test/nginx.utest.lua + ${run_test_file} ${ROOT_DIR}/test/profile.utest.lua + ${run_test_file} ${ROOT_DIR}/test/sites.utest.lua + ${run_test_file} ${ROOT_DIR}/test/crypto.utest.lua + ${run_test_file} ${ROOT_DIR}/test/login.utest.lua + ${run_test_file} ${ROOT_DIR}/test/sessions.utest.lua + ${run_test_file} ${ROOT_DIR}/test/anonymous1.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/anonymous2.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/anonymous3.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/anonymous4.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/anonymous5.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/login1.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/login2.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/login3.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/login4.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/login5.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/login6.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/login7.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/portal1.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/portal2.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/portal3.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/portal4.ctest.lua + ${run_test_file} ${ROOT_DIR}/test/portal5.ctest.lua + +test-env: run-env target/dist/etc/nginx/ssso ${lua_cmods}/bit32.so ${lua_cmods}/cjson.so ${lua_mods}/resty/easy-crypto.lua ${lua_mods}/luaunit.lua + +target/dist/etc/nginx/ssso: src test/global.json test/login test/portal test/sites + rm -rf target/dist/etc/nginx/ssso; \ + mkdir -p target/dist/etc/nginx; \ + cp -rs ${ROOT_DIR}/src target/dist/etc/nginx/ssso; \ + cp -rs ${ROOT_DIR}/test/{global.json,login,portal,sites} target/dist/etc/nginx/ssso/ + +run-env: ${lua_root}/bin/lua${lua_suffix} + +${lua_root}/bin/lua${lua_suffix}: target/src/lua/lua-${lua_version}.tar.gz + ( \ + cd target/src/lua; \ + rm -rf lua-${lua_version}; \ + tar -xzf lua-${lua_version}.tar.gz; \ + cd lua-${lua_version}; \ + make ${lua_build_platform} test install INSTALL_TOP="${lua_root}" \ + ) + +${lua_root}/bin/luarocks: target/src/luarocks/luarocks-${luarocks_version}.tar.gz + ( \ + cd target/src/luarocks; \ + rm -rf luarocks-${luarocks_version}; \ + tar -xzf luarocks-${luarocks_version}.tar.gz; \ + cd luarocks-${luarocks_version}; \ + ./configure --prefix="${lua_root}" --with-lua="${lua_root}" --lua-suffix=${lua_suffix}; \ + make; \ + make install \ + ) + +${lua_cmods}/bit32.so: ${lua_root}/bin/luarocks + ${lua_root}/bin/luarocks install bit32 + +${lua_cmods}/cjson.so: ${lua_root}/bin/luarocks + ${lua_root}/bin/luarocks install lua-cjson + +${lua_mods}/resty/easy-crypto.lua: ${lua_root}/bin/luarocks + ${lua_root}/bin/luarocks install lua-easy-crypto + +${lua_mods}/luaunit.lua: ${lua_root}/bin/luarocks + ${lua_root}/bin/luarocks install luaunit + +target/src/lua/lua-${lua_version}.tar.gz: + mkdir -p target/src/lua; \ + curl -so target/src/lua/lua-${lua_version}.tar.gz "${lua_src}" + +target/src/luarocks/luarocks-${luarocks_version}.tar.gz: + mkdir -p target/src/luarocks; \ + curl -so target/src/luarocks/luarocks-${luarocks_version}.tar.gz "${luarocks_src}" + +.PHONY: all run-env test-env test diff --git a/doc/samples/global.json b/doc/samples/global.json new file mode 100644 index 0000000..8da972f --- /dev/null +++ b/doc/samples/global.json @@ -0,0 +1,8 @@ +{ + "auth": { + "check": "/usr/bin/ldapsearch -x -D \"uid=\ru.,ou=users,dc=example,dc=org\" -w \"\rp.\" -b 'ou=users,dc=example,dc=org' -s one -LLL -l 1 -z 1 \"(uid=\ru.)\" cn mail | /usr/bin/gawk '/^cn/{n=gensub(/cn: */,\"\",1)};/^mail/{m=gensub(/mail: */,\"\",1)};END{printf(\"%s\\n%s\\n\",n,m)}'" + }, + "session_seconds": 3600, + "sso_host": "example.org", + "sso_prefix": "/ssso" +} diff --git a/doc/samples/login/login.css b/doc/samples/login/login.css new file mode 100644 index 0000000..53ee7a0 --- /dev/null +++ b/doc/samples/login/login.css @@ -0,0 +1,32 @@ +body { + max-width: 100ex; + margin: 0 auto; + background-color: white; + color: black; + text-align: center; +} +form > * { + display: block; + clear: both; + margin: 0.5ex auto; +} +label input { + display: block; + width: 50%; + float: right; + margin: 0 0 1ex; +} +#messages { + margin: 1em 0; + background-color: rgba(255,0,0,0.1); + border: 0.3ex outset rgba(255,0,0,0.1); + border-radius: 1ex; + padding: 0 1ex; + text-align: left; +} +#messages .info { + color: orangered; +} +#messages .warning { + color: blue; +} diff --git a/doc/samples/login/login.html b/doc/samples/login/login.html new file mode 100644 index 0000000..151de65 --- /dev/null +++ b/doc/samples/login/login.html @@ -0,0 +1,14 @@ + + Login + + + +

Single Sign-On for example.org

+ +
+ + + + +
+ diff --git a/doc/samples/login/login.js b/doc/samples/login/login.js new file mode 100644 index 0000000..a688119 --- /dev/null +++ b/doc/samples/login/login.js @@ -0,0 +1 @@ +//JS diff --git a/doc/samples/portal/portal.css b/doc/samples/portal/portal.css new file mode 100644 index 0000000..b9f4552 --- /dev/null +++ b/doc/samples/portal/portal.css @@ -0,0 +1,50 @@ +body { + max-width: 100ex; + margin: 0 auto; + background-color: white; + color: black; + text-align: center; +} +#sites ul { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + row-gap: 1ex; + column-gap: 1ex; + margin: 0; +} +#sites li { + display: flex; + flex-grow: 1; + margin: 0; + padding: 0; +} +#sites ul::after { + content: ""; + flex-grow: 10; +} +#sites a { + display: block; + position: relative; + flex-grow: 1; + min-height: 3em; + min-width: 5em; + max-width: 10em; + color: rgb(255, 255, 127); + background-color: rgb(70, 130, 180); + border: 0.3ex outset rgb(70, 130, 180); + border-radius: 1ex; + padding: 1ex; +} +#sites a span { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} +#sites a:hover { + border-style: inset; + color: rgb(245, 245, 117); + background-color: rgb(65, 125, 175); +} diff --git a/doc/samples/portal/portal.html b/doc/samples/portal/portal.html new file mode 100644 index 0000000..7d9baf6 --- /dev/null +++ b/doc/samples/portal/portal.html @@ -0,0 +1,9 @@ + + Single Sign-On Portal + + + +

Available pages for SSSO_USER

+

Welcome SSSO_NAME! Here are the pages you can open using your single sign-on:

+ + diff --git a/doc/samples/portal/portal.js b/doc/samples/portal/portal.js new file mode 100644 index 0000000..a688119 --- /dev/null +++ b/doc/samples/portal/portal.js @@ -0,0 +1 @@ +//JS diff --git a/doc/samples/test_pages/private_restricted.php b/doc/samples/test_pages/private_restricted.php new file mode 100644 index 0000000..5ccb6c1 --- /dev/null +++ b/doc/samples/test_pages/private_restricted.php @@ -0,0 +1,2 @@ +' .. util.str_to_html(cause) .. "

" + else + paras = "" + end + if warnings then + for _, m in ipairs(warnings) do + paras = paras .. '

' .. util.str_to_html(m) .. "

" + end + end + if paras ~= "" then + html = html:gsub("", '
' .. paras .. "
", 1) + end + local back_url = req_data.query_params.back + if not back_url then + back_url = "" + end + html = html:gsub("BACK_URL", util.str_to_html(back_url)) + return html +end + +local function check_login(req_data) + req_data = nginx.with_post_parameters(req_data) + local user = req_data.query_params["login"] or "" + local password = req_data.query_params["password"] or "" + local user_data = auth.read_user(user, password) + if user_data then + log.debug("Credentials accepted for user " .. user_data.name) + local profile = prof.build_profile(user, password, user_data.name, user_data.email) + profile = sites.with_sites(profile, user) + log.debug("Building JWS") + local jws, tslimit = crypto.get_jws_and_tslimit(profile) + if not jws then + return nginx.answer_unexpected_error() + end + nginx.set_jws_cookie(jws, tslimit) + local back_url = req_data.query_params.back + log.debug("Redirecting") + if back_url then + return nginx.redirect_to_page(back_url) + else + return nginx.redirect_to_portal() + end + else + log.debug("Credentials rejected") + local html = inject_data(contents("login.html"), req_data, { + "These credentials were rejected." + }) + return nginx.return_contents(html, "text/html; charset=UTF-8") + end +end + +local function answer_request(req_data) + local login = conf.get_sso_prefix() .. "/login" + if nginx.is(req_data, login) then + if nginx.has_method(req_data, "POST") then + log.info("Checking login") + return check_login(req_data) + else + local html = inject_data(contents("login.html"), req_data, nil) + return nginx.return_contents(html, "text/html; charset=UTF-8") + end + elseif nginx.is(req_data, login .. ".css") then + return nginx.return_contents(contents("login.css"), "text/css; charset=UTF-8") + elseif nginx.is(req_data, login .. ".js") then + return nginx.return_contents(contents("login.js"), "application/javascript; charset=UTF-8") + else + log.info("Unknown login file") + return nginx.answer_not_found(req_data) + end +end + +return { + answer_request = answer_request, + set_root = set_root, +} diff --git a/src/ssso_nginx.lua b/src/ssso_nginx.lua new file mode 100644 index 0000000..889c726 --- /dev/null +++ b/src/ssso_nginx.lua @@ -0,0 +1,156 @@ +local ngx = require("ngx") +local util = require("ssso_util") +local conf = require("ssso_config") + +local function get_request() + local vars = ngx.var + local request = { + referer = vars.http_referer, + host = vars.host, + method = vars.request_method, + uri = vars.request_uri, + } + if request.referer == "" then + request.referer = nil + end + + local target, qp = request.uri, {} + local qm, _ = target:find("%?") + if qm then + qp = ngx.decode_args(target:sub(qm + 1)) + target = target:sub(1, qm - 1) + end + request["target"] = target + request["query_params"] = qp + + local https = vars.proxy_https or vars.https + if https and https ~= "" then + request["scheme"] = "https" + else + request["scheme"] = "http" + end + return request +end + +local function with_post_parameters(req_data) + ngx.req.read_body() + local args, _ = ngx.req.get_post_args() + if args then + for key, val in pairs(args) do + req_data.query_params[key] = val + end + end + return req_data +end + +local function get_jws_cookie() + return ngx.var.cookie_SSSO_TOKEN +end + +local function set_jws_cookie(jws, tslimit) + ngx.header["Set-Cookie"] = "SSSO_TOKEN=" .. jws .. + "; Path=/; Expires=" .. ngx.cookie_time(tslimit) .. + "; Secure" +end + +local function get_seconds_since_epoch() + return math.floor(ngx.now()) +end + +local function add_cookie(name, value) + local cookie = name .. "=" .. value + local old_cookie = ngx.var.http_cookie + if old_cookie and old_cookie ~= "" then + cookie = old_cookie .. "; " .. cookie + end + ngx.log(ngx.DEBUG, "Overriding request Cookie header: " .. cookie) + ngx.req.set_header("Cookie", cookie) +end + +local function add_header(name, value) + ngx.log(ngx.DEBUG, "Setting request " .. name .. " header: " .. value) + ngx.req.set_header(name, value) +end + +local function has_method(req_data, method) + return string.upper(method) == string.upper(req_data.method) +end + +local function is(req_data, url) + return req_data.target == url +end + +local function matches(req_data, lua_pattern) + return req_data.target:match(lua_pattern) +end + +local function starts_with(req_data, prefix) + local i, _ = req_data.target:find(util.str_to_pattern(prefix)) + return 1 == i +end + +local function has_param(req_data, param, value) + return req_data.query_params[param] ~= nil and (value == nil or req_data.query_params[param] == value) +end + +local function answer_not_found(req_data) + return ngx.exit(404) +end + +local function answer_unexpected_error() + ngx.log(ngx.ERROR, "Unexpected Simple-SSO error.") + return ngx.exit(500) +end + +local function redirect_to_page(uri) + return ngx.redirect("https://" .. conf.get_sso_host() .. uri, 302) +end + +local function redirect_to_login(req_data, status) + return ngx.redirect("https://" .. + conf.get_sso_host() .. + conf.get_sso_prefix() .. + "/login?back=" .. + ngx.escape_uri(req_data.uri) .. + "&cause=" .. tostring(status), + 307) +end + +local function redirect_to_portal() + return ngx.redirect("https://" .. conf.get_sso_host() .. conf.get_sso_prefix() .. "/portal", 307) +end + +local function return_contents(contents, mime_and_charset) + ngx.header["Content-Type"] = mime_and_charset + ngx.header["Content-Length"] = tostring(#contents) + ngx.header["Cache-Control"] = "no-store,max-age=0" + ngx.say(contents) + -- TODO: CSRF + return ngx.exit(200) +end + +local function forward_request(req_data) + return +end + +return { + add_cookie = add_cookie, + add_header = add_header, + answer_not_found = answer_not_found, + forward_request = forward_request, + get_jws_cookie = get_jws_cookie, + get_request = get_request, + get_seconds_since_epoch = get_seconds_since_epoch, + has_method = has_method, + has_param = has_param, + is = is, + matches = matches, + redirect_to_login = redirect_to_login, + redirect_to_page = redirect_to_page, + redirect_to_portal = redirect_to_portal, + return_contents = return_contents, + set_jws_cookie = set_jws_cookie, + starts_with = starts_with, + answer_unexpected_error = answer_unexpected_error, + with_post_parameters = with_post_parameters, +} diff --git a/src/ssso_oauth2.lua b/src/ssso_oauth2.lua new file mode 100644 index 0000000..028bb83 --- /dev/null +++ b/src/ssso_oauth2.lua @@ -0,0 +1,14 @@ +local function answer_oidc_webfinger(req_data) + -- TODO + return "TODO" +end + +local function answer_request(req_data, auth) + -- TODO + return "TODO" +end + +return { + answer_oidc_webfinger = answer_oidc_webfinger, + answer_request = answer_request, +} diff --git a/src/ssso_portal.lua b/src/ssso_portal.lua new file mode 100644 index 0000000..5c6a9d3 --- /dev/null +++ b/src/ssso_portal.lua @@ -0,0 +1,55 @@ +local util = require("ssso_util") +local conf = require("ssso_config") +local nginx = require("ssso_nginx") +local prof = require("ssso_profile") +local sites = require("ssso_sites") + +local root = "" + +local function set_root(prefix) + root = prefix .. "/portal/" +end + +local function contents(relative) + local file = assert(io.open(root .. relative, "r"), "Cannot open portal file " .. root .. relative) + local data = file:read("*all") + file:close() + return data +end + +local function inject_data(html, profile) + html = html:gsub("SSSO_USER", util.str_to_html(prof.user(profile))) + html = html:gsub("SSSO_NAME", util.str_to_html(prof.name(profile))) + html = html:gsub("SSSO_EMAIL", util.str_to_html(prof.email(profile))) + local links = "" + local allowed = sites.authorized_links(prof.user(profile)) + table.sort(allowed, function(a1, a2) return a1.label < a2.label end) + for _, allow in ipairs(allowed) do + links = links .. '
  • ' .. util.str_to_html(prof.format(allow.label, profile)) .. "
  • " + end + if links ~= "" then + html = html:gsub('', '") + else + html = html:gsub('', '

    No link to display.

    ') + end + return html +end + +local function answer_request(req_data, profile) + local portal = conf.get_sso_prefix() .. "/portal" + if nginx.is(req_data, portal) then + local html = inject_data(contents("portal.html"), profile) + return nginx.return_contents(html, "text/html; charset=UTF-8") + elseif nginx.is(req_data, portal .. ".css") then + return nginx.return_contents(contents("portal.css"), "text/css; charset=UTF-8") + elseif nginx.is(req_data, portal .. ".js") then + return nginx.return_contents(contents("portal.js"), "application/javascript; charset=UTF-8") + else + return nginx.answer_not_found(req_data) + end +end + +return { + answer_request = answer_request, + set_root = set_root, +} diff --git a/src/ssso_profile.lua b/src/ssso_profile.lua new file mode 100644 index 0000000..e1c5ed1 --- /dev/null +++ b/src/ssso_profile.lua @@ -0,0 +1,63 @@ +local b64 = require("ssso_base64") + +local function build_profile(user, password, name, email) + return { + u = user, + p = password, + n = name, + e = email, + } +end + +local function serialize(profile) + return (profile.u or "\025") .. "\031" .. + (profile.p or "\025") .. "\031" .. + (profile.n or "\025") .. "\031" .. + (profile.e or "\025") .. "\031" +end + +local function deserialize(ser) + local profile + local remainder = ser:gsub("^(.-)\031(.-)\031(.-)\031(.-)\031", function (u, p, n, e) + if u == "\025" then u = nil end + if p == "\025" then p = nil end + if n == "\025" then n = nil end + if e == "\025" then e = nil end + profile = build_profile(u, p, n, e) + return "" + end) + return profile, remainder +end + +local function format(template, profile) + local s = template + s = s:gsub("\ru%.", profile.u or "") + s = s:gsub("\rp%.", profile.p or "") + s = s:gsub("\rn%.", profile.n or "") + s = s:gsub("\re%.", profile.e or "") + s = s:gsub("\rb64%(([^\r]-)%)%.", function(x) return b64.encode_base64(x) end) + s = s:gsub("\ru64%(([^\r]-)%)%.", function(x) return b64.encode_base64url(x) end) + return s +end + +local function email(profile) + return profile.e +end + +local function name(profile) + return profile.n +end + +local function user(profile) + return profile.u +end + +return { + build_profile = build_profile, + deserialize = deserialize, -- TODO: test + serialize = serialize, -- TODO: test + email = email, + format = format, + name = name, + user = user, +} diff --git a/src/ssso_sessions.lua b/src/ssso_sessions.lua new file mode 100644 index 0000000..b681943 --- /dev/null +++ b/src/ssso_sessions.lua @@ -0,0 +1,24 @@ +local crypto = require("ssso_crypto") +local nginx = require("ssso_nginx") + +local function get_session() + local cookie = nginx.get_jws_cookie() + + if not cookie or cookie == "" then + return nil, 401 + end + + local session, jws, tslimit = crypto.get_data_and_new_jws(cookie) + + if session then + nginx.set_jws_cookie(jws, tslimit) + return session, 200 + else + return nil, 403 + end +end + + +return { + get_session = get_session, +} diff --git a/src/ssso_sites.lua b/src/ssso_sites.lua new file mode 100644 index 0000000..c9a8091 --- /dev/null +++ b/src/ssso_sites.lua @@ -0,0 +1,215 @@ +local json = require("cjson.safe") +local nginx = require("ssso_nginx") +local prof = require("ssso_profile") + +local known_private_re = {} +local known_sites = {} + +local function load_sites(prefix) + local f, site + local ls = assert(io.popen("/bin/ls -f1Nb \"" .. prefix .. "/sites\"/*.json", "r"), "popen is required") + for name in ls:lines() do + f = assert(io.open(name, "r"), "File " .. name .. " cannot be read") + site = assert(json.decode(f:read("*all"))) + f:close() + table.insert(known_sites, name) + for _, pat in ipairs(site.patterns) do + if not pat.public then + for _, r in ipairs(pat.lua_regex) do + table.insert(known_private_re, r) + end + end + end + end + ls:close() +end + +local function is_known_private(req_data) + for _, r in ipairs(known_private_re) do + if nginx.matches(req_data, r) then + return true + end + end + return false +end + +local function handle_request(req_data, auth) + if auth then + for _, site in ipairs(auth.ok) do + for _, r in ipairs(site.r) do + if nginx.matches(req_data, r) then + for _, a in ipairs(site.a) do + if a[1] == "C" then + nginx.add_cookie(a[2], prof.format(a[3], auth)) + elseif a[1] == "H" then + nginx.add_header(a[2], prof.format(a[3], auth)) + end + end + return nginx.forward_request(req_data) + end + end + end + for _, r in ipairs(auth.ko) do + if nginx.matches(req_data, r) then + return nginx.redirect_to_login(req_data, 403) + end + end + return nginx.forward_request(req_data) + elseif is_known_private(req_data) then + return nginx.redirect_to_login(req_data, 401) + else + return nginx.forward_request(req_data) + end +end + +local function format_pattern(pattern) + local a_type + local ok = { + r = pattern.lua_regex or {}, + a = {}, + } + for _, action in ipairs(pattern.actions or {}) do + if action.type == "header" then + a_type = "H" + elseif action.type == "cookie" then + a_type = "C" + else + a_type = nil + end + if a_type then + table.insert(ok.a, {a_type, action.name, action.value}) + end + end + return ok +end + +local function with_sites(profile, user) + local f, site, go_on + local ok_list = {} + local ko_list = {} + for _, name in ipairs(known_sites) do + f = io.open(name, "r") + if f then + site = json.decode(f:read("*all")) + f:close() + for _, pat in ipairs(site.patterns or {}) do + go_on = true + for _, denied in ipairs(pat.deny or {}) do + if denied == user then + go_on = false + for _, re in ipairs(pat.lua_regex or {}) do + table.insert(ko_list, re) + end + break + end + end + if go_on then + if pat.public then + local ok = format_pattern(pat) + table.insert(ok_list, ok) + else + for _, allowed in ipairs(pat.allow or {}) do + if allowed == "*" or allowed == user then + local ok = format_pattern(pat) + table.insert(ok_list, ok) + break + end + end + end + end + end + end + end + profile["ok"] = ok_list + profile["ko"] = ko_list + return profile +end + +local function serialize(profile) + local ser_s = "" + for _, site in ipairs(profile.ok or {}) do + for _, r in ipairs(site.r) do + ser_s = ser_s .. r .. "\029" + end + for _, a in ipairs(site.a) do + ser_s = ser_s .. a[1] .. a[2] .. "=" .. a[3] .. "\028" + end + ser_s = ser_s .. "\031" + end + for _, r in ipairs(profile.ko or {}) do + ser_s = ser_s .. r .. "\030" + end + return ser_s +end + +local function deserialize_update(ser, profile) + if not ser or ser == "" then + return profile + end + local ok_list = {} + local ko_list = {} + local remainder = ser:gsub("(.-)\031", function (ser_ok) + local ok = { + r = {}, + a = {}, + } + ser_ok = ser_ok:gsub("(.-)\029", function(r) table.insert(ok.r, r); return "" end) + ser_ok:gsub("(.)([^=]-)=(.-)\028", function(t, n, v) table.insert(ok.a, {t, n, v}) end) + table.insert(ok_list, ok) + return "" + end) + remainder = remainder:gsub("(.-)\030", function (ko) + table.insert(ko_list, ko) + return "" + end) + profile.ok = ok_list + profile.ko = ko_list + return profile, remainder +end + +local function authorized_links(user) + local links = {} + local f, site, go_on + for _, name in ipairs(known_sites) do + f = io.open(name, "r") + if f then + site = json.decode(f:read("*all")) + f:close() + for _, pat in ipairs(site.patterns or {}) do + go_on = true + for _, denied in ipairs(pat.deny or {}) do + if denied == user then + go_on = false + break + end + end + if go_on then + if pat.public then + for link, label in pairs(pat.portal or {}) do + table.insert(links, {link = link, label = label}) + end + else + for _, allowed in ipairs(pat.allow or {}) do + if allowed == "*" or allowed == user then + for link, label in pairs(pat.portal or {}) do + table.insert(links, {link = link, label = label}) + end + break + end + end + end + end + end + end + end + return links +end + +return { + authorized_links = authorized_links, + deserialize_update = deserialize_update, -- TODO: test + handle_request = handle_request, + load_sites = load_sites, + serialize = serialize, -- TODO: test + with_sites = with_sites, +} diff --git a/src/ssso_util.lua b/src/ssso_util.lua new file mode 100644 index 0000000..98f4107 --- /dev/null +++ b/src/ssso_util.lua @@ -0,0 +1,18 @@ +local function str_to_html(s) + s = s:gsub("&", "&") + s = s:gsub("<", "<") + s = s:gsub(">", ">") + s = s:gsub('"', """) + s = s:gsub('%%', "%") -- avoid unwanted substitutions + return s +end + +local function str_to_pattern(s) + s = s:gsub("([%(%)%%%.%+%-%*%?%^%$])", "%%%1") + return s +end + +return { + str_to_html = str_to_html, + str_to_pattern = str_to_pattern, +} diff --git a/test/aes.utest.lua b/test/aes.utest.lua new file mode 100644 index 0000000..dc968ba --- /dev/null +++ b/test/aes.utest.lua @@ -0,0 +1,23 @@ +local lu = require("luaunit") +local aes = require("resty.openssl.cipher") + +function test_aes() + local aes1 = aes.new(nil) + local aes2 = aes.new(nil) + local enc1 = assert(aes1:encrypt("a", nil, "test", nil, nil)) + local enc2 = assert(aes2:encrypt("b", nil, "other", nil, nil)) + local tag1 = aes1:get_aead_tag() + local tag2 = aes2:get_aead_tag() + local aes3 = aes.new(nil) + local aes4 = aes.new(nil) + lu.assertEquals(#tag1, 16) + lu.assertEquals(#tag2, 16) + lu.assertNotEquals(enc1, "test") + lu.assertNotEquals(enc2, "other") + lu.assertNotEquals(enc1 .. tag1, "test") + lu.assertNotEquals(enc2 .. tag2, "other") + lu.assertEquals(aes3:decrypt("a", nil, enc1, nil, nil, tag1), "test") + lu.assertEquals(aes4:decrypt("b", nil, enc2, nil, nil, tag2), "other") +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/alt/bit.lua b/test/alt/bit.lua new file mode 100644 index 0000000..1fd2377 --- /dev/null +++ b/test/alt/bit.lua @@ -0,0 +1 @@ +return require("bit32") diff --git a/test/alt/ngx.lua b/test/alt/ngx.lua new file mode 100644 index 0000000..f56bdd2 --- /dev/null +++ b/test/alt/ngx.lua @@ -0,0 +1,135 @@ +local var = {} +local post_var = {} +local header = {} +local req_header = {} +local resp_body = nil + +local function cookie_time(time) + return tostring(time) +end + +local function now() + return 1626546790.456 +end + +local function set_req_header(k, v) + req_header[k] = v + var["http_" .. k:lower()] = v +end + +local function get_req_header(_, k) + return req_header[k] +end + +local function reset_req_header() + req_header = {} +end + +local function set_header(_, k, v) + if k == "Set-Cookie" then + header[k] = {v = v, link = header[k]} + else + header[k] = v + end +end + +local function get_header(_, k) + return header[k] +end + +local function reset_header() + header = {} +end + +local function set_var(_, k, v) + var[k] = v +end + +local function get_var(_, k) + return var[k] +end + +local function reset_var() + var = {} +end + +local function set_post_var(_, k, v) + post_var[k] = v +end + +local function get_post_var(_, k) + return post_var[k] +end + +local function reset_post_var() + post_var = {} +end + +local function decode_args(argstr) + local params = {} + for p in argstr:gmatch("([^?&=]+[^?&]*)") do + local eq, _ = p:find("=") + if eq then + params[p:sub(1, eq - 1)] = p:sub(eq + 1):lower() -- lower → fake URL-decoding + else + params[p] = true + end + end + return params +end + +local function escape_uri(uri) + return uri +end + +local function exit(status) + return status +end + +local function redirect(url, status) + return tostring(status) .. ":" .. url +end + +local function reset_resp_body() + resp_body = nil +end + +local function say(set_or_get) + if set_or_get then + resp_body = set_or_get + else + return resp_body + end +end + +local function log(level, message) + print(level .. message) +end + +return { + cookie_time = cookie_time, + decode_args = decode_args, + exit = exit, + escape_uri = escape_uri, + header = setmetatable({}, {__newindex = set_header, __index = get_header}), + log = log, + DEBUG = "DEBUG: ", + INFO = "INFO: ", + ERROR = "ERROR: ", + now = now, + post_var = setmetatable({}, {__newindex = set_post_var, __index = get_post_var}), + redirect = redirect, + req = { + get_post_args = function() return post_var end, + header = setmetatable({}, {__newindex = function(_,k,v) set_req_header(k,v) end, __index = get_req_header}), + read_body = function() end, + reset = reset_req_header, + set_header = set_req_header, + }, + reset_header = reset_header, + reset_post_var = reset_post_var, + reset_resp_body = reset_resp_body, + reset_var = reset_var, + say = say, + var = setmetatable({}, {__newindex = set_var, __index = get_var}), +} diff --git a/test/alt/ngx/base64.lua b/test/alt/ngx/base64.lua new file mode 100644 index 0000000..51096be --- /dev/null +++ b/test/alt/ngx/base64.lua @@ -0,0 +1,18 @@ +local b64 = require("base64") + +local function decode_base64url(base64) + base64 = base64:gsub("-", "+") + base64 = base64:gsub("_", "/") + return b64.decode(base64) +end + +local function encode_base64url(plaintext) + local plain = b64.encode(plaintext) + plain = plain:gsub("/", "_") + return plain:gsub("%+", "-") +end + +return { + decode_base64url = decode_base64url, + encode_base64url = encode_base64url, +} diff --git a/test/alt/resty/openssl/cipher.lua b/test/alt/resty/openssl/cipher.lua new file mode 100644 index 0000000..6200328 --- /dev/null +++ b/test/alt/resty/openssl/cipher.lua @@ -0,0 +1,35 @@ +local real_aes = require("resty.easy-crypto") + +local function new(_) + local fake_instance = {} + + function fake_instance:encrypt(key, _, data, _, _) + local aes = real_aes:new({ + saltSize = 16, + ivSize = 12, + iterationCount = 2, + }) + local encrypted = assert(aes:encrypt(key, data)) + self.tag = encrypted:sub(-16) + return encrypted:sub(1, -17), nil + end + + function fake_instance:get_aead_tag() + return self.tag + end + + function fake_instance:decrypt(key, _, data, _, _, tag) + local aes = real_aes:new({ + saltSize = 16, + ivSize = 12, + iterationCount = 2, + }) + return aes:decrypt(key, data .. tag) + end + + return fake_instance +end + +return { + new = new, +} diff --git a/test/alt/resty/random.lua b/test/alt/resty/random.lua new file mode 100644 index 0000000..86434b8 --- /dev/null +++ b/test/alt/resty/random.lua @@ -0,0 +1,9 @@ +local ssl_rand = require("openssl.rand") + +local function bytes(count, _) + return ssl_rand.bytes(count) +end + +return { + bytes = bytes, +} diff --git a/test/alt/resty/sha256.lua b/test/alt/resty/sha256.lua new file mode 100644 index 0000000..03b7ba4 --- /dev/null +++ b/test/alt/resty/sha256.lua @@ -0,0 +1,20 @@ +local real_sha = require("bgcrypto.sha256") +local sha_proxy = {} + +function sha_proxy:new() + local fake_instance = { + data = "", + } + + function fake_instance:update(data) + self.data = self.data .. data + end + + function fake_instance:final() + return real_sha.digest(self.data, true) + end + + return fake_instance +end + +return sha_proxy diff --git a/test/anonymous1.ctest.lua b/test/anonymous1.ctest.lua new file mode 100644 index 0000000..acfb01b --- /dev/null +++ b/test/anonymous1.ctest.lua @@ -0,0 +1,18 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_anonymous_access_to_unknown_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/unknown" + -- when + local resp = require("do_access") + -- then + lu.assertTrue(resp) -- require changes nil into true + lu.assertNil(ngx.req.header["Cookie"]) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/anonymous2.ctest.lua b/test/anonymous2.ctest.lua new file mode 100644 index 0000000..5ce6f9f --- /dev/null +++ b/test/anonymous2.ctest.lua @@ -0,0 +1,18 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_anonymous_access_to_public_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/public/page" + -- when + local resp = require("do_access") + -- then + lu.assertTrue(resp) -- require changes nil into true + lu.assertNil(ngx.req.header["Cookie"]) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/anonymous3.ctest.lua b/test/anonymous3.ctest.lua new file mode 100644 index 0000000..9986310 --- /dev/null +++ b/test/anonymous3.ctest.lua @@ -0,0 +1,18 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_anonymous_access_to_public_page_of_mixed_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/mixed/bob/wiki/foo.adoc" + -- when + local resp = require("do_access") + -- then + lu.assertTrue(resp) -- require changes nil into true + lu.assertNil(ngx.req.header["Cookie"]) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/anonymous4.ctest.lua b/test/anonymous4.ctest.lua new file mode 100644 index 0000000..7210893 --- /dev/null +++ b/test/anonymous4.ctest.lua @@ -0,0 +1,18 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_anonymous_access_to_private_page_of_mixed_site_sent_to_login_401() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/mixed/bob/wiki/_new" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/mixed/bob/wiki/_new&cause=401") + lu.assertNil(ngx.req.header["Cookie"]) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/anonymous5.ctest.lua b/test/anonymous5.ctest.lua new file mode 100644 index 0000000..89fcfe8 --- /dev/null +++ b/test/anonymous5.ctest.lua @@ -0,0 +1,18 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_anonymous_access_to_private_site_sent_to_login_401() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/private/page" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/private/page&cause=401") + lu.assertNil(ngx.req.header["Authorization"]) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/auth.utest.lua b/test/auth.utest.lua new file mode 100644 index 0000000..9d4472e --- /dev/null +++ b/test/auth.utest.lua @@ -0,0 +1,19 @@ +local lu = require("luaunit") +local auth = require("ssso_auth") +local conf = require("ssso_config") + +local here = debug.getinfo(1).source:sub(2, -16) +conf.load_conf(here) + +function test_read_user_with_good_credentials_returns_name_and_email() + lu.assertEquals(auth.read_user("U", "goodpassword"), { + name = "U", + email = "U@example.org", + }) +end + +function test_read_user_with_bad_credentials_returns_nil() + lu.assertNil(auth.read_user("U", "badpassword")) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/config.utest.lua b/test/config.utest.lua new file mode 100644 index 0000000..15c2694 --- /dev/null +++ b/test/config.utest.lua @@ -0,0 +1,16 @@ +local lu = require("luaunit") +local conf = require("ssso_config") + +local here = debug.getinfo(1).source:sub(2, -18) +conf.load_conf(here) + +function test_config() + lu.assertEquals(conf.get_auth_commands(), { + check = "if [ \"\rp.\" == \"goodpassword\" ]; then printf '%s\\n%s\\n' '\ru.' '\ru.@example.org'; fi", + }) + lu.assertEquals(conf.get_session_seconds(), 3600) + lu.assertEquals(conf.get_sso_host(), "my-domain.tld") + lu.assertEquals(conf.get_sso_prefix(), "/ssso") +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/crypto.utest.lua b/test/crypto.utest.lua new file mode 100644 index 0000000..eae8cc1 --- /dev/null +++ b/test/crypto.utest.lua @@ -0,0 +1,50 @@ +local lu = require("luaunit") +local conf = require("ssso_config") +local crypt = require("ssso_crypto") + +local here = debug.getinfo(1).source:sub(2, -18) +conf.load_conf(here) + +local data = { + u = "u", + e = "u@h", + ok = { + { + r = { + "regex1", + }, + a = { + {"C", "Cn", "Cv"}, + {"H", "Hn", "Hv"}, + } + }, + }, + ko = { + "regex2", + }, +} + +function test_jws_is_well_structured() + local jws, _ = crypt.get_jws_and_tslimit(data) + lu.assertStrMatches(jws, "[^%.]+%.[^%.]+%.[^%.]+") +end + +function test_jws_can_be_decoded() + local jws, _ = crypt.get_jws_and_tslimit(data) + local stored, _, _ = crypt.get_data_and_new_jws(jws) + lu.assertEquals(stored, data) +end + +function test_data_must_contain_field_u() + local wrong = { + i = 1, + f = 2.3, + b = true, + n = nil, + } + local jws, ts = crypt.get_jws_and_tslimit(wrong) + lu.assertNil(jws) + lu.assertNil(ts) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/global.json b/test/global.json new file mode 100644 index 0000000..3c5296d --- /dev/null +++ b/test/global.json @@ -0,0 +1,8 @@ +{ + "auth": { + "check": "if [ \"\rp.\" == \"goodpassword\" ]; then printf '%s\\n%s\\n' '\ru.' '\ru.@example.org'; fi" + }, + "session_seconds": 3600, + "sso_host": "my-domain.tld", + "sso_prefix": "/ssso" +} diff --git a/test/login.utest.lua b/test/login.utest.lua new file mode 100644 index 0000000..4f8e367 --- /dev/null +++ b/test/login.utest.lua @@ -0,0 +1,203 @@ +local lu = require("luaunit") +local ngx = require("ngx") +local conf = require("ssso_config") +local login = require("ssso_login") +local ng = require("ssso_nginx") + +local here = debug.getinfo(1).source:sub(2, -17) +conf.load_conf(here) +login.set_root(here) + +function test_get_login_url_returns_html_with_back_url_substitution() + -- given + ngx.reset_header() + ngx.reset_var() + ngx.reset_resp_body() + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/login?back=/somewhere" + local r = ng.get_request() + local expected = [[ + + + + + + +]] + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +function test_login_css_url_returns_css() + -- given + ngx.reset_header() + ngx.reset_var() + ngx.reset_resp_body() + ngx.var.request_method = "BLABLA" + ngx.var.request_uri = "/ssso/login.css" + local r = ng.get_request() + local expected = "/*CSS*/\n" + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "text/css; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +function test_login_js_url_returns_js() + -- given + ngx.reset_header() + ngx.reset_var() + ngx.reset_resp_body() + ngx.var.request_method = "BLABLA" + ngx.var.request_uri = "/ssso/login.js" + local r = ng.get_request() + local expected = "//JS\n" + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "application/javascript; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +function test_unknown_login_url_returns_404() + -- given + ngx.reset_var() + ngx.reset_resp_body() + ngx.var.request_method = "BLABLA" + ngx.var.request_uri = "/ssso/login/unknown" + local r = ng.get_request() + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, 404) + lu.assertNil(ngx.say()) +end + +function test_get_login_url_with_cause_401_returns_html_with_associated_message() + -- given + ngx.reset_header() + ngx.reset_var() + ngx.reset_resp_body() + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/login?cause=401" + local r = ng.get_request() + local expected = [[ + + + +

    Credentials are needed to access this resource.

    + + +]] + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +function test_get_login_url_with_cause_403_returns_html_with_associated_message() + -- given + ngx.reset_header() + ngx.reset_var() + ngx.reset_resp_body() + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/login?cause=403" + local r = ng.get_request() + local expected = [[ + + + +

    Different credentials might grant access to this resource.

    + + +]] + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +function test_post_login_url_with_wrong_credentials_returns_html_with_associated_message() + -- given + ngx.reset_header() + ngx.reset_var() + ngx.reset_resp_body() + ngx.reset_post_var() + ngx.var.request_method = "POST" + ngx.var.request_uri = "/ssso/login" + local r = ng.get_request() + local expected = [[ + + + +

    These credentials were rejected.

    + + +]] + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +function test_post_login_url_with_good_credentials_redirects_to_portal_with_session_cookie() + -- given + ngx.reset_header() + ngx.reset_var() + ngx.reset_resp_body() + ngx.reset_post_var() + ngx.var.request_method = "POST" + ngx.var.request_uri = "/ssso/login" + ngx.post_var.login = "goodlogin" + ngx.post_var.password = "goodpassword" + local r = ng.get_request() + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/portal") + lu.assertNil(ngx.say()) + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +function test_post_login_url_with_good_credentials_and_back_url_redirects_to_given_url_with_session_cookie() + -- given + ngx.reset_header() + ngx.reset_var() + ngx.reset_resp_body() + ngx.reset_post_var() + ngx.var.request_method = "POST" + ngx.var.request_uri = "/ssso/login" + ngx.post_var.login = "goodlogin" + ngx.post_var.password = "goodpassword" + ngx.post_var.back = "/somewhere" + local r = ng.get_request() + -- when + local resp = login.answer_request(r) + -- then + lu.assertEquals(resp, "302:https://my-domain.tld/somewhere") + lu.assertNil(ngx.say()) + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/login/login.css b/test/login/login.css new file mode 100644 index 0000000..89f2b16 --- /dev/null +++ b/test/login/login.css @@ -0,0 +1 @@ +/*CSS*/ diff --git a/test/login/login.html b/test/login/login.html new file mode 100644 index 0000000..cc731c0 --- /dev/null +++ b/test/login/login.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/login/login.js b/test/login/login.js new file mode 100644 index 0000000..a688119 --- /dev/null +++ b/test/login/login.js @@ -0,0 +1 @@ +//JS diff --git a/test/login1.ctest.lua b/test/login1.ctest.lua new file mode 100644 index 0000000..59c08d0 --- /dev/null +++ b/test/login1.ctest.lua @@ -0,0 +1,28 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_login_url_returns_html_with_cause_displayed() + -- given + ngx.reset_resp_body() + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/login?back=/private/page&cause=403" + local expected = [[ + + + +

    Different credentials might grant access to this resource.

    + + +]] + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/login2.ctest.lua b/test/login2.ctest.lua new file mode 100644 index 0000000..a131c13 --- /dev/null +++ b/test/login2.ctest.lua @@ -0,0 +1,21 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_login_css_url_returns_css() + -- given + ngx.reset_resp_body() + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/login.css" + local expected = "/*CSS*/\n" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.header["Content-Type"], "text/css; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) + lu.assertEquals(ngx.say(), expected) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/login3.ctest.lua b/test/login3.ctest.lua new file mode 100644 index 0000000..dc101cc --- /dev/null +++ b/test/login3.ctest.lua @@ -0,0 +1,21 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_login_js_url_returns_js() + -- given + ngx.reset_resp_body() + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/login.js" + local expected = "//JS\n" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.header["Content-Type"], "application/javascript; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) + lu.assertEquals(ngx.say(), expected) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/login4.ctest.lua b/test/login4.ctest.lua new file mode 100644 index 0000000..c227f61 --- /dev/null +++ b/test/login4.ctest.lua @@ -0,0 +1,20 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_unknown_login_url_returns_404() + -- given + ngx.reset_resp_body() + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/login.html" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 404) + lu.assertNil(ngx.header["Content-Type"]) + lu.assertNil(ngx.header["Content-Length"]) + lu.assertNil(ngx.say()) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/login5.ctest.lua b/test/login5.ctest.lua new file mode 100644 index 0000000..6edc285 --- /dev/null +++ b/test/login5.ctest.lua @@ -0,0 +1,23 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_successful_login_with_back_url_goes_to_url() + -- given + ngx.reset_var() + ngx.reset_post_var() + ngx.var.request_method = "POST" + ngx.var.request_uri = "/ssso/login" + ngx.post_var.login = "goodlogin" + ngx.post_var.password = "goodpassword" + ngx.post_var.back = "/private/page" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, "302:https://my-domain.tld/private/page") + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/login6.ctest.lua b/test/login6.ctest.lua new file mode 100644 index 0000000..2da72aa --- /dev/null +++ b/test/login6.ctest.lua @@ -0,0 +1,32 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_failed_login_returns_to_login_with_message() + -- given + ngx.reset_var() + ngx.reset_post_var() + ngx.var.request_method = "POST" + ngx.var.request_uri = "/ssso/login" + ngx.post_var.login = "goodlogin" + ngx.post_var.password = "badpassword" + ngx.post_var.back = "/private/page" + local expected = [[ + + + +

    These credentials were rejected.

    + + +]] + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/login7.ctest.lua b/test/login7.ctest.lua new file mode 100644 index 0000000..91300b5 --- /dev/null +++ b/test/login7.ctest.lua @@ -0,0 +1,22 @@ +local lu = require("luaunit") +local ngx = require("ngx") + +require("do_init") + +function test_successful_login_without_back_url_goes_to_portal() + -- given + ngx.reset_var() + ngx.reset_post_var() + ngx.var.request_method = "POST" + ngx.var.request_uri = "/ssso/login" + ngx.post_var.login = "goodlogin" + ngx.post_var.password = "goodpassword" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/portal") + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/nginx.utest.lua b/test/nginx.utest.lua new file mode 100644 index 0000000..fbbe1d5 --- /dev/null +++ b/test/nginx.utest.lua @@ -0,0 +1,313 @@ +local lu = require("luaunit") +local conf = require("ssso_config") + +local here = debug.getinfo(1).source:sub(2, -17) +conf.load_conf(here) + +local ngx = require("ngx") +local ng = require("ssso_nginx") + +function test_refe_host_meth_uri_taken_from_ngx() + -- given + ngx.reset_var() + ngx.var.http_referer = "R" + ngx.var.host = "H" + ngx.var.request_method = "M" + ngx.var.request_uri = "U" + -- when + local r = ng.get_request() + -- then + lu.assertEquals(r.referer, "R") + lu.assertEquals(r.host, "H") + lu.assertEquals(r.method, "M") + lu.assertEquals(r.uri, "U") + lu.assertEquals(r.target, "U") + lu.assertEquals(r.query_params, {}) +end + +function test_empty_referer_reported_as_nil() + -- given + ngx.reset_var() + ngx.var.request_uri = "U" + ngx.var.http_referer = "" + -- when + local r = ng.get_request() + -- then + lu.assertEquals(r.referer, nil) +end + +function test_query_params_split_from_uri_and_decoded() + -- given + ngx.reset_var() + ngx.var.request_uri = "U?P=V&Q=W" + -- when + local r = ng.get_request() + -- then + lu.assertEquals(r.uri, "U?P=V&Q=W") + lu.assertEquals(r.target, "U") + lu.assertEquals(r.query_params, {P = "v", Q = "w"}) +end + +function test_default_scheme_is_http() + -- given + ngx.reset_var() + ngx.var.request_uri = "U" + -- when + local r = ng.get_request() + -- then + lu.assertEquals(r.scheme, "http") +end + +function test_scheme_is_https_when_proxy_https_var() + -- given + ngx.reset_var() + ngx.var.request_uri = "U" + ngx.var.proxy_https = 1 + -- when + local r = ng.get_request() + -- then + lu.assertEquals(r.scheme, "https") +end + +function test_scheme_is_https_when_https_var() + -- given + ngx.reset_var() + ngx.var.request_uri = "U" + ngx.var.https = 1 + -- when + local r = ng.get_request() + -- then + lu.assertEquals(r.scheme, "https") +end + +function test_get_jws_cookie_returns_the_cookie_from_nginx() + -- given + ngx.reset_var() + ngx.var.cookie_SSSO_TOKEN = "cookie" + -- when + local c = ng.get_jws_cookie() + -- then + lu.assertEquals(c, "cookie") +end + +function test_jws_cookie_sent_back_through_ngx() + -- given + ngx.reset_header() + -- when + ng.set_jws_cookie("J", 999) + -- then + lu.assertEquals(ngx.header["Set-Cookie"], {v = "SSSO_TOKEN=J; Path=/; Expires=999; Secure", link = nil}) +end + +function test_ngx_now_converted_to_integer() + lu.assertEquals(ng.get_seconds_since_epoch(), 1626546790) +end + +function test_add_cookie_works_once() + -- given + ngx.reset_var() + ngx.req.reset() + -- when + ng.add_cookie("C", "V") + -- then + lu.assertEquals(ngx.req.header["Cookie"], "C=V") +end + +function test_add_cookie_works_twice() + -- given + ngx.reset_var() + ngx.req.reset() + -- when + ng.add_cookie("C1", "V1") + ng.add_cookie("C2", "V2") + -- then + lu.assertEquals(ngx.req.header["Cookie"], "C1=V1; C2=V2") +end + +function test_add_header_works() + -- given + ngx.reset_var() + ngx.req.reset() + -- when + ng.add_header("H1", "V1") + ng.add_header("H2", "V2") + ng.add_header("H1", "V3") + -- then + lu.assertEquals(ngx.req.header["H1"], "V3") + lu.assertEquals(ngx.req.header["H2"], "V2") +end + +function test_method_is_recognized_case_insensitive() + -- given + ngx.reset_var() + ngx.var.request_method = "get" + ngx.var.request_uri = "U" + local r = ng.get_request() + -- when + local is_get = ng.has_method(r, "GET") + local is_post = ng.has_method(r, "POST") + -- then + lu.assertTrue(is_get and true) + lu.assertFalse(is_post or false) +end + +function test_uri_identity_ignores_query_parameters() + -- given + ngx.reset_var() + ngx.var.request_uri = "U?P=V&Q=W" + local r = ng.get_request() + -- when + local is_without_qp = ng.is(r, "U") + local is_with_qp = ng.is(r, "U?P=V&Q=W") + -- then + lu.assertTrue(is_without_qp and true) + lu.assertFalse(is_with_qp or false) +end + +function test_uri_match_ignores_query_parameters() + -- given + ngx.reset_var() + ngx.var.request_uri = "/aa?bb" + local r = ng.get_request() + -- when + local matches_without_qp = ng.matches(r, "/a+$") + local matches_with_qp = ng.matches(r, "/a.*b") + -- then + lu.assertTrue(matches_without_qp and true) + lu.assertFalse(matches_with_qp or false) +end + +function test_starts_with_ignores_query_parameters() + -- given + ngx.reset_var() + ngx.var.request_uri = "/aa?bb" + local r = ng.get_request() + -- when + local start_without_qp = ng.starts_with(r, "/a") + local start_with_qp = ng.starts_with(r, "/aa?b") + -- then + lu.assertTrue(start_without_qp and true) + lu.assertFalse(start_with_qp or false) +end + +function test_starts_with_must_start_with_given_value() + -- given + ngx.reset_var() + ngx.var.request_uri = "/aa?bb" + local r = ng.get_request() + -- when + local start_in_middle = ng.starts_with(r, "aa") + local does_not_start = ng.starts_with(r, "x") + -- then + lu.assertFalse(start_in_middle or false) + lu.assertFalse(does_not_start or false) +end + +function test_has_param_works_disregarding_the_value() + -- given + ngx.reset_var() + ngx.var.request_uri = "/aa?bb&c=1" + local r = ng.get_request() + -- when + local has_unknown_param = ng.has_param(r, "b") + local has_unvalued_param = ng.has_param(r, "bb") + local has_valued_param = ng.has_param(r, "c") + -- then + lu.assertFalse(has_unknown_param or false) + lu.assertTrue(has_unvalued_param and true) + lu.assertTrue(has_valued_param and true) +end + +function test_has_param_works_with_a_correct_value() + -- given + ngx.reset_var() + ngx.var.request_uri = "/aa?bb&c=1" + local r = ng.get_request() + -- when + local has_unvalued_param = ng.has_param(r, "bb", true) + local has_valued_param = ng.has_param(r, "c", "1") + -- then + lu.assertTrue(has_unvalued_param and true) + lu.assertTrue(has_valued_param and true) +end + +function test_has_param_works_with_a_wrong_value() + -- given + ngx.reset_var() + ngx.var.request_uri = "/aa?bb&c=1" + local r = ng.get_request() + -- when + local has_unknown_param = ng.has_param(r, "b", "x") + local has_unvalued_param = ng.has_param(r, "bb", "x") + local has_valued_param = ng.has_param(r, "c", "x") + -- then + lu.assertFalse(has_unknown_param or false) + lu.assertFalse(has_unvalued_param or false) + lu.assertFalse(has_valued_param or false) +end + +function test_answer_not_found_returns_404() + lu.assertEquals(ng.answer_not_found(), 404) +end + +function test_answer_unexpected_error_returns_500() + lu.assertEquals(ng.answer_unexpected_error(), 500) +end + +function test_redirect_to_page_prepends_host() + lu.assertEquals(ng.redirect_to_page("/url"), "302:https://my-domain.tld/url") +end + +function test_redirect_to_portal() + lu.assertEquals(ng.redirect_to_portal(), "307:https://my-domain.tld/ssso/portal") +end + +function test_return_contents_returns_data_and_headers_http_200() + -- given + ngx.reset_resp_body() + ngx.reset_header() + -- when + local resp = ng.return_contents("Contents", "Mime; Charset") + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), "Contents") + lu.assertEquals(ngx.header["Content-Type"], "Mime; Charset") + lu.assertEquals(ngx.header["Content-Length"], "8") + lu.assertEquals(ngx.header["Cache-Control"], "no-store,max-age=0") +end + +function test_with_post_parameters_merges_post_parameters_to_request_data() + -- given + ngx.reset_post_var() + ngx.reset_var() + ngx.var.request_uri = "url?p&q=3" + ngx.post_var.p = "5" + ngx.post_var.r = "hello" + -- when (1) + local r = ng.get_request() + -- then (1) + lu.assertEquals(r, { + scheme = "http", + uri = "url?p&q=3", + target = "url", + query_params = { + p = true, + q = "3", + }, + }) + -- when (2) + r = ng.with_post_parameters(r) + -- then (2) + lu.assertEquals(r, { + scheme = "http", + uri = "url?p&q=3", + target = "url", + query_params = { + p = "5", + q = "3", + r = "hello", + }, + }) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/portal/portal.css b/test/portal/portal.css new file mode 100644 index 0000000..89f2b16 --- /dev/null +++ b/test/portal/portal.css @@ -0,0 +1 @@ +/*CSS*/ diff --git a/test/portal/portal.html b/test/portal/portal.html new file mode 100644 index 0000000..a961516 --- /dev/null +++ b/test/portal/portal.html @@ -0,0 +1,7 @@ + + + + + SSSO_USER SSSO_NAME SSSO_EMAIL + + diff --git a/test/portal/portal.js b/test/portal/portal.js new file mode 100644 index 0000000..a688119 --- /dev/null +++ b/test/portal/portal.js @@ -0,0 +1 @@ +//JS diff --git a/test/portal1.ctest.lua b/test/portal1.ctest.lua new file mode 100644 index 0000000..2bc2122 --- /dev/null +++ b/test/portal1.ctest.lua @@ -0,0 +1,37 @@ +local lu = require("luaunit") +local ngx = require("ngx") +local crypto = require("ssso_crypto") + +require("do_init") + +function test_portal_url_returns_html_with_authorized_links_and_identity() + -- given + local jws, _ = crypto.get_jws_and_tslimit({ + u = "guest", + p = "", + n = "Guest", + e = "guest@example.org", + }) + ngx.reset_resp_body() + ngx.reset_var() + ngx.var.cookie_SSSO_TOKEN = jws + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/portal" + local expected = [[ + + + + guest Guest guest@example.org + + +]] + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.say(), expected) + lu.assertEquals(ngx.header["Content-Type"], "text/html; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/portal2.ctest.lua b/test/portal2.ctest.lua new file mode 100644 index 0000000..82a66cd --- /dev/null +++ b/test/portal2.ctest.lua @@ -0,0 +1,25 @@ +local lu = require("luaunit") +local ngx = require("ngx") +local crypto = require("ssso_crypto") + +require("do_init") + +function test_portal_css_url_returns_css() + -- given + local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"}) + ngx.reset_resp_body() + ngx.reset_var() + ngx.var.cookie_SSSO_TOKEN = jws + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/portal.css" + local expected = "/*CSS*/\n" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.header["Content-Type"], "text/css; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) + lu.assertEquals(ngx.say(), expected) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/portal3.ctest.lua b/test/portal3.ctest.lua new file mode 100644 index 0000000..982928a --- /dev/null +++ b/test/portal3.ctest.lua @@ -0,0 +1,25 @@ +local lu = require("luaunit") +local ngx = require("ngx") +local crypto = require("ssso_crypto") + +require("do_init") + +function test_portal_js_url_returns_js() + -- given + local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"}) + ngx.reset_resp_body() + ngx.reset_var() + ngx.var.cookie_SSSO_TOKEN = jws + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/portal.js" + local expected = "//JS\n" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 200) + lu.assertEquals(ngx.header["Content-Type"], "application/javascript; charset=UTF-8") + lu.assertEquals(ngx.header["Content-Length"], tostring(#expected)) + lu.assertEquals(ngx.say(), expected) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/portal4.ctest.lua b/test/portal4.ctest.lua new file mode 100644 index 0000000..935cff6 --- /dev/null +++ b/test/portal4.ctest.lua @@ -0,0 +1,22 @@ +local lu = require("luaunit") +local ngx = require("ngx") +local crypto = require("ssso_crypto") + +require("do_init") + +function test_unknown_portal_url_returns_404() + -- given + local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"}) + ngx.reset_resp_body() + ngx.reset_var() + ngx.var.cookie_SSSO_TOKEN = jws + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/portal.html" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, 404) + lu.assertNil(ngx.say()) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/portal5.ctest.lua b/test/portal5.ctest.lua new file mode 100644 index 0000000..50fb389 --- /dev/null +++ b/test/portal5.ctest.lua @@ -0,0 +1,19 @@ +local lu = require("luaunit") +local ngx = require("ngx") +local crypto = require("ssso_crypto") + +require("do_init") + +function test_portal_url_is_for_authenticated_users_only() + -- given + ngx.reset_resp_body() + ngx.reset_var() + ngx.var.request_method = "GET" + ngx.var.request_uri = "/ssso/portal" + -- when + local resp = require("do_access") + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/ssso/portal&cause=401") +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/profile.utest.lua b/test/profile.utest.lua new file mode 100644 index 0000000..577cd79 --- /dev/null +++ b/test/profile.utest.lua @@ -0,0 +1,81 @@ +local lu = require("luaunit") +local prf = require("ssso_profile") + +function test_format_replaces_user_placeholders() + local profile = { + u = "U", + } + local template = '{user: "\ru.", foo: "bar", name: "\ru."}' + lu.assertEquals(prf.format(template, profile), '{user: "U", foo: "bar", name: "U"}') +end + +function test_format_replaces_password_placeholders() + local profile = { + p = "P", + } + local template = '{pass: "\rp.", foo: "bar", secret: "\rp."}' + lu.assertEquals(prf.format(template, profile), '{pass: "P", foo: "bar", secret: "P"}') +end + +function test_format_replaces_name_placeholders() + local profile = { + n = "N", + } + local template = '{name: "\rn.", foo: "bar", nickname: "\rn."}' + lu.assertEquals(prf.format(template, profile), '{name: "N", foo: "bar", nickname: "N"}') +end + +function test_format_replaces_email_placeholders() + local profile = { + e = "user@host", + } + local template = '{user: "\re.", foo: "bar", mail: "\re."}' + lu.assertEquals(prf.format(template, profile), '{user: "user@host", foo: "bar", mail: "user@host"}') +end + +function test_format_replaces_base64_calls() + local profile = { + u = "👤", + p = "🔒", + } + local template = 'Authorization: Basic \rb64(\ru.:\rp.).' + lu.assertEquals(prf.format(template, profile), 'Authorization: Basic 8J+RpDrwn5SS') +end + +function test_format_replaces_base64url_calls() + local profile = { + u = "👤", + p = "🔒", + } + local template = '?authorization=Basic+\ru64(\ru.:\rp.).' + lu.assertEquals(prf.format(template, profile), '?authorization=Basic+8J-RpDrwn5SS') +end + +function test_email_returns_the_profile_s_email() + local profile = { + e = "E", + } + lu.assertEquals(prf.email(profile), "E") +end + + +function test_name_returns_the_profile_s_name() + local profile = { + n = "N", + } + lu.assertEquals(prf.name(profile), "N") +end + + +function test_user_returns_the_profile_s_user() + local profile = { + u = "U", + } + lu.assertEquals(prf.user(profile), "U") +end + +function test_build_profile_returns_the_given_information() + lu.assertEquals(prf.build_profile("U", "P", "N", "E"), {u = "U", p = "P", n = "N", e = "E"}) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/random.utest.lua b/test/random.utest.lua new file mode 100644 index 0000000..6b20b49 --- /dev/null +++ b/test/random.utest.lua @@ -0,0 +1,12 @@ +local lu = require("luaunit") +local rnd = require("resty.random") + +function test_random() + local r1 = rnd.bytes(5, true) + local r2 = rnd.bytes(5, false) + lu.assertEquals(#r1, 5) + lu.assertEquals(#r2, 5) + lu.assertNotEquals(r1, r2) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/sessions.utest.lua b/test/sessions.utest.lua new file mode 100644 index 0000000..28d1f02 --- /dev/null +++ b/test/sessions.utest.lua @@ -0,0 +1,61 @@ +local lu = require("luaunit") +local sess = require("ssso_sessions") +local conf = require("ssso_config") +local crypt = require("ssso_crypto") +local ngx = require("ngx") + +local here = debug.getinfo(1).source:sub(2, -20) +conf.load_conf(here) + +function test_no_session_and_hint_401_if_no_cookie() + -- given + ngx.req.reset() + ngx.reset_var() + -- when + local s, h = sess.get_session() + -- then + lu.assertNil(s) + lu.assertEquals(h, 401) +end + +function test_no_session_and_hint_401_if_empty_cookie() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.cookie_SSSO_TOKEN = "" + -- when + local s, h = sess.get_session() + -- then + lu.assertNil(s) + lu.assertEquals(h, 401) +end + +function test_no_session_and_hint_403_if_bad_cookie() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.cookie_SSSO_TOKEN = "zzz" + -- when + local s, h = sess.get_session() + -- then + lu.assertNil(s) + lu.assertEquals(h, 403) +end + +function test_session_and_cookie_renewal_if_good_cookie() + -- given + ngx.req.reset() + ngx.reset_var() + local data = {u = "bob"} + local c, _ = crypt.get_jws_and_tslimit(data) + ngx.var.cookie_SSSO_TOKEN = c + -- when + local s, h = sess.get_session() + -- then + lu.assertEquals(s, data) + lu.assertEquals(h, 200) + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/sha256.utest.lua b/test/sha256.utest.lua new file mode 100644 index 0000000..551daf6 --- /dev/null +++ b/test/sha256.utest.lua @@ -0,0 +1,13 @@ +local lu = require("luaunit") +local sha256 = require("resty.sha256") + +function test_sha256() + local sha1 = sha256:new() + sha1:update("test") + local sha2 = sha256:new() + sha2:update("other") + lu.assertEquals(sha1:final(), "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08") + lu.assertEquals(sha2:final(), "d9298a10d1b0735837dc4bd85dac641b0f3cef27a47e5d53a54f2f3f5b2fcffa") +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/sites.utest.lua b/test/sites.utest.lua new file mode 100644 index 0000000..23e346f --- /dev/null +++ b/test/sites.utest.lua @@ -0,0 +1,289 @@ +local lu = require("luaunit") +local ngx = require("ngx") +local conf = require("ssso_config") +local ng = require("ssso_nginx") +local sites = require("ssso_sites") + +local here = debug.getinfo(1).source:sub(2, -17) +conf.load_conf(here) +sites.load_sites(here) + +function test_anonymous_access_to_unknown_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/unknown" + local r = ng.get_request() + -- when + local resp = sites.handle_request(r, nil) + -- then + lu.assertNil(resp) + lu.assertNil(ngx.req.header["Cookie"]) +end + +function test_anonymous_access_to_public_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/public/page" + local r = ng.get_request() + -- when + local resp = sites.handle_request(r, nil) + -- then + lu.assertNil(resp) + lu.assertNil(ngx.req.header["Cookie"]) +end + +function test_anonymous_access_to_public_page_of_mixed_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/mixed/bob/wiki/foo.adoc" + local r = ng.get_request() + -- when + local resp = sites.handle_request(r, nil) + -- then + lu.assertNil(resp) + lu.assertNil(ngx.req.header["Cookie"]) +end + +function test_anonymous_access_to_private_page_of_mixed_site_redirected_401() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/mixed/bob/wiki/_new" + local r = ng.get_request() + -- when + local resp = sites.handle_request(r, nil) + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/mixed/bob/wiki/_new&cause=401") + lu.assertNil(ngx.req.header["Cookie"]) +end + +function test_anonymous_access_to_private_site_redirected_401() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/private/page" + local r = ng.get_request() + -- when + local resp = sites.handle_request(r, nil) + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/private/page&cause=401") + lu.assertNil(ngx.req.header["Authorization"]) +end + +function test_authenticated_access_to_unknown_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/unknown" + local r = ng.get_request() + local profile = { + u = "U", + ok = {}, + ko = {}, + } + -- when + local resp = sites.handle_request(r, profile) + -- then + lu.assertNil(resp) + lu.assertNil(ngx.req.header["Cookie"]) +end + +function test_authenticated_access_to_public_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/public/page" + local r = ng.get_request() + local profile = { + u = "U", + p = "P", + ok = { + { + r = { + "^/public", + }, + a = { + {"C", "X-PROXY-USER", "\ru."}, + {"C", "X-PROXY-PASS", "\rp."}, + }, + }, + } + } + -- when + local resp = sites.handle_request(r, profile) + -- then + lu.assertNil(resp) + lu.assertEquals(ngx.req.header["Cookie"], "X-PROXY-USER=U; X-PROXY-PASS=P") +end + + +function test_authenticated_access_to_public_site_can_be_denied() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/public/page" + local r = ng.get_request() + local profile = { + u = "banned", + ok = {}, + ko = { + "^/public", + } + } + -- when + local resp = sites.handle_request(r, profile) + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/public/page&cause=403") + lu.assertNil(ngx.req.header["Cookie"]) +end + +function test_authenticated_access_to_public_page_of_mixed_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/mixed/bob/wiki/foo.adoc" + local r = ng.get_request() + local profile = { + u = "U", + p = "P", + ok = { + { + r = { + "^/public", + }, + a = { + {"C", "X-PROXY-USER", "\ru."}, + {"C", "X-PROXY-PASS", "\rp."}, + }, + }, + { + r = { + "^/mixed/admin", + "^/mixed/.-/wiki/_new", + }, + a = { + {"C", "X-PROXY-USER", "\ru."}, + {"C", "X-PROXY-PASSWORD", "\rp."}, + {"H", "Authorization", "Basic \rb64(\ru.:\rp.)."}, + }, + }, + { + r = { + "^/mixed", + }, + a = { + {"C", "X-PROXY-USER", "\ru."}, + {"C", "X-PROXY-PASSWORD", "\rp."}, + }, + }, + } + } + -- when + local resp = sites.handle_request(r, profile) + -- then + lu.assertNil(resp) + lu.assertEquals(ngx.req.header["Cookie"], "X-PROXY-USER=U; X-PROXY-PASSWORD=P") + lu.assertNil(ngx.req.header["Authorization"]) +end + +function test_authenticated_access_to_private_page_of_mixed_site_accepted() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/mixed/bob/wiki/_new" + local r = ng.get_request() + local profile = { + u = "U", + p = "P", + ok = { + { + r = { + "^/public", + }, + a = { + {"C", "X-PROXY-USER", "\ru."}, + {"C", "X-PROXY-PASS", "\rp."}, + }, + }, + { + r = { + "^/mixed/admin", + "^/mixed/.-/wiki/_new", + }, + a = { + {"C", "X-PROXY-USER", "\ru."}, + {"C", "X-PROXY-PASSWORD", "\rp."}, + {"H", "Authorization", "Basic \rb64(\ru.:\rp.)."}, + }, + }, + { + r = { + "^/mixed", + }, + a = { + {"C", "X-PROXY-USER", "\ru."}, + {"C", "X-PROXY-PASSWORD", "\rp."}, + }, + }, + } + } + -- when + local resp = sites.handle_request(r, profile) + -- then + lu.assertNil(resp) + lu.assertEquals(ngx.req.header["Cookie"], "X-PROXY-USER=U; X-PROXY-PASSWORD=P") + lu.assertEquals(ngx.req.header["Authorization"], "Basic VTpQ") +end + +function test_authenticated_access_to_private_site_accepted_with_the_right_user() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/private/page" + local r = ng.get_request() + local profile = { + u = "jean", + p = "P", + ok = { + { + r = { + "^/private", + }, + a = { + {"H", "Authorization", "Basic \rb64(\ru.:\rp.)."}, + }, + }, + } + } + -- when + local resp = sites.handle_request(r, profile) + -- then + lu.assertNil(resp) + lu.assertEquals(ngx.req.header["Authorization"], "Basic amVhbjpQ") +end + +function test_authenticated_access_to_private_site_redirected_403_with_the_wrong_user() + -- given + ngx.req.reset() + ngx.reset_var() + ngx.var.request_uri = "/private/page" + local r = ng.get_request() + local profile = { + u = "U", + p = "P", + ok = {}, + ko = { + "^/private", + } + } + -- when + local resp = sites.handle_request(r, profile) + -- then + lu.assertEquals(resp, "307:https://my-domain.tld/ssso/login?back=/private/page&cause=403") + lu.assertNil(ngx.req.header["Authorization"]) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/sites/mixed.json b/test/sites/mixed.json new file mode 100644 index 0000000..fc918dd --- /dev/null +++ b/test/sites/mixed.json @@ -0,0 +1,56 @@ +{ + "patterns": [ + { + "lua_regex": [ + "^/mixed/admin", + "^/mixed/.-/wiki/_new" + ], + "public": false, + "allow": [ + "*" + ], + "deny": [ + "guest" + ], + "actions": [ + { + "type": "cookie", + "name": "X-PROXY-USER", + "value": "\ru." + }, + { + "type": "cookie", + "name": "X-PROXY-PASSWORD", + "value": "\rp." + }, + { + "type": "header", + "name": "Authorization", + "value": "Basic \rb64(\ru.:\rp.)." + } + ] + }, + { + "lua_regex": [ + "^/mixed" + ], + "public": true, + "actions": [ + { + "type": "cookie", + "name": "X-PROXY-USER", + "value": "\ru." + }, + { + "type": "cookie", + "name": "X-PROXY-PASSWORD", + "value": "\rp." + } + ], + "portal": { + "/mixed/\ru./files": "My files", + "/mixed/\ru./webmail": "My e-mail" + } + } + ] +} diff --git a/test/sites/private.json b/test/sites/private.json new file mode 100644 index 0000000..97b3100 --- /dev/null +++ b/test/sites/private.json @@ -0,0 +1,25 @@ +{ + "patterns": [ + { + "lua_regex": [ + "^/private" + ], + "public": false, + "allow": [ + "jean" + ], + "deny": [ + ], + "actions": [ + { + "type": "header", + "name": "Authorization", + "value": "Basic \rb64(\ru.:\rp.)." + } + ], + "portal": { + "/private/directory": "Directory management" + } + } + ] +} diff --git a/test/sites/public.json b/test/sites/public.json new file mode 100644 index 0000000..ceade74 --- /dev/null +++ b/test/sites/public.json @@ -0,0 +1,31 @@ +{ + "patterns": [ + { + "lua_regex": [ + "^/public" + ], + "public": true, + "allow": [ + "*" + ], + "deny": [ + "banned" + ], + "actions": [ + { + "type": "cookie", + "name": "X-PROXY-USER", + "value": "\ru." + }, + { + "type": "cookie", + "name": "X-PROXY-PASS", + "value": "\rp." + } + ], + "portal": { + "/public/git/\ru.": "My projects" + } + } + ] +} diff --git a/test/util.utest.lua b/test/util.utest.lua new file mode 100644 index 0000000..d5d6da6 --- /dev/null +++ b/test/util.utest.lua @@ -0,0 +1,14 @@ +local lu = require("luaunit") +local util = require("ssso_util") + +function test_str_to_html_replaces_lt_gt_amp_quot_and_percent() + lu.assertEquals(util.str_to_html('-&<>"%\r\n=&<>"%+'), '-&<>"%\r\n=&<>"%+') +end + +function test_str_to_pattern_removes_lua_pattern_meanings() + lu.assertEquals( + util.str_to_pattern("_()%.+-*?^$%a%A%c%C%d%D%l%L%p%P%s%S%u%U%w%W%x%X%z%Z%0%1%2%3%4%5%6%7%8%9%b%fZ\r\n_()%.+-*?^$%a%A%c%C%d%D%l%L%p%P%s%S%u%U%w%W%x%X%z%Z%0%1%2%3%4%5%6%7%8%9%b%fZ"), + "_%(%)%%%.%+%-%*%?%^%$%%a%%A%%c%%C%%d%%D%%l%%L%%p%%P%%s%%S%%u%%U%%w%%W%%x%%X%%z%%Z%%0%%1%%2%%3%%4%%5%%6%%7%%8%%9%%b%%fZ\r\n_%(%)%%%.%+%-%*%?%^%$%%a%%A%%c%%C%%d%%D%%l%%L%%p%%P%%s%%S%%u%%U%%w%%W%%x%%X%%z%%Z%%0%%1%%2%%3%%4%%5%%6%%7%%8%%9%%b%%fZ") +end + +os.exit(lu.LuaUnit.run()) From 5f44ced0651f98f0a8c97bc3da4fbec6c968ec33 Mon Sep 17 00:00:00 2001 From: Yves G Date: Thu, 2 Sep 2021 22:58:01 +0200 Subject: [PATCH 2/5] HTTP basic auth --- doc/samples/login/login.html | 2 +- src/ssso_base64.lua | 7 +++- src/ssso_login.lua | 19 ++++++--- src/ssso_nginx.lua | 35 ++++++++++++++++- src/ssso_sessions.lua | 19 +++++++-- test/nginx.utest.lua | 76 ++++++++++++++++++++++++++++++++++++ test/sessions.utest.lua | 61 ++++++++++++++++++++++++++++- 7 files changed, 203 insertions(+), 16 deletions(-) diff --git a/doc/samples/login/login.html b/doc/samples/login/login.html index 151de65..780cb0f 100644 --- a/doc/samples/login/login.html +++ b/doc/samples/login/login.html @@ -6,7 +6,7 @@

    Single Sign-On for example.org

    - + diff --git a/src/ssso_base64.lua b/src/ssso_base64.lua index 6179da1..d13eade 100644 --- a/src/ssso_base64.lua +++ b/src/ssso_base64.lua @@ -10,9 +10,12 @@ end if not b64["encode_base64"] then b64.encode_base64 = function(plaintext) - local plain = b64.encode_base64url(plaintext) + local plain, err = b64.encode_base64url(plaintext) + if not plain then + return nil, err + end plain = plain:gsub("_", "/") - return plain:gsub("%-", "+") + return plain:gsub("%-", "+"), nil end end diff --git a/src/ssso_login.lua b/src/ssso_login.lua index e016810..1f4169d 100644 --- a/src/ssso_login.lua +++ b/src/ssso_login.lua @@ -47,15 +47,23 @@ local function inject_data(html, req_data, warnings) return html end -local function check_login(req_data) - req_data = nginx.with_post_parameters(req_data) - local user = req_data.query_params["login"] or "" - local password = req_data.query_params["password"] or "" +local function check_credentials_and_get_profile(user, password) local user_data = auth.read_user(user, password) if user_data then log.debug("Credentials accepted for user " .. user_data.name) local profile = prof.build_profile(user, password, user_data.name, user_data.email) - profile = sites.with_sites(profile, user) + return sites.with_sites(profile, user) + else + return nil + end +end + +local function check_login(req_data) + req_data = nginx.with_post_parameters(req_data) + local user = req_data.query_params["login"] or "" + local password = req_data.query_params["password"] or "" + local profile = check_credentials_and_get_profile(user, password) + if profile then log.debug("Building JWS") local jws, tslimit = crypto.get_jws_and_tslimit(profile) if not jws then @@ -100,5 +108,6 @@ end return { answer_request = answer_request, + check_credentials_and_get_profile = check_credentials_and_get_profile, set_root = set_root, } diff --git a/src/ssso_nginx.lua b/src/ssso_nginx.lua index 889c726..8be3dc1 100644 --- a/src/ssso_nginx.lua +++ b/src/ssso_nginx.lua @@ -1,4 +1,5 @@ local ngx = require("ngx") +local b64 = require("ssso_base64") local util = require("ssso_util") local conf = require("ssso_config") @@ -43,6 +44,36 @@ local function with_post_parameters(req_data) return req_data end +local function str_starts_with(str, begin) + local i, _ = str:find(util.str_to_pattern(begin)) + return 1 == i +end + +local function get_basic_auth() + local header = ngx.var.Authentication + if (not header) or (#header < 7) or (not str_starts_with(header, "Basic ")) then + return nil, nil + end + local text, _ = b64.decode_base64(header:sub(7)) + if not text then + ngx.log(ngx.DEBUG, "Invalid Authentication header: " .. header) + return nil, nil + end + local colon + colon, _ = text:find(":") + if not colon then + return nil, nil + end + local login, password = "", "" + if colon > 1 then + login = text:sub(1, colon - 1) + end + if colon < #text then + password = text:sub(colon + 1, #text) + end + return login, password +end + local function get_jws_cookie() return ngx.var.cookie_SSSO_TOKEN end @@ -85,8 +116,7 @@ local function matches(req_data, lua_pattern) end local function starts_with(req_data, prefix) - local i, _ = req_data.target:find(util.str_to_pattern(prefix)) - return 1 == i + return str_starts_with(req_data.target, prefix) end local function has_param(req_data, param, value) @@ -138,6 +168,7 @@ return { add_header = add_header, answer_not_found = answer_not_found, forward_request = forward_request, + get_basic_auth = get_basic_auth, get_jws_cookie = get_jws_cookie, get_request = get_request, get_seconds_since_epoch = get_seconds_since_epoch, diff --git a/src/ssso_sessions.lua b/src/ssso_sessions.lua index b681943..c910930 100644 --- a/src/ssso_sessions.lua +++ b/src/ssso_sessions.lua @@ -1,14 +1,25 @@ local crypto = require("ssso_crypto") +local login = require("ssso_login") local nginx = require("ssso_nginx") local function get_session() - local cookie = nginx.get_jws_cookie() + local session, jws, tslimit + local user, password = nginx.get_basic_auth() - if not cookie or cookie == "" then - return nil, 401 + if user and password then + session = login.check_credentials_and_get_profile(user, password) + if session then + jws, tslimit = crypto.get_jws_and_tslimit(session) + end end - local session, jws, tslimit = crypto.get_data_and_new_jws(cookie) + if not session then + local cookie = nginx.get_jws_cookie() + if not cookie or cookie == "" then + return nil, 401 + end + session, jws, tslimit = crypto.get_data_and_new_jws(cookie) + end if session then nginx.set_jws_cookie(jws, tslimit) diff --git a/test/nginx.utest.lua b/test/nginx.utest.lua index fbbe1d5..26d7ce2 100644 --- a/test/nginx.utest.lua +++ b/test/nginx.utest.lua @@ -310,4 +310,80 @@ function test_with_post_parameters_merges_post_parameters_to_request_data() }) end +function test_get_basic_auth_with_no_header_returns_nil() + -- given + ngx.reset_header() + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertNil(u) + lu.assertNil(p) +end + +function test_get_basic_auth_with_non_basic_header_returns_nil() + -- given + ngx.reset_header() + ngx.var.Authentication = "Bearer uuid" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertNil(u) + lu.assertNil(p) +end + +function test_get_basic_auth_with_basic_header_but_no_base64_returns_nil() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic " + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertNil(u) + lu.assertNil(p) +end + +function test_get_basic_auth_with_basic_header_but_invalid_base64_returns_nil() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic !!!!" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertNil(u) + lu.assertNil(p) +end + +function test_get_basic_auth_with_valid_basic_header_returns_auth() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic dTpw" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertEquals(u, "u") + lu.assertEquals(p, "p") +end + +function test_get_basic_auth_works_with_an_empty_login() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic OnA=" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertEquals(u, "") + lu.assertEquals(p, "p") +end + +function test_get_basic_auth_works_with_an_empty_password() + -- given + ngx.reset_header() + ngx.var.Authentication = "Basic dTo=" + -- when + local u, p = ng.get_basic_auth() + -- then + lu.assertEquals(u, "u") + lu.assertEquals(p, "") +end + os.exit(lu.LuaUnit.run()) diff --git a/test/sessions.utest.lua b/test/sessions.utest.lua index 28d1f02..3e34dc9 100644 --- a/test/sessions.utest.lua +++ b/test/sessions.utest.lua @@ -1,11 +1,15 @@ local lu = require("luaunit") -local sess = require("ssso_sessions") +local ngx = require("ngx") +local b64 = require("ssso_base64") local conf = require("ssso_config") local crypt = require("ssso_crypto") -local ngx = require("ngx") +local login = require("ssso_login") +local sess = require("ssso_sessions") +local sites = require("ssso_sites") local here = debug.getinfo(1).source:sub(2, -20) conf.load_conf(here) +sites.load_sites(here) function test_no_session_and_hint_401_if_no_cookie() -- given @@ -45,6 +49,7 @@ end function test_session_and_cookie_renewal_if_good_cookie() -- given ngx.req.reset() + ngx.reset_header() ngx.reset_var() local data = {u = "bob"} local c, _ = crypt.get_jws_and_tslimit(data) @@ -58,4 +63,56 @@ function test_session_and_cookie_renewal_if_good_cookie() lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") end +function test_good_basic_auth_credentials_generate_a_session_and_a_cookie() + -- given + ngx.req.reset() + ngx.reset_header() + ngx.reset_var() + ngx.var.Authentication = "Basic " .. b64.encode_base64("bob:goodpassword") + local expected = login.check_credentials_and_get_profile("bob", "goodpassword") + -- when + local s, h = sess.get_session() + -- then + lu.assertEquals(h, 200) + lu.assertEquals(s, expected) + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +function test_basic_auth_takes_precedence_over_cookie() + -- given + ngx.req.reset() + ngx.reset_header() + ngx.reset_var() + local data = {u = "forget me"} + local c, _ = crypt.get_jws_and_tslimit(data) + ngx.var.cookie_SSSO_TOKEN = c + ngx.var.Authentication = "Basic " .. b64.encode_base64("bob:goodpassword") + -- when + local s, h = sess.get_session() + -- then + lu.assertEquals(h, 200) + lu.assertEquals(s.u, "bob") + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + +function test_basic_auth_ignored_if_invalid() + -- given + ngx.req.reset() + ngx.reset_header() + ngx.reset_var() + local data = {u = "do not forget me"} + local c, _ = crypt.get_jws_and_tslimit(data) + ngx.var.cookie_SSSO_TOKEN = c + ngx.var.Authentication = "Basic !!!!" + -- when + local s, h = sess.get_session() + -- then + lu.assertEquals(h, 200) + lu.assertEquals(s.u, "do not forget me") + lu.assertNil(ngx.header["Set-Cookie"].link) + lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") +end + os.exit(lu.LuaUnit.run()) From 20fc2dbf8b439c1ce7d61c4f434b4827c0bf179c Mon Sep 17 00:00:00 2001 From: Yves G Date: Sun, 5 Sep 2021 17:44:46 +0200 Subject: [PATCH 3/5] documentation --- .editorconfig | 3 + README.md | 217 +++++++++++++++++++++++++++++++++++++++++++++ src/ssso_login.lua | 2 +- src/ssso_sites.lua | 6 +- 4 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 README.md diff --git a/.editorconfig b/.editorconfig index b860e67..ad1bd2a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,6 @@ insert_final_newline = true [Makefile] indent_style = tab tab_width = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3d70fa --- /dev/null +++ b/README.md @@ -0,0 +1,217 @@ +# Simple-SSO + +This software is similar to [SSOwat](https://github.com/YunoHost/SSOwat) in purpose. It aims to implement a light-weight Single Sign-On layer between client HTTP software and HTTP-enabled applications hosted on a single [OpenResty](https://github.com/openresty/lua-nginx-module) server. +The main target is self-hosting. + +> ℹ️ [OpenResty](https://openresty.org/) is an open-source package which bundles [Nginx](http://nginx.org/) web server [with LuaJIT](https://www.nginx.com/resources/wiki/modules/lua/) and some chosen libraries. + +## Goals / features — status + +> ℹ️ Each feature is backed by [tests](./test/). + +Goal / feature (functional) | status +--------------------------- | ------ +A new site can be handled by dropping a site-specific configuration file in a dedicated folder | Done +A site can be broken-down into sub-parts by a series of Lua regular expressions to match them | Done +Each part of a site has its own access rules and authentication mode | Done +A site part can be public: unauthenticated visitors can access it | Done +A site part can be private: only authenticated users can access it | Done +A site part can accept all authenticated users or grant access to a chosen list | Done +A site part, even a public one, can be forbidden to some authenticated users | Done +A rejected access is redirected to the login page | Done +The login page can be customized | Done +On successful login, the user is sent back to the page that was aimed at before authentication | Done +A portal page is available, where each site part can add its own links | Done +The portal page can be customized | Done +HTTP Basic Authentication can be used instead of interactive login | Done +Each site can configure its logout method to reach a “Single Sign-Off” | To do +Users can change their password in the identity directory | To do + +Goal / feature (technical) | status +-------------------------- | ------ +A [JWS](https://www.rfc-editor.org/rfc/rfc7515.html) is used for authentication | Done +The [JWT](https://www.rfc-editor.org/rfc/rfc7519) is short-lived, and automatically renewed on each access | Done +The JWT is self-sufficient to allow or deny access to a site | Done +For better performance, the sites’ data is only read after a successful login (to build the first JWT), or to display the portal | Done +The JWS is stored in a cookie with no impact on the integrated applications | Done +The JWS can be stored in its regular header | To do +Credentials and authorization data in the JWT are encrypted with 256-bit AES | Done +Implementation of the OAuth2 and OpenID-Connect protocols | To do +Implementation of the OpenID-Connect Discovery protocol | To do +LDAP is usable as an authentication back-end | Done +Any authentication back-end can be configured through shell commands | Done + +## Installation / usage + +A directory must be created (for example `/etc/nginx/simplesso/`) with this structure: + +``` +─┬─ login/ + │ ├─ login.html + │ ├─ login.css + │ └─ login.js + ├─ portal/ + │ ├─ portal.html + │ ├─ portal.css + │ └─ portal.js + ├─ sites/ + │ ├─ SITE1.json + │ ├─ SITE2.json + │ └─ … + ├─ global.json + └─ *.lua +``` + +The `*.lua` files are all files in the [src/](./src/) source-code directory. Among these are ① `do_init.lua` and ② `do_access.lua`; these are the files that Nginx calls ① when starting, and then ② for each request, to check the access permissions. +To this end, Nginx must be configured thus (assuming the directory created above is `/etc/nginx/simplesso/`): + +* In the `http {…}` section of `nginx.conf`, add this line: +`init_by_lua_file /etc/nginx/simplesso/do_init.lua;` +* In the `server {…}` section of `nginx.conf`, add this line: +`access_by_lua_file /etc/nginx/simplesso/do_access.lua;` + +### The main configuration file: `global.json` + +The `global.json` file has this [JSON](https://www.json.org/) structure: + +```json +{ + "auth": { + "check": "AN AUTHENTICATING SHELL COMMAND" + }, + "session_seconds": 300, + "sso_host": "example.org", + "sso_prefix": "/simple-sso" +} +``` + +The `sso_host` and `sso_prefix` are simply the public path to your SSO, hosted on your OpenResty server. In the above example: + +* the login page would be at `https://example.org/simple-sso/login`; +* the portal page would be at `https://example.org/simple-sso/portal`. + +The parameter `session_seconds` is the number of seconds (here: 5 minutes) after which a session expires in the absence of any request to the server; if there is a request, the session token is renewed, and the counter is reset. + +Finally, the `auth` array contains the shell commands that communicate with the authentication backend. +Each command can make full use of the system shell features (loops, pipes, parameter expansion…). +Here are the needed commands: + +* `check` is the command that checks the validity of given credentials. If 2 non-empty lines can be read in the standard output of this command, then the credentials are validated, else they are rejected. + * In the command, “`\ru.`” is replaced by the user identifier that was given, and “`\rp.`” is replaced by the password that was given. Both are properly secured for the shell: any newline character is removed, and each occurrence of “`"`” is escaped to become “`\"`”; thus “`\ru.`” and “`\rp.`” are expected to be used in double-quoted parameters in the chosen command. + * In the output of the command, the first line is used as the user _name_, and the second line is used as the user _email_. + +> ⚠️ Remember that the commands are written as _strings_ in a JSON file, and thus the [JSON syntax](https://www.json.org/) applies. In particular, literal characters `"` and `\` (backslash) would respectively appear as `\"` and `\\`. + +An [example `global.json` file](./doc/samples/global.json) is provided, with a command that can authenticate a user using an LDAP directory. + +### The login page + +Although [sample login files](./doc/samples/login/) are provided, each can be freely replaced, knowing these facts: + +* All three files must be encoded in UTF-8. +* In `login.html`: + * only _one_ CSS file is allowed, and this file is `login.css`; + * only _one_ Javascript file is allowed, and this file is `login.js`; + * the placeholder “``” gets replaced at runtime with messages (if there are any), in this format: + ```html +
    +

    An information message.

    +

    A warning message.

    + … +
    + ``` + * the login action must be “`login`” with method `POST`; + * the `form` parameters for the login and password must be named “`login`” and “`password`”; + * for the redirection to work, this hidden input must be provided: + `` + or the user will always get redirected to the portal after login. + +### The portal page + +Although [sample portal files](./doc/samples/portal/) are provided, each can be freely replaced, knowing these facts: + +* All three files must be encoded in UTF-8. +* In `portal.html`: + * only _one_ CSS file is allowed, and this file is `portal.css`; + * only _one_ Javascript file is allowed, and this file is `portal.js`; + * the placeholders “`SSSO_USER`”, “`SSSO_NAME`”, and “`SSSO_EMAIL`” get respectively replaced at runtime by the logged-in user’s login identifier, name, and e-mail address, HTML-encoded; + * for this file to have any usefulness, it must contain this HTML fragment: + `` + which gets: + * _filled_ at runtime with a list of links if there are links authorized for the logged-in user: + ```html + + ``` + * or _replaced_ at runtime with a paragraph if no links are authorized for the logged-in user: + ```html +

    A substitute message.

    + ``` + +### The sites’ description + +Each site is described as a [JSON object](https://www.json.org/) having one field named “`patterns`”, which is a list: + +```json +{"patterns": [ … ]} +``` + +This list contains one JSON object per part of the site that needs to be described separately. +A part of a site contains at least the field `lua_regex`, that lists the possible [patterns](https://www.lua.org/manual/5.3/manual.html#6.4.1) that an URL must match to be considered as being in this part of the site: + +```json +{"lua_regex": [ … ]} +``` + +In addition, the part of the site may specify: + +* `public` (a boolean, `false` by default): whether unauthenticated users may visit this part of the site; +* `allow` (a list, empty by default, i.e. nobody): what authenticated users are allowed to visit this part of the site (“`*`” means everyone); +* `deny` (a list, empty by default, i.e. nobody): what authenticated users are denied access to this part of the site; +* `actions` (a list of objects, empty by default): how the single sign-on should be forwarded to the site; each object in this list has three fields: + * `type` is either “`cookie`” (send a cookie) or “`header`” (send a header), + * `name` is the name of the cookie or header, + * `value` is the value to put in this cookie or header, where the following substitutions are done: + * “`\ru.`” gets replaced by the logged-in user identifier, + * “`\rp.`” gets replaced by the user’s password, + * “`\rn.`” gets replaced by the user’s name, + * “`\re.`” gets replaced by the user’s e-mail address, + * “`\rb64(…).`” gets replaced by the [Base64 encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-4) of what is inside the parentheses, + * “`\ru64(…).`” gets replaced by the [Base64 encoding with the URL variant](https://datatracker.ietf.org/doc/html/rfc4648#section-5) of what is inside the parentheses; +* `portal` (an object, empty by default): what links shall be added to the portal page; for each of this object: + * the key (i.e. field name) is the URL of the link, relative to the root of the server, + * the value is the label to show for this link. + +To summarise, here is a minimal, valid but useless, example: + +```json +{"patterns": [{"lua_regex": ["/private"]}]} +``` + +And here is a fuller example: + +```json +{"patterns": [ + { "lua_regex": ["/site/assets", "/site/wiki"], "public": true, "allow": ["*"], + "actions": [ + {"type": "header", "name": "X-WIKI-USER", "value": "\rn."}, + {"type": "header", "name": "X-WIKI-MAIL", "value": "\re."} + ], + "portal": { + "/site/wiki/overview.html": "Wiki pages" + } + }, + {"lua_regex": ["/site/admin"], "allow": ["*"], "deny": ["former-administrator"], + "actions": [ + {"type": "header", "name": "Authorization", "value": "Basic \rb64(\ru.:\rp.)."} + ], + "portal": { + "/site/admin/plugins.php": "Wiki plugins", + "/site/admin/statistics.php": "Wiki visitors statistics" + } + } +]} +``` diff --git a/src/ssso_login.lua b/src/ssso_login.lua index 1f4169d..3b529f3 100644 --- a/src/ssso_login.lua +++ b/src/ssso_login.lua @@ -108,6 +108,6 @@ end return { answer_request = answer_request, - check_credentials_and_get_profile = check_credentials_and_get_profile, + check_credentials_and_get_profile = check_credentials_and_get_profile, -- TODO: test set_root = set_root, } diff --git a/src/ssso_sites.lua b/src/ssso_sites.lua index c9a8091..bc64c7f 100644 --- a/src/ssso_sites.lua +++ b/src/ssso_sites.lua @@ -92,12 +92,12 @@ local function with_sites(profile, user) if f then site = json.decode(f:read("*all")) f:close() - for _, pat in ipairs(site.patterns or {}) do + for _, pat in ipairs(site.patterns) do go_on = true for _, denied in ipairs(pat.deny or {}) do if denied == user then go_on = false - for _, re in ipairs(pat.lua_regex or {}) do + for _, re in ipairs(pat.lua_regex) do table.insert(ko_list, re) end break @@ -175,7 +175,7 @@ local function authorized_links(user) if f then site = json.decode(f:read("*all")) f:close() - for _, pat in ipairs(site.patterns or {}) do + for _, pat in ipairs(site.patterns) do go_on = true for _, denied in ipairs(pat.deny or {}) do if denied == user then From 489350fe210b33d2e5979ec0bb629bd88679599f Mon Sep 17 00:00:00 2001 From: Yves G Date: Sat, 2 Oct 2021 23:45:31 +0200 Subject: [PATCH 4/5] refactor: use OOP where appropriate --- Makefile | 2 +- src/do_access.lua | 14 ++--- src/ssso_config.lua | 2 +- src/ssso_crypto.lua | 30 +++++------ src/ssso_identity.lua | 60 ++++++++++++++++++++++ src/ssso_login.lua | 14 +++-- src/ssso_nginx.lua | 66 ++++++++++++------------ src/ssso_portal.lua | 17 +++---- src/ssso_profile.lua | 63 ----------------------- src/ssso_sessions.lua | 16 +++--- src/ssso_sites.lua | 110 +++++++++++++++++++++++++--------------- test/crypto.utest.lua | 24 ++++----- test/identity.utest.lua | 61 ++++++++++++++++++++++ test/login.utest.lua | 18 +++---- test/nginx.utest.lua | 68 ++++++++++++------------- test/portal1.ctest.lua | 9 ++-- test/portal2.ctest.lua | 4 +- test/portal3.ctest.lua | 4 +- test/portal4.ctest.lua | 4 +- test/portal5.ctest.lua | 1 - test/profile.utest.lua | 81 ----------------------------- test/sessions.utest.lua | 18 +++---- test/sites.utest.lua | 93 +++++++++++++++------------------ 23 files changed, 382 insertions(+), 397 deletions(-) create mode 100644 src/ssso_identity.lua delete mode 100644 src/ssso_profile.lua create mode 100644 test/identity.utest.lua delete mode 100644 test/profile.utest.lua diff --git a/Makefile b/Makefile index c24bd96..a5c1bc7 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ test: test-env ${run_test_file} ${ROOT_DIR}/test/config.utest.lua ${run_test_file} ${ROOT_DIR}/test/auth.utest.lua ${run_test_file} ${ROOT_DIR}/test/nginx.utest.lua - ${run_test_file} ${ROOT_DIR}/test/profile.utest.lua + ${run_test_file} ${ROOT_DIR}/test/identity.utest.lua ${run_test_file} ${ROOT_DIR}/test/sites.utest.lua ${run_test_file} ${ROOT_DIR}/test/crypto.utest.lua ${run_test_file} ${ROOT_DIR}/test/login.utest.lua diff --git a/src/do_access.lua b/src/do_access.lua index d216c7d..7ca51cf 100644 --- a/src/do_access.lua +++ b/src/do_access.lua @@ -9,11 +9,11 @@ local nginx = require("ssso_nginx") -local req_data = nginx.get_request() +local req_data = nginx.class__request:current() -if nginx.is(req_data, "/.well-known/webfinger") - and nginx.has_param(req_data, "rel", "http://openid.net/specs/connect/1.0/issuer") - and nginx.has_param(req_data, "resource") +if req_data:is("/.well-known/webfinger") + and req_data:has_param("rel", "http://openid.net/specs/connect/1.0/issuer") + and req_data:has_param("resource") then -- https://openid.net/specs/openid-connect-discovery-1_0.html local oauth2 = require("ssso_oauth2") @@ -27,14 +27,14 @@ local sites = require("ssso_sites") local sso_prefix = conf.get_sso_prefix() local auth, status = sess.get_session() -if nginx.starts_with(req_data, sso_prefix) then +if req_data:starts_with(sso_prefix) then -- SSO-specific URL - if nginx.starts_with(req_data, sso_prefix .. "/login") then + if req_data:starts_with(sso_prefix .. "/login") then local login = require("ssso_login") return login.answer_request(req_data) - elseif nginx.starts_with(req_data, sso_prefix .. "/oauth2") then + elseif req_data:starts_with(sso_prefix .. "/oauth2") then local oauth2 = require("ssso_oauth2") return oauth2.answer_request(req_data, auth) elseif auth then diff --git a/src/ssso_config.lua b/src/ssso_config.lua index 0a98178..d7733ef 100644 --- a/src/ssso_config.lua +++ b/src/ssso_config.lua @@ -7,7 +7,7 @@ local function load_conf(prefix) local file = assert(io.open(prefix .. name_suffix, "r"), "File " .. prefix .. name_suffix .. " not found") conf = json.decode(file:read("*all")) file:close() - assert(conf["auth"], "Simple-SSO configuration is missing a `auth` entry") + assert(conf["auth"], "Simple-SSO configuration is missing an `auth` entry") assert(conf["sso_host"], "Simple-SSO configuration is missing a `sso_host` entry") assert(conf["sso_prefix"], "Simple-SSO configuration is missing a `sso_prefix` entry") assert(conf["session_seconds"], "Simple-SSO configuration is missing a `session_seconds` entry") diff --git a/src/ssso_crypto.lua b/src/ssso_crypto.lua index 1ae97aa..7a451d5 100644 --- a/src/ssso_crypto.lua +++ b/src/ssso_crypto.lua @@ -7,7 +7,6 @@ local b64 = require("ssso_base64") local config = require("ssso_config") local log = require("ssso_log") local nginx = require("ssso_nginx") -local prof = require("ssso_profile") local sites = require("ssso_sites") local KEY_SIZE = 32 -- 256 bits for AES-256-GCM’s key and SHA-256 @@ -88,12 +87,12 @@ end -- https://www.rfc-editor.org/rfc/rfc7519 -- https://openid.net/specs/openid-connect-core-1_0.html -local function get_jws_and_tslimit(data) - local user = prof.user(data) - local ser_data = prof.serialize(data) .. sites.serialize(data) - log.debug("Creating JWS with data: " .. ser_data:gsub("([\031\030\029\028])", function(s) return "[" .. s:byte() .. "]" end)) - local crypted_data = encrypt(ser_data) - if not user or not crypted_data then +local function get_jws_and_tslimit(profile) + local user = profile:user() + local ser_profile = profile:serialize() + log.debug("Creating JWS with profile: " .. ser_profile:gsub("([\031\030\029\028\027\026])", function(s) return "[" .. s:byte() .. "]" end)) + local crypted_profile = encrypt(ser_profile) + if not user or not crypted_profile then return nil, nil end local iat = nginx.get_seconds_since_epoch() @@ -104,30 +103,29 @@ local function get_jws_and_tslimit(data) aud = user, exp = exp, iat = iat, - x_ssso = b64.encode_base64url(crypted_data), + x_ssso = b64.encode_base64url(crypted_profile), } return to_jws(jwt), exp end -local function get_data_and_new_jws(jws) +local function get_profile_and_new_jws(jws) local jwt = to_jwt(jws) local iat = nginx.get_seconds_since_epoch() if jwt == nil or not jwt["x_ssso"] or not jwt["exp"] or jwt.exp < iat then return nil, nil, nil end - local ser_data = decrypt(b64.decode_base64url(jwt.x_ssso)) - if not ser_data then + local ser_profile = decrypt(b64.decode_base64url(jwt.x_ssso)) + if not ser_profile then return nil, nil, nil end - log.debug("Read data from JWS: " .. ser_data:gsub("([\031\030\029\028])", function(s) return "[" .. s:byte() .. "]" end)) - local data, remainder, _ = prof.deserialize(ser_data) - data, _ = sites.deserialize_update(remainder, data) + log.debug("Read profile from JWS: " .. ser_profile:gsub("([\031\030\029\028])", function(s) return "[" .. s:byte() .. "]" end)) + local profile = sites.class__profile:deserialize(ser_profile) jwt.iat = iat jwt.exp = iat + config.get_session_seconds() - return data, to_jws(jwt), jwt.exp + return profile, to_jws(jwt), jwt.exp end return { get_jws_and_tslimit = get_jws_and_tslimit, - get_data_and_new_jws = get_data_and_new_jws, + get_profile_and_new_jws = get_profile_and_new_jws, } diff --git a/src/ssso_identity.lua b/src/ssso_identity.lua new file mode 100644 index 0000000..3d155d8 --- /dev/null +++ b/src/ssso_identity.lua @@ -0,0 +1,60 @@ +local b64 = require("ssso_base64") + +local class__identity = {} + +function class__identity:build(user, password, name, email) + local identity = { + u = user, + p = password, + n = name, + e = email, + } + setmetatable(identity, {__index = self}) + return identity +end + +function class__identity:serialize() -- TODO: test + return (self.u or "\025") + .. "\031" .. (self.p or "\025") + .. "\031" .. (self.n or "\025") + .. "\031" .. (self.e or "\025") +end + +function class__identity:deserialize(ser) -- TODO: test + local identity + ser:gsub("^(.-)\031(.-)\031(.-)\031(.*)", function (u, p, n, e) + if u == "\025" then u = nil end + if p == "\025" then p = nil end + if n == "\025" then n = nil end + if e == "\025" then e = nil end + identity = self:build(u, p, n, e) + end) + return identity +end + +function class__identity:format(template) + local s = template + s = s:gsub("\ru%.", self.u or "") + s = s:gsub("\rp%.", self.p or "") + s = s:gsub("\rn%.", self.n or "") + s = s:gsub("\re%.", self.e or "") + s = s:gsub("\rb64%(([^\r]-)%)%.", function(x) return b64.encode_base64(x) end) + s = s:gsub("\ru64%(([^\r]-)%)%.", function(x) return b64.encode_base64url(x) end) + return s +end + +function class__identity:email() + return self.e +end + +function class__identity:name() + return self.n +end + +function class__identity:user() + return self.u +end + +return { + class__identity = class__identity, +} diff --git a/src/ssso_login.lua b/src/ssso_login.lua index 3b529f3..17536c0 100644 --- a/src/ssso_login.lua +++ b/src/ssso_login.lua @@ -3,7 +3,6 @@ local conf = require("ssso_config") local crypto = require("ssso_crypto") local log = require("ssso_log") local nginx = require("ssso_nginx") -local prof = require("ssso_profile") local sites = require("ssso_sites") local util = require("ssso_util") @@ -51,15 +50,14 @@ local function check_credentials_and_get_profile(user, password) local user_data = auth.read_user(user, password) if user_data then log.debug("Credentials accepted for user " .. user_data.name) - local profile = prof.build_profile(user, password, user_data.name, user_data.email) - return sites.with_sites(profile, user) + return sites.class__profile:build_from_conf(user, password, user_data.name, user_data.email) else return nil end end local function check_login(req_data) - req_data = nginx.with_post_parameters(req_data) + req_data = req_data:with_post_parameters() local user = req_data.query_params["login"] or "" local password = req_data.query_params["password"] or "" local profile = check_credentials_and_get_profile(user, password) @@ -88,17 +86,17 @@ end local function answer_request(req_data) local login = conf.get_sso_prefix() .. "/login" - if nginx.is(req_data, login) then - if nginx.has_method(req_data, "POST") then + if req_data:is(login) then + if req_data:has_method("POST") then log.info("Checking login") return check_login(req_data) else local html = inject_data(contents("login.html"), req_data, nil) return nginx.return_contents(html, "text/html; charset=UTF-8") end - elseif nginx.is(req_data, login .. ".css") then + elseif req_data:is(login .. ".css") then return nginx.return_contents(contents("login.css"), "text/css; charset=UTF-8") - elseif nginx.is(req_data, login .. ".js") then + elseif req_data:is(login .. ".js") then return nginx.return_contents(contents("login.js"), "application/javascript; charset=UTF-8") else log.info("Unknown login file") diff --git a/src/ssso_nginx.lua b/src/ssso_nginx.lua index 8be3dc1..03ef55c 100644 --- a/src/ssso_nginx.lua +++ b/src/ssso_nginx.lua @@ -3,7 +3,14 @@ local b64 = require("ssso_base64") local util = require("ssso_util") local conf = require("ssso_config") -local function get_request() +local class__request = {} + +local function str_starts_with(str, begin) + local i, _ = str:find(util.str_to_pattern(begin)) + return 1 == i +end + +function class__request:current() local vars = ngx.var local request = { referer = vars.http_referer, @@ -11,6 +18,8 @@ local function get_request() method = vars.request_method, uri = vars.request_uri, } + setmetatable(request, {__index = self}) + if request.referer == "" then request.referer = nil end @@ -33,20 +42,35 @@ local function get_request() return request end -local function with_post_parameters(req_data) +function class__request:with_post_parameters() ngx.req.read_body() local args, _ = ngx.req.get_post_args() if args then for key, val in pairs(args) do - req_data.query_params[key] = val + self.query_params[key] = val end end - return req_data + return self end -local function str_starts_with(str, begin) - local i, _ = str:find(util.str_to_pattern(begin)) - return 1 == i +function class__request:has_method(method) + return string.upper(method) == string.upper(self.method) +end + +function class__request:is(url) + return self.target == url +end + +function class__request:matches(lua_pattern) + return self.target:match(lua_pattern) +end + +function class__request:starts_with(prefix) + return str_starts_with(self.target, prefix) +end + +function class__request:has_param(param, value) + return self.query_params[param] ~= nil and (value == nil or self.query_params[param] == value) end local function get_basic_auth() @@ -103,26 +127,6 @@ local function add_header(name, value) ngx.req.set_header(name, value) end -local function has_method(req_data, method) - return string.upper(method) == string.upper(req_data.method) -end - -local function is(req_data, url) - return req_data.target == url -end - -local function matches(req_data, lua_pattern) - return req_data.target:match(lua_pattern) -end - -local function starts_with(req_data, prefix) - return str_starts_with(req_data.target, prefix) -end - -local function has_param(req_data, param, value) - return req_data.query_params[param] ~= nil and (value == nil or req_data.query_params[param] == value) -end - local function answer_not_found(req_data) return ngx.exit(404) end @@ -167,21 +171,15 @@ return { add_cookie = add_cookie, add_header = add_header, answer_not_found = answer_not_found, + class__request = class__request, forward_request = forward_request, get_basic_auth = get_basic_auth, get_jws_cookie = get_jws_cookie, - get_request = get_request, get_seconds_since_epoch = get_seconds_since_epoch, - has_method = has_method, - has_param = has_param, - is = is, - matches = matches, redirect_to_login = redirect_to_login, redirect_to_page = redirect_to_page, redirect_to_portal = redirect_to_portal, return_contents = return_contents, set_jws_cookie = set_jws_cookie, - starts_with = starts_with, answer_unexpected_error = answer_unexpected_error, - with_post_parameters = with_post_parameters, } diff --git a/src/ssso_portal.lua b/src/ssso_portal.lua index 5c6a9d3..1d8e743 100644 --- a/src/ssso_portal.lua +++ b/src/ssso_portal.lua @@ -1,7 +1,6 @@ local util = require("ssso_util") local conf = require("ssso_config") local nginx = require("ssso_nginx") -local prof = require("ssso_profile") local sites = require("ssso_sites") local root = "" @@ -18,14 +17,14 @@ local function contents(relative) end local function inject_data(html, profile) - html = html:gsub("SSSO_USER", util.str_to_html(prof.user(profile))) - html = html:gsub("SSSO_NAME", util.str_to_html(prof.name(profile))) - html = html:gsub("SSSO_EMAIL", util.str_to_html(prof.email(profile))) + html = html:gsub("SSSO_USER", util.str_to_html(profile:user())) + html = html:gsub("SSSO_NAME", util.str_to_html(profile:name())) + html = html:gsub("SSSO_EMAIL", util.str_to_html(profile:email())) local links = "" - local allowed = sites.authorized_links(prof.user(profile)) + local allowed = profile:authorized_links() table.sort(allowed, function(a1, a2) return a1.label < a2.label end) for _, allow in ipairs(allowed) do - links = links .. '
  • ' .. util.str_to_html(prof.format(allow.label, profile)) .. "
  • " + links = links .. '
  • ' .. util.str_to_html(profile:format(allow.label)) .. "
  • " end if links ~= "" then html = html:gsub('', '") @@ -37,12 +36,12 @@ end local function answer_request(req_data, profile) local portal = conf.get_sso_prefix() .. "/portal" - if nginx.is(req_data, portal) then + if req_data:is(portal) then local html = inject_data(contents("portal.html"), profile) return nginx.return_contents(html, "text/html; charset=UTF-8") - elseif nginx.is(req_data, portal .. ".css") then + elseif req_data:is(portal .. ".css") then return nginx.return_contents(contents("portal.css"), "text/css; charset=UTF-8") - elseif nginx.is(req_data, portal .. ".js") then + elseif req_data:is(portal .. ".js") then return nginx.return_contents(contents("portal.js"), "application/javascript; charset=UTF-8") else return nginx.answer_not_found(req_data) diff --git a/src/ssso_profile.lua b/src/ssso_profile.lua deleted file mode 100644 index e1c5ed1..0000000 --- a/src/ssso_profile.lua +++ /dev/null @@ -1,63 +0,0 @@ -local b64 = require("ssso_base64") - -local function build_profile(user, password, name, email) - return { - u = user, - p = password, - n = name, - e = email, - } -end - -local function serialize(profile) - return (profile.u or "\025") .. "\031" .. - (profile.p or "\025") .. "\031" .. - (profile.n or "\025") .. "\031" .. - (profile.e or "\025") .. "\031" -end - -local function deserialize(ser) - local profile - local remainder = ser:gsub("^(.-)\031(.-)\031(.-)\031(.-)\031", function (u, p, n, e) - if u == "\025" then u = nil end - if p == "\025" then p = nil end - if n == "\025" then n = nil end - if e == "\025" then e = nil end - profile = build_profile(u, p, n, e) - return "" - end) - return profile, remainder -end - -local function format(template, profile) - local s = template - s = s:gsub("\ru%.", profile.u or "") - s = s:gsub("\rp%.", profile.p or "") - s = s:gsub("\rn%.", profile.n or "") - s = s:gsub("\re%.", profile.e or "") - s = s:gsub("\rb64%(([^\r]-)%)%.", function(x) return b64.encode_base64(x) end) - s = s:gsub("\ru64%(([^\r]-)%)%.", function(x) return b64.encode_base64url(x) end) - return s -end - -local function email(profile) - return profile.e -end - -local function name(profile) - return profile.n -end - -local function user(profile) - return profile.u -end - -return { - build_profile = build_profile, - deserialize = deserialize, -- TODO: test - serialize = serialize, -- TODO: test - email = email, - format = format, - name = name, - user = user, -} diff --git a/src/ssso_sessions.lua b/src/ssso_sessions.lua index c910930..a34a3e3 100644 --- a/src/ssso_sessions.lua +++ b/src/ssso_sessions.lua @@ -3,27 +3,27 @@ local login = require("ssso_login") local nginx = require("ssso_nginx") local function get_session() - local session, jws, tslimit + local profile, jws, tslimit local user, password = nginx.get_basic_auth() if user and password then - session = login.check_credentials_and_get_profile(user, password) - if session then - jws, tslimit = crypto.get_jws_and_tslimit(session) + profile = login.check_credentials_and_get_profile(user, password) + if profile then + jws, tslimit = crypto.get_jws_and_tslimit(profile) end end - if not session then + if not profile then local cookie = nginx.get_jws_cookie() if not cookie or cookie == "" then return nil, 401 end - session, jws, tslimit = crypto.get_data_and_new_jws(cookie) + profile, jws, tslimit = crypto.get_profile_and_new_jws(cookie) end - if session then + if profile then nginx.set_jws_cookie(jws, tslimit) - return session, 200 + return profile, 200 else return nil, 403 end diff --git a/src/ssso_sites.lua b/src/ssso_sites.lua index bc64c7f..ebf7192 100644 --- a/src/ssso_sites.lua +++ b/src/ssso_sites.lua @@ -1,6 +1,6 @@ local json = require("cjson.safe") +local id = require("ssso_identity") local nginx = require("ssso_nginx") -local prof = require("ssso_profile") local known_private_re = {} local known_sites = {} @@ -26,7 +26,7 @@ end local function is_known_private(req_data) for _, r in ipairs(known_private_re) do - if nginx.matches(req_data, r) then + if req_data:matches(r) then return true end end @@ -37,12 +37,12 @@ local function handle_request(req_data, auth) if auth then for _, site in ipairs(auth.ok) do for _, r in ipairs(site.r) do - if nginx.matches(req_data, r) then + if req_data:matches(r) then for _, a in ipairs(site.a) do if a[1] == "C" then - nginx.add_cookie(a[2], prof.format(a[3], auth)) + nginx.add_cookie(a[2], auth:format(a[3])) elseif a[1] == "H" then - nginx.add_header(a[2], prof.format(a[3], auth)) + nginx.add_header(a[2], auth:format(a[3])) end end return nginx.forward_request(req_data) @@ -50,7 +50,7 @@ local function handle_request(req_data, auth) end end for _, r in ipairs(auth.ko) do - if nginx.matches(req_data, r) then + if req_data:matches(r) then return nginx.redirect_to_login(req_data, 403) end end @@ -83,12 +83,31 @@ local function format_pattern(pattern) return ok end -local function with_sites(profile, user) +local class__profile = {} +setmetatable(class__profile, {__index = id.class__identity}) + +function class__profile:build(delegate_identity, ok_list, ko_list) + local profile = { + delegate = delegate_identity, + ok = ok_list, + ko = ko_list, + } + setmetatable(profile, {__index = self}) + return profile +end + +function class__profile:build_from_lists(user, password, name, email, ok_list, ko_list) + local delegate_identity = id.class__identity:build(user, password, name, email) + return self:build(delegate_identity, ok_list, ko_list) +end + +function class__profile:build_from_conf(user, password, name, email) local f, site, go_on local ok_list = {} local ko_list = {} - for _, name in ipairs(known_sites) do - f = io.open(name, "r") + local delegate_identity = id.class__identity:build(user, password, name, email) + for _, known in ipairs(known_sites) do + f = io.open(known, "r") if f then site = json.decode(f:read("*all")) f:close() @@ -120,14 +139,28 @@ local function with_sites(profile, user) end end end - profile["ok"] = ok_list - profile["ko"] = ko_list - return profile + return self:build(delegate_identity, ok_list, ko_list) end -local function serialize(profile) +function class__profile:email() + return self.delegate:email() +end + +function class__profile:name() + return self.delegate:name() +end + +function class__profile:user() + return self.delegate:user() +end + +function class__profile:format(template) + return self.delegate:format(template) +end + +function class__profile:serialize() local ser_s = "" - for _, site in ipairs(profile.ok or {}) do + for _, site in ipairs(self.ok or {}) do for _, r in ipairs(site.r) do ser_s = ser_s .. r .. "\029" end @@ -136,40 +169,40 @@ local function serialize(profile) end ser_s = ser_s .. "\031" end - for _, r in ipairs(profile.ko or {}) do + for _, r in ipairs(self.ko or {}) do ser_s = ser_s .. r .. "\030" end - return ser_s + return ser_s .. "\026" .. self.delegate:serialize() end -local function deserialize_update(ser, profile) - if not ser or ser == "" then - return profile - end +function class__profile:deserialize(ser) local ok_list = {} local ko_list = {} - local remainder = ser:gsub("(.-)\031", function (ser_ok) - local ok = { - r = {}, - a = {}, - } - ser_ok = ser_ok:gsub("(.-)\029", function(r) table.insert(ok.r, r); return "" end) - ser_ok:gsub("(.)([^=]-)=(.-)\028", function(t, n, v) table.insert(ok.a, {t, n, v}) end) - table.insert(ok_list, ok) + ser = ser:gsub("^(.-)\026", function (ser_sites) + ser_sites = ser_sites:gsub("(.-)\031", function (ser_ok) + local ok = { + r = {}, + a = {}, + } + ser_ok = ser_ok:gsub("(.-)\029", function(r) table.insert(ok.r, r); return "" end) + ser_ok:gsub("(.)([^=]-)=(.-)\028", function(t, n, v) table.insert(ok.a, {t, n, v}) end) + table.insert(ok_list, ok) + return "" + end) + ser_sites = ser_sites:gsub("(.-)\030", function (ko) + table.insert(ko_list, ko) + return "" + end) return "" end) - remainder = remainder:gsub("(.-)\030", function (ko) - table.insert(ko_list, ko) - return "" - end) - profile.ok = ok_list - profile.ko = ko_list - return profile, remainder + local delegate_identity = id.class__identity:deserialize(ser) + return self:build(delegate_identity, ok_list, ko_list) end -local function authorized_links(user) +function class__profile:authorized_links() local links = {} local f, site, go_on + local user = self:user() for _, name in ipairs(known_sites) do f = io.open(name, "r") if f then @@ -206,10 +239,7 @@ local function authorized_links(user) end return { - authorized_links = authorized_links, - deserialize_update = deserialize_update, -- TODO: test + class__profile = class__profile, handle_request = handle_request, load_sites = load_sites, - serialize = serialize, -- TODO: test - with_sites = with_sites, } diff --git a/test/crypto.utest.lua b/test/crypto.utest.lua index eae8cc1..7a0b1c6 100644 --- a/test/crypto.utest.lua +++ b/test/crypto.utest.lua @@ -1,14 +1,13 @@ local lu = require("luaunit") local conf = require("ssso_config") local crypt = require("ssso_crypto") +local sites = require("ssso_sites") local here = debug.getinfo(1).source:sub(2, -18) conf.load_conf(here) -local data = { - u = "u", - e = "u@h", - ok = { +local data = sites.class__profile:build_from_lists("u", nil, nil, "u@h", + { { r = { "regex1", @@ -19,10 +18,10 @@ local data = { } }, }, - ko = { + { "regex2", - }, -} + } +) function test_jws_is_well_structured() local jws, _ = crypt.get_jws_and_tslimit(data) @@ -31,17 +30,12 @@ end function test_jws_can_be_decoded() local jws, _ = crypt.get_jws_and_tslimit(data) - local stored, _, _ = crypt.get_data_and_new_jws(jws) + local stored, _, _ = crypt.get_profile_and_new_jws(jws) lu.assertEquals(stored, data) end -function test_data_must_contain_field_u() - local wrong = { - i = 1, - f = 2.3, - b = true, - n = nil, - } +function test_data_must_be_a_profile_with_a_user() + local wrong = sites.class__profile:build_from_conf(nil, "P", "N", "E") local jws, ts = crypt.get_jws_and_tslimit(wrong) lu.assertNil(jws) lu.assertNil(ts) diff --git a/test/identity.utest.lua b/test/identity.utest.lua new file mode 100644 index 0000000..dc0d780 --- /dev/null +++ b/test/identity.utest.lua @@ -0,0 +1,61 @@ +local lu = require("luaunit") +local id = require("ssso_identity") + +function test_format_replaces_user_placeholders() + local identity = id.class__identity:build("U", nil, nil, nil) + local template = '{user: "\ru.", foo: "bar", name: "\ru."}' + lu.assertEquals(identity:format(template), '{user: "U", foo: "bar", name: "U"}') +end + +function test_format_replaces_password_placeholders() + local identity = id.class__identity:build(nil, "P", nil, nil) + local template = '{pass: "\rp.", foo: "bar", secret: "\rp."}' + lu.assertEquals(identity:format(template), '{pass: "P", foo: "bar", secret: "P"}') +end + +function test_format_replaces_name_placeholders() + local identity = id.class__identity:build(nil, nil, "N", nil) + local template = '{name: "\rn.", foo: "bar", nickname: "\rn."}' + lu.assertEquals(identity:format(template), '{name: "N", foo: "bar", nickname: "N"}') +end + +function test_format_replaces_email_placeholders() + local identity = id.class__identity:build(nil, nil, nil, "user@host") + local template = '{user: "\re.", foo: "bar", mail: "\re."}' + lu.assertEquals(identity:format(template), '{user: "user@host", foo: "bar", mail: "user@host"}') +end + +function test_format_replaces_base64_calls() + local identity = id.class__identity:build("👤", "🔒", nil, nil) + local template = 'Authorization: Basic \rb64(\ru.:\rp.).' + lu.assertEquals(identity:format(template), 'Authorization: Basic 8J+RpDrwn5SS') +end + +function test_format_replaces_base64url_calls() + local identity = id.class__identity:build("👤", "🔒", nil, nil) + local template = '?authorization=Basic+\ru64(\ru.:\rp.).' + lu.assertEquals(identity:format(template), '?authorization=Basic+8J-RpDrwn5SS') +end + +function test_email_returns_the_identity_s_email() + local identity = id.class__identity:build(nil, nil, nil, "E") + lu.assertEquals(identity:email(), "E") +end + + +function test_name_returns_the_identity_s_name() + local identity = id.class__identity:build(nil, nil, "N", nil) + lu.assertEquals(identity:name(), "N") +end + + +function test_user_returns_the_identity_s_user() + local identity = id.class__identity:build("U", nil, nil, nil) + lu.assertEquals(identity:user(), "U") +end + +function test_build_identity_returns_the_given_information() + lu.assertEquals(id.class__identity:build("U", "P", "N", "E"), {u = "U", p = "P", n = "N", e = "E"}) +end + +os.exit(lu.LuaUnit.run()) diff --git a/test/login.utest.lua b/test/login.utest.lua index 4f8e367..9000bd0 100644 --- a/test/login.utest.lua +++ b/test/login.utest.lua @@ -15,7 +15,7 @@ function test_get_login_url_returns_html_with_back_url_substitution() ngx.reset_resp_body() ngx.var.request_method = "GET" ngx.var.request_uri = "/ssso/login?back=/somewhere" - local r = ng.get_request() + local r = ng.class__request:current() local expected = [[ @@ -40,7 +40,7 @@ function test_login_css_url_returns_css() ngx.reset_resp_body() ngx.var.request_method = "BLABLA" ngx.var.request_uri = "/ssso/login.css" - local r = ng.get_request() + local r = ng.class__request:current() local expected = "/*CSS*/\n" -- when local resp = login.answer_request(r) @@ -58,7 +58,7 @@ function test_login_js_url_returns_js() ngx.reset_resp_body() ngx.var.request_method = "BLABLA" ngx.var.request_uri = "/ssso/login.js" - local r = ng.get_request() + local r = ng.class__request:current() local expected = "//JS\n" -- when local resp = login.answer_request(r) @@ -75,7 +75,7 @@ function test_unknown_login_url_returns_404() ngx.reset_resp_body() ngx.var.request_method = "BLABLA" ngx.var.request_uri = "/ssso/login/unknown" - local r = ng.get_request() + local r = ng.class__request:current() -- when local resp = login.answer_request(r) -- then @@ -90,7 +90,7 @@ function test_get_login_url_with_cause_401_returns_html_with_associated_message( ngx.reset_resp_body() ngx.var.request_method = "GET" ngx.var.request_uri = "/ssso/login?cause=401" - local r = ng.get_request() + local r = ng.class__request:current() local expected = [[ @@ -115,7 +115,7 @@ function test_get_login_url_with_cause_403_returns_html_with_associated_message( ngx.reset_resp_body() ngx.var.request_method = "GET" ngx.var.request_uri = "/ssso/login?cause=403" - local r = ng.get_request() + local r = ng.class__request:current() local expected = [[ @@ -141,7 +141,7 @@ function test_post_login_url_with_wrong_credentials_returns_html_with_associated ngx.reset_post_var() ngx.var.request_method = "POST" ngx.var.request_uri = "/ssso/login" - local r = ng.get_request() + local r = ng.class__request:current() local expected = [[ @@ -169,7 +169,7 @@ function test_post_login_url_with_good_credentials_redirects_to_portal_with_sess ngx.var.request_uri = "/ssso/login" ngx.post_var.login = "goodlogin" ngx.post_var.password = "goodpassword" - local r = ng.get_request() + local r = ng.class__request:current() -- when local resp = login.answer_request(r) -- then @@ -190,7 +190,7 @@ function test_post_login_url_with_good_credentials_and_back_url_redirects_to_giv ngx.post_var.login = "goodlogin" ngx.post_var.password = "goodpassword" ngx.post_var.back = "/somewhere" - local r = ng.get_request() + local r = ng.class__request:current() -- when local resp = login.answer_request(r) -- then diff --git a/test/nginx.utest.lua b/test/nginx.utest.lua index 26d7ce2..7fc376c 100644 --- a/test/nginx.utest.lua +++ b/test/nginx.utest.lua @@ -15,7 +15,7 @@ function test_refe_host_meth_uri_taken_from_ngx() ngx.var.request_method = "M" ngx.var.request_uri = "U" -- when - local r = ng.get_request() + local r = ng.class__request:current() -- then lu.assertEquals(r.referer, "R") lu.assertEquals(r.host, "H") @@ -31,7 +31,7 @@ function test_empty_referer_reported_as_nil() ngx.var.request_uri = "U" ngx.var.http_referer = "" -- when - local r = ng.get_request() + local r = ng.class__request:current() -- then lu.assertEquals(r.referer, nil) end @@ -41,7 +41,7 @@ function test_query_params_split_from_uri_and_decoded() ngx.reset_var() ngx.var.request_uri = "U?P=V&Q=W" -- when - local r = ng.get_request() + local r = ng.class__request:current() -- then lu.assertEquals(r.uri, "U?P=V&Q=W") lu.assertEquals(r.target, "U") @@ -53,7 +53,7 @@ function test_default_scheme_is_http() ngx.reset_var() ngx.var.request_uri = "U" -- when - local r = ng.get_request() + local r = ng.class__request:current() -- then lu.assertEquals(r.scheme, "http") end @@ -64,7 +64,7 @@ function test_scheme_is_https_when_proxy_https_var() ngx.var.request_uri = "U" ngx.var.proxy_https = 1 -- when - local r = ng.get_request() + local r = ng.class__request:current() -- then lu.assertEquals(r.scheme, "https") end @@ -75,7 +75,7 @@ function test_scheme_is_https_when_https_var() ngx.var.request_uri = "U" ngx.var.https = 1 -- when - local r = ng.get_request() + local r = ng.class__request:current() -- then lu.assertEquals(r.scheme, "https") end @@ -142,10 +142,10 @@ function test_method_is_recognized_case_insensitive() ngx.reset_var() ngx.var.request_method = "get" ngx.var.request_uri = "U" - local r = ng.get_request() + local r = ng.class__request:current() -- when - local is_get = ng.has_method(r, "GET") - local is_post = ng.has_method(r, "POST") + local is_get = r:has_method("GET") + local is_post = r:has_method("POST") -- then lu.assertTrue(is_get and true) lu.assertFalse(is_post or false) @@ -155,10 +155,10 @@ function test_uri_identity_ignores_query_parameters() -- given ngx.reset_var() ngx.var.request_uri = "U?P=V&Q=W" - local r = ng.get_request() + local r = ng.class__request:current() -- when - local is_without_qp = ng.is(r, "U") - local is_with_qp = ng.is(r, "U?P=V&Q=W") + local is_without_qp = r:is("U") + local is_with_qp = r:is("U?P=V&Q=W") -- then lu.assertTrue(is_without_qp and true) lu.assertFalse(is_with_qp or false) @@ -168,10 +168,10 @@ function test_uri_match_ignores_query_parameters() -- given ngx.reset_var() ngx.var.request_uri = "/aa?bb" - local r = ng.get_request() + local r = ng.class__request:current() -- when - local matches_without_qp = ng.matches(r, "/a+$") - local matches_with_qp = ng.matches(r, "/a.*b") + local matches_without_qp = r:matches("/a+$") + local matches_with_qp = r:matches("/a.*b") -- then lu.assertTrue(matches_without_qp and true) lu.assertFalse(matches_with_qp or false) @@ -181,10 +181,10 @@ function test_starts_with_ignores_query_parameters() -- given ngx.reset_var() ngx.var.request_uri = "/aa?bb" - local r = ng.get_request() + local r = ng.class__request:current() -- when - local start_without_qp = ng.starts_with(r, "/a") - local start_with_qp = ng.starts_with(r, "/aa?b") + local start_without_qp = r:starts_with("/a") + local start_with_qp = r:starts_with("/aa?b") -- then lu.assertTrue(start_without_qp and true) lu.assertFalse(start_with_qp or false) @@ -194,10 +194,10 @@ function test_starts_with_must_start_with_given_value() -- given ngx.reset_var() ngx.var.request_uri = "/aa?bb" - local r = ng.get_request() + local r = ng.class__request:current() -- when - local start_in_middle = ng.starts_with(r, "aa") - local does_not_start = ng.starts_with(r, "x") + local start_in_middle = r:starts_with("aa") + local does_not_start = r:starts_with("x") -- then lu.assertFalse(start_in_middle or false) lu.assertFalse(does_not_start or false) @@ -207,11 +207,11 @@ function test_has_param_works_disregarding_the_value() -- given ngx.reset_var() ngx.var.request_uri = "/aa?bb&c=1" - local r = ng.get_request() + local r = ng.class__request:current() -- when - local has_unknown_param = ng.has_param(r, "b") - local has_unvalued_param = ng.has_param(r, "bb") - local has_valued_param = ng.has_param(r, "c") + local has_unknown_param = r:has_param("b") + local has_unvalued_param = r:has_param("bb") + local has_valued_param = r:has_param("c") -- then lu.assertFalse(has_unknown_param or false) lu.assertTrue(has_unvalued_param and true) @@ -222,10 +222,10 @@ function test_has_param_works_with_a_correct_value() -- given ngx.reset_var() ngx.var.request_uri = "/aa?bb&c=1" - local r = ng.get_request() + local r = ng.class__request:current() -- when - local has_unvalued_param = ng.has_param(r, "bb", true) - local has_valued_param = ng.has_param(r, "c", "1") + local has_unvalued_param = r:has_param("bb", true) + local has_valued_param = r:has_param("c", "1") -- then lu.assertTrue(has_unvalued_param and true) lu.assertTrue(has_valued_param and true) @@ -235,11 +235,11 @@ function test_has_param_works_with_a_wrong_value() -- given ngx.reset_var() ngx.var.request_uri = "/aa?bb&c=1" - local r = ng.get_request() + local r = ng.class__request:current() -- when - local has_unknown_param = ng.has_param(r, "b", "x") - local has_unvalued_param = ng.has_param(r, "bb", "x") - local has_valued_param = ng.has_param(r, "c", "x") + local has_unknown_param = r:has_param("b", "x") + local has_unvalued_param = r:has_param("bb", "x") + local has_valued_param = r:has_param("c", "x") -- then lu.assertFalse(has_unknown_param or false) lu.assertFalse(has_unvalued_param or false) @@ -284,7 +284,7 @@ function test_with_post_parameters_merges_post_parameters_to_request_data() ngx.post_var.p = "5" ngx.post_var.r = "hello" -- when (1) - local r = ng.get_request() + local r = ng.class__request:current() -- then (1) lu.assertEquals(r, { scheme = "http", @@ -296,7 +296,7 @@ function test_with_post_parameters_merges_post_parameters_to_request_data() }, }) -- when (2) - r = ng.with_post_parameters(r) + r = r:with_post_parameters() -- then (2) lu.assertEquals(r, { scheme = "http", diff --git a/test/portal1.ctest.lua b/test/portal1.ctest.lua index 2bc2122..e43d156 100644 --- a/test/portal1.ctest.lua +++ b/test/portal1.ctest.lua @@ -1,17 +1,14 @@ local lu = require("luaunit") local ngx = require("ngx") local crypto = require("ssso_crypto") +local sites = require("ssso_sites") require("do_init") function test_portal_url_returns_html_with_authorized_links_and_identity() -- given - local jws, _ = crypto.get_jws_and_tslimit({ - u = "guest", - p = "", - n = "Guest", - e = "guest@example.org", - }) + local profile = sites.class__profile:build_from_lists("guest", "", "Guest", "guest@example.org") + local jws, _ = crypto.get_jws_and_tslimit(profile) ngx.reset_resp_body() ngx.reset_var() ngx.var.cookie_SSSO_TOKEN = jws diff --git a/test/portal2.ctest.lua b/test/portal2.ctest.lua index 82a66cd..eeb2542 100644 --- a/test/portal2.ctest.lua +++ b/test/portal2.ctest.lua @@ -1,12 +1,14 @@ local lu = require("luaunit") local ngx = require("ngx") local crypto = require("ssso_crypto") +local sites = require("ssso_sites") require("do_init") function test_portal_css_url_returns_css() -- given - local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"}) + local profile = sites.class__profile:build_from_lists("U", "P", "N", "u@h") + local jws, _ = crypto.get_jws_and_tslimit(profile) ngx.reset_resp_body() ngx.reset_var() ngx.var.cookie_SSSO_TOKEN = jws diff --git a/test/portal3.ctest.lua b/test/portal3.ctest.lua index 982928a..c31167b 100644 --- a/test/portal3.ctest.lua +++ b/test/portal3.ctest.lua @@ -1,12 +1,14 @@ local lu = require("luaunit") local ngx = require("ngx") local crypto = require("ssso_crypto") +local sites = require("ssso_sites") require("do_init") function test_portal_js_url_returns_js() -- given - local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"}) + local profile = sites.class__profile:build_from_lists("U", "P", "N", "u@h") + local jws, _ = crypto.get_jws_and_tslimit(profile) ngx.reset_resp_body() ngx.reset_var() ngx.var.cookie_SSSO_TOKEN = jws diff --git a/test/portal4.ctest.lua b/test/portal4.ctest.lua index 935cff6..b4fe14a 100644 --- a/test/portal4.ctest.lua +++ b/test/portal4.ctest.lua @@ -1,12 +1,14 @@ local lu = require("luaunit") local ngx = require("ngx") local crypto = require("ssso_crypto") +local sites = require("ssso_sites") require("do_init") function test_unknown_portal_url_returns_404() -- given - local jws, _ = crypto.get_jws_and_tslimit({u = "U", p = "P", n = "N", e = "u@h"}) + local profile = sites.class__profile:build_from_lists("U", "P", "N", "u@h") + local jws, _ = crypto.get_jws_and_tslimit(profile) ngx.reset_resp_body() ngx.reset_var() ngx.var.cookie_SSSO_TOKEN = jws diff --git a/test/portal5.ctest.lua b/test/portal5.ctest.lua index 50fb389..4a66fc6 100644 --- a/test/portal5.ctest.lua +++ b/test/portal5.ctest.lua @@ -1,6 +1,5 @@ local lu = require("luaunit") local ngx = require("ngx") -local crypto = require("ssso_crypto") require("do_init") diff --git a/test/profile.utest.lua b/test/profile.utest.lua deleted file mode 100644 index 577cd79..0000000 --- a/test/profile.utest.lua +++ /dev/null @@ -1,81 +0,0 @@ -local lu = require("luaunit") -local prf = require("ssso_profile") - -function test_format_replaces_user_placeholders() - local profile = { - u = "U", - } - local template = '{user: "\ru.", foo: "bar", name: "\ru."}' - lu.assertEquals(prf.format(template, profile), '{user: "U", foo: "bar", name: "U"}') -end - -function test_format_replaces_password_placeholders() - local profile = { - p = "P", - } - local template = '{pass: "\rp.", foo: "bar", secret: "\rp."}' - lu.assertEquals(prf.format(template, profile), '{pass: "P", foo: "bar", secret: "P"}') -end - -function test_format_replaces_name_placeholders() - local profile = { - n = "N", - } - local template = '{name: "\rn.", foo: "bar", nickname: "\rn."}' - lu.assertEquals(prf.format(template, profile), '{name: "N", foo: "bar", nickname: "N"}') -end - -function test_format_replaces_email_placeholders() - local profile = { - e = "user@host", - } - local template = '{user: "\re.", foo: "bar", mail: "\re."}' - lu.assertEquals(prf.format(template, profile), '{user: "user@host", foo: "bar", mail: "user@host"}') -end - -function test_format_replaces_base64_calls() - local profile = { - u = "👤", - p = "🔒", - } - local template = 'Authorization: Basic \rb64(\ru.:\rp.).' - lu.assertEquals(prf.format(template, profile), 'Authorization: Basic 8J+RpDrwn5SS') -end - -function test_format_replaces_base64url_calls() - local profile = { - u = "👤", - p = "🔒", - } - local template = '?authorization=Basic+\ru64(\ru.:\rp.).' - lu.assertEquals(prf.format(template, profile), '?authorization=Basic+8J-RpDrwn5SS') -end - -function test_email_returns_the_profile_s_email() - local profile = { - e = "E", - } - lu.assertEquals(prf.email(profile), "E") -end - - -function test_name_returns_the_profile_s_name() - local profile = { - n = "N", - } - lu.assertEquals(prf.name(profile), "N") -end - - -function test_user_returns_the_profile_s_user() - local profile = { - u = "U", - } - lu.assertEquals(prf.user(profile), "U") -end - -function test_build_profile_returns_the_given_information() - lu.assertEquals(prf.build_profile("U", "P", "N", "E"), {u = "U", p = "P", n = "N", e = "E"}) -end - -os.exit(lu.LuaUnit.run()) diff --git a/test/sessions.utest.lua b/test/sessions.utest.lua index 3e34dc9..eb95441 100644 --- a/test/sessions.utest.lua +++ b/test/sessions.utest.lua @@ -51,13 +51,13 @@ function test_session_and_cookie_renewal_if_good_cookie() ngx.req.reset() ngx.reset_header() ngx.reset_var() - local data = {u = "bob"} - local c, _ = crypt.get_jws_and_tslimit(data) + local profile = sites.class__profile:build_from_lists("bob", nil, nil, nil, {}, {}) + local c, _ = crypt.get_jws_and_tslimit(profile) ngx.var.cookie_SSSO_TOKEN = c -- when local s, h = sess.get_session() -- then - lu.assertEquals(s, data) + lu.assertEquals(s, profile) lu.assertEquals(h, 200) lu.assertNil(ngx.header["Set-Cookie"].link) lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") @@ -84,15 +84,15 @@ function test_basic_auth_takes_precedence_over_cookie() ngx.req.reset() ngx.reset_header() ngx.reset_var() - local data = {u = "forget me"} - local c, _ = crypt.get_jws_and_tslimit(data) + local profile = sites.class__profile:build_from_lists("forget me", nil, nil, nil, {}, {}) + local c, _ = crypt.get_jws_and_tslimit(profile) ngx.var.cookie_SSSO_TOKEN = c ngx.var.Authentication = "Basic " .. b64.encode_base64("bob:goodpassword") -- when local s, h = sess.get_session() -- then lu.assertEquals(h, 200) - lu.assertEquals(s.u, "bob") + lu.assertEquals(s:user(), "bob") lu.assertNil(ngx.header["Set-Cookie"].link) lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") end @@ -102,15 +102,15 @@ function test_basic_auth_ignored_if_invalid() ngx.req.reset() ngx.reset_header() ngx.reset_var() - local data = {u = "do not forget me"} - local c, _ = crypt.get_jws_and_tslimit(data) + local profile = sites.class__profile:build_from_lists("do not forget me", nil, nil, nil, {}, {}) + local c, _ = crypt.get_jws_and_tslimit(profile) ngx.var.cookie_SSSO_TOKEN = c ngx.var.Authentication = "Basic !!!!" -- when local s, h = sess.get_session() -- then lu.assertEquals(h, 200) - lu.assertEquals(s.u, "do not forget me") + lu.assertEquals(s:user(), "do not forget me") lu.assertNil(ngx.header["Set-Cookie"].link) lu.assertStrMatches(ngx.header["Set-Cookie"].v, "SSSO_TOKEN=[^%.]+%.[^%.]+%.[^%.]+; Path=/; Expires=1626550390; Secure") end diff --git a/test/sites.utest.lua b/test/sites.utest.lua index 23e346f..7ac5720 100644 --- a/test/sites.utest.lua +++ b/test/sites.utest.lua @@ -13,7 +13,7 @@ function test_anonymous_access_to_unknown_site_accepted() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/unknown" - local r = ng.get_request() + local r = ng.class__request:current() -- when local resp = sites.handle_request(r, nil) -- then @@ -26,7 +26,7 @@ function test_anonymous_access_to_public_site_accepted() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/public/page" - local r = ng.get_request() + local r = ng.class__request:current() -- when local resp = sites.handle_request(r, nil) -- then @@ -39,7 +39,7 @@ function test_anonymous_access_to_public_page_of_mixed_site_accepted() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/mixed/bob/wiki/foo.adoc" - local r = ng.get_request() + local r = ng.class__request:current() -- when local resp = sites.handle_request(r, nil) -- then @@ -52,7 +52,7 @@ function test_anonymous_access_to_private_page_of_mixed_site_redirected_401() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/mixed/bob/wiki/_new" - local r = ng.get_request() + local r = ng.class__request:current() -- when local resp = sites.handle_request(r, nil) -- then @@ -65,7 +65,7 @@ function test_anonymous_access_to_private_site_redirected_401() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/private/page" - local r = ng.get_request() + local r = ng.class__request:current() -- when local resp = sites.handle_request(r, nil) -- then @@ -78,12 +78,8 @@ function test_authenticated_access_to_unknown_site_accepted() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/unknown" - local r = ng.get_request() - local profile = { - u = "U", - ok = {}, - ko = {}, - } + local r = ng.class__request:current() + local profile = sites.class__profile:build_from_lists("U", nil, nil, nil, {}, {}) -- when local resp = sites.handle_request(r, profile) -- then @@ -96,11 +92,9 @@ function test_authenticated_access_to_public_site_accepted() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/public/page" - local r = ng.get_request() - local profile = { - u = "U", - p = "P", - ok = { + local r = ng.class__request:current() + local profile = sites.class__profile:build_from_lists("U", "P", nil, nil, + { { r = { "^/public", @@ -110,8 +104,9 @@ function test_authenticated_access_to_public_site_accepted() {"C", "X-PROXY-PASS", "\rp."}, }, }, - } - } + }, + {} + ) -- when local resp = sites.handle_request(r, profile) -- then @@ -125,14 +120,13 @@ function test_authenticated_access_to_public_site_can_be_denied() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/public/page" - local r = ng.get_request() - local profile = { - u = "banned", - ok = {}, - ko = { + local r = ng.class__request:current() + local profile = sites.class__profile:build_from_lists("banned", nil, nil, nil, + {}, + { "^/public", } - } + ) -- when local resp = sites.handle_request(r, profile) -- then @@ -145,11 +139,9 @@ function test_authenticated_access_to_public_page_of_mixed_site_accepted() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/mixed/bob/wiki/foo.adoc" - local r = ng.get_request() - local profile = { - u = "U", - p = "P", - ok = { + local r = ng.class__request:current() + local profile = sites.class__profile:build_from_lists("U", "P", nil, nil, + { { r = { "^/public", @@ -179,8 +171,9 @@ function test_authenticated_access_to_public_page_of_mixed_site_accepted() {"C", "X-PROXY-PASSWORD", "\rp."}, }, }, - } - } + }, + {} + ) -- when local resp = sites.handle_request(r, profile) -- then @@ -194,11 +187,9 @@ function test_authenticated_access_to_private_page_of_mixed_site_accepted() ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/mixed/bob/wiki/_new" - local r = ng.get_request() - local profile = { - u = "U", - p = "P", - ok = { + local r = ng.class__request:current() + local profile = sites.class__profile:build_from_lists("U", "P", nil, nil, + { { r = { "^/public", @@ -228,8 +219,9 @@ function test_authenticated_access_to_private_page_of_mixed_site_accepted() {"C", "X-PROXY-PASSWORD", "\rp."}, }, }, - } - } + }, + {} + ) -- when local resp = sites.handle_request(r, profile) -- then @@ -243,11 +235,9 @@ function test_authenticated_access_to_private_site_accepted_with_the_right_user( ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/private/page" - local r = ng.get_request() - local profile = { - u = "jean", - p = "P", - ok = { + local r = ng.class__request:current() + local profile = sites.class__profile:build_from_lists("jean", "P", nil, nil, + { { r = { "^/private", @@ -256,8 +246,9 @@ function test_authenticated_access_to_private_site_accepted_with_the_right_user( {"H", "Authorization", "Basic \rb64(\ru.:\rp.)."}, }, }, - } - } + }, + {} + ) -- when local resp = sites.handle_request(r, profile) -- then @@ -270,15 +261,13 @@ function test_authenticated_access_to_private_site_redirected_403_with_the_wrong ngx.req.reset() ngx.reset_var() ngx.var.request_uri = "/private/page" - local r = ng.get_request() - local profile = { - u = "U", - p = "P", - ok = {}, - ko = { + local r = ng.class__request:current() + local profile = sites.class__profile:build_from_lists("U", "P", nil, nil, + {}, + { "^/private", } - } + ) -- when local resp = sites.handle_request(r, profile) -- then From 969367dc9f54ba16567f7ce6502736c6b4574aa0 Mon Sep 17 00:00:00 2001 From: Yves G Date: Sun, 3 Oct 2021 14:05:10 +0200 Subject: [PATCH 5/5] refactor: remove code duplication --- src/ssso_nginx.lua | 2 +- src/ssso_portal.lua | 1 - src/ssso_sites.lua | 138 ++++++++++++++++++-------------------------- 3 files changed, 58 insertions(+), 83 deletions(-) diff --git a/src/ssso_nginx.lua b/src/ssso_nginx.lua index 03ef55c..8808eff 100644 --- a/src/ssso_nginx.lua +++ b/src/ssso_nginx.lua @@ -1,4 +1,4 @@ -local ngx = require("ngx") +local ngx = require("ngx") local b64 = require("ssso_base64") local util = require("ssso_util") local conf = require("ssso_config") diff --git a/src/ssso_portal.lua b/src/ssso_portal.lua index 1d8e743..0fecac6 100644 --- a/src/ssso_portal.lua +++ b/src/ssso_portal.lua @@ -1,7 +1,6 @@ local util = require("ssso_util") local conf = require("ssso_config") local nginx = require("ssso_nginx") -local sites = require("ssso_sites") local root = "" diff --git a/src/ssso_sites.lua b/src/ssso_sites.lua index ebf7192..89eb03a 100644 --- a/src/ssso_sites.lua +++ b/src/ssso_sites.lua @@ -62,25 +62,36 @@ local function handle_request(req_data, auth) end end -local function format_pattern(pattern) - local a_type - local ok = { - r = pattern.lua_regex or {}, - a = {}, - } - for _, action in ipairs(pattern.actions or {}) do - if action.type == "header" then - a_type = "H" - elseif action.type == "cookie" then - a_type = "C" - else - a_type = nil - end - if a_type then - table.insert(ok.a, {a_type, action.name, action.value}) +local function parse_known_sites(user, denied_handler, allowed_handler) + local f, site, go_on + for _, known in ipairs(known_sites) do + f = io.open(known, "r") + if f then + site = json.decode(f:read("*all")) + f:close() + for _, pat in ipairs(site.patterns) do + go_on = true + for _, denied in ipairs(pat.deny or {}) do + if denied == user then + go_on = false + denied_handler(pat) + end + end + if go_on then + if pat.public then + allowed_handler(pat) + else + for _, allowed in ipairs(pat.allow or {}) do + if allowed == "*" or allowed == user then + allowed_handler(pat) + break + end + end + end + end + end end end - return ok end local class__profile = {} @@ -102,43 +113,35 @@ function class__profile:build_from_lists(user, password, name, email, ok_list, k end function class__profile:build_from_conf(user, password, name, email) - local f, site, go_on local ok_list = {} local ko_list = {} local delegate_identity = id.class__identity:build(user, password, name, email) - for _, known in ipairs(known_sites) do - f = io.open(known, "r") - if f then - site = json.decode(f:read("*all")) - f:close() - for _, pat in ipairs(site.patterns) do - go_on = true - for _, denied in ipairs(pat.deny or {}) do - if denied == user then - go_on = false - for _, re in ipairs(pat.lua_regex) do - table.insert(ko_list, re) - end - break - end + parse_known_sites(user, + function (ko_pat) + for _, re in ipairs(ko_pat.lua_regex) do + table.insert(ko_list, re) + end + end, + function (ok_pat) + local a_type + local ok = { + r = ok_pat.lua_regex or {}, + a = {}, + } + for _, action in ipairs(ok_pat.actions or {}) do + if action.type == "header" then + a_type = "H" + elseif action.type == "cookie" then + a_type = "C" + else + a_type = nil end - if go_on then - if pat.public then - local ok = format_pattern(pat) - table.insert(ok_list, ok) - else - for _, allowed in ipairs(pat.allow or {}) do - if allowed == "*" or allowed == user then - local ok = format_pattern(pat) - table.insert(ok_list, ok) - break - end - end - end + if a_type then + table.insert(ok.a, {a_type, action.name, action.value}) end end - end - end + table.insert(ok_list, ok) + end) return self:build(delegate_identity, ok_list, ko_list) end @@ -201,40 +204,13 @@ end function class__profile:authorized_links() local links = {} - local f, site, go_on - local user = self:user() - for _, name in ipairs(known_sites) do - f = io.open(name, "r") - if f then - site = json.decode(f:read("*all")) - f:close() - for _, pat in ipairs(site.patterns) do - go_on = true - for _, denied in ipairs(pat.deny or {}) do - if denied == user then - go_on = false - break - end - end - if go_on then - if pat.public then - for link, label in pairs(pat.portal or {}) do - table.insert(links, {link = link, label = label}) - end - else - for _, allowed in ipairs(pat.allow or {}) do - if allowed == "*" or allowed == user then - for link, label in pairs(pat.portal or {}) do - table.insert(links, {link = link, label = label}) - end - break - end - end - end - end + parse_known_sites(self:user(), + function (_) end, + function (ok_pat) + for link, label in pairs(ok_pat.portal or {}) do + table.insert(links, {link = link, label = label}) end - end - end + end) return links end