Kaynağa Gözat

Merge pull request #11386 from lafirest/feat/ldap_authn

feat(authn): integrate the LDAP authentication
lafirest 2 yıl önce
ebeveyn
işleme
1b0b15786c

+ 1 - 1
apps/emqx_authn/src/emqx_authn.app.src

@@ -1,7 +1,7 @@
 %% -*- mode: erlang -*-
 {application, emqx_authn, [
     {description, "EMQX Authentication"},
-    {vsn, "0.1.23"},
+    {vsn, "0.1.24"},
     {modules, []},
     {registered, [emqx_authn_sup, emqx_authn_registry]},
     {applications, [

+ 2 - 1
apps/emqx_authn/src/emqx_authn.erl

@@ -40,7 +40,8 @@ providers() ->
         {{password_based, http}, emqx_authn_http},
         {jwt, emqx_authn_jwt},
         {{scram, built_in_database}, emqx_enhanced_authn_scram_mnesia}
-    ].
+    ] ++
+        emqx_authn_enterprise:providers().
 
 check_config(Config) ->
     check_config(Config, #{}).

+ 2 - 1
apps/emqx_authn/src/emqx_authn_api.erl

@@ -876,7 +876,8 @@ resource_provider() ->
         emqx_authn_mongodb,
         emqx_authn_redis,
         emqx_authn_http
-    ].
+    ] ++
+        emqx_authn_enterprise:resource_provider().
 
 lookup_from_local_node(ChainName, AuthenticatorID) ->
     NodeId = node(self()),

+ 24 - 0
apps/emqx_authn/src/emqx_authn_enterprise.erl

@@ -0,0 +1,24 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_authn_enterprise).
+
+-export([providers/0, resource_provider/0]).
+
+-if(?EMQX_RELEASE_EDITION == ee).
+
+providers() ->
+    [{{password_based, ldap}, emqx_ldap_authn}].
+
+resource_provider() ->
+    [emqx_ldap_authn].
+
+-else.
+
+providers() ->
+    [].
+
+resource_provider() ->
+    [].
+-endif.

+ 38 - 42
apps/emqx_authn/src/emqx_authn_utils.erl

@@ -35,7 +35,8 @@
     ensure_apps_started/1,
     cleanup_resources/0,
     make_resource_id/1,
