Skip to content

Commit

Permalink
Merge pull request #3501 from esl/acl-spec-map
Browse files Browse the repository at this point in the history
Keep ACL conditions as maps
  • Loading branch information
arcusfelis authored Jan 17, 2022
2 parents 74c6d93 + 85bb20b commit 86ba0f1
Show file tree
Hide file tree
Showing 30 changed files with 239 additions and 309 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, match => current_domain}]).

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
27 changes: 17 additions & 10 deletions doc/configuration/acl.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,29 @@ 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.
Each pattern consists of one or more conditions, specified with the options listed below.
All defined conditions need to be satisfied 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* (the one of the server) are matched.
You can set this option to `"all"`, extending the pattern to users from other domains.
This makes a difference for some [access rules](access.md), e.g. MAM, MUC and registration ones.
Setting the option to `"none"` makes the pattern never match.

```toml
everyone = [
Expand Down Expand Up @@ -85,6 +85,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
5 changes: 5 additions & 0 deletions doc/migrations/5.0.0_5.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

The configuration format has slightly changed and you might need to amend `mongooseim.toml`.

### Section `acl`

The implicit check for user's domain in patterns is now configurable and the default behaviour (previously undocumented) is more consistent - the check is always performed unless disabled with `match = "all"`.
See the description of [`current_domain`](../configuration/acl.md#aclmatch) for more details.

### Section `auth`

* Each authentication method needs a TOML section, e.g. if you have the `rdbms` method enabled, you need to have the `[auth.rdbms]` section in the configuration file, even if it is empty. The `methods` option is not required anymore and especially if you are using only one method, you can remove it.
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
194 changes: 61 additions & 133 deletions src/acl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -26,87 +26,47 @@
-module(acl).
-author('[email protected]').

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

-ignore_xref([add/3, delete/3, match_rule/4]).
-export([match_rule/3, 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]).

-type rule() :: 'all' | 'none' | atom().
-type domain_or_global() :: jid:lserver() | global.
-type host_type_or_global() :: mongooseim:host_type() | global.
-type regexp() :: iolist() | binary().
-type aclspec() :: all
| none
| {user, jid:user()}
| {user, jid:user(), jid:server()}
| {server, jid:server()}
| {resource, jid:resource()}
| {user_regexp, regexp()}
| {user_regexp, regexp(), jid:server()}
| {server_regexp, regexp()}
| {resource_regexp, regexp()}
| {node_regexp, regexp(), regexp()}
| {user_glob, regexp()}
| {user_glob, regexp(), jid:server()}
| {server_glob, regexp()}
| {resource_glob, regexp()}
| {node_glob, regexp(), regexp()}.
-export_type([rule/0]).

-type rule() :: atom().
-type acl_spec() :: #{match := all | none | current_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 in the {user, U} pattern:
%% - 'global' - any domain 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) ->
%% Skips the domain check for the 'match => current_domain' condition
-spec match_rule(mongooseim:host_type_or_global(), rule(), jid:jid()) -> acl_result().
match_rule(HostType, Rule, JID) ->
match_rule(HostType, JID#jid.lserver, Rule, JID).

-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 @@ -128,10 +88,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 @@ -142,79 +101,48 @@ 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) ->
false;
match_acl(Rule, JID, HostType, Domain) ->
LJID = jid:to_lower(JID),
AllSpecs = case HostType of
global -> get_acl_specs(Rule, global);
_ -> get_acl_specs(Rule, HostType) ++ get_acl_specs(Rule, global)
end,
Pred = fun(ACLSpec) -> match(ACLSpec, LJID, Domain) end,
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:simple_jid(), domain_or_global()) -> boolean().
match(all, _LJID, _Domain) ->
true;
match({user, U}, {User, Server, _Resource}, Domain) ->
U == User andalso is_server_valid(Domain, Server);
match({user, U, S}, {User, Server, _Resource}, _Domain) ->
U == User andalso S == Server;
match({server, S}, {_User, Server, _Resource}, _Domain) ->
S == Server;
match({resource, Res}, {_User, _Server, Resource}, _Domain) ->
Resource == Res;
match({user_regexp, UserReg}, {User, Server, _Resource}, Domain) ->
is_server_valid(Domain, Server) andalso is_regexp_match(User, UserReg);
match({user_regexp, UserReg, MServer}, {User, Server, _Resource}, _Domain) ->
MServer == Server andalso is_regexp_match(User, UserReg);
match({server_regexp, ServerReg}, {_User, Server, _Resource}, _Domain) ->
is_regexp_match(Server, ServerReg);
match({resource_regexp, ResourceReg}, {_User, _Server, Resource}, _Domain) ->
is_regexp_match(Resource, ResourceReg);
match({node_regexp, UserReg, ServerReg}, {User, Server, _Resource}, _Domain) ->
is_regexp_match(Server, ServerReg) andalso
is_regexp_match(User, UserReg);
match({user_glob, UserGlob}, {User, Server, _Resource}, Domain) ->
is_server_valid(Domain, Server) andalso is_glob_match(User, UserGlob);
match({user_glob, UserGlob, MServer}, {User, Server, _Resource}, _Domain) ->
MServer == Server andalso is_glob_match(User, UserGlob);
match({server_glob, ServerGlob}, {_User, Server, _Resource}, _Domain) ->
is_glob_match(Server, ServerGlob);
match({resource_glob, ResourceGlob}, {_User, _Server, Resource}, _Domain) ->
is_glob_match(Resource, ResourceGlob);
match({node_glob, UserGlob, ServerGlob}, {User, Server, _Resource}, _Domain) ->
is_glob_match(Server, ServerGlob) andalso is_glob_match(User, UserGlob);
match(WrongSpec, _LJID, _Domain) ->
?LOG_ERROR(#{what => wrong_acl_expression,
text => <<"Wrong ACL expression in the configuration file">>,
wrong_spec => WrongSpec}),
false.

-spec is_regexp_match(binary(), Regex :: regexp()) -> boolean().
%% @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) ->
try re:run(String, RegExp, [{capture, none}]) of
nomatch ->
Expand All @@ -227,6 +155,6 @@ is_regexp_match(String, RegExp) ->
false
end.

-spec is_glob_match(binary(), Glob :: regexp()) -> boolean().
-spec is_glob_match(binary(), Glob :: binary()) -> boolean().
is_glob_match(String, Glob) ->
is_regexp_match(String, xmerl_regexp:sh_to_awk(binary_to_list(Glob))).
Loading

0 comments on commit 86ba0f1

Please sign in to comment.