Software /
code /
prosody-modules
File
mod_audit/mod_audit.lua @ 5738:8488ebde5739
mod_http_oauth2: Skip consent screen if requested by client and same scopes already granted
This follows the intent behind the OpenID Connect 'prompt' parameter
when it does not include the 'consent' keyword, that is the client
wishes to skip the consent screen. If the user has already granted the
exact same scopes to the exact same client in the past, then one can
assume that they may grant it again.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Tue, 14 Nov 2023 23:03:37 +0100 |
parent | 5737:c77010f25b14 |
child | 5754:1bdc6b5979ee |
line wrap: on
line source
module:set_global(); local time_now = os.time; local parse_duration = require "util.human.io".parse_duration; local ip = require "util.ip"; local st = require "util.stanza"; local moduleapi = require "core.moduleapi"; local host_wide_user = "@"; local cleanup_after = module:get_option_string("audit_log_expires_after", "28d"); if cleanup_after == "never" then cleanup_after = nil; else cleanup_after = parse_duration(cleanup_after); end local attach_ips = module:get_option_boolean("audit_log_ips", true); local attach_ipv4_prefix = module:get_option_number("audit_log_ipv4_prefix", nil); local attach_ipv6_prefix = module:get_option_number("audit_log_ipv6_prefix", nil); local have_geoip, geoip = pcall(require, "geoip.country"); local attach_location = have_geoip and module:get_option_boolean("audit_log_location", true); local geoip4_country, geoip6_country; if have_geoip and attach_location then geoip4_country = geoip.open(module:get_option_string("geoip_ipv4_country", "/usr/share/GeoIP/GeoIP.dat")); geoip6_country = geoip.open(module:get_option_string("geoip_ipv6_country", "/usr/share/GeoIP/GeoIPv6.dat")); end local stores = {}; local function get_store(self, host) local store = rawget(self, host); if store then return store end store = module:context(host):open_store("audit", "archive"); rawset(self, host, store); return store; end setmetatable(stores, { __index = get_store }); local function prune_audit_log(host) local before = os.time() - cleanup_after; module:context(host):log("debug", "Pruning audit log for entries older than %s", os.date("%Y-%m-%d %R:%S", before)); local ok, err = stores[host]:delete(nil, { ["end"] = before }); if not ok then module:context(host):log("error", "Unable to prune audit log: %s", err); return; end local sum = tonumber(ok); if sum then module:context(host):log("debug", "Pruned %d expired audit log entries", sum); return sum > 0; end module:context(host):log("debug", "Pruned expired audit log entries"); return true; end local function get_ip_network(ip_addr) local proto = ip_addr.proto; local network; if proto == "IPv4" and attach_ipv4_prefix then network = ip.truncate(ip_addr, attach_ipv4_prefix).normal.."/"..attach_ipv4_prefix; elseif proto == "IPv6" and attach_ipv6_prefix then network = ip.truncate(ip_addr, attach_ipv6_prefix).normal.."/"..attach_ipv6_prefix; end return network; end local function session_extra(session) local attr = { xmlns = "xmpp:prosody.im/audit", }; if session.id then attr.id = session.id; end if session.type then attr.type = session.type; end local stanza = st.stanza("session", attr); local remote_ip = session.ip and ip.new_ip(session.ip); if attach_ips and remote_ip then local network; if attach_ipv4_prefix or attach_ipv6_prefix then network = get_ip_network(remote_ip); end stanza:text_tag("remote-ip", network or remote_ip.normal); end if attach_location and remote_ip then local geoip_info = remote_ip.proto == "IPv6" and geoip6_country:query_by_addr6(remote_ip.normal) or geoip4_country:query_by_addr(remote_ip.normal); stanza:text_tag("location", geoip_info.name, { country = geoip_info.code; continent = geoip_info.continent; }):up(); end if session.client_id then stanza:text_tag("client", session.client_id); end return stanza end local function audit(host, user, source, event_type, extra) if not host or host == "*" then error("cannot log audit events for global"); end local user_key = user or host_wide_user; local attr = { ["source"] = source, ["type"] = event_type, }; if user_key ~= host_wide_user then attr.user = user_key; end local stanza = st.stanza("audit-event", attr); if extra then if extra.session then local child = session_extra(extra.session); if child then stanza:add_child(child); end end if extra.custom then for _, child in ipairs(extra.custom) do if not st.is_stanza(child) then error("all extra.custom items must be stanzas") end stanza:add_child(child); end end end local store = stores[host]; local id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key); if not id then if err == "quota-limit" then local limit = store.caps and store.caps.quota or 1000; local truncate_to = math.floor(limit * 0.99); if type(cleanup_after) == "number" then module:log("debug", "Audit log has reached quota - forcing prune"); if prune_audit_log(host) then -- Retry append id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key); end end if not id and (store.caps and store.caps.truncate) then module:log("debug", "Audit log has reached quota - truncating"); local truncated = store:delete(nil, { truncate = truncate_to; }); if truncated then -- Retry append id, err = store:append(nil, nil, stanza, extra and extra.timestamp or time_now(), user_key); end end end if not id then module:log("error", "Failed to persist audit event: %s", err); return; end else module:log("debug", "Persisted audit event %s as %s", stanza:top_tag(), id); end end function moduleapi.audit(module, user, event_type, extra) audit(module.host, user, "mod_" .. module:get_name(), event_type, extra); end function module.command(arg_) local jid = require "util.jid"; local arg = require "util.argparse".parse(arg_, { value_params = { "limit" }; }); module:log("debug", "arg = %q", arg); local query_user, host = jid.prepped_split(arg[1]); if arg.prune then local sm = require "core.storagemanager"; if host then sm.initialize_host(host); prune_audit_log(host); else for _host in pairs(prosody.hosts) do sm.initialize_host(_host); prune_audit_log(_host); end end return; end if not host then print("EE: Please supply the host for which you want to show events"); return 1; elseif not prosody.hosts[host] then print("EE: Unknown host: "..host); return 1; end require "core.storagemanager".initialize_host(host); local store = stores[host]; local c = 0; if arg.global then if query_user then print("WW: Specifying a user account is incompatible with --global. Showing only global events."); end query_user = "@"; end local results, err = store:find(nil, { with = query_user; limit = arg.limit and tonumber(arg.limit) or nil; reverse = true; }) if not results then print("EE: Failed to query audit log: "..tostring(err)); return 1; end local colspec = { { title = "Date", key = "when", width = 19, mapper = function (when) return os.date("%Y-%m-%d %R:%S", when); end }; { title = "Source", key = "source", width = "2p" }; { title = "Event", key = "event_type", width = "2p" }; }; if arg.show_user ~= false and (not arg.global and not query_user) or arg.show_user then table.insert(colspec, { title = "User", key = "username", width = "2p", mapper = function (user) if user == "@" then return ""; end if user:sub(-#host-1, -1) == ("@"..host) then return (user:gsub("@.+$", "")); end end; }); end if arg.show_ip ~= false and (not arg.global and attach_ips) or arg.show_ip then table.insert(colspec, { title = "IP", key = "ip", width = "2p"; }); end if arg.show_location ~= false and (not arg.global and attach_location) or arg.show_location then table.insert(colspec, { title = "Location", key = "country", width = 2; }); end if arg.show_note then table.insert(colspec, { title = "Note", key = "note", width = "2p"; }); end local row, width = require "util.human.io".table(colspec); print(string.rep("-", width)); print(row()); print(string.rep("-", width)); for _, entry, when, user in results do if arg.global ~= false or user ~= "@" then c = c + 1; print(row({ when = when; source = entry.attr.source; event_type = entry.attr.type:gsub("%-", " "); username = user; ip = entry:find("{xmpp:prosody.im/audit}session/remote-ip#"); country = entry:find("{xmpp:prosody.im/audit}session/location@country"); note = entry:get_child_text("note"); })); end end print(string.rep("-", width)); print(("%d records displayed"):format(c)); end function module.add_host(host_module) host_module:depends("cron"); host_module:daily("Prune audit logs", function () prune_audit_log(host_module.host); end); end