# HG changeset patch # User Matthew Wild # Date 1680353813 -3600 # Node ID 8ef197cccd74902953d54cc4026c8fe9fc224a89 # Parent fa97de0b0961e3ff8ae2638cf18605b4839451da mod_client_management: Add XMPP and shell interfaces to fetch client list diff -r fa97de0b0961 -r 8ef197cccd74 mod_client_management/mod_client_management.lua --- a/mod_client_management/mod_client_management.lua Sat Apr 01 13:56:15 2023 +0100 +++ b/mod_client_management/mod_client_management.lua Sat Apr 01 13:56:53 2023 +0100 @@ -1,7 +1,10 @@ local modulemanager = require "core.modulemanager"; local usermanager = require "core.usermanager"; +local array = require "util.array"; +local dt = require "util.datetime"; local id = require "util.id"; +local it = require "util.iterators"; local jid = require "util.jid"; local st = require "util.stanza"; @@ -187,26 +190,26 @@ if client.auth_token_id then local grant = tokenauth.get_grant_info(client.auth_token_id); if grant then - status.active_grant = grant; + status.grant = grant; end end -- Check for active FAST tokens if client.fast_auth then if mod_fast.is_client_fast(username, client.id, last_password_change) then - status.active_fast = client.fast_auth; + status.fast = client.fast_auth; end end -- Client has access if any password-based SASL mechanisms have been used since last password change for mech, mech_last_used in pairs(client.mechanisms) do if is_password_mechanism(mech) and mech_last_used >= last_password_change then - status.active_password = mech_last_used; + status.password = mech_last_used; end end if prosody.full_sessions[client.full_jid] then - status.active_connected = true; + status.connected = true; end if next(status) == nil then @@ -229,8 +232,8 @@ client.type = "session"; client.active = active; table.insert(active_clients, client); - if active.active_grant then - used_grants[active.active_grant.id] = true; + if active.grant then + used_grants[active.grant.id] = true; end end end @@ -244,7 +247,7 @@ first_seen = grant.created; last_seen = grant.accessed; active = { - active_grant = grant; + grant = grant; }; user_agent = get_user_agent(nil, grant); }); @@ -268,3 +271,93 @@ return active_clients; end + +-- Protocol + +local xmlns_manage_clients = "xmpp:prosody.im/protocol/manage-clients"; + +module:hook("iq-get/self/xmpp:prosody.im/protocol/manage-clients:list", function (event) + local origin, stanza = event.origin, event.stanza; + + if not module:may(":list-clients", event) then + origin.send(st.error_reply(stanza, "auth", "forbidden")); + return true; + end + + local reply = st.reply(stanza) + :tag("clients", { xmlns = xmlns_manage_clients }); + + local active_clients = get_active_clients(event.origin.username); + for _, client in ipairs(active_clients) do + local auth_type = st.stanza("auth"); + if client.active then + if client.active.password then + auth_type:text_tag("password"); + end + if client.active.grant then + auth_type:text_tag("bearer-token"); + end + if client.active.fast then + auth_type:text_tag("fast"); + end + end + + local user_agent = st.stanza("user-agent"); + if client.user_agent then + if client.user_agent.software then + user_agent:text_tag("software", client.user_agent.software); + end + if client.user_agent.device then + user_agent:text_tag("device", client.user_agent.device); + end + if client.user_agent.uri then + user_agent:text_tag("uri", client.user_agent.uri); + end + end + + local connected = client.active and client.active.connected; + reply:tag("client", { id = client.id, connected = connected and "true" or "false" }) + :text_tag("first-seen", dt.datetime(client.first_seen)) + :text_tag("last-seen", dt.datetime(client.last_seen)) + :add_child(auth_type) + :add_child(user_agent) + :up(); + end + reply:up(); + + origin.send(reply); + return true; +end); + +-- Command + +module:once(function () + local console_env = module:shared("/*/admin_shell/env"); + if not console_env.user then return; end -- admin_shell probably not loaded + + function console_env.user:clients(username) + local clients = get_active_clients(username); + if not clients or #clients == 0 then + return true, "No clients associated with this account"; + end + + local colspec = { + { title = "Software", key = "software" }; + { title = "Last seen", key = "last_seen" }; + { title = "Authentication", key = "auth_methods" }; + }; + + local row = require "util.human.io".table(colspec, self.session.width); + + local print = self.session.print; + print(row()); + for _, client in ipairs(clients) do + print(row({ + software = client.user_agent.software; + last_seen = os.date("%Y-%m-%d", client.last_seen); + auth_methods = array.collect(it.keys(client.active)):sort(); + })); + end + print(("%d clients"):format(#clients)); + end +end);