changeset 4296:08138de4cb88

Prosodoy module to externalize MUC authorization via HTTP
author Seve Ferrer <seve@delape.net>
date Sat, 12 Dec 2020 18:19:14 +0100
parents d44a8d3dd571
children 4a5c4a352b78
files mod_muc_http_auth/README.md mod_muc_http_auth/mod_muc_http_auth.lua
diffstat 2 files changed, 143 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_http_auth/README.md	Sat Dec 12 18:19:14 2020 +0100
@@ -0,0 +1,63 @@
+# Introduction
+
+This module externalizes MUC authorization via HTTP.  
+Whenever a user wants to join a MUC, an HTTP GET request is made to `authorization_url`
+with the user bare jid (`userJID`) and the MUC jid (`mucJID`) as GET parameters.  
+Example:  
+`https://www.prosody.im/users/can-join/?userJID=romeo@example.com&mucJID=teaparty@chat.example.com`
+
+This allows an external service to decide whether a user is authorized to join a MUC or not.  
+
+When a user is authorized to join a MUC, this module expects the following JSON payload:
+```
+{
+    allowed: true,
+    error: "",
+}
+```
+Otherwise, either the user not being authorized or some failure in the external service:
+```
+{
+    allowed: false,
+    error: "Some error message to be displayed in this module's logs",
+}
+```
+
+# Configuring
+
+## Enabling
+
+``` {.lua}
+Component "rooms.example.net" "muc"
+
+modules_enabled = {
+    "muc_http_auth";
+}
+
+```
+
+
+## Settings
+
+|Name |Description |Default |
+|-----|------------|--------|
+|muc_http_auth_url| URL of the external HTTP service to which send `userJID` and `mucJID` in a GET request | "" |
+|muc_http_auth_enabled_for| List of MUC names (node part) to enable this module for | nil |
+|muc_http_auth_disabled_for| List of MUC names (node part) to disable this module for | nil |
+|muc_http_auth_insecure| Disable certificate verification for request. Only intended for development of the external service. | false |
+
+
+This module can be enabled/disabled for specific rooms. Only one of the following settings must be set.
+```
+-- muc_http_auth_enabled_for = {"teaparty"}
+-- muc_http_auth_disabled_for = {"teaparty"}
+```
+If none or both are found, all rooms in the MUC component will have this module enabled.
+
+Note: Use the node part of the MUC jid for these lists. Example:  
+
+Wrong:
+`muc_http_auth_enabled_for = {"teaparty@rooms.example.net"}`
+
+Correct:
+`muc_http_auth_enabled_for = {"teaparty"}`
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_http_auth/mod_muc_http_auth.lua	Sat Dec 12 18:19:14 2020 +0100
@@ -0,0 +1,80 @@
+local wait_for = require "util.async".wait_for;
+local http = require "net.http";
+local json = require "util.json";
+local st = require "util.stanza";
+local jid_node = require "util.jid".node;
+local jid_bare = require "util.jid".bare;
+
+local authorization_url = module:get_option("muc_http_auth_url", "")
+local enabled_for = module:get_option_set("muc_http_auth_enabled_for",  nil)
+local disabled_for = module:get_option_set("muc_http_auth_disabled_for",  nil)
+local insecure = module:get_option("muc_http_auth_insecure", false) --For development purposes
+
+local function must_be_authorized(room_node)
+	-- If none of these is set, all rooms need authorization
+	if not enabled_for and not disabled_for then return true; end
+
+	if enabled_for and not disabled_for then
+		for _, _room_node in ipairs(enabled_for) do
+			if _room_node == room_node then
+				return true;
+			end
+		end
+	end
+
+	if disabled_for and not enabled_for then
+		for _, _room_node in ipairs(disabled_for) do
+			if _room_node == room_node then
+				return false;
+			end
+		end
+	end
+
+	return true;
+end
+
+local function handle_success(response)
+	local body = json.decode(response.body or "") or {}
+	response = {
+		err = body.error,
+		allowed = body.allowed,
+		code = response.code
+	}
+	return {response=response, err=response.err};
+end
+
+local function handle_error(err)
+	return {err=err};
+end
+
+local function handle_presence(event)
+	local stanza = event.stanza;
+	if stanza.name ~= "presence" or stanza.attr.type == "unavailable" then
+		return;
+	end
+
+	local room, origin = event.room, event.origin;
+	if (not room) or (not origin) then return; end
+
+	if not must_be_authorized(jid_node(room.jid)) then return; end
+
+	local user_bare_jid = jid_bare(stanza.attr.from);
+	local url = authorization_url .. "?userJID=" .. user_bare_jid .."&mucJID=" .. room.jid;
+
+	local result = wait_for(http.request(url, {method="GET", insecure=insecure}):next(handle_success, handle_error));
+	local response, err = result.response, result.err;
+
+	if not (response and response.allowed) then
+		-- User is not authorized to join this room
+		err = (response or {}).err or err
+		module:log("debug", user_bare_jid .. " is not authorized to join " .. room.jid .. " Error: " .. tostring(err));
+		origin.send(st.error_reply(stanza, "error", "not-authorized", nil, module.host));
+		return true;
+	end
+
+	module:log("debug", user_bare_jid .. " is authorized to join " .. room.jid);
+	return;
+end
+
+
+module:hook("muc-occupant-pre-join", handle_presence);
\ No newline at end of file