feat(ldap-authz): integrate the LDAP authorization

This commit is contained in:
firest 2023-08-04 14:51:45 +08:00
parent 7055eafb91
commit 0571fd8cac
10 changed files with 456 additions and 13 deletions

View File

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

View File

@ -95,7 +95,9 @@ fields(position) ->
in => body in => body
} }
)} )}
]. ];
fields(MaybeEnterprise) ->
emqx_authz_enterprise:fields(MaybeEnterprise).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% http type funcs %% http type funcs
@ -283,7 +285,7 @@ authz_sources_types(Type) ->
mysql, mysql,
postgresql, postgresql,
file file
]. ] ++ emqx_authz_enterprise:authz_sources_types().
to_list(A) when is_atom(A) -> to_list(A) when is_atom(A) ->
atom_to_list(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([ -export([
headers_no_content_type/1, headers_no_content_type/1,
headers/1, headers/1,
default_authz/0 default_authz/0,
authz_common_fields/1
]). ]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -64,7 +65,8 @@ type_names() ->
redis_single, redis_single,
redis_sentinel, redis_sentinel,
redis_cluster redis_cluster
]. ] ++
emqx_authz_enterprise:type_names().
namespace() -> authz. namespace() -> authz.
@ -176,7 +178,9 @@ fields("node_error") ->
[ [
node_name(), node_name(),
{"error", ?HOCON(string(), #{desc => ?DESC("node_error")})} {"error", ?HOCON(string(), #{desc => ?DESC("node_error")})}
]. ];
fields(MaybeEnterprise) ->
emqx_authz_enterprise:fields(MaybeEnterprise).
common_field() -> common_field() ->
[ [
@ -220,8 +224,8 @@ desc(redis_sentinel) ->
?DESC(redis_sentinel); ?DESC(redis_sentinel);
desc(redis_cluster) -> desc(redis_cluster) ->
?DESC(redis_cluster); ?DESC(redis_cluster);
desc(_) -> desc(MaybeEnterprise) ->
undefined. emqx_authz_enterprise:desc(MaybeEnterprise).
authz_common_fields(Type) -> authz_common_fields(Type) ->
[ [

View File

@ -4,5 +4,6 @@
{deps, [ {deps, [
{emqx_connector, {path, "../../apps/emqx_connector"}}, {emqx_connector, {path, "../../apps/emqx_connector"}},
{emqx_resource, {path, "../../apps/emqx_resource"}}, {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, [ {applications, [
kernel, kernel,
stdlib, stdlib,
emqx_authn emqx_authn,
emqx_authz
]}, ]},
{env, []}, {env, []},
{modules, []}, {modules, []},

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

@ -1,7 +1,6 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. %% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_ldap_authn_SUITE). -module(emqx_ldap_authn_SUITE).
-compile(nowarn_export_all). -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

@ -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"""
}