feat: add authz (#4852)

* feat(authorization): add authorization api

* feat(authorization): add check function

* feat(authorization): use hocon config file

* feat(authz): add mysql connector

* feat(authz): support pgsql

* feat(connector): support redis

* chore(authz): use "publish/subscribe/all" instead of "pub/sub/pubsub"
This commit is contained in:
Rory Z 2021-06-23 10:55:38 +08:00 committed by GitHub
parent d7ed66f234
commit 263aaff6d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1782 additions and 17 deletions

19
apps/emqx_authz/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
.rebar3
_*
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
logs
_build
.idea
*.iml
rebar3.crashdump
*~

130
apps/emqx_authz/README.md Normal file
View File

@ -0,0 +1,130 @@
# emqx_authz
## Configure
File: etc/pulgins/authz.conf
```json
authz:{
rules: [
{
type: mysql
config: {
server: "127.0.0.1:3306"
database: mqtt
pool_size: 1
username: root
password: public
auto_reconnect: true
ssl: false
}
sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or clientid = '%c'"
},
{
type: pgsql
config: {
server: "127.0.0.1:5432"
database: mqtt
pool_size: 1
username: root
password: public
auto_reconnect: true
ssl: false
}
sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"
},
{
type: redis
config: {
servers: "127.0.0.1:6379"
database: 0
pool_size: 1
password: public
auto_reconnect: true
ssl: false
}
cmd: "HGETALL mqtt_acl:%u"
},
{
principal: {username: "^admin?"}
permission: allow
action: subscribe
topics: ["$SYS/#"]
},
{
permission: deny
action: subscribe
topics: ["$SYS/#"]
},
{
permission: allow
action: all
topics: ["#"]
}
]
}
```
## Database Management
#### Mysql
Create Example Table
```sql
CREATE TABLE `mqtt_acl` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`ipaddress` VARCHAR(60) NOT NULL DEFAULT '',
`username` VARCHAR(100) NOT NULL DEFAULT '',
`clientid` VARCHAR(100) NOT NULL DEFAULT '',
`action` ENUM('publish', 'subscribe', 'all') NOT NULL,
`permission` ENUM('allow', 'deny') NOT NULL,
`topic` VARCHAR(100) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
Sample data in the default configuration:
```sql
-- Only 127.0.0.1 users can subscribe to system topics
INSERT INTO mqtt_acl (ipaddress, username, clientid, action, permission, topic) VALUES ('127.0.0.1', '', '', 'subscribe', 'allow', '$SYS/#');
```
#### Pgsql
Create Example Table
```sql
CREATE TYPE ACTION AS ENUM('publish','subscribe','all');
CREATE TYPE PERMISSION AS ENUM('allow','deny');
CREATE TABLE mqtt_acl (
id SERIAL PRIMARY KEY,
ipaddress CHARACTER VARYING(60) NOT NULL DEFAULT '',
username CHARACTER VARYING(100) NOT NULL DEFAULT '',
clientid CHARACTER VARYING(100) NOT NULL DEFAULT '',
action ACTION,
permission PERMISSION,
topic CHARACTER VARYING(100) NOT NULL
);
```
Sample data in the default configuration:
```sql
-- Only 127.0.0.1 users can subscribe to system topics
INSERT INTO mqtt_acl (ipaddress, username, clientid, action, permission, topic) VALUES ('127.0.0.1', '', '', 'subscribe', 'allow', '$SYS/#');
```
#### Redis
Sample data in the default configuration:
```
HSET mqtt_acl:emqx '$SYS/#' subscribe
```
A rule of Redis ACL defines `publish`, `subscribe`, or `all `information. All lists in the rule are **allow** lists.

View File

@ -0,0 +1,47 @@
authz:{
rules: [
# {
# type: mysql
# config: {
# server: "127.0.0.1:3306"
# database: mqtt
# pool_size: 1
# username: root
# password: public
# auto_reconnect: true
# ssl: false
# }
# sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or clientid = '%c'"
# },
# {
# type: pgsql
# config: {
# server: "127.0.0.1:5432"
# database: mqtt
# pool_size: 1
# username: root
# password: public
# auto_reconnect: true
# ssl: false
# }
# sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"
# },
# {
# type: redis
# config: {
# servers: "127.0.0.1:6379"
# database: 0
# pool_size: 1
# password: public
# auto_reconnect: true
# ssl: false
# }
# cmd: "HGETALL mqtt_acl:%u"
# },
{
permission: allow
action: all
topics: ["#"]
}
]
}

View File

@ -0,0 +1,19 @@
-type(rule() :: #{binary() => any()}).
-type(rules() :: [rule()]).
-define(APP, emqx_authz).
-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))).
-define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= all))).
-record(acl_metrics, {
allow = 'client.acl.allow',
deny = 'client.acl.deny',
ignore = 'client.acl.ignore'
}).
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
-define(METRICS(Type, K), #Type{}#Type.K).
-define(ACL_METRICS, ?METRICS(acl_metrics)).
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).

View File

@ -0,0 +1,7 @@
{erl_opts, [debug_info, nowarn_unused_import]}.
{deps, []}.
{shell, [
% {config, "config/sys.config"},
{apps, [emqx_authz]}
]}.

View File

@ -0,0 +1,16 @@
{application, emqx_authz,
[{description, "An OTP application"},
{vsn, "0.1.0"},
{registered, []},
{mod, {emqx_authz_app, []}},
{applications,
[kernel,
stdlib,
emqx_connector
]},
{env,[]},
{modules, []},
{licenses, ["Apache 2.0"]},
{links, []}
]}.

View File

@ -0,0 +1,261 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz).
-include("emqx_authz.hrl").
-include_lib("emqx/include/logger.hrl").
-logger_header("[AuthZ]").
-export([ register_metrics/0
, init/0
, compile/1
, lookup/0
, update/1
, check_authz/5
, match/4
]).
-spec(register_metrics() -> ok).
register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
init() ->
ok = register_metrics(),
Conf = filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'),
{ok, RawConf} = hocon:load(Conf),
#{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, RawConf),
ok = application:set_env(?APP, rules, Rules),
NRules = [compile(Rule) || Rule <- Rules],
ok = emqx_hooks:add('client.check_acl', {?MODULE, check_authz, [NRules]}, -1).
lookup() ->
application:get_env(?APP, rules, []).
update(Rules) ->
ok = application:set_env(?APP, rules, Rules),
NRules = [compile(Rule) || Rule <- Rules],
Action = find_action_in_hooks(),
ok = emqx_hooks:del('client.check_acl', Action),
ok = emqx_hooks:add('client.check_acl', {?MODULE, check_authz, [NRules]}, -1),
ok = emqx_acl_cache:empty_acl_cache().
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
find_action_in_hooks() ->
Callbacks = emqx_hooks:lookup('client.check_acl'),
[Action] = [Action || {callback,{?MODULE, check_authz, _} = Action, _, _} <- Callbacks ],
Action.
create_resource(#{<<"type">> := DB,
<<"config">> := Config
} = Rule) ->
ResourceID = iolist_to_binary([io_lib:format("~s_~s",[?APP, DB]), "_", integer_to_list(erlang:system_time())]),
case emqx_resource:check_and_create(
ResourceID,
list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])),
Config)
of
{ok, _} ->
Rule#{<<"resource_id">> => ResourceID};
{error, already_created} ->
Rule#{<<"resource_id">> => ResourceID};
{error, Reason} ->
error({load_sql_error, Reason})
end.
-spec(compile(rule()) -> rule()).
compile(#{<<"topics">> := Topics,
<<"action">> := Action,
<<"permission">> := Permission,
<<"principal">> := Principal
} = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) ->
NTopics = [compile_topic(Topic) || Topic <- Topics],
Rule#{<<"principal">> => compile_principal(Principal),
<<"topics">> => NTopics
};
compile(#{<<"principal">> := Principal,
<<"type">> := redis
} = Rule) ->
NRule = create_resource(Rule),
NRule#{<<"principal">> => compile_principal(Principal)};
compile(#{<<"principal">> := Principal,
<<"type">> := DB,
<<"sql">> := SQL
} = Rule) when DB =:= mysql;
DB =:= pgsql ->
Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])),
NRule = create_resource(Rule),
NRule#{<<"principal">> => compile_principal(Principal),
<<"sql">> => Mod:parse_query(SQL)
}.
compile_principal(all) -> all;
compile_principal(#{<<"username">> := Username}) ->
{ok, MP} = re:compile(bin(Username)),
#{<<"username">> => MP};
compile_principal(#{<<"clientid">> := Clientid}) ->
{ok, MP} = re:compile(bin(Clientid)),
#{<<"clientid">> => MP};
compile_principal(#{<<"ipaddress">> := IpAddress}) ->
#{<<"ipaddress">> => esockd_cidr:parse(b2l(IpAddress), true)};
compile_principal(#{<<"and">> := Principals}) when is_list(Principals) ->
#{<<"and">> => [compile_principal(Principal) || Principal <- Principals]};
compile_principal(#{<<"or">> := Principals}) when is_list(Principals) ->
#{<<"or">> => [compile_principal(Principal) || Principal <- Principals]}.
compile_topic(<<"eq ", Topic/binary>>) ->
compile_topic(#{<<"eq">> => Topic});
compile_topic(#{<<"eq">> := Topic}) ->
#{<<"eq">> => emqx_topic:words(bin(Topic))};
compile_topic(Topic) when is_binary(Topic)->
Words = emqx_topic:words(bin(Topic)),
case pattern(Words) of
true -> #{<<"pattern">> => Words};
false -> Words
end.
pattern(Words) ->
lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words).
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(B) when is_binary(B) -> B;
bin(L) when is_list(L) -> list_to_binary(L);
bin(X) -> X.
b2l(B) when is_list(B) -> B;
b2l(B) when is_binary(B) -> binary_to_list(B).
%%--------------------------------------------------------------------
%% ACL callbacks
%%--------------------------------------------------------------------
%% @doc Check ACL
-spec(check_authz(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules())
-> {ok, allow} | {ok, deny} | deny).
check_authz(#{username := Username,
peerhost := IpAddress
} = Client, PubSub, Topic, DefaultResult, Rules) ->
case do_check_authz(Client, PubSub, Topic, Rules) of
{matched, allow} ->
?LOG(info, "Client succeeded authorizationa: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]),
emqx_metrics:inc(?ACL_METRICS(allow)),
{stop, allow};
{matched, deny} ->
?LOG(info, "Client failed authorizationa: Username: ~p, IP: ~p, Topic: ~p, Permission: deny", [Username, IpAddress, Topic]),
emqx_metrics:inc(?ACL_METRICS(deny)),
{stop, deny};
nomatch ->
?LOG(info, "Client failed authorizationa: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]),
DefaultResult
end.
do_check_authz(Client, PubSub, Topic,
[Connector = #{<<"principal">> := Principal,
<<"type">> := DB} | Tail] ) ->
case match_principal(Client, Principal) of
true ->
Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, DB])),
case Mod:check_authz(Client, PubSub, Topic, Connector) of
nomatch -> do_check_authz(Client, PubSub, Topic, Tail);
Matched -> Matched
end;
false -> do_check_authz(Client, PubSub, Topic, Tail)
end;
do_check_authz(Client, PubSub, Topic,
[#{<<"permission">> := Permission} = Rule | Tail]) ->
case match(Client, PubSub, Topic, Rule) of
true -> {matched, Permission};
false -> do_check_authz(Client, PubSub, Topic, Tail)
end;
do_check_authz(_Client, _PubSub, _Topic, []) -> nomatch.
match(Client, PubSub, Topic,
#{<<"principal">> := Principal,
<<"topics">> := TopicFilters,
<<"action">> := Action
}) ->
match_action(PubSub, Action) andalso
match_principal(Client, Principal) andalso
match_topics(Client, Topic, TopicFilters).
match_action(publish, publish) -> true;
match_action(subscribe, subscribe) -> true;
match_action(_, all) -> true;
match_action(_, _) -> false.
match_principal(_, all) -> true;
match_principal(#{username := undefined}, #{<<"username">> := _MP}) ->
false;
match_principal(#{username := Username}, #{<<"username">> := MP}) ->
case re:run(Username, MP) of
{match, _} -> true;
_ -> false
end;
match_principal(#{clientid := Clientid}, #{<<"clientid">> := MP}) ->
case re:run(Clientid, MP) of
{match, _} -> true;
_ -> false
end;
match_principal(#{peerhost := undefined}, #{<<"ipaddress">> := _CIDR}) ->
false;
match_principal(#{peerhost := IpAddress}, #{<<"ipaddress">> := CIDR}) ->
esockd_cidr:match(IpAddress, CIDR);
match_principal(ClientInfo, #{<<"and">> := Principals}) when is_list(Principals) ->
lists:foldl(fun(Principal, Permission) ->
match_principal(ClientInfo, Principal) andalso Permission
end, true, Principals);
match_principal(ClientInfo, #{<<"or">> := Principals}) when is_list(Principals) ->
lists:foldl(fun(Principal, Permission) ->
match_principal(ClientInfo, Principal) orelse Permission
end, false, Principals);
match_principal(_, _) -> false.
match_topics(_ClientInfo, _Topic, []) ->
false;
match_topics(ClientInfo, Topic, [#{<<"pattern">> := PatternFilter}|Filters]) ->
TopicFilter = feed_var(ClientInfo, PatternFilter),
match_topic(emqx_topic:words(Topic), TopicFilter)
orelse match_topics(ClientInfo, Topic, Filters);
match_topics(ClientInfo, Topic, [TopicFilter|Filters]) ->
match_topic(emqx_topic:words(Topic), TopicFilter)
orelse match_topics(ClientInfo, Topic, Filters).
match_topic(Topic, #{<<"eq">> := TopicFilter}) ->
Topic == TopicFilter;
match_topic(Topic, TopicFilter) ->
emqx_topic:match(Topic, TopicFilter).
feed_var(ClientInfo, Pattern) ->
feed_var(ClientInfo, Pattern, []).
feed_var(_ClientInfo, [], Acc) ->
lists:reverse(Acc);
feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) ->
feed_var(ClientInfo, Words, [<<"%c">>|Acc]);
feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) ->
feed_var(ClientInfo, Words, [ClientId |Acc]);
feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) ->
feed_var(ClientInfo, Words, [<<"%u">>|Acc]);
feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) ->
feed_var(ClientInfo, Words, [Username|Acc]);
feed_var(ClientInfo, [W|Words], Acc) ->
feed_var(ClientInfo, Words, [W|Acc]).

View File

@ -0,0 +1,91 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_api).
-include("emqx_authz.hrl").
-rest_api(#{name => lookup_authz,
method => 'GET',
path => "/authz",
func => lookup_authz,
descr => "Lookup Authorization"
}).
-rest_api(#{name => update_authz,
method => 'PUT',
path => "/authz",
func => update_authz,
descr => "Rewrite authz list"
}).
-rest_api(#{name => append_authz,
method => 'POST',
path => "/authz/append",
func => append_authz,
descr => "Add a new rule at the end of the authz list"
}).
-rest_api(#{name => push_authz,
method => 'POST',
path => "/authz/push",
func => push_authz,
descr => "Add a new rule at the start of the authz list"
}).
-export([ lookup_authz/2
, update_authz/2
, append_authz/2
, push_authz/2
]).
lookup_authz(_Bindings, _Params) ->
minirest:return({ok, emqx_authz:lookup()}).
update_authz(_Bindings, Params) ->
Rules = get_rules(Params),
minirest:return(emqx_authz:update(Rules)).
append_authz(_Bindings, Params) ->
Rules = get_rules(Params),
NRules = lists:append(emqx_authz:lookup(), Rules),
minirest:return(emqx_authz:update(NRules)).
push_authz(_Bindings, Params) ->
Rules = get_rules(Params),
NRules = lists:append(Rules, emqx_authz:lookup()),
minirest:return(emqx_authz:update(NRules)).
%%------------------------------------------------------------------------------
%% Interval Funcs
%%------------------------------------------------------------------------------
get_rules(Params) ->
% #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, #{<<"authz">> => Params}),
{ok, Conf} = hocon:binary(jsx:encode(#{<<"authz">> => Params}), #{format => richmap}),
CheckConf = hocon_schema:check(emqx_authz_schema, Conf),
#{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:richmap_to_map(CheckConf),
Rules.
%%--------------------------------------------------------------------
%% EUnits
%%--------------------------------------------------------------------
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

View File

@ -0,0 +1,20 @@
%%%-------------------------------------------------------------------
%% @doc emqx_authz public API
%% @end
%%%-------------------------------------------------------------------
-module(emqx_authz_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
{ok, Sup} = emqx_authz_sup:start_link(),
ok = emqx_authz:init(),
{ok, Sup}.
stop(_State) ->
ok.
%% internal functions

View File

@ -0,0 +1,141 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_mysql).
-include("emqx_authz.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl").
%% ACL Callbacks
-export([ description/0
, parse_query/1
, check_authz/4
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
description() ->
"AuthZ with Mysql".
parse_query(undefined) ->
undefined;
parse_query(Sql) ->
case re:run(Sql, "'%[ucCad]'", [global, {capture, all, list}]) of
{match, Variables} ->
Params = [Var || [Var] <- Variables],
{re:replace(Sql, "'%[ucCad]'", "?", [global, {return, list}]), Params};
nomatch ->
{Sql, []}
end.
check_authz(Client, PubSub, Topic,
#{<<"resource_id">> := ResourceID,
<<"sql">> := {SQL, Params}
}) ->
case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of
{ok, _Columns, []} -> nomatch;
{ok, Columns, Rows} ->
do_check_authz(Client, PubSub, Topic, Columns, Rows);
{error, Reason} ->
?LOG(error, "[AuthZ] Query mysql error: ~p~n", [Reason]),
nomatch
end.
do_check_authz(_Client, _PubSub, _Topic, _Columns, []) ->
nomatch;
do_check_authz(Client, PubSub, Topic, Columns, [Row | Tail]) ->
case match(Client, PubSub, Topic, format_result(Columns, Row)) of
{matched, Permission} -> {matched, Permission};
nomatch -> do_check_authz(Client, PubSub, Topic, Columns, Tail)
end.
format_result(Columns, Row) ->
L = [ begin
K = lists:nth(I, Columns),
V = lists:nth(I, Row),
{K, V}
end || I <- lists:seq(1, length(Columns)) ],
maps:from_list(L).
match(Client, PubSub, Topic,
#{<<"permission">> := Permission,
<<"action">> := Action,
<<"clientid">> := ClientId,
<<"username">> := Username,
<<"ipaddress">> := IpAddress,
<<"topic">> := TopicFilter
}) ->
Rule = #{<<"principal">> => principal(IpAddress, Username, ClientId),
<<"topics">> => [TopicFilter],
<<"action">> => Action,
<<"permission">> => Permission
},
#{<<"simple_rule">> :=
#{<<"permission">> := NPermission} = NRule
} = hocon_schema:check_plain(
emqx_authz_schema,
#{<<"simple_rule">> => Rule},
#{},
[simple_rule]),
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of
true -> {matched, NPermission};
false -> nomatch
end.
principal(CIDR, Username, ClientId) ->
Cols = [{<<"ipaddress">>, CIDR}, {<<"username">>, Username}, {<<"clientid">>, ClientId}],
case [#{C => V} || {C, V} <- Cols, not empty(V)] of
[] -> throw(undefined_who);
[Who] -> Who;
Conds -> #{<<"and">> => Conds}
end.
empty(null) -> true;
empty("") -> true;
empty(<<>>) -> true;
empty(_) -> false.
replvar(Params, ClientInfo) ->
replvar(Params, ClientInfo, []).
replvar([], _ClientInfo, Acc) ->
lists:reverse(Acc);
replvar(["'%u'" | Params], ClientInfo, Acc) ->
replvar(Params, ClientInfo, [safe_get(username, ClientInfo) | Acc]);
replvar(["'%c'" | Params], ClientInfo = #{clientid := ClientId}, Acc) ->
replvar(Params, ClientInfo, [ClientId | Acc]);
replvar(["'%a'" | Params], ClientInfo = #{peerhost := IpAddr}, Acc) ->
replvar(Params, ClientInfo, [inet_parse:ntoa(IpAddr) | Acc]);
replvar(["'%C'" | Params], ClientInfo, Acc) ->
replvar(Params, ClientInfo, [safe_get(cn, ClientInfo)| Acc]);
replvar(["'%d'" | Params], ClientInfo, Acc) ->
replvar(Params, ClientInfo, [safe_get(dn, ClientInfo)| Acc]);
replvar([Param | Params], ClientInfo, Acc) ->
replvar(Params, ClientInfo, [Param | Acc]).
safe_get(K, ClientInfo) ->
bin(maps:get(K, ClientInfo, "undefined")).
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(B) when is_binary(B) -> B;
bin(L) when is_list(L) -> list_to_binary(L);
bin(X) -> X.

View File

@ -0,0 +1,145 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_pgsql).
-include("emqx_authz.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl").
%% ACL Callbacks
-export([ description/0
, parse_query/1
, check_authz/4
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
description() ->
"AuthZ with pgsql".
parse_query(undefined) ->
undefined;
parse_query(Sql) ->
case re:run(Sql, "'%[ucCad]'", [global, {capture, all, list}]) of
{match, Variables} ->
Params = [Var || [Var] <- Variables],
Vars = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Params))],
NSql = lists:foldl(fun({Param, Var}, S) ->
re:replace(S, Param, Var, [{return, list}])
end, Sql, lists:zip(Params, Vars)),
{NSql, Params};
nomatch ->
{Sql, []}
end.
check_authz(Client, PubSub, Topic,
#{<<"resource_id">> := ResourceID,
<<"sql">> := {SQL, Params}
}) ->
case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of
{ok, _Columns, []} -> nomatch;
{ok, Columns, Rows} ->
do_check_authz(Client, PubSub, Topic, Columns, Rows);
{error, Reason} ->
?LOG(error, "[AuthZ] Query pgsql error: ~p~n", [Reason]),
nomatch
end.
do_check_authz(_Client, _PubSub, _Topic, _Columns, []) ->
nomatch;
do_check_authz(Client, PubSub, Topic, Columns, [Row | Tail]) ->
case match(Client, PubSub, Topic, format_result(Columns, Row)) of
{matched, Permission} -> {matched, Permission};
nomatch -> do_check_authz(Client, PubSub, Topic, Columns, Tail)
end.
format_result(Columns, Row) ->
L = [ begin
{column, K, _, _, _, _, _, _, _} = lists:nth(I, Columns),
V = lists:nth(I, tuple_to_list(Row)),
{K, V}
end || I <- lists:seq(1, length(Columns)) ],
maps:from_list(L).
match(Client, PubSub, Topic,
#{<<"permission">> := Permission,
<<"action">> := Action,
<<"clientid">> := ClientId,
<<"username">> := Username,
<<"ipaddress">> := IpAddress,
<<"topic">> := TopicFilter
}) ->
Rule = #{<<"principal">> => principal(IpAddress, Username, ClientId),
<<"topics">> => [TopicFilter],
<<"action">> => Action,
<<"permission">> => Permission
},
#{<<"simple_rule">> :=
#{<<"permission">> := NPermission} = NRule
} = hocon_schema:check_plain(
emqx_authz_schema,
#{<<"simple_rule">> => Rule},
#{},
[simple_rule]),
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of
true -> {matched, NPermission};
false -> nomatch
end.
principal(CIDR, Username, ClientId) ->
Cols = [{<<"ipaddress">>, CIDR}, {<<"username">>, Username}, {<<"clientid">>, ClientId}],
case [#{C => V} || {C, V} <- Cols, not empty(V)] of
[] -> throw(undefined_who);
[Who] -> Who;
Conds -> #{<<"and">> => Conds}
end.
empty(null) -> true;
empty("") -> true;
empty(<<>>) -> true;
empty(_) -> false.
replvar(Params, ClientInfo) ->
replvar(Params, ClientInfo, []).
replvar([], _ClientInfo, Acc) ->
lists:reverse(Acc);
replvar(["'%u'" | Params], ClientInfo, Acc) ->
replvar(Params, ClientInfo, [safe_get(username, ClientInfo) | Acc]);
replvar(["'%c'" | Params], ClientInfo = #{clientid := ClientId}, Acc) ->
replvar(Params, ClientInfo, [ClientId | Acc]);
replvar(["'%a'" | Params], ClientInfo = #{peerhost := IpAddr}, Acc) ->
replvar(Params, ClientInfo, [inet_parse:ntoa(IpAddr) | Acc]);
replvar(["'%C'" | Params], ClientInfo, Acc) ->
replvar(Params, ClientInfo, [safe_get(cn, ClientInfo)| Acc]);
replvar(["'%d'" | Params], ClientInfo, Acc) ->
replvar(Params, ClientInfo, [safe_get(dn, ClientInfo)| Acc]);
replvar([Param | Params], ClientInfo, Acc) ->
replvar(Params, ClientInfo, [Param | Acc]).
safe_get(K, ClientInfo) ->
bin(maps:get(K, ClientInfo, "undefined")).
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(B) when is_binary(B) -> B;
bin(L) when is_list(L) -> list_to_binary(L);
bin(X) -> X.

View File

@ -0,0 +1,97 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_redis).
-include("emqx_authz.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl").
%% ACL Callbacks
-export([ check_authz/4
, description/0
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
description() ->
"AuthZ with redis".
check_authz(Client, PubSub, Topic,
#{<<"resource_id">> := ResourceID,
<<"cmd">> := CMD
}) ->
NCMD = string:tokens(replvar(CMD, Client), " "),
case emqx_resource:query(ResourceID, {cmd, NCMD}) of
{ok, []} -> nomatch;
{ok, Rows} ->
do_check_authz(Client, PubSub, Topic, Rows);
{error, Reason} ->
?LOG(error, "[AuthZ] Query redis error: ~p", [Reason]),
nomatch
end.
do_check_authz(_Client, _PubSub, _Topic, []) ->
nomatch;
do_check_authz(Client, PubSub, Topic, [TopicFilter, Action | Tail]) ->
case match(Client, PubSub, Topic,
#{topics => TopicFilter,
action => Action
})
of
{matched, Permission} -> {matched, Permission};
nomatch -> do_check_authz(Client, PubSub, Topic, Tail)
end.
match(Client, PubSub, Topic,
#{topics := TopicFilter,
action := Action
}) ->
Rule = #{<<"principal">> => all,
<<"topics">> => [TopicFilter],
<<"action">> => Action,
<<"permission">> => allow
},
#{<<"simple_rule">> := NRule
} = hocon_schema:check_plain(
emqx_authz_schema,
#{<<"simple_rule">> => Rule},
#{},
[simple_rule]),
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of
true -> {matched, allow};
false -> nomatch
end.
replvar(Cmd, Client = #{cn := CN}) ->
replvar(repl(Cmd, "%C", CN), maps:remove(cn, Client));
replvar(Cmd, Client = #{dn := DN}) ->
replvar(repl(Cmd, "%d", DN), maps:remove(dn, Client));
replvar(Cmd, Client = #{clientid := ClientId}) ->
replvar(repl(Cmd, "%c", ClientId), maps:remove(clientid, Client));
replvar(Cmd, Client = #{username := Username}) ->
replvar(repl(Cmd, "%u", Username), maps:remove(username, Client));
replvar(Cmd, _) ->
Cmd.
repl(S, _Var, undefined) ->
S;
repl(S, Var, Val) ->
NVal = re:replace(Val, "&", "\\\\&", [global, {return, list}]),
re:replace(S, Var, NVal, [{return, list}]).

View File

@ -0,0 +1,104 @@
-module(emqx_authz_schema).
-include_lib("typerefl/include/types.hrl").
-type action() :: publish | subscribe | all.
-type permission() :: allow | deny.
-reflect_type([ permission/0
, action/0
]).
-export([structs/0, fields/1]).
structs() -> [authz].
fields(authz) ->
[ {rules, rules()}
];
fields(redis_connector) ->
[ {principal, principal()}
, {type, #{type => hoconsc:enum([redis])}}
, {config, #{type => map()}}
, {cmd, query()}
];
fields(sql_connector) ->
[ {principal, principal() }
, {type, #{type => hoconsc:enum([mysql, pgsql])}}
, {config, #{type => map()}}
, {sql, query()}
];
fields(simple_rule) ->
[ {permission, #{type => permission()}}
, {action, #{type => action()}}
, {topics, #{type => union_array(
[ binary()
, hoconsc:ref(eq_topic)
]
)}}
, {principal, principal()}
];
fields(username) ->
[{username, #{type => binary()}}];
fields(clientid) ->
[{clientid, #{type => binary()}}];
fields(ipaddress) ->
[{ipaddress, #{type => string()}}];
fields(andlist) ->
[{'and', #{type => union_array(
[ hoconsc:ref(username)
, hoconsc:ref(clientid)
, hoconsc:ref(ipaddress)
])
}
}
];
fields(orlist) ->
[{'or', #{type => union_array(
[ hoconsc:ref(username)
, hoconsc:ref(clientid)
, hoconsc:ref(ipaddress)
])
}
}
];
fields(eq_topic) ->
[{eq, #{type => binary()}}].
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
union_array(Item) when is_list(Item) ->
hoconsc:array(hoconsc:union(Item)).
rules() ->
#{type => union_array(
[ hoconsc:ref(simple_rule)
, hoconsc:ref(sql_connector)
, hoconsc:ref(redis_connector)
])
}.
principal() ->
#{default => all,
type => hoconsc:union(
[ all
, hoconsc:ref(username)
, hoconsc:ref(clientid)
, hoconsc:ref(ipaddress)
, hoconsc:ref(andlist)
, hoconsc:ref(orlist)
])
}.
query() ->
#{type => binary(),
validator => fun(S) ->
case size(S) > 0 of
true -> ok;
_ -> {error, "Request query"}
end
end
}.

View File

@ -0,0 +1,35 @@
%%%-------------------------------------------------------------------
%% @doc emqx_authz top level supervisor.
%% @end
%%%-------------------------------------------------------------------
-module(emqx_authz_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%% sup_flags() = #{strategy => strategy(), % optional
%% intensity => non_neg_integer(), % optional
%% period => pos_integer()} % optional
%% child_spec() = #{id => child_id(), % mandatory
%% start => mfargs(), % mandatory
%% restart => restart(), % optional
%% shutdown => shutdown(), % optional
%% type => worker(), % optional
%% modules => modules()} % optional
init([]) ->
SupFlags = #{strategy => one_for_all,
intensity => 0,
period => 1},
ChildSpecs = [],
{ok, {SupFlags, ChildSpecs}}.
%% internal functions

View File

@ -0,0 +1,168 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include("emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_ct:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1),
Config.
end_per_suite(_Config) ->
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
emqx_ct_helpers:stop_apps([emqx_authz]).
set_special_configs(emqx) ->
application:set_env(emqx, allow_anonymous, true),
application:set_env(emqx, enable_acl_cache, false),
application:set_env(emqx, acl_nomatch, deny),
ok;
set_special_configs(emqx_authz) ->
application:set_env(emqx, plugins_etc_dir,
emqx_ct_helpers:deps_path(emqx_authz, "test")),
Conf = #{<<"authz">> => #{<<"rules">> => []}},
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
ok;
set_special_configs(_App) ->
ok.
-define(RULE1, #{<<"principal">> => all,
<<"topics">> => [<<"#">>],
<<"action">> => all,
<<"permission">> => deny}
).
-define(RULE2, #{<<"principal">> =>
#{<<"ipaddress">> => <<"127.0.0.1">>},
<<"topics">> =>
[#{<<"eq">> => <<"#">>},
#{<<"eq">> => <<"+">>}
] ,
<<"action">> => all,
<<"permission">> => allow}
).
-define(RULE3,#{<<"principal">> =>
#{<<"and">> => [#{<<"username">> => "^test?"},
#{<<"clientid">> => "^test?"}
]},
<<"topics">> => [<<"test">>],
<<"action">> => publish,
<<"permission">> => allow}
).
-define(RULE4,#{<<"principal">> =>
#{<<"or">> => [#{<<"username">> => <<"^test">>},
#{<<"clientid">> => <<"test?">>}
]},
<<"topics">> => [<<"%u">>,<<"%c">>],
<<"action">> => publish,
<<"permission">> => deny}
).
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_compile(_) ->
?assertEqual(#{<<"permission">> => deny,
<<"action">> => all,
<<"principal">> => all,
<<"topics">> => [['#']]
},emqx_authz:compile(?RULE1)),
?assertEqual(#{<<"permission">> => allow,
<<"action">> => all,
<<"principal">> =>
#{<<"ipaddress">> => {{127,0,0,1},{127,0,0,1},32}},
<<"topics">> => [#{<<"eq">> => ['#']},
#{<<"eq">> => ['+']}]
}, emqx_authz:compile(?RULE2)),
?assertMatch(
#{<<"permission">> := allow,
<<"action">> := publish,
<<"principal">> :=
#{<<"and">> := [#{<<"username">> := {re_pattern, _, _, _, _}},
#{<<"clientid">> := {re_pattern, _, _, _, _}}
]
},
<<"topics">> := [[<<"test">>]]
}, emqx_authz:compile(?RULE3)),
?assertMatch(
#{<<"permission">> := deny,
<<"action">> := publish,
<<"principal">> :=
#{<<"or">> := [#{<<"username">> := {re_pattern, _, _, _, _}},
#{<<"clientid">> := {re_pattern, _, _, _, _}}
]
},
<<"topics">> := [#{<<"pattern">> := [<<"%u">>]},
#{<<"pattern">> := [<<"%c">>]}
]
}, emqx_authz:compile(?RULE4)),
ok.
t_authz(_) ->
ClientInfo1 = #{clientid => <<"test">>,
username => <<"test">>,
peerhost => {127,0,0,1}
},
ClientInfo2 = #{clientid => <<"test">>,
username => <<"test">>,
peerhost => {192,168,0,10}
},
ClientInfo3 = #{clientid => <<"test">>,
username => <<"fake">>,
peerhost => {127,0,0,1}
},
ClientInfo4 = #{clientid => <<"fake">>,
username => <<"test">>,
peerhost => {127,0,0,1}
},
Rules1 = [emqx_authz:compile(Rule) || Rule <- [?RULE1, ?RULE2]],
Rules2 = [emqx_authz:compile(Rule) || Rule <- [?RULE2, ?RULE1]],
Rules3 = [emqx_authz:compile(Rule) || Rule <- [?RULE3, ?RULE4]],
Rules4 = [emqx_authz:compile(Rule) || Rule <- [?RULE4, ?RULE1]],
?assertEqual(deny,
emqx_authz:check_authz(ClientInfo1, subscribe, <<"#">>, deny, [])),
?assertEqual({stop, deny},
emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules1)),
?assertEqual({stop, allow},
emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules2)),
?assertEqual({stop, allow},
emqx_authz:check_authz(ClientInfo1, publish, <<"test">>, deny, Rules3)),
?assertEqual({stop, deny},
emqx_authz:check_authz(ClientInfo1, publish, <<"test">>, deny, Rules4)),
?assertEqual({stop, deny},
emqx_authz:check_authz(ClientInfo2, subscribe, <<"#">>, deny, Rules2)),
?assertEqual({stop, deny},
emqx_authz:check_authz(ClientInfo3, publish, <<"test">>, deny, Rules3)),
?assertEqual({stop, deny},
emqx_authz:check_authz(ClientInfo3, publish, <<"fake">>, deny, Rules4)),
?assertEqual({stop, deny},
emqx_authz:check_authz(ClientInfo4, publish, <<"test">>, deny, Rules3)),
?assertEqual({stop, deny},
emqx_authz:check_authz(ClientInfo4, publish, <<"fake">>, deny, Rules4)),
ok.

View File

@ -0,0 +1,130 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_api_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include("emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-import(emqx_ct_http, [ request_api/3
, request_api/5
, get_http_data/1
, create_default_app/0
, delete_default_app/0
, default_auth_header/0
]).
-define(HOST, "http://127.0.0.1:8081/").
-define(API_VERSION, "v4").
-define(BASE_PATH, "api").
all() ->
emqx_ct:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_management], fun set_special_configs/1),
create_default_app(),
Config.
end_per_suite(_Config) ->
delete_default_app(),
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
emqx_ct_helpers:stop_apps([emqx_authz, emqx_management]).
set_special_configs(emqx) ->
application:set_env(emqx, allow_anonymous, true),
application:set_env(emqx, enable_acl_cache, false),
ok;
set_special_configs(emqx_authz) ->
application:set_env(emqx, plugins_etc_dir,
emqx_ct_helpers:deps_path(emqx_authz, "test")),
Conf = #{<<"authz">> => #{<<"rules">> => []}},
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
ok;
set_special_configs(_App) ->
ok.
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_api(_Config) ->
Rule1 = #{<<"principal">> =>
#{<<"and">> => [#{<<"username">> => <<"^test?">>},
#{<<"clientid">> => <<"^test?">>}
]},
<<"action">> => <<"subscribe">>,
<<"topics">> => [<<"%u">>],
<<"permission">> => <<"allow">>
},
{ok, _} = request_http_rest_add(["authz/push"], #{rules => [Rule1]}),
{ok, Result1} = request_http_rest_lookup(["authz"]),
?assertMatch([Rule1 | _ ], get_http_data(Result1)),
Rule2 = #{<<"principal">> => #{<<"ipaddress">> => <<"127.0.0.1">>},
<<"action">> => <<"publish">>,
<<"topics">> => [#{<<"eq">> => <<"#">>},
#{<<"eq">> => <<"+">>}
],
<<"permission">> => <<"deny">>
},
{ok, _} = request_http_rest_add(["authz/append"], #{rules => [Rule2]}),
{ok, Result2} = request_http_rest_lookup(["authz"]),
?assertEqual(Rule2#{<<"principal">> => #{<<"ipaddress">> => "127.0.0.1"}},
lists:last(get_http_data(Result2))),
{ok, _} = request_http_rest_update(["authz"], #{rules => []}),
{ok, Result3} = request_http_rest_lookup(["authz"]),
?assertEqual([], get_http_data(Result3)),
ok.
%%--------------------------------------------------------------------
%% HTTP Request
%%--------------------------------------------------------------------
request_http_rest_list(Path) ->
request_api(get, uri(Path), default_auth_header()).
request_http_rest_lookup(Path) ->
request_api(get, uri([Path]), default_auth_header()).
request_http_rest_add(Path, Params) ->
request_api(post, uri(Path), [], default_auth_header(), Params).
request_http_rest_update(Path, Params) ->
request_api(put, uri([Path]), [], default_auth_header(), Params).
request_http_rest_delete(Login) ->
request_api(delete, uri([Login]), default_auth_header()).
uri() -> uri([]).
uri(Parts) when is_list(Parts) ->
NParts = [b2l(E) || E <- Parts],
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
%% @private
b2l(B) when is_binary(B) ->
binary_to_list(B);
b2l(L) when is_list(L) ->
L.

View File

@ -0,0 +1,117 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_mysql_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include("emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_ct:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ),
ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1),
Config.
end_per_suite(_Config) ->
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]),
meck:unload(emqx_resource).
set_special_configs(emqx) ->
application:set_env(emqx, allow_anonymous, false),
application:set_env(emqx, enable_acl_cache, false),
application:set_env(emqx, acl_nomatch, deny),
application:set_env(emqx, plugins_loaded_file,
emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")),
ok;
set_special_configs(emqx_authz) ->
application:set_env(emqx, plugins_etc_dir,
emqx_ct_helpers:deps_path(emqx_authz, "test")),
Conf = #{<<"authz">> =>
#{<<"rules">> =>
[#{<<"config">> =>#{<<"meck">> => <<"fake">>},
<<"principal">> => all,
<<"sql">> => <<"fake sql">>,
<<"type">> => mysql}
]}},
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
ok;
set_special_configs(_App) ->
ok.
-define(COLUMNS, [ <<"ipaddress">>
, <<"username">>
, <<"clientid">>
, <<"action">>
, <<"permission">>
, <<"topic">>
]).
-define(RULE1, [[<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"deny">>, <<"#">>]]).
-define(RULE2, [[<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"allow">>, <<"eq #">>]]).
-define(RULE3, [[<<>>, <<"^test">>, <<"^test">> ,<<"subscribe">>, <<"allow">>, <<"test/%c">>]]).
-define(RULE4, [[<<>>, <<"^test">>, <<"^test">> ,<<"publish">>, <<"allow">>, <<"test/%u">>]]).
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_authz(_) ->
ClientInfo1 = #{clientid => <<"test">>,
username => <<"test">>,
peerhost => {127,0,0,1},
zone => zone
},
ClientInfo2 = #{clientid => <<"test_clientid">>,
username => <<"test_username">>,
peerhost => {192,168,0,10},
zone => zone
},
ClientInfo3 = #{clientid => <<"test_clientid">>,
username => <<"fake_username">>,
peerhost => {127,0,0,1},
zone => zone
},
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), % nomatch
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"#">>)), % nomatch
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"+">>)),
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end),
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)),
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end),
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_clientid">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_clientid">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_username">>)),
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_username">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, subscribe, <<"test">>)), % nomatch
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, publish, <<"test">>)), % nomatch
ok.

View File

@ -0,0 +1,117 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_pgsql_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include("emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_ct:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ),
ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1),
Config.
end_per_suite(_Config) ->
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]),
meck:unload(emqx_resource).
set_special_configs(emqx) ->
application:set_env(emqx, allow_anonymous, false),
application:set_env(emqx, enable_acl_cache, false),
application:set_env(emqx, acl_nomatch, deny),
application:set_env(emqx, plugins_loaded_file,
emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")),
ok;
set_special_configs(emqx_authz) ->
application:set_env(emqx, plugins_etc_dir,
emqx_ct_helpers:deps_path(emqx_authz, "test")),
Conf = #{<<"authz">> =>
#{<<"rules">> =>
[#{<<"config">> =>#{<<"meck">> => <<"fake">>},
<<"principal">> => all,
<<"sql">> => <<"fake sql">>,
<<"type">> => pgsql}
]}},
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
ok;
set_special_configs(_App) ->
ok.
-define(COLUMNS, [ {column, <<"ipaddress">>, meck, meck, meck, meck, meck, meck, meck}
, {column, <<"username">>, meck, meck, meck, meck, meck, meck, meck}
, {column, <<"clientid">>, meck, meck, meck, meck, meck, meck, meck}
, {column, <<"action">>, meck, meck, meck, meck, meck, meck, meck}
, {column, <<"permission">>, meck, meck, meck, meck, meck, meck, meck}
, {column, <<"topic">>, meck, meck, meck, meck, meck, meck, meck}
]).
-define(RULE1, [{<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"deny">>, <<"#">>}]).
-define(RULE2, [{<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"allow">>, <<"eq #">>}]).
-define(RULE3, [{<<>>, <<"^test">>, <<"^test">> ,<<"subscribe">>, <<"allow">>, <<"test/%c">>}]).
-define(RULE4, [{<<>>, <<"^test">>, <<"^test">> ,<<"publish">>, <<"allow">>, <<"test/%u">>}]).
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_authz(_) ->
ClientInfo1 = #{clientid => <<"test">>,
username => <<"test">>,
peerhost => {127,0,0,1},
zone => zone
},
ClientInfo2 = #{clientid => <<"test_clientid">>,
username => <<"test_username">>,
peerhost => {192,168,0,10},
zone => zone
},
ClientInfo3 = #{clientid => <<"test_clientid">>,
username => <<"fake_username">>,
peerhost => {127,0,0,1},
zone => zone
},
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), % nomatch
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"#">>)), % nomatch
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"+">>)),
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end),
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"+">>)),
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end),
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_clientid">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_clientid">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_username">>)),
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_username">>)),
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, subscribe, <<"test">>)), % nomatch
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, publish, <<"test">>)), % nomatch
ok.

View File

@ -0,0 +1,108 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_authz_redis_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include("emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_ct:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ),
ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1),
Config.
end_per_suite(_Config) ->
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]),
meck:unload(emqx_resource).
set_special_configs(emqx) ->
application:set_env(emqx, allow_anonymous, true),
application:set_env(emqx, enable_acl_cache, false),
application:set_env(emqx, acl_nomatch, deny),
application:set_env(emqx, plugins_loaded_file,
emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")),
ok;
set_special_configs(emqx_authz) ->
application:set_env(emqx, plugins_etc_dir,
emqx_ct_helpers:deps_path(emqx_authz, "test")),
Conf = #{<<"authz">> =>
#{<<"rules">> =>
[#{<<"config">> =>#{<<"meck">> => <<"fake">>},
<<"principal">> => all,
<<"cmd">> => <<"fake cmd">>,
<<"type">> => redis}
]}},
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
ok;
set_special_configs(_App) ->
ok.
-define(RULE1, [<<"test/%u">>, <<"publish">>]).
-define(RULE2, [<<"test/%c">>, <<"publish">>]).
-define(RULE3, [<<"#">>, <<"subscribe">>]).
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_authz(_) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => zone
},
meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end),
% nomatch
?assertEqual(deny,
emqx_access_control:check_acl(ClientInfo, subscribe, <<"#">>)),
?assertEqual(deny,
emqx_access_control:check_acl(ClientInfo, publish, <<"#">>)),
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE1 ++ ?RULE2} end),
% nomatch
?assertEqual(deny,
emqx_access_control:check_acl(ClientInfo, subscribe, <<"+">>)),
% nomatch
?assertEqual(deny,
emqx_access_control:check_acl(ClientInfo, subscribe, <<"test/username">>)),
?assertEqual(allow,
emqx_access_control:check_acl(ClientInfo, publish, <<"test/clientid">>)),
?assertEqual(allow,
emqx_access_control:check_acl(ClientInfo, publish, <<"test/clientid">>)),
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE3} end),
?assertEqual(allow,
emqx_access_control:check_acl(ClientInfo, subscribe, <<"#">>)),
% nomatch
?assertEqual(deny,
emqx_access_control:check_acl(ClientInfo, publish, <<"#">>)),
ok.

View File

@ -84,10 +84,15 @@ listener_name(Proto) ->
list_to_atom(atom_to_list(Proto) ++ ":management"). list_to_atom(atom_to_list(Proto) ++ ":management").
http_handlers() -> http_handlers() ->
Apps = [ App || {App, _, _} <- application:which_applications(),
case re:run(atom_to_list(App), "^emqx") of
{match,[{0,4}]} -> true;
_ -> false
end],
Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()), Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()),
[{"/api/v4", minirest:handler(#{apps => Plugins ++ [emqx_modules] -- ?EXCEPT_PLUGIN, [{"/api/v4", minirest:handler(#{apps => Plugins ++ Apps -- ?EXCEPT_PLUGIN,
except => ?EXCEPT, except => ?EXCEPT,
filter => fun filter/1}), filter => fun(_) -> true end}),
[{authorization, fun authorize_appid/1}]}]. [{authorization, fun authorize_appid/1}]}].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -119,19 +124,6 @@ authorize_appid(Req) ->
_ -> false _ -> false
end. end.
-ifdef(EMQX_ENTERPRISE).
filter(_) ->
true.
-else.
filter(#{app := emqx_modules}) -> true;
filter(#{app := App}) ->
case emqx_plugins:find_plugin(App) of
false -> false;
Plugin -> Plugin#plugin.active
end.
-endif.
format(Port) when is_integer(Port) -> format(Port) when is_integer(Port) ->
io_lib:format("0.0.0.0:~w", [Port]); io_lib:format("0.0.0.0:~w", [Port]);
format({Addr, Port}) when is_list(Addr) -> format({Addr, Port}) when is_list(Addr) ->

View File

@ -1,2 +1 @@
{emqx_mod_acl_internal, true}.
{emqx_mod_presence, true}. {emqx_mod_presence, true}.

View File

@ -8,7 +8,7 @@
{edoc_opts, [{preprocess,true}]}. {edoc_opts, [{preprocess,true}]}.
{erl_opts, [warn_unused_vars,warn_shadow_vars,warn_unused_import, {erl_opts, [warn_unused_vars,warn_shadow_vars,warn_unused_import,
warn_obsolete_guard,compressed, warn_obsolete_guard,compressed, nowarn_unused_import,
{d, snk_kind, msg}]}. {d, snk_kind, msg}]}.
{xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, {xref_checks,[undefined_function_calls,undefined_functions,locals_not_used,

View File

@ -242,6 +242,7 @@ relx_apps(ReleaseType) ->
, {mnesia, load} , {mnesia, load}
, {ekka, load} , {ekka, load}
, {emqx_plugin_libs, load} , {emqx_plugin_libs, load}
, emqx_authz
, observer_cli , observer_cli
, emqx_http_lib , emqx_http_lib
, emqx_resource , emqx_resource
@ -379,6 +380,7 @@ emqx_etc_overlay_common() ->
{"{{base_dir}}/lib/emqx/etc/emqx.conf", "etc/emqx.conf"}, {"{{base_dir}}/lib/emqx/etc/emqx.conf", "etc/emqx.conf"},
{"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}, {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"},
{"{{base_dir}}/lib/emqx_data_bridge/etc/emqx_data_bridge.conf", "etc/plugins/emqx_data_bridge.conf"}, {"{{base_dir}}/lib/emqx_data_bridge/etc/emqx_data_bridge.conf", "etc/plugins/emqx_data_bridge.conf"},
{"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"},
%% TODO: check why it has to end with .paho %% TODO: check why it has to end with .paho
%% and why it is put to etc/plugins dir %% and why it is put to etc/plugins dir
{"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}]. {"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}].