comparison mod_client_management/mod_client_management.lua @ 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 385346b6c81d
children 717ff9468464
comparison
equal deleted inserted replaced
5300:fa97de0b0961 5301:8ef197cccd74
1 local modulemanager = require "core.modulemanager"; 1 local modulemanager = require "core.modulemanager";
2 local usermanager = require "core.usermanager"; 2 local usermanager = require "core.usermanager";
3 3
4 local array = require "util.array";
5 local dt = require "util.datetime";
4 local id = require "util.id"; 6 local id = require "util.id";
7 local it = require "util.iterators";
5 local jid = require "util.jid"; 8 local jid = require "util.jid";
6 local st = require "util.stanza"; 9 local st = require "util.stanza";
7 10
8 local strict = module:get_option_boolean("enforce_client_ids", false); 11 local strict = module:get_option_boolean("enforce_client_ids", false);
9 12
185 188
186 -- Check for an active token grant that has been previously used by this client 189 -- Check for an active token grant that has been previously used by this client
187 if client.auth_token_id then 190 if client.auth_token_id then
188 local grant = tokenauth.get_grant_info(client.auth_token_id); 191 local grant = tokenauth.get_grant_info(client.auth_token_id);
189 if grant then 192 if grant then
190 status.active_grant = grant; 193 status.grant = grant;
191 end 194 end
192 end 195 end
193 196
194 -- Check for active FAST tokens 197 -- Check for active FAST tokens
195 if client.fast_auth then 198 if client.fast_auth then
196 if mod_fast.is_client_fast(username, client.id, last_password_change) then 199 if mod_fast.is_client_fast(username, client.id, last_password_change) then
197 status.active_fast = client.fast_auth; 200 status.fast = client.fast_auth;
198 end 201 end
199 end 202 end
200 203
201 -- Client has access if any password-based SASL mechanisms have been used since last password change 204 -- Client has access if any password-based SASL mechanisms have been used since last password change
202 for mech, mech_last_used in pairs(client.mechanisms) do 205 for mech, mech_last_used in pairs(client.mechanisms) do
203 if is_password_mechanism(mech) and mech_last_used >= last_password_change then 206 if is_password_mechanism(mech) and mech_last_used >= last_password_change then
204 status.active_password = mech_last_used; 207 status.password = mech_last_used;
205 end 208 end
206 end 209 end
207 210
208 if prosody.full_sessions[client.full_jid] then 211 if prosody.full_sessions[client.full_jid] then
209 status.active_connected = true; 212 status.connected = true;
210 end 213 end
211 214
212 if next(status) == nil then 215 if next(status) == nil then
213 return nil; 216 return nil;
214 end 217 end
227 local active = is_client_active(client); 230 local active = is_client_active(client);
228 if active then 231 if active then
229 client.type = "session"; 232 client.type = "session";
230 client.active = active; 233 client.active = active;
231 table.insert(active_clients, client); 234 table.insert(active_clients, client);
232 if active.active_grant then 235 if active.grant then
233 used_grants[active.active_grant.id] = true; 236 used_grants[active.grant.id] = true;
234 end 237 end
235 end 238 end
236 end 239 end
237 240
238 -- Next, account for any grants that have been issued, but never actually logged in 241 -- Next, account for any grants that have been issued, but never actually logged in
242 id = grant_id; 245 id = grant_id;
243 type = "access"; 246 type = "access";
244 first_seen = grant.created; 247 first_seen = grant.created;
245 last_seen = grant.accessed; 248 last_seen = grant.accessed;
246 active = { 249 active = {
247 active_grant = grant; 250 grant = grant;
248 }; 251 };
249 user_agent = get_user_agent(nil, grant); 252 user_agent = get_user_agent(nil, grant);
250 }); 253 });
251 end 254 end
252 end 255 end
266 return a.id < b.id; 269 return a.id < b.id;
267 end); 270 end);
268 271
269 return active_clients; 272 return active_clients;
270 end 273 end
274
275 -- Protocol
276
277 local xmlns_manage_clients = "xmpp:prosody.im/protocol/manage-clients";
278
279 module:hook("iq-get/self/xmpp:prosody.im/protocol/manage-clients:list", function (event)
280 local origin, stanza = event.origin, event.stanza;
281
282 if not module:may(":list-clients", event) then
283 origin.send(st.error_reply(stanza, "auth", "forbidden"));
284 return true;
285 end
286
287 local reply = st.reply(stanza)
288 :tag("clients", { xmlns = xmlns_manage_clients });
289
290 local active_clients = get_active_clients(event.origin.username);
291 for _, client in ipairs(active_clients) do
292 local auth_type = st.stanza("auth");
293 if client.active then
294 if client.active.password then
295 auth_type:text_tag("password");
296 end
297 if client.active.grant then
298 auth_type:text_tag("bearer-token");
299 end
300 if client.active.fast then
301 auth_type:text_tag("fast");
302 end
303 end
304
305 local user_agent = st.stanza("user-agent");
306 if client.user_agent then
307 if client.user_agent.software then
308 user_agent:text_tag("software", client.user_agent.software);
309 end
310 if client.user_agent.device then
311 user_agent:text_tag("device", client.user_agent.device);
312 end
313 if client.user_agent.uri then
314 user_agent:text_tag("uri", client.user_agent.uri);
315 end
316 end
317
318 local connected = client.active and client.active.connected;
319 reply:tag("client", { id = client.id, connected = connected and "true" or "false" })
320 :text_tag("first-seen", dt.datetime(client.first_seen))
321 :text_tag("last-seen", dt.datetime(client.last_seen))
322 :add_child(auth_type)
323 :add_child(user_agent)
324 :up();
325 end
326 reply:up();
327
328 origin.send(reply);
329 return true;
330 end);
331
332 -- Command
333
334 module:once(function ()
335 local console_env = module:shared("/*/admin_shell/env");
336 if not console_env.user then return; end -- admin_shell probably not loaded
337
338 function console_env.user:clients(username)
339 local clients = get_active_clients(username);
340 if not clients or #clients == 0 then
341 return true, "No clients associated with this account";
342 end
343
344 local colspec = {
345 { title = "Software", key = "software" };
346 { title = "Last seen", key = "last_seen" };
347 { title = "Authentication", key = "auth_methods" };
348 };
349
350 local row = require "util.human.io".table(colspec, self.session.width);
351
352 local print = self.session.print;
353 print(row());
354 for _, client in ipairs(clients) do
355 print(row({
356 software = client.user_agent.software;
357 last_seen = os.date("%Y-%m-%d", client.last_seen);
358 auth_methods = array.collect(it.keys(client.active)):sort();
359 }));
360 end
361 print(("%d clients"):format(#clients));
362 end
363 end);