Merge pull request #11392 from lafirest/feat/ldap_authz

feat(ldap-authz): integrate the LDAP authorization
This commit is contained in:
lafirest 2023-08-07 11:12:05 +08:00 committed by GitHub
commit 2b03436552
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 476 additions and 33 deletions

View File

@ -19,7 +19,8 @@
-export([
hash/2,
hash_data/2,
check_pass/3
check_pass/3,
compare_secure/2
]).
-export_type([

View File

@ -1,5 +1,5 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_authn_enterprise).

View File

@ -1,7 +1,7 @@
%% -*- mode: erlang -*-
{application, emqx_authz, [
{description, "An OTP application"},
{vsn, "0.1.24"},
{vsn, "0.1.25"},
{registered, []},
{mod, {emqx_authz_app, []}},
{applications, [

View File

@ -19,6 +19,8 @@
-behaviour(emqx_config_handler).
-behaviour(emqx_config_backup).
-dialyzer({nowarn_function, [authz_module/1]}).
-include("emqx_authz.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_hooks.hrl").
@ -571,7 +573,12 @@ find_action_in_hooks() ->
authz_module(built_in_database) ->
emqx_authz_mnesia;
authz_module(Type) ->
list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)).
case emqx_authz_enterprise:is_enterprise_module(Type) of
{ok, Module} ->
Module;
_ ->
list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type))
end.
type(#{type := Type}) -> type(Type);
type(#{<<"type">> := Type}) -> type(Type);
@ -591,8 +598,7 @@ type(built_in_database) -> built_in_database;
type(<<"built_in_database">>) -> built_in_database;
type(client_info) -> client_info;
type(<<"client_info">>) -> client_info;
%% should never happen if the input is type-checked by hocon schema
type(Unknown) -> throw({unknown_authz_source_type, Unknown}).
type(MaybeEnterprise) -> emqx_authz_enterprise:type(MaybeEnterprise).
maybe_write_files(#{<<"type">> := <<"file">>} = Source) ->
write_acl_file(Source);

View File

@ -95,7 +95,9 @@ fields(position) ->
in => body
}
)}
].
];
fields(MaybeEnterprise) ->
emqx_authz_enterprise:fields(MaybeEnterprise).
%%------------------------------------------------------------------------------
%% http type funcs
@ -283,7 +285,7 @@ authz_sources_types(Type) ->
mysql,
postgresql,
file
].
] ++ emqx_authz_enterprise:authz_sources_types().
to_list(A) when is_atom(A) ->
atom_to_list(A);

View File

@ -0,0 +1,66 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_authz_enterprise).
-export([
type_names/0,
fields/1,
is_enterprise_module/1,
authz_sources_types/0,
type/1,
desc/1
]).
-if(?EMQX_RELEASE_EDITION == ee).
%% type name set
type_names() ->
[ldap].
%% type -> type schema
fields(ldap) ->
emqx_ldap_authz:fields(config).
%% type -> type module
is_enterprise_module(ldap) ->
{ok, emqx_ldap_authz};
is_enterprise_module(_) ->
false.
%% api sources set
authz_sources_types() ->
[ldap].
%% atom-able name -> type
type(<<"ldap">>) -> ldap;
type(ldap) -> ldap;
type(Unknown) -> throw({unknown_authz_source_type, Unknown}).
desc(ldap) ->
emqx_ldap_authz:description();
desc(_) ->
undefined.
-else.
-dialyzer({nowarn_function, [fields/1, type/1, desc/1]}).
type_names() ->
[].
fields(Any) ->
error({invalid_field, Any}).
is_enterprise_module(_) ->
false.
authz_sources_types() ->
[].
%% should never happen if the input is type-checked by hocon schema
type(Unknown) -> throw({unknown_authz_source_type, Unknown}).
desc(_) ->
undefined.
-endif.

View File

