chore(auth): split emqx_authn and emqx_authz apps

This commit is contained in:
Ilya Averyanov 2023-08-23 21:48:44 +03:00
parent e049dc0e76
commit 1eb75b43c4
295 changed files with 5746 additions and 3974 deletions

4
.github/CODEOWNERS vendored
View File

@ -3,9 +3,9 @@
## apps ## apps
/apps/emqx/ @emqx/emqx-review-board @lafirest /apps/emqx/ @emqx/emqx-review-board @lafirest
/apps/emqx_authn/ @emqx/emqx-review-board @JimMoen @savonarola
/apps/emqx_authz/ @emqx/emqx-review-board @JimMoen @savonarola
/apps/emqx_connector/ @emqx/emqx-review-board /apps/emqx_connector/ @emqx/emqx-review-board
/apps/emqx_auth/ @emqx/emqx-review-board @JimMoen @savonarola
/apps/emqx_connector/ @emqx/emqx-review-board @JimMoen
/apps/emqx_dashboard/ @emqx/emqx-review-board @JimMoen @lafirest /apps/emqx_dashboard/ @emqx/emqx-review-board @JimMoen @lafirest
/apps/emqx_dashboard_rbac/ @emqx/emqx-review-board @lafirest /apps/emqx_dashboard_rbac/ @emqx/emqx-review-board @lafirest
/apps/emqx_dashboard_sso/ @emqx/emqx-review-board @JimMoen @lafirest /apps/emqx_dashboard_sso/ @emqx/emqx-review-board @JimMoen @lafirest

View File

@ -132,7 +132,7 @@ jobs:
-Dpgsql_user="root" \ -Dpgsql_user="root" \
-Dpgsql_pwd="public" \ -Dpgsql_pwd="public" \
-Ddbname="mqtt" \ -Ddbname="mqtt" \
-Droute="apps/emqx_authn/test/data/certs" \ -Droute="apps/emqx_auth/test/data/certs" \
-Dca_name="ca.crt" \ -Dca_name="ca.crt" \
-Dkey_name="client.key" \ -Dkey_name="client.key" \
-Dcert_name="client.crt" \ -Dcert_name="client.crt" \
@ -195,7 +195,7 @@ jobs:
-Dmysql_user="root" \ -Dmysql_user="root" \
-Dmysql_pwd="public" \ -Dmysql_pwd="public" \
-Ddbname="mqtt" \ -Ddbname="mqtt" \
-Droute="apps/emqx_authn/test/data/certs" \ -Droute="apps/emqx_auth/test/data/certs" \
-Dca_name="ca.crt" \ -Dca_name="ca.crt" \
-Dkey_name="client.key" \ -Dkey_name="client.key" \
-Dcert_name="client.crt" \ -Dcert_name="client.crt" \

View File

