Merge pull request #11379 from lafirest/feat/ldap
feat(ldap): add LDAP connector
This commit is contained in:
commit
22b4f4d256
|
@ -6,11 +6,11 @@ services:
|
|||
build:
|
||||
context: ../..
|
||||
dockerfile: .ci/docker-compose-file/openldap/Dockerfile
|
||||
args:
|
||||
args:
|
||||
LDAP_TAG: ${LDAP_TAG}
|
||||
image: openldap
|
||||
ports:
|
||||
- 389:389
|
||||
image: openldap
|
||||
#ports:
|
||||
# - 389:389
|
||||
restart: always
|
||||
networks:
|
||||
- emqx_bridge
|
|
@ -1,18 +1,20 @@
|
|||
FROM buildpack-deps:stretch
|
||||
FROM buildpack-deps:bookworm
|
||||
|
||||
ARG LDAP_TAG=2.4.50
|
||||
ARG LDAP_TAG=2.5.16
|
||||
|
||||
RUN apt-get update && apt-get install -y groff groff-base
|
||||
RUN wget ftp://ftp.openldap.org/pub/OpenLDAP/openldap-release/openldap-${LDAP_TAG}.tgz \
|
||||
&& gunzip -c openldap-${LDAP_TAG}.tgz | tar xvfB - \
|
||||
RUN wget https://www.openldap.org/software/download/OpenLDAP/openldap-release/openldap-${LDAP_TAG}.tgz \
|
||||
&& tar xvzf openldap-${LDAP_TAG}.tgz \
|
||||
&& cd openldap-${LDAP_TAG} \
|
||||
&& ./configure && make depend && make && make install \
|
||||
&& cd .. && rm -rf openldap-${LDAP_TAG}
|
||||
|
||||
COPY .ci/docker-compose-file/openldap/slapd.conf /usr/local/etc/openldap/slapd.conf
|
||||
COPY apps/emqx_authn/test/data/emqx.io.ldif /usr/local/etc/openldap/schema/emqx.io.ldif
|
||||
COPY apps/emqx_authn/test/data/emqx.schema /usr/local/etc/openldap/schema/emqx.schema
|
||||
COPY apps/emqx_authn/test/data/certs/*.pem /usr/local/etc/openldap/
|
||||
COPY apps/emqx_ldap/test/data/emqx.io.ldif /usr/local/etc/openldap/schema/emqx.io.ldif
|
||||
COPY apps/emqx_ldap/test/data/emqx.schema /usr/local/etc/openldap/schema/emqx.schema
|
||||
COPY .ci/docker-compose-file/certs/ca.crt /usr/local/etc/openldap/cacert.pem
|
||||
COPY .ci/docker-compose-file/certs/server.crt /usr/local/etc/openldap/cert.pem
|
||||
COPY .ci/docker-compose-file/certs/server.key /usr/local/etc/openldap/key.pem
|
||||
|
||||
RUN mkdir -p /usr/local/etc/openldap/data \
|
||||
&& slapadd -l /usr/local/etc/openldap/schema/emqx.io.ldif -f /usr/local/etc/openldap/slapd.conf
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
src/emqx_ldap_filter_lexer.erl
|
||||
src/emqx_ldap_filter_parser.erl
|
|
@ -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 @@
|
|||
ldap
|
|
@ -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,237 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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 => <<"(objectClass=mqttUser)">>})},
|
||||
{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) ->
|
||||
#{hostname := 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),
|
||||
case emqx_ldap_filter_parser:scan_and_parse(FilterBin) of
|
||||
{ok, Filter} ->
|
||||
do_ldap_query(
|
||||
InstId,
|
||||
[{base, Base}, {filter, Filter} | SearchOptions],
|
||||
State
|
||||
);
|
||||
{error, Reason} = Error ->
|
||||
?SLOG(error, #{
|
||||
msg => "filter_parse_failed",
|
||||
filter => FilterBin,
|
||||
reason => Reason
|
||||
}),
|
||||
Error
|
||||
end
|
||||
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, Result#eldap_search_result.entries};
|
||||
{error, 'noSuchObject'} ->
|
||||
{ok, []};
|
||||
{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)});
|
||||
do_prepare_template([], State) ->
|
||||
State.
|
|
@ -0,0 +1,31 @@
|
|||
Definitions.
|
||||
|
||||
Control = [()&|!=~><:*]
|
||||
White = [\s\t\n\r]+
|
||||
NonString = [^()&|!=~><:*\s\t\n\r]
|
||||
String = {NonString}+
|
||||
|
||||
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}}.
|
||||
%% Leex will hang if a composite operation is missing a character
|
||||
{Control} : {error, lists:flatten(io_lib:format("Unexpected Tokens:~ts", [TokenChars]))}.
|
||||
|
||||
Erlang code.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
|
@ -0,0 +1,149 @@
|
|||
Header "%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------".
|
||||
|
||||
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' filter: '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', ['$1']).
|
||||
|
||||
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.
|
||||
-export([scan_and_parse/1]).
|
||||
-ignore_xref({return_error, 2}).
|
||||
|
||||
'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) ->
|
||||
[List, {any, Item}].
|
||||
|
||||
extensible(Value, Opts) -> eldap:extensibleMatch(Value, Opts).
|
||||
|
||||
flatten(List) -> lists:flatten(List).
|
||||
|
||||
get_value({_Token, _Line, Value}) ->
|
||||
Value.
|
||||
|
||||
scan_and_parse(Bin) when is_binary(Bin) ->
|
||||
scan_and_parse(erlang:binary_to_list(Bin));
|
||||
scan_and_parse(String) ->
|
||||
case emqx_ldap_filter_lexer:string(String) of
|
||||
{ok, Tokens, _} ->
|
||||
parse(Tokens);
|
||||
{error, Reason, _} ->
|
||||
{error, Reason}
|
||||
end.
|
|
@ -0,0 +1,135 @@
|
|||
## create emqx.io
|
||||
|
||||
dn:dc=emqx,dc=io
|
||||
objectclass: top
|
||||
objectclass: dcobject
|
||||
objectclass: organization
|
||||
dc:emqx
|
||||
o:emqx,Inc.
|
||||
|
||||
# create testdevice.emqx.io
|
||||
dn:ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectclass:organizationalUnit
|
||||
ou:testdevice
|
||||
|
||||
# create user admin
|
||||
dn:uid=admin,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: account
|
||||
userPassword:: e1NIQX1XNnBoNU1tNVB6OEdnaVVMYlBnekczN21qOWc9
|
||||
uid: admin
|
||||
|
||||
## create user=mqttuser0001,
|
||||
# password=mqttuser0001,
|
||||
# passhash={SHA}mlb3fat40MKBTXUVZwCKmL73R/0=
|
||||
# base64passhash=e1NIQX1tbGIzZmF0NDBNS0JUWFVWWndDS21MNzNSLzA9
|
||||
dn:uid=mqttuser0001,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0001
|
||||
isEnabled: TRUE
|
||||
mqttAccountName: user1
|
||||
mqttPublishTopic: mqttuser0001/pub/1
|
||||
mqttPublishTopic: mqttuser0001/pub/+
|
||||
mqttPublishTopic: mqttuser0001/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0001/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0001/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0001/sub/#
|
||||
mqttPubSubTopic: mqttuser0001/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0001/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0001/pubsub/#
|
||||
userPassword:: e1NIQX1tbGIzZmF0NDBNS0JUWFVWWndDS21MNzNSLzA9
|
||||
|
||||
## create user=mqttuser0002
|
||||
# password=mqttuser0002,
|
||||
# passhash={SSHA}n9XdtoG4Q/TQ3TQF4Y+khJbMBH4qXj4M
|
||||
# base64passhash=e1NTSEF9bjlYZHRvRzRRL1RRM1RRRjRZK2toSmJNQkg0cVhqNE0=
|
||||
dn:uid=mqttuser0002,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0002
|
||||
isEnabled: TRUE
|
||||
mqttAccountName: user2
|
||||
mqttPublishTopic: mqttuser0002/pub/1
|
||||
mqttPublishTopic: mqttuser0002/pub/+
|
||||
mqttPublishTopic: mqttuser0002/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0002/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0002/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0002/sub/#
|
||||
mqttPubSubTopic: mqttuser0002/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0002/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0002/pubsub/#
|
||||
userPassword:: e1NTSEF9bjlYZHRvRzRRL1RRM1RRRjRZK2toSmJNQkg0cVhqNE0=
|
||||
|
||||
## create user mqttuser0003
|
||||
# password=mqttuser0003,
|
||||
# passhash={MD5}ybsPGoaK3nDyiQvveiCOIw==
|
||||
# base64passhash=e01ENX15YnNQR29hSzNuRHlpUXZ2ZWlDT0l3PT0=
|
||||
dn:uid=mqttuser0003,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0003
|
||||
isEnabled: TRUE
|
||||
mqttPublishTopic: mqttuser0003/pub/1
|
||||
mqttPublishTopic: mqttuser0003/pub/+
|
||||
mqttPublishTopic: mqttuser0003/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0003/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0003/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0003/sub/#
|
||||
mqttPubSubTopic: mqttuser0003/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0003/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0003/pubsub/#
|
||||
userPassword:: e01ENX15YnNQR29hSzNuRHlpUXZ2ZWlDT0l3PT0=
|
||||
|
||||
## create user mqttuser0004
|
||||
# password=mqttuser0004,
|
||||
# passhash={MD5}2Br6pPDSEDIEvUlu9+s+MA==
|
||||
# base64passhash=e01ENX0yQnI2cFBEU0VESUV2VWx1OStzK01BPT0=
|
||||
dn:uid=mqttuser0004,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0004
|
||||
isEnabled: TRUE
|
||||
mqttPublishTopic: mqttuser0004/pub/1
|
||||
mqttPublishTopic: mqttuser0004/pub/+
|
||||
mqttPublishTopic: mqttuser0004/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0004/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0004/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0004/sub/#
|
||||
mqttPubSubTopic: mqttuser0004/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0004/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0004/pubsub/#
|
||||
userPassword: {MD5}2Br6pPDSEDIEvUlu9+s+MA==
|
||||
|
||||
## create user mqttuser0005
|
||||
# password=mqttuser0005,
|
||||
# passhash={SHA}jKnxeEDGR14kE8AR7yuVFOelhz4=
|
||||
# base64passhash=e1NIQX1qS254ZUVER1IxNGtFOEFSN3l1VkZPZWxoejQ9
|
||||
objectClass: top
|
||||
dn:uid=mqttuser0005,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0005
|
||||
isEnabled: TRUE
|
||||
mqttPublishTopic: mqttuser0005/pub/1
|
||||
mqttPublishTopic: mqttuser0005/pub/+
|
||||
mqttPublishTopic: mqttuser0005/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0005/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0005/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0005/sub/#
|
||||
mqttPubSubTopic: mqttuser0005/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0005/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0005/pubsub/#
|
||||
userPassword: {SHA}jKnxeEDGR14kE8AR7yuVFOelhz4=
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#
|
||||
# Preliminary Apple OS X Native LDAP Schema
|
||||
# This file is subject to change.
|
||||
#
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.1.3 NAME 'isEnabled'
|
||||
EQUALITY booleanMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
|
||||
SINGLE-VALUE
|
||||
USAGE userApplications )
|
||||
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.1 NAME ( 'mqttPublishTopic' 'mpt' )
|
||||
EQUALITY caseIgnoreMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications )
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.2 NAME ( 'mqttSubscriptionTopic' 'mst' )
|
||||
EQUALITY caseIgnoreMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications )
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.3 NAME ( 'mqttPubSubTopic' 'mpst' )
|
||||
EQUALITY caseIgnoreMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications )
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.4 NAME ( 'mqttAccountName' 'man' )
|
||||
EQUALITY caseIgnoreMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications )
|
||||
|
||||
|
||||
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4 NAME 'mqttUser'
|
||||
AUXILIARY
|
||||
MAY ( mqttPublishTopic $ mqttSubscriptionTopic $ mqttPubSubTopic $ mqttAccountName) )
|
||||
|
||||
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.2 NAME 'mqttDevice'
|
||||
SUP top
|
||||
STRUCTURAL
|
||||
MUST ( uid )
|
||||
MAY ( isEnabled ) )
|
||||
|
||||
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.3 NAME 'mqttSecurity'
|
||||
SUP top
|
||||
AUXILIARY
|
||||
MAY ( userPassword $ userPKCS12 $ pwdAttribute $ pwdLockout ) )
|
|
@ -0,0 +1,195 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-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").
|
||||
-include_lib("eldap/include/eldap.hrl").
|
||||
|
||||
-define(LDAP_HOST, "ldap").
|
||||
-define(LDAP_RESOURCE_MOD, emqx_ldap).
|
||||
|
||||
all() ->
|
||||
[
|
||||
{group, tcp},
|
||||
{group, ssl}
|
||||
].
|
||||
|
||||
groups() ->
|
||||
Cases = emqx_common_test_helpers:all(?MODULE),
|
||||
[
|
||||
{tcp, Cases},
|
||||
{ssl, Cases}
|
||||
].
|
||||
|
||||
init_per_group(Group, Config) ->
|
||||
[{group, Group} | Config].
|
||||
|
||||
end_per_group(_, Config) ->
|
||||
proplists:delete(group, Config).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
Port = port(tcp),
|
||||
case emqx_common_test_helpers:is_tcp_server_available(?LDAP_HOST, 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(Config)
|
||||
).
|
||||
|
||||
perform_lifecycle_check(ResourceId, InitialConfig) ->
|
||||
{ok, #{config := CheckedConfig}} =
|
||||
emqx_resource:check_config(?LDAP_RESOURCE_MOD, InitialConfig),
|
||||
{ok, #{
|
||||
state := #{pool_name := PoolName} = State,
|
||||
status := InitialStatus
|
||||
}} = emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?CONNECTOR_RESOURCE_GROUP,
|
||||
?LDAP_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, [#eldap_entry{attributes = [_, _ | _]}]},
|
||||
emqx_resource:query(ResourceId, test_query_no_attr())
|
||||
),
|
||||
?assertMatch(
|
||||
{ok, [#eldap_entry{attributes = [{"mqttAccountName", _}]}]},
|
||||
emqx_resource:query(ResourceId, test_query_with_attr())
|
||||
),
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
emqx_resource:query(
|
||||
ResourceId,
|
||||
test_query_with_attr_and_timeout()
|
||||
)
|
||||
),
|
||||
?assertMatch({ok, []}, emqx_resource:query(ResourceId, test_query_not_exists())),
|
||||
?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, _}, emqx_resource:query(ResourceId, test_query_no_attr())),
|
||||
?assertMatch({ok, _}, emqx_resource:query(ResourceId, test_query_with_attr())),
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
emqx_resource:query(
|
||||
ResourceId,
|
||||
test_query_with_attr_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(Config) ->
|
||||
RawConfig = list_to_binary(
|
||||
io_lib:format(
|
||||
""
|
||||
"\n"
|
||||
" auto_reconnect = true\n"
|
||||
" username= \"cn=root,dc=emqx,dc=io\"\n"
|
||||
" password = public\n"
|
||||
" pool_size = 8\n"
|
||||
" server = \"~s:~b\"\n"
|
||||
" base_object=\"uid=${username},ou=testdevice,dc=emqx,dc=io\"\n"
|
||||
" filter =\"(objectClass=mqttUser)\"\n"
|
||||
" ~ts\n"
|
||||
"",
|
||||
[?LDAP_HOST, port(Config), ssl(Config)]
|
||||
)
|
||||
),
|
||||
|
||||
{ok, LDConfig} = hocon:binary(RawConfig),
|
||||
#{<<"config">> => LDConfig}.
|
||||
|
||||
test_query_no_attr() ->
|
||||
{query, data()}.
|
||||
|
||||
test_query_with_attr() ->
|
||||
{query, data(), ["mqttAccountName"]}.
|
||||
|
||||
test_query_with_attr_and_timeout() ->
|
||||
{query, data(), ["mqttAccountName"], 5000}.
|
||||
|
||||
test_query_not_exists() ->
|
||||
{query, #{username => <<"not_exists">>}}.
|
||||
|
||||
data() ->
|
||||
#{username => <<"mqttuser0001">>}.
|
||||
|
||||
port(tcp) -> 389;
|
||||
port(ssl) -> 636;
|
||||
port(Config) -> port(proplists:get_value(group, Config)).
|
||||
|
||||
ssl(Config) ->
|
||||
case proplists:get_value(group, Config) of
|
||||
tcp ->
|
||||
"ssl.enable=false";
|
||||
ssl ->
|
||||
"ssl.enable=true\n"
|
||||
"ssl.cacertfile=\"etc/openldap/cacert.pem\""
|
||||
end.
|
|
@ -0,0 +1,234 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ldap_filter_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("stdlib/include/assert.hrl").
|
||||
|
||||
-import(eldap, [
|
||||
'and'/1,
|
||||
'or'/1,
|
||||
'not'/1,
|
||||
equalityMatch/2,
|
||||
substrings/2,
|
||||
present/1,
|
||||
greaterOrEqual/2,
|
||||
lessOrEqual/2,
|
||||
approxMatch/2,
|
||||
extensibleMatch/2
|
||||
]).
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
_ = application:stop(emqx_connector).
|
||||
|
||||
% %%------------------------------------------------------------------------------
|
||||
% %% Testcases
|
||||
% %%------------------------------------------------------------------------------
|
||||
|
||||
t_and(_Config) ->
|
||||
?assertEqual('and'([equalityMatch("a", "1")]), parse("(&(a=1))")),
|
||||
?assertEqual(
|
||||
'and'([equalityMatch("a", "1"), (equalityMatch("b", "2"))]),
|
||||
parse("(&(a=1)(b=2))")
|
||||
),
|
||||
?assertMatch({error, _}, scan_and_parse("(&)")).
|
||||
|
||||
t_or(_Config) ->
|
||||
?assertEqual('or'([equalityMatch("a", "1")]), parse("(|(a=1))")),
|
||||
?assertEqual(
|
||||
'or'([equalityMatch("a", "1"), (equalityMatch("b", "2"))]),
|
||||
parse("(|(a=1)(b=2))")
|
||||
),
|
||||
?assertMatch({error, _}, scan_and_parse("(|)")).
|
||||
|
||||
t_not(_Config) ->
|
||||
?assertEqual('not'(equalityMatch("a", "1")), parse("(!(a=1))")),
|
||||
?assertMatch({error, _}, scan_and_parse("(!)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(!(a=1)(b=1))")).
|
||||
|
||||
t_equalityMatch(_Config) ->
|
||||
?assertEqual(equalityMatch("attr", "value"), parse("(attr=value)")),
|
||||
?assertEqual(equalityMatch("attr", "value"), parse("(attr = value)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(attr=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(=value)")).
|
||||
|
||||
t_substrings_initial(_Config) ->
|
||||
?assertEqual(substrings("attr", [{initial, "initial"}]), parse("(attr=initial*)")),
|
||||
?assertEqual(
|
||||
substrings("attr", [{initial, "initial"}, {any, "a"}]),
|
||||
parse("(attr=initial*a*)")
|
||||
),
|
||||
?assertEqual(
|
||||
substrings("attr", [{initial, "initial"}, {any, "a"}, {any, "b"}]),
|
||||
parse("(attr=initial*a*b*)")
|
||||
).
|
||||
|
||||
t_substrings_final(_Config) ->
|
||||
?assertEqual(substrings("attr", [{final, "final"}]), parse("(attr=*final)")),
|
||||
?assertEqual(
|
||||
substrings("attr", [{any, "a"}, {final, "final"}]),
|
||||
parse("(attr=*a*final)")
|
||||
),
|
||||
?assertEqual(
|
||||
substrings("attr", [{any, "a"}, {any, "b"}, {final, "final"}]),
|
||||
parse("(attr=*a*b*final)")
|
||||
).
|
||||
|
||||
t_substrings_initial_final(_Config) ->
|
||||
?assertEqual(
|
||||
substrings("attr", [{initial, "initial"}, {final, "final"}]),
|
||||
parse("(attr=initial*final)")
|
||||
),
|
||||
?assertEqual(
|
||||
substrings("attr", [{initial, "initial"}, {any, "a"}, {final, "final"}]),
|
||||
parse("(attr=initial*a*final)")
|
||||
),
|
||||
?assertEqual(
|
||||
substrings(
|
||||
"attr",
|
||||
[{initial, "initial"}, {any, "a"}, {any, "b"}, {final, "final"}]
|
||||
),
|
||||
parse("(attr=initial*a*b*final)")
|
||||
).
|
||||
|
||||
t_substrings_only_any(_Config) ->
|
||||
?assertEqual(present("attr"), parse("(attr=*)")),
|
||||
?assertEqual(substrings("attr", [{any, "a"}]), parse("(attr=*a*)")),
|
||||
?assertEqual(
|
||||
substrings("attr", [{any, "a"}, {any, "b"}]),
|
||||
parse("(attr=*a*b*)")
|
||||
).
|
||||
|
||||
t_greaterOrEqual(_Config) ->
|
||||
?assertEqual(greaterOrEqual("attr", "value"), parse("(attr>=value)")),
|
||||
?assertEqual(greaterOrEqual("attr", "value"), parse("(attr >= value )")),
|
||||
?assertMatch({error, _}, scan_and_parse("(attr>=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(>=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(>=value)")).
|
||||
|
||||
t_lessOrEqual(_Config) ->
|
||||
?assertEqual(lessOrEqual("attr", "value"), parse("(attr<=value)")),
|
||||
?assertEqual(lessOrEqual("attr", "value"), parse("( attr <= value )")),
|
||||
?assertMatch({error, _}, scan_and_parse("(attr<=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(<=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(<=value)")).
|
||||
|
||||
t_present(_Config) ->
|
||||
?assertEqual(present("attr"), parse("(attr=*)")),
|
||||
?assertEqual(present("attr"), parse("( attr = * )")).
|
||||
|
||||
t_approxMatch(_Config) ->
|
||||
?assertEqual(approxMatch("attr", "value"), parse("(attr~=value)")),
|
||||
?assertEqual(approxMatch("attr", "value"), parse("( attr ~= value )")),
|
||||
?assertMatch({error, _}, scan_and_parse("(attr~=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(~=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(~=value)")).
|
||||
|
||||
t_extensibleMatch_dn(_Config) ->
|
||||
?assertEqual(
|
||||
extensibleMatch("value", [{type, "attr"}, {dnAttributes, true}]), parse("(attr:dn:=value)")
|
||||
),
|
||||
?assertEqual(
|
||||
extensibleMatch("value", [{type, "attr"}, {dnAttributes, true}]),
|
||||
parse("( attr:dn := value )")
|
||||
).
|
||||
|
||||
t_extensibleMatch_rule(_Config) ->
|
||||
?assertEqual(
|
||||
extensibleMatch("value", [{type, "attr"}, {matchingRule, "objectClass"}]),
|
||||
parse("(attr:objectClass:=value)")
|
||||
),
|
||||
?assertEqual(
|
||||
extensibleMatch("value", [{type, "attr"}, {matchingRule, "objectClass"}]),
|
||||
parse("( attr:objectClass := value )")
|
||||
).
|
||||
|
||||
t_extensibleMatch_dn_rule(_Config) ->
|
||||
?assertEqual(
|
||||
extensibleMatch(
|
||||
"value",
|
||||
[
|
||||
{type, "attr"},
|
||||
{dnAttributes, true},
|
||||
{matchingRule, "objectClass"}
|
||||
]
|
||||
),
|
||||
parse("(attr:dn:objectClass:=value)")
|
||||
),
|
||||
?assertEqual(
|
||||
extensibleMatch(
|
||||
"value",
|
||||
[
|
||||
{type, "attr"},
|
||||
{dnAttributes, true},
|
||||
{matchingRule, "objectClass"}
|
||||
]
|
||||
),
|
||||
parse("( attr:dn:objectClass :=value)")
|
||||
).
|
||||
|
||||
t_extensibleMatch_no_dn_rule(_Config) ->
|
||||
?assertEqual(extensibleMatch("value", [{type, "attr"}]), parse("(attr:=value)")),
|
||||
?assertEqual(extensibleMatch("value", [{type, "attr"}]), parse("( attr := value )")).
|
||||
|
||||
t_extensibleMatch_no_type_dn(_Config) ->
|
||||
?assertEqual(
|
||||
extensibleMatch("value", [{matchingRule, "objectClass"}]),
|
||||
parse("(:objectClass:=value)")
|
||||
),
|
||||
?assertEqual(
|
||||
extensibleMatch("value", [{matchingRule, "objectClass"}]),
|
||||
parse("( :objectClass := value )")
|
||||
).
|
||||
|
||||
t_extensibleMatch_no_type_no_dn(_Config) ->
|
||||
?assertEqual(
|
||||
extensibleMatch(
|
||||
"value",
|
||||
[{dnAttributes, true}, {matchingRule, "objectClass"}]
|
||||
),
|
||||
parse("(:dn:objectClass:=value)")
|
||||
),
|
||||
?assertEqual(
|
||||
extensibleMatch(
|
||||
"value",
|
||||
[{dnAttributes, true}, {matchingRule, "objectClass"}]
|
||||
),
|
||||
parse("( :dn:objectClass :=value)")
|
||||
).
|
||||
|
||||
t_extensibleMatch_error(_Config) ->
|
||||
?assertMatch({error, _}, scan_and_parse("(:dn:=value)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(::=value)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(:=)")),
|
||||
?assertMatch({error, _}, scan_and_parse("(attr:=)")).
|
||||
|
||||
t_error(_Config) ->
|
||||
?assertMatch({error, _}, scan_and_parse("(attr=value")),
|
||||
?assertMatch({error, _}, scan_and_parse("attr=value")),
|
||||
?assertMatch({error, _}, scan_and_parse("(a=b)(c=d)")).
|
||||
|
||||
% %%------------------------------------------------------------------------------
|
||||
% %% Helpers
|
||||
% %%------------------------------------------------------------------------------
|
||||
parse(Str) ->
|
||||
{ok, Res} = scan_and_parse(Str),
|
||||
Res.
|
||||
|
||||
scan_and_parse(Str) ->
|
||||
emqx_ldap_filter_parser:scan_and_parse(Str).
|
|
@ -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"""
|
||||
|
||||
}
|
|
@ -225,6 +225,9 @@ for dep in ${CT_DEPS}; do
|
|||
greptimedb)
|
||||
FILES+=( '.ci/docker-compose-file/docker-compose-greptimedb.yaml' )
|
||||
;;
|
||||
ldap)
|
||||
FILES+=( '.ci/docker-compose-file/docker-compose-ldap.yaml' )
|
||||
;;
|
||||
*)
|
||||
echo "unknown_ct_dependency $dep"
|
||||
exit 1
|
||||
|
|
Loading…
Reference in New Issue