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:
parent
9dfffd90d0
commit
1c2f9321d1
|
@ -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.
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue