feat(emqx): add file-sourced generic secrets

These secrets follow the same `emqx_secret` convention of 0-arity
functions. Also provide a simple HOCON schema module for use in
application schemas.
This commit is contained in:
Andrew Mayorov 2023-10-24 14:45:04 +07:00
parent 9dfffd90d0
commit 1c2f9321d1
No known key found for this signature in database
GPG Key ID: 2837C62ACFBFED5D
4 changed files with 205 additions and 1 deletions

View File

@ -0,0 +1,107 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 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.
%%--------------------------------------------------------------------
%% @doc HOCON schema that defines _secret_ concept.
-module(emqx_schema_secret).
-include_lib("emqx/include/logger.hrl").
-include_lib("typerefl/include/types.hrl").
-export([mk/1]).
%% HOCON Schema API
-export([convert_secret/2]).
%% Target of `emqx_secret:wrap/3`
-export([load/1]).
%% @doc Secret value.
-type t() :: binary().
%% @doc Source of the secret value.
%% * "file://...": file path to a file containing secret value.
%% * other binaries: secret value itself.
-type source() :: iodata().
-type secret() :: binary() | function().
-reflect_type([secret/0]).
-define(SCHEMA, #{
required => false,
format => <<"password">>,
sensitive => true,
converter => fun ?MODULE:convert_secret/2
}).
-dialyzer({nowarn_function, source/1}).
%%
-spec mk(#{atom() => _}) -> hocon_schema:field_schema().
mk(Overrides = #{}) ->
hoconsc:mk(secret(), maps:merge(?SCHEMA, Overrides)).
convert_secret(undefined, #{}) ->
undefined;
convert_secret(Secret, #{make_serializable := true}) ->
unicode:characters_to_binary(source(Secret));
convert_secret(Secret, #{}) when is_function(Secret, 0) ->
Secret;
convert_secret(Secret, #{}) when is_integer(Secret) ->
wrap(integer_to_binary(Secret));
convert_secret(Secret, #{}) ->
try unicode:characters_to_binary(Secret) of
String when is_binary(String) ->
wrap(String);
{error, _, _} ->
throw(invalid_string)
catch
error:_ ->
throw(invalid_type)
end.
-spec wrap(source()) -> emqx_secret:t(t()).
wrap(Source) ->
try
_Secret = load(Source),
emqx_secret:wrap(?MODULE, load, Source)
catch
error:Reason ->
% NOTE: This should be a term serializable as JSON value.
throw(emqx_utils:format(Reason))
end.
-spec source(emqx_secret:t(t())) -> source().
source(Secret) when is_function(Secret) ->
emqx_secret:term(Secret);
source(Secret) ->
Secret.
%%
-spec load(source()) -> t().
load(<<"file://", Filename/binary>>) ->
load_file(Filename);
load(Secret) ->
Secret.
load_file(Filename) ->
case file:read_file(Filename) of
{ok, Secret} ->
string:trim(Secret, trailing, [$\n]);
{error, Reason} ->
error({inaccessible_secret_file, Reason}, [Filename])
end.

View File

@ -19,7 +19,7 @@
-module(emqx_secret). -module(emqx_secret).
%% API: %% API:
-export([wrap/1, unwrap/1]). -export([wrap/1, wrap/3, unwrap/1, term/1]).
-export_type([t/1]). -export_type([t/1]).
@ -29,13 +29,38 @@
%% API funcions %% API funcions
%%================================================================================ %%================================================================================
%% @doc Wrap a term in a secret closure.
%% This effectively hides the term from any term formatting / printing code.
-spec wrap(T) -> t(T).
wrap(Term) -> wrap(Term) ->
fun() -> fun() ->
Term Term
end. end.
%% @doc Wrap a function call over a term in a secret closure.
%% This is slightly more flexible form of `wrap/1` with the same basic purpose.
-spec wrap(module(), atom(), _Term) -> t(_).
wrap(Module, Function, Term) ->
fun() ->
apply(Module, Function, [Term])
end.
%% @doc Unwrap a secret closure, revealing the secret.
%% This is either `Term` or `Module:Function(Term)` depending on how it was wrapped.
-spec unwrap(t(T)) -> T.
unwrap(Term) when is_function(Term, 0) -> unwrap(Term) when is_function(Term, 0) ->
%% Handle potentially nested funs %% Handle potentially nested funs
unwrap(Term()); unwrap(Term());
unwrap(Term) -> unwrap(Term) ->
Term. Term.
%% @doc Inspect the term wrapped in a secret closure.
-spec term(t(_)) -> _Term.
term(Wrap) when is_function(Wrap, 0) ->
case erlang:fun_info(Wrap, module) of
{module, ?MODULE} ->
{env, Env} = erlang:fun_info(Wrap, env),
lists:last(Env);
_ ->
error(badarg, [Wrap])
end.

View File

@ -0,0 +1,69 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 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_secret_tests).
-export([ident/1]).
-include_lib("eunit/include/eunit.hrl").
wrap_unwrap_test() ->
?assertEqual(
42,
emqx_secret:unwrap(emqx_secret:wrap(42))
).
unwrap_immediate_test() ->
?assertEqual(
42,
emqx_secret:unwrap(42)
).
wrap_unwrap_external_test() ->
?assertEqual(
ident({foo, bar}),
emqx_secret:unwrap(emqx_secret:wrap(?MODULE, ident, {foo, bar}))
).
wrap_unwrap_transform_test() ->
?assertEqual(
<<"this_was_an_atom">>,
emqx_secret:unwrap(emqx_secret:wrap(erlang, atom_to_binary, this_was_an_atom))
).
wrap_term_test() ->
?assertEqual(
42,
emqx_secret:term(emqx_secret:wrap(42))
).
wrap_external_term_test() ->
?assertEqual(
this_was_an_atom,
emqx_secret:term(emqx_secret:wrap(erlang, atom_to_binary, this_was_an_atom))
).
external_fun_term_error_test() ->
Term = {foo, bar},
?assertError(
badarg,
emqx_secret:term(fun() -> Term end)
).
%%
ident(X) ->
X.

View File

@ -908,6 +908,9 @@ typename_to_spec("port_number()", _Mod) ->
range("1..65535"); range("1..65535");
typename_to_spec("secret_access_key()", _Mod) -> typename_to_spec("secret_access_key()", _Mod) ->
#{type => string, example => <<"TW8dPwmjpjJJuLW....">>}; #{type => string, example => <<"TW8dPwmjpjJJuLW....">>};
typename_to_spec("secret()", _Mod) ->
%% TODO: ideally, this should be dispatched to the module that defines this type
#{type => string, example => <<"R4ND0M/S∃CЯ∃T"/utf8>>};
typename_to_spec(Name, Mod) -> typename_to_spec(Name, Mod) ->
try_convert_to_spec(Name, Mod, [ try_convert_to_spec(Name, Mod, [
fun try_remote_module_type/2, fun try_remote_module_type/2,