Merge pull request #5319 from tigercl/feat/authn-http-api

feat(authn): provide http api and improve update mechanism
This commit is contained in:
tigercl 2021-07-27 11:16:59 +08:00 committed by GitHub
commit 4dea41f8a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1666 additions and 793 deletions

View File

@ -1,13 +1,11 @@
emqx_authn: { emqx_authn: {
enable: false enable: false
authenticators: [ authenticators: [
# { # {
# name: "authenticator1" # name: "authenticator1"
# mechanism: password-based # mechanism: password-based
# config: { # server_type: built-in-database
# server_type: built-in-database # user_id_type: clientid
# user_id_type: clientid # }
# }
# }
] ]
} }

View File

@ -17,21 +17,20 @@
-define(APP, emqx_authn). -define(APP, emqx_authn).
-define(CHAIN, <<"mqtt">>). -define(CHAIN, <<"mqtt">>).
-type chain_id() :: binary(). -define(VER_1, <<"1">>).
-type authenticator_name() :: binary(). -define(VER_2, <<"2">>).
-type mechanism() :: 'password-based' | jwt | scram.
-record(authenticator, -record(authenticator,
{ name :: authenticator_name() { id :: binary()
, mechanism :: mechanism() , name :: binary()
, provider :: module() , provider :: module()
, config :: map() , config :: map()
, state :: map() , state :: map()
}). }).
-record(chain, -record(chain,
{ id :: chain_id() { id :: binary()
, authenticators :: [{authenticator_name(), #authenticator{}}] , authenticators :: [{binary(), binary(), #authenticator{}}]
, created_at :: integer() , created_at :: integer()
}). }).

View File

@ -31,10 +31,9 @@
, create_authenticator/2 , create_authenticator/2
, delete_authenticator/2 , delete_authenticator/2
, update_authenticator/3 , update_authenticator/3
, update_or_create_authenticator/3
, lookup_authenticator/2 , lookup_authenticator/2
, list_authenticators/1 , list_authenticators/1
, move_authenticator_to_the_front/2
, move_authenticator_to_the_end/2
, move_authenticator_to_the_nth/3 , move_authenticator_to_the_nth/3
]). ]).
@ -95,7 +94,7 @@ authenticate(Credential, _AuthResult) ->
do_authenticate([], _) -> do_authenticate([], _) ->
{stop, {error, not_authorized}}; {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 case Provider:authenticate(Credential, State) of
ignore -> ignore ->
do_authenticate(More, Credential); do_authenticate(More, Credential);
@ -130,7 +129,7 @@ delete_chain(ID) ->
[] -> [] ->
{error, {not_found, {chain, ID}}}; {error, {not_found, {chain, ID}}};
[#chain{authenticators = Authenticators}] -> [#chain{authenticators = Authenticators}] ->
_ = [do_delete_authenticator(Authenticator) || {_, Authenticator} <- Authenticators], _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators],
mnesia:delete(?CHAIN_TAB, ID, write) mnesia:delete(?CHAIN_TAB, ID, write)
end end
end). end).
@ -147,25 +146,21 @@ list_chains() ->
Chains = ets:tab2list(?CHAIN_TAB), Chains = ets:tab2list(?CHAIN_TAB),
{ok, [serialize_chain(Chain) || Chain <- Chains]}. {ok, [serialize_chain(Chain) || Chain <- Chains]}.
create_authenticator(ChainID, #{name := Name, create_authenticator(ChainID, #{name := Name} = Config) ->
mechanism := Mechanism,
config := Config}) ->
UpdateFun = UpdateFun =
fun(Chain = #chain{authenticators = Authenticators}) -> fun(Chain = #chain{authenticators = Authenticators}) ->
case lists:keymember(Name, 1, Authenticators) of case lists:keymember(Name, 2, Authenticators) of
true -> true ->
{error, {already_exists, {authenticator, Name}}}; {error, name_has_be_used};
false -> false ->
Provider = authenticator_provider(Mechanism, Config), AlreadyExist = fun(ID) ->
case Provider:create(ChainID, Name, Config) of lists:keymember(ID, 1, Authenticators)
{ok, State} -> end,
Authenticator = #authenticator{name = Name, AuthenticatorID = gen_id(AlreadyExist),
mechanism = Mechanism, case do_create_authenticator(ChainID, AuthenticatorID, Config) of
provider = Provider, {ok, Authenticator} ->
config = Config, NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}],
state = State}, ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write),
NChain = Chain#chain{authenticators = Authenticators ++ [{Name, Authenticator}]},
ok = mnesia:write(?CHAIN_TAB, NChain, write),
{ok, serialize_authenticator(Authenticator)}; {ok, serialize_authenticator(Authenticator)};
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
@ -174,12 +169,12 @@ create_authenticator(ChainID, #{name := Name,
end, end,
update_chain(ChainID, UpdateFun). update_chain(ChainID, UpdateFun).
delete_authenticator(ChainID, AuthenticatorName) -> delete_authenticator(ChainID, AuthenticatorID) ->
UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
case lists:keytake(AuthenticatorName, 1, Authenticators) of case lists:keytake(AuthenticatorID, 1, Authenticators) of
false -> false ->
{error, {not_found, {authenticator, AuthenticatorName}}}; {error, {not_found, {authenticator, AuthenticatorID}}};
{value, {_, Authenticator}, NAuthenticators} -> {value, {_, _, Authenticator}, NAuthenticators} ->
_ = do_delete_authenticator(Authenticator), _ = do_delete_authenticator(Authenticator),
NChain = Chain#chain{authenticators = NAuthenticators}, NChain = Chain#chain{authenticators = NAuthenticators},
mnesia:write(?CHAIN_TAB, NChain, write) mnesia:write(?CHAIN_TAB, NChain, write)
@ -187,38 +182,86 @@ delete_authenticator(ChainID, AuthenticatorName) ->
end, end,
update_chain(ChainID, UpdateFun). 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}) -> UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
case proplists:get_value(AuthenticatorName, Authenticators, undefined) of case lists:keytake(AuthenticatorID, 1, Authenticators) of
undefined -> false ->
{error, {not_found, {authenticator, AuthenticatorName}}}; case CreateWhenNotFound of
#authenticator{provider = Provider, true ->
config = OriginalConfig, case lists:keymember(NewName, 2, Authenticators) of
state = State} = Authenticator -> true ->
NewConfig = maps:merge(OriginalConfig, Config), {error, name_has_be_used};
case Provider:update(ChainID, AuthenticatorName, NewConfig, State) of false ->
{ok, NState} -> case do_create_authenticator(ChainID, AuthenticatorID, Config) of
NAuthenticator = Authenticator#authenticator{config = NewConfig, {ok, Authenticator} ->
state = NState}, NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}],
NAuthenticators = update_value(AuthenticatorName, NAuthenticator, Authenticators), ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write),
ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), {ok, serialize_authenticator(Authenticator)};
{ok, serialize_authenticator(NAuthenticator)}; {error, Reason} ->
{error, Reason} -> {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 = <<ChainID/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
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 = <<ChainID/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
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
end, end,
update_chain(ChainID, UpdateFun). update_chain(ChainID, UpdateFun).
lookup_authenticator(ChainID, AuthenticatorName) -> lookup_authenticator(ChainID, AuthenticatorID) ->
case mnesia:dirty_read(?CHAIN_TAB, ChainID) of case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
[] -> [] ->
{error, {not_found, {chain, ChainID}}}; {error, {not_found, {chain, ChainID}}};
[#chain{authenticators = Authenticators}] -> [#chain{authenticators = Authenticators}] ->
case proplists:get_value(AuthenticatorName, Authenticators, undefined) of case lists:keyfind(AuthenticatorID, 1, Authenticators) of
undefined -> false ->
{error, {not_found, {authenticator, AuthenticatorName}}}; {error, {not_found, {authenticator, AuthenticatorID}}};
Authenticator -> {_, _, Authenticator} ->
{ok, serialize_authenticator(Authenticator)} {ok, serialize_authenticator(Authenticator)}
end end
end. end.
@ -231,9 +274,9 @@ list_authenticators(ChainID) ->
{ok, serialize_authenticators(Authenticators)} {ok, serialize_authenticators(Authenticators)}
end. end.
move_authenticator_to_the_front(ChainID, AuthenticatorName) -> move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) ->
UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> 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} -> {ok, NAuthenticators} ->
NChain = Chain#chain{authenticators = NAuthenticators}, NChain = Chain#chain{authenticators = NAuthenticators},
mnesia:write(?CHAIN_TAB, NChain, write); mnesia:write(?CHAIN_TAB, NChain, write);
@ -243,108 +286,94 @@ move_authenticator_to_the_front(ChainID, AuthenticatorName) ->
end, end,
update_chain(ChainID, UpdateFun). update_chain(ChainID, UpdateFun).
move_authenticator_to_the_end(ChainID, AuthenticatorName) -> import_users(ChainID, AuthenticatorID, Filename) ->
UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]).
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).
move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) -> add_user(ChainID, AuthenticatorID, UserInfo) ->
UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]).
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).
import_users(ChainID, AuthenticatorName, Filename) -> delete_user(ChainID, AuthenticatorID, UserID) ->
call_authenticator(ChainID, AuthenticatorName, import_users, [Filename]). call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]).
add_user(ChainID, AuthenticatorName, UserInfo) -> update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) ->
call_authenticator(ChainID, AuthenticatorName, add_user, [UserInfo]). call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]).
delete_user(ChainID, AuthenticatorName, UserID) -> lookup_user(ChainID, AuthenticatorID, UserID) ->
call_authenticator(ChainID, AuthenticatorName, delete_user, [UserID]). call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]).
update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) -> list_users(ChainID, AuthenticatorID) ->
call_authenticator(ChainID, AuthenticatorName, update_user, [UserID, NewUserInfo]). call_authenticator(ChainID, AuthenticatorID, list_users, []).
lookup_user(ChainID, AuthenticatorName, UserID) ->
call_authenticator(ChainID, AuthenticatorName, lookup_user, [UserID]).
list_users(ChainID, AuthenticatorName) ->
call_authenticator(ChainID, AuthenticatorName, list_users, []).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Internal functions %% Internal functions
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
authenticator_provider('password-based', #{server_type := 'built-in-database'}) -> authenticator_provider(#{mechanism := 'password-based', server_type := 'built-in-database'}) ->
emqx_authn_mnesia; emqx_authn_mnesia;
authenticator_provider('password-based', #{server_type := 'mysql'}) -> authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'}) ->
emqx_authn_mysql; emqx_authn_mysql;
authenticator_provider('password-based', #{server_type := 'pgsql'}) -> authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) ->
emqx_authn_pgsql; emqx_authn_pgsql;
authenticator_provider('password-based', #{server_type := 'http-server'}) -> authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
emqx_authn_http; emqx_authn_http;
authenticator_provider(jwt, _) -> authenticator_provider(#{mechanism := jwt}) ->
emqx_authn_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. 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 = <<ChainID/binary, "/", AuthenticatorID/binary, ":", ?VER_1/binary>>,
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}) -> do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
Provider:destroy(State). _ = Provider:destroy(State),
ok.
update_value(Key, Value, List) -> replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) ->
lists:keyreplace(Key, 1, List, {Key, Value}). lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}).
move_authenticator_to_the_front_(AuthenticatorName, Authenticators) -> move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N)
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)
when N =< length(Authenticators) andalso N > 0 -> 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_(_, _, _) -> move_authenticator_to_the_nth_(_, _, _) ->
{error, out_of_range}. {error, out_of_range}.
move_authenticator_to_the_nth_(AuthenticatorName, [], _, _) -> move_authenticator_to_the_nth_(AuthenticatorID, [], _, _) ->
{error, {not_found, {authenticator, AuthenticatorName}}}; {error, {not_found, {authenticator, AuthenticatorID}}};
move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed)
when N =< length(Passed) -> when N =< length(Passed) ->
{L1, L2} = lists:split(N - 1, lists:reverse(Passed)), {L1, L2} = lists:split(N - 1, lists:reverse(Passed)),
{ok, L1 ++ [Authenticator] ++ L2 ++ More}; {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), {L1, L2} = lists:split(N - length(Passed) - 1, More),
{ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2}; {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2};
move_authenticator_to_the_nth_(AuthenticatorName, [Authenticator | More], N, Passed) -> move_authenticator_to_the_nth_(AuthenticatorID, [Authenticator | More], N, Passed) ->
move_authenticator_to_the_nth_(AuthenticatorName, More, N, [Authenticator | Passed]). move_authenticator_to_the_nth_(AuthenticatorID, More, N, [Authenticator | Passed]).
update_chain(ChainID, UpdateFun) -> update_chain(ChainID, UpdateFun) ->
trans( trans(
@ -357,24 +386,15 @@ update_chain(ChainID, UpdateFun) ->
end end
end). end).
% lookup_chain_by_listener(ListenerID, AuthNType) -> call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
% 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) ->
case mnesia:dirty_read(?CHAIN_TAB, ChainID) of case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
[] -> [] ->
{error, {not_found, {chain, ChainID}}}; {error, {not_found, {chain, ChainID}}};
[#chain{authenticators = Authenticators}] -> [#chain{authenticators = Authenticators}] ->
case proplists:get_value(AuthenticatorName, Authenticators, undefined) of case lists:keyfind(AuthenticatorID, 1, Authenticators) of
undefined -> false ->
{error, {not_found, {authenticator, AuthenticatorName}}}; {error, {not_found, {authenticator, AuthenticatorID}}};
#authenticator{provider = Provider, state = State} -> {_, _, #authenticator{provider = Provider, state = State}} ->
case erlang:function_exported(Provider, Func, length(Args) + 1) of case erlang:function_exported(Provider, Func, length(Args) + 1) of
true -> true ->
erlang:apply(Provider, Func, Args ++ [State]); erlang:apply(Provider, Func, Args ++ [State]);
@ -391,20 +411,12 @@ serialize_chain(#chain{id = ID,
authenticators => serialize_authenticators(Authenticators), authenticators => serialize_authenticators(Authenticators),
created_at => CreatedAt}. created_at => CreatedAt}.
% serialize_binding(#binding{bound = {ListenerID, _},
% chain_id = ChainID}) ->
% #{listener_id => ListenerID,
% chain_id => ChainID}.
serialize_authenticators(Authenticators) -> serialize_authenticators(Authenticators) ->
[serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators]. [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators].
serialize_authenticator(#authenticator{name = Name, serialize_authenticator(#authenticator{id = ID,
mechanism = Mechanism,
config = Config}) -> config = Config}) ->
#{name => Name, Config#{id => ID}.
mechanism => Mechanism,
config => Config}.
trans(Fun) -> trans(Fun) ->
trans(Fun, []). trans(Fun, []).

