Merge pull request #9718 from zmstone/0108-refactor-authn-schema-union-member-selector
0108 refactor authn schema union member selector
This commit is contained in:
commit
d628aba079
|
@ -759,9 +759,10 @@ maybe_unhook(State) ->
|
||||||
State.
|
State.
|
||||||
|
|
||||||
do_create_authenticator(AuthenticatorID, #{enable := Enable} = Config, Providers) ->
|
do_create_authenticator(AuthenticatorID, #{enable := Enable} = Config, Providers) ->
|
||||||
case maps:get(authn_type(Config), Providers, undefined) of
|
Type = authn_type(Config),
|
||||||
|
case maps:get(Type, Providers, undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{error, no_available_provider};
|
{error, {no_available_provider_for, Type}};
|
||||||
Provider ->
|
Provider ->
|
||||||
case Provider:create(AuthenticatorID, Config) of
|
case Provider:create(AuthenticatorID, Config) of
|
||||||
{ok, State} ->
|
{ok, State} ->
|
||||||
|
|
|
@ -136,7 +136,7 @@ do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}
|
||||||
) ->
|
) ->
|
||||||
ok | {ok, map()} | {error, term()}.
|
ok | {ok, map()} | {error, term()}.
|
||||||
post_config_update(_, UpdateReq, NewConfig, OldConfig, AppEnvs) ->
|
post_config_update(_, UpdateReq, NewConfig, OldConfig, AppEnvs) ->
|
||||||
do_post_config_update(UpdateReq, check_configs(to_list(NewConfig)), OldConfig, AppEnvs).
|
do_post_config_update(UpdateReq, to_list(NewConfig), OldConfig, AppEnvs).
|
||||||
|
|
||||||
do_post_config_update({create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs) ->
|
do_post_config_update({create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs) ->
|
||||||
NConfig = get_authenticator_config(authenticator_id(Config), NewConfig),
|
NConfig = get_authenticator_config(authenticator_id(Config), NewConfig),
|
||||||
|
@ -175,56 +175,6 @@ do_post_config_update(
|
||||||
) ->
|
) ->
|
||||||
emqx_authentication:move_authenticator(ChainName, AuthenticatorID, Position).
|
emqx_authentication:move_authenticator(ChainName, AuthenticatorID, Position).
|
||||||
|
|
||||||
check_configs(Configs) ->
|
|
||||||
Providers = emqx_authentication:get_providers(),
|
|
||||||
lists:map(fun(C) -> do_check_config(C, Providers) end, Configs).
|
|
||||||
|
|
||||||
do_check_config(Config, Providers) ->
|
|
||||||
Type = authn_type(Config),
|
|
||||||
case maps:get(Type, Providers, false) of
|
|
||||||
false ->
|
|
||||||
?SLOG(warning, #{
|
|
||||||
msg => "unknown_authn_type",
|
|
||||||
type => Type,
|
|
||||||
providers => Providers
|
|
||||||
}),
|
|
||||||
throw({unknown_authn_type, Type});
|
|
||||||
Module ->
|
|
||||||
do_check_config(Type, Config, Module)
|
|
||||||
end.
|
|
||||||
|
|
||||||
do_check_config(Type, Config, Module) ->
|
|
||||||
F =
|
|
||||||
case erlang:function_exported(Module, check_config, 1) of
|
|
||||||
true ->
|
|
||||||
fun Module:check_config/1;
|
|
||||||
false ->
|
|
||||||
fun(C) ->
|
|
||||||
Key = list_to_binary(?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME),
|
|
||||||
AtomKey = list_to_atom(?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME),
|
|
||||||
R = hocon_tconf:check_plain(
|
|
||||||
Module,
|
|
||||||
#{Key => C},
|
|
||||||
#{atom_key => true}
|
|
||||||
),
|
|
||||||
maps:get(AtomKey, R)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
try
|
|
||||||
F(Config)
|
|
||||||
catch
|
|
||||||
C:E:S ->
|
|
||||||
?SLOG(warning, #{
|
|
||||||
msg => "failed_to_check_config",
|
|
||||||
config => Config,
|
|
||||||
type => Type,
|
|
||||||
exception => C,
|
|
||||||
reason => E,
|
|
||||||
stacktrace => S
|
|
||||||
}),
|
|
||||||
throw({bad_authenticator_config, #{type => Type, reason => E}})
|
|
||||||
end.
|
|
||||||
|
|
||||||
to_list(undefined) -> [];
|
to_list(undefined) -> [];
|
||||||
to_list(M) when M =:= #{} -> [];
|
to_list(M) when M =:= #{} -> [];
|
||||||
to_list(M) when is_map(M) -> [M];
|
to_list(M) when is_map(M) -> [M];
|
||||||
|
|
|
@ -366,13 +366,6 @@ schema_default(Schema) ->
|
||||||
case hocon_schema:field_schema(Schema, type) of
|
case hocon_schema:field_schema(Schema, type) of
|
||||||
?ARRAY(_) ->
|
?ARRAY(_) ->
|
||||||
[];
|
[];
|
||||||
?LAZY(?ARRAY(_)) ->
|
|
||||||
[];
|
|
||||||
?LAZY(?UNION(Members)) ->
|
|
||||||
case [A || ?ARRAY(A) <- hoconsc:union_members(Members)] of
|
|
||||||
[_ | _] -> [];
|
|
||||||
_ -> #{}
|
|
||||||
end;
|
|
||||||
_ ->
|
_ ->
|
||||||
#{}
|
#{}
|
||||||
end.
|
end.
|
||||||
|
@ -407,8 +400,7 @@ merge_envs(SchemaMod, RawConf) ->
|
||||||
Opts = #{
|
Opts = #{
|
||||||
required => false,
|
required => false,
|
||||||
format => map,
|
format => map,
|
||||||
apply_override_envs => true,
|
apply_override_envs => true
|
||||||
check_lazy => true
|
|
||||||
},
|
},
|
||||||
hocon_tconf:merge_env_overrides(SchemaMod, RawConf, all, Opts).
|
hocon_tconf:merge_env_overrides(SchemaMod, RawConf, all, Opts).
|
||||||
|
|
||||||
|
@ -421,39 +413,15 @@ check_config(SchemaMod, RawConf, Opts0) ->
|
||||||
try
|
try
|
||||||
do_check_config(SchemaMod, RawConf, Opts0)
|
do_check_config(SchemaMod, RawConf, Opts0)
|
||||||
catch
|
catch
|
||||||
throw:{Schema, Errors} ->
|
throw:Errors:Stacktrace ->
|
||||||
compact_errors(Schema, Errors)
|
{error, Reason} = emqx_hocon:compact_errors(Errors, Stacktrace),
|
||||||
|
erlang:raise(throw, Reason, Stacktrace)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% HOCON tries to be very informative about all the detailed errors
|
|
||||||
%% it's maybe too much when reporting to the user
|
|
||||||
-spec compact_errors(any(), any()) -> no_return().
|
|
||||||
compact_errors(Schema, [Error0 | More]) when is_map(Error0) ->
|
|
||||||
Error1 =
|
|
||||||
case length(More) of
|
|
||||||
0 ->
|
|
||||||
Error0;
|
|
||||||
_ ->
|
|
||||||
Error0#{unshown_errors => length(More)}
|
|
||||||
end,
|
|
||||||
Error =
|
|
||||||
case is_atom(Schema) of
|
|
||||||
true ->
|
|
||||||
Error1#{schema_module => Schema};
|
|
||||||
false ->
|
|
||||||
Error1
|
|
||||||
end,
|
|
||||||
throw(Error);
|
|
||||||
compact_errors(Schema, Errors) ->
|
|
||||||
%% unexpected, we need the stacktrace reported, hence error
|
|
||||||
error({Schema, Errors}).
|
|
||||||
|
|
||||||
do_check_config(SchemaMod, RawConf, Opts0) ->
|
do_check_config(SchemaMod, RawConf, Opts0) ->
|
||||||
Opts1 = #{
|
Opts1 = #{
|
||||||
return_plain => true,
|
return_plain => true,
|
||||||
format => map,
|
format => map
|
||||||
%% Don't check lazy types, such as authenticate
|
|
||||||
check_lazy => false
|
|
||||||
},
|
},
|
||||||
Opts = maps:merge(Opts0, Opts1),
|
Opts = maps:merge(Opts0, Opts1),
|
||||||
{AppEnvs, CheckedConf} =
|
{AppEnvs, CheckedConf} =
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
-export([
|
-export([
|
||||||
format_path/1,
|
format_path/1,
|
||||||
check/2,
|
check/2,
|
||||||
|
check/3,
|
||||||
|
compact_errors/2,
|
||||||
format_error/1,
|
format_error/1,
|
||||||
format_error/2,
|
format_error/2,
|
||||||
make_schema/1
|
make_schema/1
|
||||||
|
@ -36,20 +38,23 @@ format_path([Name | Rest]) -> [iol(Name), "." | format_path(Rest)].
|
||||||
%% Always return plain map with atom keys.
|
%% Always return plain map with atom keys.
|
||||||
-spec check(module(), hocon:config() | iodata()) ->
|
-spec check(module(), hocon:config() | iodata()) ->
|
||||||
{ok, hocon:config()} | {error, any()}.
|
{ok, hocon:config()} | {error, any()}.
|
||||||
check(SchemaModule, Conf) when is_map(Conf) ->
|
check(SchemaModule, Conf) ->
|
||||||
%% TODO: remove required
|
%% TODO: remove required
|
||||||
%% fields should state required or not in their schema
|
%% fields should state required or not in their schema
|
||||||
Opts = #{atom_key => true, required => false},
|
Opts = #{atom_key => true, required => false},
|
||||||
|
check(SchemaModule, Conf, Opts).
|
||||||
|
|
||||||
|
check(SchemaModule, Conf, Opts) when is_map(Conf) ->
|
||||||
try
|
try
|
||||||
{ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts)}
|
{ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts)}
|
||||||
catch
|
catch
|
||||||
throw:Reason ->
|
throw:Errors:Stacktrace ->
|
||||||
{error, Reason}
|
compact_errors(Errors, Stacktrace)
|
||||||
end;
|
end;
|
||||||
check(SchemaModule, HoconText) ->
|
check(SchemaModule, HoconText, Opts) ->
|
||||||
case hocon:binary(HoconText, #{format => map}) of
|
case hocon:binary(HoconText, #{format => map}) of
|
||||||
{ok, MapConfig} ->
|
{ok, MapConfig} ->
|
||||||
check(SchemaModule, MapConfig);
|
check(SchemaModule, MapConfig, Opts);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
@ -90,3 +95,34 @@ iol(L) when is_list(L) -> L.
|
||||||
|
|
||||||
no_stacktrace(Map) ->
|
no_stacktrace(Map) ->
|
||||||
maps:without([stacktrace], Map).
|
maps:without([stacktrace], Map).
|
||||||
|
|
||||||
|
%% @doc HOCON tries to be very informative about all the detailed errors
|
||||||
|
%% it's maybe too much when reporting to the user
|
||||||
|
-spec compact_errors(any(), Stacktrace :: list()) -> {error, any()}.
|
||||||
|
compact_errors({SchemaModule, Errors}, Stacktrace) ->
|
||||||
|
compact_errors(SchemaModule, Errors, Stacktrace).
|
||||||
|
|
||||||
|
compact_errors(SchemaModule, [Error0 | More], _Stacktrace) when is_map(Error0) ->
|
||||||
|
Error1 =
|
||||||
|
case length(More) of
|
||||||
|
0 ->
|
||||||
|
Error0;
|
||||||
|
N ->
|
||||||
|
Error0#{unshown_errors_count => N}
|
||||||
|
end,
|
||||||
|
Error =
|
||||||
|
case is_atom(SchemaModule) of
|
||||||
|
true ->
|
||||||
|
Error1#{schema_module => SchemaModule};
|
||||||
|
false ->
|
||||||
|
Error1
|
||||||
|
end,
|
||||||
|
{error, Error};
|
||||||
|
compact_errors(SchemaModule, Error, Stacktrace) ->
|
||||||
|
%% unexpected, we need the stacktrace reported
|
||||||
|
%% if this happens it's a bug in hocon_tconf
|
||||||
|
{error, #{
|
||||||
|
schema_module => SchemaModule,
|
||||||
|
exception => Error,
|
||||||
|
stacktrace => Stacktrace
|
||||||
|
}}.
|
||||||
|
|
|
@ -2352,25 +2352,23 @@ authentication(Which) ->
|
||||||
global -> ?DESC(global_authentication);
|
global -> ?DESC(global_authentication);
|
||||||
listener -> ?DESC(listener_authentication)
|
listener -> ?DESC(listener_authentication)
|
||||||
end,
|
end,
|
||||||
%% The runtime module injection
|
%% poor man's dependency injection
|
||||||
%% from EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY
|
%% this is due to the fact that authn is implemented outside of 'emqx' app.
|
||||||
%% is for now only affecting document generation.
|
%% so it can not be a part of emqx_schema since 'emqx' app is supposed to
|
||||||
%% maybe in the future, we can find a more straightforward way to support
|
%% work standalone.
|
||||||
%% * document generation (at compile time)
|
Type =
|
||||||
%% * type checks before boot (in bin/emqx config generation)
|
|
||||||
%% * type checks at runtime (when changing configs via management API)
|
|
||||||
Type0 =
|
|
||||||
case persistent_term:get(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, undefined) of
|
case persistent_term:get(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, undefined) of
|
||||||
undefined -> hoconsc:array(typerefl:map());
|
undefined ->
|
||||||
Module -> Module:root_type()
|
hoconsc:array(typerefl:map());
|
||||||
|
Module ->
|
||||||
|
Module:root_type()
|
||||||
end,
|
end,
|
||||||
%% It is a lazy type because when handling runtime update requests
|
hoconsc:mk(Type, #{desc => Desc, converter => fun ensure_array/2}).
|
||||||
%% the config is not checked by emqx_schema, but by the injected schema
|
|
||||||
Type = hoconsc:lazy(Type0),
|
%% the older version schema allows individual element (instead of a chain) in config
|
||||||
#{
|
ensure_array(undefined, _) -> undefined;
|
||||||
type => Type,
|
ensure_array(L, _) when is_list(L) -> L;
|
||||||
desc => Desc
|
ensure_array(M, _) -> [M].
|
||||||
}.
|
|
||||||
|
|
||||||
-spec qos() -> typerefl:type().
|
-spec qos() -> typerefl:type().
|
||||||
qos() ->
|
qos() ->
|
||||||
|
|
|
@ -52,50 +52,10 @@
|
||||||
)
|
)
|
||||||
).
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Hocon Schema
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
roots() ->
|
|
||||||
[
|
|
||||||
{config, #{
|
|
||||||
type => hoconsc:union([
|
|
||||||
hoconsc:ref(?MODULE, type1),
|
|
||||||
hoconsc:ref(?MODULE, type2)
|
|
||||||
])
|
|
||||||
}}
|
|
||||||
].
|
|
||||||
|
|
||||||
fields(type1) ->
|
|
||||||
[
|
|
||||||
{mechanism, {enum, [password_based]}},
|
|
||||||
{backend, {enum, [built_in_database]}},
|
|
||||||
{enable, fun enable/1}
|
|
||||||
];
|
|
||||||
fields(type2) ->
|
|
||||||
[
|
|
||||||
{mechanism, {enum, [password_based]}},
|
|
||||||
{backend, {enum, [mysql]}},
|
|
||||||
{enable, fun enable/1}
|
|
||||||
].
|
|
||||||
|
|
||||||
enable(type) -> boolean();
|
|
||||||
enable(default) -> true;
|
|
||||||
enable(_) -> undefined.
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Callbacks
|
%% Callbacks
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
check_config(C) ->
|
|
||||||
#{config := R} =
|
|
||||||
hocon_tconf:check_plain(
|
|
||||||
?MODULE,
|
|
||||||
#{<<"config">> => C},
|
|
||||||
#{atom_key => true}
|
|
||||||
),
|
|
||||||
R.
|
|
||||||
|
|
||||||
create(_AuthenticatorID, _Config) ->
|
create(_AuthenticatorID, _Config) ->
|
||||||
{ok, #{mark => 1}}.
|
{ok, #{mark => 1}}.
|
||||||
|
|
||||||
|
@ -200,7 +160,7 @@ t_authenticator(Config) when is_list(Config) ->
|
||||||
% Create an authenticator when the provider does not exist
|
% Create an authenticator when the provider does not exist
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
{error, no_available_provider},
|
{error, {no_available_provider_for, {password_based, built_in_database}}},
|
||||||
?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)
|
?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -335,14 +295,14 @@ t_update_config(Config) when is_list(Config) ->
|
||||||
ok = register_provider(?config("auth2"), ?MODULE),
|
ok = register_provider(?config("auth2"), ?MODULE),
|
||||||
Global = ?config(global),
|
Global = ?config(global),
|
||||||
AuthenticatorConfig1 = #{
|
AuthenticatorConfig1 = #{
|
||||||
<<"mechanism">> => <<"password_based">>,
|
mechanism => password_based,
|
||||||
<<"backend">> => <<"built_in_database">>,
|
backend => built_in_database,
|
||||||
<<"enable">> => true
|
enable => true
|
||||||
},
|
},
|
||||||
AuthenticatorConfig2 = #{
|
AuthenticatorConfig2 = #{
|
||||||
<<"mechanism">> => <<"password_based">>,
|
mechanism => password_based,
|
||||||
<<"backend">> => <<"mysql">>,
|
backend => mysql,
|
||||||
<<"enable">> => true
|
enable => true
|
||||||
},
|
},
|
||||||
ID1 = <<"password_based:built_in_database">>,
|
ID1 = <<"password_based:built_in_database">>,
|
||||||
ID2 = <<"password_based:mysql">>,
|
ID2 = <<"password_based:mysql">>,
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
providers/0,
|
providers/0,
|
||||||
check_config/1,
|
check_config/1,
|
||||||
check_config/2,
|
check_config/2,
|
||||||
check_configs/1,
|
|
||||||
%% for telemetry information
|
%% for telemetry information
|
||||||
get_enabled_authns/0
|
get_enabled_authns/0
|
||||||
]).
|
]).
|
||||||
|
@ -39,16 +38,6 @@ providers() ->
|
||||||
{{scram, built_in_database}, emqx_enhanced_authn_scram_mnesia}
|
{{scram, built_in_database}, emqx_enhanced_authn_scram_mnesia}
|
||||||
].
|
].
|
||||||
|
|
||||||
check_configs(CM) when is_map(CM) ->
|
|
||||||
check_configs([CM]);
|
|
||||||
check_configs(CL) ->
|
|
||||||
check_configs(CL, 1).
|
|
||||||
|
|
||||||
check_configs([], _Nth) ->
|
|
||||||
[];
|
|
||||||
check_configs([Config | Configs], Nth) ->
|
|
||||||
[check_config(Config, #{id_for_log => Nth}) | check_configs(Configs, Nth + 1)].
|
|
||||||
|
|
||||||
check_config(Config) ->
|
check_config(Config) ->
|
||||||
check_config(Config, #{}).
|
check_config(Config, #{}).
|
||||||
|
|
||||||
|
@ -67,21 +56,32 @@ do_check_config(#{<<"mechanism">> := Mec0} = Config, Opts) ->
|
||||||
end,
|
end,
|
||||||
case lists:keyfind(Key, 1, providers()) of
|
case lists:keyfind(Key, 1, providers()) of
|
||||||
false ->
|
false ->
|
||||||
throw(#{error => unknown_authn_provider, which => Key});
|
Reason =
|
||||||
|
case Key of
|
||||||
|
{M, B} ->
|
||||||
|
#{mechanism => M, backend => B};
|
||||||
|
M ->
|
||||||
|
#{mechanism => M}
|
||||||
|
end,
|
||||||
|
throw(Reason#{error => unknown_authn_provider});
|
||||||
{_, ProviderModule} ->
|
{_, ProviderModule} ->
|
||||||
hocon_tconf:check_plain(
|
do_check_config_maybe_throw(ProviderModule, Config, Opts)
|
||||||
ProviderModule,
|
|
||||||
#{?CONF_NS_BINARY => Config},
|
|
||||||
Opts#{atom_key => true}
|
|
||||||
)
|
|
||||||
end;
|
end;
|
||||||
do_check_config(Config, Opts) when is_map(Config) ->
|
do_check_config(Config, _Opts) when is_map(Config) ->
|
||||||
throw(#{
|
throw(#{
|
||||||
error => invalid_config,
|
error => invalid_config,
|
||||||
which => maps:get(id_for_log, Opts, unknown),
|
|
||||||
reason => "mechanism_field_required"
|
reason => "mechanism_field_required"
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
do_check_config_maybe_throw(ProviderModule, Config0, Opts) ->
|
||||||
|
Config = #{?CONF_NS_BINARY => Config0},
|
||||||
|
case emqx_hocon:check(ProviderModule, Config, Opts#{atom_key => true}) of
|
||||||
|
{ok, Checked} ->
|
||||||
|
Checked;
|
||||||
|
{error, Reason} ->
|
||||||
|
throw(Reason)
|
||||||
|
end.
|
||||||
|
|
||||||
%% The atoms have to be loaded already,
|
%% The atoms have to be loaded already,
|
||||||
%% which might be an issue for plugins which are loaded after node boot
|
%% which might be an issue for plugins which are loaded after node boot
|
||||||
%% but they should really manage their own configs in that case.
|
%% but they should really manage their own configs in that case.
|
||||||
|
|
|
@ -1232,15 +1232,10 @@ serialize_error({unknown_authn_type, Type}) ->
|
||||||
code => <<"BAD_REQUEST">>,
|
code => <<"BAD_REQUEST">>,
|
||||||
message => binfmt("Unknown type '~p'", [Type])
|
message => binfmt("Unknown type '~p'", [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, #{
|
{400, #{
|
||||||
code => <<"BAD_REQUEST">>,
|
code => <<"BAD_REQUEST">>,
|
||||||
message => binfmt("~p", [Reason])
|
message => binfmt("~0p", [Reason])
|
||||||
}}.
|
}}.
|
||||||
|
|
||||||
parse_position(<<"front">>) ->
|
parse_position(<<"front">>) ->
|
||||||
|
|
|
@ -35,6 +35,9 @@
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
|
%% required by test cases, ensure the injection of
|
||||||
|
%% EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY
|
||||||
|
_ = emqx_conf_schema:roots(),
|
||||||
ok = mria_rlog:wait_for_shards([?AUTH_SHARD], infinity),
|
ok = mria_rlog:wait_for_shards([?AUTH_SHARD], infinity),
|
||||||
{ok, Sup} = emqx_authn_sup:start_link(),
|
{ok, Sup} = emqx_authn_sup:start_link(),
|
||||||
case initialize() of
|
case initialize() of
|
||||||
|
@ -43,34 +46,23 @@ start(_StartType, _StartArgs) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
ok = deinitialize(),
|
ok = deinitialize().
|
||||||
ok.
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
initialize() ->
|
initialize() ->
|
||||||
try
|
|
||||||
ok = ?AUTHN:register_providers(emqx_authn:providers()),
|
ok = ?AUTHN:register_providers(emqx_authn:providers()),
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun({ChainName, RawAuthConfigs}) ->
|
fun({ChainName, AuthConfig}) ->
|
||||||
AuthConfig = emqx_authn:check_configs(RawAuthConfigs),
|
|
||||||
?AUTHN:initialize_authentication(
|
?AUTHN:initialize_authentication(
|
||||||
ChainName,
|
ChainName,
|
||||||
AuthConfig
|
AuthConfig
|
||||||
)
|
)
|
||||||
end,
|
end,
|
||||||
chain_configs()
|
chain_configs()
|
||||||
)
|
).
|
||||||
of
|
|
||||||
ok -> ok
|
|
||||||
catch
|
|
||||||
throw:Reason ->
|
|
||||||
?SLOG(error, #{msg => "failed_to_initialize_authentication", reason => Reason}),
|
|
||||||
{error, {failed_to_initialize_authentication, Reason}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
deinitialize() ->
|
deinitialize() ->
|
||||||
ok = ?AUTHN:deregister_providers(provider_types()),
|
ok = ?AUTHN:deregister_providers(provider_types()),
|
||||||
|
@ -80,12 +72,12 @@ chain_configs() ->
|
||||||
[global_chain_config() | listener_chain_configs()].
|
[global_chain_config() | listener_chain_configs()].
|
||||||
|
|
||||||
global_chain_config() ->
|
global_chain_config() ->
|
||||||
{?GLOBAL, emqx:get_raw_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY], [])}.
|
{?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY], [])}.
|
||||||
|
|
||||||
listener_chain_configs() ->
|
listener_chain_configs() ->
|
||||||
lists:map(
|
lists:map(
|
||||||
fun({ListenerID, _}) ->
|
fun({ListenerID, _}) ->
|
||||||
{ListenerID, emqx:get_raw_config(auth_config_path(ListenerID), [])}
|
{ListenerID, emqx:get_config(auth_config_path(ListenerID), [])}
|
||||||
end,
|
end,
|
||||||
emqx_listeners:list()
|
emqx_listeners:list()
|
||||||
).
|
).
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
]).
|
]).
|
||||||
|
|
||||||
namespace() -> "authn-hash".
|
namespace() -> "authn-hash".
|
||||||
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
|
roots() -> [pbkdf2, bcrypt, bcrypt_rw, simple].
|
||||||
|
|
||||||
fields(bcrypt_rw) ->
|
fields(bcrypt_rw) ->
|
||||||
fields(bcrypt) ++
|
fields(bcrypt) ++
|
||||||
|
@ -96,7 +96,7 @@ fields(pbkdf2) ->
|
||||||
)},
|
)},
|
||||||
{dk_length, fun dk_length/1}
|
{dk_length, fun dk_length/1}
|
||||||
];
|
];
|
||||||
fields(other_algorithms) ->
|
fields(simple) ->
|
||||||
[
|
[
|
||||||
{name,
|
{name,
|
||||||
sc(
|
sc(
|
||||||
|
@ -112,8 +112,8 @@ desc(bcrypt) ->
|
||||||
"Settings for bcrypt password hashing algorithm.";
|
"Settings for bcrypt password hashing algorithm.";
|
||||||
desc(pbkdf2) ->
|
desc(pbkdf2) ->
|
||||||
"Settings for PBKDF2 password hashing algorithm.";
|
"Settings for PBKDF2 password hashing algorithm.";
|
||||||
desc(other_algorithms) ->
|
desc(simple) ->
|
||||||
"Settings for other password hashing algorithms.";
|
"Settings for simple algorithms.";
|
||||||
desc(_) ->
|
desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
|
@ -231,17 +231,31 @@ check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHa
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
rw_refs() ->
|
rw_refs() ->
|
||||||
[
|
All = [
|
||||||
hoconsc:ref(?MODULE, bcrypt_rw),
|
hoconsc:ref(?MODULE, bcrypt_rw),
|
||||||
hoconsc:ref(?MODULE, pbkdf2),
|
hoconsc:ref(?MODULE, pbkdf2),
|
||||||
hoconsc:ref(?MODULE, other_algorithms)
|
hoconsc:ref(?MODULE, simple)
|
||||||
].
|
],
|
||||||
|
fun
|
||||||
|
(all_union_members) -> All;
|
||||||
|
({value, #{<<"name">> := <<"bcrypt">>}}) -> [hoconsc:ref(?MODULE, bcrypt_rw)];
|
||||||
|
({value, #{<<"name">> := <<"pbkdf2">>}}) -> [hoconsc:ref(?MODULE, pbkdf2)];
|
||||||
|
({value, #{<<"name">> := _}}) -> [hoconsc:ref(?MODULE, simple)];
|
||||||
|
({value, _}) -> throw(#{reason => "algorithm_name_missing"})
|
||||||
|
end.
|
||||||
|
|
||||||
ro_refs() ->
|
ro_refs() ->
|
||||||
[
|
All = [
|
||||||
hoconsc:ref(?MODULE, bcrypt),
|
hoconsc:ref(?MODULE, bcrypt),
|
||||||
hoconsc:ref(?MODULE, pbkdf2),
|
hoconsc:ref(?MODULE, pbkdf2),
|
||||||
hoconsc:ref(?MODULE, other_algorithms)
|
hoconsc:ref(?MODULE, simple)
|
||||||
].
|
],
|
||||||
|
fun
|
||||||
|
(all_union_members) -> All;
|
||||||
|
({value, #{<<"name">> := <<"bcrypt">>}}) -> [hoconsc:ref(?MODULE, bcrypt)];
|
||||||
|
({value, #{<<"name">> := <<"pbkdf2">>}}) -> [hoconsc:ref(?MODULE, pbkdf2)];
|
||||||
|
({value, #{<<"name">> := _}}) -> [hoconsc:ref(?MODULE, simple)];
|
||||||
|
({value, _}) -> throw(#{reason => "algorithm_name_missing"})
|
||||||
|
end.
|
||||||
|
|
||||||
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
||||||
|
|
|
@ -45,24 +45,79 @@ enable(desc) -> ?DESC(?FUNCTION_NAME);
|
||||||
enable(_) -> undefined.
|
enable(_) -> undefined.
|
||||||
|
|
||||||
authenticator_type() ->
|
authenticator_type() ->
|
||||||
hoconsc:union(config_refs([Module || {_AuthnType, Module} <- emqx_authn:providers()])).
|
hoconsc:union(union_member_selector(emqx_authn:providers())).
|
||||||
|
|
||||||
authenticator_type_without_scram() ->
|
authenticator_type_without_scram() ->
|
||||||
Providers = lists:filtermap(
|
Providers = lists:filtermap(
|
||||||
fun
|
fun
|
||||||
({{password_based, _Backend}, Mod}) ->
|
|
||||||
{true, Mod};
|
|
||||||
({jwt, Mod}) ->
|
|
||||||
{true, Mod};
|
|
||||||
({{scram, _Backend}, _Mod}) ->
|
({{scram, _Backend}, _Mod}) ->
|
||||||
false
|
false;
|
||||||
|
(_) ->
|
||||||
|
true
|
||||||
end,
|
end,
|
||||||
emqx_authn:providers()
|
emqx_authn:providers()
|
||||||
),
|
),
|
||||||
hoconsc:union(config_refs(Providers)).
|
hoconsc:union(union_member_selector(Providers)).
|
||||||
|
|
||||||
config_refs(Modules) ->
|
config_refs(Providers) ->
|
||||||
lists:append([Module:refs() || Module <- Modules]).
|
lists:append([Module:refs() || {_, Module} <- Providers]).
|
||||||
|
|
||||||
|
union_member_selector(Providers) ->
|
||||||
|
Types = config_refs(Providers),
|
||||||
|
fun
|
||||||
|
(all_union_members) -> Types;
|
||||||
|
({value, Value}) -> select_union_member(Value, Providers)
|
||||||
|
end.
|
||||||
|
|
||||||
|
select_union_member(#{<<"mechanism">> := _} = Value, Providers0) ->
|
||||||
|
BackendVal = maps:get(<<"backend">>, Value, undefined),
|
||||||
|
MechanismVal = maps:get(<<"mechanism">>, Value),
|
||||||
|
BackendFilterFn = fun
|
||||||
|
({{_Mec, Backend}, _Mod}) ->
|
||||||
|
BackendVal =:= atom_to_binary(Backend);
|
||||||
|
(_) ->
|
||||||
|
BackendVal =:= undefined
|
||||||
|
end,
|
||||||
|
MechanismFilterFn = fun
|
||||||
|
({{Mechanism, _Backend}, _Mod}) ->
|
||||||
|
MechanismVal =:= atom_to_binary(Mechanism);
|
||||||
|
({Mechanism, _Mod}) ->
|
||||||
|
MechanismVal =:= atom_to_binary(Mechanism)
|
||||||
|
end,
|
||||||
|
case lists:filter(BackendFilterFn, Providers0) of
|
||||||
|
[] ->
|
||||||
|
throw(#{reason => "unknown_backend", backend => BackendVal});
|
||||||
|
Providers1 ->
|
||||||
|
case lists:filter(MechanismFilterFn, Providers1) of
|
||||||
|
[] ->
|
||||||
|
throw(#{
|
||||||
|
reason => "unsupported_mechanism",
|
||||||
|
mechanism => MechanismVal,
|
||||||
|
backend => BackendVal
|
||||||
|
});
|
||||||
|
[{_, Module}] ->
|
||||||
|
try_select_union_member(Module, Value)
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
select_union_member(Value, _Providers) when is_map(Value) ->
|
||||||
|
throw(#{reason => "missing_mechanism_field"});
|
||||||
|
select_union_member(Value, _Providers) ->
|
||||||
|
throw(#{reason => "not_a_struct", value => Value}).
|
||||||
|
|
||||||
|
try_select_union_member(Module, Value) ->
|
||||||
|
%% some modules have `union_member_selector/1' exported to help selecting
|
||||||
|
%% the sub-types, they are:
|
||||||
|
%% emqx_authn_http
|
||||||
|
%% emqx_authn_jwt
|
||||||
|
%% emqx_authn_mongodb
|
||||||
|
%% emqx_authn_redis
|
||||||
|
try
|
||||||
|
Module:union_member_selector({value, Value})
|
||||||
|
catch
|
||||||
|
error:undef ->
|
||||||
|
%% otherwise expect only one member from this module
|
||||||
|
Module:refs()
|
||||||
|
end.
|
||||||
|
|
||||||
%% authn is a core functionality however implemented outside of emqx app
|
%% authn is a core functionality however implemented outside of emqx app
|
||||||
%% in emqx_schema, 'authentication' is a map() type which is to allow
|
%% in emqx_schema, 'authentication' is a map() type which is to allow
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
refs/0,
|
refs/0,
|
||||||
|
union_member_selector/1,
|
||||||
create/2,
|
create/2,
|
||||||
update/2,
|
update/2,
|
||||||
authenticate/2,
|
authenticate/2,
|
||||||
|
@ -59,19 +60,19 @@ roots() ->
|
||||||
[
|
[
|
||||||
{?CONF_NS,
|
{?CONF_NS,
|
||||||
hoconsc:mk(
|
hoconsc:mk(
|
||||||
hoconsc:union(refs()),
|
hoconsc:union(fun union_member_selector/1),
|
||||||
#{}
|
#{}
|
||||||
)}
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
fields(get) ->
|
fields(get) ->
|
||||||
[
|
[
|
||||||
{method, #{type => get, required => true, default => get, desc => ?DESC(method)}},
|
{method, #{type => get, required => true, desc => ?DESC(method)}},
|
||||||
{headers, fun headers_no_content_type/1}
|
{headers, fun headers_no_content_type/1}
|
||||||
] ++ common_fields();
|
] ++ common_fields();
|
||||||
fields(post) ->
|
fields(post) ->
|
||||||
[
|
[
|
||||||
{method, #{type => post, required => true, default => post, desc => ?DESC(method)}},
|
{method, #{type => post, required => true, desc => ?DESC(method)}},
|
||||||
{headers, fun headers/1}
|
{headers, fun headers/1}
|
||||||
] ++ common_fields().
|
] ++ common_fields().
|
||||||
|
|
||||||
|
@ -159,6 +160,21 @@ refs() ->
|
||||||
hoconsc:ref(?MODULE, post)
|
hoconsc:ref(?MODULE, post)
|
||||||
].
|
].
|
||||||
|
|
||||||
|
union_member_selector(all_union_members) ->
|
||||||
|
refs();
|
||||||
|
union_member_selector({value, Value}) ->
|
||||||
|
refs(Value).
|
||||||
|
|
||||||
|
refs(#{<<"method">> := <<"get">>}) ->
|
||||||
|
[hoconsc:ref(?MODULE, get)];
|
||||||
|
refs(#{<<"method">> := <<"post">>}) ->
|
||||||
|
[hoconsc:ref(?MODULE, post)];
|
||||||
|
refs(_) ->
|
||||||
|
throw(#{
|
||||||
|
field_name => method,
|
||||||
|
expected => "get | post"
|
||||||
|
}).
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
namespace/0,
|
namespace/0,
|
||||||
|
@ -33,6 +32,7 @@
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
refs/0,
|
refs/0,
|
||||||
|
union_member_selector/1,
|
||||||
create/2,
|
create/2,
|
||||||
update/2,
|
update/2,
|
||||||
authenticate/2,
|
authenticate/2,
|
||||||
|
@ -52,7 +52,7 @@ roots() ->
|
||||||
[
|
[
|
||||||
{?CONF_NS,
|
{?CONF_NS,
|
||||||
hoconsc:mk(
|
hoconsc:mk(
|
||||||
hoconsc:union(refs()),
|
hoconsc:union(fun union_member_selector/1),
|
||||||
#{}
|
#{}
|
||||||
)}
|
)}
|
||||||
].
|
].
|
||||||
|
@ -165,6 +165,31 @@ refs() ->
|
||||||
hoconsc:ref(?MODULE, 'jwks')
|
hoconsc:ref(?MODULE, 'jwks')
|
||||||
].
|
].
|
||||||
|
|
||||||
|
union_member_selector(all_union_members) ->
|
||||||
|
refs();
|
||||||
|
union_member_selector({value, V}) ->
|
||||||
|
UseJWKS = maps:get(<<"use_jwks">>, V, undefined),
|
||||||
|
select_ref(boolean(UseJWKS), V).
|
||||||
|
|
||||||
|
%% this field is technically a boolean type,
|
||||||
|
%% but union member selection is done before type casting (by typrefl),
|
||||||
|
%% so we have to allow strings too
|
||||||
|
boolean(<<"true">>) -> true;
|
||||||
|
boolean(<<"false">>) -> false;
|
||||||
|
boolean(Other) -> Other.
|
||||||
|
|
||||||
|
select_ref(true, _) ->
|
||||||
|
[hoconsc:ref(?MODULE, 'jwks')];
|
||||||
|
select_ref(false, #{<<"public_key">> := _}) ->
|
||||||
|
[hoconsc:ref(?MODULE, 'public-key')];
|
||||||
|
select_ref(false, _) ->
|
||||||
|
[hoconsc:ref(?MODULE, 'hmac-based')];
|
||||||
|
select_ref(_, _) ->
|
||||||
|
throw(#{
|
||||||
|
field_name => use_jwks,
|
||||||
|
expected => "true | false"
|
||||||
|
}).
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
refs/0,
|
refs/0,
|
||||||
|
union_member_selector/1,
|
||||||
create/2,
|
create/2,
|
||||||
update/2,
|
update/2,
|
||||||
authenticate/2,
|
authenticate/2,
|
||||||
|
@ -52,7 +53,7 @@ roots() ->
|
||||||
[
|
[
|
||||||
{?CONF_NS,
|
{?CONF_NS,
|
||||||
hoconsc:mk(
|
hoconsc:mk(
|
||||||
hoconsc:union(refs()),
|
hoconsc:union(fun union_member_selector/1),
|
||||||
#{}
|
#{}
|
||||||
)}
|
)}
|
||||||
].
|
].
|
||||||
|
@ -246,3 +247,20 @@ is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) ->
|
||||||
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => IsSuperuser});
|
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => IsSuperuser});
|
||||||
is_superuser(_, _) ->
|
is_superuser(_, _) ->
|
||||||
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => false}).
|
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => false}).
|
||||||
|
|
||||||
|
union_member_selector(all_union_members) ->
|
||||||
|
refs();
|
||||||
|
union_member_selector({value, Value}) ->
|
||||||
|
refs(Value).
|
||||||
|
|
||||||
|
refs(#{<<"mongo_type">> := <<"single">>}) ->
|
||||||
|
[hoconsc:ref(?MODULE, standalone)];
|
||||||
|
refs(#{<<"mongo_type">> := <<"rs">>}) ->
|
||||||
|
[hoconsc:ref(?MODULE, 'replica-set')];
|
||||||
|
refs(#{<<"mongo_type">> := <<"sharded">>}) ->
|
||||||
|
[hoconsc:ref(?MODULE, 'sharded-cluster')];
|
||||||
|
refs(_) ->
|
||||||
|
throw(#{
|
||||||
|
field_name => mongo_type,
|
||||||
|
expected => "single | rs | sharded"
|
||||||
|
}).
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
refs/0,
|
refs/0,
|
||||||
|
union_member_selector/1,
|
||||||
create/2,
|
create/2,
|
||||||
update/2,
|
update/2,
|
||||||
authenticate/2,
|
authenticate/2,
|
||||||
|
@ -52,7 +53,7 @@ roots() ->
|
||||||
[
|
[
|
||||||
{?CONF_NS,
|
{?CONF_NS,
|
||||||
hoconsc:mk(
|
hoconsc:mk(
|
||||||
hoconsc:union(refs()),
|
hoconsc:union(fun union_member_selector/1),
|
||||||
#{}
|
#{}
|
||||||
)}
|
)}
|
||||||
].
|
].
|
||||||
|
@ -97,6 +98,23 @@ refs() ->
|
||||||
hoconsc:ref(?MODULE, sentinel)
|
hoconsc:ref(?MODULE, sentinel)
|
||||||
].
|
].
|
||||||
|
|
||||||
|
union_member_selector(all_union_members) ->
|
||||||
|
refs();
|
||||||
|
union_member_selector({value, Value}) ->
|
||||||
|
refs(Value).
|
||||||
|
|
||||||
|
refs(#{<<"redis_type">> := <<"single">>}) ->
|
||||||
|
[hoconsc:ref(?MODULE, standalone)];
|
||||||
|
refs(#{<<"redis_type">> := <<"cluster">>}) ->
|
||||||
|
[hoconsc:ref(?MODULE, cluster)];
|
||||||
|
refs(#{<<"redis_type">> := <<"sentinel">>}) ->
|
||||||
|
[hoconsc:ref(?MODULE, sentinel)];
|
||||||
|
refs(_) ->
|
||||||
|
throw(#{
|
||||||
|
field_name => redis_type,
|
||||||
|
expected => "single | cluster | sentinel"
|
||||||
|
}).
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
|
|
|
@ -168,6 +168,38 @@ t_password_undefined(Config) when is_list(Config) ->
|
||||||
end,
|
end,
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_union_selector_errors({init, Config}) ->
|
||||||
|
Config;
|
||||||
|
t_union_selector_errors({'end', _Config}) ->
|
||||||
|
ok;
|
||||||
|
t_union_selector_errors(Config) when is_list(Config) ->
|
||||||
|
Conf0 = #{
|
||||||
|
<<"mechanism">> => <<"password_based">>,
|
||||||
|
<<"backend">> => <<"mysql">>
|
||||||
|
},
|
||||||
|
Conf1 = Conf0#{<<"mechanism">> => <<"unknown-atom-xx">>},
|
||||||
|
?assertThrow(#{error := unknown_mechanism}, emqx_authn:check_config(Conf1)),
|
||||||
|
Conf2 = Conf0#{<<"backend">> => <<"unknown-atom-xx">>},
|
||||||
|
?assertThrow(#{error := unknown_backend}, emqx_authn:check_config(Conf2)),
|
||||||
|
Conf3 = Conf0#{<<"backend">> => <<"unknown">>, <<"mechanism">> => <<"unknown">>},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
error := unknown_authn_provider,
|
||||||
|
backend := unknown,
|
||||||
|
mechanism := unknown
|
||||||
|
},
|
||||||
|
emqx_authn:check_config(Conf3)
|
||||||
|
),
|
||||||
|
Res = catch (emqx_authn:check_config(#{<<"mechanism">> => <<"unknown">>})),
|
||||||
|
?assertEqual(
|
||||||
|
#{
|
||||||
|
error => unknown_authn_provider,
|
||||||
|
mechanism => unknown
|
||||||
|
},
|
||||||
|
Res
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
parse(Bytes) ->
|
parse(Bytes) ->
|
||||||
{ok, Frame, <<>>, {none, _}} = emqx_frame:parse(Bytes),
|
{ok, Frame, <<>>, {none, _}} = emqx_frame:parse(Bytes),
|
||||||
Frame.
|
Frame.
|
||||||
|
|
|
@ -49,36 +49,6 @@ end_per_testcase(_Case, Config) ->
|
||||||
%% Tests
|
%% Tests
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
-define(CONF(Conf), #{?CONF_NS_BINARY => Conf}).
|
|
||||||
|
|
||||||
t_check_schema(_Config) ->
|
|
||||||
ConfigOk = #{
|
|
||||||
<<"mechanism">> => <<"password_based">>,
|
|
||||||
<<"backend">> => <<"built_in_database">>,
|
|
||||||
<<"user_id_type">> => <<"username">>,
|
|
||||||
<<"password_hash_algorithm">> => #{
|
|
||||||
<<"name">> => <<"bcrypt">>,
|
|
||||||
<<"salt_rounds">> => <<"6">>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigOk)),
|
|
||||||
|
|
||||||
ConfigNotOk = #{
|
|
||||||
<<"mechanism">> => <<"password_based">>,
|
|
||||||
<<"backend">> => <<"built_in_database">>,
|
|
||||||
<<"user_id_type">> => <<"username">>,
|
|
||||||
<<"password_hash_algorithm">> => #{
|
|
||||||
<<"name">> => <<"md6">>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
?assertException(
|
|
||||||
throw,
|
|
||||||
{emqx_authn_mnesia, _},
|
|
||||||
hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigNotOk))
|
|
||||||
).
|
|
||||||
|
|
||||||
t_create(_) ->
|
t_create(_) ->
|
||||||
Config0 = config(),
|
Config0 = config(),
|
||||||
|
|
||||||
|
|
|
@ -105,17 +105,12 @@ t_update_with_invalid_config(_Config) ->
|
||||||
AuthConfig = raw_pgsql_auth_config(),
|
AuthConfig = raw_pgsql_auth_config(),
|
||||||
BadConfig = maps:without([<<"server">>], AuthConfig),
|
BadConfig = maps:without([<<"server">>], AuthConfig),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error,
|
{error, #{
|
||||||
{bad_authenticator_config, #{
|
|
||||||
reason :=
|
|
||||||
{emqx_authn_pgsql, [
|
|
||||||
#{
|
|
||||||
kind := validation_error,
|
kind := validation_error,
|
||||||
path := "authentication.server",
|
matched_type := "authn-postgresql:authentication",
|
||||||
|
path := "authentication.1.server",
|
||||||
reason := required_field
|
reason := required_field
|
||||||
}
|
}},
|
||||||
]}
|
|
||||||
}}},
|
|
||||||
emqx:update_config(
|
emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, BadConfig}
|
{create_authenticator, ?GLOBAL, BadConfig}
|
||||||
|
|
|
@ -160,10 +160,12 @@ t_create_invalid_config(_Config) ->
|
||||||
Config0 = raw_redis_auth_config(),
|
Config0 = raw_redis_auth_config(),
|
||||||
Config = maps:without([<<"server">>], Config0),
|
Config = maps:without([<<"server">>], Config0),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error,
|
{error, #{
|
||||||
{bad_authenticator_config, #{
|
kind := validation_error,
|
||||||
reason := {emqx_authn_redis, [#{kind := validation_error}]}
|
matched_type := "authn-redis:standalone",
|
||||||
}}},
|
path := "authentication.1.server",
|
||||||
|
reason := required_field
|
||||||
|
}},
|
||||||
emqx:update_config(?PATH, {create_authenticator, ?GLOBAL, Config})
|
emqx:update_config(?PATH, {create_authenticator, ?GLOBAL, Config})
|
||||||
),
|
),
|
||||||
?assertMatch([], emqx_config:get_raw([authentication])),
|
?assertMatch([], emqx_config:get_raw([authentication])),
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
-module(emqx_authn_schema_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-include("emqx_authn.hrl").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
_ = application:load(emqx_conf),
|
||||||
|
emqx_common_test_helpers:start_apps([emqx_authn]),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_) ->
|
||||||
|
emqx_common_test_helpers:stop_apps([emqx_authn]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
init_per_testcase(_Case, Config) ->
|
||||||
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
|
mria:clear_table(emqx_authn_mnesia),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_testcase(_Case, Config) ->
|
||||||
|
Config.
|
||||||
|
|
||||||
|
-define(CONF(Conf), #{?CONF_NS_BINARY => Conf}).
|
||||||
|
|
||||||
|
t_check_schema(_Config) ->
|
||||||
|
Check = fun(C) -> emqx_config:check_config(emqx_schema, ?CONF(C)) end,
|
||||||
|
ConfigOk = #{
|
||||||
|
<<"mechanism">> => <<"password_based">>,
|
||||||
|
<<"backend">> => <<"built_in_database">>,
|
||||||
|
<<"user_id_type">> => <<"username">>,
|
||||||
|
<<"password_hash_algorithm">> => #{
|
||||||
|
<<"name">> => <<"bcrypt">>,
|
||||||
|
<<"salt_rounds">> => <<"6">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = Check(ConfigOk),
|
||||||
|
|
||||||
|
ConfigNotOk = #{
|
||||||
|
<<"mechanism">> => <<"password_based">>,
|
||||||
|
<<"backend">> => <<"built_in_database">>,
|
||||||
|
<<"user_id_type">> => <<"username">>,
|
||||||
|
<<"password_hash_algorithm">> => #{
|
||||||
|
<<"name">> => <<"md6">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
path := "authentication.1.password_hash_algorithm.name",
|
||||||
|
matched_type := "authn-builtin_db:authentication/authn-hash:simple",
|
||||||
|
reason := unable_to_convert_to_enum_symbol
|
||||||
|
},
|
||||||
|
Check(ConfigNotOk)
|
||||||
|
),
|
||||||
|
|
||||||
|
ConfigMissingAlgoName = #{
|
||||||
|
<<"mechanism">> => <<"password_based">>,
|
||||||
|
<<"backend">> => <<"built_in_database">>,
|
||||||
|
<<"user_id_type">> => <<"username">>,
|
||||||
|
<<"password_hash_algorithm">> => #{
|
||||||
|
<<"foo">> => <<"bar">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
path := "authentication.1.password_hash_algorithm",
|
||||||
|
reason := "algorithm_name_missing",
|
||||||
|
matched_type := "authn-builtin_db:authentication"
|
||||||
|
},
|
||||||
|
Check(ConfigMissingAlgoName)
|
||||||
|
).
|
||||||
|
|
||||||
|
t_union_member_selector(_) ->
|
||||||
|
?assertMatch(#{authentication := undefined}, check(undefined)),
|
||||||
|
C1 = #{<<"backend">> => <<"built_in_database">>},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
path := "authentication.1",
|
||||||
|
reason := "missing_mechanism_field"
|
||||||
|
},
|
||||||
|
check(C1)
|
||||||
|
),
|
||||||
|
C2 = <<"foobar">>,
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
path := "authentication.1",
|
||||||
|
reason := "not_a_struct",
|
||||||
|
value := <<"foobar">>
|
||||||
|
},
|
||||||
|
check(C2)
|
||||||
|
),
|
||||||
|
Base = #{
|
||||||
|
<<"user_id_type">> => <<"username">>,
|
||||||
|
<<"password_hash_algorithm">> => #{
|
||||||
|
<<"name">> => <<"plain">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BadBackend = Base#{<<"mechanism">> => <<"password_based">>, <<"backend">> => <<"bar">>},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
reason := "unknown_backend",
|
||||||
|
backend := <<"bar">>
|
||||||
|
},
|
||||||
|
check(BadBackend)
|
||||||
|
),
|
||||||
|
BadMechanism = Base#{<<"mechanism">> => <<"foo">>, <<"backend">> => <<"built_in_database">>},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
reason := "unsupported_mechanism",
|
||||||
|
mechanism := <<"foo">>,
|
||||||
|
backend := <<"built_in_database">>
|
||||||
|
},
|
||||||
|
check(BadMechanism)
|
||||||
|
),
|
||||||
|
BadCombination = Base#{<<"mechanism">> => <<"scram">>, <<"backend">> => <<"http">>},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
reason := "unsupported_mechanism",
|
||||||
|
mechanism := <<"scram">>,
|
||||||
|
backend := <<"http">>
|
||||||
|
},
|
||||||
|
check(BadCombination)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_http_auth_selector(_) ->
|
||||||
|
C1 = #{
|
||||||
|
<<"mechanism">> => <<"password_based">>,
|
||||||
|
<<"backend">> => <<"http">>
|
||||||
|
},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
field_name := method,
|
||||||
|
expected := "get | post"
|
||||||
|
},
|
||||||
|
check(C1)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_mongo_auth_selector(_) ->
|
||||||
|
C1 = #{
|
||||||
|
<<"mechanism">> => <<"password_based">>,
|
||||||
|
<<"backend">> => <<"mongodb">>
|
||||||
|
},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
field_name := mongo_type,
|
||||||
|
expected := "single | rs | sharded"
|
||||||
|
},
|
||||||
|
check(C1)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_redis_auth_selector(_) ->
|
||||||
|
C1 = #{
|
||||||
|
<<"mechanism">> => <<"password_based">>,
|
||||||
|
<<"backend">> => <<"redis">>
|
||||||
|
},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
field_name := redis_type,
|
||||||
|
expected := "single | cluster | sentinel"
|
||||||
|
},
|
||||||
|
check(C1)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_redis_jwt_selector(_) ->
|
||||||
|
C1 = #{
|
||||||
|
<<"mechanism">> => <<"jwt">>
|
||||||
|
},
|
||||||
|
?assertThrow(
|
||||||
|
#{
|
||||||
|
field_name := use_jwks,
|
||||||
|
expected := "true | false"
|
||||||
|
},
|
||||||
|
check(C1)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
check(C) ->
|
||||||
|
{_Mappings, Checked} = emqx_config:check_config(emqx_schema, ?CONF(C)),
|
||||||
|
Checked.
|
|
@ -0,0 +1,135 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_authn_schema_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
%% schema error
|
||||||
|
-define(ERR(Reason), {error, Reason}).
|
||||||
|
|
||||||
|
union_member_selector_mongo_test_() ->
|
||||||
|
Check = fun(Txt) -> check(emqx_authn_mongodb, Txt) end,
|
||||||
|
[
|
||||||
|
{"unknown", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{field_name := mongo_type, expected := _}),
|
||||||
|
Check("{mongo_type: foobar}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"single", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-mongodb:standalone"}),
|
||||||
|
Check("{mongo_type: single}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"replica-set", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-mongodb:replica-set"}),
|
||||||
|
Check("{mongo_type: rs}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"sharded", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-mongodb:sharded-cluster"}),
|
||||||
|
Check("{mongo_type: sharded}")
|
||||||
|
)
|
||||||
|
end}
|
||||||
|
].
|
||||||
|
|
||||||
|
union_member_selector_jwt_test_() ->
|
||||||
|
Check = fun(Txt) -> check(emqx_authn_jwt, Txt) end,
|
||||||
|
[
|
||||||
|
{"unknown", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{field_name := use_jwks, expected := "true | false"}),
|
||||||
|
Check("{use_jwks = 1}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"jwks", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-jwt:jwks"}),
|
||||||
|
Check("{use_jwks = true}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"publick-key", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-jwt:public-key"}),
|
||||||
|
Check("{use_jwks = false, public_key = 1}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"hmac-based", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-jwt:hmac-based"}),
|
||||||
|
Check("{use_jwks = false}")
|
||||||
|
)
|
||||||
|
end}
|
||||||
|
].
|
||||||
|
|
||||||
|
union_member_selector_redis_test_() ->
|
||||||
|
Check = fun(Txt) -> check(emqx_authn_redis, Txt) end,
|
||||||
|
[
|
||||||
|
{"unknown", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{field_name := redis_type, expected := _}),
|
||||||
|
Check("{redis_type = 1}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"single", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-redis:standalone"}),
|
||||||
|
Check("{redis_type = single}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"cluster", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-redis:cluster"}),
|
||||||
|
Check("{redis_type = cluster}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"sentinel", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-redis:sentinel"}),
|
||||||
|
Check("{redis_type = sentinel}")
|
||||||
|
)
|
||||||
|
end}
|
||||||
|
].
|
||||||
|
|
||||||
|
union_member_selector_http_test_() ->
|
||||||
|
Check = fun(Txt) -> check(emqx_authn_http, Txt) end,
|
||||||
|
[
|
||||||
|
{"unknown", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{field_name := method, expected := _}),
|
||||||
|
Check("{method = 1}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"get", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-http:get"}),
|
||||||
|
Check("{method = get}")
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"post", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
?ERR(#{matched_type := "authn-http:post"}),
|
||||||
|
Check("{method = post}")
|
||||||
|
)
|
||||||
|
end}
|
||||||
|
].
|
||||||
|
|
||||||
|
check(Module, HoconConf) ->
|
||||||
|
emqx_hocon:check(Module, ["authentication= ", HoconConf]).
|
|
@ -1,6 +1,6 @@
|
||||||
{application, emqx_conf, [
|
{application, emqx_conf, [
|
||||||
{description, "EMQX configuration management"},
|
{description, "EMQX configuration management"},
|
||||||
{vsn, "0.1.11"},
|
{vsn, "0.1.12"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{mod, {emqx_conf_app, []}},
|
{mod, {emqx_conf_app, []}},
|
||||||
{applications, [kernel, stdlib]},
|
{applications, [kernel, stdlib]},
|
||||||
|
|
|
@ -296,8 +296,6 @@ hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
|
||||||
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
||||||
{Schema, Refs} = hocon_schema_to_spec(Item, LocalModule),
|
{Schema, Refs} = hocon_schema_to_spec(Item, LocalModule),
|
||||||
{#{type => array, items => Schema}, Refs};
|
{#{type => array, items => Schema}, Refs};
|
||||||
hocon_schema_to_spec(?LAZY(Item), LocalModule) ->
|
|
||||||
hocon_schema_to_spec(Item, LocalModule);
|
|
||||||
hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
||||||
{#{type => enum, symbols => Items}, []};
|
{#{type => enum, symbols => Items}, []};
|
||||||
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
||||||
|
|
|
@ -609,8 +609,6 @@ hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
|
||||||
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
|
||||||
{Schema, Refs} = hocon_schema_to_spec(Item, LocalModule),
|
{Schema, Refs} = hocon_schema_to_spec(Item, LocalModule),
|
||||||
{#{type => array, items => Schema}, Refs};
|
{#{type => array, items => Schema}, Refs};
|
||||||
hocon_schema_to_spec(?LAZY(Item), LocalModule) ->
|
|
||||||
hocon_schema_to_spec(Item, LocalModule);
|
|
||||||
hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
|
||||||
{#{type => string, enum => Items}, []};
|
{#{type => string, enum => Items}, []};
|
||||||
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
|
||||||
|
|
2
bin/emqx
2
bin/emqx
|
@ -911,7 +911,7 @@ fi
|
||||||
if [ $IS_BOOT_COMMAND = 'yes' ] && [ "$COOKIE" = "$EMQX_DEFAULT_ERLANG_COOKIE" ]; then
|
if [ $IS_BOOT_COMMAND = 'yes' ] && [ "$COOKIE" = "$EMQX_DEFAULT_ERLANG_COOKIE" ]; then
|
||||||
logwarn "Default (insecure) Erlang cookie is in use."
|
logwarn "Default (insecure) Erlang cookie is in use."
|
||||||
logwarn "Configure node.cookie in $EMQX_ETC_DIR/emqx.conf or override from environment variable EMQX_NODE__COOKIE"
|
logwarn "Configure node.cookie in $EMQX_ETC_DIR/emqx.conf or override from environment variable EMQX_NODE__COOKIE"
|
||||||
logwarn "Use the same config value for all nodes in the cluster."
|
logwarn "NOTE: Use the same cookie for all nodes in the cluster."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
## check if OTP version has mnesia_hook feature; if not, fallback to
|
## check if OTP version has mnesia_hook feature; if not, fallback to
|
||||||
|
|
Loading…
Reference in New Issue