Merge pull request #5478 from tigercl/feat/authn-hot-config
feat(authn): support superuser and hot config
This commit is contained in:
commit
ee57df416a
|
@ -27,9 +27,14 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-spec(authenticate(emqx_types:clientinfo()) ->
|
-spec(authenticate(emqx_types:clientinfo()) ->
|
||||||
ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}).
|
{ok, map()} | {ok, map(), binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}).
|
||||||
authenticate(Credential) ->
|
authenticate(Credential) ->
|
||||||
run_hooks('client.authenticate', [Credential], ok).
|
case run_hooks('client.authenticate', [Credential], {ok, #{superuser => false}}) of
|
||||||
|
ok ->
|
||||||
|
{ok, #{superuser => false}};
|
||||||
|
Other ->
|
||||||
|
Other
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc Check Authorization
|
%% @doc Check Authorization
|
||||||
-spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic())
|
-spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic())
|
||||||
|
|
|
@ -1299,14 +1299,17 @@ authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properti
|
||||||
{error, ?RC_BAD_AUTHENTICATION_METHOD}
|
{error, ?RC_BAD_AUTHENTICATION_METHOD}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) ->
|
do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = ClientInfo} = Channel) ->
|
||||||
Properties = #{'Authentication-Method' => AuthMethod},
|
Properties = #{'Authentication-Method' => AuthMethod},
|
||||||
case emqx_access_control:authenticate(Credential) of
|
case emqx_access_control:authenticate(Credential) of
|
||||||
ok ->
|
{ok, Result} ->
|
||||||
{ok, Properties, Channel#channel{auth_cache = #{}}};
|
{ok, Properties,
|
||||||
{ok, AuthData} ->
|
Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)},
|
||||||
|
auth_cache = #{}}};
|
||||||
|
{ok, Result, AuthData} ->
|
||||||
{ok, Properties#{'Authentication-Data' => AuthData},
|
{ok, Properties#{'Authentication-Data' => AuthData},
|
||||||
Channel#channel{auth_cache = #{}}};
|
Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)},
|
||||||
|
auth_cache = #{}}};
|
||||||
{continue, AuthCache} ->
|
{continue, AuthCache} ->
|
||||||
{continue, Properties, Channel#channel{auth_cache = AuthCache}};
|
{continue, Properties, Channel#channel{auth_cache = AuthCache}};
|
||||||
{continue, AuthData, AuthCache} ->
|
{continue, AuthData, AuthCache} ->
|
||||||
|
@ -1316,10 +1319,10 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) ->
|
||||||
{error, emqx_reason_codes:connack_error(Reason)}
|
{error, emqx_reason_codes:connack_error(Reason)}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
do_authenticate(Credential, Channel) ->
|
do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) ->
|
||||||
case emqx_access_control:authenticate(Credential) of
|
case emqx_access_control:authenticate(Credential) of
|
||||||
ok ->
|
{ok, #{superuser := Superuser}} ->
|
||||||
{ok, #{}, Channel};
|
{ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, emqx_reason_codes:connack_error(Reason)}
|
{error, emqx_reason_codes:connack_error(Reason)}
|
||||||
end.
|
end.
|
||||||
|
|
|
@ -101,9 +101,9 @@
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%% raw_config() is the config that is NOT parsed and tranlated by hocon schema
|
%% raw_config() is the config that is NOT parsed and tranlated by hocon schema
|
||||||
-type raw_config() :: #{binary() => term()} | undefined.
|
-type raw_config() :: #{binary() => term()} | list() | undefined.
|
||||||
%% config() is the config that is parsed and tranlated by hocon schema
|
%% config() is the config that is parsed and tranlated by hocon schema
|
||||||
-type config() :: #{atom() => term()} | undefined.
|
-type config() :: #{atom() => term()} | list() | undefined.
|
||||||
-type app_envs() :: [proplists:property()].
|
-type app_envs() :: [proplists:property()].
|
||||||
|
|
||||||
%% @doc For the given path, get root value enclosed in a single-key map.
|
%% @doc For the given path, get root value enclosed in a single-key map.
|
||||||
|
|
|
@ -33,7 +33,7 @@ end_per_suite(_Config) ->
|
||||||
emqx_ct_helpers:stop_apps([]).
|
emqx_ct_helpers:stop_apps([]).
|
||||||
|
|
||||||
t_authenticate(_) ->
|
t_authenticate(_) ->
|
||||||
?assertMatch(ok, emqx_access_control:authenticate(clientinfo())).
|
?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())).
|
||||||
|
|
||||||
t_authorize(_) ->
|
t_authorize(_) ->
|
||||||
Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>),
|
Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>),
|
||||||
|
|
|
@ -181,7 +181,7 @@ init_per_suite(Config) ->
|
||||||
%% Access Control Meck
|
%% Access Control Meck
|
||||||
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
|
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
|
||||||
ok = meck:expect(emqx_access_control, authenticate,
|
ok = meck:expect(emqx_access_control, authenticate,
|
||||||
fun(_) -> ok end),
|
fun(_) -> {ok, #{superuser => false}} end),
|
||||||
ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end),
|
ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end),
|
||||||
%% Broker Meck
|
%% Broker Meck
|
||||||
ok = meck:new(emqx_broker, [passthrough, no_history, no_link]),
|
ok = meck:new(emqx_broker, [passthrough, no_history, no_link]),
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
user_id,password_hash,salt
|
user_id,password_hash,salt,superuser
|
||||||
myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235
|
myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true
|
||||||
myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139
|
myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false
|
||||||
|
|
|
|
@ -2,11 +2,13 @@
|
||||||
{
|
{
|
||||||
"user_id":"myuser1",
|
"user_id":"myuser1",
|
||||||
"password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
|
"password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
|
||||||
"salt": "e378187547bf2d6f0545a3f441aa4d8a"
|
"salt": "e378187547bf2d6f0545a3f441aa4d8a",
|
||||||
|
"superuser": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"user_id":"myuser2",
|
"user_id":"myuser2",
|
||||||
"password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
|
"password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
|
||||||
"salt": "6d3f9bd5b54d94b98adbcfe10b6d181f"
|
"salt": "6d3f9bd5b54d94b98adbcfe10b6d181f",
|
||||||
|
"superuser": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
{ id :: binary()
|
{ id :: binary()
|
||||||
, name :: binary()
|
, name :: binary()
|
||||||
, provider :: module()
|
, provider :: module()
|
||||||
, config :: map()
|
|
||||||
, state :: map()
|
, state :: map()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,17 @@
|
||||||
|
|
||||||
-module(emqx_authn).
|
-module(emqx_authn).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-behaviour(emqx_config_handler).
|
||||||
|
|
||||||
-include("emqx_authn.hrl").
|
-include("emqx_authn.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-export([ pre_config_update/2
|
||||||
|
, post_config_update/3
|
||||||
|
, update_config/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ enable/0
|
-export([ enable/0
|
||||||
, disable/0
|
, disable/0
|
||||||
|
@ -25,6 +35,10 @@
|
||||||
|
|
||||||
-export([authenticate/2]).
|
-export([authenticate/2]).
|
||||||
|
|
||||||
|
-export([ start_link/0
|
||||||
|
, stop/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ create_chain/1
|
-export([ create_chain/1
|
||||||
, delete_chain/1
|
, delete_chain/1
|
||||||
, lookup_chain/1
|
, lookup_chain/1
|
||||||
|
@ -35,7 +49,7 @@
|
||||||
, update_or_create_authenticator/3
|
, update_or_create_authenticator/3
|
||||||
, lookup_authenticator/2
|
, lookup_authenticator/2
|
||||||
, list_authenticators/1
|
, list_authenticators/1
|
||||||
, move_authenticator_to_the_nth/3
|
, move_authenticator/3
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([ import_users/3
|
-export([ import_users/3
|
||||||
|
@ -46,34 +60,138 @@
|
||||||
, list_users/2
|
, list_users/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([mnesia/1]).
|
%% gen_server callbacks
|
||||||
|
-export([ init/1
|
||||||
-boot_mnesia({mnesia, [boot]}).
|
, handle_call/3
|
||||||
-copy_mnesia({mnesia, [copy]}).
|
, handle_cast/2
|
||||||
|
, handle_info/2
|
||||||
|
, terminate/2
|
||||||
|
, code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
-define(CHAIN_TAB, emqx_authn_chain).
|
-define(CHAIN_TAB, emqx_authn_chain).
|
||||||
|
|
||||||
-rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}).
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Mnesia bootstrap
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
%% @doc Create or replicate tables.
|
pre_config_update({enable, Enable}, _OldConfig) ->
|
||||||
-spec(mnesia(boot) -> ok).
|
{ok, Enable};
|
||||||
mnesia(boot) ->
|
pre_config_update({create_authenticator, Config}, OldConfig) ->
|
||||||
%% Optimize storage
|
{ok, OldConfig ++ [Config]};
|
||||||
StoreProps = [{ets, [{read_concurrency, true}]}],
|
pre_config_update({delete_authenticator, ID}, OldConfig) ->
|
||||||
%% Chain table
|
case lookup_authenticator(?CHAIN, ID) of
|
||||||
ok = ekka_mnesia:create_table(?CHAIN_TAB, [
|
{error, Reason} -> {error, Reason};
|
||||||
{ram_copies, [node()]},
|
{ok, #{name := Name}} ->
|
||||||
{record_name, chain},
|
NewConfig = lists:filter(fun(#{<<"name">> := N}) ->
|
||||||
{local_content, true},
|
N =/= Name
|
||||||
{attributes, record_info(fields, chain)},
|
end, OldConfig),
|
||||||
{storage_properties, StoreProps}]);
|
{ok, NewConfig}
|
||||||
|
end;
|
||||||
|
pre_config_update({update_authenticator, ID, Config}, OldConfig) ->
|
||||||
|
case lookup_authenticator(?CHAIN, ID) of
|
||||||
|
{error, Reason} -> {error, Reason};
|
||||||
|
{ok, #{name := Name}} ->
|
||||||
|
NewConfig = lists:map(fun(#{<<"name">> := N} = C) ->
|
||||||
|
case N =:= Name of
|
||||||
|
true -> Config;
|
||||||
|
false -> C
|
||||||
|
end
|
||||||
|
end, OldConfig),
|
||||||
|
{ok, NewConfig}
|
||||||
|
end;
|
||||||
|
pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) ->
|
||||||
|
case lookup_authenticator(?CHAIN, ID) of
|
||||||
|
{error, _Reason} -> OldConfig ++ [Config];
|
||||||
|
{ok, #{name := Name}} ->
|
||||||
|
NewConfig = lists:map(fun(#{<<"name">> := N} = C) ->
|
||||||
|
case N =:= Name of
|
||||||
|
true -> Config;
|
||||||
|
false -> C
|
||||||
|
end
|
||||||
|
end, OldConfig),
|
||||||
|
{ok, NewConfig}
|
||||||
|
end;
|
||||||
|
pre_config_update({move_authenticator, ID, Position}, OldConfig) ->
|
||||||
|
case lookup_authenticator(?CHAIN, ID) of
|
||||||
|
{error, Reason} -> {error, Reason};
|
||||||
|
{ok, #{name := Name}} ->
|
||||||
|
{ok, Found, Part1, Part2} = split_by_name(Name, OldConfig),
|
||||||
|
case Position of
|
||||||
|
<<"top">> ->
|
||||||
|
{ok, [Found | Part1] ++ Part2};
|
||||||
|
<<"bottom">> ->
|
||||||
|
{ok, Part1 ++ Part2 ++ [Found]};
|
||||||
|
Before ->
|
||||||
|
case binary:split(Before, <<":">>, [global]) of
|
||||||
|
[<<"before">>, ID0] ->
|
||||||
|
case lookup_authenticator(?CHAIN, ID0) of
|
||||||
|
{error, Reason} -> {error, Reason};
|
||||||
|
{ok, #{name := Name1}} ->
|
||||||
|
{ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 ++ Part2),
|
||||||
|
{ok, NPart1 ++ [Found, NFound | NPart2]}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, {invalid_parameter, position}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
mnesia(copy) ->
|
post_config_update({enable, true}, _NewConfig, _OldConfig) ->
|
||||||
ok = ekka_mnesia:copy_table(?CHAIN_TAB, ram_copies).
|
emqx_authn:enable();
|
||||||
|
post_config_update({enable, false}, _NewConfig, _OldConfig) ->
|
||||||
|
emqx_authn:disable();
|
||||||
|
post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig) ->
|
||||||
|
case lists:filter(
|
||||||
|
fun(#{name := N}) ->
|
||||||
|
N =:= Name
|
||||||
|
end, NewConfig) of
|
||||||
|
[Config] ->
|
||||||
|
create_authenticator(?CHAIN, Config);
|
||||||
|
[_Config | _] ->
|
||||||
|
{error, name_has_be_used}
|
||||||
|
end;
|
||||||
|
post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig) ->
|
||||||
|
case delete_authenticator(?CHAIN, ID) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, Reason} -> throw(Reason)
|
||||||
|
end;
|
||||||
|
post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) ->
|
||||||
|
case lists:filter(
|
||||||
|
fun(#{name := N}) ->
|
||||||
|
N =:= Name
|
||||||
|
end, NewConfig) of
|
||||||
|
[Config] ->
|
||||||
|
update_authenticator(?CHAIN, ID, Config);
|
||||||
|
[_Config | _] ->
|
||||||
|
{error, name_has_be_used}
|
||||||
|
end;
|
||||||
|
post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) ->
|
||||||
|
case lists:filter(
|
||||||
|
fun(#{name := N}) ->
|
||||||
|
N =:= Name
|
||||||
|
end, NewConfig) of
|
||||||
|
[Config] ->
|
||||||
|
update_or_create_authenticator(?CHAIN, ID, Config);
|
||||||
|
[_Config | _] ->
|
||||||
|
{error, name_has_be_used}
|
||||||
|
end;
|
||||||
|
post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig) ->
|
||||||
|
NPosition = case Position of
|
||||||
|
<<"top">> -> top;
|
||||||
|
<<"bottom">> -> bottom;
|
||||||
|
Before ->
|
||||||
|
case binary:split(Before, <<":">>, [global]) of
|
||||||
|
[<<"before">>, ID0] ->
|
||||||
|
{before, ID0};
|
||||||
|
_ ->
|
||||||
|
{error, {invalid_parameter, position}}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
move_authenticator(?CHAIN, ID, NPosition).
|
||||||
|
|
||||||
|
update_config(Path, ConfigRequest) ->
|
||||||
|
emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).
|
||||||
|
|
||||||
enable() ->
|
enable() ->
|
||||||
case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of
|
case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of
|
||||||
|
@ -94,7 +212,7 @@ is_enabled() ->
|
||||||
end, Callbacks).
|
end, Callbacks).
|
||||||
|
|
||||||
authenticate(Credential, _AuthResult) ->
|
authenticate(Credential, _AuthResult) ->
|
||||||
case mnesia:dirty_read(?CHAIN_TAB, ?CHAIN) of
|
case ets:lookup(?CHAIN_TAB, ?CHAIN) of
|
||||||
[#chain{authenticators = Authenticators}] ->
|
[#chain{authenticators = Authenticators}] ->
|
||||||
do_authenticate(Authenticators, Credential);
|
do_authenticate(Authenticators, Credential);
|
||||||
[] ->
|
[] ->
|
||||||
|
@ -108,162 +226,48 @@ do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | Mo
|
||||||
ignore ->
|
ignore ->
|
||||||
do_authenticate(More, Credential);
|
do_authenticate(More, Credential);
|
||||||
Result ->
|
Result ->
|
||||||
%% ok
|
%% {ok, Extra}
|
||||||
%% {ok, AuthData}
|
%% {ok, Extra, AuthData}
|
||||||
|
%% {ok, MetaData}
|
||||||
%% {continue, AuthCache}
|
%% {continue, AuthCache}
|
||||||
%% {continue, AuthData, AuthCache}
|
%% {continue, AuthData, AuthCache}
|
||||||
%% {error, Reason}
|
%% {error, Reason}
|
||||||
{stop, Result}
|
{stop, Result}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
stop() ->
|
||||||
|
gen_server:stop(?MODULE).
|
||||||
|
|
||||||
create_chain(#{id := ID}) ->
|
create_chain(#{id := ID}) ->
|
||||||
trans(
|
gen_server:call(?MODULE, {create_chain, ID}).
|
||||||
fun() ->
|
|
||||||
case mnesia:read(?CHAIN_TAB, ID, write) of
|
|
||||||
[] ->
|
|
||||||
Chain = #chain{id = ID,
|
|
||||||
authenticators = [],
|
|
||||||
created_at = erlang:system_time(millisecond)},
|
|
||||||
mnesia:write(?CHAIN_TAB, Chain, write),
|
|
||||||
{ok, serialize_chain(Chain)};
|
|
||||||
[_ | _] ->
|
|
||||||
{error, {already_exists, {chain, ID}}}
|
|
||||||
end
|
|
||||||
end).
|
|
||||||
|
|
||||||
delete_chain(ID) ->
|
delete_chain(ID) ->
|
||||||
trans(
|
gen_server:call(?MODULE, {delete_chain, ID}).
|
||||||
fun() ->
|
|
||||||
case mnesia:read(?CHAIN_TAB, ID, write) of
|
|
||||||
[] ->
|
|
||||||
{error, {not_found, {chain, ID}}};
|
|
||||||
[#chain{authenticators = Authenticators}] ->
|
|
||||||
_ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators],
|
|
||||||
mnesia:delete(?CHAIN_TAB, ID, write)
|
|
||||||
end
|
|
||||||
end).
|
|
||||||
|
|
||||||
lookup_chain(ID) ->
|
lookup_chain(ID) ->
|
||||||
case mnesia:dirty_read(?CHAIN_TAB, ID) of
|
gen_server:call(?MODULE, {lookup_chain, ID}).
|
||||||
[] ->
|
|
||||||
{error, {not_found, {chain, ID}}};
|
|
||||||
[Chain] ->
|
|
||||||
{ok, serialize_chain(Chain)}
|
|
||||||
end.
|
|
||||||
|
|
||||||
list_chains() ->
|
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} = Config) ->
|
create_authenticator(ChainID, Config) ->
|
||||||
UpdateFun =
|
gen_server:call(?MODULE, {create_authenticator, ChainID, Config}).
|
||||||
fun(Chain = #chain{authenticators = Authenticators}) ->
|
|
||||||
case lists:keymember(Name, 2, Authenticators) of
|
|
||||||
true ->
|
|
||||||
{error, name_has_be_used};
|
|
||||||
false ->
|
|
||||||
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}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
update_chain(ChainID, UpdateFun).
|
|
||||||
|
|
||||||
delete_authenticator(ChainID, AuthenticatorID) ->
|
delete_authenticator(ChainID, AuthenticatorID) ->
|
||||||
UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
|
gen_server:call(?MODULE, {delete_authenticator, ChainID, AuthenticatorID}).
|
||||||
case lists:keytake(AuthenticatorID, 1, Authenticators) of
|
|
||||||
false ->
|
|
||||||
{error, {not_found, {authenticator, AuthenticatorID}}};
|
|
||||||
{value, {_, _, Authenticator}, NAuthenticators} ->
|
|
||||||
_ = do_delete_authenticator(Authenticator),
|
|
||||||
NChain = Chain#chain{authenticators = NAuthenticators},
|
|
||||||
mnesia:write(?CHAIN_TAB, NChain, write)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
update_chain(ChainID, UpdateFun).
|
|
||||||
|
|
||||||
update_authenticator(ChainID, AuthenticatorID, Config) ->
|
update_authenticator(ChainID, AuthenticatorID, Config) ->
|
||||||
do_update_authenticator(ChainID, AuthenticatorID, Config, false).
|
gen_server:call(?MODULE, {update_authenticator, ChainID, AuthenticatorID, Config}).
|
||||||
|
|
||||||
update_or_create_authenticator(ChainID, AuthenticatorID, Config) ->
|
update_or_create_authenticator(ChainID, AuthenticatorID, Config) ->
|
||||||
do_update_authenticator(ChainID, AuthenticatorID, Config, true).
|
gen_server:call(?MODULE, {update_or_create_authenticator, ChainID, AuthenticatorID, Config}).
|
||||||
|
|
||||||
do_update_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) ->
|
|
||||||
UpdateFun = fun(Chain = #chain{authenticators = 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(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, AuthenticatorID) ->
|
lookup_authenticator(ChainID, AuthenticatorID) ->
|
||||||
case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
|
case ets:lookup(?CHAIN_TAB, ChainID) of
|
||||||
[] ->
|
[] ->
|
||||||
{error, {not_found, {chain, ChainID}}};
|
{error, {not_found, {chain, ChainID}}};
|
||||||
[#chain{authenticators = Authenticators}] ->
|
[#chain{authenticators = Authenticators}] ->
|
||||||
|
@ -276,42 +280,180 @@ lookup_authenticator(ChainID, AuthenticatorID) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
list_authenticators(ChainID) ->
|
list_authenticators(ChainID) ->
|
||||||
case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
|
case ets:lookup(?CHAIN_TAB, ChainID) of
|
||||||
[] ->
|
[] ->
|
||||||
{error, {not_found, {chain, ChainID}}};
|
{error, {not_found, {chain, ChainID}}};
|
||||||
[#chain{authenticators = Authenticators}] ->
|
[#chain{authenticators = Authenticators}] ->
|
||||||
{ok, serialize_authenticators(Authenticators)}
|
{ok, serialize_authenticators(Authenticators)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) ->
|
move_authenticator(ChainID, AuthenticatorID, Position) ->
|
||||||
UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
|
gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, Position}).
|
||||||
case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) of
|
|
||||||
|
import_users(ChainID, AuthenticatorID, Filename) ->
|
||||||
|
gen_server:call(?MODULE, {import_users, ChainID, AuthenticatorID, Filename}).
|
||||||
|
|
||||||
|
add_user(ChainID, AuthenticatorID, UserInfo) ->
|
||||||
|
gen_server:call(?MODULE, {add_user, ChainID, AuthenticatorID, UserInfo}).
|
||||||
|
|
||||||
|
delete_user(ChainID, AuthenticatorID, UserID) ->
|
||||||
|
gen_server:call(?MODULE, {delete_user, ChainID, AuthenticatorID, UserID}).
|
||||||
|
|
||||||
|
update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) ->
|
||||||
|
gen_server:call(?MODULE, {update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}).
|
||||||
|
|
||||||
|
lookup_user(ChainID, AuthenticatorID, UserID) ->
|
||||||
|
gen_server:call(?MODULE, {lookup_user, ChainID, AuthenticatorID, UserID}).
|
||||||
|
|
||||||
|
%% TODO: Support pagination
|
||||||
|
list_users(ChainID, AuthenticatorID) ->
|
||||||
|
gen_server:call(?MODULE, {list_users, ChainID, AuthenticatorID}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% gen_server callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init(_Opts) ->
|
||||||
|
_ = ets:new(?CHAIN_TAB, [ named_table, set, public
|
||||||
|
, {keypos, #chain.id}
|
||||||
|
, {read_concurrency, true}]),
|
||||||
|
{ok, #{}}.
|
||||||
|
|
||||||
|
handle_call({create_chain, ID}, _From, State) ->
|
||||||
|
case ets:member(?CHAIN_TAB, ID) of
|
||||||
|
true ->
|
||||||
|
reply({error, {already_exists, {chain, ID}}}, State);
|
||||||
|
false ->
|
||||||
|
Chain = #chain{id = ID,
|
||||||
|
authenticators = [],
|
||||||
|
created_at = erlang:system_time(millisecond)},
|
||||||
|
true = ets:insert(?CHAIN_TAB, Chain),
|
||||||
|
reply({ok, serialize_chain(Chain)}, State)
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({delete_chain, ID}, _From, State) ->
|
||||||
|
case ets:lookup(?CHAIN_TAB, ID) of
|
||||||
|
[] ->
|
||||||
|
reply({error, {not_found, {chain, ID}}}, State);
|
||||||
|
[#chain{authenticators = Authenticators}] ->
|
||||||
|
_ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators],
|
||||||
|
true = ets:delete(?CHAIN_TAB, ID),
|
||||||
|
reply(ok, State)
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({lookup_chain, ID}, _From, State) ->
|
||||||
|
case ets:lookup(?CHAIN_TAB, ID) of
|
||||||
|
[] ->
|
||||||
|
reply({error, {not_found, {chain, ID}}}, State);
|
||||||
|
[Chain] ->
|
||||||
|
reply({ok, serialize_chain(Chain)}, State)
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, State) ->
|
||||||
|
UpdateFun =
|
||||||
|
fun(#chain{authenticators = Authenticators} = Chain) ->
|
||||||
|
case lists:keymember(Name, 2, Authenticators) of
|
||||||
|
true ->
|
||||||
|
{error, name_has_be_used};
|
||||||
|
false ->
|
||||||
|
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}],
|
||||||
|
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
|
||||||
|
{ok, serialize_authenticator(Authenticator)};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Reply = update_chain(ChainID, UpdateFun),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
|
handle_call({delete_authenticator, ChainID, AuthenticatorID}, _From, State) ->
|
||||||
|
UpdateFun =
|
||||||
|
fun(#chain{authenticators = Authenticators} = Chain) ->
|
||||||
|
case lists:keytake(AuthenticatorID, 1, Authenticators) of
|
||||||
|
false ->
|
||||||
|
{error, {not_found, {authenticator, AuthenticatorID}}};
|
||||||
|
{value, {_, _, Authenticator}, NAuthenticators} ->
|
||||||
|
_ = do_delete_authenticator(Authenticator),
|
||||||
|
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
|
||||||
|
ok
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Reply = update_chain(ChainID, UpdateFun),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
|
handle_call({update_authenticator, ChainID, AuthenticatorID, Config}, _From, State) ->
|
||||||
|
Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, false),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
|
handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, _From, State) ->
|
||||||
|
Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
|
handle_call({move_authenticator, ChainID, AuthenticatorID, Position}, _From, State) ->
|
||||||
|
UpdateFun =
|
||||||
|
fun(#chain{authenticators = Authenticators} = Chain) ->
|
||||||
|
case do_move_authenticator(AuthenticatorID, Authenticators, Position) of
|
||||||
{ok, NAuthenticators} ->
|
{ok, NAuthenticators} ->
|
||||||
NChain = Chain#chain{authenticators = NAuthenticators},
|
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
|
||||||
mnesia:write(?CHAIN_TAB, NChain, write);
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
update_chain(ChainID, UpdateFun).
|
Reply = update_chain(ChainID, UpdateFun),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
import_users(ChainID, AuthenticatorID, Filename) ->
|
handle_call({import_users, ChainID, AuthenticatorID, Filename}, _From, State) ->
|
||||||
call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]).
|
Reply = call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
add_user(ChainID, AuthenticatorID, UserInfo) ->
|
handle_call({add_user, ChainID, AuthenticatorID, UserInfo}, _From, State) ->
|
||||||
call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]).
|
Reply = call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
delete_user(ChainID, AuthenticatorID, UserID) ->
|
handle_call({delete_user, ChainID, AuthenticatorID, UserID}, _From, State) ->
|
||||||
call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]).
|
Reply = call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) ->
|
handle_call({update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}, _From, State) ->
|
||||||
call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]).
|
Reply = call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
lookup_user(ChainID, AuthenticatorID, UserID) ->
|
handle_call({lookup_user, ChainID, AuthenticatorID, UserID}, _From, State) ->
|
||||||
call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]).
|
Reply = call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
list_users(ChainID, AuthenticatorID) ->
|
handle_call({list_users, ChainID, AuthenticatorID}, _From, State) ->
|
||||||
call_authenticator(ChainID, AuthenticatorID, list_users, []).
|
Reply = call_authenticator(ChainID, AuthenticatorID, list_users, []),
|
||||||
|
reply(Reply, State);
|
||||||
|
|
||||||
|
handle_call(Req, _From, State) ->
|
||||||
|
?LOG(error, "Unexpected call: ~p", [Req]),
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast(Req, State) ->
|
||||||
|
?LOG(error, "Unexpected case: ~p", [Req]),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(Info, State) ->
|
||||||
|
?LOG(error, "Unexpected info: ~p", [Info]),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
reply(Reply, State) ->
|
||||||
|
{reply, Reply, State}.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
|
@ -348,6 +490,21 @@ switch_version(State = #{version := ?VER_2}) ->
|
||||||
switch_version(State) ->
|
switch_version(State) ->
|
||||||
State#{version => ?VER_1}.
|
State#{version => ?VER_1}.
|
||||||
|
|
||||||
|
split_by_name(Name, Config) ->
|
||||||
|
{Part1, Part2, true} = lists:foldl(
|
||||||
|
fun(#{<<"name">> := N} = C, {P1, P2, F0}) ->
|
||||||
|
F = case N =:= Name of
|
||||||
|
true -> true;
|
||||||
|
false -> F0
|
||||||
|
end,
|
||||||
|
case F of
|
||||||
|
false -> {[C | P1], P2, F};
|
||||||
|
true -> {P1, [C | P2], F}
|
||||||
|
end
|
||||||
|
end, {[], [], false}, Config),
|
||||||
|
[Found | NPart2] = lists:reverse(Part2),
|
||||||
|
{ok, Found, lists:reverse(Part1), NPart2}.
|
||||||
|
|
||||||
do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
|
do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
|
||||||
Provider = authenticator_provider(Config),
|
Provider = authenticator_provider(Config),
|
||||||
Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", ?VER_1/binary>>,
|
Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", ?VER_1/binary>>,
|
||||||
|
@ -356,7 +513,6 @@ do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
|
||||||
Authenticator = #authenticator{id = AuthenticatorID,
|
Authenticator = #authenticator{id = AuthenticatorID,
|
||||||
name = Name,
|
name = Name,
|
||||||
provider = Provider,
|
provider = Provider,
|
||||||
config = Config,
|
|
||||||
state = switch_version(State)},
|
state = switch_version(State)},
|
||||||
{ok, Authenticator};
|
{ok, Authenticator};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
@ -367,43 +523,106 @@ do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
|
||||||
_ = Provider:destroy(State),
|
_ = Provider:destroy(State),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) ->
|
||||||
|
UpdateFun =
|
||||||
|
fun(#chain{authenticators = Authenticators} = Chain) ->
|
||||||
|
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}],
|
||||||
|
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
|
||||||
|
{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,
|
||||||
|
state = switch_version(NewState)},
|
||||||
|
NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
|
||||||
|
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}),
|
||||||
|
{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,
|
||||||
|
state = switch_version(NewState)},
|
||||||
|
NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
|
||||||
|
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}),
|
||||||
|
_ = Provider:destroy(State),
|
||||||
|
{ok, serialize_authenticator(NewAuthenticator)};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
update_chain(ChainID, UpdateFun).
|
||||||
|
|
||||||
replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) ->
|
replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) ->
|
||||||
lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}).
|
lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}).
|
||||||
|
|
||||||
move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N)
|
do_move_authenticator(AuthenticatorID, Authenticators, Position) when is_binary(AuthenticatorID) ->
|
||||||
when N =< length(Authenticators) andalso N > 0 ->
|
case lists:keytake(AuthenticatorID, 1, Authenticators) of
|
||||||
move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N, []);
|
false ->
|
||||||
move_authenticator_to_the_nth_(_, _, _) ->
|
|
||||||
{error, out_of_range}.
|
|
||||||
|
|
||||||
move_authenticator_to_the_nth_(AuthenticatorID, [], _, _) ->
|
|
||||||
{error, {not_found, {authenticator, AuthenticatorID}}};
|
{error, {not_found, {authenticator, AuthenticatorID}}};
|
||||||
move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed)
|
{value, Authenticator, NAuthenticators} ->
|
||||||
when N =< length(Passed) ->
|
do_move_authenticator(Authenticator, NAuthenticators, Position)
|
||||||
{L1, L2} = lists:split(N - 1, lists:reverse(Passed)),
|
end;
|
||||||
{ok, L1 ++ [Authenticator] ++ L2 ++ More};
|
|
||||||
move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed) ->
|
do_move_authenticator(Authenticator, Authenticators, top) ->
|
||||||
{L1, L2} = lists:split(N - length(Passed) - 1, More),
|
{ok, [Authenticator | Authenticators]};
|
||||||
{ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2};
|
do_move_authenticator(Authenticator, Authenticators, bottom) ->
|
||||||
move_authenticator_to_the_nth_(AuthenticatorID, [Authenticator | More], N, Passed) ->
|
{ok, Authenticators ++ [Authenticator]};
|
||||||
move_authenticator_to_the_nth_(AuthenticatorID, More, N, [Authenticator | Passed]).
|
do_move_authenticator(Authenticator, Authenticators, {before, ID}) ->
|
||||||
|
insert(Authenticator, Authenticators, ID, []).
|
||||||
|
|
||||||
|
insert(_, [], ID, _) ->
|
||||||
|
{error, {not_found, {authenticator, ID}}};
|
||||||
|
insert(Authenticator, [{ID, _, _} | _] = Authenticators, ID, Acc) ->
|
||||||
|
{ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]};
|
||||||
|
insert(Authenticator, [{_, _, _} = Authenticator0 | More], ID, Acc) ->
|
||||||
|
insert(Authenticator, More, ID, [Authenticator0 | Acc]).
|
||||||
|
|
||||||
update_chain(ChainID, UpdateFun) ->
|
update_chain(ChainID, UpdateFun) ->
|
||||||
trans(
|
case ets:lookup(?CHAIN_TAB, ChainID) of
|
||||||
fun() ->
|
|
||||||
case mnesia:read(?CHAIN_TAB, ChainID, write) of
|
|
||||||
[] ->
|
[] ->
|
||||||
{error, {not_found, {chain, ChainID}}};
|
{error, {not_found, {chain, ChainID}}};
|
||||||
[Chain] ->
|
[Chain] ->
|
||||||
UpdateFun(Chain)
|
UpdateFun(Chain)
|
||||||
end
|
end.
|
||||||
end).
|
|
||||||
|
|
||||||
call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
|
call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
|
||||||
case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
|
UpdateFun =
|
||||||
[] ->
|
fun(#chain{authenticators = Authenticators}) ->
|
||||||
{error, {not_found, {chain, ChainID}}};
|
|
||||||
[#chain{authenticators = Authenticators}] ->
|
|
||||||
case lists:keyfind(AuthenticatorID, 1, Authenticators) of
|
case lists:keyfind(AuthenticatorID, 1, Authenticators) of
|
||||||
false ->
|
false ->
|
||||||
{error, {not_found, {authenticator, AuthenticatorID}}};
|
{error, {not_found, {authenticator, AuthenticatorID}}};
|
||||||
|
@ -415,7 +634,8 @@ call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
|
||||||
{error, unsupported_feature}
|
{error, unsupported_feature}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end.
|
end,
|
||||||
|
update_chain(ChainID, UpdateFun).
|
||||||
|
|
||||||
serialize_chain(#chain{id = ID,
|
serialize_chain(#chain{id = ID,
|
||||||
authenticators = Authenticators,
|
authenticators = Authenticators,
|
||||||
|
@ -428,14 +648,7 @@ serialize_authenticators(Authenticators) ->
|
||||||
[serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators].
|
[serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators].
|
||||||
|
|
||||||
serialize_authenticator(#authenticator{id = ID,
|
serialize_authenticator(#authenticator{id = ID,
|
||||||
config = Config}) ->
|
name = Name,
|
||||||
Config#{id => ID}.
|
provider = Provider,
|
||||||
|
state = State}) ->
|
||||||
trans(Fun) ->
|
#{id => ID, name => Name, provider => Provider, state => State}.
|
||||||
trans(Fun, []).
|
|
||||||
|
|
||||||
trans(Fun, Args) ->
|
|
||||||
case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of
|
|
||||||
{atomic, Res} -> Res;
|
|
||||||
{aborted, Reason} -> {error, Reason}
|
|
||||||
end.
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
, authentication/2
|
, authentication/2
|
||||||
, authenticators/2
|
, authenticators/2
|
||||||
, authenticators2/2
|
, authenticators2/2
|
||||||
, position/2
|
, move/2
|
||||||
, import_users/2
|
, import_users/2
|
||||||
, users/2
|
, users/2
|
||||||
, users2/2
|
, users2/2
|
||||||
|
@ -109,7 +109,7 @@ api_spec() ->
|
||||||
{[ authentication_api()
|
{[ authentication_api()
|
||||||
, authenticators_api()
|
, authenticators_api()
|
||||||
, authenticators_api2()
|
, authenticators_api2()
|
||||||
, position_api()
|
, move_api()
|
||||||
, import_users_api()
|
, import_users_api()
|
||||||
, users_api()
|
, users_api()
|
||||||
, users2_api()
|
, users2_api()
|
||||||
|
@ -405,10 +405,10 @@ authenticators_api2() ->
|
||||||
},
|
},
|
||||||
{"/authentication/authenticators/:id", Metadata, authenticators2}.
|
{"/authentication/authenticators/:id", Metadata, authenticators2}.
|
||||||
|
|
||||||
position_api() ->
|
move_api() ->
|
||||||
Metadata = #{
|
Metadata = #{
|
||||||
post => #{
|
post => #{
|
||||||
description => "Change the order of authenticators",
|
description => "Move authenticator",
|
||||||
parameters => [
|
parameters => [
|
||||||
#{
|
#{
|
||||||
name => id,
|
name => id,
|
||||||
|
@ -423,14 +423,30 @@ position_api() ->
|
||||||
content => #{
|
content => #{
|
||||||
'application/json' => #{
|
'application/json' => #{
|
||||||
schema => #{
|
schema => #{
|
||||||
|
oneOf => [
|
||||||
|
#{
|
||||||
type => object,
|
type => object,
|
||||||
required => [position],
|
required => [position],
|
||||||
properties => #{
|
properties => #{
|
||||||
position => #{
|
position => #{
|
||||||
type => integer,
|
type => string,
|
||||||
example => 1
|
enum => [<<"top">>, <<"bottom">>],
|
||||||
|
example => <<"top">>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
required => [position],
|
||||||
|
properties => #{
|
||||||
|
position => #{
|
||||||
|
type => string,
|
||||||
|
description => <<"before:<authenticator_id>">>,
|
||||||
|
example => <<"before:67e4c9d3">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -444,7 +460,7 @@ position_api() ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{"/authentication/authenticators/:id/position", Metadata, position}.
|
{"/authentication/authenticators/:id/move", Metadata, move}.
|
||||||
|
|
||||||
import_users_api() ->
|
import_users_api() ->
|
||||||
Metadata = #{
|
Metadata = #{
|
||||||
|
@ -512,6 +528,10 @@ users_api() ->
|
||||||
},
|
},
|
||||||
password => #{
|
password => #{
|
||||||
type => string
|
type => string
|
||||||
|
},
|
||||||
|
superuser => #{
|
||||||
|
type => boolean,
|
||||||
|
default => false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -525,10 +545,12 @@ users_api() ->
|
||||||
'application/json' => #{
|
'application/json' => #{
|
||||||
schema => #{
|
schema => #{
|
||||||
type => object,
|
type => object,
|
||||||
required => [user_id],
|
|
||||||
properties => #{
|
properties => #{
|
||||||
user_id => #{
|
user_id => #{
|
||||||
type => string
|
type => string
|
||||||
|
},
|
||||||
|
superuser => #{
|
||||||
|
type => boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -560,10 +582,12 @@ users_api() ->
|
||||||
type => array,
|
type => array,
|
||||||
items => #{
|
items => #{
|
||||||
type => object,
|
type => object,
|
||||||
required => [user_id],
|
|
||||||
properties => #{
|
properties => #{
|
||||||
user_id => #{
|
user_id => #{
|
||||||
type => string
|
type => string
|
||||||
|
},
|
||||||
|
superuser => #{
|
||||||
|
type => boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -604,10 +628,12 @@ users2_api() ->
|
||||||
'application/json' => #{
|
'application/json' => #{
|
||||||
schema => #{
|
schema => #{
|
||||||
type => object,
|
type => object,
|
||||||
required => [password],
|
|
||||||
properties => #{
|
properties => #{
|
||||||
password => #{
|
password => #{
|
||||||
type => string
|
type => string
|
||||||
|
},
|
||||||
|
superuser => #{
|
||||||
|
type => boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -626,6 +652,9 @@ users2_api() ->
|
||||||
properties => #{
|
properties => #{
|
||||||
user_id => #{
|
user_id => #{
|
||||||
type => string
|
type => string
|
||||||
|
},
|
||||||
|
superuser => #{
|
||||||
|
type => boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -669,6 +698,9 @@ users2_api() ->
|
||||||
properties => #{
|
properties => #{
|
||||||
user_id => #{
|
user_id => #{
|
||||||
type => string
|
type => string
|
||||||
|
},
|
||||||
|
superuser => #{
|
||||||
|
type => boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -758,6 +790,7 @@ definitions() ->
|
||||||
, minirest:ref(<<"password_based_mysql">>)
|
, minirest:ref(<<"password_based_mysql">>)
|
||||||
, minirest:ref(<<"password_based_pgsql">>)
|
, minirest:ref(<<"password_based_pgsql">>)
|
||||||
, minirest:ref(<<"password_based_mongodb">>)
|
, minirest:ref(<<"password_based_mongodb">>)
|
||||||
|
, minirest:ref(<<"password_based_redis">>)
|
||||||
, minirest:ref(<<"password_based_http_server">>)
|
, minirest:ref(<<"password_based_http_server">>)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1054,7 +1087,13 @@ definitions() ->
|
||||||
|
|
||||||
PasswordBasedRedisDef = #{
|
PasswordBasedRedisDef = #{
|
||||||
type => object,
|
type => object,
|
||||||
required => [],
|
required => [ server_type
|
||||||
|
, server
|
||||||
|
, servers
|
||||||
|
, password
|
||||||
|
, database
|
||||||
|
, query
|
||||||
|
],
|
||||||
properties => #{
|
properties => #{
|
||||||
server_type => #{
|
server_type => #{
|
||||||
type => string,
|
type => string,
|
||||||
|
@ -1083,7 +1122,7 @@ definitions() ->
|
||||||
},
|
},
|
||||||
database => #{
|
database => #{
|
||||||
type => integer,
|
type => integer,
|
||||||
exmaple => 0
|
example => 0
|
||||||
},
|
},
|
||||||
query => #{
|
query => #{
|
||||||
type => string,
|
type => string,
|
||||||
|
@ -1253,14 +1292,9 @@ definitions() ->
|
||||||
authentication(post, Request) ->
|
authentication(post, Request) ->
|
||||||
{ok, Body, _} = cowboy_req:read_body(Request),
|
{ok, Body, _} = cowboy_req:read_body(Request),
|
||||||
case emqx_json:decode(Body, [return_maps]) of
|
case emqx_json:decode(Body, [return_maps]) of
|
||||||
#{<<"enable">> := true} ->
|
#{<<"enable">> := Enable} ->
|
||||||
ok = emqx_authn:enable(),
|
{ok, _} = emqx_authn:update_config([authentication, enable], {enable, Enable}),
|
||||||
{204};
|
{204};
|
||||||
#{<<"enable">> := false} ->
|
|
||||||
ok = emqx_authn:disable(),
|
|
||||||
{204};
|
|
||||||
#{<<"enable">> := _} ->
|
|
||||||
serialize_error({invalid_parameter, enable});
|
|
||||||
_ ->
|
_ ->
|
||||||
serialize_error({missing_parameter, enable})
|
serialize_error({missing_parameter, enable})
|
||||||
end;
|
end;
|
||||||
|
@ -1270,97 +1304,101 @@ authentication(get, _Request) ->
|
||||||
|
|
||||||
authenticators(post, Request) ->
|
authenticators(post, Request) ->
|
||||||
{ok, Body, _} = cowboy_req:read_body(Request),
|
{ok, Body, _} = cowboy_req:read_body(Request),
|
||||||
AuthenticatorConfig = emqx_json:decode(Body, [return_maps]),
|
Config = emqx_json:decode(Body, [return_maps]),
|
||||||
Config = #{<<"authentication">> => #{
|
case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of
|
||||||
<<"authenticators">> => [AuthenticatorConfig]
|
{ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}},
|
||||||
}},
|
raw_config := RawConfig}} ->
|
||||||
NConfig = hocon_schema:check_plain(emqx_authn_schema, Config,
|
[RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name],
|
||||||
#{nullable => true}),
|
{200, RawConfig1#{id => ID}};
|
||||||
#{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig),
|
{error, {_, _, Reason}} ->
|
||||||
case emqx_authn:create_authenticator(?CHAIN, NAuthenticatorConfig) of
|
|
||||||
{ok, Authenticator2} ->
|
|
||||||
{201, Authenticator2};
|
|
||||||
{error, Reason} ->
|
|
||||||
serialize_error(Reason)
|
serialize_error(Reason)
|
||||||
end;
|
end;
|
||||||
authenticators(get, _Request) ->
|
authenticators(get, _Request) ->
|
||||||
|
RawConfig = get_raw_config([authentication, authenticators]),
|
||||||
{ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN),
|
{ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN),
|
||||||
{200, Authenticators}.
|
NAuthenticators = lists:zipwith(fun(#{<<"name">> := Name} = Config, #{id := ID, name := Name}) ->
|
||||||
|
Config#{id => ID}
|
||||||
|
end, RawConfig, Authenticators),
|
||||||
|
{200, NAuthenticators}.
|
||||||
|
|
||||||
authenticators2(get, Request) ->
|
authenticators2(get, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of
|
case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of
|
||||||
{ok, Authenticator} ->
|
{ok, #{id := ID, name := Name}} ->
|
||||||
{200, Authenticator};
|
RawConfig = get_raw_config([authentication, authenticators]),
|
||||||
|
[RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name],
|
||||||
|
{200, RawConfig1#{id => ID}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
serialize_error(Reason)
|
serialize_error(Reason)
|
||||||
end;
|
end;
|
||||||
authenticators2(put, Request) ->
|
authenticators2(put, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
{ok, Body, _} = cowboy_req:read_body(Request),
|
{ok, Body, _} = cowboy_req:read_body(Request),
|
||||||
AuthenticatorConfig = emqx_json:decode(Body, [return_maps]),
|
Config = emqx_json:decode(Body, [return_maps]),
|
||||||
Config = #{<<"authentication">> => #{
|
case emqx_authn:update_config([authentication, authenticators],
|
||||||
<<"authenticators">> => [AuthenticatorConfig]
|
{update_or_create_authenticator, AuthenticatorID, Config}) of
|
||||||
}},
|
{ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}},
|
||||||
NConfig = hocon_schema:check_plain(emqx_authn_schema, Config,
|
raw_config := RawConfig}} ->
|
||||||
#{nullable => true}),
|
[RawConfig0] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name],
|
||||||
#{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig),
|
{200, RawConfig0#{id => ID}};
|
||||||
case emqx_authn:update_or_create_authenticator(?CHAIN, AuthenticatorID, NAuthenticatorConfig) of
|
{error, {_, _, Reason}} ->
|
||||||
{ok, Authenticator} ->
|
|
||||||
{200, Authenticator};
|
|
||||||
{error, Reason} ->
|
|
||||||
serialize_error(Reason)
|
serialize_error(Reason)
|
||||||
end;
|
end;
|
||||||
authenticators2(delete, Request) ->
|
authenticators2(delete, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
case emqx_authn:delete_authenticator(?CHAIN, AuthenticatorID) of
|
case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of
|
||||||
ok ->
|
{ok, _} ->
|
||||||
{204};
|
{204};
|
||||||
{error, Reason} ->
|
{error, {_, _, Reason}} ->
|
||||||
serialize_error(Reason)
|
serialize_error(Reason)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
position(post, Request) ->
|
move(post, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
{ok, Body, _} = cowboy_req:read_body(Request),
|
{ok, Body, _} = cowboy_req:read_body(Request),
|
||||||
NBody = emqx_json:decode(Body, [return_maps]),
|
case emqx_json:decode(Body, [return_maps]) of
|
||||||
Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"position">> => NBody},
|
#{<<"position">> := Position} ->
|
||||||
#{nullable => true}, ["position"]),
|
case emqx_authn:update_config([authentication, authenticators], {move_authenticator, AuthenticatorID, Position}) of
|
||||||
#{position := #{position := Position}} = emqx_map_lib:unsafe_atom_key_map(Config),
|
{ok, _} -> {204};
|
||||||
case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of
|
{error, {_, _, Reason}} -> serialize_error(Reason)
|
||||||
ok ->
|
end;
|
||||||
{204};
|
_ ->
|
||||||
{error, Reason} ->
|
serialize_error({missing_parameter, position})
|
||||||
serialize_error(Reason)
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
import_users(post, Request) ->
|
import_users(post, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
{ok, Body, _} = cowboy_req:read_body(Request),
|
{ok, Body, _} = cowboy_req:read_body(Request),
|
||||||
NBody = emqx_json:decode(Body, [return_maps]),
|
case emqx_json:decode(Body, [return_maps]) of
|
||||||
Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"filename">> => NBody},
|
#{<<"filename">> := Filename} ->
|
||||||
#{nullable => true}, ["filename"]),
|
|
||||||
#{filename := #{filename := Filename}} = emqx_map_lib:unsafe_atom_key_map(Config),
|
|
||||||
case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of
|
case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of
|
||||||
ok ->
|
ok -> {204};
|
||||||
{204};
|
{error, Reason} -> serialize_error(Reason)
|
||||||
{error, Reason} ->
|
end;
|
||||||
serialize_error(Reason)
|
_ ->
|
||||||
|
serialize_error({missing_parameter, filename})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
users(post, Request) ->
|
users(post, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
{ok, Body, _} = cowboy_req:read_body(Request),
|
{ok, Body, _} = cowboy_req:read_body(Request),
|
||||||
NBody = emqx_json:decode(Body, [return_maps]),
|
case emqx_json:decode(Body, [return_maps]) of
|
||||||
Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"user_info">> => NBody},
|
#{ <<"user_id">> := UserID
|
||||||
#{nullable => true}, ["user_info"]),
|
, <<"password">> := Password} = UserInfo ->
|
||||||
#{user_info := UserInfo} = emqx_map_lib:unsafe_atom_key_map(Config),
|
Superuser = maps:get(<<"superuser">>, UserInfo, false),
|
||||||
case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of
|
case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID
|
||||||
|
, password => Password
|
||||||
|
, superuser => Superuser}) of
|
||||||
{ok, User} ->
|
{ok, User} ->
|
||||||
{201, User};
|
{201, User};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
serialize_error(Reason)
|
serialize_error(Reason)
|
||||||
end;
|
end;
|
||||||
|
#{<<"user_id">> := _} ->
|
||||||
|
serialize_error({missing_parameter, password});
|
||||||
|
_ ->
|
||||||
|
serialize_error({missing_parameter, user_id})
|
||||||
|
end;
|
||||||
users(get, Request) ->
|
users(get, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
case emqx_authn:list_users(?CHAIN, AuthenticatorID) of
|
case emqx_authn:list_users(?CHAIN, AuthenticatorID) of
|
||||||
|
@ -1374,15 +1412,18 @@ users2(patch, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
UserID = cowboy_req:binding(user_id, Request),
|
UserID = cowboy_req:binding(user_id, Request),
|
||||||
{ok, Body, _} = cowboy_req:read_body(Request),
|
{ok, Body, _} = cowboy_req:read_body(Request),
|
||||||
NBody = emqx_json:decode(Body, [return_maps]),
|
UserInfo = emqx_json:decode(Body, [return_maps]),
|
||||||
Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"new_user_info">> => NBody},
|
NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo),
|
||||||
#{nullable => true}, ["new_user_info"]),
|
case NUserInfo =:= #{} of
|
||||||
#{new_user_info := NewUserInfo} = emqx_map_lib:unsafe_atom_key_map(Config),
|
true ->
|
||||||
case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, NewUserInfo) of
|
serialize_error({missing_parameter, password});
|
||||||
|
false ->
|
||||||
|
case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of
|
||||||
{ok, User} ->
|
{ok, User} ->
|
||||||
{200, User};
|
{200, User};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
serialize_error(Reason)
|
serialize_error(Reason)
|
||||||
|
end
|
||||||
end;
|
end;
|
||||||
users2(get, Request) ->
|
users2(get, Request) ->
|
||||||
AuthenticatorID = cowboy_req:binding(id, Request),
|
AuthenticatorID = cowboy_req:binding(id, Request),
|
||||||
|
@ -1403,15 +1444,17 @@ users2(delete, Request) ->
|
||||||
serialize_error(Reason)
|
serialize_error(Reason)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
get_raw_config(ConfKeyPath) ->
|
||||||
|
%% TODO: call emqx_config:get_raw(ConfKeyPath) directly
|
||||||
|
NConfKeyPath = [atom_to_binary(Key, utf8) || Key <- ConfKeyPath],
|
||||||
|
emqx_map_lib:deep_get(NConfKeyPath, emqx_config:fill_defaults(emqx_config:get_raw([]))).
|
||||||
|
|
||||||
serialize_error({not_found, {authenticator, ID}}) ->
|
serialize_error({not_found, {authenticator, ID}}) ->
|
||||||
{404, #{code => <<"NOT_FOUND">>,
|
{404, #{code => <<"NOT_FOUND">>,
|
||||||
message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}};
|
message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}};
|
||||||
serialize_error(name_has_be_used) ->
|
serialize_error(name_has_be_used) ->
|
||||||
{409, #{code => <<"ALREADY_EXISTS">>,
|
{409, #{code => <<"ALREADY_EXISTS">>,
|
||||||
message => <<"Name has be used">>}};
|
message => <<"Name has be used">>}};
|
||||||
serialize_error(out_of_range) ->
|
|
||||||
{400, #{code => <<"OUT_OF_RANGE">>,
|
|
||||||
message => <<"Out of range">>}};
|
|
||||||
serialize_error({missing_parameter, Name}) ->
|
serialize_error({missing_parameter, Name}) ->
|
||||||
{400, #{code => <<"MISSING_PARAMETER">>,
|
{400, #{code => <<"MISSING_PARAMETER">>,
|
||||||
message => list_to_binary(
|
message => list_to_binary(
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity),
|
ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity),
|
||||||
{ok, Sup} = emqx_authn_sup:start_link(),
|
{ok, Sup} = emqx_authn_sup:start_link(),
|
||||||
|
emqx_config_handler:add_handler([authentication, authenticators], emqx_authn),
|
||||||
initialize(),
|
initialize(),
|
||||||
{ok, Sup}.
|
{ok, Sup}.
|
||||||
|
|
||||||
|
|
|
@ -26,4 +26,11 @@ start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
{ok, {{one_for_one, 10, 10}, []}}.
|
ChildSpecs = [
|
||||||
|
#{id => emqx_authn,
|
||||||
|
start => {emqx_authn, start_link, []},
|
||||||
|
restart => permanent,
|
||||||
|
type => worker,
|
||||||
|
modules => [emqx_authn]}
|
||||||
|
],
|
||||||
|
{ok, {{one_for_one, 10, 10}, ChildSpecs}}.
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
-module(emqx_enhanced_authn_scram_mnesia).
|
-module(emqx_enhanced_authn_scram_mnesia).
|
||||||
|
|
||||||
-include("emqx_authn.hrl").
|
-include("emqx_authn.hrl").
|
||||||
-include_lib("esasl/include/esasl_scram.hrl").
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
@ -48,6 +47,14 @@
|
||||||
|
|
||||||
-rlog_shard({?AUTH_SHARD, ?TAB}).
|
-rlog_shard({?AUTH_SHARD, ?TAB}).
|
||||||
|
|
||||||
|
-record(user_info,
|
||||||
|
{ user_id
|
||||||
|
, stored_key
|
||||||
|
, server_key
|
||||||
|
, salt
|
||||||
|
, superuser
|
||||||
|
}).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Mnesia bootstrap
|
%% Mnesia bootstrap
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -57,8 +64,8 @@
|
||||||
mnesia(boot) ->
|
mnesia(boot) ->
|
||||||
ok = ekka_mnesia:create_table(?TAB, [
|
ok = ekka_mnesia:create_table(?TAB, [
|
||||||
{disc_copies, [node()]},
|
{disc_copies, [node()]},
|
||||||
{record_name, scram_user_credentail},
|
{record_name, user_info},
|
||||||
{attributes, record_info(fields, scram_user_credentail)},
|
{attributes, record_info(fields, user_info)},
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]);
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}]);
|
||||||
|
|
||||||
mnesia(copy) ->
|
mnesia(copy) ->
|
||||||
|
@ -126,20 +133,21 @@ authenticate(_Credential, _State) ->
|
||||||
destroy(#{user_group := UserGroup}) ->
|
destroy(#{user_group := UserGroup}) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}],
|
MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_', '_'}, [], ['$_']}],
|
||||||
ok = lists:foreach(fun(UserCredential) ->
|
ok = lists:foreach(fun(UserInfo) ->
|
||||||
mnesia:delete_object(?TAB, UserCredential, write)
|
mnesia:delete_object(?TAB, UserInfo, write)
|
||||||
end, mnesia:select(?TAB, MatchSpec, write))
|
end, mnesia:select(?TAB, MatchSpec, write))
|
||||||
end).
|
end).
|
||||||
|
|
||||||
add_user(#{user_id := UserID,
|
add_user(#{user_id := UserID,
|
||||||
password := Password}, #{user_group := UserGroup} = State) ->
|
password := Password} = UserInfo, #{user_group := UserGroup} = State) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
[] ->
|
[] ->
|
||||||
add_user(UserID, Password, State),
|
Superuser = maps:get(superuser, UserInfo, false),
|
||||||
{ok, #{user_id => UserID}};
|
add_user(UserID, Password, Superuser, State),
|
||||||
|
{ok, #{user_id => UserID, superuser => Superuser}};
|
||||||
[_] ->
|
[_] ->
|
||||||
{error, already_exist}
|
{error, already_exist}
|
||||||
end
|
end
|
||||||
|
@ -156,31 +164,41 @@ delete_user(UserID, #{user_group := UserGroup}) ->
|
||||||
end
|
end
|
||||||
end).
|
end).
|
||||||
|
|
||||||
update_user(UserID, #{password := Password},
|
update_user(UserID, User,
|
||||||
#{user_group := UserGroup} = State) ->
|
#{user_group := UserGroup} = State) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
[] ->
|
[] ->
|
||||||
{error, not_found};
|
{error, not_found};
|
||||||
[_] ->
|
[#user_info{superuser = Superuser} = UserInfo] ->
|
||||||
add_user(UserID, Password, State),
|
UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)},
|
||||||
{ok, #{user_id => UserID}}
|
UserInfo2 = case maps:get(password, User, undefined) of
|
||||||
|
undefined ->
|
||||||
|
UserInfo1;
|
||||||
|
Password ->
|
||||||
|
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
|
||||||
|
UserInfo1#user_info{stored_key = StoredKey,
|
||||||
|
server_key = ServerKey,
|
||||||
|
salt = Salt}
|
||||||
|
end,
|
||||||
|
mnesia:write(?TAB, UserInfo2, write),
|
||||||
|
{ok, serialize_user_info(UserInfo2)}
|
||||||
end
|
end
|
||||||
end).
|
end).
|
||||||
|
|
||||||
lookup_user(UserID, #{user_group := UserGroup}) ->
|
lookup_user(UserID, #{user_group := UserGroup}) ->
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
[#scram_user_credentail{user_id = {_, UserID}}] ->
|
[UserInfo] ->
|
||||||
{ok, #{user_id => UserID}};
|
{ok, serialize_user_info(UserInfo)};
|
||||||
[] ->
|
[] ->
|
||||||
{error, not_found}
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% TODO: Support Pagination
|
%% TODO: Support Pagination
|
||||||
list_users(#{user_group := UserGroup}) ->
|
list_users(#{user_group := UserGroup}) ->
|
||||||
Users = [#{user_id => UserID} ||
|
Users = [serialize_user_info(UserInfo) ||
|
||||||
#scram_user_credentail{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
|
#user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
|
||||||
{ok, Users}.
|
{ok, Users}.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -195,13 +213,13 @@ ensure_auth_method(_, _) ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) ->
|
check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) ->
|
||||||
LookupFun = fun(Username) ->
|
RetrieveFun = fun(Username) ->
|
||||||
lookup_user2(Username, State)
|
retrieve(Username, State)
|
||||||
end,
|
end,
|
||||||
case esasl_scram:check_client_first_message(
|
case esasl_scram:check_client_first_message(
|
||||||
Bin,
|
Bin,
|
||||||
#{iteration_count => IterationCount,
|
#{iteration_count => IterationCount,
|
||||||
lookup => LookupFun}
|
retrieve => RetrieveFun}
|
||||||
) of
|
) of
|
||||||
{cotinue, ServerFirstMessage, Cache} ->
|
{cotinue, ServerFirstMessage, Cache} ->
|
||||||
{cotinue, ServerFirstMessage, Cache};
|
{cotinue, ServerFirstMessage, Cache};
|
||||||
|
@ -209,25 +227,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S
|
||||||
{error, not_authorized}
|
{error, not_authorized}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_client_final_message(Bin, Cache, #{algorithm := Alg}) ->
|
check_client_final_message(Bin, #{superuser := Superuser} = Cache, #{algorithm := Alg}) ->
|
||||||
case esasl_scram:check_client_final_message(
|
case esasl_scram:check_client_final_message(
|
||||||
Bin,
|
Bin,
|
||||||
Cache#{algorithm => Alg}
|
Cache#{algorithm => Alg}
|
||||||
) of
|
) of
|
||||||
{ok, ServerFinalMessage} ->
|
{ok, ServerFinalMessage} ->
|
||||||
{ok, ServerFinalMessage};
|
{ok, #{superuser => Superuser}, ServerFinalMessage};
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
{error, not_authorized}
|
{error, not_authorized}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
add_user(UserID, Password, State) ->
|
add_user(UserID, Password, Superuser, State) ->
|
||||||
UserCredential = esasl_scram:generate_user_credential(UserID, Password, State),
|
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
|
||||||
mnesia:write(?TAB, UserCredential, write).
|
UserInfo = #user_info{user_id = UserID,
|
||||||
|
stored_key = StoredKey,
|
||||||
|
server_key = ServerKey,
|
||||||
|
salt = Salt,
|
||||||
|
superuser = Superuser},
|
||||||
|
mnesia:write(?TAB, UserInfo, write).
|
||||||
|
|
||||||
lookup_user2(UserID, #{user_group := UserGroup}) ->
|
retrieve(UserID, #{user_group := UserGroup}) ->
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
[#scram_user_credentail{} = UserCredential] ->
|
[#user_info{stored_key = StoredKey,
|
||||||
{ok, UserCredential};
|
server_key = ServerKey,
|
||||||
|
salt = Salt,
|
||||||
|
superuser = Superuser}] ->
|
||||||
|
{ok, #{stored_key => StoredKey,
|
||||||
|
server_key => ServerKey,
|
||||||
|
salt => Salt,
|
||||||
|
superuser => Superuser}};
|
||||||
[] ->
|
[] ->
|
||||||
{error, not_found}
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
|
@ -241,3 +270,6 @@ trans(Fun, Args) ->
|
||||||
{atomic, Res} -> Res;
|
{atomic, Res} -> Res;
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) ->
|
||||||
|
#{user_id => UserID, superuser => Superuser}.
|
|
@ -154,15 +154,16 @@ authenticate(Credential, #{'_unique' := Unique,
|
||||||
try
|
try
|
||||||
Request = generate_request(Credential, State),
|
Request = generate_request(Credential, State),
|
||||||
case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of
|
case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of
|
||||||
{ok, 204, _Headers} -> ok;
|
{ok, 204, _Headers} -> {ok, #{superuser => false}};
|
||||||
{ok, 200, Headers, Body} ->
|
{ok, 200, Headers, Body} ->
|
||||||
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
||||||
case safely_parse_body(ContentType, Body) of
|
case safely_parse_body(ContentType, Body) of
|
||||||
{ok, _NBody} ->
|
{ok, NBody} ->
|
||||||
%% TODO: Return by user property
|
%% TODO: Return by user property
|
||||||
ok;
|
{ok, #{superuser => maps:get(<<"superuser">>, NBody, false),
|
||||||
|
user_property => NBody}};
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
ok
|
{ok, #{superuser => false}}
|
||||||
end;
|
end;
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
ignore
|
ignore
|
||||||
|
@ -291,8 +292,8 @@ safely_parse_body(ContentType, Body) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_body(<<"application/json">>, Body) ->
|
parse_body(<<"application/json">>, Body) ->
|
||||||
{ok, emqx_json:decode(Body)};
|
{ok, emqx_json:decode(Body, [return_maps])};
|
||||||
parse_body(<<"application/x-www-form-urlencoded">>, Body) ->
|
parse_body(<<"application/x-www-form-urlencoded">>, Body) ->
|
||||||
{ok, cow_qs:parse_qs(Body)};
|
{ok, maps:from_list(cow_qs:parse_qs(Body))};
|
||||||
parse_body(ContentType, _) ->
|
parse_body(ContentType, _) ->
|
||||||
{error, {unsupported_content_type, ContentType}}.
|
{error, {unsupported_content_type, ContentType}}.
|
|
@ -169,7 +169,7 @@ authenticate(Credential = #{password := JWT}, #{jwk := JWK,
|
||||||
end,
|
end,
|
||||||
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
||||||
case verify(JWT, JWKs, VerifyClaims) of
|
case verify(JWT, JWKs, VerifyClaims) of
|
||||||
ok -> ok;
|
{ok, Extra} -> {ok, Extra};
|
||||||
{error, invalid_signature} -> ignore;
|
{error, invalid_signature} -> ignore;
|
||||||
{error, {claims, _}} -> {error, bad_username_or_password}
|
{error, {claims, _}} -> {error, bad_username_or_password}
|
||||||
end.
|
end.
|
||||||
|
@ -239,7 +239,12 @@ verify(JWS, [JWK | More], VerifyClaims) ->
|
||||||
try jose_jws:verify(JWK, JWS) of
|
try jose_jws:verify(JWK, JWS) of
|
||||||
{true, Payload, _JWS} ->
|
{true, Payload, _JWS} ->
|
||||||
Claims = emqx_json:decode(Payload, [return_maps]),
|
Claims = emqx_json:decode(Payload, [return_maps]),
|
||||||
verify_claims(Claims, VerifyClaims);
|
case verify_claims(Claims, VerifyClaims) of
|
||||||
|
ok ->
|
||||||
|
{ok, #{superuser => maps:get(<<"superuser">>, Claims, false)}};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end;
|
||||||
{false, _, _} ->
|
{false, _, _} ->
|
||||||
verify(JWS, More, VerifyClaims)
|
verify(JWS, More, VerifyClaims)
|
||||||
catch
|
catch
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
{ user_id :: {user_group(), user_id()}
|
{ user_id :: {user_group(), user_id()}
|
||||||
, password_hash :: binary()
|
, password_hash :: binary()
|
||||||
, salt :: binary()
|
, salt :: binary()
|
||||||
|
, superuser :: boolean()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-reflect_type([ user_id_type/0 ]).
|
-reflect_type([ user_id_type/0 ]).
|
||||||
|
@ -147,13 +148,13 @@ authenticate(#{password := Password} = Credential,
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
[] ->
|
[] ->
|
||||||
ignore;
|
ignore;
|
||||||
[#user_info{password_hash = PasswordHash, salt = Salt0}] ->
|
[#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] ->
|
||||||
Salt = case Algorithm of
|
Salt = case Algorithm of
|
||||||
bcrypt -> PasswordHash;
|
bcrypt -> PasswordHash;
|
||||||
_ -> Salt0
|
_ -> Salt0
|
||||||
end,
|
end,
|
||||||
case PasswordHash =:= hash(Algorithm, Password, Salt) of
|
case PasswordHash =:= hash(Algorithm, Password, Salt) of
|
||||||
true -> ok;
|
true -> {ok, #{superuser => Superuser}};
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
@ -161,7 +162,7 @@ authenticate(#{password := Password} = Credential,
|
||||||
destroy(#{user_group := UserGroup}) ->
|
destroy(#{user_group := UserGroup}) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_'}, [], ['$_']}],
|
MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}],
|
||||||
ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write))
|
ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write))
|
||||||
end).
|
end).
|
||||||
|
|
||||||
|
@ -179,14 +180,16 @@ import_users(Filename0, State) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
add_user(#{user_id := UserID,
|
add_user(#{user_id := UserID,
|
||||||
password := Password},
|
password := Password} = UserInfo,
|
||||||
#{user_group := UserGroup} = State) ->
|
#{user_group := UserGroup} = State) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
[] ->
|
[] ->
|
||||||
add(UserID, Password, State),
|
{PasswordHash, Salt} = hash(Password, State),
|
||||||
{ok, #{user_id => UserID}};
|
Superuser = maps:get(superuser, UserInfo, false),
|
||||||
|
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
|
||||||
|
{ok, #{user_id => UserID, superuser => Superuser}};
|
||||||
[_] ->
|
[_] ->
|
||||||
{error, already_exist}
|
{error, already_exist}
|
||||||
end
|
end
|
||||||
|
@ -203,29 +206,38 @@ delete_user(UserID, #{user_group := UserGroup}) ->
|
||||||
end
|
end
|
||||||
end).
|
end).
|
||||||
|
|
||||||
update_user(UserID, #{password := Password},
|
update_user(UserID, UserInfo,
|
||||||
#{user_group := UserGroup} = State) ->
|
#{user_group := UserGroup} = State) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
[] ->
|
[] ->
|
||||||
{error, not_found};
|
{error, not_found};
|
||||||
[_] ->
|
[#user_info{ password_hash = PasswordHash
|
||||||
add(UserID, Password, State),
|
, salt = Salt
|
||||||
{ok, #{user_id => UserID}}
|
, superuser = Superuser}] ->
|
||||||
|
NSuperuser = maps:get(superuser, UserInfo, Superuser),
|
||||||
|
{NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{PasswordHash, Salt};
|
||||||
|
Password ->
|
||||||
|
hash(Password, State)
|
||||||
|
end,
|
||||||
|
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
|
||||||
|
{ok, #{user_id => UserID, superuser => NSuperuser}}
|
||||||
end
|
end
|
||||||
end).
|
end).
|
||||||
|
|
||||||
lookup_user(UserID, #{user_group := UserGroup}) ->
|
lookup_user(UserID, #{user_group := UserGroup}) ->
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
[#user_info{user_id = {_, UserID}}] ->
|
[UserInfo] ->
|
||||||
{ok, #{user_id => UserID}};
|
{ok, serialize_user_info(UserInfo)};
|
||||||
[] ->
|
[] ->
|
||||||
{error, not_found}
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
list_users(#{user_group := UserGroup}) ->
|
list_users(#{user_group := UserGroup}) ->
|
||||||
Users = [#{user_id => UserID} || #user_info{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
|
Users = [serialize_user_info(UserInfo) || #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
|
||||||
{ok, Users}.
|
{ok, Users}.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -268,7 +280,8 @@ import(UserGroup, [#{<<"user_id">> := UserID,
|
||||||
<<"password_hash">> := PasswordHash} = UserInfo | More])
|
<<"password_hash">> := PasswordHash} = UserInfo | More])
|
||||||
when is_binary(UserID) andalso is_binary(PasswordHash) ->
|
when is_binary(UserID) andalso is_binary(PasswordHash) ->
|
||||||
Salt = maps:get(<<"salt">>, UserInfo, <<>>),
|
Salt = maps:get(<<"salt">>, UserInfo, <<>>),
|
||||||
insert_user(UserGroup, UserID, PasswordHash, Salt),
|
Superuser = maps:get(<<"superuser">>, UserInfo, false),
|
||||||
|
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
|
||||||
import(UserGroup, More);
|
import(UserGroup, More);
|
||||||
import(_UserGroup, [_ | _More]) ->
|
import(_UserGroup, [_ | _More]) ->
|
||||||
{error, bad_format}.
|
{error, bad_format}.
|
||||||
|
@ -282,7 +295,8 @@ import(UserGroup, File, Seq) ->
|
||||||
{ok, #{user_id := UserID,
|
{ok, #{user_id := UserID,
|
||||||
password_hash := PasswordHash} = UserInfo} ->
|
password_hash := PasswordHash} = UserInfo} ->
|
||||||
Salt = maps:get(salt, UserInfo, <<>>),
|
Salt = maps:get(salt, UserInfo, <<>>),
|
||||||
insert_user(UserGroup, UserID, PasswordHash, Salt),
|
Superuser = maps:get(superuser, UserInfo, false),
|
||||||
|
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
|
||||||
import(UserGroup, File, Seq);
|
import(UserGroup, File, Seq);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
|
@ -307,8 +321,6 @@ get_csv_header(File) ->
|
||||||
get_user_info_by_seq(Fields, Seq) ->
|
get_user_info_by_seq(Fields, Seq) ->
|
||||||
get_user_info_by_seq(Fields, Seq, #{}).
|
get_user_info_by_seq(Fields, Seq, #{}).
|
||||||
|
|
||||||
get_user_info_by_seq([], [], #{user_id := _, password_hash := _, salt := _} = Acc) ->
|
|
||||||
{ok, Acc};
|
|
||||||
get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) ->
|
get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) ->
|
||||||
{ok, Acc};
|
{ok, Acc};
|
||||||
get_user_info_by_seq(_, [], _) ->
|
get_user_info_by_seq(_, [], _) ->
|
||||||
|
@ -319,19 +331,13 @@ get_user_info_by_seq([PasswordHash | More1], [<<"password_hash">> | More2], Acc)
|
||||||
get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash});
|
get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash});
|
||||||
get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) ->
|
get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) ->
|
||||||
get_user_info_by_seq(More1, More2, Acc#{salt => Salt});
|
get_user_info_by_seq(More1, More2, Acc#{salt => Salt});
|
||||||
|
get_user_info_by_seq([<<"true">> | More1], [<<"superuser">> | More2], Acc) ->
|
||||||
|
get_user_info_by_seq(More1, More2, Acc#{superuser => true});
|
||||||
|
get_user_info_by_seq([<<"false">> | More1], [<<"superuser">> | More2], Acc) ->
|
||||||
|
get_user_info_by_seq(More1, More2, Acc#{superuser => false});
|
||||||
get_user_info_by_seq(_, _, _) ->
|
get_user_info_by_seq(_, _, _) ->
|
||||||
{error, bad_format}.
|
{error, bad_format}.
|
||||||
|
|
||||||
-compile({inline, [add/3]}).
|
|
||||||
add(UserID, Password, #{user_group := UserGroup,
|
|
||||||
password_hash_algorithm := Algorithm} = State) ->
|
|
||||||
Salt = gen_salt(State),
|
|
||||||
PasswordHash = hash(Algorithm, Password, Salt),
|
|
||||||
case Algorithm of
|
|
||||||
bcrypt -> insert_user(UserGroup, UserID, PasswordHash);
|
|
||||||
_ -> insert_user(UserGroup, UserID, PasswordHash, Salt)
|
|
||||||
end.
|
|
||||||
|
|
||||||
gen_salt(#{password_hash_algorithm := plain}) ->
|
gen_salt(#{password_hash_algorithm := plain}) ->
|
||||||
<<>>;
|
<<>>;
|
||||||
gen_salt(#{password_hash_algorithm := bcrypt,
|
gen_salt(#{password_hash_algorithm := bcrypt,
|
||||||
|
@ -347,13 +353,16 @@ hash(bcrypt, Password, Salt) ->
|
||||||
hash(Algorithm, Password, Salt) ->
|
hash(Algorithm, Password, Salt) ->
|
||||||
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>).
|
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>).
|
||||||
|
|
||||||
insert_user(UserGroup, UserID, PasswordHash) ->
|
hash(Password, #{password_hash_algorithm := Algorithm} = State) ->
|
||||||
insert_user(UserGroup, UserID, PasswordHash, <<>>).
|
Salt = gen_salt(State),
|
||||||
|
PasswordHash = hash(Algorithm, Password, Salt),
|
||||||
|
{PasswordHash, Salt}.
|
||||||
|
|
||||||
insert_user(UserGroup, UserID, PasswordHash, Salt) ->
|
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) ->
|
||||||
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
||||||
password_hash = PasswordHash,
|
password_hash = PasswordHash,
|
||||||
salt = Salt},
|
salt = Salt,
|
||||||
|
superuser = Superuser},
|
||||||
mnesia:write(?TAB, UserInfo, write).
|
mnesia:write(?TAB, UserInfo, write).
|
||||||
|
|
||||||
delete_user2(UserInfo) ->
|
delete_user2(UserInfo) ->
|
||||||
|
@ -376,8 +385,10 @@ trans(Fun, Args) ->
|
||||||
{aborted, Reason} -> {error, Reason}
|
{aborted, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
to_binary(B) when is_binary(B) ->
|
to_binary(B) when is_binary(B) ->
|
||||||
B;
|
B;
|
||||||
to_binary(L) when is_list(L) ->
|
to_binary(L) when is_list(L) ->
|
||||||
iolist_to_binary(L).
|
iolist_to_binary(L).
|
||||||
|
|
||||||
|
serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) ->
|
||||||
|
#{user_id => UserID, superuser => Superuser}.
|
|
@ -140,7 +140,8 @@ authenticate(#{password := Password} = Credential,
|
||||||
ignore;
|
ignore;
|
||||||
Doc ->
|
Doc ->
|
||||||
case check_password(Password, Doc, State) of
|
case check_password(Password, Doc, State) of
|
||||||
ok -> ok;
|
ok ->
|
||||||
|
{ok, #{superuser => superuser(Doc, State)}};
|
||||||
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
||||||
?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]),
|
?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]),
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
|
@ -221,6 +222,11 @@ check_password(Password,
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
superuser(Doc, #{superuser_field := SuperuserField}) ->
|
||||||
|
maps:get(SuperuserField, Doc, false);
|
||||||
|
superuser(_, _) ->
|
||||||
|
false.
|
||||||
|
|
||||||
hash(Algorithm, Password, Salt, prefix) ->
|
hash(Algorithm, Password, Salt, prefix) ->
|
||||||
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
|
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
|
||||||
hash(Algorithm, Password, Salt, suffix) ->
|
hash(Algorithm, Password, Salt, suffix) ->
|
||||||
|
|
|
@ -112,15 +112,19 @@ authenticate(#{password := Password} = Credential,
|
||||||
case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of
|
case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, _Columns, []} -> ignore;
|
||||||
{ok, Columns, Rows} ->
|
{ok, Columns, Rows} ->
|
||||||
%% TODO: Support superuser
|
|
||||||
Selected = maps:from_list(lists:zip(Columns, Rows)),
|
Selected = maps:from_list(lists:zip(Columns, Rows)),
|
||||||
check_password(Password, Selected, State);
|
case check_password(Password, Selected, State) of
|
||||||
|
ok ->
|
||||||
|
{ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end;
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
ignore
|
ignore
|
||||||
end
|
end
|
||||||
catch
|
catch
|
||||||
error:Reason ->
|
error:Error ->
|
||||||
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]),
|
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -135,17 +139,17 @@ destroy(#{'_unique' := Unique}) ->
|
||||||
check_password(undefined, _Selected, _State) ->
|
check_password(undefined, _Selected, _State) ->
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
check_password(Password,
|
check_password(Password,
|
||||||
#{password_hash := Hash},
|
#{<<"password_hash">> := Hash},
|
||||||
#{password_hash_algorithm := bcrypt}) ->
|
#{password_hash_algorithm := bcrypt}) ->
|
||||||
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
end;
|
end;
|
||||||
check_password(Password,
|
check_password(Password,
|
||||||
#{password_hash := Hash} = Selected,
|
#{<<"password_hash">> := Hash} = Selected,
|
||||||
#{password_hash_algorithm := Algorithm,
|
#{password_hash_algorithm := Algorithm,
|
||||||
salt_position := SaltPosition}) ->
|
salt_position := SaltPosition}) ->
|
||||||
Salt = maps:get(salt, Selected, <<>>),
|
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
||||||
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
|
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% 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.
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
-include("emqx_authn.hrl").
|
-include("emqx_authn.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("epgsql/include/epgsql.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
@ -98,15 +99,20 @@ authenticate(#{password := Password} = Credential,
|
||||||
case emqx_resource:query(Unique, {sql, Query, Params}) of
|
case emqx_resource:query(Unique, {sql, Query, Params}) of
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, _Columns, []} -> ignore;
|
||||||
{ok, Columns, Rows} ->
|
{ok, Columns, Rows} ->
|
||||||
%% TODO: Support superuser
|
NColumns = [Name || #column{name = Name} <- Columns],
|
||||||
Selected = maps:from_list(lists:zip(Columns, Rows)),
|
Selected = maps:from_list(lists:zip(NColumns, Rows)),
|
||||||
check_password(Password, Selected, State);
|
case check_password(Password, Selected, State) of
|
||||||
|
ok ->
|
||||||
|
{ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end;
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
ignore
|
ignore
|
||||||
end
|
end
|
||||||
catch
|
catch
|
||||||
error:Reason ->
|
error:Error ->
|
||||||
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]),
|
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -121,17 +127,17 @@ destroy(#{'_unique' := Unique}) ->
|
||||||
check_password(undefined, _Selected, _State) ->
|
check_password(undefined, _Selected, _State) ->
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
check_password(Password,
|
check_password(Password,
|
||||||
#{password_hash := Hash},
|
#{<<"password_hash">> := Hash},
|
||||||
#{password_hash_algorithm := bcrypt}) ->
|
#{password_hash_algorithm := bcrypt}) ->
|
||||||
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
end;
|
end;
|
||||||
check_password(Password,
|
check_password(Password,
|
||||||
#{password_hash := Hash} = Selected,
|
#{<<"password_hash">> := Hash} = Selected,
|
||||||
#{password_hash_algorithm := Algorithm,
|
#{password_hash_algorithm := Algorithm,
|
||||||
salt_position := SaltPosition}) ->
|
salt_position := SaltPosition}) ->
|
||||||
Salt = maps:get(salt, Selected, <<>>),
|
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
||||||
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
|
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
|
|
|
@ -124,7 +124,13 @@ authenticate(#{password := Password} = Credential,
|
||||||
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
|
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
|
||||||
case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of
|
case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of
|
||||||
{ok, Values} ->
|
{ok, Values} ->
|
||||||
check_password(Password, merge(Fields, Values), State);
|
Selected = merge(Fields, Values),
|
||||||
|
case check_password(Password, Selected, State) of
|
||||||
|
ok ->
|
||||||
|
{ok, #{superuser => maps:get("superuser", Selected, false)}};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
|
?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
|
||||||
ignore
|
ignore
|
||||||
|
@ -166,8 +172,8 @@ check_fields(["password_hash" | More], false) ->
|
||||||
check_fields(More, true);
|
check_fields(More, true);
|
||||||
check_fields(["salt" | More], HasPassHash) ->
|
check_fields(["salt" | More], HasPassHash) ->
|
||||||
check_fields(More, HasPassHash);
|
check_fields(More, HasPassHash);
|
||||||
% check_fields(["is_superuser" | More], HasPassHash) ->
|
check_fields(["superuser" | More], HasPassHash) ->
|
||||||
% check_fields(More, HasPassHash);
|
check_fields(More, HasPassHash);
|
||||||
check_fields([Field | _], _) ->
|
check_fields([Field | _], _) ->
|
||||||
error({unsupported_field, Field}).
|
error({unsupported_field, Field}).
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
user_id,password_hash,salt
|
user_id,password_hash,salt,superuser
|
||||||
myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235
|
myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true
|
||||||
myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139
|
myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false
|
||||||
|
|
|
|
@ -2,11 +2,13 @@
|
||||||
{
|
{
|
||||||
"user_id":"myuser1",
|
"user_id":"myuser1",
|
||||||
"password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
|
"password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
|
||||||
"salt": "e378187547bf2d6f0545a3f441aa4d8a"
|
"salt": "e378187547bf2d6f0545a3f441aa4d8a",
|
||||||
|
"superuser": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"user_id":"myuser2",
|
"user_id":"myuser2",
|
||||||
"password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
|
"password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
|
||||||
"salt": "6d3f9bd5b54d94b98adbcfe10b6d181f"
|
"salt": "6d3f9bd5b54d94b98adbcfe10b6d181f",
|
||||||
|
"superuser": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -71,7 +71,7 @@ t_authenticator(_) ->
|
||||||
secret => <<"abcdef">>,
|
secret => <<"abcdef">>,
|
||||||
secret_base64_encoded => false,
|
secret_base64_encoded => false,
|
||||||
verify_claims => []},
|
verify_claims => []},
|
||||||
{ok, #{name := AuthenticatorName1, id := ID1, mechanism := jwt}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2),
|
{ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2),
|
||||||
|
|
||||||
ID2 = <<"random">>,
|
ID2 = <<"random">>,
|
||||||
?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
|
?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
|
||||||
|
@ -79,17 +79,25 @@ t_authenticator(_) ->
|
||||||
|
|
||||||
AuthenticatorName2 = <<"myauthenticator2">>,
|
AuthenticatorName2 = <<"myauthenticator2">>,
|
||||||
AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2},
|
AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2},
|
||||||
{ok, #{name := AuthenticatorName2, id := ID2, secret := <<"abcdef">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3),
|
{ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3),
|
||||||
?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)),
|
?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">>}),
|
{ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}),
|
||||||
|
|
||||||
?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)),
|
?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)),
|
||||||
?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
|
?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
|
||||||
|
|
||||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)),
|
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
|
||||||
?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
|
?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:move_authenticator(?CHAIN, ID2, bottom)),
|
||||||
|
?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
|
||||||
|
|
||||||
|
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})),
|
||||||
|
|
||||||
|
?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
|
||||||
|
|
||||||
|
?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})),
|
||||||
|
|
||||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
|
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
|
||||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
|
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
|
||||||
?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)),
|
?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)),
|
||||||
|
@ -100,7 +108,7 @@ t_authenticate(_) ->
|
||||||
listener => mqtt_tcp,
|
listener => mqtt_tcp,
|
||||||
username => <<"myuser">>,
|
username => <<"myuser">>,
|
||||||
password => <<"mypass">>},
|
password => <<"mypass">>},
|
||||||
?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)),
|
?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
|
||||||
?assertEqual(false, emqx_authn:is_enabled()),
|
?assertEqual(false, emqx_authn:is_enabled()),
|
||||||
emqx_authn:enable(),
|
emqx_authn:enable(),
|
||||||
?assertEqual(true, emqx_authn:is_enabled()),
|
?assertEqual(true, emqx_authn:is_enabled()),
|
||||||
|
|
|
@ -52,21 +52,27 @@ t_jwt_authenticator(_) ->
|
||||||
JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
|
JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
|
||||||
ClientInfo = #{username => <<"myuser">>,
|
ClientInfo = #{username => <<"myuser">>,
|
||||||
password => JWS},
|
password => JWS},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
|
||||||
|
|
||||||
|
Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true},
|
||||||
|
JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>),
|
||||||
|
ClientInfo1 = #{username => <<"myuser">>,
|
||||||
|
password => JWS1},
|
||||||
|
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
|
||||||
|
|
||||||
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
|
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
|
||||||
ClientInfo2 = ClientInfo#{password => BadJWS},
|
ClientInfo2 = ClientInfo#{password => BadJWS},
|
||||||
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)),
|
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
|
||||||
|
|
||||||
%% 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, ID, Config2)),
|
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)),
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
|
||||||
|
|
||||||
Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
|
Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
|
||||||
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
|
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
|
||||||
?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)),
|
||||||
|
|
||||||
%% Expiration
|
%% Expiration
|
||||||
|
@ -74,39 +80,39 @@ t_jwt_authenticator(_) ->
|
||||||
, <<"exp">> => erlang:system_time(second) - 60},
|
, <<"exp">> => erlang:system_time(second) - 60},
|
||||||
JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
|
JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
|
||||||
ClientInfo3 = ClientInfo#{password => JWS3},
|
ClientInfo3 = ClientInfo#{password => JWS3},
|
||||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)),
|
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
|
||||||
|
|
||||||
Payload4 = #{ <<"username">> => <<"myuser">>
|
Payload4 = #{ <<"username">> => <<"myuser">>
|
||||||
, <<"exp">> => erlang:system_time(second) + 60},
|
, <<"exp">> => erlang:system_time(second) + 60},
|
||||||
JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
|
JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
|
||||||
ClientInfo4 = ClientInfo#{password => JWS4},
|
ClientInfo4 = ClientInfo#{password => JWS4},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
|
||||||
|
|
||||||
%% Issued At
|
%% Issued At
|
||||||
Payload5 = #{ <<"username">> => <<"myuser">>
|
Payload5 = #{ <<"username">> => <<"myuser">>
|
||||||
, <<"iat">> => erlang:system_time(second) - 60},
|
, <<"iat">> => erlang:system_time(second) - 60},
|
||||||
JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
|
JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
|
||||||
ClientInfo5 = ClientInfo#{password => JWS5},
|
ClientInfo5 = ClientInfo#{password => JWS5},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)),
|
||||||
|
|
||||||
Payload6 = #{ <<"username">> => <<"myuser">>
|
Payload6 = #{ <<"username">> => <<"myuser">>
|
||||||
, <<"iat">> => erlang:system_time(second) + 60},
|
, <<"iat">> => erlang:system_time(second) + 60},
|
||||||
JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
|
JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
|
||||||
ClientInfo6 = ClientInfo#{password => JWS6},
|
ClientInfo6 = ClientInfo#{password => JWS6},
|
||||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ok)),
|
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)),
|
||||||
|
|
||||||
%% Not Before
|
%% Not Before
|
||||||
Payload7 = #{ <<"username">> => <<"myuser">>
|
Payload7 = #{ <<"username">> => <<"myuser">>
|
||||||
, <<"nbf">> => erlang:system_time(second) - 60},
|
, <<"nbf">> => erlang:system_time(second) - 60},
|
||||||
JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
|
JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
|
||||||
ClientInfo7 = ClientInfo#{password => JWS7},
|
ClientInfo7 = ClientInfo#{password => JWS7},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)),
|
||||||
|
|
||||||
Payload8 = #{ <<"username">> => <<"myuser">>
|
Payload8 = #{ <<"username">> => <<"myuser">>
|
||||||
, <<"nbf">> => erlang:system_time(second) + 60},
|
, <<"nbf">> => erlang:system_time(second) + 60},
|
||||||
JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
|
JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
|
||||||
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, ignored)),
|
||||||
|
|
||||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
|
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
|
||||||
ok.
|
ok.
|
||||||
|
@ -128,8 +134,8 @@ t_jwt_authenticator2(_) ->
|
||||||
JWS = generate_jws('public-key', Payload, PrivateKey),
|
JWS = generate_jws('public-key', Payload, PrivateKey),
|
||||||
ClientInfo = #{username => <<"myuser">>,
|
ClientInfo = #{username => <<"myuser">>,
|
||||||
password => JWS},
|
password => JWS},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
|
||||||
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)),
|
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)),
|
||||||
|
|
||||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
|
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -50,33 +50,36 @@ t_mnesia_authenticator(_) ->
|
||||||
|
|
||||||
UserInfo = #{user_id => <<"myuser">>,
|
UserInfo = #{user_id => <<"myuser">>,
|
||||||
password => <<"mypass">>},
|
password => <<"mypass">>},
|
||||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
|
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
|
||||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
|
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
|
||||||
|
|
||||||
ClientInfo = #{zone => external,
|
ClientInfo = #{zone => external,
|
||||||
username => <<"myuser">>,
|
username => <<"myuser">>,
|
||||||
password => <<"mypass">>},
|
password => <<"mypass">>},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
|
||||||
?AUTH:enable(),
|
?AUTH:enable(),
|
||||||
?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)),
|
?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
|
||||||
|
|
||||||
ClientInfo2 = ClientInfo#{username => <<"baduser">>},
|
ClientInfo2 = ClientInfo#{username => <<"baduser">>},
|
||||||
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)),
|
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
|
||||||
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)),
|
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)),
|
||||||
|
|
||||||
ClientInfo3 = ClientInfo#{password => <<"badpass">>},
|
ClientInfo3 = ClientInfo#{password => <<"badpass">>},
|
||||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)),
|
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
|
||||||
?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, ID, <<"myuser">>, UserInfo2)),
|
?assertMatch({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, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
|
||||||
|
|
||||||
|
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})),
|
||||||
|
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
|
||||||
|
|
||||||
?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
|
?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
|
||||||
?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
|
?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
|
||||||
|
|
||||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
|
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
|
||||||
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
|
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
|
||||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
|
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
|
||||||
|
|
||||||
|
@ -104,10 +107,16 @@ t_import(_) ->
|
||||||
|
|
||||||
ClientInfo1 = #{username => <<"myuser1">>,
|
ClientInfo1 = #{username => <<"myuser1">>,
|
||||||
password => <<"mypassword1">>},
|
password => <<"mypassword1">>},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)),
|
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
|
||||||
ClientInfo2 = ClientInfo1#{username => <<"myuser3">>,
|
|
||||||
|
ClientInfo2 = ClientInfo1#{username => <<"myuser2">>,
|
||||||
|
password => <<"mypassword2">>},
|
||||||
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
|
||||||
|
|
||||||
|
ClientInfo3 = ClientInfo1#{username => <<"myuser3">>,
|
||||||
password => <<"mypassword3">>},
|
password => <<"mypassword3">>},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
|
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)),
|
||||||
|
|
||||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
|
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
@ -131,11 +140,11 @@ t_multi_mnesia_authenticator(_) ->
|
||||||
{ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
|
{ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
|
||||||
{ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
|
{ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
|
||||||
|
|
||||||
?assertEqual({ok, #{user_id => <<"myuser">>}},
|
?assertMatch({ok, #{user_id := <<"myuser">>}},
|
||||||
?AUTH:add_user(?CHAIN, ID1,
|
?AUTH:add_user(?CHAIN, ID1,
|
||||||
#{user_id => <<"myuser">>,
|
#{user_id => <<"myuser">>,
|
||||||
password => <<"mypass1">>})),
|
password => <<"mypass1">>})),
|
||||||
?assertEqual({ok, #{user_id => <<"myclient">>}},
|
?assertMatch({ok, #{user_id := <<"myclient">>}},
|
||||||
?AUTH:add_user(?CHAIN, ID2,
|
?AUTH:add_user(?CHAIN, ID2,
|
||||||
#{user_id => <<"myclient">>,
|
#{user_id => <<"myclient">>,
|
||||||
password => <<"mypass2">>})),
|
password => <<"mypass2">>})),
|
||||||
|
@ -143,12 +152,12 @@ t_multi_mnesia_authenticator(_) ->
|
||||||
ClientInfo1 = #{username => <<"myuser">>,
|
ClientInfo1 = #{username => <<"myuser">>,
|
||||||
clientid => <<"myclient">>,
|
clientid => <<"myclient">>,
|
||||||
password => <<"mypass1">>},
|
password => <<"mypass1">>},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
|
||||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)),
|
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
|
||||||
|
|
||||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)),
|
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)),
|
||||||
ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
|
ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
|
||||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
|
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
|
||||||
|
|
||||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
|
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
|
||||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
|
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
|
||||||
|
|
|
@ -73,7 +73,7 @@ authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) ->
|
||||||
chain_id => ChainId
|
chain_id => ChainId
|
||||||
},
|
},
|
||||||
case emqx_access_control:authenticate(ClientInfo) of
|
case emqx_access_control:authenticate(ClientInfo) of
|
||||||
ok ->
|
{ok, _} ->
|
||||||
{ok, mountpoint(ClientInfo)};
|
{ok, mountpoint(ClientInfo)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
|
|
|
@ -87,7 +87,7 @@ init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">>
|
||||||
ClientInfo = clientinfo(Lwm2mState),
|
ClientInfo = clientinfo(Lwm2mState),
|
||||||
_ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined),
|
_ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined),
|
||||||
case emqx_access_control:authenticate(ClientInfo) of
|
case emqx_access_control:authenticate(ClientInfo) of
|
||||||
ok ->
|
{ok, _} ->
|
||||||
_ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined),
|
_ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined),
|
||||||
|
|
||||||
%% FIXME:
|
%% FIXME:
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}}
|
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}}
|
||||||
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}}
|
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}}
|
||||||
, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}}
|
, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}}
|
||||||
, {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.1.0"}}}
|
, {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{xref_ignores,
|
{xref_ignores,
|
||||||
|
|
Loading…
Reference in New Issue