Merge pull request #10765 from zhongwencool/port-10742-to-release-50
Port 10742 to release 50
This commit is contained in:
commit
a1a5681d5b
|
@ -238,6 +238,8 @@ render_and_load_app_config(App, Opts) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_render_app_config(App, Schema, ConfigFile, Opts) ->
|
do_render_app_config(App, Schema, ConfigFile, Opts) ->
|
||||||
|
%% copy acl_conf must run before read_schema_configs
|
||||||
|
copy_acl_conf(),
|
||||||
Vars = mustache_vars(App, Opts),
|
Vars = mustache_vars(App, Opts),
|
||||||
RenderedConfigFile = render_config_file(ConfigFile, Vars),
|
RenderedConfigFile = render_config_file(ConfigFile, Vars),
|
||||||
read_schema_configs(Schema, RenderedConfigFile),
|
read_schema_configs(Schema, RenderedConfigFile),
|
||||||
|
@ -497,6 +499,16 @@ copy_certs(emqx_conf, Dest0) ->
|
||||||
copy_certs(_, _) ->
|
copy_certs(_, _) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
copy_acl_conf() ->
|
||||||
|
Dest = filename:join([code:lib_dir(emqx), "etc/acl.conf"]),
|
||||||
|
case code:lib_dir(emqx_authz) of
|
||||||
|
{error, bad_name} ->
|
||||||
|
(not filelib:is_regular(Dest)) andalso file:write_file(Dest, <<"">>);
|
||||||
|
_ ->
|
||||||
|
{ok, _} = file:copy(deps_path(emqx_authz, "etc/acl.conf"), Dest)
|
||||||
|
end,
|
||||||
|
ok.
|
||||||
|
|
||||||
load_config(SchemaModule, Config) ->
|
load_config(SchemaModule, Config) ->
|
||||||
ConfigBin =
|
ConfigBin =
|
||||||
case is_map(Config) of
|
case is_map(Config) of
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_authz, [
|
{application, emqx_authz, [
|
||||||
{description, "An OTP application"},
|
{description, "An OTP application"},
|
||||||
{vsn, "0.1.19"},
|
{vsn, "0.1.20"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{mod, {emqx_authz_app, []}},
|
{mod, {emqx_authz_app, []}},
|
||||||
{applications, [
|
{applications, [
|
||||||
|
|
|
@ -140,7 +140,12 @@ update(Cmd, Sources) ->
|
||||||
emqx_authz_utils:update_config(?CONF_KEY_PATH, {Cmd, Sources}).
|
emqx_authz_utils:update_config(?CONF_KEY_PATH, {Cmd, Sources}).
|
||||||
|
|
||||||
pre_config_update(_, Cmd, Sources) ->
|
pre_config_update(_, Cmd, Sources) ->
|
||||||
{ok, do_pre_config_update(Cmd, Sources)}.
|
try do_pre_config_update(Cmd, Sources) of
|
||||||
|
{error, Reason} -> {error, Reason};
|
||||||
|
NSources -> {ok, NSources}
|
||||||
|
catch
|
||||||
|
_:Reason -> {error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
do_pre_config_update({?CMD_MOVE, _, _} = Cmd, Sources) ->
|
do_pre_config_update({?CMD_MOVE, _, _} = Cmd, Sources) ->
|
||||||
do_move(Cmd, Sources);
|
do_move(Cmd, Sources);
|
||||||
|
@ -475,11 +480,14 @@ maybe_write_files(#{<<"type">> := <<"file">>} = Source) ->
|
||||||
maybe_write_files(NewSource) ->
|
maybe_write_files(NewSource) ->
|
||||||
maybe_write_certs(NewSource).
|
maybe_write_certs(NewSource).
|
||||||
|
|
||||||
write_acl_file(#{<<"rules">> := Rules} = Source) ->
|
write_acl_file(#{<<"rules">> := Rules} = Source0) ->
|
||||||
NRules = check_acl_file_rules(Rules),
|
AclPath = ?MODULE:acl_conf_file(),
|
||||||
Path = ?MODULE:acl_conf_file(),
|
%% Always check if the rules are valid before writing to the file
|
||||||
{ok, _Filename} = write_file(Path, NRules),
|
%% If the rules are invalid, the old file will be kept
|
||||||
maps:without([<<"rules">>], Source#{<<"path">> => Path}).
|
ok = check_acl_file_rules(AclPath, Rules),
|
||||||
|
ok = write_file(AclPath, Rules),
|
||||||
|
Source1 = maps:remove(<<"rules">>, Source0),
|
||||||
|
maps:put(<<"path">>, AclPath, Source1).
|
||||||
|
|
||||||
%% @doc where the acl.conf file is stored.
|
%% @doc where the acl.conf file is stored.
|
||||||
acl_conf_file() ->
|
acl_conf_file() ->
|
||||||
|
@ -506,7 +514,7 @@ write_file(Filename, Bytes) ->
|
||||||
ok = filelib:ensure_dir(Filename),
|
ok = filelib:ensure_dir(Filename),
|
||||||
case file:write_file(Filename, Bytes) of
|
case file:write_file(Filename, Bytes) of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, iolist_to_binary(Filename)};
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{filename => Filename, msg => "write_file_error", reason => Reason}),
|
?SLOG(error, #{filename => Filename, msg => "write_file_error", reason => Reason}),
|
||||||
throw(Reason)
|
throw(Reason)
|
||||||
|
@ -528,6 +536,14 @@ get_source_by_type(Type, Sources) ->
|
||||||
update_authz_chain(Actions) ->
|
update_authz_chain(Actions) ->
|
||||||
emqx_hooks:put('client.authorize', {?MODULE, authorize, [Actions]}, ?HP_AUTHZ).
|
emqx_hooks:put('client.authorize', {?MODULE, authorize, [Actions]}, ?HP_AUTHZ).
|
||||||
|
|
||||||
check_acl_file_rules(RawRules) ->
|
check_acl_file_rules(Path, Rules) ->
|
||||||
%% TODO: make sure the bin rules checked
|
TmpPath = Path ++ ".tmp",
|
||||||
RawRules.
|
try
|
||||||
|
ok = write_file(TmpPath, Rules),
|
||||||
|
{ok, _} = emqx_authz_file:validate(TmpPath),
|
||||||
|
ok
|
||||||
|
catch
|
||||||
|
throw:Reason -> throw(Reason)
|
||||||
|
after
|
||||||
|
_ = file:delete(TmpPath)
|
||||||
|
end.
|
||||||
|
|
|
@ -39,8 +39,10 @@ fields(file) ->
|
||||||
type => binary(),
|
type => binary(),
|
||||||
required => true,
|
required => true,
|
||||||
example =>
|
example =>
|
||||||
<<"{allow,{username,\"^dashboard?\"},", "subscribe,[\"$SYS/#\"]}.\n",
|
<<
|
||||||
"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>,
|
"{allow,{username,{re,\"^dashboard$\"}},subscribe,[\"$SYS/#\"]}.\n",
|
||||||
|
"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
|
||||||
|
>>,
|
||||||
desc => ?DESC(rules)
|
desc => ?DESC(rules)
|
||||||
}}
|
}}
|
||||||
];
|
];
|
||||||
|
|
|
@ -33,13 +33,14 @@
|
||||||
update/1,
|
update/1,
|
||||||
destroy/1,
|
destroy/1,
|
||||||
authorize/4,
|
authorize/4,
|
||||||
|
validate/1,
|
||||||
read_file/1
|
read_file/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
description() ->
|
description() ->
|
||||||
"AuthZ with static rules".
|
"AuthZ with static rules".
|
||||||
|
|
||||||
create(#{path := Path0} = Source) ->
|
validate(Path0) ->
|
||||||
Path = filename(Path0),
|
Path = filename(Path0),
|
||||||
Rules =
|
Rules =
|
||||||
case file:consult(Path) of
|
case file:consult(Path) of
|
||||||
|
@ -54,8 +55,12 @@ create(#{path := Path0} = Source) ->
|
||||||
throw(failed_to_read_acl_file);
|
throw(failed_to_read_acl_file);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),
|
?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),
|
||||||
throw(bad_acl_file_content)
|
throw({bad_acl_file_content, Reason})
|
||||||
end,
|
end,
|
||||||
|
{ok, Rules}.
|
||||||
|
|
||||||
|
create(#{path := Path} = Source) ->
|
||||||
|
{ok, Rules} = validate(Path),
|
||||||
Source#{annotations => #{rules => Rules}}.
|
Source#{annotations => #{rules => Rules}}.
|
||||||
|
|
||||||
update(#{path := _Path} = Source) ->
|
update(#{path := _Path} = Source) ->
|
||||||
|
|
|
@ -68,7 +68,13 @@ compile({Permission, Who, Action, TopicFilters}) when
|
||||||
{atom(Permission), compile_who(Who), atom(Action), [
|
{atom(Permission), compile_who(Who), atom(Action), [
|
||||||
compile_topic(Topic)
|
compile_topic(Topic)
|
||||||
|| Topic <- TopicFilters
|
|| Topic <- TopicFilters
|
||||||
]}.
|
]};
|
||||||
|
compile({Permission, _Who, _Action, _TopicFilter}) when not ?ALLOW_DENY(Permission) ->
|
||||||
|
throw({invalid_authorization_permission, Permission});
|
||||||
|
compile({_Permission, _Who, Action, _TopicFilter}) when not ?PUBSUB(Action) ->
|
||||||
|
throw({invalid_authorization_action, Action});
|
||||||
|
compile(BadRule) ->
|
||||||
|
throw({invalid_authorization_rule, BadRule}).
|
||||||
|
|
||||||
compile_who(all) ->
|
compile_who(all) ->
|
||||||
all;
|
all;
|
||||||
|
|
|
@ -78,7 +78,17 @@ fields("authorization") ->
|
||||||
authz_fields();
|
authz_fields();
|
||||||
fields(file) ->
|
fields(file) ->
|
||||||
authz_common_fields(file) ++
|
authz_common_fields(file) ++
|
||||||
[{path, ?HOCON(string(), #{required => true, desc => ?DESC(path)})}];
|
[
|
||||||
|
{path,
|
||||||
|
?HOCON(
|
||||||
|
string(),
|
||||||
|
#{
|
||||||
|
required => true,
|
||||||
|
validator => fun(Path) -> element(1, emqx_authz_file:validate(Path)) end,
|
||||||
|
desc => ?DESC(path)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
];
|
||||||
fields(http_get) ->
|
fields(http_get) ->
|
||||||
authz_common_fields(http) ++
|
authz_common_fields(http) ++
|
||||||
http_common_fields() ++
|
http_common_fields() ++
|
||||||
|
@ -496,7 +506,7 @@ authz_fields() ->
|
||||||
%% doc_lift is force a root level reference instead of nesting sub-structs
|
%% doc_lift is force a root level reference instead of nesting sub-structs
|
||||||
extra => #{doc_lift => true},
|
extra => #{doc_lift => true},
|
||||||
%% it is recommended to configure authz sources from dashboard
|
%% it is recommended to configure authz sources from dashboard
|
||||||
%% hance the importance level for config is low
|
%% hence the importance level for config is low
|
||||||
importance => ?IMPORTANCE_LOW
|
importance => ?IMPORTANCE_LOW
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -155,22 +155,36 @@ set_special_configs(_App) ->
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
|
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
|
||||||
}).
|
}).
|
||||||
-define(SOURCE6, #{
|
|
||||||
|
-define(FILE_SOURCE(Rules), #{
|
||||||
<<"type">> => <<"file">>,
|
<<"type">> => <<"file">>,
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
<<"rules">> =>
|
<<"rules">> => Rules
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(SOURCE6,
|
||||||
|
?FILE_SOURCE(
|
||||||
<<
|
<<
|
||||||
"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
|
"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
|
||||||
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
|
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
|
||||||
>>
|
>>
|
||||||
}).
|
)
|
||||||
-define(SOURCE7, #{
|
).
|
||||||
|
-define(SOURCE7,
|
||||||
|
?FILE_SOURCE(
|
||||||
|
<<
|
||||||
|
"{allow,{username,\"some_client\"},publish,[\"some_client/lwt\"]}.\n"
|
||||||
|
"{deny, all}."
|
||||||
|
>>
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
|
-define(BAD_FILE_SOURCE2, #{
|
||||||
<<"type">> => <<"file">>,
|
<<"type">> => <<"file">>,
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
<<"rules">> =>
|
<<"rules">> =>
|
||||||
<<
|
<<
|
||||||
"{allow,{username,\"some_client\"},publish,[\"some_client/lwt\"]}.\n"
|
"{not_allow,{username,\"some_client\"},publish,[\"some_client/lwt\"]}."
|
||||||
"{deny, all}."
|
|
||||||
>>
|
>>
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
@ -178,6 +192,40 @@ set_special_configs(_App) ->
|
||||||
%% Testcases
|
%% Testcases
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(UPDATE_ERROR(Err), {error, {pre_config_update, emqx_authz, Err}}).
|
||||||
|
|
||||||
|
t_bad_file_source(_) ->
|
||||||
|
BadContent = ?FILE_SOURCE(<<"{allow,{username,\"bar\"}, publish, [\"test\"]}">>),
|
||||||
|
BadContentErr = {bad_acl_file_content, {1, erl_parse, ["syntax error before: ", []]}},
|
||||||
|
BadRule = ?FILE_SOURCE(<<"{allow,{username,\"bar\"},publish}.">>),
|
||||||
|
BadRuleErr = {invalid_authorization_rule, {allow, {username, "bar"}, publish}},
|
||||||
|
BadPermission = ?FILE_SOURCE(<<"{not_allow,{username,\"bar\"},publish,[\"test\"]}.">>),
|
||||||
|
BadPermissionErr = {invalid_authorization_permission, not_allow},
|
||||||
|
BadAction = ?FILE_SOURCE(<<"{allow,{username,\"bar\"},pubsub,[\"test\"]}.">>),
|
||||||
|
BadActionErr = {invalid_authorization_action, pubsub},
|
||||||
|
lists:foreach(
|
||||||
|
fun({Source, Error}) ->
|
||||||
|
File = emqx_authz:acl_conf_file(),
|
||||||
|
{ok, Bin1} = file:read_file(File),
|
||||||
|
?assertEqual(?UPDATE_ERROR(Error), emqx_authz:update(?CMD_REPLACE, [Source])),
|
||||||
|
?assertEqual(?UPDATE_ERROR(Error), emqx_authz:update(?CMD_PREPEND, Source)),
|
||||||
|
?assertEqual(?UPDATE_ERROR(Error), emqx_authz:update(?CMD_APPEND, Source)),
|
||||||
|
%% Check file content not changed if update failed
|
||||||
|
{ok, Bin2} = file:read_file(File),
|
||||||
|
?assertEqual(Bin1, Bin2)
|
||||||
|
end,
|
||||||
|
[
|
||||||
|
{BadContent, BadContentErr},
|
||||||
|
{BadRule, BadRuleErr},
|
||||||
|
{BadPermission, BadPermissionErr},
|
||||||
|
{BadAction, BadActionErr}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
?assertMatch(
|
||||||
|
[],
|
||||||
|
emqx_conf:get([authorization, sources], [])
|
||||||
|
).
|
||||||
|
|
||||||
t_update_source(_) ->
|
t_update_source(_) ->
|
||||||
%% replace all
|
%% replace all
|
||||||
{ok, _} = emqx_authz:update(?CMD_REPLACE, [?SOURCE3]),
|
{ok, _} = emqx_authz:update(?CMD_REPLACE, [?SOURCE3]),
|
||||||
|
|
|
@ -120,7 +120,9 @@ t_superuser(_Config) ->
|
||||||
|
|
||||||
t_invalid_file(_Config) ->
|
t_invalid_file(_Config) ->
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, bad_acl_file_content},
|
{error,
|
||||||
|
{pre_config_update, emqx_authz,
|
||||||
|
{bad_acl_file_content, {1, erl_parse, ["syntax error before: ", "term"]}}}},
|
||||||
emqx_authz:update(?CMD_REPLACE, [?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>}])
|
emqx_authz:update(?CMD_REPLACE, [?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>}])
|
||||||
).
|
).
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
""").
|
""").
|
||||||
|
|
||||||
array_nodes_test() ->
|
array_nodes_test() ->
|
||||||
|
ensure_acl_conf(),
|
||||||
ExpectNodes = ['emqx1@127.0.0.1', 'emqx2@127.0.0.1'],
|
ExpectNodes = ['emqx1@127.0.0.1', 'emqx2@127.0.0.1'],
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Nodes) ->
|
fun(Nodes) ->
|
||||||
|
@ -79,6 +80,7 @@ array_nodes_test() ->
|
||||||
).
|
).
|
||||||
|
|
||||||
authn_validations_test() ->
|
authn_validations_test() ->
|
||||||
|
ensure_acl_conf(),
|
||||||
BaseConf = to_bin(?BASE_CONF, ["emqx1@127.0.0.1", "emqx1@127.0.0.1"]),
|
BaseConf = to_bin(?BASE_CONF, ["emqx1@127.0.0.1", "emqx1@127.0.0.1"]),
|
||||||
|
|
||||||
OKHttps = to_bin(?BASE_AUTHN_ARRAY, [post, true, <<"https://127.0.0.1:8080">>]),
|
OKHttps = to_bin(?BASE_AUTHN_ARRAY, [post, true, <<"https://127.0.0.1:8080">>]),
|
||||||
|
@ -128,6 +130,7 @@ authn_validations_test() ->
|
||||||
).
|
).
|
||||||
|
|
||||||
listeners_test() ->
|
listeners_test() ->
|
||||||
|
ensure_acl_conf(),
|
||||||
BaseConf = to_bin(?BASE_CONF, ["emqx1@127.0.0.1", "emqx1@127.0.0.1"]),
|
BaseConf = to_bin(?BASE_CONF, ["emqx1@127.0.0.1", "emqx1@127.0.0.1"]),
|
||||||
|
|
||||||
Conf = <<BaseConf/binary, ?LISTENERS>>,
|
Conf = <<BaseConf/binary, ?LISTENERS>>,
|
||||||
|
@ -198,6 +201,7 @@ listeners_test() ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
doc_gen_test() ->
|
doc_gen_test() ->
|
||||||
|
ensure_acl_conf(),
|
||||||
%% the json file too large to encode.
|
%% the json file too large to encode.
|
||||||
{
|
{
|
||||||
timeout,
|
timeout,
|
||||||
|
@ -220,3 +224,11 @@ doc_gen_test() ->
|
||||||
|
|
||||||
to_bin(Format, Args) ->
|
to_bin(Format, Args) ->
|
||||||
iolist_to_binary(io_lib:format(Format, Args)).
|
iolist_to_binary(io_lib:format(Format, Args)).
|
||||||
|
|
||||||
|
ensure_acl_conf() ->
|
||||||
|
File = emqx_schema:naive_env_interpolation(<<"${EMQX_ETC_DIR}/acl.conf">>),
|
||||||
|
ok = filelib:ensure_dir(filename:dirname(File)),
|
||||||
|
case filelib:is_regular(File) of
|
||||||
|
true -> ok;
|
||||||
|
false -> file:write_file(File, <<"">>)
|
||||||
|
end.
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Check the correctness of the rules before saving the authorization file source.
|
||||||
|
Previously, Saving wrong rules could lead to restart failure.
|
Loading…
Reference in New Issue