• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 873

18 Dec 2024 03:24PM UTC coverage: 32.87% (+0.2%) from 32.698%
873

push

github

jsautret
Merge branch 'master' of github.com:processone/ejabberd

25 of 154 new or added lines in 10 files covered. (16.23%)

1847 existing lines in 9 files now uncovered.

14620 of 44478 relevant lines covered (32.87%)

618.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/mod_privilege.erl
1
%%%-------------------------------------------------------------------
2
%%% File    : mod_privilege.erl
3
%%% Author  : Anna Mukharram <amuhar3@gmail.com>
4
%%% Purpose : XEP-0356: Privileged Entity
5
%%%
6
%%%
7
%%% ejabberd, Copyright (C) 2002-2024   ProcessOne
8
%%%
9
%%% This program is free software; you can redistribute it and/or
10
%%% modify it under the terms of the GNU General Public License as
11
%%% published by the Free Software Foundation; either version 2 of the
12
%%% License, or (at your option) any later version.
13
%%%
14
%%% This program is distributed in the hope that it will be useful,
15
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
16
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17
%%% General Public License for more details.
18
%%%
19
%%% You should have received a copy of the GNU General Public License along
20
%%% with this program; if not, write to the Free Software Foundation, Inc.,
21
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
%%%
23
%%%-------------------------------------------------------------------
24
-module(mod_privilege).
25

26
-author('amuhar3@gmail.com').
27

28
-protocol({xep, 356, '0.4.1', '24.10', "complete", ""}).
29

30
-behaviour(gen_server).
31
-behaviour(gen_mod).
32

33
%% API
34
-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]).
35
-export([mod_doc/0]).
36
%% gen_server callbacks
37
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
38
         terminate/2, code_change/3]).
39
-export([component_connected/1, component_disconnected/2,
40
         component_send_packet/1,
41
         roster_access/2, process_message/1,
42
         process_presence_out/1, process_presence_in/1]).
43

44
-include("logger.hrl").
45
-include_lib("xmpp/include/xmpp.hrl").
46
-include("translate.hrl").
47

48
-type roster_permission() :: both | get | set.
49
-type iq_permission() :: both | get | set.
50
-type presence_permission() :: managed_entity | roster.
51
-type message_permission() :: outgoing.
52
-type roster_permissions() :: [{roster_permission(), acl:acl()}].
53
-type iq_permissions() :: [{iq_permission(), acl:acl()}].
54
-type presence_permissions() :: [{presence_permission(), acl:acl()}].
55
-type message_permissions() :: [{message_permission(), acl:acl()}].
56
-type access() :: [{roster, roster_permission()} |
57
                   {iq, [privilege_namespace()]} |
58
                   {presence, presence_permission()} |
59
                   {message, message_permission()}].
60
-type permissions() :: #{binary() => access()}.
61
-record(state, {server_host = <<"">> :: binary()}).
62

63
%%%===================================================================
64
%%% API
65
%%%===================================================================
66
start(Host, Opts) ->
67
    gen_mod:start_child(?MODULE, Host, Opts).
×
68

69
stop(Host) ->
70
    gen_mod:stop_child(?MODULE, Host).
×
71

72
reload(_Host, _NewOpts, _OldOpts) ->
73
    ok.
×
74

