Skip to content

Commit

Permalink
Merge pull request #3539 from esl/mu-auth-mods-override
Browse files Browse the repository at this point in the history
Allow to customize auth module list per listener
  • Loading branch information
NelsonVides committed Feb 14, 2022
2 parents 7f04df9 + 48e9f2f commit 414cad3
Show file tree
Hide file tree
Showing 19 changed files with 193 additions and 43 deletions.
1 change: 1 addition & 0 deletions big_tests/default.spec
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
{suites, "tests", domain_removal_SUITE}.
{suites, "tests", mam_send_message_SUITE}.
{suites, "tests", dynamic_domains_SUITE}.
{suites, "tests", auth_methods_for_c2s_SUITE}.

{config, ["test.config"]}.
{logdir, "ct_report"}.
Expand Down
1 change: 1 addition & 0 deletions big_tests/dynamic_domains.spec
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
{suites, "tests", xep_0352_csi_SUITE}.

{suites, "tests", domain_removal_SUITE}.
{suites, "tests", auth_methods_for_c2s_SUITE}.

{config, ["dynamic_domains.config", "test.config"]}.

Expand Down
82 changes: 82 additions & 0 deletions big_tests/tests/auth_methods_for_c2s_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
-module(auth_methods_for_c2s_SUITE).

-compile([export_all, nowarn_export_all]).

-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("exml/include/exml.hrl").

-import(distributed_helper, [mim/0, rpc/4]).

all() ->
[{group, two_methods_enabled}].

groups() ->
[{two_methods_enabled, [parallel],
[can_login_with_allowed_method,
cannot_login_with_not_allowed_method,
can_login_to_another_listener]}].

init_per_suite(Config) ->
Config0 = escalus:init_per_suite(Config),
Config1 = ejabberd_node_utils:init(Config0),
ejabberd_node_utils:backup_config_file(Config1),
Config1.

end_per_suite(Config) ->
ejabberd_node_utils:restore_config_file(Config),
ejabberd_node_utils:restart_application(mongooseim),
escalus:end_per_suite(Config).

init_per_group(_, Config) ->
modify_config_and_restart(Config),
escalus_cleaner:start(Config).

end_per_group(_, _Config) ->
escalus_fresh:clean().

init_per_testcase(TC, Config) ->
Spec = escalus_fresh:freshen_spec(Config, alice),
Clean = register_internal_user(Spec),
[{clean_fn, Clean}, {spec, Spec}|escalus:init_per_testcase(TC, Config)].

end_per_testcase(TC, Config) ->
Clean = proplists:get_value(clean_fn, Config),
Clean(),
escalus:end_per_testcase(TC, Config).

modify_config_and_restart(Config) ->
NewConfigValues = [{auth_method, "internal]\n [auth.dummy"},
{auth_method_opts, false},
{allowed_auth_methods, "\"internal\""}],
ejabberd_node_utils:modify_config_file(NewConfigValues, Config),
ejabberd_node_utils:restart_application(mongooseim).

can_login_with_allowed_method(Config) ->
Spec = proplists:get_value(spec, Config),
{ok, _, _} = escalus_connection:start(Spec).

cannot_login_with_not_allowed_method(Config) ->
Spec = proplists:get_value(spec, Config),
{error, _} = escalus_connection:start([{password, <<"wrong">>}|Spec]).

can_login_to_another_listener(Config) ->
Spec = proplists:get_value(spec, Config),
Spec2 = [{port, ct:get_config({hosts, mim, c2s_tls_port})},
{password, <<"wrong">>}|Spec],
{ok, _, _} = escalus_connection:start(Spec2).

%% Helpers

