aboutsummaryrefslogtreecommitdiff
path: root/src/mod_privilege.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/mod_privilege.erl')
-rw-r--r--src/mod_privilege.erl348
1 files changed, 348 insertions, 0 deletions
diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl
new file mode 100644
index 000000000..50212b7ae
--- /dev/null
+++ b/src/mod_privilege.erl
@@ -0,0 +1,348 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 11 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(mod_privilege).
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start_link/2]).
+-export([start/2, stop/1, mod_opt_type/1, depends/2]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+-export([component_connected/1, component_disconnected/2,
+ roster_access/2, process_message/3,
+ process_presence_out/4, process_presence_in/5]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+-include("xmpp.hrl").
+
+-record(state, {server_host = <<"">> :: binary(),
+ permissions = dict:new() :: ?TDICT}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ PingSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
+ transient, 2000, worker, [?MODULE]},
+ supervisor:start_child(ejabberd_sup, PingSpec).
+
+stop(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?MODULE),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+mod_opt_type(roster) -> v_roster();
+mod_opt_type(message) -> v_message();
+mod_opt_type(presence) -> v_presence();
+mod_opt_type(_) ->
+ [roster, message, presence].
+
+depends(_, _) ->
+ [].
+
+-spec component_connected(binary()) -> ok.
+component_connected(Host) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_connected, Host})
+ end, ?MYHOSTS).
+
+-spec component_disconnected(binary(), binary()) -> ok.
+component_disconnected(Host, _Reason) ->
+ lists:foreach(
+ fun(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ gen_server:cast(Proc, {component_disconnected, Host})
+ end, ?MYHOSTS).
+
+-spec process_message(jid(), jid(), stanza()) -> stop | ok.
+process_message(#jid{luser = <<"">>, lresource = <<"">>} = From,
+ #jid{lresource = <<"">>} = To,
+ #message{lang = Lang, type = T} = Msg) when T /= error ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ Permissions = get_permissions(ServerHost),
+ case dict:find(Host, Permissions) of
+ {ok, Access} ->
+ case proplists:get_value(message, Access, none) of
+ outgoing ->
+ forward_message(From, To, Msg);
+ none ->
+ Txt = <<"Insufficient privilege">>,
+ Err = xmpp:err_forbidden(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end,
+ stop;
+ error ->
+ %% Component is disconnected
+ ok
+ end;
+process_message(_From, _To, _Stanza) ->
+ ok.
+
+-spec roster_access(boolean(), iq()) -> boolean().
+roster_access(true, _) ->
+ true;
+roster_access(false, #iq{from = From, to = To, type = Type}) ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ Permissions = get_permissions(ServerHost),
+ case dict:find(Host, Permissions) of
+ {ok, Access} ->
+ Permission = proplists:get_value(roster, Access, none),
+ (Permission == both)
+ orelse (Permission == get andalso Type == get)
+ orelse (Permission == set andalso Type == set);
+ error ->
+ %% Component is disconnected
+ false
+ end.
+
+-spec process_presence_out(stanza(), ejabberd_c2s:state(), jid(), jid()) -> stanza().
+process_presence_out(#presence{type = Type} = Pres, _C2SState,
+ #jid{luser = LUser, lserver = LServer} = From,
+ #jid{luser = LUser, lserver = LServer, lresource = <<"">>})
+ when Type == available; Type == unavailable ->
+ %% Self-presence processing
+ Permissions = get_permissions(LServer),
+ lists:foreach(
+ fun({Host, Access}) ->
+ Permission = proplists:get_value(presence, Access, none),
+ if Permission == roster; Permission == managed_entity ->
+ To = jid:make(Host),
+ ejabberd_router:route(
+ From, To, xmpp:set_from_to(Pres, From, To));
+ true ->
+ ok
+ end
+ end, dict:to_list(Permissions)),
+ Pres;
+process_presence_out(Acc, _, _, _) ->
+ Acc.
+
+-spec process_presence_in(stanza(), ejabberd_c2s:state(),
+ jid(), jid(), jid()) -> stanza().
+process_presence_in(#presence{type = Type} = Pres, _C2SState, _,
+ #jid{luser = U, lserver = S} = From,
+ #jid{luser = LUser, lserver = LServer})
+ when {U, S} /= {LUser, LServer} andalso
+ (Type == available orelse Type == unavailable) ->
+ Permissions = get_permissions(LServer),
+ lists:foreach(
+ fun({Host, Access}) ->
+ case proplists:get_value(presence, Access, none) of
+ roster ->
+ Permission = proplists:get_value(roster, Access, none),
+ if Permission == both; Permission == get ->
+ To = jid:make(Host),
+ ejabberd_router:route(
+ From, To, xmpp:set_from_to(Pres, From, To));
+ true ->
+ ok
+ end;
+ true ->
+ ok
+ end
+ end, dict:to_list(Permissions)),
+ Pres;
+process_presence_in(Acc, _, _, _, _) ->
+ Acc.
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+init([Host, _Opts]) ->
+ ejabberd_hooks:add(component_connected, ?MODULE,
+ component_connected, 50),
+ ejabberd_hooks:add(component_disconnected, ?MODULE,
+ component_disconnected, 50),
+ ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE,
+ process_message, 50),
+ ejabberd_hooks:add(roster_remote_access, Host, ?MODULE,
+ roster_access, 50),
+ ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
+ process_presence_out, 50),
+ ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
+ process_presence_in, 50),
+ {ok, #state{server_host = Host}}.
+
+handle_call(get_permissions, _From, State) ->
+ {reply, {ok, State#state.permissions}, State};
+handle_call(_Request, _From, State) ->
+ Reply = ok,
+ {reply, Reply, State}.
+
+handle_cast({component_connected, Host}, State) ->
+ ServerHost = State#state.server_host,
+ From = jid:make(ServerHost),
+ To = jid:make(Host),
+ RosterPerm = get_roster_permission(ServerHost, Host),
+ PresencePerm = get_presence_permission(ServerHost, Host),
+ MessagePerm = get_message_permission(ServerHost, Host),
+ if RosterPerm /= none, PresencePerm /= none, MessagePerm /= none ->
+ Priv = #privilege{perms = [#privilege_perm{access = message,
+ type = MessagePerm},
+ #privilege_perm{access = roster,
+ type = RosterPerm},
+ #privilege_perm{access = presence,
+ type = PresencePerm}]},
+ ?INFO_MSG("Granting permissions to external "
+ "component '~s': roster = ~s, presence = ~s, "
+ "message = ~s",
+ [Host, RosterPerm, PresencePerm, MessagePerm]),
+ Msg = #message{from = From, to = To, sub_els = [Priv]},
+ ejabberd_router:route(From, To, Msg),
+ Permissions = dict:store(Host, [{roster, RosterPerm},
+ {presence, PresencePerm},
+ {message, MessagePerm}],
+ State#state.permissions),
+ {noreply, State#state{permissions = Permissions}};
+ true ->
+ ?INFO_MSG("Granting no permissions to external component '~s'",
+ [Host]),
+ {noreply, State}
+ end;
+handle_cast({component_disconnected, Host}, State) ->
+ Permissions = dict:erase(Host, State#state.permissions),
+ {noreply, State#state{permissions = Permissions}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ %% Note: we don't remove component_* hooks because they are global
+ %% and might be registered within a module on another virtual host
+ Host = State#state.server_host,
+ ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE,
+ process_message, 50),
+ ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE,
+ roster_access, 50),
+ ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
+ process_presence_out, 50),
+ ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE,
+ process_presence_in, 50).
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+get_permissions(ServerHost) ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?MODULE),
+ try gen_server:call(Proc, get_permissions) of
+ {ok, Permissions} ->
+ Permissions
+ catch exit:{noproc, _} ->
+ %% No module is loaded for this virtual host
+ dict:new()
+ end.
+
+forward_message(From, To, Msg) ->
+ Host = From#jid.lserver,
+ ServerHost = To#jid.lserver,
+ case xmpp:get_subtag(Msg, #privilege{}) of
+ #privilege{forwarded = #forwarded{sub_els = [#message{} = SubEl]}} ->
+ case SubEl#message.from of
+ #jid{lresource = <<"">>, lserver = ServerHost} ->
+ ejabberd_router:route(
+ xmpp:get_from(SubEl), xmpp:get_to(SubEl), SubEl);
+ _ ->
+ Lang = xmpp:get_lang(Msg),
+ Txt = <<"Invalid 'from' attribute">>,
+ Err = xmpp:err_forbidden(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end;
+ _ ->
+ ?ERROR_MSG("got invalid forwarded payload from external "
+ "component '~s':~n~s", [Host, xmpp:pp(Msg)]),
+ Lang = xmpp:get_lang(Msg),
+ Txt = <<"Invalid forwarded payload">>,
+ Err = xmpp:err_bad_request(Txt, Lang),
+ ejabberd_router:route_error(To, From, Msg, Err)
+ end.
+
+get_roster_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, roster,
+ v_roster(), []),
+ 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.
+
+get_message_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, message,
+ v_message(), []),
+ case match_rule(ServerHost, Host, Perms, outgoing) of
+ allow -> outgoing;
+ deny -> none
+ end.
+
+get_presence_permission(ServerHost, Host) ->
+ Perms = gen_mod:get_module_opt(ServerHost, ?MODULE, presence,
+ v_presence(), []),
+ case match_rule(ServerHost, Host, Perms, roster) of
+ allow ->
+ roster;
+ deny ->
+ case match_rule(ServerHost, Host, Perms, managed_entity) of
+ allow -> managed_entity;
+ deny -> none
+ end
+ end.
+
+match_rule(ServerHost, Host, Perms, Type) ->
+ Access = proplists:get_value(Type, Perms, none),
+ acl:match_rule(ServerHost, Access, jid:make(Host)).
+
+v_roster() ->
+ fun(Props) ->
+ lists:map(
+ fun({both, ACL}) -> {both, acl:access_rules_validator(ACL)};
+ ({get, ACL}) -> {get, acl:access_rules_validator(ACL)};
+ ({set, ACL}) -> {set, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.
+
+v_message() ->
+ fun(Props) ->
+ lists:map(
+ fun({outgoing, ACL}) -> {outgoing, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.
+
+v_presence() ->
+ fun(Props) ->
+ lists:map(
+ fun({managed_entity, ACL}) ->
+ {managed_entity, acl:access_rules_validator(ACL)};
+ ({roster, ACL}) ->
+ {roster, acl:access_rules_validator(ACL)}
+ end, Props)
+ end.