75
mod_opt_type(roster) ->
76
    econf:options(
×
77
      #{both => econf:acl(), get => econf:acl(), set => econf:acl()});
78
mod_opt_type(iq) ->
79
    econf:map(
×
80
      econf:binary(),
81
      econf:options(#{both => econf:acl(), get => econf:acl(), set => econf:acl()}));
82
mod_opt_type(message) ->
83
    econf:options(
×
84
      #{outgoing => econf:acl()});
85
mod_opt_type(presence) ->
86
    econf:options(
×
87
      #{managed_entity => econf:acl(), roster => econf:acl()}).
88

89
mod_options(_) ->
90
    [{roster, [{both, none}, {get, none}, {set, none}]},
×
91
     {iq, []},
92
     {presence, [{managed_entity, none}, {roster, none}]},
93
     {message, [{outgoing,none}]}].
94

95
mod_doc() ->
96
    #{desc =>
×
97
          [?T("This module is an implementation of "
98
              "https://xmpp.org/extensions/xep-0356.html"
99
              "[XEP-0356: Privileged Entity]. This extension "
100
              "allows components to have privileged access to "
101
              "other entity data (send messages on behalf of the "
102
              "server or on behalf of a user, get/set user roster, "
103
              "access presence information, etc.). This may be used "
104
              "to write powerful external components, for example "
105
              "implementing an external "
106
              "https://xmpp.org/extensions/xep-0163.html[PEP] or "
107
              "https://xmpp.org/extensions/xep-0313.html[MAM] service."), "",
108
           ?T("By default a component does not have any privileged access. "
109
              "It is worth noting that the permissions grant access to "
110
              "the component to a specific data type for all users of "
111
              "the virtual host on which 'mod_privilege' is loaded."), "",
112
           ?T("Make sure you have a listener configured to connect your "
113
              "component. Check the section about listening ports for more "
114
              "information."), "",
115
           ?T("WARNING: Security issue: Privileged access gives components "
116
              "access to sensitive data, so permission should be granted "
117
              "carefully, only if you trust a component."), "",
118
           ?T("NOTE: This module is complementary to _`mod_delegation`_, "
119
              "but can also be used separately.")],
120
      note => "improved in 24.10",
121
      opts =>
122
          [{roster,
123
            #{value => ?T("Options"),
124
              desc =>
125
                  ?T("This option defines roster permissions. "
126
                     "By default no permissions are given. "
127
                     "The 'Options' are:")},
128
            [{both,
129
              #{value => ?T("AccessName"),
130
                desc =>
131
                    ?T("Sets read/write access to a user's roster. "
132
                       "The default value is 'none'.")}},
133
             {get,
134
              #{value => ?T("AccessName"),
135
                desc =>
136
                    ?T("Sets read access to a user's roster. "
137
                       "The default value is 'none'.")}},
138
             {set,
139
              #{value => ?T("AccessName"),
140
                desc =>
141
                    ?T("Sets write access to a user's roster. "
142
                       "The default value is 'none'.")}}]},
143
           {iq,
144
            #{value => "{Namespace: Options}",
145
              desc =>
146
                  ?T("This option defines namespaces and their IQ permissions. "
147
                     "By default no permissions are given. "
148
                     "The 'Options' are:")},
149
            [{both,
150
              #{value => ?T("AccessName"),
151
                desc =>
152
                    ?T("Allows sending IQ stanzas of type 'get' and 'set'. "
153
                       "The default value is 'none'.")}},
154
             {get,
155
              #{value => ?T("AccessName"),
156
                desc =>
157
                    ?T("Allows sending IQ stanzas of type 'get'. "
158
                       "The default value is 'none'.")}},
159
             {set,
160
              #{value => ?T("AccessName"),
161
                desc =>
162
                    ?T("Allows sending IQ stanzas of type 'set'. "
163
                       "The default value is 'none'.")}}]},
164
           {message,
165
            #{value => ?T("Options"),
166
              desc =>
167
                  ?T("This option defines permissions for messages. "
168
                     "By default no permissions are given. "
169
                     "The 'Options' are:")},
170
            [{outgoing,
171
              #{value => ?T("AccessName"),
172
                desc =>
173
                    ?T("The option defines an access rule for sending "
174
                       "outgoing messages by the component. "
175
                       "The default value is 'none'.")}}]},
176
           {presence,
177
            #{value => ?T("Options"),
178
              desc =>
179
                  ?T("This option defines permissions for presences. "
180
                     "By default no permissions are given. "
181
                     "The 'Options' are:")},
182
            [{managed_entity,
183
              #{value => ?T("AccessName"),
184
                desc =>
185
                    ?T("An access rule that gives permissions to "
186
                       "the component to receive server presences. "
187
                       "The default value is 'none'.")}},
188
             {roster,
189
              #{value => ?T("AccessName"),
190
                desc =>
191
                    ?T("An access rule that gives permissions to "
192
                       "the component to receive the presence of both "
193
                       "the users and the contacts in their roster. "
194
                       "The default value is 'none'.")}}]}],
195
      example =>
196
          ["modules:",
197
           "  mod_privilege:",
198
           "    iq:",
199
           "      http://jabber.org/protocol/pubsub:",
200
           "        get: all",
201
           "    roster:",
202
           "      get: all",
203
           "    presence:",
204
           "      managed_entity: all",
205
           "    message:",
206
           "      outgoing: all"]}.
207

208
depends(_, _) ->
209
    [].
×
210

