diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index 3e69ae46d..7c1bc711c 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,13 +1,11 @@ emqx_authn: { enable: false authenticators: [ - # { - # name: "authenticator1" - # mechanism: password-based - # config: { - # server_type: built-in-database - # user_id_type: clientid - # } - # } + # { + # name: "authenticator1" + # mechanism: password-based + # server_type: built-in-database + # user_id_type: clientid + # } ] } diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index bb353348f..5a8bfb66a 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -17,21 +17,20 @@ -define(APP, emqx_authn). -define(CHAIN, <<"mqtt">>). --type chain_id() :: binary(). --type authenticator_name() :: binary(). --type mechanism() :: 'password-based' | jwt | scram. +-define(VER_1, <<"1">>). +-define(VER_2, <<"2">>). -record(authenticator, - { name :: authenticator_name() - , mechanism :: mechanism() + { id :: binary() + , name :: binary() , provider :: module() , config :: map() , state :: map() }). -record(chain, - { id :: chain_id() - , authenticators :: [{authenticator_name(), #authenticator{}}] + { id :: binary() + , authenticators :: [{binary(), binary(), #authenticator{}}] , created_at :: integer() }). diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 731ac31fe..f41921e04 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -31,10 +31,9 @@ , create_authenticator/2 , delete_authenticator/2 , update_authenticator/3 + , update_or_create_authenticator/3 , lookup_authenticator/2 , list_authenticators/1 - , move_authenticator_to_the_front/2 - , move_authenticator_to_the_end/2 , move_authenticator_to_the_nth/3 ]). @@ -95,7 +94,7 @@ authenticate(Credential, _AuthResult) -> do_authenticate([], _) -> {stop, {error, not_authorized}}; -do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], Credential) -> +do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | More], Credential) -> case Provider:authenticate(Credential, State) of ignore -> do_authenticate(More, Credential); @@ -130,7 +129,7 @@ delete_chain(ID) -> [] -> {error, {not_found, {chain, ID}}}; [#chain{authenticators = Authenticators}] -> - _ = [do_delete_authenticator(Authenticator) || {_, Authenticator} <- Authenticators], + _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators], mnesia:delete(?CHAIN_TAB, ID, write) end end). @@ -147,25 +146,21 @@ list_chains() -> Chains = ets:tab2list(?CHAIN_TAB), {ok, [serialize_chain(Chain) || Chain <- Chains]}. -create_authenticator(ChainID, #{name := Name, - mechanism := Mechanism, - config := Config}) -> +create_authenticator(ChainID, #{name := Name} = Config) -> UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case lists:keymember(Name, 1, Authenticators) of + case lists:keymember(Name, 2, Authenticators) of true -> - {error, {already_exists, {authenticator, Name}}}; + {error, name_has_be_used}; false -> - Provider = authenticator_provider(Mechanism, Config), - case Provider:create(ChainID, Name, Config) of - {ok, State} -> - Authenticator = #authenticator{name = Name, - mechanism = Mechanism, - provider = Provider, - config = Config, - state = State}, - NChain = Chain#chain{authenticators = Authenticators ++ [{Name, Authenticator}]}, - ok = mnesia:write(?CHAIN_TAB, NChain, write), + AlreadyExist = fun(ID) -> + lists:keymember(ID, 1, Authenticators) + end, + AuthenticatorID = gen_id(AlreadyExist), + case do_create_authenticator(ChainID, AuthenticatorID, Config) of + {ok, Authenticator} -> + NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}], + ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), {ok, serialize_authenticator(Authenticator)}; {error, Reason} -> {error, Reason} @@ -174,12 +169,12 @@ create_authenticator(ChainID, #{name := Name, end, update_chain(ChainID, UpdateFun). -delete_authenticator(ChainID, AuthenticatorName) -> +delete_authenticator(ChainID, AuthenticatorID) -> UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case lists:keytake(AuthenticatorName, 1, Authenticators) of + case lists:keytake(AuthenticatorID, 1, Authenticators) of false -> - {error, {not_found, {authenticator, AuthenticatorName}}}; - {value, {_, Authenticator}, NAuthenticators} -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {value, {_, _, Authenticator}, NAuthenticators} -> _ = do_delete_authenticator(Authenticator), NChain = Chain#chain{authenticators = NAuthenticators}, mnesia:write(?CHAIN_TAB, NChain, write) @@ -187,38 +182,86 @@ delete_authenticator(ChainID, AuthenticatorName) -> end, update_chain(ChainID, UpdateFun). -update_authenticator(ChainID, AuthenticatorName, Config) -> +update_authenticator(ChainID, AuthenticatorID, Config) -> + do_update_authenticator(ChainID, AuthenticatorID, Config, false). + +update_or_create_authenticator(ChainID, AuthenticatorID, Config) -> + do_update_authenticator(ChainID, AuthenticatorID, Config, true). + +do_update_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) -> UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case proplists:get_value(AuthenticatorName, Authenticators, undefined) of - undefined -> - {error, {not_found, {authenticator, AuthenticatorName}}}; - #authenticator{provider = Provider, - config = OriginalConfig, - state = State} = Authenticator -> - NewConfig = maps:merge(OriginalConfig, Config), - case Provider:update(ChainID, AuthenticatorName, NewConfig, State) of - {ok, NState} -> - NAuthenticator = Authenticator#authenticator{config = NewConfig, - state = NState}, - NAuthenticators = update_value(AuthenticatorName, NAuthenticator, Authenticators), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), - {ok, serialize_authenticator(NAuthenticator)}; - {error, Reason} -> - {error, Reason} + case lists:keytake(AuthenticatorID, 1, Authenticators) of + false -> + case CreateWhenNotFound of + true -> + case lists:keymember(NewName, 2, Authenticators) of + true -> + {error, name_has_be_used}; + false -> + case do_create_authenticator(ChainID, AuthenticatorID, Config) of + {ok, Authenticator} -> + NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}], + ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end; + false -> + {error, {not_found, {authenticator, AuthenticatorID}}} + end; + {value, + {_, _, #authenticator{provider = Provider, + state = #{version := Version} = State} = Authenticator}, + Others} -> + case lists:keymember(NewName, 2, Others) of + true -> + {error, name_has_be_used}; + false -> + case (NewProvider = authenticator_provider(Config)) =:= Provider of + true -> + Unique = <>, + case Provider:update(Config#{'_unique' => Unique}, State) of + {ok, NewState} -> + NewAuthenticator = Authenticator#authenticator{name = NewName, + config = Config, + state = switch_version(NewState)}, + NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), + ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), + {ok, serialize_authenticator(NewAuthenticator)}; + {error, Reason} -> + {error, Reason} + end; + false -> + Unique = <>, + case NewProvider:create(Config#{'_unique' => Unique}) of + {ok, NewState} -> + NewAuthenticator = Authenticator#authenticator{name = NewName, + provider = NewProvider, + config = Config, + state = switch_version(NewState)}, + NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), + ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), + _ = Provider:destroy(State), + {ok, serialize_authenticator(NewAuthenticator)}; + {error, Reason} -> + {error, Reason} + end + end end end - end, + end, update_chain(ChainID, UpdateFun). -lookup_authenticator(ChainID, AuthenticatorName) -> +lookup_authenticator(ChainID, AuthenticatorID) -> case mnesia:dirty_read(?CHAIN_TAB, ChainID) of [] -> {error, {not_found, {chain, ChainID}}}; [#chain{authenticators = Authenticators}] -> - case proplists:get_value(AuthenticatorName, Authenticators, undefined) of - undefined -> - {error, {not_found, {authenticator, AuthenticatorName}}}; - Authenticator -> + case lists:keyfind(AuthenticatorID, 1, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {_, _, Authenticator} -> {ok, serialize_authenticator(Authenticator)} end end. @@ -231,9 +274,9 @@ list_authenticators(ChainID) -> {ok, serialize_authenticators(Authenticators)} end. -move_authenticator_to_the_front(ChainID, AuthenticatorName) -> +move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) -> UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case move_authenticator_to_the_front_(AuthenticatorName, Authenticators) of + case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) of {ok, NAuthenticators} -> NChain = Chain#chain{authenticators = NAuthenticators}, mnesia:write(?CHAIN_TAB, NChain, write); @@ -243,108 +286,94 @@ move_authenticator_to_the_front(ChainID, AuthenticatorName) -> end, update_chain(ChainID, UpdateFun). -move_authenticator_to_the_end(ChainID, AuthenticatorName) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case move_authenticator_to_the_end_(AuthenticatorName, Authenticators) of - {ok, NAuthenticators} -> - NChain = Chain#chain{authenticators = NAuthenticators}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). +import_users(ChainID, AuthenticatorID, Filename) -> + call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]). -move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) of - {ok, NAuthenticators} -> - NChain = Chain#chain{authenticators = NAuthenticators}, - mnesia:write(?CHAIN_TAB, NChain, write); - {error, Reason} -> - {error, Reason} - end - end, - update_chain(ChainID, UpdateFun). +add_user(ChainID, AuthenticatorID, UserInfo) -> + call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]). -import_users(ChainID, AuthenticatorName, Filename) -> - call_authenticator(ChainID, AuthenticatorName, import_users, [Filename]). +delete_user(ChainID, AuthenticatorID, UserID) -> + call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]). -add_user(ChainID, AuthenticatorName, UserInfo) -> - call_authenticator(ChainID, AuthenticatorName, add_user, [UserInfo]). +update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) -> + call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]). -delete_user(ChainID, AuthenticatorName, UserID) -> - call_authenticator(ChainID, AuthenticatorName, delete_user, [UserID]). +lookup_user(ChainID, AuthenticatorID, UserID) -> + call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]). -update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) -> - call_authenticator(ChainID, AuthenticatorName, update_user, [UserID, NewUserInfo]). - -lookup_user(ChainID, AuthenticatorName, UserID) -> - call_authenticator(ChainID, AuthenticatorName, lookup_user, [UserID]). - -list_users(ChainID, AuthenticatorName) -> - call_authenticator(ChainID, AuthenticatorName, list_users, []). +list_users(ChainID, AuthenticatorID) -> + call_authenticator(ChainID, AuthenticatorID, list_users, []). %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ -authenticator_provider('password-based', #{server_type := 'built-in-database'}) -> +authenticator_provider(#{mechanism := 'password-based', server_type := 'built-in-database'}) -> emqx_authn_mnesia; -authenticator_provider('password-based', #{server_type := 'mysql'}) -> +authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'}) -> emqx_authn_mysql; -authenticator_provider('password-based', #{server_type := 'pgsql'}) -> +authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) -> emqx_authn_pgsql; -authenticator_provider('password-based', #{server_type := 'http-server'}) -> +authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) -> emqx_authn_http; -authenticator_provider(jwt, _) -> +authenticator_provider(#{mechanism := jwt}) -> emqx_authn_jwt; -authenticator_provider(scram, #{server_type := 'built-in-database'}) -> +authenticator_provider(#{mechanism := scram, server_type := 'built-in-database'}) -> emqx_enhanced_authn_scram_mnesia. +gen_id(AlreadyExist) -> + ID = list_to_binary(emqx_rule_id:gen()), + case AlreadyExist(ID) of + true -> gen_id(AlreadyExist); + false -> ID + end. + +switch_version(State = #{version := ?VER_1}) -> + State#{version := ?VER_2}; +switch_version(State = #{version := ?VER_2}) -> + State#{version := ?VER_1}; +switch_version(State) -> + State#{version => ?VER_1}. + +do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> + Provider = authenticator_provider(Config), + Unique = <>, + case Provider:create(Config#{'_unique' => Unique}) of + {ok, State} -> + Authenticator = #authenticator{id = AuthenticatorID, + name = Name, + provider = Provider, + config = Config, + state = switch_version(State)}, + {ok, Authenticator}; + {error, Reason} -> + {error, Reason} + end. + do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> - Provider:destroy(State). + _ = Provider:destroy(State), + ok. -update_value(Key, Value, List) -> - lists:keyreplace(Key, 1, List, {Key, Value}). +replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) -> + lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}). -move_authenticator_to_the_front_(AuthenticatorName, Authenticators) -> - move_authenticator_to_the_front_(AuthenticatorName, Authenticators, []). - -move_authenticator_to_the_front_(AuthenticatorName, [], _) -> - {error, {not_found, {authenticator, AuthenticatorName}}}; -move_authenticator_to_the_front_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) -> - {ok, [Authenticator | (lists:reverse(Passed) ++ More)]}; -move_authenticator_to_the_front_(AuthenticatorName, [Authenticator | More], Passed) -> - move_authenticator_to_the_front_(AuthenticatorName, More, [Authenticator | Passed]). - -move_authenticator_to_the_end_(AuthenticatorName, Authenticators) -> - move_authenticator_to_the_end_(AuthenticatorName, Authenticators, []). - -move_authenticator_to_the_end_(AuthenticatorName, [], _) -> - {error, {not_found, {authenticator, AuthenticatorName}}}; -move_authenticator_to_the_end_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) -> - {ok, lists:reverse(Passed) ++ More ++ [Authenticator]}; -move_authenticator_to_the_end_(AuthenticatorName, [Authenticator | More], Passed) -> - move_authenticator_to_the_end_(AuthenticatorName, More, [Authenticator | Passed]). - -move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) +move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) when N =< length(Authenticators) andalso N > 0 -> - move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N, []); + move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N, []); move_authenticator_to_the_nth_(_, _, _) -> {error, out_of_range}. -move_authenticator_to_the_nth_(AuthenticatorName, [], _, _) -> - {error, {not_found, {authenticator, AuthenticatorName}}}; -move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) +move_authenticator_to_the_nth_(AuthenticatorID, [], _, _) -> + {error, {not_found, {authenticator, AuthenticatorID}}}; +move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed) when N =< length(Passed) -> {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), {ok, L1 ++ [Authenticator] ++ L2 ++ More}; -move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) -> +move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed) -> {L1, L2} = lists:split(N - length(Passed) - 1, More), {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2}; -move_authenticator_to_the_nth_(AuthenticatorName, [Authenticator | More], N, Passed) -> - move_authenticator_to_the_nth_(AuthenticatorName, More, N, [Authenticator | Passed]). +move_authenticator_to_the_nth_(AuthenticatorID, [Authenticator | More], N, Passed) -> + move_authenticator_to_the_nth_(AuthenticatorID, More, N, [Authenticator | Passed]). update_chain(ChainID, UpdateFun) -> trans( @@ -357,24 +386,15 @@ update_chain(ChainID, UpdateFun) -> end end). -% lookup_chain_by_listener(ListenerID, AuthNType) -> -% case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of -% [] -> -% {error, not_found}; -% [#binding{chain_id = ChainID}] -> -% {ok, ChainID} -% end. - - -call_authenticator(ChainID, AuthenticatorName, Func, Args) -> +call_authenticator(ChainID, AuthenticatorID, Func, Args) -> case mnesia:dirty_read(?CHAIN_TAB, ChainID) of [] -> {error, {not_found, {chain, ChainID}}}; [#chain{authenticators = Authenticators}] -> - case proplists:get_value(AuthenticatorName, Authenticators, undefined) of - undefined -> - {error, {not_found, {authenticator, AuthenticatorName}}}; - #authenticator{provider = Provider, state = State} -> + case lists:keyfind(AuthenticatorID, 1, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {_, _, #authenticator{provider = Provider, state = State}} -> case erlang:function_exported(Provider, Func, length(Args) + 1) of true -> erlang:apply(Provider, Func, Args ++ [State]); @@ -391,20 +411,12 @@ serialize_chain(#chain{id = ID, authenticators => serialize_authenticators(Authenticators), created_at => CreatedAt}. -% serialize_binding(#binding{bound = {ListenerID, _}, -% chain_id = ChainID}) -> -% #{listener_id => ListenerID, -% chain_id => ChainID}. - serialize_authenticators(Authenticators) -> - [serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators]. + [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators]. -serialize_authenticator(#authenticator{name = Name, - mechanism = Mechanism, +serialize_authenticator(#authenticator{id = ID, config = Config}) -> - #{name => Name, - mechanism => Mechanism, - config => Config}. + Config#{id => ID}. trans(Fun) -> trans(Fun, []). diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index c875cc717..1c4d0575d 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -16,338 +16,1106 @@ -module(emqx_authn_api). +-behavior(minirest_api). + -include("emqx_authn.hrl"). --export([ create_authenticator/2 - , delete_authenticator/2 - , update_authenticator/2 - , lookup_authenticator/2 - , list_authenticators/2 - , move_authenticator/2 +-export([ api_spec/0 + , authenticators/2 + , authenticators2/2 + , position/2 , import_users/2 - , add_user/2 - , delete_user/2 - , update_user/2 - , lookup_user/2 - , list_users/2 + , users/2 + , users2/2 ]). --rest_api(#{name => create_authenticator, - method => 'POST', - path => "/authentication/authenticators", - func => create_authenticator, - descr => "Create authenticator" - }). +-define(EXAMPLE_1, #{name => <<"example 1">>, + mechanism => <<"password-based">>, + server_type => <<"built-in-example">>, + user_id_type => <<"username">>, + password_hash_algorithm => #{ + name => <<"sha256">> + }}). --rest_api(#{name => delete_authenticator, - method => 'DELETE', - path => "/authentication/authenticators/:bin:name", - func => delete_authenticator, - descr => "Delete authenticator" - }). +-define(EXAMPLE_2, #{name => <<"example 2">>, + mechanism => <<"password-based">>, + server_type => <<"http-server">>, + method => <<"post">>, + url => <<"http://localhost:80/login">>, + headers => #{ + <<"content-type">> => <<"application/json">> + }, + form_data => #{ + <<"username">> => <<"${mqtt-username}">>, + <<"password">> => <<"${mqtt-password}">> + }}). --rest_api(#{name => update_authenticator, - method => 'PUT', - path => "/authentication/authenticators/:bin:name", - func => update_authenticator, - descr => "Update authenticator" - }). +-define(EXAMPLE_3, #{name => <<"example 3">>, + mechanism => <<"jwt">>, + use_jwks => false, + algorithm => <<"hmac-based">>, + secret => <<"mysecret">>, + secret_base64_encoded => false, + verify_claims => #{ + <<"username">> => <<"${mqtt-username}">> + }}). --rest_api(#{name => lookup_authenticator, - method => 'GET', - path => "/authentication/authenticators/:bin:name", - func => lookup_authenticator, - descr => "Lookup authenticator" - }). +-define(ERR_RESPONSE(Desc), #{description => Desc, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>), + examples => #{ + example1 => #{ + summary => <<"Not Found">>, + value => #{code => <<"NOT_FOUND">>, message => <<"Authenticator '67e4c9d3' does not exist">>} + }, + example2 => #{ + summary => <<"Conflict">>, + value => #{code => <<"ALREADY_EXISTS">>, message => <<"Name has be used">>} + }, + example3 => #{ + summary => <<"Bad Request 1">>, + value => #{code => <<"OUT_OF_RANGE">>, message => <<"Out of range">>} + } + }}}}). --rest_api(#{name => list_authenticators, - method => 'GET', - path => "/authentication/authenticators", - func => list_authenticators, - descr => "List authenticators" - }). +api_spec() -> + {[ authenticators_api() + , authenticators_api2() + , position_api() + , import_users_api() + , users_api() + , users2_api() + ], definitions()}. --rest_api(#{name => move_authenticator, - method => 'POST', - path => "/authentication/authenticators/:bin:name/position", - func => move_authenticator, - descr => "Change the order of authenticators" - }). - --rest_api(#{name => import_users, - method => 'POST', - path => "/authentication/authenticators/:bin:name/import-users", - func => import_users, - descr => "Import users" - }). - --rest_api(#{name => add_user, - method => 'POST', - path => "/authentication/authenticators/:bin:name/users", - func => add_user, - descr => "Add user" - }). - --rest_api(#{name => delete_user, - method => 'DELETE', - path => "/authentication/authenticators/:bin:name/users/:bin:user_id", - func => delete_user, - descr => "Delete user" - }). - --rest_api(#{name => update_user, - method => 'PUT', - path => "/authentication/authenticators/:bin:name/users/:bin:user_id", - func => update_user, - descr => "Update user" - }). - --rest_api(#{name => lookup_user, - method => 'GET', - path => "/authentication/authenticators/:bin:name/users/:bin:user_id", - func => lookup_user, - descr => "Lookup user" - }). - -%% TODO: Support pagination --rest_api(#{name => list_users, - method => 'GET', - path => "/authentication/authenticators/:bin:name/users", - func => list_users, - descr => "List all users" - }). - -create_authenticator(Binding, Params) -> - do_create_authenticator(uri_decode(Binding), lists_to_map(Params)). - -do_create_authenticator(_Binding, Authenticator0) -> - Config = #{<<"emqx_authn">> => #{ - <<"authenticators">> => [Authenticator0] - }}, - #{emqx_authn := #{authenticators := [Authenticator1]}} - = hocon_schema:check_plain(emqx_authn_schema, Config, - #{atom_key => true, nullable => true}), - case emqx_authn:create_authenticator(?CHAIN, Authenticator1) of - {ok, Authenticator2} -> - return({ok, Authenticator2}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -delete_authenticator(Binding, Params) -> - do_delete_authenticator(uri_decode(Binding), maps:from_list(Params)). - -do_delete_authenticator(#{name := Name}, _Params) -> - case emqx_authn:delete_authenticator(?CHAIN, Name) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -%% TODO: Support incremental update -update_authenticator(Binding, Params) -> - do_update_authenticator(uri_decode(Binding), lists_to_map(Params)). - -%% TOOD: PUT method supports creation and update -do_update_authenticator(#{name := Name}, NewConfig0) -> - case emqx_authn:lookup_authenticator(?CHAIN, Name) of - {ok, #{mechanism := Mechanism}} -> - Authenticator = #{<<"name">> => Name, - <<"mechanism">> => Mechanism, - <<"config">> => NewConfig0}, - Config = #{<<"emqx_authn">> => #{ - <<"authenticators">> => [Authenticator] - }}, - #{ - emqx_authn := #{ - authenticators := [#{ - config := NewConfig1 - }] +authenticators_api() -> + Metadata = #{ + post => #{ + description => "Create authenticator", + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"authenticator">>), + examples => #{ + default => #{ + summary => <<"Default">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + http => #{ + summary => <<"Authentication provided by HTTP Server">>, + value => emqx_json:encode(?EXAMPLE_2) + }, + jwt => #{ + summary => <<"JWT Authentication">>, + value => emqx_json:encode(?EXAMPLE_3) + } + } + } } - } = hocon_schema:check_plain(emqx_authn_schema, Config, - #{atom_key => true, nullable => true}), - case emqx_authn:update_authenticator(?CHAIN, Name, NewConfig1) of - {ok, NAuthenticator} -> - return({ok, NAuthenticator}); - {error, Reason} -> - return(serialize_error(Reason)) - end; + }, + responses => #{ + <<"201">> => #{ + description => <<"Created">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"returned_authenticator">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) + } + } + } + } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) + } + }, + get => #{ + description => "List authenticators", + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => minirest:ref(<<"returned_authenticator">>) + }, + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode([ maps:put(id, <<"example 1">>, ?EXAMPLE_1) + , maps:put(id, <<"example 2">>, ?EXAMPLE_2) + , maps:put(id, <<"example 3">>, ?EXAMPLE_3) + ]) + } + } + } + } + } + } + } + }, + {"/authentication/authenticators", Metadata, authenticators}. + +authenticators_api2() -> + Metadata = #{ + get => #{ + description => "Get authenicator by id", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"returned_authenticator">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) + } + } + } + } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }, + put => #{ + description => "Update authenticator", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + oneOf => [ minirest:ref(<<"password_based">>) + , minirest:ref(<<"jwt">>) + , minirest:ref(<<"scram">>) + ] + }, + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?EXAMPLE_2) + } + } + } + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"returned_authenticator">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) + } + } + } + } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) + } + }, + delete => #{ + description => "Delete authenticator", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + } + }, + {"/authentication/authenticators/:id", Metadata, authenticators2}. + +position_api() -> + Metadata = #{ + post => #{ + description => "Change the order of authenticators", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => integer, + example => 1 + } + } + } + } + } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + } + }, + {"/authentication/authenticators/:id/position", Metadata, position}. + +import_users_api() -> + Metadata = #{ + post => #{ + description => "Import users from json/csv file", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [filename], + properties => #{ + filename => #{ + type => string + } + } + } + } + } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + } + }, + {"/authentication/authenticators/:id/import-users", Metadata, import_users}. + +users_api() -> + Metadata = #{ + post => #{ + description => "Add user", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [user_id, password], + properties => #{ + user_id => #{ + type => string + }, + password => #{ + type => string + } + } + } + } + } + }, + responses => #{ + <<"201">> => #{ + description => <<"Created">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [user_id], + properties => #{ + user_id => #{ + type => string + } + } + } + } + } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }, + get => #{ + description => "List users", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ + type => object, + required => [user_id], + properties => #{ + user_id => #{ + type => string + } + } + } + } + } + } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + } + }, + {"/authentication/authenticators/:id/users", Metadata, users}. + +users2_api() -> + Metadata = #{ + patch => #{ + description => "Update user", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [password], + properties => #{ + password => #{ + type => string + } + } + } + } + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ + type => object, + required => [user_id], + properties => #{ + user_id => #{ + type => string + } + } + } + } + } + } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }, + get => #{ + description => "Get user info", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ + type => object, + required => [user_id], + properties => #{ + user_id => #{ + type => string + } + } + } + } + } + } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }, + delete => #{ + description => "Delete user", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + } + }, + {"/authentication/authenticators/:id/users/:user_id", Metadata, users2}. + + +definitions() -> + AuthenticatorDef = #{ + oneOf => [ minirest:ref(<<"password_based">>) + , minirest:ref(<<"jwt">>) + , minirest:ref(<<"scram">>) + ] + }, + + ReturnedAuthenticatorDef = #{ + allOf => [ + #{ + type => object, + properties => #{ + id => #{ + type => string + } + } + }, + #{ + oneOf => [ minirest:ref(<<"password_based">>) + , minirest:ref(<<"jwt">>) + , minirest:ref(<<"scram">>) + ] + } + ] + }, + + PasswordBasedDef = #{ + allOf => [ + #{ + type => object, + required => [name, mechanism], + properties => #{ + name => #{ + type => string, + example => "exmaple" + }, + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + } + } + }, + #{ + oneOf => [ minirest:ref(<<"password_based_built_in_database">>) + , minirest:ref(<<"password_based_mysql">>) + , minirest:ref(<<"password_based_pgsql">>) + , minirest:ref(<<"password_based_http_server">>) + ] + } + ] + }, + + JWTDef = #{ + type => object, + required => [name, mechanism], + properties => #{ + name => #{ + type => string, + example => "exmaple" + }, + mechanism => #{ + type => string, + enum => [<<"jwt">>], + example => <<"jwt">> + }, + use_jwks => #{ + type => boolean, + default => false, + example => false + }, + algorithm => #{ + type => string, + enum => [<<"hmac-based">>, <<"public-key">>], + default => <<"hmac-based">>, + example => <<"hmac-based">> + }, + secret => #{ + type => string + }, + secret_base64_encoded => #{ + type => boolean, + default => false + }, + certificate => #{ + type => string + }, + verify_claims => #{ + type => object, + additionalProperties => #{ + type => string + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + + SCRAMDef = #{ + type => object, + required => [name, mechanism], + properties => #{ + name => #{ + type => string, + example => "exmaple" + }, + mechanism => #{ + type => string, + enum => [<<"scram">>], + example => <<"scram">> + }, + server_type => #{ + type => string, + enum => [<<"built-in-database">>], + default => <<"built-in-database">> + }, + algorithm => #{ + type => string, + enum => [<<"sha256">>, <<"sha512">>], + default => <<"sha256">> + }, + iteration_count => #{ + type => integer, + default => 4096 + } + } + }, + + PasswordBasedBuiltInDatabaseDef = #{ + type => object, + properties => #{ + server_type => #{ + type => string, + enum => [<<"built-in-database">>], + example => <<"built-in-database">> + }, + user_id_type => #{ + type => string, + enum => [<<"username">>, <<"clientid">>], + default => <<"username">>, + example => <<"username">> + }, + password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>) + } + }, + + PasswordBasedMySQLDef = #{ + type => object, + properties => #{ + server_type => #{ + type => string, + enum => [<<"mysql">>], + example => <<"mysql">> + }, + server => #{ + type => string, + example => <<"localhost:3306">> + }, + database => #{ + type => string + }, + pool_size => #{ + type => integer, + default => 8 + }, + username => #{ + type => string + }, + password => #{ + type => string + }, + auto_reconnect => #{ + type => boolean, + default => true + }, + ssl => minirest:ref(<<"ssl">>), + password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>), + salt_position => #{ + type => string, + enum => [<<"prefix">>, <<"suffix">>], + default => <<"prefix">> + }, + query => #{ + type => string, + example => <<"SELECT password_hash FROM mqtt_user WHERE username = ${mqtt-username}">> + }, + query_timeout => #{ + type => integer, + description => <<"Query timeout, Unit: Milliseconds">>, + default => 5000 + } + } + }, + + PasswordBasedPgSQLDef = #{ + type => object, + properties => #{ + server_type => #{ + type => string, + enum => [<<"pgsql">>], + example => <<"pgsql">> + }, + server => #{ + type => string, + example => <<"localhost:5432">> + }, + database => #{ + type => string + }, + pool_size => #{ + type => integer, + default => 8 + }, + username => #{ + type => string + }, + password => #{ + type => string + }, + auto_reconnect => #{ + type => boolean, + default => true + }, + password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>), + salt_position => #{ + type => string, + enum => [<<"prefix">>, <<"suffix">>], + default => <<"prefix">> + }, + query => #{ + type => string, + example => <<"SELECT password_hash FROM mqtt_user WHERE username = ${mqtt-username}">> + } + } + }, + + PasswordBasedHTTPServerDef = #{ + type => object, + properties => #{ + server_type => #{ + type => string, + enum => [<<"http-server">>], + example => <<"http-server">> + }, + method => #{ + type => string, + enum => [<<"get">>, <<"post">>], + default => <<"post">> + }, + url => #{ + type => string, + example => <<"http://localhost:80/login">> + }, + headers => #{ + type => object, + additionalProperties => #{ + type => string + } + }, + format_data => #{ + type => string + }, + connect_timeout => #{ + type => integer, + default => 5000 + }, + max_retries => #{ + type => integer, + default => 5 + }, + retry_interval => #{ + type => integer, + default => 1000 + }, + request_timout => #{ + type => integer, + default => 5000 + }, + pool_size => #{ + type => integer, + default => 8 + }, + enable_pipelining => #{ + type => boolean, + default => true + } + } + }, + + PasswordHashAlgorithmDef = #{ + type => object, + required => [name], + properties => #{ + name => #{ + type => string, + enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], + default => <<"sha256">> + }, + salt_rounds => #{ + type => integer, + default => 10 + } + } + }, + + SSLDef = #{ + type => object, + properties => #{ + enable => #{ + type => boolean, + default => false + }, + certfile => #{ + type => string + }, + keyfile => #{ + type => string + }, + cacertfile => #{ + type => string + }, + verify => #{ + type => boolean, + default => true + }, + server_name_indication => #{ + type => object, + properties => #{ + enable => #{ + type => boolean, + default => false + }, + hostname => #{ + type => string + } + } + } + } + }, + + ErrorDef = #{ + type => object, + properties => #{ + code => #{ + type => string, + enum => [<<"NOT_FOUND">>], + example => <<"NOT_FOUND">> + }, + message => #{ + type => string + } + } + }, + + [ #{<<"authenticator">> => AuthenticatorDef} + , #{<<"returned_authenticator">> => ReturnedAuthenticatorDef} + , #{<<"password_based">> => PasswordBasedDef} + , #{<<"jwt">> => JWTDef} + , #{<<"scram">> => SCRAMDef} + , #{<<"password_based_built_in_database">> => PasswordBasedBuiltInDatabaseDef} + , #{<<"password_based_mysql">> => PasswordBasedMySQLDef} + , #{<<"password_based_pgsql">> => PasswordBasedPgSQLDef} + , #{<<"password_based_http_server">> => PasswordBasedHTTPServerDef} + , #{<<"password_hash_algorithm">> => PasswordHashAlgorithmDef} + , #{<<"ssl">> => SSLDef} + , #{<<"error">> => ErrorDef} + ]. + +authenticators(post, Request) -> + {ok, Body, _} = cowboy_req:read_body(Request), + AuthenticatorConfig = emqx_json:decode(Body, [return_maps]), + Config = #{<<"emqx_authn">> => #{ + <<"authenticators">> => [AuthenticatorConfig] + }}, + NConfig = hocon_schema:check_plain(emqx_authn_schema, Config, + #{nullable => true}), + #{emqx_authn := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig), + case emqx_authn:create_authenticator(?CHAIN, NAuthenticatorConfig) of + {ok, Authenticator2} -> + {201, Authenticator2}; {error, Reason} -> - return(serialize_error(Reason)) - end. + serialize_error(Reason) + end; +authenticators(get, _Request) -> + {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN), + {200, Authenticators}. -lookup_authenticator(Binding, Params) -> - do_lookup_authenticator(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_authenticator(#{name := Name}, _Params) -> - case emqx_authn:lookup_authenticator(?CHAIN, Name) of +authenticators2(get, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of {ok, Authenticator} -> - return({ok, Authenticator}); + {200, Authenticator}; {error, Reason} -> - return(serialize_error(Reason)) + serialize_error(Reason) + end; +authenticators2(put, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + AuthenticatorConfig = emqx_json:decode(Body, [return_maps]), + Config = #{<<"emqx_authn">> => #{ + <<"authenticators">> => [AuthenticatorConfig] + }}, + NConfig = hocon_schema:check_plain(emqx_authn_schema, Config, + #{nullable => true}), + #{emqx_authn := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig), + case emqx_authn:update_or_create_authenticator(?CHAIN, AuthenticatorID, NAuthenticatorConfig) of + {ok, Authenticator} -> + {200, Authenticator}; + {error, Reason} -> + serialize_error(Reason) + end; +authenticators2(delete, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + case emqx_authn:delete_authenticator(?CHAIN, AuthenticatorID) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) end. -list_authenticators(Binding, Params) -> - do_list_authenticators(uri_decode(Binding), maps:from_list(Params)). - -do_list_authenticators(_Binding, _Params) -> - case emqx_authn:list_authenticators(?CHAIN) of - {ok, Authenticators} -> - return({ok, Authenticators}); +position(post, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + NBody = emqx_json:decode(Body, [return_maps]), + Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"position">> => NBody}, + #{nullable => true}, ["position"]), + #{position := #{position := Position}} = emqx_map_lib:unsafe_atom_key_map(Config), + case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of + ok -> + {204}; {error, Reason} -> - return(serialize_error(Reason)) + serialize_error(Reason) end. -move_authenticator(Binding, Params) -> - do_move_authenticator(uri_decode(Binding), maps:from_list(Params)). - -do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the front">>}) -> - case emqx_authn:move_authenticator_to_the_front(?CHAIN, Name) of +import_users(post, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + NBody = emqx_json:decode(Body, [return_maps]), + Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"filename">> => NBody}, + #{nullable => true}, ["filename"]), + #{filename := #{filename := Filename}} = emqx_map_lib:unsafe_atom_key_map(Config), + case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of ok -> - return(ok); + {204}; {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the end">>}) -> - case emqx_authn:move_authenticator_to_the_end(?CHAIN, Name) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_authenticator(#{name := Name}, #{<<"position">> := N}) when is_number(N) -> - case emqx_authn:move_authenticator_to_the_nth(?CHAIN, Name, N) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_move_authenticator(_Binding, _Params) -> - return(serialize_error({missing_parameter, <<"position">>})). + serialize_error(Reason) + end. -import_users(Binding, Params) -> - do_import_users(uri_decode(Binding), maps:from_list(Params)). - -do_import_users(#{name := Name}, - #{<<"filename">> := Filename}) -> - case emqx_authn:import_users(?CHAIN, Name, Filename) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -do_import_users(_Binding, Params) -> - Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]), - return(serialize_error({missing_parameter, Missed})). - -add_user(Binding, Params) -> - do_add_user(uri_decode(Binding), maps:from_list(Params)). - -do_add_user(#{name := Name}, UserInfo) -> - case emqx_authn:add_user(?CHAIN, Name, UserInfo) of +users(post, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + NBody = emqx_json:decode(Body, [return_maps]), + Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"user_info">> => NBody}, + #{nullable => true}, ["user_info"]), + #{user_info := UserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), + case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of {ok, User} -> - return({ok, User}); + {201, User}; {error, Reason} -> - return(serialize_error(Reason)) - end. - -delete_user(Binding, Params) -> - do_delete_user(uri_decode(Binding), maps:from_list(Params)). - -do_delete_user(#{name := Name, - user_id := UserID}, _Params) -> - case emqx_authn:delete_user(?CHAIN, Name, UserID) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -update_user(Binding, Params) -> - do_update_user(uri_decode(Binding), maps:from_list(Params)). - -do_update_user(#{name := Name, - user_id := UserID}, NewUserInfo) -> - case emqx_authn:update_user(?CHAIN, Name, UserID, NewUserInfo) of - {ok, User} -> - return({ok, User}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_user(Binding, Params) -> - do_lookup_user(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_user(#{name := Name, - user_id := UserID}, _Params) -> - case emqx_authn:lookup_user(?CHAIN, Name, UserID) of - {ok, User} -> - return({ok, User}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_users(Binding, Params) -> - do_list_users(uri_decode(Binding), maps:from_list(Params)). - -do_list_users(#{name := Name}, _Params) -> - case emqx_authn:list_users(?CHAIN, Name) of + serialize_error(Reason) + end; +users(get, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + case emqx_authn:list_users(?CHAIN, AuthenticatorID) of {ok, Users} -> - return({ok, Users}); + {200, Users}; {error, Reason} -> - return(serialize_error(Reason)) + serialize_error(Reason) end. -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ +users2(patch, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + UserID = cowboy_req:binding(user_id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + NBody = emqx_json:decode(Body, [return_maps]), + Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"new_user_info">> => NBody}, + #{nullable => true}, ["new_user_info"]), + #{new_user_info := NewUserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), + case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, NewUserInfo) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end; +users2(get, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + UserID = cowboy_req:binding(user_id, Request), + case emqx_authn:lookup_user(?CHAIN, AuthenticatorID, UserID) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end; +users2(delete, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + UserID = cowboy_req:binding(user_id, Request), + case emqx_authn:delete_user(?CHAIN, AuthenticatorID, UserID) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) + end. -uri_decode(Params) -> - maps:fold(fun(K, V, Acc) -> - Acc#{K => emqx_http_lib:uri_decode(V)} - end, #{}, Params). - -lists_to_map(L) -> - lists_to_map(L, #{}). - -lists_to_map([], Acc) -> - Acc; -lists_to_map([{K, V} | More], Acc) when is_list(V) -> - NV = lists_to_map(V), - lists_to_map(More, Acc#{K => NV}); -lists_to_map([{K, V} | More], Acc) -> - lists_to_map(More, Acc#{K => V}); -lists_to_map([_ | _] = L, _) -> - L. - -serialize_error({already_exists, {Type, ID}}) -> - {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))}; -serialize_error({not_found, {Type, ID}}) -> - {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))}; -serialize_error({duplicate, Name}) -> - {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Authenticator name '~s' is duplicated", [Name]))}; -serialize_error({missing_parameter, Names = [_ | Rest]}) -> - Format = ["~s," || _ <- Rest] ++ ["~s"], - NFormat = binary_to_list(iolist_to_binary(Format)), - {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))}; +serialize_error({not_found, {authenticator, ID}}) -> + {404, #{code => <<"NOT_FOUND">>, + message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}}; +serialize_error(name_has_be_used) -> + {409, #{code => <<"ALREADY_EXISTS">>, + message => <<"Name has be used">>}}; +serialize_error(out_of_range) -> + {400, #{code => <<"OUT_OF_RANGE">>, + message => <<"Out of range">>}}; serialize_error({missing_parameter, Name}) -> - {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))}; -serialize_error(_) -> - {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. - -serialize_type(authenticator) -> - "Authenticator". - -get_missed_params(Actual, Expected) -> - Keys = lists:foldl(fun(Key, Acc) -> - case maps:is_key(Key, Actual) of - true -> Acc; - false -> [Key | Acc] - end - end, [], Expected), - lists:reverse(Keys). - -return(_) -> -%% TODO: V5 API - ok. + {400, #{code => <<"MISSING_PARAMETER">>, + message => list_to_binary( + io_lib:format("The input parameter '~p' that is mandatory for processing this request is not supplied", [Name]) + )}}; +serialize_error(Reason) -> + {400, #{code => <<"BAD_REQUEST">>, + message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 225969cd2..a279ff5e7 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -42,16 +42,16 @@ initialize() -> authenticators => []}), initialize(AuthNConfig). -initialize(#{enable := Enable, authenticators := Authenticators}) -> +initialize(#{enable := Enable, authenticators := AuthenticatorsConfig}) -> {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), - initialize_authenticators(Authenticators), + initialize_authenticators(AuthenticatorsConfig), Enable =:= true andalso emqx_authn:enable(), ok. initialize_authenticators([]) -> ok; -initialize_authenticators([#{name := Name} = Authenticator | More]) -> - case emqx_authn:create_authenticator(?CHAIN, Authenticator) of +initialize_authenticators([#{name := Name} = AuthenticatorConfig | More]) -> + case emqx_authn:create_authenticator(?CHAIN, AuthenticatorConfig) of {ok, _} -> initialize_authenticators(More); {error, Reason} -> diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index 7ed5a9999..030867ed7 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -25,57 +25,34 @@ , fields/1 ]). --reflect_type([ authenticator_name/0 - ]). +-export([ authenticator_name/1 + ]). -structs() -> ["emqx_authn"]. +structs() -> [ "emqx_authn" ]. fields("emqx_authn") -> [ {enable, fun enable/1} , {authenticators, fun authenticators/1} - ]; - -fields('password-based') -> - [ {name, fun authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {config, hoconsc:t(hoconsc:union( - [ hoconsc:ref(emqx_authn_mnesia, config) - , hoconsc:ref(emqx_authn_mysql, config) - , hoconsc:ref(emqx_authn_pgsql, config) - , hoconsc:ref(emqx_authn_http, get) - , hoconsc:ref(emqx_authn_http, post) - ]))} - ]; - -fields(jwt) -> - [ {name, fun authenticator_name/1} - , {mechanism, {enum, [jwt]}} - , {config, hoconsc:t(hoconsc:union( - [ hoconsc:ref(emqx_authn_jwt, 'hmac-based') - , hoconsc:ref(emqx_authn_jwt, 'public-key') - , hoconsc:ref(emqx_authn_jwt, 'jwks') - ]))} - ]; - -fields(scram) -> - [ {name, fun authenticator_name/1} - , {mechanism, {enum, [scram]}} - , {config, hoconsc:t(hoconsc:union( - [ hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) - ]))} ]. +authenticator_name(type) -> binary(); +authenticator_name(nullable) -> false; +authenticator_name(_) -> undefined. + enable(type) -> boolean(); -enable(defualt) -> false; +enable(default) -> false; enable(_) -> undefined. authenticators(type) -> - hoconsc:array({union, [ hoconsc:ref(?MODULE, 'password-based') - , hoconsc:ref(?MODULE, jwt) - , hoconsc:ref(?MODULE, scram)]}); + hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config) + , hoconsc:ref(emqx_authn_mysql, config) + , hoconsc:ref(emqx_authn_pgsql, config) + , hoconsc:ref(emqx_authn_http, get) + , hoconsc:ref(emqx_authn_http, post) + , hoconsc:ref(emqx_authn_jwt, 'hmac-based') + , hoconsc:ref(emqx_authn_jwt, 'public-key') + , hoconsc:ref(emqx_authn_jwt, 'jwks') + , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) + ]}); authenticators(default) -> []; authenticators(_) -> undefined. - -authenticator_name(type) -> authenticator_name(); -authenticator_name(nullable) -> false; -authenticator_name(_) -> undefined. diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index d1d564bf3..52c301946 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -26,8 +26,8 @@ , fields/1 ]). --export([ create/3 - , update/4 +-export([ create/1 + , update/2 , authenticate/2 , destroy/1 ]). @@ -71,7 +71,9 @@ mnesia(copy) -> structs() -> [config]. fields(config) -> - [ {server_type, fun server_type/1} + [ {name, fun emqx_authn_schema:authenticator_name/1} + , {mechanism, {enum, [scram]}} + , {server_type, fun server_type/1} , {algorithm, fun algorithm/1} , {iteration_count, fun iteration_count/1} ]. @@ -80,7 +82,7 @@ server_type(type) -> hoconsc:enum(['built-in-database']); server_type(default) -> 'built-in-database'; server_type(_) -> undefined. -algorithm(type) -> hoconsc:enum([sha256, sha256]); +algorithm(type) -> hoconsc:enum([sha256, sha512]); algorithm(default) -> sha256; algorithm(_) -> undefined. @@ -92,16 +94,18 @@ iteration_count(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ -create(ChainID, Authenticator, #{algorithm := Algorithm, - iteration_count := IterationCount}) -> - State = #{user_group => {ChainID, Authenticator}, +create(#{ algorithm := Algorithm + , iteration_count := IterationCount + , '_unique' := Unique + }) -> + State = #{user_group => Unique, algorithm => Algorithm, iteration_count => IterationCount}, {ok, State}. -update(_ChainID, _Authenticator, _Config, _State) -> - {error, update_not_suppored}. - +update(Config, #{user_group := Unique}) -> + create(Config#{'_unique' => Unique}). + authenticate(#{auth_method := AuthMethod, auth_data := AuthData, auth_cache := AuthCache}, State) -> @@ -129,8 +133,8 @@ destroy(#{user_group := UserGroup}) -> end). %% TODO: binary to atom -add_user(#{<<"user_id">> := UserID, - <<"password">> := Password}, #{user_group := UserGroup} = State) -> +add_user(#{user_id := UserID, + password := Password}, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of @@ -153,7 +157,7 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{<<"password">> := Password}, +update_user(UserID, #{password := Password}, #{user_group := UserGroup} = State) -> trans( fun() -> 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 14240b578..964e78a02 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -26,15 +26,8 @@ , validations/0 ]). --type accept() :: 'application/json' | 'application/x-www-form-urlencoded'. --type content_type() :: accept(). - --reflect_type([ accept/0 - , content_type/0 - ]). - --export([ create/3 - , update/4 +-export([ create/1 + , update/2 , authenticate/2 , destroy/1 ]). @@ -53,45 +46,55 @@ fields("") -> fields(get) -> [ {method, #{type => get, - default => get}} + default => post}} + , {headers, fun headers_no_content_type/1} ] ++ common_fields(); fields(post) -> [ {method, #{type => post, - default => get}} - , {content_type, fun content_type/1} + default => post}} + , {headers, fun headers/1} ] ++ common_fields(). common_fields() -> - [ {server_type, {enum, ['http-server']}} + [ {name, fun emqx_authn_schema:authenticator_name/1} + , {mechanism, {enum, ['password-based']}} + , {server_type, {enum, ['http-server']}} , {url, fun url/1} - , {accept, fun accept/1} - , {headers, fun headers/1} , {form_data, fun form_data/1} , {request_timeout, fun request_timeout/1} - ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)). + ] ++ maps:to_list(maps:without([ base_url + , pool_type], + maps:from_list(emqx_connector_http:fields(config)))). validations() -> - [ {check_ssl_opts, fun check_ssl_opts/1} ]. + [ {check_ssl_opts, fun check_ssl_opts/1} + , {check_headers, fun check_headers/1} + ]. url(type) -> binary(); url(nullable) -> false; url(validate) -> [fun check_url/1]; url(_) -> undefined. -accept(type) -> accept(); -accept(default) -> 'application/json'; -accept(_) -> undefined. - -content_type(type) -> content_type(); -content_type(default) -> 'application/json'; -content_type(_) -> undefined. - -headers(type) -> list(); -headers(default) -> []; +headers(type) -> map(); +headers(converter) -> + fun(Headers) -> + maps:merge(default_headers(), transform_header_name(Headers)) + end; +headers(default) -> default_headers(); headers(_) -> undefined. -form_data(type) -> binary(); +headers_no_content_type(type) -> map(); +headers_no_content_type(converter) -> + fun(Headers) -> + maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) + end; +headers_no_content_type(default) -> default_headers_no_content_type(); +headers_no_content_type(_) -> undefined. + +%% TODO: Using map() +form_data(type) -> map(); form_data(nullable) -> false; form_data(validate) -> [fun check_form_data/1]; form_data(_) -> undefined. @@ -104,46 +107,41 @@ request_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ -create(ChainID, AuthenticatorName, - #{method := Method, - url := URL, - accept := Accept, - headers := Headers, - form_data := FormData, - request_timeout := RequestTimeout} = Config) -> - ContentType = maps:get(content_type, Config, undefined), - DefaultHeader0 = case ContentType of - undefined -> #{}; - _ -> #{<<"content-type">> => atom_to_binary(ContentType, utf8)} - end, - DefaultHeader = DefaultHeader0#{<<"accept">> => atom_to_binary(Accept, utf8)}, - NHeaders = maps:to_list(maps:merge(DefaultHeader, maps:from_list(Headers))), - NFormData = preprocess_form_data(FormData), +create(#{ method := Method + , url := URL + , headers := Headers + , form_data := FormData + , request_timeout := RequestTimeout + , '_unique' := Unique + } = Config) -> #{path := Path, query := Query} = URIMap = parse_url(URL), - BaseURL = generate_base_url(URIMap), - State = #{method => Method, - path => Path, - base_query => cow_qs:parse_qs(list_to_binary(Query)), - accept => Accept, - content_type => ContentType, - headers => NHeaders, - form_data => NFormData, - request_timeout => RequestTimeout}, - ResourceID = <>, - case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url => BaseURL}) of + State = #{ method => Method + , path => Path + , base_query => cow_qs:parse_qs(list_to_binary(Query)) + , headers => normalize_headers(Headers) + , form_data => maps:to_list(FormData) + , request_timeout => RequestTimeout + }, + case emqx_resource:create_local(Unique, + emqx_connector_http, + Config#{base_url => maps:remove(query, URIMap), + pool_type => random}) of {ok, _} -> - {ok, State#{resource_id => ResourceID}}; + {ok, State#{resource_id => Unique}}; {error, already_created} -> - {ok, State#{resource_id => ResourceID}}; + {ok, State#{resource_id => Unique}}; {error, Reason} -> {error, Reason} end. -update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = State) -> - case emqx_resource:update_local(ResourceID, emqx_connector_http, Config, []) of - {ok, _} -> {ok, State}; - {error, Reason} -> {error, Reason} +update(Config, State) -> + case create(Config) of + {ok, NewState} -> + ok = destroy(State), + {ok, NewState}; + {error, Reason} -> + {error, Reason} end. authenticate(#{auth_method := _}, _) -> @@ -182,26 +180,38 @@ check_url(URL) -> end. check_form_data(FormData) -> - KVs = binary:split(FormData, [<<"&">>], [global]), - case false =:= lists:any(fun(T) -> T =:= <<>> end, KVs) of - true -> - NKVs = [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs], - false =:= - lists:any(fun({K, V}) -> - K =:= <<>> orelse V =:= <<>>; - (_) -> - true - end, NKVs); - false -> - false - end. + lists:any(fun({_, V}) -> + not is_binary(V) + end, maps:to_list(FormData)). + +default_headers() -> + maps:put(<<"content-type">>, + <<"application/json">>, + default_headers_no_content_type()). + +default_headers_no_content_type() -> + #{ <<"accept">> => <<"application/json">> + , <<"cache-control">> => <<"no-cache">> + , <<"connection">> => <<"keep-alive">> + , <<"keep-alive">> => <<"timeout=5">> + }. + +transform_header_name(Headers) -> + maps:fold(fun(K0, V, Acc) -> + K = list_to_binary(string:to_lower(binary_to_list(K0))), + maps:put(K, V, Acc) + end, #{}, Headers). check_ssl_opts(Conf) -> emqx_connector_http:check_ssl_opts("url", Conf). -preprocess_form_data(FormData) -> - KVs = binary:split(FormData, [<<"&">>], [global]), - [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs]. +check_headers(Conf) -> + Method = hocon_schema:get_value("method", Conf), + Headers = hocon_schema:get_value("headers", Conf), + case Method =:= get andalso maps:get(<<"content-type">>, Headers, undefined) =/= undefined of + true -> false; + false -> true + end. parse_url(URL) -> {ok, URIMap} = emqx_http_lib:uri_parse(URL), @@ -212,15 +222,12 @@ parse_url(URL) -> URIMap end. -generate_base_url(#{scheme := Scheme, - host := Host, - port := Port}) -> - iolist_to_binary(io_lib:format("~p://~s:~p", [Scheme, Host, Port])). +normalize_headers(Headers) -> + [{atom_to_binary(K), V} || {K, V} <- maps:to_list(Headers)]. generate_request(Credential, #{method := Method, path := Path, base_query := BaseQuery, - content_type := ContentType, headers := Headers, form_data := FormData0}) -> FormData = replace_placeholders(FormData0, Credential), @@ -230,6 +237,7 @@ generate_request(Credential, #{method := Method, {NPath, Headers}; post -> NPath = append_query(Path, BaseQuery), + ContentType = proplists:get_value(<<"content-type">>, Headers), Body = serialize_body(ContentType, FormData), {NPath, Headers, Body} end. @@ -249,6 +257,8 @@ replace_placeholder(<<"${mqtt-username}">>, Credential) -> maps:get(username, Credential, undefined); replace_placeholder(<<"${mqtt-clientid}">>, Credential) -> maps:get(clientid, Credential, undefined); +replace_placeholder(<<"${mqtt-password}">>, Credential) -> + maps:get(password, Credential, undefined); replace_placeholder(<<"${ip-address}">>, Credential) -> maps:get(peerhost, Credential, undefined); replace_placeholder(<<"${cert-subject}">>, Credential) -> @@ -272,9 +282,9 @@ qs([], Acc) -> qs([{K, V} | More], Acc) -> qs(More, [["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] | Acc]). -serialize_body('application/json', FormData) -> +serialize_body(<<"application/json">>, FormData) -> emqx_json:encode(FormData); -serialize_body('application/x-www-form-urlencoded', FormData) -> +serialize_body(<<"application/x-www-form-urlencoded">>, FormData) -> qs(FormData). safely_parse_body(ContentType, Body) -> 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 437dac72d..fe034994e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -22,11 +22,10 @@ -export([ structs/0 , fields/1 - , validations/0 ]). --export([ create/3 - , update/4 +-export([ create/1 + , update/2 , authenticate/2 , destroy/1 ]). @@ -49,27 +48,24 @@ fields('hmac-based') -> , {algorithm, {enum, ['hmac-based']}} , {secret, fun secret/1} , {secret_base64_encoded, fun secret_base64_encoded/1} - , {verify_claims, fun verify_claims/1} - ]; + ] ++ common_fields(); fields('public-key') -> [ {use_jwks, {enum, [false]}} , {algorithm, {enum, ['public-key']}} , {certificate, fun certificate/1} - , {verify_claims, fun verify_claims/1} - ]; + ] ++ common_fields(); fields('jwks') -> [ {use_jwks, {enum, [true]}} , {endpoint, fun endpoint/1} , {refresh_interval, fun refresh_interval/1} - , {verify_claims, fun verify_claims/1} , {ssl, #{type => hoconsc:union( [ hoconsc:ref(?MODULE, ssl_enable) , hoconsc:ref(?MODULE, ssl_disable) ]), default => #{<<"enable">> => false}}} - ]; + ] ++ common_fields(); fields(ssl_enable) -> [ {enable, #{type => true}} @@ -81,19 +77,19 @@ fields(ssl_enable) -> ]; fields(ssl_disable) -> - [ {enable, #{type => false}} ]; + [ {enable, #{type => false}} ]. -fields(claim) -> - [ {"$name", fun expected_claim_value/1} ]. - -validations() -> - [ {check_verify_claims, fun check_verify_claims/1} ]. +common_fields() -> + [ {name, fun emqx_authn_schema:authenticator_name/1} + , {mechanism, {enum, [jwt]}} + , {verify_claims, fun verify_claims/1} + ]. secret(type) -> string(); secret(_) -> undefined. secret_base64_encoded(type) -> boolean(); -secret_base64_encoded(defualt) -> false; +secret_base64_encoded(default) -> false; secret_base64_encoded(_) -> undefined. certificate(type) -> string(); @@ -123,29 +119,31 @@ verify(_) -> undefined. server_name_indication(type) -> string(); server_name_indication(_) -> undefined. -verify_claims(type) -> hoconsc:array(hoconsc:ref(claim)); -verify_claims(default) -> []; +verify_claims(type) -> list(); +verify_claims(default) -> #{}; +verify_claims(validate) -> [fun check_verify_claims/1]; +verify_claims(converter) -> + fun(VerifyClaims) -> + maps:to_list(VerifyClaims) + end; verify_claims(_) -> undefined. -expected_claim_value(type) -> string(); -expected_claim_value(_) -> undefined. - %%------------------------------------------------------------------------------ %% APIs %%------------------------------------------------------------------------------ -create(_ChainID, _AuthenticatorName, Config) -> - create(Config). +create(#{verify_claims := VerifyClaims} = Config) -> + create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}). -update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, #{jwk := Connector}) +update(#{use_jwks := false} = Config, #{jwk := Connector}) when is_pid(Connector) -> _ = emqx_authn_jwks_connector:stop(Connector), create(Config); -update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, _) -> +update(#{use_jwks := false} = Config, _) -> create(Config); -update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Connector} = State) +update(#{use_jwks := true} = Config, #{jwk := Connector} = State) when is_pid(Connector) -> ok = emqx_authn_jwks_connector:update(Connector, Config), case maps:get(verify_cliams, Config, undefined) of @@ -155,7 +153,7 @@ update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Conn {ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}} end; -update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) -> +update(#{use_jwks := true} = Config, _) -> create(Config). authenticate(#{auth_method := _}, _) -> @@ -186,9 +184,6 @@ destroy(_) -> %% Internal functions %%-------------------------------------------------------------------- -create(#{verify_claims := VerifyClaims} = Config) -> - create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}). - create2(#{use_jwks := false, algorithm := 'hmac-based', secret := Secret0, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 4b1bcbb76..ce845d4e3 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -23,8 +23,8 @@ -export([ structs/0, fields/1 ]). --export([ create/3 - , update/4 +-export([ create/1 + , update/2 , authenticate/2 , destroy/1 ]). @@ -39,7 +39,7 @@ -type user_id_type() :: clientid | username. --type user_group() :: {chain_id(), authenticator_name()}. +-type user_group() :: {binary(), binary()}. -type user_id() :: binary(). -record(user_info, @@ -81,8 +81,10 @@ mnesia(copy) -> structs() -> [config]. fields(config) -> - [ {server_type, {enum, ['built-in-database']}} - , {user_id_type, fun user_id_type/1} + [ {name, fun emqx_authn_schema:authenticator_name/1} + , {mechanism, {enum, ['password-based']}} + , {server_type, {enum, ['built-in-database']}} + , {user_id_type, fun user_id_type/1} , {password_hash_algorithm, fun password_hash_algorithm/1} ]; @@ -111,25 +113,29 @@ salt_rounds(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ -create(ChainID, AuthenticatorName, #{user_id_type := Type, - password_hash_algorithm := #{name := bcrypt, - salt_rounds := SaltRounds}}) -> +create(#{ user_id_type := Type + , password_hash_algorithm := #{name := bcrypt, + salt_rounds := SaltRounds} + , '_unique' := Unique + }) -> {ok, _} = application:ensure_all_started(bcrypt), - State = #{user_group => {ChainID, AuthenticatorName}, + State = #{user_group => Unique, user_id_type => Type, password_hash_algorithm => bcrypt, salt_rounds => SaltRounds}, {ok, State}; -create(ChainID, AuthenticatorName, #{user_id_type := Type, - password_hash_algorithm := #{name := Name}}) -> - State = #{user_group => {ChainID, AuthenticatorName}, +create(#{ user_id_type := Type + , password_hash_algorithm := #{name := Name} + , '_unique' := Unique + }) -> + State = #{user_group => Unique, user_id_type => Type, password_hash_algorithm => Name}, {ok, State}. -update(ChainID, AuthenticatorName, Config, _State) -> - create(ChainID, AuthenticatorName, Config). +update(Config, #{user_group := Unique}) -> + create(Config#{'_unique' => Unique}). authenticate(#{auth_method := _}, _) -> ignore; @@ -172,8 +178,8 @@ import_users(Filename0, State) -> {error, {unsupported_file_format, Extension}} end. -add_user(#{<<"user_id">> := UserID, - <<"password">> := Password}, +add_user(#{user_id := UserID, + password := Password}, #{user_group := UserGroup} = State) -> trans( fun() -> @@ -197,7 +203,7 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{<<"password">> := Password}, +update_user(UserID, #{password := Password}, #{user_group := UserGroup} = State) -> trans( fun() -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index c76ece1c4..200d7afc4 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -21,10 +21,12 @@ -behaviour(hocon_schema). --export([ structs/0, fields/1 ]). +-export([ structs/0 + , fields/1 + ]). --export([ create/3 - , update/4 +-export([ create/1 + , update/2 , authenticate/2 , destroy/1 ]). @@ -36,50 +38,77 @@ structs() -> [config]. fields(config) -> - [ {server_type, {enum, [mysql]}} + [ {name, fun emqx_authn_schema:authenticator_name/1} + , {mechanism, {enum, ['password-based']}} + , {server_type, {enum, [mysql]}} , {password_hash_algorithm, fun password_hash_algorithm/1} - , {salt_position, {enum, [prefix, suffix]}} + , {salt_position, fun salt_position/1} , {query, fun query/1} , {query_timeout, fun query_timeout/1} ] ++ emqx_connector_schema_lib:relational_db_fields() - ++ emqx_connector_schema_lib:ssl_fields(). + ++ emqx_connector_schema_lib:ssl_fields(); -password_hash_algorithm(type) -> string(); +fields(bcrypt) -> + [ {name, {enum, [bcrypt]}} + , {salt_rounds, fun salt_rounds/1} + ]; + +fields(other_algorithms) -> + [ {name, {enum, [plain, md5, sha, sha256, sha512]}} + ]. + +password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; +password_hash_algorithm(default) -> #{<<"name">> => sha256}; password_hash_algorithm(_) -> undefined. +salt_rounds(type) -> integer(); +salt_rounds(default) -> 10; +salt_rounds(_) -> undefined. + +salt_position(type) -> {enum, [prefix, suffix]}; +salt_position(default) -> prefix; +salt_position(_) -> undefined. + query(type) -> string(); query(nullable) -> false; query(_) -> undefined. query_timeout(type) -> integer(); -query_timeout(defualt) -> 5000; +query_timeout(default) -> 5000; query_timeout(_) -> undefined. %%------------------------------------------------------------------------------ %% APIs %%------------------------------------------------------------------------------ -create(ChainID, AuthenticatorName, - #{query := Query0, - password_hash_algorithm := Algorithm} = Config) -> +create(#{ password_hash_algorithm := Algorithm + , salt_position := SaltPosition + , query := Query0 + , query_timeout := QueryTimeout + , '_unique' := Unique + } = Config) -> {Query, PlaceHolders} = parse_query(Query0), - ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, AuthenticatorName])), - State = #{query => Query, + State = #{password_hash_algorithm => Algorithm, + salt_position => SaltPosition, + query => Query, placeholders => PlaceHolders, - password_hash_algorithm => Algorithm}, - case emqx_resource:create_local(ResourceID, emqx_connector_mysql, Config) of + query_timeout => QueryTimeout}, + case emqx_resource:create_local(Unique, emqx_connector_mysql, Config) of {ok, _} -> - {ok, State#{resource_id => ResourceID}}; + {ok, State#{resource_id => Unique}}; {error, already_created} -> - {ok, State#{resource_id => ResourceID}}; + {ok, State#{resource_id => Unique}}; {error, Reason} -> {error, Reason} end. -update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = State) -> - case emqx_resource:update_local(ResourceID, emqx_connector_mysql, Config, []) of - {ok, _} -> {ok, State}; - {error, Reason} -> {error, Reason} +update(Config, State) -> + case create(Config) of + {ok, NewState} -> + ok = destroy(State), + {ok, NewState}; + {error, Reason} -> + {error, Reason} end. authenticate(#{auth_method := _}, _) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl new file mode 100644 index 000000000..0f5c8abb8 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl @@ -0,0 +1,58 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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_other_schema). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1 + ]). + +structs() -> [ "filename", "position", "user_info", "new_user_info"]. + +fields("filename") -> + [ {filename, fun filename/1} ]; +fields("position") -> + [ {position, fun position/1} ]; +fields("user_info") -> + [ {user_id, fun user_id/1} + , {password, fun password/1} + ]; +fields("new_user_info") -> + [ {password, fun password/1} + ]. + +filename(type) -> string(); +filename(nullable) -> false; +filename(_) -> undefined. + +position(type) -> integer(); +position(validate) -> [fun (Position) -> Position > 0 end]; +position(nullable) -> false; +position(_) -> undefined. + +user_id(type) -> binary(); +user_id(nullable) -> false; +user_id(_) -> undefined. + +password(type) -> binary(); +password(nullable) -> false; +password(_) -> undefined. + diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index 700298c46..100e5cce7 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -23,8 +23,8 @@ -export([ structs/0, fields/1 ]). --export([ create/3 - , update/4 +-export([ create/1 + , update/2 , authenticate/2 , destroy/1 ]). @@ -36,7 +36,9 @@ structs() -> [config]. fields(config) -> - [ {server_type, {enum, [pgsql]}} + [ {name, fun emqx_authn_schema:authenticator_name/1} + , {mechanism, {enum, ['password-based']}} + , {server_type, {enum, [pgsql]}} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, {enum, [prefix, suffix]}} , {query, fun query/1} @@ -54,26 +56,32 @@ query(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ -create(ChainID, ServiceName, #{query := Query0, - password_hash_algorithm := Algorithm} = Config) -> +create(#{ query := Query0 + , password_hash_algorithm := Algorithm + , salt_position := SaltPosition + , '_unique' := Unique + } = Config) -> {Query, PlaceHolders} = parse_query(Query0), - ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])), State = #{query => Query, placeholders => PlaceHolders, - password_hash_algorithm => Algorithm}, - case emqx_resource:create_local(ResourceID, emqx_connector_pgsql, Config) of + password_hash_algorithm => Algorithm, + salt_position => SaltPosition}, + case emqx_resource:create_local(Unique, emqx_connector_pgsql, Config) of {ok, _} -> - {ok, State#{resource_id => ResourceID}}; + {ok, State#{resource_id => Unique}}; {error, already_created} -> - {ok, State#{resource_id => ResourceID}}; + {ok, State#{resource_id => Unique}}; {error, Reason} -> {error, Reason} end. -update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> - case emqx_resource:update_local(ResourceID, emqx_connector_pgsql, Config, []) of - {ok, _} -> {ok, State}; - {error, Reason} -> {error, Reason} +update(Config, State) -> + case create(Config) of + {ok, NewState} -> + ok = destroy(State), + {ok, NewState}; + {error, Reason} -> + {error, Reason} end. authenticate(#{auth_method := _}, _) -> diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 59103662c..36b0eabf3 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -54,34 +54,44 @@ t_authenticator(_) -> AuthenticatorName1 = <<"myauthenticator1">>, AuthenticatorConfig1 = #{name => AuthenticatorName1, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}}, - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName1)), - ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), + server_type => 'built-in-database', + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}, + {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), + ?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)), + ?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), + + AuthenticatorConfig2 = #{name => AuthenticatorName1, + mechanism => jwt, + use_jwks => false, + algorithm => 'hmac-based', + secret => <<"abcdef">>, + secret_base64_encoded => false, + verify_claims => []}, + {ok, #{name := AuthenticatorName1, id := ID1, mechanism := jwt}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2), + + ID2 = <<"random">>, + ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), + ?assertEqual({error, name_has_be_used}, ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), AuthenticatorName2 = <<"myauthenticator2">>, - AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2}, - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), - ?assertMatch({ok, #{id := ?CHAIN, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(?CHAIN)), - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)), + AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2}, + {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"abcdef">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3), + ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)), + {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"fedcba">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(?CHAIN, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 1)), - ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 3)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 0)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), + ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)), + ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), + + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), + ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 0)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), ok. diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index b22e72a95..7435deaa0 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -39,15 +39,14 @@ end_per_suite(_) -> t_jwt_authenticator(_) -> AuthenticatorName = <<"myauthenticator">>, - Config = #{use_jwks => false, + Config = #{name => AuthenticatorName, + mechanism => jwt, + use_jwks => false, algorithm => 'hmac-based', secret => <<"abcdef">>, secret_base64_encoded => false, verify_claims => []}, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => jwt, - config => Config}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), Payload = #{<<"username">> => <<"myuser">>}, JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), @@ -62,11 +61,11 @@ t_jwt_authenticator(_) -> %% secret_base64_encoded Config2 = Config#{secret => base64:encode(<<"abcdef">>), secret_base64_encoded => true}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config2)), + ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config3)), + ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), @@ -109,7 +108,7 @@ t_jwt_authenticator(_) -> ClientInfo8 = ClientInfo#{password => JWS8}, ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. t_jwt_authenticator2(_) -> @@ -117,14 +116,13 @@ t_jwt_authenticator2(_) -> PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), AuthenticatorName = <<"myauthenticator">>, - Config = #{use_jwks => false, + Config = #{name => AuthenticatorName, + mechanism => jwt, + use_jwks => false, algorithm => 'public-key', certificate => PublicKey, verify_claims => []}, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => jwt, - config => Config}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), Payload = #{<<"username">> => <<"myuser">>}, JWS = generate_jws('public-key', Payload, PrivateKey), @@ -133,7 +131,7 @@ t_jwt_authenticator2(_) -> ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. generate_jws('hmac-based', Payload, Secret) -> diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index c8c787cf1..4a5a24844 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -41,18 +41,17 @@ t_mnesia_authenticator(_) -> AuthenticatorName = <<"myauthenticator">>, AuthenticatorConfig = #{name => AuthenticatorName, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + server_type => 'built-in-database', + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}, + {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - UserInfo = #{<<"user_id">> => <<"myuser">>, - <<"password">> => <<"mypass">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + UserInfo = #{user_id => <<"myuser">>, + password => <<"mypass">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ClientInfo = #{zone => external, username => <<"myuser">>, @@ -69,40 +68,39 @@ t_mnesia_authenticator(_) -> ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), - UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, AuthenticatorName, <<"myuser">>, UserInfo2)), + UserInfo2 = UserInfo#{password => <<"mypass2">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), - ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), - ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), + ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), ok. t_import(_) -> AuthenticatorName = <<"myauthenticator">>, AuthenticatorConfig = #{name => AuthenticatorName, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + server_type => 'built-in-database', + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}, + {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), Dir = code:lib_dir(emqx_authn, test), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), - ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser1">>)), - ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser3">>)), + ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))), + ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))), + ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)), + ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)), ClientInfo1 = #{username => <<"myuser1">>, password => <<"mypassword1">>}, @@ -110,50 +108,48 @@ t_import(_) -> ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, password => <<"mypassword3">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. t_multi_mnesia_authenticator(_) -> AuthenticatorName1 = <<"myauthenticator1">>, AuthenticatorConfig1 = #{name => AuthenticatorName1, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}}, + server_type => 'built-in-database', + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}, AuthenticatorName2 = <<"myauthenticator2">>, AuthenticatorConfig2 = #{name => AuthenticatorName2, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => clientid, - password_hash_algorithm => #{ - name => sha256 - }}}, - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), + server_type => 'built-in-database', + user_id_type => clientid, + password_hash_algorithm => #{ + name => sha256 + }}, + {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), + {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), ?assertEqual({ok, #{user_id => <<"myuser">>}}, - ?AUTH:add_user(?CHAIN, AuthenticatorName1, - #{<<"user_id">> => <<"myuser">>, - <<"password">> => <<"mypass1">>})), + ?AUTH:add_user(?CHAIN, ID1, + #{user_id => <<"myuser">>, + password => <<"mypass1">>})), ?assertEqual({ok, #{user_id => <<"myclient">>}}, - ?AUTH:add_user(?CHAIN, AuthenticatorName2, - #{<<"user_id">> => <<"myclient">>, - <<"password">> => <<"mypass2">>})), + ?AUTH:add_user(?CHAIN, ID2, + #{user_id => <<"myclient">>, + password => <<"mypass2">>})), ClientInfo1 = #{username => <<"myuser">>, clientid => <<"myclient">>, password => <<"mypass1">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)), ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), ok. diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index d6d30eb76..11860f32d 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -53,14 +53,15 @@ fields("") -> [{config, #{type => hoconsc:ref(?MODULE, config)}}]; fields(config) -> - [ {base_url, fun base_url/1} - , {connect_timeout, fun connect_timeout/1} - , {max_retries, fun max_retries/1} - , {retry_interval, fun retry_interval/1} - , {pool_type, fun pool_type/1} - , {pool_size, fun pool_size/1} - , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), - default => #{}}} + [ {base_url, fun base_url/1} + , {connect_timeout, fun connect_timeout/1} + , {max_retries, fun max_retries/1} + , {retry_interval, fun retry_interval/1} + , {pool_type, fun pool_type/1} + , {pool_size, fun pool_size/1} + , {enable_pipelining, fun enable_pipelining/1} + , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), + default => #{}}} ]; fields(ssl_opts) -> @@ -101,6 +102,10 @@ pool_size(type) -> non_neg_integer(); pool_size(default) -> 8; pool_size(_) -> undefined. +enable_pipelining(type) -> boolean(); +enable_pipelining(default) -> true; +enable_pipelining(_) -> undefined. + cacertfile(type) -> string(); cacertfile(nullable) -> true; cacertfile(_) -> undefined. @@ -147,7 +152,7 @@ on_start(InstId, #{base_url := #{scheme := Scheme, , {pool_type, PoolType} , {pool_size, PoolSize} , {transport, Transport} - , {transport, NTransportOpts}], + , {transport_opts, NTransportOpts}], PoolName = emqx_plugin_libs_pool:pool_name(InstId), {ok, _} = ehttpc_sup:start_pool(PoolName, PoolOpts), {ok, #{pool_name => PoolName, diff --git a/rebar.config b/rebar.config index ea9e11f4a..2ff90e6d1 100644 --- a/rebar.config +++ b/rebar.config @@ -43,7 +43,7 @@ {deps, [ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps - , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.7"}}} + , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.8"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}