From d8c74604c0a5918c57a00a94b0214fcc327d5611 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 31 Mar 2020 02:20:40 +0200 Subject: [PATCH 01/15] portal with the new config file --- access.lua | 129 ++++++++++++++++++++++++++-------------------------- helpers.lua | 20 +++++--- 2 files changed, 79 insertions(+), 70 deletions(-) diff --git a/access.lua b/access.lua index 2616c3a..4ca0f4a 100644 --- a/access.lua +++ b/access.lua @@ -30,6 +30,8 @@ local logger = require("log") ngx.header["X-SSO-WAT"] = "You've just been SSOed" +local is_logged_in = hlp.is_logged_in() + -- -- 1. LOGIN -- @@ -66,7 +68,7 @@ end -- If the URL matches the portal URL, serve a portal file or proceed to a -- portal operation -- -if (ngx.var.host == conf["portal_domain"] or hlp.is_logged_in()) +if (ngx.var.host == conf["portal_domain"] or is_logged_in) and hlp.string.starts(ngx.var.uri, string.sub(conf["portal_path"], 1, -2)) then @@ -95,7 +97,7 @@ then -- If the `r` URI argument is set, it means that we want to -- be redirected (typically after a login phase) - elseif hlp.is_logged_in() and uri_args.r then + elseif is_logged_in and uri_args.r then -- Decode back url back_url = ngx.decode_base64(uri_args.r) @@ -146,7 +148,7 @@ then -- In case we want to serve portal login or assets for portal, just -- serve it - elseif hlp.is_logged_in() + elseif is_logged_in or ngx.var.uri == conf["portal_path"] or (hlp.string.starts(ngx.var.uri, conf["portal_path"].."assets") and (not ngx.var.http_referer @@ -238,32 +240,8 @@ if conf["redirected_regex"] then end end - -local longest_protected_match = hlp.longest_url_path(hlp.get_matches("protected")) or "" -local longest_skipped_match = hlp.longest_url_path(hlp.get_matches("skipped")) or "" -local longest_unprotected_match = hlp.longest_url_path(hlp.get_matches("unprotected")) or "" - -logger.debug("longest skipped "..longest_skipped_match) -logger.debug("longest unprotected "..longest_unprotected_match) -logger.debug("longest protected "..longest_protected_match) - -- --- 4. Skipped URLs --- --- If the URL matches one of the `skipped_urls` in the configuration file, --- it means that the URL should not be protected by the SSO and no header --- has to be sent, even if the user is already authenticated. --- - -if longest_skipped_match ~= "" -and string.len(longest_skipped_match) >= string.len(longest_protected_match) -and string.len(longest_skipped_match) > string.len(longest_unprotected_match) then - logger.debug("Skipping "..ngx.var.uri) - return hlp.pass() -end - --- --- 5. Specific files (used in YunoHost) +-- 4. Specific files (used in YunoHost) -- -- We want to serve specific portal assets right at the root of the domain. -- @@ -304,42 +282,65 @@ function serveYnhpanel() scandir("/usr/share/ssowat/portal/assets/themes/"..conf.theme, serveThemeFile) end --- --- 6. Unprotected URLs --- --- If the URL matches one of the `unprotected_urls` in the configuration file, --- it means that the URL should not be protected by the SSO *but* headers have --- to be sent if the user is already authenticated. --- --- It means that you can let anyone access to an app, but if a user has already --- been authenticated on the portal, he can have his authentication headers --- passed to the app. --- - -if longest_unprotected_match ~= "" -and string.len(longest_unprotected_match) > string.len(longest_protected_match) then - if hlp.is_logged_in() then - serveYnhpanel() - - hlp.set_headers() - end - logger.debug(ngx.var.uri.." is in unprotected_urls") - return hlp.pass() -end - -if hlp.is_logged_in() then - serveYnhpanel() - - -- If user has no access to this URL, redirect him to the portal - if not hlp.has_access() then - return hlp.redirect(conf.portal_url) - end - - -- If the user is authenticated and has access to the URL, set the headers - -- and let it be - hlp.set_headers() - return hlp.pass() -end +-- local longest_protected_match = hlp.longest_url_path(hlp.get_matches("protected")) or "" +-- local longest_skipped_match = hlp.longest_url_path(hlp.get_matches("skipped")) or "" +-- local longest_unprotected_match = hlp.longest_url_path(hlp.get_matches("unprotected")) or "" +-- +-- logger.debug("longest skipped "..longest_skipped_match) +-- logger.debug("longest unprotected "..longest_unprotected_match) +-- logger.debug("longest protected "..longest_protected_match) +-- +-- -- +-- -- 4. Skipped URLs +-- -- +-- -- If the URL matches one of the `skipped_urls` in the configuration file, +-- -- it means that the URL should not be protected by the SSO and no header +-- -- has to be sent, even if the user is already authenticated. +-- -- +-- +-- if longest_skipped_match ~= "" +-- and string.len(longest_skipped_match) >= string.len(longest_protected_match) +-- and string.len(longest_skipped_match) > string.len(longest_unprotected_match) then +-- logger.debug("Skipping "..ngx.var.uri) +-- return hlp.pass() +-- end +-- +-- -- +-- -- 6. Unprotected URLs +-- -- +-- -- If the URL matches one of the `unprotected_urls` in the configuration file, +-- -- it means that the URL should not be protected by the SSO *but* headers have +-- -- to be sent if the user is already authenticated. +-- -- +-- -- It means that you can let anyone access to an app, but if a user has already +-- -- been authenticated on the portal, he can have his authentication headers +-- -- passed to the app. +-- -- +-- +-- if longest_unprotected_match ~= "" +-- and string.len(longest_unprotected_match) > string.len(longest_protected_match) then +-- if is_logged_in then +-- serveYnhpanel() +-- +-- hlp.set_headers() +-- end +-- logger.debug(ngx.var.uri.." is in unprotected_urls") +-- return hlp.pass() +-- end +-- +-- if is_logged_in then +-- serveYnhpanel() +-- +-- -- If user has no access to this URL, redirect him to the portal +-- if not hlp.has_access() then +-- return hlp.redirect(conf.portal_url) +-- end +-- +-- -- If the user is authenticated and has access to the URL, set the headers +-- -- and let it be +-- hlp.set_headers() +-- return hlp.pass() +-- end -- diff --git a/helpers.lua b/helpers.lua index 4ca3b1a..a32f81d 100644 --- a/helpers.lua +++ b/helpers.lua @@ -229,6 +229,8 @@ function is_logged_in() local user = ngx.var.cookie_SSOwAuthUser local authHash = ngx.var.cookie_SSOwAuthHash + authUser = nil + if expireTime and expireTime ~= "" and authHash and authHash ~= "" and user and user ~= "" @@ -682,14 +684,20 @@ function get_data_for(view) -- Add user's accessible URLs using the ACLs. -- It is typically used to build the app list. - for url, name in pairs(conf["users"][user]) do + for permission_name, permission in pairs(conf["permissions"]) do + -- We want to display a tile, and uris is not empty + if permission['show_tile'] and next(permission['uris']) ~= nil then + url = permission['uris'][1] + name = permission['label'] - if ngx.var.host == conf["local_portal_domain"] then - url = string.gsub(url, conf["original_portal_domain"], conf["local_portal_domain"]) + if ngx.var.host == conf["local_portal_domain"] then + url = string.gsub(url, conf["original_portal_domain"], conf["local_portal_domain"]) + end + + table.insert(sorted_apps, name) + table.sort(sorted_apps) + table.insert(data["app"], index_of(sorted_apps, name), { url = url, name = name }) end - table.insert(sorted_apps, name) - table.sort(sorted_apps) - table.insert(data["app"], index_of(sorted_apps, name), { url = url, name = name }) end end From 0fc89d0fc9cdc82de70e445ca07c6934a28722d2 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Wed, 1 Apr 2020 00:43:59 +0200 Subject: [PATCH 02/15] Rework access --- access.lua | 89 +++++++---------------- helpers.lua | 206 ++++++++++++++++++++++------------------------------ 2 files changed, 111 insertions(+), 184 deletions(-) diff --git a/access.lua b/access.lua index 4ca0f4a..6476d63 100644 --- a/access.lua +++ b/access.lua @@ -29,8 +29,7 @@ local logger = require("log") -- Just a note for the client to know that he passed through the SSO ngx.header["X-SSO-WAT"] = "You've just been SSOed" - -local is_logged_in = hlp.is_logged_in() +local is_logged_in = hlp.refresh_logged_in() -- -- 1. LOGIN @@ -282,66 +281,27 @@ function serveYnhpanel() scandir("/usr/share/ssowat/portal/assets/themes/"..conf.theme, serveThemeFile) end --- local longest_protected_match = hlp.longest_url_path(hlp.get_matches("protected")) or "" --- local longest_skipped_match = hlp.longest_url_path(hlp.get_matches("skipped")) or "" --- local longest_unprotected_match = hlp.longest_url_path(hlp.get_matches("unprotected")) or "" --- --- logger.debug("longest skipped "..longest_skipped_match) --- logger.debug("longest unprotected "..longest_unprotected_match) --- logger.debug("longest protected "..longest_protected_match) --- --- -- --- -- 4. Skipped URLs --- -- --- -- If the URL matches one of the `skipped_urls` in the configuration file, --- -- it means that the URL should not be protected by the SSO and no header --- -- has to be sent, even if the user is already authenticated. --- -- --- --- if longest_skipped_match ~= "" --- and string.len(longest_skipped_match) >= string.len(longest_protected_match) --- and string.len(longest_skipped_match) > string.len(longest_unprotected_match) then --- logger.debug("Skipping "..ngx.var.uri) --- return hlp.pass() --- end --- --- -- --- -- 6. Unprotected URLs --- -- --- -- If the URL matches one of the `unprotected_urls` in the configuration file, --- -- it means that the URL should not be protected by the SSO *but* headers have --- -- to be sent if the user is already authenticated. --- -- --- -- It means that you can let anyone access to an app, but if a user has already --- -- been authenticated on the portal, he can have his authentication headers --- -- passed to the app. --- -- --- --- if longest_unprotected_match ~= "" --- and string.len(longest_unprotected_match) > string.len(longest_protected_match) then --- if is_logged_in then --- serveYnhpanel() --- --- hlp.set_headers() --- end --- logger.debug(ngx.var.uri.." is in unprotected_urls") --- return hlp.pass() --- end --- --- if is_logged_in then --- serveYnhpanel() --- --- -- If user has no access to this URL, redirect him to the portal --- if not hlp.has_access() then --- return hlp.redirect(conf.portal_url) --- end --- --- -- If the user is authenticated and has access to the URL, set the headers --- -- and let it be --- hlp.set_headers() --- return hlp.pass() --- end +local permission = hlp.get_best_permission() +if permission then + if is_logged_in then + serveYnhpanel() + + -- If the user is authenticated and has access to the URL, set the headers + -- and let it be + if permission["auth_header"] then + logger.debug("Set Headers") + hlp.set_headers() + end + end + + -- If user has no access to this URL, redirect him to the portal + if not hlp.has_access(permission) then + return hlp.redirect(conf.portal_url) + end + + return hlp.pass() +end -- -- 7. Basic HTTP Authentication @@ -364,11 +324,14 @@ if auth_header then logger.debug("User got authenticated through basic auth") -- If user has no access to this URL, redirect him to the portal - if not hlp.has_access(user) then + if not permission or not hlp.has_access(permission, user) then return hlp.redirect(conf.portal_url) end - hlp.set_headers(user) + if permission["auth_header"] then + logger.debug("Set Headers") + hlp.set_headers(user) + end return hlp.pass() end end diff --git a/helpers.lua b/helpers.lua index a32f81d..6fa9cd9 100644 --- a/helpers.lua +++ b/helpers.lua @@ -17,6 +17,8 @@ local url_parser = require "socket.url" -- Import Perl regular expressions library local rex = require "rex_pcre" +local is_logged_in = false + function refresh_config() conf = config.get_config() end @@ -224,7 +226,7 @@ end -- Check if the session cookies are set, and rehash server + client information -- to match the session hash. -- -function is_logged_in() +function refresh_logged_in() local expireTime = ngx.var.cookie_SSOwAuthExpire local user = ngx.var.cookie_SSOwAuthUser local authHash = ngx.var.cookie_SSOwAuthHash @@ -250,7 +252,8 @@ function is_logged_in() if hash ~= authHash then logger.info("Hash "..authHash.." rejected for "..user.."@"..ngx.var.remote_addr) end - return hash == authHash + is_logged_in = hash == authHash + return is_logged_in end end end @@ -268,34 +271,60 @@ function log_access(user, uri) end end +function get_best_permission() + if not conf["permissions"] then + conf["permissions"] = {} + end + + local permission_match = nil + local longest_url_match = "" + + for permission_name, permission in pairs(conf["permissions"]) do + if next(permission['uris']) ~= nil then + for _, url in pairs(permission['uris']) do + if string.starts(url, "re:") then + url = string.sub(url, 4, string.len(url)) + end + + local m = match(ngx.var.host..ngx.var.uri..uri_args_string(), url) + if m ~= nil and string.len(m) > string.len(longest_url_match) then + longest_url_match = m + permission_match = permission + logger.debug("Match "..m) + end + end + end + end + + return permission_match +end -- Check whether a user is allowed to access a URL using the `permissions` directive -- of the configuration file -function has_access(user) +function has_access(permission, user) user = user or authUser - logger.debug("User "..user.." try to access "..ngx.var.uri) - - -- Get the longest url permission - longest_permission_match = longest_url_path(permission_matches()) or "" - - logger.debug("Longest permission match : "..longest_permission_match) - - -- If no permission matches, it means that there is no permission defined for this url. - if longest_permission_match == "" then - logger.debug("No access rules defined for user "..user..", assuming it cannot access.") + if permission == nil then return false end + -- Public access + if user == nil or permission["public"] then + logger.debug("A visitor try to access "..ngx.var.uri) + return permission["public"] + end + + logger.debug("User "..user.." try to access "..ngx.var.uri) + -- All user in this permission - allowed_users = conf["permissions"][longest_permission_match] + allowed_users = permission["users"] -- The user has permission to access the content if he is in the list of this one if allowed_users then for _, u in pairs(allowed_users) do if u == user then - logger.debug("User "..user.." can access "..ngx.var.uri) - log_access(user, longest_permission_match) + logger.debug("User "..user.." can access "..ngx.var.host..ngx.var.uri..uri_args_string()) + log_access(user, ngx.var.host..ngx.var.uri..uri_args_string()) return true end end @@ -305,76 +334,6 @@ function has_access(user) return false end -function permission_matches() - if not conf["permissions"] then - conf["permissions"] = {} - end - - local url_matches = {} - - for url, permission in pairs(conf["permissions"]) do - if string.starts(ngx.var.host..ngx.var.uri..uri_args_string(), url) then - logger.debug("Url permission match current uri : "..url) - - table.insert(url_matches, url) - end - end - - return url_matches -end - -function get_matches(section) - if not conf[section.."_urls"] then - conf[section.."_urls"] = {} - end - if not conf[section.."_regex"] then - conf[section.."_regex"] = {} - end - - local url_matches = {} - - for _, url in ipairs(conf[section.."_urls"]) do - if string.starts(ngx.var.host..ngx.var.uri..uri_args_string(), url) - or string.starts(ngx.var.uri..uri_args_string(), url) then - logger.debug(section.."_url match current uri : "..url) - table.insert(url_matches, url) - end - end - for _, regex in ipairs(conf[section.."_regex"]) do - local m1 = match(ngx.var.host..ngx.var.uri..uri_args_string(), regex) - local m2 = match(ngx.var.uri..uri_args_string(), regex) - if m1 then - logger.debug(section.."_regex match current uri : "..regex.." with "..m1) - table.insert(url_matches, m1) - end - if m2 then - logger.debug(section.."_regex match current uri : "..regex.." with "..m2) - table.insert(url_matches, m2) - end - end - - return url_matches -end - -function longest_url_path(urls) - local longest = nil - for _, url in ipairs(urls) do - -- Turn the url into a path, e.g.: - -- /foo/bar -> /foo/bar - -- domain.tld/foo/bar -> /foo/bar - -- https://domain.tld:1234/foo/bar -> /foo/bar - current = url_parser.parse(url).path - if not longest or string.len(longest) < string.len(current) then - longest = current - end - end - if longest and string.ends(longest, "/") then - longest = string.sub(longest, 1, -2) - end - return longest -end - - -- Authenticate a user against the LDAP database using a username or an email -- address. -- Reminder: conf["ldap_identifier"] is "uid" by default @@ -557,7 +516,7 @@ function serve(uri, cache) -- Load login.html as index if rel_path == "/" then - if is_logged_in() then + if is_logged_in then rel_path = "/portal.html" else rel_path = "/login.html" @@ -658,45 +617,48 @@ function get_data_for(view) -- Needed if the LDAP db is changed outside ssowat (from the cli for example). -- Not doing it for ynhpanel.json only for performance reasons, -- so the panel could show wrong first name, last name or main email address - if view ~= "ynhpanel.json" then - delete_user_info_cache(user) - end + -- TODO: What if we remove info during logout? + --if view ~= "ynhpanel.json" then + -- delete_user_info_cache(user) + --end -- Be sure cache is loaded - set_headers(user) + if user then + set_headers(user) - local mails = get_mails(user) - data = { - connected = true, - theme = conf.theme, - portal_url = conf.portal_url, - uid = user, - cn = cache:get(user.."-cn"), - sn = cache:get(user.."-sn"), - givenName = cache:get(user.."-givenName"), - mail = mails["mail"], - mailalias = mails["mailalias"], - maildrop = mails["maildrop"], - app = {} - } + local mails = get_mails(user) + data = { + connected = true, + theme = conf.theme, + portal_url = conf.portal_url, + uid = user, + cn = cache:get(user.."-cn"), + sn = cache:get(user.."-sn"), + givenName = cache:get(user.."-givenName"), + mail = mails["mail"], + mailalias = mails["mailalias"], + maildrop = mails["maildrop"], + app = {} + } - local sorted_apps = {} + local sorted_apps = {} - -- Add user's accessible URLs using the ACLs. - -- It is typically used to build the app list. - for permission_name, permission in pairs(conf["permissions"]) do - -- We want to display a tile, and uris is not empty - if permission['show_tile'] and next(permission['uris']) ~= nil then - url = permission['uris'][1] - name = permission['label'] + -- Add user's accessible URLs using the ACLs. + -- It is typically used to build the app list. + for permission_name, permission in pairs(conf["permissions"]) do + -- We want to display a tile, and uris is not empty + if permission['show_tile'] and next(permission['uris']) ~= nil and has_access(permission, user) then + url = permission['uris'][1] + name = permission['label'] - if ngx.var.host == conf["local_portal_domain"] then - url = string.gsub(url, conf["original_portal_domain"], conf["local_portal_domain"]) + if ngx.var.host == conf["local_portal_domain"] then + url = string.gsub(url, conf["original_portal_domain"], conf["local_portal_domain"]) + end + + table.insert(sorted_apps, name) + table.sort(sorted_apps) + table.insert(data["app"], index_of(sorted_apps, name), { url = url, name = name }) end - - table.insert(sorted_apps, name) - table.sort(sorted_apps) - table.insert(data["app"], index_of(sorted_apps, name), { url = url, name = name }) end end end @@ -776,7 +738,7 @@ function edit_user() -- Ensure that user is logged in and has passed information -- before continuing. - if is_logged_in() and args + if is_logged_in and args then -- Set HTTP status to 201 @@ -1069,12 +1031,14 @@ function logout() local args = ngx.req.get_uri_args() -- Delete user cookie if logged in (that should always be the case) - if is_logged_in() then + if is_logged_in then delete_cookie() cache:delete("session_"..authUser) cache:delete(authUser.."-"..conf["ldap_identifier"]) -- Ugly trick to reload cache cache:delete(authUser.."-password") + delete_user_info_cache(authUser) flash("info", t("logged_out")) + is_logged_in = false end -- Redirect with the `r` URI argument if it exists or redirect to portal From 400f88e6cabcb5cac18a39d6373df36084f17ead Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 21 May 2020 21:51:55 +0200 Subject: [PATCH 03/15] fix the redirect loop \o/ --- access.lua | 20 ++++++++++++-------- config.lua | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/access.lua b/access.lua index e4e76a1..9595491 100644 --- a/access.lua +++ b/access.lua @@ -343,16 +343,20 @@ end -- The default is to protect every URL by default. -- --- Only display this if HTTPS. For HTTP, we can't know if the user really is --- logged in or not, because the cookie is available only in HTTP... -if ngx.var.scheme == "https" then - hlp.flash("info", hlp.t("please_login")) -end - -- Force the scheme to HTTPS. This is to avoid an issue with redirection loop -- when trying to access http://main.domain.tld/ (SSOwat finds that user aint -- logged in, therefore redirects to SSO, which redirects to the back_url, which -- redirect to SSO, ..) logger.debug("No rule found for "..ngx.var.uri..". By default, redirecting to portal") -local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. hlp.uri_args_string() -return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url)) +if is_logged_in then + return hlp.redirect(conf.portal_url) +else + -- Only display this if HTTPS. For HTTP, we can't know if the user really is + -- logged in or not, because the cookie is available only in HTTP... + if ngx.var.scheme == "https" then + hlp.flash("info", hlp.t("please_login")) + end + + local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. hlp.uri_args_string() + return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url)) +end diff --git a/config.lua b/config.lua index 1bf9151..e15ed38 100644 --- a/config.lua +++ b/config.lua @@ -82,7 +82,7 @@ function get_config() -- Always skip the portal to avoid redirection looping. - table.insert(conf["skipped_urls"], conf["portal_domain"]..conf["portal_path"]) + table.insert(conf["permissions"]["core_skipped"]["uris"], conf["portal_domain"]..conf["portal_path"]) -- Set the prefered language from the `Accept-Language` header From 24b3f7dc3a73a5d642e599ea9b671cc90f053270 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 21 May 2020 21:53:04 +0200 Subject: [PATCH 04/15] HTTP Auth before permissions managment --- access.lua | 71 +++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/access.lua b/access.lua index 9595491..3e829fe 100644 --- a/access.lua +++ b/access.lua @@ -239,7 +239,40 @@ if conf["redirected_regex"] then end -- --- 4. Specific files (used in YunoHost) +-- 4. Basic HTTP Authentication +-- +-- If the `Authorization` header is set before reaching the SSO, we want to +-- match user and password against the user database. +-- +-- It allows you to bypass the cookie-based procedure with a per-request +-- authentication. Very usefull when you are trying to reach a specific URL +-- via cURL for example. +-- + +local auth_header = ngx.req.get_headers()["Authorization"] + +if auth_header then + _, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$") + _, _, user, password = string.find(ngx.decode_base64(b64_cred), "^(.+):(.+)$") + user = hlp.authenticate(user, password) + if user then + logger.debug("User got authenticated through basic auth") + + -- If user has no access to this URL, redirect him to the portal + if not permission or not hlp.has_access(permission, user) then + return hlp.redirect(conf.portal_url) + end + + if permission["auth_header"] then + logger.debug("Set Headers") + hlp.set_headers(user) + end + return hlp.pass() + end +end + +-- +-- 5. Specific files (used in YunoHost) -- -- We want to serve specific portal assets right at the root of the domain. -- @@ -303,41 +336,7 @@ if permission then end -- --- 7. Basic HTTP Authentication --- --- If the `Authorization` header is set before reaching the SSO, we want to --- match user and password against the user database. --- --- It allows you to bypass the cookie-based procedure with a per-request --- authentication. Very usefull when you are trying to reach a specific URL --- via cURL for example. --- - -local auth_header = ngx.req.get_headers()["Authorization"] - -if auth_header then - _, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$") - _, _, user, password = string.find(ngx.decode_base64(b64_cred), "^(.+):(.+)$") - user = hlp.authenticate(user, password) - if user then - logger.debug("User got authenticated through basic auth") - - -- If user has no access to this URL, redirect him to the portal - if not permission or not hlp.has_access(permission, user) then - return hlp.redirect(conf.portal_url) - end - - if permission["auth_header"] then - logger.debug("Set Headers") - hlp.set_headers(user) - end - return hlp.pass() - end -end - - --- --- 8. Redirect to login +-- 6. Redirect to login -- -- If no previous rule has matched, just redirect to the portal login. -- The default is to protect every URL by default. From 720e35df4ec37516ab183b1d18469ddf8f930de1 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 21 May 2020 22:56:52 +0200 Subject: [PATCH 05/15] do not reauth if we are already logged in --- access.lua | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/access.lua b/access.lua index 3e829fe..649c9e3 100644 --- a/access.lua +++ b/access.lua @@ -249,25 +249,27 @@ end -- via cURL for example. -- -local auth_header = ngx.req.get_headers()["Authorization"] +if not is_logged_in then + local auth_header = ngx.req.get_headers()["Authorization"] -if auth_header then - _, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$") - _, _, user, password = string.find(ngx.decode_base64(b64_cred), "^(.+):(.+)$") - user = hlp.authenticate(user, password) - if user then - logger.debug("User got authenticated through basic auth") + if auth_header then + _, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$") + _, _, user, password = string.find(ngx.decode_base64(b64_cred), "^(.+):(.+)$") + user = hlp.authenticate(user, password) + if user then + logger.debug("User got authenticated through basic auth") - -- If user has no access to this URL, redirect him to the portal - if not permission or not hlp.has_access(permission, user) then - return hlp.redirect(conf.portal_url) + -- If user has no access to this URL, redirect him to the portal + if not permission or not hlp.has_access(permission, user) then + return hlp.redirect(conf.portal_url) + end + + if permission["auth_header"] then + logger.debug("Set Headers") + hlp.set_headers(user) + end + return hlp.pass() end - - if permission["auth_header"] then - logger.debug("Set Headers") - hlp.set_headers(user) - end - return hlp.pass() end end From 6a240e1deac06e3110f07f0c62173d9b640e3a33 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 21 May 2020 22:57:05 +0200 Subject: [PATCH 06/15] better log message --- helpers.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers.lua b/helpers.lua index 6fa9cd9..53f45e8 100644 --- a/helpers.lua +++ b/helpers.lua @@ -310,11 +310,12 @@ function has_access(permission, user) -- Public access if user == nil or permission["public"] then - logger.debug("A visitor try to access "..ngx.var.uri) + user = user or "A visitor" + logger.debug(user.." tries to access "..ngx.var.uri) return permission["public"] end - logger.debug("User "..user.." try to access "..ngx.var.uri) + logger.debug("User "..user.." tries to access "..ngx.var.uri) -- All user in this permission allowed_users = permission["users"] From 397f7b3910a9eae42fffec131e5c884d0300fe91 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 21 May 2020 22:57:57 +0200 Subject: [PATCH 07/15] authUser is defined only if authHash is accepted --- helpers.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers.lua b/helpers.lua index 53f45e8..8d9fada 100644 --- a/helpers.lua +++ b/helpers.lua @@ -244,13 +244,14 @@ function refresh_logged_in() if session_key and session_key ~= "" then -- Check cache if cache:get(user.."-password") then - authUser = user local hash = hmac_sha512(srvkey, - authUser.. + user.. "|"..expireTime.. "|"..session_key) if hash ~= authHash then logger.info("Hash "..authHash.." rejected for "..user.."@"..ngx.var.remote_addr) + else + authUser = user end is_logged_in = hash == authHash return is_logged_in From fb45cd0441c21d3f79628ec579183a68fd8afa82 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 18 Jun 2020 14:48:14 +0200 Subject: [PATCH 08/15] do not compare the same thing several times --- helpers.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers.lua b/helpers.lua index 8d9fada..6736d3a 100644 --- a/helpers.lua +++ b/helpers.lua @@ -248,12 +248,12 @@ function refresh_logged_in() user.. "|"..expireTime.. "|"..session_key) - if hash ~= authHash then + is_logged_in = hash == authHash + if not is_logged_in then logger.info("Hash "..authHash.." rejected for "..user.."@"..ngx.var.remote_addr) else authUser = user end - is_logged_in = hash == authHash return is_logged_in end end From b2b9b9c8e36ef782a4547a6205860d528ef679ab Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 20 Sep 2020 17:47:24 +0200 Subject: [PATCH 09/15] Refactor/move handling of portal assets --- access.lua | 62 +++++++++++++++++++++++------------------------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/access.lua b/access.lua index a14c4ad..725d224 100644 --- a/access.lua +++ b/access.lua @@ -198,6 +198,32 @@ then end end +-- +-- 2 ... continued : portal assets that are available on every domains +-- +-- For example: `https://whatever.org/ynhpanel.js` will serve the +-- `/yunohost/sso/assets/js/ynhpanel.js` file. +-- + +if is_logged_in then + assets = {} + assets["/ynh_portal.js"] = "js/ynh_portal.js" + assets["/ynh_overlay.css"] = "css/ynh_overlay.css" + theme_dir = "/usr/share/ssowat/portal/assets/themes/"..conf.theme + local pfile = io.popen('find "'..theme_dir..'" -type f -exec realpath --relative-to "'..theme_dir..'" {} \\;') + for filename in pfile:lines() do + assets["/ynhtheme/"..filename] = "themes/"..conf.theme.."/"..filename + end + pfile:close() + + for shortcut, full in pairs(assets) do + if string.match(ngx.var.uri, "^"..shortcut.."$") then + logger.debug("Serving static asset "..full) + return hlp.serve("/yunohost/sso/assets/"..full, "static_asset") + end + end +end + -- -- 3. Redirected URLs @@ -274,46 +300,10 @@ if not is_logged_in then end -- --- 5. Specific files (used in YunoHost) -- --- We want to serve specific portal assets right at the root of the domain. -- --- For example: `https://mydomain.org/ynhpanel.js` will serve the --- `/yunohost/sso/assets/js/ynhpanel.js` file. -- -function scandir(directory, callback) - -- use find (and not ls) to list only files recursively and with their full path relative to the asked directory - local pfile = io.popen('find "'..directory..'" -type f -exec realpath --relative-to "'..directory..'" {} \\;') - for filename in pfile:lines() do - callback(filename) - end - pfile:close() -end - -function serveAsset(shortcut, full) - if string.match(ngx.var.uri, "^"..shortcut.."$") then - logger.debug("Serving static asset "..full) - hlp.serve("/yunohost/sso/assets/"..full, "static_asset") - end -end - -function serveThemeFile(filename) - serveAsset("/ynhtheme/"..filename, "themes/"..conf.theme.."/"..filename) -end - -function serveYnhpanel() - logger.debug("Serving ynhpanel") - - -- serve ynhpanel files - serveAsset("/ynh_portal.js", "js/ynh_portal.js") - serveAsset("/ynh_overlay.css", "css/ynh_overlay.css") - -- serve theme's files - -- FIXME? I think it would be better here not to use an absolute path - -- but I didn't succeed to figure out where is the current location of the script - -- if you call it from "portal/assets/themes/" the ls fails - scandir("/usr/share/ssowat/portal/assets/themes/"..conf.theme, serveThemeFile) -end local permission = hlp.get_best_permission() From abc38bbffe03286537ab87febbd614cee9e60ab8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 20 Sep 2020 17:53:18 +0200 Subject: [PATCH 10/15] Move handling of login through HTTP headers to is_logged_in helper --- access.lua | 27 --------------------------- helpers.lua | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/access.lua b/access.lua index 725d224..825e678 100644 --- a/access.lua +++ b/access.lua @@ -265,39 +265,12 @@ if conf["redirected_regex"] then end -- --- 4. Basic HTTP Authentication -- --- If the `Authorization` header is set before reaching the SSO, we want to --- match user and password against the user database. -- --- It allows you to bypass the cookie-based procedure with a per-request --- authentication. Very usefull when you are trying to reach a specific URL --- via cURL for example. -- -if not is_logged_in then - local auth_header = ngx.req.get_headers()["Authorization"] - if auth_header then - _, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$") - _, _, user, password = string.find(ngx.decode_base64(b64_cred), "^(.+):(.+)$") - user = hlp.authenticate(user, password) - if user then - logger.debug("User got authenticated through basic auth") - -- If user has no access to this URL, redirect him to the portal - if not permission or not hlp.has_access(permission, user) then - return hlp.redirect(conf.portal_url) - end - - if permission["auth_header"] then - logger.debug("Set Headers") - hlp.set_headers(user) - end - return hlp.pass() - end - end -end -- -- diff --git a/helpers.lua b/helpers.lua index 937584b..6aca31d 100644 --- a/helpers.lua +++ b/helpers.lua @@ -260,6 +260,30 @@ function refresh_logged_in() end end + -- If client set the `Authorization` header before reaching the SSO, + -- we want to match user and password against the user database. + -- + -- It allows to bypass the cookie-based procedure with a per-request + -- authentication. This is useful to authenticate on the SSO during + -- curl requests for example. + + local auth_header = ngx.req.get_headers()["Authorization"] + + if auth_header then + _, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$") + _, _, user, password = string.find(ngx.decode_base64(b64_cred), "^(.+):(.+)$") + user = authenticate(user, password) + if user then + logger.debug("User got authenticated through basic auth") + authUser = user + is_logged_in = true + else + is_logged_in = false + end + return is_logged_in + end + + is_logged_in = false return false end From a11d8f0d87c2b8d1cd0cee31a43f98a5e339373d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 20 Sep 2020 17:57:23 +0200 Subject: [PATCH 11/15] Move identification of relevant permission from helpers.lua to access.lua --- access.lua | 35 ++++++++++++++++++++++++++++++++++- config.lua | 3 ++- helpers.lua | 28 ---------------------------- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/access.lua b/access.lua index 825e678..9688b25 100644 --- a/access.lua +++ b/access.lua @@ -265,12 +265,46 @@ if conf["redirected_regex"] then end -- +-- 4. IDENTIFY THE RELEVANT PERMISSION +-- +-- In particular, the conf is filled with permissions such as: +-- +-- "foobar": { +-- "auth_header": false, +-- "label": "Foobar permission", +-- "public": false, +-- "show_tile": true, +-- "uris": [ +-- "yolo.test/foobar", +-- "re:^[^/]*/%.well%-known/foobar/.*$", +-- ], +-- "users": ["alice", "bob"] +-- } -- -- +-- And we find the best matching permission by trying to match the request uri +-- against all the uris rules/regexes from the conf and keep the longest matching one. -- +permission = nil +longest_url_match = "" +for permission_name, permission_infos in pairs(conf["permissions"]) do + if next(permission_infos['uris']) ~= nil then + for _, url in pairs(permission_infos['uris']) do + if string.starts(url, "re:") then + url = string.sub(url, 4, string.len(url)) + end + local m = hlp.match(ngx.var.host..ngx.var.uri..hlp.uri_args_string(), url) + if m ~= nil and string.len(m) > string.len(longest_url_match) then + longest_url_match = m + permission = permission_infos + permission["id"] = permission_name + end + end + end +end -- -- @@ -278,7 +312,6 @@ end -- -local permission = hlp.get_best_permission() if permission then if is_logged_in then diff --git a/config.lua b/config.lua index 9d2395d..029ffe7 100644 --- a/config.lua +++ b/config.lua @@ -60,7 +60,8 @@ function get_config() allow_mail_authentication = true, default_language = "en", theme = "default", - logging = "fatal" -- Only log fatal messages by default (so apriori nothing) + logging = "fatal", -- Only log fatal messages by default (so apriori nothing) + permissions = {} } diff --git a/helpers.lua b/helpers.lua index 6aca31d..74e06d6 100644 --- a/helpers.lua +++ b/helpers.lua @@ -296,34 +296,6 @@ function log_access(user, uri) end end -function get_best_permission() - if not conf["permissions"] then - conf["permissions"] = {} - end - - local permission_match = nil - local longest_url_match = "" - - for permission_name, permission in pairs(conf["permissions"]) do - if next(permission['uris']) ~= nil then - for _, url in pairs(permission['uris']) do - if string.starts(url, "re:") then - url = string.sub(url, 4, string.len(url)) - end - - local m = match(ngx.var.host..ngx.var.uri..uri_args_string(), url) - if m ~= nil and string.len(m) > string.len(longest_url_match) then - longest_url_match = m - permission_match = permission - logger.debug("Match "..m) - end - end - end - end - - return permission_match -end - -- Check whether a user is allowed to access a URL using the `permissions` directive -- of the configuration file function has_access(permission, user) From dcbf66d4e4f3145726f4914f745b11654af74972 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 20 Sep 2020 18:00:37 +0200 Subject: [PATCH 12/15] Rework/simplify code that effectively apply the permission --- access.lua | 56 +++++++++++++++++++++--------------------------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/access.lua b/access.lua index 9688b25..4b093b9 100644 --- a/access.lua +++ b/access.lua @@ -308,52 +308,40 @@ end -- -- +-- 5. APPLY PERMISSION -- -- +-- 1st case : client has access +if hlp.has_access(permission) then -if permission then if is_logged_in then - serveYnhpanel() + -- If the user is logged in, we set some additional headers + hlp.set_headers() - -- If the user is authenticated and has access to the URL, set the headers - -- and let it be - if permission["auth_header"] and hlp.has_access(permission) then - logger.debug("Set Headers") - hlp.set_headers() + -- If Basic Authorization header are disabled for this permission, + -- remove them from the response + if not permission["auth_header"] then + ngx.req.clear_header("Authorization") end end - -- If user has no access to this URL, redirect him to the portal - if not hlp.has_access(permission) then - return hlp.redirect(conf.portal_url) - end - return hlp.pass() -end --- --- 6. Redirect to login --- --- If no previous rule has matched, just redirect to the portal login. --- The default is to protect every URL by default. --- - --- Force the scheme to HTTPS. This is to avoid an issue with redirection loop --- when trying to access http://main.domain.tld/ (SSOwat finds that user aint --- logged in, therefore redirects to SSO, which redirects to the back_url, which --- redirect to SSO, ..) -logger.debug("No rule found for "..ngx.var.uri..". By default, redirecting to portal") -if is_logged_in then - return hlp.redirect(conf.portal_url) +-- 2nd case : no access ... redirect to portal / login form else - -- Only display this if HTTPS. For HTTP, we can't know if the user really is - -- logged in or not, because the cookie is available only in HTTP... - if ngx.var.scheme == "https" then - hlp.flash("info", hlp.t("please_login")) - end - local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. hlp.uri_args_string() - return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url)) + if is_logged_in then + return hlp.redirect(conf.portal_url) + else + -- Only display this if HTTPS. For HTTP, we can't know if the user really is + -- logged in or not, because the cookie is available only in HTTP... + if ngx.var.scheme == "https" then + hlp.flash("info", hlp.t("please_login")) + end + + local back_url = "https://" .. ngx.var.host .. ngx.var.uri .. hlp.uri_args_string() + return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url)) + end end From 41ed91bbcb93e7d7b3fe5996748c54fc2024fb3a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 20 Sep 2020 18:00:49 +0200 Subject: [PATCH 13/15] Misc cosmetics / debug tweaks --- access.lua | 3 ++- helpers.lua | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/access.lua b/access.lua index 4b093b9..f25cac2 100644 --- a/access.lua +++ b/access.lua @@ -226,11 +226,12 @@ end -- --- 3. Redirected URLs +-- 3. REDIRECTED URLS -- -- If the URL matches one of the `redirected_urls` in the configuration file, -- just redirect to the target URL/URI -- + function detect_redirection(redirect_url) if hlp.string.starts(redirect_url, "http://") or hlp.string.starts(redirect_url, "https://") then diff --git a/helpers.lua b/helpers.lua index 74e06d6..068d6f4 100644 --- a/helpers.lua +++ b/helpers.lua @@ -232,7 +232,7 @@ function refresh_logged_in() local authHash = ngx.var.cookie_SSOwAuthHash authUser = nil - + if expireTime and expireTime ~= "" and authHash and authHash ~= "" and user and user ~= "" @@ -302,17 +302,18 @@ function has_access(permission, user) user = user or authUser if permission == nil then + logger.debug("No permission matching request for "..ngx.var.uri) return false end -- Public access if user == nil or permission["public"] then user = user or "A visitor" - logger.debug(user.." tries to access "..ngx.var.uri) + logger.debug(user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")") return permission["public"] end - logger.debug("User "..user.." tries to access "..ngx.var.uri) + logger.debug("User "..user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")") -- All user in this permission allowed_users = permission["users"] @@ -508,7 +509,7 @@ end -- It is used to render the SSOwat portal *only*. function serve(uri, cache) - logger.debug("Serving portal uri "..uri.." (if the corresponding file exists)") + logger.debug("Serving portal uri "..uri) rel_path = string.gsub(uri, conf["portal_path"], "/") From 06f1f3022679267372e9188b49e66a988c95e273 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Sep 2020 14:40:37 +0200 Subject: [PATCH 14/15] Update access.lua Co-authored-by: Kayou --- access.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/access.lua b/access.lua index f25cac2..b655b72 100644 --- a/access.lua +++ b/access.lua @@ -206,9 +206,10 @@ end -- if is_logged_in then - assets = {} - assets["/ynh_portal.js"] = "js/ynh_portal.js" - assets["/ynh_overlay.css"] = "css/ynh_overlay.css" + assets = { + ["/ynh_portal.js"] = "js/ynh_portal.js", + ["/ynh_overlay.css"] = "css/ynh_overlay.css" + } theme_dir = "/usr/share/ssowat/portal/assets/themes/"..conf.theme local pfile = io.popen('find "'..theme_dir..'" -type f -exec realpath --relative-to "'..theme_dir..'" {} \\;') for filename in pfile:lines() do From ed6fa1aa499507f52f62461d3fa1e3dffd54715f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Sep 2020 14:41:23 +0200 Subject: [PATCH 15/15] Add a small helper to check if an element is in a table ... in turn fixing a bug related to calling has_access --- helpers.lua | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/helpers.lua b/helpers.lua index 068d6f4..c70463e 100644 --- a/helpers.lua +++ b/helpers.lua @@ -315,21 +315,26 @@ function has_access(permission, user) logger.debug("User "..user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")") - -- All user in this permission - allowed_users = permission["users"] + -- The user has permission to access the content if he is in the list of allowed users + if element_is_in_table(user, permission["users"]) then + logger.debug("User "..user.." can access "..ngx.var.host..ngx.var.uri..uri_args_string()) + log_access(user, ngx.var.host..ngx.var.uri..uri_args_string()) + return true + else + logger.debug("User "..user.." cannot access "..ngx.var.uri) + return false + end +end - -- The user has permission to access the content if he is in the list of this one - if allowed_users then - for _, u in pairs(allowed_users) do - if u == user then - logger.debug("User "..user.." can access "..ngx.var.host..ngx.var.uri..uri_args_string()) - log_access(user, ngx.var.host..ngx.var.uri..uri_args_string()) +function element_is_in_table(element, table) + if table then + for _, el in pairs(table) do + if el == element then return true end end end - logger.debug("User "..user.." cannot access "..ngx.var.uri) return false end @@ -646,7 +651,7 @@ function get_data_for(view) -- It is typically used to build the app list. for permission_name, permission in pairs(conf["permissions"]) do -- We want to display a tile, and uris is not empty - if permission['show_tile'] and next(permission['uris']) ~= nil and has_access(permission, user) then + if permission['show_tile'] and next(permission['uris']) ~= nil and element_is_in_table(user, permission["users"]) then url = permission['uris'][1] name = permission['label']