211
-spec component_connected(binary()) -> ok.
212
component_connected(Host) ->
213
    lists:foreach(
×
214
      fun(ServerHost) ->
215
              Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
×
216
              gen_server:cast(Proc, {component_connected, Host})
×
217
      end, ejabberd_option:hosts()).
218

219
-spec component_disconnected(binary(), binary()) -> ok.
220
component_disconnected(Host, _Reason) ->
221
    lists:foreach(
×
222
      fun(ServerHost) ->
223
              Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
×
224
              gen_server:cast(Proc, {component_disconnected, Host})
×
225
      end, ejabberd_option:hosts()).
226

227
%%
228
%% Message processing
229
%%
230

231
-spec process_message(stanza()) -> stop | ok.
232
process_message(#message{from = #jid{luser = <<"">>, lresource = <<"">>} = From,
233
                         to = #jid{lresource = <<"">>} = To,
234
                         lang = Lang, type = T} = Msg) when T /= error ->
235
    Host = From#jid.lserver,
×
236
    ServerHost = To#jid.lserver,
×
237
    Permissions = get_permissions(ServerHost),
×
238
    case maps:find(Host, Permissions) of
×
239
        {ok, Access} ->
240
            case proplists:get_value(message, Access, none) of
×
241
                outgoing ->
242
                    forward_message(Msg);
×
243
                _ ->
244
                    Txt = ?T("Insufficient privilege"),
×
245
                    Err = xmpp:err_forbidden(Txt, Lang),
×
246
                    ejabberd_router:route_error(Msg, Err)
×
247
            end,
248
            stop;
×
249
        error ->
250
            %% Component is disconnected
251
            ok
×
252
    end;
253

254
process_message(_Stanza) ->
255
    ok.
×
256

257
%%
258
%% IQ processing
259
%%
260

261
%% @format-begin
262

263
component_send_packet({#iq{from = From,
264
                           to = #jid{lresource = <<"">>} = To,
265
                           id = Id,
266
                           type = Type} =
267
                           IQ,
268
                       State})
269
    when Type /= error ->
270
    Host = From#jid.lserver,
×
271
    ServerHost = To#jid.lserver,
×
272
    Permissions = get_permissions(ServerHost),
×
273
    Result =
×
274
        case {maps:find(Host, Permissions), get_iq_encapsulated_details(IQ)} of
275
            {{ok, Access}, {ok, EncapType, EncapNs, EncapFrom, EncIq}}
276
                when (EncapType == Type) and ((EncapFrom == undefined) or (EncapFrom == To)) ->
277
                NsPermissions = proplists:get_value(iq, Access, []),
×
278
                Permission =
×
279
                    case lists:keyfind(EncapNs, 2, NsPermissions) of
280
                        #privilege_namespace{type = AllowedType} ->
281
                            AllowedType;
×
282
                        _ ->
283
                            none
×
284
                    end,
285
                case Permission == both
×
286
                     orelse Permission == get andalso Type == get
×
287
                     orelse Permission == set andalso Type == set
×
288
                of
289
                    true ->
290
                        forward_iq(Host, To, Id, EncIq);
×
291
                    false ->
292
                        ?INFO_MSG("IQ not forwarded: Permission not granted to ns=~s with type=~p",
×
293
                                  [EncapNs, Type]),
×
294
                        drop
×
295
                end;
296
            {error, _} ->
297
                %% Component is disconnected
298
                ?INFO_MSG("IQ not forwarded: Component seems disconnected", []),
×
299
                drop;
×
300
            {_, {ok, E, _, _, _}} when E /= Type ->
301
                ?INFO_MSG("IQ not forwarded: The encapsulated IQ stanza type=~p "
×
302
                          "does not match the top-level IQ stanza type=~p",
303
                          [E, Type]),
×
304
                drop;
×
305
            {_, {ok, _, _, EF, _}} when (EF /= undefined) and (EF /= To) ->
306
                ?INFO_MSG("IQ not forwarded: The FROM attribute in the encapsulated "
×
307
                          "IQ stanza and the TO in top-level IQ stanza do not match",
308
                          []),
×
309
                drop;
×
310
            {_, {error, no_privileged_iq, _Err}} ->
NEW
311
                ?INFO_MSG("IQ not forwarded: Component tried to send not wrapped IQ stanza.", []),
×
312
                drop;
×
313
            {_, {error, roster_query, _Err}} ->
UNCOV
314
                IQ;
×
315
            {_, {error, ErrType, _Err}} ->
