comparison plugins/mod_pubsub/pubsub.lib.lua @ 14118:221f3378e7f4

mod_pubsub, util.pubsub: Support for 'authorize' access model
author Matthew Wild <mwild1@gmail.com>
date Tue, 31 Mar 2026 14:18:06 +0100
parents 11573716205f
children
comparison
equal deleted inserted replaced
14117:354427afb638 14118:221f3378e7f4
3 local jid_prep = require "prosody.util.jid".prep; 3 local jid_prep = require "prosody.util.jid".prep;
4 local set = require "prosody.util.set"; 4 local set = require "prosody.util.set";
5 local st = require "prosody.util.stanza"; 5 local st = require "prosody.util.stanza";
6 local it = require "prosody.util.iterators"; 6 local it = require "prosody.util.iterators";
7 local uuid_generate = require "prosody.util.uuid".generate; 7 local uuid_generate = require "prosody.util.uuid".generate;
8 local get_form_type = require "prosody.util.dataforms".get_type;
8 local dataform = require"prosody.util.dataforms".new; 9 local dataform = require"prosody.util.dataforms".new;
9 local errors = require "prosody.util.error"; 10 local errors = require "prosody.util.error";
10 11
11 local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; 12 local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
12 local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; 13 local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
14 15
15 local _M = {}; 16 local _M = {};
16 17
17 local handlers = {}; 18 local handlers = {};
18 _M.handlers = handlers; 19 _M.handlers = handlers;
20
21 local form_handlers = {};
22 _M.form_handlers = form_handlers;
19 23
20 local pubsub_errors = errors.init("pubsub", xmlns_pubsub_errors, { 24 local pubsub_errors = errors.init("pubsub", xmlns_pubsub_errors, {
21 ["conflict"] = { "cancel", "conflict" }; 25 ["conflict"] = { "cancel", "conflict" };
22 ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; 26 ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
23 ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; 27 ["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
30 ["not-acceptable"] = { "modify", "not-acceptable" }; 34 ["not-acceptable"] = { "modify", "not-acceptable" };
31 ["internal-server-error"] = { "wait", "internal-server-error" }; 35 ["internal-server-error"] = { "wait", "internal-server-error" };
32 ["precondition-not-met"] = { "cancel", "conflict", nil, "precondition-not-met" }; 36 ["precondition-not-met"] = { "cancel", "conflict", nil, "precondition-not-met" };
33 ["invalid-item"] = { "modify", "bad-request", "invalid item" }; 37 ["invalid-item"] = { "modify", "bad-request", "invalid item" };
34 ["persistent-items-unsupported"] = { "cancel", "feature-not-implemented", nil, "persistent-items" }; 38 ["persistent-items-unsupported"] = { "cancel", "feature-not-implemented", nil, "persistent-items" };
39 ["already-subscribed"] = { "cancel", "bad-request" };
35 }); 40 });
36 local function pubsub_error_reply(stanza, error, context) 41 local function pubsub_error_reply(stanza, error, context)
37 local err = pubsub_errors.wrap(error, context); 42 local err = pubsub_errors.wrap(error, context);
38 if error == "precondition-not-met" and type(context) == "table" and type(context.field) == "string" then 43 if error == "precondition-not-met" and type(context) == "table" and type(context.field) == "string" then
39 err.text = "Field does not match: " .. context.field; 44 err.text = "Field does not match: " .. context.field;
225 name = "publish_model"; 230 name = "publish_model";
226 var = "pubsub#publish_model"; 231 var = "pubsub#publish_model";
227 }; 232 };
228 }; 233 };
229 _M.node_metadata_form = node_metadata_form; 234 _M.node_metadata_form = node_metadata_form;
235
236 local sub_approval_form = dataform {
237 {
238 type = "hidden";
239 var = "FORM_TYPE";
240 value = "http://jabber.org/protocol/pubsub#subscribe_authorization";
241 };
242 {
243 type = "text-single";
244 name = "node";
245 var = "pubsub#node";
246 label = "Node name";
247 };
248 {
249 type = "jid-single";
250 name = "jid";
251 var = "pubsub#subscriber_jid";
252 label = "Subscriber JID";
253 };
254 {
255 type = "boolean";
256 name = "allow";
257 var = "pubsub#allow";
258 label = "Allow subscription";
259 };
260 };
230 261
231 local service_method_feature_map = { 262 local service_method_feature_map = {
232 add_subscription = { "subscribe", "subscription-options" }; 263 add_subscription = { "subscribe", "subscription-options" };
233 create = { "create-nodes", "instant-nodes", "item-ids", "create-and-configure" }; 264 create = { "create-nodes", "instant-nodes", "item-ids", "create-and-configure" };
234 delete = { "delete-nodes" }; 265 delete = { "delete-nodes" };
267 end 298 end
268 end 299 end
269 end 300 end
270 301
271 for affiliation in pairs(service.config.capabilities) do 302 for affiliation in pairs(service.config.capabilities) do
272 if affiliation ~= "none" and affiliation ~= "owner" then 303 if affiliation ~= "none" and affiliation ~= "unauthorized" and affiliation ~= "owner" then
273 supported_features:add(affiliation.."-affiliation"); 304 supported_features:add(affiliation.."-affiliation");
274 end 305 end
275 end 306 end
276 307
277 if service.node_defaults.access_model then 308 if service.node_defaults.access_model then
335 handler(origin, stanza, action, service); 366 handler(origin, stanza, action, service);
336 return true; 367 return true;
337 end 368 end
338 end 369 end
339 370
371 function _M.handle_pubsub_message(event, service)
372 local origin, stanza = event.origin, event.stanza;
373
374 if stanza.attr.type == "error" then
375 return;
376 end
377
378 local form = stanza:get_child("x", "jabber:x:data");
379 if form and form.attr.type == "submit" then
380 local form_type = get_form_type(form);
381 if not form_type then return; end
382
383 local form_name = form_type:match("http://jabber.org/protocol/pubsub#([%w_]+)$");
384 if not form_name then return; end
385
386 local handler = form_handlers[form_name];
387 if not handler then return; end
388
389 handler(origin, stanza, form, service);
390 return true;
391 end
392 end
393
340 function handlers.get_items(origin, stanza, items, service) 394 function handlers.get_items(origin, stanza, items, service)
341 local node = items.attr.node; 395 local node = items.attr.node;
342 396
343 local requested_items = {}; 397 local requested_items = {};
344 for item in items:childtags("item") do 398 for item in items:childtags("item") do
396 return true; 450 return true;
397 end 451 end
398 452
399 function handlers.get_subscriptions(origin, stanza, subscriptions, service) 453 function handlers.get_subscriptions(origin, stanza, subscriptions, service)
400 local node = subscriptions.attr.node; 454 local node = subscriptions.attr.node;
401 local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); 455 local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from, true);
402 if not ok then 456 if not ok then
403 origin.send(pubsub_error_reply(stanza, ret)); 457 origin.send(pubsub_error_reply(stanza, ret));
404 return true; 458 return true;
405 end 459 end
406 local reply = st.reply(stanza) 460 local reply = st.reply(stanza)
407 :tag("pubsub", { xmlns = xmlns_pubsub }) 461 :tag("pubsub", { xmlns = xmlns_pubsub })
408 :tag("subscriptions"); 462 :tag("subscriptions");
409 for _, sub in ipairs(ret) do 463 for _, sub in ipairs(ret) do
410 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); 464 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = sub.subscription and 'subscribed' or 'pending' }):up();
411 end 465 end
412 origin.send(reply); 466 origin.send(reply);
413 return true; 467 return true;
414 end 468 end
415 469
416 function handlers.owner_get_subscriptions(origin, stanza, subscriptions, service) 470 function handlers.owner_get_subscriptions(origin, stanza, subscriptions, service)
417 local node = subscriptions.attr.node; 471 local node = subscriptions.attr.node;
418 local ok, ret = service:get_subscriptions(node, stanza.attr.from); 472 local ok, ret = service:get_subscriptions(node, stanza.attr.from, nil, true);
419 if not ok then 473 if not ok then
420 origin.send(pubsub_error_reply(stanza, ret)); 474 origin.send(pubsub_error_reply(stanza, ret));
421 return true; 475 return true;
422 end 476 end
477
423 local reply = st.reply(stanza) 478 local reply = st.reply(stanza)
424 :tag("pubsub", { xmlns = xmlns_pubsub_owner }) 479 :tag("pubsub", { xmlns = xmlns_pubsub_owner })
425 :tag("subscriptions"); 480 :tag("subscriptions");
426 for _, sub in ipairs(ret) do 481 for _, sub in ipairs(ret) do
427 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); 482 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = sub.subscription and 'subscribed' or 'pending' }):up();
428 end 483 end
484
429 origin.send(reply); 485 origin.send(reply);
430 return true; 486 return true;
431 end 487 end
432 488
433 function handlers.owner_set_subscriptions(origin, stanza, subscriptions, service) 489 function handlers.owner_set_subscriptions(origin, stanza, subscriptions, service)
543 if err then 599 if err then
544 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err))); 600 origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err)));
545 return true 601 return true
546 end 602 end
547 end 603 end
548 local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); 604
605 -- Test whether we can subscribe or need to request a subscription
606 -- Known issue: this test fails if we are subscribing someone else,
607 -- but it would be an unusual case to have permission to do that but
608 -- not subscribe ourselves.
609 local ok, ret, success_state;
610 if service:may(node, stanza.attr.from, "subscribe") then
611 module:log("debug", "Subscribing to %s", node);
612 success_state = "subscribed";
613 ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
614 else
615 module:log("debug", "Requesting subscription to %s (%q %q)", node, stanza.attr.from, jid);
616 success_state = "pending";
617 ok, ret = service:request_subscription(node, stanza.attr.from, jid, options);
618 end
619
549 local reply; 620 local reply;
550 if ok then 621 if ok then
551 reply = st.reply(stanza) 622 reply = st.reply(stanza)
552 :tag("pubsub", { xmlns = xmlns_pubsub }) 623 :tag("pubsub", { xmlns = xmlns_pubsub })
553 :tag("subscription", { 624 :tag("subscription", {
554 node = node, 625 node = node,
555 jid = jid, 626 jid = jid,
556 subscription = "subscribed" 627 subscription = success_state;
557 }):up(); 628 }):up();
558 if options_tag then 629 if options_tag then
559 reply:add_child(options_tag); 630 reply:add_child(options_tag);
560 end 631 end
561 else 632 else
869 for affiliation_tag in affiliations:childtags("affiliation") do 940 for affiliation_tag in affiliations:childtags("affiliation") do
870 local jid = affiliation_tag.attr.jid; 941 local jid = affiliation_tag.attr.jid;
871 local affiliation = affiliation_tag.attr.affiliation; 942 local affiliation = affiliation_tag.attr.affiliation;
872 943
873 jid = jid_prep(jid); 944 jid = jid_prep(jid);
874 if affiliation == "none" then affiliation = nil; end 945 if affiliation == "none" or affiliation == "unauthorized" then
946 affiliation = nil;
947 end
875 948
876 local ok, err = service:set_affiliation(node, stanza.attr.from, jid, affiliation); 949 local ok, err = service:set_affiliation(node, stanza.attr.from, jid, affiliation);
877 if not ok then 950 if not ok then
878 -- FIXME Incomplete error handling, 951 -- FIXME Incomplete error handling,
879 -- see XEP 60 8.9.2.4 Multiple Simultaneous Modifications 952 -- see XEP 60 8.9.2.4 Multiple Simultaneous Modifications
971 end 1044 end
972 return get_set; 1045 return get_set;
973 end 1046 end
974 _M.archive_itemstore = archive_itemstore; 1047 _M.archive_itemstore = archive_itemstore;
975 1048
1049 local function notify_subscription_request(event)
1050 local service, node = event.service, event.node;
1051
1052 local node_obj = service.nodes[node];
1053 if not node_obj then
1054 return;
1055 end
1056
1057 local notify_jids = {};
1058
1059 for jid, affiliation in pairs(node_obj.affiliations) do
1060 if affiliation == "owner" then
1061 table.insert(notify_jids, jid);
1062 end
1063 end
1064
1065 if #notify_jids == 0 then
1066 return;
1067 end
1068
1069 local notification = st.message({ from = module.host })
1070 :add_child(sub_approval_form:form({
1071 node = node;
1072 jid = event.jid;
1073 allow = false;
1074 }));
1075
1076 module:log("debug", "Sending subscription request notification to %d JIDs", #notify_jids);
1077
1078 if #notify_jids == 1 then
1079 notification.attr.to = notify_jids[1];
1080 module:send(notification);
1081 return;
1082 end
1083
1084 for _, recipient in ipairs(notify_jids) do
1085 local clone = st.clone(notification);
1086 clone.attr.to = recipient;
1087 module:send(clone);
1088 end
1089 end
1090
1091 function _M.form_handlers.subscribe_authorization(origin, stanza, form, service)
1092 local form_data, errs = sub_approval_form:data(form);
1093 if not form_data then
1094 local reply = st.error_reply(stanza, "modify", "not-acceptable", dataform_error_message(errs));
1095 origin.send(reply);
1096 return;
1097 end
1098
1099 local node, jid, allow = form_data.node, form_data.jid, form_data.allow;
1100
1101 local ok, ret;
1102 if allow then
1103 ok, ret = service:add_subscription(node, stanza.attr.from, jid);
1104 else
1105 ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
1106 end
1107
1108 if not ok then
1109 origin.send(pubsub_error_reply(stanza, ret));
1110 return;
1111 end
1112 end
1113
1114 function _M.add_service_hooks(service)
1115 module:hook_object_event(service.events, "subscription-requested", notify_subscription_request);
1116 end
1117
976 return _M; 1118 return _M;