From 4ad032f25e74e2600856e303dc50cab74b7dbaf8 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 21 May 2021 18:10:33 +0800 Subject: [PATCH 01/15] feat(new authentication): implement new auth design - Implement auth chain - Implement Mnesia auth service - Support importing user credentials from JSON or CSV file to Mnesia --- .../data/user-credentials.csv | 2 + .../data/user-credentials.json | 4 + .../include/emqx_authentication.hrl | 39 ++ apps/emqx_authentication/rebar.config | 18 + .../src/emqx_authentication.app.src | 12 + .../src/emqx_authentication.erl | 441 ++++++++++++++++++ .../src/emqx_authentication_app.erl | 33 ++ .../src/emqx_authentication_mnesia.erl | 257 ++++++++++ .../src/emqx_authentication_sup.erl | 29 ++ rebar.config.erl | 1 + 10 files changed, 836 insertions(+) create mode 100644 apps/emqx_authentication/data/user-credentials.csv create mode 100644 apps/emqx_authentication/data/user-credentials.json create mode 100644 apps/emqx_authentication/include/emqx_authentication.hrl create mode 100644 apps/emqx_authentication/rebar.config create mode 100644 apps/emqx_authentication/src/emqx_authentication.app.src create mode 100644 apps/emqx_authentication/src/emqx_authentication.erl create mode 100644 apps/emqx_authentication/src/emqx_authentication_app.erl create mode 100644 apps/emqx_authentication/src/emqx_authentication_mnesia.erl create mode 100644 apps/emqx_authentication/src/emqx_authentication_sup.erl diff --git a/apps/emqx_authentication/data/user-credentials.csv b/apps/emqx_authentication/data/user-credentials.csv new file mode 100644 index 000000000..7ee4fe8f1 --- /dev/null +++ b/apps/emqx_authentication/data/user-credentials.csv @@ -0,0 +1,2 @@ +myuser3,mypassword3 +myuser4,mypassword4 \ No newline at end of file diff --git a/apps/emqx_authentication/data/user-credentials.json b/apps/emqx_authentication/data/user-credentials.json new file mode 100644 index 000000000..6c4689433 --- /dev/null +++ b/apps/emqx_authentication/data/user-credentials.json @@ -0,0 +1,4 @@ +{ + "myuser1": "mypassword1", + "myuser2": "mypassword2" +} \ No newline at end of file diff --git a/apps/emqx_authentication/include/emqx_authentication.hrl b/apps/emqx_authentication/include/emqx_authentication.hrl new file mode 100644 index 000000000..58846cab5 --- /dev/null +++ b/apps/emqx_authentication/include/emqx_authentication.hrl @@ -0,0 +1,39 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 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. +%%-------------------------------------------------------------------- + +-define(APP, emqx_authentication). + +-record(chain, + { id + , services + , created_at}). + +-record(service, + { name + , type %% service_type + , provider + , state + }). + +-record(service_type, + { name + , provider + , params_spec + }). + + +-type(chain_id() :: binary()). +-type(service_name() :: binary()). \ No newline at end of file diff --git a/apps/emqx_authentication/rebar.config b/apps/emqx_authentication/rebar.config new file mode 100644 index 000000000..73696b033 --- /dev/null +++ b/apps/emqx_authentication/rebar.config @@ -0,0 +1,18 @@ +{deps, []}. + +{edoc_opts, [{preprocess, true}]}. +{erl_opts, [warn_unused_vars, + warn_shadow_vars, + warnings_as_errors, + warn_unused_import, + warn_obsolete_guard, + debug_info, + {parse_transform}]}. + +{xref_checks, [undefined_function_calls, undefined_functions, + locals_not_used, deprecated_function_calls, + warnings_as_errors, deprecated_functions]}. + +{cover_enabled, true}. +{cover_opts, [verbose]}. +{cover_export_enabled, true}. diff --git a/apps/emqx_authentication/src/emqx_authentication.app.src b/apps/emqx_authentication/src/emqx_authentication.app.src new file mode 100644 index 000000000..e94f131ec --- /dev/null +++ b/apps/emqx_authentication/src/emqx_authentication.app.src @@ -0,0 +1,12 @@ +{application, emqx_authentication, + [{description, "EMQ X Authentication"}, + {vsn, "4.3.0"}, + {modules, []}, + {registered, [emqx_authentication_sup, emqx_authentication_registry]}, + {applications, [kernel,stdlib]}, + {mod, {emqx_authentication_app,[]}}, + {env, []}, + {licenses, ["Apache-2.0"]}, + {maintainers, ["EMQ X Team "]}, + {links, [{"Homepage", "https://emqx.io/"}]} + ]}. diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl new file mode 100644 index 000000000..fbc59bc94 --- /dev/null +++ b/apps/emqx_authentication/src/emqx_authentication.erl @@ -0,0 +1,441 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 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_authentication). + +-include("emqx_authentication.hrl"). + +-export([ enable/0 + , disable/0 + ]). + +-export([authenticate/1]). + +-export([register_service_types/0]). + +-export([ create_chain/1 + , delete_chain/1 + , add_services_to_chain/2 + , delete_services_from_chain/2 + , move_service_to_the_front_of_chain/2 + , move_service_to_the_end_of_chain/2 + , move_service_to_the_nth_of_chain/3 + ]). + +-export([ import_user_credentials/4 + , add_user_credential/3 + , delete_user_credential/3 + , update_user_credential/3 + , lookup_user_credential/3 + ]). + +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-define(CHAIN_TAB, emqx_authentication_chain). +-define(SERVICE_TYPE_TAB, emqx_authentication_service_type). + +%%------------------------------------------------------------------------------ +%% Mnesia bootstrap +%%------------------------------------------------------------------------------ + +%% @doc Create or replicate tables. +-spec(mnesia(boot | copy) -> ok). +mnesia(boot) -> + %% Optimize storage + StoreProps = [{ets, [{read_concurrency, true}]}], + %% Chain table + ok = ekka_mnesia:create_table(?CHAIN_TAB, [ + {disc_copies, [node()]}, + {record_name, chain}, + {attributes, record_info(fields, chain)}, + {storage_properties, StoreProps}]), + %% Service type table + ok = ekka_mnesia:create_table(?SERVICE_TYPE_TAB, [ + {ram_copies, [node()]}, + {record_name, service_type}, + {attributes, record_info(fields, service_type)}, + {storage_properties, StoreProps}]); + +mnesia(copy) -> + %% Copy chain table + ok = ekka_mnesia:copy_table(?CHAIN_TAB, disc_copies), + %% Copy service type table + ok = ekka_mnesia:copy_table(?SERVICE_TYPE_TAB, ram_copies). + +enable() -> + case emqx:hook('client.authenticate', fun emqx_authentication:authenticate/2) of + ok -> ok; + {error, already_exists} -> ok + end, + case emqx:hook('client.enhanced_authenticate', fun emqx_authentication:enhanced_authenticate/2) of + ok -> ok; + {error, already_exists} -> ok + end. + +disable() -> + emqx:unhook('client.authenticate', {}), + emqx:unhook('client.enhanced_authenticate', {}), + ok. + +authenticate(#{chain_id := ChainID} = ClientInfo) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [#chain{services = []}] -> + {error, todo}; + [#chain{services = Services}] -> + do_authenticate(Services, ClientInfo); + [] -> + {error, todo} + end. + +do_authenticate([], _) -> + {error, user_credential_not_found}; +do_authenticate([{_, #service{provider = Provider, state = State}} | More], ClientInfo) -> + case Provider:authenticate(ClientInfo, State) of + ignore -> do_authenticate(More, ClientInfo); + ok -> ok; + {ok, NewClientInfo} -> {ok, NewClientInfo}; + {stop, Reason} -> {error, Reason} + end. + +register_service_types() -> + Attrs = find_attrs(?APP, service_type), + register_service_types(Attrs). + +register_service_types(Attrs) -> + register_service_types(Attrs, []). + +register_service_types([], Acc) -> + do_register_service_types(Acc); +register_service_types([{_App, Mod, #{name := Name, + params_spec := ParamsSpec}} | Types], Acc) -> + %% TODO: Temporary realization + ok = emqx_rule_validator:validate_spec(ParamsSpec), + ServiceType = #service_type{name = Name, + provider = Mod, + params_spec = ParamsSpec}, + register_service_types(Types, [ServiceType | Acc]). + +create_chain(Params = #{chain_id := ChainID}) -> + ServiceParams = maps:get(service_params, Params, []), + case validate_service_params(ServiceParams) of + {ok, NServiceParams} -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + case create_services(ChainID, NServiceParams) of + {ok, Services} -> + Chain = #chain{id = ChainID, + services = Services, + created_at = erlang:system_time(millisecond)}, + mnesia:write(?CHAIN_TAB, Chain, write), + {ok, Chain}; + {error, Reason} -> + {error, Reason} + end; + [_ | _] -> + {error, {already_exists, {chain, ChainID}}} + end + end); + {error, Reason} -> + {error, Reason} + end. + +delete_chain(ChainID) -> + mnesia:transaction( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{services = Services}] -> + ok = delete_services(Services), + mnesia:delete(?CHAIN_TAB, ChainID, write) + end + end). + +add_services_to_chain(ChainID, ServiceParams) -> + case validate_service_params(ServiceParams) of + {ok, NServiceParams} -> + UpdateFun = fun(Chain = #chain{services = Services}) -> + Names = [Name || {Name, _} <- Services] ++ [Name || #{name := Name} <- NServiceParams], + case no_duplicate_names(Names) of + ok -> + case create_services(ChainID, NServiceParams) of + {ok, NServices} -> + NChain = Chain#chain{services = Services ++ NServices}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end; + {error, {duplicate, Name}} -> + {error, {already_exists, {service, Name}}} + end + end, + update_chain(ChainID, UpdateFun); + {error, Reason} -> + {error, Reason} + end. + +delete_services_from_chain(ChainID, ServiceNames) -> + case no_duplicate_names(ServiceNames) of + ok -> + UpdateFun = fun(Chain = #chain{services = Services}) -> + case extract_services(ServiceNames, Services) of + {ok, Extracted, Rest} -> + ok = delete_services(Extracted), + NChain = Chain#chain{services = Rest}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun); + {error, Reason} -> + {error, Reason} + end. + +move_service_to_the_front_of_chain(ChainID, ServiceName) -> + UpdateFun = fun(Chain = #chain{services = Services}) -> + case move_service_to_the_front(ServiceName, Services) of + {ok, NServices} -> + NChain = Chain#chain{services = NServices}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +move_service_to_the_end_of_chain(ChainID, ServiceName) -> + UpdateFun = fun(Chain = #chain{services = Services}) -> + case move_service_to_the_end(ServiceName, Services) of + {ok, NServices} -> + NChain = Chain#chain{services = NServices}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +move_service_to_the_nth_of_chain(ChainID, ServiceName, N) -> + UpdateFun = fun(Chain = #chain{services = Services}) -> + case move_service_to_nth(ServiceName, Services, N) of + {ok, NServices} -> + NChain = Chain#chain{services = NServices}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +update_chain(ChainID, UpdateFun) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [Chain] -> + UpdateFun(Chain) + end + end). + +import_user_credentials(ChainID, ServiceName, Filename, FileFormat) -> + call_service(ChainID, ServiceName, import_user_credentials, [Filename, FileFormat]). + +add_user_credential(ChainID, ServiceName, Credential) -> + call_service(ChainID, ServiceName, add_user_credential, [Credential]). + +delete_user_credential(ChainID, ServiceName, UserIdentity) -> + call_service(ChainID, ServiceName, delete_user_credential, [UserIdentity]). + +update_user_credential(ChainID, ServiceName, Credential) -> + call_service(ChainID, ServiceName, update_user_credential, [Credential]). + +lookup_user_credential(ChainID, ServiceName, UserIdentity) -> + call_service(ChainID, ServiceName, lookup_user_credential, [UserIdentity]). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +find_attrs(App, AttrName) -> + [{App, Mod, Attr} || {ok, Modules} <- [application:get_key(App, modules)], + Mod <- Modules, + {Name, Attrs} <- module_attributes(Mod), Name =:= AttrName, + Attr <- Attrs]. + +module_attributes(Module) -> + try Module:module_info(attributes) + catch + error:undef -> [] + end. + +do_register_service_types(ServiceTypes) -> + trans(fun lists:foreach/2, [fun insert_service_type/1, ServiceTypes]). + +insert_service_type(ServiceType) -> + mnesia:write(?SERVICE_TYPE_TAB, ServiceType, write). + +find_service_type(Name) -> + case mnesia:dirty_read(?SERVICE_TYPE_TAB, Name) of + [ServiceType] -> {ok, ServiceType}; + [] -> {error, not_found} + end. + +validate_service_params(ServiceParams) -> + case validate_service_names(ServiceParams) of + ok -> + validate_other_service_params(ServiceParams); + {error, Reason} -> + {error, Reason} + end. + +validate_service_names(ServiceParams) -> + Names = [Name || #{name := Name} <- ServiceParams], + no_duplicate_names(Names). + +validate_other_service_params(ServiceParams) -> + validate_other_service_params(ServiceParams, []). + +validate_other_service_params([], Acc) -> + {ok, lists:reverse(Acc)}; +validate_other_service_params([#{type := Type, params := Params} = ServiceParams | More], Acc) -> + case find_service_type(Type) of + {ok, #service_type{provider = Provider, params_spec = ParamsSpec}} -> + NParams = emqx_rule_validator:validate_params(Params, ParamsSpec), + validate_other_service_params(More, + [ServiceParams#{params => NParams, + provider => Provider} | Acc]); + {error, not_found} -> + {error, {not_found, {service_type, Type}}} + end. + +no_duplicate_names(Names) -> + no_duplicate_names(Names, #{}). + +no_duplicate_names([], _) -> + ok; +no_duplicate_names([Name | More], Acc) -> + case maps:is_key(Name, Acc) of + false -> no_duplicate_names(More, Acc#{Name => true}); + true -> {error, {duplicate, Name}} + end. + +create_services(ChainID, ServiceParams) -> + create_services(ChainID, ServiceParams, []). + +create_services(_ChainID, [], Acc) -> + {ok, lists:reverse(Acc)}; +create_services(ChainID, [#{name := Name, type := Type, provider := Provider, params := Params} | More], Acc) -> + case Provider:create(ChainID, Name, Params) of + {ok, State} -> + Service = #service{name = Name, + type = Type, + provider = Provider, + state = State}, + create_services(ChainID, More, [{Name, Service} | Acc]); + {error, Reason} -> + delete_services(Acc), + {error, Reason} + end. + +delete_services([]) -> + ok; +delete_services([{_, #service{provider = Provider, state = State}} | More]) -> + Provider:destroy(State), + delete_services(More). + +extract_services(ServiceNames, Services) -> + extract_services(ServiceNames, Services, []). + +extract_services([], Rest, Extracted) -> + {ok, lists:reverse(Extracted), Rest}; +extract_services([ServiceName | More], Services, Acc) -> + case lists:keytake(ServiceName, 1, Services) of + {value, Extracted, Rest} -> + extract_services(More, Rest, [Extracted | Acc]); + false -> + {error, {not_found, {service, ServiceName}}} + end. + +move_service_to_the_front(ServiceName, Services) -> + move_service_to_the_front(ServiceName, Services, []). + +move_service_to_the_front(ServiceName, [], _) -> + {error, {not_found, {service, ServiceName}}}; +move_service_to_the_front(ServiceName, [{ServiceName, _} = Service | More], Passed) -> + {ok, [Service | (lists:reverse(Passed) ++ More)]}; +move_service_to_the_front(ServiceName, [Service | More], Passed) -> + move_service_to_the_front(ServiceName, More, [Service | Passed]). + +move_service_to_the_end(ServiceName, Services) -> + move_service_to_the_end(ServiceName, Services, []). + +move_service_to_the_end(ServiceName, [], _) -> + {error, {not_found, {service, ServiceName}}}; +move_service_to_the_end(ServiceName, [{ServiceName, _} = Service | More], Passed) -> + {ok, lists:reverse(Passed) ++ More ++ [Service]}; +move_service_to_the_end(ServiceName, [Service | More], Passed) -> + move_service_to_the_end(ServiceName, More, [Service | Passed]). + +move_service_to_nth(ServiceName, Services, N) + when length(Services) < N -> + move_service_to_nth(ServiceName, Services, N, []); +move_service_to_nth(_, _, _) -> + {error, out_of_range}. + +move_service_to_nth(ServiceName, [], _, _) -> + {error, {not_found, {service, ServiceName}}}; +move_service_to_nth(ServiceName, [{ServiceName, _} = Service | More], N, Passed) + when N =< length(Passed) -> + {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), + {ok, L1 ++ [Service] + L2 + More}; +move_service_to_nth(ServiceName, [{ServiceName, _} = Service | More], N, Passed) -> + {L1, L2} = lists:split(N - length(Passed) - 1, More), + {ok, lists:reverse(Passed) ++ L1 ++ [Service] ++ L2}. + +call_service(ChainID, ServiceName, Func, Args) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{services = Services}] -> + case proplists:get_value(ServiceName, Services, undefined) of + undefined -> + {error, {not_found, {service, ServiceName}}}; + #service{provider = Provider, + state = State} -> + case erlang:function_exported(Provider, Func, length(Args) + 1) of + true -> + erlang:apply(Provider, Func, Args ++ [State]); + false -> + {error, unsupported_feature} + end + end + end. + +trans(Fun) -> + trans(Fun, []). + +trans(Fun, Args) -> + case mnesia:transaction(Fun, Args) of + {atomic, Res} -> Res; + {aborted, Reason} -> {error, Reason} + end. \ No newline at end of file diff --git a/apps/emqx_authentication/src/emqx_authentication_app.erl b/apps/emqx_authentication/src/emqx_authentication_app.erl new file mode 100644 index 000000000..5fccf7ed6 --- /dev/null +++ b/apps/emqx_authentication/src/emqx_authentication_app.erl @@ -0,0 +1,33 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 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_authentication_app). + +-behaviour(application). + +%% Application callbacks +-export([ start/2 + , stop/1 + ]). + +start(_StartType, _StartArgs) -> + {ok, Sup} = emqx_authentication_sup:start_link(), + ok = emqx_authentication:register_service_types(), + {ok, Sup}. + +stop(_State) -> + ok. + diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl new file mode 100644 index 000000000..7521d4922 --- /dev/null +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -0,0 +1,257 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 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_authentication_mnesia). + +-include("emqx_authentication.hrl"). + +-export([ create/3 + , authenticate/2 + , destroy/1 + ]). + +-export([ import_user_credentials/3 + , add_user_credential/2 + , delete_user_credential/2 + , update_user_credential/2 + , lookup_user_credential/2 + ]). + +-service_type(#{ + name => mnesia, + params_spec => #{ + user_identity_type => #{ + order => 1, + type => string, + required => true, + enum => [<<"username">>, <<"clientid">>, <<"ip">>, <<"common name">>, <<"issuer">>], + default => <<"username">> + }, + password_hash_algorithm => #{ + order => 2, + type => string, + required => true, + enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>], + default => <<"sha256">> + } + } +}). + +-record(user_credential, + { user_identity :: {user_group(), user_identity()} + , password_hash :: binary() + }). + +-type(user_group() :: {chain_id(), service_name()}). +-type(user_identity() :: binary()). + +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-define(TAB, mnesia_basic_auth). + +%%------------------------------------------------------------------------------ +%% Mnesia bootstrap +%%------------------------------------------------------------------------------ + +%% @doc Create or replicate tables. +-spec(mnesia(boot | copy) -> ok). +mnesia(boot) -> + ok = ekka_mnesia:create_table(?TAB, [ + {disc_copies, [node()]}, + {record_name, user_credential}, + {attributes, record_info(fields, user_credential)}, + {storage_properties, [{ets, [{read_concurrency, true}]}]}]); + +mnesia(copy) -> + ok = ekka_mnesia:copy_table(?TAB, disc_copies). + +create(ChainID, ServiceName, #{<<"user_identity_type">> := Type, + <<"password_hash_algorithm">> := Algorithm}) -> + State = #{user_group => {ChainID, ServiceName}, + user_identity_type => binary_to_atom(Type, utf8), + password_hash_algorithm => binary_to_atom(Algorithm, utf8)}, + {ok, State}. + +authenticate(ClientInfo = #{password := Password}, + #{user_group := UserGroup, + user_identity_type := Type, + password_hash_algorithm := Algorithm}) -> + UserIdentity = get_user_identity(ClientInfo, Type), + case mnesia:dirty_read(?TAB, {UserGroup, UserIdentity}) of + [] -> + ignore; + [#user_credential{password_hash = Hash}] -> + case Hash =:= emqx_passwd:hash(Algorithm, Password) of + true -> + ok; + false -> + {stop, bad_password} + end + end. + +destroy(#{user_group := UserGroup}) -> + trans( + fun() -> + MatchSpec = [{#user_credential{user_identity = {UserGroup, '_'}, _ = '_'}, [], ['$_']}], + lists:foreach(fun delete_user_credential/1, mnesia:select(?TAB, MatchSpec, write)) + end). + +%% Example: +%% { +%% "myuser1":"mypassword1", +%% "myuser2":"mypassword2" +%% } +import_user_credentials(Filename, json, + #{user_group := UserGroup, + password_hash_algorithm := Algorithm}) -> + case file:read_file(Filename) of + {ok, Bin} -> + case emqx_json:safe_decode(Bin) of + {ok, List} -> + import(UserGroup, List, Algorithm); + {error, Reason} -> + {error, Reason} + end; + {error, Reason} -> + {error, Reason} + end; +%% Example: +%% myuser1,mypassword1 +%% myuser2,mypassword2 +import_user_credentials(Filename, csv, + #{user_group := UserGroup, + password_hash_algorithm := Algorithm}) -> + case file:open(Filename, [read, binary]) of + {ok, File} -> + import(UserGroup, File, Algorithm), + file:close(File); + {error, Reason} -> + {error, Reason} + end. + +add_user_credential(#{user_identity := UserIdentity, password := Password}, + #{user_group := UserGroup, + password_hash_algorithm := Algorithm}) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserIdentity}, write) of + [] -> + add(UserGroup, UserIdentity, Password, Algorithm); + [_] -> + {error, already_exist} + end + end). + +delete_user_credential(UserIdentity, #{user_group := UserGroup}) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserIdentity}, write) of + [] -> + {error, not_found}; + [_] -> + mnesia:delete(?TAB, {UserGroup, UserIdentity}, write) + end + end). + +update_user_credential(#{user_identity := UserIdentity, password := Password}, + #{user_group := UserGroup, + password_hash_algorithm := Algorithm}) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserIdentity}, write) of + [] -> + {error, not_found}; + [_] -> + add(UserGroup, UserIdentity, Password, Algorithm) + end + end). + +lookup_user_credential(UserIdentity, #{user_group := UserGroup}) -> + case mnesia:dirty_read(?TAB, {UserGroup, UserIdentity}) of + [#user_credential{user_identity = {_, UserIdentity}, + password_hash = PassHash}] -> + {ok, #{user_identity => UserIdentity, + password_hash => PassHash}}; + [] -> {error, not_found} + end. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +import(UserGroup, ListOrFile, Algorithm) -> + trans(fun do_import/3, [UserGroup, ListOrFile, Algorithm]). + +do_import(_UserGroup, [], _Algorithm) -> + ok; +do_import(UserGroup, [{UserIdentity, Password} | More], Algorithm) + when is_binary(UserIdentity) andalso is_binary(Password) -> + add(UserGroup, UserIdentity, Password, Algorithm), + do_import(UserGroup, More, Algorithm); +do_import(_UserGroup, [_ | _More], _Algorithm) -> + {error, bad_format}; + +%% Importing 5w credentials needs 1.7 seconds +do_import(UserGroup, File, Algorithm) -> + case file:read_line(File) of + {ok, Line} -> + case binary:split(Line, <<",">>, [global]) of + [UserIdentity, Password] -> + add(UserGroup, UserIdentity, Password, Algorithm), + do_import(UserGroup, File, Algorithm); + _ -> + {error, bad_format} + end; + eof -> + ok; + {error, Reason} -> + {error, Reason} + end. + +-compile({inline, [add/4]}). +add(UserGroup, UserIdentity, Password, Algorithm) -> + Credential = #user_credential{user_identity = {UserGroup, UserIdentity}, + password_hash = emqx_passwd:hash(Algorithm, Password)}, + mnesia:write(?TAB, Credential, write). + +delete_user_credential(UserCredential) -> + mnesia:delete_object(?TAB, UserCredential, write). + +%% TODO: Support other type +get_user_identity(#{username := Username}, username) -> + Username; +get_user_identity(#{clientid := ClientID}, clientid) -> + ClientID; +get_user_identity(_, Type) -> + {error, {bad_user_identity_type, Type}}. + +trans(Fun) -> + trans(Fun, []). + +trans(Fun, Args) -> + case mnesia:transaction(Fun, Args) of + {atomic, Res} -> Res; + {aborted, Reason} -> {error, Reason} + end. + + + + + + diff --git a/apps/emqx_authentication/src/emqx_authentication_sup.erl b/apps/emqx_authentication/src/emqx_authentication_sup.erl new file mode 100644 index 000000000..f22212b89 --- /dev/null +++ b/apps/emqx_authentication/src/emqx_authentication_sup.erl @@ -0,0 +1,29 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020 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_authentication_sup). + +-behaviour(supervisor). + +-export([ start_link/0 + , init/1 + ]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + {ok, {{one_for_one, 10, 10}, []}}. \ No newline at end of file diff --git a/rebar.config.erl b/rebar.config.erl index c4c0419b9..93a0c9592 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -267,6 +267,7 @@ relx_plugin_apps(ReleaseType) -> , emqx_sn , emqx_coap , emqx_stomp + , emqx_authentication , emqx_auth_http , emqx_auth_mysql , emqx_auth_jwt From c2f14c745b1742dffc3e90d89a935ff5a3ed5c4a Mon Sep 17 00:00:00 2001 From: zhouzb Date: Mon, 24 May 2021 11:38:32 +0800 Subject: [PATCH 02/15] chore(app vsn): update app vsn --- apps/emqx_authentication/src/emqx_authentication.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_authentication/src/emqx_authentication.app.src b/apps/emqx_authentication/src/emqx_authentication.app.src index e94f131ec..4f55ca0a7 100644 --- a/apps/emqx_authentication/src/emqx_authentication.app.src +++ b/apps/emqx_authentication/src/emqx_authentication.app.src @@ -1,6 +1,6 @@ {application, emqx_authentication, [{description, "EMQ X Authentication"}, - {vsn, "4.3.0"}, + {vsn, "0.1.0"}, {modules, []}, {registered, [emqx_authentication_sup, emqx_authentication_registry]}, {applications, [kernel,stdlib]}, From 3a96aa3db26d35167cae871bf70b893d77e6a72f Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 25 May 2021 15:00:34 +0800 Subject: [PATCH 03/15] test(authentication): add test case and fix some errors --- .../etc/emqx_authentication.conf | 0 .../include/emqx_authentication.hrl | 1 + .../priv/emqx_authentication.schema | 0 .../src/emqx_authentication.erl | 101 ++++++--- .../src/emqx_authentication_mnesia.erl | 10 +- .../test/data/user-credentials.csv | 2 + .../test/data/user-credentials.json | 4 + .../test/emqx_authentication_SUITE.erl | 192 ++++++++++++++++++ 8 files changed, 284 insertions(+), 26 deletions(-) create mode 100644 apps/emqx_authentication/etc/emqx_authentication.conf create mode 100644 apps/emqx_authentication/priv/emqx_authentication.schema create mode 100644 apps/emqx_authentication/test/data/user-credentials.csv create mode 100644 apps/emqx_authentication/test/data/user-credentials.json create mode 100644 apps/emqx_authentication/test/emqx_authentication_SUITE.erl diff --git a/apps/emqx_authentication/etc/emqx_authentication.conf b/apps/emqx_authentication/etc/emqx_authentication.conf new file mode 100644 index 000000000..e69de29bb diff --git a/apps/emqx_authentication/include/emqx_authentication.hrl b/apps/emqx_authentication/include/emqx_authentication.hrl index 58846cab5..72de452d4 100644 --- a/apps/emqx_authentication/include/emqx_authentication.hrl +++ b/apps/emqx_authentication/include/emqx_authentication.hrl @@ -25,6 +25,7 @@ { name , type %% service_type , provider + , params , state }). diff --git a/apps/emqx_authentication/priv/emqx_authentication.schema b/apps/emqx_authentication/priv/emqx_authentication.schema new file mode 100644 index 000000000..e69de29bb diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl index fbc59bc94..d02da5cf5 100644 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ b/apps/emqx_authentication/src/emqx_authentication.erl @@ -28,8 +28,11 @@ -export([ create_chain/1 , delete_chain/1 + , lookup_chain/1 , add_services_to_chain/2 , delete_services_from_chain/2 + , lookup_service/2 + , list_services/1 , move_service_to_the_front_of_chain/2 , move_service_to_the_end_of_chain/2 , move_service_to_the_nth_of_chain/3 @@ -79,18 +82,18 @@ mnesia(copy) -> ok = ekka_mnesia:copy_table(?SERVICE_TYPE_TAB, ram_copies). enable() -> - case emqx:hook('client.authenticate', fun emqx_authentication:authenticate/2) of - ok -> ok; - {error, already_exists} -> ok - end, - case emqx:hook('client.enhanced_authenticate', fun emqx_authentication:enhanced_authenticate/2) of + case emqx:hook('client.authenticate', fun emqx_authentication:authenticate/1) of ok -> ok; {error, already_exists} -> ok end. + % case emqx:hook('client.enhanced_authenticate', fun emqx_authentication:enhanced_authenticate/2) of + % ok -> ok; + % {error, already_exists} -> ok + % end. disable() -> emqx:unhook('client.authenticate', {}), - emqx:unhook('client.enhanced_authenticate', {}), + % emqx:unhook('client.enhanced_authenticate', {}), ok. authenticate(#{chain_id := ChainID} = ClientInfo) -> @@ -145,7 +148,7 @@ create_chain(Params = #{chain_id := ChainID}) -> services = Services, created_at = erlang:system_time(millisecond)}, mnesia:write(?CHAIN_TAB, Chain, write), - {ok, Chain}; + {ok, ChainID}; {error, Reason} -> {error, Reason} end; @@ -158,7 +161,7 @@ create_chain(Params = #{chain_id := ChainID}) -> end. delete_chain(ChainID) -> - mnesia:transaction( + trans( fun() -> case mnesia:read(?CHAIN_TAB, ChainID, write) of [] -> @@ -169,6 +172,14 @@ delete_chain(ChainID) -> end end). +lookup_chain(ChainID) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [Chain] -> + {ok, serialize_chain(Chain)} + end. + add_services_to_chain(ChainID, ServiceParams) -> case validate_service_params(ServiceParams) of {ok, NServiceParams} -> @@ -210,6 +221,27 @@ delete_services_from_chain(ChainID, ServiceNames) -> {error, Reason} end. +lookup_service(ChainID, ServiceName) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{services = Services}] -> + case lists:keytake(ServiceName, 1, Services) of + {value, Service, _} -> + {ok, serialize_service(Service)}; + false -> + {error, {not_found, {service, ServiceName}}} + end + end. + +list_services(ChainID) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{services = Services}] -> + {ok, [serialize_service(Service) || Service <- Services]} + end. + move_service_to_the_front_of_chain(ChainID, ServiceName) -> UpdateFun = fun(Chain = #chain{services = Services}) -> case move_service_to_the_front(ServiceName, Services) of @@ -246,17 +278,6 @@ move_service_to_the_nth_of_chain(ChainID, ServiceName, N) -> end, update_chain(ChainID, UpdateFun). -update_chain(ChainID, UpdateFun) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ChainID, write) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [Chain] -> - UpdateFun(Chain) - end - end). - import_user_credentials(ChainID, ServiceName, Filename, FileFormat) -> call_service(ChainID, ServiceName, import_user_credentials, [Filename, FileFormat]). @@ -323,6 +344,7 @@ validate_other_service_params([#{type := Type, params := Params} = ServiceParams NParams = emqx_rule_validator:validate_params(Params, ParamsSpec), validate_other_service_params(More, [ServiceParams#{params => NParams, + original_params => Params, provider => Provider} | Acc]); {error, not_found} -> {error, {not_found, {service_type, Type}}} @@ -344,12 +366,17 @@ create_services(ChainID, ServiceParams) -> create_services(_ChainID, [], Acc) -> {ok, lists:reverse(Acc)}; -create_services(ChainID, [#{name := Name, type := Type, provider := Provider, params := Params} | More], Acc) -> +create_services(ChainID, [#{name := Name, + type := Type, + provider := Provider, + params := Params, + original_params := OriginalParams} | More], Acc) -> case Provider:create(ChainID, Name, Params) of {ok, State} -> Service = #service{name = Name, type = Type, provider = Provider, + params = OriginalParams, state = State}, create_services(ChainID, More, [{Name, Service} | Acc]); {error, Reason} -> @@ -397,7 +424,7 @@ move_service_to_the_end(ServiceName, [Service | More], Passed) -> move_service_to_the_end(ServiceName, More, [Service | Passed]). move_service_to_nth(ServiceName, Services, N) - when length(Services) < N -> + when N =< length(Services) andalso N > 0 -> move_service_to_nth(ServiceName, Services, N, []); move_service_to_nth(_, _, _) -> {error, out_of_range}. @@ -407,10 +434,23 @@ move_service_to_nth(ServiceName, [], _, _) -> move_service_to_nth(ServiceName, [{ServiceName, _} = Service | More], N, Passed) when N =< length(Passed) -> {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), - {ok, L1 ++ [Service] + L2 + More}; + {ok, L1 ++ [Service] ++ L2 ++ More}; move_service_to_nth(ServiceName, [{ServiceName, _} = Service | More], N, Passed) -> {L1, L2} = lists:split(N - length(Passed) - 1, More), - {ok, lists:reverse(Passed) ++ L1 ++ [Service] ++ L2}. + {ok, lists:reverse(Passed) ++ L1 ++ [Service] ++ L2}; +move_service_to_nth(ServiceName, [Service | More], N, Passed) -> + move_service_to_nth(ServiceName, More, N, [Service | Passed]). + +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_service(ChainID, ServiceName, Func, Args) -> case mnesia:dirty_read(?CHAIN_TAB, ChainID) of @@ -431,6 +471,21 @@ call_service(ChainID, ServiceName, Func, Args) -> end end. +serialize_chain(#chain{id = ID, + services = Services, + created_at = CreatedAt}) -> + #{id => ID, + services => [serialize_service(Service) || Service <- Services], + created_at => CreatedAt}. + + +serialize_service({_, #service{name = Name, + type = Type, + params = Params}}) -> + #{name => Name, + type => Type, + params => Params}. + trans(Fun) -> trans(Fun, []). diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index 7521d4922..1744a5a91 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -139,8 +139,9 @@ import_user_credentials(Filename, csv, password_hash_algorithm := Algorithm}) -> case file:open(Filename, [read, binary]) of {ok, File} -> - import(UserGroup, File, Algorithm), - file:close(File); + Result = import(UserGroup, File, Algorithm), + file:close(File), + Result; {error, Reason} -> {error, Reason} end. @@ -211,7 +212,10 @@ do_import(_UserGroup, [_ | _More], _Algorithm) -> do_import(UserGroup, File, Algorithm) -> case file:read_line(File) of {ok, Line} -> - case binary:split(Line, <<",">>, [global]) of + case binary:split(Line, [<<",">>, <<"\n">>], [global]) of + [UserIdentity, Password, <<>>] -> + add(UserGroup, UserIdentity, Password, Algorithm), + do_import(UserGroup, File, Algorithm); [UserIdentity, Password] -> add(UserGroup, UserIdentity, Password, Algorithm), do_import(UserGroup, File, Algorithm); diff --git a/apps/emqx_authentication/test/data/user-credentials.csv b/apps/emqx_authentication/test/data/user-credentials.csv new file mode 100644 index 000000000..7ee4fe8f1 --- /dev/null +++ b/apps/emqx_authentication/test/data/user-credentials.csv @@ -0,0 +1,2 @@ +myuser3,mypassword3 +myuser4,mypassword4 \ No newline at end of file diff --git a/apps/emqx_authentication/test/data/user-credentials.json b/apps/emqx_authentication/test/data/user-credentials.json new file mode 100644 index 000000000..6c4689433 --- /dev/null +++ b/apps/emqx_authentication/test/data/user-credentials.json @@ -0,0 +1,4 @@ +{ + "myuser1": "mypassword1", + "myuser2": "mypassword2" +} \ No newline at end of file diff --git a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl new file mode 100644 index 000000000..e4a0c3182 --- /dev/null +++ b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl @@ -0,0 +1,192 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-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_authentication_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authentication). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([emqx_authentication]), + Config. + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([emqx_authentication]), + ok. + +t_chain(_) -> + ChainID = <<"mychain">>, + ChainParams = #{chain_id => ChainID, + service_params => []}, + ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), + ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(ChainParams)), + ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), + ok. + +t_service(_) -> + ChainID = <<"mychain">>, + ServiceName1 = <<"myservice1">>, + ServiceParams1 = #{name => ServiceName1, + type => mnesia, + params => #{ + user_identity_type => <<"username">>, + password_hash_algorithm => <<"sha256">>}}, + ChainParams = #{chain_id => ChainID, + service_params => [ServiceParams1]}, + ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), + Service1 = ServiceParams1, + ?assertMatch({ok, #{id := ChainID, services := [Service1]}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual({ok, Service1}, ?AUTH:lookup_service(ChainID, ServiceName1)), + ?assertEqual({ok, [Service1]}, ?AUTH:list_services(ChainID)), + ?assertEqual({error, {already_exists, {service, ServiceName1}}}, ?AUTH:add_services_to_chain(ChainID, [ServiceParams1])), + ServiceName2 = <<"myservice2">>, + ServiceParams2 = ServiceParams1#{name => ServiceName2}, + ?assertEqual(ok, ?AUTH:add_services_to_chain(ChainID, [ServiceParams2])), + Service2 = ServiceParams2, + ?assertMatch({ok, #{id := ChainID, services := [Service1, Service2]}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual({ok, Service2}, ?AUTH:lookup_service(ChainID, ServiceName2)), + ?assertEqual({ok, [Service1, Service2]}, ?AUTH:list_services(ChainID)), + + ?assertEqual(ok, ?AUTH:move_service_to_the_front_of_chain(ChainID, ServiceName2)), + ?assertEqual({ok, [Service2, Service1]}, ?AUTH:list_services(ChainID)), + ?assertEqual(ok, ?AUTH:move_service_to_the_end_of_chain(ChainID, ServiceName2)), + ?assertEqual({ok, [Service1, Service2]}, ?AUTH:list_services(ChainID)), + ?assertEqual(ok, ?AUTH:move_service_to_the_nth_of_chain(ChainID, ServiceName2, 1)), + ?assertEqual({ok, [Service2, Service1]}, ?AUTH:list_services(ChainID)), + ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth_of_chain(ChainID, ServiceName2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth_of_chain(ChainID, ServiceName2, 0)), + ?assertEqual(ok, ?AUTH:delete_services_from_chain(ChainID, [ServiceName1, ServiceName2])), + ?assertEqual({ok, []}, ?AUTH:list_services(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_mnesia_service(_) -> + ChainID = <<"mychain">>, + ServiceName = <<"myservice">>, + ServiceParams = #{name => ServiceName, + type => mnesia, + params => #{ + user_identity_type => <<"username">>, + password_hash_algorithm => <<"sha256">>}}, + ChainParams = #{chain_id => ChainID, + service_params => [ServiceParams]}, + ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), + UserCredential = #{user_identity => <<"myuser">>, + password => <<"mypass">>}, + ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, ServiceName, UserCredential)), + ?assertMatch({ok, #{user_identity := <<"myuser">>, password_hash := _}}, + ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), + ClientInfo = #{chain_id => ChainID, + username => <<"myuser">>, + password => <<"mypass">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ClientInfo2 = ClientInfo#{username => <<"baduser">>}, + ?assertEqual({error, user_credential_not_found}, ?AUTH:authenticate(ClientInfo2)), + ClientInfo3 = ClientInfo#{password => <<"badpass">>}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + UserCredential2 = UserCredential#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:update_user_credential(ChainID, ServiceName, UserCredential2)), + ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + ?assertEqual(ok, ?AUTH:delete_user_credential(ChainID, ServiceName, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), + + ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, ServiceName, UserCredential)), + ?assertMatch({ok, #{user_identity := <<"myuser">>}}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_services_from_chain(ChainID, [ServiceName])), + ?assertEqual(ok, ?AUTH:add_services_to_chain(ChainID, [ServiceParams])), + ?assertMatch({error, not_found}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), + + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual([], ets:tab2list(mnesia_basic_auth)), + ok. + +t_import(_) -> + ChainID = <<"mychain">>, + ServiceName = <<"myservice">>, + ServiceParams = #{name => ServiceName, + type => mnesia, + params => #{ + user_identity_type => <<"username">>, + password_hash_algorithm => <<"sha256">>}}, + ChainParams = #{chain_id => ChainID, + service_params => [ServiceParams]}, + ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), + Dir = code:lib_dir(emqx_authentication, test), + ?assertEqual(ok, ?AUTH:import_user_credentials(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.json"]), json)), + ?assertEqual(ok, ?AUTH:import_user_credentials(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.csv"]), csv)), + ?assertMatch({ok, #{user_identity := <<"myuser1">>}}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser1">>)), + ?assertMatch({ok, #{user_identity := <<"myuser3">>}}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser3">>)), + ClientInfo1 = #{chain_id => ChainID, + username => <<"myuser1">>, + password => <<"mypassword1">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, + password => <<"mypassword3">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_multi_mnesia_service(_) -> + ChainID = <<"mychain">>, + ServiceName1 = <<"myservice1">>, + ServiceParams1 = #{name => ServiceName1, + type => mnesia, + params => #{ + user_identity_type => <<"username">>, + password_hash_algorithm => <<"sha256">>}}, + ServiceName2 = <<"myservice2">>, + ServiceParams2 = #{name => ServiceName2, + type => mnesia, + params => #{ + user_identity_type => <<"clientid">>, + password_hash_algorithm => <<"sha256">>}}, + ChainParams = #{chain_id => ChainID, + service_params => [ServiceParams1, ServiceParams2]}, + ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), + + ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, + ServiceName1, + #{user_identity => <<"myuser">>, + password => <<"mypass1">>})), + ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, + ServiceName2, + #{user_identity => <<"myclient">>, + password => <<"mypass2">>})), + ClientInfo1 = #{chain_id => ChainID, + username => <<"myuser">>, + clientid => <<"myclient">>, + password => <<"mypass1">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ?assertEqual(ok, ?AUTH:move_service_to_the_front_of_chain(ChainID, ServiceName2)), + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), + ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + + + From c3231130683ed66495a91dc7bebba4fe71479af6 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 25 May 2021 15:02:31 +0800 Subject: [PATCH 04/15] fix(authentication): fix unhook --- apps/emqx_authentication/src/emqx_authentication.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl index d02da5cf5..4cf7a0e34 100644 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ b/apps/emqx_authentication/src/emqx_authentication.erl @@ -92,7 +92,7 @@ enable() -> % end. disable() -> - emqx:unhook('client.authenticate', {}), + emqx:unhook('client.authenticate', fun emqx_authentication:authenticate/1), % emqx:unhook('client.enhanced_authenticate', {}), ok. From 54106790c71d3bdaa37ca732f3466342e593f775 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 27 May 2021 10:19:38 +0800 Subject: [PATCH 05/15] feat(authentication): add new apis --- apps/emqx_authentication/rebar.config | 2 +- .../src/emqx_authentication.erl | 114 +++++--- .../src/emqx_authentication_api.erl | 245 ++++++++++++++++++ .../src/emqx_authentication_app.erl | 2 +- .../src/emqx_authentication_mnesia.erl | 6 +- .../src/emqx_authentication_sup.erl | 2 +- .../test/emqx_authentication_SUITE.erl | 32 +-- 7 files changed, 341 insertions(+), 62 deletions(-) create mode 100644 apps/emqx_authentication/src/emqx_authentication_api.erl diff --git a/apps/emqx_authentication/rebar.config b/apps/emqx_authentication/rebar.config index 73696b033..0a0af8c29 100644 --- a/apps/emqx_authentication/rebar.config +++ b/apps/emqx_authentication/rebar.config @@ -15,4 +15,4 @@ {cover_enabled, true}. {cover_opts, [verbose]}. -{cover_export_enabled, true}. +{cover_export_enabled, true}. \ No newline at end of file diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl index 4cf7a0e34..84d730706 100644 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ b/apps/emqx_authentication/src/emqx_authentication.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. +%% 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. @@ -29,13 +29,15 @@ -export([ create_chain/1 , delete_chain/1 , lookup_chain/1 - , add_services_to_chain/2 - , delete_services_from_chain/2 + , list_chains/0 + , add_services/2 + , delete_services/2 + , update_service/3 , lookup_service/2 , list_services/1 - , move_service_to_the_front_of_chain/2 - , move_service_to_the_end_of_chain/2 - , move_service_to_the_nth_of_chain/3 + , move_service_to_the_front/2 + , move_service_to_the_end/2 + , move_service_to_the_nth/3 ]). -export([ import_user_credentials/4 @@ -135,7 +137,7 @@ register_service_types([{_App, Mod, #{name := Name, register_service_types(Types, [ServiceType | Acc]). create_chain(Params = #{chain_id := ChainID}) -> - ServiceParams = maps:get(service_params, Params, []), + ServiceParams = maps:get(services, Params, []), case validate_service_params(ServiceParams) of {ok, NServiceParams} -> trans( @@ -167,7 +169,7 @@ delete_chain(ChainID) -> [] -> {error, {not_found, {chain, ChainID}}}; [#chain{services = Services}] -> - ok = delete_services(Services), + ok = delete_services_(Services), mnesia:delete(?CHAIN_TAB, ChainID, write) end end). @@ -180,7 +182,11 @@ lookup_chain(ChainID) -> {ok, serialize_chain(Chain)} end. -add_services_to_chain(ChainID, ServiceParams) -> +list_chains() -> + Chains = ets:tab2list(?CHAIN_TAB), + [serialize_chain(Chain) || Chain <- Chains]. + +add_services(ChainID, ServiceParams) -> case validate_service_params(ServiceParams) of {ok, NServiceParams} -> UpdateFun = fun(Chain = #chain{services = Services}) -> @@ -203,13 +209,13 @@ add_services_to_chain(ChainID, ServiceParams) -> {error, Reason} end. -delete_services_from_chain(ChainID, ServiceNames) -> +delete_services(ChainID, ServiceNames) -> case no_duplicate_names(ServiceNames) of ok -> UpdateFun = fun(Chain = #chain{services = Services}) -> case extract_services(ServiceNames, Services) of {ok, Extracted, Rest} -> - ok = delete_services(Extracted), + ok = delete_services_(Extracted), NChain = Chain#chain{services = Rest}, mnesia:write(?CHAIN_TAB, NChain, write); {error, Reason} -> @@ -221,6 +227,31 @@ delete_services_from_chain(ChainID, ServiceNames) -> {error, Reason} end. +update_service(ChainID, ServiceName, NewParams) -> + UpdateFun = fun(Chain = #chain{services = Services}) -> + case proplists:get_value(ServiceName, Services, undefined) of + undefined -> + {error, {not_found, {service, ServiceName}}}; + #service{type = Type, + provider = Provider, + params = OriginalParams, + state = State} = Service -> + Params = maps:merge(OriginalParams, NewParams), + {ok, #service_type{params_spec = ParamsSpec}} = find_service_type(Type), + NParams = emqx_rule_validator:validate_params(Params, ParamsSpec), + case Provider:update(ChainID, ServiceName, NParams, State) of + {ok, NState} -> + NService = Service#service{params = Params, + state = NState}, + NServices = lists:keyreplace(ServiceName, 1, Services, [{ServiceName, NService}]), + mnesia:write(?CHAIN_TAB, Chain#chain{services = NServices}, write); + {error, Reason} -> + {error, Reason} + end + end + end, + update_chain(ChainID, UpdateFun). + lookup_service(ChainID, ServiceName) -> case mnesia:dirty_read(?CHAIN_TAB, ChainID) of [] -> @@ -242,9 +273,9 @@ list_services(ChainID) -> {ok, [serialize_service(Service) || Service <- Services]} end. -move_service_to_the_front_of_chain(ChainID, ServiceName) -> +move_service_to_the_front(ChainID, ServiceName) -> UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_front(ServiceName, Services) of + case move_service_to_the_front_(ServiceName, Services) of {ok, NServices} -> NChain = Chain#chain{services = NServices}, mnesia:write(?CHAIN_TAB, NChain, write); @@ -254,9 +285,9 @@ move_service_to_the_front_of_chain(ChainID, ServiceName) -> end, update_chain(ChainID, UpdateFun). -move_service_to_the_end_of_chain(ChainID, ServiceName) -> +move_service_to_the_end(ChainID, ServiceName) -> UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_end(ServiceName, Services) of + case move_service_to_the_end_(ServiceName, Services) of {ok, NServices} -> NChain = Chain#chain{services = NServices}, mnesia:write(?CHAIN_TAB, NChain, write); @@ -266,9 +297,9 @@ move_service_to_the_end_of_chain(ChainID, ServiceName) -> end, update_chain(ChainID, UpdateFun). -move_service_to_the_nth_of_chain(ChainID, ServiceName, N) -> +move_service_to_the_nth(ChainID, ServiceName, N) -> UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_nth(ServiceName, Services, N) of + case move_service_to_the_nth_(ServiceName, Services, N) of {ok, NServices} -> NChain = Chain#chain{services = NServices}, mnesia:write(?CHAIN_TAB, NChain, write); @@ -380,15 +411,15 @@ create_services(ChainID, [#{name := Name, state = State}, create_services(ChainID, More, [{Name, Service} | Acc]); {error, Reason} -> - delete_services(Acc), + delete_services_(Acc), {error, Reason} end. -delete_services([]) -> +delete_services_([]) -> ok; -delete_services([{_, #service{provider = Provider, state = State}} | More]) -> +delete_services_([{_, #service{provider = Provider, state = State}} | More]) -> Provider:destroy(State), - delete_services(More). + delete_services_(More). extract_services(ServiceNames, Services) -> extract_services(ServiceNames, Services, []). @@ -403,43 +434,43 @@ extract_services([ServiceName | More], Services, Acc) -> {error, {not_found, {service, ServiceName}}} end. -move_service_to_the_front(ServiceName, Services) -> - move_service_to_the_front(ServiceName, Services, []). +move_service_to_the_front_(ServiceName, Services) -> + move_service_to_the_front_(ServiceName, Services, []). -move_service_to_the_front(ServiceName, [], _) -> +move_service_to_the_front_(ServiceName, [], _) -> {error, {not_found, {service, ServiceName}}}; -move_service_to_the_front(ServiceName, [{ServiceName, _} = Service | More], Passed) -> +move_service_to_the_front_(ServiceName, [{ServiceName, _} = Service | More], Passed) -> {ok, [Service | (lists:reverse(Passed) ++ More)]}; -move_service_to_the_front(ServiceName, [Service | More], Passed) -> - move_service_to_the_front(ServiceName, More, [Service | Passed]). +move_service_to_the_front_(ServiceName, [Service | More], Passed) -> + move_service_to_the_front_(ServiceName, More, [Service | Passed]). -move_service_to_the_end(ServiceName, Services) -> - move_service_to_the_end(ServiceName, Services, []). +move_service_to_the_end_(ServiceName, Services) -> + move_service_to_the_end_(ServiceName, Services, []). -move_service_to_the_end(ServiceName, [], _) -> +move_service_to_the_end_(ServiceName, [], _) -> {error, {not_found, {service, ServiceName}}}; -move_service_to_the_end(ServiceName, [{ServiceName, _} = Service | More], Passed) -> +move_service_to_the_end_(ServiceName, [{ServiceName, _} = Service | More], Passed) -> {ok, lists:reverse(Passed) ++ More ++ [Service]}; -move_service_to_the_end(ServiceName, [Service | More], Passed) -> - move_service_to_the_end(ServiceName, More, [Service | Passed]). +move_service_to_the_end_(ServiceName, [Service | More], Passed) -> + move_service_to_the_end_(ServiceName, More, [Service | Passed]). -move_service_to_nth(ServiceName, Services, N) +move_service_to_the_nth_(ServiceName, Services, N) when N =< length(Services) andalso N > 0 -> - move_service_to_nth(ServiceName, Services, N, []); -move_service_to_nth(_, _, _) -> + move_service_to_the_nth_(ServiceName, Services, N, []); +move_service_to_the_nth_(_, _, _) -> {error, out_of_range}. -move_service_to_nth(ServiceName, [], _, _) -> +move_service_to_the_nth_(ServiceName, [], _, _) -> {error, {not_found, {service, ServiceName}}}; -move_service_to_nth(ServiceName, [{ServiceName, _} = Service | More], N, Passed) +move_service_to_the_nth_(ServiceName, [{ServiceName, _} = Service | More], N, Passed) when N =< length(Passed) -> {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), {ok, L1 ++ [Service] ++ L2 ++ More}; -move_service_to_nth(ServiceName, [{ServiceName, _} = Service | More], N, Passed) -> +move_service_to_the_nth_(ServiceName, [{ServiceName, _} = Service | More], N, Passed) -> {L1, L2} = lists:split(N - length(Passed) - 1, More), {ok, lists:reverse(Passed) ++ L1 ++ [Service] ++ L2}; -move_service_to_nth(ServiceName, [Service | More], N, Passed) -> - move_service_to_nth(ServiceName, More, N, [Service | Passed]). +move_service_to_the_nth_(ServiceName, [Service | More], N, Passed) -> + move_service_to_the_nth_(ServiceName, More, N, [Service | Passed]). update_chain(ChainID, UpdateFun) -> trans( @@ -478,7 +509,6 @@ serialize_chain(#chain{id = ID, services => [serialize_service(Service) || Service <- Services], created_at => CreatedAt}. - serialize_service({_, #service{name = Name, type = Type, params = Params}}) -> diff --git a/apps/emqx_authentication/src/emqx_authentication_api.erl b/apps/emqx_authentication/src/emqx_authentication_api.erl new file mode 100644 index 000000000..a3ed80250 --- /dev/null +++ b/apps/emqx_authentication/src/emqx_authentication_api.erl @@ -0,0 +1,245 @@ +%%-------------------------------------------------------------------- +%% 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_authentication_api). + +-export([ create_chain/2 + , delete_chain/2 + , lookup_chain/2 + , list_chains/2 + , add_services/2 + , delete_services/2 + , update_service/2 + , lookup_service/2 + , list_services/2 + , move_service/2 + , import_user_credentials/2 + , add_user_creadential/2 + ]). + +-import(minirest, [return/1]). + +-rest_api(#{name => list_chains, + method => 'GET', + path => "/authentication/chains", + func => list_chains, + descr => "List all chains" + }). + +-rest_api(#{name => create_chain, + method => 'POST', + path => "/authentication/chains", + func => create_chain, + descr => "Create a chain" + }). + +create_chain(_Binding, Params = #{chain_id := ChainID}) -> + case emqx_authentication:create_chain(Params) of + {ok, ChainID} -> + return({ok, ChainID}); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +create_chain(_Binding, _Params) -> + return({error, serialize_error({missing_parameter, chain_id})}). + +delete_chain(_Binding, #{chain_id := ChainID}) -> + case emqx_authentication:delete_chain(ChainID) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end. + +lookup_chain(_Binding, #{chain_id := ChainID}) -> + case emqx_authentication:lookup_chain(ChainID) of + {ok, Chain} -> + return({ok, Chain}); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +lookup_chain(_Binding, _Params) -> + return({error, serialize_error({missing_parameter, chain_id})}). + +list_chains(_Binding, _Params) -> + emqx_authentication:list_chains(). + +add_services(_Binding, Params = #{chain_id := ChainID}) -> + case maps:get(services, Params, []) of + [] -> return(ok); + Services -> + case emqx_authentication:add_services(ChainID, Services) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end + end; +add_services(_Binding, _Params) -> + return({error, serialize_error({missing_parameter, chain_id})}). + +delete_services(_Binding, #{chain_id := ChainID, + service_names := ServiceNames}) -> + case emqx_authentication:delete_services(ChainID, ServiceNames) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +delete_services(_Binding, #{chain_id := _}) -> + return({error, serialize_error({missing_parameter, service_names})}); +delete_services(_Binding, #{service_names := _}) -> + return({error, serialize_error({missing_parameter, chain_id})}). + +%% TODO: better input parameters +update_service(_Binding, #{chain_id := ChainID, + service_name := ServiceName, + service_params := Params}) -> + case emqx_authentication:update_service(ChainID, ServiceName, Params) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +update_service(_Binding, #{chain_id := _}) -> + return({error, serialize_error({missing_parameter, service_name})}); +update_service(_Binding, #{service_name := _}) -> + return({error, serialize_error({missing_parameter, chain_id})}). + +lookup_service(_Binding, #{chain_id := ChainID, + service_name := ServiceName}) -> + case emqx_authentication:lookup_service(ChainID, ServiceName) of + {ok, Service} -> + return({ok, Service}); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +lookup_service(_Binding, #{chain_id := _}) -> + return({error, serialize_error({missing_parameter, service_name})}); +lookup_service(_Binding, #{service_name := _}) -> + return({error, serialize_error({missing_parameter, chain_id})}). + +list_services(_Binding, #{chain_id := ChainID}) -> + case emqx_authentication:list_services(ChainID) of + {ok, Service} -> + return({ok, Service}); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +list_services(_Binding, _Params) -> + return({error, serialize_error({missing_parameter, chain_id})}). + +move_service(_Binding, #{chain_id := ChainID, + service_name := ServiceName, + to := <<"the front">>}) -> + case emqx_authenticaiton:move_service_to_the_front(ChainID, ServiceName) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +move_service(_Binding, #{chain_id := ChainID, + service_name := ServiceName, + to := <<"the end">>}) -> + case emqx_authenticaiton:move_service_to_the_end(ChainID, ServiceName) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +move_service(_Binding, #{chain_id := ChainID, + service_name := ServiceName, + to := N}) when is_number(N) -> + case emqx_authenticaiton:move_service_to_the_nth(ChainID, ServiceName, N) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +move_service(_Binding, Params) -> + Missed = get_missed_params(Params, [chain_id, service_name, to]), + return({error, serialize_error({missing_parameter, Missed})}). + +import_user_credentials(_Binding, #{chain_id := ChainID, + service_name := ServiceName, + filename := Filename, + file_format := FileFormat}) -> + case emqx_authentication:import_user_credentials(ChainID, ServiceName, Filename, FileFormat) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +import_user_credentials(_Binding, Params) -> + Missed = get_missed_params(Params, [chain_id, service_name, filename, file_format]), + return({error, serialize_error({missing_parameter, Missed})}). + +add_user_creadential(_Binding, #{chain_id := ChainID, + service_name := ServiceName, + credential := Credential}) -> + case emqx_authentication:add_user_creadentials(ChainID, ServiceName, Credential) of + ok -> + return(ok); + {error, Reason} -> + return({error, serialize_error(Reason)}) + end; +add_user_creadential(_Binding, Params) -> + Missed = get_missed_params(Params, [chain_id, service_name, credential]), + return({error, serialize_error({missing_parameter, Missed})}). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +serialize_error(Reason) when not is_map(Reason) -> + Error = serialize_error_(Reason), + emqx_json:encode(Error). + +serialize_error_({already_exists, {Type, ID}}) -> + #{code => "ALREADY_EXISTS", + message => io_lib:format("~p ~p already exists", [serialize_type(Type), ID])}; +serialize_error_({not_found, {Type, ID}}) -> + #{code => "NOT_FOUND", + message => io_lib:format("~p ~p not found", [serialize_type(Type), ID])}; +serialize_error_({duplicate, Name}) -> + #{code => "INVALID_PARAMETER", + message => io_lib:format("Service name ~p is duplicated", [Name])}; +serialize_error_({missing_parameter, Names = [_ | Rest]}) -> + Format = ["~p," || _ <- Rest] ++ ["~p"], + NFormat = binary_to_list(iolist_to_binary(Format)), + #{code => "MISSING_PARAMETER", + message => io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names)}; +serialize_error_({missing_parameter, Name}) -> + #{code => "MISSING_PARAMETER", + message => io_lib:format("The input parameter ~p that is mandatory for processing this request is not supplied.", [Name])}; +serialize_error_(_) -> + #{code => "UNKNOWN_ERROR"}. + +serialize_type(service) -> + "Service"; +serialize_type(chain) -> + "Chain"; +serialize_type(service_type) -> + "Service type". + +get_missed_params(Actual, Expected) -> + Keys = lists:fold(fun(Key, Acc) -> + case maps:is_key(Key, Actual) of + true -> Acc; + false -> [Key | Acc] + end + end, [], Expected), + lists:reverse(Keys). \ No newline at end of file diff --git a/apps/emqx_authentication/src/emqx_authentication_app.erl b/apps/emqx_authentication/src/emqx_authentication_app.erl index 5fccf7ed6..473375455 100644 --- a/apps/emqx_authentication/src/emqx_authentication_app.erl +++ b/apps/emqx_authentication/src/emqx_authentication_app.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. +%% 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. diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index 1744a5a91..63b37f05d 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. +%% 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. @@ -19,6 +19,7 @@ -include("emqx_authentication.hrl"). -export([ create/3 + , update/4 , authenticate/2 , destroy/1 ]). @@ -88,6 +89,9 @@ create(ChainID, ServiceName, #{<<"user_identity_type">> := Type, password_hash_algorithm => binary_to_atom(Algorithm, utf8)}, {ok, State}. +update(ChainID, ServiceName, Params, _State) -> + create(ChainID, ServiceName, Params). + authenticate(ClientInfo = #{password := Password}, #{user_group := UserGroup, user_identity_type := Type, diff --git a/apps/emqx_authentication/src/emqx_authentication_sup.erl b/apps/emqx_authentication/src/emqx_authentication_sup.erl index f22212b89..a36617ac8 100644 --- a/apps/emqx_authentication/src/emqx_authentication_sup.erl +++ b/apps/emqx_authentication/src/emqx_authentication_sup.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. +%% 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. diff --git a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl index e4a0c3182..38efe0fe3 100644 --- a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl +++ b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl @@ -38,7 +38,7 @@ end_per_suite(_) -> t_chain(_) -> ChainID = <<"mychain">>, ChainParams = #{chain_id => ChainID, - service_params => []}, + services => []}, ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(ChainParams)), ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), @@ -55,30 +55,30 @@ t_service(_) -> user_identity_type => <<"username">>, password_hash_algorithm => <<"sha256">>}}, ChainParams = #{chain_id => ChainID, - service_params => [ServiceParams1]}, + services => [ServiceParams1]}, ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), Service1 = ServiceParams1, ?assertMatch({ok, #{id := ChainID, services := [Service1]}}, ?AUTH:lookup_chain(ChainID)), ?assertEqual({ok, Service1}, ?AUTH:lookup_service(ChainID, ServiceName1)), ?assertEqual({ok, [Service1]}, ?AUTH:list_services(ChainID)), - ?assertEqual({error, {already_exists, {service, ServiceName1}}}, ?AUTH:add_services_to_chain(ChainID, [ServiceParams1])), + ?assertEqual({error, {already_exists, {service, ServiceName1}}}, ?AUTH:add_services(ChainID, [ServiceParams1])), ServiceName2 = <<"myservice2">>, ServiceParams2 = ServiceParams1#{name => ServiceName2}, - ?assertEqual(ok, ?AUTH:add_services_to_chain(ChainID, [ServiceParams2])), + ?assertEqual(ok, ?AUTH:add_services(ChainID, [ServiceParams2])), Service2 = ServiceParams2, ?assertMatch({ok, #{id := ChainID, services := [Service1, Service2]}}, ?AUTH:lookup_chain(ChainID)), ?assertEqual({ok, Service2}, ?AUTH:lookup_service(ChainID, ServiceName2)), ?assertEqual({ok, [Service1, Service2]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_front_of_chain(ChainID, ServiceName2)), + ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)), ?assertEqual({ok, [Service2, Service1]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_end_of_chain(ChainID, ServiceName2)), + ?assertEqual(ok, ?AUTH:move_service_to_the_end(ChainID, ServiceName2)), ?assertEqual({ok, [Service1, Service2]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_nth_of_chain(ChainID, ServiceName2, 1)), + ?assertEqual(ok, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 1)), ?assertEqual({ok, [Service2, Service1]}, ?AUTH:list_services(ChainID)), - ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth_of_chain(ChainID, ServiceName2, 3)), - ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth_of_chain(ChainID, ServiceName2, 0)), - ?assertEqual(ok, ?AUTH:delete_services_from_chain(ChainID, [ServiceName1, ServiceName2])), + ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 0)), + ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName1, ServiceName2])), ?assertEqual({ok, []}, ?AUTH:list_services(ChainID)), ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), ok. @@ -92,7 +92,7 @@ t_mnesia_service(_) -> user_identity_type => <<"username">>, password_hash_algorithm => <<"sha256">>}}, ChainParams = #{chain_id => ChainID, - service_params => [ServiceParams]}, + services => [ServiceParams]}, ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), UserCredential = #{user_identity => <<"myuser">>, password => <<"mypass">>}, @@ -116,8 +116,8 @@ t_mnesia_service(_) -> ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, ServiceName, UserCredential)), ?assertMatch({ok, #{user_identity := <<"myuser">>}}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_services_from_chain(ChainID, [ServiceName])), - ?assertEqual(ok, ?AUTH:add_services_to_chain(ChainID, [ServiceParams])), + ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName])), + ?assertEqual(ok, ?AUTH:add_services(ChainID, [ServiceParams])), ?assertMatch({error, not_found}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), @@ -133,7 +133,7 @@ t_import(_) -> user_identity_type => <<"username">>, password_hash_algorithm => <<"sha256">>}}, ChainParams = #{chain_id => ChainID, - service_params => [ServiceParams]}, + services => [ServiceParams]}, ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), Dir = code:lib_dir(emqx_authentication, test), ?assertEqual(ok, ?AUTH:import_user_credentials(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.json"]), json)), @@ -165,7 +165,7 @@ t_multi_mnesia_service(_) -> user_identity_type => <<"clientid">>, password_hash_algorithm => <<"sha256">>}}, ChainParams = #{chain_id => ChainID, - service_params => [ServiceParams1, ServiceParams2]}, + services => [ServiceParams1, ServiceParams2]}, ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, @@ -181,7 +181,7 @@ t_multi_mnesia_service(_) -> clientid => <<"myclient">>, password => <<"mypass1">>}, ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), - ?assertEqual(ok, ?AUTH:move_service_to_the_front_of_chain(ChainID, ServiceName2)), + ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)), ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), From 9d4af87eb720de068a897c1e0e39250611222e47 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Sat, 29 May 2021 09:58:27 +0800 Subject: [PATCH 06/15] feat(authentication): implement http api and change external apis --- .../data/user-credentials.csv | 4 +- .../data/user-credentials.json | 4 +- .../src/emqx_authentication.erl | 115 +++-- .../src/emqx_authentication_api.erl | 452 ++++++++++++------ .../src/emqx_authentication_app.erl | 2 + .../src/emqx_authentication_mnesia.erl | 234 +++++---- .../test/data/user-credentials.csv | 4 +- .../test/data/user-credentials.json | 4 +- .../test/emqx_authentication_SUITE.erl | 117 +++-- 9 files changed, 563 insertions(+), 373 deletions(-) diff --git a/apps/emqx_authentication/data/user-credentials.csv b/apps/emqx_authentication/data/user-credentials.csv index 7ee4fe8f1..98bd92d52 100644 --- a/apps/emqx_authentication/data/user-credentials.csv +++ b/apps/emqx_authentication/data/user-credentials.csv @@ -1,2 +1,2 @@ -myuser3,mypassword3 -myuser4,mypassword4 \ No newline at end of file +myuser3,8d41233e39c95b5da13361e354e1c9e639f07b27d397463a8f91b71ee07ccfb2 +myuser4,5809df0154f3cb4ac5c3a5572eaca0c5f7f9d858e887fc675b2becab9feb19d1 \ No newline at end of file diff --git a/apps/emqx_authentication/data/user-credentials.json b/apps/emqx_authentication/data/user-credentials.json index 6c4689433..a153be616 100644 --- a/apps/emqx_authentication/data/user-credentials.json +++ b/apps/emqx_authentication/data/user-credentials.json @@ -1,4 +1,4 @@ { - "myuser1": "mypassword1", - "myuser2": "mypassword2" + "myuser1": "09343625c6c123d3434932fe1ce08bae5ac00a8f95bd746e10491b0bafdd1817", + "myuser2": "8767a7d316ad68cb607c7c805b859ffa78277dda13b7a3e2e8b53cad3cabbc6e" } \ No newline at end of file diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl index 84d730706..1ee5f46bc 100644 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ b/apps/emqx_authentication/src/emqx_authentication.erl @@ -40,11 +40,12 @@ , move_service_to_the_nth/3 ]). --export([ import_user_credentials/4 - , add_user_credential/3 - , delete_user_credential/3 - , update_user_credential/3 - , lookup_user_credential/3 +-export([ import_users/3 + , add_user/3 + , delete_user/3 + , update_user/4 + , lookup_user/3 + , list_users/2 ]). -export([mnesia/1]). @@ -101,7 +102,7 @@ disable() -> authenticate(#{chain_id := ChainID} = ClientInfo) -> case mnesia:dirty_read(?CHAIN_TAB, ChainID) of [#chain{services = []}] -> - {error, todo}; + {error, no_services}; [#chain{services = Services}] -> do_authenticate(Services, ClientInfo); [] -> @@ -109,7 +110,7 @@ authenticate(#{chain_id := ChainID} = ClientInfo) -> end. do_authenticate([], _) -> - {error, user_credential_not_found}; + {error, user_not_found}; do_authenticate([{_, #service{provider = Provider, state = State}} | More], ClientInfo) -> case Provider:authenticate(ClientInfo, State) of ignore -> do_authenticate(More, ClientInfo); @@ -136,55 +137,44 @@ register_service_types([{_App, Mod, #{name := Name, params_spec = ParamsSpec}, register_service_types(Types, [ServiceType | Acc]). -create_chain(Params = #{chain_id := ChainID}) -> - ServiceParams = maps:get(services, Params, []), - case validate_service_params(ServiceParams) of - {ok, NServiceParams} -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ChainID, write) of - [] -> - case create_services(ChainID, NServiceParams) of - {ok, Services} -> - Chain = #chain{id = ChainID, - services = Services, - created_at = erlang:system_time(millisecond)}, - mnesia:write(?CHAIN_TAB, Chain, write), - {ok, ChainID}; - {error, Reason} -> - {error, Reason} - end; - [_ | _] -> - {error, {already_exists, {chain, ChainID}}} - end - end); - {error, Reason} -> - {error, Reason} - end. - -delete_chain(ChainID) -> +create_chain(#{id := ID}) -> trans( fun() -> - case mnesia:read(?CHAIN_TAB, ChainID, write) of + case mnesia:read(?CHAIN_TAB, ID, write) of [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{services = Services}] -> - ok = delete_services_(Services), - mnesia:delete(?CHAIN_TAB, ChainID, write) + Chain = #chain{id = ID, + services = [], + created_at = erlang:system_time(millisecond)}, + mnesia:write(?CHAIN_TAB, Chain, write), + {ok, serialize_chain(Chain)}; + [_ | _] -> + {error, {already_exists, {chain, ID}}} end end). -lookup_chain(ChainID) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of +delete_chain(ID) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ID, write) of + [] -> + {error, {not_found, {chain, ID}}}; + [#chain{services = Services}] -> + ok = delete_services_(Services), + mnesia:delete(?CHAIN_TAB, ID, write) + end + end). + +lookup_chain(ID) -> + case mnesia:dirty_read(?CHAIN_TAB, ID) of [] -> - {error, {not_found, {chain, ChainID}}}; + {error, {not_found, {chain, ID}}}; [Chain] -> {ok, serialize_chain(Chain)} end. list_chains() -> Chains = ets:tab2list(?CHAIN_TAB), - [serialize_chain(Chain) || Chain <- Chains]. + {ok, [serialize_chain(Chain) || Chain <- Chains]}. add_services(ChainID, ServiceParams) -> case validate_service_params(ServiceParams) of @@ -195,8 +185,10 @@ add_services(ChainID, ServiceParams) -> ok -> case create_services(ChainID, NServiceParams) of {ok, NServices} -> + io:format("~p~n", [NServices]), NChain = Chain#chain{services = Services ++ NServices}, - mnesia:write(?CHAIN_TAB, NChain, write); + ok = mnesia:write(?CHAIN_TAB, NChain, write), + {ok, serialize_services(NServices)}; {error, Reason} -> {error, Reason} end; @@ -243,8 +235,9 @@ update_service(ChainID, ServiceName, NewParams) -> {ok, NState} -> NService = Service#service{params = Params, state = NState}, - NServices = lists:keyreplace(ServiceName, 1, Services, [{ServiceName, NService}]), - mnesia:write(?CHAIN_TAB, Chain#chain{services = NServices}, write); + NServices = lists:keyreplace(ServiceName, 1, Services, {ServiceName, NService}), + ok = mnesia:write(?CHAIN_TAB, Chain#chain{services = NServices}, write), + {ok, serialize_service({ServiceName, NService})}; {error, Reason} -> {error, Reason} end @@ -270,7 +263,7 @@ list_services(ChainID) -> [] -> {error, {not_found, {chain, ChainID}}}; [#chain{services = Services}] -> - {ok, [serialize_service(Service) || Service <- Services]} + {ok, serialize_services(Services)} end. move_service_to_the_front(ChainID, ServiceName) -> @@ -309,20 +302,23 @@ move_service_to_the_nth(ChainID, ServiceName, N) -> end, update_chain(ChainID, UpdateFun). -import_user_credentials(ChainID, ServiceName, Filename, FileFormat) -> - call_service(ChainID, ServiceName, import_user_credentials, [Filename, FileFormat]). +import_users(ChainID, ServiceName, Filename) -> + call_service(ChainID, ServiceName, import_users, [Filename]). -add_user_credential(ChainID, ServiceName, Credential) -> - call_service(ChainID, ServiceName, add_user_credential, [Credential]). +add_user(ChainID, ServiceName, UserInfo) -> + call_service(ChainID, ServiceName, add_user, [UserInfo]). -delete_user_credential(ChainID, ServiceName, UserIdentity) -> - call_service(ChainID, ServiceName, delete_user_credential, [UserIdentity]). +delete_user(ChainID, ServiceName, UserID) -> + call_service(ChainID, ServiceName, delete_user, [UserID]). -update_user_credential(ChainID, ServiceName, Credential) -> - call_service(ChainID, ServiceName, update_user_credential, [Credential]). +update_user(ChainID, ServiceName, UserID, NewUserInfo) -> + call_service(ChainID, ServiceName, update_user, [UserID, NewUserInfo]). -lookup_user_credential(ChainID, ServiceName, UserIdentity) -> - call_service(ChainID, ServiceName, lookup_user_credential, [UserIdentity]). +lookup_user(ChainID, ServiceName, UserID) -> + call_service(ChainID, ServiceName, lookup_user, [UserID]). + +list_users(ChainID, ServiceName) -> + call_service(ChainID, ServiceName, list_users, []). %%------------------------------------------------------------------------------ %% Internal functions @@ -506,9 +502,12 @@ serialize_chain(#chain{id = ID, services = Services, created_at = CreatedAt}) -> #{id => ID, - services => [serialize_service(Service) || Service <- Services], + services => serialize_services(Services), created_at => CreatedAt}. +serialize_services(Services) -> + [serialize_service(Service) || Service <- Services]. + serialize_service({_, #service{name = Name, type = Type, params = Params}}) -> diff --git a/apps/emqx_authentication/src/emqx_authentication_api.erl b/apps/emqx_authentication/src/emqx_authentication_api.erl index a3ed80250..443091664 100644 --- a/apps/emqx_authentication/src/emqx_authentication_api.erl +++ b/apps/emqx_authentication/src/emqx_authentication_api.erl @@ -20,25 +20,22 @@ , delete_chain/2 , lookup_chain/2 , list_chains/2 - , add_services/2 - , delete_services/2 + , add_service/2 + , delete_service/2 , update_service/2 , lookup_service/2 , list_services/2 , move_service/2 - , import_user_credentials/2 - , add_user_creadential/2 + , import_users/2 + , add_user/2 + , delete_user/2 + , update_user/2 + , lookup_user/2 + , list_users/2 ]). -import(minirest, [return/1]). --rest_api(#{name => list_chains, - method => 'GET', - path => "/authentication/chains", - func => list_chains, - descr => "List all chains" - }). - -rest_api(#{name => create_chain, method => 'POST', path => "/authentication/chains", @@ -46,187 +43,352 @@ descr => "Create a chain" }). -create_chain(_Binding, Params = #{chain_id := ChainID}) -> - case emqx_authentication:create_chain(Params) of - {ok, ChainID} -> - return({ok, ChainID}); - {error, Reason} -> - return({error, serialize_error(Reason)}) - end; -create_chain(_Binding, _Params) -> - return({error, serialize_error({missing_parameter, chain_id})}). +-rest_api(#{name => delete_chain, + method => 'DELETE', + path => "/authentication/chains/:bin:chain_id", + func => delete_chain, + descr => "Delete chain" + }). -delete_chain(_Binding, #{chain_id := ChainID}) -> +-rest_api(#{name => lookup_chain, + method => 'GET', + path => "/authentication/chains/:bin:chain_id", + func => lookup_chain, + descr => "Lookup chain" + }). + +-rest_api(#{name => list_chains, + method => 'GET', + path => "/authentication/chains", + func => list_chains, + descr => "List all chains" + }). + +-rest_api(#{name => add_service, + method => 'POST', + path => "/authentication/chains/:bin:chain_id/services", + func => add_service, + descr => "Add service to chain" + }). + +-rest_api(#{name => delete_service, + method => 'DELETE', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name", + func => delete_service, + descr => "Delete service from chain" + }). + +-rest_api(#{name => update_service, + method => 'PUT', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name", + func => update_service, + descr => "Update service in chain" + }). + +-rest_api(#{name => lookup_service, + method => 'GET', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name", + func => lookup_service, + descr => "Lookup service in chain" + }). + +-rest_api(#{name => list_services, + method => 'GET', + path => "/authentication/chains/:bin:chain_id/services", + func => list_services, + descr => "List services in chain" + }). + +-rest_api(#{name => move_service, + method => 'POST', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/position", + func => move_service, + descr => "Change the order of services" + }). + +-rest_api(#{name => import_users, + method => 'POST', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/import-users", + func => import_users, + descr => "Import users" + }). + +-rest_api(#{name => add_user, + method => 'POST', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users", + func => add_user, + descr => "Add user" + }). + +-rest_api(#{name => delete_user, + method => 'DELETE', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users/:bin:user_id", + func => delete_user, + descr => "Delete user" + }). + +-rest_api(#{name => update_user, + method => 'PUT', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users/:bin:user_id", + func => update_user, + descr => "Update user" + }). + +-rest_api(#{name => lookup_user, + method => 'GET', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users/:bin:user_id", + func => lookup_user, + descr => "Lookup user" + }). + +%% TODO: Support pagination +-rest_api(#{name => list_users, + method => 'GET', + path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users", + func => list_users, + descr => "List all users" + }). + +create_chain(Binding, Params) -> + do_create_chain(uri_decode(Binding), maps:from_list(Params)). + +do_create_chain(_Binding, #{<<"chain_id">> := ChainID}) -> + case emqx_authentication:create_chain(ChainID) of + {ok, Chain} -> + return({ok, Chain}); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_create_chain(_Binding, _Params) -> + return(serialize_error({missing_parameter, chain_id})). + +delete_chain(Binding, Params) -> + do_delete_chain(uri_decode(Binding), maps:from_list(Params)). + +do_delete_chain(#{chain_id := ChainID}, _Params) -> case emqx_authentication:delete_chain(ChainID) of ok -> return(ok); {error, Reason} -> - return({error, serialize_error(Reason)}) + return(serialize_error(Reason)) end. -lookup_chain(_Binding, #{chain_id := ChainID}) -> +lookup_chain(Binding, Params) -> + do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_chain(#{chain_id := ChainID}, _Params) -> case emqx_authentication:lookup_chain(ChainID) of {ok, Chain} -> return({ok, Chain}); {error, Reason} -> - return({error, serialize_error(Reason)}) + return(serialize_error(Reason)) + end. + +list_chains(Binding, Params) -> + do_list_chains(uri_decode(Binding), maps:from_list(Params)). + +do_list_chains(_Binding, _Params) -> + {ok, Chains} = emqx_authentication:list_chains(), + return({ok, Chains}). + +add_service(Binding, Params) -> + do_add_service(uri_decode(Binding), maps:from_list(Params)). + +do_add_service(#{chain_id := ChainID}, #{<<"name">> := Name, + <<"type">> := Type, + <<"params">> := Params}) -> + case emqx_authentication:add_services(ChainID, [#{name => Name, + type => binary_to_existing_atom(Type, utf8), + params => maps:from_list(Params)}]) of + {ok, Services} -> + return({ok, Services}); + {error, Reason} -> + return(serialize_error(Reason)) end; -lookup_chain(_Binding, _Params) -> - return({error, serialize_error({missing_parameter, chain_id})}). +%% TODO: Check missed field in params +do_add_service(_Binding, Params) -> + Missed = get_missed_params(Params, [<<"name">>, <<"type">>, <<"params">>]), + return(serialize_error({missing_parameter, Missed})). -list_chains(_Binding, _Params) -> - emqx_authentication:list_chains(). +delete_service(Binding, Params) -> + do_delete_service(uri_decode(Binding), maps:from_list(Params)). -add_services(_Binding, Params = #{chain_id := ChainID}) -> - case maps:get(services, Params, []) of - [] -> return(ok); - Services -> - case emqx_authentication:add_services(ChainID, Services) of - ok -> - return(ok); - {error, Reason} -> - return({error, serialize_error(Reason)}) - end - end; -add_services(_Binding, _Params) -> - return({error, serialize_error({missing_parameter, chain_id})}). - -delete_services(_Binding, #{chain_id := ChainID, - service_names := ServiceNames}) -> - case emqx_authentication:delete_services(ChainID, ServiceNames) of +do_delete_service(#{chain_id := ChainID, + service_name := ServiceName}, _Params) -> + case emqx_authentication:delete_services(ChainID, [ServiceName]) of ok -> return(ok); {error, Reason} -> - return({error, serialize_error(Reason)}) - end; -delete_services(_Binding, #{chain_id := _}) -> - return({error, serialize_error({missing_parameter, service_names})}); -delete_services(_Binding, #{service_names := _}) -> - return({error, serialize_error({missing_parameter, chain_id})}). + return(serialize_error(Reason)) + end. -%% TODO: better input parameters -update_service(_Binding, #{chain_id := ChainID, - service_name := ServiceName, - service_params := Params}) -> +update_service(Binding, Params) -> + do_update_service(uri_decode(Binding), maps:from_list(Params)). + +%% TOOD: PUT 方法支持创建和更新 +do_update_service(#{chain_id := ChainID, + service_name := ServiceName}, Params) -> case emqx_authentication:update_service(ChainID, ServiceName, Params) of - ok -> - return(ok); + {ok, Service} -> + return({ok, Service}); {error, Reason} -> - return({error, serialize_error(Reason)}) - end; -update_service(_Binding, #{chain_id := _}) -> - return({error, serialize_error({missing_parameter, service_name})}); -update_service(_Binding, #{service_name := _}) -> - return({error, serialize_error({missing_parameter, chain_id})}). + return(serialize_error(Reason)) + end. -lookup_service(_Binding, #{chain_id := ChainID, - service_name := ServiceName}) -> +lookup_service(Binding, Params) -> + do_lookup_service(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_service(#{chain_id := ChainID, + service_name := ServiceName}, _Params) -> case emqx_authentication:lookup_service(ChainID, ServiceName) of {ok, Service} -> return({ok, Service}); {error, Reason} -> - return({error, serialize_error(Reason)}) - end; -lookup_service(_Binding, #{chain_id := _}) -> - return({error, serialize_error({missing_parameter, service_name})}); -lookup_service(_Binding, #{service_name := _}) -> - return({error, serialize_error({missing_parameter, chain_id})}). + return(serialize_error(Reason)) + end. -list_services(_Binding, #{chain_id := ChainID}) -> +list_services(Binding, Params) -> + do_list_services(uri_decode(Binding), maps:from_list(Params)). + +do_list_services(#{chain_id := ChainID}, _Params) -> case emqx_authentication:list_services(ChainID) of - {ok, Service} -> - return({ok, Service}); + {ok, Services} -> + return({ok, Services}); {error, Reason} -> - return({error, serialize_error(Reason)}) - end; -list_services(_Binding, _Params) -> - return({error, serialize_error({missing_parameter, chain_id})}). + return(serialize_error(Reason)) + end. -move_service(_Binding, #{chain_id := ChainID, - service_name := ServiceName, - to := <<"the front">>}) -> - case emqx_authenticaiton:move_service_to_the_front(ChainID, ServiceName) of - ok -> - return(ok); - {error, Reason} -> - return({error, serialize_error(Reason)}) - end; -move_service(_Binding, #{chain_id := ChainID, - service_name := ServiceName, - to := <<"the end">>}) -> - case emqx_authenticaiton:move_service_to_the_end(ChainID, ServiceName) of - ok -> - return(ok); - {error, Reason} -> - return({error, serialize_error(Reason)}) - end; -move_service(_Binding, #{chain_id := ChainID, - service_name := ServiceName, - to := N}) when is_number(N) -> - case emqx_authenticaiton:move_service_to_the_nth(ChainID, ServiceName, N) of - ok -> - return(ok); - {error, Reason} -> - return({error, serialize_error(Reason)}) - end; -move_service(_Binding, Params) -> - Missed = get_missed_params(Params, [chain_id, service_name, to]), - return({error, serialize_error({missing_parameter, Missed})}). +move_service(Binding, Params) -> + do_move_service(uri_decode(Binding), maps:from_list(Params)). -import_user_credentials(_Binding, #{chain_id := ChainID, - service_name := ServiceName, - filename := Filename, - file_format := FileFormat}) -> - case emqx_authentication:import_user_credentials(ChainID, ServiceName, Filename, FileFormat) of +do_move_service(#{chain_id := ChainID, + service_name := ServiceName}, #{<<"position">> := <<"the front">>}) -> + case emqx_authentication:move_service_to_the_front(ChainID, ServiceName) of ok -> return(ok); {error, Reason} -> - return({error, serialize_error(Reason)}) + return(serialize_error(Reason)) end; -import_user_credentials(_Binding, Params) -> - Missed = get_missed_params(Params, [chain_id, service_name, filename, file_format]), - return({error, serialize_error({missing_parameter, Missed})}). +do_move_service(#{chain_id := ChainID, + service_name := ServiceName}, #{<<"position">> := <<"the end">>}) -> + case emqx_authentication:move_service_to_the_end(ChainID, ServiceName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_service(#{chain_id := ChainID, + service_name := ServiceName}, #{<<"position">> := N}) when is_number(N) -> + case emqx_authentication:move_service_to_the_nth(ChainID, ServiceName, N) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_service(_Binding, _Params) -> + return(serialize_error({missing_parameter, <<"position">>})). -add_user_creadential(_Binding, #{chain_id := ChainID, - service_name := ServiceName, - credential := Credential}) -> - case emqx_authentication:add_user_creadentials(ChainID, ServiceName, Credential) of +import_users(Binding, Params) -> + do_import_users(uri_decode(Binding), maps:from_list(Params)). + +do_import_users(#{chain_id := ChainID, service_name := ServiceName}, + #{<<"filename">> := Filename}) -> + case emqx_authentication:import_users(ChainID, ServiceName, Filename) of ok -> return(ok); {error, Reason} -> - return({error, serialize_error(Reason)}) + return(serialize_error(Reason)) end; -add_user_creadential(_Binding, Params) -> - Missed = get_missed_params(Params, [chain_id, service_name, credential]), - return({error, serialize_error({missing_parameter, Missed})}). +do_import_users(_Binding, Params) -> + Missed = get_missed_params(Params, [<<"filename">>, <<"file_format">>]), + return(serialize_error({missing_parameter, Missed})). + +add_user(Binding, Params) -> + do_add_user(uri_decode(Binding), maps:from_list(Params)). + +do_add_user(#{chain_id := ChainID, + service_name := ServiceName}, UserInfo) -> + case emqx_authentication:add_user(ChainID, ServiceName, UserInfo) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_user(Binding, Params) -> + do_delete_user(uri_decode(Binding), maps:from_list(Params)). + +do_delete_user(#{chain_id := ChainID, + service_name := ServiceName, + user_id := UserID}, _Params) -> + case emqx_authentication:delete_user(ChainID, ServiceName, UserID) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +update_user(Binding, Params) -> + do_update_user(uri_decode(Binding), maps:from_list(Params)). + +do_update_user(#{chain_id := ChainID, + service_name := ServiceName, + user_id := UserID}, NewUserInfo) -> + case emqx_authentication:update_user(ChainID, ServiceName, UserID, NewUserInfo) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_user(Binding, Params) -> + do_lookup_user(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_user(#{chain_id := ChainID, + service_name := ServiceName, + user_id := UserID}, _Params) -> + case emqx_authentication:lookup_user(ChainID, ServiceName, UserID) of + {ok, User} -> + return({ok, User}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +list_users(Binding, Params) -> + do_list_users(uri_decode(Binding), maps:from_list(Params)). + +do_list_users(#{chain_id := ChainID, + service_name := ServiceName}, _Params) -> + case emqx_authentication:list_users(ChainID, ServiceName) of + {ok, Users} -> + return({ok, Users}); + {error, Reason} -> + return(serialize_error(Reason)) + end. %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ -serialize_error(Reason) when not is_map(Reason) -> - Error = serialize_error_(Reason), - emqx_json:encode(Error). +uri_decode(Params) -> + maps:fold(fun(K, V, Acc) -> + Acc#{K => emqx_http_lib:uri_decode(V)} + end, #{}, Params). -serialize_error_({already_exists, {Type, ID}}) -> - #{code => "ALREADY_EXISTS", - message => io_lib:format("~p ~p already exists", [serialize_type(Type), ID])}; -serialize_error_({not_found, {Type, ID}}) -> - #{code => "NOT_FOUND", - message => io_lib:format("~p ~p not found", [serialize_type(Type), ID])}; -serialize_error_({duplicate, Name}) -> - #{code => "INVALID_PARAMETER", - message => io_lib:format("Service name ~p is duplicated", [Name])}; -serialize_error_({missing_parameter, Names = [_ | Rest]}) -> +serialize_error({already_exists, {Type, ID}}) -> + {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~p ~p already exists", [serialize_type(Type), ID]))}; +serialize_error({not_found, {Type, ID}}) -> + {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~p ~p not found", [serialize_type(Type), ID]))}; +serialize_error({duplicate, Name}) -> + {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Service name ~p is duplicated", [Name]))}; +serialize_error({missing_parameter, Names = [_ | Rest]}) -> Format = ["~p," || _ <- Rest] ++ ["~p"], NFormat = binary_to_list(iolist_to_binary(Format)), - #{code => "MISSING_PARAMETER", - message => io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names)}; -serialize_error_({missing_parameter, Name}) -> - #{code => "MISSING_PARAMETER", - message => io_lib:format("The input parameter ~p that is mandatory for processing this request is not supplied.", [Name])}; -serialize_error_(_) -> - #{code => "UNKNOWN_ERROR"}. + {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))}; +serialize_error({missing_parameter, Name}) -> + {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter ~p that is mandatory for processing this request is not supplied.", [Name]))}; +serialize_error(_) -> + {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. serialize_type(service) -> "Service"; diff --git a/apps/emqx_authentication/src/emqx_authentication_app.erl b/apps/emqx_authentication/src/emqx_authentication_app.erl index 473375455..bfdbefb36 100644 --- a/apps/emqx_authentication/src/emqx_authentication_app.erl +++ b/apps/emqx_authentication/src/emqx_authentication_app.erl @@ -18,6 +18,8 @@ -behaviour(application). +-emqx_plugin(?MODULE). + %% Application callbacks -export([ start/2 , stop/1 diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index 63b37f05d..9cc077452 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -24,17 +24,18 @@ , destroy/1 ]). --export([ import_user_credentials/3 - , add_user_credential/2 - , delete_user_credential/2 - , update_user_credential/2 - , lookup_user_credential/2 +-export([ import_users/2 + , add_user/2 + , delete_user/2 + , update_user/3 + , lookup_user/2 + , list_users/1 ]). -service_type(#{ name => mnesia, params_spec => #{ - user_identity_type => #{ + user_id_type => #{ order => 1, type => string, required => true, @@ -51,13 +52,13 @@ } }). --record(user_credential, - { user_identity :: {user_group(), user_identity()} +-record(user_info, + { user_id :: {user_group(), user_id()} , password_hash :: binary() }). -type(user_group() :: {chain_id(), service_name()}). --type(user_identity() :: binary()). +-type(user_id() :: binary()). -export([mnesia/1]). @@ -66,6 +67,8 @@ -define(TAB, mnesia_basic_auth). +%% TODO: Support salt + %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -75,17 +78,17 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {disc_copies, [node()]}, - {record_name, user_credential}, - {attributes, record_info(fields, user_credential)}, + {record_name, user_info}, + {attributes, record_info(fields, user_info)}, {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB, disc_copies). -create(ChainID, ServiceName, #{<<"user_identity_type">> := Type, +create(ChainID, ServiceName, #{<<"user_id_type">> := Type, <<"password_hash_algorithm">> := Algorithm}) -> State = #{user_group => {ChainID, ServiceName}, - user_identity_type => binary_to_atom(Type, utf8), + user_id_type => binary_to_atom(Type, utf8), password_hash_algorithm => binary_to_atom(Algorithm, utf8)}, {ok, State}. @@ -94,13 +97,13 @@ update(ChainID, ServiceName, Params, _State) -> authenticate(ClientInfo = #{password := Password}, #{user_group := UserGroup, - user_identity_type := Type, + user_id_type := Type, password_hash_algorithm := Algorithm}) -> - UserIdentity = get_user_identity(ClientInfo, Type), - case mnesia:dirty_read(?TAB, {UserGroup, UserIdentity}) of + UserID = get_user_identity(ClientInfo, Type), + case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_credential{password_hash = Hash}] -> + [#user_info{password_hash = Hash}] -> case Hash =:= emqx_passwd:hash(Algorithm, Password) of true -> ok; @@ -112,117 +115,133 @@ authenticate(ClientInfo = #{password := Password}, destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{#user_credential{user_identity = {UserGroup, '_'}, _ = '_'}, [], ['$_']}], - lists:foreach(fun delete_user_credential/1, mnesia:select(?TAB, MatchSpec, write)) + MatchSpec = [{#user_info{user_id = {UserGroup, '_'}, _ = '_'}, [], ['$_']}], + lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write)) end). +import_users(Filename0, State) -> + Filename = to_binary(Filename0), + case filename:extension(Filename) of + <<".json">> -> + import_users_from_json(Filename, State); + <<".csv">> -> + import_users_from_csv(Filename, State); + <<>> -> + {error, unknown_file_format}; + Extension -> + {error, {unsupported_file_format, Extension}} + end. + +add_user(#{<<"user_id">> := UserID, + <<"password">> := Password}, + #{user_group := UserGroup, + password_hash_algorithm := Algorithm}) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + add(UserGroup, UserID, Password, Algorithm), + {ok, #{user_id => UserID}}; + [_] -> + {error, already_exist} + end + end). + +delete_user(UserID, #{user_group := UserGroup}) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + {error, not_found}; + [_] -> + mnesia:delete(?TAB, {UserGroup, UserID}, write) + end + end). + +update_user(UserID, #{<<"password">> := Password}, + #{user_group := UserGroup, + password_hash_algorithm := Algorithm}) -> + trans( + fun() -> + case mnesia:read(?TAB, {UserGroup, UserID}, write) of + [] -> + {error, not_found}; + [_] -> + add(UserGroup, UserID, Password, Algorithm), + {ok, #{user_id => UserID}} + 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}}; + [] -> + {error, not_found} + end. + +list_users(#{user_group := UserGroup}) -> + Users = [#{user_id => UserID} || #user_info{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], + {ok, Users}. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + %% Example: %% { -%% "myuser1":"mypassword1", -%% "myuser2":"mypassword2" +%% "myuser1":"password_hash1", +%% "myuser2":"password_hash2" %% } -import_user_credentials(Filename, json, - #{user_group := UserGroup, - password_hash_algorithm := Algorithm}) -> +import_users_from_json(Filename, #{user_group := UserGroup}) -> case file:read_file(Filename) of {ok, Bin} -> case emqx_json:safe_decode(Bin) of {ok, List} -> - import(UserGroup, List, Algorithm); + import(UserGroup, List); {error, Reason} -> {error, Reason} end; {error, Reason} -> {error, Reason} - end; + end. + %% Example: -%% myuser1,mypassword1 -%% myuser2,mypassword2 -import_user_credentials(Filename, csv, - #{user_group := UserGroup, - password_hash_algorithm := Algorithm}) -> +%% myuser1,password_hash1 +%% myuser2,password_hash2 +import_users_from_csv(Filename, #{user_group := UserGroup}) -> case file:open(Filename, [read, binary]) of {ok, File} -> - Result = import(UserGroup, File, Algorithm), + Result = import(UserGroup, File), file:close(File), Result; {error, Reason} -> {error, Reason} end. -add_user_credential(#{user_identity := UserIdentity, password := Password}, - #{user_group := UserGroup, - password_hash_algorithm := Algorithm}) -> - trans( - fun() -> - case mnesia:read(?TAB, {UserGroup, UserIdentity}, write) of - [] -> - add(UserGroup, UserIdentity, Password, Algorithm); - [_] -> - {error, already_exist} - end - end). +import(UserGroup, ListOrFile) -> + trans(fun do_import/2, [UserGroup, ListOrFile]). -delete_user_credential(UserIdentity, #{user_group := UserGroup}) -> - trans( - fun() -> - case mnesia:read(?TAB, {UserGroup, UserIdentity}, write) of - [] -> - {error, not_found}; - [_] -> - mnesia:delete(?TAB, {UserGroup, UserIdentity}, write) - end - end). - -update_user_credential(#{user_identity := UserIdentity, password := Password}, - #{user_group := UserGroup, - password_hash_algorithm := Algorithm}) -> - trans( - fun() -> - case mnesia:read(?TAB, {UserGroup, UserIdentity}, write) of - [] -> - {error, not_found}; - [_] -> - add(UserGroup, UserIdentity, Password, Algorithm) - end - end). - -lookup_user_credential(UserIdentity, #{user_group := UserGroup}) -> - case mnesia:dirty_read(?TAB, {UserGroup, UserIdentity}) of - [#user_credential{user_identity = {_, UserIdentity}, - password_hash = PassHash}] -> - {ok, #{user_identity => UserIdentity, - password_hash => PassHash}}; - [] -> {error, not_found} - end. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -import(UserGroup, ListOrFile, Algorithm) -> - trans(fun do_import/3, [UserGroup, ListOrFile, Algorithm]). - -do_import(_UserGroup, [], _Algorithm) -> +do_import(_UserGroup, []) -> ok; -do_import(UserGroup, [{UserIdentity, Password} | More], Algorithm) - when is_binary(UserIdentity) andalso is_binary(Password) -> - add(UserGroup, UserIdentity, Password, Algorithm), - do_import(UserGroup, More, Algorithm); -do_import(_UserGroup, [_ | _More], _Algorithm) -> +do_import(UserGroup, [{UserID, PasswordHash} | More]) + when is_binary(UserID) andalso is_binary(PasswordHash) -> + import_user(UserGroup, UserID, PasswordHash), + do_import(UserGroup, More); +do_import(_UserGroup, [_ | _More]) -> {error, bad_format}; -%% Importing 5w credentials needs 1.7 seconds -do_import(UserGroup, File, Algorithm) -> +%% Importing 5w users needs 1.7 seconds +do_import(UserGroup, File) -> case file:read_line(File) of {ok, Line} -> case binary:split(Line, [<<",">>, <<"\n">>], [global]) of - [UserIdentity, Password, <<>>] -> - add(UserGroup, UserIdentity, Password, Algorithm), - do_import(UserGroup, File, Algorithm); - [UserIdentity, Password] -> - add(UserGroup, UserIdentity, Password, Algorithm), - do_import(UserGroup, File, Algorithm); + [UserID, PasswordHash, <<>>] -> + import_user(UserGroup, UserID, PasswordHash), + do_import(UserGroup, File); + [UserID, PasswordHash] -> + import_user(UserGroup, UserID, PasswordHash), + do_import(UserGroup, File); _ -> {error, bad_format} end; @@ -233,13 +252,18 @@ do_import(UserGroup, File, Algorithm) -> end. -compile({inline, [add/4]}). -add(UserGroup, UserIdentity, Password, Algorithm) -> - Credential = #user_credential{user_identity = {UserGroup, UserIdentity}, - password_hash = emqx_passwd:hash(Algorithm, Password)}, +add(UserGroup, UserID, Password, Algorithm) -> + Credential = #user_info{user_id = {UserGroup, UserID}, + password_hash = emqx_passwd:hash(Algorithm, Password)}, mnesia:write(?TAB, Credential, write). -delete_user_credential(UserCredential) -> - mnesia:delete_object(?TAB, UserCredential, write). +import_user(UserGroup, UserID, PasswordHash) -> + Credential = #user_info{user_id = {UserGroup, UserID}, + password_hash = PasswordHash}, + mnesia:write(?TAB, Credential, write). + +delete_user2(UserInfo) -> + mnesia:delete_object(?TAB, UserInfo, write). %% TODO: Support other type get_user_identity(#{username := Username}, username) -> @@ -259,6 +283,10 @@ trans(Fun, Args) -> end. +to_binary(B) when is_binary(B) -> + B; +to_binary(L) when is_list(L) -> + iolist_to_binary(L). diff --git a/apps/emqx_authentication/test/data/user-credentials.csv b/apps/emqx_authentication/test/data/user-credentials.csv index 7ee4fe8f1..98bd92d52 100644 --- a/apps/emqx_authentication/test/data/user-credentials.csv +++ b/apps/emqx_authentication/test/data/user-credentials.csv @@ -1,2 +1,2 @@ -myuser3,mypassword3 -myuser4,mypassword4 \ No newline at end of file +myuser3,8d41233e39c95b5da13361e354e1c9e639f07b27d397463a8f91b71ee07ccfb2 +myuser4,5809df0154f3cb4ac5c3a5572eaca0c5f7f9d858e887fc675b2becab9feb19d1 \ No newline at end of file diff --git a/apps/emqx_authentication/test/data/user-credentials.json b/apps/emqx_authentication/test/data/user-credentials.json index 6c4689433..a153be616 100644 --- a/apps/emqx_authentication/test/data/user-credentials.json +++ b/apps/emqx_authentication/test/data/user-credentials.json @@ -1,4 +1,4 @@ { - "myuser1": "mypassword1", - "myuser2": "mypassword2" + "myuser1": "09343625c6c123d3434932fe1ce08bae5ac00a8f95bd746e10491b0bafdd1817", + "myuser2": "8767a7d316ad68cb607c7c805b859ffa78277dda13b7a3e2e8b53cad3cabbc6e" } \ No newline at end of file diff --git a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl index 38efe0fe3..d110d940a 100644 --- a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl +++ b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl @@ -37,10 +37,8 @@ end_per_suite(_) -> t_chain(_) -> ChainID = <<"mychain">>, - ChainParams = #{chain_id => ChainID, - services => []}, - ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), - ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(ChainParams)), + ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), + ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(#{id => ChainID})), ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), @@ -48,34 +46,33 @@ t_chain(_) -> t_service(_) -> ChainID = <<"mychain">>, + ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), + ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:lookup_chain(ChainID)), + ServiceName1 = <<"myservice1">>, ServiceParams1 = #{name => ServiceName1, type => mnesia, params => #{ - user_identity_type => <<"username">>, + user_id_type => <<"username">>, password_hash_algorithm => <<"sha256">>}}, - ChainParams = #{chain_id => ChainID, - services => [ServiceParams1]}, - ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), - Service1 = ServiceParams1, - ?assertMatch({ok, #{id := ChainID, services := [Service1]}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual({ok, Service1}, ?AUTH:lookup_service(ChainID, ServiceName1)), - ?assertEqual({ok, [Service1]}, ?AUTH:list_services(ChainID)), + ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])), + ?assertEqual({ok, ServiceParams1}, ?AUTH:lookup_service(ChainID, ServiceName1)), + ?assertEqual({ok, [ServiceParams1]}, ?AUTH:list_services(ChainID)), ?assertEqual({error, {already_exists, {service, ServiceName1}}}, ?AUTH:add_services(ChainID, [ServiceParams1])), + ServiceName2 = <<"myservice2">>, ServiceParams2 = ServiceParams1#{name => ServiceName2}, - ?assertEqual(ok, ?AUTH:add_services(ChainID, [ServiceParams2])), - Service2 = ServiceParams2, - ?assertMatch({ok, #{id := ChainID, services := [Service1, Service2]}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual({ok, Service2}, ?AUTH:lookup_service(ChainID, ServiceName2)), - ?assertEqual({ok, [Service1, Service2]}, ?AUTH:list_services(ChainID)), + ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])), + ?assertMatch({ok, #{id := ChainID, services := [ServiceParams1, ServiceParams2]}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual({ok, ServiceParams2}, ?AUTH:lookup_service(ChainID, ServiceName2)), + ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)), ?assertEqual(ok, ?AUTH:move_service_to_the_front(ChainID, ServiceName2)), - ?assertEqual({ok, [Service2, Service1]}, ?AUTH:list_services(ChainID)), + ?assertEqual({ok, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)), ?assertEqual(ok, ?AUTH:move_service_to_the_end(ChainID, ServiceName2)), - ?assertEqual({ok, [Service1, Service2]}, ?AUTH:list_services(ChainID)), + ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)), ?assertEqual(ok, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 1)), - ?assertEqual({ok, [Service2, Service1]}, ?AUTH:list_services(ChainID)), + ?assertEqual({ok, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)), ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 3)), ?assertEqual({error, out_of_range}, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 0)), ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName1, ServiceName2])), @@ -85,40 +82,40 @@ t_service(_) -> t_mnesia_service(_) -> ChainID = <<"mychain">>, + ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), + ServiceName = <<"myservice">>, ServiceParams = #{name => ServiceName, type => mnesia, params => #{ - user_identity_type => <<"username">>, + user_id_type => <<"username">>, password_hash_algorithm => <<"sha256">>}}, - ChainParams = #{chain_id => ChainID, - services => [ServiceParams]}, - ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), - UserCredential = #{user_identity => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, ServiceName, UserCredential)), - ?assertMatch({ok, #{user_identity := <<"myuser">>, password_hash := _}}, - ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), + ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), + + UserInfo = #{<<"user_id">> => <<"myuser">>, + <<"password">> => <<"mypass">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, ServiceName, UserInfo)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), ClientInfo = #{chain_id => ChainID, username => <<"myuser">>, password => <<"mypass">>}, ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({error, user_credential_not_found}, ?AUTH:authenticate(ClientInfo2)), + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), ClientInfo3 = ClientInfo#{password => <<"badpass">>}, ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), - UserCredential2 = UserCredential#{password => <<"mypass2">>}, - ?assertEqual(ok, ?AUTH:update_user_credential(ChainID, ServiceName, UserCredential2)), + UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, ServiceName, <<"myuser">>, UserInfo2)), ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), - ?assertEqual(ok, ?AUTH:delete_user_credential(ChainID, ServiceName, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_user(ChainID, ServiceName, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, ServiceName, UserCredential)), - ?assertMatch({ok, #{user_identity := <<"myuser">>}}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, ServiceName, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), ?assertEqual(ok, ?AUTH:delete_services(ChainID, [ServiceName])), - ?assertEqual(ok, ?AUTH:add_services(ChainID, [ServiceParams])), - ?assertMatch({error, not_found}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser">>)), + ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), + ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser">>)), ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), ?assertEqual([], ets:tab2list(mnesia_basic_auth)), @@ -126,20 +123,21 @@ t_mnesia_service(_) -> t_import(_) -> ChainID = <<"mychain">>, + ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), + ServiceName = <<"myservice">>, ServiceParams = #{name => ServiceName, type => mnesia, params => #{ - user_identity_type => <<"username">>, + user_id_type => <<"username">>, password_hash_algorithm => <<"sha256">>}}, - ChainParams = #{chain_id => ChainID, - services => [ServiceParams]}, - ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), + ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), + Dir = code:lib_dir(emqx_authentication, test), - ?assertEqual(ok, ?AUTH:import_user_credentials(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.json"]), json)), - ?assertEqual(ok, ?AUTH:import_user_credentials(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.csv"]), csv)), - ?assertMatch({ok, #{user_identity := <<"myuser1">>}}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser1">>)), - ?assertMatch({ok, #{user_identity := <<"myuser3">>}}, ?AUTH:lookup_user_credential(ChainID, ServiceName, <<"myuser3">>)), + ?assertEqual(ok, ?AUTH:import_users(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.json"]))), + ?assertEqual(ok, ?AUTH:import_users(ChainID, ServiceName, filename:join([Dir, "data/user-credentials.csv"]))), + ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser1">>)), + ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, ServiceName, <<"myuser3">>)), ClientInfo1 = #{chain_id => ChainID, username => <<"myuser1">>, password => <<"mypassword1">>}, @@ -152,30 +150,31 @@ t_import(_) -> t_multi_mnesia_service(_) -> ChainID = <<"mychain">>, + ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), + ServiceName1 = <<"myservice1">>, ServiceParams1 = #{name => ServiceName1, type => mnesia, params => #{ - user_identity_type => <<"username">>, + user_id_type => <<"username">>, password_hash_algorithm => <<"sha256">>}}, ServiceName2 = <<"myservice2">>, ServiceParams2 = #{name => ServiceName2, type => mnesia, params => #{ - user_identity_type => <<"clientid">>, + user_id_type => <<"clientid">>, password_hash_algorithm => <<"sha256">>}}, - ChainParams = #{chain_id => ChainID, - services => [ServiceParams1, ServiceParams2]}, - ?assertEqual({ok, ChainID}, ?AUTH:create_chain(ChainParams)), + ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])), + ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])), - ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, - ServiceName1, - #{user_identity => <<"myuser">>, - password => <<"mypass1">>})), - ?assertEqual(ok, ?AUTH:add_user_credential(ChainID, - ServiceName2, - #{user_identity => <<"myclient">>, - password => <<"mypass2">>})), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, + ?AUTH:add_user(ChainID, ServiceName1, + #{<<"user_id">> => <<"myuser">>, + <<"password">> => <<"mypass1">>})), + ?assertEqual({ok, #{user_id => <<"myclient">>}}, + ?AUTH:add_user(ChainID, ServiceName2, + #{<<"user_id">> => <<"myclient">>, + <<"password">> => <<"mypass2">>})), ClientInfo1 = #{chain_id => ChainID, username => <<"myuser">>, clientid => <<"myclient">>, From 72c469658478d19522d9848be39409cb1db1febf Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 1 Jun 2021 16:11:28 +0800 Subject: [PATCH 07/15] chore(newline): insert final newline --- .../src/emqx_authentication.erl | 7 +------ .../src/emqx_authentication_api.erl | 14 +++++++------- .../src/emqx_authentication_app.erl | 1 - .../src/emqx_authentication_mnesia.erl | 4 ---- .../src/emqx_authentication_sup.erl | 2 +- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl index 1ee5f46bc..c41ca9249 100644 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ b/apps/emqx_authentication/src/emqx_authentication.erl @@ -89,14 +89,9 @@ enable() -> ok -> ok; {error, already_exists} -> ok end. - % case emqx:hook('client.enhanced_authenticate', fun emqx_authentication:enhanced_authenticate/2) of - % ok -> ok; - % {error, already_exists} -> ok - % end. disable() -> emqx:unhook('client.authenticate', fun emqx_authentication:authenticate/1), - % emqx:unhook('client.enhanced_authenticate', {}), ok. authenticate(#{chain_id := ChainID} = ClientInfo) -> @@ -522,4 +517,4 @@ trans(Fun, Args) -> case mnesia:transaction(Fun, Args) of {atomic, Res} -> Res; {aborted, Reason} -> {error, Reason} - end. \ No newline at end of file + end. diff --git a/apps/emqx_authentication/src/emqx_authentication_api.erl b/apps/emqx_authentication/src/emqx_authentication_api.erl index 443091664..f43e5c0df 100644 --- a/apps/emqx_authentication/src/emqx_authentication_api.erl +++ b/apps/emqx_authentication/src/emqx_authentication_api.erl @@ -398,10 +398,10 @@ serialize_type(service_type) -> "Service type". get_missed_params(Actual, Expected) -> - Keys = lists:fold(fun(Key, Acc) -> - case maps:is_key(Key, Actual) of - true -> Acc; - false -> [Key | Acc] - end - end, [], Expected), - lists:reverse(Keys). \ No newline at end of file + Keys = lists:foldl(fun(Key, Acc) -> + case maps:is_key(Key, Actual) of + true -> Acc; + false -> [Key | Acc] + end + end, [], Expected), + lists:reverse(Keys). diff --git a/apps/emqx_authentication/src/emqx_authentication_app.erl b/apps/emqx_authentication/src/emqx_authentication_app.erl index bfdbefb36..2d395def7 100644 --- a/apps/emqx_authentication/src/emqx_authentication_app.erl +++ b/apps/emqx_authentication/src/emqx_authentication_app.erl @@ -32,4 +32,3 @@ start(_StartType, _StartArgs) -> stop(_State) -> ok. - diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index 9cc077452..08e0cf98e 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -287,7 +287,3 @@ to_binary(B) when is_binary(B) -> B; to_binary(L) when is_list(L) -> iolist_to_binary(L). - - - - diff --git a/apps/emqx_authentication/src/emqx_authentication_sup.erl b/apps/emqx_authentication/src/emqx_authentication_sup.erl index a36617ac8..06e12ce6c 100644 --- a/apps/emqx_authentication/src/emqx_authentication_sup.erl +++ b/apps/emqx_authentication/src/emqx_authentication_sup.erl @@ -26,4 +26,4 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_one, 10, 10}, []}}. \ No newline at end of file + {ok, {{one_for_one, 10, 10}, []}}. From 661f2de01eec006f5c5843aa69c06cbaa278ee7b Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 2 Jun 2021 14:04:38 +0800 Subject: [PATCH 08/15] feat(authentication): require header in csv file and use new json format --- .../data/user-credentials.csv | 3 +- .../data/user-credentials.json | 14 +++- .../src/emqx_authentication_mnesia.erl | 82 ++++++++++++------- .../test/data/user-credentials.csv | 3 +- .../test/data/user-credentials.json | 14 +++- 5 files changed, 76 insertions(+), 40 deletions(-) diff --git a/apps/emqx_authentication/data/user-credentials.csv b/apps/emqx_authentication/data/user-credentials.csv index 98bd92d52..0a0affaa2 100644 --- a/apps/emqx_authentication/data/user-credentials.csv +++ b/apps/emqx_authentication/data/user-credentials.csv @@ -1,2 +1,3 @@ +user_id,password_hash myuser3,8d41233e39c95b5da13361e354e1c9e639f07b27d397463a8f91b71ee07ccfb2 -myuser4,5809df0154f3cb4ac5c3a5572eaca0c5f7f9d858e887fc675b2becab9feb19d1 \ No newline at end of file +myuser4,5809df0154f3cb4ac5c3a5572eaca0c5f7f9d858e887fc675b2becab9feb19d1 diff --git a/apps/emqx_authentication/data/user-credentials.json b/apps/emqx_authentication/data/user-credentials.json index a153be616..a910a8dc2 100644 --- a/apps/emqx_authentication/data/user-credentials.json +++ b/apps/emqx_authentication/data/user-credentials.json @@ -1,4 +1,10 @@ -{ - "myuser1": "09343625c6c123d3434932fe1ce08bae5ac00a8f95bd746e10491b0bafdd1817", - "myuser2": "8767a7d316ad68cb607c7c805b859ffa78277dda13b7a3e2e8b53cad3cabbc6e" -} \ No newline at end of file +[ + { + "user_id":"myuser1", + "password_hash":"09343625c6c123d3434932fe1ce08bae5ac00a8f95bd746e10491b0bafdd1817" + }, + { + "user_id":"myuser2", + "password_hash":"8767a7d316ad68cb607c7c805b859ffa78277dda13b7a3e2e8b53cad3cabbc6e" + } +] diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index 08e0cf98e..0fa28d178 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -52,6 +52,8 @@ } }). + + -record(user_info, { user_id :: {user_group(), user_id()} , password_hash :: binary() @@ -188,17 +190,13 @@ list_users(#{user_group := UserGroup}) -> %% Internal functions %%------------------------------------------------------------------------------ -%% Example: -%% { -%% "myuser1":"password_hash1", -%% "myuser2":"password_hash2" -%% } +%% Example: data/user-credentials.json import_users_from_json(Filename, #{user_group := UserGroup}) -> case file:read_file(Filename) of {ok, Bin} -> - case emqx_json:safe_decode(Bin) of + case emqx_json:safe_decode(Bin, [return_maps]) of {ok, List} -> - import(UserGroup, List); + trans(fun import/2, [UserGroup, List]); {error, Reason} -> {error, Reason} end; @@ -206,44 +204,43 @@ import_users_from_json(Filename, #{user_group := UserGroup}) -> {error, Reason} end. -%% Example: -%% myuser1,password_hash1 -%% myuser2,password_hash2 +%% Example: data/user-credentials.csv import_users_from_csv(Filename, #{user_group := UserGroup}) -> case file:open(Filename, [read, binary]) of {ok, File} -> - Result = import(UserGroup, File), - file:close(File), - Result; + case get_csv_header(File) of + {ok, Seq} -> + Result = trans(fun import/3, [UserGroup, File, Seq]), + file:close(File), + Result; + {error, Reason} -> + {error, Reason} + end; {error, Reason} -> {error, Reason} end. -import(UserGroup, ListOrFile) -> - trans(fun do_import/2, [UserGroup, ListOrFile]). - -do_import(_UserGroup, []) -> +import(_UserGroup, []) -> ok; -do_import(UserGroup, [{UserID, PasswordHash} | More]) +import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> import_user(UserGroup, UserID, PasswordHash), - do_import(UserGroup, More); -do_import(_UserGroup, [_ | _More]) -> - {error, bad_format}; + import(UserGroup, More); +import(_UserGroup, [_ | _More]) -> + {error, bad_format}. %% Importing 5w users needs 1.7 seconds -do_import(UserGroup, File) -> +import(UserGroup, File, Seq) -> case file:read_line(File) of {ok, Line} -> - case binary:split(Line, [<<",">>, <<"\n">>], [global]) of - [UserID, PasswordHash, <<>>] -> - import_user(UserGroup, UserID, PasswordHash), - do_import(UserGroup, File); - [UserID, PasswordHash] -> + Fields = binary:split(Line, [<<",">>, <<" ">>, <<"\n">>], [global, trim_all]), + case get_user_info_by_seq(Fields, Seq) of + {ok, #{user_id := UserID, + password_hash := PasswordHash}} -> import_user(UserGroup, UserID, PasswordHash), - do_import(UserGroup, File); - _ -> - {error, bad_format} + import(UserGroup, File, Seq); + {error, Reason} -> + {error, Reason} end; eof -> ok; @@ -251,6 +248,31 @@ do_import(UserGroup, File) -> {error, Reason} end. +get_csv_header(File) -> + case file:read_line(File) of + {ok, Line} -> + Seq = binary:split(Line, [<<",">>, <<" ">>, <<"\n">>], [global, trim_all]), + {ok, Seq}; + eof -> + {error, empty_file}; + {error, Reason} -> + {error, Reason} + end. + +get_user_info_by_seq(Fields, Seq) -> + get_user_info_by_seq(Fields, Seq, #{}). + +get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) -> + {ok, Acc}; +get_user_info_by_seq(_, [], _) -> + {error, bad_format}; +get_user_info_by_seq([UserID | More1], [<<"user_id">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{user_id => UserID}); +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(_, _, _) -> + {error, bad_format}. + -compile({inline, [add/4]}). add(UserGroup, UserID, Password, Algorithm) -> Credential = #user_info{user_id = {UserGroup, UserID}, diff --git a/apps/emqx_authentication/test/data/user-credentials.csv b/apps/emqx_authentication/test/data/user-credentials.csv index 98bd92d52..0a0affaa2 100644 --- a/apps/emqx_authentication/test/data/user-credentials.csv +++ b/apps/emqx_authentication/test/data/user-credentials.csv @@ -1,2 +1,3 @@ +user_id,password_hash myuser3,8d41233e39c95b5da13361e354e1c9e639f07b27d397463a8f91b71ee07ccfb2 -myuser4,5809df0154f3cb4ac5c3a5572eaca0c5f7f9d858e887fc675b2becab9feb19d1 \ No newline at end of file +myuser4,5809df0154f3cb4ac5c3a5572eaca0c5f7f9d858e887fc675b2becab9feb19d1 diff --git a/apps/emqx_authentication/test/data/user-credentials.json b/apps/emqx_authentication/test/data/user-credentials.json index a153be616..a910a8dc2 100644 --- a/apps/emqx_authentication/test/data/user-credentials.json +++ b/apps/emqx_authentication/test/data/user-credentials.json @@ -1,4 +1,10 @@ -{ - "myuser1": "09343625c6c123d3434932fe1ce08bae5ac00a8f95bd746e10491b0bafdd1817", - "myuser2": "8767a7d316ad68cb607c7c805b859ffa78277dda13b7a3e2e8b53cad3cabbc6e" -} \ No newline at end of file +[ + { + "user_id":"myuser1", + "password_hash":"09343625c6c123d3434932fe1ce08bae5ac00a8f95bd746e10491b0bafdd1817" + }, + { + "user_id":"myuser2", + "password_hash":"8767a7d316ad68cb607c7c805b859ffa78277dda13b7a3e2e8b53cad3cabbc6e" + } +] From f297c36929815a0a178fd6960c3672a3156c0e7b Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 8 Jun 2021 11:40:46 +0800 Subject: [PATCH 09/15] feat(jwt auth): support JWT auth --- .../src/emqx_authentication.erl | 1 - .../src/emqx_authentication_api.erl | 82 ++-- .../emqx_authentication_jwks_connector.erl | 171 ++++++++ .../src/emqx_authentication_jwt.erl | 391 ++++++++++++++++++ .../src/emqx_authentication_mnesia.erl | 2 - 5 files changed, 603 insertions(+), 44 deletions(-) create mode 100644 apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl create mode 100644 apps/emqx_authentication/src/emqx_authentication_jwt.erl diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl index c41ca9249..90a234386 100644 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ b/apps/emqx_authentication/src/emqx_authentication.erl @@ -180,7 +180,6 @@ add_services(ChainID, ServiceParams) -> ok -> case create_services(ChainID, NServiceParams) of {ok, NServices} -> - io:format("~p~n", [NServices]), NChain = Chain#chain{services = Services ++ NServices}, ok = mnesia:write(?CHAIN_TAB, NChain, write), {ok, serialize_services(NServices)}; diff --git a/apps/emqx_authentication/src/emqx_authentication_api.erl b/apps/emqx_authentication/src/emqx_authentication_api.erl index f43e5c0df..74887a0b2 100644 --- a/apps/emqx_authentication/src/emqx_authentication_api.erl +++ b/apps/emqx_authentication/src/emqx_authentication_api.erl @@ -45,14 +45,14 @@ -rest_api(#{name => delete_chain, method => 'DELETE', - path => "/authentication/chains/:bin:chain_id", + path => "/authentication/chains/:bin:id", func => delete_chain, descr => "Delete chain" }). -rest_api(#{name => lookup_chain, method => 'GET', - path => "/authentication/chains/:bin:chain_id", + path => "/authentication/chains/:bin:id", func => lookup_chain, descr => "Lookup chain" }). @@ -66,77 +66,77 @@ -rest_api(#{name => add_service, method => 'POST', - path => "/authentication/chains/:bin:chain_id/services", + path => "/authentication/chains/:bin:id/services", func => add_service, descr => "Add service to chain" }). -rest_api(#{name => delete_service, method => 'DELETE', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name", + path => "/authentication/chains/:bin:id/services/:bin:service_name", func => delete_service, descr => "Delete service from chain" }). -rest_api(#{name => update_service, method => 'PUT', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name", + path => "/authentication/chains/:bin:id/services/:bin:service_name", func => update_service, descr => "Update service in chain" }). -rest_api(#{name => lookup_service, method => 'GET', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name", + path => "/authentication/chains/:bin:id/services/:bin:service_name", func => lookup_service, descr => "Lookup service in chain" }). -rest_api(#{name => list_services, method => 'GET', - path => "/authentication/chains/:bin:chain_id/services", + path => "/authentication/chains/:bin:id/services", func => list_services, descr => "List services in chain" }). -rest_api(#{name => move_service, method => 'POST', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/position", + path => "/authentication/chains/:bin:id/services/:bin:service_name/position", func => move_service, descr => "Change the order of services" }). -rest_api(#{name => import_users, method => 'POST', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/import-users", + path => "/authentication/chains/:bin:id/services/:bin:service_name/import-users", func => import_users, descr => "Import users" }). -rest_api(#{name => add_user, method => 'POST', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users", + path => "/authentication/chains/:bin:id/services/:bin:service_name/users", func => add_user, descr => "Add user" }). -rest_api(#{name => delete_user, method => 'DELETE', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users/:bin:user_id", + path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", func => delete_user, descr => "Delete user" }). -rest_api(#{name => update_user, method => 'PUT', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users/:bin:user_id", + path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", func => update_user, descr => "Update user" }). -rest_api(#{name => lookup_user, method => 'GET', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users/:bin:user_id", + path => "/authentication/chains/:bin:id/services/:bin:service_name/users/:bin:user_id", func => lookup_user, descr => "Lookup user" }). @@ -144,7 +144,7 @@ %% TODO: Support pagination -rest_api(#{name => list_users, method => 'GET', - path => "/authentication/chains/:bin:chain_id/services/:bin:service_name/users", + path => "/authentication/chains/:bin:id/services/:bin:service_name/users", func => list_users, descr => "List all users" }). @@ -152,20 +152,20 @@ create_chain(Binding, Params) -> do_create_chain(uri_decode(Binding), maps:from_list(Params)). -do_create_chain(_Binding, #{<<"chain_id">> := ChainID}) -> - case emqx_authentication:create_chain(ChainID) of +do_create_chain(_Binding, #{<<"id">> := ChainID}) -> + case emqx_authentication:create_chain(#{id => ChainID}) of {ok, Chain} -> return({ok, Chain}); {error, Reason} -> return(serialize_error(Reason)) end; do_create_chain(_Binding, _Params) -> - return(serialize_error({missing_parameter, chain_id})). + return(serialize_error({missing_parameter, id})). delete_chain(Binding, Params) -> do_delete_chain(uri_decode(Binding), maps:from_list(Params)). -do_delete_chain(#{chain_id := ChainID}, _Params) -> +do_delete_chain(#{id := ChainID}, _Params) -> case emqx_authentication:delete_chain(ChainID) of ok -> return(ok); @@ -176,7 +176,7 @@ do_delete_chain(#{chain_id := ChainID}, _Params) -> lookup_chain(Binding, Params) -> do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). -do_lookup_chain(#{chain_id := ChainID}, _Params) -> +do_lookup_chain(#{id := ChainID}, _Params) -> case emqx_authentication:lookup_chain(ChainID) of {ok, Chain} -> return({ok, Chain}); @@ -194,9 +194,9 @@ do_list_chains(_Binding, _Params) -> add_service(Binding, Params) -> do_add_service(uri_decode(Binding), maps:from_list(Params)). -do_add_service(#{chain_id := ChainID}, #{<<"name">> := Name, - <<"type">> := Type, - <<"params">> := Params}) -> +do_add_service(#{id := ChainID}, #{<<"name">> := Name, + <<"type">> := Type, + <<"params">> := Params}) -> case emqx_authentication:add_services(ChainID, [#{name => Name, type => binary_to_existing_atom(Type, utf8), params => maps:from_list(Params)}]) of @@ -213,7 +213,7 @@ do_add_service(_Binding, Params) -> delete_service(Binding, Params) -> do_delete_service(uri_decode(Binding), maps:from_list(Params)). -do_delete_service(#{chain_id := ChainID, +do_delete_service(#{id := ChainID, service_name := ServiceName}, _Params) -> case emqx_authentication:delete_services(ChainID, [ServiceName]) of ok -> @@ -225,8 +225,8 @@ do_delete_service(#{chain_id := ChainID, update_service(Binding, Params) -> do_update_service(uri_decode(Binding), maps:from_list(Params)). -%% TOOD: PUT 方法支持创建和更新 -do_update_service(#{chain_id := ChainID, +%% TOOD: PUT method supports creation and update +do_update_service(#{id := ChainID, service_name := ServiceName}, Params) -> case emqx_authentication:update_service(ChainID, ServiceName, Params) of {ok, Service} -> @@ -238,7 +238,7 @@ do_update_service(#{chain_id := ChainID, lookup_service(Binding, Params) -> do_lookup_service(uri_decode(Binding), maps:from_list(Params)). -do_lookup_service(#{chain_id := ChainID, +do_lookup_service(#{id := ChainID, service_name := ServiceName}, _Params) -> case emqx_authentication:lookup_service(ChainID, ServiceName) of {ok, Service} -> @@ -250,7 +250,7 @@ do_lookup_service(#{chain_id := ChainID, list_services(Binding, Params) -> do_list_services(uri_decode(Binding), maps:from_list(Params)). -do_list_services(#{chain_id := ChainID}, _Params) -> +do_list_services(#{id := ChainID}, _Params) -> case emqx_authentication:list_services(ChainID) of {ok, Services} -> return({ok, Services}); @@ -261,7 +261,7 @@ do_list_services(#{chain_id := ChainID}, _Params) -> move_service(Binding, Params) -> do_move_service(uri_decode(Binding), maps:from_list(Params)). -do_move_service(#{chain_id := ChainID, +do_move_service(#{id := ChainID, service_name := ServiceName}, #{<<"position">> := <<"the front">>}) -> case emqx_authentication:move_service_to_the_front(ChainID, ServiceName) of ok -> @@ -269,7 +269,7 @@ do_move_service(#{chain_id := ChainID, {error, Reason} -> return(serialize_error(Reason)) end; -do_move_service(#{chain_id := ChainID, +do_move_service(#{id := ChainID, service_name := ServiceName}, #{<<"position">> := <<"the end">>}) -> case emqx_authentication:move_service_to_the_end(ChainID, ServiceName) of ok -> @@ -277,7 +277,7 @@ do_move_service(#{chain_id := ChainID, {error, Reason} -> return(serialize_error(Reason)) end; -do_move_service(#{chain_id := ChainID, +do_move_service(#{id := ChainID, service_name := ServiceName}, #{<<"position">> := N}) when is_number(N) -> case emqx_authentication:move_service_to_the_nth(ChainID, ServiceName, N) of ok -> @@ -291,7 +291,7 @@ do_move_service(_Binding, _Params) -> import_users(Binding, Params) -> do_import_users(uri_decode(Binding), maps:from_list(Params)). -do_import_users(#{chain_id := ChainID, service_name := ServiceName}, +do_import_users(#{id := ChainID, service_name := ServiceName}, #{<<"filename">> := Filename}) -> case emqx_authentication:import_users(ChainID, ServiceName, Filename) of ok -> @@ -306,7 +306,7 @@ do_import_users(_Binding, Params) -> add_user(Binding, Params) -> do_add_user(uri_decode(Binding), maps:from_list(Params)). -do_add_user(#{chain_id := ChainID, +do_add_user(#{id := ChainID, service_name := ServiceName}, UserInfo) -> case emqx_authentication:add_user(ChainID, ServiceName, UserInfo) of {ok, User} -> @@ -318,7 +318,7 @@ do_add_user(#{chain_id := ChainID, delete_user(Binding, Params) -> do_delete_user(uri_decode(Binding), maps:from_list(Params)). -do_delete_user(#{chain_id := ChainID, +do_delete_user(#{id := ChainID, service_name := ServiceName, user_id := UserID}, _Params) -> case emqx_authentication:delete_user(ChainID, ServiceName, UserID) of @@ -331,7 +331,7 @@ do_delete_user(#{chain_id := ChainID, update_user(Binding, Params) -> do_update_user(uri_decode(Binding), maps:from_list(Params)). -do_update_user(#{chain_id := ChainID, +do_update_user(#{id := ChainID, service_name := ServiceName, user_id := UserID}, NewUserInfo) -> case emqx_authentication:update_user(ChainID, ServiceName, UserID, NewUserInfo) of @@ -344,7 +344,7 @@ do_update_user(#{chain_id := ChainID, lookup_user(Binding, Params) -> do_lookup_user(uri_decode(Binding), maps:from_list(Params)). -do_lookup_user(#{chain_id := ChainID, +do_lookup_user(#{id := ChainID, service_name := ServiceName, user_id := UserID}, _Params) -> case emqx_authentication:lookup_user(ChainID, ServiceName, UserID) of @@ -357,7 +357,7 @@ do_lookup_user(#{chain_id := ChainID, list_users(Binding, Params) -> do_list_users(uri_decode(Binding), maps:from_list(Params)). -do_list_users(#{chain_id := ChainID, +do_list_users(#{id := ChainID, service_name := ServiceName}, _Params) -> case emqx_authentication:list_users(ChainID, ServiceName) of {ok, Users} -> @@ -376,17 +376,17 @@ uri_decode(Params) -> end, #{}, Params). serialize_error({already_exists, {Type, ID}}) -> - {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~p ~p already exists", [serialize_type(Type), ID]))}; + {error, <<"ALREADY_EXISTS">>, list_to_binary(io_lib:format("~s '~s' already exists", [serialize_type(Type), ID]))}; serialize_error({not_found, {Type, ID}}) -> - {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~p ~p not found", [serialize_type(Type), ID]))}; + {error, <<"NOT_FOUND">>, list_to_binary(io_lib:format("~s '~s' not found", [serialize_type(Type), ID]))}; serialize_error({duplicate, Name}) -> - {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Service name ~p is duplicated", [Name]))}; + {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Service name '~s' is duplicated", [Name]))}; serialize_error({missing_parameter, Names = [_ | Rest]}) -> - Format = ["~p," || _ <- Rest] ++ ["~p"], + Format = ["~s," || _ <- Rest] ++ ["~s"], NFormat = binary_to_list(iolist_to_binary(Format)), {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameters " ++ NFormat ++ " that are mandatory for processing this request are not supplied.", Names))}; serialize_error({missing_parameter, Name}) -> - {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter ~p that is mandatory for processing this request is not supplied.", [Name]))}; + {error, <<"MISSING_PARAMETER">>, list_to_binary(io_lib:format("The input parameter '~s' that is mandatory for processing this request is not supplied.", [Name]))}; serialize_error(_) -> {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. diff --git a/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl b/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl new file mode 100644 index 000000000..9dafc9f5e --- /dev/null +++ b/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl @@ -0,0 +1,171 @@ +%%-------------------------------------------------------------------- +%% 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_authentication_jwks_connector). + +-behaviour(gen_server). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("jose/include/jose_jwk.hrl"). + +-export([ start_link/1 + , stop/1 + ]). + +-export([ get_jwks/1 + , update/2 + ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +start_link(Opts) -> + gen_server:start_link(?MODULE, [Opts], []). + +stop(Pid) -> + gen_server:stop(Pid). + +get_jwks(Pid) -> + gen_server:call(Pid, get_cached_jwks, 5000). + +update(Pid, Opts) -> + gen_server:call(Pid, {update, Opts}, 5000). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Opts]) -> + ok = jose:json_module(jiffy), + State = handle_options(Opts), + {ok, refresh_jwks(State)}. + +handle_call(get_cached_jwks, _From, #{jwks := Jwks} = State) -> + {reply, {ok, Jwks}, State}; + +handle_call({update, Opts}, _From, State) -> + State = handle_options(Opts), + {reply, ok, refresh_jwks(State)}; + +handle_call(_Req, _From, State) -> + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({refresh_jwks, _TRef, refresh}, #{request_id := RequestID} = State) -> + case RequestID of + undefined -> ok; + _ -> + ok = httpc:cancel_request(RequestID), + receive + {http, _} -> ok + after 0 -> + ok + end + end, + {noreply, refresh_jwks(State)}; + +handle_info({http, {RequestID, Result}}, + #{request_id := RequestID, endpoint := Endpoint} = State0) -> + State1 = State0#{request_id := undefined}, + case Result of + {error, Reason} -> + ?LOG(error, "Failed to request jwks endpoint(~s): ~p", [Endpoint, Reason]), + State1; + {_StatusLine, _Headers, Body} -> + try + JWKS = jose_jwk:from(emqx_json:decode(Body, [return_maps])), + {_, JWKs} = JWKS#jose_jwk.keys, + State1#{jwks := JWKs} + catch _:_ -> + ?LOG(error, "Invalid jwks returned from jwks endpoint(~s): ~p~n", [Endpoint, Body]), + State1 + end + end; + +handle_info({http, {_, _}}, State) -> + %% ignore + {noreply, State}; + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, State) -> + _ = cancel_timer(State), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +handle_options(Opts) -> + #{endpoint => proplists:get_value(jwks_endpoint, Opts), + refresh_interval => limit_refresh_interval(proplists:get_value(refresh_interval, Opts)), + ssl_opts => get_ssl_opts(Opts), + jwks => [], + request_id => undefined}. + +get_ssl_opts(Opts) -> + case proplists:get_value(enable_ssl, Opts) of + false -> []; + true -> + maps:to_list(maps:with([cacertfile, + keyfile, + certfile, + verify, + server_name_indication], maps:from_list(Opts))) + end. + +refresh_jwks(#{endpoint := Endpoint, + ssl_opts := SSLOpts} = State) -> + HTTPOpts = [{timeout, 5000}, {connect_timeout, 5000}, {ssl, SSLOpts}], + NState = case httpc:request(get, {Endpoint, [{"Accept", "application/json"}]}, HTTPOpts, + [{body_format, binary}, {sync, false}, {receiver, self()}]) of + {error, Reason} -> + ?LOG(error, "Failed to request jwks endpoint(~s): ~p", [Endpoint, Reason]), + State; + {ok, RequestID} -> + State#{request_id := RequestID} + end, + ensure_expiry_timer(NState). + +ensure_expiry_timer(State = #{refresh_interval := Interval}) -> + State#{refresh_timer := emqx_misc:start_timer(timer:seconds(Interval), refresh_jwks)}. + +cancel_timer(State = #{refresh_timer := undefined}) -> + State; +cancel_timer(State = #{refresh_timer := TRef}) -> + _ = emqx_misc:cancel_timer(TRef), + State#{refresh_timer := undefined}. + +limit_refresh_interval(Interval) when Interval < 10 -> + 10; +limit_refresh_interval(Interval) -> + Interval. diff --git a/apps/emqx_authentication/src/emqx_authentication_jwt.erl b/apps/emqx_authentication/src/emqx_authentication_jwt.erl new file mode 100644 index 000000000..f80c90a36 --- /dev/null +++ b/apps/emqx_authentication/src/emqx_authentication_jwt.erl @@ -0,0 +1,391 @@ +%%-------------------------------------------------------------------- +%% 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_authentication_jwt). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +-service_type(#{ + name => jwt, + params_spec => #{ + use_jwks => #{ + order => 1, + type => boolean + }, + jwks_endpoint => #{ + order => 2, + type => string + }, + refresh_interval => #{ + order => 3, + type => number + }, + algorithm => #{ + order => 3, + type => string, + enum => [<<"hmac-based">>, <<"public-key">>] + }, + secret => #{ + order => 4, + type => string + }, + jwt_certfile => #{ + order => 5, + type => file + }, + cacertfile => #{ + order => 6, + type => file + }, + keyfile => #{ + order => 7, + type => file + }, + certfile => #{ + order => 8, + type => file + }, + verify => #{ + order => 9, + type => boolean + }, + server_name_indication => #{ + order => 10, + type => string + } + } +}). + +-define(RULES, + #{ + use_jwks => [], + jwks_endpoint => [use_jwks], + refresh_interval => [use_jwks], + algorithm => [use_jwks], + secret => [algorithm], + jwt_certfile => [algorithm], + cacertfile => [jwks_endpoint], + keyfile => [jwks_endpoint], + certfile => [jwks_endpoint], + verify => [jwks_endpoint], + server_name_indication => [jwks_endpoint], + verify_claims => [] + }). + +create(_ChainID, _ServiceName, Params) -> + try handle_options(Params) of + Opts -> + do_create(Opts) + catch + {error, Reason} -> + {error, Reason} + end. + +update(_ChainID, _ServiceName, Params, State) -> + try handle_options(Params) of + Opts -> + do_update(Opts, State) + catch + {error, Reason} -> + {error, Reason} + end. + +authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, + jwks_connector := Connector, + verify_claims := VerifyClaims0}) -> + JWKs = case Connector of + undefined -> + [JWK]; + _ -> + {ok, JWKs0} = emqx_authentication_jwks_connector:get_jwks(Connector), + JWKs0 + end, + VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), + verify(JWT, JWKs, VerifyClaims). + +destroy(#{jwks_connector := undefined}) -> + ok; +destroy(#{jwks_connector := Connector}) -> + emqx_authentication_jwks_connector:stop(Connector), + ok. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +do_create(#{use_jwks := false, + algorithm := 'hmac-based', + secret := Secret, + verify_claims := VerifyClaims}) -> + JWK = jose_jwk:from_oct(Secret), + {ok, #{jwk => JWK, + jwks_connector => undefined, + verify_claims => VerifyClaims}}; +do_create(#{use_jwks := false, + algorithm := 'public-key', + jwt_certfile := Certfile, + verify_claims := VerifyClaims}) -> + JWK = jose_jwk:from_pem_file(Certfile), + {ok, #{jwk => JWK, + jwks_connector => undefined, + verify_claims => VerifyClaims}}; +do_create(#{use_jwks := true, + verify_claims := VerifyClaims} = Opts) -> + case emqx_authentication_jwks_connector:start_link(Opts) of + {ok, Connector} -> + {ok, #{jwk => undefined, + jwks_connector => Connector, + verify_claims => VerifyClaims}}; + {error, Reason} -> + {error, Reason} + end. + +do_update(Opts, #{jwk_connector := undefined}) -> + do_create(Opts); +do_update(#{use_jwks := false} = Opts, #{jwk_connector := Connector}) -> + emqx_authentication_jwks_connector:stop(Connector), + do_create(Opts); +do_update(#{use_jwks := true} = Opts, #{jwk_connector := Connector} = State) -> + ok = emqx_authentication_jwks_connector:update(Connector, Opts), + {ok, State}. + +replace_placeholder(L, Variables) -> + replace_placeholder(L, Variables, []). + +replace_placeholder([], _Variables, Acc) -> + Acc; +replace_placeholder([{Name, {placeholder, PL}} | More], Variables, Acc) -> + Value = maps:get(PL, Variables), + replace_placeholder(More, Variables, [{Name, Value} | Acc]); +replace_placeholder([{Name, Value} | More], Variables, Acc) -> + replace_placeholder(More, Variables, [{Name, Value} | Acc]). + +verify(_JWS, [], _VerifyClaims) -> + {error, invalid_signature}; +verify(JWS, [JWK | More], VerifyClaims) -> + case jose_jws:verify(JWK, JWS) of + {true, Payload, _JWS} -> + Claims = emqx_json:decode(Payload, [return_maps]), + verify_claims(Claims, VerifyClaims); + {false, _, _} -> + verify(JWS, More, VerifyClaims) + end. + +verify_claims(Claims, VerifyClaims0) -> + Now = os:system_time(seconds), + VerifyClaims = [{<<"exp">>, fun(ExpireTime) -> + Now < ExpireTime + end}, + {<<"iat">>, fun(IssueAt) -> + IssueAt =< Now + end}, + {<<"nbf">>, fun(NotBefore) -> + NotBefore =< Now + end}] ++ VerifyClaims0, + do_verify_claims(Claims, VerifyClaims). + +do_verify_claims(_Claims, []) -> + ok; +do_verify_claims(Claims, [{Name, Fun} | More]) when is_function(Fun) -> + case maps:take(Name, Claims) of + error -> + do_verify_claims(Claims, More); + {Value, NClaims} -> + case Fun(Value) of + true -> + do_verify_claims(NClaims, More); + _ -> + {error, {claims, {Name, Value}}} + end + end; +do_verify_claims(Claims, [{Name, Value} | More]) -> + case maps:take(Name, Claims) of + error -> + do_verify_claims(Claims, More); + {Value, NClaims} -> + do_verify_claims(NClaims, More); + {Value0, _} -> + {error, {claims, {Name, Value0}}} + end. + +handle_options(Opts0) when is_map(Opts0) -> + Ks = maps:fold(fun(K, _, Acc) -> + [atom_to_binary(K, utf8) | Acc] + end, [], ?RULES), + Opts1 = maps:to_list(maps:with(Ks, Opts0)), + handle_options([{binary_to_existing_atom(K, utf8), V} || {K, V} <- Opts1]); + +handle_options(Opts0) when is_list(Opts0) -> + Opts1 = add_missing_options(Opts0), + process_options({Opts1, [], length(Opts1)}, #{}). + +add_missing_options(Opts) -> + AllOpts = maps:keys(?RULES), + Fun = fun(K, Acc) -> + case proplists:is_defined(K, Acc) of + true -> + Acc; + false -> + [{K, unbound} | Acc] + end + end, + lists:foldl(Fun, Opts, AllOpts). + +process_options({[], [], _}, OptsMap) -> + OptsMap; +process_options({[], Skipped, Counter}, OptsMap) + when length(Skipped) < Counter -> + process_options({Skipped, [], length(Skipped)}, OptsMap); +process_options({[], _Skipped, _Counter}, _OptsMap) -> + throw({error, faulty_configuration}); +process_options({[{K, V} = Opt | More], Skipped, Counter}, OptsMap0) -> + case check_dependencies(K, OptsMap0) of + true -> + OptsMap1 = handle_option(K, V, OptsMap0), + process_options({More, Skipped, Counter}, OptsMap1); + false -> + process_options({More, [Opt | Skipped], Counter}, OptsMap0) + end. + +%% TODO: This is not a particularly good implementation(K => needless), it needs to be improved +handle_option(use_jwks, true, OptsMap) -> + OptsMap#{use_jwks => true, + algorithm => needless}; +handle_option(use_jwks, false, OptsMap) -> + OptsMap#{use_jwks => false, + jwks_endpoint => needless}; +handle_option(jwks_endpoint = Opt, unbound, #{use_jwks := true}) -> + throw({error, {options, {Opt, unbound}}}); +handle_option(jwks_endpoint, Value, #{use_jwks := true} = OptsMap) + when Value =/= unbound -> + case emqx_http_lib:uri_parse(Value) of + {ok, #{scheme := http}} -> + OptsMap#{enable_ssl => false, + jwks_endpoint => Value}; + {ok, #{scheme := https}} -> + OptsMap#{enable_ssl => true, + jwks_endpoint => Value}; + {error, _Reason} -> + throw({error, {options, {jwks_endpoint, Value}}}) + end; +handle_option(refresh_interval = Opt, Value0, #{use_jwks := true} = OptsMap) -> + Value = validate_option(Opt, Value0), + OptsMap#{Opt => Value}; +handle_option(algorithm = Opt, Value0, #{use_jwks := false} = OptsMap) -> + Value = validate_option(Opt, Value0), + OptsMap#{Opt => Value}; +handle_option(secret = Opt, unbound, #{algorithm := 'hmac-based'}) -> + throw({error, {options, {Opt, unbound}}}); +handle_option(secret = Opt, Value, #{algorithm := 'hmac-based'} = OptsMap) -> + OptsMap#{Opt => Value}; +handle_option(jwt_certfile = Opt, unbound, #{algorithm := 'public-key'}) -> + throw({error, {options, {Opt, unbound}}}); +handle_option(jwt_certfile = Opt, Value, #{algorithm := 'public-key'} = OptsMap) -> + OptsMap#{Opt => Value}; +handle_option(verify = Opt, Value0, #{enable_ssl := true} = OptsMap) -> + Value = validate_option(Opt, Value0), + OptsMap#{Opt => Value}; +handle_option(cacertfile = Opt, Value, #{enable_ssl := true} = OptsMap) + when Value =/= unbound -> + OptsMap#{Opt => Value}; +handle_option(certfile, unbound, #{enable_ssl := true} = OptsMap) -> + OptsMap; +handle_option(certfile = Opt, Value, #{enable_ssl := true} = OptsMap) -> + OptsMap#{Opt => Value}; +handle_option(keyfile, unbound, #{enable_ssl := true} = OptsMap) -> + OptsMap; +handle_option(keyfile = Opt, Value, #{enable_ssl := true} = OptsMap) -> + OptsMap#{Opt => Value}; +handle_option(server_name_indication = Opt, Value0, #{enable_ssl := true} = OptsMap) -> + Value = validate_option(Opt, Value0), + OptsMap#{Opt => Value}; +handle_option(verify_claims = Opt, Value0, OptsMap) -> + Value = handle_verify_claims(Value0), + OptsMap#{Opt => Value}; +handle_option(_Opt, _Value, OptsMap) -> + OptsMap. + +validate_option(refresh_interval, unbound) -> + 300; +validate_option(refresh_interval, Value) when is_integer(Value) -> + Value; +validate_option(algorithm, <<"hmac-based">>) -> + 'hmac-based'; +validate_option(algorithm, <<"public-key">>) -> + 'public-key'; +validate_option(verify, unbound) -> + verify_none; +validate_option(verify, true) -> + verify_peer; +validate_option(verify, false) -> + verify_none; +validate_option(server_name_indication, unbound) -> + disable; +validate_option(server_name_indication, <<"disable">>) -> + disable; +validate_option(server_name_indication, Value) when is_list(Value) -> + Value; +validate_option(Opt, Value) -> + throw({error, {options, {Opt, Value}}}). + +handle_verify_claims(Opts0) -> + try handle_verify_claims(Opts0, []) + catch + error:_ -> + throw({error, {options, {verify_claims, Opts0}}}) + end. + +handle_verify_claims([], Acc) -> + Acc; +handle_verify_claims([{Name, Expected0} | More], Acc) + when is_binary(Name) andalso is_binary(Expected0) -> + Expected = handle_placeholder(Expected0), + handle_verify_claims(More, [{Name, Expected} | Acc]). + +handle_placeholder(Placeholder0) -> + case re:run(Placeholder0, "^\\$\\{[a-z0-9\\_]+\\}$", [{capture, all}]) of + {match, [{Offset, Length}]} -> + Placeholder1 = binary:part(Placeholder0, Offset + 2, Length - 3), + Placeholder2 = validate_placeholder(Placeholder1), + {placeholder, Placeholder2}; + nomatch -> + Placeholder0 + end. + +validate_placeholder(<<"clientid">>) -> + clientid; +validate_placeholder(<<"username">>) -> + username. + +check_dependencies(Opt, OptsMap) -> + case maps:get(Opt, ?RULES) of + [] -> + true; + Deps -> + option_already_defined(Opt, OptsMap) orelse + dependecies_already_defined(Deps, OptsMap) + end. + +option_already_defined(Opt, OptsMap) -> + maps:get(Opt, OptsMap, unbound) =/= unbound. + +dependecies_already_defined(Deps, OptsMap) -> + Fun = fun(Opt) -> option_already_defined(Opt, OptsMap) end, + lists:all(Fun, Deps). diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index 0fa28d178..d671a234d 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -52,8 +52,6 @@ } }). - - -record(user_info, { user_id :: {user_group(), user_id()} , password_hash :: binary() From 0c237bf797bdc649e7902d11bf815483e1bae3de Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 8 Jun 2021 15:56:13 +0800 Subject: [PATCH 10/15] feat(jwt): support based encoded secret --- .../src/emqx_authentication_jwt.erl | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/apps/emqx_authentication/src/emqx_authentication_jwt.erl b/apps/emqx_authentication/src/emqx_authentication_jwt.erl index f80c90a36..63261b6e5 100644 --- a/apps/emqx_authentication/src/emqx_authentication_jwt.erl +++ b/apps/emqx_authentication/src/emqx_authentication_jwt.erl @@ -46,28 +46,32 @@ order => 4, type => string }, - jwt_certfile => #{ + secret_base64_encoded => #{ order => 5, - type => file + type => boolean }, - cacertfile => #{ + jwt_certfile => #{ order => 6, type => file }, - keyfile => #{ + cacertfile => #{ order => 7, type => file }, - certfile => #{ + keyfile => #{ order => 8, type => file }, - verify => #{ + certfile => #{ order => 9, + type => file + }, + verify => #{ + order => 10, type => boolean }, server_name_indication => #{ - order => 10, + order => 11, type => string } } @@ -80,6 +84,7 @@ refresh_interval => [use_jwks], algorithm => [use_jwks], secret => [algorithm], + secret_base64_encoded => [algorithm], jwt_certfile => [algorithm], cacertfile => [jwks_endpoint], keyfile => [jwks_endpoint], @@ -132,8 +137,15 @@ destroy(#{jwks_connector := Connector}) -> do_create(#{use_jwks := false, algorithm := 'hmac-based', - secret := Secret, + secret := Secret0, + secret_base64_encoded := Base64Encoded, verify_claims := VerifyClaims}) -> + Secret = case Base64Encoded of + true -> + base64:decode(Secret0); + false -> + Secret0 + end, JWK = jose_jwk:from_oct(Secret), {ok, #{jwk => JWK, jwks_connector => undefined, @@ -295,6 +307,9 @@ handle_option(secret = Opt, unbound, #{algorithm := 'hmac-based'}) -> throw({error, {options, {Opt, unbound}}}); handle_option(secret = Opt, Value, #{algorithm := 'hmac-based'} = OptsMap) -> OptsMap#{Opt => Value}; +handle_option(secret_base64_encoded = Opt, Value0, #{algorithm := 'hmac-based'} = OptsMap) -> + Value = validate_option(Opt, Value0), + OptsMap#{Opt => Value}; handle_option(jwt_certfile = Opt, unbound, #{algorithm := 'public-key'}) -> throw({error, {options, {Opt, unbound}}}); handle_option(jwt_certfile = Opt, Value, #{algorithm := 'public-key'} = OptsMap) -> @@ -330,6 +345,10 @@ validate_option(algorithm, <<"hmac-based">>) -> 'hmac-based'; validate_option(algorithm, <<"public-key">>) -> 'public-key'; +validate_option(secret_base64_encoded, unbound) -> + false; +validate_option(secret_base64_encoded, Value) when is_boolean(Value) -> + Value; validate_option(verify, unbound) -> verify_none; validate_option(verify, true) -> From 2bfb7f74df807cb2ab73a2f9a71fa9363791d24e Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 9 Jun 2021 10:36:06 +0800 Subject: [PATCH 11/15] fix(jwt): fix chain interruption --- .../src/emqx_authentication_jwt.erl | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/emqx_authentication/src/emqx_authentication_jwt.erl b/apps/emqx_authentication/src/emqx_authentication_jwt.erl index 63261b6e5..45c8cd429 100644 --- a/apps/emqx_authentication/src/emqx_authentication_jwt.erl +++ b/apps/emqx_authentication/src/emqx_authentication_jwt.erl @@ -113,17 +113,20 @@ update(_ChainID, _ServiceName, Params, State) -> end. authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, - jwks_connector := Connector, verify_claims := VerifyClaims0}) -> - JWKs = case Connector of - undefined -> + JWKs = case erlang:is_pid(JWK) of + false -> [JWK]; - _ -> - {ok, JWKs0} = emqx_authentication_jwks_connector:get_jwks(Connector), + true -> + {ok, JWKs0} = emqx_authentication_jwks_connector:get_jwks(JWK), JWKs0 end, VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), - verify(JWT, JWKs, VerifyClaims). + case verify(JWT, JWKs, VerifyClaims) of + ok -> ok; + {error, invalid_signature} -> ignore; + {error, {claims, _}} -> {stop, bad_passowrd} + end. destroy(#{jwks_connector := undefined}) -> ok; @@ -138,8 +141,7 @@ destroy(#{jwks_connector := Connector}) -> do_create(#{use_jwks := false, algorithm := 'hmac-based', secret := Secret0, - secret_base64_encoded := Base64Encoded, - verify_claims := VerifyClaims}) -> + secret_base64_encoded := Base64Encoded} = Opts) -> Secret = case Base64Encoded of true -> base64:decode(Secret0); @@ -148,23 +150,20 @@ do_create(#{use_jwks := false, end, JWK = jose_jwk:from_oct(Secret), {ok, #{jwk => JWK, - jwks_connector => undefined, - verify_claims => VerifyClaims}}; + verify_claims => maps:get(verify_claims, Opts)}}; + do_create(#{use_jwks := false, algorithm := 'public-key', - jwt_certfile := Certfile, - verify_claims := VerifyClaims}) -> + jwt_certfile := Certfile} = Opts) -> JWK = jose_jwk:from_pem_file(Certfile), {ok, #{jwk => JWK, - jwks_connector => undefined, - verify_claims => VerifyClaims}}; -do_create(#{use_jwks := true, - verify_claims := VerifyClaims} = Opts) -> + verify_claims => maps:get(verify_claims, Opts)}}; + +do_create(#{use_jwks := true} = Opts) -> case emqx_authentication_jwks_connector:start_link(Opts) of {ok, Connector} -> - {ok, #{jwk => undefined, - jwks_connector => Connector, - verify_claims => VerifyClaims}}; + {ok, #{jwk => Connector, + verify_claims => maps:get(verify_claims, Opts)}}; {error, Reason} -> {error, Reason} end. From d838206a56e00d590d1f42b2e6a5d783e3c54a15 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 9 Jun 2021 11:10:03 +0800 Subject: [PATCH 12/15] chore(code): add type spec --- .../include/emqx_authentication.hrl | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/emqx_authentication/include/emqx_authentication.hrl b/apps/emqx_authentication/include/emqx_authentication.hrl index 72de452d4..9fe8f58d2 100644 --- a/apps/emqx_authentication/include/emqx_authentication.hrl +++ b/apps/emqx_authentication/include/emqx_authentication.hrl @@ -16,25 +16,26 @@ -define(APP, emqx_authentication). --record(chain, - { id - , services - , created_at}). - --record(service, - { name - , type %% service_type - , provider - , params - , state - }). +-type(service_type_name() :: atom()). +-type(service_name() :: binary()). +-type(chain_id() :: binary()). -record(service_type, - { name - , provider - , params_spec + { name :: service_type_name() + , provider :: module() + , params_spec :: #{atom() => term()} }). +-record(service, + { name :: service_name() + , type :: service_type_name() + , provider :: module() + , params :: map() + , state :: map() + }). --type(chain_id() :: binary()). --type(service_name() :: binary()). \ No newline at end of file +-record(chain, + { id :: chain_id() + , services :: [#service{}] + , created_at :: integer() + }). From ae8be5d66f362a140971007087d155272762f591 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 9 Jun 2021 16:53:40 +0800 Subject: [PATCH 13/15] feat(authentication): add a 16-byte salt in front of the password before hashing --- .../data/user-credentials.csv | 6 +-- .../data/user-credentials.json | 6 ++- .../src/emqx_authentication_mnesia.erl | 38 ++++++++++++------- .../test/data/user-credentials.csv | 6 +-- .../test/data/user-credentials.json | 6 ++- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/apps/emqx_authentication/data/user-credentials.csv b/apps/emqx_authentication/data/user-credentials.csv index 0a0affaa2..108c9f10d 100644 --- a/apps/emqx_authentication/data/user-credentials.csv +++ b/apps/emqx_authentication/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash -myuser3,8d41233e39c95b5da13361e354e1c9e639f07b27d397463a8f91b71ee07ccfb2 -myuser4,5809df0154f3cb4ac5c3a5572eaca0c5f7f9d858e887fc675b2becab9feb19d1 +user_id,password_hash,salt +myuser3,13de23cc872cf8198797914e95b9ec4e123fd7aaea186aea824452ec0f651a91,DztjMHrbVWmzkF1/dKD/ag== +myuser4,5231a927328f24e7254513819e47277feeb379a724f5e784ddbb09db42d322b7,+kGfV4AH+MR3f30zhoPIkQ== diff --git a/apps/emqx_authentication/data/user-credentials.json b/apps/emqx_authentication/data/user-credentials.json index a910a8dc2..1361592da 100644 --- a/apps/emqx_authentication/data/user-credentials.json +++ b/apps/emqx_authentication/data/user-credentials.json @@ -1,10 +1,12 @@ [ { "user_id":"myuser1", - "password_hash":"09343625c6c123d3434932fe1ce08bae5ac00a8f95bd746e10491b0bafdd1817" + "password_hash":"3e4845e5fc818ac1bfe6a3f77ab665e7721700b5803b6f76def5dce6aacdc42c", + "salt": "LvLGNfMjUJhUpuWIubv4Gg==" }, { "user_id":"myuser2", - "password_hash":"8767a7d316ad68cb607c7c805b859ffa78277dda13b7a3e2e8b53cad3cabbc6e" + "password_hash":"54bd059dc88e6dd9158306a638215fdce5545eac351fdf2affc8ee94686711c5", + "salt": "wF1mavOutYiNvwyYw0PmbQ==" } ] diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index d671a234d..ce2286c28 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -32,20 +32,19 @@ , list_users/1 ]). +%% TODO: support bcrypt -service_type(#{ name => mnesia, params_spec => #{ user_id_type => #{ order => 1, type => string, - required => true, enum => [<<"username">>, <<"clientid">>, <<"ip">>, <<"common name">>, <<"issuer">>], default => <<"username">> }, password_hash_algorithm => #{ order => 2, type => string, - required => true, enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>], default => <<"sha256">> } @@ -55,6 +54,7 @@ -record(user_info, { user_id :: {user_group(), user_id()} , password_hash :: binary() + , salt :: binary() }). -type(user_group() :: {chain_id(), service_name()}). @@ -67,8 +67,6 @@ -define(TAB, mnesia_basic_auth). -%% TODO: Support salt - %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -103,8 +101,8 @@ authenticate(ClientInfo = #{password := Password}, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = Hash}] -> - case Hash =:= emqx_passwd:hash(Algorithm, Password) of + [#user_info{password_hash = Hash, salt = Salt}] -> + case Hash =:= emqx_passwd:hash(Algorithm, <>) of true -> ok; false -> @@ -220,9 +218,11 @@ import_users_from_csv(Filename, #{user_group := UserGroup}) -> import(_UserGroup, []) -> ok; -import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} | More]) +import(UserGroup, [#{<<"user_id">> := UserID, + <<"password_hash">> := PasswordHash, + <<"salt">> := Salt} | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> - import_user(UserGroup, UserID, PasswordHash), + import_user(UserGroup, UserID, PasswordHash, Salt), import(UserGroup, More); import(_UserGroup, [_ | _More]) -> {error, bad_format}. @@ -234,8 +234,9 @@ import(UserGroup, File, Seq) -> Fields = binary:split(Line, [<<",">>, <<" ">>, <<"\n">>], [global, trim_all]), case get_user_info_by_seq(Fields, Seq) of {ok, #{user_id := UserID, - password_hash := PasswordHash}} -> - import_user(UserGroup, UserID, PasswordHash), + password_hash := PasswordHash, + salt := Salt}} -> + import_user(UserGroup, UserID, PasswordHash, Salt), import(UserGroup, File, Seq); {error, Reason} -> {error, Reason} @@ -260,7 +261,7 @@ 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 := _} = Acc) -> +get_user_info_by_seq([], [], #{user_id := _, password_hash := _, salt := _} = Acc) -> {ok, Acc}; get_user_info_by_seq(_, [], _) -> {error, bad_format}; @@ -268,18 +269,27 @@ get_user_info_by_seq([UserID | More1], [<<"user_id">> | More2], Acc) -> get_user_info_by_seq(More1, More2, Acc#{user_id => UserID}); 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(_, _, _) -> {error, bad_format}. -compile({inline, [add/4]}). add(UserGroup, UserID, Password, Algorithm) -> + Salt = case Algorithm of + <<"plain">> -> <<>>; + _ -> crypto:strong_rand_bytes(16) + end, + SaltedPassword = <>, Credential = #user_info{user_id = {UserGroup, UserID}, - password_hash = emqx_passwd:hash(Algorithm, Password)}, + password_hash = emqx_passwd:hash(Algorithm, SaltedPassword), + salt = Salt}, mnesia:write(?TAB, Credential, write). -import_user(UserGroup, UserID, PasswordHash) -> +import_user(UserGroup, UserID, PasswordHash, Salt) -> Credential = #user_info{user_id = {UserGroup, UserID}, - password_hash = PasswordHash}, + password_hash = PasswordHash, + salt = base64:decode(Salt)}, mnesia:write(?TAB, Credential, write). delete_user2(UserInfo) -> diff --git a/apps/emqx_authentication/test/data/user-credentials.csv b/apps/emqx_authentication/test/data/user-credentials.csv index 0a0affaa2..108c9f10d 100644 --- a/apps/emqx_authentication/test/data/user-credentials.csv +++ b/apps/emqx_authentication/test/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash -myuser3,8d41233e39c95b5da13361e354e1c9e639f07b27d397463a8f91b71ee07ccfb2 -myuser4,5809df0154f3cb4ac5c3a5572eaca0c5f7f9d858e887fc675b2becab9feb19d1 +user_id,password_hash,salt +myuser3,13de23cc872cf8198797914e95b9ec4e123fd7aaea186aea824452ec0f651a91,DztjMHrbVWmzkF1/dKD/ag== +myuser4,5231a927328f24e7254513819e47277feeb379a724f5e784ddbb09db42d322b7,+kGfV4AH+MR3f30zhoPIkQ== diff --git a/apps/emqx_authentication/test/data/user-credentials.json b/apps/emqx_authentication/test/data/user-credentials.json index a910a8dc2..1361592da 100644 --- a/apps/emqx_authentication/test/data/user-credentials.json +++ b/apps/emqx_authentication/test/data/user-credentials.json @@ -1,10 +1,12 @@ [ { "user_id":"myuser1", - "password_hash":"09343625c6c123d3434932fe1ce08bae5ac00a8f95bd746e10491b0bafdd1817" + "password_hash":"3e4845e5fc818ac1bfe6a3f77ab665e7721700b5803b6f76def5dce6aacdc42c", + "salt": "LvLGNfMjUJhUpuWIubv4Gg==" }, { "user_id":"myuser2", - "password_hash":"8767a7d316ad68cb607c7c805b859ffa78277dda13b7a3e2e8b53cad3cabbc6e" + "password_hash":"54bd059dc88e6dd9158306a638215fdce5545eac351fdf2affc8ee94686711c5", + "salt": "wF1mavOutYiNvwyYw0PmbQ==" } ] From bb62b44554fc0ec476a09ebc47395ac3fe439019 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 9 Jun 2021 18:57:08 +0800 Subject: [PATCH 14/15] feat(auth): support bcrypt --- .../data/user-credentials.csv | 4 +- .../data/user-credentials.json | 8 +- .../src/emqx_authentication_mnesia.erl | 95 ++++++++++++------- .../test/data/user-credentials.csv | 4 +- .../test/data/user-credentials.json | 8 +- 5 files changed, 71 insertions(+), 48 deletions(-) diff --git a/apps/emqx_authentication/data/user-credentials.csv b/apps/emqx_authentication/data/user-credentials.csv index 108c9f10d..2543d39ca 100644 --- a/apps/emqx_authentication/data/user-credentials.csv +++ b/apps/emqx_authentication/data/user-credentials.csv @@ -1,3 +1,3 @@ user_id,password_hash,salt -myuser3,13de23cc872cf8198797914e95b9ec4e123fd7aaea186aea824452ec0f651a91,DztjMHrbVWmzkF1/dKD/ag== -myuser4,5231a927328f24e7254513819e47277feeb379a724f5e784ddbb09db42d322b7,+kGfV4AH+MR3f30zhoPIkQ== +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 diff --git a/apps/emqx_authentication/data/user-credentials.json b/apps/emqx_authentication/data/user-credentials.json index 1361592da..169122bd2 100644 --- a/apps/emqx_authentication/data/user-credentials.json +++ b/apps/emqx_authentication/data/user-credentials.json @@ -1,12 +1,12 @@ [ { "user_id":"myuser1", - "password_hash":"3e4845e5fc818ac1bfe6a3f77ab665e7721700b5803b6f76def5dce6aacdc42c", - "salt": "LvLGNfMjUJhUpuWIubv4Gg==" + "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", + "salt": "e378187547bf2d6f0545a3f441aa4d8a" }, { "user_id":"myuser2", - "password_hash":"54bd059dc88e6dd9158306a638215fdce5545eac351fdf2affc8ee94686711c5", - "salt": "wF1mavOutYiNvwyYw0PmbQ==" + "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" } ] diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index ce2286c28..626ec27b7 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -45,8 +45,13 @@ password_hash_algorithm => #{ order => 2, type => string, - enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>], + enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], default => <<"sha256">> + }, + salt_rounds => #{ + order => 3, + type => number, + default => 10 } } }). @@ -84,10 +89,13 @@ mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB, disc_copies). create(ChainID, ServiceName, #{<<"user_id_type">> := Type, - <<"password_hash_algorithm">> := Algorithm}) -> + <<"password_hash_algorithm">> := Algorithm, + <<"salt_rounds">> := SaltRounds}) -> + Algorithm =:= <<"bcrypt">> andalso ({ok, _} = application:ensure_all_started(bcrypt)), State = #{user_group => {ChainID, ServiceName}, user_id_type => binary_to_atom(Type, utf8), - password_hash_algorithm => binary_to_atom(Algorithm, utf8)}, + password_hash_algorithm => binary_to_atom(Algorithm, utf8), + salt_rounds => SaltRounds}, {ok, State}. update(ChainID, ServiceName, Params, _State) -> @@ -101,12 +109,10 @@ authenticate(ClientInfo = #{password := Password}, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = Hash, salt = Salt}] -> - case Hash =:= emqx_passwd:hash(Algorithm, <>) of - true -> - ok; - false -> - {stop, bad_password} + [#user_info{password_hash = PasswordHash, salt = Salt}] -> + case PasswordHash =:= hash(Algorithm, Password, Salt) of + true -> ok; + false -> {stop, bad_password} end end. @@ -132,13 +138,12 @@ import_users(Filename0, State) -> add_user(#{<<"user_id">> := UserID, <<"password">> := Password}, - #{user_group := UserGroup, - password_hash_algorithm := Algorithm}) -> + #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add(UserGroup, UserID, Password, Algorithm), + add(UserID, Password, State), {ok, #{user_id => UserID}}; [_] -> {error, already_exist} @@ -157,15 +162,14 @@ delete_user(UserID, #{user_group := UserGroup}) -> end). update_user(UserID, #{<<"password">> := Password}, - #{user_group := UserGroup, - password_hash_algorithm := Algorithm}) -> + #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; [_] -> - add(UserGroup, UserID, Password, Algorithm), + add(UserID, Password, State), {ok, #{user_id => UserID}} end end). @@ -219,10 +223,10 @@ import_users_from_csv(Filename, #{user_group := UserGroup}) -> import(_UserGroup, []) -> ok; import(UserGroup, [#{<<"user_id">> := UserID, - <<"password_hash">> := PasswordHash, - <<"salt">> := Salt} | More]) + <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> - import_user(UserGroup, UserID, PasswordHash, Salt), + Salt = maps:get(<<"salt">>, UserInfo, <<>>), + insert_user(UserGroup, UserID, PasswordHash, Salt), import(UserGroup, More); import(_UserGroup, [_ | _More]) -> {error, bad_format}. @@ -234,9 +238,9 @@ import(UserGroup, File, Seq) -> Fields = binary:split(Line, [<<",">>, <<" ">>, <<"\n">>], [global, trim_all]), case get_user_info_by_seq(Fields, Seq) of {ok, #{user_id := UserID, - password_hash := PasswordHash, - salt := Salt}} -> - import_user(UserGroup, UserID, PasswordHash, Salt), + password_hash := PasswordHash} = UserInfo} -> + Salt = maps:get(salt, UserInfo, <<>>), + insert_user(UserGroup, UserID, PasswordHash, Salt), import(UserGroup, File, Seq); {error, Reason} -> {error, Reason} @@ -263,6 +267,8 @@ 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(_, [], _) -> {error, bad_format}; get_user_info_by_seq([UserID | More1], [<<"user_id">> | More2], Acc) -> @@ -274,22 +280,39 @@ get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) -> get_user_info_by_seq(_, _, _) -> {error, bad_format}. --compile({inline, [add/4]}). -add(UserGroup, UserID, Password, Algorithm) -> - Salt = case Algorithm of - <<"plain">> -> <<>>; - _ -> crypto:strong_rand_bytes(16) - end, - SaltedPassword = <>, - Credential = #user_info{user_id = {UserGroup, UserID}, - password_hash = emqx_passwd:hash(Algorithm, SaltedPassword), - salt = Salt}, - mnesia:write(?TAB, Credential, write). +-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. -import_user(UserGroup, UserID, PasswordHash, Salt) -> - Credential = #user_info{user_id = {UserGroup, UserID}, - password_hash = PasswordHash, - salt = base64:decode(Salt)}, +gen_salt(#{password_hash_algorithm := plain}) -> + <<>>; +gen_salt(#{password_hash_algorithm := bcrypt, + salt_rounds := Rounds}) -> + {ok, Salt} = bcrypt:gen_salt(Rounds), + Salt; +gen_salt(_) -> + <> = crypto:strong_rand_bytes(16), + iolist_to_binary(io_lib:format("~32.16.0b", [X])). + +hash(bcrypt, Password, Salt) -> + {ok, Hash} = bcrypt:hashpw(Password, Salt), + list_to_binary(Hash); +hash(Algorithm, Password, Salt) -> + emqx_passwd:hash(Algorithm, <>). + +insert_user(UserGroup, UserID, PasswordHash) -> + insert_user(UserGroup, UserID, PasswordHash, <<>>). + +insert_user(UserGroup, UserID, PasswordHash, Salt) -> + Credential = #user_info{user_id = {UserGroup, UserID}, + password_hash = PasswordHash, + salt = Salt}, mnesia:write(?TAB, Credential, write). delete_user2(UserInfo) -> diff --git a/apps/emqx_authentication/test/data/user-credentials.csv b/apps/emqx_authentication/test/data/user-credentials.csv index 108c9f10d..2543d39ca 100644 --- a/apps/emqx_authentication/test/data/user-credentials.csv +++ b/apps/emqx_authentication/test/data/user-credentials.csv @@ -1,3 +1,3 @@ user_id,password_hash,salt -myuser3,13de23cc872cf8198797914e95b9ec4e123fd7aaea186aea824452ec0f651a91,DztjMHrbVWmzkF1/dKD/ag== -myuser4,5231a927328f24e7254513819e47277feeb379a724f5e784ddbb09db42d322b7,+kGfV4AH+MR3f30zhoPIkQ== +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 diff --git a/apps/emqx_authentication/test/data/user-credentials.json b/apps/emqx_authentication/test/data/user-credentials.json index 1361592da..169122bd2 100644 --- a/apps/emqx_authentication/test/data/user-credentials.json +++ b/apps/emqx_authentication/test/data/user-credentials.json @@ -1,12 +1,12 @@ [ { "user_id":"myuser1", - "password_hash":"3e4845e5fc818ac1bfe6a3f77ab665e7721700b5803b6f76def5dce6aacdc42c", - "salt": "LvLGNfMjUJhUpuWIubv4Gg==" + "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", + "salt": "e378187547bf2d6f0545a3f441aa4d8a" }, { "user_id":"myuser2", - "password_hash":"54bd059dc88e6dd9158306a638215fdce5545eac351fdf2affc8ee94686711c5", - "salt": "wF1mavOutYiNvwyYw0PmbQ==" + "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" } ] From cbc5c64a165c1db7b1ab10c768ba90d59a340826 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 10 Jun 2021 16:18:50 +0800 Subject: [PATCH 15/15] chore(dialyzer): make code pass dialyzer --- apps/emqx_authentication/include/emqx_authentication.hrl | 2 +- apps/emqx_authentication/src/emqx_authentication_jwt.erl | 4 ++-- apps/emqx_authentication/src/emqx_authentication_mnesia.erl | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/emqx_authentication/include/emqx_authentication.hrl b/apps/emqx_authentication/include/emqx_authentication.hrl index 9fe8f58d2..09d3c5fc4 100644 --- a/apps/emqx_authentication/include/emqx_authentication.hrl +++ b/apps/emqx_authentication/include/emqx_authentication.hrl @@ -36,6 +36,6 @@ -record(chain, { id :: chain_id() - , services :: [#service{}] + , services :: [{service_name(), #service{}}] , created_at :: integer() }). diff --git a/apps/emqx_authentication/src/emqx_authentication_jwt.erl b/apps/emqx_authentication/src/emqx_authentication_jwt.erl index 45c8cd429..2b8024e1c 100644 --- a/apps/emqx_authentication/src/emqx_authentication_jwt.erl +++ b/apps/emqx_authentication/src/emqx_authentication_jwt.erl @@ -131,7 +131,7 @@ authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, destroy(#{jwks_connector := undefined}) -> ok; destroy(#{jwks_connector := Connector}) -> - emqx_authentication_jwks_connector:stop(Connector), + _ = emqx_authentication_jwks_connector:stop(Connector), ok. %%-------------------------------------------------------------------- @@ -171,7 +171,7 @@ do_create(#{use_jwks := true} = Opts) -> do_update(Opts, #{jwk_connector := undefined}) -> do_create(Opts); do_update(#{use_jwks := false} = Opts, #{jwk_connector := Connector}) -> - emqx_authentication_jwks_connector:stop(Connector), + _ = emqx_authentication_jwks_connector:stop(Connector), do_create(Opts); do_update(#{use_jwks := true} = Opts, #{jwk_connector := Connector} = State) -> ok = emqx_authentication_jwks_connector:update(Connector, Opts), diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl index 626ec27b7..53dc4dd73 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authentication/src/emqx_authentication_mnesia.erl @@ -119,8 +119,8 @@ authenticate(ClientInfo = #{password := Password}, destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{#user_info{user_id = {UserGroup, '_'}, _ = '_'}, [], ['$_']}], - lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write)) + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_'}, [], ['$_']}], + ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write)) end). import_users(Filename0, State) -> @@ -211,7 +211,7 @@ import_users_from_csv(Filename, #{user_group := UserGroup}) -> case get_csv_header(File) of {ok, Seq} -> Result = trans(fun import/3, [UserGroup, File, Seq]), - file:close(File), + _ = file:close(File), Result; {error, Reason} -> {error, Reason}