316
                ?INFO_MSG("IQ not forwarded: Component tried to send not valid IQ stanza: ~p.",
×
317
                          [ErrType]),
×
UNCOV
318
                drop
×
319
        end,
UNCOV
320
    {Result, State};
×
321
component_send_packet(Acc) ->
UNCOV
322
    Acc.
×
323
%% @format-end
324

325
%%
326
%% Roster processing
327
%%
328

329
-spec roster_access({true, iq()} | false, iq()) -> {true, iq()} | false.
330
roster_access({true, _IQ} = Acc, _) ->
UNCOV
331
    Acc;
×
332
roster_access(false, #iq{from = From, to = To, type = Type} = IQ) ->
333
    Host = From#jid.lserver,
×
334
    ServerHost = To#jid.lserver,
×
335
    Permissions = get_permissions(ServerHost),
×
UNCOV
336
    case maps:find(Host, Permissions) of
×
337
        {ok, Access} ->
338
            Permission = proplists:get_value(roster, Access, none),
×
339
            case (Permission == both)
×
340
                     orelse (Permission == get andalso Type == get)
×
UNCOV
341
                     orelse (Permission == set andalso Type == set) of
×
342
                true ->
UNCOV
343
                    {true, xmpp:put_meta(IQ, privilege_from, To)};
×
344
                false ->
UNCOV
345
                    false
×
346
            end;
347
        error ->
348
            %% Component is disconnected
UNCOV
349
            false
×
350
    end.
351

352
-spec process_presence_out({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}.
353
process_presence_out({#presence{
354
                         from = #jid{luser = LUser, lserver = LServer} = From,
355
                         to = #jid{luser = LUser, lserver = LServer, lresource = <<"">>},
356
                         type = Type} = Pres, C2SState})
357
  when Type == available; Type == unavailable ->
358
    %% Self-presence processing
359
    Permissions = get_permissions(LServer),
×
UNCOV
360
    lists:foreach(
×
361
      fun({Host, Access}) ->
362
              Permission = proplists:get_value(presence, Access, none),
×
363
              if Permission == roster; Permission == managed_entity ->
×
364
                      To = jid:make(Host),
×
UNCOV
365
                      ejabberd_router:route(
×
366
                        xmpp:set_from_to(Pres, From, To));
367
                 true ->
UNCOV
368
                      ok
×
369
              end
370
      end, maps:to_list(Permissions)),
UNCOV
371
    {Pres, C2SState};
×
372
process_presence_out(Acc) ->
UNCOV
373
    Acc.
×
374

375
-spec process_presence_in({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}.
376
process_presence_in({#presence{
377
                        from = #jid{luser = U, lserver = S} = From,
378
                        to = #jid{luser = LUser, lserver = LServer},
379
                        type = Type} = Pres, C2SState})
380
  when {U, S} /= {LUser, LServer} andalso
381
       (Type == available orelse Type == unavailable) ->
382
    Permissions = get_permissions(LServer),
×
UNCOV
383
    lists:foreach(
×
384
      fun({Host, Access}) ->
UNCOV
385
              case proplists:get_value(presence, Access, none) of
×
386
                  roster ->
387
                      Permission = proplists:get_value(roster, Access, none),
×
388
                      if Permission == both; Permission == get ->
×
389
                              To = jid:make(Host),
×
UNCOV
390
                              ejabberd_router:route(
×
391
                                xmpp:set_from_to(Pres, From, To));
392
                         true ->
UNCOV
393
                              ok
×
394
                      end;
395
                 _ ->
UNCOV
396
                      ok
×
397
              end
398
      end, maps:to_list(Permissions)),
UNCOV
399
    {Pres, C2SState};
×
400
process_presence_in(Acc) ->
UNCOV
401
    Acc.
×
402

403
%%%===================================================================
404
%%% gen_server callbacks
405
%%%===================================================================
406
init([Host|_]) ->
407
    process_flag(trap_exit, true),
×
UNCOV
408
    catch ets:new(?MODULE,
×
409
                  [named_table, public,
410
                   {heir, erlang:group_leader(), none}]),
UNCOV
411
    ejabberd_hooks:add(component_connected, ?MODULE,
×
412
                       component_connected, 50),
UNCOV
413
    ejabberd_hooks:add(component_disconnected, ?MODULE,
×
414
                       component_disconnected, 50),
UNCOV
415
    ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE,
×
416
                       process_message, 50),
