diff 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
line wrap: on
line diff
--- a/plugins/mod_pubsub/pubsub.lib.lua	Tue Mar 31 14:15:00 2026 +0100
+++ b/plugins/mod_pubsub/pubsub.lib.lua	Tue Mar 31 14:18:06 2026 +0100
@@ -5,6 +5,7 @@
 local st = require "prosody.util.stanza";
 local it = require "prosody.util.iterators";
 local uuid_generate = require "prosody.util.uuid".generate;
+local get_form_type = require "prosody.util.dataforms".get_type;
 local dataform = require"prosody.util.dataforms".new;
 local errors = require "prosody.util.error";
 
@@ -17,6 +18,9 @@
 local handlers = {};
 _M.handlers = handlers;
 
+local form_handlers = {};
+_M.form_handlers = form_handlers;
+
 local pubsub_errors = errors.init("pubsub", xmlns_pubsub_errors, {
 	["conflict"] = { "cancel", "conflict" };
 	["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
@@ -32,6 +36,7 @@
 	["precondition-not-met"] = { "cancel", "conflict", nil, "precondition-not-met" };
 	["invalid-item"] = { "modify", "bad-request", "invalid item" };
 	["persistent-items-unsupported"] = { "cancel", "feature-not-implemented", nil, "persistent-items" };
+	["already-subscribed"] = { "cancel", "bad-request" };
 });
 local function pubsub_error_reply(stanza, error, context)
 	local err = pubsub_errors.wrap(error, context);
@@ -228,6 +233,32 @@
 };
 _M.node_metadata_form = node_metadata_form;
 
+local sub_approval_form = dataform {
+	{
+		type = "hidden";
+		var = "FORM_TYPE";
+		value = "http://jabber.org/protocol/pubsub#subscribe_authorization";
+	};
+	{
+		type = "text-single";
+		name = "node";
+		var = "pubsub#node";
+		label = "Node name";
+	};
+	{
+		type = "jid-single";
+		name = "jid";
+		var = "pubsub#subscriber_jid";
+		label = "Subscriber JID";
+	};
+	{
+		type = "boolean";
+		name = "allow";
+		var = "pubsub#allow";
+		label = "Allow subscription";
+	};
+};
+
 local service_method_feature_map = {
 	add_subscription = { "subscribe", "subscription-options" };
 	create = { "create-nodes", "instant-nodes", "item-ids", "create-and-configure" };
@@ -269,7 +300,7 @@
 	end
 
 	for affiliation in pairs(service.config.capabilities) do
-		if affiliation ~= "none" and affiliation ~= "owner" then
+		if affiliation ~= "none" and affiliation ~= "unauthorized" and affiliation ~= "owner" then
 			supported_features:add(affiliation.."-affiliation");
 		end
 	end
@@ -337,6 +368,29 @@
 	end
 end
 
+function _M.handle_pubsub_message(event, service)
+	local origin, stanza = event.origin, event.stanza;
+
+	if stanza.attr.type == "error" then
+		return;
+	end
+
+	local form = stanza:get_child("x", "jabber:x:data");
+	if form and form.attr.type == "submit" then
+		local form_type = get_form_type(form);
+		if not form_type then return; end
+
+		local form_name = form_type:match("http://jabber.org/protocol/pubsub#([%w_]+)$");
+		if not form_name then return; end
+
+		local handler = form_handlers[form_name];
+		if not handler then return; end
+
+		handler(origin, stanza, form, service);
+		return true;
+	end
+end
+
 function handlers.get_items(origin, stanza, items, service)
 	local node = items.attr.node;
 
@@ -398,7 +452,7 @@
 
 function handlers.get_subscriptions(origin, stanza, subscriptions, service)
 	local node = subscriptions.attr.node;
-	local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
+	local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from, true);
 	if not ok then
 		origin.send(pubsub_error_reply(stanza, ret));
 		return true;
@@ -407,7 +461,7 @@
 		:tag("pubsub", { xmlns = xmlns_pubsub })
 			:tag("subscriptions");
 	for _, sub in ipairs(ret) do
-		reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
+		reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = sub.subscription and 'subscribed' or 'pending' }):up();
 	end
 	origin.send(reply);
 	return true;
