changeset 5301:8ef197cccd74

mod_client_management: Add XMPP and shell interfaces to fetch client list
author Matthew Wild <mwild1@gmail.com>
date Sat, 01 Apr 2023 13:56:53 +0100
parents fa97de0b0961
children ba94a5301985
files mod_client_management/mod_client_management.lua
diffstat 1 files changed, 100 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- 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);