Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
chrzaszcz committed Jan 14, 2022
1 parent 74c8d10 commit 3f66cb1
Show file tree
Hide file tree
Showing 23 changed files with 242 additions and 297 deletions.
2 changes: 1 addition & 1 deletion big_tests/tests/login_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ set_acl_for_blocking(Config, Spec) ->
User = proplists:get_value(username, Spec),
LUser = jid:nodeprep(User),
mongoose_helper:backup_and_set_config_option(Config, {acl, blocked, host_type()},
[{user, LUser}]).
[#{user => LUser}]).

unset_acl_for_blocking(Config) ->
mongoose_helper:restore_config_option(Config, {acl, blocked, host_type()}).
Expand Down
176 changes: 51 additions & 125 deletions src/acl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -26,76 +26,42 @@
-module(acl).
-author('[email protected]').

-export([match_rule/3,
match_rule/4,
match_rule_for_host_type/4,
match_rule_for_host_type/5]).

-export([prepare_spec/1]).

-ignore_xref([add/3, delete/3, match_rule/4]).
-export([match_rule/4, match_rule/5]).

-include("jlib.hrl").
-include("mongoose.hrl").

-export_type([rule/0, domain_or_global/0, host_type_or_global/0]).
-export_type([rule/0]).

-type rule() :: 'all' | 'none' | atom().
-type domain_or_global() :: jid:lserver() | global.
-type host_type_or_global() :: mongooseim:host_type() | global.
-type aclspec() :: #{match => all | none} | #{jid_part() => acl_pattern()}.
-type jid_part() :: user | server | resource.
-type acl_pattern() :: {value | regexp | glob, binary()}.
-type rule() :: atom().
-type acl_spec() :: #{match := all | none | valid_domain} |
#{acl_spec_key() => binary()}.
-type acl_spec_key() :: user | user_regexp | user_glob
| server | server_regexp | server_glob
| resource | resource_regexp | resource_glob.

-type acl_result() :: allow | deny | term().

%% legacy API, use match_rule_for_host_type instead
-spec match_rule(Domain :: domain_or_global(),
Rule :: rule(),
JID :: jid:jid()) -> acl_result().
match_rule(Domain, Rule, JID) ->
match_rule(Domain, Rule, JID, deny).

-spec match_rule_for_host_type(HostType :: host_type_or_global(),
Domain :: domain_or_global(),
Rule :: rule(),
JID :: jid:jid()) -> acl_result().
match_rule_for_host_type(HostType, Domain, Rule, JID) ->
match_rule_for_host_type(HostType, Domain, Rule, JID, deny).

%% legacy API, use match_rule_for_host_type instead
-spec match_rule(Domain :: domain_or_global(),
Rule :: rule(),
JID :: jid:jid(),
Default :: acl_result()) -> acl_result().
match_rule(Domain, Rule, JID, Default) ->
%% We don't want to cast Domain to HostType here.
%% Developers should start using match_rule_for_host_type explicitly.
match_rule_for_host_type(Domain, Domain, Rule, JID, Default).

%% HostType determines which rules and ACLs are checked:
%% - 'global' - only global ones
%% - a specific host type - both global and per-host-type ones
%% Domain is only used for validating the user's domain name:
%% - 'global' - any domain hosted by the server is accepted
%% - a specific domain name - only the provided name is accepted
-spec match_rule_for_host_type(HostType :: host_type_or_global(),
Domain :: domain_or_global(),
Rule :: rule(),
JID :: jid:jid(),
Default :: acl_result()) -> acl_result().
match_rule_for_host_type(_HostType, _, all, _, _Default) ->
-spec match_rule(mongooseim:host_type_or_global(), jid:lserver(), rule(), jid:jid()) ->
acl_result().
match_rule(_HostType, _Domain, all, _JID) ->
allow;
match_rule_for_host_type(_HostType, _, none, _, _Default) ->
match_rule(_HostType, _Domain, none, _JID) ->
deny;
match_rule_for_host_type(global, Domain, Rule, JID, Default) ->
match_rule(HostType, Domain, Rule, JID) ->
match_rule(HostType, Domain, Rule, JID, deny).

-spec match_rule(mongooseim:host_type_or_global(), jid:lserver(), rule(), jid:jid(),
Default :: acl_result()) ->
acl_result().
match_rule(global, Domain, Rule, JID, Default) ->
case mongoose_config:lookup_opt({access, Rule, global}) of
{error, not_found} ->
Default;
{ok, GACLs} ->
match_acls(GACLs, JID, global, Domain)
{ok, ACLs} ->
match_acls(ACLs, JID, global, Domain)
end;
match_rule_for_host_type(HostType, Domain, Rule, JID, Default) ->
match_rule(HostType, Domain, Rule, JID, Default) ->
case {mongoose_config:lookup_opt({access, Rule, global}),
mongoose_config:lookup_opt({access, Rule, HostType})} of
{{error, not_found}, {error, not_found}} ->
Expand All @@ -117,10 +83,8 @@ merge_acls(Global, HostLocal) ->
Global ++ HostLocal
end.

-spec match_acls(ACLs :: [{any(), rule()}],
JID :: jid:jid(),
HostType :: host_type_or_global(),
Domain :: domain_or_global()) -> deny | term().
-spec match_acls(ACLs :: [{any(), rule()}], jid:jid(), mongooseim:host_type(), jid:lserver()) ->
acl_result().
match_acls([], _, _HostType, _Domain) ->
deny;
match_acls([{Value, Rule} | ACLs], JID, HostType, Domain) ->
Expand All @@ -131,10 +95,7 @@ match_acls([{Value, Rule} | ACLs], JID, HostType, Domain) ->
match_acls(ACLs, JID, HostType, Domain)
end.

-spec match_acl(Rule :: rule(),
JID :: jid:jid(),
HostType :: host_type_or_global(),
Domain :: domain_or_global()) -> boolean().
-spec match_acl(rule(), jid:jid(), mongooseim:host_type_or_global(), jid:lserver()) -> boolean().
match_acl(all, _JID, _HostType, _Domain) ->
true;
match_acl(none, _JID, _HostType, _Domain) ->
Expand All @@ -144,71 +105,36 @@ match_acl(Rule, JID, HostType, Domain) ->
global -> get_acl_specs(Rule, global);
_ -> get_acl_specs(Rule, HostType) ++ get_acl_specs(Rule, global)
end,
Pred = fun(ACLSpec) -> match(ACLSpec, JID) end,
is_server_valid(Domain, JID#jid.lserver) andalso lists:any(Pred, AllSpecs).
Pred = fun(ACLSpec) -> match(ACLSpec, Domain, JID) end,
lists:any(Pred, AllSpecs).

-spec get_acl_specs(rule(), host_type_or_global()) -> [aclspec()].
-spec get_acl_specs(rule(), mongooseim:host_type()) -> [acl_spec()].
get_acl_specs(Rule, HostType) ->
mongoose_config:get_opt({acl, Rule, HostType}, []).

-spec is_server_valid(domain_or_global(), jid:lserver()) -> boolean().
is_server_valid(Domain, Domain) ->
true;
is_server_valid(global, JIDServer) ->
case mongoose_domain_api:get_domain_host_type(JIDServer) of
{ok, _HostType} ->
true;
_ ->
false
end;
is_server_valid(_Domain, _JIDServer) ->
false.

-spec match(aclspec(), jid:jid()) -> boolean().
match(#{match := Match}, _JID) ->
Match =:= all;
match(M, JID) ->
lists:all(fun({K, V}) -> check(K, V, JID) end, maps:to_list(M)).

-spec check(jid_part(), acl_pattern(), jid:jid()) -> boolean().
check(user, Spec, #jid{luser = LUser}) -> check(Spec, LUser);
check(server, Spec, #jid{lserver = LServer}) -> check(Spec, LServer);
check(resource, Spec, #jid{lresource = LResource}) -> check(Spec, LResource).

-spec check(acl_pattern(), binary()) -> boolean().
check({regexp, Regexp}, Value) -> is_regexp_match(Value, Regexp);
check({glob, Glob}, Value) -> is_glob_match(Value, Glob);
check({value, ExpValue}, Value) -> ExpValue =:= Value.

-spec prepare_spec(map()) -> aclspec().
prepare_spec(M) ->
KVs = [prepare_check(K, V) || {K, V} <- maps:to_list(M)],
case conflicts(proplists:get_keys(KVs)) of
[] -> maps:from_list(KVs);
Conflicts ->
error(#{what => wrong_acl_expression,
text => <<"Wrong ACL expression in the configuration file">>,
wrong_spec => M,
conflicts => Conflicts})
end.

-spec conflicts([atom()]) -> [atom()].
conflicts(Keys) ->
case lists:member(match, Keys) of
true -> Keys -- [match];
false -> Keys -- [user, server, resource]
end.

prepare_check(user, User) -> {user, {value, User}};
prepare_check(server, Server) -> {server, {value, Server}};
prepare_check(resource, Resource) -> {resource, {value, Resource}};
prepare_check(user_regexp, Regexp) -> {user, {regexp, Regexp}};
prepare_check(server_regexp, Regexp) -> {server, {regexp, Regexp}};
prepare_check(resource_regexp, Regexp) -> {resource, {regexp, Regexp}};
prepare_check(user_glob, Glob) -> {user, {glob, Glob}};
prepare_check(server_glob, Glob) -> {server, {glob, Glob}};
prepare_check(resource_glob, Glob) -> {resource, {glob, Glob}};
prepare_check(match, Value) -> {match, Value}.
%% @doc Check if all conditions from ACLSpec are satisfied by JID
-spec match(acl_spec(), jid:lserver(), jid:jid()) -> boolean().
match(ACLSpec, Domain, JID) ->
match_step(maps:next(maps:iterator(ACLSpec)), Domain, JID).

match_step({K, V, I}, Domain, JID) ->
check(K, V, Domain, JID) andalso match_step(maps:next(I), Domain, JID);
match_step(none, _Domain, _JID) ->
true.

-spec check(acl_spec_key(), binary(), jid:lserver(), jid:jid()) -> boolean().
check(match, all, _, _) -> true;
check(match, none, _, _) -> false;
check(match, current_domain, Domain, JID) -> JID#jid.lserver =:= Domain;
check(user, User, _, JID) -> JID#jid.luser =:= User;
check(user_regexp, Regexp, _, JID) -> is_regexp_match(JID#jid.luser, Regexp);
check(user_glob, Glob, _, JID) -> is_glob_match(JID#jid.luser, Glob);
check(server, Server, _, JID) -> JID#jid.lserver =:= Server;
check(server_regexp, Regexp, _, JID) -> is_regexp_match(JID#jid.lserver, Regexp);
check(server_glob, Glob, _, JID) -> is_glob_match(JID#jid.lserver, Glob);
check(resource, Resource, _, JID) -> JID#jid.lresource =:= Resource;
check(resource_regexp, Regexp, _, JID) -> is_regexp_match(JID#jid.lresource, Regexp);
check(resource_glob, Glob, _, JID) -> is_glob_match(JID#jid.lresource, Glob).

-spec is_regexp_match(binary(), RegExp :: iodata()) -> boolean().
is_regexp_match(String, RegExp) ->
Expand Down
35 changes: 22 additions & 13 deletions src/config/mongoose_config_spec.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
process_riak_credentials/1,
process_iqdisc/1,
process_shaper/1,
process_acl_condition/1,
process_access_rule_item/1,
process_s2s_address_family/1,
process_s2s_host_policy/1,
Expand Down Expand Up @@ -824,22 +825,23 @@ acl() ->

%% path: (host_config[].)acl.*[]
acl_item() ->
Cond = #option{type = binary,
process = fun ?MODULE:process_acl_condition/1},
#section{
items = #{<<"match">> => #option{type = atom,
validate = {enum, [all, none]}},
<<"user">> => #option{type = binary},
<<"server">> => #option{type = binary},
<<"resource">> => #option{type = binary},
<<"user_regexp">> => #option{type = binary},
<<"server_regexp">> => #option{type = binary},
<<"resource_regexp">> => #option{type = binary},
<<"user_glob">> => #option{type = binary},
<<"server_glob">> => #option{type = binary},
<<"resource_glob">> => #option{type = binary}
validate = {enum, [all, none, current_domain]}},
<<"user">> => Cond,
<<"server">> => Cond,
<<"resource">> => Cond,
<<"user_regexp">> => Cond,
<<"server_regexp">> => Cond,
<<"resource_regexp">> => Cond,
<<"user_glob">> => Cond,
<<"server_glob">> => Cond,
<<"resource_glob">> => Cond
},
validate_keys = non_empty,
format_items = map,
process = fun acl:prepare_spec/1
defaults = #{<<"match">> => current_domain},
format_items = map
}.

%% path: (host_config[].)access
Expand Down Expand Up @@ -1235,6 +1237,13 @@ wpool_strategy_values() ->
process_shaper([MaxRate]) ->
MaxRate.

process_acl_condition(Value) ->
case jid:nodeprep(Value) of
error -> error(#{what => incorrect_acl_condition_value,
text => <<"Value could not be parsed as a JID node part">>});
Node -> Node
end.

process_access_rule_item(KVs) ->
{[[{acl, Acl}], [{value, Value}]], []} = proplists:split(KVs, [acl, value]),
{Value, Acl}.
Expand Down
11 changes: 5 additions & 6 deletions src/ejabberd_c2s.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1612,11 +1612,10 @@ generate_random_resource() ->
<<(mongoose_bin:gen_from_crypto())/binary, (mongoose_bin:gen_from_timestamp())/binary>>.

-spec change_shaper(state(), jid:jid()) -> any().
change_shaper(StateData, JID) ->
Shaper = acl:match_rule(StateData#state.server,
StateData#state.shaper, JID),
(StateData#state.sockmod):change_shaper(StateData#state.socket, Shaper).

change_shaper(#state{host_type = HostType, server = Server, shaper = ShaperRule,
socket = Socket, sockmod = SockMod}, JID) ->
Shaper = acl:match_rule(HostType, Server, ShaperRule, JID),
SockMod:change_shaper(Socket, Shaper).

-spec send_text(state(), Text :: binary()) -> any().
send_text(StateData, Text) ->
Expand Down Expand Up @@ -3316,7 +3315,7 @@ handle_sasl_step(#state{host_type = HostType, server = Server, socket = Sock} =
end.

user_allowed(JID, #state{host_type = HostType, server = Server, access = Access}) ->
case acl:match_rule_for_host_type(HostType, Server, Access, JID) of
case acl:match_rule(HostType, Server, Access, JID) of
allow ->
open_session_allowed_hook(HostType, JID);
deny ->
Expand Down
3 changes: 2 additions & 1 deletion src/ejabberd_commands.erl
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,8 @@ check_access(Access, Auth) ->
{ok, JID} = check_auth(Auth),
%% Check this user has access permission
{_, LServer} = jid:to_lus(JID),
case acl:match_rule(LServer, Access, JID) of
{ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer),
case acl:match_rule(HostType, LServer, Access, JID) of
allow -> true;
deny -> false
end.
Expand Down
6 changes: 4 additions & 2 deletions src/ejabberd_s2s.erl
Original file line number Diff line number Diff line change
Expand Up @@ -437,16 +437,18 @@ new_connection(MyServer, Server, From, FromTo = {FromServer, ToServer},

-spec max_s2s_connections_number(fromto()) -> pos_integer().
max_s2s_connections_number({From, To}) ->
{ok, HostType} = mongoose_domain_api:get_host_type(From),
case acl:match_rule(
From, max_s2s_connections, jid:make(<<"">>, To, <<"">>)) of
HostType, To, max_s2s_connections, jid:make(<<"">>, To, <<"">>)) of
Max when is_integer(Max) -> Max;
_ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER
end.

-spec max_s2s_connections_number_per_node(fromto()) -> pos_integer().
max_s2s_connections_number_per_node({From, To}) ->
{ok, HostType} = mongoose_domain_api:get_host_type(From),
case acl:match_rule(
From, max_s2s_connections_per_node, jid:make(<<"">>, To, <<"">>)) of
HostType, To, max_s2s_connections_per_node, jid:make(<<"">>, To, <<"">>)) of
Max when is_integer(Max) -> Max;
_ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE
end.
Expand Down
5 changes: 3 additions & 2 deletions src/ejabberd_s2s_in.erl
Original file line number Diff line number Diff line change
Expand Up @@ -586,9 +586,10 @@ stream_features(Domain) ->
{error, not_found} -> []
end.

-spec change_shaper(state(), Host :: 'global' | binary(), jid:jid()) -> any().
-spec change_shaper(state(), Host :: binary(), jid:jid()) -> any().
change_shaper(StateData, Host, JID) ->
Shaper = acl:match_rule(Host, StateData#state.shaper, JID),
{ok, HostType} = mongoose_domain_api:get_host_type(Host),
Shaper = acl:match_rule(HostType, Host, StateData#state.shaper, JID),
(StateData#state.sockmod):change_shaper(StateData#state.socket, Shaper).


Expand Down
2 changes: 1 addition & 1 deletion src/ejabberd_service.erl
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ handle_info({route, From, To, Acc}, StateName, StateData) ->
?LOG_DEBUG(#{what => comp_route,
text => <<"Route packet to an external component">>,
component => component_host(StateData), acc => Acc}),
case acl:match_rule(global, StateData#state.access, From) of
case acl:match_rule(global, From#jid.lserver, StateData#state.access, From) of
allow ->
mongoose_hooks:packet_to_component(Acc, From, To),
Attrs2 = jlib:replace_from_to_attrs(jid:to_binary(From),
Expand Down
2 changes: 1 addition & 1 deletion src/ejabberd_sm.erl
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,7 @@ check_max_sessions(HostType, LUser, LServer, ReplacedPIDs) ->
Result :: infinity | pos_integer().
get_max_user_sessions(HostType, LUser, LServer) ->
JID = jid:make_noprep(LUser, LServer, <<>>),
case acl:match_rule_for_host_type(HostType, LServer, max_user_sessions, JID) of
case acl:match_rule(HostType, LServer, max_user_sessions, JID) of
Max when is_integer(Max) -> Max;
infinity -> infinity;
_ -> ?MAX_USER_SESSIONS
Expand Down
2 changes: 1 addition & 1 deletion src/mam/mod_mam.erl
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ acc_to_host_type(Acc) ->
Action :: mam_iq:action(), From :: jid:jid(),
To :: jid:jid()) -> boolean().
is_action_allowed(HostType, Action, From, To) ->
case acl:match_rule_for_host_type(HostType, To#jid.lserver, Action, From, default) of
case acl:match_rule(HostType, To#jid.lserver, Action, From, default) of
allow -> true;
deny -> false;
default -> is_action_allowed_by_default(Action, From, To)
Expand Down
2 changes: 1 addition & 1 deletion src/mam/mod_mam_muc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ forget_room(Acc, _HostType, MucServer, RoomName) ->
-spec check_action_allowed(host_type(), mongoose_acc:t(), jid:lserver(), mam_iq:action(), muc_action(),
jid:jid(), jid:jid()) -> ok | {error, binary()}.
check_action_allowed(HostType, Acc, Domain, Action, MucAction, From, To) ->
case acl:match_rule_for_host_type(HostType, Domain, MucAction, From, default) of
case acl:match_rule(HostType, Domain, MucAction, From, default) of
allow -> ok;
deny -> {false, <<"Blocked by service policy.">>};
default -> check_room_action_allowed_by_default(HostType, Acc, Action, From, To)
Expand Down
Loading

0 comments on commit 3f66cb1

Please sign in to comment.