@ -43,7 +43,8 @@
-export([
headers_no_content_type/1,
headers/1,
default_authz/0
default_authz/0,
authz_common_fields/1
]).
%%--------------------------------------------------------------------
@ -64,7 +65,8 @@ type_names() ->
redis_single,
redis_sentinel,
redis_cluster
].
] ++
emqx_authz_enterprise:type_names().
namespace() -> authz.
@ -176,7 +178,9 @@ fields("node_error") ->
[
node_name(),
{"error", ?HOCON(string(), #{desc => ?DESC("node_error")})}
].
];
fields(MaybeEnterprise) ->
emqx_authz_enterprise:fields(MaybeEnterprise).
common_field() ->
[
@ -220,8 +224,8 @@ desc(redis_sentinel) ->
?DESC(redis_sentinel);
desc(redis_cluster) ->
?DESC(redis_cluster);
desc(_) ->
undefined.
desc(MaybeEnterprise) ->
emqx_authz_enterprise:desc(MaybeEnterprise).
authz_common_fields(Type) ->
[

View File

@ -4,5 +4,6 @@
{deps, [
{emqx_connector, {path, "../../apps/emqx_connector"}},
{emqx_resource, {path, "../../apps/emqx_resource"}},
{emqx_authn, {path, "../../apps/emqx_authn"}}
{emqx_authn, {path, "../../apps/emqx_authn"}},
{emqx_authz, {path, "../../apps/emqx_authz"}}
]}.

View File

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

View File

@ -1,5 +1,5 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_ldap_authn).
@ -47,7 +47,7 @@ tags() ->
%% used for config check when the schema module is resolved
roots() ->
[{?CONF_NS, hoconsc:mk(hoconsc:ref(?MODULE, mysql))}].
[{?CONF_NS, hoconsc:mk(hoconsc:ref(?MODULE, ldap))}].
fields(ldap) ->
[
@ -73,7 +73,7 @@ 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(type) -> emqx_schema:timeout_duration_ms();
query_timeout(desc) -> ?DESC(?FUNCTION_NAME);
query_timeout(default) -> <<"5s">>;
query_timeout(_) -> undefined.
@ -173,7 +173,7 @@ ensure_password(
undefined ->
{error, no_password};
[LDAPPassword | _] ->
extract_hash_algorithm(LDAPPassword, Password, fun try_decode_passowrd/4, Entry, State)
extract_hash_algorithm(LDAPPassword, Password, fun try_decode_password/4, Entry, State)
end.
%% RFC 2307 format password
@ -207,7 +207,7 @@ is_valid_algorithm(HashType, PasswordHash, Password, Entry, State) ->
end.
%% this password is in LDIF format which is base64 encoding
try_decode_passowrd(LDAPPassword, Password, Entry, State) ->
try_decode_password(LDAPPassword, Password, Entry, State) ->
case safe_base64_decode(LDAPPassword) of
{ok, Decode} ->
extract_hash_algorithm(
@ -279,9 +279,7 @@ hash_password(Algorithm, Salt, suffix, Password) ->
hash_password(Algorithm, Data) ->
crypto:hash(Algorithm, Data).
compare_password(hash, PasswordHash, PasswordHash) ->
true;
compare_password(hash, LDAPPasswordHash, PasswordHash) ->
emqx_passwd:compare_secure(LDAPPasswordHash, PasswordHash);
compare_password(base64, Base64HashData, PasswordHash) ->
Base64HashData =:= base64:encode(PasswordHash);
compare_password(_, _, _) ->
false.
emqx_passwd:compare_secure(Base64HashData, base64:encode(PasswordHash)).

View File

@ -0,0 +1,164 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_ldap_authz).
-include_lib("emqx_authz/include/emqx_authz.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("eldap/include/eldap.hrl").
-behaviour(emqx_authz).
-define(PREPARE_KEY, ?MODULE).
%% AuthZ Callbacks
-export([
description/0,
create/1,
update/1,
destroy/1,
authorize/4
]).
-export([fields/1]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
%%------------------------------------------------------------------------------
%% Hocon Schema
%%------------------------------------------------------------------------------
fields(config) ->
emqx_authz_schema:authz_common_fields(ldap) ++
[
{publish_attribute, attribute_meta(publish_attribute, <<"mqttPublishTopic">>)},
{subscribe_attribute, attribute_meta(subscribe_attribute, <<"mqttSubscriptionTopic">>)},
{all_attribute, attribute_meta(all_attribute, <<"mqttPubSubTopic">>)},
{query_timeout,
?HOCON(
emqx_schema:timeout_duration_ms(),
#{
desc => ?DESC(query_timeout),
default => <<"5s">>
}
)}
] ++
emqx_ldap:fields(config).
attribute_meta(Name, Default) ->
?HOCON(
string(),
#{
default => Default,
desc => ?DESC(Name)
}
).
%%------------------------------------------------------------------------------
%% AuthZ Callbacks
%%------------------------------------------------------------------------------
description() ->
"AuthZ with LDAP".
create(Source) ->
ResourceId = emqx_authz_utils:make_resource_id(?MODULE),
{ok, _Data} = emqx_authz_utils:create_resource(ResourceId, emqx_ldap, Source),
Annotations = new_annotations(#{id => ResourceId}, Source),
Source#{annotations => Annotations}.
update(Source) ->
case emqx_authz_utils:update_resource(emqx_ldap, Source) of
{error, Reason} ->
error({load_config_error, Reason});
{ok, Id} ->
Annotations = new_annotations(#{id => Id}, Source),
Source#{annotations => Annotations}
end.
destroy(#{annotations := #{id := Id}}) ->
ok = emqx_resource:remove_local(Id).
authorize(
Client,
Action,
Topic,
#{
query_timeout := QueryTimeout,
annotations := #{id := ResourceID} = Annotations
}
) ->
Attrs = select_attrs(Action, Annotations),
case emqx_resource:simple_sync_query(ResourceID, {query, Client, Attrs, QueryTimeout}) of
{ok, []} ->
nomatch;
{ok, [Entry | _]} ->
do_authorize(Action, Topic, Attrs, Entry);
{error, Reason} ->
?SLOG(error, #{
msg => "query_ldap_error",
reason => Reason,
resource_id => ResourceID
}),
nomatch
end.
do_authorize(Action, Topic, [Attr | T], Entry) ->
Topics = proplists:get_value(Attr, Entry#eldap_entry.attributes, []),
case match_topic(Topic, Topics) of
true ->
{matched, allow};
false ->
do_authorize(Action, Topic, T, Entry)
end;
do_authorize(_Action, _Topic, [], _Entry) ->
nomatch.
new_annotations(Init, Source) ->
lists:foldl(
fun(Attr, Acc) ->
Acc#{
Attr =>
case maps:get(Attr, Source) of
Value when is_binary(Value) ->
erlang:binary_to_list(Value);
Value ->
Value
end
}
end,
Init,
[publish_attribute, subscribe_attribute, all_attribute]
).
select_attrs(#{action_type := publish}, #{publish_attribute := Pub, all_attribute := All}) ->
[Pub, All];
select_attrs(_, #{subscribe_attribute := Sub, all_attribute := All}) ->
[Sub, All].
match_topic(Target, Topics) ->
lists:any(
fun(Topic) ->
emqx_topic:match(Target, erlang:list_to_binary(Topic))
end,
Topics
).

View File

@ -27,5 +27,5 @@ dn : {token, {dn, TokenLine}}.
Erlang code.
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------

View File

@ -1,5 +1,5 @@
Header "%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------".
Nonterminals

View File

@ -1,5 +1,5 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_ldap_SUITE).

View File

@ -1,7 +1,6 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_ldap_authn_SUITE).
-compile(nowarn_export_all).

View File

@ -0,0 +1,173 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_ldap_authz_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include("emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(LDAP_HOST, "ldap").
-define(LDAP_DEFAULT_PORT, 389).
-define(LDAP_RESOURCE, <<"emqx_ldap_authz_SUITE">>).
all() ->
emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()).
groups() ->
emqx_authz_test_lib:table_groups(t_run_case, cases()).
init_per_suite(Config) ->
ok = stop_apps([emqx_resource]),
case emqx_common_test_helpers:is_tcp_server_available(?LDAP_HOST, ?LDAP_DEFAULT_PORT) of
true ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
ok = start_apps([emqx_resource]),
ok = create_ldap_resource(),
Config;
false ->
{skip, no_ldap}
end.
end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_resource:remove_local(?LDAP_RESOURCE),
ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_group(Group, Config) ->
[{test_case, emqx_authz_test_lib:get_case(Group, cases())} | Config].
end_per_group(_Group, _Config) ->
ok.
init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(),
Config.
end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
ok.
set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
ok.
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_run_case(Config) ->
Case = ?config(test_case, Config),
ok = setup_authz_source(),
ok = emqx_authz_test_lib:run_checks(Case).
t_create_invalid(_Config) ->
ok = setup_authz_source(),
BadConfig = maps:merge(
raw_ldap_authz_config(),
#{<<"server">> => <<"255.255.255.255:33333">>}
),
{ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
[_] = emqx_authz:lookup().
%%------------------------------------------------------------------------------
%% Case
%%------------------------------------------------------------------------------
cases() ->
[
#{
name => simpe_publish,
client_info => #{username => <<"mqttuser0001">>},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"mqttuser0001/pub/1">>},
{allow, ?AUTHZ_PUBLISH, <<"mqttuser0001/pub/+">>},
{allow, ?AUTHZ_PUBLISH, <<"mqttuser0001/pub/#">>}
]
},
#{
name => simpe_subscribe,
client_info => #{username => <<"mqttuser0001">>},
checks => [
{allow, ?AUTHZ_SUBSCRIBE, <<"mqttuser0001/sub/1">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"mqttuser0001/sub/+">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"mqttuser0001/sub/#">>}
]
},
#{
name => simpe_pubsub,
client_info => #{username => <<"mqttuser0001">>},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"mqttuser0001/pubsub/1">>},
{allow, ?AUTHZ_PUBLISH, <<"mqttuser0001/pubsub/+">>},
{allow, ?AUTHZ_PUBLISH, <<"mqttuser0001/pubsub/#">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"mqttuser0001/pubsub/1">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"mqttuser0001/pubsub/+">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"mqttuser0001/pubsub/#">>}
]
},
#{
name => simpe_unmatched,
client_info => #{username => <<"mqttuser0001">>},
checks => [
{deny, ?AUTHZ_PUBLISH, <<"mqttuser0001/req/mqttuser0001/+">>},
{deny, ?AUTHZ_PUBLISH, <<"mqttuser0001/req/mqttuser0002/+">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"mqttuser0001/req/+/mqttuser0002">>}
]
}
].
%%------------------------------------------------------------------------------
%% Helpers
%%------------------------------------------------------------------------------
setup_authz_source() ->
setup_config(#{}).
raw_ldap_authz_config() ->
#{
<<"enable">> => <<"true">>,
<<"type">> => <<"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
}.
setup_config(SpecialParams) ->
emqx_authz_test_lib:setup_config(
raw_ldap_authz_config(),
SpecialParams
).
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).
create_ldap_resource() ->
{ok, _} = emqx_resource:create_local(
?LDAP_RESOURCE,
?RESOURCE_GROUP,
emqx_ldap,
ldap_config(),
#{}
),
ok.

View File

@ -1,5 +1,5 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_ldap_filter_SUITE).

View File

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

View File

@ -0,0 +1 @@
Integrated LDAP as a authorization source.

View File

@ -10,7 +10,7 @@ password_attribute.label:
"""Password Attribute"""
is_superuser_attribute.desc:
"""Indicates which attribute is used to represent whether the user is a super user."""
"""Indicates which attribute is used to represent whether the user is a superuser."""
is_superuser_attribute.label:
"""IsSuperuser Attribute"""

View File

@ -0,0 +1,27 @@
emqx_ldap_authz {
publish_attribute.desc:
"""Indicates which attribute is used to represent the allowed topics list of the `publish`."""
publish_attribute.label:
"""Publish Attribute"""
subscribe_attribute.desc:
"""Indicates which attribute is used to represent the allowed topics list of the `subscribe`."""
subscribe_attribute.label:
"""Subscribe Attribute"""
all_attribute.desc:
"""Indicates which attribute is used to represent the both allowed topics list of `publish` and `subscribe`."""
all_attribute.label:
"""All Attribute"""
query_timeout.desc:
"""Timeout for the LDAP query."""
query_timeout.label:
"""Query Timeout"""
}