fix(authn): save certificates to certs dir

This commit is contained in:
Zaiming Shi 2021-10-24 15:54:39 +02:00
parent 71d2e6bebd
commit a7413bc11e
7 changed files with 72 additions and 90 deletions

View File

@ -66,6 +66,7 @@
, remove_config/2 , remove_config/2
, reset_config/2 , reset_config/2
, data_dir/0 , data_dir/0
, certs_dir/0
]). ]).
-define(APP, ?MODULE). -define(APP, ?MODULE).
@ -250,3 +251,6 @@ reset_config([RootName | _] = KeyPath, Opts) ->
data_dir() -> data_dir() ->
application:get_env(emqx, data_dir, "data"). application:get_env(emqx, data_dir, "data").
certs_dir() ->
filename:join([data_dir(), certs]).

View File

@ -56,29 +56,24 @@
-spec pre_config_update(update_request(), emqx_config:raw_config()) -spec pre_config_update(update_request(), emqx_config:raw_config())
-> {ok, map() | list()} | {error, term()}. -> {ok, map() | list()} | {error, term()}.
pre_config_update(UpdateReq, OldConfig) -> pre_config_update(UpdateReq, OldConfig) ->
case do_pre_config_update(UpdateReq, to_list(OldConfig)) of try do_pre_config_update(UpdateReq, to_list(OldConfig)) of
{error, Reason} -> {error, Reason}; {error, Reason} -> {error, Reason};
{ok, NewConfig} -> {ok, return_map(NewConfig)} {ok, NewConfig} -> {ok, return_map(NewConfig)}
catch
throw : Reason ->
{error, Reason}
end. end.
do_pre_config_update({create_authenticator, ChainName, Config}, OldConfig) -> do_pre_config_update({create_authenticator, ChainName, Config}, OldConfig) ->
try
CertsDir = certs_dir(ChainName, Config), CertsDir = certs_dir(ChainName, Config),
NConfig = convert_certs(CertsDir, Config), NConfig = convert_certs(CertsDir, Config),
{ok, OldConfig ++ [NConfig]} {ok, OldConfig ++ [NConfig]};
catch
error:{save_cert_to_file, _} = Reason ->
{error, Reason};
error:{missing_parameter, _} = Reason ->
{error, Reason}
end;
do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) -> do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) ->
NewConfig = lists:filter(fun(OldConfig0) -> NewConfig = lists:filter(fun(OldConfig0) ->
AuthenticatorID =/= authenticator_id(OldConfig0) AuthenticatorID =/= authenticator_id(OldConfig0)
end, OldConfig), end, OldConfig),
{ok, NewConfig}; {ok, NewConfig};
do_pre_config_update({update_authenticator, ChainName, AuthenticatorID, Config}, OldConfig) -> do_pre_config_update({update_authenticator, ChainName, AuthenticatorID, Config}, OldConfig) ->
try
CertsDir = certs_dir(ChainName, AuthenticatorID), CertsDir = certs_dir(ChainName, AuthenticatorID),
NewConfig = lists:map( NewConfig = lists:map(
fun(OldConfig0) -> fun(OldConfig0) ->
@ -87,13 +82,7 @@ do_pre_config_update({update_authenticator, ChainName, AuthenticatorID, Config},
false -> OldConfig0 false -> OldConfig0
end end
end, OldConfig), end, OldConfig),
{ok, NewConfig} {ok, NewConfig};
catch
error:{save_cert_to_file, _} = Reason ->
{error, Reason};
error:{missing_parameter, _} = Reason ->
{error, Reason}
end;
do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) -> do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) ->
case split_by_id(AuthenticatorID, OldConfig) of case split_by_id(AuthenticatorID, OldConfig) of
{error, Reason} -> {error, Reason}; {error, Reason} -> {error, Reason};
@ -152,7 +141,7 @@ do_check_conifg(Config, Providers) ->
?SLOG(warning, #{msg => "unknown_authn_type", ?SLOG(warning, #{msg => "unknown_authn_type",
type => Type, type => Type,
providers => Providers}), providers => Providers}),
throw(unknown_authn_type); throw({unknown_authn_type, Type});
Module -> Module ->
do_check_conifg(Type, Config, Module) do_check_conifg(Type, Config, Module)
end. end.
@ -180,7 +169,7 @@ do_check_conifg(Type, Config, Module) ->
reason => E, reason => E,
stacktrace => S stacktrace => S
}), }),
throw(bad_authenticator_config) throw({bad_authenticator_config, #{type => Type, reason => E}})
end. end.
return_map([L]) -> L; return_map([L]) -> L;
@ -197,7 +186,7 @@ convert_certs(CertsDir, Config) ->
new_ssl_config(Config, SSL); new_ssl_config(Config, SSL);
{error, Reason} -> {error, Reason} ->
?SLOG(error, Reason#{msg => bad_ssl_config}), ?SLOG(error, Reason#{msg => bad_ssl_config}),
throw(bad_ssl_config) throw({bad_ssl_config, Reason})
end. end.
convert_certs(CertsDir, NewConfig, OldConfig) -> convert_certs(CertsDir, NewConfig, OldConfig) ->
@ -209,7 +198,7 @@ convert_certs(CertsDir, NewConfig, OldConfig) ->
new_ssl_config(NewConfig, NewSSL1); new_ssl_config(NewConfig, NewSSL1);
{error, Reason} -> {error, Reason} ->
?SLOG(error, Reason#{msg => bad_ssl_config}), ?SLOG(error, Reason#{msg => bad_ssl_config}),
throw(bad_ssl_config) throw({bad_ssl_config, Reason})
end. end.
new_ssl_config(Config, undefined) -> Config; new_ssl_config(Config, undefined) -> Config;
@ -257,8 +246,8 @@ authenticator_id(#{<<"mechanism">> := Mechanism, <<"backend">> := Backend}) ->
<<Mechanism/binary, ":", Backend/binary>>; <<Mechanism/binary, ":", Backend/binary>>;
authenticator_id(#{<<"mechanism">> := Mechanism}) -> authenticator_id(#{<<"mechanism">> := Mechanism}) ->
Mechanism; Mechanism;
authenticator_id(C) -> authenticator_id(_C) ->
error({missing_parameter, mechanism, C}). throw({missing_parameter, #{name => mechanism}}).
%% @doc Make the authentication type. %% @doc Make the authentication type.
authn_type(#{mechanism := M, backend := B}) -> {atom(M), atom(B)}; authn_type(#{mechanism := M, backend := B}) -> {atom(M), atom(B)};
@ -270,7 +259,13 @@ atom(Bin) ->
binary_to_existing_atom(Bin, utf8). binary_to_existing_atom(Bin, utf8).
%% The relative dir for ssl files. %% The relative dir for ssl files.
certs_dir(ChainName, ID) when is_binary(ID) -> certs_dir(ChainName, ConfigOrID) ->
filename:join([to_bin(ChainName), ID]); DirName = dir(ChainName, ConfigOrID),
certs_dir(ChainName, Config) when is_map(Config) -> SubDir = iolist_to_binary(filename:join(["authn", DirName])),
certs_dir(ChainName, authenticator_id(Config)). binary:replace(SubDir, <<":">>, <<"-">>, [global]).
dir(ChainName, ID) when is_binary(ID) ->
binary:replace(iolist_to_binary([to_bin(ChainName), "-", ID]), <<":">>, <<"-">>);
dir(ChainName, Config) when is_map(Config) ->
dir(ChainName, authenticator_id(Config)).

View File

@ -292,8 +292,8 @@ do_ensure_ssl_file(Dir, Key, Opts, MaybePem, DryRun) ->
true -> {ok, Opts}; true -> {ok, Opts};
{error, enoent} when DryRun -> {ok, Opts}; {error, enoent} when DryRun -> {ok, Opts};
{error, Reason} -> {error, Reason} ->
{error, #{file_path => MaybePem, {error, #{pem_check => invalid_pem,
reason => Reason file_read => Reason
}} }}
end end
end. end.
@ -333,12 +333,16 @@ save_pem_file(Dir, Key, Pem, DryRun) ->
%% compute the filename for a PEM format key/certificate %% compute the filename for a PEM format key/certificate
%% the filename is prefixed by the option name without the 'file' part %% the filename is prefixed by the option name without the 'file' part
%% and suffixed with the first 8 byets of base64 encode result of the PEM content's %% and suffixed with the first 8 byets the PEM content's md5 checksum.
%% md5 checksum. e.g. key-EKjjO9um, cert-TwuCW1vh, and cacert-6ZaWqNuC %% e.g. key-1234567890abcdef, cert-1234567890abcdef, and cacert-1234567890abcdef
pem_file_name(Dir, Key, Pem) -> pem_file_name(Dir, Key, Pem) ->
<<CK:8/binary, _/binary>> = base64:encode(crypto:hash(md5, Pem)), <<CK:8/binary, _/binary>> = crypto:hash(md5, Pem),
FileName = binary:replace(Key, <<"file">>, <<"-", CK/binary>>), Suffix = hex_str(CK),
filename:join([emqx:data_dir(), Dir, FileName]). FileName = binary:replace(Key, <<"file">>, <<"-", Suffix/binary>>),
filename:join([emqx:certs_dir(), Dir, FileName]).
hex_str(Bin) ->
iolist_to_binary([io_lib:format("~2.16.0b",[X]) || <<X:8>> <= Bin ]).
is_valid_pem_file(Path) -> is_valid_pem_file(Path) ->
case file:read_file(Path) of case file:read_file(Path) of

View File

@ -79,7 +79,7 @@ ssl_files_failure_test_() ->
{"enoent_key_file", {"enoent_key_file",
fun() -> fun() ->
NonExistingFile = filename:join("/tmp", integer_to_list(erlang:system_time(microsecond))), NonExistingFile = filename:join("/tmp", integer_to_list(erlang:system_time(microsecond))),
?assertMatch({error, #{reason := enoent}}, ?assertMatch({error, #{file_read := enoent, pem_check := invalid_pem}},
emqx_tls_lib:ensure_ssl_files("/tmp", #{<<"keyfile">> => NonExistingFile})) emqx_tls_lib:ensure_ssl_files("/tmp", #{<<"keyfile">> => NonExistingFile}))
end}, end},
{"bad_pem_string", {"bad_pem_string",
@ -93,7 +93,7 @@ ssl_files_failure_test_() ->
TmpFile = filename:join("/tmp", integer_to_list(erlang:system_time(microsecond))), TmpFile = filename:join("/tmp", integer_to_list(erlang:system_time(microsecond))),
try try
ok = file:write_file(TmpFile, <<"not a valid pem">>), ok = file:write_file(TmpFile, <<"not a valid pem">>),
?assertMatch({error, #{file_path := _, reason := not_pem}}, ?assertMatch({error, #{file_read := not_pem}},
emqx_tls_lib:ensure_ssl_files("/tmp", #{<<"cacertfile">> => bin(TmpFile)})) emqx_tls_lib:ensure_ssl_files("/tmp", #{<<"cacertfile">> => bin(TmpFile)}))
after after
file:delete(TmpFile) file:delete(TmpFile)
@ -106,7 +106,7 @@ ssl_files_save_delete_test() ->
Dir = filename:join(["/tmp", "ssl-test-dir"]), Dir = filename:join(["/tmp", "ssl-test-dir"]),
{ok, SSL} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0), {ok, SSL} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0),
File = maps:get(<<"keyfile">>, SSL), File = maps:get(<<"keyfile">>, SSL),
?assertMatch(<<"/tmp/ssl-test-dir/key-", _:8/binary>>, File), ?assertMatch(<<"/tmp/ssl-test-dir/key-", _:16/binary>>, File),
?assertEqual({ok, bin(test_key())}, file:read_file(File)), ?assertEqual({ok, bin(test_key())}, file:read_file(File)),
%% no old file to delete %% no old file to delete
ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, undefined), ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, undefined),

View File

@ -1989,69 +1989,46 @@ convert_certs(Config) ->
serialize_error({not_found, {authenticator, ID}}) -> serialize_error({not_found, {authenticator, ID}}) ->
{404, #{code => <<"NOT_FOUND">>, {404, #{code => <<"NOT_FOUND">>,
message => list_to_binary( message => binfmt("Authenticator '~ts' does not exist", [ID]) }};
io_lib:format("Authenticator '~ts' does not exist", [ID])
)}};
serialize_error({not_found, {listener, ID}}) -> serialize_error({not_found, {listener, ID}}) ->
{404, #{code => <<"NOT_FOUND">>, {404, #{code => <<"NOT_FOUND">>,
message => list_to_binary( message => binfmt("Listener '~ts' does not exist", [ID])}};
io_lib:format("Listener '~ts' does not exist", [ID])
)}};
serialize_error({not_found, {chain, ?GLOBAL}}) -> serialize_error({not_found, {chain, ?GLOBAL}}) ->
{500, #{code => <<"INTERNAL_SERVER_ERROR">>, {404, #{code => <<"NOT_FOUND">>,
message => <<"Authentication status is abnormal">>}}; message => <<"Authenticator not found in the 'global' scope">>}};
serialize_error({not_found, {chain, Name}}) -> serialize_error({not_found, {chain, Name}}) ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{code => <<"BAD_REQUEST">>,
message => list_to_binary( message => binfmt("No authentication has been create for listener '~ts'", [Name])}};
io_lib:format("No authentication has been create for listener '~ts'", [Name])
)}};
serialize_error({already_exists, {authenticator, ID}}) -> serialize_error({already_exists, {authenticator, ID}}) ->
{409, #{code => <<"ALREADY_EXISTS">>, {409, #{code => <<"ALREADY_EXISTS">>,
message => list_to_binary( message => binfmt("Authenticator '~ts' already exist", [ID])}};
io_lib:format("Authenticator '~ts' already exist", [ID])
)}};
serialize_error(no_available_provider) -> serialize_error(no_available_provider) ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{code => <<"BAD_REQUEST">>,
message => <<"Unsupported authentication type">>}}; message => <<"Unsupported authentication type">>}};
serialize_error(change_of_authentication_type_is_not_allowed) -> serialize_error(change_of_authentication_type_is_not_allowed) ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{code => <<"BAD_REQUEST">>,
message => <<"Change of authentication type is not allowed">>}}; message => <<"Change of authentication type is not allowed">>}};
serialize_error(unsupported_operation) -> serialize_error(unsupported_operation) ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{code => <<"BAD_REQUEST">>,
message => <<"Operation not supported in this authentication type">>}}; message => <<"Operation not supported in this authentication type">>}};
serialize_error({bad_ssl_config, Details}) ->
serialize_error({save_cert_to_file, invalid_certificate}) ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{code => <<"BAD_REQUEST">>,
message => <<"Invalid certificate">>}}; message => binfmt("bad_ssl_config ~p", [Details])}};
serialize_error({missing_parameter, Detail}) ->
serialize_error({save_cert_to_file, {_, Reason}}) ->
{500, #{code => <<"INTERNAL_SERVER_ERROR">>,
message => list_to_binary(
io_lib:format("Cannot save certificate to file due to '~p'", [Reason])
)}};
serialize_error({missing_parameter, Name}) ->
{400, #{code => <<"MISSING_PARAMETER">>, {400, #{code => <<"MISSING_PARAMETER">>,
message => list_to_binary( message => binfmt("Missing required parameter", [Detail])}};
io_lib:format("The input parameter '~p' that is mandatory for processing this request is not supplied", [Name])
)}};
serialize_error({invalid_parameter, Name}) -> serialize_error({invalid_parameter, Name}) ->
{400, #{code => <<"INVALID_PARAMETER">>, {400, #{code => <<"INVALID_PARAMETER">>,
message => list_to_binary( message => binfmt("Invalid value for '~p'", [Name])}};
io_lib:format("The value of input parameter '~p' is invalid", [Name]) serialize_error({unknown_authn_type, Type}) ->
)}}; {400, #{code => <<"BAD_REQUEST">>,
message => binfmt("Unknown type '~ts'", [Type])}};
serialize_error({bad_authenticator_config, Reason}) ->
{400, #{code => <<"BAD_REQUEST">>,
message => binfmt("Bad authenticator config ~p", [Reason])}};
serialize_error(Reason) -> serialize_error(Reason) ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{code => <<"BAD_REQUEST">>,
message => list_to_binary(io_lib:format("~p", [Reason]))}}. message => binfmt("~p", [Reason])}}.
parse_position(<<"top">>) -> parse_position(<<"top">>) ->
{ok, top}; {ok, top};
@ -2069,3 +2046,5 @@ to_atom(B) when is_binary(B) ->
binary_to_atom(B); binary_to_atom(B);
to_atom(A) when is_atom(A) -> to_atom(A) when is_atom(A) ->
A. A.
binfmt(Fmt, Args) -> iolist_to_binary(io_lib:format(Fmt, Args)).

View File

@ -438,7 +438,7 @@ read_certs(Source) -> Source.
maybe_write_certs(#{<<"ssl">> := #{<<"enable">> := true} = SSL} = Source) -> maybe_write_certs(#{<<"ssl">> := #{<<"enable">> := true} = SSL} = Source) ->
Type = maps:get(<<"type">>, Source), Type = maps:get(<<"type">>, Source),
emqx_tls_lib:ensure_ssl_files(filename:join(["authz", "certs", Type]), SSL); emqx_tls_lib:ensure_ssl_files(filename:join(["authz", Type]), SSL);
maybe_write_certs(Source) -> Source. maybe_write_certs(Source) -> Source.
write_file(Filename, Bytes0) -> write_file(Filename, Bytes0) ->

View File

@ -76,7 +76,7 @@ Path to the file which contains the ACL rules.<br>
If the file provisioned before starting EMQ X node, it can be placed anywhere If the file provisioned before starting EMQ X node, it can be placed anywhere
as long as EMQ X has read access to it. as long as EMQ X has read access to it.
In case rule set is created from EMQ X dashboard or management HTTP API, In case rule set is created from EMQ X dashboard or management HTTP API,
the file will be placed in `authz` sub directory inside EMQ X's `data_dir`, the file will be placed in `certs/authz` sub directory inside EMQ X's `data_dir`,
and the new rules will override all rules from the old config file. and the new rules will override all rules from the old config file.
""" """
}} }}