From 2f5ff8bacfb555a959f78baca4f2e27f77af75c9 Mon Sep 17 00:00:00 2001 From: Badlop Date: Thu, 26 Sep 2024 12:07:52 +0200 Subject: [PATCH] Preliminary support for "IQ permission" from XEP-0356 0.4.1 (VERSION 2) --- mix.exs | 2 +- mix.lock | 2 +- rebar.config | 2 +- rebar.lock | 4 +- src/ejabberd_router.erl | 21 ++++- src/mod_privilege.erl | 191 ++++++++++++++++++++++++++++++++++++-- src/mod_privilege_opt.erl | 7 ++ 7 files changed, 215 insertions(+), 14 deletions(-) diff --git a/mix.exs b/mix.exs index 171e8762b4a..6c8fb1461f8 100644 --- a/mix.exs +++ b/mix.exs @@ -144,7 +144,7 @@ defmodule Ejabberd.MixProject do {:p1_utils, "~> 1.0"}, {:pkix, "~> 1.0"}, {:stringprep, ">= 1.0.26"}, - {:xmpp, git: "https://github.com/processone/xmpp.git", ref: "2a54443436dc8a942969f2ef7c5654d5acab7533", override: true}, + {:xmpp, git: "https://github.com/badlop/xmpp", ref: "3a066e4a6e8bc1260b8a7aa7aee62d87f0eb9006", override: true}, {:yconf, "~> 1.0"}] ++ cond_deps() end diff --git a/mix.lock b/mix.lock index 065ae647e98..857fe18ef3c 100644 --- a/mix.lock +++ b/mix.lock @@ -32,6 +32,6 @@ "stringprep": {:hex, :stringprep, "1.0.30", "46cf0ff631b3e7328f61f20b454d59428d87738f25d709798b5dcbb9b83c23f1", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "f6fc9b3384a03877830f89b2f38580caf3f4a27448a4a333d6a8c3975c220b9a"}, "stun": {:hex, :stun, "1.2.14", "6f538ac80c842131dbd149055570d116bfabc9b5ebff4bd6af2e7888958c660c", [:rebar3], [{:fast_tls, "1.1.21", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "e134807b1b7a8dffd94e64eefee00e65c7b4042f3d14e16f8f43566d20371583"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "xmpp": {:git, "https://github.com/processone/xmpp.git", "2a54443436dc8a942969f2ef7c5654d5acab7533", [ref: "2a54443436dc8a942969f2ef7c5654d5acab7533"]}, + "xmpp": {:git, "https://github.com/badlop/xmpp", "3a066e4a6e8bc1260b8a7aa7aee62d87f0eb9006", [ref: "3a066e4a6e8bc1260b8a7aa7aee62d87f0eb9006"]}, "yconf": {:hex, :yconf, "1.0.16", "d59521d66ff89f219411b6e9277cd6feec7cc6fce11554e67de02a8d0a470479", [:rebar3], [{:fast_yaml, "1.0.37", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "e947813273f38711c7b2e5a8e4acc9a51c7bbe854f744a345f60300b38586c89"}, } diff --git a/rebar.config b/rebar.config index 5fcc1e5bbfb..dc11a56bd41 100644 --- a/rebar.config +++ b/rebar.config @@ -69,7 +69,7 @@ {stringprep, "~> 1.0.29", {git, "https://github.com/processone/stringprep", {tag, "1.0.30"}}}, {if_var_true, stun, {stun, "~> 1.2.12", {git, "https://github.com/processone/stun", {tag, "1.2.14"}}}}, - {xmpp, "~> 1.8.3", {git, "https://github.com/processone/xmpp", "2a54443436dc8a942969f2ef7c5654d5acab7533"}}, + {xmpp, ".*", {git, "https://github.com/badlop/xmpp", "3a066e4a6e8bc1260b8a7aa7aee62d87f0eb9006"}}, {yconf, "~> 1.0.15", {git, "https://github.com/processone/yconf", {tag, "1.0.16"}}} ]}. diff --git a/rebar.lock b/rebar.lock index b539c60b8d8..7f866ef9aca 100644 --- a/rebar.lock +++ b/rebar.lock @@ -31,8 +31,8 @@ {<<"stun">>,{pkg,<<"stun">>,<<"1.2.14">>},0}, {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1}, {<<"xmpp">>, - {git,"https://github.com/processone/xmpp", - {ref,"2a54443436dc8a942969f2ef7c5654d5acab7533"}}, + {git,"https://github.com/badlop/xmpp", + {ref,"3a066e4a6e8bc1260b8a7aa7aee62d87f0eb9006"}}, 0}, {<<"yconf">>,{pkg,<<"yconf">>,<<"1.0.16">>},0}]}. [ diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 94f25f80d15..8f877dfaf95 100644 --- a/src/ejabberd_router.erl +++ b/src/ejabberd_router.erl @@ -380,8 +380,9 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%-------------------------------------------------------------------- -spec do_route(stanza()) -> ok. -do_route(OrigPacket) -> - ?DEBUG("Route:~n~ts", [xmpp:pp(OrigPacket)]), +do_route(OrigPacket1) -> + ?DEBUG("Route:~n~ts", [xmpp:pp(OrigPacket1)]), + OrigPacket = process_privilege_iq(OrigPacket1), case ejabberd_hooks:run_fold(filter_packet, OrigPacket, []) of drop -> ok; @@ -405,6 +406,22 @@ do_route(OrigPacket) -> end end. +%% @format-begin +process_privilege_iq(Packet) -> + To = xmpp:get_to(Packet), + case xmpp:get_meta(Packet, privilege_iq, none) of + {OriginalId, OriginalHost, ReplacedJid} when ReplacedJid == To -> + Privilege = #privilege{forwarded = #forwarded{sub_els = [Packet]}}, + #iq{type = xmpp:get_type(Packet), + id = OriginalId, + to = jid:make(OriginalHost), + from = ReplacedJid, + sub_els = [Privilege]}; + _ -> + Packet + end. +%% @format-end + -spec do_route(stanza(), #route{}) -> any(). do_route(Pkt, #route{local_hint = LocalHint, pid = Pid}) when is_pid(Pid) -> diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl index 7524af18102..12e01dacfd1 100644 --- a/src/mod_privilege.erl +++ b/src/mod_privilege.erl @@ -25,7 +25,7 @@ -author('amuhar3@gmail.com'). --protocol({xep, 356, '0.2.1', '16.09', "", ""}). +-protocol({xep, 356, '0.4.1', '24.xx', "", ""}). -behaviour(gen_server). -behaviour(gen_mod). @@ -37,6 +37,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([component_connected/1, component_disconnected/2, + component_send_packet/1, roster_access/2, process_message/1, process_presence_out/1, process_presence_in/1]). @@ -45,14 +46,17 @@ -include("translate.hrl"). -type roster_permission() :: both | get | set. +-type iq_permission() :: both | get | set. -type presence_permission() :: managed_entity | roster. -type message_permission() :: outgoing. -type roster_permissions() :: [{roster_permission(), acl:acl()}]. +-type iq_permissions() :: [{iq_permission(), acl:acl()}]. -type presence_permissions() :: [{presence_permission(), acl:acl()}]. -type message_permissions() :: [{message_permission(), acl:acl()}]. --type access() :: [{roster, roster_permissions()} | - {presence, presence_permissions()} | - {message, message_permissions()}]. +-type access() :: [{roster, roster_permission()} | + {iq, [privilege_namespace()]} | + {presence, presence_permission()} | + {message, message_permission()}]. -type permissions() :: #{binary() => access()}. -record(state, {server_host = <<"">> :: binary()}). @@ -71,6 +75,10 @@ reload(_Host, _NewOpts, _OldOpts) -> mod_opt_type(roster) -> econf:options( #{both => econf:acl(), get => econf:acl(), set => econf:acl()}); +mod_opt_type(iq) -> + econf:map( + econf:binary(), + econf:options(#{both => econf:acl(), get => econf:acl(), set => econf:acl()})); mod_opt_type(message) -> econf:options( #{outgoing => econf:acl()}); @@ -80,6 +88,7 @@ mod_opt_type(presence) -> mod_options(_) -> [{roster, [{both, none}, {get, none}, {set, none}]}, + {iq, []}, {presence, [{managed_entity, none}, {roster, none}]}, {message, [{outgoing,none}]}]. @@ -130,6 +139,27 @@ mod_doc() -> desc => ?T("Sets write access to a user's roster. " "The default value is 'none'.")}}]}, + {iq, + #{value => "{Namespace: Options}", + desc => + ?T("This option defines namespaces and their IQ permissions. " + "By default no permissions are given. " + "The 'Options' are:")}, + [{both, + #{value => ?T("AccessName"), + desc => + ?T("Allows sending IQ stanzas of type 'get' and 'set'. " + "The default value is 'none'.")}}, + {get, + #{value => ?T("AccessName"), + desc => + ?T("Allows sending IQ stanzas of type 'get'. " + "The default value is 'none'.")}}, + {set, + #{value => ?T("AccessName"), + desc => + ?T("Allows sending IQ stanzas of type 'set'. " + "The default value is 'none'.")}}]}, {message, #{value => ?T("Options"), desc => @@ -164,6 +194,9 @@ mod_doc() -> example => ["modules:", " mod_privilege:", + " iq:", + " http://jabber.org/protocol/pubsub:", + " get: all", " roster:", " get: all", " presence:", @@ -190,6 +223,10 @@ component_disconnected(Host, _Reason) -> gen_server:cast(Proc, {component_disconnected, Host}) end, ejabberd_option:hosts()). +%% +%% Message processing +%% + -spec process_message(stanza()) -> stop | ok. process_message(#message{from = #jid{luser = <<"">>, lresource = <<"">>} = From, to = #jid{lresource = <<"">>} = To, @@ -212,9 +249,73 @@ process_message(#message{from = #jid{luser = <<"">>, lresource = <<"">>} = From, %% Component is disconnected ok end; + process_message(_Stanza) -> ok. +%% +%% IQ processing +%% + +%% @format-begin + +component_send_packet({#iq{from = From, + to = #jid{lresource = <<"">>} = To, + id = Id, + type = Type} = + IQ, + State}) + when Type /= error -> + Host = From#jid.lserver, + ServerHost = To#jid.lserver, + Permissions = get_permissions(ServerHost), + Result = + case {maps:find(Host, Permissions), get_iq_encapsulated_details(IQ)} of + {{ok, Access}, {ok, EncapType, EncapNs, EncapFrom, EncIq}} + when (EncapType == Type) and ((EncapFrom == undefined) or (EncapFrom == To)) -> + NsPermissions = proplists:get_value(iq, Access, none), + Permission = + case lists:keyfind(EncapNs, 2, NsPermissions) of + #privilege_namespace{type = AllowedType} -> + AllowedType; + _ -> + none + end, + case Permission == both + orelse Permission == get andalso Type == get + orelse Permission == set andalso Type == set + of + true -> + forward_iq(Host, To, Id, EncIq); + false -> + ?INFO_MSG("IQ not forwarded: Permission not granted to ns=~s with type=~p", + [EncapNs, Type]), + drop + end; + {error, _} -> + %% Component is disconnected + ?INFO_MSG("IQ not forwarded: Component seems disconnected", []), + drop; + {_, {ok, E, _, _, _}} when E /= Type -> + ?INFO_MSG("IQ not forwarded: The encapsulated IQ stanza type=~p " + "does not match the top-level IQ stanza type=~p", + [E, Type]), + drop; + {_, {ok, _, _, EF, _}} when (EF /= undefined) and (EF /= To) -> + ?INFO_MSG("IQ not forwarded: The FROM attribute in the encapsulated " + "IQ stanza and the TO in top-level IQ stanza do not match", + []), + drop + end, + {Result, State}; +component_send_packet(Acc) -> + Acc. +%% @format-end + +%% +%% Roster processing +%% + -spec roster_access({true, iq()} | false, iq()) -> {true, iq()} | false. roster_access({true, _IQ} = Acc, _) -> Acc; @@ -309,6 +410,8 @@ init([Host|_]) -> process_presence_out, 50), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, process_presence_in, 50), + ejabberd_hooks:add(component_send_packet, ?MODULE, + component_send_packet, 50), {ok, #state{server_host = Host}}. handle_call(Request, From, State) -> @@ -320,22 +423,26 @@ handle_cast({component_connected, Host}, State) -> From = jid:make(ServerHost), To = jid:make(Host), RosterPerm = get_roster_permission(ServerHost, Host), + IqNamespaces = get_iq_namespaces(ServerHost, Host), PresencePerm = get_presence_permission(ServerHost, Host), MessagePerm = get_message_permission(ServerHost, Host), - if RosterPerm /= none; PresencePerm /= none; MessagePerm /= none -> + if RosterPerm /= none; IqNamespaces /= []; PresencePerm /= none; MessagePerm /= none -> Priv = #privilege{perms = [#privilege_perm{access = message, type = MessagePerm}, #privilege_perm{access = roster, type = RosterPerm}, + #privilege_perm{access = iq, + namespaces = IqNamespaces}, #privilege_perm{access = presence, type = PresencePerm}]}, ?INFO_MSG("Granting permissions to external " "component '~ts': roster = ~ts, presence = ~ts, " - "message = ~ts", - [Host, RosterPerm, PresencePerm, MessagePerm]), + "message = ~ts,~n iq = ~p", + [Host, RosterPerm, PresencePerm, MessagePerm, IqNamespaces]), Msg = #message{from = From, to = To, sub_els = [Priv]}, ejabberd_router:route(Msg), Permissions = maps:put(Host, [{roster, RosterPerm}, + {iq, IqNamespaces}, {presence, PresencePerm}, {message, MessagePerm}], get_permissions(ServerHost)), @@ -366,6 +473,8 @@ terminate(_Reason, State) -> Host = State#state.server_host, case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> + ejabberd_hooks:delete(component_send_packet, ?MODULE, + component_send_packet, 50), ejabberd_hooks:delete(component_connected, ?MODULE, component_connected, 50), ejabberd_hooks:delete(component_disconnected, ?MODULE, @@ -395,6 +504,10 @@ get_permissions(ServerHost) -> catch _:badarg -> #{} end. +%% +%% Message +%% + -spec forward_message(message()) -> ok. forward_message(#message{to = To} = Msg) -> ServerHost = To#jid.lserver, @@ -435,6 +548,45 @@ forward_message(#message{to = To} = Msg) -> ejabberd_router:route_error(Msg, Err) end. +%% +%% IQ +%% + +%% @format-begin + +-spec get_iq_encapsulated_details(iq()) -> + {ok, set | get, binary(), jid(), iq()} | + {error, Why :: atom(), any(), iq()}. +get_iq_encapsulated_details(#iq{sub_els = [IqSub]} = Msg) -> + Lang = xmpp:get_lang(Msg), + try xmpp:try_subtag(Msg, #privileged_iq{}) of + #privileged_iq{iq = #iq{type = EncapsulatedType, from = From} = EncIq} -> + [IqSubSub] = xmpp:get_els(IqSub), + [Element] = xmpp:get_els(IqSubSub), + Ns = xmpp:get_ns(Element), + {ok, EncapsulatedType, Ns, From, EncIq}; + _ -> + Txt = ?T("No element found"), + Err = xmpp:err_bad_request(Txt, Lang), + {error, no_privileged_iq, Err} + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + {error, codec_error, Err} + end. + +-spec forward_iq(binary(), jid(), binary(), iq()) -> iq(). +forward_iq(Host, ToplevelTo, Id, Iq) -> + FromJID = ToplevelTo, + NewIq0 = Iq#iq{from = FromJID}, + xmpp:put_meta(NewIq0, privilege_iq, {Id, Host, FromJID}). +%% @format-end + +%% +%% Permissions +%% + -spec get_roster_permission(binary(), binary()) -> roster_permission() | none. get_roster_permission(ServerHost, Host) -> Perms = mod_privilege_opt:roster(ServerHost), @@ -451,6 +603,26 @@ get_roster_permission(ServerHost, Host) -> end end. +-spec get_iq_namespaces(binary(), binary()) -> [privilege_namespace()]. +get_iq_namespaces(ServerHost, Host) -> + NsPerms = mod_privilege_opt:iq(ServerHost), + [#privilege_namespace{ns = Ns, type = get_iq_permission(ServerHost, Host, Perms)} || {Ns, Perms} <- NsPerms]. + +-spec get_iq_permission(binary(), binary(), [iq_permission()]) -> iq_permission() | none. +get_iq_permission(ServerHost, Host, Perms) -> + case match_rule(ServerHost, Host, Perms, both) of + allow -> + both; + deny -> + Get = match_rule(ServerHost, Host, Perms, get), + Set = match_rule(ServerHost, Host, Perms, set), + if Get == allow, Set == allow -> both; + Get == allow -> get; + Set == allow -> set; + true -> none + end + end. + -spec get_message_permission(binary(), binary()) -> message_permission() | none. get_message_permission(ServerHost, Host) -> Perms = mod_privilege_opt:message(ServerHost), @@ -472,7 +644,12 @@ get_presence_permission(ServerHost, Host) -> end end. +-ifdef(OTP_BELOW_26). +-dialyzer({no_contracts, match_rule/4}). +-endif. + -spec match_rule(binary(), binary(), roster_permissions(), roster_permission()) -> allow | deny; + (binary(), binary(), iq_permissions(), iq_permission()) -> allow | deny; (binary(), binary(), presence_permissions(), presence_permission()) -> allow | deny; (binary(), binary(), message_permissions(), message_permission()) -> allow | deny. match_rule(ServerHost, Host, Perms, Type) -> diff --git a/src/mod_privilege_opt.erl b/src/mod_privilege_opt.erl index 64198b387e5..36bf54efa56 100644 --- a/src/mod_privilege_opt.erl +++ b/src/mod_privilege_opt.erl @@ -3,10 +3,17 @@ -module(mod_privilege_opt). +-export([iq/1]). -export([message/1]). -export([presence/1]). -export([roster/1]). +-spec iq(gen_mod:opts() | global | binary()) -> [{binary(),[{'both',acl:acl()} | {'get',acl:acl()} | {'set',acl:acl()}]}]. +iq(Opts) when is_map(Opts) -> + gen_mod:get_opt(iq, Opts); +iq(Host) -> + gen_mod:get_module_opt(Host, mod_privilege, iq). + -spec message(gen_mod:opts() | global | binary()) -> [{'outgoing','none' | acl:acl()}]. message(Opts) when is_map(Opts) -> gen_mod:get_opt(message, Opts);