File diff suppressed because it is too large Load Diff

View File

@ -42,16 +42,16 @@ initialize() ->
authenticators => []}), authenticators => []}),
initialize(AuthNConfig). initialize(AuthNConfig).
initialize(#{enable := Enable, authenticators := Authenticators}) -> initialize(#{enable := Enable, authenticators := AuthenticatorsConfig}) ->
{ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}),
initialize_authenticators(Authenticators), initialize_authenticators(AuthenticatorsConfig),
Enable =:= true andalso emqx_authn:enable(), Enable =:= true andalso emqx_authn:enable(),
ok. ok.
initialize_authenticators([]) -> initialize_authenticators([]) ->
ok; ok;
initialize_authenticators([#{name := Name} = Authenticator | More]) -> initialize_authenticators([#{name := Name} = AuthenticatorConfig | More]) ->
case emqx_authn:create_authenticator(?CHAIN, Authenticator) of case emqx_authn:create_authenticator(?CHAIN, AuthenticatorConfig) of
{ok, _} -> {ok, _} ->
initialize_authenticators(More); initialize_authenticators(More);
{error, Reason} -> {error, Reason} ->

View File

@ -25,57 +25,34 @@
, fields/1 , fields/1
]). ]).
-reflect_type([ authenticator_name/0 -export([ authenticator_name/1
]). ]).
structs() -> ["emqx_authn"]. structs() -> [ "emqx_authn" ].
fields("emqx_authn") -> fields("emqx_authn") ->
[ {enable, fun enable/1} [ {enable, fun enable/1}
, {authenticators, fun authenticators/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(type) -> boolean();
enable(defualt) -> false; enable(default) -> false;
enable(_) -> undefined. enable(_) -> undefined.
authenticators(type) -> authenticators(type) ->
hoconsc:array({union, [ hoconsc:ref(?MODULE, 'password-based') hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config)
, hoconsc:ref(?MODULE, jwt) , hoconsc:ref(emqx_authn_mysql, config)
, hoconsc:ref(?MODULE, scram)]}); , 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(default) -> [];
authenticators(_) -> undefined. authenticators(_) -> undefined.
authenticator_name(type) -> authenticator_name();
authenticator_name(nullable) -> false;
authenticator_name(_) -> undefined.

View File

@ -26,8 +26,8 @@
, fields/1 , fields/1
]). ]).
-export([ create/3 -export([ create/1
, update/4 , update/2
, authenticate/2 , authenticate/2
, destroy/1 , destroy/1
]). ]).
@ -71,7 +71,9 @@ mnesia(copy) ->
structs() -> [config]. structs() -> [config].
fields(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} , {algorithm, fun algorithm/1}
, {iteration_count, fun iteration_count/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(default) -> 'built-in-database';
server_type(_) -> undefined. server_type(_) -> undefined.
algorithm(type) -> hoconsc:enum([sha256, sha256]); algorithm(type) -> hoconsc:enum([sha256, sha512]);
algorithm(default) -> sha256; algorithm(default) -> sha256;
algorithm(_) -> undefined. algorithm(_) -> undefined.
@ -92,16 +94,18 @@ iteration_count(_) -> undefined.
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
create(ChainID, Authenticator, #{algorithm := Algorithm, create(#{ algorithm := Algorithm
iteration_count := IterationCount}) -> , iteration_count := IterationCount
State = #{user_group => {ChainID, Authenticator}, , '_unique' := Unique
}) ->
State = #{user_group => Unique,
algorithm => Algorithm, algorithm => Algorithm,
iteration_count => IterationCount}, iteration_count => IterationCount},
{ok, State}. {ok, State}.
update(_ChainID, _Authenticator, _Config, _State) -> update(Config, #{user_group := Unique}) ->
{error, update_not_suppored}. create(Config#{'_unique' => Unique}).
authenticate(#{auth_method := AuthMethod, authenticate(#{auth_method := AuthMethod,
auth_data := AuthData, auth_data := AuthData,
auth_cache := AuthCache}, State) -> auth_cache := AuthCache}, State) ->
@ -129,8 +133,8 @@ destroy(#{user_group := UserGroup}) ->
end). end).
%% TODO: binary to atom %% TODO: binary to atom
add_user(#{<<"user_id">> := UserID, add_user(#{user_id := UserID,
<<"password">> := Password}, #{user_group := UserGroup} = State) -> password := Password}, #{user_group := UserGroup} = State) ->
trans( trans(
fun() -> fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of case mnesia:read(?TAB, {UserGroup, UserID}, write) of
@ -153,7 +157,7 @@ delete_user(UserID, #{user_group := UserGroup}) ->
end end
end). end).
update_user(UserID, #{<<"password">> := Password}, update_user(UserID, #{password := Password},
#{user_group := UserGroup} = State) -> #{user_group := UserGroup} = State) ->
trans( trans(
fun() -> fun() ->

View File

@ -26,15 +26,8 @@
, validations/0 , validations/0
]). ]).
-type accept() :: 'application/json' | 'application/x-www-form-urlencoded'. -export([ create/1
-type content_type() :: accept(). , update/2
-reflect_type([ accept/0
, content_type/0
]).
-export([ create/3
, update/4
, authenticate/2 , authenticate/2
, destroy/1 , destroy/1
]). ]).
@ -53,45 +46,55 @@ fields("") ->
fields(get) -> fields(get) ->
[ {method, #{type => get, [ {method, #{type => get,
default => get}} default => post}}
, {headers, fun headers_no_content_type/1}
] ++ common_fields(); ] ++ common_fields();
fields(post) -> fields(post) ->
[ {method, #{type => post, [ {method, #{type => post,
default => get}} default => post}}
, {content_type, fun content_type/1} , {headers, fun headers/1}
] ++ common_fields(). ] ++ common_fields().
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} , {url, fun url/1}
, {accept, fun accept/1}
, {headers, fun headers/1}
, {form_data, fun form_data/1} , {form_data, fun form_data/1}
, {request_timeout, fun request_timeout/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() -> 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(type) -> binary();
url(nullable) -> false; url(nullable) -> false;
url(validate) -> [fun check_url/1]; url(validate) -> [fun check_url/1];
url(_) -> undefined. url(_) -> undefined.
accept(type) -> accept(); headers(type) -> map();
accept(default) -> 'application/json'; headers(converter) ->
accept(_) -> undefined. fun(Headers) ->
maps:merge(default_headers(), transform_header_name(Headers))
content_type(type) -> content_type(); end;
content_type(default) -> 'application/json'; headers(default) -> default_headers();
content_type(_) -> undefined.
headers(type) -> list();
headers(default) -> [];
headers(_) -> undefined. 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(nullable) -> false;
form_data(validate) -> [fun check_form_data/1]; form_data(validate) -> [fun check_form_data/1];
form_data(_) -> undefined. form_data(_) -> undefined.
@ -104,46 +107,41 @@ request_timeout(_) -> undefined.
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
create(ChainID, AuthenticatorName, create(#{ method := Method
#{method := Method, , url := URL
url := URL, , headers := Headers
accept := Accept, , form_data := FormData
headers := Headers, , request_timeout := RequestTimeout
form_data := FormData, , '_unique' := Unique
request_timeout := RequestTimeout} = Config) -> } = 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),
#{path := Path, #{path := Path,
query := Query} = URIMap = parse_url(URL), query := Query} = URIMap = parse_url(URL),
BaseURL = generate_base_url(URIMap), State = #{ method => Method
State = #{method => Method, , path => Path
path => Path, , base_query => cow_qs:parse_qs(list_to_binary(Query))
base_query => cow_qs:parse_qs(list_to_binary(Query)), , headers => normalize_headers(Headers)
accept => Accept, , form_data => maps:to_list(FormData)
content_type => ContentType, , request_timeout => RequestTimeout
headers => NHeaders, },
form_data => NFormData, case emqx_resource:create_local(Unique,
request_timeout => RequestTimeout}, emqx_connector_http,
ResourceID = <<ChainID/binary, "/", AuthenticatorName/binary>>, Config#{base_url => maps:remove(query, URIMap),
case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url => BaseURL}) of pool_type => random}) of
{ok, _} -> {ok, _} ->
{ok, State#{resource_id => ResourceID}}; {ok, State#{resource_id => Unique}};
{error, already_created} -> {error, already_created} ->
{ok, State#{resource_id => ResourceID}}; {ok, State#{resource_id => Unique}};
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
end. end.
update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = State) -> update(Config, State) ->
case emqx_resource:update_local(ResourceID, emqx_connector_http, Config, []) of case create(Config) of
{ok, _} -> {ok, State}; {ok, NewState} ->
{error, Reason} -> {error, Reason} ok = destroy(State),
{ok, NewState};
{error, Reason} ->
{error, Reason}
end. end.
authenticate(#{auth_method := _}, _) -> authenticate(#{auth_method := _}, _) ->
@ -182,26 +180,38 @@ check_url(URL) ->
end. end.
check_form_data(FormData) -> check_form_data(FormData) ->
KVs = binary:split(FormData, [<<"&">>], [global]), lists:any(fun({_, V}) ->
case false =:= lists:any(fun(T) -> T =:= <<>> end, KVs) of not is_binary(V)
true -> end, maps:to_list(FormData)).
NKVs = [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs],
false =:= default_headers() ->
lists:any(fun({K, V}) -> maps:put(<<"content-type">>,
K =:= <<>> orelse V =:= <<>>; <<"application/json">>,
(_) -> default_headers_no_content_type()).
true
end, NKVs); default_headers_no_content_type() ->
false -> #{ <<"accept">> => <<"application/json">>
false , <<"cache-control">> => <<"no-cache">>
end. , <<"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) -> check_ssl_opts(Conf) ->
emqx_connector_http:check_ssl_opts("url", Conf). emqx_connector_http:check_ssl_opts("url", Conf).
preprocess_form_data(FormData) -> check_headers(Conf) ->
KVs = binary:split(FormData, [<<"&">>], [global]), Method = hocon_schema:get_value("method", Conf),
[list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs]. 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) -> parse_url(URL) ->
{ok, URIMap} = emqx_http_lib:uri_parse(URL), {ok, URIMap} = emqx_http_lib:uri_parse(URL),
@ -212,15 +222,12 @@ parse_url(URL) ->
URIMap URIMap
end. end.
generate_base_url(#{scheme := Scheme, normalize_headers(Headers) ->
host := Host, [{atom_to_binary(K), V} || {K, V} <- maps:to_list(Headers)].
port := Port}) ->
iolist_to_binary(io_lib:format("~p://~s:~p", [Scheme, Host, Port])).
generate_request(Credential, #{method := Method, generate_request(Credential, #{method := Method,
path := Path, path := Path,
base_query := BaseQuery, base_query := BaseQuery,
content_type := ContentType,
headers := Headers, headers := Headers,
form_data := FormData0}) -> form_data := FormData0}) ->
FormData = replace_placeholders(FormData0, Credential), FormData = replace_placeholders(FormData0, Credential),
@ -230,6 +237,7 @@ generate_request(Credential, #{method := Method,
{NPath, Headers}; {NPath, Headers};
post -> post ->
NPath = append_query(Path, BaseQuery), NPath = append_query(Path, BaseQuery),
ContentType = proplists:get_value(<<"content-type">>, Headers),
Body = serialize_body(ContentType, FormData), Body = serialize_body(ContentType, FormData),
{NPath, Headers, Body} {NPath, Headers, Body}
end. end.
@ -249,6 +257,8 @@ replace_placeholder(<<"${mqtt-username}">>, Credential) ->
maps:get(username, Credential, undefined); maps:get(username, Credential, undefined);
replace_placeholder(<<"${mqtt-clientid}">>, Credential) -> replace_placeholder(<<"${mqtt-clientid}">>, Credential) ->
maps:get(clientid, Credential, undefined); maps:get(clientid, Credential, undefined);
replace_placeholder(<<"${mqtt-password}">>, Credential) ->
maps:get(password, Credential, undefined);
replace_placeholder(<<"${ip-address}">>, Credential) -> replace_placeholder(<<"${ip-address}">>, Credential) ->
maps:get(peerhost, Credential, undefined); maps:get(peerhost, Credential, undefined);
replace_placeholder(<<"${cert-subject}">>, Credential) -> replace_placeholder(<<"${cert-subject}">>, Credential) ->
@ -272,9 +282,9 @@ qs([], Acc) ->
qs([{K, V} | More], Acc) -> qs([{K, V} | More], Acc) ->
qs(More, [["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] | 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); emqx_json:encode(FormData);
serialize_body('application/x-www-form-urlencoded', FormData) -> serialize_body(<<"application/x-www-form-urlencoded">>, FormData) ->
qs(FormData). qs(FormData).
safely_parse_body(ContentType, Body) -> safely_parse_body(ContentType, Body) ->

View File

@ -22,11 +22,10 @@
-export([ structs/0 -export([ structs/0
, fields/1 , fields/1
, validations/0
]). ]).
-export([ create/3 -export([ create/1
, update/4 , update/2
, authenticate/2 , authenticate/2
, destroy/1 , destroy/1
]). ]).
@ -49,27 +48,24 @@ fields('hmac-based') ->
, {algorithm, {enum, ['hmac-based']}} , {algorithm, {enum, ['hmac-based']}}
, {secret, fun secret/1} , {secret, fun secret/1}
, {secret_base64_encoded, fun secret_base64_encoded/1} , {secret_base64_encoded, fun secret_base64_encoded/1}
, {verify_claims, fun verify_claims/1} ] ++ common_fields();
];
fields('public-key') -> fields('public-key') ->
[ {use_jwks, {enum, [false]}} [ {use_jwks, {enum, [false]}}
, {algorithm, {enum, ['public-key']}} , {algorithm, {enum, ['public-key']}}
, {certificate, fun certificate/1} , {certificate, fun certificate/1}
, {verify_claims, fun verify_claims/1} ] ++ common_fields();
];
fields('jwks') -> fields('jwks') ->
[ {use_jwks, {enum, [true]}} [ {use_jwks, {enum, [true]}}
, {endpoint, fun endpoint/1} , {endpoint, fun endpoint/1}
, {refresh_interval, fun refresh_interval/1} , {refresh_interval, fun refresh_interval/1}
, {verify_claims, fun verify_claims/1}
, {ssl, #{type => hoconsc:union( , {ssl, #{type => hoconsc:union(
[ hoconsc:ref(?MODULE, ssl_enable) [ hoconsc:ref(?MODULE, ssl_enable)
, hoconsc:ref(?MODULE, ssl_disable) , hoconsc:ref(?MODULE, ssl_disable)
]), ]),
default => #{<<"enable">> => false}}} default => #{<<"enable">> => false}}}
]; ] ++ common_fields();
fields(ssl_enable) -> fields(ssl_enable) ->
[ {enable, #{type => true}} [ {enable, #{type => true}}
@ -81,19 +77,19 @@ fields(ssl_enable) ->
]; ];
fields(ssl_disable) -> fields(ssl_disable) ->
[ {enable, #{type => false}} ]; [ {enable, #{type => false}} ].
fields(claim) -> common_fields() ->
[ {"$name", fun expected_claim_value/1} ]. [ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, [jwt]}}
validations() -> , {verify_claims, fun verify_claims/1}
[ {check_verify_claims, fun check_verify_claims/1} ]. ].
secret(type) -> string(); secret(type) -> string();
secret(_) -> undefined. secret(_) -> undefined.
secret_base64_encoded(type) -> boolean(); secret_base64_encoded(type) -> boolean();
secret_base64_encoded(defualt) -> false; secret_base64_encoded(default) -> false;
secret_base64_encoded(_) -> undefined. secret_base64_encoded(_) -> undefined.
certificate(type) -> string(); certificate(type) -> string();
@ -123,29 +119,31 @@ verify(_) -> undefined.
server_name_indication(type) -> string(); server_name_indication(type) -> string();
server_name_indication(_) -> undefined. server_name_indication(_) -> undefined.
verify_claims(type) -> hoconsc:array(hoconsc:ref(claim)); verify_claims(type) -> list();
verify_claims(default) -> []; verify_claims(default) -> #{};
verify_claims(validate) -> [fun check_verify_claims/1];
verify_claims(converter) ->
fun(VerifyClaims) ->
maps:to_list(VerifyClaims)
end;
verify_claims(_) -> undefined. verify_claims(_) -> undefined.
expected_claim_value(type) -> string();
expected_claim_value(_) -> undefined.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
create(_ChainID, _AuthenticatorName, Config) -> create(#{verify_claims := VerifyClaims} = Config) ->
create(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) -> when is_pid(Connector) ->
_ = emqx_authn_jwks_connector:stop(Connector), _ = emqx_authn_jwks_connector:stop(Connector),
create(Config); create(Config);
update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, _) -> update(#{use_jwks := false} = Config, _) ->
create(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) -> when is_pid(Connector) ->
ok = emqx_authn_jwks_connector:update(Connector, Config), ok = emqx_authn_jwks_connector:update(Connector, Config),
case maps:get(verify_cliams, Config, undefined) of 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)}} {ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}}
end; end;
update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) -> update(#{use_jwks := true} = Config, _) ->
create(Config). create(Config).
authenticate(#{auth_method := _}, _) -> authenticate(#{auth_method := _}, _) ->
@ -186,9 +184,6 @@ destroy(_) ->
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
create(#{verify_claims := VerifyClaims} = Config) ->
create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
create2(#{use_jwks := false, create2(#{use_jwks := false,
algorithm := 'hmac-based', algorithm := 'hmac-based',
secret := Secret0, secret := Secret0,

View File

@ -23,8 +23,8 @@
-export([ structs/0, fields/1 ]). -export([ structs/0, fields/1 ]).
-export([ create/3 -export([ create/1
, update/4 , update/2
, authenticate/2 , authenticate/2
, destroy/1 , destroy/1
]). ]).
@ -39,7 +39,7 @@
-type user_id_type() :: clientid | username. -type user_id_type() :: clientid | username.
-type user_group() :: {chain_id(), authenticator_name()}. -type user_group() :: {binary(), binary()}.
-type user_id() :: binary(). -type user_id() :: binary().
-record(user_info, -record(user_info,
@ -81,8 +81,10 @@ mnesia(copy) ->
structs() -> [config]. structs() -> [config].
fields(config) -> fields(config) ->
[ {server_type, {enum, ['built-in-database']}} [ {name, fun emqx_authn_schema:authenticator_name/1}
, {user_id_type, fun user_id_type/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} , {password_hash_algorithm, fun password_hash_algorithm/1}
]; ];
@ -111,25 +113,29 @@ salt_rounds(_) -> undefined.
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
create(ChainID, AuthenticatorName, #{user_id_type := Type, create(#{ user_id_type := Type
password_hash_algorithm := #{name := bcrypt, , password_hash_algorithm := #{name := bcrypt,
salt_rounds := SaltRounds}}) -> salt_rounds := SaltRounds}
, '_unique' := Unique
}) ->
{ok, _} = application:ensure_all_started(bcrypt), {ok, _} = application:ensure_all_started(bcrypt),
State = #{user_group => {ChainID, AuthenticatorName}, State = #{user_group => Unique,
user_id_type => Type, user_id_type => Type,
password_hash_algorithm => bcrypt, password_hash_algorithm => bcrypt,
salt_rounds => SaltRounds}, salt_rounds => SaltRounds},
{ok, State}; {ok, State};
create(ChainID, AuthenticatorName, #{user_id_type := Type, create(#{ user_id_type := Type
password_hash_algorithm := #{name := Name}}) -> , password_hash_algorithm := #{name := Name}
State = #{user_group => {ChainID, AuthenticatorName}, , '_unique' := Unique
}) ->
State = #{user_group => Unique,
user_id_type => Type, user_id_type => Type,
password_hash_algorithm => Name}, password_hash_algorithm => Name},
{ok, State}. {ok, State}.
update(ChainID, AuthenticatorName, Config, _State) -> update(Config, #{user_group := Unique}) ->
create(ChainID, AuthenticatorName, Config). create(Config#{'_unique' => Unique}).
authenticate(#{auth_method := _}, _) -> authenticate(#{auth_method := _}, _) ->
ignore; ignore;
@ -172,8 +178,8 @@ import_users(Filename0, State) ->
{error, {unsupported_file_format, Extension}} {error, {unsupported_file_format, Extension}}
end. end.
add_user(#{<<"user_id">> := UserID, add_user(#{user_id := UserID,
<<"password">> := Password}, password := Password},
#{user_group := UserGroup} = State) -> #{user_group := UserGroup} = State) ->
trans( trans(
fun() -> fun() ->
@ -197,7 +203,7 @@ delete_user(UserID, #{user_group := UserGroup}) ->
end end
end). end).
update_user(UserID, #{<<"password">> := Password}, update_user(UserID, #{password := Password},
#{user_group := UserGroup} = State) -> #{user_group := UserGroup} = State) ->
trans( trans(
fun() -> fun() ->

View File

@ -21,10 +21,12 @@
-behaviour(hocon_schema). -behaviour(hocon_schema).
-export([ structs/0, fields/1 ]). -export([ structs/0
, fields/1
]).
-export([ create/3 -export([ create/1
, update/4 , update/2
, authenticate/2 , authenticate/2
, destroy/1 , destroy/1
]). ]).
@ -36,50 +38,77 @@
structs() -> [config]. structs() -> [config].
fields(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} , {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, {enum, [prefix, suffix]}} , {salt_position, fun salt_position/1}
, {query, fun query/1} , {query, fun query/1}
, {query_timeout, fun query_timeout/1} , {query_timeout, fun query_timeout/1}
] ++ emqx_connector_schema_lib:relational_db_fields() ] ++ 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. 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(type) -> string();
query(nullable) -> false; query(nullable) -> false;
query(_) -> undefined. query(_) -> undefined.
query_timeout(type) -> integer(); query_timeout(type) -> integer();
query_timeout(defualt) -> 5000; query_timeout(default) -> 5000;
query_timeout(_) -> undefined. query_timeout(_) -> undefined.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
create(ChainID, AuthenticatorName, create(#{ password_hash_algorithm := Algorithm
#{query := Query0, , salt_position := SaltPosition
password_hash_algorithm := Algorithm} = Config) -> , query := Query0
, query_timeout := QueryTimeout
, '_unique' := Unique
} = Config) ->
{Query, PlaceHolders} = parse_query(Query0), {Query, PlaceHolders} = parse_query(Query0),
ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, AuthenticatorName])), State = #{password_hash_algorithm => Algorithm,
State = #{query => Query, salt_position => SaltPosition,
query => Query,
placeholders => PlaceHolders, placeholders => PlaceHolders,
password_hash_algorithm => Algorithm}, query_timeout => QueryTimeout},
case emqx_resource:create_local(ResourceID, emqx_connector_mysql, Config) of case emqx_resource:create_local(Unique, emqx_connector_mysql, Config) of
{ok, _} -> {ok, _} ->
{ok, State#{resource_id => ResourceID}}; {ok, State#{resource_id => Unique}};
{error, already_created} -> {error, already_created} ->
{ok, State#{resource_id => ResourceID}}; {ok, State#{resource_id => Unique}};
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
end. end.
update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = State) -> update(Config, State) ->
case emqx_resource:update_local(ResourceID, emqx_connector_mysql, Config, []) of case create(Config) of
{ok, _} -> {ok, State}; {ok, NewState} ->
{error, Reason} -> {error, Reason} ok = destroy(State),
{ok, NewState};
{error, Reason} ->
{error, Reason}
end. end.
authenticate(#{auth_method := _}, _) -> authenticate(#{auth_method := _}, _) ->

View File

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

View File

@ -23,8 +23,8 @@
-export([ structs/0, fields/1 ]). -export([ structs/0, fields/1 ]).
-export([ create/3 -export([ create/1
, update/4 , update/2
, authenticate/2 , authenticate/2
, destroy/1 , destroy/1
]). ]).
@ -36,7 +36,9 @@
structs() -> [config]. structs() -> [config].
fields(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} , {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, {enum, [prefix, suffix]}} , {salt_position, {enum, [prefix, suffix]}}
, {query, fun query/1} , {query, fun query/1}
@ -54,26 +56,32 @@ query(_) -> undefined.
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
create(ChainID, ServiceName, #{query := Query0, create(#{ query := Query0
password_hash_algorithm := Algorithm} = Config) -> , password_hash_algorithm := Algorithm
, salt_position := SaltPosition
, '_unique' := Unique
} = Config) ->
{Query, PlaceHolders} = parse_query(Query0), {Query, PlaceHolders} = parse_query(Query0),
ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])),
State = #{query => Query, State = #{query => Query,
placeholders => PlaceHolders, placeholders => PlaceHolders,
password_hash_algorithm => Algorithm}, password_hash_algorithm => Algorithm,
case emqx_resource:create_local(ResourceID, emqx_connector_pgsql, Config) of salt_position => SaltPosition},
case emqx_resource:create_local(Unique, emqx_connector_pgsql, Config) of
{ok, _} -> {ok, _} ->
{ok, State#{resource_id => ResourceID}}; {ok, State#{resource_id => Unique}};
{error, already_created} -> {error, already_created} ->
{ok, State#{resource_id => ResourceID}}; {ok, State#{resource_id => Unique}};
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
end. end.
update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> update(Config, State) ->
case emqx_resource:update_local(ResourceID, emqx_connector_pgsql, Config, []) of case create(Config) of
{ok, _} -> {ok, State}; {ok, NewState} ->
{error, Reason} -> {error, Reason} ok = destroy(State),
{ok, NewState};
{error, Reason} ->
{error, Reason}
end. end.
authenticate(#{auth_method := _}, _) -> authenticate(#{auth_method := _}, _) ->

View File

@ -54,34 +54,44 @@ t_authenticator(_) ->
AuthenticatorName1 = <<"myauthenticator1">>, AuthenticatorName1 = <<"myauthenticator1">>,
AuthenticatorConfig1 = #{name => AuthenticatorName1, AuthenticatorConfig1 = #{name => AuthenticatorName1,
mechanism => 'password-based', mechanism => 'password-based',
config => #{ server_type => 'built-in-database',
server_type => 'built-in-database', user_id_type => username,
user_id_type => username, password_hash_algorithm => #{
password_hash_algorithm => #{ name => sha256
name => sha256 }},
}}}, {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), ?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)),
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName1)), ?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), ?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?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">>, AuthenticatorName2 = <<"myauthenticator2">>,
AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2}, AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2},
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"abcdef">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3),
?assertMatch({ok, #{id := ?CHAIN, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(?CHAIN)), ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)),
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName2)), {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"fedcba">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}),
?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)),
?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?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, ID2, 1)),
?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 1)), ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?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, AuthenticatorName2, 3)), ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 0)),
?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 0)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)),
?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)),
ok. ok.

