feat: use union member selector for authn config schema

This commit is contained in:
Zaiming (Stone) Shi 2023-01-09 09:28:03 +01:00
parent c6a78cbfda
commit 6aaff6211f
5 changed files with 134 additions and 12 deletions

View File

@ -45,24 +45,105 @@ enable(desc) -> ?DESC(?FUNCTION_NAME);
enable(_) -> undefined.
authenticator_type() ->
hoconsc:union(config_refs([Module || {_AuthnType, Module} <- emqx_authn:providers()])).
hoconsc:union(union_member_selector(emqx_authn:providers())).
authenticator_type_without_scram() ->
Providers = lists:filtermap(
fun
({{password_based, _Backend}, Mod}) ->
{true, Mod};
({jwt, Mod}) ->
{true, Mod};
({{scram, _Backend}, _Mod}) ->
false
false;
(_) ->
true
end,
emqx_authn:providers()
),
hoconsc:union(config_refs(Providers)).
hoconsc:union(union_member_selector(Providers)).
config_refs(Modules) ->
lists:append([Module:refs() || Module <- Modules]).
config_refs(Providers) ->
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, Providers) ->
select_union_member(Value, Providers, #{});
select_union_member(_Value, _) ->
throw(#{hint => "missing 'mechanism' field"}).
select_union_member(Value, [], ReasonsMap) when ReasonsMap =:= #{} ->
BackendVal = maps:get(<<"backend">>, Value, undefined),
MechanismVal = maps:get(<<"mechanism">>, Value),
throw(#{
backend => BackendVal,
mechanism => MechanismVal,
hint => "unknown_mechanism_or_backend"
});
select_union_member(_Value, [], ReasonsMap) ->
throw(ReasonsMap);
select_union_member(Value, [Provider | Providers], ReasonsMap) ->
{Mechanism, Backend, Module} =
case Provider of
{{M, B}, Mod} -> {atom_to_binary(M), atom_to_binary(B), Mod};
{M, Mod} when is_atom(M) -> {atom_to_binary(M), undefined, Mod}
end,
case do_select_union_member(Mechanism, Backend, Module, Value) of
{ok, Type} ->
[Type];
{error, nomatch} ->
%% obvious mismatch, do not complain
%% e.g. when 'backend' is "http", but the value is "redis",
%% then there is no need to record the error like
%% "'http' is exepcted but got 'redis'"
select_union_member(Value, Providers, ReasonsMap);
{error, Reason} ->
%% more interesting error message
%% e.g. when 'backend' is "http", but there is no "method" field
%% found so there is no way to tell if it's the 'get' type or 'post' type.
%% hence the error message is like:
%% #{emqx_auth_http => "'http' auth backend must have get|post as 'method'"}
select_union_member(Value, Providers, ReasonsMap#{Module => Reason})
end.
do_select_union_member(Mechanism, Backend, Module, Value) ->
BackendVal = maps:get(<<"backend">>, Value, undefined),
MechanismVal = maps:get(<<"mechanism">>, Value),
case MechanismVal =:= Mechanism of
true when Backend =:= undefined ->
case BackendVal =:= undefined of
true ->
%% e.g. jwt has no 'backend'
try_select_union_member(Module, Value);
false ->
{error, "unexpected 'backend' for " ++ binary_to_list(Mechanism)}
end;
true ->
case Backend =:= BackendVal of
true ->
try_select_union_member(Module, Value);
false ->
%% 'backend' not matching
{error, nomatch}
end;
false ->
%% 'mechanism' not matching
{error, nomatch}
end.
try_select_union_member(Module, Value) ->
try
%% some modules have refs/1 exported to help selectin the sub-types
%% emqx_authn_http, emqx_authn_jwt, emqx_authn_mongodb and emqx_authn_redis
Module:refs(Value)
catch
error:undef ->
%% otherwise expect only one member from this module
[Type] = Module:refs(),
{ok, Type}
end.
%% authn is a core functionality however implemented outside of emqx app
%% in emqx_schema, 'authentication' is a map() type which is to allow

View File

@ -40,6 +40,7 @@
-export([
refs/0,
refs/1,
create/2,
update/2,
authenticate/2,
@ -66,12 +67,12 @@ roots() ->
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}
] ++ common_fields();
fields(post) ->
[
{method, #{type => post, required => true, default => post, desc => ?DESC(method)}},
{method, #{type => post, required => true, desc => ?DESC(method)}},
{headers, fun headers/1}
] ++ common_fields().
@ -159,6 +160,13 @@ refs() ->
hoconsc:ref(?MODULE, post)
].
refs(#{<<"method">> := <<"get">>}) ->
{ok, hoconsc:ref(?MODULE, get)};
refs(#{<<"method">> := <<"post">>}) ->
{ok, hoconsc:ref(?MODULE, post)};
refs(_) ->
{error, "'http' auth backend must have get|post as 'method'"}.
create(_AuthenticatorID, Config) ->
create(Config).

View File

@ -21,7 +21,6 @@
-include_lib("hocon/include/hoconsc.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([
namespace/0,
@ -33,6 +32,7 @@
-export([
refs/0,
refs/1,
create/2,
update/2,
authenticate/2,
@ -169,6 +169,19 @@ refs() ->
hoconsc:ref(?MODULE, 'jwks')
].
refs(#{<<"mechanism">> := <<"jwt">>} = V) ->
UseJWKS = maps:get(<<"use_jwks">>, V, undefined),
select_ref(UseJWKS, V).
select_ref(true, _) ->
{ok, hoconsc:ref(?MODULE, 'jwks')};
select_ref(false, #{<<"public_key">> := _}) ->
{ok, hoconsc:ref(?MODULE, 'public-key')};
select_ref(false, _) ->
{ok, hoconsc:ref(?MODULE, 'hmac-based')};
select_ref(_, _) ->
{error, "use_jwks must be set"}.
create(_AuthenticatorID, Config) ->
create(Config).

View File

@ -33,6 +33,7 @@
-export([
refs/0,
refs/1,
create/2,
update/2,
authenticate/2,
@ -130,6 +131,15 @@ refs() ->
hoconsc:ref(?MODULE, 'sharded-cluster')
].
refs(#{<<"mongo_type">> := <<"single">>}) ->
{ok, hoconsc:ref(?MODULE, standalone)};
refs(#{<<"mongo_type">> := <<"rs">>}) ->
{ok, hoconsc:ref(?MODULE, 'replica-set')};
refs(#{<<"mongo_type">> := <<"sharded">>}) ->
{ok, hoconsc:ref(?MODULE, 'sharded-cluster')};
refs(_) ->
{error, "unknown 'mongo_type'"}.
create(_AuthenticatorID, Config) ->
create(Config).

View File

@ -33,6 +33,7 @@
-export([
refs/0,
refs/1,
create/2,
update/2,
authenticate/2,
@ -97,6 +98,15 @@ refs() ->
hoconsc:ref(?MODULE, sentinel)
].
refs(#{<<"redis_type">> := <<"single">>}) ->
{ok, hoconsc:ref(?MODULE, standalone)};
refs(#{<<"redis_type">> := <<"cluster">>}) ->
{ok, hoconsc:ref(?MODULE, cluster)};
refs(#{<<"redis_type">> := <<"sentinel">>}) ->
{ok, hoconsc:ref(?MODULE, sentinel)};
refs(_) ->
{error, "unknown 'redis_type'"}.
create(_AuthenticatorID, Config) ->
create(Config).