@ -37,25 +37,25 @@ format_path([Name | Rest]) -> [iol(Name), "." | format_path(Rest)].
%% @doc Plain check the input config. %% @doc Plain check the input config.
%% The input can either be `richmap' or plain `map'. %% The input can either be `richmap' or plain `map'.
%% Always return plain map with atom keys. %% Always return plain map with atom keys.
-spec check(module(), hocon:config() | iodata()) -> -spec check(hocon_schema:schema(), hocon:config() | iodata()) ->
{ok, hocon:config()} | {error, any()}. {ok, hocon:config()} | {error, any()}.
check(SchemaModule, Conf) -> check(Schema, Conf) ->
%% TODO: remove required %% TODO: remove required
%% fields should state required or not in their schema %% fields should state required or not in their schema
Opts = #{atom_key => true, required => false}, Opts = #{atom_key => true, required => false},
check(SchemaModule, Conf, Opts). check(Schema, Conf, Opts).
check(SchemaModule, Conf, Opts) when is_map(Conf) -> check(Schema, Conf, Opts) when is_map(Conf) ->
try try
{ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts)} {ok, hocon_tconf:check_plain(Schema, Conf, Opts)}
catch catch
throw:Errors:Stacktrace -> throw:Errors:Stacktrace ->
compact_errors(Errors, Stacktrace) compact_errors(Errors, Stacktrace)
end; end;
check(SchemaModule, HoconText, Opts) -> check(Schema, HoconText, Opts) ->
case hocon:binary(HoconText, #{format => map}) of case hocon:binary(HoconText, #{format => map}) of
{ok, MapConfig} -> {ok, MapConfig} ->
check(SchemaModule, MapConfig, Opts); check(Schema, MapConfig, Opts);
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
end. end.

View File

@ -216,7 +216,8 @@ roots(high) ->
} }
)} )}
] ++ ] ++
emqx_schema_hooks:injection_point('roots.high') ++ emqx_schema_hooks:injection_point(
'roots.high',
[ [
%% NOTE: authorization schema here is only to keep emqx app pure %% NOTE: authorization schema here is only to keep emqx app pure
%% the full schema for EMQX node is injected in emqx_conf_schema. %% the full schema for EMQX node is injected in emqx_conf_schema.
@ -225,7 +226,8 @@ roots(high) ->
ref(?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME), ref(?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME),
#{importance => ?IMPORTANCE_HIDDEN} #{importance => ?IMPORTANCE_HIDDEN}
)} )}
]; ]
);
roots(medium) -> roots(medium) ->
[ [
{"broker", {"broker",

View File

@ -30,6 +30,7 @@
-export([ -export([
injection_point/1, injection_point/1,
injection_point/2,
inject_from_modules/1 inject_from_modules/1
]). ]).
@ -43,9 +44,15 @@
%% API %% API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec injection_point(hookpoint()) -> [hocon_schema:field()].
injection_point(PointName) -> injection_point(PointName) ->
persistent_term:get(?HOOKPOINT_PT_KEY(PointName), []). injection_point(PointName, []).
-spec injection_point(hookpoint(), [hocon_schema:field()]) -> [hocon_schema:field()].
injection_point(PointName, Default) ->
persistent_term:get(?HOOKPOINT_PT_KEY(PointName), Default).
-spec erase_injections() -> ok.
erase_injections() -> erase_injections() ->
lists:foreach( lists:foreach(
fun fun
@ -57,6 +64,7 @@ erase_injections() ->
persistent_term:get() persistent_term:get()
). ).
-spec any_injections() -> boolean().
any_injections() -> any_injections() ->
lists:any( lists:any(
fun fun
@ -68,6 +76,7 @@ any_injections() ->
persistent_term:get() persistent_term:get()
). ).
-spec inject_from_modules([module() | {module(), term()}]) -> ok.
inject_from_modules(Modules) -> inject_from_modules(Modules) ->
Injections = Injections =
lists:foldl( lists:foldl(

View File

@ -527,11 +527,11 @@ copy_certs(_, _) ->
copy_acl_conf() -> copy_acl_conf() ->
Dest = filename:join([code:lib_dir(emqx), "etc/acl.conf"]), Dest = filename:join([code:lib_dir(emqx), "etc/acl.conf"]),
case code:lib_dir(emqx_authz) of case code:lib_dir(emqx_auth_file) of
{error, bad_name} -> {error, bad_name} ->
(not filelib:is_regular(Dest)) andalso file:write_file(Dest, <<"">>); (not filelib:is_regular(Dest)) andalso file:write_file(Dest, <<"">>);
_ -> _ ->
{ok, _} = file:copy(deps_path(emqx_authz, "etc/acl.conf"), Dest) {ok, _} = file:copy(deps_path(emqx_auth_file, "etc/acl.conf"), Dest)
end, end,
ok. ok.

View File

@ -330,7 +330,7 @@ default_appspec(emqx, SuiteOpts) ->
% overwrite everything with a default configuration. % overwrite everything with a default configuration.
before_start => fun inhibit_config_loader/2 before_start => fun inhibit_config_loader/2
}; };
default_appspec(emqx_authz, _SuiteOpts) -> default_appspec(emqx_auth, _SuiteOpts) ->
#{ #{
config => #{ config => #{
% NOTE % NOTE
@ -356,7 +356,7 @@ default_appspec(emqx_conf, SuiteOpts) ->
Config, Config,
[ [
emqx, emqx,
emqx_authz emqx_auth
] ]
), ),
#{ #{

View File

@ -0,0 +1,22 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-ifndef(EMQX_AUTH_HRL).
-define(EMQX_AUTH_HRL, true).
-define(APP, emqx_auth).
-endif.

View File

@ -17,16 +17,12 @@
-ifndef(EMQX_AUTHN_HRL). -ifndef(EMQX_AUTHN_HRL).
-define(EMQX_AUTHN_HRL, true). -define(EMQX_AUTHN_HRL, true).
-include_lib("emqx_authentication.hrl"). -include("emqx_authn_chains.hrl").
-define(APP, emqx_authn). -define(AUTHN, emqx_authn_chains).
-define(AUTHN, emqx_authentication).
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}"). -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}").
-define(AUTH_SHARD, emqx_authn_shard).
%% has to be the same as the root field name defined in emqx_schema %% has to be the same as the root field name defined in emqx_schema
-define(CONF_NS, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME). -define(CONF_NS, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME).
-define(CONF_NS_ATOM, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM). -define(CONF_NS_ATOM, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
@ -34,6 +30,6 @@
-type authenticator_id() :: binary(). -type authenticator_id() :: binary().
-define(RESOURCE_GROUP, <<"emqx_authn">>). -define(AUTHN_RESOURCE_GROUP, <<"emqx_authn">>).
-endif. -endif.

View File

@ -14,8 +14,8 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-ifndef(EMQX_AUTHENTICATION_HRL). -ifndef(EMQX_AUTHN_CHAINS_HRL).
-define(EMQX_AUTHENTICATION_HRL, true). -define(EMQX_AUTHN_CHAINS_HRL, true).
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_access_control.hrl"). -include_lib("emqx/include/emqx_access_control.hrl").

View File

@ -0,0 +1,37 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-ifndef(EMQX_AUTHN_SOURCES_HRL).
-define(EMQX_AUTHN_SOURCES_HRL, true).
-define(PROVIDER_SCHEMA_MODS, [
emqx_authn_mnesia_schema,
emqx_authn_mysql_schema,
emqx_authn_postgresql_schema,
emqx_authn_mongodb_schema,
emqx_authn_redis_schema,
emqx_authn_http_schema,
emqx_authn_jwt_schema,
emqx_authn_scram_mnesia_schema
]).
-define(EE_PROVIDER_SCHEMA_MODS, [
emqx_authn_ldap_schema,
emqx_authn_ldap_bind_schema,
emqx_gcp_device_authn_schema
]).
-endif.

View File

@ -16,7 +16,7 @@
-include_lib("emqx/include/emqx_access_control.hrl"). -include_lib("emqx/include/emqx_access_control.hrl").
-define(APP, emqx_authz). -include("emqx_auth.hrl").
%% authz_mnesia %% authz_mnesia
-define(ACL_TABLE, emqx_acl). -define(ACL_TABLE, emqx_acl).
@ -157,7 +157,7 @@
count => 1 count => 1
}). }).
-define(RESOURCE_GROUP, <<"emqx_authz">>). -define(AUTHZ_RESOURCE_GROUP, <<"emqx_authz">>).
-define(AUTHZ_FEATURES, [rich_actions]). -define(AUTHZ_FEATURES, [rich_actions]).

View File

@ -0,0 +1,34 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-ifndef(EMQX_AUTHZ_SCHEMA_HRL).
-define(EMQX_AUTHZ_SCHEMA_HRL, true).
-define(SOURCE_SCHEMA_MODS, [
emqx_authz_file_schema,
emqx_authz_mnesia_schema,
emqx_authz_http_schema,
emqx_authz_redis_schema,
emqx_authz_mysql_schema,
emqx_authz_postgresql_schema,
emqx_authz_mongodb_schema
]).
-define(EE_SOURCE_SCHEMA_MODS, [
emqx_authz_ldap_schema
]).
-endif.

View File

@ -2,12 +2,7 @@
{deps, [ {deps, [
{emqx, {path, "../emqx"}}, {emqx, {path, "../emqx"}},
{emqx_utils, {path, "../emqx_utils"}}, {emqx_utils, {path, "../emqx_utils"}}
{emqx_connector, {path, "../emqx_connector"}},
{emqx_mongodb, {path, "../emqx_mongodb"}},
{emqx_redis, {path, "../emqx_redis"}},
{emqx_mysql, {path, "../emqx_mysql"}},
{emqx_bridge_http, {path, "../emqx_bridge_http"}}
]}. ]}.
{edoc_opts, [{preprocess, true}]}. {edoc_opts, [{preprocess, true}]}.
@ -34,6 +29,4 @@
{cover_opts, [verbose]}. {cover_opts, [verbose]}.
{cover_export_enabled, true}. {cover_export_enabled, true}.
{erl_first_files, ["src/emqx_authentication.erl"]}.
{project_plugins, [erlfmt]}. {project_plugins, [erlfmt]}.

View File

@ -0,0 +1,17 @@
%% -*- mode: erlang -*-
{application, emqx_auth, [
{description, "EMQX Authentication and authorization"},
{vsn, "0.1.27"},
{modules, []},
{registered, [emqx_auth_sup]},
{applications, [
kernel,
stdlib,
emqx
]},
{mod, {emqx_auth_app, []}},
{env, []},
{licenses, ["Apache-2.0"]},
{maintainers, ["EMQX Team <contact@emqx.io>"]},
{links, [{"Homepage", "https://emqx.io/"}]}
]}.

View File

@ -14,7 +14,7 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_authn_app). -module(emqx_auth_app).
-include("emqx_authn.hrl"). -include("emqx_authn.hrl").
@ -26,7 +26,7 @@
stop/1 stop/1
]). ]).
-include_lib("emqx_authentication.hrl"). -include_lib("emqx_authn_chains.hrl").
-dialyzer({nowarn_function, [start/2]}). -dialyzer({nowarn_function, [start/2]}).
@ -37,56 +37,17 @@
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
%% required by test cases, ensure the injection of schema %% required by test cases, ensure the injection of schema
_ = emqx_conf_schema:roots(), _ = emqx_conf_schema:roots(),
ok = mria_rlog:wait_for_shards([?AUTH_SHARD], infinity),
{ok, Sup} = emqx_authn_sup:start_link(), {ok, Sup} = emqx_authn_sup:start_link(),
case initialize() of ok = emqx_authz:init(),
ok -> {ok, Sup}; {ok, Sup}.
{error, Reason} -> {error, Reason}
end.
stop(_State) -> stop(_State) ->
ok = deinitialize(). ok = deinitialize(),
ok = emqx_authz:deinit().
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Internal functions %% Internal functions
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
initialize() ->
ok = ?AUTHN:register_providers(emqx_authn:providers()),
lists:foreach(
fun({ChainName, AuthConfig}) ->
?AUTHN:initialize_authentication(
ChainName,
AuthConfig
)
end,
chain_configs()
).
deinitialize() -> deinitialize() ->
ok = ?AUTHN:deregister_providers(provider_types()),
ok = emqx_authn_utils:cleanup_resources(). ok = emqx_authn_utils:cleanup_resources().
chain_configs() ->
[global_chain_config() | listener_chain_configs()].
global_chain_config() ->
{?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM], [])}.
listener_chain_configs() ->
lists:map(
fun({ListenerID, _}) ->
{ListenerID, emqx:get_config(auth_config_path(ListenerID), [])}
end,
emqx_listeners:list()
).
auth_config_path(ListenerID) ->
Names = [
binary_to_existing_atom(N, utf8)
|| N <- binary:split(atom_to_binary(ListenerID), <<":">>)
],
[listeners] ++ Names ++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM].
provider_types() ->
lists:map(fun({Type, _Module}) -> Type end, emqx_authn:providers()).

View File

@ -0,0 +1,28 @@
%%--------------------------------------------------------------------
%% 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_auth_schema).
-export([
namespace/0,
roots/0
]).
namespace() -> auth.
%% @doc auth schema is not exported
%% but directly used by emqx_schema
roots() -> [].

View File

@ -1,5 +1,5 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Copyright (c) 2017-2023 EMQ Technologies Co., Ltd. All Rights Reserved. %% Copyright (c) 2021-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%% %%
%% Licensed under the Apache License, Version 2.0 (the "License"); %% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License. %% you may not use this file except in compliance with the License.
@ -14,25 +14,18 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_authentication_sup). -module(emqx_auth_sup).
-behaviour(supervisor). -behaviour(supervisor).
-export([start_link/0]). -export([
start_link/0,
-export([init/1]). init/1
]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() -> start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%%--------------------------------------------------------------------
%% Supervisor callbacks
%%--------------------------------------------------------------------
init([]) -> init([]) ->
SupFlags = #{ SupFlags = #{
strategy => one_for_one, strategy => one_for_one,
@ -41,12 +34,21 @@ init([]) ->
}, },
AuthN = #{ AuthN = #{
id => emqx_authentication, id => emqx_authn_sup,
start => {emqx_authentication, start_link, []}, start => {emqx_authn_sup, start_link, []},
restart => permanent, restart => permanent,
shutdown => 1000, shutdown => 1000,
type => worker, type => supervisor
modules => [emqx_authentication]
}, },
{ok, {SupFlags, [AuthN]}}. AuthZ = #{
id => emqx_authz_sup,
start => {emqx_authz_sup, start_link, []},
restart => permanent,
shutdown => 1000,
type => supervisor
},
ChildSpecs = [AuthN, AuthZ],
{ok, {SupFlags, ChildSpecs}}.

View File

@ -19,95 +19,42 @@
-behaviour(emqx_config_backup). -behaviour(emqx_config_backup).
-export([ -export([
providers/0, fill_defaults/1,
check_config/1,
check_config/2,
%% for telemetry information %% for telemetry information
get_enabled_authns/0 get_enabled_authns/0,
register_provider/2,
deregister_provider/1
]). ]).
-export([merge_config/1, merge_config_local/2, import_config/1]). -export([merge_config/1, merge_config_local/2, import_config/1]).
-include("emqx_authn.hrl"). -include("emqx_authn.hrl").
providers() -> fill_defaults(Config) ->
[ #{?CONF_NS_BINARY := WithDefaults} = do_fill_defaults(Config),
{{password_based, built_in_database}, emqx_authn_mnesia}, WithDefaults.
{{password_based, mysql}, emqx_authn_mysql},
{{password_based, postgresql}, emqx_authn_pgsql},
{{password_based, mongodb}, emqx_authn_mongodb},
{{password_based, redis}, emqx_authn_redis},
{{password_based, http}, emqx_authn_http},
{jwt, emqx_authn_jwt},
{{scram, built_in_database}, emqx_enhanced_authn_scram_mnesia}
] ++
emqx_authn_enterprise:providers().
check_config(Config) -> do_fill_defaults(Config0) ->
check_config(Config, #{}).
check_config(Config, Opts) ->
case do_check_config(Config, Opts) of
#{?CONF_NS_ATOM := Checked} -> Checked;
#{?CONF_NS_BINARY := WithDefaults} -> WithDefaults
end.
do_check_config(#{<<"mechanism">> := Mec0} = Config, Opts) ->
Mec = atom(Mec0, #{error => unknown_mechanism}),
Key =
case maps:get(<<"backend">>, Config, false) of
false -> Mec;
Backend -> {Mec, atom(Backend, #{error => unknown_backend})}
end,
case lists:keyfind(Key, 1, providers()) of
false ->
Reason =
case Key of
{M, B} ->
#{mechanism => M, backend => B};
M ->
#{mechanism => M}
end,
throw(Reason#{error => unknown_authn_provider});
{_, ProviderModule} ->
do_check_config_maybe_throw(ProviderModule, Config, Opts)
end;
do_check_config(Config, _Opts) when is_map(Config) ->
throw(#{
error => invalid_config,
reason => "mechanism_field_required"
}).
do_check_config_maybe_throw(ProviderModule, Config0, Opts) ->
Config = #{?CONF_NS_BINARY => Config0}, Config = #{?CONF_NS_BINARY => Config0},
case emqx_hocon:check(ProviderModule, Config, Opts#{atom_key => true}) of Schema = #{roots => [{?CONF_NS, hoconsc:mk(emqx_authn_schema:authenticator_type())}]},
case emqx_hocon:check(Schema, Config, #{make_serializable => true}) of
{ok, Checked} -> {ok, Checked} ->
Checked; Checked;
{error, Reason} -> {error, Reason} ->
throw(Reason) throw(Reason)
end. end.
%% The atoms have to be loaded already,
%% which might be an issue for plugins which are loaded after node boot
%% but they should really manage their own configs in that case.
atom(Bin, ErrorContext) ->
try
binary_to_existing_atom(Bin, utf8)
catch
_:_ ->
throw(ErrorContext#{value => Bin})
end.
-spec get_enabled_authns() -> -spec get_enabled_authns() ->
#{ #{
authenticators => [authenticator_id()], authenticators => [authenticator_id()],
overridden_listeners => #{authenticator_id() => pos_integer()} overridden_listeners => #{authenticator_id() => pos_integer()}
}. }.
get_enabled_authns() -> get_enabled_authns() ->
%% at the moment of writing, `emqx_authentication:list_chains/0' %% at the moment of writing, `emqx_authn_chains:list_chains/0'
%% result is always wrapped in `{ok, _}', and it cannot return any %% result is always wrapped in `{ok, _}', and it cannot return any
%% error values. %% error values.
{ok, Chains} = emqx_authentication:list_chains(), {ok, Chains} = emqx_authn_chains:list_chains(),
AuthnTypes = lists:usort([ AuthnTypes = lists:usort([
Type Type
|| #{authenticators := As} <- Chains, || #{authenticators := As} <- Chains,
@ -132,6 +79,12 @@ get_enabled_authns() ->
tally_authenticators(#{id := AuthenticatorName}, Acc) -> tally_authenticators(#{id := AuthenticatorName}, Acc) ->
maps:update_with(AuthenticatorName, fun(N) -> N + 1 end, 1, Acc). maps:update_with(AuthenticatorName, fun(N) -> N + 1 end, 1, Acc).
register_provider(ProviderType, ProviderModule) ->
ok = ?AUTHN:register_provider(ProviderType, ProviderModule).
deregister_provider(ProviderType) ->
ok = ?AUTHN:deregister_provider(ProviderType).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Data backup %% Data backup
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -142,7 +95,7 @@ import_config(RawConf) ->
AuthnList = authn_list(maps:get(?CONF_NS_BINARY, RawConf, [])), AuthnList = authn_list(maps:get(?CONF_NS_BINARY, RawConf, [])),
OldAuthnList = emqx:get_raw_config([?CONF_NS_BINARY], []), OldAuthnList = emqx:get_raw_config([?CONF_NS_BINARY], []),
MergedAuthnList = emqx_utils:merge_lists( MergedAuthnList = emqx_utils:merge_lists(
OldAuthnList, AuthnList, fun emqx_authentication:authenticator_id/1 OldAuthnList, AuthnList, fun emqx_authn_chains:authenticator_id/1
), ),
case emqx_conf:update([?CONF_NS_ATOM], MergedAuthnList, ?IMPORT_OPTS) of case emqx_conf:update([?CONF_NS_ATOM], MergedAuthnList, ?IMPORT_OPTS) of
{ok, #{raw_config := NewRawConf}} -> {ok, #{raw_config := NewRawConf}} ->
@ -152,9 +105,9 @@ import_config(RawConf) ->
end. end.
changed_paths(OldAuthnList, NewAuthnList) -> changed_paths(OldAuthnList, NewAuthnList) ->
KeyFun = fun emqx_authentication:authenticator_id/1, KeyFun = fun emqx_authn_chains:authenticator_id/1,
Changed = maps:get(changed, emqx_utils:diff_lists(NewAuthnList, OldAuthnList, KeyFun)), Changed = maps:get(changed, emqx_utils:diff_lists(NewAuthnList, OldAuthnList, KeyFun)),
[[?CONF_NS_BINARY, emqx_authentication:authenticator_id(OldAuthn)] || {OldAuthn, _} <- Changed]. [[?CONF_NS_BINARY, emqx_authn_chains:authenticator_id(OldAuthn)] || {OldAuthn, _} <- Changed].
authn_list(Authn) when is_list(Authn) -> authn_list(Authn) when is_list(Authn) ->
Authn; Authn;

View File

@ -30,7 +30,7 @@
-define(NOT_FOUND, 'NOT_FOUND'). -define(NOT_FOUND, 'NOT_FOUND').
-define(ALREADY_EXISTS, 'ALREADY_EXISTS'). -define(ALREADY_EXISTS, 'ALREADY_EXISTS').
-define(INTERNAL_ERROR, 'INTERNAL_ERROR'). -define(INTERNAL_ERROR, 'INTERNAL_ERROR').
-define(CONFIG, emqx_authentication_config). -define(CONFIG, emqx_authn_config).
% Swagger % Swagger
@ -823,7 +823,7 @@ find_listener(ListenerID) ->
end. end.
with_chain(ListenerID, Fun) -> with_chain(ListenerID, Fun) ->
{ok, ChainNames} = emqx_authentication:list_chain_names(), {ok, ChainNames} = emqx_authn_chains:list_chain_names(),
ListenerChainName = ListenerChainName =
[Name || Name <- ChainNames, atom_to_binary(Name) =:= ListenerID], [Name || Name <- ChainNames, atom_to_binary(Name) =:= ListenerID],
case ListenerChainName of case ListenerChainName of
@ -852,7 +852,7 @@ list_authenticators(ConfKeyPath) ->
NAuthenticators = [ NAuthenticators = [
maps:put( maps:put(
id, id,
emqx_authentication:authenticator_id(AuthenticatorConfig), emqx_authn_chains:authenticator_id(AuthenticatorConfig),
convert_certs(AuthenticatorConfig) convert_certs(AuthenticatorConfig)
) )
|| AuthenticatorConfig <- AuthenticatorsConfig || AuthenticatorConfig <- AuthenticatorsConfig
@ -868,33 +868,25 @@ list_authenticator(_, ConfKeyPath, AuthenticatorID) ->
end end
). ).
resource_provider() -> %% TODO
[ %% This breaks encapsulation, resource_id should be obtained
emqx_authn_mysql, %% through provider callback
emqx_authn_pgsql,
emqx_authn_mongodb,
emqx_authn_redis,
emqx_authn_http
] ++
emqx_authn_enterprise:resource_provider().
lookup_from_local_node(ChainName, AuthenticatorID) -> lookup_from_local_node(ChainName, AuthenticatorID) ->
NodeId = node(self()), NodeId = node(self()),
case emqx_authentication:lookup_authenticator(ChainName, AuthenticatorID) of case emqx_authn_chains:lookup_authenticator(ChainName, AuthenticatorID) of
{ok, #{provider := Provider, state := State}} -> {ok, #{state := State}} ->
MetricsId = emqx_authentication:metrics_id(ChainName, AuthenticatorID), MetricsId = emqx_authn_chains:metrics_id(ChainName, AuthenticatorID),
Metrics = emqx_metrics_worker:get_metrics(authn_metrics, MetricsId), Metrics = emqx_metrics_worker:get_metrics(authn_metrics, MetricsId),
case lists:member(Provider, resource_provider()) of case State of
false -> #{resource_id := ResourceId} ->
{ok, {NodeId, connected, Metrics, #{}}};
true ->
#{resource_id := ResourceId} = State,
case emqx_resource:get_instance(ResourceId) of case emqx_resource:get_instance(ResourceId) of
{error, not_found} -> {error, not_found} ->
{error, {NodeId, not_found_resource}}; {error, {NodeId, not_found_resource}};
{ok, _, #{status := Status}} -> {ok, _, #{status := Status}} ->
{ok, {NodeId, Status, Metrics, emqx_resource:get_metrics(ResourceId)}} {ok, {NodeId, Status, Metrics, emqx_resource:get_metrics(ResourceId)}}
end end;
_ ->
{ok, {NodeId, connected, Metrics, #{}}}
end; end;
{error, Reason} -> {error, Reason} ->
{error, {NodeId, list_to_binary(io_lib:format("~p", [Reason]))}} {error, {NodeId, list_to_binary(io_lib:format("~p", [Reason]))}}
@ -1064,7 +1056,7 @@ add_user(
) -> ) ->
IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false), IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false),
case case
emqx_authentication:add_user( emqx_authn_chains:add_user(
ChainName, ChainName,
AuthenticatorID, AuthenticatorID,
#{ #{
@ -1090,7 +1082,7 @@ update_user(ChainName, AuthenticatorID, UserID, UserInfo0) ->
serialize_error({missing_parameter, password}); serialize_error({missing_parameter, password});
false -> false ->
UserInfo = emqx_utils_maps:safe_atom_key_map(UserInfo0), UserInfo = emqx_utils_maps:safe_atom_key_map(UserInfo0),
case emqx_authentication:update_user(ChainName, AuthenticatorID, UserID, UserInfo) of case emqx_authn_chains:update_user(ChainName, AuthenticatorID, UserID, UserInfo) of
{ok, User} -> {ok, User} ->
{200, User}; {200, User};
{error, Reason} -> {error, Reason} ->
@ -1099,7 +1091,7 @@ update_user(ChainName, AuthenticatorID, UserID, UserInfo0) ->
end. end.
find_user(ChainName, AuthenticatorID, UserID) -> find_user(ChainName, AuthenticatorID, UserID) ->
case emqx_authentication:lookup_user(ChainName, AuthenticatorID, UserID) of case emqx_authn_chains:lookup_user(ChainName, AuthenticatorID, UserID) of
{ok, User} -> {ok, User} ->
{200, User}; {200, User};
{error, Reason} -> {error, Reason} ->
@ -1107,7 +1099,7 @@ find_user(ChainName, AuthenticatorID, UserID) ->
end. end.
delete_user(ChainName, AuthenticatorID, UserID) -> delete_user(ChainName, AuthenticatorID, UserID) ->
case emqx_authentication:delete_user(ChainName, AuthenticatorID, UserID) of case emqx_authn_chains:delete_user(ChainName, AuthenticatorID, UserID) of
ok -> ok ->
{204}; {204};
{error, Reason} -> {error, Reason} ->
@ -1115,7 +1107,7 @@ delete_user(ChainName, AuthenticatorID, UserID) ->
end. end.
list_users(ChainName, AuthenticatorID, QueryString) -> list_users(ChainName, AuthenticatorID, QueryString) ->
case emqx_authentication:list_users(ChainName, AuthenticatorID, QueryString) of case emqx_authn_chains:list_users(ChainName, AuthenticatorID, QueryString) of
{error, page_limit_invalid} -> {error, page_limit_invalid} ->
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}}; {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
{error, Reason} -> {error, Reason} ->
@ -1143,7 +1135,7 @@ find_config(AuthenticatorID, AuthenticatorsConfig) ->
[ [
AC AC
|| AC <- ensure_list(AuthenticatorsConfig), || AC <- ensure_list(AuthenticatorsConfig),
AuthenticatorID =:= emqx_authentication:authenticator_id(AC) AuthenticatorID =:= emqx_authn_chains:authenticator_id(AC)
], ],
case MatchingACs of case MatchingACs of
[] -> {error, {not_found, {authenticator, AuthenticatorID}}}; [] -> {error, {not_found, {authenticator, AuthenticatorID}}};
@ -1153,17 +1145,19 @@ find_config(AuthenticatorID, AuthenticatorsConfig) ->
fill_defaults(Configs) when is_list(Configs) -> fill_defaults(Configs) when is_list(Configs) ->
lists:map(fun fill_defaults/1, Configs); lists:map(fun fill_defaults/1, Configs);
fill_defaults(Config) -> fill_defaults(Config) ->
emqx_authn:check_config(merge_default_headers(Config), #{make_serializable => true}). emqx_authn:fill_defaults(merge_default_headers(Config)).
%% TODO
%% this breaks encapsulation, this should be done through provider callbacks
merge_default_headers(Config) -> merge_default_headers(Config) ->
case maps:find(<<"headers">>, Config) of case maps:find(<<"headers">>, Config) of
{ok, Headers} -> {ok, Headers} ->
NewHeaders = NewHeaders =
case Config of case Config of
#{<<"method">> := <<"get">>} -> #{<<"method">> := <<"get">>} ->
(emqx_authn_http:headers_no_content_type(converter))(Headers); emqx_authn_utils:convert_headers_no_content_type(Headers);
#{<<"method">> := <<"post">>} -> #{<<"method">> := <<"post">>} ->
(emqx_authn_http:headers(converter))(Headers); emqx_authn_utils:convert_headers(Headers);
_ -> _ ->
Headers Headers
end, end,

View File

@ -18,11 +18,11 @@
%% Authentication is a core functionality of MQTT, %% Authentication is a core functionality of MQTT,
%% the 'emqx' APP provides APIs for other APPs to implement %% the 'emqx' APP provides APIs for other APPs to implement
%% the authentication callbacks. %% the authentication callbacks.
-module(emqx_authentication). -module(emqx_authn_chains).
-behaviour(gen_server). -behaviour(gen_server).
-include("emqx_authentication.hrl"). -include("emqx_authn_chains.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").
-include_lib("stdlib/include/ms_transform.hrl"). -include_lib("stdlib/include/ms_transform.hrl").
@ -43,7 +43,8 @@
%% The authentication entrypoint. %% The authentication entrypoint.
-export([ -export([
authenticate/2 authenticate/2,
authenticate_deny/2
]). ]).
%% Authenticator manager process start/stop %% Authenticator manager process start/stop
@ -55,7 +56,6 @@
%% Authenticator management APIs %% Authenticator management APIs
-export([ -export([
initialize_authentication/2,
register_provider/2, register_provider/2,
register_providers/1, register_providers/1,
deregister_provider/1, deregister_provider/1,
@ -89,6 +89,7 @@
handle_call/3, handle_call/3,
handle_cast/2, handle_cast/2,
handle_info/2, handle_info/2,
handle_continue/2,
terminate/2, terminate/2,
code_change/3 code_change/3
]). ]).
@ -99,7 +100,8 @@
-export_type([ -export_type([
authenticator_id/0, authenticator_id/0,
position/0, position/0,
chain_name/0 chain_name/0,
authn_type/0
]). ]).
-ifdef(TEST). -ifdef(TEST).
@ -135,7 +137,7 @@ end).
state := map() state := map()
}. }.
-type config() :: emqx_authentication_config:config(). -type config() :: emqx_authn_config:config().
-type state() :: #{atom() => term()}. -type state() :: #{atom() => term()}.
-type extra() :: #{ -type extra() :: #{
is_superuser := boolean(), is_superuser := boolean(),
@ -146,85 +148,7 @@ end).
atom() => term() atom() => term()
}. }.
%% @doc check_config takes raw config from config file, -export_type([authenticator/0, config/0, state/0, extra/0, user_info/0]).
%% parse and validate it, and return parsed result.
-callback check_config(config()) -> config().
-callback create(AuthenticatorID, Config) ->
{ok, State}
| {error, term()}
when
AuthenticatorID :: authenticator_id(), Config :: config(), State :: state().
-callback update(Config, State) ->
{ok, NewState}
| {error, term()}
when
Config :: config(), State :: state(), NewState :: state().
-callback authenticate(Credential, State) ->
ignore
| {ok, Extra}
| {ok, Extra, AuthData}
| {continue, AuthCache}
| {continue, AuthData, AuthCache}
| {error, term()}
when
Credential :: map(),
State :: state(),
Extra :: extra(),
AuthData :: binary(),
AuthCache :: map().
-callback destroy(State) ->
ok
when
State :: state().
-callback import_users({Filename, FileData}, State) ->
ok
| {error, term()}
when
Filename :: binary(), FileData :: binary(), State :: state().
-callback add_user(UserInfo, State) ->
{ok, User}
| {error, term()}
when
UserInfo :: user_info(), State :: state(), User :: user_info().
-callback delete_user(UserID, State) ->
ok
| {error, term()}
when
UserID :: binary(), State :: state().
-callback update_user(UserID, UserInfo, State) ->
{ok, User}
| {error, term()}
when
UserID :: binary(), UserInfo :: map(), State :: state(), User :: user_info().
-callback lookup_user(UserID, UserInfo, State) ->
{ok, User}
| {error, term()}
when
UserID :: binary(), UserInfo :: map(), State :: state(), User :: user_info().
-callback list_users(State) ->
{ok, Users}
when
State :: state(), Users :: [user_info()].
-optional_callbacks([
import_users/2,
add_user/2,
delete_user/2,
update_user/3,
lookup_user/3,
list_users/1,
check_config/1
]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Authenticate %% Authenticate
@ -244,6 +168,9 @@ authenticate(#{listener := Listener, protocol := Protocol} = Credential, AuthRes
?TRACE_RESULT("authentication_result", AuthResult, no_chain) ?TRACE_RESULT("authentication_result", AuthResult, no_chain)
end. end.
authenticate_deny(_Credential, _AuthResult) ->
?TRACE_RESULT("authentication_result", {ok, {error, not_authorized}}, not_initialized).
get_authenticators(Listener, Global) -> get_authenticators(Listener, Global) ->
case ets:lookup(?CHAINS_TAB, Listener) of case ets:lookup(?CHAINS_TAB, Listener) of
[#chain{name = Name, authenticators = Authenticators}] -> [#chain{name = Name, authenticators = Authenticators}] ->
@ -274,35 +201,14 @@ get_providers() ->
%% and maybe a 'backend' key. %% and maybe a 'backend' key.
%% This function works with both parsed (atom keys) and raw (binary keys) %% This function works with both parsed (atom keys) and raw (binary keys)
%% configurations. %% configurations.
-spec authenticator_id(config()) -> authenticator_id().
authenticator_id(Config) -> authenticator_id(Config) ->
emqx_authentication_config:authenticator_id(Config). emqx_authn_config:authenticator_id(Config).
%% @doc Call this API to initialize authenticators implemented in another APP.
-spec initialize_authentication(chain_name(), [config()]) -> ok.
initialize_authentication(_, []) ->
ok;
initialize_authentication(ChainName, AuthenticatorsConfig) ->
CheckedConfig = to_list(AuthenticatorsConfig),
lists:foreach(
fun(AuthenticatorConfig) ->
case create_authenticator(ChainName, AuthenticatorConfig) of
{ok, _} ->
ok;
{error, Reason} ->
?SLOG(error, #{
msg => "failed_to_create_authenticator",
authenticator => authenticator_id(AuthenticatorConfig),
reason => Reason
})
end
end,
CheckedConfig
).
-spec start_link() -> {ok, pid()} | ignore | {error, term()}. -spec start_link() -> {ok, pid()} | ignore | {error, term()}.
start_link() -> start_link() ->
%% Create chains ETS table here so that it belongs to the supervisor %% Create chains ETS table here so that it belongs to the supervisor
%% and survives `emqx_authentication` crashes. %% and survives `emqx_authn_chains` crashes.
ok = create_chain_table(), ok = create_chain_table(),
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@ -316,7 +222,7 @@ stop() ->
%% For example, ``[{{'password_based', redis}, emqx_authn_redis}]'' %% For example, ``[{{'password_based', redis}, emqx_authn_redis}]''
%% NOTE: Later registered provider may override earlier registered if they %% NOTE: Later registered provider may override earlier registered if they
%% happen to clash the same `AuthNType'. %% happen to clash the same `AuthNType'.
-spec register_providers([{authn_type(), module()}]) -> ok. -spec register_providers([{authn_type(), module()}]) -> ok | {error, term()}.
register_providers(Providers) -> register_providers(Providers) ->
call({register_providers, Providers}). call({register_providers, Providers}).
@ -437,10 +343,12 @@ list_users(ChainName, AuthenticatorID, FuzzyParams) ->
init(_Opts) -> init(_Opts) ->
process_flag(trap_exit, true), process_flag(trap_exit, true),
Module = emqx_authentication_config, Module = emqx_authn_config,
ok = emqx_config_handler:add_handler([?CONF_ROOT], Module), ok = emqx_config_handler:add_handler([?CONF_ROOT], Module),
ok = emqx_config_handler:add_handler([listeners, '?', '?', ?CONF_ROOT], Module), ok = emqx_config_handler:add_handler([listeners, '?', '?', ?CONF_ROOT], Module),
{ok, #{hooked => false, providers => #{}}}. ok = hook_deny(),
{ok, #{hooked => false, providers => #{}, init_done => false},
{continue, initialize_authentication}}.
handle_call(get_providers, _From, #{providers := Providers} = State) -> handle_call(get_providers, _From, #{providers := Providers} = State) ->
reply(Providers, State); reply(Providers, State);
@ -458,7 +366,7 @@ handle_call(
Reg0, Reg0,
Providers Providers
), ),
reply(ok, State#{providers := Reg}); reply(ok, State#{providers := Reg}, initialize_authentication);
Clashes -> Clashes ->
reply({error, {authentication_type_clash, Clashes}}, State) reply({error, {authentication_type_clash, Clashes}}, State)
end; end;
@ -523,6 +431,12 @@ handle_call(Req, _From, State) ->
?SLOG(error, #{msg => "unexpected_call", call => Req}), ?SLOG(error, #{msg => "unexpected_call", call => Req}),
{reply, ignored, State}. {reply, ignored, State}.
handle_continue(initialize_authentication, #{init_done := true} = State) ->
{noreply, State};
handle_continue(initialize_authentication, #{providers := Providers} = State) ->
InitDone = initialize_authentication(Providers),
{noreply, State#{init_done := InitDone}}.
handle_cast(Req, State) -> handle_cast(Req, State) ->
?SLOG(error, #{msg => "unexpected_cast", cast => Req}), ?SLOG(error, #{msg => "unexpected_cast", cast => Req}),
{noreply, State}. {noreply, State}.
@ -554,6 +468,71 @@ code_change(_OldVsn, State, _Extra) ->
%% Private functions %% Private functions
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
initialize_authentication(Providers) ->
Chains = chain_configs(),
ProviderTypes = maps:keys(Providers),
HasProviders = lists:all(
fun({_, ChainConfigs}) ->
has_providers_for_configs(ChainConfigs, ProviderTypes)
end,
Chains
),
do_initialize_authentication(Providers, Chains, HasProviders).
do_initialize_authentication(_Providers, _Chains, _HasProviders = false) ->
false;
do_initialize_authentication(Providers, Chains, _HasProviders = true) ->
ok = lists:foreach(
fun({ChainName, ChainConfigs}) ->
initialize_chain_authentication(Providers, ChainName, ChainConfigs)
end,
Chains
),
ok = unhook_deny(),
true.
initialize_chain_authentication(_Providers, _ChainName, []) ->
ok;
initialize_chain_authentication(Providers, ChainName, AuthenticatorsConfig) ->
lists:foreach(
fun(AuthenticatorConfig) ->
CreateResult = with_new_chain(ChainName, fun(Chain) ->
handle_create_authenticator(Chain, AuthenticatorConfig, Providers)
end),
case CreateResult of
{ok, _} ->
ok;
{error, Reason} ->
?SLOG(error, #{
msg => "failed_to_create_authenticator",
authenticator => authenticator_id(AuthenticatorConfig),
reason => Reason
})
end
end,
to_list(AuthenticatorsConfig)
).
has_providers_for_configs(AuthConfig, ProviderTypes) ->
Configs = to_list(AuthConfig),
lists:all(
fun(Config) ->
has_providers_for_config(Config, ProviderTypes)
end,
Configs
).
has_providers_for_config(_Config, []) ->
false;
has_providers_for_config(#{mechanism := Mechanism, backend := Backend}, [
{Mechanism, Backend} | _ProviderTypes
]) ->
true;
has_providers_for_config(#{mechanism := Mechanism}, [Mechanism | _ProviderTypes]) ->
true;
has_providers_for_config(Config, [_ProviderType | ProviderTypes]) ->
has_providers_for_config(Config, ProviderTypes).
handle_update_authenticator(Chain, AuthenticatorID, Config) -> handle_update_authenticator(Chain, AuthenticatorID, Config) ->
#chain{authenticators = Authenticators} = Chain, #chain{authenticators = Authenticators} = Chain,
case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
@ -700,6 +679,9 @@ authenticate_with_provider(#authenticator{id = ID, provider = Provider, state =
reply(Reply, State) -> reply(Reply, State) ->
{reply, Reply, State}. {reply, Reply, State}.
reply(Reply, State, Continue) ->
{reply, Reply, State, {continue, Continue}}.
save_chain(#chain{ save_chain(#chain{
name = Name, name = Name,
authenticators = [] authenticators = []
@ -773,6 +755,12 @@ maybe_unhook(#{hooked := true} = State) ->
maybe_unhook(State) -> maybe_unhook(State) ->
State. State.
hook_deny() ->
ok = emqx_hooks:put('client.authenticate', {?MODULE, authenticate_deny, []}, ?HP_AUTHN).
unhook_deny() ->
ok = emqx_hooks:del('client.authenticate', {?MODULE, authenticate_deny, []}).
do_create_authenticator(AuthenticatorID, #{enable := Enable} = Config, Providers) -> do_create_authenticator(AuthenticatorID, #{enable := Enable} = Config, Providers) ->
Type = authn_type(Config), Type = authn_type(Config),
case maps:get(Type, Providers, undefined) of case maps:get(Type, Providers, undefined) of
@ -945,6 +933,27 @@ insert_user_group(_Chain, Config) ->
metrics_id(ChainName, AuthenticatorId) -> metrics_id(ChainName, AuthenticatorId) ->
iolist_to_binary([atom_to_binary(ChainName), <<"-">>, AuthenticatorId]). iolist_to_binary([atom_to_binary(ChainName), <<"-">>, AuthenticatorId]).
chain_configs() ->
[global_chain_config() | listener_chain_configs()].
global_chain_config() ->
{?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM], [])}.
listener_chain_configs() ->
lists:map(
fun({ListenerID, _}) ->
{ListenerID, emqx:get_config(auth_config_path(ListenerID), [])}
end,
emqx_listeners:list()
).
auth_config_path(ListenerID) ->
Names = [
binary_to_existing_atom(N, utf8)
|| N <- binary:split(atom_to_binary(ListenerID), <<":">>)
],
[listeners] ++ Names ++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM].
to_list(undefined) -> []; to_list(undefined) -> [];
to_list(M) when M =:= #{} -> []; to_list(M) when M =:= #{} -> [];
to_list(M) when is_map(M) -> [M]; to_list(M) when is_map(M) -> [M];

View File

@ -15,7 +15,7 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% @doc Authenticator configuration management module. %% @doc Authenticator configuration management module.
-module(emqx_authentication_config). -module(emqx_authn_config).
-behaviour(emqx_config_handler). -behaviour(emqx_config_handler).
@ -40,7 +40,7 @@
-export_type([config/0]). -export_type([config/0]).
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include("emqx_authentication.hrl"). -include("emqx_authn_chains.hrl").
-type parsed_config() :: #{ -type parsed_config() :: #{
mechanism := atom(), mechanism := atom(),
@ -50,9 +50,9 @@
-type raw_config() :: #{binary() => term()}. -type raw_config() :: #{binary() => term()}.
-type config() :: parsed_config() | raw_config(). -type config() :: parsed_config() | raw_config().
-type authenticator_id() :: emqx_authentication:authenticator_id(). -type authenticator_id() :: emqx_authn_chains:authenticator_id().
-type position() :: emqx_authentication:position(). -type position() :: emqx_authn_chains:position().
-type chain_name() :: emqx_authentication:chain_name(). -type chain_name() :: emqx_authn_chains:chain_name().
-type update_request() :: -type update_request() ::
{create_authenticator, chain_name(), map()} {create_authenticator, chain_name(), map()}
| {delete_authenticator, chain_name(), authenticator_id()} | {delete_authenticator, chain_name(), authenticator_id()}
@ -164,7 +164,7 @@ do_post_config_update(
_, {create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs _, {create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs
) -> ) ->
NConfig = get_authenticator_config(authenticator_id(Config), NewConfig), NConfig = get_authenticator_config(authenticator_id(Config), NewConfig),
emqx_authentication:create_authenticator(ChainName, NConfig); emqx_authn_chains:create_authenticator(ChainName, NConfig);
do_post_config_update( do_post_config_update(
_, _,
{delete_authenticator, ChainName, AuthenticatorID}, {delete_authenticator, ChainName, AuthenticatorID},
@ -172,7 +172,7 @@ do_post_config_update(
_OldConfig, _OldConfig,
_AppEnvs _AppEnvs
) -> ) ->
emqx_authentication:delete_authenticator(ChainName, AuthenticatorID); emqx_authn_chains:delete_authenticator(ChainName, AuthenticatorID);
do_post_config_update( do_post_config_update(
_, _,
{update_authenticator, ChainName, AuthenticatorID, Config}, {update_authenticator, ChainName, AuthenticatorID, Config},
@ -184,7 +184,7 @@ do_post_config_update(
{error, not_found} -> {error, not_found} ->
{error, {not_found, {authenticator, AuthenticatorID}}}; {error, {not_found, {authenticator, AuthenticatorID}}};
NConfig -> NConfig ->
emqx_authentication:update_authenticator(ChainName, AuthenticatorID, NConfig) emqx_authn_chains:update_authenticator(ChainName, AuthenticatorID, NConfig)
end; end;
do_post_config_update( do_post_config_update(
_, _,
@ -193,7 +193,7 @@ do_post_config_update(
_OldConfig, _OldConfig,
_AppEnvs _AppEnvs
) -> ) ->
emqx_authentication:move_authenticator(ChainName, AuthenticatorID, Position); emqx_authn_chains:move_authenticator(ChainName, AuthenticatorID, Position);
do_post_config_update(_, _UpdateReq, OldConfig, OldConfig, _AppEnvs) -> do_post_config_update(_, _UpdateReq, OldConfig, OldConfig, _AppEnvs) ->
ok; ok;
do_post_config_update(ConfPath, _UpdateReq, NewConfig0, OldConfig0, _AppEnvs) -> do_post_config_update(ConfPath, _UpdateReq, NewConfig0, OldConfig0, _AppEnvs) ->
@ -204,7 +204,7 @@ do_post_config_update(ConfPath, _UpdateReq, NewConfig0, OldConfig0, _AppEnvs) ->
NewIds = lists:map(fun authenticator_id/1, NewConfig), NewIds = lists:map(fun authenticator_id/1, NewConfig),
ok = delete_authenticators(NewIds, ChainName, OldConfig), ok = delete_authenticators(NewIds, ChainName, OldConfig),
ok = create_or_update_authenticators(OldIds, ChainName, NewConfig), ok = create_or_update_authenticators(OldIds, ChainName, NewConfig),
ok = emqx_authentication:reorder_authenticator(ChainName, NewIds), ok = emqx_authn_chains:reorder_authenticator(ChainName, NewIds),
ok. ok.
%% @doc Handle listener config changes made at higher level. %% @doc Handle listener config changes made at higher level.
@ -228,9 +228,9 @@ create_or_update_authenticators(OldIds, ChainName, NewConfig) ->
Id = authenticator_id(Conf), Id = authenticator_id(Conf),
case lists:member(Id, OldIds) of case lists:member(Id, OldIds) of
true -> true ->
emqx_authentication:update_authenticator(ChainName, Id, Conf); emqx_authn_chains:update_authenticator(ChainName, Id, Conf);
false -> false ->
emqx_authentication:create_authenticator(ChainName, Conf) emqx_authn_chains:create_authenticator(ChainName, Conf)
end end
end, end,
NewConfig NewConfig
@ -245,7 +245,7 @@ delete_authenticators(NewIds, ChainName, OldConfig) ->
true -> true ->
ok; ok;
false -> false ->
emqx_authentication:delete_authenticator(ChainName, Id) emqx_authn_chains:delete_authenticator(ChainName, Id)
end end
end, end,
OldConfig OldConfig

View File

@ -0,0 +1,36 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_authn_enterprise).
-include("emqx_authn_schema.hrl").
-export([provider_schema_mods/0]).
-if(?EMQX_RELEASE_EDITION == ee).
% providers() ->
% [
% {{password_based, ldap}, emqx_authn_ldap},
% {{password_based, ldap_bind}, emqx_ldap_authn_bind},
% {gcp_device, emqx_gcp_device_authn}
% ].
% resource_provider() ->
% [emqx_authn_ldap, emqx_ldap_authn_bind].
provider_schema_mods() ->
?EE_PROVIDER_SCHEMA_MODS.
-else.
provider_schema_mods() ->
[].
% providers() ->
% [].
% resource_provider() ->
% [].
-endif.

View File

@ -0,0 +1,98 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2021-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_provider).
-type authenticator_id() :: emqx_authn_chains:authenticator_id().
-type config() :: emqx_authn_chains:config().
-type state() :: emqx_authn_chains:state().
-type extra() :: emqx_authn_chains:extra().
-type user_info() :: emqx_authn_chains:user_info().
-callback create(AuthenticatorID, Config) ->
{ok, State}
| {error, term()}
when
AuthenticatorID :: authenticator_id(), Config :: config(), State :: state().
-callback update(Config, State) ->
{ok, NewState}
| {error, term()}
when
Config :: config(), State :: state(), NewState :: state().
-callback authenticate(Credential, State) ->
ignore
| {ok, Extra}
| {ok, Extra, AuthData}
| {continue, AuthCache}
| {continue, AuthData, AuthCache}
| {error, term()}
when
Credential :: map(),
State :: state(),
Extra :: extra(),
AuthData :: binary(),
AuthCache :: map().
-callback destroy(State) ->
ok
when
State :: state().
-callback import_users({Filename, FileData}, State) ->
ok
| {error, term()}
when
Filename :: binary(), FileData :: binary(), State :: state().
-callback add_user(UserInfo, State) ->
{ok, User}
| {error, term()}
when
UserInfo :: user_info(), State :: state(), User :: user_info().
-callback delete_user(UserID, State) ->
ok
| {error, term()}
when
UserID :: binary(), State :: state().
-callback update_user(UserID, UserInfo, State) ->
{ok, User}
| {error, term()}
when
UserID :: binary(), UserInfo :: map(), State :: state(), User :: user_info().
-callback lookup_user(UserID, UserInfo, State) ->
{ok, User}
| {error, term()}
when
UserID :: binary(), UserInfo :: map(), State :: state(), User :: user_info().
-callback list_users(State) ->
{ok, Users}
when
State :: state(), Users :: [user_info()].
-optional_callbacks([
import_users/2,
add_user/2,
delete_user/2,
update_user/3,
lookup_user/3,
list_users/1
]).

View File

@ -19,7 +19,8 @@
-elvis([{elvis_style, invalid_dynamic_call, disable}]). -elvis([{elvis_style, invalid_dynamic_call, disable}]).
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-include("emqx_authn.hrl"). -include("emqx_authn.hrl").
-include("emqx_authentication.hrl"). -include("emqx_authn_schema.hrl").
-include("emqx_authn_chains.hrl").
-behaviour(emqx_schema_hooks). -behaviour(emqx_schema_hooks).
-export([ -export([
@ -29,7 +30,7 @@
-export([ -export([
common_fields/0, common_fields/0,
roots/0, roots/0,
validations/0, % validations/0,
tags/0, tags/0,
fields/1, fields/1,
authenticator_type/0, authenticator_type/0,
@ -38,6 +39,15 @@
backend/1 backend/1
]). ]).
%%--------------------------------------------------------------------
%% Authn Source Schema Behaviour
%%--------------------------------------------------------------------
-type schema_ref() :: ?R_REF(module(), hocon_schema:name()).
-callback refs() -> [schema_ref()].
-callback select_union_member(emqx_config:raw_config()) -> schema_ref() | undefined | no_return().
-callback fields(hocon_schema:name()) -> [hocon_schema:field()].
roots() -> []. roots() -> [].
injected_fields() -> injected_fields() ->
@ -49,95 +59,53 @@ injected_fields() ->
tags() -> tags() ->
[<<"Authentication">>]. [<<"Authentication">>].
common_fields() ->
[{enable, fun enable/1}].
enable(type) -> boolean();
enable(default) -> true;
enable(desc) -> ?DESC(?FUNCTION_NAME);
enable(_) -> undefined.
authenticator_type() -> authenticator_type() ->
hoconsc:union(union_member_selector(emqx_authn:providers())). hoconsc:union(union_member_selector(provider_schema_mods())).
authenticator_type_without_scram() -> authenticator_type_without_scram() ->
Providers = lists:filtermap( hoconsc:union(
fun union_member_selector(provider_schema_mods() -- [emqx_authn_scram_mnesia_schema])
({{scram, _Backend}, _Mod}) -> ).
false;
(_) ->
true
end,
emqx_authn:providers()
),
hoconsc:union(union_member_selector(Providers)).
config_refs(Providers) -> union_member_selector(Mods) ->
lists:append([Module:refs() || {_, Module} <- Providers]). AllTypes = config_refs(Mods),
union_member_selector(Providers) ->
Types = config_refs(Providers),
fun fun
(all_union_members) -> Types; (all_union_members) -> AllTypes;
({value, Value}) -> select_union_member(Value, Providers) ({value, Value}) -> select_union_member(Value, Mods)
end. end.
select_union_member(#{<<"mechanism">> := _} = Value, Providers0) -> select_union_member(#{<<"mechanism">> := Mechanism, <<"backend">> := Backend}, []) ->
BackendVal = maps:get(<<"backend">>, Value, undefined),
MechanismVal = maps:get(<<"mechanism">>, Value),
BackendFilterFn = fun
({{_Mec, Backend}, _Mod}) ->
BackendVal =:= atom_to_binary(Backend);
(_) ->
BackendVal =:= undefined
end,
MechanismFilterFn = fun
({{Mechanism, _Backend}, _Mod}) ->
MechanismVal =:= atom_to_binary(Mechanism);
({Mechanism, _Mod}) ->
MechanismVal =:= atom_to_binary(Mechanism)
end,
case lists:filter(BackendFilterFn, Providers0) of
[] ->
throw(#{reason => "unknown_backend", backend => BackendVal});
Providers1 ->
case lists:filter(MechanismFilterFn, Providers1) of
[] ->
throw(#{ throw(#{
reason => "unsupported_mechanism", reason => "unsupported_mechanism",
mechanism => MechanismVal, mechanism => Mechanism,
backend => BackendVal backend => Backend
}); });
[{_, Module}] -> select_union_member(#{<<"mechanism">> := Mechanism}, []) ->
try_select_union_member(Module, Value) throw(#{
end reason => "unsupported_mechanism",
mechanism => Mechanism
});
select_union_member(#{<<"mechanism">> := _} = Value, [Mod | Mods]) ->
case Mod:select_union_member(Value) of
undefined ->
select_union_member(Value, Mods);
Member ->
Member
end; end;
select_union_member(Value, _Providers) when is_map(Value) -> select_union_member(#{} = _Value, _Mods) ->
throw(#{reason => "missing_mechanism_field"}); throw(#{reason => "missing_mechanism_field"});
select_union_member(Value, _Providers) -> select_union_member(Value, _Mods) ->
throw(#{reason => "not_a_struct", value => Value}). throw(#{reason => "not_a_struct", value => Value}).
try_select_union_member(Module, Value) -> config_refs(Mods) ->
%% some modules have `union_member_selector/1' exported to help selecting lists:append([Mod:refs() || Mod <- Mods]).
%% the sub-types, they are:
%% emqx_authn_http
%% emqx_authn_jwt
%% emqx_authn_mongodb
%% emqx_authn_redis
try
Module:union_member_selector({value, Value})
catch
error:undef ->
%% otherwise expect only one member from this module
Module:refs()
end.
root_type() -> root_type() ->
hoconsc:array(authenticator_type()). hoconsc:array(authenticator_type()).
global_auth_fields() -> global_auth_fields() ->
[ [
{?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM, {?CONF_NS_ATOM,
hoconsc:mk(root_type(), #{ hoconsc:mk(root_type(), #{
desc => ?DESC(global_authentication), desc => ?DESC(global_authentication),
converter => fun ensure_array/2, converter => fun ensure_array/2,
@ -148,7 +116,7 @@ global_auth_fields() ->
mqtt_listener_auth_fields() -> mqtt_listener_auth_fields() ->
[ [
{?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM, {?CONF_NS_ATOM,
hoconsc:mk(root_type(), #{ hoconsc:mk(root_type(), #{
desc => ?DESC(listener_authentication), desc => ?DESC(listener_authentication),
converter => fun ensure_array/2, converter => fun ensure_array/2,
@ -177,6 +145,14 @@ backend(Name) ->
desc => ?DESC("backend") desc => ?DESC("backend")
}). }).
common_fields() ->
[{enable, fun enable/1}].
enable(type) -> boolean();
enable(default) -> true;
enable(desc) -> ?DESC(?FUNCTION_NAME);
enable(_) -> undefined.
fields("metrics_status_fields") -> fields("metrics_status_fields") ->
[ [
{"resource_metrics", ?HOCON(?R_REF("resource_metrics"), #{desc => ?DESC("metrics")})}, {"resource_metrics", ?HOCON(?R_REF("resource_metrics"), #{desc => ?DESC("metrics")})},
@ -230,6 +206,9 @@ common_field() ->
{"rate_last5m", ?HOCON(float(), #{desc => ?DESC("rate_last5m")})} {"rate_last5m", ?HOCON(float(), #{desc => ?DESC("rate_last5m")})}
]. ].
provider_schema_mods() ->
?PROVIDER_SCHEMA_MODS ++ emqx_authn_enterprise:provider_schema_mods().
status() -> status() ->
hoconsc:enum([connected, disconnected, connecting]). hoconsc:enum([connected, disconnected, connecting]).
@ -244,27 +223,3 @@ array(Name) ->
array(Name, DescId) -> array(Name, DescId) ->
{Name, ?HOCON(?R_REF(Name), #{desc => ?DESC(DescId)})}. {Name, ?HOCON(?R_REF(Name), #{desc => ?DESC(DescId)})}.
validations() ->
[
{check_http_ssl_opts, fun(Conf) ->
CheckFun = fun emqx_authn_http:check_ssl_opts/1,
validation(Conf, CheckFun)
end},
{check_http_headers, fun(Conf) ->
CheckFun = fun emqx_authn_http:check_headers/1,
validation(Conf, CheckFun)
end}
].
validation(Conf, CheckFun) when is_map(Conf) ->
validation(hocon_maps:get(?CONF_NS, Conf), CheckFun);
validation(undefined, _) ->
ok;
validation([], _) ->
ok;
validation([AuthN | Tail], CheckFun) ->
case CheckFun(#{?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY => AuthN}) of
ok -> validation(Tail, CheckFun);
Error -> Error
end.

View File

@ -27,15 +27,21 @@ start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) -> init([]) ->
AuthNSup = #{ SupFlags = #{
id => emqx_authentication_sup, strategy => one_for_one,
start => {emqx_authentication_sup, start_link, []}, intensity => 100,
restart => permanent, period => 10
shutdown => infinity,
type => supervisor,
modules => [emqx_authentication_sup]
}, },
ChildSpecs = [AuthNSup], AuthN = #{
id => emqx_authn_chains,
start => {emqx_authn_chains, start_link, []},
restart => permanent,
shutdown => 1000,
type => worker,
modules => [emqx_authn_chains]
},
{ok, {{one_for_one, 10, 10}, ChildSpecs}}. ChildSpecs = [AuthN],
{ok, {SupFlags, ChildSpecs}}.

View File

@ -92,7 +92,7 @@ authenticator_import_users(
} }
) -> ) ->
[{FileName, FileData}] = maps:to_list(maps:without([type], File)), [{FileName, FileData}] = maps:to_list(maps:without([type], File)),
case emqx_authentication:import_users(?GLOBAL, AuthenticatorID, {FileName, FileData}) of case emqx_authn_chains:import_users(?GLOBAL, AuthenticatorID, {FileName, FileData}) of
ok -> {204}; ok -> {204};
{error, Reason} -> emqx_authn_api:serialize_error(Reason) {error, Reason} -> emqx_authn_api:serialize_error(Reason)
end; end;
@ -110,9 +110,7 @@ listener_authenticator_import_users(
emqx_authn_api:with_chain( emqx_authn_api:with_chain(
ListenerID, ListenerID,
fun(ChainName) -> fun(ChainName) ->
case case emqx_authn_chains:import_users(ChainName, AuthenticatorID, {FileName, FileData}) of
emqx_authentication:import_users(ChainName, AuthenticatorID, {FileName, FileData})
of
ok -> {204}; ok -> {204};
{error, Reason} -> emqx_authn_api:serialize_error(Reason) {error, Reason} -> emqx_authn_api:serialize_error(Reason)
end end

View File

@ -36,7 +36,12 @@
cleanup_resources/0, cleanup_resources/0,
make_resource_id/1, make_resource_id/1,
without_password/1, without_password/1,
to_bool/1 to_bool/1,
parse_url/1,
convert_headers/1,
convert_headers_no_content_type/1,
default_headers/0,
default_headers_no_content_type/0
]). ]).
-define(AUTHN_PLACEHOLDERS, [ -define(AUTHN_PLACEHOLDERS, [
@ -59,7 +64,7 @@
create_resource(ResourceId, Module, Config) -> create_resource(ResourceId, Module, Config) ->
Result = emqx_resource:create_local( Result = emqx_resource:create_local(
ResourceId, ResourceId,
?RESOURCE_GROUP, ?AUTHN_RESOURCE_GROUP,
Module, Module,
Config, Config,
?DEFAULT_RESOURCE_OPTS ?DEFAULT_RESOURCE_OPTS
@ -163,7 +168,7 @@ bin(X) -> X.
cleanup_resources() -> cleanup_resources() ->
lists:foreach( lists:foreach(
fun emqx_resource:remove_local/1, fun emqx_resource:remove_local/1,
emqx_resource:list_group_instances(?RESOURCE_GROUP) emqx_resource:list_group_instances(?AUTHN_RESOURCE_GROUP)
). ).
make_resource_id(Name) -> make_resource_id(Name) ->
@ -207,6 +212,49 @@ to_bool(MaybeBinInt) when is_binary(MaybeBinInt) ->
to_bool(_) -> to_bool(_) ->
false. false.
parse_url(Url) ->
case string:split(Url, "//", leading) of
[Scheme, UrlRem] ->
case string:split(UrlRem, "/", leading) of
[HostPort, Remaining] ->
BaseUrl = iolist_to_binary([Scheme, "//", HostPort]),
case string:split(Remaining, "?", leading) of
[Path, QueryString] ->
{BaseUrl, <<"/", Path/binary>>, QueryString};
[Path] ->
{BaseUrl, <<"/", Path/binary>>, <<>>}
end;
[HostPort] ->
{iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>}
end;
[Url] ->
throw({invalid_url, Url})
end.
convert_headers(Headers) ->
maps:merge(default_headers(), transform_header_name(Headers)).
convert_headers_no_content_type(Headers) ->
maps:without(
[<<"content-type">>],
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
).
default_headers() ->
maps:put(
<<"content-type">>,
<<"application/json">>,
default_headers_no_content_type()
).
default_headers_no_content_type() ->
#{
<<"accept">> => <<"application/json">>,
<<"cache-control">> => <<"no-cache">>,
<<"connection">> => <<"keep-alive">>,
<<"keep-alive">> => <<"timeout=30, max=1000">>
}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -242,3 +290,20 @@ mapping_credential(C = #{cn := CN, dn := DN}) ->
C#{cert_common_name => CN, cert_subject => DN}; C#{cert_common_name => CN, cert_subject => DN};
mapping_credential(C) -> mapping_credential(C) ->
C. C.
transform_header_name(Headers) ->
maps:fold(
fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc)
end,
#{},
Headers
).
to_list(A) when is_atom(A) ->
atom_to_list(A);
to_list(B) when is_binary(B) ->
binary_to_list(B);
to_list(L) when is_list(L) ->
L.

View File

@ -27,9 +27,12 @@
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-export([ -export([
register_source/2,
unregister_source/1,
register_metrics/0, register_metrics/0,
init/0, init/0,
deinit/0, deinit/0,
merge_defaults/1,
lookup/0, lookup/0,
lookup/1, lookup/1,
move/2, move/2,
@ -37,6 +40,7 @@
merge/1, merge/1,
merge_local/2, merge_local/2,
authorize/5, authorize/5,
authorize_deny/4,
%% for telemetry information %% for telemetry information
get_enabled_authzs/0 get_enabled_authzs/0
]). ]).
@ -48,24 +52,26 @@
-export([post_config_update/5, pre_config_update/3]). -export([post_config_update/5, pre_config_update/3]).
-export([acl_conf_file/0]). -export([
maybe_read_source_files/1,
maybe_read_source_files_safe/1
]).
% -export([acl_conf_file/0]).
%% Data backup %% Data backup
-export([ -export([
import_config/1, import_config/1,
maybe_read_acl_file/1, maybe_read_files/1,
maybe_write_acl_file/1 maybe_write_files/1
]). ]).
-type source() :: map().
-type match_result() :: {matched, allow} | {matched, deny} | nomatch.
-type default_result() :: allow | deny. -type default_result() :: allow | deny.
-type authz_result_value() :: #{result := allow | deny, from => _}. -type authz_result_value() :: #{result := allow | deny, from => _}.
-type authz_result() :: {stop, authz_result_value()} | {ok, authz_result_value()} | ignore. -type authz_result() :: {stop, authz_result_value()} | {ok, authz_result_value()} | ignore.
-type source() :: emqx_authz_source:source().
-type sources() :: [source()]. -type sources() :: [source()].
-define(METRIC_SUPERUSER, 'authorization.superuser'). -define(METRIC_SUPERUSER, 'authorization.superuser').
@ -75,51 +81,70 @@
-define(METRICS, [?METRIC_SUPERUSER, ?METRIC_ALLOW, ?METRIC_DENY, ?METRIC_NOMATCH]). -define(METRICS, [?METRIC_SUPERUSER, ?METRIC_ALLOW, ?METRIC_DENY, ?METRIC_NOMATCH]).
%% Initialize authz backend.
%% Populate the passed configuration map with necessary data,
%% like `ResourceID`s
-callback create(source()) -> source().
%% Update authz backend.
%% Change configuration, or simply enable/disable
-callback update(source()) -> source().
%% Destroy authz backend.
%% Make cleanup of all allocated data.
%% An authz backend will not be used after `destroy`.
-callback destroy(source()) -> ok.
%% Get authz text description.
-callback description() -> string().
%% Authorize client action.
-callback authorize(
emqx_types:clientinfo(),
emqx_types:pubsub(),
emqx_types:topic(),
source()
) -> match_result().
-optional_callbacks([
update/1
]).
-spec register_metrics() -> ok. -spec register_metrics() -> ok.
register_metrics() -> register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?METRICS). lists:foreach(fun emqx_metrics:ensure/1, ?METRICS).
init() -> init() ->
ok = register_metrics(), ok = register_metrics(),
ok = init_metrics(client_info_source()),
emqx_conf:add_handler(?CONF_KEY_PATH, ?MODULE), emqx_conf:add_handler(?CONF_KEY_PATH, ?MODULE),
emqx_conf:add_handler(?ROOT_KEY, ?MODULE), emqx_conf:add_handler(?ROOT_KEY, ?MODULE),
emqx_authz_source_registry:create(),
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize_deny, []}, ?HP_AUTHZ),
ok = register_source(client_info, emqx_authz_client_info),
ok.
register_source(Type, Module) ->
ok = emqx_authz_source_registry:register(Type, Module),
install_sources(not is_hook_installed() andalso are_all_providers_registered()).
unregister_source(Type) ->
ok = emqx_authz_source_registry:unregister(Type).
is_hook_installed() ->
lists:any(
fun(Callback) ->
case emqx_hooks:callback_action(Callback) of
{?MODULE, authorize, _} -> true;
_ -> false
end
end,
emqx_hooks:lookup('client.authorize')
).
are_all_providers_registered() ->
try
_ = lists:foreach(
fun(Type) ->
_ = emqx_authz_source_registry:get(Type)
end,
configured_types()
),
true
catch
{unknown_authz_source_type, _Type} ->
false
end.
configured_types() ->
lists:map(
fun(#{type := Type}) -> Type end,
emqx_conf:get(?CONF_KEY_PATH, [])
).
install_sources(true) ->
ok = init_metrics(client_info_source()),
Sources = emqx_conf:get(?CONF_KEY_PATH, []), Sources = emqx_conf:get(?CONF_KEY_PATH, []),
ok = check_dup_types(Sources), ok = check_dup_types(Sources),
NSources = create_sources(Sources), NSources = create_sources(Sources),
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [NSources]}, ?HP_AUTHZ). ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [NSources]}, ?HP_AUTHZ),
ok = emqx_hooks:del('client.authorize', {?MODULE, authorize_deny});
install_sources(false) ->
ok.
deinit() -> deinit() ->
ok = emqx_hooks:del('client.authorize', {?MODULE, authorize}), ok = emqx_hooks:del('client.authorize', {?MODULE, authorize}),
ok = emqx_hooks:del('client.authorize', {?MODULE, authorize_deny}),
emqx_conf:remove_handler(?CONF_KEY_PATH), emqx_conf:remove_handler(?CONF_KEY_PATH),
emqx_conf:remove_handler(?ROOT_KEY), emqx_conf:remove_handler(?ROOT_KEY),
emqx_authz_utils:cleanup_resources(). emqx_authz_utils:cleanup_resources().
@ -163,7 +188,14 @@ pre_config_update(Path, Cmd, Sources) ->
{error, Reason} -> {error, Reason}; {error, Reason} -> {error, Reason};
NSources -> {ok, NSources} NSources -> {ok, NSources}
catch catch
_:Reason -> {error, Reason} Error:Reason:Stack ->
?SLOG(info, #{
msg => "error_in_pre_config_update",
exception => Error,
reason => Reason,
stacktrace => Stack
}),
{error, Reason}
end. end.
do_pre_config_update(?CONF_KEY_PATH, Cmd, Sources) -> do_pre_config_update(?CONF_KEY_PATH, Cmd, Sources) ->
@ -191,17 +223,17 @@ do_pre_config_replace(NewConf, OldConf) ->
do_pre_config_update({?CMD_MOVE, _, _} = Cmd, Sources) -> do_pre_config_update({?CMD_MOVE, _, _} = Cmd, Sources) ->
do_move(Cmd, Sources); do_move(Cmd, Sources);
do_pre_config_update({?CMD_PREPEND, Source}, Sources) -> do_pre_config_update({?CMD_PREPEND, Source}, Sources) ->
NSource = maybe_write_files(Source), NSource = maybe_write_source_files(Source),
NSources = [NSource] ++ Sources, NSources = [NSource] ++ Sources,
ok = check_dup_types(NSources), ok = check_dup_types(NSources),
NSources; NSources;
do_pre_config_update({?CMD_APPEND, Source}, Sources) -> do_pre_config_update({?CMD_APPEND, Source}, Sources) ->
NSource = maybe_write_files(Source), NSource = maybe_write_source_files(Source),
NSources = Sources ++ [NSource], NSources = Sources ++ [NSource],
ok = check_dup_types(NSources), ok = check_dup_types(NSources),
NSources; NSources;
do_pre_config_update({{?CMD_REPLACE, Type}, Source}, Sources) -> do_pre_config_update({{?CMD_REPLACE, Type}, Source}, Sources) ->
NSource = maybe_write_files(Source), NSource = maybe_write_source_files(Source),
{_Old, Front, Rear} = take(Type, Sources), {_Old, Front, Rear} = take(Type, Sources),
NSources = Front ++ [NSource | Rear], NSources = Front ++ [NSource | Rear],
ok = check_dup_types(NSources), ok = check_dup_types(NSources),
@ -212,7 +244,7 @@ do_pre_config_update({{?CMD_DELETE, Type}, _Source}, Sources) ->
NSources; NSources;
do_pre_config_update({?CMD_REPLACE, Sources}, _OldSources) -> do_pre_config_update({?CMD_REPLACE, Sources}, _OldSources) ->
%% overwrite the entire config! %% overwrite the entire config!
NSources = lists:map(fun maybe_write_files/1, Sources), NSources = lists:map(fun maybe_write_source_files/1, Sources),
ok = check_dup_types(NSources), ok = check_dup_types(NSources),
NSources; NSources;
do_pre_config_update({Op, Source}, Sources) -> do_pre_config_update({Op, Source}, Sources) ->
@ -356,6 +388,32 @@ init_metrics(Source) ->
%% AuthZ callbacks %% AuthZ callbacks
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
-spec authorize_deny(
emqx_types:clientinfo(),
emqx_types:pubsub(),
emqx_types:topic(),
default_result()
) ->
{stop, #{result => deny, from => ?MODULE}}.
authorize_deny(
#{
username := Username,
peerhost := IpAddress
} = _Client,
_PubSub,
Topic,
_DefaultResult
) ->
emqx_metrics:inc(?METRIC_DENY),
?SLOG(warning, #{
msg => "authorization_not_initialized",
username => Username,
ipaddr => IpAddress,
topic => Topic,
source => ?MODULE
}),
{stop, #{result => deny, from => ?MODULE}}.
%% @doc Check AuthZ %% @doc Check AuthZ
-spec authorize( -spec authorize(
emqx_types:clientinfo(), emqx_types:clientinfo(),
@ -502,29 +560,23 @@ changed_paths(OldSources, NewSources) ->
Changed = maps:get(changed, emqx_utils:diff_lists(NewSources, OldSources, fun type/1)), Changed = maps:get(changed, emqx_utils:diff_lists(NewSources, OldSources, fun type/1)),
[?CONF_KEY_PATH ++ [type(OldSource)] || {OldSource, _} <- Changed]. [?CONF_KEY_PATH ++ [type(OldSource)] || {OldSource, _} <- Changed].
maybe_read_acl_file(RawConf) -> maybe_read_files(RawConf) ->
maybe_convert_acl_file(RawConf, fun read_acl_file/1). maybe_convert_sources(RawConf, fun maybe_read_source_files/1).
maybe_write_acl_file(RawConf) -> maybe_write_files(RawConf) ->
maybe_convert_acl_file(RawConf, fun write_acl_file/1). maybe_convert_sources(RawConf, fun maybe_write_source_files/1).
maybe_convert_acl_file( maybe_convert_sources(
#{?CONF_NS_BINARY := #{<<"sources">> := Sources} = AuthRawConf} = RawConf, Fun #{?CONF_NS_BINARY := #{<<"sources">> := Sources} = AuthRawConf} = RawConf, Fun
) -> ) ->
Sources1 = lists:map( Sources1 = lists:map(Fun, Sources),
fun
(#{<<"type">> := <<"file">>} = FileSource) -> Fun(FileSource);
(Source) -> Source
end,
Sources
),
RawConf#{?CONF_NS_BINARY => AuthRawConf#{<<"sources">> => Sources1}}; RawConf#{?CONF_NS_BINARY => AuthRawConf#{<<"sources">> => Sources1}};
maybe_convert_acl_file(RawConf, _Fun) -> maybe_convert_sources(RawConf, _Fun) ->
RawConf. RawConf.
read_acl_file(#{<<"path">> := Path} = Source) -> % read_acl_file(#{<<"path">> := Path} = Source) ->
{ok, Rules} = emqx_authz_file:read_file(Path), % {ok, Rules} = emqx_authz_file:read_file(Path),
maps:remove(<<"path">>, Source#{<<"rules">> => Rules}). % maps:remove(<<"path">>, Source#{<<"rules">> => Rules}).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Extended Features %% Extended Features
@ -566,59 +618,79 @@ take(Type, Sources) ->
end. end.
find_action_in_hooks() -> find_action_in_hooks() ->
Callbacks = emqx_hooks:lookup('client.authorize'), Actions = lists:filtermap(
[Action] = [Action || {callback, {?MODULE, authorize, _} = Action, _, _} <- Callbacks], fun(Callback) ->
Action. case emqx_hooks:callback_action(Callback) of
{?MODULE, authorize, _} = Action -> {true, Action};
authz_module(built_in_database) -> _ -> false
emqx_authz_mnesia; end
authz_module(Type) -> end,
case emqx_authz_enterprise:is_enterprise_module(Type) of emqx_hooks:lookup('client.authorize')
{ok, Module} -> ),
Module; case Actions of
_ -> [] ->
list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)) ?SLOG(error, #{
msg => "authz_not_initialized",
configured_types => configured_types(),
registered_types => emqx_authz_source_registry:get()
}),
error(authz_not_initialized);
[Action] ->
Action
end. end.
type(#{type := Type}) -> type(Type); authz_module(Type) ->
type(#{<<"type">> := Type}) -> type(Type); emqx_authz_source_registry:module(Type).
type(file) -> file;
type(<<"file">>) -> file;
type(http) -> http;
type(<<"http">>) -> http;
type(mongodb) -> mongodb;
type(<<"mongodb">>) -> mongodb;
type(mysql) -> mysql;
type(<<"mysql">>) -> mysql;
type(redis) -> redis;
type(<<"redis">>) -> redis;
type(postgresql) -> postgresql;
type(<<"postgresql">>) -> postgresql;
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(MaybeEnterprise) -> emqx_authz_enterprise:type(MaybeEnterprise).
maybe_write_files(#{<<"type">> := <<"file">>} = Source) -> type(#{type := Type}) ->
write_acl_file(Source); type(Type);
maybe_write_files(NewSource) -> type(#{<<"type">> := Type}) ->
maybe_write_certs(NewSource). type(Type);
type(Type) when is_atom(Type) orelse is_binary(Type) ->
emqx_authz_source_registry:get(Type).
write_acl_file(#{<<"rules">> := Rules} = Source0) -> merge_defaults(Source) ->
AclPath = ?MODULE:acl_conf_file(), Type = type(Source),
%% Always check if the rules are valid before writing to the file Mod = authz_module(Type),
%% If the rules are invalid, the old file will be kept try
ok = check_acl_file_rules(AclPath, Rules), Mod:merge_defaults(Source)
ok = write_file(AclPath, Rules), catch
Source1 = maps:remove(<<"rules">>, Source0), error:undef ->
maps:put(<<"path">>, AclPath, Source1); Source
write_acl_file(Source) -> end.
Source.
%% @doc where the acl.conf file is stored. maybe_write_source_files(Source) ->
acl_conf_file() -> Module = authz_module(type(Source)),
filename:join([emqx:data_dir(), "authz", "acl.conf"]). case erlang:function_exported(Module, write_files, 1) of
true ->
Module:write_files(Source);
false ->
maybe_write_certs(Source)
end.
maybe_read_source_files(Source) ->
Module = authz_module(type(Source)),
case erlang:function_exported(Module, read_files, 1) of
true ->
Module:read_files(Source);
false ->
Source
end.
maybe_read_source_files_safe(Source0) ->
try maybe_read_source_files(Source0) of
Source1 ->
{ok, Source1}
catch
Error:Reason:Stacktrace ->
?SLOG(error, #{
msg => "error_in_maybe_read_source_files",
exception => Error,
reason => Reason,
stacktrace => Stacktrace
}),
{error, Reason}
end.
maybe_write_certs(#{<<"type">> := Type, <<"ssl">> := SSL = #{}} = Source) -> maybe_write_certs(#{<<"type">> := Type, <<"ssl">> := SSL = #{}} = Source) ->
case emqx_tls_lib:ensure_ssl_files(ssl_file_path(Type), SSL) of case emqx_tls_lib:ensure_ssl_files(ssl_file_path(Type), SSL) of
@ -631,16 +703,6 @@ maybe_write_certs(#{<<"type">> := Type, <<"ssl">> := SSL = #{}} = Source) ->
maybe_write_certs(#{} = Source) -> maybe_write_certs(#{} = Source) ->
Source. Source.
write_file(Filename, Bytes) ->
ok = filelib:ensure_dir(Filename),
case file:write_file(Filename, Bytes) of
ok ->
ok;
{error, Reason} ->
?SLOG(error, #{filename => Filename, msg => "write_file_error", reason => Reason}),
throw(Reason)
end.
ssl_file_path(Type) -> ssl_file_path(Type) ->
filename:join(["authz", Type]). filename:join(["authz", Type]).
@ -652,18 +714,6 @@ get_source_by_type(Type, Sources) ->
update_authz_chain(Actions) -> update_authz_chain(Actions) ->
emqx_hooks:put('client.authorize', {?MODULE, authorize, [Actions]}, ?HP_AUTHZ). emqx_hooks:put('client.authorize', {?MODULE, authorize, [Actions]}, ?HP_AUTHZ).
check_acl_file_rules(Path, Rules) ->
TmpPath = Path ++ ".tmp",
try
ok = write_file(TmpPath, Rules),
{ok, _} = emqx_authz_file:validate(TmpPath),
ok
catch
throw:Reason -> throw(Reason)
after
_ = file:delete(TmpPath)
end.
merge_sources(OriginConf, NewConf) -> merge_sources(OriginConf, NewConf) ->
{OriginSource, NewSources} = {OriginSource, NewSources} =
lists:foldl( lists:foldl(

View File

@ -20,8 +20,6 @@
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/1, ref/2]).
-export([ -export([
api_spec/0, api_spec/0,
paths/0, paths/0,

View File

@ -22,13 +22,11 @@
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/1, mk/2, ref/2, array/1, enum/1]). -import(hoconsc, [mk/1, mk/2, ref/2, enum/1]).
-define(BAD_REQUEST, 'BAD_REQUEST'). -define(BAD_REQUEST, 'BAD_REQUEST').
-define(NOT_FOUND, 'NOT_FOUND'). -define(NOT_FOUND, 'NOT_FOUND').
-define(API_SCHEMA_MODULE, emqx_authz_api_schema).
-export([ -export([
get_raw_sources/0, get_raw_sources/0,
get_raw_source/1, get_raw_source/1,
@ -65,7 +63,19 @@ paths() ->
]. ].
fields(sources) -> fields(sources) ->
[{sources, mk(array(hoconsc:union(authz_sources_type_refs())), #{desc => ?DESC(sources)})}]. emqx_authz_schema:api_authz_fields();
fields(position) ->
[
{position,
mk(
string(),
#{
desc => ?DESC(position),
required => true,
in => body
}
)}
].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Schema for each URI %% Schema for each URI
@ -87,7 +97,7 @@ schema("/authorization/sources") ->
description => ?DESC(authorization_sources_post), description => ?DESC(authorization_sources_post),
tags => ?TAGS, tags => ?TAGS,
'requestBody' => mk( 'requestBody' => mk(
hoconsc:union(authz_sources_type_refs()), emqx_authz_schema:api_source_type(),
#{desc => ?DESC(source_config)} #{desc => ?DESC(source_config)}
), ),
responses => responses =>
@ -111,7 +121,7 @@ schema("/authorization/sources/:type") ->
responses => responses =>
#{ #{
200 => mk( 200 => mk(
hoconsc:union(authz_sources_type_refs()), emqx_authz_schema:api_source_type(),
#{desc => ?DESC(source)} #{desc => ?DESC(source)}
), ),
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>) 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
@ -122,7 +132,7 @@ schema("/authorization/sources/:type") ->
description => ?DESC(authorization_sources_type_put), description => ?DESC(authorization_sources_type_put),
tags => ?TAGS, tags => ?TAGS,
parameters => parameters_field(), parameters => parameters_field(),
'requestBody' => mk(hoconsc:union(authz_sources_type_refs())), 'requestBody' => mk(emqx_authz_schema:api_source_type()),
responses => responses =>
#{ #{
204 => <<"Authorization source updated successfully">>, 204 => <<"Authorization source updated successfully">>,
@ -172,7 +182,7 @@ schema("/authorization/sources/:type/move") ->
parameters => parameters_field(), parameters => parameters_field(),
'requestBody' => 'requestBody' =>
emqx_dashboard_swagger:schema_with_examples( emqx_dashboard_swagger:schema_with_examples(
ref(?API_SCHEMA_MODULE, position), ref(?MODULE, position),
position_example() position_example()
), ),
responses => responses =>
@ -196,35 +206,14 @@ sources(Method, #{bindings := #{type := Type} = Bindings} = Req) when
sources(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}}); sources(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
sources(get, _) -> sources(get, _) ->
Sources = lists:foldl( Sources = lists:foldl(
fun fun(Source0, AccIn) ->
( try emqx_authz:maybe_read_source_files(Source0) of
#{ Source1 ->
<<"type">> := <<"file">>, lists:append(AccIn, [Source1])
<<"enable">> := Enable, catch
<<"path">> := Path _Error:_Reason ->
}, lists:append(AccIn, [Source0])
AccIn end
) ->
case emqx_authz_file:read_file(Path) of
{ok, Rules} ->
lists:append(AccIn, [
#{
type => file,
enable => Enable,
rules => Rules
}
]);
{error, _} ->
lists:append(AccIn, [
#{
type => file,
enable => Enable,
rules => <<"">>
}
])
end;
(Source, AccIn) ->
lists:append(AccIn, [Source])
end, end,
[], [],
get_raw_sources() get_raw_sources()
@ -240,23 +229,17 @@ source(Method, #{bindings := #{type := Type} = Bindings} = Req) when
source(get, #{bindings := #{type := Type}}) -> source(get, #{bindings := #{type := Type}}) ->
with_source( with_source(
Type, Type,
fun fun(Source0) ->
(#{<<"type">> := <<"file">>, <<"enable">> := Enable, <<"path">> := Path}) -> try emqx_authz:maybe_read_source_files(Source0) of
case emqx_authz_file:read_file(Path) of Source1 ->
{ok, Rules} -> {200, Source1}
{200, #{ catch
type => file, _Error:Reason ->
enable => Enable,
rules => Rules
}};
{error, Reason} ->
{500, #{ {500, #{
code => <<"INTERNAL_ERROR">>, code => <<"INTERNAL_ERROR">>,
message => bin(Reason) message => bin(Reason)
}} }}
end; end
(Source) ->
{200, Source}
end end
); );
source(put, #{bindings := #{type := Type}, body := #{<<"type">> := Type} = Body}) -> source(put, #{bindings := #{type := Type}, body := #{<<"type">> := Type} = Body}) ->
@ -474,29 +457,10 @@ get_raw_sources() ->
Schema = emqx_hocon:make_schema(emqx_authz_schema:authz_fields()), Schema = emqx_hocon:make_schema(emqx_authz_schema:authz_fields()),
Conf = #{<<"sources">> => RawSources}, Conf = #{<<"sources">> => RawSources},
#{<<"sources">> := Sources} = hocon_tconf:make_serializable(Schema, Conf, #{}), #{<<"sources">> := Sources} = hocon_tconf:make_serializable(Schema, Conf, #{}),
merge_default_headers(Sources). merge_defaults(Sources).
merge_default_headers(Sources) -> merge_defaults(Sources) ->
lists:map( lists:map(fun emqx_authz:merge_defaults/1, Sources).
fun(Source) ->
case maps:find(<<"headers">>, Source) of
{ok, Headers} ->
NewHeaders =
case Source of
#{<<"method">> := <<"get">>} ->
(emqx_authz_schema:headers_no_content_type(converter))(Headers);
#{<<"method">> := <<"post">>} ->
(emqx_authz_schema:headers(converter))(Headers);
_ ->
Headers
end,
Source#{<<"headers">> => NewHeaders};
error ->
Source
end
end,
Sources
).
get_raw_source(Type) -> get_raw_source(Type) ->
lists:filter( lists:filter(
@ -546,7 +510,7 @@ parameters_field() ->
[ [
{type, {type,
mk( mk(
enum(?API_SCHEMA_MODULE:authz_sources_types(simple)), enum(emqx_authz_schema:source_types()),
#{in => path, desc => ?DESC(source_type)} #{in => path, desc => ?DESC(source_type)}
)} )}
]. ].
@ -590,12 +554,6 @@ position_example() ->
} }
}. }.
authz_sources_type_refs() ->
[
ref(?API_SCHEMA_MODULE, Type)
|| Type <- emqx_authz_api_schema:authz_sources_types(detailed)
].
bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])). bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])).
status_metrics_example() -> status_metrics_example() ->

View File

@ -18,7 +18,7 @@
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-behaviour(emqx_authz). -behaviour(emqx_authz_source).
-ifdef(TEST). -ifdef(TEST).
-compile(export_all). -compile(export_all).

View File

@ -0,0 +1,24 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_authz_enterprise).
-include("emqx_authz_schema.hrl").
-export([
source_schema_mods/0
]).
-if(?EMQX_RELEASE_EDITION == ee).
source_schema_mods() ->
?EE_SOURCE_SCHEMA_MODS.
-else.
-dialyzer({nowarn_function, [source_schema_mods/0]}).
source_schema_mods() ->
[].
-endif.

View File

@ -0,0 +1,231 @@
%%--------------------------------------------------------------------
%% 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_authz_schema).
-include("emqx_authz.hrl").
-include("emqx_authz_schema.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-export([
roots/0,
fields/1,
desc/1
]).
-export([
authz_fields/0,
api_authz_fields/0,
api_source_type/0,
source_types/0
]).
-export([
injected_fields/0
]).
-export([
default_authz/0,
authz_common_fields/1
]).
%%--------------------------------------------------------------------
%% Authz Source Schema Behaviour
%%--------------------------------------------------------------------
-type schema_ref() :: ?R_REF(module(), hocon_schema:name()).
-callback type() -> emqx_authz_source:source_type().
-callback source_refs() -> [schema_ref()].
-callback select_union_member(emqx_config:raw_config()) -> schema_ref() | undefined | no_return().
-callback fields(hocon_schema:name()) -> [hocon_schema:field()].
-callback api_source_refs() -> [schema_ref()].
-optional_callbacks([
api_source_refs/0
]).
%%--------------------------------------------------------------------
%% Hocon Schema
%%--------------------------------------------------------------------
roots() -> [].
fields(?CONF_NS) ->
emqx_schema:authz_fields() ++ authz_fields();
fields("metrics_status_fields") ->
[
{"resource_metrics", ?HOCON(?R_REF("resource_metrics"), #{desc => ?DESC("metrics")})},
{"node_resource_metrics", array("node_resource_metrics", "node_metrics")},
{"metrics", ?HOCON(?R_REF("metrics"), #{desc => ?DESC("metrics")})},
{"node_metrics", array("node_metrics")},
{"status", ?HOCON(cluster_status(), #{desc => ?DESC("status")})},
{"node_status", array("node_status")},
{"node_error", array("node_error")}
];
fields("metrics") ->
[
{"total", ?HOCON(integer(), #{desc => ?DESC("metrics_total")})},
{"allow", ?HOCON(integer(), #{desc => ?DESC("allow")})},
{"deny", ?HOCON(integer(), #{desc => ?DESC("deny")})},
{"nomatch", ?HOCON(float(), #{desc => ?DESC("nomatch")})}
] ++ common_rate_field();
fields("node_metrics") ->
[
node_name(),
{"metrics", ?HOCON(?R_REF("metrics"), #{desc => ?DESC("metrics")})}
];
fields("resource_metrics") ->
common_field();
fields("node_resource_metrics") ->
[
node_name(),
{"metrics", ?HOCON(?R_REF("resource_metrics"), #{desc => ?DESC("metrics")})}
];
fields("node_status") ->
[
node_name(),
{"status", ?HOCON(status(), #{desc => ?DESC("node_status")})}
];
fields("node_error") ->
[
node_name(),
{"error", ?HOCON(string(), #{desc => ?DESC("node_error")})}
].
desc(?CONF_NS) ->
?DESC(?CONF_NS);
desc(_) ->
undefined.
%%--------------------------------------------------------------------
%% emqx_schema_hooks behaviour
%%--------------------------------------------------------------------
injected_fields() ->
#{
'roots.high' => [
{?CONF_NS, ?HOCON(?R_REF("authorization"), #{desc => ?DESC(?CONF_NS)})}
]
}.
authz_fields() ->
AuthzSchemaMods = source_schema_mods(),
AllTypes = lists:concat([Mod:source_refs() || Mod <- AuthzSchemaMods]),
UnionMemberSelector =
fun
(all_union_members) -> AllTypes;
%% must return list
({value, Value}) -> [select_union_member(Value, AuthzSchemaMods)]
end,
[
{sources,
?HOCON(
?ARRAY(?UNION(UnionMemberSelector)),
#{
default => [default_authz()],
desc => ?DESC(sources),
%% doc_lift is force a root level reference instead of nesting sub-structs
extra => #{doc_lift => true},
%% it is recommended to configure authz sources from dashboard
%% hence the importance level for config is low
importance => ?IMPORTANCE_LOW
}
)}
].
api_authz_fields() ->
[{sources, ?HOCON(?ARRAY(api_source_type()), #{desc => ?DESC(sources)})}].
api_source_type() ->
?UNION(api_authz_refs()).
api_authz_refs() ->
lists:concat([api_source_refs(Mod) || Mod <- source_schema_mods()]).
authz_common_fields(Type) ->
[
{type, ?HOCON(Type, #{required => true, desc => ?DESC(type)})},
{enable, ?HOCON(boolean(), #{default => true, desc => ?DESC(enable)})}
].
source_types() ->
[Mod:type() || Mod <- source_schema_mods()].
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
api_source_refs(Mod) ->
try
Mod:api_source_refs()
catch
error:undef ->
Mod:source_refs()
end.
source_schema_mods() ->
?SOURCE_SCHEMA_MODS ++ emqx_authz_enterprise:source_schema_mods().
common_rate_field() ->
[
{"rate", ?HOCON(float(), #{desc => ?DESC("rate")})},
{"rate_max", ?HOCON(float(), #{desc => ?DESC("rate_max")})},
{"rate_last5m", ?HOCON(float(), #{desc => ?DESC("rate_last5m")})}
].
array(Ref) -> array(Ref, Ref).
array(Ref, DescId) ->
?HOCON(?ARRAY(?R_REF(Ref)), #{desc => ?DESC(DescId)}).
select_union_member(#{<<"type">> := Type}, []) ->
throw(#{
reason => "unknown_authz_type",
got => Type
});
select_union_member(#{<<"type">> := _} = Value, [Mod | Mods]) ->
case Mod:select_union_member(Value) of
undefined ->
select_union_member(Value, Mods);
Member ->
Member
end;
select_union_member(_Value, _Mods) ->
throw("missing_type_field").
default_authz() ->
#{
<<"type">> => <<"file">>,
<<"enable">> => true,
<<"path">> => <<"${EMQX_ETC_DIR}/acl.conf">>
}.
common_field() ->
[
{"matched", ?HOCON(integer(), #{desc => ?DESC("matched")})},
{"success", ?HOCON(integer(), #{desc => ?DESC("success")})},
{"failed", ?HOCON(integer(), #{desc => ?DESC("failed")})}
] ++ common_rate_field().
status() ->
hoconsc:enum([connected, disconnected, connecting]).
cluster_status() ->
hoconsc:enum([connected, disconnected, connecting, inconsistent]).
node_name() ->
{"node", ?HOCON(binary(), #{desc => ?DESC("node"), example => "emqx@127.0.0.1"})}.

View File

@ -0,0 +1,69 @@
%%--------------------------------------------------------------------
%% 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_authz_source).
-type source_type() :: atom().
-type source() :: #{type => source_type(), _ => _}.
-type raw_source() :: map().
-type match_result() :: {matched, allow} | {matched, deny} | nomatch.
-export_type([
source_type/0,
source/0,
match_result/0
]).
%% Initialize authz backend.
%% Populate the passed configuration map with necessary data,
%% like `ResourceID`s
-callback create(source()) -> source().
%% Update authz backend.
%% Change configuration, or simply enable/disable
-callback update(source()) -> source().
%% Destroy authz backend.
%% Make cleanup of all allocated data.
%% An authz backend will not be used after `destroy`.
-callback destroy(source()) -> ok.
%% Get authz text description.
-callback description() -> string().
%% Authorize client action.
-callback authorize(
emqx_types:clientinfo(),
emqx_types:pubsub(),
emqx_types:topic(),
source()
) -> match_result().
%% Convert filepath values to the content of the files.
-callback write_files(raw_source()) -> raw_source() | no_return().
%% Convert filepath values to the content of the files.
-callback read_files(raw_source()) -> raw_source() | no_return().
%% Merge default values to the source, for example, for exposing via API
-callback merge_defaults(raw_source()) -> raw_source().
-optional_callbacks([
update/1,
write_files/1,
read_files/1,
merge_defaults/1
]).

View File

@ -0,0 +1,78 @@
%%--------------------------------------------------------------------
%% 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_authz_source_registry).
-export([
create/0,
register/2,
unregister/1,
get/0,
get/1,
module/1
]).
-define(TAB, ?MODULE).
-type fuzzy_type() :: emqx_authz_source:source_type() | binary().
-spec create() -> ok.
create() ->
_ = ets:new(?TAB, [named_table, public, set]),
ok.
-spec register(emqx_authz_source:source_type(), module()) -> ok | {error, term()}.
register(Type, Module) when is_atom(Type) andalso is_atom(Module) ->
case ets:insert_new(?TAB, {Type, Type, Module}) of
true ->
_ = ets:insert(?TAB, {atom_to_binary(Type), Type, Module}),
ok;
false ->
{error, {already_registered, Type}}
end.
-spec unregister(emqx_authz_source:source_type()) -> ok.
unregister(Type) when is_atom(Type) ->
_ = ets:delete(?TAB, Type),
_ = ets:delete(?TAB, atom_to_binary(Type)),
ok.
-spec get(fuzzy_type()) ->
emqx_authz_source:source_type() | no_return().
get(FuzzyType) when is_atom(FuzzyType) orelse is_binary(FuzzyType) ->
case ets:lookup(?TAB, FuzzyType) of
[] ->
throw({unknown_authz_source_type, FuzzyType});
[{FuzzyType, Type, _Module}] ->
Type
end.
-spec get() -> [emqx_authz_source:source_type()].
get() ->
Types = lists:map(
fun({_, Type, _}) -> Type end,
ets:tab2list(?TAB)
),
lists:usort(Types).
-spec module(fuzzy_type()) -> module() | no_return().
module(FuzzyType) when is_atom(FuzzyType) orelse is_binary(FuzzyType) ->
case ets:lookup(?TAB, FuzzyType) of
[] ->
throw({unknown_authz_source_type, FuzzyType});
[{FuzzyType, _Type, Module}] ->
Module
end.

View File

@ -24,6 +24,7 @@
create_resource/2, create_resource/2,
create_resource/3, create_resource/3,
update_resource/2, update_resource/2,
remove_resource/1,
update_config/2, update_config/2,
parse_deep/2, parse_deep/2,
parse_str/2, parse_str/2,
@ -59,7 +60,7 @@ create_resource(Module, Config) ->
create_resource(ResourceId, Module, Config) -> create_resource(ResourceId, Module, Config) ->
Result = emqx_resource:create_local( Result = emqx_resource:create_local(
ResourceId, ResourceId,
?RESOURCE_GROUP, ?AUTHZ_RESOURCE_GROUP,
Module, Module,
Config, Config,
?DEFAULT_RESOURCE_OPTS ?DEFAULT_RESOURCE_OPTS
@ -81,6 +82,9 @@ update_resource(Module, #{annotations := #{id := ResourceId}} = Config) ->
end, end,
start_resource_if_enabled(Result, ResourceId, Config). start_resource_if_enabled(Result, ResourceId, Config).
remove_resource(ResourceId) ->
emqx_resource:remove_local(ResourceId).
start_resource_if_enabled({ok, _} = Result, ResourceId, #{enable := true}) -> start_resource_if_enabled({ok, _} = Result, ResourceId, #{enable := true}) ->
_ = emqx_resource:start(ResourceId), _ = emqx_resource:start(ResourceId),
Result; Result;
@ -90,7 +94,7 @@ start_resource_if_enabled(Result, _ResourceId, _Config) ->
cleanup_resources() -> cleanup_resources() ->
lists:foreach( lists:foreach(
fun emqx_resource:remove_local/1, fun emqx_resource:remove_local/1,
emqx_resource:list_group_instances(?RESOURCE_GROUP) emqx_resource:list_group_instances(?AUTHZ_RESOURCE_GROUP)
). ).
make_resource_id(Name) -> make_resource_id(Name) ->

View File

@ -53,9 +53,36 @@ end_per_testcase(Case, Config) ->
%% Testcases %% Testcases
%%================================================================================= %%=================================================================================
t_fill_defaults({init, Config}) ->
Config;
t_fill_defaults({'end', _Config}) ->
ok;
t_fill_defaults(Config) when is_list(Config) ->
Conf0 = #{
<<"mechanism">> => <<"password_based">>,
<<"backend">> => <<"mysql">>,
<<"query">> => <<"SELECT 1">>,
<<"server">> => <<"mysql:3306">>,
<<"database">> => <<"mqtt">>
},
%% Missing defaults are filled
?assertMatch(
#{<<"query_timeout">> := _},
emqx_authn:fill_defaults(Conf0)
),
Conf1 = Conf0#{<<"mechanism">> => <<"unknown-xx">>},
%% fill_defaults (check_config formerly) is actually never called on unvalidated config
%% so it will not meet validation errors
%% However, we still test it here
?assertThrow(
#{reason := "unknown_mechanism"}, emqx_authn:fill_defaults(Conf1)
).
t_will_message_connection_denied({init, Config}) -> t_will_message_connection_denied({init, Config}) ->
emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]), emqx_common_test_helpers:start_apps([emqx_conf, emqx_auth, emqx_auth_file]),
mria:clear_table(emqx_authn_mnesia), emqx_authn_test_lib:register_fake_providers([{password_based, built_in_database}]),
AuthnConfig = #{ AuthnConfig = #{
<<"mechanism">> => <<"password_based">>, <<"mechanism">> => <<"password_based">>,
<<"backend">> => <<"built_in_database">>, <<"backend">> => <<"built_in_database">>,
@ -68,7 +95,7 @@ t_will_message_connection_denied({init, Config}) ->
), ),
User = #{user_id => <<"subscriber">>, password => <<"p">>}, User = #{user_id => <<"subscriber">>, password => <<"p">>},
AuthenticatorID = <<"password_based:built_in_database">>, AuthenticatorID = <<"password_based:built_in_database">>,
{ok, _} = emqx_authentication:add_user( {ok, _} = emqx_authn_chains:add_user(
Chain, Chain,
AuthenticatorID, AuthenticatorID,
User User
@ -79,10 +106,11 @@ t_will_message_connection_denied({'end', _Config}) ->
[authentication], [authentication],
{delete_authenticator, 'mqtt:global', <<"password_based:built_in_database">>} {delete_authenticator, 'mqtt:global', <<"password_based:built_in_database">>}
), ),
emqx_common_test_helpers:stop_apps([emqx_authn, emqx_conf]), emqx_common_test_helpers:stop_apps([emqx_auth_file, emqx_auth, emqx_conf]),
mria:clear_table(emqx_authn_mnesia),
ok; ok;
t_will_message_connection_denied(Config) when is_list(Config) -> t_will_message_connection_denied(Config) when is_list(Config) ->
process_flag(trap_exit, true),
{ok, Subscriber} = emqtt:start_link([ {ok, Subscriber} = emqtt:start_link([
{clientid, <<"subscriber">>}, {clientid, <<"subscriber">>},
{password, <<"p">>} {password, <<"p">>}
@ -90,8 +118,6 @@ t_will_message_connection_denied(Config) when is_list(Config) ->
{ok, _} = emqtt:connect(Subscriber), {ok, _} = emqtt:connect(Subscriber),
{ok, _, [?RC_SUCCESS]} = emqtt:subscribe(Subscriber, <<"lwt">>), {ok, _, [?RC_SUCCESS]} = emqtt:subscribe(Subscriber, <<"lwt">>),
process_flag(trap_exit, true),
{ok, Publisher} = emqtt:start_link([ {ok, Publisher} = emqtt:start_link([
{clientid, <<"publisher">>}, {clientid, <<"publisher">>},
{will_topic, <<"lwt">>}, {will_topic, <<"lwt">>},
@ -120,7 +146,10 @@ t_will_message_connection_denied(Config) when is_list(Config) ->
%% With auth enabled, send CONNECT without password field, %% With auth enabled, send CONNECT without password field,
%% expect CONNACK with reason_code=5 and socket close %% expect CONNACK with reason_code=5 and socket close
t_password_undefined({init, Config}) -> t_password_undefined({init, Config}) ->
emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]), emqx_common_test_helpers:start_apps([emqx_conf, emqx_auth]),
emqx_authn_test_lib:register_fake_providers([
{password_based, built_in_database}
]),
AuthnConfig = #{ AuthnConfig = #{
<<"mechanism">> => <<"password_based">>, <<"mechanism">> => <<"password_based">>,
<<"backend">> => <<"built_in_database">>, <<"backend">> => <<"built_in_database">>,
@ -137,7 +166,7 @@ t_password_undefined({'end', _Config}) ->
[authentication], [authentication],
{delete_authenticator, 'mqtt:global', <<"password_based:built_in_database">>} {delete_authenticator, 'mqtt:global', <<"password_based:built_in_database">>}
), ),
emqx_common_test_helpers:stop_apps([emqx_authn, emqx_conf]), emqx_common_test_helpers:stop_apps([emqx_auth, emqx_conf]),
ok; ok;
t_password_undefined(Config) when is_list(Config) -> t_password_undefined(Config) when is_list(Config) ->
Payload = <<16, 19, 0, 4, 77, 81, 84, 84, 4, 130, 0, 60, 0, 2, 97, 49, 0, 3, 97, 97, 97>>, Payload = <<16, 19, 0, 4, 77, 81, 84, 84, 4, 130, 0, 60, 0, 2, 97, 49, 0, 3, 97, 97, 97>>,
@ -168,45 +197,18 @@ t_password_undefined(Config) when is_list(Config) ->
end, end,
ok. ok.
t_union_selector_errors({init, Config}) ->
Config;
t_union_selector_errors({'end', _Config}) ->
ok;
t_union_selector_errors(Config) when is_list(Config) ->
Conf0 = #{
<<"mechanism">> => <<"password_based">>,
<<"backend">> => <<"mysql">>
},
Conf1 = Conf0#{<<"mechanism">> => <<"unknown-atom-xx">>},
?assertThrow(#{error := unknown_mechanism}, emqx_authn:check_config(Conf1)),
Conf2 = Conf0#{<<"backend">> => <<"unknown-atom-xx">>},
?assertThrow(#{error := unknown_backend}, emqx_authn:check_config(Conf2)),
Conf3 = Conf0#{<<"backend">> => <<"unknown">>, <<"mechanism">> => <<"unknown">>},
?assertThrow(
#{
error := unknown_authn_provider,
backend := unknown,
mechanism := unknown
},
emqx_authn:check_config(Conf3)
),
Res = catch (emqx_authn:check_config(#{<<"mechanism">> => <<"unknown">>})),
?assertEqual(
#{
error => unknown_authn_provider,
mechanism => unknown
},
Res
),
ok.
t_update_conf({init, Config}) -> t_update_conf({init, Config}) ->
emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]), emqx_common_test_helpers:start_apps([emqx_conf, emqx_auth]),
emqx_authn_test_lib:register_fake_providers([
{password_based, built_in_database},
{password_based, http},
jwt
]),
{ok, _} = emqx:update_config([authentication], []), {ok, _} = emqx:update_config([authentication], []),
Config; Config;
t_update_conf({'end', _Config}) -> t_update_conf({'end', _Config}) ->
{ok, _} = emqx:update_config([authentication], []), {ok, _} = emqx:update_config([authentication], []),
emqx_common_test_helpers:stop_apps([emqx_authn, emqx_conf]), emqx_common_test_helpers:stop_apps([emqx_auth, emqx_conf]),
ok; ok;
t_update_conf(Config) when is_list(Config) -> t_update_conf(Config) when is_list(Config) ->
Authn1 = #{ Authn1 = #{
@ -242,11 +244,11 @@ t_update_conf(Config) when is_list(Config) ->
#{ #{
enable := true, enable := true,
id := <<"password_based:built_in_database">>, id := <<"password_based:built_in_database">>,
provider := emqx_authn_mnesia provider := emqx_authn_fake_provider
} }
] ]
}}, }},
emqx_authentication:lookup_chain(Chain) emqx_authn_chains:lookup_chain(Chain)
), ),
{ok, _} = emqx:update_config([authentication], [Authn1, Authn2, Authn3]), {ok, _} = emqx:update_config([authentication], [Authn1, Authn2, Authn3]),
@ -256,21 +258,21 @@ t_update_conf(Config) when is_list(Config) ->
#{ #{
enable := true, enable := true,
id := <<"password_based:built_in_database">>, id := <<"password_based:built_in_database">>,
provider := emqx_authn_mnesia provider := emqx_authn_fake_provider
}, },
#{ #{
enable := true, enable := true,
id := <<"password_based:http">>, id := <<"password_based:http">>,
provider := emqx_authn_http provider := emqx_authn_fake_provider
}, },
#{ #{
enable := true, enable := true,
id := <<"jwt">>, id := <<"jwt">>,
provider := emqx_authn_jwt provider := emqx_authn_fake_provider
} }
] ]
}}, }},
emqx_authentication:lookup_chain(Chain) emqx_authn_chains:lookup_chain(Chain)
), ),
{ok, _} = emqx:update_config([authentication], [Authn2, Authn1]), {ok, _} = emqx:update_config([authentication], [Authn2, Authn1]),
?assertMatch( ?assertMatch(
@ -279,16 +281,16 @@ t_update_conf(Config) when is_list(Config) ->
#{ #{
enable := true, enable := true,
id := <<"password_based:http">>, id := <<"password_based:http">>,
provider := emqx_authn_http provider := emqx_authn_fake_provider
}, },
#{ #{
enable := true, enable := true,
id := <<"password_based:built_in_database">>, id := <<"password_based:built_in_database">>,
provider := emqx_authn_mnesia provider := emqx_authn_fake_provider
} }
] ]
}}, }},
emqx_authentication:lookup_chain(Chain) emqx_authn_chains:lookup_chain(Chain)
), ),
{ok, _} = emqx:update_config([authentication], [Authn3, Authn2, Authn1]), {ok, _} = emqx:update_config([authentication], [Authn3, Authn2, Authn1]),
@ -298,26 +300,26 @@ t_update_conf(Config) when is_list(Config) ->
#{ #{
enable := true, enable := true,
id := <<"jwt">>, id := <<"jwt">>,
provider := emqx_authn_jwt provider := emqx_authn_fake_provider
}, },
#{ #{
enable := true, enable := true,
id := <<"password_based:http">>, id := <<"password_based:http">>,
provider := emqx_authn_http provider := emqx_authn_fake_provider
}, },
#{ #{
enable := true, enable := true,
id := <<"password_based:built_in_database">>, id := <<"password_based:built_in_database">>,
provider := emqx_authn_mnesia provider := emqx_authn_fake_provider
} }
] ]
}}, }},
emqx_authentication:lookup_chain(Chain) emqx_authn_chains:lookup_chain(Chain)
), ),
{ok, _} = emqx:update_config([authentication], []), {ok, _} = emqx:update_config([authentication], []),
?assertMatch( ?assertMatch(
{error, {not_found, {chain, Chain}}}, {error, {not_found, {chain, Chain}}},
emqx_authentication:lookup_chain(Chain) emqx_authn_chains:lookup_chain(Chain)
), ),
ok. ok.

View File

@ -18,7 +18,6 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-compile(export_all). -compile(export_all).
-import(emqx_dashboard_api_test_helpers, [multipart_formdata_request/3]).
-import(emqx_mgmt_api_test_util, [request/3, uri/1]). -import(emqx_mgmt_api_test_util, [request/3, uri/1]).
-include("emqx_authn.hrl"). -include("emqx_authn.hrl").
@ -53,8 +52,6 @@ init_per_testcase(_Case, Config) ->
[listeners, tcp, default, ?CONF_NS_ATOM], [listeners, tcp, default, ?CONF_NS_ATOM],
?TCP_DEFAULT ?TCP_DEFAULT
), ),
{atomic, ok} = mria:clear_table(emqx_authn_mnesia),
Config. Config.
end_per_testcase(t_authenticator_fail, Config) -> end_per_testcase(t_authenticator_fail, Config) ->
@ -68,7 +65,7 @@ init_per_suite(Config) ->
[ [
emqx, emqx,
emqx_conf, emqx_conf,
emqx_authn, emqx_auth,
emqx_management, emqx_management,
{emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"} {emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"}
], ],
@ -77,6 +74,12 @@ init_per_suite(Config) ->
} }
), ),
_ = emqx_common_test_http:create_default_app(), _ = emqx_common_test_http:create_default_app(),
ok = emqx_authn_test_lib:register_fake_providers([
{password_based, built_in_database},
{password_based, redis},
{password_based, http},
jwt
]),
?AUTHN:delete_chain(?GLOBAL), ?AUTHN:delete_chain(?GLOBAL),
{ok, Chains} = ?AUTHN:list_chains(), {ok, Chains} = ?AUTHN:list_chains(),
?assertEqual(length(Chains), 0), ?assertEqual(length(Chains), 0),
@ -116,36 +119,18 @@ t_authenticator_fail(_) ->
) )
). ).
t_authenticator_users(_) ->
test_authenticator_users([]).
t_authenticator_user(_) ->
test_authenticator_user([]).
t_authenticator_position(_) -> t_authenticator_position(_) ->
test_authenticator_position([]). test_authenticator_position([]).
t_authenticator_import_users(_) ->
test_authenticator_import_users([]).
%t_listener_authenticators(_) -> %t_listener_authenticators(_) ->
% test_authenticators(["listeners", ?TCP_DEFAULT]). % test_authenticators(["listeners", ?TCP_DEFAULT]).
%t_listener_authenticator(_) -> %t_listener_authenticator(_) ->
% test_authenticator(["listeners", ?TCP_DEFAULT]). % test_authenticator(["listeners", ?TCP_DEFAULT]).
%t_listener_authenticator_users(_) ->
% test_authenticator_users(["listeners", ?TCP_DEFAULT]).
%t_listener_authenticator_user(_) ->
% test_authenticator_user(["listeners", ?TCP_DEFAULT]).
%t_listener_authenticator_position(_) -> %t_listener_authenticator_position(_) ->
% test_authenticator_position(["listeners", ?TCP_DEFAULT]). % test_authenticator_position(["listeners", ?TCP_DEFAULT]).
%t_listener_authenticator_import_users(_) ->
% test_authenticator_import_users(["listeners", ?TCP_DEFAULT]).
t_aggregate_metrics(_) -> t_aggregate_metrics(_) ->
Metrics = #{ Metrics = #{
'emqx@node1.emqx.io' => #{ 'emqx@node1.emqx.io' => #{
@ -329,218 +314,6 @@ test_authenticator(PathPrefix) ->
?assertAuthenticatorsMatch([], PathPrefix ++ [?CONF_NS]). ?assertAuthenticatorsMatch([], PathPrefix ++ [?CONF_NS]).
test_authenticator_users(PathPrefix) ->
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
{ok, 200, _} = request(
post,
uri(PathPrefix ++ [?CONF_NS]),
emqx_authn_test_lib:built_in_database_example()
),
{ok, Client} = emqtt:start_link(
[
{username, <<"u_event">>},
{clientid, <<"c_event">>},
{proto_ver, v5},
{properties, #{'Session-Expiry-Interval' => 60}}
]
),
process_flag(trap_exit, true),
?assertMatch({error, _}, emqtt:connect(Client)),
timer:sleep(300),
UsersUri0 = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "status"]),
{ok, 200, PageData0} = request(get, UsersUri0),
case PathPrefix of
[] ->
#{
<<"metrics">> := #{
<<"total">> := 1,
<<"success">> := 0,
<<"failed">> := 1
}
} = emqx_utils_json:decode(PageData0, [return_maps]);
["listeners", 'tcp:default'] ->
#{
<<"metrics">> := #{
<<"total">> := 1,
<<"success">> := 0,
<<"nomatch">> := 1
}
} = emqx_utils_json:decode(PageData0, [return_maps])
end,
InvalidUsers = [
#{clientid => <<"u1">>, password => <<"p1">>},
#{user_id => <<"u2">>},
#{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}
],
lists:foreach(
fun(User) -> {ok, 400, _} = request(post, UsersUri, User) end,
InvalidUsers
),
ValidUsers = [
#{user_id => <<"u1">>, password => <<"p1">>},
#{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true},
#{user_id => <<"u3">>, password => <<"p3">>}
],
lists:foreach(
fun(User) ->
{ok, 201, UserData} = request(post, UsersUri, User),
CreatedUser = emqx_utils_json:decode(UserData, [return_maps]),
?assertMatch(#{<<"user_id">> := _}, CreatedUser)
end,
ValidUsers
),
{ok, Client1} = emqtt:start_link(
[
{username, <<"u1">>},
{password, <<"p1">>},
{clientid, <<"c_event">>},
{proto_ver, v5},
{properties, #{'Session-Expiry-Interval' => 60}}
]
),
{ok, _} = emqtt:connect(Client1),
timer:sleep(300),
UsersUri01 = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "status"]),
{ok, 200, PageData01} = request(get, UsersUri01),
case PathPrefix of
[] ->
#{
<<"metrics">> := #{
<<"total">> := 2,
<<"success">> := 1,
<<"failed">> := 1
}
} = emqx_utils_json:decode(PageData01, [return_maps]);
["listeners", 'tcp:default'] ->
#{
<<"metrics">> := #{
<<"total">> := 2,
<<"success">> := 1,
<<"nomatch">> := 1
}
} = emqx_utils_json:decode(PageData01, [return_maps])
end,
{ok, 200, Page1Data} = request(get, UsersUri ++ "?page=1&limit=2"),
#{
<<"data">> := Page1Users,
<<"meta">> :=
#{
<<"page">> := 1,
<<"limit">> := 2,
<<"count">> := 3
}
} =
emqx_utils_json:decode(Page1Data, [return_maps]),
{ok, 200, Page2Data} = request(get, UsersUri ++ "?page=2&limit=2"),
#{
<<"data">> := Page2Users,
<<"meta">> :=
#{
<<"page">> := 2,
<<"limit">> := 2,
<<"count">> := 3
}
} = emqx_utils_json:decode(Page2Data, [return_maps]),
?assertEqual(2, length(Page1Users)),
?assertEqual(1, length(Page2Users)),
?assertEqual(
[<<"u1">>, <<"u2">>, <<"u3">>],
lists:usort([UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])
),
{ok, 200, Super1Data} = request(get, UsersUri ++ "?page=1&limit=3&is_superuser=true"),
#{
<<"data">> := Super1Users,
<<"meta">> :=
#{
<<"page">> := 1,
<<"limit">> := 3,
<<"count">> := 1
}
} = emqx_utils_json:decode(Super1Data, [return_maps]),
?assertEqual(
[<<"u2">>],
lists:usort([UserId || #{<<"user_id">> := UserId} <- Super1Users])
),
{ok, 200, Super2Data} = request(get, UsersUri ++ "?page=1&limit=3&is_superuser=false"),
#{
<<"data">> := Super2Users,
<<"meta">> :=
#{
<<"page">> := 1,
<<"limit">> := 3,
<<"count">> := 2
}
} = emqx_utils_json:decode(Super2Data, [return_maps]),
?assertEqual(
[<<"u1">>, <<"u3">>],
lists:usort([UserId || #{<<"user_id">> := UserId} <- Super2Users])
),
ok.
test_authenticator_user(PathPrefix) ->
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
{ok, 200, _} = request(
post,
uri(PathPrefix ++ [?CONF_NS]),
emqx_authn_test_lib:built_in_database_example()
),
User = #{user_id => <<"u1">>, password => <<"p1">>},
{ok, 201, _} = request(post, UsersUri, User),
{ok, 404, _} = request(get, UsersUri ++ "/u123"),
{ok, 409, _} = request(post, UsersUri, User),
{ok, 200, UserData} = request(get, UsersUri ++ "/u1"),
FetchedUser = emqx_utils_json:decode(UserData, [return_maps]),
?assertMatch(#{<<"user_id">> := <<"u1">>}, FetchedUser),
?assertNotMatch(#{<<"password">> := _}, FetchedUser),
ValidUserUpdates = [
#{password => <<"p1">>},
#{password => <<"p1">>, is_superuser => true}
],
lists:foreach(
fun(UserUpdate) -> {ok, 200, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
ValidUserUpdates
),
InvalidUserUpdates = [#{user_id => <<"u1">>, password => <<"p1">>}],
lists:foreach(
fun(UserUpdate) -> {ok, 400, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
InvalidUserUpdates
),
{ok, 404, _} = request(delete, UsersUri ++ "/u123"),
{ok, 204, _} = request(delete, UsersUri ++ "/u1").
test_authenticator_position(PathPrefix) -> test_authenticator_position(PathPrefix) ->
AuthenticatorConfs = [ AuthenticatorConfs = [
emqx_authn_test_lib:http_example(), emqx_authn_test_lib:http_example(),
@ -660,37 +433,6 @@ test_authenticator_position(PathPrefix) ->
PathPrefix ++ [?CONF_NS] PathPrefix ++ [?CONF_NS]
). ).
test_authenticator_import_users(PathPrefix) ->
ImportUri = uri(
PathPrefix ++
[?CONF_NS, "password_based:built_in_database", "import_users"]
),
{ok, 200, _} = request(
post,
uri(PathPrefix ++ [?CONF_NS]),
emqx_authn_test_lib:built_in_database_example()
),
{ok, 400, _} = multipart_formdata_request(ImportUri, [], []),
{ok, 400, _} = multipart_formdata_request(ImportUri, [], [
{filenam, "user-credentials.json", <<>>}
]),
Dir = code:lib_dir(emqx_authn, test),
JSONFileName = filename:join([Dir, <<"data/user-credentials.json">>]),
CSVFileName = filename:join([Dir, <<"data/user-credentials.csv">>]),
{ok, JSONData} = file:read_file(JSONFileName),
{ok, 204, _} = multipart_formdata_request(ImportUri, [], [
{filename, "user-credentials.json", JSONData}
]),
{ok, CSVData} = file:read_file(CSVFileName),
{ok, 204, _} = multipart_formdata_request(ImportUri, [], [
{filename, "user-credentials.csv", CSVData}
]).
%% listener authn api is not supported since 5.1.0 %% listener authn api is not supported since 5.1.0
%% Don't support listener switch to global chain. %% Don't support listener switch to global chain.
ignore_switch_to_global_chain(_) -> ignore_switch_to_global_chain(_) ->

View File

@ -14,10 +14,10 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_authentication_SUITE). -module(emqx_authn_chains_SUITE).
-behaviour(hocon_schema). -behaviour(hocon_schema).
-behaviour(emqx_authentication). -behaviour(emqx_authn_provider).
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
@ -26,9 +26,9 @@
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include("emqx_authentication.hrl"). -include("emqx_authn_chains.hrl").
-define(AUTHN, emqx_authentication). -define(AUTHN, emqx_authn_chains).
-define(config(KEY), -define(config(KEY),
(fun() -> (fun() ->
{KEY, _V_} = lists:keyfind(KEY, 1, Config), {KEY, _V_} = lists:keyfind(KEY, 1, Config),
@ -98,7 +98,7 @@ init_per_suite(Config) ->
[ [
emqx, emqx,
emqx_conf, emqx_conf,
emqx_authn emqx_auth
], ],
#{work_dir => ?config(priv_dir)} #{work_dir => ?config(priv_dir)}
), ),
@ -295,9 +295,9 @@ t_update_config({init, Config}) ->
| Config | Config
]; ];
t_update_config(Config) when is_list(Config) -> t_update_config(Config) when is_list(Config) ->
emqx_config_handler:add_handler([?CONF_ROOT], emqx_authentication_config), emqx_config_handler:add_handler([?CONF_ROOT], emqx_authn_config),
ok = emqx_config_handler:add_handler( ok = emqx_config_handler:add_handler(
[listeners, '?', '?', ?CONF_ROOT], emqx_authentication_config [listeners, '?', '?', ?CONF_ROOT], emqx_authn_config
), ),
ok = register_provider(?config("auth1"), ?MODULE), ok = register_provider(?config("auth1"), ?MODULE),
ok = register_provider(?config("auth2"), ?MODULE), ok = register_provider(?config("auth2"), ?MODULE),
@ -469,8 +469,8 @@ t_restart(Config) when is_list(Config) ->
?assertEqual({ok, [test_chain]}, ?AUTHN:list_chain_names()), ?assertEqual({ok, [test_chain]}, ?AUTHN:list_chain_names()),
ok = supervisor:terminate_child(emqx_authentication_sup, ?AUTHN), ok = supervisor:terminate_child(emqx_authn_sup, ?AUTHN),
{ok, _} = supervisor:restart_child(emqx_authentication_sup, ?AUTHN), {ok, _} = supervisor:restart_child(emqx_authn_sup, ?AUTHN),
?assertEqual({ok, [test_chain]}, ?AUTHN:list_chain_names()); ?assertEqual({ok, [test_chain]}, ?AUTHN:list_chain_names());
t_restart({'end', _Config}) -> t_restart({'end', _Config}) ->
@ -493,7 +493,7 @@ t_combine_authn_and_callback(Config) when is_list(Config) ->
password => <<"any">> password => <<"any">>
}, },
%% no emqx_authentication authenticators, anonymous is allowed %% no emqx_authn_chains authenticators, anonymous is allowed
?assertAuthSuccessForUser(bad), ?assertAuthSuccessForUser(bad),
AuthNType = ?config(authn_type), AuthNType = ?config(authn_type),
@ -506,7 +506,7 @@ t_combine_authn_and_callback(Config) when is_list(Config) ->
}, },
{ok, _} = ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig), {ok, _} = ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig),
%% emqx_authentication alone %% emqx_authn_chains alone
?assertAuthSuccessForUser(good), ?assertAuthSuccessForUser(good),
?assertAuthFailureForUser(ignore), ?assertAuthFailureForUser(ignore),
?assertAuthFailureForUser(bad), ?assertAuthFailureForUser(bad),
@ -520,12 +520,12 @@ t_combine_authn_and_callback(Config) when is_list(Config) ->
?assertAuthFailureForUser(bad), ?assertAuthFailureForUser(bad),
%% higher-priority hook can permit access with {ok,...}, %% higher-priority hook can permit access with {ok,...},
%% then emqx_authentication overrides the result %% then emqx_authn_chains overrides the result
?assertAuthFailureForUser(hook_user_good), ?assertAuthFailureForUser(hook_user_good),
?assertAuthFailureForUser(hook_user_bad), ?assertAuthFailureForUser(hook_user_bad),
%% higher-priority hook can permit and return {stop,...}, %% higher-priority hook can permit and return {stop,...},
%% then emqx_authentication cannot override the result %% then emqx_authn_chains cannot override the result
?assertAuthSuccessForUser(hook_user_finally_good), ?assertAuthSuccessForUser(hook_user_finally_good),
?assertAuthFailureForUser(hook_user_finally_bad), ?assertAuthFailureForUser(hook_user_finally_bad),
@ -539,13 +539,13 @@ t_combine_authn_and_callback(Config) when is_list(Config) ->
?assertAuthFailureForUser(bad), ?assertAuthFailureForUser(bad),
?assertAuthFailureForUser(ignore), ?assertAuthFailureForUser(ignore),
%% lower-priority hook can overrride emqx_authentication result %% lower-priority hook can overrride emqx_authn_chains result
%% for ignored users %% for ignored users
?assertAuthSuccessForUser(emqx_authn_ignore_for_hook_good), ?assertAuthSuccessForUser(emqx_authn_ignore_for_hook_good),
?assertAuthFailureForUser(emqx_authn_ignore_for_hook_bad), ?assertAuthFailureForUser(emqx_authn_ignore_for_hook_bad),
%% lower-priority hook cannot overrride %% lower-priority hook cannot overrride
%% successful/unsuccessful emqx_authentication result %% successful/unsuccessful emqx_authn_chains result
?assertAuthFailureForUser(hook_user_finally_good), ?assertAuthFailureForUser(hook_user_finally_good),
?assertAuthFailureForUser(hook_user_finally_bad), ?assertAuthFailureForUser(hook_user_finally_bad),
?assertAuthFailureForUser(hook_user_good), ?assertAuthFailureForUser(hook_user_good),

View File

@ -30,9 +30,10 @@ all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{ Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_auth], #{
work_dir => ?config(priv_dir, Config) work_dir => ?config(priv_dir, Config)
}), }),
ok = emqx_authn_test_lib:register_fake_providers([{password_based, built_in_database}]),
[{apps, Apps} | Config]. [{apps, Apps} | Config].
end_per_suite(Config) -> end_per_suite(Config) ->

View File

@ -0,0 +1,72 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2021-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_fake_provider).
-behaviour(emqx_authn_provider).
-export([
create/2,
update/2,
authenticate/2,
destroy/1,
add_user/2
]).
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
create(_AuthenticatorID, Config) ->
create(Config).
create(#{} = Config) ->
UserTab = ets:new(?MODULE, [set, public]),
{ok, #{users => UserTab, config => Config}}.
update(Config, _State) ->
create(Config).
authenticate(Credentials, #{users := UserTab} = _State) ->
IsValid = lists:any(
fun(User) -> are_credentials_matching(Credentials, User) end, ets:tab2list(UserTab)
),
case IsValid of
true ->
{ok, #{is_superuser => true}};
false ->
{error, bad_username_or_password}
end.
destroy(#{users := UserTab}) ->
true = ets:delete(UserTab),
ok.
add_user(#{user_id := UserId, password := Password} = User, #{users := UserTab} = _State) ->
true = ets:insert(UserTab, {UserId, Password}),
{ok, User}.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
are_credentials_matching(#{username := Username, password := Password}, {Username, Password}) ->
true;
are_credentials_matching(#{clientid := ClientId, password := Password}, {ClientId, Password}) ->
true;
are_credentials_matching(_Credentials, _User) ->
false.

View File

@ -0,0 +1,85 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-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_init_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(CLIENTINFO, #{
zone => default,
listener => {tcp, default},
protocol => mqtt,
peerhost => {127, 0, 0, 1},
clientid => <<"clientid">>,
username => <<"username">>,
password => <<"passwd">>,
is_superuser => false,
mountpoint => undefined
}).
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_testcase(_Case, Config) ->
Apps = emqx_cth_suite:start(
[
emqx,
{emqx_conf,
"authentication = [{mechanism = password_based, backend = built_in_database}]"}
],
#{
work_dir => ?config(priv_dir, Config)
}
),
[{apps, Apps} | Config].
end_per_testcase(_Case, Config) ->
_ = application:stop(emqx_auth),
ok = emqx_cth_suite:stop(?config(apps, Config)),
ok.
t_initialize(_Config) ->
?assertMatch(
{ok, _},
emqx_access_control:authenticate(?CLIENTINFO)
),
ok = application:start(emqx_auth),
?assertMatch(
{error, not_authorized},
emqx_access_control:authenticate(?CLIENTINFO)
),
ok = emqx_authn_test_lib:register_fake_providers([{password_based, built_in_database}]),
?assertMatch(
{error, not_authorized},
emqx_access_control:authenticate(?CLIENTINFO)
),
_ = emqx_authn_chains:add_user(
'mqtt:global',
<<"password_based:built_in_database">>,
#{user_id => <<"username">>, password => <<"passwd">>}
),
?assertMatch(
{ok, _},
emqx_access_control:authenticate(?CLIENTINFO)
).

View File

@ -28,9 +28,13 @@ all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{ Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_auth], #{
work_dir => ?config(priv_dir, Config) work_dir => ?config(priv_dir, Config)
}), }),
ok = emqx_authn_test_lib:register_fake_providers([
{password_based, built_in_database},
{password_based, redis}
]),
[{apps, Apps} | Config]. [{apps, Apps} | Config].
end_per_suite(Config) -> end_per_suite(Config) ->
@ -70,14 +74,14 @@ t_create_update_delete(Config) ->
#{ #{
id := <<"password_based:built_in_database">>, id := <<"password_based:built_in_database">>,
state := #{ state := #{
user_id_type := clientid config := #{user_id_type := clientid}
} }
} }
], ],
name := 'tcp:listener0' name := 'tcp:listener0'
} }
]}, ]},
emqx_authentication:list_chains() emqx_authn_chains:list_chains()
), ),
%% Drop old, create new %% Drop old, create new
@ -99,14 +103,14 @@ t_create_update_delete(Config) ->
#{ #{
id := <<"password_based:built_in_database">>, id := <<"password_based:built_in_database">>,
state := #{ state := #{
user_id_type := clientid config := #{user_id_type := clientid}
} }
} }
], ],
name := 'tcp:listener1' name := 'tcp:listener1'
} }
]}, ]},
emqx_authentication:list_chains() emqx_authn_chains:list_chains()
), ),
%% Update %% Update
@ -128,14 +132,14 @@ t_create_update_delete(Config) ->
#{ #{
id := <<"password_based:built_in_database">>, id := <<"password_based:built_in_database">>,
state := #{ state := #{
user_id_type := username config := #{user_id_type := username}
} }
} }
], ],
name := 'tcp:listener1' name := 'tcp:listener1'
} }
]}, ]},
emqx_authentication:list_chains() emqx_authn_chains:list_chains()
), ),
%% Update by listener path %% Update by listener path
@ -153,14 +157,14 @@ t_create_update_delete(Config) ->
#{ #{
id := <<"password_based:built_in_database">>, id := <<"password_based:built_in_database">>,
state := #{ state := #{
user_id_type := clientid config := #{user_id_type := clientid}
} }
} }
], ],
name := 'tcp:listener1' name := 'tcp:listener1'
} }
]}, ]},
emqx_authentication:list_chains() emqx_authn_chains:list_chains()
), ),
%% Delete %% Delete
@ -170,7 +174,7 @@ t_create_update_delete(Config) ->
), ),
?assertMatch( ?assertMatch(
{ok, []}, {ok, []},
emqx_authentication:list_chains() emqx_authn_chains:list_chains()
). ).
t_convert_certs(Config) -> t_convert_certs(Config) ->
@ -236,7 +240,7 @@ listener_mqtt_tcp_conf(Config) ->
}. }.
some_pem() -> some_pem() ->
Dir = code:lib_dir(emqx_authn, test), Dir = code:lib_dir(emqx_auth, test),
Path = filename:join([Dir, "data", "private_key.pem"]), Path = filename:join([Dir, "data", "private_key.pem"]),
{ok, Pem} = file:read_file(Path), {ok, Pem} = file:read_file(Path),
Pem. Pem.

View File

@ -12,7 +12,7 @@ all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{ Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_auth], #{
work_dir => ?config(priv_dir, Config) work_dir => ?config(priv_dir, Config)
}), }),
[{apps, Apps} | Config]. [{apps, Apps} | Config].
@ -54,7 +54,7 @@ t_check_schema(_Config) ->
?assertThrow( ?assertThrow(
#{ #{
path := "authentication.1.password_hash_algorithm.name", path := "authentication.1.password_hash_algorithm.name",
matched_type := "authn:builtin_db/authn-hash:simple", matched_type := "builtin_db/authn-hash:simple",
reason := unable_to_convert_to_enum_symbol reason := unable_to_convert_to_enum_symbol
}, },
Check(ConfigNotOk) Check(ConfigNotOk)
@ -73,7 +73,7 @@ t_check_schema(_Config) ->
#{ #{
path := "authentication.1.password_hash_algorithm", path := "authentication.1.password_hash_algorithm",
reason := "algorithm_name_missing", reason := "algorithm_name_missing",
matched_type := "authn:builtin_db" matched_type := "builtin_db"
}, },
Check(ConfigMissingAlgoName) Check(ConfigMissingAlgoName)
). ).
@ -107,7 +107,8 @@ t_union_member_selector(_) ->
BadBackend = Base#{<<"mechanism">> => <<"password_based">>, <<"backend">> => <<"bar">>}, BadBackend = Base#{<<"mechanism">> => <<"password_based">>, <<"backend">> => <<"bar">>},
?assertThrow( ?assertThrow(
#{ #{
reason := "unknown_backend", reason := "unsupported_mechanism",
mechanism := <<"password_based">>,
backend := <<"bar">> backend := <<"bar">>
}, },
check(BadBackend) check(BadBackend)
@ -124,9 +125,8 @@ t_union_member_selector(_) ->
BadCombination = Base#{<<"mechanism">> => <<"scram">>, <<"backend">> => <<"http">>}, BadCombination = Base#{<<"mechanism">> => <<"scram">>, <<"backend">> => <<"http">>},
?assertThrow( ?assertThrow(
#{ #{
reason := "unsupported_mechanism", reason := "unknown_mechanism",
mechanism := <<"scram">>, expected := "password_based"
backend := <<"http">>
}, },
check(BadCombination) check(BadCombination)
), ),

View File

@ -35,7 +35,7 @@ jwt_example() ->
authenticator_example(jwt). authenticator_example(jwt).
delete_authenticators(Path, Chain) -> delete_authenticators(Path, Chain) ->
case emqx_authentication:list_authenticators(Chain) of case emqx_authn_chains:list_authenticators(Chain) of
{error, _} -> {error, _} ->
ok; ok;
{ok, Authenticators} -> {ok, Authenticators} ->
@ -60,9 +60,20 @@ delete_config(ID) ->
). ).
client_ssl_cert_opts() -> client_ssl_cert_opts() ->
Dir = code:lib_dir(emqx_authn, test), Dir = code:lib_dir(emqx_auth, test),
#{ #{
<<"keyfile">> => filename:join([Dir, <<"data/certs">>, <<"client.key">>]), <<"keyfile">> => filename:join([Dir, <<"data/certs">>, <<"client.key">>]),
<<"certfile">> => filename:join([Dir, <<"data/certs">>, <<"client.crt">>]), <<"certfile">> => filename:join([Dir, <<"data/certs">>, <<"client.crt">>]),
<<"cacertfile">> => filename:join([Dir, <<"data/certs">>, <<"ca.crt">>]) <<"cacertfile">> => filename:join([Dir, <<"data/certs">>, <<"ca.crt">>])
}. }.
register_fake_providers(ProviderTypes) ->
Providers = [
{ProviderType, emqx_authn_fake_provider}
|| ProviderType <- ProviderTypes
],
emqx_authn_chains:register_providers(Providers).
deregister_providers() ->
ProviderTypes = maps:keys(emqx_authn_chains:get_providers()),
emqx_authn_chains:deregister_providers(ProviderTypes).

View File

@ -39,20 +39,33 @@ init_per_suite(Config) ->
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, remove_local, fun(_) -> ok end), meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
meck:expect( meck:expect(
emqx_authz, emqx_authz_file,
acl_conf_file, acl_conf_file,
fun() -> fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf") emqx_common_test_helpers:deps_path(emqx_auth_file, "etc/acl.conf")
end end
), ),
Apps = emqx_cth_suite:start(
ok = emqx_common_test_helpers:start_apps( [
[emqx_conf, emqx_authz], emqx,
fun set_special_configs/1 {emqx_conf,
"authorization { cache { enable = false }, no_match = deny, sources = [] }"},
emqx_auth,
emqx_auth_file,
emqx_auth_http,
emqx_auth_mnesia,
emqx_auth_redis,
emqx_auth_postgresql,
emqx_auth_mysql,
emqx_auth_mongodb
],
#{
work_dir => filename:join(?config(priv_dir, Config), ?MODULE)
}
), ),
Config. [{suite_apps, Apps} | Config].
end_per_suite(_Config) -> end_per_suite(Config) ->
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
[authorization], [authorization],
#{ #{
@ -61,7 +74,7 @@ end_per_suite(_Config) ->
<<"sources">> => [] <<"sources">> => []
} }
), ),
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]), emqx_cth_suite:stop(?config(suite_apps, Config)),
meck:unload(emqx_resource), meck:unload(emqx_resource),
ok. ok.
@ -73,7 +86,7 @@ init_per_testcase(TestCase, Config) when
{ok, _} = emqx_authz:update(?CMD_REPLACE, []), {ok, _} = emqx_authz:update(?CMD_REPLACE, []),
{ok, _} = emqx:update_config([authorization, deny_action], disconnect), {ok, _} = emqx:update_config([authorization, deny_action], disconnect),
Config; Config;
init_per_testcase(_, Config) -> init_per_testcase(_TestCase, Config) ->
{ok, _} = emqx_authz:update(?CMD_REPLACE, []), {ok, _} = emqx_authz:update(?CMD_REPLACE, []),
Config. Config.
@ -90,14 +103,6 @@ end_per_testcase(_TestCase, _Config) ->
emqx_common_test_helpers:call_janitor(), emqx_common_test_helpers:call_janitor(),
ok. ok.
set_special_configs(emqx_authz) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
{ok, _} = emqx:update_config([authorization, sources], []),
ok;
set_special_configs(_App) ->
ok.
-define(SOURCE1, #{ -define(SOURCE1, #{
<<"type">> => <<"http">>, <<"type">> => <<"http">>,
<<"enable">> => true, <<"enable">> => true,
@ -205,7 +210,7 @@ t_bad_file_source(_) ->
BadActionErr = {invalid_authorization_action, pubsub}, BadActionErr = {invalid_authorization_action, pubsub},
lists:foreach( lists:foreach(
fun({Source, Error}) -> fun({Source, Error}) ->
File = emqx_authz:acl_conf_file(), File = emqx_authz_file:acl_conf_file(),
{ok, Bin1} = file:read_file(File), {ok, Bin1} = file:read_file(File),
?assertEqual(?UPDATE_ERROR(Error), emqx_authz:update(?CMD_REPLACE, [Source])), ?assertEqual(?UPDATE_ERROR(Error), emqx_authz:update(?CMD_REPLACE, [Source])),
?assertEqual(?UPDATE_ERROR(Error), emqx_authz:update(?CMD_PREPEND, Source)), ?assertEqual(?UPDATE_ERROR(Error), emqx_authz:update(?CMD_PREPEND, Source)),

View File

@ -33,7 +33,7 @@ groups() ->
init_per_suite(Config) -> init_per_suite(Config) ->
ok = emqx_mgmt_api_test_util:init_suite( ok = emqx_mgmt_api_test_util:init_suite(
[emqx_conf, emqx_authz], [emqx_conf, emqx_auth],
fun set_special_configs/1 fun set_special_configs/1
), ),
Config. Config.
@ -47,12 +47,12 @@ end_per_suite(_Config) ->
<<"sources">> => [] <<"sources">> => []
} }
), ),
emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]), emqx_mgmt_api_test_util:end_suite([emqx_auth, emqx_conf]),
ok. ok.
set_special_configs(emqx_dashboard) -> set_special_configs(emqx_dashboard) ->
emqx_dashboard_api_test_helpers:set_default_config(); emqx_dashboard_api_test_helpers:set_default_config();
set_special_configs(emqx_authz) -> set_special_configs(emqx_auth) ->
{ok, _} = emqx:update_config([authorization, cache, enable], true), {ok, _} = emqx:update_config([authorization, cache, enable], true),
{ok, _} = emqx:update_config([authorization, no_match], deny), {ok, _} = emqx:update_config([authorization, no_match], deny),
{ok, _} = emqx:update_config([authorization, sources], []), {ok, _} = emqx:update_config([authorization, sources], []),

View File

@ -31,7 +31,7 @@ groups() ->
init_per_suite(Config) -> init_per_suite(Config) ->
ok = emqx_mgmt_api_test_util:init_suite( ok = emqx_mgmt_api_test_util:init_suite(
[emqx_conf, emqx_authz, emqx_dashboard], [emqx_conf, emqx_auth, emqx_dashboard],
fun set_special_configs/1 fun set_special_configs/1
), ),
Config. Config.
@ -46,12 +46,12 @@ end_per_suite(_Config) ->
} }
), ),
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]), emqx_mgmt_api_test_util:end_suite([emqx_auth, emqx_conf]),
ok. ok.
set_special_configs(emqx_dashboard) -> set_special_configs(emqx_dashboard) ->
emqx_dashboard_api_test_helpers:set_default_config(); emqx_dashboard_api_test_helpers:set_default_config();
set_special_configs(emqx_authz) -> set_special_configs(emqx_auth) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny), {ok, _} = emqx:update_config([authorization, no_match], deny),
{ok, _} = emqx:update_config([authorization, sources], []), {ok, _} = emqx:update_config([authorization, sources], []),

View File

@ -21,6 +21,7 @@
-import(emqx_mgmt_api_test_util, [request/3, uri/1]). -import(emqx_mgmt_api_test_util, [request/3, uri/1]).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
-define(MONGO_SINGLE_HOST, "mongo"). -define(MONGO_SINGLE_HOST, "mongo").
@ -101,27 +102,42 @@ groups() ->
[]. [].
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource]),
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, health_check, fun(St) -> {ok, St} end), meck:expect(emqx_resource, health_check, fun(St) -> {ok, St} end),
meck:expect(emqx_resource, remove_local, fun(_) -> ok end), meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
meck:expect( meck:expect(
emqx_authz, emqx_authz_file,
acl_conf_file, acl_conf_file,
fun() -> fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf") emqx_common_test_helpers:deps_path(emqx_auth_file, "etc/acl.conf")
end end
), ),
ok = emqx_mgmt_api_test_util:init_suite( Apps = emqx_cth_suite:start(
[emqx_conf, emqx_authz], [
fun set_special_configs/1 emqx,
{emqx_conf,
"authorization { cache { enable = false }, no_match = deny, sources = [] }"},
emqx_auth,
emqx_auth_file,
emqx_auth_http,
emqx_auth_mnesia,
emqx_auth_redis,
emqx_auth_postgresql,
emqx_auth_mysql,
emqx_auth_mongodb,
emqx_management,
{emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"}
],
#{
work_dir => filename:join(?config(priv_dir, Config), ?MODULE)
}
), ),
ok = start_apps([emqx_resource]), _ = emqx_common_test_http:create_default_app(),
Config. [{suite_apps, Apps} | Config].
end_per_suite(_Config) -> end_per_suite(Config) ->
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
[authorization], [authorization],
#{ #{
@ -130,23 +146,11 @@ end_per_suite(_Config) ->
<<"sources">> => [] <<"sources">> => []
} }
), ),
%% resource and connector should be stop first, _ = emqx_common_test_http:delete_default_app(),
%% or authz_[mysql|pgsql|redis..]_SUITE would be failed emqx_cth_suite:stop(?config(suite_apps, Config)),
ok = stop_apps([emqx_resource]),
emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]),
meck:unload(emqx_resource), meck:unload(emqx_resource),
ok. ok.
set_special_configs(emqx_dashboard) ->
emqx_dashboard_api_test_helpers:set_default_config();
set_special_configs(emqx_authz) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
{ok, _} = emqx:update_config([authorization, sources], []),
ok;
set_special_configs(_App) ->
ok.
init_per_testcase(t_api, Config) -> init_per_testcase(t_api, Config) ->
meck:new(emqx_utils, [non_strict, passthrough, no_history, no_link]), meck:new(emqx_utils, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_utils, gen_id, fun() -> "fake" end), meck:expect(emqx_utils, gen_id, fun() -> "fake" end),
@ -206,7 +210,7 @@ t_api(_) ->
], ],
Sources Sources
), ),
?assert(filelib:is_file(emqx_authz:acl_conf_file())), ?assert(filelib:is_file(emqx_authz_file:acl_conf_file())),
{ok, 204, _} = request( {ok, 204, _} = request(
put, put,

View File

@ -34,10 +34,12 @@ groups() ->
init_per_testcase(TestCase, Config) -> init_per_testcase(TestCase, Config) ->
Apps = emqx_cth_suite:start( Apps = emqx_cth_suite:start(
[ [
emqx,
{emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"}, {emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"},
emqx_authz emqx_auth,
emqx_auth_file
], ],
#{work_dir => emqx_cth_suite:work_dir(TestCase, Config)} #{work_dir => filename:join(?config(priv_dir, Config), TestCase)}
), ),
[{tc_apps, Apps} | Config]. [{tc_apps, Apps} | Config].

View File

@ -35,7 +35,7 @@ all() ->
init_per_suite(Config) -> init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz], [emqx_conf, emqx_auth],
fun set_special_configs/1 fun set_special_configs/1
), ),
Config. Config.
@ -49,7 +49,7 @@ end_per_suite(_Config) ->
<<"sources">> => [] <<"sources">> => []
} }
), ),
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]), emqx_common_test_helpers:stop_apps([emqx_auth, emqx_conf]),
ok. ok.
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
@ -58,7 +58,7 @@ end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true), _ = emqx_authz:set_feature_available(rich_actions, true),
ok. ok.
set_special_configs(emqx_authz) -> set_special_configs(emqx_auth) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny), {ok, _} = emqx:update_config([authorization, no_match], deny),
{ok, _} = emqx:update_config([authorization, sources], []), {ok, _} = emqx:update_config([authorization, sources], []),

View File

@ -0,0 +1,23 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-ifndef(EMQX_AUTH_FILE_HRL).
-define(EMQX_AUTH_FILE_HRL, true).
-define(AUTHZ_TYPE, file).
-define(AUTHZ_TYPE_BIN, <<"file">>).
-endif.

View File

@ -0,0 +1,7 @@
%% -*- mode: erlang -*-
{deps, [
{emqx, {path, "../emqx"}},
{emqx_utils, {path, "../emqx_utils"}},
{emqx_auth, {path, "../emqx_auth"}}
]}.

View File

@ -0,0 +1,18 @@
%% -*- mode: erlang -*-
{application, emqx_auth_file, [
{description, "EMQX File-based Authentication and Authorization"},
{vsn, "0.1.0"},
{registered, []},
{mod, {emqx_auth_file_app, []}},
{applications, [
kernel,
stdlib,
emqx,
emqx_auth
]},
{env, []},
{modules, []},
{licenses, ["Apache 2.0"]},
{links, []}
]}.

View File

@ -0,0 +1,32 @@
%%--------------------------------------------------------------------
%% 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_auth_file_app).
-include("emqx_auth_file.hrl").
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
ok = emqx_authz:register_source(?AUTHZ_TYPE, emqx_authz_file),
{ok, Sup} = emqx_auth_file_sup:start_link(),
{ok, Sup}.
stop(_State) ->
ok = emqx_authz:unregister_source(?AUTHZ_TYPE),
ok.

View File

@ -0,0 +1,37 @@
%%--------------------------------------------------------------------
%% 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_auth_file_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
init([]) ->
SupFlags = #{
strategy => one_for_all,
intensity => 0,
period => 1
},
ChildSpecs = [],
{ok, {SupFlags, ChildSpecs}}.

View File

@ -18,7 +18,7 @@
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-behaviour(emqx_authz). -behaviour(emqx_authz_source).
-ifdef(TEST). -ifdef(TEST).
-compile(export_all). -compile(export_all).
@ -31,14 +31,56 @@
create/1, create/1,
update/1, update/1,
destroy/1, destroy/1,
authorize/4, authorize/4
validate/1,
read_file/1
]). ]).
-export([
validate/1,
write_files/1,
read_files/1
]).
%% For testing
-export([
acl_conf_file/0
]).
%%------------------------------------------------------------------------------
%% Authz Source Callbacks
%%------------------------------------------------------------------------------
description() -> description() ->
"AuthZ with static rules". "AuthZ with static rules".
create(#{path := Path} = Source) ->
{ok, Rules} = validate(Path),
Source#{annotations => #{rules => Rules}}.
update(#{path := _Path} = Source) ->
create(Source).
destroy(_Source) -> ok.
authorize(Client, PubSub, Topic, #{annotations := #{rules := Rules}}) ->
emqx_authz_rule:matches(Client, PubSub, Topic, Rules).
read_files(#{<<"path">> := Path} = Source) ->
{ok, Rules} = read_file(Path),
maps:remove(<<"path">>, Source#{<<"rules">> => Rules}).
write_files(#{<<"rules">> := Rules} = Source0) ->
AclPath = ?MODULE:acl_conf_file(),
%% Always check if the rules are valid before writing to the file
%% If the rules are invalid, the old file will be kept
ok = check_acl_file_rules(AclPath, Rules),
ok = write_file(AclPath, Rules),
Source1 = maps:remove(<<"rules">>, Source0),
maps:put(<<"path">>, AclPath, Source1).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
validate(Path0) -> validate(Path0) ->
Path = filename(Path0), Path = filename(Path0),
Rules = Rules =
@ -53,25 +95,39 @@ validate(Path0) ->
}), }),
throw(failed_to_read_acl_file); throw(failed_to_read_acl_file);
{error, Reason} -> {error, Reason} ->
?SLOG(alert, #{msg => "bad_acl_file_content", path => Path, reason => Reason}), ?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),
throw({bad_acl_file_content, Reason}) throw({bad_acl_file_content, Reason})
end, end,
{ok, Rules}. {ok, Rules}.
create(#{path := Path} = Source) ->
{ok, Rules} = validate(Path),
Source#{annotations => #{rules => Rules}}.
update(#{path := _Path} = Source) ->
create(Source).
destroy(_Source) -> ok.
authorize(Client, PubSub, Topic, #{annotations := #{rules := Rules}}) ->
emqx_authz_rule:matches(Client, PubSub, Topic, Rules).
read_file(Path) -> read_file(Path) ->
file:read_file(filename(Path)). file:read_file(filename(Path)).
write_file(Filename, Bytes) ->
ok = filelib:ensure_dir(Filename),
case file:write_file(Filename, Bytes) of
ok ->
ok;
{error, Reason} ->
?SLOG(error, #{filename => Filename, msg => "write_file_error", reason => Reason}),
throw(Reason)
end.
filename(PathMaybeTemplate) -> filename(PathMaybeTemplate) ->
emqx_schema:naive_env_interpolation(PathMaybeTemplate). emqx_schema:naive_env_interpolation(PathMaybeTemplate).
%% @doc where the acl.conf file is stored.
acl_conf_file() ->
filename:join([emqx:data_dir(), "authz", "acl.conf"]).
check_acl_file_rules(Path, Rules) ->
TmpPath = Path ++ ".tmp",
try
ok = write_file(TmpPath, Rules),
{ok, _} = validate(TmpPath),
ok
catch
throw:Reason -> throw(Reason)
after
_ = file:delete(TmpPath)
end.

View File

@ -0,0 +1,80 @@
%%--------------------------------------------------------------------
%% 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_authz_file_schema).
-include("emqx_auth_file.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-behaviour(emqx_authz_schema).
-export([
type/0,
fields/1,
desc/1,
source_refs/0,
api_source_refs/0,
select_union_member/1
]).
type() -> ?AUTHZ_TYPE.
fields(file) ->
emqx_authz_schema:authz_common_fields(?AUTHZ_TYPE) ++
[
{path,
?HOCON(
string(),
#{
required => true,
validator => fun(Path) -> element(1, emqx_authz_file:validate(Path)) end,
desc => ?DESC(path)
}
)}
];
fields(api_file) ->
emqx_authz_schema:authz_common_fields(?AUTHZ_TYPE) ++
[
{rules,
?HOCON(
binary(),
#{
required => true,
example =>
<<
"{allow,{username,{re,\"^dashboard$\"}},subscribe,[\"$SYS/#\"]}.\n",
"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
>>,
desc => ?DESC(rules)
}
)}
].
desc(file) ->
?DESC(file);
desc(api_file) ->
?DESC(file).
source_refs() ->
[?R_REF(file)].
api_source_refs() ->
[?R_REF(api_file)].
select_union_member(#{<<"type">> := ?AUTHZ_TYPE_BIN}) ->
?R_REF(file);
select_union_member(_Value) ->
undefined.

View File

@ -18,7 +18,7 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-compile(export_all). -compile(export_all).
-include("emqx_authz.hrl"). -include_lib("emqx_auth/include/emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
@ -42,9 +42,11 @@ init_per_testcase(TestCase, Config) ->
Apps = emqx_cth_suite:start( Apps = emqx_cth_suite:start(
[ [
{emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"}, {emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"},
emqx_authz emqx,
emqx_auth,
emqx_auth_file
], ],
#{work_dir => emqx_cth_suite:work_dir(TestCase, Config)} #{work_dir => filename:join(?config(priv_dir, Config), TestCase)}
), ),
[{tc_apps, Apps} | Config]. [{tc_apps, Apps} | Config].

View File

@ -0,0 +1,29 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-ifndef(EMQX_AUTH_HTTP_HRL).
-define(EMQX_AUTH_HTTP_HRL, true).
-define(AUTHZ_TYPE, http).
-define(AUTHZ_TYPE_BIN, <<"http">>).
-define(AUTHN_MECHANISM, password_based).
-define(AUTHN_MECHANISM_BIN, <<"password_based">>).
-define(AUTHN_BACKEND, http).
-define(AUTHN_BACKEND_BIN, <<"http">>).
-define(AUTHN_TYPE, {?AUTHN_MECHANISM, ?AUTHN_BACKEND}).
-endif.

View File

@ -0,0 +1,6 @@
%% -*- mode: erlang -*-
{deps, [
{emqx, {path, "../emqx"}},
{emqx_utils, {path, "../emqx_utils"}},
{emqx_auth, {path, "../emqx_auth"}}
]}.

View File

@ -0,0 +1,19 @@
%% -*- mode: erlang -*-
{application, emqx_auth_http, [
{description, "EMQX External HTTP API Authentication and Authorization"},
{vsn, "0.1.0"},
{registered, []},
{mod, {emqx_auth_http_app, []}},
{applications, [
kernel,
stdlib,
emqx_auth,
emqx_connector,
emqx_resource
]},
{env, []},
{modules, []},
{licenses, ["Apache 2.0"]},
{links, []}
]}.

View File

@ -0,0 +1,34 @@
%%--------------------------------------------------------------------
%% 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_auth_http_app).
-include("emqx_auth_http.hrl").
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
ok = emqx_authz:register_source(?AUTHZ_TYPE, emqx_authz_http),
ok = emqx_authn:register_provider(?AUTHN_TYPE, emqx_authn_http),
{ok, Sup} = emqx_auth_http_sup:start_link(),
{ok, Sup}.
stop(_State) ->
ok = emqx_authn:deregister_provider(?AUTHN_TYPE),
ok = emqx_authz:unregister_source(?AUTHZ_TYPE),
ok.

View File

@ -0,0 +1,37 @@
%%--------------------------------------------------------------------
%% 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_auth_http_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
init([]) ->
SupFlags = #{
strategy => one_for_all,
intensity => 0,
period => 1
},
ChildSpecs = [],
{ok, {SupFlags, ChildSpecs}}.

View File

@ -16,188 +16,47 @@
-module(emqx_authn_http). -module(emqx_authn_http).
-include("emqx_authn.hrl"). -include_lib("emqx_auth/include/emqx_authn.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx_connector/include/emqx_connector.hrl").
-behaviour(hocon_schema). -behaviour(emqx_authn_provider).
-behaviour(emqx_authentication).
-export([ -export([
namespace/0,
tags/0,
roots/0,
fields/1,
desc/1,
validations/0
]).
-export([
headers_no_content_type/1,
headers/1
]).
-export([check_headers/1, check_ssl_opts/1]).
-export([
refs/0,
union_member_selector/1,
create/2, create/2,
update/2, update/2,
authenticate/2, authenticate/2,
destroy/1 destroy/1
]). ]).
%%------------------------------------------------------------------------------
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn".
tags() ->
[<<"Authentication">>].
%% used for config check when the schema module is resolved
roots() ->
[
{?CONF_NS,
hoconsc:mk(
hoconsc:union(fun ?MODULE:union_member_selector/1),
#{}
)}
].
fields(http_get) ->
[
{method, #{type => get, required => true, desc => ?DESC(method)}},
{headers, fun headers_no_content_type/1}
] ++ common_fields();
fields(http_post) ->
[
{method, #{type => post, required => true, desc => ?DESC(method)}},
{headers, fun headers/1}
] ++ common_fields().
desc(http_get) ->
?DESC(get);
desc(http_post) ->
?DESC(post);
desc(_) ->
undefined.
common_fields() ->
[
{mechanism, emqx_authn_schema:mechanism(password_based)},
{backend, emqx_authn_schema:backend(http)},
{url, fun url/1},
{body,
hoconsc:mk(map([{fuzzy, term(), binary()}]), #{
required => false, desc => ?DESC(body)
})},
{request_timeout, fun request_timeout/1}
] ++ emqx_authn_schema:common_fields() ++
maps:to_list(
maps:without(
[
pool_type
],
maps:from_list(emqx_bridge_http_connector:fields(config))
)
).
validations() ->
[
{check_ssl_opts, fun ?MODULE:check_ssl_opts/1},
{check_headers, fun ?MODULE:check_headers/1}
].
url(type) -> binary();
url(desc) -> ?DESC(?FUNCTION_NAME);
url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")];
url(required) -> true;
url(_) -> undefined.
headers(type) ->
map();
headers(desc) ->
?DESC(?FUNCTION_NAME);
headers(converter) ->
fun(Headers) ->
maps:merge(default_headers(), transform_header_name(Headers))
end;
headers(default) ->
default_headers();
headers(_) ->
undefined.
headers_no_content_type(type) ->
map();
headers_no_content_type(desc) ->
?DESC(?FUNCTION_NAME);
headers_no_content_type(converter) ->
fun(Headers) ->
maps:without(
[<<"content-type">>],
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
)
end;
headers_no_content_type(default) ->
default_headers_no_content_type();
headers_no_content_type(_) ->
undefined.
request_timeout(type) -> emqx_schema:duration_ms();
request_timeout(desc) -> ?DESC(?FUNCTION_NAME);
request_timeout(default) -> <<"5s">>;
request_timeout(_) -> undefined.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
refs() ->
[
hoconsc:ref(?MODULE, http_get),
hoconsc:ref(?MODULE, http_post)
].
union_member_selector(all_union_members) ->
refs();
union_member_selector({value, Value}) ->
refs(Value).
refs(#{<<"method">> := <<"get">>}) ->
[hoconsc:ref(?MODULE, http_get)];
refs(#{<<"method">> := <<"post">>}) ->
[hoconsc:ref(?MODULE, http_post)];
refs(_) ->
throw(#{
field_name => method,
expected => "get | post"
}).
create(_AuthenticatorID, Config) -> create(_AuthenticatorID, Config) ->
create(Config). create(Config).
create(Config0) -> create(Config0) ->
with_validated_config(Config0, fun(Config, State) ->
ResourceId = emqx_authn_utils:make_resource_id(?MODULE), ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
{Config, State} = parse_config(Config0), % {Config, State} = parse_config(Config0),
{ok, _Data} = emqx_authn_utils:create_resource( {ok, _Data} = emqx_authn_utils:create_resource(
ResourceId, ResourceId,
emqx_bridge_http_connector, emqx_bridge_http_connector,
Config Config
), ),
{ok, State#{resource_id => ResourceId}}. {ok, State#{resource_id => ResourceId}}
end).
update(Config0, #{resource_id := ResourceId} = _State) -> update(Config0, #{resource_id := ResourceId} = _State) ->
{Config, NState} = parse_config(Config0), with_validated_config(Config0, fun(Config, NState) ->
% {Config, NState} = parse_config(Config0),
case emqx_authn_utils:update_resource(emqx_bridge_http_connector, Config, ResourceId) of case emqx_authn_utils:update_resource(emqx_bridge_http_connector, Config, ResourceId) of
{error, Reason} -> {error, Reason} ->
error({load_config_error, Reason}); error({load_config_error, Reason});
{ok, _} -> {ok, _} ->
{ok, NState#{resource_id => ResourceId}} {ok, NState#{resource_id => ResourceId}}
end. end
end).
authenticate(#{auth_method := _}, _) -> authenticate(#{auth_method := _}, _) ->
ignore; ignore;
@ -237,92 +96,35 @@ destroy(#{resource_id := ResourceId}) ->
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
default_headers() -> with_validated_config(Config, Fun) ->
maps:put( Pipeline = [
<<"content-type">>, fun check_ssl_opts/1,
<<"application/json">>, fun check_headers/1,
default_headers_no_content_type() fun parse_config/1
). ],
case emqx_utils:pipeline(Pipeline, Config, undefined) of
default_headers_no_content_type() -> {ok, NConfig, ProviderState} ->
#{ Fun(NConfig, ProviderState);
<<"accept">> => <<"application/json">>, {error, Reason, _} ->
<<"cache-control">> => <<"no-cache">>, {error, Reason}
<<"connection">> => <<"keep-alive">>,
<<"keep-alive">> => <<"timeout=30, max=1000">>
}.
transform_header_name(Headers) ->
maps:fold(
fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc)
end,
#{},
Headers
).
check_ssl_opts(Conf) ->
case is_backend_http(Conf) of
true ->
Url = get_conf_val("url", Conf),
{BaseUrl, _Path, _Query} = parse_url(Url),
case BaseUrl of
<<"https://", _/binary>> ->
case get_conf_val("ssl.enable", Conf) of
true ->
ok;
false ->
<<"it's required to enable the TLS option to establish a https connection">>
end;
<<"http://", _/binary>> ->
ok
end;
false ->
ok
end. end.
check_headers(Conf) -> check_ssl_opts(#{url := <<"https://", _/binary>>, ssl := #{enable := false}}) ->
case is_backend_http(Conf) of {error,
true -> {invalid_ssl_opts,
Headers = get_conf_val("headers", Conf), <<"it's required to enable the TLS option to establish a https connection">>}};
case to_bin(get_conf_val("method", Conf)) of check_ssl_opts(_) ->
<<"post">> -> ok.
ok;
<<"get">> -> check_headers(#{headers := Headers, method := get}) ->
case maps:is_key(<<"content-type">>, Headers) of case maps:is_key(<<"content-type">>, Headers) of
false -> ok;
true -> <<"HTTP GET requests cannot include content-type header.">>
end
end;
false -> false ->
ok ok;
end. true ->
{error, {invalid_headers, <<"HTTP GET requests cannot include content-type header.">>}}
is_backend_http(Conf) ->
case get_conf_val("backend", Conf) of
http -> true;
_ -> false
end.
parse_url(Url) ->
case string:split(Url, "//", leading) of
[Scheme, UrlRem] ->
case string:split(UrlRem, "/", leading) of
[HostPort, Remaining] ->
BaseUrl = iolist_to_binary([Scheme, "//", HostPort]),
case string:split(Remaining, "?", leading) of
[Path, QueryString] ->
{BaseUrl, <<"/", Path/binary>>, QueryString};
[Path] ->
{BaseUrl, <<"/", Path/binary>>, <<>>}
end; end;
[HostPort] -> check_headers(_) ->
{iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>} ok.
end;
[Url] ->
throw({invalid_url, Url})
end.
parse_config( parse_config(
#{ #{
@ -332,7 +134,7 @@ parse_config(
request_timeout := RequestTimeout request_timeout := RequestTimeout
} = Config } = Config
) -> ) ->
{BaseUrl0, Path, Query} = parse_url(RawUrl), {BaseUrl0, Path, Query} = emqx_authn_utils:parse_url(RawUrl),
{ok, BaseUrl} = emqx_http_lib:uri_parse(BaseUrl0), {ok, BaseUrl} = emqx_http_lib:uri_parse(BaseUrl0),
State = #{ State = #{
method => Method, method => Method,
@ -346,7 +148,7 @@ parse_config(
request_timeout => RequestTimeout, request_timeout => RequestTimeout,
url => RawUrl url => RawUrl
}, },
{Config#{base_url => BaseUrl, pool_type => random}, State}. {ok, Config#{base_url => BaseUrl, pool_type => random}, State}.
generate_request(Credential, #{ generate_request(Credential, #{
method := Method, method := Method,
@ -469,16 +271,11 @@ to_list(B) when is_binary(B) ->
to_list(L) when is_list(L) -> to_list(L) when is_list(L) ->
L. L.
to_bin(A) when is_atom(A) ->
atom_to_binary(A);
to_bin(B) when is_binary(B) -> to_bin(B) when is_binary(B) ->
B; B;
to_bin(L) when is_list(L) -> to_bin(L) when is_list(L) ->
list_to_binary(L). list_to_binary(L).
get_conf_val(Name, Conf) ->
hocon_maps:get(?CONF_NS ++ "." ++ Name, Conf).
ensure_header_name_type(Headers) -> ensure_header_name_type(Headers) ->
Fun = fun Fun = fun
(Key, _Val, Acc) when is_binary(Key) -> (Key, _Val, Acc) when is_binary(Key) ->

View File

@ -0,0 +1,132 @@
%%--------------------------------------------------------------------
%% 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_http_schema).
-include("emqx_auth_http.hrl").
-include_lib("emqx_auth/include/emqx_authn.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-behaviour(emqx_authn_schema).
-export([
fields/1,
desc/1,
refs/0,
select_union_member/1
]).
-define(NOT_EMPTY(MSG), emqx_resource_validator:not_empty(MSG)).
refs() ->
[?R_REF(http_get), ?R_REF(http_post)].
select_union_member(
#{<<"mechanism">> := ?AUTHN_MECHANISM_BIN, <<"backend">> := ?AUTHN_BACKEND_BIN} = Value
) ->
Method = maps:get(<<"method">>, Value, undefined),
case Method of
<<"get">> ->
[?R_REF(http_get)];
<<"post">> ->
[?R_REF(http_post)];
Else ->
throw(#{
reason => "unknown_http_method",
expected => "get | post",
field_name => method,
got => Else
})
end;
select_union_member(#{<<"backend">> := ?AUTHN_BACKEND_BIN}) ->
throw(#{
reason => "unknown_mechanism",
expected => "password_based",
got => undefined
});
select_union_member(_Value) ->
undefined.
fields(http_get) ->
[
{method, #{type => get, required => true, desc => ?DESC(method)}},
{headers, fun headers_no_content_type/1}
] ++ common_fields();
fields(http_post) ->
[
{method, #{type => post, required => true, desc => ?DESC(method)}},
{headers, fun headers/1}
] ++ common_fields().
desc(http_get) ->
?DESC(get);
desc(http_post) ->
?DESC(post);
desc(_) ->
undefined.
common_fields() ->
[
{mechanism, emqx_authn_schema:mechanism(?AUTHN_MECHANISM)},
{backend, emqx_authn_schema:backend(?AUTHN_BACKEND)},
{url, fun url/1},
{body,
hoconsc:mk(map([{fuzzy, term(), binary()}]), #{
required => false, desc => ?DESC(body)
})},
{request_timeout, fun request_timeout/1}
] ++ emqx_authn_schema:common_fields() ++
maps:to_list(
maps:without(
[
pool_type
],
maps:from_list(emqx_bridge_http_connector:fields(config))
)
).
url(type) -> binary();
url(desc) -> ?DESC(?FUNCTION_NAME);
url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")];
url(required) -> true;
url(_) -> undefined.
headers(type) ->
map();
headers(desc) ->
?DESC(?FUNCTION_NAME);
headers(converter) ->
fun emqx_authn_utils:convert_headers/1;
headers(default) ->
emqx_authn_utils:default_headers();
headers(_) ->
undefined.
headers_no_content_type(type) ->
map();
headers_no_content_type(desc) ->
?DESC(?FUNCTION_NAME);
headers_no_content_type(converter) ->
fun emqx_authn_utils:convert_headers_no_content_type/1;
headers_no_content_type(default) ->
emqx_authn_utils:default_headers_no_content_type();
headers_no_content_type(_) ->
undefined.
request_timeout(type) -> emqx_schema:duration_ms();
request_timeout(desc) -> ?DESC(?FUNCTION_NAME);
request_timeout(default) -> <<"5s">>;
request_timeout(_) -> undefined.

View File

@ -16,13 +16,11 @@
-module(emqx_authz_http). -module(emqx_authz_http).
-include("emqx_authz.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-behaviour(emqx_authz). -behaviour(emqx_authz_source).
%% AuthZ Callbacks %% AuthZ Callbacks
-export([ -export([
@ -31,6 +29,7 @@
update/1, update/1,
destroy/1, destroy/1,
authorize/4, authorize/4,
merge_defaults/1,
parse_url/1 parse_url/1
]). ]).
@ -73,7 +72,7 @@ update(Config) ->
end. end.
destroy(#{annotations := #{id := Id}}) -> destroy(#{annotations := #{id := Id}}) ->
ok = emqx_resource:remove_local(Id). emqx_authz_utils:remove_resource(Id).
authorize( authorize(
Client, Client,
@ -95,7 +94,7 @@ authorize(
case emqx_authz_utils:parse_http_resp_body(ContentType, Body) of case emqx_authz_utils:parse_http_resp_body(ContentType, Body) of
error -> error ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "authz_http_response_incorrect", msg => authz_http_response_incorrect,
content_type => ContentType, content_type => ContentType,
body => Body body => Body
}), }),
@ -119,11 +118,25 @@ authorize(
ignore ignore
end. end.
merge_defaults(#{<<"headers">> := Headers} = Source) ->
NewHeaders =
case Source of
#{<<"method">> := <<"get">>} ->
(emqx_authz_http_schema:headers_no_content_type(converter))(Headers);
#{<<"method">> := <<"post">>} ->
(emqx_authz_http_schema:headers(converter))(Headers);
_ ->
Headers
end,
Source#{<<"headers">> => NewHeaders};
merge_defaults(Source) ->
Source.
log_nomtach_msg(Status, Headers, Body) -> log_nomtach_msg(Status, Headers, Body) ->
?SLOG( ?SLOG(
debug, debug,
#{ #{
msg => "unexpected_authz_http_response", msg => unexpected_authz_http_response,
status => Status, status => Status,
content_type => emqx_authz_utils:content_type(Headers), content_type => emqx_authz_utils:content_type(Headers),
body => Body body => Body

View File

@ -0,0 +1,179 @@
%%--------------------------------------------------------------------
%% 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_authz_http_schema).
-include("emqx_auth_http.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-behaviour(emqx_authz_schema).
-export([
type/0,
fields/1,
desc/1,
source_refs/0,
select_union_member/1
]).
-export([
headers_no_content_type/1,
headers/1
]).
-define(NOT_EMPTY(MSG), emqx_resource_validator:not_empty(MSG)).
-import(emqx_schema, [mk_duration/2]).
type() -> ?AUTHZ_TYPE.
source_refs() ->
[?R_REF(http_get), ?R_REF(http_post)].
fields(http_get) ->
emqx_authz_schema:authz_common_fields(?AUTHZ_TYPE) ++
http_common_fields() ++
[
{method, method(get)},
{headers, fun headers_no_content_type/1}
];
fields(http_post) ->
emqx_authz_schema:authz_common_fields(?AUTHZ_TYPE) ++
http_common_fields() ++
[
{method, method(post)},
{headers, fun headers/1}
].
desc(http_get) ->
?DESC(http_get);
desc(http_post) ->
?DESC(http_post);
desc(_) ->
undefined.
select_union_member(#{<<"type">> := ?AUTHZ_TYPE_BIN} = Value) ->
Method = maps:get(<<"method">>, Value, undefined),
case Method of
<<"get">> ->
?R_REF(http_get);
<<"post">> ->
?R_REF(http_post);
Else ->
throw(#{
reason => "unknown_http_method",
expected => "get | post",
got => Else
})
end;
select_union_member(_Value) ->
undefined.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
method(Method) ->
?HOCON(Method, #{required => true, desc => ?DESC(method)}).
http_common_fields() ->
[
{url, fun url/1},
{request_timeout,
mk_duration("Request timeout", #{
required => false, default => <<"30s">>, desc => ?DESC(request_timeout)
})},
{body, ?HOCON(map(), #{required => false, desc => ?DESC(body)})}
] ++
lists:keydelete(
pool_type,
1,
emqx_bridge_http_connector:fields(config)
).
headers(type) ->
list({binary(), binary()});
headers(desc) ->
?DESC(?FUNCTION_NAME);
headers(converter) ->
fun(Headers) ->
maps:to_list(maps:merge(default_headers(), transform_header_name(Headers)))
end;
headers(default) ->
default_headers();
headers(_) ->
undefined.
headers_no_content_type(type) ->
list({binary(), binary()});
headers_no_content_type(desc) ->
?DESC(?FUNCTION_NAME);
headers_no_content_type(converter) ->
fun(Headers) ->
maps:to_list(
maps:without(
[<<"content-type">>],
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
)
)
end;
headers_no_content_type(default) ->
default_headers_no_content_type();
headers_no_content_type(validator) ->
fun(Headers) ->
case lists:keyfind(<<"content-type">>, 1, Headers) of
false -> ok;
_ -> {error, do_not_include_content_type}
end
end;
headers_no_content_type(_) ->
undefined.
url(type) -> binary();
url(desc) -> ?DESC(?FUNCTION_NAME);
url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")];
url(required) -> true;
url(_) -> undefined.
default_headers() ->
maps:put(
<<"content-type">>,
<<"application/json">>,
default_headers_no_content_type()
).
default_headers_no_content_type() ->
#{
<<"accept">> => <<"application/json">>,
<<"cache-control">> => <<"no-cache">>,
<<"connection">> => <<"keep-alive">>,
<<"keep-alive">> => <<"timeout=30, max=1000">>
}.
transform_header_name(Headers) ->
maps:fold(
fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc)
end,
#{},
Headers
).
to_list(A) when is_atom(A) ->
atom_to_list(A);
to_list(B) when is_binary(B) ->
binary_to_list(B).

View File

@ -19,7 +19,7 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-compile(export_all). -compile(export_all).
-include("emqx_authn.hrl"). -include_lib("emqx_auth/include/emqx_authn.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
@ -65,7 +65,7 @@ all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
Apps = emqx_cth_suite:start([cowboy, emqx, emqx_conf, emqx_authn], #{ Apps = emqx_cth_suite:start([cowboy, emqx, emqx_conf, emqx_auth, emqx_auth_http], #{
work_dir => ?config(priv_dir, Config) work_dir => ?config(priv_dir, Config)
}), }),
[{apps, Apps} | Config]. [{apps, Apps} | Config].
@ -102,7 +102,7 @@ t_create(_Config) ->
{create_authenticator, ?GLOBAL, AuthConfig} {create_authenticator, ?GLOBAL, AuthConfig}
), ),
{ok, [#{provider := emqx_authn_http}]} = emqx_authentication:list_authenticators(?GLOBAL). {ok, [#{provider := emqx_authn_http}]} = emqx_authn_chains:list_authenticators(?GLOBAL).
t_create_invalid(_Config) -> t_create_invalid(_Config) ->
AuthConfig = raw_http_auth_config(), AuthConfig = raw_http_auth_config(),
@ -128,7 +128,7 @@ t_create_invalid(_Config) ->
end, end,
?assertEqual( ?assertEqual(
{error, {not_found, {chain, ?GLOBAL}}}, {error, {not_found, {chain, ?GLOBAL}}},
emqx_authentication:list_authenticators(?GLOBAL) emqx_authn_chains:list_authenticators(?GLOBAL)
) )
end, end,
InvalidConfigs InvalidConfigs
@ -274,7 +274,7 @@ t_destroy(_Config) ->
), ),
{ok, [#{provider := emqx_authn_http, state := State}]} = {ok, [#{provider := emqx_authn_http, state := State}]} =
emqx_authentication:list_authenticators(?GLOBAL), emqx_authn_chains:list_authenticators(?GLOBAL),
Credentials = maps:with([username, password], ?CREDENTIALS), Credentials = maps:with([username, password], ?CREDENTIALS),

Some files were not shown because too many files have changed in this diff Show More