fix(authz): resources not created when authz started

This commit is contained in:
Shawn 2021-07-16 18:04:12 +08:00
parent bb6d7c4e0b
commit 6d871cc52f
11 changed files with 89 additions and 84 deletions

View File

@ -113,12 +113,9 @@ do_update_config([], Handlers, OldConf, UpdateReq) ->
call_handle_update_config(Handlers, OldConf, UpdateReq); call_handle_update_config(Handlers, OldConf, UpdateReq);
do_update_config([ConfKey | ConfKeyPath], Handlers, OldConf, UpdateReq) -> do_update_config([ConfKey | ConfKeyPath], Handlers, OldConf, UpdateReq) ->
SubOldConf = get_sub_config(ConfKey, OldConf), SubOldConf = get_sub_config(ConfKey, OldConf),
case maps:find(ConfKey, Handlers) of SubHandlers = maps:get(ConfKey, Handlers, #{}),
error -> throw({handler_not_found, ConfKey}); NewUpdateReq = do_update_config(ConfKeyPath, SubHandlers, SubOldConf, UpdateReq),
{ok, SubHandlers} -> call_handle_update_config(Handlers, OldConf, #{bin(ConfKey) => NewUpdateReq}).
NewUpdateReq = do_update_config(ConfKeyPath, SubHandlers, SubOldConf, UpdateReq),
call_handle_update_config(Handlers, OldConf, #{bin(ConfKey) => NewUpdateReq})
end.
get_sub_config(_, undefined) -> get_sub_config(_, undefined) ->
undefined; undefined;
@ -131,7 +128,7 @@ call_handle_update_config(Handlers, OldConf, UpdateReq) ->
HandlerName = maps:get(?MOD, Handlers, undefined), HandlerName = maps:get(?MOD, Handlers, undefined),
case erlang:function_exported(HandlerName, handle_update_config, 2) of case erlang:function_exported(HandlerName, handle_update_config, 2) of
true -> HandlerName:handle_update_config(UpdateReq, OldConf); true -> HandlerName:handle_update_config(UpdateReq, OldConf);
false -> UpdateReq %% the default behaviour is overwriting the old config false -> merge_to_old_config(UpdateReq, OldConf)
end. end.
%% callbacks for the top-level handler %% callbacks for the top-level handler
@ -139,11 +136,15 @@ handle_update_config(UpdateReq, OldConf) ->
FullRawConf = merge_to_old_config(UpdateReq, OldConf), FullRawConf = merge_to_old_config(UpdateReq, OldConf),
{maps:keys(UpdateReq), FullRawConf}. {maps:keys(UpdateReq), FullRawConf}.
%% default callback of config handlers %% The default callback of config handlers
merge_to_old_config(UpdateReq, undefined) -> %% the behaviour is overwriting the old config if:
merge_to_old_config(UpdateReq, #{}); %% 1. the old config is undefined
merge_to_old_config(UpdateReq, RawConf) -> %% 2. either the old or the new config is not of map type
maps:merge(RawConf, UpdateReq). %% the behaviour is merging the new the config to the old config if they are maps.
merge_to_old_config(UpdateReq, RawConf) when is_map(UpdateReq), is_map(RawConf) ->
maps:merge(RawConf, UpdateReq);
merge_to_old_config(UpdateReq, _RawConf) ->
UpdateReq.
%%============================================================================ %%============================================================================
save_configs(RootKeys, RawConf) -> save_configs(RootKeys, RawConf) ->
@ -199,8 +200,9 @@ load_config_file() ->
end, #{}, emqx:get_env(config_files, [])). end, #{}, emqx:get_env(config_files, [])).
emqx_override_conf_name() -> emqx_override_conf_name() ->
filename:join([emqx:get_env(data_dir), "emqx_override.conf"]). File = filename:join([emqx:get_env(data_dir), "emqx_override.conf"]),
ok = filelib:ensure_dir(File),
File.
to_richmap(Map) -> to_richmap(Map) ->
{ok, RichMap} = hocon:binary(jsx:encode(Map), #{format => richmap}), {ok, RichMap} = hocon:binary(jsx:encode(Map), #{format => richmap}),

View File

@ -15,6 +15,7 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_authz). -module(emqx_authz).
-behaviour(emqx_config_handler).
-include("emqx_authz.hrl"). -include("emqx_authz.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
@ -23,30 +24,41 @@
-export([ register_metrics/0 -export([ register_metrics/0
, init/0 , init/0
, compile/1 , init_rule/1
, lookup/0 , lookup/0
, update/1 , update/1
, authorize/4 , authorize/5
, match/4 , match/4
]). ]).
-export([handle_update_config/2]).
-define(CONF_KEY_PATH, [emqx_authz, rules]).
-spec(register_metrics() -> ok). -spec(register_metrics() -> ok).
register_metrics() -> register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?AUTHZ_METRICS). lists:foreach(fun emqx_metrics:ensure/1, ?AUTHZ_METRICS).
init() -> init() ->
ok = register_metrics(), ok = register_metrics(),
ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, []}, -1). emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE),
NRules = [init_rule(Rule) || Rule <- lookup()],
ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1).
lookup() -> lookup() ->
emqx_config:get([emqx_authz, rules], []). emqx_config:get(?CONF_KEY_PATH, []).
update(Rules) -> update(Rules) ->
emqx_config:put([emqx_authz], #{rules => Rules}), emqx_config:update_config(?CONF_KEY_PATH, Rules).
%% For now we only support re-creating the entire rule list
handle_update_config(Rules, _OldConf) ->
InitedRules = [init_rule(Rule) || Rule <- Rules],
Action = find_action_in_hooks(), Action = find_action_in_hooks(),
ok = emqx_hooks:del('client.authorize', Action), ok = emqx_hooks:del('client.authorize', Action),
ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, []}, -1), ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [InitedRules]}, -1),
ok = emqx_acl_cache:empty_acl_cache(). ok = emqx_acl_cache:drain_cache(),
Rules.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
@ -74,27 +86,27 @@ create_resource(#{type := DB,
error({load_config_error, Reason}) error({load_config_error, Reason})
end. end.
-spec(compile(rule()) -> rule()). -spec(init_rule(rule()) -> rule()).
compile(#{topics := Topics, init_rule(#{topics := Topics,
action := Action, action := Action,
permission := Permission, permission := Permission,
principal := Principal principal := Principal
} = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) -> } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) ->
NTopics = [compile_topic(Topic) || Topic <- Topics], NTopics = [compile_topic(Topic) || Topic <- Topics],
Rule#{principal => compile_principal(Principal), Rule#{principal => compile_principal(Principal),
topics => NTopics topics => NTopics
}; };
compile(#{principal := Principal, init_rule(#{principal := Principal,
type := DB type := DB
} = Rule) when DB =:= redis; } = Rule) when DB =:= redis;
DB =:= mongo -> DB =:= mongo ->
NRule = create_resource(Rule), NRule = create_resource(Rule),
NRule#{principal => compile_principal(Principal)}; NRule#{principal => compile_principal(Principal)};
compile(#{principal := Principal, init_rule(#{principal := Principal,
type := DB, type := DB,
sql := SQL sql := SQL
} = Rule) when DB =:= mysql; } = Rule) when DB =:= mysql;
DB =:= pgsql -> DB =:= pgsql ->
Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])), Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])),
@ -144,11 +156,12 @@ b2l(B) when is_binary(B) -> binary_to_list(B).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% @doc Check AuthZ %% @doc Check AuthZ
-spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), -spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules())
emqx_permission_rule:acl_result()) -> {stop, allow} | {ok, deny}). -> {stop, allow} | {ok, deny}).
authorize(#{username := Username, peerhost := IpAddress} = Client, authorize(#{username := Username,
PubSub, Topic, _DefaultResult) -> peerhost := IpAddress
case do_authorize(Client, PubSub, Topic, [compile(Rule) || Rule <- lookup()]) of } = Client, PubSub, Topic, _DefaultResult, Rules) ->
case do_authorize(Client, PubSub, Topic, Rules) of
{matched, allow} -> {matched, allow} ->
?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]), ?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]),
emqx_metrics:inc(?AUTHZ_METRICS(allow)), emqx_metrics:inc(?AUTHZ_METRICS(allow)),

