diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index f40e759f0..9c4c7d89a 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/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)). + 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 diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index b6b68eab9..ecd084c08 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/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). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 852139875..f8a278ee5 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/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). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index bcdc7b0b1..3820005af 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/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). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 8c9cb5333..cafcf1830 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/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).