refactor(ldap): merge the `ldap-bind` backend as a type for the `ldap` backend
This commit is contained in:
parent
9dfffd90d0
commit
cbfd02d1b0
|
@ -26,10 +26,6 @@
|
||||||
-define(AUTHN_BACKEND, ldap).
|
-define(AUTHN_BACKEND, ldap).
|
||||||
-define(AUTHN_BACKEND_BIN, <<"ldap">>).
|
-define(AUTHN_BACKEND_BIN, <<"ldap">>).
|
||||||
|
|
||||||
-define(AUTHN_BACKEND_BIND, ldap_bind).
|
|
||||||
-define(AUTHN_BACKEND_BIND_BIN, <<"ldap_bind">>).
|
|
||||||
|
|
||||||
-define(AUTHN_TYPE, {?AUTHN_MECHANISM, ?AUTHN_BACKEND}).
|
-define(AUTHN_TYPE, {?AUTHN_MECHANISM, ?AUTHN_BACKEND}).
|
||||||
-define(AUTHN_TYPE_BIND, {?AUTHN_MECHANISM, ?AUTHN_BACKEND_BIND}).
|
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
|
@ -25,12 +25,10 @@
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
ok = emqx_authz:register_source(?AUTHZ_TYPE, emqx_authz_ldap),
|
ok = emqx_authz:register_source(?AUTHZ_TYPE, emqx_authz_ldap),
|
||||||
ok = emqx_authn:register_provider(?AUTHN_TYPE, emqx_authn_ldap),
|
ok = emqx_authn:register_provider(?AUTHN_TYPE, emqx_authn_ldap),
|
||||||
ok = emqx_authn:register_provider(?AUTHN_TYPE_BIND, emqx_authn_ldap_bind),
|
|
||||||
{ok, Sup} = emqx_auth_ldap_sup:start_link(),
|
{ok, Sup} = emqx_auth_ldap_sup:start_link(),
|
||||||
{ok, Sup}.
|
{ok, Sup}.
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
ok = emqx_authn:deregister_provider(?AUTHN_TYPE),
|
ok = emqx_authn:deregister_provider(?AUTHN_TYPE),
|
||||||
ok = emqx_authn:deregister_provider(?AUTHN_TYPE_BIND),
|
|
||||||
ok = emqx_authz:unregister_source(?AUTHZ_TYPE),
|
ok = emqx_authz:unregister_source(?AUTHZ_TYPE),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -16,19 +16,10 @@
|
||||||
|
|
||||||
-module(emqx_authn_ldap).
|
-module(emqx_authn_ldap).
|
||||||
|
|
||||||
-include_lib("emqx_auth/include/emqx_authn.hrl").
|
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("eldap/include/eldap.hrl").
|
|
||||||
|
|
||||||
-behaviour(emqx_authn_provider).
|
-behaviour(emqx_authn_provider).
|
||||||
|
|
||||||
%% 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([
|
-export([
|
||||||
create/2,
|
create/2,
|
||||||
update/2,
|
update/2,
|
||||||
|
@ -69,163 +60,25 @@ authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := undefined}, _) ->
|
authenticate(#{password := undefined}, _) ->
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
authenticate(
|
authenticate(Credential, #{method := #{type := Type}} = State) ->
|
||||||
#{password := Password} = Credential,
|
case Type of
|
||||||
#{
|
hash ->
|
||||||
password_attribute := PasswordAttr,
|
emqx_authn_ldap_hash:authenticate(Credential, State);
|
||||||
is_superuser_attribute := IsSuperuserAttr,
|
bind ->
|
||||||
query_timeout := Timeout,
|
emqx_authn_ldap_bind:authenticate(Credential, State)
|
||||||
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.
|
end.
|
||||||
|
|
||||||
|
%% it used the deprecated config form
|
||||||
|
parse_config(
|
||||||
|
#{password_attribute := PasswordAttr, is_superuser_attribute := IsSuperuserAttr} = Config0
|
||||||
|
) ->
|
||||||
|
Config = maps:without([password_attribute, is_superuser_attribute], Config0),
|
||||||
|
parse_config(Config#{
|
||||||
|
method => #{
|
||||||
|
type => hash,
|
||||||
|
password_attribute => PasswordAttr,
|
||||||
|
is_superuser_attribute => IsSuperuserAttr
|
||||||
|
}
|
||||||
|
});
|
||||||
parse_config(Config) ->
|
parse_config(Config) ->
|
||||||
maps:with([query_timeout, password_attribute, is_superuser_attribute], Config).
|
maps:with([query_timeout, method], Config).
|
||||||
|
|
||||||
%% 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_password/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_password(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, bad_username_or_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, LDAPPasswordHash, PasswordHash) ->
|
|
||||||
emqx_passwd:compare_secure(LDAPPasswordHash, PasswordHash);
|
|
||||||
compare_password(base64, Base64HashData, PasswordHash) ->
|
|
||||||
emqx_passwd:compare_secure(Base64HashData, base64:encode(PasswordHash)).
|
|
||||||
|
|
|
@ -20,32 +20,13 @@
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("eldap/include/eldap.hrl").
|
-include_lib("eldap/include/eldap.hrl").
|
||||||
|
|
||||||
-behaviour(emqx_authn_provider).
|
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
create/2,
|
authenticate/2
|
||||||
update/2,
|
|
||||||
authenticate/2,
|
|
||||||
destroy/1
|
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
|
||||||
emqx_authn_ldap:do_create(?MODULE, Config).
|
|
||||||
|
|
||||||
update(Config, State) ->
|
|
||||||
emqx_authn_ldap:update(Config, State).
|
|
||||||
|
|
||||||
destroy(State) ->
|
|
||||||
emqx_authn_ldap:destroy(State).
|
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
|
||||||
ignore;
|
|
||||||
authenticate(#{password := undefined}, _) ->
|
|
||||||
{error, bad_username_or_password};
|
|
||||||
authenticate(
|
authenticate(
|
||||||
#{password := _Password} = Credential,
|
#{password := _Password} = Credential,
|
||||||
#{
|
#{
|
||||||
|
|
|
@ -1,63 +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_authn_ldap_bind_schema).
|
|
||||||
|
|
||||||
-include("emqx_auth_ldap.hrl").
|
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
|
||||||
|
|
||||||
-behaviour(emqx_authn_schema).
|
|
||||||
|
|
||||||
-export([
|
|
||||||
fields/1,
|
|
||||||
desc/1,
|
|
||||||
refs/0,
|
|
||||||
select_union_member/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
refs() ->
|
|
||||||
[?R_REF(ldap_bind)].
|
|
||||||
|
|
||||||
select_union_member(#{
|
|
||||||
<<"mechanism">> := ?AUTHN_MECHANISM_BIN, <<"backend">> := ?AUTHN_BACKEND_BIND_BIN
|
|
||||||
}) ->
|
|
||||||
refs();
|
|
||||||
select_union_member(#{<<"backend">> := ?AUTHN_BACKEND_BIND_BIN}) ->
|
|
||||||
throw(#{
|
|
||||||
reason => "unknown_mechanism",
|
|
||||||
expected => ?AUTHN_MECHANISM
|
|
||||||
});
|
|
||||||
select_union_member(_) ->
|
|
||||||
undefined.
|
|
||||||
|
|
||||||
fields(ldap_bind) ->
|
|
||||||
[
|
|
||||||
{mechanism, emqx_authn_schema:mechanism(?AUTHN_MECHANISM)},
|
|
||||||
{backend, emqx_authn_schema:backend(?AUTHN_BACKEND_BIND)},
|
|
||||||
{query_timeout, fun query_timeout/1}
|
|
||||||
] ++
|
|
||||||
emqx_authn_schema:common_fields() ++
|
|
||||||
emqx_ldap:fields(config) ++ emqx_ldap:fields(bind_opts).
|
|
||||||
|
|
||||||
desc(ldap_bind) ->
|
|
||||||
?DESC(ldap_bind);
|
|
||||||
desc(_) ->
|
|
||||||
undefined.
|
|
||||||
|
|
||||||
query_timeout(type) -> emqx_schema:timeout_duration_ms();
|
|
||||||
query_timeout(desc) -> ?DESC(?FUNCTION_NAME);
|
|
||||||
query_timeout(default) -> <<"5s">>;
|
|
||||||
query_timeout(_) -> undefined.
|
|
|
@ -0,0 +1,197 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 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_authn_ldap_hash).
|
||||||
|
|
||||||
|
-include_lib("emqx_auth/include/emqx_authn.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("eldap/include/eldap.hrl").
|
||||||
|
|
||||||
|
%% 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([
|
||||||
|
authenticate/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-import(proplists, [get_value/2, get_value/3]).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
authenticate(
|
||||||
|
#{password := Password} = Credential,
|
||||||
|
#{
|
||||||
|
method := #{
|
||||||
|
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.
|
||||||
|
|
||||||
|
%% 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,
|
||||||
|
#{method := #{password_attribute := PasswordAttr}} = State
|
||||||
|
) ->
|
||||||
|
case get_value(PasswordAttr, Attributes) of
|
||||||
|
undefined ->
|
||||||
|
{error, no_password};
|
||||||
|
[LDAPPassword | _] ->
|
||||||
|
extract_hash_algorithm(LDAPPassword, Password, fun try_decode_password/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_password(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, bad_username_or_password}
|
||||||
|
end.
|
||||||
|
|
||||||
|
is_superuser(Entry, #{method := #{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, LDAPPasswordHash, PasswordHash) ->
|
||||||
|
emqx_passwd:compare_secure(LDAPPasswordHash, PasswordHash);
|
||||||
|
compare_password(base64, Base64HashData, PasswordHash) ->
|
||||||
|
emqx_passwd:compare_secure(Base64HashData, base64:encode(PasswordHash)).
|
|
@ -29,7 +29,7 @@
|
||||||
]).
|
]).
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
[?R_REF(ldap)].
|
[?R_REF(ldap), ?R_REF(ldap_deprecated)].
|
||||||
|
|
||||||
select_union_member(#{<<"mechanism">> := ?AUTHN_MECHANISM_BIN, <<"backend">> := ?AUTHN_BACKEND_BIN}) ->
|
select_union_member(#{<<"mechanism">> := ?AUTHN_MECHANISM_BIN, <<"backend">> := ?AUTHN_BACKEND_BIN}) ->
|
||||||
refs();
|
refs();
|
||||||
|
@ -41,12 +41,34 @@ select_union_member(#{<<"backend">> := ?AUTHN_BACKEND_BIN}) ->
|
||||||
select_union_member(_) ->
|
select_union_member(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
|
fields(ldap_deprecated) ->
|
||||||
|
common_fields() ++
|
||||||
|
[
|
||||||
|
{password_attribute, password_attribute()},
|
||||||
|
{is_superuser_attribute, is_superuser_attribute()}
|
||||||
|
];
|
||||||
fields(ldap) ->
|
fields(ldap) ->
|
||||||
|
common_fields() ++
|
||||||
|
[
|
||||||
|
{method,
|
||||||
|
?HOCON(
|
||||||
|
?UNION([?R_REF(hash_method), ?R_REF(bind_method)]),
|
||||||
|
#{desc => ?DESC(method)}
|
||||||
|
)}
|
||||||
|
];
|
||||||
|
fields(hash_method) ->
|
||||||
|
[
|
||||||
|
{type, method_type(hash)},
|
||||||
|
{password_attribute, password_attribute()},
|
||||||
|
{is_superuser_attribute, is_superuser_attribute()}
|
||||||
|
];
|
||||||
|
fields(bind_method) ->
|
||||||
|
[{type, method_type(bind)}] ++ emqx_ldap:fields(bind_opts).
|
||||||
|
|
||||||
|
common_fields() ->
|
||||||
[
|
[
|
||||||
{mechanism, emqx_authn_schema:mechanism(?AUTHN_MECHANISM)},
|
{mechanism, emqx_authn_schema:mechanism(?AUTHN_MECHANISM)},
|
||||||
{backend, emqx_authn_schema:backend(?AUTHN_BACKEND)},
|
{backend, emqx_authn_schema:backend(?AUTHN_BACKEND)},
|
||||||
{password_attribute, fun password_attribute/1},
|
|
||||||
{is_superuser_attribute, fun is_superuser_attribute/1},
|
|
||||||
{query_timeout, fun query_timeout/1}
|
{query_timeout, fun query_timeout/1}
|
||||||
] ++
|
] ++
|
||||||
emqx_authn_schema:common_fields() ++
|
emqx_authn_schema:common_fields() ++
|
||||||
|
@ -54,18 +76,35 @@ fields(ldap) ->
|
||||||
|
|
||||||
desc(ldap) ->
|
desc(ldap) ->
|
||||||
?DESC(ldap);
|
?DESC(ldap);
|
||||||
|
desc(ldap_deprecated) ->
|
||||||
|
?DESC(ldap_deprecated);
|
||||||
|
desc(hash_method) ->
|
||||||
|
?DESC(hash_method);
|
||||||
|
desc(bind_method) ->
|
||||||
|
?DESC(bind_method);
|
||||||
desc(_) ->
|
desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
password_attribute(type) -> string();
|
method_type(Type) ->
|
||||||
password_attribute(desc) -> ?DESC(?FUNCTION_NAME);
|
?HOCON(?ENUM([Type]), #{desc => ?DESC(?FUNCTION_NAME), default => Type}).
|
||||||
password_attribute(default) -> <<"userPassword">>;
|
|
||||||
password_attribute(_) -> undefined.
|
|
||||||
|
|
||||||
is_superuser_attribute(type) -> string();
|
password_attribute() ->
|
||||||
is_superuser_attribute(desc) -> ?DESC(?FUNCTION_NAME);
|
?HOCON(
|
||||||
is_superuser_attribute(default) -> <<"isSuperuser">>;
|
string(),
|
||||||
is_superuser_attribute(_) -> undefined.
|
#{
|
||||||
|
desc => ?DESC(?FUNCTION_NAME),
|
||||||
|
default => <<"userPassword">>
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
|
is_superuser_attribute() ->
|
||||||
|
?HOCON(
|
||||||
|
string(),
|
||||||
|
#{
|
||||||
|
desc => ?DESC(?FUNCTION_NAME),
|
||||||
|
default => <<"isSuperuser">>
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
query_timeout(type) -> emqx_schema:timeout_duration_ms();
|
query_timeout(type) -> emqx_schema:timeout_duration_ms();
|
||||||
query_timeout(desc) -> ?DESC(?FUNCTION_NAME);
|
query_timeout(desc) -> ?DESC(?FUNCTION_NAME);
|
||||||
|
|
|
@ -70,6 +70,29 @@ end_per_suite(Config) ->
|
||||||
%% Tests
|
%% Tests
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_create_with_deprecated_cfg(_Config) ->
|
||||||
|
AuthConfig = deprecated_raw_ldap_auth_config(),
|
||||||
|
|
||||||
|
{ok, _} = emqx:update_config(
|
||||||
|
?PATH,
|
||||||
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
|
{ok, [#{provider := emqx_authn_ldap, state := State}]} = emqx_authn_chains:list_authenticators(
|
||||||
|
?GLOBAL
|
||||||
|
),
|
||||||
|
?assertMatch(
|
||||||
|
#{
|
||||||
|
method := #{
|
||||||
|
type := hash,
|
||||||
|
is_superuser_attribute := _,
|
||||||
|
password_attribute := "not_the_default_value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
emqx_authn_test_lib:delete_config(?ResourceID).
|
||||||
|
|
||||||
t_create(_Config) ->
|
t_create(_Config) ->
|
||||||
AuthConfig = raw_ldap_auth_config(),
|
AuthConfig = raw_ldap_auth_config(),
|
||||||
|
|
||||||
|
@ -225,6 +248,19 @@ raw_ldap_auth_config() ->
|
||||||
<<"pool_size">> => 8
|
<<"pool_size">> => 8
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
deprecated_raw_ldap_auth_config() ->
|
||||||
|
#{
|
||||||
|
<<"mechanism">> => <<"password_based">>,
|
||||||
|
<<"backend">> => <<"ldap">>,
|
||||||
|
<<"server">> => ldap_server(),
|
||||||
|
<<"is_superuser_attribute">> => <<"isSuperuser">>,
|
||||||
|
<<"password_attribute">> => <<"not_the_default_value">>,
|
||||||
|
<<"base_dn">> => <<"uid=${username},ou=testdevice,dc=emqx,dc=io">>,
|
||||||
|
<<"username">> => <<"cn=root,dc=emqx,dc=io">>,
|
||||||
|
<<"password">> => <<"public">>,
|
||||||
|
<<"pool_size">> => 8
|
||||||
|
}.
|
||||||
|
|
||||||
user_seeds() ->
|
user_seeds() ->
|
||||||
New = fun(Username, Password, Result) ->
|
New = fun(Username, Password, Result) ->
|
||||||
#{
|
#{
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
-define(LDAP_RESOURCE, <<"emqx_authn_ldap_bind_SUITE">>).
|
-define(LDAP_RESOURCE, <<"emqx_authn_ldap_bind_SUITE">>).
|
||||||
|
|
||||||
-define(PATH, [authentication]).
|
-define(PATH, [authentication]).
|
||||||
-define(ResourceID, <<"password_based:ldap_bind">>).
|
-define(ResourceID, <<"password_based:ldap">>).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
@ -78,7 +78,7 @@ t_create(_Config) ->
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
),
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_ldap_bind}]} = emqx_authn_chains:list_authenticators(?GLOBAL),
|
{ok, [#{provider := emqx_authn_ldap}]} = emqx_authn_chains:list_authenticators(?GLOBAL),
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID).
|
emqx_authn_test_lib:delete_config(?ResourceID).
|
||||||
|
|
||||||
t_create_invalid(_Config) ->
|
t_create_invalid(_Config) ->
|
||||||
|
@ -146,10 +146,10 @@ t_destroy(_Config) ->
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
),
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_ldap_bind, state := State}]} =
|
{ok, [#{provider := emqx_authn_ldap, state := State}]} =
|
||||||
emqx_authn_chains:list_authenticators(?GLOBAL),
|
emqx_authn_chains:list_authenticators(?GLOBAL),
|
||||||
|
|
||||||
{ok, _} = emqx_authn_ldap_bind:authenticate(
|
{ok, _} = emqx_authn_ldap:authenticate(
|
||||||
#{
|
#{
|
||||||
username => <<"mqttuser0001">>,
|
username => <<"mqttuser0001">>,
|
||||||
password => <<"mqttuser0001">>
|
password => <<"mqttuser0001">>
|
||||||
|
@ -165,7 +165,7 @@ t_destroy(_Config) ->
|
||||||
% Authenticator should not be usable anymore
|
% Authenticator should not be usable anymore
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
ignore,
|
ignore,
|
||||||
emqx_authn_ldap_bind:authenticate(
|
emqx_authn_ldap:authenticate(
|
||||||
#{
|
#{
|
||||||
username => <<"mqttuser0001">>,
|
username => <<"mqttuser0001">>,
|
||||||
password => <<"mqttuser0001">>
|
password => <<"mqttuser0001">>
|
||||||
|
@ -199,7 +199,7 @@ t_update(_Config) ->
|
||||||
% We update with config with correct query, provider should update and work properly
|
% We update with config with correct query, provider should update and work properly
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{update_authenticator, ?GLOBAL, <<"password_based:ldap_bind">>, CorrectConfig}
|
{update_authenticator, ?GLOBAL, <<"password_based:ldap">>, CorrectConfig}
|
||||||
),
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_access_control:authenticate(
|
{ok, _} = emqx_access_control:authenticate(
|
||||||
|
@ -218,14 +218,17 @@ t_update(_Config) ->
|
||||||
raw_ldap_auth_config() ->
|
raw_ldap_auth_config() ->
|
||||||
#{
|
#{
|
||||||
<<"mechanism">> => <<"password_based">>,
|
<<"mechanism">> => <<"password_based">>,
|
||||||
<<"backend">> => <<"ldap_bind">>,
|
<<"backend">> => <<"ldap">>,
|
||||||
<<"server">> => ldap_server(),
|
<<"server">> => ldap_server(),
|
||||||
<<"base_dn">> => <<"ou=testdevice,dc=emqx,dc=io">>,
|
<<"base_dn">> => <<"ou=testdevice,dc=emqx,dc=io">>,
|
||||||
<<"filter">> => <<"(uid=${username})">>,
|
<<"filter">> => <<"(uid=${username})">>,
|
||||||
<<"username">> => <<"cn=root,dc=emqx,dc=io">>,
|
<<"username">> => <<"cn=root,dc=emqx,dc=io">>,
|
||||||
<<"password">> => <<"public">>,
|
<<"password">> => <<"public">>,
|
||||||
<<"pool_size">> => 8,
|
<<"pool_size">> => 8,
|
||||||
<<"bind_password">> => <<"${password}">>
|
<<"method">> => #{
|
||||||
|
<<"type">> => <<"bind">>,
|
||||||
|
<<"bind_password">> => <<"${password}">>
|
||||||
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
user_seeds() ->
|
user_seeds() ->
|
||||||
|
|
|
@ -58,8 +58,7 @@
|
||||||
emqx_authn_http_schema,
|
emqx_authn_http_schema,
|
||||||
emqx_authn_jwt_schema,
|
emqx_authn_jwt_schema,
|
||||||
emqx_authn_scram_mnesia_schema,
|
emqx_authn_scram_mnesia_schema,
|
||||||
emqx_authn_ldap_schema,
|
emqx_authn_ldap_schema
|
||||||
emqx_authn_ldap_bind_schema
|
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-define(EE_AUTHN_PROVIDER_SCHEMA_MODS, [
|
-define(EE_AUTHN_PROVIDER_SCHEMA_MODS, [
|
||||||
|
|
|
@ -92,7 +92,7 @@ parse_config(Config0) ->
|
||||||
%% In this feature, the `bind_password` is fixed, so it should conceal from the swagger,
|
%% In this feature, the `bind_password` is fixed, so it should conceal from the swagger,
|
||||||
%% but the connector still needs it, hence we should add it back here
|
%% but the connector still needs it, hence we should add it back here
|
||||||
ensure_bind_password(Config) ->
|
ensure_bind_password(Config) ->
|
||||||
Config#{bind_password => <<"${password}">>}.
|
Config#{method => #{type => bind, bind_password => <<"${password}">>}}.
|
||||||
|
|
||||||
adjust_ldap_fields(Fields) ->
|
adjust_ldap_fields(Fields) ->
|
||||||
lists:map(fun adjust_ldap_field/1, Fields).
|
lists:map(fun adjust_ldap_field/1, Fields).
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
-spec on_start(binary(), hoconsc:config(), proplists:proplist(), map()) ->
|
-spec on_start(binary(), hoconsc:config(), proplists:proplist(), map()) ->
|
||||||
{ok, binary(), map()} | {error, _}.
|
{ok, binary(), map()} | {error, _}.
|
||||||
on_start(InstId, #{bind_password := _} = Config, Options, State) ->
|
on_start(InstId, #{method := #{bind_password := _}} = Config, Options, State) ->
|
||||||
PoolName = pool_name(InstId),
|
PoolName = pool_name(InstId),
|
||||||
?SLOG(info, #{
|
?SLOG(info, #{
|
||||||
msg => "starting_ldap_bind_worker",
|
msg => "starting_ldap_bind_worker",
|
||||||
|
@ -108,15 +108,10 @@ on_query(
|
||||||
connect(Conf) ->
|
connect(Conf) ->
|
||||||
emqx_ldap:connect(Conf).
|
emqx_ldap:connect(Conf).
|
||||||
|
|
||||||
prepare_template(Config, State) ->
|
prepare_template(#{method := #{bind_password := V}}, State) ->
|
||||||
do_prepare_template(maps:to_list(maps:with([bind_password], Config)), State).
|
|
||||||
|
|
||||||
do_prepare_template([{bind_password, V} | T], State) ->
|
|
||||||
%% This is sensitive data
|
%% This is sensitive data
|
||||||
%% to reduce match cases, here we reuse the existing sensitive filter key: bind_password
|
%% to reduce match cases, here we reuse the existing sensitive filter key: bind_password
|
||||||
do_prepare_template(T, State#{bind_password => emqx_placeholder:preproc_tmpl(V)});
|
State#{bind_password => emqx_placeholder:preproc_tmpl(V)}.
|
||||||
do_prepare_template([], State) ->
|
|
||||||
State.
|
|
||||||
|
|
||||||
pool_name(InstId) ->
|
pool_name(InstId) ->
|
||||||
<<InstId/binary, "-", ?POOL_NAME_SUFFIX>>.
|
<<InstId/binary, "-", ?POOL_NAME_SUFFIX>>.
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
emqx_authn_ldap_bind_schema {
|
|
||||||
|
|
||||||
ldap_bind.desc:
|
|
||||||
"""Configuration of authenticator using the LDAP bind operation as the authentication method."""
|
|
||||||
|
|
||||||
query_timeout.desc:
|
|
||||||
"""Timeout for the LDAP query."""
|
|
||||||
|
|
||||||
query_timeout.label:
|
|
||||||
"""Query Timeout"""
|
|
||||||
}
|
|
|
@ -3,6 +3,9 @@ emqx_authn_ldap_schema {
|
||||||
ldap.desc:
|
ldap.desc:
|
||||||
"""Configuration of authenticator using LDAP as authentication data source."""
|
"""Configuration of authenticator using LDAP as authentication data source."""
|
||||||
|
|
||||||
|
ldap_deprecated.desc:
|
||||||
|
"""This is a deprecated form, you should avoid using it."""
|
||||||
|
|
||||||
password_attribute.desc:
|
password_attribute.desc:
|
||||||
"""Indicates which attribute is used to represent the user's password."""
|
"""Indicates which attribute is used to represent the user's password."""
|
||||||
|
|
||||||
|
@ -21,4 +24,16 @@ query_timeout.desc:
|
||||||
query_timeout.label:
|
query_timeout.label:
|
||||||
"""Query Timeout"""
|
"""Query Timeout"""
|
||||||
|
|
||||||
|
hash_method.desc:
|
||||||
|
"""Authenticate by comparing the hashed password which was provided by the `password attribute`."""
|
||||||
|
|
||||||
|
bind_method.desc:
|
||||||
|
"""Authenticate by the LDAP bind operation."""
|
||||||
|
|
||||||
|
method.desc:
|
||||||
|
"""Authentication method."""
|
||||||
|
|
||||||
|
method_type.desc:
|
||||||
|
"""Authentication method type."""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue