Merge pull request #5732 from tigercl/feat/upload-certs
feat(upload certs): save certs to file
This commit is contained in:
commit
c4403e886d
|
@ -73,6 +73,11 @@
|
|||
, code_change/3
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
-endif.
|
||||
|
||||
-define(CHAINS_TAB, emqx_authn_chains).
|
||||
|
||||
-define(VER_1, <<"1">>).
|
||||
|
@ -193,20 +198,31 @@ pre_config_update(UpdateReq, OldConfig) ->
|
|||
end.
|
||||
|
||||
do_pre_config_update({create_authenticator, _ChainName, Config}, OldConfig) ->
|
||||
{ok, OldConfig ++ [Config]};
|
||||
try convert_certs(Config) of
|
||||
NConfig ->
|
||||
{ok, OldConfig ++ [NConfig]}
|
||||
catch
|
||||
error:{save_cert_to_file, _} = Reason ->
|
||||
{error, Reason}
|
||||
end;
|
||||
do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) ->
|
||||
NewConfig = lists:filter(fun(OldConfig0) ->
|
||||
AuthenticatorID =/= generate_id(OldConfig0)
|
||||
end, OldConfig),
|
||||
{ok, NewConfig};
|
||||
do_pre_config_update({update_authenticator, _ChainName, AuthenticatorID, Config}, OldConfig) ->
|
||||
NewConfig = lists:map(fun(OldConfig0) ->
|
||||
try lists:map(fun(OldConfig0) ->
|
||||
case AuthenticatorID =:= generate_id(OldConfig0) of
|
||||
true -> maps:merge(OldConfig0, Config);
|
||||
true -> convert_certs(Config, OldConfig0);
|
||||
false -> OldConfig0
|
||||
end
|
||||
end, OldConfig),
|
||||
{ok, NewConfig};
|
||||
end, OldConfig) of
|
||||
NewConfig ->
|
||||
{ok, NewConfig}
|
||||
catch
|
||||
error:{save_cert_to_file, _} = Reason ->
|
||||
{error, Reason}
|
||||
end;
|
||||
do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) ->
|
||||
case split_by_id(AuthenticatorID, OldConfig) of
|
||||
{error, Reason} -> {error, Reason};
|
||||
|
@ -600,6 +616,85 @@ reply(Reply, State) ->
|
|||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
convert_certs(#{<<"ssl">> := SSLOpts} = Config) ->
|
||||
NSSLOPts = lists:foldl(fun(K, Acc) ->
|
||||
case maps:get(K, Acc, undefined) of
|
||||
undefined -> Acc;
|
||||
PemBin ->
|
||||
CertFile = generate_filename(K),
|
||||
ok = save_cert_to_file(CertFile, PemBin),
|
||||
Acc#{K => CertFile}
|
||||
end
|
||||
end, SSLOpts, [<<"certfile">>, <<"keyfile">>, <<"cacertfile">>]),
|
||||
Config#{<<"ssl">> => NSSLOPts};
|
||||
convert_certs(Config) ->
|
||||
Config.
|
||||
|
||||
convert_certs(#{<<"ssl">> := NewSSLOpts} = NewConfig, OldConfig) ->
|
||||
OldSSLOpts = maps:get(<<"ssl">>, OldConfig, #{}),
|
||||
Diff = diff_certs(NewSSLOpts, OldSSLOpts),
|
||||
NSSLOpts = lists:foldl(fun({identical, K}, Acc) ->
|
||||
Acc#{K => maps:get(K, OldSSLOpts)};
|
||||
({_, K}, Acc) ->
|
||||
CertFile = generate_filename(K),
|
||||
ok = save_cert_to_file(CertFile, maps:get(K, NewSSLOpts)),
|
||||
Acc#{K => CertFile}
|
||||
end, NewSSLOpts, Diff),
|
||||
NewConfig#{<<"ssl">> => NSSLOpts};
|
||||
convert_certs(NewConfig, _OldConfig) ->
|
||||
NewConfig.
|
||||
|
||||
save_cert_to_file(Filename, PemBin) ->
|
||||
case public_key:pem_decode(PemBin) =/= [] of
|
||||
true ->
|
||||
case filelib:ensure_dir(Filename) of
|
||||
ok ->
|
||||
case file:write_file(Filename, PemBin) of
|
||||
ok -> ok;
|
||||
{error, Reason} -> error({save_cert_to_file, {write_file, Reason}})
|
||||
end;
|
||||
{error, Reason} ->
|
||||
error({save_cert_to_file, {ensure_dir, Reason}})
|
||||
end;
|
||||
false ->
|
||||
error({save_cert_to_file, invalid_certificate})
|
||||
end.
|
||||
|
||||
generate_filename(Key) ->
|
||||
Prefix = case Key of
|
||||
<<"keyfile">> -> "key-";
|
||||
<<"certfile">> -> "cert-";
|
||||
<<"cacertfile">> -> "cacert-"
|
||||
end,
|
||||
to_bin(filename:join([emqx:get_config([node, data_dir]), "certs/authn", Prefix ++ emqx_misc:gen_id() ++ ".pem"])).
|
||||
|
||||
diff_certs(NewSSLOpts, OldSSLOpts) ->
|
||||
Keys = [<<"cacertfile">>, <<"certfile">>, <<"keyfile">>],
|
||||
CertPems = maps:with(Keys, NewSSLOpts),
|
||||
CertFiles = maps:with(Keys, OldSSLOpts),
|
||||
Diff = lists:foldl(fun({K, CertFile}, Acc) ->
|
||||
case maps:find(K, CertPems) of
|
||||
error -> Acc;
|
||||
{ok, PemBin1} ->
|
||||
{ok, PemBin2} = file:read_file(CertFile),
|
||||
case diff_cert(PemBin1, PemBin2) of
|
||||
true ->
|
||||
[{changed, K} | Acc];
|
||||
false ->
|
||||
[{identical, K} | Acc]
|
||||
end
|
||||
end
|
||||
end,
|
||||
[], maps:to_list(CertFiles)),
|
||||
Added = [{added, K} || K <- maps:keys(maps:without(maps:keys(CertFiles), CertPems))],
|
||||
Diff ++ Added.
|
||||
|
||||
diff_cert(Pem1, Pem2) ->
|
||||
cal_md5_for_cert(Pem1) =/= cal_md5_for_cert(Pem2).
|
||||
|
||||
cal_md5_for_cert(Pem) ->
|
||||
crypto:hash(md5, term_to_binary(public_key:pem_decode(Pem))).
|
||||
|
||||
split_by_id(ID, AuthenticatorsConfig) ->
|
||||
case lists:foldl(
|
||||
fun(C, {P1, P2, F0}) ->
|
||||
|
@ -777,3 +872,6 @@ to_list(M) when is_map(M) ->
|
|||
[M];
|
||||
to_list(L) when is_list(L) ->
|
||||
L.
|
||||
|
||||
to_bin(B) when is_binary(B) -> B;
|
||||
to_bin(L) when is_list(L) -> list_to_binary(L).
|
||||
|
|
|
@ -45,6 +45,8 @@
|
|||
, index_of/2
|
||||
, maybe_parse_ip/1
|
||||
, ipv6_probe/1
|
||||
, gen_id/0
|
||||
, gen_id/1
|
||||
]).
|
||||
|
||||
-export([ bin2hexstr_A_F/1
|
||||
|
@ -52,6 +54,8 @@
|
|||
, hexstr2bin/1
|
||||
]).
|
||||
|
||||
-define(SHORT, 8).
|
||||
|
||||
%% @doc Parse v4 or v6 string format address to tuple.
|
||||
%% `Host' itself is returned if it's not an ip string.
|
||||
maybe_parse_ip(Host) ->
|
||||
|
@ -298,6 +302,39 @@ hexchar2int(I) when I >= $0 andalso I =< $9 -> I - $0;
|
|||
hexchar2int(I) when I >= $A andalso I =< $F -> I - $A + 10;
|
||||
hexchar2int(I) when I >= $a andalso I =< $f -> I - $a + 10.
|
||||
|
||||
-spec(gen_id() -> list()).
|
||||
gen_id() ->
|
||||
gen_id(?SHORT).
|
||||
|
||||
-spec(gen_id(integer()) -> list()).
|
||||
gen_id(Len) ->
|
||||
BitLen = Len * 4,
|
||||
<<R:BitLen>> = crypto:strong_rand_bytes(Len div 2),
|
||||
int_to_hex(R, Len).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal Functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
int_to_hex(I, N) when is_integer(I), I >= 0 ->
|
||||
int_to_hex([], I, 1, N).
|
||||
|
||||
int_to_hex(L, I, Count, N)
|
||||
when I < 16 ->
|
||||
pad([int_to_hex(I) | L], N - Count);
|
||||
int_to_hex(L, I, Count, N) ->
|
||||
int_to_hex([int_to_hex(I rem 16) | L], I div 16, Count + 1, N).
|
||||
|
||||
int_to_hex(I) when 0 =< I, I =< 9 ->
|
||||
I + $0;
|
||||
int_to_hex(I) when 10 =< I, I =< 15 ->
|
||||
(I - 10) + $a.
|
||||
|
||||
pad(L, 0) ->
|
||||
L;
|
||||
pad(L, Count) ->
|
||||
pad([$0 | L], Count - 1).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
|
|
|
@ -92,6 +92,19 @@ end_per_suite(_) ->
|
|||
emqx_ct_helpers:stop_apps([]),
|
||||
ok.
|
||||
|
||||
init_per_testcase(_, Config) ->
|
||||
meck:new(emqx, [non_strict, passthrough, no_history, no_link]),
|
||||
meck:expect(emqx, get_config, fun([node, data_dir]) ->
|
||||
{data_dir, Data} = lists:keyfind(data_dir, 1, Config),
|
||||
Data;
|
||||
(C) -> meck:passthrough([C])
|
||||
end),
|
||||
Config.
|
||||
|
||||
end_per_testcase(_, _Config) ->
|
||||
meck:unload(emqx),
|
||||
ok.
|
||||
|
||||
t_chain(_) ->
|
||||
% CRUD of authentication chain
|
||||
ChainName = 'test',
|
||||
|
@ -203,7 +216,7 @@ t_update_config(_) ->
|
|||
?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig2})),
|
||||
?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID2)),
|
||||
|
||||
?assertMatch({ok, _}, update_config([authentication], {update_authenticator, Global, ID1, #{}})),
|
||||
?assertMatch({ok, _}, update_config([authentication], {update_authenticator, Global, ID1, AuthenticatorConfig1#{enable => false}})),
|
||||
?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(Global, ID1)),
|
||||
|
||||
?assertMatch({ok, _}, update_config([authentication], {move_authenticator, Global, ID2, top})),
|
||||
|
@ -220,7 +233,7 @@ t_update_config(_) ->
|
|||
?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig2})),
|
||||
?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID2)),
|
||||
|
||||
?assertMatch({ok, _}, update_config(ConfKeyPath, {update_authenticator, ListenerID, ID1, #{}})),
|
||||
?assertMatch({ok, _}, update_config(ConfKeyPath, {update_authenticator, ListenerID, ID1, AuthenticatorConfig1#{enable => false}})),
|
||||
?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)),
|
||||
|
||||
?assertMatch({ok, _}, update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, top})),
|
||||
|
@ -234,5 +247,41 @@ t_update_config(_) ->
|
|||
?AUTHN:remove_provider(AuthNType2),
|
||||
ok.
|
||||
|
||||
t_convert_cert_options(_) ->
|
||||
Certs = certs([ {<<"keyfile">>, "key.pem"}
|
||||
, {<<"certfile">>, "cert.pem"}
|
||||
, {<<"cacertfile">>, "cacert.pem"}
|
||||
]),
|
||||
#{<<"ssl">> := NCerts} = ?AUTHN:convert_certs(#{<<"ssl">> => Certs}),
|
||||
?assertEqual(false, diff_cert(maps:get(<<"keyfile">>, NCerts), maps:get(<<"keyfile">>, Certs))),
|
||||
|
||||
Certs2 = certs([ {<<"keyfile">>, "key.pem"}
|
||||
, {<<"certfile">>, "cert.pem"}
|
||||
]),
|
||||
#{<<"ssl">> := NCerts2} = ?AUTHN:convert_certs(#{<<"ssl">> => Certs2}, #{<<"ssl">> => NCerts}),
|
||||
?assertEqual(false, diff_cert(maps:get(<<"keyfile">>, NCerts2), maps:get(<<"keyfile">>, Certs2))),
|
||||
?assertEqual(maps:get(<<"keyfile">>, NCerts), maps:get(<<"keyfile">>, NCerts2)),
|
||||
?assertEqual(maps:get(<<"certfile">>, NCerts), maps:get(<<"certfile">>, NCerts2)),
|
||||
|
||||
Certs3 = certs([ {<<"keyfile">>, "client-key.pem"}
|
||||
, {<<"certfile">>, "client-cert.pem"}
|
||||
, {<<"cacertfile">>, "cacert.pem"}
|
||||
]),
|
||||
#{<<"ssl">> := NCerts3} = ?AUTHN:convert_certs(#{<<"ssl">> => Certs3}, #{<<"ssl">> => NCerts2}),
|
||||
?assertEqual(false, diff_cert(maps:get(<<"keyfile">>, NCerts3), maps:get(<<"keyfile">>, Certs3))),
|
||||
?assertNotEqual(maps:get(<<"keyfile">>, NCerts2), maps:get(<<"keyfile">>, NCerts3)),
|
||||
?assertNotEqual(maps:get(<<"certfile">>, NCerts2), maps:get(<<"certfile">>, NCerts3)).
|
||||
|
||||
update_config(Path, ConfigRequest) ->
|
||||
emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).
|
||||
|
||||
certs(Certs) ->
|
||||
CertsPath = emqx_ct_helpers:deps_path(emqx, "etc/certs"),
|
||||
lists:foldl(fun({Key, Filename}, Acc) ->
|
||||
{ok, Bin} = file:read_file(filename:join([CertsPath, Filename])),
|
||||
Acc#{Key => Bin}
|
||||
end, #{}, Certs).
|
||||
|
||||
diff_cert(CertFile, CertPem2) ->
|
||||
{ok, CertPem1} = file:read_file(CertFile),
|
||||
?AUTHN:diff_cert(CertPem1, CertPem2).
|
|
@ -1841,14 +1841,14 @@ create_authenticator(ConfKeyPath, ChainName0, Config) ->
|
|||
{ok, #{post_config_update := #{?AUTHN := #{id := ID}},
|
||||
raw_config := AuthenticatorsConfig}} ->
|
||||
{ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
|
||||
{200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))};
|
||||
{200, maps:put(id, ID, convert_certs(fill_defaults(AuthenticatorConfig)))};
|
||||
{error, {_, _, Reason}} ->
|
||||
serialize_error(Reason)
|
||||
end.
|
||||
|
||||
list_authenticators(ConfKeyPath) ->
|
||||
AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath),
|
||||
NAuthenticators = [maps:put(id, ?AUTHN:generate_id(AuthenticatorConfig), AuthenticatorConfig)
|
||||
NAuthenticators = [maps:put(id, ?AUTHN:generate_id(AuthenticatorConfig), convert_certs(AuthenticatorConfig))
|
||||
|| AuthenticatorConfig <- AuthenticatorsConfig],
|
||||
{200, NAuthenticators}.
|
||||
|
||||
|
@ -1856,7 +1856,7 @@ list_authenticator(ConfKeyPath, AuthenticatorID) ->
|
|||
AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath),
|
||||
case find_config(AuthenticatorID, AuthenticatorsConfig) of
|
||||
{ok, AuthenticatorConfig} ->
|
||||
{200, AuthenticatorConfig#{id => AuthenticatorID}};
|
||||
{200, maps:put(id, AuthenticatorID, convert_certs(AuthenticatorConfig))};
|
||||
{error, Reason} ->
|
||||
serialize_error(Reason)
|
||||
end.
|
||||
|
@ -1867,7 +1867,7 @@ update_authenticator(ConfKeyPath, ChainName0, AuthenticatorID, Config) ->
|
|||
{ok, #{post_config_update := #{?AUTHN := #{id := ID}},
|
||||
raw_config := AuthenticatorsConfig}} ->
|
||||
{ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
|
||||
{200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))};
|
||||
{200, maps:put(id, ID, convert_certs(fill_defaults(AuthenticatorConfig)))};
|
||||
{error, {_, _, Reason}} ->
|
||||
serialize_error(Reason)
|
||||
end.
|
||||
|
@ -1971,6 +1971,19 @@ fill_defaults(Config) ->
|
|||
?AUTHN, #{<<"authentication">> => Config}, #{nullable => true, no_conversion => true}),
|
||||
CheckedConfig.
|
||||
|
||||
convert_certs(#{<<"ssl">> := SSLOpts} = Config) ->
|
||||
NSSLOpts = lists:foldl(fun(K, Acc) ->
|
||||
case maps:get(K, Acc, undefined) of
|
||||
undefined -> Acc;
|
||||
Filename ->
|
||||
{ok, Bin} = file:read_file(Filename),
|
||||
Acc#{K => Bin}
|
||||
end
|
||||
end, SSLOpts, [<<"certfile">>, <<"keyfile">>, <<"cacertfile">>]),
|
||||
Config#{<<"ssl">> => NSSLOpts};
|
||||
convert_certs(Config) ->
|
||||
Config.
|
||||
|
||||
serialize_error({not_found, {authenticator, ID}}) ->
|
||||
{404, #{code => <<"NOT_FOUND">>,
|
||||
message => list_to_binary(
|
||||
|
@ -2011,6 +2024,16 @@ serialize_error(unsupported_operation) ->
|
|||
{400, #{code => <<"BAD_REQUEST">>,
|
||||
message => <<"Operation not supported in this authentication type">>}};
|
||||
|
||||
serialize_error({save_cert_to_file, invalid_certificate}) ->
|
||||
{400, #{code => <<"BAD_REQUEST">>,
|
||||
message => <<"Invalid certificate">>}};
|
||||
|
||||
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">>,
|
||||
message => list_to_binary(
|
||||
|
|
|
@ -107,15 +107,15 @@ auto_reconnect(default) -> true;
|
|||
auto_reconnect(_) -> undefined.
|
||||
|
||||
cacertfile(type) -> string();
|
||||
cacertfile(default) -> "";
|
||||
cacertfile(nullable) -> true;
|
||||
cacertfile(_) -> undefined.
|
||||
|
||||
keyfile(type) -> string();
|
||||
keyfile(default) -> "";
|
||||
keyfile(nullable) -> true;
|
||||
keyfile(_) -> undefined.
|
||||
|
||||
certfile(type) -> string();
|
||||
certfile(default) -> "";
|
||||
certfile(nullable) -> true;
|
||||
certfile(_) -> undefined.
|
||||
|
||||
verify(type) -> boolean();
|
||||
|
|
Loading…
Reference in New Issue