View File

@ -39,15 +39,14 @@ end_per_suite(_) ->
t_jwt_authenticator(_) -> t_jwt_authenticator(_) ->
AuthenticatorName = <<"myauthenticator">>, AuthenticatorName = <<"myauthenticator">>,
Config = #{use_jwks => false, Config = #{name => AuthenticatorName,
mechanism => jwt,
use_jwks => false,
algorithm => 'hmac-based', algorithm => 'hmac-based',
secret => <<"abcdef">>, secret => <<"abcdef">>,
secret_base64_encoded => false, secret_base64_encoded => false,
verify_claims => []}, verify_claims => []},
AuthenticatorConfig = #{name => AuthenticatorName, {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
mechanism => jwt,
config => Config},
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
Payload = #{<<"username">> => <<"myuser">>}, Payload = #{<<"username">> => <<"myuser">>},
JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
@ -62,11 +61,11 @@ t_jwt_authenticator(_) ->
%% secret_base64_encoded %% secret_base64_encoded
Config2 = Config#{secret => base64:encode(<<"abcdef">>), Config2 = Config#{secret => base64:encode(<<"abcdef">>),
secret_base64_encoded => true}, 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)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, 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, ok}, ?AUTH:authenticate(ClientInfo, ok)),
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, 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}, ClientInfo8 = ClientInfo#{password => JWS8},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)), ?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. ok.
t_jwt_authenticator2(_) -> t_jwt_authenticator2(_) ->
@ -117,14 +116,13 @@ t_jwt_authenticator2(_) ->
PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])),
PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])),
AuthenticatorName = <<"myauthenticator">>, AuthenticatorName = <<"myauthenticator">>,
Config = #{use_jwks => false, Config = #{name => AuthenticatorName,
mechanism => jwt,
use_jwks => false,
algorithm => 'public-key', algorithm => 'public-key',
certificate => PublicKey, certificate => PublicKey,
verify_claims => []}, verify_claims => []},
AuthenticatorConfig = #{name => AuthenticatorName, {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
mechanism => jwt,
config => Config},
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
Payload = #{<<"username">> => <<"myuser">>}, Payload = #{<<"username">> => <<"myuser">>},
JWS = generate_jws('public-key', Payload, PrivateKey), JWS = generate_jws('public-key', Payload, PrivateKey),
@ -133,7 +131,7 @@ t_jwt_authenticator2(_) ->
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, 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. ok.
generate_jws('hmac-based', Payload, Secret) -> generate_jws('hmac-based', Payload, Secret) ->