-    without_password/1
+    without_password/1,
+    to_bool/1
 ]).
 
 -define(AUTHN_PLACEHOLDERS, [
@@ -144,47 +145,8 @@ render_sql_params(ParamList, Credential) ->
         #{return => rawlist, var_trans => fun handle_sql_var/2}
     ).
 
-%% true
-is_superuser(#{<<"is_superuser">> := <<"true">>}) ->
-    #{is_superuser => true};
-is_superuser(#{<<"is_superuser">> := true}) ->
-    #{is_superuser => true};
-is_superuser(#{<<"is_superuser">> := <<"1">>}) ->
-    #{is_superuser => true};
-is_superuser(#{<<"is_superuser">> := I}) when
-    is_integer(I) andalso I >= 1
-->
-    #{is_superuser => true};
-%% false
-is_superuser(#{<<"is_superuser">> := <<"">>}) ->
-    #{is_superuser => false};
-is_superuser(#{<<"is_superuser">> := <<"0">>}) ->
-    #{is_superuser => false};
-is_superuser(#{<<"is_superuser">> := 0}) ->
-    #{is_superuser => false};
-is_superuser(#{<<"is_superuser">> := null}) ->
-    #{is_superuser => false};
-is_superuser(#{<<"is_superuser">> := undefined}) ->
-    #{is_superuser => false};
-is_superuser(#{<<"is_superuser">> := <<"false">>}) ->
-    #{is_superuser => false};
-is_superuser(#{<<"is_superuser">> := false}) ->
-    #{is_superuser => false};
-is_superuser(#{<<"is_superuser">> := MaybeBinInt}) when
-    is_binary(MaybeBinInt)
-->
-    try binary_to_integer(MaybeBinInt) of
-        Int when Int >= 1 ->
-            #{is_superuser => true};
-        Int when Int =< 0 ->
-            #{is_superuser => false}
-    catch
-        error:badarg ->
-            #{is_superuser => false}
-    end;
-%% fallback to default
-is_superuser(#{<<"is_superuser">> := _}) ->
-    #{is_superuser => false};
+is_superuser(#{<<"is_superuser">> := Value}) ->
+    #{is_superuser => to_bool(Value)};
 is_superuser(#{}) ->
     #{is_superuser => false}.
 
@@ -211,6 +173,40 @@ make_resource_id(Name) ->
 without_password(Credential) ->
     without_password(Credential, [password, <<"password">>]).
 
+to_bool(<<"true">>) ->
+    true;
+to_bool(true) ->
+    true;
+to_bool(<<"1">>) ->
+    true;
+to_bool(I) when is_integer(I) andalso I >= 1 ->
+    true;
+%% false
+to_bool(<<"">>) ->
+    false;
+to_bool(<<"0">>) ->
+    false;
+to_bool(0) ->
+    false;
+to_bool(null) ->
+    false;
+to_bool(undefined) ->
+    false;
+to_bool(<<"false">>) ->
+    false;
+to_bool(false) ->
+    false;
+to_bool(MaybeBinInt) when is_binary(MaybeBinInt) ->
+    try
+        binary_to_integer(MaybeBinInt) >= 1
+    catch
+        error:badarg ->
+            false
+    end;
+%% fallback to default
+to_bool(_) ->
+    false.
+
 %%--------------------------------------------------------------------
 %% Internal functions
 %%--------------------------------------------------------------------

+ 0 - 134
apps/emqx_authn/test/data/emqx.io.ldif

@@ -1,134 +0,0 @@
-## 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 - 46
apps/emqx_authn/test/data/emqx.schema

@@ -1,46 +0,0 @@
-#
-# 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 ) )

+ 1 - 1
apps/emqx_connector/src/emqx_connector.app.src

@@ -1,7 +1,7 @@
 %% -*- mode: erlang -*-
 {application, emqx_connector, [
     {description, "EMQX Data Integration Connectors"},
-    {vsn, "0.1.28"},
+    {vsn, "0.1.29"},
     {registered, []},
     {mod, {emqx_connector_app, []}},
     {applications, [

+ 0 - 199
apps/emqx_connector/src/emqx_connector_ldap.erl

@@ -1,199 +0,0 @@
-%%--------------------------------------------------------------------
-%% 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_connector_ldap).
-
--include("emqx_connector.hrl").
--include_lib("typerefl/include/types.hrl").
--include_lib("emqx/include/logger.hrl").
-
--export([roots/0, fields/1]).
-
--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
-]).
-
--export([connect/1]).
-
--export([search/4]).
-
-%% port is not expected from configuration because
-%% all servers expected to use the same port number
--define(LDAP_HOST_OPTIONS, #{no_port => true}).
-
-%%=====================================================================
-roots() ->
-    ldap_fields() ++ emqx_connector_schema_lib:ssl_fields().
-
-%% this schema has no sub-structs
-fields(_) -> [].
-
-%% ===================================================================
-callback_mode() -> always_sync.
-
-on_start(
-    InstId,
-    #{
-        servers := Servers0,
-        port := Port,
-        bind_dn := BindDn,
-        bind_password := BindPassword,
-        timeout := Timeout,
-        pool_size := PoolSize,
-        ssl := SSL
-    } = Config
-) ->
-    ?SLOG(info, #{
-        msg => "starting_ldap_connector",
-        connector => InstId,
-        config => emqx_utils:redact(Config)
-    }),
-    Servers1 = emqx_schema:parse_servers(Servers0, ?LDAP_HOST_OPTIONS),
-    Servers =
-        lists:map(
-            fun
-                (#{hostname := Host, port := Port0}) ->
-                    {Host, Port0};
-                (#{hostname := Host}) ->
-                    Host
-            end,
-            Servers1
-        ),
-    SslOpts =
-        case maps:get(enable, SSL) of
-            true ->
-                [
-                    {ssl, true},
-                    {sslopts, emqx_tls_lib:to_client_opts(SSL)}
-                ];
-            false ->
-                [{ssl, false}]
-        end,
-    Opts = [
-        {servers, Servers},
-        {port, Port},
-        {bind_dn, BindDn},
-        {bind_password, BindPassword},
-        {timeout, Timeout},
-        {pool_size, PoolSize},
-        {auto_reconnect, ?AUTO_RECONNECT_INTERVAL}
-    ],
-    case emqx_resource_pool:start(InstId, ?MODULE, Opts ++ SslOpts) of
-        ok -> {ok, #{pool_name => InstId}};
-        {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, {search, Base, Filter, Attributes}, #{pool_name := PoolName} = State) ->
-    Request = {Base, Filter, Attributes},
-    ?TRACE(
-        "QUERY",
-        "ldap_connector_received",
-        #{request => Request, connector => InstId, state => State}
-    ),
-    case
-        Result = ecpool:pick_and_do(
-            PoolName,
-            {?MODULE, search, [Base, Filter, Attributes]},
-            no_handover
-        )
-    of
-        {error, Reason} ->
-            ?SLOG(error, #{
-                msg => "ldap_connector_do_request_failed",
-                request => Request,
-                connector => InstId,
-                reason => Reason
-            }),
-            case Reason of
-                ecpool_empty ->
-                    {error, {recoverable_error, Reason}};
-                _ ->
-                    Result
-            end;
-        _ ->
-            Result
-    end.
-
-on_get_status(_InstId, _State) -> connected.
-
-search(Conn, Base, Filter, Attributes) ->
-    eldap2:search(Conn, [
-        {base, Base},
-        {filter, Filter},
-        {attributes, Attributes},
-        {deref, eldap2:'derefFindingBaseObj'()}
-    ]).
-
-%% ===================================================================
-connect(Opts) ->
-    Servers = proplists:get_value(servers, Opts, ["localhost"]),
-    Port = proplists:get_value(port, Opts, 389),
-    Timeout = proplists:get_value(timeout, Opts, 30),
-    BindDn = proplists:get_value(bind_dn, Opts),
-    BindPassword = proplists:get_value(bind_password, Opts),
-    SslOpts =
-        case proplists:get_value(ssl, Opts, false) of
-            true ->
-                [{sslopts, proplists:get_value(sslopts, Opts, [])}, {ssl, true}];
-            false ->
-                [{ssl, false}]
-        end,
-    LdapOpts =
-        [
-            {port, Port},
-            {timeout, Timeout}
-        ] ++ SslOpts,
-    {ok, LDAP} = eldap2:open(Servers, LdapOpts),
-    ok = eldap2:simple_bind(LDAP, BindDn, BindPassword),
-    {ok, LDAP}.
-
-ldap_fields() ->
-    [
-        {servers, servers()},
-        {port, fun port/1},
-        {pool_size, fun emqx_connector_schema_lib:pool_size/1},
-        {bind_dn, fun bind_dn/1},
-        {bind_password, fun emqx_connector_schema_lib:password/1},
-        {timeout, fun duration/1},
-        {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1}
-    ].
-
-servers() ->
-    emqx_schema:servers_sc(#{}, ?LDAP_HOST_OPTIONS).
-
-bind_dn(type) -> binary();
-bind_dn(default) -> 0;
-bind_dn(_) -> undefined.
-
-port(type) -> integer();
-port(default) -> 389;
-port(_) -> undefined.
-
-duration(type) -> emqx_schema:timeout_duration_ms();
-duration(_) -> undefined.

+ 2 - 1
apps/emqx_ldap/rebar.config

@@ -3,5 +3,6 @@
 {erl_opts, [debug_info]}.
 {deps, [
         {emqx_connector, {path, "../../apps/emqx_connector"}},
-        {emqx_resource, {path, "../../apps/emqx_resource"}}
+        {emqx_resource, {path, "../../apps/emqx_resource"}},
+        {emqx_authn, {path, "../../apps/emqx_authn"}}
 ]}.

+ 2 - 1
apps/emqx_ldap/src/emqx_ldap.app.src

@@ -4,7 +4,8 @@
     {registered, []},
     {applications, [
         kernel,
-        stdlib
+        stdlib,
+        emqx_authn
     ]},
     {env, []},
     {modules, []},

+ 25 - 23
apps/emqx_ldap/src/emqx_ldap.erl

@@ -57,11 +57,18 @@ fields(config) ->
         {base_object,
             ?HOCON(binary(), #{
                 desc => ?DESC(base_object),
-                required => true
+                required => true,
+                validator => fun emqx_schema:non_empty_string/1
             })},
         {filter,
-            ?HOCON(binary(), #{desc => ?DESC(filter), default => <<"(objectClass=mqttUser)">>})},
-        {auto_reconnect, fun ?ECS:auto_reconnect/1}
+            ?HOCON(
+                binary(),
+                #{
+                    desc => ?DESC(filter),
+                    default => <<"(objectClass=mqttUser)">>,
+                    validator => fun emqx_schema:non_empty_string/1
+                }
+            )}
     ] ++ emqx_connector_schema_lib:ssl_fields().
 
 server() ->
@@ -165,26 +172,21 @@ on_query(
     #{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
+    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.
 
 do_ldap_query(

+ 287 - 0
apps/emqx_ldap/src/emqx_ldap_authn.erl

@@ -0,0 +1,287 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_ldap_authn).
+
+-include_lib("emqx_authn/include/emqx_authn.hrl").
+-include_lib("emqx/include/logger.hrl").
+-include_lib("hocon/include/hoconsc.hrl").
+-include_lib("eldap/include/eldap.hrl").
+
+-behaviour(hocon_schema).
+-behaviour(emqx_authentication).
+
+%% a compatible attribute for version 4.x
+-define(ISENABLED_ATTR, "isEnabled").
+-define(VALID_ALGORITHMS, [md5, ssha, sha, sha256, sha384, sha512]).
+%% TODO
+%% 1. Supports more salt algorithms, SMD5 SSHA 256/384/512
+%% 2. Supports https://datatracker.ietf.org/doc/html/rfc3112
+
+-export([
+    namespace/0,
+    tags/0,
+    roots/0,
+    fields/1,
+    desc/1
+]).
+
+-export([
+    refs/0,
+    create/2,
+    update/2,
+    authenticate/2,
+    destroy/1
+]).
+
+-import(proplists, [get_value/2, get_value/3]).
+%%------------------------------------------------------------------------------
+%% Hocon Schema
+%%------------------------------------------------------------------------------
+
+namespace() -> "authn".
+
+tags() ->
+    [<<"Authentication">>].
+
+%% used for config check when the schema module is resolved
+roots() ->
+    [{?CONF_NS, hoconsc:mk(hoconsc:ref(?MODULE, mysql))}].
+
+fields(ldap) ->
+    [
+        {mechanism, emqx_authn_schema:mechanism(password_based)},
+        {backend, emqx_authn_schema:backend(ldap)},
+        {password_attribute, fun password_attribute/1},
+        {is_superuser_attribute, fun is_superuser_attribute/1},
+        {query_timeout, fun query_timeout/1}
+    ] ++ emqx_authn_schema:common_fields() ++ emqx_ldap:fields(config).
+
+desc(ldap) ->
+    ?DESC(ldap);
+desc(_) ->
+    undefined.
+
+password_attribute(type) -> string();
+password_attribute(desc) -> ?DESC(?FUNCTION_NAME);
+password_attribute(default) -> <<"userPassword">>;
+password_attribute(_) -> undefined.
+
+is_superuser_attribute(type) -> string();
+is_superuser_attribute(desc) -> ?DESC(?FUNCTION_NAME);
+is_superuser_attribute(default) -> <<"isSuperuser">>;
+is_superuser_attribute(_) -> undefined.
+
+query_timeout(type) -> emqx_schema:duration_ms();
+query_timeout(desc) -> ?DESC(?FUNCTION_NAME);
+query_timeout(default) -> <<"5s">>;
+query_timeout(_) -> undefined.
+
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
+refs() ->
+    [hoconsc:ref(?MODULE, ldap)].
+
+create(_AuthenticatorID, Config) ->
+    create(Config).
+
+create(Config0) ->
+    ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
+    {Config, State} = parse_config(Config0),
+    {ok, _Data} = emqx_authn_utils:create_resource(ResourceId, emqx_ldap, Config),
+    {ok, State#{resource_id => ResourceId}}.
+
+update(Config0, #{resource_id := ResourceId} = _State) ->
+    {Config, NState} = parse_config(Config0),
+    case emqx_authn_utils:update_resource(emqx_ldap, Config, ResourceId) of
+        {error, Reason} ->
+            error({load_config_error, Reason});
+        {ok, _} ->
+            {ok, NState#{resource_id => ResourceId}}
+    end.
+
+destroy(#{resource_id := ResourceId}) ->
+    _ = emqx_resource:remove_local(ResourceId),
+    ok.
+
+authenticate(#{auth_method := _}, _) ->
+    ignore;
+authenticate(
+    #{password := Password} = Credential,
+    #{
+        password_attribute := PasswordAttr,
+        is_superuser_attribute := IsSuperuserAttr,
+        query_timeout := Timeout,
+        resource_id := ResourceId
+    } = State
+) ->
+    case
+        emqx_resource:simple_sync_query(
+            ResourceId,
+            {query, Credential, [PasswordAttr, IsSuperuserAttr, ?ISENABLED_ATTR], Timeout}
+        )
+    of
+        {ok, []} ->
+            ignore;
+        {ok, [Entry | _]} ->
+            is_enabled(Password, Entry, State);
+        {error, Reason} ->
+            ?TRACE_AUTHN_PROVIDER(error, "ldap_query_failed", #{
+                resource => ResourceId,
+                timeout => Timeout,
+                reason => Reason
+            }),
+            ignore
+    end.
+
+parse_config(Config) ->
+    State = lists:foldl(
+        fun(Key, Acc) ->
+            Value =
+                case maps:get(Key, Config) of
+                    Bin when is_binary(Bin) ->
+                        erlang:binary_to_list(Bin);
+                    Any ->
+                        Any
+                end,
+            Acc#{Key => Value}
+        end,
+        #{},
+        [password_attribute, is_superuser_attribute, query_timeout]
+    ),
+    {Config, State}.
+
+%% To compatible v4.x
+is_enabled(Password, #eldap_entry{attributes = Attributes} = Entry, State) ->
+    IsEnabled = get_lower_bin_value(?ISENABLED_ATTR, Attributes, "true"),
+    case emqx_authn_utils:to_bool(IsEnabled) of
+        true ->
+            ensure_password(Password, Entry, State);
+        _ ->
+            {error, user_disabled}
+    end.
+
+ensure_password(
+    Password,
+    #eldap_entry{attributes = Attributes} = Entry,
+    #{password_attribute := PasswordAttr} = State
+) ->
+    case get_value(PasswordAttr, Attributes) of
+        undefined ->
+            {error, no_password};
+        [LDAPPassword | _] ->
+            extract_hash_algorithm(LDAPPassword, Password, fun try_decode_passowrd/4, Entry, State)
+    end.
+
+%% RFC 2307 format password
+%% https://datatracker.ietf.org/doc/html/rfc2307
+extract_hash_algorithm(LDAPPassword, Password, OnFail, Entry, State) ->
+    case
+        re:run(
+            LDAPPassword,
+            "{([^{}]+)}(.+)",
+            [{capture, all_but_first, list}, global]
+        )
+    of
+        {match, [[HashTypeStr, PasswordHashStr]]} ->
+            case emqx_utils:safe_to_existing_atom(string:to_lower(HashTypeStr)) of
+                {ok, HashType} ->
+                    PasswordHash = to_binary(PasswordHashStr),
+                    is_valid_algorithm(HashType, PasswordHash, Password, Entry, State);
+                _Error ->
+                    {error, invalid_hash_type}
+            end;
+        _ ->
+            OnFail(LDAPPassword, Password, Entry, State)
+    end.
+
+is_valid_algorithm(HashType, PasswordHash, Password, Entry, State) ->
+    case lists:member(HashType, ?VALID_ALGORITHMS) of
+        true ->
+            verify_password(HashType, PasswordHash, Password, Entry, State);
+        _ ->
+            {error, {invalid_hash_type, HashType}}
+    end.
+
+%% this password is in LDIF format which is base64 encoding
+try_decode_passowrd(LDAPPassword, Password, Entry, State) ->
+    case safe_base64_decode(LDAPPassword) of
+        {ok, Decode} ->
+            extract_hash_algorithm(
+                Decode,
+                Password,
+                fun(_, _, _, _) ->
+                    {error, invalid_password}
+                end,
+                Entry,
+                State
+            );
+        {error, Reason} ->
+            {error, {invalid_password, Reason}}
+    end.
+
+%% sha with salt
+%% https://www.openldap.org/faq/data/cache/347.html
+verify_password(ssha, PasswordData, Password, Entry, State) ->
+    case safe_base64_decode(PasswordData) of
+        {ok, <<PasswordHash:20/binary, Salt/binary>>} ->
+            verify_password(sha, hash, PasswordHash, Salt, suffix, Password, Entry, State);
+        {ok, _} ->
+            {error, invalid_ssha_password};
+        {error, Reason} ->
+            {error, {invalid_password, Reason}}
+    end;
+verify_password(
+    Algorithm,
+    Base64HashData,
+    Password,
+    Entry,
+    State
+) ->
+    verify_password(Algorithm, base64, Base64HashData, <<>>, disable, Password, Entry, State).
+
+verify_password(Algorithm, LDAPPasswordType, LDAPPassword, Salt, Position, Password, Entry, State) ->
+    PasswordHash = hash_password(Algorithm, Salt, Position, Password),
+    case compare_password(LDAPPasswordType, LDAPPassword, PasswordHash) of
+        true ->
+            {ok, is_superuser(Entry, State)};
+        _ ->
+            {error, invalid_password}
+    end.
+
+is_superuser(Entry, #{is_superuser_attribute := Attr} = _State) ->
+    Value = get_lower_bin_value(Attr, Entry#eldap_entry.attributes, "false"),
+    #{is_superuser => emqx_authn_utils:to_bool(Value)}.
+
+safe_base64_decode(Data) ->
+    try
+        {ok, base64:decode(Data)}
+    catch
+        _:Reason ->
+            {error, {invalid_base64_data, Reason}}
+    end.
+
+get_lower_bin_value(Key, Proplists, Default) ->
+    [Value | _] = get_value(Key, Proplists, [Default]),
+    to_binary(string:to_lower(Value)).
+
+to_binary(Value) ->
+    erlang:list_to_binary(Value).
+
+hash_password(Algorithm, _Salt, disable, Password) ->
+    hash_password(Algorithm, Password);
+hash_password(Algorithm, Salt, suffix, Password) ->
+    hash_password(Algorithm, <<Password/binary, Salt/binary>>).
+
+hash_password(Algorithm, Data) ->
+    crypto:hash(Algorithm, Data).
+
+compare_password(hash, PasswordHash, PasswordHash) ->
+    true;
+compare_password(base64, Base64HashData, PasswordHash) ->
+    Base64HashData =:= base64:encode(PasswordHash);
+compare_password(_, _, _) ->
+    false.

+ 17 - 0
apps/emqx_ldap/test/data/emqx.io.ldif

@@ -133,3 +133,20 @@ mqttPubSubTopic: mqttuser0005/pubsub/+
 mqttPubSubTopic: mqttuser0005/pubsub/#
 userPassword: {SHA}jKnxeEDGR14kE8AR7yuVFOelhz4=
 
+objectClass: top
+dn:uid=mqttuser0006,ou=testdevice,dc=emqx,dc=io
+objectClass: mqttUser
+objectClass: mqttDevice
+objectClass: mqttSecurity
+uid: mqttuser0006
+isEnabled: FALSE
+userPassword: {SHA}AlNm2FUO8G5BK5pCggfrPauRqN0=
+
+objectClass: top
+dn:uid=mqttuser0007,ou=testdevice,dc=emqx,dc=io
+objectClass: mqttUser
+objectClass: mqttDevice
+objectClass: mqttSecurity
+uid: mqttuser0007
+isSuperuser: TRUE
+userPassword: {SHA}axpQGbl00j3jvOG058y313ocnBk=

+ 8 - 2
apps/emqx_ldap/test/data/emqx.schema

@@ -8,6 +8,12 @@ attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.1.3 NAME 'isEnabled'
 	SINGLE-VALUE
 	USAGE userApplications )
 
+attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.1.4 NAME 'isSuperuser'
+	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
@@ -32,7 +38,7 @@ attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.4 NAME ( 'mqttAccountName' 'ma
 
 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) )
+	MAY ( mqttPublishTopic $ mqttSubscriptionTopic $ mqttPubSubTopic $ mqttAccountName $ isSuperuser) )
 
 objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.2 NAME 'mqttDevice'
 	SUP top
@@ -43,4 +49,4 @@ objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.2 NAME 'mqttDevice'
 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 ) )
+	MUST ( userPassword ) )

+ 2 - 3
apps/emqx_ldap/test/emqx_ldap_SUITE.erl

@@ -150,7 +150,6 @@ ldap_config(Config) ->
         io_lib:format(
             ""
             "\n"
-            "    auto_reconnect = true\n"
             "    username= \"cn=root,dc=emqx,dc=io\"\n"
             "    password = public\n"
             "    pool_size = 8\n"
@@ -183,10 +182,10 @@ data() ->
 
 port(tcp) -> 389;
 port(ssl) -> 636;
-port(Config) -> port(proplists:get_value(group, Config)).
+port(Config) -> port(proplists:get_value(group, Config, tcp)).
 
 ssl(Config) ->
-    case proplists:get_value(group, Config) of
+    case proplists:get_value(group, Config, tcp) of
         tcp ->
             "ssl.enable=false";
         ssl ->

+ 259 - 0
apps/emqx_ldap/test/emqx_ldap_authn_SUITE.erl

@@ -0,0 +1,259 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_ldap_authn_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include_lib("emqx_authn/include/emqx_authn.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+-define(LDAP_HOST, "ldap").
+-define(LDAP_DEFAULT_PORT, 389).
+-define(LDAP_RESOURCE, <<"emqx_authn_ldap_SUITE">>).
+
+-define(PATH, [authentication]).
+-define(ResourceID, <<"password_based:ldap">>).
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_testcase(_, Config) ->
+    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
+    emqx_authentication:initialize_authentication(?GLOBAL, []),
+    emqx_authn_test_lib:delete_authenticators(
+        [authentication],
+        ?GLOBAL
+    ),
+    Config.
+
+init_per_suite(Config) ->
+    _ = application:load(emqx_conf),
+    case emqx_common_test_helpers:is_tcp_server_available(?LDAP_HOST, ?LDAP_DEFAULT_PORT) of
+        true ->
+            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
+            ok = start_apps([emqx_resource]),
+            {ok, _} = emqx_resource:create_local(
+                ?LDAP_RESOURCE,
+                ?RESOURCE_GROUP,
+                emqx_ldap,
+                ldap_config(),
+                #{}
+            ),
+            Config;
+        false ->
+            {skip, no_ldap}
+    end.
+
+end_per_suite(_Config) ->
+    emqx_authn_test_lib:delete_authenticators(
+        [authentication],
+        ?GLOBAL
+    ),
+    ok = emqx_resource:remove_local(?LDAP_RESOURCE),
+    ok = stop_apps([emqx_resource]),
+    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_create(_Config) ->
+    AuthConfig = raw_ldap_auth_config(),
+
+    {ok, _} = emqx:update_config(
+        ?PATH,
+        {create_authenticator, ?GLOBAL, AuthConfig}
+    ),
+
+    {ok, [#{provider := emqx_ldap_authn}]} = emqx_authentication:list_authenticators(?GLOBAL),
+    emqx_authn_test_lib:delete_config(?ResourceID).
+
+t_create_invalid(_Config) ->
+    AuthConfig = raw_ldap_auth_config(),
+
+    InvalidConfigs =
+        [
+            AuthConfig#{<<"server">> => <<"unknownhost:3333">>},
+            AuthConfig#{<<"password">> => <<"wrongpass">>}
+        ],
+
+    lists:foreach(
+        fun(Config) ->
+            {ok, _} = emqx:update_config(
+                ?PATH,
+                {create_authenticator, ?GLOBAL, Config}
+            ),
+            emqx_authn_test_lib:delete_config(?ResourceID),
+            ?assertEqual(
+                {error, {not_found, {chain, ?GLOBAL}}},
+                emqx_authentication:list_authenticators(?GLOBAL)
+            )
+        end,
+        InvalidConfigs
+    ).
+
+t_authenticate(_Config) ->
+    ok = lists:foreach(
+        fun(Sample) ->
+            ct:pal("test_user_auth sample: ~p", [Sample]),
+            test_user_auth(Sample)
+        end,
+        user_seeds()
+    ).
+
+test_user_auth(#{
+    credentials := Credentials0,
+    config_params := SpecificConfigParams,
+    result := Result
+}) ->
+    AuthConfig = maps:merge(raw_ldap_auth_config(), SpecificConfigParams),
+
+    {ok, _} = emqx:update_config(
+        ?PATH,
+        {create_authenticator, ?GLOBAL, AuthConfig}
+    ),
+
+    Credentials = Credentials0#{
+        listener => 'tcp:default',
+        protocol => mqtt
+    },
+
+    ?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
+
+    emqx_authn_test_lib:delete_authenticators(
+        [authentication],
+        ?GLOBAL
+    ).
+
+t_destroy(_Config) ->
+    AuthConfig = raw_ldap_auth_config(),
+
+    {ok, _} = emqx:update_config(
+        ?PATH,
+        {create_authenticator, ?GLOBAL, AuthConfig}
+    ),
+
+    {ok, [#{provider := emqx_ldap_authn, state := State}]} =
+        emqx_authentication:list_authenticators(?GLOBAL),
+
+    {ok, _} = emqx_ldap_authn:authenticate(
+        #{
+            username => <<"mqttuser0001">>,
+            password => <<"mqttuser0001">>
+        },
+        State
+    ),
+
+    emqx_authn_test_lib:delete_authenticators(
+        [authentication],
+        ?GLOBAL
+    ),
+
+    % Authenticator should not be usable anymore
+    ?assertMatch(
+        ignore,
+        emqx_ldap_authn:authenticate(
+            #{
+                username => <<"mqttuser0001">>,
+                password => <<"mqttuser0001">>
+            },
+            State
+        )
+    ).
+
+t_update(_Config) ->
+    CorrectConfig = raw_ldap_auth_config(),
+    IncorrectConfig =
+        CorrectConfig#{
+            <<"base_object">> => <<"ou=testdevice,dc=emqx,dc=io">>
+        },
+
+    {ok, _} = emqx:update_config(
+        ?PATH,
+        {create_authenticator, ?GLOBAL, IncorrectConfig}
+    ),
+
+    {error, _} = emqx_access_control:authenticate(
+        #{
+            username => <<"mqttuser0001">>,
+            password => <<"mqttuser0001">>,
+            listener => 'tcp:default',
+            protocol => mqtt
+        }
+    ),
+
+    % We update with config with correct query, provider should update and work properly
+    {ok, _} = emqx:update_config(
+        ?PATH,
+        {update_authenticator, ?GLOBAL, <<"password_based:ldap">>, CorrectConfig}
+    ),
+
+    {ok, _} = emqx_access_control:authenticate(
+        #{
+            username => <<"mqttuser0001">>,
+            password => <<"mqttuser0001">>,
+            listener => 'tcp:default',
+            protocol => mqtt
+        }
+    ).
+
+%%------------------------------------------------------------------------------
+%% Helpers
+%%------------------------------------------------------------------------------
+
+raw_ldap_auth_config() ->
+    #{
+        <<"mechanism">> => <<"password_based">>,
+        <<"backend">> => <<"ldap">>,
+        <<"server">> => ldap_server(),
+        <<"base_object">> => <<"uid=${username},ou=testdevice,dc=emqx,dc=io">>,
+        <<"username">> => <<"cn=root,dc=emqx,dc=io">>,
+        <<"password">> => <<"public">>,
+        <<"pool_size">> => 8
+    }.
+
+user_seeds() ->
+    New = fun(Username, Password, Result) ->
+        #{
+            credentials => #{
+                username => Username,
+                password => Password
+            },
+            config_params => #{},
+            result => Result
+        }
+    end,
+    Valid =
+        lists:map(
+            fun(Idx) ->
+                Username = erlang:iolist_to_binary(io_lib:format("mqttuser000~b", [Idx])),
+                New(Username, Username, {ok, #{is_superuser => false}})
+            end,
+            lists:seq(1, 5)
+        ),
+    [
+        %% Not exists
+        New(<<"notexists">>, <<"notexists">>, {error, not_authorized}),
+        %% Wrong Password
+        New(<<"mqttuser0001">>, <<"wrongpassword">>, {error, invalid_password}),
+        %% Disabled
+        New(<<"mqttuser0006">>, <<"mqttuser0006">>, {error, user_disabled}),
+        %% IsSuperuser
+        New(<<"mqttuser0007">>, <<"mqttuser0007">>, {ok, #{is_superuser => true}})
+        | Valid
+    ].
+
+ldap_server() ->
+    iolist_to_binary(io_lib:format("~s:~B", [?LDAP_HOST, ?LDAP_DEFAULT_PORT])).
+
+ldap_config() ->
+    emqx_ldap_SUITE:ldap_config([]).
+
+start_apps(Apps) ->
+    lists:foreach(fun application:ensure_all_started/1, Apps).
+
+stop_apps(Apps) ->
+    lists:foreach(fun application:stop/1, Apps).

+ 2 - 1
apps/emqx_machine/priv/reboot_lists.eterm

@@ -106,7 +106,8 @@
             emqx_schema_registry,
             emqx_eviction_agent,
             emqx_node_rebalance,
-            emqx_ft
+            emqx_ft,
+            emqx_ldap
         ],
     %% must always be of type `load'
     ce_business_apps =>

+ 1 - 1
apps/emqx_machine/src/emqx_machine.app.src

@@ -3,7 +3,7 @@
     {id, "emqx_machine"},
     {description, "The EMQX Machine"},
     % strict semver, bump manually!
-    {vsn, "0.2.9"},
+    {vsn, "0.2.10"},
     {modules, []},
     {registered, []},
     {applications, [kernel, stdlib, emqx_ctl]},

+ 1 - 0
changes/ee/feat-11386.en.md

@@ -0,0 +1 @@
+Integrated the LDAP as a new authenticator.

+ 2 - 1
mix.exs

@@ -194,7 +194,8 @@ defmodule EMQXUmbrella.MixProject do
       :emqx_schema_registry,
       :emqx_enterprise,
       :emqx_bridge_kinesis,
-      :emqx_bridge_azure_event_hub
+      :emqx_bridge_azure_event_hub,
+      :emqx_ldap
     ])
   end
 

+ 1 - 0
rebar.config.erl

@@ -106,6 +106,7 @@ is_community_umbrella_app("apps/emqx_schema_registry") -> false;
 is_community_umbrella_app("apps/emqx_enterprise") -> false;
 is_community_umbrella_app("apps/emqx_bridge_kinesis") -> false;
 is_community_umbrella_app("apps/emqx_bridge_azure_event_hub") -> false;
+is_community_umbrella_app("apps/emqx_ldap") -> false;
 is_community_umbrella_app(_) -> true.
 
 is_jq_supported() ->

+ 0 - 21
rel/i18n/emqx_connector_ldap.hocon

@@ -1,21 +0,0 @@
-emqx_connector_ldap {
-
-bind_dn.desc:
-"""LDAP's Binding Distinguished Name (DN)"""
-
-bind_dn.label:
-"""Bind DN"""
-
-port.desc:
-"""LDAP Port"""
-
-port.label:
-"""Port"""
-
-timeout.desc:
-"""LDAP's query timeout"""
-
-timeout.label:
-"""timeout"""
-
-}

+ 1 - 1
rel/i18n/emqx_ldap.hocon

@@ -3,7 +3,7 @@ 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."""
+The LDAP default port 389 is used if `[:Port]` is not specified."""
 
 server.label:
 """Server Host"""

+ 24 - 0
rel/i18n/emqx_ldap_authn.hocon

@@ -0,0 +1,24 @@
+emqx_ldap_authn {
+
+ldap.desc:
+"""Configuration of authenticator using LDAP as authentication data source."""
+
+password_attribute.desc:
+"""Indicates which attribute is used to represent the user's password."""
+
+password_attribute.label:
+"""Password Attribute"""
+
+is_superuser_attribute.desc:
+"""Indicates which attribute is used to represent whether the user is a super user."""
+
+is_superuser_attribute.label:
+"""IsSuperuser Attribute"""
+
+query_timeout.desc:
+"""Timeout for the LDAP query."""
+
+query_timeout.label:
+"""Query Timeout"""
+
+}