Merge pull request #172 from YunoHost/moar_refactoring

Moar refactoring (on top of permission rework)
This commit is contained in:
Alexandre Aubin 2020-09-24 20:22:32 +02:00 committed by GitHub
commit c97372ee97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 126 additions and 137 deletions

View file

@ -198,13 +198,41 @@ then
end end
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 = {
["/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
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 -- 3. REDIRECTED URLS
-- --
-- If the URL matches one of the `redirected_urls` in the configuration file, -- If the URL matches one of the `redirected_urls` in the configuration file,
-- just redirect to the target URL/URI -- just redirect to the target URL/URI
-- --
function detect_redirection(redirect_url) function detect_redirection(redirect_url)
if hlp.string.starts(redirect_url, "http://") if hlp.string.starts(redirect_url, "http://")
or hlp.string.starts(redirect_url, "https://") then or hlp.string.starts(redirect_url, "https://") then
@ -239,125 +267,83 @@ if conf["redirected_regex"] then
end end
-- --
-- 4. Basic HTTP Authentication -- 4. IDENTIFY THE RELEVANT PERMISSION
-- --
-- If the `Authorization` header is set before reaching the SSO, we want to -- In particular, the conf is filled with permissions such as:
-- match user and password against the user database.
-- --
-- It allows you to bypass the cookie-based procedure with a per-request -- "foobar": {
-- authentication. Very usefull when you are trying to reach a specific URL -- "auth_header": false,
-- via cURL for example. -- "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.
-- --
if not is_logged_in then permission = nil
local auth_header = ngx.req.get_headers()["Authorization"] longest_url_match = ""
if auth_header then for permission_name, permission_infos in pairs(conf["permissions"]) do
_, _, b64_cred = string.find(auth_header, "^Basic%s+(.+)$") if next(permission_infos['uris']) ~= nil then
_, _, user, password = string.find(ngx.decode_base64(b64_cred), "^(.+):(.+)$") for _, url in pairs(permission_infos['uris']) do
user = hlp.authenticate(user, password) if string.starts(url, "re:") then
if user then url = string.sub(url, 4, string.len(url))
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 end
if permission["auth_header"] then local m = hlp.match(ngx.var.host..ngx.var.uri..hlp.uri_args_string(), url)
logger.debug("Set Headers") if m ~= nil and string.len(m) > string.len(longest_url_match) then
hlp.set_headers(user) longest_url_match = m
permission = permission_infos
permission["id"] = permission_name
end end
return hlp.pass()
end end
end end
end end
-- --
-- 5. Specific files (used in YunoHost)
-- --
-- We want to serve specific portal assets right at the root of the domain. -- 5. APPLY PERMISSION
-- --
-- For example: `https://mydomain.org/ynhpanel.js` will serve the
-- `/yunohost/sso/assets/js/ynhpanel.js` file.
-- --
function scandir(directory, callback) -- 1st case : client has access
-- 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 hlp.has_access(permission) then
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()
if permission then
if is_logged_in 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 -- If Basic Authorization header are disabled for this permission,
-- and let it be -- remove them from the response
if permission["auth_header"] and hlp.has_access(permission) then if not permission["auth_header"] then
logger.debug("Set Headers") ngx.req.clear_header("Authorization")
hlp.set_headers()
end end
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() return hlp.pass()
end
-- -- 2nd case : no access ... redirect to portal / login form
-- 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)
else 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() if is_logged_in then
return hlp.redirect(conf.portal_url.."?r="..ngx.encode_base64(back_url)) 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 end

View file

@ -60,7 +60,8 @@ function get_config()
allow_mail_authentication = true, allow_mail_authentication = true,
default_language = "en", default_language = "en",
theme = "default", 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 = {}
} }

View file

@ -232,7 +232,7 @@ function refresh_logged_in()
local authHash = ngx.var.cookie_SSOwAuthHash local authHash = ngx.var.cookie_SSOwAuthHash
authUser = nil authUser = nil
if expireTime and expireTime ~= "" if expireTime and expireTime ~= ""
and authHash and authHash ~= "" and authHash and authHash ~= ""
and user and user ~= "" and user and user ~= ""
@ -260,6 +260,30 @@ function refresh_logged_in()
end end
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 return false
end end
@ -272,67 +296,45 @@ function log_access(user, uri)
end end
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 -- Check whether a user is allowed to access a URL using the `permissions` directive
-- of the configuration file -- of the configuration file
function has_access(permission, user) function has_access(permission, user)
user = user or authUser user = user or authUser
if permission == nil then if permission == nil then
logger.debug("No permission matching request for "..ngx.var.uri)
return false return false
end end
-- Public access -- Public access
if user == nil or permission["public"] then if user == nil or permission["public"] then
user = user or "A visitor" 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"] return permission["public"]
end 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 -- The user has permission to access the content if he is in the list of allowed users
allowed_users = permission["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 function element_is_in_table(element, table)
if allowed_users then if table then
for _, u in pairs(allowed_users) do for _, el in pairs(table) do
if u == user then if el == element 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 return true
end end
end end
end end
logger.debug("User "..user.." cannot access "..ngx.var.uri)
return false return false
end end
@ -512,7 +514,7 @@ end
-- It is used to render the SSOwat portal *only*. -- It is used to render the SSOwat portal *only*.
function serve(uri, cache) 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"], "/") rel_path = string.gsub(uri, conf["portal_path"], "/")
@ -649,7 +651,7 @@ function get_data_for(view)
-- It is typically used to build the app list. -- It is typically used to build the app list.
for permission_name, permission in pairs(conf["permissions"]) do for permission_name, permission in pairs(conf["permissions"]) do
-- We want to display a tile, and uris is not empty -- 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] url = permission['uris'][1]
name = permission['label'] name = permission['label']