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 b6bd1b7
Show file tree
Hide file tree
Showing 30 changed files with 270 additions and 317 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
2 changes: 1 addition & 1 deletion doc/configuration/access.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ It has the following logic:
* if the access class is `blocked`, the returned value is `"deny"`,
* otherwise, the returned value is `"allow"`.

The `blocked` access class can be defined in the `acl` section and match blacklisted users.
The `blocked` access class can be defined in the [`acl` section](acl.md) and match blacklisted users.

For this rule to take effect, it needs to be referenced in the options of a [C2S listener](listen.md#listenc2saccess).

Expand Down
29 changes: 19 additions & 10 deletions doc/configuration/acl.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,31 @@ The `acl` section is used to define **access classes** to which the connecting u
* Key is the name of the access class,
* Value is a TOML array of patterns - TOML tables, whose format is described below.
* **Default:** no default - each access class needs to be specified explicitly.
* **Example:** the `local` access class is used for the regular users connecting to the [C2S listener](listen.md#client-to-server-c2s-listenc2s).
* **Example:** the `local` access class is used for the regular users connecting to the [C2S listener](listen.md#client-to-server-c2s-listenc2s). The pattern `{}` matches all users from the current server, because it is equivalent to `{match = "current_domain}"` (see below).

```toml
local = [
{user_regexp = ""}
]
local = [{}]
```

When there are multiple patterns listed, the resulting pattern will be the union of all of them.

## Patterns

The options listed below are used to assign the users to the access class. There are no default values for any of them.

!!! Note
The options can NOT be combined with each other unless the description says otherwise.
The options listed below are used to assign the users to the access class.
Each option listed below is one condition.
All defined conditions need to succeed for the pattern to be matched successfully.

### `acl.*.match`

* **Syntax:** string, one of: `"all"`, `"none"`
* **Syntax:** string, one of: `"all"`, `"current_domain"`, `"none"`
* **Default:** `"current_domain"`
* **Example:** `match = "all"`

Matches either all users or none of them. The latter is useful for disabling access to some services.
By default only users from the current domain are matched.
You can set this option to `"all"`, extending the pattern to users from external servers.
This is effective only for specific [access rules](access.md), i.e. MAM, MUC and registration ones,
because for other rules the current domain is always the user's one.
Setting the option to `"none"` makes the match always fail regardless of the other conditions listed below.

```toml
everyone = [
Expand Down Expand Up @@ -85,6 +87,13 @@ The following class includes `alice@localhost/mobile`, but not `alice@localhost/
{resource = "mobile"}
]
```
This option can be combined with `user` and `server` - only `alice@localhost/mobile` belongs to the following class:

```toml
admin = [
{user = "alice", server = "localhost", resource = "mobile"}
]
```

### `acl.*.user_regexp`

Expand Down
4 changes: 1 addition & 3 deletions rel/files/mongooseim.toml
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,7 @@
max_rate = 1000

[acl]
local = [
{user_regexp = ""}
]
local = [{}]

[access]
max_user_sessions = [
Expand Down
177 changes: 52 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,9 @@ 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_or_global(),
jid:lserver()) ->
acl_result().
match_acls([], _, _HostType, _Domain) ->
deny;
match_acls([{Value, Rule} | ACLs], JID, HostType, Domain) ->
Expand All @@ -131,10 +96,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 +106,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_or_global()) -> [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
Loading

0 comments on commit b6bd1b7

Please sign in to comment.