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

@ -4,10 +4,8 @@ emqx_authn: {
# {
# name: "authenticator1"
# mechanism: password-based
# config: {
# server_type: built-in-database
# user_id_type: clientid
# }
# }
]
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -26,8 +26,8 @@
, fields/1
]).
-export([ create/3
, update/4
-export([ create/1
, update/2
, authenticate/2
, destroy/1
]).
@ -71,7 +71,9 @@ mnesia(copy) ->
structs() -> [config].
fields(config) ->
[ {server_type, fun server_type/1}
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, [scram]}}
, {server_type, fun server_type/1}
, {algorithm, fun algorithm/1}
, {iteration_count, fun iteration_count/1}
].
@ -80,7 +82,7 @@ server_type(type) -> hoconsc:enum(['built-in-database']);
server_type(default) -> 'built-in-database';
server_type(_) -> undefined.
algorithm(type) -> hoconsc:enum([sha256, sha256]);
algorithm(type) -> hoconsc:enum([sha256, sha512]);
algorithm(default) -> sha256;
algorithm(_) -> undefined.
@ -92,15 +94,17 @@ 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,
@ -129,8 +133,8 @@ destroy(#{user_group := UserGroup}) ->
end).
%% TODO: binary to atom
add_user(#{<<"user_id">> := UserID,
<<"password">> := Password}, #{user_group := UserGroup} = State) ->
add_user(#{user_id := UserID,
password := Password}, #{user_group := UserGroup} = State) ->
trans(
fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
@ -153,7 +157,7 @@ delete_user(UserID, #{user_group := UserGroup}) ->
end
end).
update_user(UserID, #{<<"password">> := Password},
update_user(UserID, #{password := Password},
#{user_group := UserGroup} = State) ->
trans(
fun() ->

View File

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

View File

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

View File

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

View File

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

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([ 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 := _}, _) ->

View File

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

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@
{deps,
[ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps
, {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.7"}}}
, {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.8"}}}
, {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}