%% If dummy backend is enabled, it is not possible to create new users
%% (we check if an user does exist before registering the user).
register_internal_user(Spec) ->
#{username := User, server := Server,
password := Password} = maps:from_list(Spec),
LUser = jid:nodeprep(User),
LServer = escalus_utils:jid_to_lower(Server),
HostType = domain_helper:host_type(),
rpc(mim(), ejabberd_auth_internal, try_register,
[HostType, LUser, LServer, Password]),
fun() -> rpc(mim(), ejabberd_auth_internal, remove_user,
[HostType, LUser, LServer]) end.
11 changes: 11 additions & 0 deletions doc/configuration/listen.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ The value of the access rule needs to be either the shaper name or the string `"

Enables ZLIB support, the integer value is a limit for a decompressed output size in bytes (to prevent a successful [ZLIB bomb attack](https://xmpp.org/community/security-notices/uncontrolled-resource-consumption-with-highly-compressed-xmpp-stanzas.html)).

### `listen.c2s.allowed_auth_methods`

* **Syntax:** array of strings. Allowed values: `"internal"`, `"rdbms"`, `"external"`, `"anonymous"`, `"ldap"`, `"jwt"`, `"riak"`, `"http"`, `"pki"`, `"dummy"`
* **Default:** not set
* **Example:** `allowed_auth_methods = ["internal"]`

A subset of enabled methods to login with for this listener.
This option allows to enable only some backends.
It is useful, if you want to have several listeners for different type of users (for example, some users use PKI while other users use LDAP auth).
Same syntax as for `auth.methods` option.

## TLS options for C2S

The following options allow enabling and configuring TLS which makes the client-to-server conenctions secure.
Expand Down
3 changes: 2 additions & 1 deletion include/ejabberd_c2s.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@
csi_buffer = [] :: [mongoose_acc:t()],
hibernate_after = 0 :: non_neg_integer(),
replaced_pids = [] :: [{MonitorRef :: reference(), ReplacedPid :: pid()}],
handlers = #{} :: #{ term() => {module(), atom(), term()} }
handlers = #{} :: #{ term() => {module(), atom(), term()} },
cred_opts :: mongoose_credentials:opts()
}).
-type aux_key() :: atom().
-type aux_value() :: any().
Expand Down
3 changes: 3 additions & 0 deletions rel/files/mongooseim.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@
{{#c2s_dhfile}}
tls.dhfile = {{{c2s_dhfile}}}
{{/c2s_dhfile}}
{{#allowed_auth_methods}}
allowed_auth_methods = [{{{allowed_auth_methods}}}]
{{/allowed_auth_methods}}
{{#secondary_c2s}}

{{{secondary_c2s}}}
Expand Down
18 changes: 15 additions & 3 deletions src/auth/ejabberd_auth.erl
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
-export([check_digest/4]).

-export([auth_modules/1,
auth_methods/1]).
auth_methods/1,
auth_modules_for_host_type/1,
methods_to_modules/1]).

%% Library functions for reuse in ejabberd_auth_* modules
-export([authorize_with_check_password/2]).
Expand Down Expand Up @@ -132,7 +134,6 @@ supports_sasl_module(HostType, SASLModule) ->
-spec authorize(mongoose_credentials:t()) -> {ok, mongoose_credentials:t()}
| {error, not_authorized}.
authorize(Creds) ->
HostType = mongoose_credentials:host_type(Creds),
F = fun(Mod, {_CurResult, CurCreds}) ->
case mongoose_gen_auth:authorize(Mod, CurCreds) of
{ok, NewCreds} ->
Expand All @@ -143,7 +144,7 @@ authorize(Creds) ->
end
end,
Opts = #{default => {not_authorized, Creds}, metric => authorize},
case call_auth_modules_for_host_type(HostType, F, Opts) of
case call_auth_modules_with_creds(Creds, F, Opts) of
Res = {ok, _Creds} -> Res;
{not_authorized, _Creds} -> {error, not_authorized}
end.
Expand Down Expand Up @@ -413,6 +414,10 @@ auth_modules(LServer) ->
-spec auth_modules_for_host_type(HostType :: mongooseim:host_type()) -> [authmodule()].
auth_modules_for_host_type(HostType) ->
Methods = auth_methods(HostType),
methods_to_modules(Methods).

-spec methods_to_modules([atom()]) -> [authmodule()].
methods_to_modules(Methods) ->
[auth_method_to_module(M) || M <- Methods].

-spec auth_methods(mongooseim:host_type()) -> [atom()].
Expand Down Expand Up @@ -508,6 +513,13 @@ call_auth_modules_for_host_type(HostType, F, Opts) ->
Modules = auth_modules_for_host_type(HostType),
call_auth_modules(Modules, F, Opts).

-spec call_auth_modules_with_creds(mongoose_credentials:t(),
mod_fun() | mod_fold_fun(), call_opts()) ->
mod_res() | [mod_res()].
call_auth_modules_with_creds(Creds, F, Opts) ->
Modules = mongoose_credentials:auth_modules(Creds),
call_auth_modules(Modules, F, Opts).

%% @doc Perform a map or a fold operation with function F over the provided Modules
-spec call_auth_modules([authmodule()], mod_fun() | mod_fold_fun(), call_opts()) ->
mod_res() | [mod_res()].
Expand Down
2 changes: 2 additions & 0 deletions src/config/mongoose_config_spec.erl
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ xmpp_listener_items(<<"c2s">>) ->
validate = positive},
<<"max_fsm_queue">> => #option{type = integer,
validate = positive},
<<"allowed_auth_methods">> => #list{items = #option{type = atom,
validate = {module, ejabberd_auth}}},
<<"tls">> => c2s_tls()};
xmpp_listener_items(<<"s2s">>) ->
#{<<"shaper">> => #option{type = atom,
Expand Down
33 changes: 17 additions & 16 deletions src/ejabberd_c2s.erl
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,27 @@
-export_type([broadcast/0, packet/0, state/0]).

-type packet() :: {jid:jid(), jid:jid(), exml:element()}.
-type sock_data() :: term().
-type start_result() :: {error, _}
| {ok, undefined | pid()}
| {ok, undefined | pid(), _}.

%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------

-spec start(_, list())
-> {'error', _} | {'ok', 'undefined' | pid()} | {'ok', 'undefined' | pid(), _}.
-spec start(sock_data(), ejabberd_listener:opts()) -> start_result().
start(SockData, Opts) ->
?SUPERVISOR_START(SockData, Opts).

-spec start_link(sock_data(), ejabberd_listener:opts()) -> start_result().
start_link(SockData, Opts) ->
p1_fsm_old:start_link(
ejabberd_c2s, [SockData, Opts], ?FSMOPTS ++ fsm_limit_opts(Opts)).
p1_fsm_old:start_link(ejabberd_c2s, {SockData, Opts},
?FSMOPTS ++ fsm_limit_opts(Opts)).

socket_type() ->
xml_stream.


%% @doc Return Username, Resource and presence information
get_presence(FsmRef) ->
p1_fsm_old:sync_send_all_state_event(FsmRef, get_presence, 1000).
Expand Down Expand Up @@ -174,14 +177,9 @@ run_remote_hook_after(Delay, Pid, HandlerName, Args) ->
%%% Callback functions from gen_fsm
%%%----------------------------------------------------------------------

%%----------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok, StateName, StateData} |
%% {ok, StateName, StateData, Timeout} |
%% ignore |
%% {stop, StopReason}
%%----------------------------------------------------------------------
init([{SockMod, Socket}, Opts]) ->
-spec init({sock_data(), ejabberd_listener:opts()}) ->
{stop, normal} | {ok, wait_for_stream, state(), non_neg_integer()}.
init({{SockMod, Socket}, Opts}) ->
Access = case lists:keyfind(access, 1, Opts) of
{_, A} -> A;
_ -> all
Expand Down Expand Up @@ -238,6 +236,7 @@ init([{SockMod, Socket}, Opts]) ->
false -> Socket
end,
SocketMonitor = mongoose_transport:monitor(SockMod, Socket1),
CredOpts = mongoose_credentials:make_opts(Opts),
{ok, wait_for_stream, #state{server = ?MYNAME,
socket = Socket1,
sockmod = SockMod,
Expand All @@ -254,7 +253,8 @@ init([{SockMod, Socket}, Opts]) ->
shaper = Shaper,
ip = IP,
lang = ?MYLANG,
hibernate_after= HibernateAfter
hibernate_after= HibernateAfter,
cred_opts = CredOpts
},
?C2S_OPEN_TIMEOUT}
end.
Expand Down Expand Up @@ -344,8 +344,9 @@ stream_start_negotiate_features(#state{} = S) ->
end.

stream_start_features_before_auth(#state{server = Server,
host_type = HostType} = S) ->
Creds0 = mongoose_credentials:new(Server, HostType),
host_type = HostType,
cred_opts = CredOpts} = S) ->
Creds0 = mongoose_credentials:new(Server, HostType, CredOpts),
Creds = maybe_add_cert(Creds0, S),
SASLState = cyrsasl:server_new(<<"jabber">>, Server, HostType, <<>>, [], Creds),
SockMod = (S#state.sockmod):get_sockmod(S#state.socket),
Expand Down
4 changes: 4 additions & 0 deletions src/ejabberd_listener.erl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
-export([format_error/1, socket_error/6]).

-ignore_xref([start_link/0, init/1, start_listener/1, stop_listener/1]).
-export_type([opts/0, mod/0]).

-type opts() :: list().
-type mod() :: module().

-include("mongoose.hrl").

Expand Down
8 changes: 4 additions & 4 deletions src/ejabberd_socket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
%% Function:
%% Description:
%%--------------------------------------------------------------------
-spec start(atom() | tuple(), ejabberd:sockmod(),
Socket :: port(), Opts :: [{atom(), _}]) -> ok.
-spec start(ejabberd_listener:mod(), ejabberd:sockmod(),
Socket :: port(), Opts :: ejabberd_listener:opts()) -> ok.
start(Module, SockMod, Socket, Opts) ->
case Module:socket_type() of
xml_stream ->
Expand All @@ -78,8 +78,8 @@ start(Module, SockMod, Socket, Opts) ->
start_raw_stream(Module, SockMod, Socket, Opts)
end.

-spec start_raw_stream(atom() | tuple(), ejabberd:sockmod(),
Socket :: port(), Opts :: [{atom(), _}]) -> ok.
-spec start_raw_stream(ejabberd_listener:mod(), ejabberd:sockmod(),
Socket :: port(), Opts :: ejabberd_listener:opts()) -> ok.
start_raw_stream(Module, SockMod, Socket, Opts) ->
case Module:start({SockMod, Socket}, Opts) of
{ok, Pid} ->
Expand Down
2 changes: 1 addition & 1 deletion src/mongoose_client_api/mongoose_client_api.erl
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ check_password(JID, Password) ->
{LUser, LServer} = jid:to_lus(JID),
case mongoose_domain_api:get_domain_host_type(LServer) of
{ok, HostType} ->
Creds0 = mongoose_credentials:new(LServer, HostType),
Creds0 = mongoose_credentials:new(LServer, HostType, #{}),
Creds1 = mongoose_credentials:set(Creds0, username, LUser),
Creds2 = mongoose_credentials:set(Creds1, password, Password),
case ejabberd_auth:authorize(Creds2) of
Expand Down
42 changes: 35 additions & 7 deletions src/mongoose_credentials.erl
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
-module(mongoose_credentials).

-export([new/2,
-export([make_opts/1,
new/3,
lserver/1,
host_type/1,
auth_modules/1,
get/2, get/3,
set/3,
extend/2,
register/3]).

-export_type([t/0]).
-export_type([t/0, opts/0]).

-record(mongoose_credentials, {lserver, host_type, registry = [], extra = []}).
-record(mongoose_credentials, {lserver, host_type, registry = [], extra = [], modules}).

-type auth_event() :: any().

Expand All @@ -22,18 +24,44 @@
registry :: [{ejabberd_gen_auth:t(), auth_event()}],
%% These values are dependent on the ejabberd_auth backend in use.
%% Each backend may require different values to be present.
extra :: [proplists:property()] }.
extra :: [proplists:property()],
modules :: [ejabberd_auth:authmodule()] }.

-spec new(jid:lserver(), binary()) -> mongoose_credentials:t().
new(LServer, HostType) when is_binary(LServer), is_binary(HostType) ->
#mongoose_credentials{lserver = LServer, host_type = HostType}.
-type opts() :: #{}.

-spec make_opts(ejabberd_listener:opts()) -> opts().
make_opts(Opts) ->
case lists:keyfind(allowed_auth_methods, 1, Opts) of
{allowed_auth_methods, Methods} ->
#{allowed_modules => ejabberd_auth:methods_to_modules(Methods)};
_ ->
#{}
end.

-spec new(jid:lserver(), binary(), opts()) -> mongoose_credentials:t().
new(LServer, HostType, Opts) when is_binary(LServer), is_binary(HostType) ->
HostTypeModules = ejabberd_auth:auth_modules_for_host_type(HostType),
Modules = filter_modules(HostTypeModules, Opts),
#mongoose_credentials{lserver = LServer, host_type = HostType,
modules = Modules}.

%% Allows to enable only some modules for some listeners
-spec filter_modules([ejabberd_auth:authmodule()], opts()) ->
[ejabberd_auth:authmodule()].
filter_modules(Modules, #{allowed_modules := AllowedModules}) ->
[M || M <- Modules, lists:member(M, AllowedModules)];
filter_modules(Modules, _) ->
Modules.

-spec host_type(t()) -> mongooseim:host_type().
host_type(#mongoose_credentials{host_type = HostType}) -> HostType.

-spec lserver(t()) -> jid:lserver().
lserver(#mongoose_credentials{lserver = S}) -> S.

-spec auth_modules(t()) -> [ejabberd_auth:authmodule()].
auth_modules(#mongoose_credentials{modules = Modules}) -> Modules.

%% @doc Calls erlang:error/2 when Key is not found!
-spec get(t(), Key) -> Value when
Key :: any(),
Expand Down
Loading

0 comments on commit 414cad3

Please sign in to comment.