UNCOV
417
    ejabberd_hooks:add(roster_remote_access, Host, ?MODULE,
×
418
                       roster_access, 50),
UNCOV
419
    ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
×
420
                       process_presence_out, 50),
UNCOV
421
    ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
×
422
                       process_presence_in, 50),
UNCOV
423
    ejabberd_hooks:add(component_send_packet, ?MODULE,
×
424
                       component_send_packet, 50),
UNCOV
425
    {ok, #state{server_host = Host}}.
×
426

427
handle_call(Request, From, State) ->
428
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
×
UNCOV
429
    {noreply, State}.
×
430

431
handle_cast({component_connected, Host}, State) ->
432
    ServerHost = State#state.server_host,
×
433
    From = jid:make(ServerHost),
×
434
    To = jid:make(Host),
×
435
    RosterPerm = get_roster_permission(ServerHost, Host),
×
436
    IqNamespaces = get_iq_namespaces(ServerHost, Host),
×
437
    PresencePerm = get_presence_permission(ServerHost, Host),
×
438
    MessagePerm = get_message_permission(ServerHost, Host),
×
439
    if RosterPerm /= none; IqNamespaces /= []; PresencePerm /= none; MessagePerm /= none ->
×
UNCOV
440
            Priv = #privilege{perms = [#privilege_perm{access = message,
×
441
                                                       type = MessagePerm},
442
                                       #privilege_perm{access = roster,
443
                                                       type = RosterPerm},
444
                                       #privilege_perm{access = iq,
445
                                                       namespaces = IqNamespaces},
446
                                       #privilege_perm{access = presence,
447
                                                       type = PresencePerm}]},
UNCOV
448
            ?INFO_MSG("Granting permissions to external "
×
449
                      "component '~ts': roster = ~ts, presence = ~ts, "
450
                      "message = ~ts,~n iq = ~p",
451
                      [Host, RosterPerm, PresencePerm, MessagePerm, IqNamespaces]),
×
452
            Msg = #message{from = From, to = To,  sub_els = [Priv]},
×
453
            ejabberd_router:route(Msg),
×
UNCOV
454
            Permissions = maps:put(Host, [{roster, RosterPerm},
×
455
                                          {iq, IqNamespaces},
456
                                          {presence, PresencePerm},
457
                                          {message, MessagePerm}],
458
                                   get_permissions(ServerHost)),
459
            ets:insert(?MODULE, {ServerHost, Permissions}),
×
UNCOV
460
            {noreply, State};
×
461
       true ->
462
            ?INFO_MSG("Granting no permissions to external component '~ts'",
×
463
                      [Host]),
×
UNCOV
464
            {noreply, State}
×
465
    end;
466
handle_cast({component_disconnected, Host}, State) ->
467
    ServerHost = State#state.server_host,
×
468
    Permissions = maps:remove(Host, get_permissions(ServerHost)),
×
469
    case maps:size(Permissions) of
×
470
        0 -> ets:delete(?MODULE, ServerHost);
×
UNCOV
471
        _ -> ets:insert(?MODULE, {ServerHost, Permissions})
×
472
    end,
UNCOV
473
    {noreply, State};
×
474
handle_cast(Msg, State) ->
475
    ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
×
UNCOV
476
    {noreply, State}.
×
477

478
handle_info(Info, State) ->
479
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
×
UNCOV
480
    {noreply, State}.
×
481

482
terminate(_Reason, State) ->
483
    Host = State#state.server_host,
×
UNCOV
484
    case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
×
485
        false ->
UNCOV
486
            ejabberd_hooks:delete(component_send_packet, ?MODULE,
×
487
                                  component_send_packet, 50),
UNCOV
488
            ejabberd_hooks:delete(component_connected, ?MODULE,
×
489
                                  component_connected, 50),
UNCOV
490
            ejabberd_hooks:delete(component_disconnected, ?MODULE,
×
491
                                  component_disconnected, 50);
492
        true ->
UNCOV
493
            ok
×
494
    end,
UNCOV
495
    ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE,
×
496
                          process_message, 50),
UNCOV
497
    ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE,
×
498
                          roster_access, 50),
UNCOV
499
    ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
×
500
                          process_presence_out, 50),
UNCOV
501
    ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE,
×
502
                          process_presence_in, 50),
UNCOV
503
    ets:delete(?MODULE, Host).
×
504

505
code_change(_OldVsn, State, _Extra) ->
UNCOV
506
    {ok, State}.
