From f98f97f37e460fce82894670dfe5e5d11cc11f04 Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 1 Aug 2023 18:48:43 +0800 Subject: [PATCH] feat(ldap): set test env and add test suites --- ...ldap-tcp.yaml => docker-compose-ldap.yaml} | 8 +- .ci/docker-compose-file/openldap/Dockerfile | 16 +- apps/emqx_ldap/docker-ct | 2 +- apps/emqx_ldap/src/emqx_ldap.erl | 26 +- apps/emqx_ldap/src/emqx_ldap_filter_lexer.xrl | 4 +- .../emqx_ldap/src/emqx_ldap_filter_parser.yrl | 8 +- apps/emqx_ldap/test/data/emqx.io.ldif | 135 ++++++++++ apps/emqx_ldap/test/data/emqx.schema | 46 ++++ apps/emqx_ldap/test/emqx_ldap_SUITE.erl | 103 +++++--- .../emqx_ldap/test/emqx_ldap_filter_SUITE.erl | 245 ++++++++++++++++++ scripts/ct/run.sh | 3 + 11 files changed, 533 insertions(+), 63 deletions(-) rename .ci/docker-compose-file/{docker-compose-ldap-tcp.yaml => docker-compose-ldap.yaml} (80%) create mode 100644 apps/emqx_ldap/test/data/emqx.io.ldif create mode 100644 apps/emqx_ldap/test/data/emqx.schema create mode 100644 apps/emqx_ldap/test/emqx_ldap_filter_SUITE.erl diff --git a/.ci/docker-compose-file/docker-compose-ldap-tcp.yaml b/.ci/docker-compose-file/docker-compose-ldap.yaml similarity index 80% rename from .ci/docker-compose-file/docker-compose-ldap-tcp.yaml rename to .ci/docker-compose-file/docker-compose-ldap.yaml index 61eab91ec..e6c8ba2d8 100644 --- a/.ci/docker-compose-file/docker-compose-ldap-tcp.yaml +++ b/.ci/docker-compose-file/docker-compose-ldap.yaml @@ -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 diff --git a/.ci/docker-compose-file/openldap/Dockerfile b/.ci/docker-compose-file/openldap/Dockerfile index 88a096066..dd0114b64 100644 --- a/.ci/docker-compose-file/openldap/Dockerfile +++ b/.ci/docker-compose-file/openldap/Dockerfile @@ -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 diff --git a/apps/emqx_ldap/docker-ct b/apps/emqx_ldap/docker-ct index 8b1378917..c1142c3c5 100644 --- a/apps/emqx_ldap/docker-ct +++ b/apps/emqx_ldap/docker-ct @@ -1 +1 @@ - +ldap diff --git a/apps/emqx_ldap/src/emqx_ldap.erl b/apps/emqx_ldap/src/emqx_ldap.erl index 390c34501..173edbe11 100644 --- a/apps/emqx_ldap/src/emqx_ldap.erl +++ b/apps/emqx_ldap/src/emqx_ldap.erl @@ -58,7 +58,8 @@ fields(config) -> desc => ?DESC(base_object), required => true })}, - {filter, ?HOCON(binary(), #{desc => ?DESC(filter), default => ""})}, + {filter, + ?HOCON(binary(), #{desc => ?DESC(filter), default => <<"(objectClass=mqttUser)">>})}, {auto_reconnect, fun ?ECS:auto_reconnect/1} ] ++ emqx_connector_schema_lib:ssl_fields(). @@ -143,10 +144,10 @@ do_get_status(Conn) -> %% =================================================================== connect(Options) -> - #{host := Host, username := Username, password := Password} = + #{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 + case eldap:open([Host], [{log, fun log/3} | OpenOpts]) of {ok, Handle} = Ret -> case eldap:simple_bind(Handle, Username, Password) of ok -> Ret; @@ -167,7 +168,7 @@ on_query( [] -> do_ldap_query(InstId, [{base, Base} | SearchOptions], State); _ -> - FilterBin = emqx_placeholder:proc_tmpl(FilterTks, Data, #{return => rawlist}), + FilterBin = emqx_placeholder:proc_tmpl(FilterTks, Data), case emqx_ldap_filter_parser:scan_and_parse(FilterBin) of {ok, Filter} -> do_ldap_query( @@ -195,7 +196,7 @@ do_ldap_query( case ecpool:pick_and_do( PoolName, - {eldap, search, SearchOptions}, + {eldap, search, [SearchOptions]}, handover ) of @@ -204,14 +205,9 @@ do_ldap_query( ldap_connector_query_return, #{result => Result} ), - {ok, - case Result#eldap_search_result.entries of - [First | _] -> - %% TODO Support multi entries? - First; - _ -> - undefined - end}; + {ok, Result#eldap_search_result.entries}; + {error, noSuchObject} -> + {ok, []}; {error, Reason} -> ?SLOG( error, @@ -235,4 +231,6 @@ prepare_template(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(T, State#{filter_tokens => emqx_placeholder:preproc_tmpl(V)}); +do_prepare_template([], State) -> + State. diff --git a/apps/emqx_ldap/src/emqx_ldap_filter_lexer.xrl b/apps/emqx_ldap/src/emqx_ldap_filter_lexer.xrl index e30531e2a..5c0e178d6 100644 --- a/apps/emqx_ldap/src/emqx_ldap_filter_lexer.xrl +++ b/apps/emqx_ldap/src/emqx_ldap_filter_lexer.xrl @@ -1,9 +1,9 @@ Definitions. Control = [()&|!=~><:*] -NonControl = [^()&|!=~><:*] -String = {NonControl}* White = [\s\t\n\r]+ +NonString = [^()&|!=~><:*\s\t\n\r] +String = {NonString}+ Rules. diff --git a/apps/emqx_ldap/src/emqx_ldap_filter_parser.yrl b/apps/emqx_ldap/src/emqx_ldap_filter_parser.yrl index 266d126c1..84e412f21 100644 --- a/apps/emqx_ldap/src/emqx_ldap_filter_parser.yrl +++ b/apps/emqx_ldap/src/emqx_ldap_filter_parser.yrl @@ -16,12 +16,12 @@ filtercomp -> filtercomp -> 'or' filterlist: 'or'('$2'). filtercomp -> - 'not' filterlist: 'not'('$2'). + 'not' filter: 'not'('$2'). filtercomp -> item: '$1'. filterlist -> - filter: '$1'. + filter: ['$1']. filterlist -> filter filterlist: ['$1' | '$2']. @@ -71,7 +71,7 @@ extensible -> extensible -> type matchingrule colon equal value: extensible('$5', ['$1', '$2']). extensible -> - type colon equal value: extensible('$4', []). + type colon equal value: extensible('$4', ['$1']). extensible -> dnattrs matchingrule colon equal value: extensible('$5', ['$1', '$2']). @@ -125,7 +125,7 @@ substrings(Attr, List) -> eldap:substrings(Attr, flatten(List)). 'any'(List, Item) -> - [{any, Item} | List]. + [List, {any, Item}]. extensible(Value, Opts) -> eldap:extensibleMatch(Value, Opts). diff --git a/apps/emqx_ldap/test/data/emqx.io.ldif b/apps/emqx_ldap/test/data/emqx.io.ldif new file mode 100644 index 000000000..f9833cd88 --- /dev/null +++ b/apps/emqx_ldap/test/data/emqx.io.ldif @@ -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= + diff --git a/apps/emqx_ldap/test/data/emqx.schema b/apps/emqx_ldap/test/data/emqx.schema new file mode 100644 index 000000000..55f92269b --- /dev/null +++ b/apps/emqx_ldap/test/data/emqx.schema @@ -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 ) ) diff --git a/apps/emqx_ldap/test/emqx_ldap_SUITE.erl b/apps/emqx_ldap/test/emqx_ldap_SUITE.erl index 515f8b250..8a2b67929 100644 --- a/apps/emqx_ldap/test/emqx_ldap_SUITE.erl +++ b/apps/emqx_ldap/test/emqx_ldap_SUITE.erl @@ -22,18 +22,33 @@ -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(MYSQL_HOST, "ldap"). --define(MYSQL_RESOURCE_MOD, emqx_ldap). +-define(LDAP_HOST, "ldap"). +-define(LDAP_RESOURCE_MOD, emqx_ldap). all() -> - emqx_common_test_helpers:all(?MODULE). + [ + {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) -> - case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of + 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]), @@ -58,22 +73,22 @@ end_per_testcase(_, _Config) -> % %% Testcases % %%------------------------------------------------------------------------------ -t_lifecycle(_Config) -> +t_lifecycle(Config) -> perform_lifecycle_check( <<"emqx_ldap_SUITE">>, - ldap_config() + ldap_config(Config) ). perform_lifecycle_check(ResourceId, InitialConfig) -> {ok, #{config := CheckedConfig}} = - emqx_resource:check_config(?MYSQL_RESOURCE_MOD, InitialConfig), + emqx_resource:check_config(?LDAP_RESOURCE_MOD, InitialConfig), {ok, #{ state := #{pool_name := PoolName} = State, status := InitialStatus }} = emqx_resource:create_local( ResourceId, ?CONNECTOR_RESOURCE_GROUP, - ?MYSQL_RESOURCE_MOD, + ?LDAP_RESOURCE_MOD, CheckedConfig, #{} ), @@ -86,15 +101,22 @@ perform_lifecycle_check(ResourceId, InitialConfig) -> 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]]}, + {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_params_and_timeout() + 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. @@ -116,13 +138,13 @@ perform_lifecycle_check(ResourceId, InitialConfig) -> {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, _}, emqx_resource:query(ResourceId, test_query_no_attr())), + ?assertMatch({ok, _}, emqx_resource:query(ResourceId, test_query_with_attr())), ?assertMatch( - {ok, _, [[1]]}, + {ok, _}, emqx_resource:query( ResourceId, - test_query_with_params_and_timeout() + test_query_with_attr_and_timeout() ) ), % Stop and remove the resource in one go. @@ -134,32 +156,51 @@ perform_lifecycle_check(ResourceId, InitialConfig) -> % %%------------------------------------------------------------------------------ % %% Helpers % %%------------------------------------------------------------------------------ - -ldap_config() -> +ldap_config(Config) -> RawConfig = list_to_binary( io_lib:format( "" "\n" " auto_reconnect = true\n" - " database = mqtt\n" - " username= root\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" "", - [?MYSQL_HOST, ?MYSQL_DEFAULT_PORT] + [?LDAP_HOST, port(Config), ssl(Config)] ) ), - {ok, Config} = hocon:binary(RawConfig), - #{<<"config">> => Config}. + {ok, LDConfig} = hocon:binary(RawConfig), + #{<<"config">> => LDConfig}. -test_query_no_params() -> - {sql, <<"SELECT 1">>}. +test_query_no_attr() -> + {query, data()}. -test_query_with_params() -> - {sql, <<"SELECT ?">>, [1]}. +test_query_with_attr() -> + {query, data(), ["mqttAccountName"]}. -test_query_with_params_and_timeout() -> - {sql, <<"SELECT ?">>, [1], 1000}. +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. diff --git a/apps/emqx_ldap/test/emqx_ldap_filter_SUITE.erl b/apps/emqx_ldap/test/emqx_ldap_filter_SUITE.erl new file mode 100644 index 000000000..8ea322ff1 --- /dev/null +++ b/apps/emqx_ldap/test/emqx_ldap_filter_SUITE.erl @@ -0,0 +1,245 @@ +% %%-------------------------------------------------------------------- +% %% 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_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). diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index 578b9c4de..5ad289303 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -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