Mercurial > prosody-hg
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; |