@@ -415,17 +469,19 @@
 
 function handlers.owner_get_subscriptions(origin, stanza, subscriptions, service)
 	local node = subscriptions.attr.node;
-	local ok, ret = service:get_subscriptions(node, stanza.attr.from);
+	local ok, ret = service:get_subscriptions(node, stanza.attr.from, nil, true);
 	if not ok then
 		origin.send(pubsub_error_reply(stanza, ret));
 		return true;
 	end
+
 	local reply = st.reply(stanza)
 		:tag("pubsub", { xmlns = xmlns_pubsub_owner })
 			:tag("subscriptions");
 	for _, sub in ipairs(ret) do
-		reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
+		reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = sub.subscription and 'subscribed' or 'pending' }):up();
 	end
+
 	origin.send(reply);
 	return true;
 end
@@ -545,7 +601,22 @@
 			return true
 		end
 	end
-	local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
+
+	-- Test whether we can subscribe or need to request a subscription
+	-- Known issue: this test fails if we are subscribing someone else,
+	-- but it would be an unusual case to have permission to do that but
+	-- not subscribe ourselves.
+	local ok, ret, success_state;
+	if service:may(node, stanza.attr.from, "subscribe") then
+		module:log("debug", "Subscribing to %s", node);
+		success_state = "subscribed";
+		ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
+	else
+		module:log("debug", "Requesting subscription to %s (%q %q)", node, stanza.attr.from, jid);
+		success_state = "pending";
+		ok, ret = service:request_subscription(node, stanza.attr.from, jid, options);
+	end
+
 	local reply;
 	if ok then
 		reply = st.reply(stanza)
@@ -553,7 +624,7 @@
 				:tag("subscription", {
 					node = node,
 					jid = jid,
-					subscription = "subscribed"
+					subscription = success_state;
 				}):up();
 		if options_tag then
 			reply:add_child(options_tag);
@@ -871,7 +942,9 @@
 		local affiliation = affiliation_tag.attr.affiliation;
 
 		jid = jid_prep(jid);
-		if affiliation == "none" then affiliation = nil; end
+		if affiliation == "none" or affiliation == "unauthorized" then
+			affiliation = nil;
+		end
 
 		local ok, err = service:set_affiliation(node, stanza.attr.from, jid, affiliation);
 		if not ok then
@@ -973,4 +1046,73 @@
 end
 _M.archive_itemstore = archive_itemstore;
 
+local function notify_subscription_request(event)
+	local service, node = event.service, event.node;
+
+	local node_obj = service.nodes[node];
+	if not node_obj then
+		return;
+	end
+
+	local notify_jids = {};
+
+	for jid, affiliation in pairs(node_obj.affiliations) do
+		if affiliation == "owner" then
+			table.insert(notify_jids, jid);
+		end
+	end
+
+	if #notify_jids == 0 then
+		return;
+	end
+
+	local notification = st.message({ from = module.host })
+		:add_child(sub_approval_form:form({
+			node = node;
+			jid = event.jid;
+			allow = false;
+		}));
+
+	module:log("debug", "Sending subscription request notification to %d JIDs", #notify_jids);
+
+	if #notify_jids == 1 then
+		notification.attr.to = notify_jids[1];
+		module:send(notification);
+		return;
+	end
+
+	for _, recipient in ipairs(notify_jids) do
+		local clone = st.clone(notification);
+		clone.attr.to = recipient;
+		module:send(clone);
+	end
+end
+
+function _M.form_handlers.subscribe_authorization(origin, stanza, form, service)
+	local form_data, errs = sub_approval_form:data(form);
+	if not form_data then
+		local reply = st.error_reply(stanza, "modify", "not-acceptable", dataform_error_message(errs));
+		origin.send(reply);
+		return;
+	end
+
+	local node, jid, allow = form_data.node, form_data.jid, form_data.allow;
+
+	local ok, ret;
+	if allow then
+		ok, ret = service:add_subscription(node, stanza.attr.from, jid);
+	else
+		ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
+	end
+
+	if not ok then
+		origin.send(pubsub_error_reply(stanza, ret));
+		return;
+	end
+end
+
+function _M.add_service_hooks(service)
+	module:hook_object_event(service.events, "subscription-requested", notify_subscription_request);
+end
+
 return _M;