×
507

508
%%%===================================================================
509
%%% Internal functions
510
%%%===================================================================
511
-spec get_permissions(binary()) -> permissions().
512
get_permissions(ServerHost) ->
513
    case ets:lookup(?MODULE, ServerHost) of
×
514
        [] -> #{};
×
UNCOV
515
        [{_, Permissions}] -> Permissions
×
516
    end.
517

518
%%
519
%% Message
520
%%
521

522
-spec forward_message(message()) -> ok.
523
forward_message(#message{to = To} = Msg) ->
524
    ServerHost = To#jid.lserver,
×
525
    Lang = xmpp:get_lang(Msg),
×
526
    CodecOpts = ejabberd_config:codec_options(),
×
UNCOV
527
    try xmpp:try_subtag(Msg, #privilege{}) of
×
528
        #privilege{forwarded = #forwarded{sub_els = [SubEl]}} ->
UNCOV
529
            try xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of
×
530
                #message{} = NewMsg ->
UNCOV
531
                    case NewMsg#message.from of
×
532
                        #jid{lresource = <<"">>, lserver = ServerHost} ->
533
                            FromJID = NewMsg#message.from,
×
534
                            State = #{jid => FromJID},
×
535
                            ejabberd_hooks:run_fold(user_send_packet, FromJID#jid.lserver, {NewMsg, State}, []),
×
UNCOV
536
                            ejabberd_router:route(NewMsg);
×
537
                        _ ->
538
                            Lang = xmpp:get_lang(Msg),
×
539
                            Txt = ?T("Invalid 'from' attribute in forwarded message"),
×
540
                            Err = xmpp:err_forbidden(Txt, Lang),
×
UNCOV
541
                            ejabberd_router:route_error(Msg, Err)
×
542
                    end;
543
                _ ->
544
                    Txt = ?T("Message not found in forwarded payload"),
×
545
                    Err = xmpp:err_bad_request(Txt, Lang),
×
UNCOV
546
                    ejabberd_router:route_error(Msg, Err)
×
547
            catch _:{xmpp_codec, Why} ->
548
                    Txt = xmpp:io_format_error(Why),
×
549
                    Err = xmpp:err_bad_request(Txt, Lang),
×
UNCOV
550
                    ejabberd_router:route_error(Msg, Err)
×
551
            end;
552
        _ ->
553
            Txt = ?T("No <forwarded/> element found"),
×
554
            Err = xmpp:err_bad_request(Txt, Lang),
×
UNCOV
555
            ejabberd_router:route_error(Msg, Err)
×
556
    catch _:{xmpp_codec, Why} ->
557
            Txt = xmpp:io_format_error(Why),
×
558
            Err = xmpp:err_bad_request(Txt, Lang),
×
UNCOV
559
            ejabberd_router:route_error(Msg, Err)
×
560
    end.
561

562
%%
563
%% IQ
564
%%
565

566
%% @format-begin
567

568
-spec get_iq_encapsulated_details(iq()) ->
569
                                     {ok, iq_type(), binary(), jid(), iq()} |
570
                                     {error, Why :: atom(), stanza_error()}.
571
get_iq_encapsulated_details(#iq{sub_els = [IqSub]} = Msg) ->
572
    Lang = xmpp:get_lang(Msg),
×
UNCOV
573
    try xmpp:try_subtag(Msg, #privileged_iq{}) of
×
574
        #privileged_iq{iq = #iq{type = EncapsulatedType, from = From} = EncIq} ->
575
            [IqSubSub] = xmpp:get_els(IqSub),
×
576
            [Element] = xmpp:get_els(IqSubSub),
×
577
            Ns = xmpp:get_ns(Element),
×
UNCOV
578
            {ok, EncapsulatedType, Ns, From, EncIq};
×
579
        _ ->
UNCOV
580
            try xmpp:try_subtag(Msg, #roster_query{}) of
×
581
                #roster_query{} ->
UNCOV
582
                    {error, roster_query, xmpp:err_bad_request()};
×
583
                _ ->
584
                    Txt = ?T("No <privileged_iq/> element found"),
×
585
                    Err = xmpp:err_bad_request(Txt, Lang),
×
UNCOV
586
                    {error, no_privileged_iq, Err}
×
587
            catch
588
                _:{xmpp_codec, Why} ->
589
                    Txt = xmpp:io_format_error(Why),
×
590
                    Err = xmpp:err_bad_request(Txt, Lang),
×
UNCOV
591
                    {error, codec_error, Err}
×
592
            end
593
    catch
594
        _:{xmpp_codec, Why} ->
595
            Txt = xmpp:io_format_error(Why),
×
596
            Err = xmpp:err_bad_request(Txt, Lang),
×
UNCOV
597
            {error, codec_error, Err}
×
598
    end.
599

600
-spec forward_iq(binary(), jid(), binary(), iq()) -> iq().
601
forward_iq(Host, ToplevelTo, Id, Iq) ->
602
    FromJID = ToplevelTo,
×
603
    NewIq0 = Iq#iq{from = FromJID},
×
UNCOV
604
    xmpp:put_meta(NewIq0, privilege_iq, {Id, Host, FromJID}).
×
605
%% @format-end
606

607
%%
608
%% Permissions
609
%%
610

611
-spec get_roster_permission(binary(), binary()) -> roster_permission() | none.
612
get_roster_permission(ServerHost, Host) ->
613
    Perms = mod_privilege_opt:roster(ServerHost),
×
UNCOV
614
    case match_rule(ServerHost, Host, Perms, both) of
×
615
        allow ->
UNCOV
616
            both;
×
617
        deny ->
618
            Get = match_rule(ServerHost, Host, Perms, get),
×
619
            Set = match_rule(ServerHost, Host, Perms, set),
×
620
            if Get == allow, Set == allow -> both;
×
621
               Get == allow -> get;
×
622
               Set == allow -> set;
×
UNCOV
623
               true -> none
×
624
            end
625
    end.
626

627
-spec get_iq_namespaces(binary(), binary()) -> [privilege_namespace()].
628
get_iq_namespaces(ServerHost, Host) ->
629
    NsPerms = mod_privilege_opt:iq(ServerHost),
×
UNCOV
630
    [#privilege_namespace{ns = Ns, type = get_iq_permission(ServerHost, Host, Perms)} || {Ns, Perms} <- NsPerms].
×
631

632
-spec get_iq_permission(binary(), binary(), [iq_permission()]) -> iq_permission() | none.
633
get_iq_permission(ServerHost, Host, Perms) ->
UNCOV
634
    case match_rule(ServerHost, Host, Perms, both) of
×
635
        allow ->
UNCOV
636
            both;
×
637
        deny ->
638
            Get = match_rule(ServerHost, Host, Perms, get),
×
639
            Set = match_rule(ServerHost, Host, Perms, set),
×
640
            if Get == allow, Set == allow -> both;
×
641
               Get == allow -> get;
×
642
               Set == allow -> set;
×
UNCOV
643
               true -> none
×
644
            end
645
    end.
646

647
-spec get_message_permission(binary(), binary()) -> message_permission() | none.
648
get_message_permission(ServerHost, Host) ->
649
    Perms = mod_privilege_opt:message(ServerHost),
×
650
    case match_rule(ServerHost, Host, Perms, outgoing) of
×
651
        allow -> outgoing;
×
UNCOV
652
        deny -> none
×
653
    end.
654

655
-spec get_presence_permission(binary(), binary()) -> presence_permission() | none.
656
get_presence_permission(ServerHost, Host) ->
657
    Perms = mod_privilege_opt:presence(ServerHost),
×
UNCOV
658
    case match_rule(ServerHost, Host, Perms, roster) of
×
659
        allow ->
UNCOV
660
            roster;
×
661
        deny ->
662
            case match_rule(ServerHost, Host, Perms, managed_entity) of
×
663
                allow -> managed_entity;
×
UNCOV
664
                deny -> none
×
665
            end
666
    end.
667

668
-ifdef(OTP_BELOW_26).
669
-dialyzer({no_contracts, match_rule/4}).
670
-endif.
671

672
-spec match_rule(binary(), binary(), roster_permissions(), roster_permission()) -> allow | deny;
673
                (binary(), binary(), iq_permissions(), iq_permission()) -> allow | deny;
674
                (binary(), binary(), presence_permissions(), presence_permission()) -> allow | deny;
675
                (binary(), binary(), message_permissions(), message_permission()) -> allow | deny.
676
match_rule(ServerHost, Host, Perms, Type) ->
677
    Access = proplists:get_value(Type, Perms, none),
×
UNCOV
678
    acl:match_rule(ServerHost, Access, jid:make(Host)).
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc