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

processone / ejabberd / 747

27 Jun 2024 01:43PM UTC coverage: 32.123% (+0.8%) from 31.276%
747

push

github

badlop
Set version to 24.06

14119 of 43953 relevant lines covered (32.12%)

614.73 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.2.1', '16.09', "", ""}).
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
         roster_access/2, process_message/1,
41
         process_presence_out/1, process_presence_in/1]).
42

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

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

59
%%%===================================================================
60
%%% API
61
%%%===================================================================
62
start(Host, Opts) ->
63
    gen_mod:start_child(?MODULE, Host, Opts).
×
64

65
stop(Host) ->
66
    gen_mod:stop_child(?MODULE, Host).
×
67

68
reload(_Host, _NewOpts, _OldOpts) ->
69
    ok.
×
70

71
mod_opt_type(roster) ->
72
    econf:options(
×
73
      #{both => econf:acl(), get => econf:acl(), set => econf:acl()});
74
mod_opt_type(message) ->
75
    econf:options(
×
76
      #{outgoing => econf:acl()});
77
mod_opt_type(presence) ->
78
    econf:options(
×
79
      #{managed_entity => econf:acl(), roster => econf:acl()}).
80

81
mod_options(_) ->
82
    [{roster, [{both, none}, {get, none}, {set, none}]},
×
83
     {presence, [{managed_entity, none}, {roster, none}]},
84
     {message, [{outgoing,none}]}].
85

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

174
depends(_, _) ->
175
    [].
×
176

177
-spec component_connected(binary()) -> ok.
178
component_connected(Host) ->
179
    lists:foreach(
×
180
      fun(ServerHost) ->
181
              Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
×
182
              gen_server:cast(Proc, {component_connected, Host})
×
183
      end, ejabberd_option:hosts()).
184

185
-spec component_disconnected(binary(), binary()) -> ok.
186
component_disconnected(Host, _Reason) ->
187
    lists:foreach(
×
188
      fun(ServerHost) ->
189
              Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
×
190
              gen_server:cast(Proc, {component_disconnected, Host})
×
191
      end, ejabberd_option:hosts()).
192

193
-spec process_message(stanza()) -> stop | ok.
194
process_message(#message{from = #jid{luser = <<"">>, lresource = <<"">>} = From,
195
                         to = #jid{lresource = <<"">>} = To,
196
                         lang = Lang, type = T} = Msg) when T /= error ->
197
    Host = From#jid.lserver,
×
198
    ServerHost = To#jid.lserver,
×
199
    Permissions = get_permissions(ServerHost),
×
200
    case maps:find(Host, Permissions) of
×
201
        {ok, Access} ->
202
            case proplists:get_value(message, Access, none) of
×
203
                outgoing ->
204
                    forward_message(Msg);
×
205
                _ ->
206
                    Txt = ?T("Insufficient privilege"),
×
207
                    Err = xmpp:err_forbidden(Txt, Lang),
×
208
                    ejabberd_router:route_error(Msg, Err)
×
209
            end,
210
            stop;
×
211
        error ->
212
            %% Component is disconnected
213
            ok
×
214
    end;
215
process_message(_Stanza) ->
216
    ok.
×
217

218
-spec roster_access({true, iq()} | false, iq()) -> {true, iq()} | false.
219
roster_access({true, _IQ} = Acc, _) ->
220
    Acc;
×
221
roster_access(false, #iq{from = From, to = To, type = Type} = IQ) ->
222
    Host = From#jid.lserver,
×
223
    ServerHost = To#jid.lserver,
×
224
    Permissions = get_permissions(ServerHost),
×
225
    case maps:find(Host, Permissions) of
×
226
        {ok, Access} ->
227
            Permission = proplists:get_value(roster, Access, none),
×
228
            case (Permission == both)
×
229
                     orelse (Permission == get andalso Type == get)
×
230
                     orelse (Permission == set andalso Type == set) of
×
231
                true ->
232
                    {true, xmpp:put_meta(IQ, privilege_from, To)};
×
233
                false ->
234
                    false
×
235
            end;
236
        error ->
237
            %% Component is disconnected
238
            false
×
239
    end.
240

241
-spec process_presence_out({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}.
242
process_presence_out({#presence{
243
                         from = #jid{luser = LUser, lserver = LServer} = From,
244
                         to = #jid{luser = LUser, lserver = LServer, lresource = <<"">>},
245
                         type = Type} = Pres, C2SState})
246
  when Type == available; Type == unavailable ->
247
    %% Self-presence processing
248
    Permissions = get_permissions(LServer),
×
249
    lists:foreach(
×
250
      fun({Host, Access}) ->
251
              Permission = proplists:get_value(presence, Access, none),
×
252
              if Permission == roster; Permission == managed_entity ->
×
253
                      To = jid:make(Host),
×
254
                      ejabberd_router:route(
×
255
                        xmpp:set_from_to(Pres, From, To));
256
                 true ->
257
                      ok
×
258
              end
259
      end, maps:to_list(Permissions)),
260
    {Pres, C2SState};
×
261
process_presence_out(Acc) ->
262
    Acc.
×
263

264
-spec process_presence_in({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}.
265
process_presence_in({#presence{
266
                        from = #jid{luser = U, lserver = S} = From,
267
                        to = #jid{luser = LUser, lserver = LServer},
268
                        type = Type} = Pres, C2SState})
269
  when {U, S} /= {LUser, LServer} andalso
270
       (Type == available orelse Type == unavailable) ->
271
    Permissions = get_permissions(LServer),
×
272
    lists:foreach(
×
273
      fun({Host, Access}) ->
274
              case proplists:get_value(presence, Access, none) of
×
275
                  roster ->
276
                      Permission = proplists:get_value(roster, Access, none),
×
277
                      if Permission == both; Permission == get ->
×
278
                              To = jid:make(Host),
×
279
                              ejabberd_router:route(
×
280
                                xmpp:set_from_to(Pres, From, To));
281
                         true ->
282
                              ok
×
283
                      end;
284
                 _ ->
285
                      ok
×
286
              end
287
      end, maps:to_list(Permissions)),
288
    {Pres, C2SState};
×
289
process_presence_in(Acc) ->
290
    Acc.
×
291

292
%%%===================================================================
293
%%% gen_server callbacks
294
%%%===================================================================
295
init([Host|_]) ->
296
    process_flag(trap_exit, true),
×
297
    catch ets:new(?MODULE,
×
298
                  [named_table, public,
299
                   {heir, erlang:group_leader(), none}]),
300
    ejabberd_hooks:add(component_connected, ?MODULE,
×
301
                       component_connected, 50),
302
    ejabberd_hooks:add(component_disconnected, ?MODULE,
×
303
                       component_disconnected, 50),
304
    ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE,
×
305
                       process_message, 50),
306
    ejabberd_hooks:add(roster_remote_access, Host, ?MODULE,
×
307
                       roster_access, 50),
308
    ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
×
309
                       process_presence_out, 50),
310
    ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
×
311
                       process_presence_in, 50),
312
    {ok, #state{server_host = Host}}.
×
313

314
handle_call(Request, From, State) ->
315
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
×
316
    {noreply, State}.
×
317

318
handle_cast({component_connected, Host}, State) ->
319
    ServerHost = State#state.server_host,
×
320
    From = jid:make(ServerHost),
×
321
    To = jid:make(Host),
×
322
    RosterPerm = get_roster_permission(ServerHost, Host),
×
323
    PresencePerm = get_presence_permission(ServerHost, Host),
×
324
    MessagePerm = get_message_permission(ServerHost, Host),
×
325
    if RosterPerm /= none; PresencePerm /= none; MessagePerm /= none ->
×
326
            Priv = #privilege{perms = [#privilege_perm{access = message,
×
327
                                                       type = MessagePerm},
328
                                       #privilege_perm{access = roster,
329
                                                       type = RosterPerm},
330
                                       #privilege_perm{access = presence,
331
                                                       type = PresencePerm}]},
332
            ?INFO_MSG("Granting permissions to external "
×
333
                      "component '~ts': roster = ~ts, presence = ~ts, "
334
                      "message = ~ts",
335
                      [Host, RosterPerm, PresencePerm, MessagePerm]),
×
336
            Msg = #message{from = From, to = To,  sub_els = [Priv]},
×
337
            ejabberd_router:route(Msg),
×
338
            Permissions = maps:put(Host, [{roster, RosterPerm},
×
339
                                          {presence, PresencePerm},
340
                                          {message, MessagePerm}],
341
                                   get_permissions(ServerHost)),
342
            ets:insert(?MODULE, {ServerHost, Permissions}),
×
343
            {noreply, State};
×
344
       true ->
345
            ?INFO_MSG("Granting no permissions to external component '~ts'",
×
346
                      [Host]),
×
347
            {noreply, State}
×
348
    end;
349
handle_cast({component_disconnected, Host}, State) ->
350
    ServerHost = State#state.server_host,
×
351
    Permissions = maps:remove(Host, get_permissions(ServerHost)),
×
352
    case maps:size(Permissions) of
×
353
        0 -> ets:delete(?MODULE, ServerHost);
×
354
        _ -> ets:insert(?MODULE, {ServerHost, Permissions})
×
355
    end,
356
    {noreply, State};
×
357
handle_cast(Msg, State) ->
358
    ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
×
359
    {noreply, State}.
×
360

361
handle_info(Info, State) ->
362
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
×
363
    {noreply, State}.
×
364

365
terminate(_Reason, State) ->
366
    Host = State#state.server_host,
×
367
    case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
×
368
        false ->
369
            ejabberd_hooks:delete(component_connected, ?MODULE,
×
370
                                  component_connected, 50),
371
            ejabberd_hooks:delete(component_disconnected, ?MODULE,
×
372
                                  component_disconnected, 50);
373
        true ->
374
            ok
