feat(ldap): add LDAP connector
This commit is contained in:
parent
2a9de4701b
commit
fa6343cc80
|
@ -0,0 +1,94 @@
|
|||
Business Source License 1.1
|
||||
|
||||
Licensor: Hangzhou EMQ Technologies Co., Ltd.
|
||||
Licensed Work: EMQX Enterprise Edition
|
||||
The Licensed Work is (c) 2023
|
||||
Hangzhou EMQ Technologies Co., Ltd.
|
||||
Additional Use Grant: Students and educators are granted right to copy,
|
||||
modify, and create derivative work for research
|
||||
or education.
|
||||
Change Date: 2027-02-01
|
||||
Change License: Apache License, Version 2.0
|
||||
|
||||
For information about alternative licensing arrangements for the Software,
|
||||
please contact Licensor: https://www.emqx.com/en/contact
|
||||
|
||||
Notice
|
||||
|
||||
The Business Source License (this document, or the “License”) is not an Open
|
||||
Source license. However, the Licensed Work will eventually be made available
|
||||
under an Open Source License, as stated in this License.
|
||||
|
||||
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
||||
“Business Source License” is a trademark of MariaDB Corporation Ab.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
Business Source License 1.1
|
||||
|
||||
Terms
|
||||
|
||||
The Licensor hereby grants you the right to copy, modify, create derivative
|
||||
works, redistribute, and make non-production use of the Licensed Work. The
|
||||
Licensor may make an Additional Use Grant, above, permitting limited
|
||||
production use.
|
||||
|
||||
Effective on the Change Date, or the fourth anniversary of the first publicly
|
||||
available distribution of a specific version of the Licensed Work under this
|
||||
License, whichever comes first, the Licensor hereby grants you rights under
|
||||
the terms of the Change License, and the rights granted in the paragraph
|
||||
above terminate.
|
||||
|
||||
If your use of the Licensed Work does not comply with the requirements
|
||||
currently in effect as described in this License, you must purchase a
|
||||
commercial license from the Licensor, its affiliated entities, or authorized
|
||||
resellers, or you must refrain from using the Licensed Work.
|
||||
|
||||
All copies of the original and modified Licensed Work, and derivative works
|
||||
of the Licensed Work, are subject to this License. This License applies
|
||||
separately for each version of the Licensed Work and the Change Date may vary
|
||||
for each version of the Licensed Work released by Licensor.
|
||||
|
||||
You must conspicuously display this License on each original or modified copy
|
||||
of the Licensed Work. If you receive the Licensed Work in original or
|
||||
modified form from a third party, the terms and conditions set forth in this
|
||||
License apply to your use of that work.
|
||||
|
||||
Any use of the Licensed Work in violation of this License will automatically
|
||||
terminate your rights under this License for the current and all other
|
||||
versions of the Licensed Work.
|
||||
|
||||
This License does not grant you any right in any trademark or logo of
|
||||
Licensor or its affiliates (provided that you may use a trademark or logo of
|
||||
Licensor as expressly required by this License).
|
||||
|
||||
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
||||
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
||||
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
||||
TITLE.
|
||||
|
||||
MariaDB hereby grants you permission to use this License’s text to license
|
||||
your works, and to refer to it using the trademark “Business Source License”,
|
||||
as long as you comply with the Covenants of Licensor below.
|
||||
|
||||
Covenants of Licensor
|
||||
|
||||
In consideration of the right to use this License’s text and the “Business
|
||||
Source License” name and trademark, Licensor covenants to MariaDB, and to all
|
||||
other recipients of the licensed work to be provided by Licensor:
|
||||
|
||||
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
||||
or a license that is compatible with GPL Version 2.0 or a later version,
|
||||
where “compatible” means that software provided under the Change License can
|
||||
be included in a program with software provided under GPL Version 2.0 or a
|
||||
later version. Licensor may specify additional Change Licenses without
|
||||
limitation.
|
||||
|
||||
2. To either: (a) specify an additional grant of rights to use that does not
|
||||
impose any additional restriction on the right granted in this License, as
|
||||
the Additional Use Grant; or (b) insert the text “None”.
|
||||
|
||||
3. To specify a Change Date.
|
||||
|
||||
4. Not to modify this License in any other way.
|
|
@ -0,0 +1,14 @@
|
|||
# LDAP Connector
|
||||
|
||||
This application houses the LDAP connector.
|
||||
It provides the APIs to connect to the LDAP service.
|
||||
|
||||
It is used by the emqx_authz and emqx_authn applications to check user permissions.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see our [contributing.md](../../CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
See [APL](../../APL.txt).
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
%% -*- mode: erlang; -*-
|
||||
|
||||
{erl_opts, [debug_info]}.
|
||||
{deps, [
|
||||
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
||||
{emqx_resource, {path, "../../apps/emqx_resource"}}
|
||||
]}.
|
|
@ -0,0 +1,13 @@
|
|||
{application, emqx_ldap, [
|
||||
{description, "EMQX LDAP Connector"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib
|
||||
]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
|
||||
{links, []}
|
||||
]}.
|
|
@ -0,0 +1,230 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ldap).
|
||||
|
||||
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
-include_lib("eldap/include/eldap.hrl").
|
||||
|
||||
-behaviour(emqx_resource).
|
||||
|
||||
%% callbacks of behaviour emqx_resource
|
||||
-export([
|
||||
callback_mode/0,
|
||||
on_start/2,
|
||||
on_stop/2,
|
||||
on_query/3,
|
||||
on_get_status/2
|
||||
]).
|
||||
|
||||
%% ecpool connect & reconnect
|
||||
-export([connect/1]).
|
||||
|
||||
-export([roots/0, fields/1]).
|
||||
|
||||
-export([do_get_status/1]).
|
||||
|
||||
-define(LDAP_HOST_OPTIONS, #{
|
||||
default_port => 389
|
||||
}).
|
||||
|
||||
-type params_tokens() :: #{atom() => list()}.
|
||||
-type state() ::
|
||||
#{
|
||||
pool_name := binary(),
|
||||
base_tokens := params_tokens(),
|
||||
filter_tokens := params_tokens()
|
||||
}.
|
||||
|
||||
-define(ECS, emqx_connector_schema_lib).
|
||||
|
||||
%%=====================================================================
|
||||
%% Hocon schema
|
||||
roots() ->
|
||||
[{config, #{type => hoconsc:ref(?MODULE, config)}}].
|
||||
|
||||
fields(config) ->
|
||||
[
|
||||
{server, server()},
|
||||
{pool_size, fun ?ECS:pool_size/1},
|
||||
{username, fun ensure_username/1},
|
||||
{password, fun ?ECS:password/1},
|
||||
{base_object,
|
||||
?HOCON(binary(), #{
|
||||
desc => ?DESC(base_object),
|
||||
required => true
|
||||
})},
|
||||
{filter, ?HOCON(binary(), #{desc => ?DESC(filter), default => ""})},
|
||||
{auto_reconnect, fun ?ECS:auto_reconnect/1}
|
||||
] ++ emqx_connector_schema_lib:ssl_fields().
|
||||
|
||||
server() ->
|
||||
Meta = #{desc => ?DESC("server")},
|
||||
emqx_schema:servers_sc(Meta, ?LDAP_HOST_OPTIONS).
|
||||
|
||||
ensure_username(required) ->
|
||||
true;
|
||||
ensure_username(Field) ->
|
||||
?ECS:username(Field).
|
||||
|
||||
%% ===================================================================
|
||||
callback_mode() -> always_sync.
|
||||
|
||||
-spec on_start(binary(), hoconsc:config()) -> {ok, state()} | {error, _}.
|
||||
on_start(
|
||||
InstId,
|
||||
#{
|
||||
server := Server,
|
||||
pool_size := PoolSize,
|
||||
ssl := SSL
|
||||
} = Config
|
||||
) ->
|
||||
HostPort = emqx_schema:parse_server(Server, ?LDAP_HOST_OPTIONS),
|
||||
?SLOG(info, #{
|
||||
msg => "starting_ldap_connector",
|
||||
connector => InstId,
|
||||
config => emqx_utils:redact(Config)
|
||||
}),
|
||||
|
||||
Config2 = maps:merge(Config, HostPort),
|
||||
Config3 =
|
||||
case maps:get(enable, SSL) of
|
||||
true ->
|
||||
Config2#{sslopts => emqx_tls_lib:to_client_opts(SSL)};
|
||||
false ->
|
||||
Config2
|
||||
end,
|
||||
Options = [
|
||||
{pool_size, PoolSize},
|
||||
{auto_reconnect, ?AUTO_RECONNECT_INTERVAL},
|
||||
{options, Config3}
|
||||
],
|
||||
|
||||
case emqx_resource_pool:start(InstId, ?MODULE, Options) of
|
||||
ok ->
|
||||
{ok, prepare_template(Config, #{pool_name => InstId})};
|
||||
{error, Reason} ->
|
||||
?tp(
|
||||
ldap_connector_start_failed,
|
||||
#{error => Reason}
|
||||
),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
on_stop(InstId, _State) ->
|
||||
?SLOG(info, #{
|
||||
msg => "stopping_ldap_connector",
|
||||
connector => InstId
|
||||
}),
|
||||
emqx_resource_pool:stop(InstId).
|
||||
|
||||
on_query(InstId, {query, Data}, State) ->
|
||||
on_query(InstId, {query, Data}, [], State);
|
||||
on_query(InstId, {query, Data, Attrs}, State) ->
|
||||
on_query(InstId, {query, Data}, [{attributes, Attrs}], State);
|
||||
on_query(InstId, {query, Data, Attrs, Timeout}, State) ->
|
||||
on_query(InstId, {query, Data}, [{attributes, Attrs}, {timeout, Timeout}], State).
|
||||
|
||||
on_get_status(_InstId, #{pool_name := PoolName} = _State) ->
|
||||
case emqx_resource_pool:health_check_workers(PoolName, fun ?MODULE:do_get_status/1) of
|
||||
true ->
|
||||
connected;
|
||||
false ->
|
||||
connecting
|
||||
end.
|
||||
|
||||
do_get_status(Conn) ->
|
||||
erlang:is_process_alive(Conn).
|
||||
|
||||
%% ===================================================================
|
||||
|
||||
connect(Options) ->
|
||||
#{host := Host, username := Username, password := Password} =
|
||||
Conf = proplists:get_value(options, Options),
|
||||
OpenOpts = maps:to_list(maps:with([port, sslopts], Conf)),
|
||||
case eldap:open([Host], [{log, fun log/3}, OpenOpts]) of
|
||||
{ok, Handle} = Ret ->
|
||||
case eldap:simple_bind(Handle, Username, Password) of
|
||||
ok -> Ret;
|
||||
Error -> Error
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
on_query(
|
||||
InstId,
|
||||
{query, Data},
|
||||
SearchOptions,
|
||||
#{base_tokens := BaseTks, filter_tokens := FilterTks} = State
|
||||
) ->
|
||||
Base = emqx_placeholder:proc_tmpl(BaseTks, Data),
|
||||
case FilterTks of
|
||||
[] ->
|
||||
do_ldap_query(InstId, [{base, Base} | SearchOptions], State);
|
||||
_ ->
|
||||
FilterBin = emqx_placeholder:proc_tmpl(FilterTks, Data),
|
||||
%% TODO
|
||||
Filter = FilterBin,
|
||||
do_ldap_query(
|
||||
InstId,
|
||||
[{base, Base}, {filter, Filter} | SearchOptions],
|
||||
State
|
||||
)
|
||||
end.
|
||||
|
||||
do_ldap_query(
|
||||
InstId,
|
||||
SearchOptions,
|
||||
#{pool_name := PoolName} = State
|
||||
) ->
|
||||
LogMeta = #{connector => InstId, search => SearchOptions, state => State},
|
||||
?TRACE("QUERY", "ldap_connector_received", LogMeta),
|
||||
case
|
||||
ecpool:pick_and_do(
|
||||
PoolName,
|
||||
{eldap, search, SearchOptions},
|
||||
handover
|
||||
)
|
||||
of
|
||||
{ok, Result} ->
|
||||
?tp(
|
||||
ldap_connector_query_return,
|
||||
#{result => Result}
|
||||
),
|
||||
{ok,
|
||||
case Result#eldap_search_result.entries of
|
||||
[First | _] ->
|
||||
%% TODO Support multi entries?
|
||||
First;
|
||||
_ ->
|
||||
undefined
|
||||
end};
|
||||
{error, Reason} ->
|
||||
?SLOG(
|
||||
error,
|
||||
LogMeta#{msg => "ldap_connector_do_sql_query_failed", reason => Reason}
|
||||
),
|
||||
{error, {unrecoverable_error, Reason}}
|
||||
end.
|
||||
|
||||
log(Level, Format, Args) ->
|
||||
?SLOG(
|
||||
Level,
|
||||
#{
|
||||
msg => "ldap_log",
|
||||
log => io_lib:format(Format, Args)
|
||||
}
|
||||
).
|
||||
|
||||
prepare_template(Config, State) ->
|
||||
do_prepare_template(maps:to_list(maps:with([base_object, filter], Config)), State).
|
||||
|
||||
do_prepare_template([{base_object, V} | T], State) ->
|
||||
do_prepare_template(T, State#{base_tokens => emqx_placeholder:preproc_tmpl(V)});
|
||||
do_prepare_template([{filter, V} | T], State) ->
|
||||
do_prepare_template(T, State#{filter_tokens => emqx_placeholder:preproc_tmpl(V)}).
|
|
@ -0,0 +1,24 @@
|
|||
Definitions.
|
||||
|
||||
NonControl = [^()&|!=~><:*]
|
||||
String = {NonControl}*
|
||||
White = [\s\t\n\r]+
|
||||
|
||||
Rules.
|
||||
|
||||
\( : {token, {lparen, TokenLine}}.
|
||||
\) : {token, {rparen, TokenLine}}.
|
||||
\& : {token, {'and', TokenLine}}.
|
||||
\| : {token, {'or', TokenLine}}.
|
||||
\! : {token, {'not', TokenLine}}.
|
||||
= : {token, {equal, TokenLine}}.
|
||||
~= : {token, {approx, TokenLine}}.
|
||||
>= : {token, {greaterOrEqual, TokenLine}}.
|
||||
<= : {token, {lessOrEqual, TokenLine}}.
|
||||
\* : {token, {asterisk, TokenLine}}.
|
||||
\: : {token, {colon, TokenLine}}.
|
||||
dn : {token, {dn, TokenLine}}.
|
||||
{White} : skip_token.
|
||||
{String} : {token, {string, TokenLine, TokenChars}}.
|
||||
|
||||
Erlang code.
|
|
@ -0,0 +1,133 @@
|
|||
Nonterminals
|
||||
filter filtercomp filterlist item simple present substring initial any final extensible attr value type dnattrs matchingrule.
|
||||
|
||||
Terminals
|
||||
lparen rparen 'and' 'or' 'not' equal approx greaterOrEqual lessOrEqual asterisk colon dn string.
|
||||
|
||||
Rootsymbol filter.
|
||||
Left 100 present.
|
||||
Left 500 substring.
|
||||
|
||||
filter ->
|
||||
lparen filtercomp rparen : '$2'.
|
||||
|
||||
filtercomp ->
|
||||
'and' filterlist: 'and'('$2').
|
||||
filtercomp ->
|
||||
'or' filterlist: 'or'('$2').
|
||||
filtercomp ->
|
||||
'not' filterlist: 'not'('$2').
|
||||
filtercomp ->
|
||||
item: '$1'.
|
||||
|
||||
filterlist ->
|
||||
filter: '$1'.
|
||||
filterlist ->
|
||||
filter filterlist: ['$1' | '$2'].
|
||||
|
||||
item ->
|
||||
simple: '$1'.
|
||||
item ->
|
||||
present: '$1'.
|
||||
item ->
|
||||
substring: '$1'.
|
||||
item->
|
||||
extensible: '$1'.
|
||||
|
||||
simple ->
|
||||
attr equal value: equal('$1', '$3').
|
||||
simple ->
|
||||
attr approx value: approx('$1', '$3').
|
||||
simple ->
|
||||
attr greaterOrEqual value: greaterOrEqual('$1', '$3').
|
||||
simple ->
|
||||
attr lessOrEqual value: lessOrEqual('$1', '$3').
|
||||
|
||||
present ->
|
||||
attr equal asterisk: present('$1').
|
||||
|
||||
substring ->
|
||||
attr equal initial asterisk any final: substrings('$1', ['$3', '$5', '$6']).
|
||||
substring ->
|
||||
attr equal asterisk any final: substrings('$1', ['$4', '$5']).
|
||||
substring ->
|
||||
attr equal initial asterisk any: substrings('$1', ['$3', '$5']).
|
||||
substring ->
|
||||
attr equal asterisk any: substrings('$1', ['$4']).
|
||||
|
||||
initial ->
|
||||
value: {initial, '$1'}.
|
||||
|
||||
final ->
|
||||
value: {final, '$1'}.
|
||||
|
||||
any -> any value asterisk: 'any'('$1', '$2').
|
||||
any -> '$empty': [].
|
||||
|
||||
extensible ->
|
||||
type dnattrs matchingrule colon equal value : extensible('$6', ['$1', '$2', '$3']).
|
||||
extensible ->
|
||||
type dnattrs colon equal value: extensible('$5', ['$1', '$2']).
|
||||
extensible ->
|
||||
type matchingrule colon equal value: extensible('$5', ['$1', '$2']).
|
||||
extensible ->
|
||||
type colon equal value: extensible('$4', []).
|
||||
|
||||
extensible ->
|
||||
dnattrs matchingrule colon equal value: extensible('$5', ['$1', '$2']).
|
||||
extensible ->
|
||||
matchingrule colon equal value: extensible('$4', ['$1']).
|
||||
|
||||
attr ->
|
||||
string: get_value('$1').
|
||||
|
||||
value ->
|
||||
string: get_value('$1').
|
||||
|
||||
type ->
|
||||
value: {type, '$1'}.
|
||||
|
||||
dnattrs ->
|
||||
colon dn: {dnAttributes, true}.
|
||||
|
||||
matchingrule ->
|
||||
colon value: {matchingRule, '$2'}.
|
||||
|
||||
Erlang code.
|
||||
|
||||
'and'(Value) ->
|
||||
eldap:'and'(Value).
|
||||
|
||||
'or'(Value) ->
|
||||
eldap:'or'(Value).
|
||||
|
||||
'not'(Value) ->
|
||||
eldap:'not'(Value).
|
||||
|
||||
equal(Attr, Value) ->
|
||||
eldap:equalityMatch(Attr, Value).
|
||||
|
||||
approx(Attr, Value) ->
|
||||
eldap:approxMatch(Attr, Value).
|
||||
|
||||
greaterOrEqual(Attr, Value) ->
|
||||
eldap:greaterOrEqual(Attr, Value).
|
||||
|
||||
lessOrEqual(Attr, Value) ->
|
||||
eldap:lessOrEqual(Attr, Value).
|
||||
|
||||
present(Value) ->
|
||||
eldap:present(Value).
|
||||
|
||||
substrings(Attr, List) ->
|
||||
eldap:substrings(Attr, flatten(List)).
|
||||
|
||||
'any'(List, Item) ->
|
||||
[{any, Item} | List].
|
||||
|
||||
extensible(Value, Opts) -> eldap:extensibleMatch(Value, Opts).
|
||||
|
||||
flatten(List) -> lists:flatten(List).
|
||||
|
||||
get_value({_Token, _Line, Value}) ->
|
||||
Value.
|
|
@ -0,0 +1,165 @@
|
|||
% %%--------------------------------------------------------------------
|
||||
% %% Copyright (c) 2020-2023 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_ldap_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("stdlib/include/assert.hrl").
|
||||
|
||||
-define(MYSQL_HOST, "ldap").
|
||||
-define(MYSQL_RESOURCE_MOD, emqx_ldap).
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
|
||||
true ->
|
||||
ok = emqx_common_test_helpers:start_apps([emqx_conf]),
|
||||
ok = emqx_connector_test_helpers:start_apps([emqx_resource]),
|
||||
{ok, _} = application:ensure_all_started(emqx_connector),
|
||||
Config;
|
||||
false ->
|
||||
{skip, no_ldap}
|
||||
end.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_conf]),
|
||||
ok = emqx_connector_test_helpers:stop_apps([emqx_resource]),
|
||||
_ = application:stop(emqx_connector).
|
||||
|
||||
init_per_testcase(_, Config) ->
|
||||
Config.
|
||||
|
||||
end_per_testcase(_, _Config) ->
|
||||
ok.
|
||||
|
||||
% %%------------------------------------------------------------------------------
|
||||
% %% Testcases
|
||||
% %%------------------------------------------------------------------------------
|
||||
|
||||
t_lifecycle(_Config) ->
|
||||
perform_lifecycle_check(
|
||||
<<"emqx_ldap_SUITE">>,
|
||||
ldap_config()
|
||||
).
|
||||
|
||||
perform_lifecycle_check(ResourceId, InitialConfig) ->
|
||||
{ok, #{config := CheckedConfig}} =
|
||||
emqx_resource:check_config(?MYSQL_RESOURCE_MOD, InitialConfig),
|
||||
{ok, #{
|
||||
state := #{pool_name := PoolName} = State,
|
||||
status := InitialStatus
|
||||
}} = emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?CONNECTOR_RESOURCE_GROUP,
|
||||
?MYSQL_RESOURCE_MOD,
|
||||
CheckedConfig,
|
||||
#{}
|
||||
),
|
||||
?assertEqual(InitialStatus, connected),
|
||||
% Instance should match the state and status of the just started resource
|
||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||
state := State,
|
||||
status := InitialStatus
|
||||
}} =
|
||||
emqx_resource:get_instance(ResourceId),
|
||||
?assertEqual({ok, connected}, emqx_resource:health_check(ResourceId)),
|
||||
% % Perform query as further check that the resource is working as expected
|
||||
?assertMatch({ok, _, [[1]]}, emqx_resource:query(ResourceId, test_query_no_params())),
|
||||
?assertMatch({ok, _, [[1]]}, emqx_resource:query(ResourceId, test_query_with_params())),
|
||||
?assertMatch(
|
||||
{ok, _, [[1]]},
|
||||
emqx_resource:query(
|
||||
ResourceId,
|
||||
test_query_with_params_and_timeout()
|
||||
)
|
||||
),
|
||||
?assertEqual(ok, emqx_resource:stop(ResourceId)),
|
||||
% Resource will be listed still, but state will be changed and healthcheck will fail
|
||||
% as the worker no longer exists.
|
||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||
state := State,
|
||||
status := StoppedStatus
|
||||
}} =
|
||||
emqx_resource:get_instance(ResourceId),
|
||||
?assertEqual(stopped, StoppedStatus),
|
||||
?assertEqual({error, resource_is_stopped}, emqx_resource:health_check(ResourceId)),
|
||||
% Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself.
|
||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(PoolName)),
|
||||
% Can call stop/1 again on an already stopped instance
|
||||
?assertEqual(ok, emqx_resource:stop(ResourceId)),
|
||||
% Make sure it can be restarted and the healthchecks and queries work properly
|
||||
?assertEqual(ok, emqx_resource:restart(ResourceId)),
|
||||
% async restart, need to wait resource
|
||||
timer:sleep(500),
|
||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} =
|
||||
emqx_resource:get_instance(ResourceId),
|
||||
?assertEqual({ok, connected}, emqx_resource:health_check(ResourceId)),
|
||||
?assertMatch({ok, _, [[1]]}, emqx_resource:query(ResourceId, test_query_no_params())),
|
||||
?assertMatch({ok, _, [[1]]}, emqx_resource:query(ResourceId, test_query_with_params())),
|
||||
?assertMatch(
|
||||
{ok, _, [[1]]},
|
||||
emqx_resource:query(
|
||||
ResourceId,
|
||||
test_query_with_params_and_timeout()
|
||||
)
|
||||
),
|
||||
% Stop and remove the resource in one go.
|
||||
?assertEqual(ok, emqx_resource:remove_local(ResourceId)),
|
||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(PoolName)),
|
||||
% Should not even be able to get the resource data out of ets now unlike just stopping.
|
||||
?assertEqual({error, not_found}, emqx_resource:get_instance(ResourceId)).
|
||||
|
||||
% %%------------------------------------------------------------------------------
|
||||
% %% Helpers
|
||||
% %%------------------------------------------------------------------------------
|
||||
|
||||
ldap_config() ->
|
||||
RawConfig = list_to_binary(
|
||||
io_lib:format(
|
||||
""
|
||||
"\n"
|
||||
" auto_reconnect = true\n"
|
||||
" database = mqtt\n"
|
||||
" username= root\n"
|
||||
" password = public\n"
|
||||
" pool_size = 8\n"
|
||||
" server = \"~s:~b\"\n"
|
||||
" "
|
||||
"",
|
||||
[?MYSQL_HOST, ?MYSQL_DEFAULT_PORT]
|
||||
)
|
||||
),
|
||||
|
||||
{ok, Config} = hocon:binary(RawConfig),
|
||||
#{<<"config">> => Config}.
|
||||
|
||||
test_query_no_params() ->
|
||||
{sql, <<"SELECT 1">>}.
|
||||
|
||||
test_query_with_params() ->
|
||||
{sql, <<"SELECT ?">>, [1]}.
|
||||
|
||||
test_query_with_params_and_timeout() ->
|
||||
{sql, <<"SELECT ?">>, [1], 1000}.
|
|
@ -0,0 +1,25 @@
|
|||
emqx_ldap {
|
||||
|
||||
server.desc:
|
||||
"""The IPv4 or IPv6 address or the hostname to connect to.<br/>
|
||||
A host entry has the following form: `Host[:Port]`.<br/>
|
||||
The MySQL default port 3306 is used if `[:Port]` is not specified."""
|
||||
|
||||
server.label:
|
||||
"""Server Host"""
|
||||
|
||||
base_object.desc:
|
||||
"""The name of the base object entry (or possibly the root) relative to
|
||||
which the Search is to be performed."""
|
||||
|
||||
base_object.label:
|
||||
"""Base Object"""
|
||||
|
||||
filter.desc:
|
||||
"""The filter that defines the conditions that must be fulfilled in order
|
||||
for the Search to match a given entry."""
|
||||
|
||||
filter.label:
|
||||
"""Filter"""
|
||||
|
||||
}
|
Loading…
Reference in New Issue