View File

@ -41,18 +41,17 @@ t_mnesia_authenticator(_) ->
AuthenticatorName = <<"myauthenticator">>, AuthenticatorName = <<"myauthenticator">>,
AuthenticatorConfig = #{name => AuthenticatorName, AuthenticatorConfig = #{name => AuthenticatorName,
mechanism => 'password-based', mechanism => 'password-based',
config => #{ server_type => 'built-in-database',
server_type => 'built-in-database', user_id_type => username,
user_id_type => username, password_hash_algorithm => #{
password_hash_algorithm => #{ name => sha256
name => sha256 }},
}}}, {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
UserInfo = #{<<"user_id">> => <<"myuser">>, UserInfo = #{user_id => <<"myuser">>,
<<"password">> => <<"mypass">>}, password => <<"mypass">>},
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
ClientInfo = #{zone => external, ClientInfo = #{zone => external,
username => <<"myuser">>, username => <<"myuser">>,
@ -69,40 +68,39 @@ t_mnesia_authenticator(_) ->
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)),
?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)),
UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, UserInfo2 = UserInfo#{password => <<"mypass2">>},
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, AuthenticatorName, <<"myuser">>, UserInfo2)), ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)),
?assertEqual(ok, ?AUTH:delete_user(?CHAIN, AuthenticatorName, <<"myuser">>)), ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
ok. ok.
t_import(_) -> t_import(_) ->
AuthenticatorName = <<"myauthenticator">>, AuthenticatorName = <<"myauthenticator">>,
AuthenticatorConfig = #{name => AuthenticatorName, AuthenticatorConfig = #{name => AuthenticatorName,
mechanism => 'password-based', mechanism => 'password-based',
config => #{ server_type => 'built-in-database',
server_type => 'built-in-database', user_id_type => username,
user_id_type => username, password_hash_algorithm => #{
password_hash_algorithm => #{ name => sha256
name => sha256 }},
}}}, {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
Dir = code:lib_dir(emqx_authn, test), 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, ID, filename:join([Dir, "data/user-credentials.json"]))),
?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))),
?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser1">>)), ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)),
?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser3">>)), ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)),
ClientInfo1 = #{username => <<"myuser1">>, ClientInfo1 = #{username => <<"myuser1">>,
password => <<"mypassword1">>}, password => <<"mypassword1">>},
@ -110,50 +108,48 @@ t_import(_) ->
ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, ClientInfo2 = ClientInfo1#{username => <<"myuser3">>,
password => <<"mypassword3">>}, password => <<"mypassword3">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
ok. ok.
t_multi_mnesia_authenticator(_) -> t_multi_mnesia_authenticator(_) ->
AuthenticatorName1 = <<"myauthenticator1">>, AuthenticatorName1 = <<"myauthenticator1">>,
AuthenticatorConfig1 = #{name => AuthenticatorName1, AuthenticatorConfig1 = #{name => AuthenticatorName1,
mechanism => 'password-based', mechanism => 'password-based',
config => #{ server_type => 'built-in-database',
server_type => 'built-in-database', user_id_type => username,
user_id_type => username, password_hash_algorithm => #{
password_hash_algorithm => #{ name => sha256
name => sha256 }},
}}},
AuthenticatorName2 = <<"myauthenticator2">>, AuthenticatorName2 = <<"myauthenticator2">>,
AuthenticatorConfig2 = #{name => AuthenticatorName2, AuthenticatorConfig2 = #{name => AuthenticatorName2,
mechanism => 'password-based', mechanism => 'password-based',
config => #{ server_type => 'built-in-database',
server_type => 'built-in-database', user_id_type => clientid,
user_id_type => clientid, password_hash_algorithm => #{
password_hash_algorithm => #{ name => sha256
name => sha256 }},
}}}, {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)),
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?assertEqual({ok, #{user_id => <<"myuser">>}},
?AUTH:add_user(?CHAIN, AuthenticatorName1, ?AUTH:add_user(?CHAIN, ID1,
#{<<"user_id">> => <<"myuser">>, #{user_id => <<"myuser">>,
<<"password">> => <<"mypass1">>})), password => <<"mypass1">>})),
?assertEqual({ok, #{user_id => <<"myclient">>}}, ?assertEqual({ok, #{user_id => <<"myclient">>}},
?AUTH:add_user(?CHAIN, AuthenticatorName2, ?AUTH:add_user(?CHAIN, ID2,
#{<<"user_id">> => <<"myclient">>, #{user_id => <<"myclient">>,
<<"password">> => <<"mypass2">>})), password => <<"mypass2">>})),
ClientInfo1 = #{username => <<"myuser">>, ClientInfo1 = #{username => <<"myuser">>,
clientid => <<"myclient">>, clientid => <<"myclient">>,
password => <<"mypass1">>}, password => <<"mypass1">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), ?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)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)),
ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
ok. ok.