View File

@ -71,7 +71,7 @@ match(Client, PubSub, Topic,
#{<<"simple_rule">> => Rule}, #{<<"simple_rule">> => Rule},
#{atom_key => true}, #{atom_key => true},
[simple_rule]), [simple_rule]),
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of
true -> {matched, NPermission}; true -> {matched, NPermission};
false -> nomatch false -> nomatch
end. end.

View File

@ -90,7 +90,7 @@ match(Client, PubSub, Topic,
#{<<"simple_rule">> => Rule}, #{<<"simple_rule">> => Rule},
#{atom_key => true}, #{atom_key => true},
[simple_rule]), [simple_rule]),
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of
true -> {matched, NPermission}; true -> {matched, NPermission};
false -> nomatch false -> nomatch
end. end.

View File

@ -94,7 +94,7 @@ match(Client, PubSub, Topic,
#{<<"simple_rule">> => Rule}, #{<<"simple_rule">> => Rule},
#{atom_key => true}, #{atom_key => true},
[simple_rule]), [simple_rule]),
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of
true -> {matched, NPermission}; true -> {matched, NPermission};
false -> nomatch false -> nomatch
end. end.

View File

@ -74,7 +74,7 @@ match(Client, PubSub, Topic,
#{<<"simple_rule">> => Rule}, #{<<"simple_rule">> => Rule},
#{atom_key => true}, #{atom_key => true},
[simple_rule]), [simple_rule]),
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of case emqx_authz:match(Client, PubSub, Topic, emqx_authz:init_rule(NRule)) of
true -> {matched, allow}; true -> {matched, allow};
false -> nomatch false -> nomatch
end. end.

