Skip to content

Commit

Permalink
Preliminary support for "IQ permission" from XEP-0356 0.4.1 (VERSION 2)
Browse files Browse the repository at this point in the history
  • Loading branch information
badlop committed Sep 30, 2024
1 parent 4a931b4 commit 2f5ff8b
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 14 deletions.
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
}
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}}
]}.

Expand Down
4 changes: 2 additions & 2 deletions rebar.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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}]}.
[
Expand Down
21 changes: 19 additions & 2 deletions src/ejabberd_router.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) ->
Expand Down
191 changes: 184 additions & 7 deletions src/mod_privilege.erl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

-author('[email protected]').

-protocol({xep, 356, '0.2.1', '16.09', "", ""}).
-protocol({xep, 356, '0.4.1', '24.xx', "", ""}).

-behaviour(gen_server).
-behaviour(gen_mod).
Expand All @@ -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]).

Expand All @@ -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()}).

Expand All @@ -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()});
Expand All @@ -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}]}].

Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -164,6 +194,9 @@ mod_doc() ->
example =>
["modules:",
" mod_privilege:",
" iq:",
" http://jabber.org/protocol/pubsub:",
" get: all",
" roster:",
" get: all",
" presence:",
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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) ->
Expand All @@ -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)),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 <privileged_iq/> 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),
Expand All @@ -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),
Expand All @@ -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) ->
Expand Down
Loading

0 comments on commit 2f5ff8b

Please sign in to comment.