View File

@ -53,14 +53,15 @@ fields("") ->
[{config, #{type => hoconsc:ref(?MODULE, config)}}]; [{config, #{type => hoconsc:ref(?MODULE, config)}}];
fields(config) -> fields(config) ->
[ {base_url, fun base_url/1} [ {base_url, fun base_url/1}
, {connect_timeout, fun connect_timeout/1} , {connect_timeout, fun connect_timeout/1}
, {max_retries, fun max_retries/1} , {max_retries, fun max_retries/1}
, {retry_interval, fun retry_interval/1} , {retry_interval, fun retry_interval/1}
, {pool_type, fun pool_type/1} , {pool_type, fun pool_type/1}
, {pool_size, fun pool_size/1} , {pool_size, fun pool_size/1}
, {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), , {enable_pipelining, fun enable_pipelining/1}
default => #{}}} , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts),
default => #{}}}
]; ];
fields(ssl_opts) -> fields(ssl_opts) ->
@ -101,6 +102,10 @@ pool_size(type) -> non_neg_integer();
pool_size(default) -> 8; pool_size(default) -> 8;
pool_size(_) -> undefined. pool_size(_) -> undefined.
enable_pipelining(type) -> boolean();
enable_pipelining(default) -> true;
enable_pipelining(_) -> undefined.
cacertfile(type) -> string(); cacertfile(type) -> string();
cacertfile(nullable) -> true; cacertfile(nullable) -> true;
cacertfile(_) -> undefined. cacertfile(_) -> undefined.
@ -147,7 +152,7 @@ on_start(InstId, #{base_url := #{scheme := Scheme,
, {pool_type, PoolType} , {pool_type, PoolType}
, {pool_size, PoolSize} , {pool_size, PoolSize}
, {transport, Transport} , {transport, Transport}
, {transport, NTransportOpts}], , {transport_opts, NTransportOpts}],
PoolName = emqx_plugin_libs_pool:pool_name(InstId), PoolName = emqx_plugin_libs_pool:pool_name(InstId),
{ok, _} = ehttpc_sup:start_pool(PoolName, PoolOpts), {ok, _} = ehttpc_sup:start_pool(PoolName, PoolOpts),
{ok, #{pool_name => PoolName, {ok, #{pool_name => PoolName,

View File

@ -43,7 +43,7 @@
{deps, {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 [ {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"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}