diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 65991d222..111a86112 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -27,9 +27,14 @@ %%-------------------------------------------------------------------- -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) -> - 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 -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5e4d11953..5988e03e5 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1299,14 +1299,17 @@ authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properti {error, ?RC_BAD_AUTHENTICATION_METHOD} end. -do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> +do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = ClientInfo} = Channel) -> Properties = #{'Authentication-Method' => AuthMethod}, case emqx_access_control:authenticate(Credential) of - ok -> - {ok, Properties, Channel#channel{auth_cache = #{}}}; - {ok, AuthData} -> + {ok, Result} -> + {ok, Properties, + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)}, + auth_cache = #{}}}; + {ok, Result, 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, Properties, Channel#channel{auth_cache = AuthCache}}; {continue, AuthData, AuthCache} -> @@ -1316,10 +1319,10 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> {error, emqx_reason_codes:connack_error(Reason)} end; -do_authenticate(Credential, Channel) -> +do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - ok -> - {ok, #{}, Channel}; + {ok, #{superuser := Superuser}} -> + {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} end. diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 056929123..6441afe69 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -101,9 +101,9 @@ }. %% 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 --type config() :: #{atom() => term()} | undefined. +-type config() :: #{atom() => term()} | list() | undefined. -type app_envs() :: [proplists:property()]. %% @doc For the given path, get root value enclosed in a single-key map. diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index d459d28b2..8cfa17523 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -33,7 +33,7 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). t_authenticate(_) -> - ?assertMatch(ok, emqx_access_control:authenticate(clientinfo())). + ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). t_authorize(_) -> Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index be7c94ede..dfbe56916 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -181,7 +181,7 @@ init_per_suite(Config) -> %% Access Control Meck ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), 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), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), diff --git a/apps/emqx_authn/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv index 2543d39ca..0548308b7 100644 --- a/apps/emqx_authn/data/user-credentials.csv +++ b/apps/emqx_authn/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt -myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 -myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 +user_id,password_hash,salt,superuser +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json index 169122bd2..e54501233 100644 --- a/apps/emqx_authn/data/user-credentials.json +++ b/apps/emqx_authn/data/user-credentials.json @@ -2,11 +2,13 @@ { "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", - "salt": "e378187547bf2d6f0545a3f441aa4d8a" + "salt": "e378187547bf2d6f0545a3f441aa4d8a", + "superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", - "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", + "superuser": false } ] diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index f9ba7c3b5..c5a392fd0 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -26,7 +26,6 @@ { id :: binary() , name :: binary() , provider :: module() - , config :: map() , state :: map() }). diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 034e06b89..571d76cc7 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -16,7 +16,17 @@ -module(emqx_authn). +-behaviour(gen_server). + +-behaviour(emqx_config_handler). + -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 , disable/0 @@ -25,6 +35,10 @@ -export([authenticate/2]). +-export([ start_link/0 + , stop/0 + ]). + -export([ create_chain/1 , delete_chain/1 , lookup_chain/1 @@ -35,7 +49,7 @@ , update_or_create_authenticator/3 , lookup_authenticator/2 , list_authenticators/1 - , move_authenticator_to_the_nth/3 + , move_authenticator/3 ]). -export([ import_users/3 @@ -46,34 +60,138 @@ , list_users/2 ]). --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). -define(CHAIN_TAB, emqx_authn_chain). --rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). - %%------------------------------------------------------------------------------ -%% Mnesia bootstrap +%% APIs %%------------------------------------------------------------------------------ -%% @doc Create or replicate tables. --spec(mnesia(boot) -> ok). -mnesia(boot) -> - %% Optimize storage - StoreProps = [{ets, [{read_concurrency, true}]}], - %% Chain table - ok = ekka_mnesia:create_table(?CHAIN_TAB, [ - {ram_copies, [node()]}, - {record_name, chain}, - {local_content, true}, - {attributes, record_info(fields, chain)}, - {storage_properties, StoreProps}]); +pre_config_update({enable, Enable}, _OldConfig) -> + {ok, Enable}; +pre_config_update({create_authenticator, Config}, OldConfig) -> + {ok, OldConfig ++ [Config]}; +pre_config_update({delete_authenticator, ID}, OldConfig) -> + case lookup_authenticator(?CHAIN, ID) of + {error, Reason} -> {error, Reason}; + {ok, #{name := Name}} -> + NewConfig = lists:filter(fun(#{<<"name">> := N}) -> + N =/= Name + end, OldConfig), + {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) -> - ok = ekka_mnesia:copy_table(?CHAIN_TAB, ram_copies). +post_config_update({enable, true}, _NewConfig, _OldConfig) -> + 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() -> case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of @@ -94,7 +212,7 @@ is_enabled() -> end, Callbacks). authenticate(Credential, _AuthResult) -> - case mnesia:dirty_read(?CHAIN_TAB, ?CHAIN) of + case ets:lookup(?CHAIN_TAB, ?CHAIN) of [#chain{authenticators = Authenticators}] -> do_authenticate(Authenticators, Credential); [] -> @@ -108,162 +226,48 @@ do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | Mo ignore -> do_authenticate(More, Credential); Result -> - %% ok - %% {ok, AuthData} + %% {ok, Extra} + %% {ok, Extra, AuthData} + %% {ok, MetaData} %% {continue, AuthCache} %% {continue, AuthData, AuthCache} %% {error, Reason} {stop, Result} end. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +stop() -> + gen_server:stop(?MODULE). + create_chain(#{id := ID}) -> - trans( - 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). + gen_server:call(?MODULE, {create_chain, ID}). delete_chain(ID) -> - trans( - 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). + gen_server:call(?MODULE, {delete_chain, ID}). lookup_chain(ID) -> - case mnesia:dirty_read(?CHAIN_TAB, ID) of - [] -> - {error, {not_found, {chain, ID}}}; - [Chain] -> - {ok, serialize_chain(Chain)} - end. + gen_server:call(?MODULE, {lookup_chain, ID}). list_chains() -> Chains = ets:tab2list(?CHAIN_TAB), {ok, [serialize_chain(Chain) || Chain <- Chains]}. -create_authenticator(ChainID, #{name := Name} = Config) -> - UpdateFun = - 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). +create_authenticator(ChainID, Config) -> + gen_server:call(?MODULE, {create_authenticator, ChainID, Config}). delete_authenticator(ChainID, AuthenticatorID) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - 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). + gen_server:call(?MODULE, {delete_authenticator, ChainID, AuthenticatorID}). 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) -> - do_update_authenticator(ChainID, AuthenticatorID, Config, true). - -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 = <>, - case Provider:update(Config#{'_unique' => Unique}, State) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - config = Config, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end; - false -> - Unique = <>, - case NewProvider:create(Config#{'_unique' => Unique}) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - provider = NewProvider, - config = Config, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), - _ = Provider:destroy(State), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end - end - end, - update_chain(ChainID, UpdateFun). + gen_server:call(?MODULE, {update_or_create_authenticator, ChainID, AuthenticatorID, Config}). lookup_authenticator(ChainID, AuthenticatorID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + case ets:lookup(?CHAIN_TAB, ChainID) of [] -> {error, {not_found, {chain, ChainID}}}; [#chain{authenticators = Authenticators}] -> @@ -276,42 +280,180 @@ lookup_authenticator(ChainID, AuthenticatorID) -> end. list_authenticators(ChainID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + case ets:lookup(?CHAIN_TAB, ChainID) of [] -> {error, {not_found, {chain, ChainID}}}; [#chain{authenticators = Authenticators}] -> {ok, serialize_authenticators(Authenticators)} end. -move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) -> - UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> - case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) of - {ok, NAuthenticators} -> - NChain = Chain#chain{authenticators = NAuthenticators}, - mnesia:write(?CHAIN_TAB, NChain, write); +move_authenticator(ChainID, AuthenticatorID, Position) -> + gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, Position}). + +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, - update_chain(ChainID, UpdateFun). + end + end, + Reply = update_chain(ChainID, UpdateFun), + reply(Reply, State); -import_users(ChainID, AuthenticatorID, Filename) -> - call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]). +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); -add_user(ChainID, AuthenticatorID, UserInfo) -> - call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]). +handle_call({update_authenticator, ChainID, AuthenticatorID, Config}, _From, State) -> + Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, false), + reply(Reply, State); -delete_user(ChainID, AuthenticatorID, UserID) -> - call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]). +handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, _From, State) -> + Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true), + reply(Reply, State); -update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) -> - call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]). +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} -> + true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), + ok; + {error, Reason} -> + {error, Reason} + end + end, + Reply = update_chain(ChainID, UpdateFun), + reply(Reply, State); -lookup_user(ChainID, AuthenticatorID, UserID) -> - call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]). +handle_call({import_users, ChainID, AuthenticatorID, Filename}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]), + reply(Reply, State); -list_users(ChainID, AuthenticatorID) -> - call_authenticator(ChainID, AuthenticatorID, list_users, []). +handle_call({add_user, ChainID, AuthenticatorID, UserInfo}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]), + reply(Reply, State); + +handle_call({delete_user, ChainID, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]), + reply(Reply, State); + +handle_call({update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]), + reply(Reply, State); + +handle_call({lookup_user, ChainID, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]), + reply(Reply, State); + +handle_call({list_users, ChainID, AuthenticatorID}, _From, State) -> + 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 @@ -348,6 +490,21 @@ switch_version(State = #{version := ?VER_2}) -> switch_version(State) -> 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) -> Provider = authenticator_provider(Config), Unique = <>, @@ -356,7 +513,6 @@ do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> Authenticator = #authenticator{id = AuthenticatorID, name = Name, provider = Provider, - config = Config, state = switch_version(State)}, {ok, Authenticator}; {error, Reason} -> @@ -366,44 +522,107 @@ do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> _ = Provider:destroy(State), 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 = <>, + 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 = <>, + 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) -> lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}). -move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) - when N =< length(Authenticators) andalso N > 0 -> - move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N, []); -move_authenticator_to_the_nth_(_, _, _) -> - {error, out_of_range}. +do_move_authenticator(AuthenticatorID, Authenticators, Position) when is_binary(AuthenticatorID) -> + case lists:keytake(AuthenticatorID, 1, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {value, Authenticator, NAuthenticators} -> + do_move_authenticator(Authenticator, NAuthenticators, Position) + end; -move_authenticator_to_the_nth_(AuthenticatorID, [], _, _) -> - {error, {not_found, {authenticator, AuthenticatorID}}}; -move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed) - when N =< length(Passed) -> - {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), - {ok, L1 ++ [Authenticator] ++ L2 ++ More}; -move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed) -> - {L1, L2} = lists:split(N - length(Passed) - 1, More), - {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2}; -move_authenticator_to_the_nth_(AuthenticatorID, [Authenticator | More], N, Passed) -> - move_authenticator_to_the_nth_(AuthenticatorID, More, N, [Authenticator | Passed]). +do_move_authenticator(Authenticator, Authenticators, top) -> + {ok, [Authenticator | Authenticators]}; +do_move_authenticator(Authenticator, Authenticators, bottom) -> + {ok, Authenticators ++ [Authenticator]}; +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) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ChainID, write) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [Chain] -> - UpdateFun(Chain) - end - end). - -call_authenticator(ChainID, AuthenticatorID, Func, Args) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + case ets:lookup(?CHAIN_TAB, ChainID) of [] -> {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> + [Chain] -> + UpdateFun(Chain) + end. + +call_authenticator(ChainID, AuthenticatorID, Func, Args) -> + UpdateFun = + fun(#chain{authenticators = Authenticators}) -> case lists:keyfind(AuthenticatorID, 1, Authenticators) of false -> {error, {not_found, {authenticator, AuthenticatorID}}}; @@ -415,7 +634,8 @@ call_authenticator(ChainID, AuthenticatorID, Func, Args) -> {error, unsupported_feature} end end - end. + end, + update_chain(ChainID, UpdateFun). serialize_chain(#chain{id = ID, authenticators = Authenticators, @@ -428,14 +648,7 @@ serialize_authenticators(Authenticators) -> [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators]. serialize_authenticator(#authenticator{id = ID, - config = Config}) -> - Config#{id => ID}. - -trans(Fun) -> - trans(Fun, []). - -trans(Fun, Args) -> - case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of - {atomic, Res} -> Res; - {aborted, Reason} -> {error, Reason} - end. + name = Name, + provider = Provider, + state = State}) -> + #{id => ID, name => Name, provider => Provider, state => State}. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 78ef5fd35..20a8e2f7d 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -24,7 +24,7 @@ , authentication/2 , authenticators/2 , authenticators2/2 - , position/2 + , move/2 , import_users/2 , users/2 , users2/2 @@ -109,7 +109,7 @@ api_spec() -> {[ authentication_api() , authenticators_api() , authenticators_api2() - , position_api() + , move_api() , import_users_api() , users_api() , users2_api() @@ -405,10 +405,10 @@ authenticators_api2() -> }, {"/authentication/authenticators/:id", Metadata, authenticators2}. -position_api() -> +move_api() -> Metadata = #{ post => #{ - description => "Change the order of authenticators", + description => "Move authenticator", parameters => [ #{ name => id, @@ -423,14 +423,30 @@ position_api() -> content => #{ 'application/json' => #{ schema => #{ - type => object, - required => [position], - properties => #{ - position => #{ - type => integer, - example => 1 + oneOf => [ + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + enum => [<<"top">>, <<"bottom">>], + example => <<"top">> + } + } + }, + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + description => <<"before:">>, + example => <<"before:67e4c9d3">> + } + } } - } + ] } } } @@ -444,7 +460,7 @@ position_api() -> } } }, - {"/authentication/authenticators/:id/position", Metadata, position}. + {"/authentication/authenticators/:id/move", Metadata, move}. import_users_api() -> Metadata = #{ @@ -512,6 +528,10 @@ users_api() -> }, password => #{ type => string + }, + superuser => #{ + type => boolean, + default => false } } } @@ -525,10 +545,12 @@ users_api() -> 'application/json' => #{ schema => #{ type => object, - required => [user_id], properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -560,10 +582,12 @@ users_api() -> type => array, items => #{ type => object, - required => [user_id], properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -604,10 +628,12 @@ users2_api() -> 'application/json' => #{ schema => #{ type => object, - required => [password], properties => #{ password => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -626,6 +652,9 @@ users2_api() -> properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -669,6 +698,9 @@ users2_api() -> properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -758,6 +790,7 @@ definitions() -> , minirest:ref(<<"password_based_mysql">>) , minirest:ref(<<"password_based_pgsql">>) , minirest:ref(<<"password_based_mongodb">>) + , minirest:ref(<<"password_based_redis">>) , minirest:ref(<<"password_based_http_server">>) ] } @@ -1054,7 +1087,13 @@ definitions() -> PasswordBasedRedisDef = #{ type => object, - required => [], + required => [ server_type + , server + , servers + , password + , database + , query + ], properties => #{ server_type => #{ type => string, @@ -1083,7 +1122,7 @@ definitions() -> }, database => #{ type => integer, - exmaple => 0 + example => 0 }, query => #{ type => string, @@ -1253,14 +1292,9 @@ definitions() -> authentication(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of - #{<<"enable">> := true} -> - ok = emqx_authn:enable(), + #{<<"enable">> := Enable} -> + {ok, _} = emqx_authn:update_config([authentication, enable], {enable, Enable}), {204}; - #{<<"enable">> := false} -> - ok = emqx_authn:disable(), - {204}; - #{<<"enable">> := _} -> - serialize_error({invalid_parameter, enable}); _ -> serialize_error({missing_parameter, enable}) end; @@ -1270,96 +1304,100 @@ authentication(get, _Request) -> authenticators(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), - AuthenticatorConfig = emqx_json:decode(Body, [return_maps]), - Config = #{<<"authentication">> => #{ - <<"authenticators">> => [AuthenticatorConfig] - }}, - NConfig = hocon_schema:check_plain(emqx_authn_schema, Config, - #{nullable => true}), - #{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig), - case emqx_authn:create_authenticator(?CHAIN, NAuthenticatorConfig) of - {ok, Authenticator2} -> - {201, Authenticator2}; - {error, Reason} -> + Config = emqx_json:decode(Body, [return_maps]), + case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of + {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, + raw_config := RawConfig}} -> + [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], + {200, RawConfig1#{id => ID}}; + {error, {_, _, Reason}} -> serialize_error(Reason) end; authenticators(get, _Request) -> + RawConfig = get_raw_config([authentication, authenticators]), {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) -> AuthenticatorID = cowboy_req:binding(id, Request), case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of - {ok, Authenticator} -> - {200, Authenticator}; + {ok, #{id := ID, name := Name}} -> + RawConfig = get_raw_config([authentication, authenticators]), + [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], + {200, RawConfig1#{id => ID}}; {error, Reason} -> serialize_error(Reason) end; authenticators2(put, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - AuthenticatorConfig = emqx_json:decode(Body, [return_maps]), - Config = #{<<"authentication">> => #{ - <<"authenticators">> => [AuthenticatorConfig] - }}, - NConfig = hocon_schema:check_plain(emqx_authn_schema, Config, - #{nullable => true}), - #{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig), - case emqx_authn:update_or_create_authenticator(?CHAIN, AuthenticatorID, NAuthenticatorConfig) of - {ok, Authenticator} -> - {200, Authenticator}; - {error, Reason} -> + Config = emqx_json:decode(Body, [return_maps]), + case emqx_authn:update_config([authentication, authenticators], + {update_or_create_authenticator, AuthenticatorID, Config}) of + {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, + raw_config := RawConfig}} -> + [RawConfig0] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], + {200, RawConfig0#{id => ID}}; + {error, {_, _, Reason}} -> serialize_error(Reason) end; authenticators2(delete, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), - case emqx_authn:delete_authenticator(?CHAIN, AuthenticatorID) of - ok -> + case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of + {ok, _} -> {204}; - {error, Reason} -> + {error, {_, _, Reason}} -> serialize_error(Reason) end. -position(post, Request) -> +move(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"position">> => NBody}, - #{nullable => true}, ["position"]), - #{position := #{position := Position}} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) + case emqx_json:decode(Body, [return_maps]) of + #{<<"position">> := Position} -> + case emqx_authn:update_config([authentication, authenticators], {move_authenticator, AuthenticatorID, Position}) of + {ok, _} -> {204}; + {error, {_, _, Reason}} -> serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, position}) end. import_users(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"filename">> => NBody}, - #{nullable => true}, ["filename"]), - #{filename := #{filename := Filename}} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) + case emqx_json:decode(Body, [return_maps]) of + #{<<"filename">> := Filename} -> + case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of + ok -> {204}; + {error, Reason} -> serialize_error(Reason) + end; + _ -> + serialize_error({missing_parameter, filename}) end. users(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"user_info">> => NBody}, - #{nullable => true}, ["user_info"]), - #{user_info := UserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of - {ok, User} -> - {201, User}; - {error, Reason} -> - serialize_error(Reason) + case emqx_json:decode(Body, [return_maps]) of + #{ <<"user_id">> := UserID + , <<"password">> := Password} = UserInfo -> + Superuser = maps:get(<<"superuser">>, UserInfo, false), + case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID + , password => Password + , superuser => Superuser}) of + {ok, User} -> + {201, User}; + {error, Reason} -> + serialize_error(Reason) + end; + #{<<"user_id">> := _} -> + serialize_error({missing_parameter, password}); + _ -> + serialize_error({missing_parameter, user_id}) end; users(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), @@ -1374,15 +1412,18 @@ users2(patch, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), UserID = cowboy_req:binding(user_id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - NBody = emqx_json:decode(Body, [return_maps]), - Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"new_user_info">> => NBody}, - #{nullable => true}, ["new_user_info"]), - #{new_user_info := NewUserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), - case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, NewUserInfo) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) + UserInfo = emqx_json:decode(Body, [return_maps]), + NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo), + case NUserInfo =:= #{} of + true -> + serialize_error({missing_parameter, password}); + false -> + case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end end; users2(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), @@ -1403,15 +1444,17 @@ users2(delete, Request) -> serialize_error(Reason) 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}}) -> {404, #{code => <<"NOT_FOUND">>, message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}}; serialize_error(name_has_be_used) -> {409, #{code => <<"ALREADY_EXISTS">>, message => <<"Name has be used">>}}; -serialize_error(out_of_range) -> - {400, #{code => <<"OUT_OF_RANGE">>, - message => <<"Out of range">>}}; serialize_error({missing_parameter, Name}) -> {400, #{code => <<"MISSING_PARAMETER">>, message => list_to_binary( diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 7518e5a01..b7f409bc9 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -29,6 +29,7 @@ start(_StartType, _StartArgs) -> ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), {ok, Sup} = emqx_authn_sup:start_link(), + emqx_config_handler:add_handler([authentication, authenticators], emqx_authn), initialize(), {ok, Sup}. diff --git a/apps/emqx_authn/src/emqx_authn_sup.erl b/apps/emqx_authn/src/emqx_authn_sup.erl index bb26af0ad..56fcf299a 100644 --- a/apps/emqx_authn/src/emqx_authn_sup.erl +++ b/apps/emqx_authn/src/emqx_authn_sup.erl @@ -26,4 +26,11 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). 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}}. diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index 56629c568..2d433d408 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -17,7 +17,6 @@ -module(emqx_enhanced_authn_scram_mnesia). -include("emqx_authn.hrl"). --include_lib("esasl/include/esasl_scram.hrl"). -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). @@ -48,6 +47,14 @@ -rlog_shard({?AUTH_SHARD, ?TAB}). +-record(user_info, + { user_id + , stored_key + , server_key + , salt + , superuser + }). + %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -57,8 +64,8 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {disc_copies, [node()]}, - {record_name, scram_user_credentail}, - {attributes, record_info(fields, scram_user_credentail)}, + {record_name, user_info}, + {attributes, record_info(fields, user_info)}, {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> @@ -126,20 +133,21 @@ authenticate(_Credential, _State) -> destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], - ok = lists:foreach(fun(UserCredential) -> - mnesia:delete_object(?TAB, UserCredential, write) + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_', '_'}, [], ['$_']}], + ok = lists:foreach(fun(UserInfo) -> + mnesia:delete_object(?TAB, UserInfo, write) end, mnesia:select(?TAB, MatchSpec, write)) end). add_user(#{user_id := UserID, - password := Password}, #{user_group := UserGroup} = State) -> + password := Password} = UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add_user(UserID, Password, State), - {ok, #{user_id => UserID}}; + Superuser = maps:get(superuser, UserInfo, false), + add_user(UserID, Password, Superuser, State), + {ok, #{user_id => UserID, superuser => Superuser}}; [_] -> {error, already_exist} end @@ -156,31 +164,41 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{password := Password}, +update_user(UserID, User, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [_] -> - add_user(UserID, Password, State), - {ok, #{user_id => UserID}} + [#user_info{superuser = Superuser} = UserInfo] -> + UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)}, + 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). lookup_user(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#scram_user_credentail{user_id = {_, UserID}}] -> - {ok, #{user_id => UserID}}; + [UserInfo] -> + {ok, serialize_user_info(UserInfo)}; [] -> {error, not_found} end. %% TODO: Support Pagination list_users(#{user_group := UserGroup}) -> - Users = [#{user_id => UserID} || - #scram_user_credentail{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}. %%------------------------------------------------------------------------------ @@ -195,13 +213,13 @@ ensure_auth_method(_, _) -> false. check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) -> - LookupFun = fun(Username) -> - lookup_user2(Username, State) + RetrieveFun = fun(Username) -> + retrieve(Username, State) end, case esasl_scram:check_client_first_message( Bin, #{iteration_count => IterationCount, - lookup => LookupFun} + retrieve => RetrieveFun} ) of {cotinue, ServerFirstMessage, Cache} -> {cotinue, ServerFirstMessage, Cache}; @@ -209,25 +227,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S {error, not_authorized} 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( Bin, Cache#{algorithm => Alg} ) of {ok, ServerFinalMessage} -> - {ok, ServerFinalMessage}; + {ok, #{superuser => Superuser}, ServerFinalMessage}; {error, _Reason} -> {error, not_authorized} end. -add_user(UserID, Password, State) -> - UserCredential = esasl_scram:generate_user_credential(UserID, Password, State), - mnesia:write(?TAB, UserCredential, write). +add_user(UserID, Password, Superuser, State) -> + {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State), + 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 - [#scram_user_credentail{} = UserCredential] -> - {ok, UserCredential}; + [#user_info{stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + superuser = Superuser}] -> + {ok, #{stored_key => StoredKey, + server_key => ServerKey, + salt => Salt, + superuser => Superuser}}; [] -> {error, not_found} end. @@ -241,3 +270,6 @@ trans(Fun, Args) -> {atomic, Res} -> Res; {aborted, Reason} -> {error, Reason} end. + +serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> + #{user_id => UserID, superuser => Superuser}. \ No newline at end of file diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index aa10a3b98..026df2415 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -154,15 +154,16 @@ authenticate(Credential, #{'_unique' := Unique, try Request = generate_request(Credential, State), case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of - {ok, 204, _Headers} -> ok; + {ok, 204, _Headers} -> {ok, #{superuser => false}}; {ok, 200, Headers, Body} -> ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>), case safely_parse_body(ContentType, Body) of - {ok, _NBody} -> + {ok, NBody} -> %% TODO: Return by user property - ok; + {ok, #{superuser => maps:get(<<"superuser">>, NBody, false), + user_property => NBody}}; {error, _Reason} -> - ok + {ok, #{superuser => false}} end; {error, _Reason} -> ignore @@ -291,8 +292,8 @@ safely_parse_body(ContentType, Body) -> end. 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) -> - {ok, cow_qs:parse_qs(Body)}; + {ok, maps:from_list(cow_qs:parse_qs(Body))}; parse_body(ContentType, _) -> {error, {unsupported_content_type, ContentType}}. \ No newline at end of file diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index fe034994e..74aa9e8f6 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -169,7 +169,7 @@ authenticate(Credential = #{password := JWT}, #{jwk := JWK, end, VerifyClaims = replace_placeholder(VerifyClaims0, Credential), case verify(JWT, JWKs, VerifyClaims) of - ok -> ok; + {ok, Extra} -> {ok, Extra}; {error, invalid_signature} -> ignore; {error, {claims, _}} -> {error, bad_username_or_password} end. @@ -239,7 +239,12 @@ verify(JWS, [JWK | More], VerifyClaims) -> try jose_jws:verify(JWK, JWS) of {true, Payload, _JWS} -> 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, _, _} -> verify(JWS, More, VerifyClaims) catch diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index ce845d4e3..08c0ffad1 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -46,6 +46,7 @@ { user_id :: {user_group(), user_id()} , password_hash :: binary() , salt :: binary() + , superuser :: boolean() }). -reflect_type([ user_id_type/0 ]). @@ -147,13 +148,13 @@ authenticate(#{password := Password} = Credential, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt0}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] -> Salt = case Algorithm of bcrypt -> PasswordHash; _ -> Salt0 end, case PasswordHash =:= hash(Algorithm, Password, Salt) of - true -> ok; + true -> {ok, #{superuser => Superuser}}; false -> {error, bad_username_or_password} end end. @@ -161,7 +162,7 @@ authenticate(#{password := Password} = Credential, destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_'}, [], ['$_']}], + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write)) end). @@ -179,14 +180,16 @@ import_users(Filename0, State) -> end. add_user(#{user_id := UserID, - password := Password}, + password := Password} = UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add(UserID, Password, State), - {ok, #{user_id => UserID}}; + {PasswordHash, Salt} = hash(Password, State), + Superuser = maps:get(superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), + {ok, #{user_id => UserID, superuser => Superuser}}; [_] -> {error, already_exist} end @@ -203,29 +206,38 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{password := Password}, +update_user(UserID, UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [_] -> - add(UserID, Password, State), - {ok, #{user_id => UserID}} + [#user_info{ password_hash = PasswordHash + , salt = Salt + , 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). lookup_user(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#user_info{user_id = {_, UserID}}] -> - {ok, #{user_id => UserID}}; + [UserInfo] -> + {ok, serialize_user_info(UserInfo)}; [] -> {error, not_found} end. 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}. %%------------------------------------------------------------------------------ @@ -268,7 +280,8 @@ import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> 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]) -> {error, bad_format}. @@ -282,7 +295,8 @@ import(UserGroup, File, Seq) -> {ok, #{user_id := UserID, password_hash := PasswordHash} = 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); {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([], [], #{user_id := _, password_hash := _, salt := _} = Acc) -> - {ok, Acc}; get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) -> {ok, Acc}; 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([Salt | More1], [<<"salt">> | More2], Acc) -> 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(_, _, _) -> {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 := bcrypt, @@ -347,13 +353,16 @@ hash(bcrypt, Password, Salt) -> hash(Algorithm, Password, Salt) -> emqx_passwd:hash(Algorithm, <>). -insert_user(UserGroup, UserID, PasswordHash) -> - insert_user(UserGroup, UserID, PasswordHash, <<>>). +hash(Password, #{password_hash_algorithm := Algorithm} = State) -> + 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}, password_hash = PasswordHash, - salt = Salt}, + salt = Salt, + superuser = Superuser}, mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> @@ -376,8 +385,10 @@ trans(Fun, Args) -> {aborted, Reason} -> {error, Reason} end. - to_binary(B) when is_binary(B) -> B; to_binary(L) when is_list(L) -> iolist_to_binary(L). + +serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> + #{user_id => UserID, superuser => Superuser}. \ No newline at end of file diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index ff1b2161a..56ced0104 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -140,7 +140,8 @@ authenticate(#{password := Password} = Credential, ignore; Doc -> case check_password(Password, Doc, State) of - ok -> ok; + ok -> + {ok, #{superuser => superuser(Doc, State)}}; {error, {cannot_find_password_hash_field, PasswordHashField}} -> ?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]), {error, bad_username_or_password}; @@ -221,6 +222,11 @@ check_password(Password, end end. +superuser(Doc, #{superuser_field := SuperuserField}) -> + maps:get(SuperuserField, Doc, false); +superuser(_, _) -> + false. + hash(Algorithm, Password, Salt, prefix) -> emqx_passwd:hash(Algorithm, <>); hash(Algorithm, Password, Salt, suffix) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index f2a01e7e1..75a3392ec 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -112,15 +112,19 @@ authenticate(#{password := Password} = Credential, case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> - %% TODO: Support superuser 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} -> ignore end catch - error:Reason -> - ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), + error:Error -> + ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]), ignore end. @@ -135,17 +139,17 @@ destroy(#{'_unique' := Unique}) -> check_password(undefined, _Selected, _State) -> {error, bad_username_or_password}; check_password(Password, - #{password_hash := Hash}, + #{<<"password_hash">> := Hash}, #{password_hash_algorithm := bcrypt}) -> case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of true -> ok; false -> {error, bad_username_or_password} end; check_password(Password, - #{password_hash := Hash} = Selected, + #{<<"password_hash">> := Hash} = Selected, #{password_hash_algorithm := Algorithm, salt_position := SaltPosition}) -> - Salt = maps:get(salt, Selected, <<>>), + Salt = maps:get(<<"salt">>, Selected, <<>>), case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of true -> ok; false -> {error, bad_username_or_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl deleted file mode 100644 index 0f5c8abb8..000000000 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl +++ /dev/null @@ -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. - diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index b83e111c3..44c7f7185 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -18,6 +18,7 @@ -include("emqx_authn.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("epgsql/include/epgsql.hrl"). -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). @@ -98,15 +99,20 @@ authenticate(#{password := Password} = Credential, case emqx_resource:query(Unique, {sql, Query, Params}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> - %% TODO: Support superuser - Selected = maps:from_list(lists:zip(Columns, Rows)), - check_password(Password, Selected, State); + NColumns = [Name || #column{name = Name} <- Columns], + Selected = maps:from_list(lists:zip(NColumns, Rows)), + case check_password(Password, Selected, State) of + ok -> + {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}}; + {error, Reason} -> + {error, Reason} + end; {error, _Reason} -> ignore end catch - error:Reason -> - ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), + error:Error -> + ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]), ignore end. @@ -121,17 +127,17 @@ destroy(#{'_unique' := Unique}) -> check_password(undefined, _Selected, _State) -> {error, bad_username_or_password}; check_password(Password, - #{password_hash := Hash}, + #{<<"password_hash">> := Hash}, #{password_hash_algorithm := bcrypt}) -> case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of true -> ok; false -> {error, bad_username_or_password} end; check_password(Password, - #{password_hash := Hash} = Selected, + #{<<"password_hash">> := Hash} = Selected, #{password_hash_algorithm := Algorithm, salt_position := SaltPosition}) -> - Salt = maps:get(salt, Selected, <<>>), + Salt = maps:get(<<"salt">>, Selected, <<>>), case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of true -> ok; false -> {error, bad_username_or_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 5d6e579ac..0c2696c0e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -124,7 +124,13 @@ authenticate(#{password := Password} = Credential, NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))), case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of {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} -> ?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]), ignore @@ -166,8 +172,8 @@ check_fields(["password_hash" | More], false) -> check_fields(More, true); check_fields(["salt" | More], HasPassHash) -> check_fields(More, HasPassHash); -% check_fields(["is_superuser" | More], HasPassHash) -> -% check_fields(More, HasPassHash); +check_fields(["superuser" | More], HasPassHash) -> + check_fields(More, HasPassHash); check_fields([Field | _], _) -> error({unsupported_field, Field}). diff --git a/apps/emqx_authn/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv index 2543d39ca..0548308b7 100644 --- a/apps/emqx_authn/test/data/user-credentials.csv +++ b/apps/emqx_authn/test/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt -myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 -myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 +user_id,password_hash,salt,superuser +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json index 169122bd2..e54501233 100644 --- a/apps/emqx_authn/test/data/user-credentials.json +++ b/apps/emqx_authn/test/data/user-credentials.json @@ -2,11 +2,13 @@ { "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", - "salt": "e378187547bf2d6f0545a3f441aa4d8a" + "salt": "e378187547bf2d6f0545a3f441aa4d8a", + "superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", - "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", + "superuser": false } ] diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 92e506d51..0be04d6cf 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -71,7 +71,7 @@ t_authenticator(_) -> secret => <<"abcdef">>, secret_base64_encoded => false, 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">>, ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), @@ -79,17 +79,25 @@ t_authenticator(_) -> AuthenticatorName2 = <<"myauthenticator2">>, 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)), - {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, [#{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)), - ?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, ID2)), ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), @@ -100,7 +108,7 @@ t_authenticate(_) -> listener => mqtt_tcp, username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), ?assertEqual(false, emqx_authn:is_enabled()), emqx_authn:enable(), ?assertEqual(true, emqx_authn:is_enabled()), diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 7435deaa0..ddb2bb209 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -52,21 +52,27 @@ t_jwt_authenticator(_) -> JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), ClientInfo = #{username => <<"myuser">>, 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">>), 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 Config2 = Config#{secret => base64:encode(<<"abcdef">>), secret_base64_encoded => true}, ?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}">>}]}, ?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)), %% Expiration @@ -74,39 +80,39 @@ t_jwt_authenticator(_) -> , <<"exp">> => erlang:system_time(second) - 60}, JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), 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">> , <<"exp">> => erlang:system_time(second) + 60}, JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), ClientInfo4 = ClientInfo#{password => JWS4}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), %% Issued At Payload5 = #{ <<"username">> => <<"myuser">> , <<"iat">> => erlang:system_time(second) - 60}, JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), ClientInfo5 = ClientInfo#{password => JWS5}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), Payload6 = #{ <<"username">> => <<"myuser">> , <<"iat">> => erlang:system_time(second) + 60}, JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), 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 Payload7 = #{ <<"username">> => <<"myuser">> , <<"nbf">> => erlang:system_time(second) - 60}, JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), ClientInfo7 = ClientInfo#{password => JWS7}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), Payload8 = #{ <<"username">> => <<"myuser">> , <<"nbf">> => erlang:system_time(second) + 60}, JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), 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)), ok. @@ -128,8 +134,8 @@ t_jwt_authenticator2(_) -> JWS = generate_jws('public-key', Payload, PrivateKey), ClientInfo = #{username => <<"myuser">>, password => JWS}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index 4a5a24844..d6425a89c 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -50,33 +50,36 @@ t_mnesia_authenticator(_) -> UserInfo = #{user_id => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ClientInfo = #{zone => external, username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), ?AUTH:enable(), - ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), 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)), 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)), 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">>}, - ?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({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">>)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), @@ -104,10 +107,16 @@ t_import(_) -> ClientInfo1 = #{username => <<"myuser1">>, password => <<"mypassword1">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, + ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), + + ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, + password => <<"mypassword2">>}, + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), + + ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, 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)), ok. @@ -131,11 +140,11 @@ t_multi_mnesia_authenticator(_) -> {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID1, #{user_id => <<"myuser">>, password => <<"mypass1">>})), - ?assertEqual({ok, #{user_id => <<"myclient">>}}, + ?assertMatch({ok, #{user_id := <<"myclient">>}}, ?AUTH:add_user(?CHAIN, ID2, #{user_id => <<"myclient">>, password => <<"mypass2">>})), @@ -143,12 +152,12 @@ t_multi_mnesia_authenticator(_) -> ClientInfo1 = #{username => <<"myuser">>, clientid => <<"myclient">>, password => <<"mypass1">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), + ?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">>}, - ?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, ID2)), diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index 22391d0b6..b5de6cb9a 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -73,7 +73,7 @@ authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) -> chain_id => ChainId }, case emqx_access_control:authenticate(ClientInfo) of - ok -> + {ok, _} -> {ok, mountpoint(ClientInfo)}; {error, Reason} -> {error, Reason} diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl index f14d6cb73..f96fe714c 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl @@ -87,7 +87,7 @@ init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">> ClientInfo = clientinfo(Lwm2mState), _ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined), case emqx_access_control:authenticate(ClientInfo) of - ok -> + {ok, _} -> _ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined), %% FIXME: diff --git a/rebar.config b/rebar.config index 1abaef868..8cfc3cf7c 100644 --- a/rebar.config +++ b/rebar.config @@ -63,7 +63,7 @@ , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.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"}}} - , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.1.0"}}} + , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} ]}. {xref_ignores,