From 327ff8636f9965fd95193d7e21df39cb26dbe7bc Mon Sep 17 00:00:00 2001 From: zhouzb Date: Mon, 26 Jul 2021 14:37:06 +0800 Subject: [PATCH] feat(authn http api): provide http api for authn and improve update mechanism --- apps/emqx_authn/etc/emqx_authn.conf | 14 +- apps/emqx_authn/include/emqx_authn.hrl | 14 +- apps/emqx_authn/src/emqx_authn.erl | 298 +++---- apps/emqx_authn/src/emqx_authn_api.erl | 818 ++++++++++++++++-- apps/emqx_authn/src/emqx_authn_app.erl | 8 +- apps/emqx_authn/src/emqx_authn_schema.erl | 59 +- .../emqx_enhanced_authn_scram_mnesia.erl | 22 +- .../src/simple_authn/emqx_authn_http.erl | 52 +- .../src/simple_authn/emqx_authn_jwt.erl | 34 +- .../src/simple_authn/emqx_authn_mnesia.erl | 34 +- .../src/simple_authn/emqx_authn_mysql.erl | 47 +- .../src/simple_authn/emqx_authn_pgsql.erl | 36 +- 12 files changed, 1065 insertions(+), 371 deletions(-) diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index 3e69ae46d..30ef427ae 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..ef3e2893c 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -17,21 +17,21 @@ -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() + , version :: binary() }). -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..95ace3800 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,80 @@ 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 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; + false -> + {error, {not_found, {authenticator, AuthenticatorID}}} + end; + {value, + {_, _, #authenticator{provider = Provider, + state = #{version := Version} = State}}, + Others} -> + case lists:keymember(NewName, 2, Others) of + true -> + {error, name_has_be_used}; + false -> + case (NewProvider = authenticator_provider(Config)) =:= Provider of + true -> + Unique = {ChainID, AuthenticatorID, Version}, + case Provider:update(Config#{'_unique' => Unique}, State) of + {ok, NewState} -> + NewAuthenticator = #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 -> + case NewProvider:create(Config#{'_unique' => {ChainID, AuthenticatorID, Version}}) of + {ok, NewState} -> + NewAuthenticator = #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 +268,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 +280,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 +380,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 +405,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 561f69861..c87df2dc5 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -20,12 +20,25 @@ -include("emqx_authn.hrl"). --export([ api_spec/0 ]). +-export([ api_spec/0 + , authenticators/2 + , authenticators2/2 + , position/2 + , import_users/2 + , users/2 + , users2/2 + ]). api_spec() -> - {[authenticator_api()], definitions()}. + {[ authenticators_api() + , authenticators_api2() + , position_api() + , import_users_api() + , users_api() + , users2_api() + ], definitions()}. -authenticator_api() -> +authenticators_api() -> Example1 = #{name => <<"example">>, mechanism => <<"password-based">>, config => #{ @@ -86,24 +99,506 @@ authenticator_api() -> }, responses => #{ <<"201">> => #{ - description => <<"Created successfully">>, - content => #{} + description => <<"Created">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"returned_authenticator">>) + } + } + } + } + }, + get => #{ + description => "List authenticators", + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => minirest:ref(<<"returned_authenticator">>) + } + } + } } } } }, {"/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">>) + } + } + }, + <<"404">> => #{ + description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + }, + 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">>) + ] + } + } + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"returned_authenticator">>) + } + } + }, + <<"404">> => #{ + description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + }, + delete => #{ + description => "Delete authenticator", + parameters => [ + #{ + name => id, + in => path, + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => #{ + description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + } + }, + {"/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">> + }, + <<"404">> => #{ + description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + } + }, + {"/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">> => #{ + description => <<"Bad Request">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + }, + <<"404">> => #{ + description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + } + }, + {"/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">> => #{ + description => <<"Bad Request">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + }, + 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 + } + } + } + } + } + } + } + } + } + }, + {"/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 + } + } + } + } + } + } + }, + <<"404">> => #{ + description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + }, + 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">> => #{ + description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + }, + 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">> => #{ + description => <<"Not Found">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>) + } + } + } + } + } + }, + {"/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, - required => [name], properties => #{ - name => #{ - type => string, - example => "exmaple" + id => #{ + type => string } } }, @@ -111,99 +606,108 @@ definitions() -> oneOf => [ minirest:ref(<<"password_based">>) , minirest:ref(<<"jwt">>) , minirest:ref(<<"scram">>) - ] + ] } ] }, PasswordBasedDef = #{ - type => object, - properties => #{ - mechanism => #{ - type => string, - enum => [<<"password-based">>], - example => <<"password-based">> + allOf => [ + #{ + type => object, + required => [name, mechanism], + properties => #{ + name => #{ + type => string, + example => "exmaple" + }, + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + } + } }, - config => #{ + #{ 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">> }, - config => #{ + 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, - properties => #{ - 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">>) - } - } + 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">> }, - config => #{ - type => object, - properties => #{ - 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 - } - } + 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 } } }, @@ -410,7 +914,7 @@ definitions() -> properties => #{ enable => #{ type => boolean, - default => false + default => false }, hostname => #{ type => string @@ -420,7 +924,22 @@ definitions() -> } }, + 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} @@ -430,4 +949,163 @@ definitions() -> , #{<<"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} -> + serialize_error(Reason) + end; +authenticators(get, _Request) -> + {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN), + {200, Authenticators}. + +authenticators2(get, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of + {ok, Authenticator} -> + {200, Authenticator}; + {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. + +position(post, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + case emqx_json:decode(Body, [return_maps]) of + #{<<"position">> := Position} when is_integer(Position) -> + case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, position}) + end. + +import_users(post, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + case emqx_json:decode(Body, [return_maps]) of + #{<<"filename">> := Filename} when is_binary(Filename) -> + case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, filename}) + end. + +users(post, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + case emqx_json:decode(Body, [return_maps]) of + #{<<"user_id">> := _, + <<"password">> := _} = UserInfo -> + case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of + {ok, User} -> + {201, User}; + {error, Reason} -> + serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, user_id}) + end; +users(get, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + case emqx_authn:list_users(?CHAIN, AuthenticatorID) of + {ok, Users} -> + {200, Users}; + {error, Reason} -> + serialize_error(Reason) + end. + +users2(patch, Request) -> + AuthenticatorID = cowboy_req:binding(id, Request), + UserID = cowboy_req:binding(user_id, Request), + {ok, Body, _} = cowboy_req:read_body(Request), + case emqx_json:decode(Body, [return_maps]) of + #{<<"password">> := _} = UserInfo -> + case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, password}) + 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. + +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}) -> + {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(_) -> + {400, #{code => <<"BAD_REQUEST">>, + message => <<"Todo">>}}. \ 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 14b25558a..9b147dceb 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} ]. @@ -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) -> 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 48d123b70..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,8 +26,8 @@ , validations/0 ]). --export([ create/3 - , update/4 +-export([ create/1 + , update/2 , authenticate/2 , destroy/1 ]). @@ -57,7 +57,9 @@ fields(post) -> ] ++ 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} , {form_data, fun form_data/1} , {request_timeout, fun request_timeout/1} @@ -105,37 +107,41 @@ request_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ -create(ChainID, AuthenticatorName, - #{method := Method, - url := URL, - headers := Headers, - form_data := FormData, - request_timeout := RequestTimeout} = Config) -> +create(#{ method := Method + , url := URL + , headers := Headers + , form_data := FormData + , request_timeout := RequestTimeout + , '_unique' := Unique + } = Config) -> #{path := Path, query := Query} = URIMap = parse_url(URL), - 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}, - ResourceID = <>, - case emqx_resource:create_local(ResourceID, + 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 := _}, _) -> 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 2e4e473c0..fe034994e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -24,8 +24,8 @@ , fields/1 ]). --export([ create/3 - , update/4 +-export([ create/1 + , update/2 , authenticate/2 , destroy/1 ]). @@ -48,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}} @@ -82,6 +79,12 @@ fields(ssl_enable) -> fields(ssl_disable) -> [ {enable, #{type => false}} ]. +common_fields() -> + [ {name, fun emqx_authn_schema:authenticator_name/1} + , {mechanism, {enum, [jwt]}} + , {verify_claims, fun verify_claims/1} + ]. + secret(type) -> string(); secret(_) -> undefined. @@ -129,18 +132,18 @@ verify_claims(_) -> 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 @@ -150,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 := _}, _) -> @@ -181,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..3d5ebe7a0 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; 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 c112b9666..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,7 +38,9 @@ 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, fun salt_position/1} , {query, fun query/1} @@ -70,34 +74,41 @@ 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_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 := _}, _) ->