View File

@ -30,9 +30,9 @@ groups() ->
init_per_suite(Config) -> init_per_suite(Config) ->
ok = emqx_ct_helpers:start_apps([emqx_authz]), ok = emqx_ct_helpers:start_apps([emqx_authz]),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), ok = emqx_config:update_config([zones, default, acl, cache, enable], false),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), ok = emqx_config:update_config([zones, default, acl, enable], true),
emqx_config:put([emqx_authz], #{rules => []}), emqx_authz:update([]),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
@ -74,19 +74,19 @@ end_per_suite(_Config) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_compile(_) -> t_init_rule(_) ->
?assertEqual(#{permission => deny, ?assertEqual(#{permission => deny,
action => all, action => all,
principal => all, principal => all,
topics => [['#']] topics => [['#']]
}, emqx_authz:compile(?RULE1)), }, emqx_authz:init_rule(?RULE1)),
?assertEqual(#{permission => allow, ?assertEqual(#{permission => allow,
action => all, action => all,
principal => principal =>
#{ipaddress => {{127,0,0,1},{127,0,0,1},32}}, #{ipaddress => {{127,0,0,1},{127,0,0,1},32}},
topics => [#{eq => ['#']}, topics => [#{eq => ['#']},
#{eq => ['+']}] #{eq => ['+']}]
}, emqx_authz:compile(?RULE2)), }, emqx_authz:init_rule(?RULE2)),
?assertMatch( ?assertMatch(
#{permission := allow, #{permission := allow,
action := publish, action := publish,
@ -96,7 +96,7 @@ t_compile(_) ->
] ]
}, },
topics := [[<<"test">>]] topics := [[<<"test">>]]
}, emqx_authz:compile(?RULE3)), }, emqx_authz:init_rule(?RULE3)),
?assertMatch( ?assertMatch(
#{permission := deny, #{permission := deny,
action := publish, action := publish,
@ -108,7 +108,7 @@ t_compile(_) ->
topics := [#{pattern := [<<"%u">>]}, topics := [#{pattern := [<<"%u">>]},
#{pattern := [<<"%c">>]} #{pattern := [<<"%c">>]}
] ]
}, emqx_authz:compile(?RULE4)), }, emqx_authz:init_rule(?RULE4)),
ok. ok.
t_authz(_) -> t_authz(_) ->
@ -137,39 +137,29 @@ t_authz(_) ->
listener => mqtt_tcp listener => mqtt_tcp
}, },
Rules1 = [?RULE1, ?RULE2], Rules1 = [emqx_authz:init_rule(Rule) || Rule <- [?RULE1, ?RULE2]],
Rules2 = [?RULE2, ?RULE1], Rules2 = [emqx_authz:init_rule(Rule) || Rule <- [?RULE2, ?RULE1]],
Rules3 = [?RULE3, ?RULE4], Rules3 = [emqx_authz:init_rule(Rule) || Rule <- [?RULE3, ?RULE4]],
Rules4 = [?RULE4, ?RULE1], Rules4 = [emqx_authz:init_rule(Rule) || Rule <- [?RULE4, ?RULE1]],
emqx_config:put([emqx_authz], #{rules => []}),
?assertEqual({stop, deny}, ?assertEqual({stop, deny},
emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny)), emqx_authz:authorize(ClientInfo1, subscribe, <<"#">>, deny, [])),
emqx_config:put([emqx_authz], #{rules => Rules1}),
?assertEqual({stop, deny}, ?assertEqual({stop, deny},
emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny)), emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules1)),
emqx_config:put([emqx_authz], #{rules => Rules2}),
?assertEqual({stop, allow}, ?assertEqual({stop, allow},
emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny)), emqx_authz:authorize(ClientInfo1, subscribe, <<"+">>, deny, Rules2)),
emqx_config:put([emqx_authz], #{rules => Rules3}),
?assertEqual({stop, allow}, ?assertEqual({stop, allow},
emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny)), emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules3)),
emqx_config:put([emqx_authz], #{rules => Rules4}),
?assertEqual({stop, deny}, ?assertEqual({stop, deny},
emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny)), emqx_authz:authorize(ClientInfo1, publish, <<"test">>, deny, Rules4)),
emqx_config:put([emqx_authz], #{rules => Rules2}),
?assertEqual({stop, deny}, ?assertEqual({stop, deny},
emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny)), emqx_authz:authorize(ClientInfo2, subscribe, <<"#">>, deny, Rules2)),
emqx_config:put([emqx_authz], #{rules => Rules3}),
?assertEqual({stop, deny}, ?assertEqual({stop, deny},
emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny)), emqx_authz:authorize(ClientInfo3, publish, <<"test">>, deny, Rules3)),
emqx_config:put([emqx_authz], #{rules => Rules4}),
?assertEqual({stop, deny}, ?assertEqual({stop, deny},
emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny)), emqx_authz:authorize(ClientInfo3, publish, <<"fake">>, deny, Rules4)),
emqx_config:put([emqx_authz], #{rules => Rules3}),
?assertEqual({stop, deny}, ?assertEqual({stop, deny},
emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny)), emqx_authz:authorize(ClientInfo4, publish, <<"test">>, deny, Rules3)),
emqx_config:put([emqx_authz], #{rules => Rules4}),
?assertEqual({stop, deny}, ?assertEqual({stop, deny},
emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny)), emqx_authz:authorize(ClientInfo4, publish, <<"fake">>, deny, Rules4)),
ok. ok.

