diff --git a/apps/emqx_authz/.gitignore b/apps/emqx_authz/.gitignore new file mode 100644 index 000000000..f1c455451 --- /dev/null +++ b/apps/emqx_authz/.gitignore @@ -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 +*~ diff --git a/apps/emqx_authz/README.md b/apps/emqx_authz/README.md new file mode 100644 index 000000000..0fddac9b0 --- /dev/null +++ b/apps/emqx_authz/README.md @@ -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. + diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf new file mode 100644 index 000000000..b97bc12f0 --- /dev/null +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -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: ["#"] + } + ] +} diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl new file mode 100644 index 000000000..ca595525d --- /dev/null +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -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)). diff --git a/apps/emqx_authz/rebar.config b/apps/emqx_authz/rebar.config new file mode 100644 index 000000000..ba38cc642 --- /dev/null +++ b/apps/emqx_authz/rebar.config @@ -0,0 +1,7 @@ +{erl_opts, [debug_info, nowarn_unused_import]}. +{deps, []}. + +{shell, [ + % {config, "config/sys.config"}, + {apps, [emqx_authz]} +]}. diff --git a/apps/emqx_authz/src/emqx_authz.app.src b/apps/emqx_authz/src/emqx_authz.app.src new file mode 100644 index 000000000..10801eca1 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz.app.src @@ -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, []} + ]}. diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl new file mode 100644 index 000000000..0880ef4ae --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -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]). + diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl new file mode 100644 index 000000000..6b4ed6c74 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -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. diff --git a/apps/emqx_authz/src/emqx_authz_app.erl b/apps/emqx_authz/src/emqx_authz_app.erl new file mode 100644 index 000000000..460d7cbf9 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_app.erl @@ -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 diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl new file mode 100644 index 000000000..c1ab20125 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -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. + diff --git a/apps/emqx_authz/src/emqx_authz_pgsql.erl b/apps/emqx_authz/src/emqx_authz_pgsql.erl new file mode 100644 index 000000000..edea8102f --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_pgsql.erl @@ -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. + diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl new file mode 100644 index 000000000..7a85b26af --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -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}]). diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl new file mode 100644 index 000000000..1801583c8 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -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 + }. diff --git a/apps/emqx_authz/src/emqx_authz_sup.erl b/apps/emqx_authz/src/emqx_authz_sup.erl new file mode 100644 index 000000000..11a9eb71b --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_sup.erl @@ -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 diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl new file mode 100644 index 000000000..88e250377 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -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. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl new file mode 100644 index 000000000..4871e0d7f --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -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. diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl new file mode 100644 index 000000000..4f2148522 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -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. + diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl new file mode 100644 index 000000000..dcc820a4c --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -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. + diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl new file mode 100644 index 000000000..e4b49dd5a --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -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. + diff --git a/apps/emqx_management/src/emqx_mgmt_http.erl b/apps/emqx_management/src/emqx_mgmt_http.erl index da5a75027..b902e486e 100644 --- a/apps/emqx_management/src/emqx_mgmt_http.erl +++ b/apps/emqx_management/src/emqx_mgmt_http.erl @@ -84,10 +84,15 @@ listener_name(Proto) -> list_to_atom(atom_to_list(Proto) ++ ":management"). 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()), - [{"/api/v4", minirest:handler(#{apps => Plugins ++ [emqx_modules] -- ?EXCEPT_PLUGIN, + [{"/api/v4", minirest:handler(#{apps => Plugins ++ Apps -- ?EXCEPT_PLUGIN, except => ?EXCEPT, - filter => fun filter/1}), + filter => fun(_) -> true end}), [{authorization, fun authorize_appid/1}]}]. %%-------------------------------------------------------------------- @@ -119,19 +124,6 @@ authorize_appid(Req) -> _ -> false 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) -> io_lib:format("0.0.0.0:~w", [Port]); format({Addr, Port}) when is_list(Addr) -> diff --git a/data/loaded_modules.tmpl b/data/loaded_modules.tmpl index 25ecbd4bf..f31e47900 100644 --- a/data/loaded_modules.tmpl +++ b/data/loaded_modules.tmpl @@ -1,2 +1 @@ -{emqx_mod_acl_internal, true}. {emqx_mod_presence, true}. diff --git a/rebar.config b/rebar.config index 0c4a11071..d670769c6 100644 --- a/rebar.config +++ b/rebar.config @@ -8,7 +8,7 @@ {edoc_opts, [{preprocess,true}]}. {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}]}. {xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, diff --git a/rebar.config.erl b/rebar.config.erl index fef4c864d..99293ef77 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -249,6 +249,7 @@ relx_apps(ReleaseType) -> , {mnesia, load} , {ekka, load} , {emqx_plugin_libs, load} + , emqx_authz , observer_cli , emqx_http_lib , emqx_resource @@ -386,6 +387,7 @@ emqx_etc_overlay_common() -> {"{{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_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 %% and why it is put to etc/plugins dir {"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}].