Prechádzať zdrojové kódy

feat: use union member selector for authn config schema

Zaiming (Stone) Shi 3 rokov pred
rodič
commit
6aaff6211f

+ 91 - 10
apps/emqx_authn/src/emqx_authn_schema.erl

@@ -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)).
-
-config_refs(Modules) ->
-    lists:append([Module:refs() || Module <- Modules]).
+    hoconsc:union(union_member_selector(Providers)).
+
+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

+ 10 - 2
apps/emqx_authn/src/simple_authn/emqx_authn_http.erl

@@ -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).
 

+ 14 - 1
apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl

@@ -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).
 

+ 10 - 0
apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl

@@ -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).
 

+ 10 - 0
apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl

@@ -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).