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