diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index 4e0baa8fd..b11915d14 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -17,8 +17,6 @@ -type(rule() :: {permission(), who(), action(), list(emqx_types:topic())}). -type(rules() :: [rule()]). --type(sources() :: [map()]). - -define(APP, emqx_authz). -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 0b5534608..e2bf18a70 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -27,6 +27,7 @@ -export([ register_metrics/0 , init/0 + , deinit/0 , lookup/0 , lookup/1 , move/2 @@ -42,6 +43,31 @@ -export([ph_to_re/1]). +-type(source() :: map()). + +-type(match_result() :: {matched, allow} | {matched, deny} | nomatch). + +-type(default_result() :: allow | deny). + +-type(authz_result() :: {stop, allow} | {ok, deny}). + +-type(sources() :: [source()]). + + +-callback(init(source()) -> source()). + +-callback(description() -> string()). + +-callback(destroy(source()) -> ok). + +-callback(dry_run(source()) -> ok | {error, term()}). + +-callback(authorize( + emqx_types:clientinfo(), + emqx_types:pubsub(), + emqx_types:topic(), + source()) -> match_result()). + -spec(register_metrics() -> ok). register_metrics() -> lists:foreach(fun emqx_metrics:ensure/1, ?AUTHZ_METRICS). @@ -54,6 +80,11 @@ init() -> NSources = init_sources(Sources), ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NSources]}, -1). +deinit() -> + ok = emqx_hooks:del('client.authorize', {?MODULE, authorize}), + emqx_conf:remove_handler(?CONF_KEY_PATH), + emqx_authz_utils:cleanup_resources(). + lookup() -> {_M, _F, [A]}= find_action_in_hooks(), A. @@ -115,7 +146,7 @@ do_update({{?CMD_REPLACE, Type}, #{<<"enable">> := true} = Source}, Conf) when i NConf = Front ++ [Source | Rear], ok = check_dup_types(NConf), NConf; - Error -> Error + {error, _} = Error -> Error end; do_update({{?CMD_REPLACE, Type}, Source}, Conf) when is_map(Source), is_list(Conf) -> {_Old, Front, Rear} = take(Type, Conf), @@ -178,9 +209,9 @@ do_post_update(_, NewSources) -> ok = emqx_authz_cache:drain_cache(). ensure_resource_deleted(#{enable := false}) -> ok; -ensure_resource_deleted(#{type := file}) -> ok; -ensure_resource_deleted(#{type := 'built-in-database'}) -> ok; -ensure_resource_deleted(#{annotations := #{id := Id}}) -> ok = emqx_resource:remove(Id). +ensure_resource_deleted(#{type := Type} = Source) -> + Module = authz_module(Type), + Module:destroy(Source). check_dup_types(Sources) -> check_dup_types(Sources, []). @@ -204,26 +235,10 @@ check_dup_types([Source | Sources], Checked) -> check_dup_types(Sources, [Type | Checked]) end. -create_dry_run(T, Source) -> - case is_connector_source(T) of - true -> - [CheckedSource] = check_sources([Source]), - case T of - http -> - URIMap = maps:get(url, CheckedSource), - NSource = maps:put(base_url, maps:remove(query, URIMap), CheckedSource) - end, - emqx_resource:create_dry_run(connector_module(T), NSource); - false -> - ok -end. - -is_connector_source(http) -> true; -is_connector_source(mongodb) -> true; -is_connector_source(mysql) -> true; -is_connector_source(postgresql) -> true; -is_connector_source(redis) -> true; -is_connector_source(_) -> false. +create_dry_run(Type, Source) -> + [CheckedSource] = check_sources([Source]), + Module = authz_module(Type), + Module:dry_run(CheckedSource). init_sources(Sources) -> {_Enabled, Disabled} = lists:partition(fun(#{enable := Enable}) -> Enable end, Sources), @@ -234,54 +249,9 @@ init_sources(Sources) -> lists:map(fun init_source/1, Sources). init_source(#{enable := false} = Source) -> Source; -init_source(#{type := file, - path := Path - } = Source) -> - Rules = case file:consult(Path) of - {ok, Terms} -> - [emqx_authz_rule:compile(Term) || Term <- Terms]; - {error, eacces} -> - ?SLOG(alert, #{msg => "insufficient_permissions_to_read_file", path => Path}), - error(eaccess); - {error, enoent} -> - ?SLOG(alert, #{msg => "file_does_not_exist", path => Path}), - error(enoent); - {error, Reason} -> - ?SLOG(alert, #{msg => "failed_to_read_file", path => Path, reason => Reason}), - error(Reason) - end, - Source#{annotations => #{rules => Rules}}; -init_source(#{type := http, - url := Url - } = Source) -> - NSource= maps:put(base_url, maps:remove(query, Url), Source), - case create_resource(NSource) of - {error, Reason} -> error({load_config_error, Reason}); - Id -> Source#{annotations => #{id => Id}} - end; -init_source(#{type := 'built-in-database' - } = Source) -> - Source; -init_source(#{type := DB - } = Source) when DB =:= redis; - DB =:= mongodb -> - case create_resource(Source) of - {error, Reason} -> error({load_config_error, Reason}); - Id -> Source#{annotations => #{id => Id}} - end; -init_source(#{type := DB, - query := SQL - } = Source) when DB =:= mysql; - DB =:= postgresql -> - Mod = authz_module(DB), - case create_resource(Source) of - {error, Reason} -> error({load_config_error, Reason}); - Id -> Source#{annotations => - #{id => Id, - query => erlang:apply(Mod, parse_query, [SQL]) - } - } - end. +init_source(#{type := Type} = Source) -> + Module = authz_module(Type), + Module:init(Source). %%-------------------------------------------------------------------- %% AuthZ callbacks @@ -289,11 +259,11 @@ init_source(#{type := DB, %% @doc Check AuthZ -spec(authorize( emqx_types:clientinfo() - , emqx_types:all() + , emqx_types:pubsub() , emqx_types:topic() - , allow | deny + , default_result() , sources()) - -> {stop, allow} | {ok, deny}). + -> authz_result()). authorize(#{username := Username, peerhost := IpAddress } = Client, PubSub, Topic, DefaultResult, Sources) -> @@ -325,16 +295,10 @@ do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; do_authorize(Client, PubSub, Topic, [#{enable := false} | Rest]) -> do_authorize(Client, PubSub, Topic, Rest); -do_authorize(Client, PubSub, Topic, [#{type := file} = F | Tail]) -> - #{annotations := #{rules := Rules}} = F, - case emqx_authz_rule:matches(Client, PubSub, Topic, Rules) of - nomatch -> do_authorize(Client, PubSub, Topic, Tail); - Matched -> Matched - end; do_authorize(Client, PubSub, Topic, [Connector = #{type := Type} | Tail] ) -> - Mod = authz_module(Type), - case erlang:apply(Mod, authorize, [Client, PubSub, Topic, Connector]) of + Module = authz_module(Type), + case Module:authorize(Client, PubSub, Topic, Connector) of nomatch -> do_authorize(Client, PubSub, Topic, Tail); Matched -> Matched end. @@ -367,29 +331,13 @@ find_action_in_hooks() -> [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], Action. -gen_id(Type) -> - iolist_to_binary([io_lib:format("~ts_~ts",[?APP, Type])]). - -create_resource(#{type := DB} = Source) -> - ResourceID = gen_id(DB), - case emqx_resource:create(ResourceID, connector_module(DB), Source) of - {ok, already_created} -> ResourceID; - {ok, _} -> ResourceID; - {error, Reason} -> {error, Reason} - end. - authz_module('built-in-database') -> emqx_authz_mnesia; +authz_module(file) -> + emqx_authz_rule; authz_module(Type) -> list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)). -connector_module(mongodb) -> - emqx_connector_mongo; -connector_module(postgresql) -> - emqx_connector_pgsql; -connector_module(Type) -> - list_to_existing_atom("emqx_connector_" ++ atom_to_list(Type)). - type(#{type := Type}) -> type(Type); type(#{<<"type">> := Type}) -> type(Type); type(file) -> file; diff --git a/apps/emqx_authz/src/emqx_authz_app.erl b/apps/emqx_authz/src/emqx_authz_app.erl index cf9685650..0fb5c4e02 100644 --- a/apps/emqx_authz/src/emqx_authz_app.erl +++ b/apps/emqx_authz/src/emqx_authz_app.erl @@ -34,7 +34,7 @@ start(_StartType, _StartArgs) -> {ok, Sup}. stop(_State) -> - emqx_conf:remove_handler(?CONF_KEY_PATH), + ok = emqx_authz:deinit(), ok. %% internal functions diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index 6d1324c47..62719b9ed 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -21,9 +21,14 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-behaviour(emqx_authz). + %% AuthZ Callbacks --export([ authorize/4 - , description/0 +-export([ description/0 + , init/1 + , destroy/1 + , dry_run/1 + , authorize/4 , parse_url/1 ]). @@ -35,6 +40,21 @@ description() -> "AuthZ with http". +init(#{url := Url} = Source) -> + NSource= maps:put(base_url, maps:remove(query, Url), Source), + case emqx_authz_utils:create_resource(emqx_connector_http, NSource) of + {error, Reason} -> error({load_config_error, Reason}); + {ok, Id} -> Source#{annotations => #{id => Id}} + end. + +destroy(#{annotations := #{id := Id}}) -> + ok = emqx_resource:remove(Id). + +dry_run(Source) -> + URIMap = maps:get(url, Source), + NSource = maps:put(base_url, maps:remove(query, URIMap), Source), + emqx_resource:create_dry_run(emqx_connector_http, NSource). + authorize(Client, PubSub, Topic, #{type := http, url := #{path := Path} = URL, diff --git a/apps/emqx_authz/src/emqx_authz_mnesia.erl b/apps/emqx_authz/src/emqx_authz_mnesia.erl index 3851affed..d652c6731 100644 --- a/apps/emqx_authz/src/emqx_authz_mnesia.erl +++ b/apps/emqx_authz/src/emqx_authz_mnesia.erl @@ -20,10 +20,15 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). +-behaviour(emqx_authz). + %% AuthZ Callbacks -export([ mnesia/1 - , authorize/4 , description/0 + , init/1 + , destroy/1 + , dry_run/1 + , authorize/4 ]). -ifdef(TEST). @@ -45,6 +50,12 @@ mnesia(boot) -> description() -> "AuthZ with Mnesia". +init(Source) -> Source. + +destroy(_Source) -> ok. + +dry_run(_Source) -> ok. + authorize(#{username := Username, clientid := Clientid } = Client, PubSub, Topic, #{type := 'built-in-database'}) -> diff --git a/apps/emqx_authz/src/emqx_authz_mongodb.erl b/apps/emqx_authz/src/emqx_authz_mongodb.erl index 5b55c23b7..439c2c853 100644 --- a/apps/emqx_authz/src/emqx_authz_mongodb.erl +++ b/apps/emqx_authz/src/emqx_authz_mongodb.erl @@ -21,9 +21,14 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-behaviour(emqx_authz). + %% AuthZ Callbacks --export([ authorize/4 - , description/0 +-export([ description/0 + , init/1 + , destroy/1 + , dry_run/1 + , authorize/4 ]). -ifdef(TEST). @@ -34,6 +39,18 @@ description() -> "AuthZ with MongoDB". +init(Source) -> + case emqx_authz_utils:create_resource(emqx_connector_mongo, Source) of + {error, Reason} -> error({load_config_error, Reason}); + {ok, Id} -> Source#{annotations => #{id => Id}} + end. + +dry_run(Source) -> + emqx_resource:create_dry_run(emqx_connector_mongo, Source). + +destroy(#{annotations := #{id := Id}}) -> + ok = emqx_resource:remove(Id). + authorize(Client, PubSub, Topic, #{collection := Collection, selector := Selector, diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index a3a5e1ed9..118b00a4f 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -21,9 +21,13 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-behaviour(emqx_authz). + %% AuthZ Callbacks -export([ description/0 - , parse_query/1 + , init/1 + , destroy/1 + , dry_run/1 , authorize/4 ]). @@ -35,6 +39,20 @@ description() -> "AuthZ with Mysql". +init(#{query := SQL} = Source) -> + case emqx_authz_utils:create_resource(emqx_connector_mysql, Source) of + {error, Reason} -> error({load_config_error, Reason}); + {ok, Id} -> Source#{annotations => + #{id => Id, + query => parse_query(SQL)}} + end. + +dry_run(Source) -> + emqx_resource:create_dry_run(emqx_connector_mysql, Source). + +destroy(#{annotations := #{id := Id}}) -> + ok = emqx_resource:remove(Id). + parse_query(undefined) -> undefined; parse_query(Sql) -> diff --git a/apps/emqx_authz/src/emqx_authz_postgresql.erl b/apps/emqx_authz/src/emqx_authz_postgresql.erl index 5bae5f674..6034bfd15 100644 --- a/apps/emqx_authz/src/emqx_authz_postgresql.erl +++ b/apps/emqx_authz/src/emqx_authz_postgresql.erl @@ -21,9 +21,13 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-behaviour(emqx_authz). + %% AuthZ Callbacks -export([ description/0 - , parse_query/1 + , init/1 + , destroy/1 + , dry_run/1 , authorize/4 ]). @@ -33,7 +37,21 @@ -endif. description() -> - "AuthZ with postgresql". + "AuthZ with Postgresql". + +init(#{query := SQL} = Source) -> + case emqx_authz_utils:create_resource(emqx_connector_pgsql, Source) of + {error, Reason} -> error({load_config_error, Reason}); + {ok, Id} -> Source#{annotations => + #{id => Id, + query => parse_query(SQL)}} + end. + +destroy(#{annotations := #{id := Id}}) -> + ok = emqx_resource:remove(Id). + +dry_run(Source) -> + emqx_resource:create_dry_run(emqx_connector_pgsql, Source). parse_query(undefined) -> undefined; diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 8fa1e94c3..fc60d57ad 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -21,9 +21,14 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-behaviour(emqx_authz). + %% AuthZ Callbacks --export([ authorize/4 - , description/0 +-export([ description/0 + , init/1 + , destroy/1 + , dry_run/1 + , authorize/4 ]). -ifdef(TEST). @@ -32,7 +37,19 @@ -endif. description() -> - "AuthZ with redis". + "AuthZ with Redis". + +init(Source) -> + case emqx_authz_utils:create_resource(emqx_connector_redis, Source) of + {error, Reason} -> error({load_config_error, Reason}); + {ok, Id} -> Source#{annotations => #{id => Id}} + end. + +destroy(#{annotations := #{id := Id}}) -> + ok = emqx_resource:remove(Id). + +dry_run(Source) -> + emqx_resource:create_dry_run(emqx_connector_redis, Source). authorize(Client, PubSub, Topic, #{cmd := CMD, diff --git a/apps/emqx_authz/src/emqx_authz_rule.erl b/apps/emqx_authz/src/emqx_authz_rule.erl index 5b6885e22..ade364788 100644 --- a/apps/emqx_authz/src/emqx_authz_rule.erl +++ b/apps/emqx_authz/src/emqx_authz_rule.erl @@ -20,19 +20,49 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-behaviour(emqx_authz). + -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). -endif. %% APIs --export([ match/4 - , matches/4 - , compile/1 +-export([ description/0 + , init/1 + , destroy/1 + , dry_run/1 + , authorize/4 ]). -export_type([rule/0]). +description() -> + "AuthZ with static rules". + +init(#{path := Path} = Source) -> + Rules = case file:consult(Path) of + {ok, Terms} -> + [compile(Term) || Term <- Terms]; + {error, eacces} -> + ?SLOG(alert, #{msg => "insufficient_permissions_to_read_file", path => Path}), + error(eaccess); + {error, enoent} -> + ?SLOG(alert, #{msg => "file_does_not_exist", path => Path}), + error(enoent); + {error, Reason} -> + ?SLOG(alert, #{msg => "failed_to_read_file", path => Path, reason => Reason}), + error(Reason) + end, + Source#{annotations => #{rules => Rules}}. + +destroy(_Source) -> ok. + +dry_run(_Source) -> ok. + +authorize(Client, PubSub, Topic, #{annotations := #{rules := Rules}}) -> + matches(Client, PubSub, Topic, Rules). + compile({Permission, all}) when ?ALLOW_DENY(Permission) -> {Permission, all, all, [compile_topic(<<"#">>)]}; compile({Permission, Who, Action, TopicFilters}) diff --git a/apps/emqx_authz/src/emqx_authz_utils.erl b/apps/emqx_authz/src/emqx_authz_utils.erl new file mode 100644 index 000000000..73132aacb --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_utils.erl @@ -0,0 +1,54 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 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_utils). + +-include_lib("emqx/include/emqx_placeholder.hrl"). + +-export([cleanup_resources/0, + make_resource_id/1, + create_resource/2]). + +-define(RESOURCE_GROUP, <<"emqx_authz">>). + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create_resource(Module, Config) -> + ResourceID = make_resource_id(Module), + case emqx_resource:create(ResourceID, Module, Config) of + {ok, already_created} -> {ok, ResourceID}; + {ok, _} -> {ok, ResourceID}; + {error, Reason} -> {error, Reason} + end. + +cleanup_resources() -> + lists:foreach( + fun emqx_resource:remove/1, + emqx_resource:list_group_instances(?RESOURCE_GROUP)). + +make_resource_id(Name) -> + NameBin = bin(Name), + emqx_resource:generate_id(?RESOURCE_GROUP, NameBin). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> list_to_binary(L); +bin(X) -> X.