View File

@ -33,15 +33,15 @@ init_per_suite(Config) ->
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ),
ok = emqx_ct_helpers:start_apps([emqx_authz]), ok = emqx_ct_helpers:start_apps([emqx_authz]),
ct:pal("---- emqx_hooks: ~p", [ets:tab2list(emqx_hooks)]), ct:pal("---- emqx_hooks: ~p", [ets:tab2list(emqx_hooks)]),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), ok = emqx_config:update_config([zones, default, acl, cache, enable], false),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), ok = emqx_config:update_config([zones, default, acl, enable], true),
Rules = [#{config =>#{}, Rules = [#{config =>#{},
principal => all, principal => all,
collection => <<"fake">>, collection => <<"fake">>,
find => #{<<"a">> => <<"b">>}, find => #{<<"a">> => <<"b">>},
type => mongo} type => mongo}
], ],
emqx_config:put([emqx_authz], #{rules => Rules}), emqx_authz:update(Rules),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->

View File

@ -32,14 +32,14 @@ init_per_suite(Config) ->
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ),
ok = emqx_ct_helpers:start_apps([emqx_authz]), ok = emqx_ct_helpers:start_apps([emqx_authz]),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), ok = emqx_config:update_config([zones, default, acl, cache, enable], false),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), ok = emqx_config:update_config([zones, default, acl, enable], true),
Rules = [#{config =>#{}, Rules = [#{config =>#{},
principal => all, principal => all,
sql => <<"fake">>, sql => <<"fake">>,
type => mysql} type => mysql}
], ],
emqx_config:put([emqx_authz], #{rules => Rules}), emqx_authz:update(Rules),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->

View File

@ -32,14 +32,14 @@ init_per_suite(Config) ->
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ),
ok = emqx_ct_helpers:start_apps([emqx_authz]), ok = emqx_ct_helpers:start_apps([emqx_authz]),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), ok = emqx_config:update_config([zones, default, acl, cache, enable], false),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), ok = emqx_config:update_config([zones, default, acl, enable], true),
Rules = [#{config =>#{}, Rules = [#{config =>#{},
principal => all, principal => all,
sql => <<"fake">>, sql => <<"fake">>,
type => pgsql} type => pgsql}
], ],
emqx_config:put([emqx_authz], #{rules => Rules}), emqx_authz:update(Rules),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->

View File

@ -32,14 +32,14 @@ init_per_suite(Config) ->
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ), meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end ),
ok = emqx_ct_helpers:start_apps([emqx_authz]), ok = emqx_ct_helpers:start_apps([emqx_authz]),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, cache, enable], false), ok = emqx_config:update_config([zones, default, acl, cache, enable], false),
emqx_config:put_listener_conf(default, mqtt_tcp, [acl, enable], true), ok = emqx_config:update_config([zones, default, acl, enable], true),
Rules = [#{config =>#{}, Rules = [#{config =>#{},
principal => all, principal => all,
cmd => <<"fake">>, cmd => <<"fake">>,
type => redis} type => redis}
], ],
emqx_config:put([emqx_authz], #{rules => Rules}), emqx_authz:update(Rules),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->