diff --git a/apps/emqx_authentication/etc/emqx_authentication.conf b/apps/emqx_authentication/etc/emqx_authentication.conf deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/emqx_authentication/include/emqx_authentication.hrl b/apps/emqx_authentication/include/emqx_authentication.hrl deleted file mode 100644 index 814c03eb4..000000000 --- a/apps/emqx_authentication/include/emqx_authentication.hrl +++ /dev/null @@ -1,43 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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). - --type(service_type_name() :: atom()). --type(service_name() :: binary()). --type(chain_id() :: binary()). - --record(service_type, - { 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() - }). - --record(chain, - { id :: chain_id() - , services :: [{service_name(), #service{}}] - , created_at :: integer() - }). - --define(AUTH_SHARD, emqx_authentication_shard). diff --git a/apps/emqx_authentication/priv/emqx_authentication.schema b/apps/emqx_authentication/priv/emqx_authentication.schema deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/emqx_authentication/src/emqx_authentication.erl b/apps/emqx_authentication/src/emqx_authentication.erl deleted file mode 100644 index a3d0241c0..000000000 --- a/apps/emqx_authentication/src/emqx_authentication.erl +++ /dev/null @@ -1,522 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_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 - , lookup_chain/1 - , list_chains/0 - , add_services/2 - , delete_services/2 - , update_service/3 - , lookup_service/2 - , list_services/1 - , move_service_to_the_front/2 - , move_service_to_the_end/2 - , move_service_to_the_nth/3 - ]). - --export([ import_users/3 - , add_user/3 - , delete_user/3 - , update_user/4 - , lookup_user/3 - , list_users/2 - ]). - --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). - --rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). --rlog_shard({?AUTH_SHARD, ?SERVICE_TYPE_TAB}). - -%%------------------------------------------------------------------------------ -%% 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', {emqx_authentication, authenticate, []}) of - ok -> ok; - {error, already_exists} -> ok - end. - -disable() -> - emqx:unhook('client.authenticate', {emqx_authentication, authenticate}), - ok. - -authenticate(#{chain_id := ChainID} = ClientInfo) -> - case mnesia:dirty_read(?CHAIN_TAB, ChainID) of - [#chain{services = []}] -> - {error, no_services}; - [#chain{services = Services}] -> - do_authenticate(Services, ClientInfo); - [] -> - {error, todo} - end. - -do_authenticate([], _) -> - {error, user_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(#{id := ID}) -> - trans( - fun() -> - case mnesia:read(?CHAIN_TAB, ID, write) of - [] -> - 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). - -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, ID}}}; - [Chain] -> - {ok, serialize_chain(Chain)} - end. - -list_chains() -> - Chains = ets:tab2list(?CHAIN_TAB), - {ok, [serialize_chain(Chain) || Chain <- Chains]}. - -add_services(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}, - ok = mnesia:write(?CHAIN_TAB, NChain, write), - {ok, serialize_services(NServices)}; - {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(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. - -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}), - ok = mnesia:write(?CHAIN_TAB, Chain#chain{services = NServices}, write), - {ok, serialize_service({ServiceName, NService})}; - {error, Reason} -> - {error, Reason} - end - end - end, - update_chain(ChainID, UpdateFun). - -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_services(Services)} - end. - -move_service_to_the_front(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(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(ChainID, ServiceName, N) -> - UpdateFun = fun(Chain = #chain{services = Services}) -> - case move_service_to_the_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). - -import_users(ChainID, ServiceName, Filename) -> - call_service(ChainID, ServiceName, import_users, [Filename]). - -add_user(ChainID, ServiceName, UserInfo) -> - call_service(ChainID, ServiceName, add_user, [UserInfo]). - -delete_user(ChainID, ServiceName, UserID) -> - call_service(ChainID, ServiceName, delete_user, [UserID]). - -update_user(ChainID, ServiceName, UserID, NewUserInfo) -> - call_service(ChainID, ServiceName, update_user, [UserID, NewUserInfo]). - -lookup_user(ChainID, ServiceName, UserID) -> - call_service(ChainID, ServiceName, lookup_user, [UserID]). - -list_users(ChainID, ServiceName) -> - call_service(ChainID, ServiceName, list_users, []). - -%%------------------------------------------------------------------------------ -%% 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, - original_params => Params, - 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, - 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} -> - 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_the_nth_(ServiceName, Services, N) - when N =< length(Services) andalso N > 0 -> - move_service_to_the_nth_(ServiceName, Services, N, []); -move_service_to_the_nth_(_, _, _) -> - {error, out_of_range}. - -move_service_to_the_nth_(ServiceName, [], _, _) -> - {error, {not_found, {service, ServiceName}}}; -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_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_the_nth_(ServiceName, [Service | More], N, Passed) -> - move_service_to_the_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 - [] -> - {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. - -serialize_chain(#chain{id = ID, - services = Services, - created_at = CreatedAt}) -> - #{id => ID, - services => serialize_services(Services), - created_at => CreatedAt}. - -serialize_services(Services) -> - [serialize_service(Service) || Service <- Services]. - -serialize_service({_, #service{name = Name, - type = Type, - params = Params}}) -> - #{name => Name, - type => Type, - params => Params}. - -trans(Fun) -> - trans(Fun, []). - -trans(Fun, Args) -> - case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of - {atomic, Res} -> Res; - {aborted, Reason} -> {error, Reason} - end. diff --git a/apps/emqx_authentication/src/emqx_authentication_api.erl b/apps/emqx_authentication/src/emqx_authentication_api.erl deleted file mode 100644 index 74887a0b2..000000000 --- a/apps/emqx_authentication/src/emqx_authentication_api.erl +++ /dev/null @@ -1,407 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authentication_api). - --export([ create_chain/2 - , delete_chain/2 - , lookup_chain/2 - , list_chains/2 - , add_service/2 - , delete_service/2 - , update_service/2 - , lookup_service/2 - , list_services/2 - , move_service/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 => create_chain, - method => 'POST', - path => "/authentication/chains", - func => create_chain, - descr => "Create a chain" - }). - --rest_api(#{name => delete_chain, - method => 'DELETE', - path => "/authentication/chains/:bin:id", - func => delete_chain, - descr => "Delete chain" - }). - --rest_api(#{name => lookup_chain, - method => 'GET', - path => "/authentication/chains/:bin: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:id/services", - func => add_service, - descr => "Add service to chain" - }). - --rest_api(#{name => delete_service, - method => 'DELETE', - 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:id/services/:bin:service_name", - func => update_service, - descr => "Update service in chain" - }). - --rest_api(#{name => lookup_service, - method => 'GET', - 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:id/services", - func => list_services, - descr => "List services in chain" - }). - --rest_api(#{name => move_service, - method => 'POST', - 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:id/services/:bin:service_name/import-users", - func => import_users, - descr => "Import users" - }). - --rest_api(#{name => add_user, - method => 'POST', - 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: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: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: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: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, #{<<"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, id})). - -delete_chain(Binding, Params) -> - do_delete_chain(uri_decode(Binding), maps:from_list(Params)). - -do_delete_chain(#{id := ChainID}, _Params) -> - case emqx_authentication:delete_chain(ChainID) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_chain(Binding, Params) -> - do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_chain(#{id := ChainID}, _Params) -> - case emqx_authentication:lookup_chain(ChainID) of - {ok, Chain} -> - return({ok, Chain}); - {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(#{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; -%% 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})). - -delete_service(Binding, Params) -> - do_delete_service(uri_decode(Binding), maps:from_list(Params)). - -do_delete_service(#{id := ChainID, - service_name := ServiceName}, _Params) -> - case emqx_authentication:delete_services(ChainID, [ServiceName]) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -update_service(Binding, Params) -> - do_update_service(uri_decode(Binding), maps:from_list(Params)). - -%% 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} -> - return({ok, Service}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -lookup_service(Binding, Params) -> - do_lookup_service(uri_decode(Binding), maps:from_list(Params)). - -do_lookup_service(#{id := ChainID, - service_name := ServiceName}, _Params) -> - case emqx_authentication:lookup_service(ChainID, ServiceName) of - {ok, Service} -> - return({ok, Service}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -list_services(Binding, Params) -> - do_list_services(uri_decode(Binding), maps:from_list(Params)). - -do_list_services(#{id := ChainID}, _Params) -> - case emqx_authentication:list_services(ChainID) of - {ok, Services} -> - return({ok, Services}); - {error, Reason} -> - return(serialize_error(Reason)) - end. - -move_service(Binding, Params) -> - do_move_service(uri_decode(Binding), maps:from_list(Params)). - -do_move_service(#{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(serialize_error(Reason)) - end; -do_move_service(#{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(#{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">>})). - -import_users(Binding, Params) -> - do_import_users(uri_decode(Binding), maps:from_list(Params)). - -do_import_users(#{id := ChainID, service_name := ServiceName}, - #{<<"filename">> := Filename}) -> - case emqx_authentication:import_users(ChainID, ServiceName, Filename) of - ok -> - return(ok); - {error, Reason} -> - return(serialize_error(Reason)) - end; -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(#{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(#{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(#{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(#{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(#{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 -%%------------------------------------------------------------------------------ - -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}}) -> - {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("~s '~s' not found", [serialize_type(Type), ID]))}; -serialize_error({duplicate, Name}) -> - {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Service name '~s' is duplicated", [Name]))}; -serialize_error({missing_parameter, Names = [_ | Rest]}) -> - 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 '~s' that is mandatory for processing this request is not supplied.", [Name]))}; -serialize_error(_) -> - {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. - -serialize_type(service) -> - "Service"; -serialize_type(chain) -> - "Chain"; -serialize_type(service_type) -> - "Service type". - -get_missed_params(Actual, Expected) -> - 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_jwt.erl b/apps/emqx_authentication/src/emqx_authentication_jwt.erl deleted file mode 100644 index 2b8024e1c..000000000 --- a/apps/emqx_authentication/src/emqx_authentication_jwt.erl +++ /dev/null @@ -1,409 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_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 - }, - secret_base64_encoded => #{ - order => 5, - type => boolean - }, - jwt_certfile => #{ - order => 6, - type => file - }, - cacertfile => #{ - order => 7, - type => file - }, - keyfile => #{ - order => 8, - type => file - }, - certfile => #{ - order => 9, - type => file - }, - verify => #{ - order => 10, - type => boolean - }, - server_name_indication => #{ - order => 11, - type => string - } - } -}). - --define(RULES, - #{ - use_jwks => [], - jwks_endpoint => [use_jwks], - refresh_interval => [use_jwks], - algorithm => [use_jwks], - secret => [algorithm], - secret_base64_encoded => [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, - verify_claims := VerifyClaims0}) -> - JWKs = case erlang:is_pid(JWK) of - false -> - [JWK]; - true -> - {ok, JWKs0} = emqx_authentication_jwks_connector:get_jwks(JWK), - JWKs0 - end, - VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), - case verify(JWT, JWKs, VerifyClaims) of - ok -> ok; - {error, invalid_signature} -> ignore; - {error, {claims, _}} -> {stop, bad_passowrd} - end. - -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 := Secret0, - secret_base64_encoded := Base64Encoded} = Opts) -> - Secret = case Base64Encoded of - true -> - base64:decode(Secret0); - false -> - Secret0 - end, - JWK = jose_jwk:from_oct(Secret), - {ok, #{jwk => JWK, - verify_claims => maps:get(verify_claims, Opts)}}; - -do_create(#{use_jwks := false, - algorithm := 'public-key', - jwt_certfile := Certfile} = Opts) -> - JWK = jose_jwk:from_pem_file(Certfile), - {ok, #{jwk => JWK, - 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 => Connector, - verify_claims => maps:get(verify_claims, Opts)}}; - {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(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) -> - 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(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) -> - 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/test/emqx_authentication_SUITE.erl b/apps/emqx_authentication/test/emqx_authentication_SUITE.erl deleted file mode 100644 index d9f9ace8b..000000000 --- a/apps/emqx_authentication/test/emqx_authentication_SUITE.erl +++ /dev/null @@ -1,189 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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) -> - application:set_env(ekka, strict_mode, true), - emqx_ct_helpers:start_apps([emqx_authentication]), - Config. - -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authentication]), - ok. - -t_chain(_) -> - ChainID = <<"mychain">>, - ?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)), - ok. - -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_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?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, [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, [ServiceParams2, ServiceParams1]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_end(ChainID, ServiceName2)), - ?assertEqual({ok, [ServiceParams1, ServiceParams2]}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:move_service_to_the_nth(ChainID, ServiceName2, 1)), - ?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])), - ?assertEqual({ok, []}, ?AUTH:list_services(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. - -t_mnesia_service(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName = <<"myservice">>, - ServiceParams = #{name => ServiceName, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?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_not_found}, ?AUTH:authenticate(ClientInfo2)), - ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), - 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(ChainID, ServiceName, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(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, [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)), - ok. - -t_import(_) -> - ChainID = <<"mychain">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName = <<"myservice">>, - ServiceParams = #{name => ServiceName, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams]}, ?AUTH:add_services(ChainID, [ServiceParams])), - - Dir = code:lib_dir(emqx_authentication, test), - ?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">>}, - ?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">>, - ?assertMatch({ok, #{id := ChainID, services := []}}, ?AUTH:create_chain(#{id => ChainID})), - - ServiceName1 = <<"myservice1">>, - ServiceParams1 = #{name => ServiceName1, - type => mnesia, - params => #{ - user_id_type => <<"username">>, - password_hash_algorithm => <<"sha256">>}}, - ServiceName2 = <<"myservice2">>, - ServiceParams2 = #{name => ServiceName2, - type => mnesia, - params => #{ - user_id_type => <<"clientid">>, - password_hash_algorithm => <<"sha256">>}}, - ?assertEqual({ok, [ServiceParams1]}, ?AUTH:add_services(ChainID, [ServiceParams1])), - ?assertEqual({ok, [ServiceParams2]}, ?AUTH:add_services(ChainID, [ServiceParams2])), - - ?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">>, - password => <<"mypass1">>}, - ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), - ?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)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ok. diff --git a/apps/emqx_authentication/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv similarity index 100% rename from apps/emqx_authentication/data/user-credentials.csv rename to apps/emqx_authn/data/user-credentials.csv diff --git a/apps/emqx_authentication/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json similarity index 100% rename from apps/emqx_authentication/data/user-credentials.json rename to apps/emqx_authn/data/user-credentials.json diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf new file mode 100644 index 000000000..092bd5404 --- /dev/null +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -0,0 +1,26 @@ +authn: { + chains: [ + # { + # id: "chain1" + # type: simple + # authenticators: [ + # { + # name: "authenticator1" + # type: built-in-database + # config: { + # user_id_type: clientid + # password_hash_algorithm: { + # name: sha256 + # } + # } + # } + # ] + # } + ] + bindings: [ + # { + # chain_id: "chain1" + # listeners: ["mqtt-tcp", "mqtt-ssl"] + # } + ] +} diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl new file mode 100644 index 000000000..46c1cf7ca --- /dev/null +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -0,0 +1,67 @@ +%%-------------------------------------------------------------------- +%% 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_authn). + +-type chain_id() :: binary(). +-type authn_type() :: simple | enhanced. +-type authenticator_name() :: binary(). +-type authenticator_type() :: mnesia | jwt | mysql | postgresql. +-type listener_id() :: binary(). + +-record(authenticator, + { name :: authenticator_name() + , type :: authenticator_type() + , provider :: module() + , config :: map() + , state :: map() + }). + +-record(chain, + { id :: chain_id() + , type :: authn_type() + , authenticators :: [{authenticator_name(), #authenticator{}}] + , created_at :: integer() + }). + +-record(binding, + { bound :: {listener_id(), authn_type()} + , chain_id :: chain_id() + }). + +-define(AUTH_SHARD, emqx_authn_shard). + +-define(CLUSTER_CALL(Module, Func, Args), ?CLUSTER_CALL(Module, Func, Args, ok)). + +-define(CLUSTER_CALL(Module, Func, Args, ResParttern), + fun() -> + case LocalResult = erlang:apply(Module, Func, Args) of + ResParttern -> + Nodes = nodes(), + {ResL, BadNodes} = rpc:multicall(Nodes, Module, Func, Args, 5000), + NResL = lists:zip(Nodes - BadNodes, ResL), + Errors = lists:filter(fun({_, ResParttern}) -> false; + (_) -> true + end, NResL), + OtherErrors = [{BadNode, node_does_not_exist} || BadNode <- BadNodes], + case Errors ++ OtherErrors of + [] -> LocalResult; + NErrors -> {error, NErrors} + end; + ErrorResult -> + {error, ErrorResult} + end + end()). diff --git a/apps/emqx_authentication/rebar.config b/apps/emqx_authn/rebar.config similarity index 100% rename from apps/emqx_authentication/rebar.config rename to apps/emqx_authn/rebar.config diff --git a/apps/emqx_authentication/src/emqx_authentication.app.src b/apps/emqx_authn/src/emqx_authn.app.src similarity index 63% rename from apps/emqx_authentication/src/emqx_authentication.app.src rename to apps/emqx_authn/src/emqx_authn.app.src index 4f55ca0a7..c997582ec 100644 --- a/apps/emqx_authentication/src/emqx_authentication.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -1,10 +1,10 @@ -{application, emqx_authentication, +{application, emqx_authn, [{description, "EMQ X Authentication"}, {vsn, "0.1.0"}, {modules, []}, - {registered, [emqx_authentication_sup, emqx_authentication_registry]}, + {registered, [emqx_authn_sup, emqx_authn_registry]}, {applications, [kernel,stdlib]}, - {mod, {emqx_authentication_app,[]}}, + {mod, {emqx_authn_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, {maintainers, ["EMQ X Team "]}, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl new file mode 100644 index 000000000..24bdb21e7 --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -0,0 +1,490 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn). + +-include("emqx_authn.hrl"). + +-export([ enable/0 + , disable/0 + ]). + +-export([authenticate/1]). + +-export([ create_chain/1 + , delete_chain/1 + , lookup_chain/1 + , list_chains/0 + , bind/2 + , unbind/2 + , list_bindings/1 + , list_bound_chains/1 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/3 + , lookup_authenticator/2 + , list_authenticators/1 + , move_authenticator_to_the_front/2 + , move_authenticator_to_the_end/2 + , move_authenticator_to_the_nth/3 + ]). + +-export([ import_users/3 + , add_user/3 + , delete_user/3 + , update_user/4 + , lookup_user/3 + , list_users/2 + ]). + +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). + +-define(CHAIN_TAB, emqx_authn_chain). +-define(BINDING_TAB, emqx_authn_binding). + +-rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}). +-rlog_shard({?AUTH_SHARD, ?BINDING_TAB}). + +%%------------------------------------------------------------------------------ +%% Mnesia bootstrap +%%------------------------------------------------------------------------------ + +%% @doc Create or replicate tables. +-spec(mnesia(boot) -> ok). +mnesia(boot) -> + %% Optimize storage + StoreProps = [{ets, [{read_concurrency, true}]}], + %% Chain table + ok = ekka_mnesia:create_table(?CHAIN_TAB, [ + {ram_copies, [node()]}, + {record_name, chain}, + {local_content, true}, + {attributes, record_info(fields, chain)}, + {storage_properties, StoreProps}]), + %% Binding table + ok = ekka_mnesia:create_table(?BINDING_TAB, [ + {ram_copies, [node()]}, + {record_name, binding}, + {local_content, true}, + {attributes, record_info(fields, binding)}, + {storage_properties, StoreProps}]). + +enable() -> + case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of + ok -> ok; + {error, already_exists} -> ok + end. + +disable() -> + emqx:unhook('client.authenticate', {?MODULE, authenticate, []}), + ok. + +authenticate(#{listener_id := ListenerID} = ClientInfo) -> + case lookup_chain_by_listener(ListenerID, simple) of + {error, _} -> + {error, no_authenticators}; + {ok, ChainID} -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [#chain{authenticators = []}] -> + {error, no_authenticators}; + [#chain{authenticators = Authenticators}] -> + do_authenticate(Authenticators, ClientInfo); + [] -> + {error, no_authenticators} + end + end. + +do_authenticate([], _) -> + {error, user_not_found}; +do_authenticate([{_, #authenticator{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. + +create_chain(#{id := ID, + type := Type}) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ID, write) of + [] -> + Chain = #chain{id = ID, + type = Type, + authenticators = [], + created_at = erlang:system_time(millisecond)}, + mnesia:write(?CHAIN_TAB, Chain, write), + {ok, serialize_chain(Chain)}; + [_ | _] -> + {error, {already_exists, {chain, ID}}} + end + end). + +delete_chain(ID) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ID, write) of + [] -> + {error, {not_found, {chain, ID}}}; + [#chain{authenticators = Authenticators}] -> + _ = [do_delete_authenticator(Authenticator) || {_, Authenticator} <- Authenticators], + mnesia:delete(?CHAIN_TAB, ID, write) + end + end). + +lookup_chain(ID) -> + case mnesia:dirty_read(?CHAIN_TAB, ID) of + [] -> + {error, {not_found, {chain, ID}}}; + [Chain] -> + {ok, serialize_chain(Chain)} + end. + +list_chains() -> + Chains = ets:tab2list(?CHAIN_TAB), + {ok, [serialize_chain(Chain) || Chain <- Chains]}. + +bind(ChainID, Listeners) -> + %% TODO: ensure listener id is valid + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{type = AuthNType}] -> + Result = lists:foldl( + fun(ListenerID, Acc) -> + case mnesia:read(?BINDING_TAB, {ListenerID, AuthNType}, write) of + [] -> + Binding = #binding{bound = {ListenerID, AuthNType}, chain_id = ChainID}, + mnesia:write(?BINDING_TAB, Binding, write), + Acc; + _ -> + [ListenerID | Acc] + end + end, [], Listeners), + case Result of + [] -> ok; + Listeners0 -> {error, {already_bound, Listeners0}} + end + end + end). + +unbind(ChainID, Listeners) -> + trans( + fun() -> + Result = lists:foldl( + fun(ListenerID, Acc) -> + MatchSpec = [{{binding, {ListenerID, '_'}, ChainID}, [], ['$_']}], + case mnesia:select(?BINDING_TAB, MatchSpec, write) of + [] -> + [ListenerID | Acc]; + [#binding{bound = Bound}] -> + mnesia:delete(?BINDING_TAB, Bound, write), + Acc + end + end, [], Listeners), + case Result of + [] -> ok; + Listeners0 -> + {error, {not_found, Listeners0}} + end + end). + +list_bindings(ChainID) -> + trans( + fun() -> + MatchSpec = [{{binding, {'$1', '_'}, ChainID}, [], ['$1']}], + Listeners = mnesia:select(?BINDING_TAB, MatchSpec), + {ok, #{chain_id => ChainID, listeners => Listeners}} + end). + +list_bound_chains(ListenerID) -> + trans( + fun() -> + MatchSpec = [{{binding, {ListenerID, '_'}, '_'}, [], ['$_']}], + Bindings = mnesia:select(?BINDING_TAB, MatchSpec), + Chains = [{AuthNType, ChainID} || #binding{bound = {_, AuthNType}, + chain_id = ChainID} <- Bindings], + {ok, maps:from_list(Chains)} + end). + +create_authenticator(ChainID, #{name := Name, + type := Type, + config := Config}) -> + UpdateFun = + fun(Chain = #chain{type = AuthNType, authenticators = Authenticators}) -> + case lists:keymember(Name, 1, Authenticators) of + true -> + {error, {already_exists, {authenticator, Name}}}; + false -> + Provider = authenticator_provider(AuthNType, Type), + case Provider:create(ChainID, Name, Config) of + {ok, State} -> + Authenticator = #authenticator{name = Name, + type = Type, + provider = Provider, + config = Config, + state = State}, + NChain = Chain#chain{authenticators = Authenticators ++ [{Name, Authenticator}]}, + ok = mnesia:write(?CHAIN_TAB, NChain, write), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + update_chain(ChainID, UpdateFun). + +delete_authenticator(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case lists:keytake(AuthenticatorName, 1, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + {value, {_, Authenticator}, NAuthenticators} -> + _ = do_delete_authenticator(Authenticator), + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write) + end + end, + update_chain(ChainID, UpdateFun). + +update_authenticator(ChainID, AuthenticatorName, Config) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + #authenticator{provider = Provider, + config = OriginalConfig, + state = State} = Authenticator -> + NewConfig = maps:merge(OriginalConfig, Config), + case Provider:update(ChainID, AuthenticatorName, NewConfig, State) of + {ok, NState} -> + NAuthenticator = Authenticator#authenticator{config = NewConfig, + state = NState}, + NAuthenticators = update_value(AuthenticatorName, NAuthenticator, Authenticators), + ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), + {ok, serialize_authenticator(NAuthenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + update_chain(ChainID, UpdateFun). + +lookup_authenticator(ChainID, AuthenticatorName) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + Authenticator -> + {ok, serialize_authenticator(Authenticator)} + end + end. + +list_authenticators(ChainID) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + {ok, serialize_authenticators(Authenticators)} + end. + +move_authenticator_to_the_front(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_front_(AuthenticatorName, Authenticators) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +move_authenticator_to_the_end(ChainID, AuthenticatorName) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_end_(AuthenticatorName, Authenticators) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) -> + UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) -> + case move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) of + {ok, NAuthenticators} -> + NChain = Chain#chain{authenticators = NAuthenticators}, + mnesia:write(?CHAIN_TAB, NChain, write); + {error, Reason} -> + {error, Reason} + end + end, + update_chain(ChainID, UpdateFun). + +import_users(ChainID, AuthenticatorName, Filename) -> + call_authenticator(ChainID, AuthenticatorName, import_users, [Filename]). + +add_user(ChainID, AuthenticatorName, UserInfo) -> + call_authenticator(ChainID, AuthenticatorName, add_user, [UserInfo]). + +delete_user(ChainID, AuthenticatorName, UserID) -> + call_authenticator(ChainID, AuthenticatorName, delete_user, [UserID]). + +update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) -> + call_authenticator(ChainID, AuthenticatorName, update_user, [UserID, NewUserInfo]). + +lookup_user(ChainID, AuthenticatorName, UserID) -> + call_authenticator(ChainID, AuthenticatorName, lookup_user, [UserID]). + +list_users(ChainID, AuthenticatorName) -> + call_authenticator(ChainID, AuthenticatorName, list_users, []). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +authenticator_provider(simple, 'built-in-database') -> emqx_authn_mnesia; +authenticator_provider(simple, jwt) -> emqx_authn_jwt; +authenticator_provider(simple, mysql) -> emqx_authn_mysql; +authenticator_provider(simple, postgresql) -> emqx_authn_pgsql. + +% authenticator_provider(enhanced, 'enhanced-built-in-database') -> emqx_enhanced_authn_mnesia. + +do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> + Provider:destroy(State). + +update_value(Key, Value, List) -> + lists:keyreplace(Key, 1, List, {Key, Value}). + +move_authenticator_to_the_front_(AuthenticatorName, Authenticators) -> + move_authenticator_to_the_front_(AuthenticatorName, Authenticators, []). + +move_authenticator_to_the_front_(AuthenticatorName, [], _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_front_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) -> + {ok, [Authenticator | (lists:reverse(Passed) ++ More)]}; +move_authenticator_to_the_front_(AuthenticatorName, [Authenticator | More], Passed) -> + move_authenticator_to_the_front_(AuthenticatorName, More, [Authenticator | Passed]). + +move_authenticator_to_the_end_(AuthenticatorName, Authenticators) -> + move_authenticator_to_the_end_(AuthenticatorName, Authenticators, []). + +move_authenticator_to_the_end_(AuthenticatorName, [], _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_end_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) -> + {ok, lists:reverse(Passed) ++ More ++ [Authenticator]}; +move_authenticator_to_the_end_(AuthenticatorName, [Authenticator | More], Passed) -> + move_authenticator_to_the_end_(AuthenticatorName, More, [Authenticator | Passed]). + +move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) + when N =< length(Authenticators) andalso N > 0 -> + move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N, []); +move_authenticator_to_the_nth_(_, _, _) -> + {error, out_of_range}. + +move_authenticator_to_the_nth_(AuthenticatorName, [], _, _) -> + {error, {not_found, {authenticator, AuthenticatorName}}}; +move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) + when N =< length(Passed) -> + {L1, L2} = lists:split(N - 1, lists:reverse(Passed)), + {ok, L1 ++ [Authenticator] ++ L2 ++ More}; +move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) -> + {L1, L2} = lists:split(N - length(Passed) - 1, More), + {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2}; +move_authenticator_to_the_nth_(AuthenticatorName, [Authenticator | More], N, Passed) -> + move_authenticator_to_the_nth_(AuthenticatorName, More, N, [Authenticator | Passed]). + +update_chain(ChainID, UpdateFun) -> + trans( + fun() -> + case mnesia:read(?CHAIN_TAB, ChainID, write) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [Chain] -> + UpdateFun(Chain) + end + end). + +lookup_chain_by_listener(ListenerID, AuthNType) -> + case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of + [] -> + {error, not_found}; + [#binding{chain_id = ChainID}] -> + {ok, ChainID} + end. + + +call_authenticator(ChainID, AuthenticatorName, Func, Args) -> + case mnesia:dirty_read(?CHAIN_TAB, ChainID) of + [] -> + {error, {not_found, {chain, ChainID}}}; + [#chain{authenticators = Authenticators}] -> + case proplists:get_value(AuthenticatorName, Authenticators, undefined) of + undefined -> + {error, {not_found, {authenticator, AuthenticatorName}}}; + #authenticator{provider = Provider, state = State} -> + case erlang:function_exported(Provider, Func, length(Args) + 1) of + true -> + erlang:apply(Provider, Func, Args ++ [State]); + false -> + {error, unsupported_feature} + end + end + end. + +serialize_chain(#chain{id = ID, + type = Type, + authenticators = Authenticators, + created_at = CreatedAt}) -> + #{id => ID, + type => Type, + authenticators => serialize_authenticators(Authenticators), + created_at => CreatedAt}. + +% serialize_binding(#binding{bound = {ListenerID, _}, +% chain_id = ChainID}) -> +% #{listener_id => ListenerID, +% chain_id => ChainID}. + +serialize_authenticators(Authenticators) -> + [serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators]. + +serialize_authenticator(#authenticator{name = Name, + type = Type, + config = Config}) -> + #{name => Name, + type => Type, + config => Config}. + +trans(Fun) -> + trans(Fun, []). + +trans(Fun, Args) -> + case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of + {atomic, Res} -> Res; + {aborted, Reason} -> {error, Reason} + end. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl new file mode 100644 index 000000000..ad9542958 --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -0,0 +1,544 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_api). + +-include("emqx_authn.hrl"). + +-export([ create_chain/2 + , delete_chain/2 + , lookup_chain/2 + , list_chains/2 + , bind/2 + , unbind/2 + , list_bindings/2 + , list_bound_chains/2 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/2 + , lookup_authenticator/2 + , list_authenticators/2 + , move_authenticator/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 => create_chain, + method => 'POST', + path => "/authentication/chains", + func => create_chain, + descr => "Create a chain" + }). + +-rest_api(#{name => delete_chain, + method => 'DELETE', + path => "/authentication/chains/:bin:id", + func => delete_chain, + descr => "Delete chain" + }). + +-rest_api(#{name => lookup_chain, + method => 'GET', + path => "/authentication/chains/:bin: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 => bind, + method => 'POST', + path => "/authentication/chains/:bin:id/bindings/bulk", + func => bind, + descr => "Bind" + }). + +-rest_api(#{name => unbind, + method => 'DELETE', + path => "/authentication/chains/:bin:id/bindings/bulk", + func => unbind, + descr => "Unbind" + }). + +-rest_api(#{name => list_bindings, + method => 'GET', + path => "/authentication/chains/:bin:id/bindings", + func => list_bindings, + descr => "List bindings" + }). + +-rest_api(#{name => list_bound_chains, + method => 'GET', + path => "/authentication/listeners/:bin:listener_id/bound_chains", + func => list_bound_chains, + descr => "List bound chains" + }). + +-rest_api(#{name => create_authenticator, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators", + func => create_authenticator, + descr => "Create authenticator to chain" + }). + +-rest_api(#{name => delete_authenticator, + method => 'DELETE', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => delete_authenticator, + descr => "Delete authenticator from chain" + }). + +-rest_api(#{name => update_authenticator, + method => 'PUT', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => update_authenticator, + descr => "Update authenticator in chain" + }). + +-rest_api(#{name => lookup_authenticator, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name", + func => lookup_authenticator, + descr => "Lookup authenticator in chain" + }). + +-rest_api(#{name => list_authenticators, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators", + func => list_authenticators, + descr => "List authenticators in chain" + }). + +-rest_api(#{name => move_authenticator, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/position", + func => move_authenticator, + descr => "Change the order of authenticators" + }). + +-rest_api(#{name => import_users, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/import-users", + func => import_users, + descr => "Import users" + }). + +-rest_api(#{name => add_user, + method => 'POST', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users", + func => add_user, + descr => "Add user" + }). + +-rest_api(#{name => delete_user, + method => 'DELETE', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + func => delete_user, + descr => "Delete user" + }). + +-rest_api(#{name => update_user, + method => 'PUT', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id", + func => update_user, + descr => "Update user" + }). + +-rest_api(#{name => lookup_user, + method => 'GET', + path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_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:id/authenticators/:bin:authenticator_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, Chain0) -> + Config = #{<<"authn">> => #{<<"chains">> => [Chain0#{<<"authenticators">> => []}], + <<"bindings">> => []}}, + #{authn := #{chains := [Chain1]}} + = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:create_chain(Chain1) of + {ok, Chain2} -> + return({ok, Chain2}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_chain(Binding, Params) -> + do_delete_chain(uri_decode(Binding), maps:from_list(Params)). + +do_delete_chain(#{id := ChainID}, _Params) -> + case emqx_authn:delete_chain(ChainID) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_chain(Binding, Params) -> + do_lookup_chain(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_chain(#{id := ChainID}, _Params) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, Chain} -> + return({ok, Chain}); + {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_authn:list_chains(), + return({ok, Chains}). + +bind(Binding, Params) -> + do_bind(uri_decode(Binding), lists_to_map(Params)). + +do_bind(#{id := ChainID}, #{<<"listeners">> := Listeners}) -> + % Config = #{<<"authn">> => #{<<"chains">> => [], + % <<"bindings">> => [#{<<"chain">> := ChainID, + % <<"listeners">> := Listeners}]}}, + % #{authn := #{bindings := [#{listeners := Listeners}]}} + % = hocon_schema:check_plain(emqx_authn_schema, Config, + % #{atom_key => true, nullable => true}), + case emqx_authn:bind(ChainID, Listeners) of + ok -> + return(ok); + {error, {alread_bound, Listeners}} -> + {ok, #{code => <<"ALREADY_EXISTS">>, + message => <<"ALREADY_BOUND">>, + detail => Listeners}}; + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_bind(_, _) -> + return(serialize_error({missing_parameter, <<"listeners">>})). + +unbind(Binding, Params) -> + do_unbind(uri_decode(Binding), lists_to_map(Params)). + +do_unbind(#{id := ChainID}, #{<<"listeners">> := Listeners0}) -> + case emqx_authn:unbind(ChainID, Listeners0) of + ok -> + return(ok); + {error, {not_found, Listeners1}} -> + {ok, #{code => <<"NOT_FOUND">>, + detail => Listeners1}}; + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_unbind(_, _) -> + return(serialize_error({missing_parameter, <<"listeners">>})). + +list_bindings(Binding, Params) -> + do_list_bindings(uri_decode(Binding), lists_to_map(Params)). + +do_list_bindings(#{id := ChainID}, _) -> + {ok, Binding} = emqx_authn:list_bindings(ChainID), + return({ok, Binding}). + +list_bound_chains(Binding, Params) -> + do_list_bound_chains(uri_decode(Binding), lists_to_map(Params)). + +do_list_bound_chains(#{listener_id := ListenerID}, _) -> + {ok, Chains} = emqx_authn:list_bound_chains(ListenerID), + return({ok, Chains}). + +create_authenticator(Binding, Params) -> + do_create_authenticator(uri_decode(Binding), lists_to_map(Params)). + +do_create_authenticator(#{id := ChainID}, Authenticator0) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, #{type := Type}} -> + Chain = #{<<"id">> => ChainID, + <<"type">> => Type, + <<"authenticators">> => [Authenticator0]}, + Config = #{<<"authn">> => #{<<"chains">> => [Chain], + <<"bindings">> => []}}, + #{authn := #{chains := [#{authenticators := [Authenticator1]}]}} + = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:create_authenticator(ChainID, Authenticator1) of + {ok, Authenticator2} -> + return({ok, Authenticator2}); + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end. + +delete_authenticator(Binding, Params) -> + do_delete_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_delete_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:delete_authenticator(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +%% TODO: Support incremental update +update_authenticator(Binding, Params) -> + do_update_authenticator(uri_decode(Binding), lists_to_map(Params)). + +%% TOOD: PUT method supports creation and update +do_update_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, AuthenticatorConfig0) -> + case emqx_authn:lookup_chain(ChainID) of + {ok, #{type := ChainType}} -> + case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of + {ok, #{type := Type}} -> + Authenticator = #{<<"name">> => AuthenticatorName, + <<"type">> => Type, + <<"config">> => AuthenticatorConfig0}, + Chain = #{<<"id">> => ChainID, + <<"type">> => ChainType, + <<"authenticators">> => [Authenticator]}, + Config = #{<<"authn">> => #{<<"chains">> => [Chain], + <<"bindings">> => []}}, + #{ + authn := #{ + chains := [#{ + authenticators := [#{ + config := AuthenticatorConfig1 + }] + }] + } + } = hocon_schema:check_plain(emqx_authn_schema, Config, + #{atom_key => true, nullable => true}), + case emqx_authn:update_authenticator(ChainID, AuthenticatorName, AuthenticatorConfig1) of + {ok, NAuthenticator} -> + return({ok, NAuthenticator}); + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end; + {error, Reason} -> + return(serialize_error(Reason)) + end. + +lookup_authenticator(Binding, Params) -> + do_lookup_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_lookup_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of + {ok, Authenticator} -> + return({ok, Authenticator}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +list_authenticators(Binding, Params) -> + do_list_authenticators(uri_decode(Binding), maps:from_list(Params)). + +do_list_authenticators(#{id := ChainID}, _Params) -> + case emqx_authn:list_authenticators(ChainID) of + {ok, Authenticators} -> + return({ok, Authenticators}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +move_authenticator(Binding, Params) -> + do_move_authenticator(uri_decode(Binding), maps:from_list(Params)). + +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the front">>}) -> + case emqx_authn:move_authenticator_to_the_front(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the end">>}) -> + case emqx_authn:move_authenticator_to_the_end(ChainID, AuthenticatorName) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(#{id := ChainID, + authenticator_name := AuthenticatorName}, #{<<"position">> := N}) when is_number(N) -> + case emqx_authn:move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +do_move_authenticator(_Binding, _Params) -> + return(serialize_error({missing_parameter, <<"position">>})). + +import_users(Binding, Params) -> + do_import_users(uri_decode(Binding), maps:from_list(Params)). + +do_import_users(#{id := ChainID, authenticator_name := AuthenticatorName}, + #{<<"filename">> := Filename}) -> + case emqx_authn:import_users(ChainID, AuthenticatorName, Filename) of + ok -> + return(ok); + {error, Reason} -> + return(serialize_error(Reason)) + end; +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(#{id := ChainID, + authenticator_name := AuthenticatorName}, UserInfo) -> + case emqx_authn:add_user(ChainID, AuthenticatorName, 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(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, _Params) -> + case emqx_authn:delete_user(ChainID, AuthenticatorName, 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(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, NewUserInfo) -> + case emqx_authn:update_user(ChainID, AuthenticatorName, 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(#{id := ChainID, + authenticator_name := AuthenticatorName, + user_id := UserID}, _Params) -> + case emqx_authn:lookup_user(ChainID, AuthenticatorName, 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(#{id := ChainID, + authenticator_name := AuthenticatorName}, _Params) -> + case emqx_authn:list_users(ChainID, AuthenticatorName) of + {ok, Users} -> + return({ok, Users}); + {error, Reason} -> + return(serialize_error(Reason)) + end. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +uri_decode(Params) -> + maps:fold(fun(K, V, Acc) -> + Acc#{K => emqx_http_lib:uri_decode(V)} + end, #{}, Params). + +lists_to_map(L) -> + lists_to_map(L, #{}). + +lists_to_map([], Acc) -> + Acc; +lists_to_map([{K, V} | More], Acc) when is_list(V) -> + NV = lists_to_map(V), + lists_to_map(More, Acc#{K => NV}); +lists_to_map([{K, V} | More], Acc) -> + lists_to_map(More, Acc#{K => V}); +lists_to_map([_ | _] = L, _) -> + L. + +serialize_error({already_exists, {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("~s '~s' not found", [serialize_type(Type), ID]))}; +serialize_error({duplicate, Name}) -> + {error, <<"INVALID_PARAMETER">>, list_to_binary(io_lib:format("Authenticator name '~s' is duplicated", [Name]))}; +serialize_error({missing_parameter, Names = [_ | Rest]}) -> + 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 '~s' that is mandatory for processing this request is not supplied.", [Name]))}; +serialize_error(_) -> + {error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}. + +serialize_type(authenticator) -> + "Authenticator"; +serialize_type(chain) -> + "Chain"; +serialize_type(authenticator_type) -> + "Authenticator type". + +get_missed_params(Actual, Expected) -> + 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_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl new file mode 100644 index 000000000..9e60e5dda --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -0,0 +1,80 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_app). + +-include("emqx_authn.hrl"). +-include_lib("emqx/include/logger.hrl"). + +-behaviour(application). + +-emqx_plugin(?MODULE). + +%% Application callbacks +-export([ start/2 + , stop/1 + ]). + +start(_StartType, _StartArgs) -> + {ok, Sup} = emqx_authn_sup:start_link(), + ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), + initialize(), + {ok, Sup}. + +stop(_State) -> + ok. + +initialize() -> + ConfFile = filename:join([emqx:get_env(plugins_etc_dir), ?APP]) ++ ".conf", + {ok, RawConfig} = hocon:load(ConfFile), + #{authn := #{chains := Chains, + bindings := Bindings}} + = hocon_schema:check_plain(emqx_authn_schema, RawConfig, #{atom_key => true, nullable => true}), + initialize_chains(Chains), + initialize_bindings(Bindings). + +initialize_chains([]) -> + ok; +initialize_chains([#{id := ChainID, + type := Type, + authenticators := Authenticators} | More]) -> + case emqx_authn:create_chain(#{id => ChainID, + type => Type}) of + {ok, _} -> + initialize_authenticators(ChainID, Authenticators), + initialize_chains(More); + {error, Reason} -> + ?LOG(error, "Failed to create chain '~s': ~p", [ChainID, Reason]) + end. + +initialize_authenticators(_ChainID, []) -> + ok; +initialize_authenticators(ChainID, [#{name := Name} = Authenticator | More]) -> + case emqx_authn:create_authenticator(ChainID, Authenticator) of + {ok, _} -> + initialize_authenticators(ChainID, More); + {error, Reason} -> + ?LOG(error, "Failed to create authenticator '~s' in chain '~s': ~p", [Name, ChainID, Reason]) + end. + +initialize_bindings([]) -> + ok; +initialize_bindings([#{chain_id := ChainID, listeners := Listeners} | More]) -> + case emqx_authn:bind(Listeners, ChainID) of + ok -> initialize_bindings(More); + {error, Reason} -> + ?LOG(error, "Failed to bind: ~p", [Reason]) + end. diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl new file mode 100644 index 000000000..0464350dd --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -0,0 +1,114 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_schema). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([structs/0, fields/1]). + +-reflect_type([ chain_id/0 + , authenticator_name/0 + ]). + +structs() -> [authn]. + +fields(authn) -> + [ {chains, fun chains/1} + , {bindings, fun bindings/1}]; + +fields('simple-chain') -> + [ {id, fun chain_id/1} + , {type, {enum, [simple]}} + , {authenticators, fun simple_authenticators/1} + ]; + +% fields('enhanced-chain') -> +% [ {id, fun chain_id/1} +% , {type, {enum, [enhanced]}} +% , {authenticators, fun enhanced_authenticators/1} +% ]; + +fields(binding) -> + [ {chain_id, fun chain_id/1} + , {listeners, fun listeners/1} + ]; + +fields('built-in-database') -> + [ {name, fun authenticator_name/1} + , {type, {enum, ['built-in-database']}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_mnesia, config))} + ]; + +% fields('enhanced-built-in-database') -> +% [ {name, fun authenticator_name/1} +% , {type, {enum, ['built-in-database']}} +% , {config, hoconsc:t(hoconsc:ref(emqx_enhanced_authn_mnesia, config))} +% ]; + +fields(jwt) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [jwt]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_jwt, config))} + ]; + +fields(mysql) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [mysql]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_mysql, config))} + ]; + +fields(pgsql) -> + [ {name, fun authenticator_name/1} + , {type, {enum, [postgresql]}} + , {config, hoconsc:t(hoconsc:ref(emqx_authn_pgsql, config))} + ]. + +chains(type) -> hoconsc:array({union, [hoconsc:ref('simple-chain')]}); +chains(default) -> []; +chains(_) -> undefined. + +chain_id(type) -> chain_id(); +chain_id(nullable) -> false; +chain_id(_) -> undefined. + +simple_authenticators(type) -> + hoconsc:array({union, [ hoconsc:ref('built-in-database') + , hoconsc:ref(jwt) + , hoconsc:ref(mysql) + , hoconsc:ref(pgsql)]}); +simple_authenticators(default) -> []; +simple_authenticators(_) -> undefined. + +% enhanced_authenticators(type) -> +% hoconsc:array({union, [hoconsc:ref('enhanced-built-in-database')]}); +% enhanced_authenticators(default) -> []; +% enhanced_authenticators(_) -> undefined. + +authenticator_name(type) -> authenticator_name(); +authenticator_name(nullable) -> false; +authenticator_name(_) -> undefined. + +bindings(type) -> hoconsc:array(hoconsc:ref(binding)); +bindings(default) -> []; +bindings(_) -> undefined. + +listeners(type) -> hoconsc:array(binary()); +listeners(default) -> []; +listeners(_) -> undefined. diff --git a/apps/emqx_authentication/src/emqx_authentication_sup.erl b/apps/emqx_authn/src/emqx_authn_sup.erl similarity index 96% rename from apps/emqx_authentication/src/emqx_authentication_sup.erl rename to apps/emqx_authn/src/emqx_authn_sup.erl index 06e12ce6c..bb26af0ad 100644 --- a/apps/emqx_authentication/src/emqx_authentication_sup.erl +++ b/apps/emqx_authn/src/emqx_authn_sup.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_sup). +-module(emqx_authn_sup). -behaviour(supervisor). diff --git a/apps/emqx_authn/src/emqx_authn_utils.erl b/apps/emqx_authn/src/emqx_authn_utils.erl new file mode 100644 index 000000000..98e27e76c --- /dev/null +++ b/apps/emqx_authn/src/emqx_authn_utils.erl @@ -0,0 +1,55 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_utils). + +-export([ replace_placeholder/2 + ]). + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +replace_placeholder(PlaceHolders, Data) -> + replace_placeholder(PlaceHolders, Data, []). + +replace_placeholder([], _Data, Acc) -> + lists:reverse(Acc); +replace_placeholder([<<"${mqtt-username}">> | More], #{username := Username} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(Username) | Acc]); +replace_placeholder([<<"${mqtt-clientid}">> | More], #{clientid := ClientID} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(ClientID) | Acc]); +replace_placeholder([<<"${ip-address}">> | More], #{peerhost := IPAddress} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(IPAddress) | Acc]); +replace_placeholder([<<"${cert-subject}">> | More], #{dn := Subject} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(Subject) | Acc]); +replace_placeholder([<<"${cert-common-name}">> | More], #{cn := CommonName} = Data, Acc) -> + replace_placeholder(More, Data, [convert_to_sql_param(CommonName) | Acc]); +replace_placeholder([_ | More], Data, Acc) -> + replace_placeholder(More, Data, [null | Acc]). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +convert_to_sql_param(undefined) -> + null; +convert_to_sql_param(V) -> + bin(V). + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> list_to_binary(L); +bin(X) -> X. diff --git a/apps/emqx_authentication/src/emqx_authentication_app.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl similarity index 63% rename from apps/emqx_authentication/src/emqx_authentication_app.erl rename to apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl index 3bea3c3d6..207e93495 100644 --- a/apps/emqx_authentication/src/emqx_authentication_app.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_mnesia.erl @@ -14,24 +14,4 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_app). - --behaviour(application). - --emqx_plugin(?MODULE). - --include("emqx_authentication.hrl"). - -%% Application callbacks --export([ start/2 - , stop/1 - ]). - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_authentication_sup:start_link(), - ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), - ok = emqx_authentication:register_service_types(), - {ok, Sup}. - -stop(_State) -> - ok. +-module(emqx_enhanced_authn_mnesia). diff --git a/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl similarity index 87% rename from apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl rename to apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl index 9dafc9f5e..95e4b3d6d 100644 --- a/apps/emqx_authentication/src/emqx_authentication_jwks_connector.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwks_connector.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_jwks_connector). +-module(emqx_authn_jwks_connector). -behaviour(gen_server). @@ -125,23 +125,17 @@ code_change(_OldVsn, State, _Extra) -> %% 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), +handle_options(#{endpoint := Endpoint, + refresh_interval := RefreshInterval0, + ssl_opts := SSLOpts}) -> + #{endpoint => Endpoint, + refresh_interval => limit_refresh_interval(RefreshInterval0), + ssl_opts => maps:to_list(SSLOpts), jwks => [], - request_id => undefined}. + 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. +handle_options(#{enable_ssl := false} = Opts) -> + handle_options(Opts#{ssl_opts => #{}}). refresh_jwks(#{endpoint := Endpoint, ssl_opts := SSLOpts} = State) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl new file mode 100644 index 000000000..f737d5168 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -0,0 +1,343 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_jwt). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1 + , validations/0 + ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields("") -> + [{config, {union, [ hoconsc:t('hmac-based') + , hoconsc:t('public-key') + , hoconsc:t('jwks') + , hoconsc:t('jwks-using-ssl') + ]}}]; + +fields(config) -> + [{union, [ hoconsc:t('hmac-based') + , hoconsc:t('public-key') + , hoconsc:t('jwks') + , hoconsc:t('jwks-using-ssl') + ]}]; + +fields('hmac-based') -> + [ {use_jwks, {enum, [false]}} + , {algorithm, {enum, ['hmac-based']}} + , {secret, fun secret/1} + , {secret_base64_encoded, fun secret_base64_encoded/1} + , {verify_claims, fun verify_claims/1} + ]; + +fields('public-key') -> + [ {use_jwks, {enum, [false]}} + , {algorithm, {enum, ['public-key']}} + , {certificate, fun certificate/1} + , {verify_claims, fun verify_claims/1} + ]; + +fields('jwks') -> + [ {enable_ssl, {enum, [false]}} + ] ++ jwks_fields(); + +fields('jwks-using-ssl') -> + [ {enable_ssl, {enum, [true]}} + , {ssl_opts, fun ssl_opts/1} + ] ++ jwks_fields(); + +fields(ssl_opts) -> + [ {cacertfile, fun cacertfile/1} + , {certfile, fun certfile/1} + , {keyfile, fun keyfile/1} + , {verify, fun verify/1} + , {server_name_indication, fun server_name_indication/1} + ]; + +fields(claim) -> + [ {"$name", fun expected_claim_value/1} ]. + +validations() -> + [ {check_verify_claims, fun check_verify_claims/1} ]. + +jwks_fields() -> + [ {use_jwks, {enum, [true]}} + , {endpoint, fun endpoint/1} + , {refresh_interval, fun refresh_interval/1} + , {verify_claims, fun verify_claims/1} + ]. + +secret(type) -> string(); +secret(_) -> undefined. + +secret_base64_encoded(type) -> boolean(); +secret_base64_encoded(defualt) -> false; +secret_base64_encoded(_) -> undefined. + +certificate(type) -> string(); +certificate(_) -> undefined. + +endpoint(type) -> string(); +endpoint(_) -> undefined. + +ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts)); +ssl_opts(default) -> []; +ssl_opts(_) -> undefined. + +refresh_interval(type) -> integer(); +refresh_interval(default) -> 300; +refresh_interval(validator) -> [fun(I) -> I > 0 end]; +refresh_interval(_) -> undefined. + +cacertfile(type) -> string(); +cacertfile(_) -> undefined. + +certfile(type) -> string(); +certfile(_) -> undefined. + +keyfile(type) -> string(); +keyfile(_) -> undefined. + +verify(type) -> boolean(); +verify(default) -> false; +verify(_) -> undefined. + +server_name_indication(type) -> string(); +server_name_indication(_) -> undefined. + +verify_claims(type) -> hoconsc:array(hoconsc:ref(claim)); +verify_claims(default) -> []; +verify_claims(_) -> undefined. + +expected_claim_value(type) -> string(); +expected_claim_value(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(_ChainID, _AuthenticatorName, Config) -> + create(Config). + +update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, #{jwk := Connector}) + when is_pid(Connector) -> + _ = emqx_authn_jwks_connector:stop(Connector), + create(Config); + +update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, _) -> + create(Config); + +update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Connector} = State) + when is_pid(Connector) -> + ok = emqx_authn_jwks_connector:update(Connector, Config), + case maps:get(verify_cliams, Config, undefined) of + undefined -> + {ok, State}; + VerifyClaims -> + {ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}} + end; + +update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) -> + create(Config). + +authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK, + verify_claims := VerifyClaims0}) -> + JWKs = case erlang:is_pid(JWK) of + false -> + [JWK]; + true -> + {ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK), + JWKs0 + end, + VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo), + case verify(JWT, JWKs, VerifyClaims) of + ok -> ok; + {error, invalid_signature} -> ignore; + {error, {claims, _}} -> {stop, bad_password} + end. + +destroy(#{jwk := Connector}) when is_pid(Connector) -> + _ = emqx_authn_jwks_connector:stop(Connector), + ok; +destroy(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +create(#{verify_claims := VerifyClaims} = Config) -> + create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}). + +create2(#{use_jwks := false, + algorithm := 'hmac-based', + 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, + verify_claims => VerifyClaims}}; + +create2(#{use_jwks := false, + algorithm := 'public-key', + certificate := Certificate, + verify_claims := VerifyClaims}) -> + JWK = jose_jwk:from_pem_file(Certificate), + {ok, #{jwk => JWK, + verify_claims => VerifyClaims}}; + +create2(#{use_jwks := true, + verify_claims := VerifyClaims} = Config) -> + case emqx_authn_jwks_connector:start_link(Config) of + {ok, Connector} -> + {ok, #{jwk => Connector, + verify_claims => VerifyClaims}}; + {error, Reason} -> + {error, Reason} + end. + +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) -> + try 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) + catch + _:_Reason:_Stacktrace -> + %% TODO: Add log + {error, invalid_signature} + 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 -> + {error, {missing_claim, Name}}; + {Value, NClaims} -> + do_verify_claims(NClaims, More); + {Value0, _} -> + {error, {claims, {Name, Value0}}} + end. + +check_verify_claims([]) -> + false; +check_verify_claims([{Name, Expected} | More]) -> + check_claim_name(Name) andalso + check_claim_expected(Expected) andalso + check_verify_claims(More). + +check_claim_name(exp) -> + false; +check_claim_name(iat) -> + false; +check_claim_name(nbf) -> + false; +check_claim_name(_) -> + true. + +check_claim_expected(Expected) -> + try handle_placeholder(Expected) of + _ -> true + catch + _:_ -> + false + end. + +handle_verify_claims(VerifyClaims) -> + handle_verify_claims(VerifyClaims, []). + +handle_verify_claims([], Acc) -> + Acc; +handle_verify_claims([{Name, Expected0} | More], Acc) -> + 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(<<"mqtt-clientid">>) -> + clientid; +validate_placeholder(<<"mqtt-username">>) -> + username. diff --git a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl similarity index 80% rename from apps/emqx_authentication/src/emqx_authentication_mnesia.erl rename to apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 09307e38f..26b20c517 100644 --- a/apps/emqx_authentication/src/emqx_authentication_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -14,9 +14,14 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authentication_mnesia). +-module(emqx_authn_mnesia). --include("emqx_authentication.hrl"). +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). -export([ create/3 , update/4 @@ -32,29 +37,10 @@ , list_users/1 ]). -%% TODO: support bcrypt --service_type(#{ - name => mnesia, - params_spec => #{ - user_id_type => #{ - order => 1, - type => string, - enum => [<<"username">>, <<"clientid">>, <<"ip">>, <<"common name">>, <<"issuer">>], - default => <<"username">> - }, - password_hash_algorithm => #{ - order => 2, - type => string, - enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], - default => <<"sha256">> - }, - salt_rounds => #{ - order => 3, - type => number, - default => 10 - } - } -}). +-type user_id_type() :: clientid | username. + +-type user_group() :: {chain_id(), authenticator_name()}. +-type user_id() :: binary(). -record(user_info, { user_id :: {user_group(), user_id()} @@ -62,8 +48,7 @@ , salt :: binary() }). --type(user_group() :: {chain_id(), service_name()}). --type(user_id() :: binary()). +-reflect_type([ user_id_type/0 ]). -export([mnesia/1]). @@ -73,7 +58,6 @@ -define(TAB, mnesia_basic_auth). -rlog_shard({?AUTH_SHARD, ?TAB}). - %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -90,18 +74,61 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB, disc_copies). -create(ChainID, ServiceName, #{<<"user_id_type">> := Type, - <<"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), +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +structs() -> [config]. + +fields(config) -> + [ {user_id_type, fun user_id_type/1} + , {password_hash_algorithm, fun password_hash_algorithm/1} + ]; + +fields(bcrypt) -> + [ {name, {enum, [bcrypt]}} + , {salt_rounds, fun salt_rounds/1} + ]; + +fields(other_algorithms) -> + [ {name, {enum, [plain, md5, sha, sha256, sha512]}} + ]. + +user_id_type(type) -> user_id_type(); +user_id_type(default) -> clientid; +user_id_type(_) -> undefined. + +password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; +password_hash_algorithm(default) -> sha256; +password_hash_algorithm(_) -> undefined. + +salt_rounds(type) -> integer(); +salt_rounds(default) -> 10; +salt_rounds(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, AuthenticatorName, #{user_id_type := Type, + password_hash_algorithm := #{name := bcrypt, + salt_rounds := SaltRounds}}) -> + {ok, _} = application:ensure_all_started(bcrypt), + State = #{user_group => {ChainID, AuthenticatorName}, + user_id_type => Type, + password_hash_algorithm => bcrypt, salt_rounds => SaltRounds}, + {ok, State}; + +create(ChainID, AuthenticatorName, #{user_id_type := Type, + password_hash_algorithm := #{name := Name}}) -> + State = #{user_group => {ChainID, AuthenticatorName}, + user_id_type => Type, + password_hash_algorithm => Name}, {ok, State}. -update(ChainID, ServiceName, Params, _State) -> - create(ChainID, ServiceName, Params). +update(ChainID, AuthenticatorName, Config, _State) -> + create(ChainID, AuthenticatorName, Config). authenticate(ClientInfo = #{password := Password}, #{user_group := UserGroup, @@ -111,7 +138,11 @@ authenticate(ClientInfo = #{password := Password}, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0}] -> + Salt = case Algorithm of + bcrypt -> PasswordHash; + _ -> Salt0 + end, case PasswordHash =:= hash(Algorithm, Password, Salt) of true -> ok; false -> {stop, bad_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl new file mode 100644 index 000000000..4fddcc3f7 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -0,0 +1,160 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_mysql). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +%% Config: +%% host +%% port +%% username +%% password +%% database +%% pool_size +%% connect_timeout +%% enable_ssl +%% ssl_opts +%% cacertfile +%% certfile +%% keyfile +%% verify +%% servce_name_indication +%% tls_versions +%% ciphers +%% password_hash_algorithm +%% salt_position +%% query +%% query_timeout +structs() -> [config]. + +fields(config) -> + [ {password_hash_algorithm, fun password_hash_algorithm/1} + , {salt_position, {enum, [prefix, suffix]}} + , {query, fun query/1} + , {query_timeout, fun query_timeout/1} + ]. + +password_hash_algorithm(type) -> string(); +password_hash_algorithm(_) -> undefined. + +query(type) -> string(); +query(nullable) -> false; +query(_) -> undefined. + +query_timeout(type) -> integer(); +query_timeout(defualt) -> 5000; +query_timeout(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, ServiceName, #{query := Query0, + password_hash_algorithm := Algorithm} = Config) -> + {Query, PlaceHolders} = parse_query(Query0), + ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])), + State = #{query => Query, + placeholders => PlaceHolders, + password_hash_algorithm => Algorithm}, + case emqx_resource:create_local(ResourceID, emqx_connector_mysql, Config) of + {ok, _} -> + {ok, State#{resource_id => ResourceID}}; + {error, already_created} -> + {ok, State#{resource_id => ResourceID}}; + {error, Reason} -> + {error, Reason} + end. + +update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> + case emqx_resource:update_local(ResourceID, emqx_connector_mysql, Config, []) of + {ok, _} -> {ok, State}; + {error, Reason} -> {error, Reason} + end. + +authenticate(#{password := Password} = ClientInfo, + #{resource_id := ResourceID, + placeholders := PlaceHolders, + query := Query, + query_timeout := Timeout} = State) -> + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + case emqx_resource:query(ResourceID, {sql, Query, Params, Timeout}) of + {ok, _Columns, []} -> ignore; + {ok, Columns, Rows} -> + %% TODO: Support superuser + Selected = maps:from_list(lists:zip(Columns, Rows)), + check_password(Password, Selected, State); + {error, _Reason} -> + ignore + end. + +destroy(#{resource_id := ResourceID}) -> + _ = emqx_resource:remove_local(ResourceID), + ok. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +check_password(undefined, _Algorithm, _Selected) -> + {stop, bad_password}; +check_password(Password, + #{password_hash := Hash}, + #{password_hash_algorithm := bcrypt}) -> + {ok, Hash0} = bcrypt:hashpw(Password, Hash), + case list_to_binary(Hash0) =:= Hash of + true -> ok; + false -> {stop, bad_password} + end; +check_password(Password, + #{password_hash := Hash} = Selected, + #{password_hash_algorithm := Algorithm, + salt_position := SaltPosition}) -> + Salt = maps:get(salt, Selected, <<>>), + Hash0 = case SaltPosition of + prefix -> emqx_passwd:hash(Algorithm, <>); + suffix -> emqx_passwd:hash(Algorithm, <>) + end, + case Hash0 =:= Hash of + true -> ok; + false -> {stop, bad_password} + end. + +%% TODO: Support prepare +parse_query(Query) -> + case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of + {match, Captured} -> + PlaceHolders = [PlaceHolder || PlaceHolder <- Captured], + NQuery = re:replace(Query, "'\\$\\{[a-z0-9\\_]+\\}'", "?", [global, {return, binary}]), + {NQuery, PlaceHolders}; + nomatch -> + {Query, []} + end. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl new file mode 100644 index 000000000..8c250e216 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -0,0 +1,155 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_pgsql). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0, fields/1 ]). + +-export([ create/3 + , update/4 + , authenticate/2 + , destroy/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +%% Config: +%% host +%% port +%% username +%% password +%% database +%% pool_size +%% connect_timeout +%% enable_ssl +%% cacertfile +%% certfile +%% keyfile +%% verify +%% servce_name_indication +%% tls_versions +%% ciphers +%% password_hash_algorithm +%% salt_position +%% query +structs() -> [config]. + +fields(config) -> + [ {password_hash_algorithm, fun password_hash_algorithm/1} + , {salt_position, {enum, [prefix, suffix]}} + , {query, fun query/1} + ]. + +password_hash_algorithm(type) -> string(); +password_hash_algorithm(_) -> undefined. + +query(type) -> string(); +query(nullable) -> false; +query(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(ChainID, ServiceName, #{query := Query0, + password_hash_algorithm := Algorithm} = Config) -> + {Query, PlaceHolders} = parse_query(Query0), + ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])), + State = #{query => Query, + placeholders => PlaceHolders, + password_hash_algorithm => Algorithm}, + case emqx_resource:create_local(ResourceID, emqx_connector_pgsql, Config) of + {ok, _} -> + {ok, State#{resource_id => ResourceID}}; + {error, already_created} -> + {ok, State#{resource_id => ResourceID}}; + {error, Reason} -> + {error, Reason} + end. + +update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) -> + case emqx_resource:update_local(ResourceID, emqx_connector_pgsql, Config, []) of + {ok, _} -> {ok, State}; + {error, Reason} -> {error, Reason} + end. + +authenticate(#{password := Password} = ClientInfo, + #{resource_id := ResourceID, + query := Query, + placeholders := PlaceHolders} = State) -> + Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo), + case emqx_resource:query(ResourceID, {sql, Query, Params}) of + {ok, _Columns, []} -> ignore; + {ok, Columns, Rows} -> + %% TODO: Support superuser + Selected = maps:from_list(lists:zip(Columns, Rows)), + check_password(Password, Selected, State); + {error, _Reason} -> + ignore + end. + +destroy(#{resource_id := ResourceID}) -> + _ = emqx_resource:remove_local(ResourceID), + ok. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +check_password(undefined, _Algorithm, _Selected) -> + {stop, bad_password}; +check_password(Password, + #{password_hash := Hash}, + #{password_hash_algorithm := bcrypt}) -> + {ok, Hash0} = bcrypt:hashpw(Password, Hash), + case list_to_binary(Hash0) =:= Hash of + true -> ok; + false -> {stop, bad_password} + end; +check_password(Password, + #{password_hash := Hash} = Selected, + #{password_hash_algorithm := Algorithm, + salt_position := SaltPosition}) -> + Salt = maps:get(salt, Selected, <<>>), + Hash0 = case SaltPosition of + prefix -> emqx_passwd:hash(Algorithm, <>); + suffix -> emqx_passwd:hash(Algorithm, <>) + end, + case Hash0 =:= Hash of + true -> ok; + false -> {stop, bad_password} + end. + +%% TODO: Support prepare +parse_query(Query) -> + case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of + {match, Captured} -> + PlaceHolders = [PlaceHolder || PlaceHolder <- Captured], + Replacements = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Captured))], + NQuery = lists:foldl(fun({PlaceHolder, Replacement}, Query0) -> + re:replace(Query0, <<"'\\", PlaceHolder/binary, "'">>, Replacement, [{return, binary}]) + end, Query, lists:zip(PlaceHolders, Replacements)), + {NQuery, PlaceHolders}; + nomatch -> + {Query, []} + end. diff --git a/apps/emqx_authn/test/data/private_key.pem b/apps/emqx_authn/test/data/private_key.pem new file mode 100644 index 000000000..318eefe27 --- /dev/null +++ b/apps/emqx_authn/test/data/private_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZNF0IeaTmJcBooxsY6Ze8PGFS +QJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVDeYsu15Htxpqig1L/+MphbZlF +ClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8GNp+/j2yir1Z9E2xAgMBAAEC +gYBX7HsLncMWexux6nddbl0nWwyhyPZcvgvT4TjHTPAfhNdOtfQyZCUdbv5+mqip +j6O8BE7ar2TMz5FgvVrF+O97LkYHNmZk0q3xtZlCYXp4BQqD6Wq65H5U4fAomalK +xm7HsTCSVXx5CvnZK/JbkPw18QsgwrSHEFs+4Pf2noH+FQJBAL/bpPrkDOB476Iy +RGnuCckUN1pdCU+UINC8oOWGNwsG6EE5ywlIWRXHtp4VMksG6mCLNJwGUAv2zWIs +iEjZVfsCQQCVxOciTajTtYO5bPjkXoZoe4VKKXWMYv9AXXVCjq0ff/LjrnKJjbRm +aoKQGhzjKHk5rgd9+Ydl6FnJw5K4B9dDAkEAtaHfQpZ7ildzpf4ovpBYO0EkViwW +EHyPxI2PVTwHCC1126o3CYawr+nufSJcBqN5aAThvYRMa8cvEW5PZ4g52QJALF5L +tt7Yz/crEciVp1nVaaiGISVNHIzLX28QaOpJoVZPR2ILrnJbaifNjBEgU69O0maa +85fzo54E03/rvDcebwJAfTMgIyzFQK/ESnM43bUCI/Y5XAeKFBiN1YhCioNR4Hj7 +Lkw2RdrrPC9LV+gVJK0b7VUqR5odjdj7PN6SipuXNw== +-----END RSA PRIVATE KEY----- diff --git a/apps/emqx_authn/test/data/public_key.pem b/apps/emqx_authn/test/data/public_key.pem new file mode 100644 index 000000000..b0b151981 --- /dev/null +++ b/apps/emqx_authn/test/data/public_key.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZ +NF0IeaTmJcBooxsY6Ze8PGFSQJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVD +eYsu15Htxpqig1L/+MphbZlFClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8 +GNp+/j2yir1Z9E2xAgMBAAE= +-----END PUBLIC KEY----- diff --git a/apps/emqx_authentication/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv similarity index 100% rename from apps/emqx_authentication/test/data/user-credentials.csv rename to apps/emqx_authn/test/data/user-credentials.csv diff --git a/apps/emqx_authentication/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json similarity index 100% rename from apps/emqx_authentication/test/data/user-credentials.json rename to apps/emqx_authn/test/data/user-credentials.json diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl new file mode 100644 index 000000000..17c08cc70 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -0,0 +1,142 @@ +%%-------------------------------------------------------------------- +%% 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_authn_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + application:set_env(ekka, strict_mode, true), + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_chain(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)), + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), + ok. + +t_binding(_) -> + Listener1 = <<"listener1">>, + Listener2 = <<"listener2">>, + ChainID = <<"mychain">>, + + ?assertEqual({error, {not_found, {chain, ChainID}}}, ?AUTH:bind(ChainID, [Listener1])), + + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1])), + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener2])), + ?assertEqual({error, {already_bound, [Listener1]}}, ?AUTH:bind(ChainID, [Listener1])), + {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID), + ?assertEqual(2, length(Listeners)), + ?assertMatch({ok, #{simple := ChainID}}, ?AUTH:list_bound_chains(Listener1)), + + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1])), + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener2])), + ?assertEqual({error, {not_found, [Listener1]}}, ?AUTH:unbind(ChainID, [Listener1])), + + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_binding2(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + Listener1 = <<"listener1">>, + Listener2 = <<"listener2">>, + + ?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1, Listener2])), + {ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID), + ?assertEqual(2, length(Listeners)), + ?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1, Listener2])), + ?assertMatch({ok, #{listeners := []}}, ?AUTH:list_bindings(ChainID)), + + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), + + AuthenticatorName1 = <<"myauthenticator1">>, + AuthenticatorConfig1 = #{name => AuthenticatorName1, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName1)), + ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + + AuthenticatorName2 = <<"myauthenticator2">>, + AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2}, + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)), + ?assertMatch({ok, #{id := ChainID, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(ChainID)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)), + + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(ChainID, AuthenticatorName2)), + ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 1)), + ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 0)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName2)), + ?assertEqual({ok, []}, ?AUTH:list_authenticators(ChainID)), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl new file mode 100644 index 000000000..27f34f936 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -0,0 +1,182 @@ +%%-------------------------------------------------------------------- +%% 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_authn_jwt_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_jwt_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + Config = #{use_jwks => false, + algorithm => 'hmac-based', + secret => <<"abcdef">>, + secret_base64_encoded => false, + verify_claims => []}, + AuthenticatorConfig = #{name => AuthenticatorName, + type => jwt, + config => Config}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + Payload = #{<<"username">> => <<"myuser">>}, + JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => JWS}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + + BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), + ClientInfo2 = ClientInfo#{password => BadJWS}, + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + + %% secret_base64_encoded + Config2 = Config#{secret => base64:encode(<<"abcdef">>), + secret_base64_encoded => true}, + ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config2)), + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + + Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, + ?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config3)), + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>})), + + %% Expiration + Payload3 = #{ <<"username">> => <<"myuser">> + , <<"exp">> => erlang:system_time(second) - 60}, + JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), + ClientInfo3 = ClientInfo#{password => JWS3}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + + Payload4 = #{ <<"username">> => <<"myuser">> + , <<"exp">> => erlang:system_time(second) + 60}, + JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), + ClientInfo4 = ClientInfo#{password => JWS4}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + + %% Issued At + Payload5 = #{ <<"username">> => <<"myuser">> + , <<"iat">> => erlang:system_time(second) - 60}, + JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), + ClientInfo5 = ClientInfo#{password => JWS5}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo5)), + + Payload6 = #{ <<"username">> => <<"myuser">> + , <<"iat">> => erlang:system_time(second) + 60}, + JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), + ClientInfo6 = ClientInfo#{password => JWS6}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo6)), + + %% Not Before + Payload7 = #{ <<"username">> => <<"myuser">> + , <<"nbf">> => erlang:system_time(second) - 60}, + JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), + ClientInfo7 = ClientInfo#{password => JWS7}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo7)), + + Payload8 = #{ <<"username">> => <<"myuser">> + , <<"nbf">> => erlang:system_time(second) + 60}, + JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), + ClientInfo8 = ClientInfo#{password => JWS8}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo8)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_jwt_authenticator2(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + Dir = code:lib_dir(emqx_authn, test), + PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), + PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), + AuthenticatorName = <<"myauthenticator">>, + Config = #{use_jwks => false, + algorithm => 'public-key', + certificate => PublicKey, + verify_claims => []}, + AuthenticatorConfig = #{name => AuthenticatorName, + type => jwt, + config => Config}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + Payload = #{<<"username">> => <<"myuser">>}, + JWS = generate_jws('public-key', Payload, PrivateKey), + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => JWS}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>})), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +generate_jws('hmac-based', Payload, Secret) -> + JWK = jose_jwk:from_oct(Secret), + Header = #{ <<"alg">> => <<"HS256">> + , <<"typ">> => <<"JWT">> + }, + Signed = jose_jwt:sign(JWK, Header, Payload), + {_, JWS} = jose_jws:compact(Signed), + JWS; +generate_jws('public-key', Payload, PrivateKey) -> + JWK = jose_jwk:from_pem_file(PrivateKey), + Header = #{ <<"alg">> => <<"RS256">> + , <<"typ">> => <<"JWT">> + }, + Signed = jose_jwt:sign(JWK, Header, Payload), + {_, JWS} = jose_jws:compact(Signed), + JWS. diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl new file mode 100644 index 000000000..abc7ad149 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -0,0 +1,187 @@ +%%-------------------------------------------------------------------- +%% 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_authn_mnesia_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(AUTH, emqx_authn). + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([emqx_authn], fun set_special_configs/1), + Config. + +end_per_suite(_) -> + file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf')), + emqx_ct_helpers:stop_apps([emqx_authn]), + ok. + +set_special_configs(emqx_authn) -> + application:set_env(emqx, plugins_etc_dir, + emqx_ct_helpers:deps_path(emqx_authn, "test")), + Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}}, + ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)), + ok; +set_special_configs(_App) -> + ok. + +t_mnesia_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + AuthenticatorConfig = #{name => AuthenticatorName, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + UserInfo = #{<<"user_id">> => <<"myuser">>, + <<"password">> => <<"mypass">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo = #{listener_id => ListenerID, + username => <<"myuser">>, + password => <<"mypass">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo)), + ClientInfo2 = ClientInfo#{username => <<"baduser">>}, + ?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)), + ClientInfo3 = ClientInfo#{password => <<"badpass">>}, + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)), + + UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, AuthenticatorName, <<"myuser">>, UserInfo2)), + ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)), + + ?assertEqual(ok, ?AUTH:delete_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName)), + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + ?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ?assertEqual([], ets:tab2list(mnesia_basic_auth)), + ok. + +t_import(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName = <<"myauthenticator">>, + AuthenticatorConfig = #{name => AuthenticatorName, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)), + + Dir = code:lib_dir(emqx_authn, test), + ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))), + ?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), + ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser1">>)), + ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser3">>)), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo1 = #{listener_id => ListenerID, + username => <<"myuser1">>, + password => <<"mypassword1">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, + password => <<"mypassword3">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. + +t_multi_mnesia_authenticator(_) -> + ChainID = <<"mychain">>, + Chain = #{id => ChainID, + type => simple}, + ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), + + AuthenticatorName1 = <<"myauthenticator1">>, + AuthenticatorConfig1 = #{name => AuthenticatorName1, + type => 'built-in-database', + config => #{ + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}}, + AuthenticatorName2 = <<"myauthenticator2">>, + AuthenticatorConfig2 = #{name => AuthenticatorName2, + type => 'built-in-database', + config => #{ + user_id_type => clientid, + password_hash_algorithm => #{ + name => sha256 + }}}, + ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)), + ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)), + + ?assertEqual({ok, #{user_id => <<"myuser">>}}, + ?AUTH:add_user(ChainID, AuthenticatorName1, + #{<<"user_id">> => <<"myuser">>, + <<"password">> => <<"mypass1">>})), + ?assertEqual({ok, #{user_id => <<"myclient">>}}, + ?AUTH:add_user(ChainID, AuthenticatorName2, + #{<<"user_id">> => <<"myclient">>, + <<"password">> => <<"mypass2">>})), + + ListenerID = <<"listener1">>, + ?AUTH:bind(ChainID, [ListenerID]), + + ClientInfo1 = #{listener_id => ListenerID, + username => <<"myuser">>, + clientid => <<"myclient">>, + password => <<"mypass1">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)), + + ?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)), + ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, + ?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)), + + ?AUTH:unbind([ListenerID], ChainID), + ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), + ok. diff --git a/rebar.config.erl b/rebar.config.erl index 901749d72..ff66f746c 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -248,6 +248,7 @@ relx_apps(ReleaseType) -> , {mnesia, load} , {ekka, load} , {emqx_plugin_libs, load} + , emqx_authn , emqx_authz , observer_cli , emqx_http_lib @@ -285,7 +286,6 @@ relx_plugin_apps(ReleaseType) -> , emqx_sn , emqx_coap , emqx_stomp - , emqx_authentication , emqx_statsd , emqx_rule_actions ] @@ -373,6 +373,7 @@ emqx_etc_overlay_common() -> {"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"}, {"{{base_dir}}/lib/emqx_data_bridge/etc/emqx_data_bridge.conf", "etc/plugins/emqx_data_bridge.conf"}, {"{{base_dir}}/lib/emqx_telemetry/etc/emqx_telemetry.conf", "etc/plugins/emqx_telemetry.conf"}, + {"{{base_dir}}/lib/emqx_authn/etc/emqx_authn.conf", "etc/plugins/emqx_authn.conf"}, {"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"}, %% TODO: check why it has to end with .paho %% and why it is put to etc/plugins dir