From efff585b829dd1b2bb8f998392daea5b4dd2a51a Mon Sep 17 00:00:00 2001 From: firest Date: Thu, 3 Aug 2023 15:02:16 +0800 Subject: [PATCH] feat(ldap-authn): add test suite for the LDAP authenticator --- apps/emqx_authn/src/emqx_authn.app.src | 2 +- apps/emqx_authn/test/data/emqx.io.ldif | 134 --------- apps/emqx_authn/test/data/emqx.schema | 46 ---- .../emqx_connector/src/emqx_connector.app.src | 2 +- .../src/emqx_connector_ldap.erl | 199 -------------- apps/emqx_ldap/rebar.config | 3 +- apps/emqx_ldap/src/emqx_ldap.app.src | 3 +- apps/emqx_ldap/src/emqx_ldap_authn.erl | 60 ++-- apps/emqx_ldap/test/data/emqx.io.ldif | 17 ++ apps/emqx_ldap/test/data/emqx.schema | 10 +- apps/emqx_ldap/test/emqx_ldap_SUITE.erl | 5 +- apps/emqx_ldap/test/emqx_ldap_authn_SUITE.erl | 259 ++++++++++++++++++ apps/emqx_machine/priv/reboot_lists.eterm | 3 +- apps/emqx_machine/src/emqx_machine.app.src | 2 +- mix.exs | 3 +- rebar.config.erl | 1 + rel/i18n/emqx_connector_ldap.hocon | 21 -- rel/i18n/emqx_ldap_authn.hocon | 6 - 18 files changed, 341 insertions(+), 435 deletions(-) delete mode 100644 apps/emqx_authn/test/data/emqx.io.ldif delete mode 100644 apps/emqx_authn/test/data/emqx.schema delete mode 100644 apps/emqx_connector/src/emqx_connector_ldap.erl create mode 100644 apps/emqx_ldap/test/emqx_ldap_authn_SUITE.erl delete mode 100644 rel/i18n/emqx_connector_ldap.hocon diff --git a/apps/emqx_authn/src/emqx_authn.app.src b/apps/emqx_authn/src/emqx_authn.app.src index c4cacca80..4ab86ef4a 100644 --- a/apps/emqx_authn/src/emqx_authn.app.src +++ b/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, [ diff --git a/apps/emqx_authn/test/data/emqx.io.ldif b/apps/emqx_authn/test/data/emqx.io.ldif deleted file mode 100644 index 4675717ec..000000000 --- a/apps/emqx_authn/test/data/emqx.io.ldif +++ /dev/null @@ -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= diff --git a/apps/emqx_authn/test/data/emqx.schema b/apps/emqx_authn/test/data/emqx.schema deleted file mode 100644 index 55f92269b..000000000 --- a/apps/emqx_authn/test/data/emqx.schema +++ /dev/null @@ -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 ) ) diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 7614ddac3..cd8ce864c 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/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, [ diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl deleted file mode 100644 index c8c134f55..000000000 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ /dev/null @@ -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. diff --git a/apps/emqx_ldap/rebar.config b/apps/emqx_ldap/rebar.config index e6e2db243..1e53ccafe 100644 --- a/apps/emqx_ldap/rebar.config +++ b/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"}} ]}. diff --git a/apps/emqx_ldap/src/emqx_ldap.app.src b/apps/emqx_ldap/src/emqx_ldap.app.src index 976d85371..32ffb4329 100644 --- a/apps/emqx_ldap/src/emqx_ldap.app.src +++ b/apps/emqx_ldap/src/emqx_ldap.app.src @@ -4,7 +4,8 @@ {registered, []}, {applications, [ kernel, - stdlib + stdlib, + emqx_authn ]}, {env, []}, {modules, []}, diff --git a/apps/emqx_ldap/src/emqx_ldap_authn.erl b/apps/emqx_ldap/src/emqx_ldap_authn.erl index fd09778a7..c7bf61cd2 100644 --- a/apps/emqx_ldap/src/emqx_ldap_authn.erl +++ b/apps/emqx_ldap/src/emqx_ldap_authn.erl @@ -14,6 +14,10 @@ %% 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, @@ -152,7 +156,7 @@ parse_config(Config) -> %% To compatible v4.x is_enabled(Password, #eldap_entry{attributes = Attributes} = Entry, State) -> - IsEnabled = get_lower_bin_value(?ISENABLED_ATTR, Attributes, <<"true">>), + IsEnabled = get_lower_bin_value(?ISENABLED_ATTR, Attributes, "true"), case emqx_authn_utils:to_bool(IsEnabled) of true -> ensure_password(Password, Entry, State); @@ -172,6 +176,8 @@ ensure_password( 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( @@ -184,7 +190,7 @@ extract_hash_algorithm(LDAPPassword, Password, OnFail, Entry, State) -> case emqx_utils:safe_to_existing_atom(string:to_lower(HashTypeStr)) of {ok, HashType} -> PasswordHash = to_binary(PasswordHashStr), - verify_password(HashType, PasswordHash, Password, Entry, State); + is_valid_algorithm(HashType, PasswordHash, Password, Entry, State); _Error -> {error, invalid_hash_type} end; @@ -192,6 +198,14 @@ extract_hash_algorithm(LDAPPassword, Password, OnFail, Entry, State) -> 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 @@ -209,10 +223,12 @@ try_decode_passowrd(LDAPPassword, Password, Entry, State) -> {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, <>} -> - verify_password(sha, PasswordHash, Salt, suffix, Password, Entry, State); + verify_password(sha, hash, PasswordHash, Salt, suffix, Password, Entry, State); {ok, _} -> {error, invalid_ssha_password}; {error, Reason} -> @@ -220,29 +236,24 @@ verify_password(ssha, PasswordData, Password, Entry, State) -> end; verify_password( Algorithm, - PasswordHash, + Base64HashData, Password, Entry, State ) -> - verify_password(Algorithm, PasswordHash, <<>>, disable, Password, Entry, State). + verify_password(Algorithm, base64, Base64HashData, <<>>, disable, Password, Entry, State). -verify_password(Algorithm, PasswordHash, Salt, Position, Password, Entry, State) -> - Result = emqx_passwd:check_pass( - #{name => Algorithm, salt_position => Position}, - Salt, - PasswordHash, - Password - ), - case Result of - ok -> +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 -> - Error + _ -> + {error, invalid_password} end. is_superuser(Entry, #{is_superuser_attribute := Attr} = _State) -> - Value = get_lower_bin_value(Attr, Entry#eldap_entry.attributes, <<"false">>), + Value = get_lower_bin_value(Attr, Entry#eldap_entry.attributes, "false"), #{is_superuser => emqx_authn_utils:to_bool(Value)}. safe_base64_decode(Data) -> @@ -259,3 +270,18 @@ get_lower_bin_value(Key, Proplists, Default) -> 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, <>). + +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. diff --git a/apps/emqx_ldap/test/data/emqx.io.ldif b/apps/emqx_ldap/test/data/emqx.io.ldif index f9833cd88..138651958 100644 --- a/apps/emqx_ldap/test/data/emqx.io.ldif +++ b/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= diff --git a/apps/emqx_ldap/test/data/emqx.schema b/apps/emqx_ldap/test/data/emqx.schema index 55f92269b..d08548272 100644 --- a/apps/emqx_ldap/test/data/emqx.schema +++ b/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 ) ) diff --git a/apps/emqx_ldap/test/emqx_ldap_SUITE.erl b/apps/emqx_ldap/test/emqx_ldap_SUITE.erl index 9386b1738..bf20629ec 100644 --- a/apps/emqx_ldap/test/emqx_ldap_SUITE.erl +++ b/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 -> diff --git a/apps/emqx_ldap/test/emqx_ldap_authn_SUITE.erl b/apps/emqx_ldap/test/emqx_ldap_authn_SUITE.erl new file mode 100644 index 000000000..ce7481ef8 --- /dev/null +++ b/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). diff --git a/apps/emqx_machine/priv/reboot_lists.eterm b/apps/emqx_machine/priv/reboot_lists.eterm index 03dc8618a..51c2d2274 100644 --- a/apps/emqx_machine/priv/reboot_lists.eterm +++ b/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 => diff --git a/apps/emqx_machine/src/emqx_machine.app.src b/apps/emqx_machine/src/emqx_machine.app.src index 9a9dedc28..bdd1db76e 100644 --- a/apps/emqx_machine/src/emqx_machine.app.src +++ b/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]}, diff --git a/mix.exs b/mix.exs index c145c4a66..7c8cbcbda 100644 --- a/mix.exs +++ b/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 diff --git a/rebar.config.erl b/rebar.config.erl index f679bc2bb..b45516d2b 100644 --- a/rebar.config.erl +++ b/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() -> diff --git a/rel/i18n/emqx_connector_ldap.hocon b/rel/i18n/emqx_connector_ldap.hocon deleted file mode 100644 index 64a953816..000000000 --- a/rel/i18n/emqx_connector_ldap.hocon +++ /dev/null @@ -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""" - -} diff --git a/rel/i18n/emqx_ldap_authn.hocon b/rel/i18n/emqx_ldap_authn.hocon index 0cdae89b3..7c59f2039 100644 --- a/rel/i18n/emqx_ldap_authn.hocon +++ b/rel/i18n/emqx_ldap_authn.hocon @@ -9,12 +9,6 @@ password_attribute.desc: password_attribute.label: """Password Attribute""" -salt_attribute.desc: -"""Indicates which attribute is used to represent the salt of the password.""" - -salt_attribute.label: -"""Salt Attribute""" - is_superuser_attribute.desc: """Indicates which attribute is used to represent whether the user is a super user."""