refactor(authn): support hocon for authn (#5068)
* refactor(use hocon): rename to authn, , support hocon, support two types of chains and support bind listener to chain
This commit is contained in:
parent
dae4c771d0
commit
cdcb63374a
|
@ -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).
|
|
|
@ -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.
|
|
|
@ -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).
|
|
|
@ -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).
|
|
|
@ -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.
|
|
|
@ -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"]
|
||||||
|
# }
|
||||||
|
]
|
||||||
|
}
|
|
@ -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()).
|
|
@ -1,10 +1,10 @@
|
||||||
{application, emqx_authentication,
|
{application, emqx_authn,
|
||||||
[{description, "EMQ X Authentication"},
|
[{description, "EMQ X Authentication"},
|
||||||
{vsn, "0.1.0"},
|
{vsn, "0.1.0"},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
{registered, [emqx_authentication_sup, emqx_authentication_registry]},
|
{registered, [emqx_authn_sup, emqx_authn_registry]},
|
||||||
{applications, [kernel,stdlib]},
|
{applications, [kernel,stdlib]},
|
||||||
{mod, {emqx_authentication_app,[]}},
|
{mod, {emqx_authn_app,[]}},
|
||||||
{env, []},
|
{env, []},
|
||||||
{licenses, ["Apache-2.0"]},
|
{licenses, ["Apache-2.0"]},
|
||||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
|
@ -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.
|
|
@ -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).
|
|
@ -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.
|
|
@ -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.
|
|
@ -14,7 +14,7 @@
|
||||||
%% limitations under the License.
|
%% limitations under the License.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-module(emqx_authentication_sup).
|
-module(emqx_authn_sup).
|
||||||
|
|
||||||
-behaviour(supervisor).
|
-behaviour(supervisor).
|
||||||
|
|
|
@ -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.
|
|
@ -14,24 +14,4 @@
|
||||||
%% limitations under the License.
|
%% limitations under the License.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-module(emqx_authentication_app).
|
-module(emqx_enhanced_authn_mnesia).
|
||||||
|
|
||||||
-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.
|
|
|
@ -14,7 +14,7 @@
|
||||||
%% limitations under the License.
|
%% limitations under the License.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-module(emqx_authentication_jwks_connector).
|
-module(emqx_authn_jwks_connector).
|
||||||
|
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
@ -125,23 +125,17 @@ code_change(_OldVsn, State, _Extra) ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
handle_options(Opts) ->
|
handle_options(#{endpoint := Endpoint,
|
||||||
#{endpoint => proplists:get_value(jwks_endpoint, Opts),
|
refresh_interval := RefreshInterval0,
|
||||||
refresh_interval => limit_refresh_interval(proplists:get_value(refresh_interval, Opts)),
|
ssl_opts := SSLOpts}) ->
|
||||||
ssl_opts => get_ssl_opts(Opts),
|
#{endpoint => Endpoint,
|
||||||
|
refresh_interval => limit_refresh_interval(RefreshInterval0),
|
||||||
|
ssl_opts => maps:to_list(SSLOpts),
|
||||||
jwks => [],
|
jwks => [],
|
||||||
request_id => undefined}.
|
request_id => undefined};
|
||||||
|
|
||||||
get_ssl_opts(Opts) ->
|
handle_options(#{enable_ssl := false} = Opts) ->
|
||||||
case proplists:get_value(enable_ssl, Opts) of
|
handle_options(Opts#{ssl_opts => #{}}).
|
||||||
false -> [];
|
|
||||||
true ->
|
|
||||||
maps:to_list(maps:with([cacertfile,
|
|
||||||
keyfile,
|
|
||||||
certfile,
|
|
||||||
verify,
|
|
||||||
server_name_indication], maps:from_list(Opts)))
|
|
||||||
end.
|
|
||||||
|
|
||||||
refresh_jwks(#{endpoint := Endpoint,
|
refresh_jwks(#{endpoint := Endpoint,
|
||||||
ssl_opts := SSLOpts} = State) ->
|
ssl_opts := SSLOpts} = State) ->
|
|
@ -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.
|
|
@ -14,9 +14,14 @@
|
||||||
%% limitations under the License.
|
%% 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
|
-export([ create/3
|
||||||
, update/4
|
, update/4
|
||||||
|
@ -32,29 +37,10 @@
|
||||||
, list_users/1
|
, list_users/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% TODO: support bcrypt
|
-type user_id_type() :: clientid | username.
|
||||||
-service_type(#{
|
|
||||||
name => mnesia,
|
-type user_group() :: {chain_id(), authenticator_name()}.
|
||||||
params_spec => #{
|
-type user_id() :: binary().
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).
|
|
||||||
|
|
||||||
-record(user_info,
|
-record(user_info,
|
||||||
{ user_id :: {user_group(), user_id()}
|
{ user_id :: {user_group(), user_id()}
|
||||||
|
@ -62,8 +48,7 @@
|
||||||
, salt :: binary()
|
, salt :: binary()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-type(user_group() :: {chain_id(), service_name()}).
|
-reflect_type([ user_id_type/0 ]).
|
||||||
-type(user_id() :: binary()).
|
|
||||||
|
|
||||||
-export([mnesia/1]).
|
-export([mnesia/1]).
|
||||||
|
|
||||||
|
@ -73,7 +58,6 @@
|
||||||
-define(TAB, mnesia_basic_auth).
|
-define(TAB, mnesia_basic_auth).
|
||||||
|
|
||||||
-rlog_shard({?AUTH_SHARD, ?TAB}).
|
-rlog_shard({?AUTH_SHARD, ?TAB}).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Mnesia bootstrap
|
%% Mnesia bootstrap
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -90,18 +74,61 @@ mnesia(boot) ->
|
||||||
mnesia(copy) ->
|
mnesia(copy) ->
|
||||||
ok = ekka_mnesia:copy_table(?TAB, disc_copies).
|
ok = ekka_mnesia:copy_table(?TAB, disc_copies).
|
||||||
|
|
||||||
create(ChainID, ServiceName, #{<<"user_id_type">> := Type,
|
%%------------------------------------------------------------------------------
|
||||||
<<"password_hash_algorithm">> := Algorithm,
|
%% Hocon Schema
|
||||||
<<"salt_rounds">> := SaltRounds}) ->
|
%%------------------------------------------------------------------------------
|
||||||
Algorithm =:= <<"bcrypt">> andalso ({ok, _} = application:ensure_all_started(bcrypt)),
|
|
||||||
State = #{user_group => {ChainID, ServiceName},
|
structs() -> [config].
|
||||||
user_id_type => binary_to_atom(Type, utf8),
|
|
||||||
password_hash_algorithm => binary_to_atom(Algorithm, utf8),
|
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},
|
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}.
|
{ok, State}.
|
||||||
|
|
||||||
update(ChainID, ServiceName, Params, _State) ->
|
update(ChainID, AuthenticatorName, Config, _State) ->
|
||||||
create(ChainID, ServiceName, Params).
|
create(ChainID, AuthenticatorName, Config).
|
||||||
|
|
||||||
authenticate(ClientInfo = #{password := Password},
|
authenticate(ClientInfo = #{password := Password},
|
||||||
#{user_group := UserGroup,
|
#{user_group := UserGroup,
|
||||||
|
@ -111,7 +138,11 @@ authenticate(ClientInfo = #{password := Password},
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
[] ->
|
[] ->
|
||||||
ignore;
|
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
|
case PasswordHash =:= hash(Algorithm, Password, Salt) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {stop, bad_password}
|
false -> {stop, bad_password}
|
|
@ -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, <<Salt/binary, Password/binary>>);
|
||||||
|
suffix -> emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>)
|
||||||
|
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.
|
|
@ -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, <<Salt/binary, Password/binary>>);
|
||||||
|
suffix -> emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>)
|
||||||
|
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.
|
|
@ -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-----
|
|
@ -0,0 +1,6 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHA+aGk+D/0J9ZAj3tQIAvDTnRvZ
|
||||||
|
NF0IeaTmJcBooxsY6Ze8PGFSQJ/+EJf1i1ffExeaIH99d3nwyBspNlihooGHvTVD
|
||||||
|
eYsu15Htxpqig1L/+MphbZlFClDXtzV0+GeZGoqeloHAXku3Qzk+hMxROalFtH+8
|
||||||
|
GNp+/j2yir1Z9E2xAgMBAAE=
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -248,6 +248,7 @@ relx_apps(ReleaseType) ->
|
||||||
, {mnesia, load}
|
, {mnesia, load}
|
||||||
, {ekka, load}
|
, {ekka, load}
|
||||||
, {emqx_plugin_libs, load}
|
, {emqx_plugin_libs, load}
|
||||||
|
, emqx_authn
|
||||||
, emqx_authz
|
, emqx_authz
|
||||||
, observer_cli
|
, observer_cli
|
||||||
, emqx_http_lib
|
, emqx_http_lib
|
||||||
|
@ -285,7 +286,6 @@ relx_plugin_apps(ReleaseType) ->
|
||||||
, emqx_sn
|
, emqx_sn
|
||||||
, emqx_coap
|
, emqx_coap
|
||||||
, emqx_stomp
|
, emqx_stomp
|
||||||
, emqx_authentication
|
|
||||||
, emqx_statsd
|
, emqx_statsd
|
||||||
, emqx_rule_actions
|
, 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/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_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_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"},
|
{"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"},
|
||||||
%% TODO: check why it has to end with .paho
|
%% TODO: check why it has to end with .paho
|
||||||
%% and why it is put to etc/plugins dir
|
%% and why it is put to etc/plugins dir
|
||||||
|
|
Loading…
Reference in New Issue