×
375
    end,
376
    ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE,
×
377
                          process_message, 50),
378
    ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE,
×
379
                          roster_access, 50),
380
    ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
×
381
                          process_presence_out, 50),
382
    ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE,
×
383
                          process_presence_in, 50),
384
    ets:delete(?MODULE, Host).
×
385

386
code_change(_OldVsn, State, _Extra) ->
387
    {ok, State}.
×
388

389
%%%===================================================================
390
%%% Internal functions
391
%%%===================================================================
392
-spec get_permissions(binary()) -> permissions().
393
get_permissions(ServerHost) ->
394
    try ets:lookup_element(?MODULE, ServerHost, 2)
×
395
    catch _:badarg -> #{}
×
396
    end.
397

398
-spec forward_message(message()) -> ok.
399
forward_message(#message{to = To} = Msg) ->
400
    ServerHost = To#jid.lserver,
×
401
    Lang = xmpp:get_lang(Msg),
×
402
    CodecOpts = ejabberd_config:codec_options(),
×
403
    try xmpp:try_subtag(Msg, #privilege{}) of
×
404
        #privilege{forwarded = #forwarded{sub_els = [SubEl]}} ->
405
            try xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of
×
406
                #message{} = NewMsg ->
407
                    case NewMsg#message.from of
×
408
                        #jid{lresource = <<"">>, lserver = ServerHost} ->
409
                            FromJID = NewMsg#message.from,
×
410
                            State = #{jid => FromJID},
×
411
                            ejabberd_hooks:run_fold(user_send_packet, FromJID#jid.lserver, {NewMsg, State}, []),
×
412
                            ejabberd_router:route(NewMsg);
×
413
                        _ ->
414
                            Lang = xmpp:get_lang(Msg),
×
415
                            Txt = ?T("Invalid 'from' attribute in forwarded message"),
×
416
                            Err = xmpp:err_forbidden(Txt, Lang),
×
417
                            ejabberd_router:route_error(Msg, Err)
×
418
                    end;
419
                _ ->
420
                    Txt = ?T("Message not found in forwarded payload"),
×
421
                    Err = xmpp:err_bad_request(Txt, Lang),
×
422
                    ejabberd_router:route_error(Msg, Err)
×
423
            catch _:{xmpp_codec, Why} ->
424
                    Txt = xmpp:io_format_error(Why),
×
425
                    Err = xmpp:err_bad_request(Txt, Lang),
×
426
                    ejabberd_router:route_error(Msg, Err)
×
427
            end;
428
        _ ->
429
            Txt = ?T("No <forwarded/> element found"),
×
430
            Err = xmpp:err_bad_request(Txt, Lang),
×
431
            ejabberd_router:route_error(Msg, Err)
×
432
    catch _:{xmpp_codec, Why} ->
433
            Txt = xmpp:io_format_error(Why),
×
434
            Err = xmpp:err_bad_request(Txt, Lang),
×
435
            ejabberd_router:route_error(Msg, Err)
×
436
    end.
437

438
-spec get_roster_permission(binary(), binary()) -> roster_permission() | none.
439
get_roster_permission(ServerHost, Host) ->
440
    Perms = mod_privilege_opt:roster(ServerHost),
×
441
    case match_rule(ServerHost, Host, Perms, both) of
×
442
        allow ->
443
            both;
×
444
        deny ->
445
            Get = match_rule(ServerHost, Host, Perms, get),
×
446
            Set = match_rule(ServerHost, Host, Perms, set),
×
447
            if Get == allow, Set == allow -> both;
×
448
               Get == allow -> get;
×
449
               Set == allow -> set;
×
450
               true -> none
×
451
            end
452
    end.
453

454
-spec get_message_permission(binary(), binary()) -> message_permission() | none.
455
get_message_permission(ServerHost, Host) ->
456
    Perms = mod_privilege_opt:message(ServerHost),
×
457
    case match_rule(ServerHost, Host, Perms, outgoing) of
×
458
        allow -> outgoing;
×
459
        deny -> none
×
460
    end.
461

462
-spec get_presence_permission(binary(), binary()) -> presence_permission() | none.
463
get_presence_permission(ServerHost, Host) ->
464
    Perms = mod_privilege_opt:presence(ServerHost),
×
465
    case match_rule(ServerHost, Host, Perms, roster) of
×
466
        allow ->
467
            roster;
×
468
        deny ->
469
            case match_rule(ServerHost, Host, Perms, managed_entity) of
×
470
                allow -> managed_entity;
×
471
                deny -> none
×
472
            end
473
    end.
474

475
-spec match_rule(binary(), binary(), roster_permissions(), roster_permission()) -> allow | deny;
476
                (binary(), binary(), presence_permissions(), presence_permission()) -> allow | deny;
477
                (binary(), binary(), message_permissions(), message_permission()) -> allow | deny.
478
match_rule(ServerHost, Host, Perms, Type) ->
479
    Access = proplists:get_value(Type, Perms, none),
×
480
    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