chore(auth): split emqx_authn and emqx_authz apps
This commit is contained in:
parent
e049dc0e76
commit
1eb75b43c4
|
@ -3,9 +3,9 @@
|
|||
|
||||
## apps
|
||||
/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_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_rbac/ @emqx/emqx-review-board @lafirest
|
||||
/apps/emqx_dashboard_sso/ @emqx/emqx-review-board @JimMoen @lafirest
|
||||
|
|
|
@ -132,7 +132,7 @@ jobs:
|
|||
-Dpgsql_user="root" \
|
||||
-Dpgsql_pwd="public" \
|
||||
-Ddbname="mqtt" \
|
||||
-Droute="apps/emqx_authn/test/data/certs" \
|
||||
-Droute="apps/emqx_auth/test/data/certs" \
|
||||
-Dca_name="ca.crt" \
|
||||
-Dkey_name="client.key" \
|
||||
-Dcert_name="client.crt" \
|
||||
|
@ -195,7 +195,7 @@ jobs:
|
|||
-Dmysql_user="root" \
|
||||
-Dmysql_pwd="public" \
|
||||
-Ddbname="mqtt" \
|
||||
-Droute="apps/emqx_authn/test/data/certs" \
|
||||
-Droute="apps/emqx_auth/test/data/certs" \
|
||||
-Dca_name="ca.crt" \
|
||||
-Dkey_name="client.key" \
|
||||
-Dcert_name="client.crt" \
|
||||
|
|
|
@ -37,25 +37,25 @@ format_path([Name | Rest]) -> [iol(Name), "." | format_path(Rest)].
|
|||
%% @doc Plain check the input config.
|
||||
%% The input can either be `richmap' or plain `map'.
|
||||
%% 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()}.
|
||||
check(SchemaModule, Conf) ->
|
||||
check(Schema, Conf) ->
|
||||
%% TODO: remove required
|
||||
%% fields should state required or not in their schema
|
||||
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
|
||||
{ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts)}
|
||||
{ok, hocon_tconf:check_plain(Schema, Conf, Opts)}
|
||||
catch
|
||||
throw:Errors:Stacktrace ->
|
||||
compact_errors(Errors, Stacktrace)
|
||||
end;
|
||||
check(SchemaModule, HoconText, Opts) ->
|
||||
check(Schema, HoconText, Opts) ->
|
||||
case hocon:binary(HoconText, #{format => map}) of
|
||||
{ok, MapConfig} ->
|
||||
check(SchemaModule, MapConfig, Opts);
|
||||
check(Schema, MapConfig, Opts);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
|
|
@ -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
|
||||
%% the full schema for EMQX node is injected in emqx_conf_schema.
|
||||
|
@ -225,7 +226,8 @@ roots(high) ->
|
|||
ref(?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME),
|
||||
#{importance => ?IMPORTANCE_HIDDEN}
|
||||
)}
|
||||
];
|
||||
]
|
||||
);
|
||||
roots(medium) ->
|
||||
[
|
||||
{"broker",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
-export([
|
||||
injection_point/1,
|
||||
injection_point/2,
|
||||
inject_from_modules/1
|
||||
]).
|
||||
|
||||
|
@ -43,9 +44,15 @@
|
|||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec injection_point(hookpoint()) -> [hocon_schema:field()].
|
||||
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() ->
|
||||
lists:foreach(
|
||||
fun
|
||||
|
@ -57,6 +64,7 @@ erase_injections() ->
|
|||
persistent_term:get()
|
||||
).
|
||||
|
||||
-spec any_injections() -> boolean().
|
||||
any_injections() ->
|
||||
lists:any(
|
||||
fun
|
||||
|
@ -68,6 +76,7 @@ any_injections() ->
|
|||
persistent_term:get()
|
||||
).
|
||||
|
||||
-spec inject_from_modules([module() | {module(), term()}]) -> ok.
|
||||
inject_from_modules(Modules) ->
|
||||
Injections =
|
||||
lists:foldl(
|
||||
|
|
|
@ -527,11 +527,11 @@ copy_certs(_, _) ->
|
|||
|
||||
copy_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} ->
|
||||
(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,
|
||||
ok.
|
||||
|
||||
|
|
|
@ -330,7 +330,7 @@ default_appspec(emqx, SuiteOpts) ->
|
|||
% overwrite everything with a default configuration.
|
||||
before_start => fun inhibit_config_loader/2
|
||||
};
|
||||
default_appspec(emqx_authz, _SuiteOpts) ->
|
||||
default_appspec(emqx_auth, _SuiteOpts) ->
|
||||
#{
|
||||
config => #{
|
||||
% NOTE
|
||||
|
@ -356,7 +356,7 @@ default_appspec(emqx_conf, SuiteOpts) ->
|
|||
Config,
|
||||
[
|
||||
emqx,
|
||||
emqx_authz
|
||||
emqx_auth
|
||||
]
|
||||
),
|
||||
#{
|
||||
|
|
|
@ -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.
|
|
@ -17,16 +17,12 @@
|
|||
-ifndef(EMQX_AUTHN_HRL).
|
||||
-define(EMQX_AUTHN_HRL, true).
|
||||
|
||||
-include_lib("emqx_authentication.hrl").
|
||||
-include("emqx_authn_chains.hrl").
|
||||
|
||||
-define(APP, emqx_authn).
|
||||
|
||||
-define(AUTHN, emqx_authentication).
|
||||
-define(AUTHN, emqx_authn_chains).
|
||||
|
||||
-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
|
||||
-define(CONF_NS, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME).
|
||||
-define(CONF_NS_ATOM, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
|
||||
|
@ -34,6 +30,6 @@
|
|||
|
||||
-type authenticator_id() :: binary().
|
||||
|
||||
-define(RESOURCE_GROUP, <<"emqx_authn">>).
|
||||
-define(AUTHN_RESOURCE_GROUP, <<"emqx_authn">>).
|
||||
|
||||
-endif.
|
|
@ -14,8 +14,8 @@
|
|||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-ifndef(EMQX_AUTHENTICATION_HRL).
|
||||
-define(EMQX_AUTHENTICATION_HRL, true).
|
||||
-ifndef(EMQX_AUTHN_CHAINS_HRL).
|
||||
-define(EMQX_AUTHN_CHAINS_HRL, true).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/emqx_access_control.hrl").
|
|
@ -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.
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
-include_lib("emqx/include/emqx_access_control.hrl").
|
||||
|
||||
-define(APP, emqx_authz).
|
||||
-include("emqx_auth.hrl").
|
||||
|
||||
%% authz_mnesia
|
||||
-define(ACL_TABLE, emqx_acl).
|
||||
|
@ -157,7 +157,7 @@
|
|||
count => 1
|
||||
}).
|
||||
|
||||
-define(RESOURCE_GROUP, <<"emqx_authz">>).
|
||||
-define(AUTHZ_RESOURCE_GROUP, <<"emqx_authz">>).
|
||||
|
||||
-define(AUTHZ_FEATURES, [rich_actions]).
|
||||
|
|
@ -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.
|
|
@ -2,12 +2,7 @@
|
|||
|
||||
{deps, [
|
||||
{emqx, {path, "../emqx"}},
|
||||
{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"}}
|
||||
{emqx_utils, {path, "../emqx_utils"}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
|
@ -34,6 +29,4 @@
|
|||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
||||
|
||||
{erl_first_files, ["src/emqx_authentication.erl"]}.
|
||||
|
||||
{project_plugins, [erlfmt]}.
|
|
@ -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/"}]}
|
||||
]}.
|
|
@ -14,7 +14,7 @@
|
|||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_authn_app).
|
||||
-module(emqx_auth_app).
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
|||
stop/1
|
||||
]).
|
||||
|
||||
-include_lib("emqx_authentication.hrl").
|
||||
-include_lib("emqx_authn_chains.hrl").
|
||||
|
||||
-dialyzer({nowarn_function, [start/2]}).
|
||||
|
||||
|
@ -37,56 +37,17 @@
|
|||
start(_StartType, _StartArgs) ->
|
||||
%% required by test cases, ensure the injection of schema
|
||||
_ = emqx_conf_schema:roots(),
|
||||
ok = mria_rlog:wait_for_shards([?AUTH_SHARD], infinity),
|
||||
{ok, Sup} = emqx_authn_sup:start_link(),
|
||||
case initialize() of
|
||||
ok -> {ok, Sup};
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
ok = emqx_authz:init(),
|
||||
{ok, Sup}.
|
||||
|
||||
stop(_State) ->
|
||||
ok = deinitialize().
|
||||
ok = deinitialize(),
|
||||
ok = emqx_authz:deinit().
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
initialize() ->
|
||||
ok = ?AUTHN:register_providers(emqx_authn:providers()),
|
||||
lists:foreach(
|
||||
fun({ChainName, AuthConfig}) ->
|
||||
?AUTHN:initialize_authentication(
|
||||
ChainName,
|
||||
AuthConfig
|
||||
)
|
||||
end,
|
||||
chain_configs()
|
||||
).
|
||||
|
||||
deinitialize() ->
|
||||
ok = ?AUTHN:deregister_providers(provider_types()),
|
||||
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()).
|
|
@ -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() -> [].
|
|
@ -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");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
|
@ -14,25 +14,18 @@
|
|||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_authentication_sup).
|
||||
-module(emqx_auth_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
-export([
|
||||
start_link/0,
|
||||
init/1
|
||||
]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Supervisor callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
SupFlags = #{
|
||||
strategy => one_for_one,
|
||||
|
@ -41,12 +34,21 @@ init([]) ->
|
|||
},
|
||||
|
||||
AuthN = #{
|
||||
id => emqx_authentication,
|
||||
start => {emqx_authentication, start_link, []},
|
||||
id => emqx_authn_sup,
|
||||
start => {emqx_authn_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 1000,
|
||||
type => worker,
|
||||
modules => [emqx_authentication]
|
||||
type => supervisor
|
||||
},
|
||||
|
||||
{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}}.
|
|
@ -19,95 +19,42 @@
|
|||
-behaviour(emqx_config_backup).
|
||||
|
||||
-export([
|
||||
providers/0,
|
||||
check_config/1,
|
||||
check_config/2,
|
||||
fill_defaults/1,
|
||||
%% 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]).
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
|
||||
providers() ->
|
||||
[
|
||||
{{password_based, built_in_database}, emqx_authn_mnesia},
|
||||
{{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().
|
||||
fill_defaults(Config) ->
|
||||
#{?CONF_NS_BINARY := WithDefaults} = do_fill_defaults(Config),
|
||||
WithDefaults.
|
||||
|
||||
check_config(Config) ->
|
||||
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) ->
|
||||
do_fill_defaults(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} ->
|
||||
Checked;
|
||||
{error, Reason} ->
|
||||
throw(Reason)
|
||||
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() ->
|
||||
#{
|
||||
authenticators => [authenticator_id()],
|
||||
overridden_listeners => #{authenticator_id() => pos_integer()}
|
||||
}.
|
||||
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
|
||||
%% error values.
|
||||
{ok, Chains} = emqx_authentication:list_chains(),
|
||||
{ok, Chains} = emqx_authn_chains:list_chains(),
|
||||
AuthnTypes = lists:usort([
|
||||
Type
|
||||
|| #{authenticators := As} <- Chains,
|
||||
|
@ -132,6 +79,12 @@ get_enabled_authns() ->
|
|||
tally_authenticators(#{id := AuthenticatorName}, 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
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -142,7 +95,7 @@ import_config(RawConf) ->
|
|||
AuthnList = authn_list(maps:get(?CONF_NS_BINARY, RawConf, [])),
|
||||
OldAuthnList = emqx:get_raw_config([?CONF_NS_BINARY], []),
|
||||
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
|
||||
{ok, #{raw_config := NewRawConf}} ->
|
||||
|
@ -152,9 +105,9 @@ import_config(RawConf) ->
|
|||
end.
|
||||
|
||||
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)),
|
||||
[[?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;
|
|
@ -30,7 +30,7 @@
|
|||
-define(NOT_FOUND, 'NOT_FOUND').
|
||||
-define(ALREADY_EXISTS, 'ALREADY_EXISTS').
|
||||
-define(INTERNAL_ERROR, 'INTERNAL_ERROR').
|
||||
-define(CONFIG, emqx_authentication_config).
|
||||
-define(CONFIG, emqx_authn_config).
|
||||
|
||||
% Swagger
|
||||
|
||||
|
@ -823,7 +823,7 @@ find_listener(ListenerID) ->
|
|||
end.
|
||||
|
||||
with_chain(ListenerID, Fun) ->
|
||||
{ok, ChainNames} = emqx_authentication:list_chain_names(),
|
||||
{ok, ChainNames} = emqx_authn_chains:list_chain_names(),
|
||||
ListenerChainName =
|
||||
[Name || Name <- ChainNames, atom_to_binary(Name) =:= ListenerID],
|
||||
case ListenerChainName of
|
||||
|
@ -852,7 +852,7 @@ list_authenticators(ConfKeyPath) ->
|
|||
NAuthenticators = [
|
||||
maps:put(
|
||||
id,
|
||||
emqx_authentication:authenticator_id(AuthenticatorConfig),
|
||||
emqx_authn_chains:authenticator_id(AuthenticatorConfig),
|
||||
convert_certs(AuthenticatorConfig)
|
||||
)
|
||||
|| AuthenticatorConfig <- AuthenticatorsConfig
|
||||
|
@ -868,33 +868,25 @@ list_authenticator(_, ConfKeyPath, AuthenticatorID) ->
|
|||
end
|
||||
).
|
||||
|
||||
resource_provider() ->
|
||||
[
|
||||
emqx_authn_mysql,
|
||||
emqx_authn_pgsql,
|
||||
emqx_authn_mongodb,
|
||||
emqx_authn_redis,
|
||||
emqx_authn_http
|
||||
] ++
|
||||
emqx_authn_enterprise:resource_provider().
|
||||
|
||||
%% TODO
|
||||
%% This breaks encapsulation, resource_id should be obtained
|
||||
%% through provider callback
|
||||
lookup_from_local_node(ChainName, AuthenticatorID) ->
|
||||
NodeId = node(self()),
|
||||
case emqx_authentication:lookup_authenticator(ChainName, AuthenticatorID) of
|
||||
{ok, #{provider := Provider, state := State}} ->
|
||||
MetricsId = emqx_authentication:metrics_id(ChainName, AuthenticatorID),
|
||||
case emqx_authn_chains:lookup_authenticator(ChainName, AuthenticatorID) of
|
||||
{ok, #{state := State}} ->
|
||||
MetricsId = emqx_authn_chains:metrics_id(ChainName, AuthenticatorID),
|
||||
Metrics = emqx_metrics_worker:get_metrics(authn_metrics, MetricsId),
|
||||
case lists:member(Provider, resource_provider()) of
|
||||
false ->
|
||||
{ok, {NodeId, connected, Metrics, #{}}};
|
||||
true ->
|
||||
#{resource_id := ResourceId} = State,
|
||||
case State of
|
||||
#{resource_id := ResourceId} ->
|
||||
case emqx_resource:get_instance(ResourceId) of
|
||||
{error, not_found} ->
|
||||
{error, {NodeId, not_found_resource}};
|
||||
{ok, _, #{status := Status}} ->
|
||||
{ok, {NodeId, Status, Metrics, emqx_resource:get_metrics(ResourceId)}}
|
||||
end
|
||||
end;
|
||||
_ ->
|
||||
{ok, {NodeId, connected, Metrics, #{}}}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, {NodeId, list_to_binary(io_lib:format("~p", [Reason]))}}
|
||||
|
@ -1064,7 +1056,7 @@ add_user(
|
|||
) ->
|
||||
IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false),
|
||||
case
|
||||
emqx_authentication:add_user(
|
||||
emqx_authn_chains:add_user(
|
||||
ChainName,
|
||||
AuthenticatorID,
|
||||
#{
|
||||
|
@ -1090,7 +1082,7 @@ update_user(ChainName, AuthenticatorID, UserID, UserInfo0) ->
|
|||
serialize_error({missing_parameter, password});
|
||||
false ->
|
||||
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} ->
|
||||
{200, User};
|
||||
{error, Reason} ->
|
||||
|
@ -1099,7 +1091,7 @@ update_user(ChainName, AuthenticatorID, UserID, UserInfo0) ->
|
|||
end.
|
||||
|
||||
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} ->
|
||||
{200, User};
|
||||
{error, Reason} ->
|
||||
|
@ -1107,7 +1099,7 @@ find_user(ChainName, AuthenticatorID, UserID) ->
|
|||
end.
|
||||
|
||||
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 ->
|
||||
{204};
|
||||
{error, Reason} ->
|
||||
|
@ -1115,7 +1107,7 @@ delete_user(ChainName, AuthenticatorID, UserID) ->
|
|||
end.
|
||||
|
||||
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} ->
|
||||
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||
{error, Reason} ->
|
||||
|
@ -1143,7 +1135,7 @@ find_config(AuthenticatorID, AuthenticatorsConfig) ->
|
|||
[
|
||||
AC
|
||||
|| AC <- ensure_list(AuthenticatorsConfig),
|
||||
AuthenticatorID =:= emqx_authentication:authenticator_id(AC)
|
||||
AuthenticatorID =:= emqx_authn_chains:authenticator_id(AC)
|
||||
],
|
||||
case MatchingACs of
|
||||
[] -> {error, {not_found, {authenticator, AuthenticatorID}}};
|
||||
|
@ -1153,17 +1145,19 @@ find_config(AuthenticatorID, AuthenticatorsConfig) ->
|
|||
fill_defaults(Configs) when is_list(Configs) ->
|
||||
lists:map(fun fill_defaults/1, Configs);
|
||||
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) ->
|
||||
case maps:find(<<"headers">>, Config) of
|
||||
{ok, Headers} ->
|
||||
NewHeaders =
|
||||
case Config of
|
||||
#{<<"method">> := <<"get">>} ->
|
||||
(emqx_authn_http:headers_no_content_type(converter))(Headers);
|
||||
emqx_authn_utils:convert_headers_no_content_type(Headers);
|
||||
#{<<"method">> := <<"post">>} ->
|
||||
(emqx_authn_http:headers(converter))(Headers);
|
||||
emqx_authn_utils:convert_headers(Headers);
|
||||
_ ->
|
||||
Headers
|
||||
end,
|
|
@ -18,11 +18,11 @@
|
|||
%% Authentication is a core functionality of MQTT,
|
||||
%% the 'emqx' APP provides APIs for other APPs to implement
|
||||
%% the authentication callbacks.
|
||||
-module(emqx_authentication).
|
||||
-module(emqx_authn_chains).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("emqx_authentication.hrl").
|
||||
-include("emqx_authn_chains.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/emqx_hooks.hrl").
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
|
@ -43,7 +43,8 @@
|
|||
|
||||
%% The authentication entrypoint.
|
||||
-export([
|
||||
authenticate/2
|
||||
authenticate/2,
|
||||
authenticate_deny/2
|
||||
]).
|
||||
|
||||
%% Authenticator manager process start/stop
|
||||
|
@ -55,7 +56,6 @@
|
|||
|
||||
%% Authenticator management APIs
|
||||
-export([
|
||||
initialize_authentication/2,
|
||||
register_provider/2,
|
||||
register_providers/1,
|
||||
deregister_provider/1,
|
||||
|
@ -89,6 +89,7 @@
|
|||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
handle_continue/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
@ -99,7 +100,8 @@
|
|||
-export_type([
|
||||
authenticator_id/0,
|
||||
position/0,
|
||||
chain_name/0
|
||||
chain_name/0,
|
||||
authn_type/0
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
|
@ -135,7 +137,7 @@ end).
|
|||
state := map()
|
||||
}.
|
||||
|
||||
-type config() :: emqx_authentication_config:config().
|
||||
-type config() :: emqx_authn_config:config().
|
||||
-type state() :: #{atom() => term()}.
|
||||
-type extra() :: #{
|
||||
is_superuser := boolean(),
|
||||
|
@ -146,85 +148,7 @@ end).
|
|||
atom() => term()
|
||||
}.
|
||||
|
||||
%% @doc check_config takes raw config from config file,
|
||||
%% 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
|
||||
]).
|
||||
-export_type([authenticator/0, config/0, state/0, extra/0, user_info/0]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Authenticate
|
||||
|
@ -244,6 +168,9 @@ authenticate(#{listener := Listener, protocol := Protocol} = Credential, AuthRes
|
|||
?TRACE_RESULT("authentication_result", AuthResult, no_chain)
|
||||
end.
|
||||
|
||||
authenticate_deny(_Credential, _AuthResult) ->
|
||||
?TRACE_RESULT("authentication_result", {ok, {error, not_authorized}}, not_initialized).
|
||||
|
||||
get_authenticators(Listener, Global) ->
|
||||
case ets:lookup(?CHAINS_TAB, Listener) of
|
||||
[#chain{name = Name, authenticators = Authenticators}] ->
|
||||
|
@ -274,35 +201,14 @@ get_providers() ->
|
|||
%% and maybe a 'backend' key.
|
||||
%% This function works with both parsed (atom keys) and raw (binary keys)
|
||||
%% configurations.
|
||||
-spec authenticator_id(config()) -> authenticator_id().
|
||||
authenticator_id(Config) ->
|
||||
emqx_authentication_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
|
||||
).
|
||||
emqx_authn_config:authenticator_id(Config).
|
||||
|
||||
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
|
||||
start_link() ->
|
||||
%% 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(),
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
|
@ -316,7 +222,7 @@ stop() ->
|
|||
%% For example, ``[{{'password_based', redis}, emqx_authn_redis}]''
|
||||
%% NOTE: Later registered provider may override earlier registered if they
|
||||
%% 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) ->
|
||||
call({register_providers, Providers}).
|
||||
|
||||
|
@ -437,10 +343,12 @@ list_users(ChainName, AuthenticatorID, FuzzyParams) ->
|
|||
|
||||
init(_Opts) ->
|
||||
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([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) ->
|
||||
reply(Providers, State);
|
||||
|
@ -458,7 +366,7 @@ handle_call(
|
|||
Reg0,
|
||||
Providers
|
||||
),
|
||||
reply(ok, State#{providers := Reg});
|
||||
reply(ok, State#{providers := Reg}, initialize_authentication);
|
||||
Clashes ->
|
||||
reply({error, {authentication_type_clash, Clashes}}, State)
|
||||
end;
|
||||
|
@ -523,6 +431,12 @@ handle_call(Req, _From, State) ->
|
|||
?SLOG(error, #{msg => "unexpected_call", call => Req}),
|
||||
{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) ->
|
||||
?SLOG(error, #{msg => "unexpected_cast", cast => Req}),
|
||||
{noreply, State}.
|
||||
|
@ -554,6 +468,71 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
%% 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) ->
|
||||
#chain{authenticators = Authenticators} = Chain,
|
||||
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, Continue) ->
|
||||
{reply, Reply, State, {continue, Continue}}.
|
||||
|
||||
save_chain(#chain{
|
||||
name = Name,
|
||||
authenticators = []
|
||||
|
@ -773,6 +755,12 @@ maybe_unhook(#{hooked := true} = State) ->
|
|||
maybe_unhook(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) ->
|
||||
Type = authn_type(Config),
|
||||
case maps:get(Type, Providers, undefined) of
|
||||
|
@ -945,6 +933,27 @@ insert_user_group(_Chain, Config) ->
|
|||
metrics_id(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(M) when M =:= #{} -> [];
|
||||
to_list(M) when is_map(M) -> [M];
|
|
@ -15,7 +15,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc Authenticator configuration management module.
|
||||
-module(emqx_authentication_config).
|
||||
-module(emqx_authn_config).
|
||||
|
||||
-behaviour(emqx_config_handler).
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
|||
-export_type([config/0]).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include("emqx_authentication.hrl").
|
||||
-include("emqx_authn_chains.hrl").
|
||||
|
||||
-type parsed_config() :: #{
|
||||
mechanism := atom(),
|
||||
|
@ -50,9 +50,9 @@
|
|||
-type raw_config() :: #{binary() => term()}.
|
||||
-type config() :: parsed_config() | raw_config().
|
||||
|
||||
-type authenticator_id() :: emqx_authentication:authenticator_id().
|
||||
-type position() :: emqx_authentication:position().
|
||||
-type chain_name() :: emqx_authentication:chain_name().
|
||||
-type authenticator_id() :: emqx_authn_chains:authenticator_id().
|
||||
-type position() :: emqx_authn_chains:position().
|
||||
-type chain_name() :: emqx_authn_chains:chain_name().
|
||||
-type update_request() ::
|
||||
{create_authenticator, chain_name(), map()}
|
||||
| {delete_authenticator, chain_name(), authenticator_id()}
|
||||
|
@ -164,7 +164,7 @@ do_post_config_update(
|
|||
_, {create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs
|
||||
) ->
|
||||
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(
|
||||
_,
|
||||
{delete_authenticator, ChainName, AuthenticatorID},
|
||||
|
@ -172,7 +172,7 @@ do_post_config_update(
|
|||
_OldConfig,
|
||||
_AppEnvs
|
||||
) ->
|
||||
emqx_authentication:delete_authenticator(ChainName, AuthenticatorID);
|
||||
emqx_authn_chains:delete_authenticator(ChainName, AuthenticatorID);
|
||||
do_post_config_update(
|
||||
_,
|
||||
{update_authenticator, ChainName, AuthenticatorID, Config},
|
||||
|
@ -184,7 +184,7 @@ do_post_config_update(
|
|||
{error, not_found} ->
|
||||
{error, {not_found, {authenticator, AuthenticatorID}}};
|
||||
NConfig ->
|
||||
emqx_authentication:update_authenticator(ChainName, AuthenticatorID, NConfig)
|
||||
emqx_authn_chains:update_authenticator(ChainName, AuthenticatorID, NConfig)
|
||||
end;
|
||||
do_post_config_update(
|
||||
_,
|
||||
|
@ -193,7 +193,7 @@ do_post_config_update(
|
|||
_OldConfig,
|
||||
_AppEnvs
|
||||
) ->
|
||||
emqx_authentication:move_authenticator(ChainName, AuthenticatorID, Position);
|
||||
emqx_authn_chains:move_authenticator(ChainName, AuthenticatorID, Position);
|
||||
do_post_config_update(_, _UpdateReq, OldConfig, OldConfig, _AppEnvs) ->
|
||||
ok;
|
||||
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),
|
||||
ok = delete_authenticators(NewIds, ChainName, OldConfig),
|
||||
ok = create_or_update_authenticators(OldIds, ChainName, NewConfig),
|
||||
ok = emqx_authentication:reorder_authenticator(ChainName, NewIds),
|
||||
ok = emqx_authn_chains:reorder_authenticator(ChainName, NewIds),
|
||||
ok.
|
||||
|
||||
%% @doc Handle listener config changes made at higher level.
|
||||
|
@ -228,9 +228,9 @@ create_or_update_authenticators(OldIds, ChainName, NewConfig) ->
|
|||
Id = authenticator_id(Conf),
|
||||
case lists:member(Id, OldIds) of
|
||||
true ->
|
||||
emqx_authentication:update_authenticator(ChainName, Id, Conf);
|
||||
emqx_authn_chains:update_authenticator(ChainName, Id, Conf);
|
||||
false ->
|
||||
emqx_authentication:create_authenticator(ChainName, Conf)
|
||||
emqx_authn_chains:create_authenticator(ChainName, Conf)
|
||||
end
|
||||
end,
|
||||
NewConfig
|
||||
|
@ -245,7 +245,7 @@ delete_authenticators(NewIds, ChainName, OldConfig) ->
|
|||
true ->
|
||||
ok;
|
||||
false ->
|
||||
emqx_authentication:delete_authenticator(ChainName, Id)
|
||||
emqx_authn_chains:delete_authenticator(ChainName, Id)
|
||||
end
|
||||
end,
|
||||
OldConfig
|
|
@ -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.
|
|
@ -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
|
||||
]).
|
|
@ -19,7 +19,8 @@
|
|||
-elvis([{elvis_style, invalid_dynamic_call, disable}]).
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-include("emqx_authn.hrl").
|
||||
-include("emqx_authentication.hrl").
|
||||
-include("emqx_authn_schema.hrl").
|
||||
-include("emqx_authn_chains.hrl").
|
||||
|
||||
-behaviour(emqx_schema_hooks).
|
||||
-export([
|
||||
|
@ -29,7 +30,7 @@
|
|||
-export([
|
||||
common_fields/0,
|
||||
roots/0,
|
||||
validations/0,
|
||||
% validations/0,
|
||||
tags/0,
|
||||
fields/1,
|
||||
authenticator_type/0,
|
||||
|
@ -38,6 +39,15 @@
|
|||
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() -> [].
|
||||
|
||||
injected_fields() ->
|
||||
|
@ -49,95 +59,53 @@ injected_fields() ->
|
|||
tags() ->
|
||||
[<<"Authentication">>].
|
||||
|
||||
common_fields() ->
|
||||
[{enable, fun enable/1}].
|
||||
|
||||
enable(type) -> boolean();
|
||||
enable(default) -> true;
|
||||
enable(desc) -> ?DESC(?FUNCTION_NAME);
|
||||
enable(_) -> undefined.
|
||||
|
||||
authenticator_type() ->
|
||||
hoconsc:union(union_member_selector(emqx_authn:providers())).
|
||||
hoconsc:union(union_member_selector(provider_schema_mods())).
|
||||
|
||||
authenticator_type_without_scram() ->
|
||||
Providers = lists:filtermap(
|
||||
fun
|
||||
({{scram, _Backend}, _Mod}) ->
|
||||
false;
|
||||
(_) ->
|
||||
true
|
||||
end,
|
||||
emqx_authn:providers()
|
||||
),
|
||||
hoconsc:union(union_member_selector(Providers)).
|
||||
hoconsc:union(
|
||||
union_member_selector(provider_schema_mods() -- [emqx_authn_scram_mnesia_schema])
|
||||
).
|
||||
|
||||
config_refs(Providers) ->
|
||||
lists:append([Module:refs() || {_, Module} <- Providers]).
|
||||
|
||||
union_member_selector(Providers) ->
|
||||
Types = config_refs(Providers),
|
||||
union_member_selector(Mods) ->
|
||||
AllTypes = config_refs(Mods),
|
||||
fun
|
||||
(all_union_members) -> Types;
|
||||
({value, Value}) -> select_union_member(Value, Providers)
|
||||
(all_union_members) -> AllTypes;
|
||||
({value, Value}) -> select_union_member(Value, Mods)
|
||||
end.
|
||||
|
||||
select_union_member(#{<<"mechanism">> := _} = Value, Providers0) ->
|
||||
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
|
||||
[] ->
|
||||
select_union_member(#{<<"mechanism">> := Mechanism, <<"backend">> := Backend}, []) ->
|
||||
throw(#{
|
||||
reason => "unsupported_mechanism",
|
||||
mechanism => MechanismVal,
|
||||
backend => BackendVal
|
||||
mechanism => Mechanism,
|
||||
backend => Backend
|
||||
});
|
||||
[{_, Module}] ->
|
||||
try_select_union_member(Module, Value)
|
||||
end
|
||||
select_union_member(#{<<"mechanism">> := Mechanism}, []) ->
|
||||
throw(#{
|
||||
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;
|
||||
select_union_member(Value, _Providers) when is_map(Value) ->
|
||||
select_union_member(#{} = _Value, _Mods) ->
|
||||
throw(#{reason => "missing_mechanism_field"});
|
||||
select_union_member(Value, _Providers) ->
|
||||
select_union_member(Value, _Mods) ->
|
||||
throw(#{reason => "not_a_struct", value => Value}).
|
||||
|
||||
try_select_union_member(Module, Value) ->
|
||||
%% some modules have `union_member_selector/1' exported to help selecting
|
||||
%% 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.
|
||||
config_refs(Mods) ->
|
||||
lists:append([Mod:refs() || Mod <- Mods]).
|
||||
|
||||
root_type() ->
|
||||
hoconsc:array(authenticator_type()).
|
||||
|
||||
global_auth_fields() ->
|
||||
[
|
||||
{?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM,
|
||||
{?CONF_NS_ATOM,
|
||||
hoconsc:mk(root_type(), #{
|
||||
desc => ?DESC(global_authentication),
|
||||
converter => fun ensure_array/2,
|
||||
|
@ -148,7 +116,7 @@ global_auth_fields() ->
|
|||
|
||||
mqtt_listener_auth_fields() ->
|
||||
[
|
||||
{?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM,
|
||||
{?CONF_NS_ATOM,
|
||||
hoconsc:mk(root_type(), #{
|
||||
desc => ?DESC(listener_authentication),
|
||||
converter => fun ensure_array/2,
|
||||
|
@ -177,6 +145,14 @@ backend(Name) ->
|
|||
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") ->
|
||||
[
|
||||
{"resource_metrics", ?HOCON(?R_REF("resource_metrics"), #{desc => ?DESC("metrics")})},
|
||||
|
@ -230,6 +206,9 @@ common_field() ->
|
|||
{"rate_last5m", ?HOCON(float(), #{desc => ?DESC("rate_last5m")})}
|
||||
].
|
||||
|
||||
provider_schema_mods() ->
|
||||
?PROVIDER_SCHEMA_MODS ++ emqx_authn_enterprise:provider_schema_mods().
|
||||
|
||||
status() ->
|
||||
hoconsc:enum([connected, disconnected, connecting]).
|
||||
|
||||
|
@ -244,27 +223,3 @@ array(Name) ->
|
|||
|
||||
array(Name, 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.
|
|
@ -27,15 +27,21 @@ start_link() ->
|
|||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
AuthNSup = #{
|
||||
id => emqx_authentication_sup,
|
||||
start => {emqx_authentication_sup, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => infinity,
|
||||
type => supervisor,
|
||||
modules => [emqx_authentication_sup]
|
||||
SupFlags = #{
|
||||
strategy => one_for_one,
|
||||
intensity => 100,
|
||||
period => 10
|
||||
},
|
||||
|
||||
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}}.
|
|
@ -92,7 +92,7 @@ authenticator_import_users(
|
|||
}
|
||||
) ->
|
||||
[{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};
|
||||
{error, Reason} -> emqx_authn_api:serialize_error(Reason)
|
||||
end;
|
||||
|
@ -110,9 +110,7 @@ listener_authenticator_import_users(
|
|||
emqx_authn_api:with_chain(
|
||||
ListenerID,
|
||||
fun(ChainName) ->
|
||||
case
|
||||
emqx_authentication:import_users(ChainName, AuthenticatorID, {FileName, FileData})
|
||||
of
|
||||
case emqx_authn_chains:import_users(ChainName, AuthenticatorID, {FileName, FileData}) of
|
||||
ok -> {204};
|
||||
{error, Reason} -> emqx_authn_api:serialize_error(Reason)
|
||||
end
|
|
@ -36,7 +36,12 @@
|
|||
cleanup_resources/0,
|
||||
make_resource_id/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, [
|
||||
|
@ -59,7 +64,7 @@
|
|||
create_resource(ResourceId, Module, Config) ->
|
||||
Result = emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?RESOURCE_GROUP,
|
||||
?AUTHN_RESOURCE_GROUP,
|
||||
Module,
|
||||
Config,
|
||||
?DEFAULT_RESOURCE_OPTS
|
||||
|
@ -163,7 +168,7 @@ bin(X) -> X.
|
|||
cleanup_resources() ->
|
||||
lists:foreach(
|
||||
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) ->
|
||||
|
@ -207,6 +212,49 @@ to_bool(MaybeBinInt) when is_binary(MaybeBinInt) ->
|
|||
to_bool(_) ->
|
||||
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
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -242,3 +290,20 @@ mapping_credential(C = #{cn := CN, dn := DN}) ->
|
|||
C#{cert_common_name => CN, cert_subject => DN};
|
||||
mapping_credential(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.
|
|
@ -27,9 +27,12 @@
|
|||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
-export([
|
||||
register_source/2,
|
||||
unregister_source/1,
|
||||
register_metrics/0,
|
||||
init/0,
|
||||
deinit/0,
|
||||
merge_defaults/1,
|
||||
lookup/0,
|
||||
lookup/1,
|
||||
move/2,
|
||||
|
@ -37,6 +40,7 @@
|
|||
merge/1,
|
||||
merge_local/2,
|
||||
authorize/5,
|
||||
authorize_deny/4,
|
||||
%% for telemetry information
|
||||
get_enabled_authzs/0
|
||||
]).
|
||||
|
@ -48,24 +52,26 @@
|
|||
|
||||
-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
|
||||
-export([
|
||||
import_config/1,
|
||||
maybe_read_acl_file/1,
|
||||
maybe_write_acl_file/1
|
||||
maybe_read_files/1,
|
||||
maybe_write_files/1
|
||||
]).
|
||||
|
||||
-type source() :: map().
|
||||
|
||||
-type match_result() :: {matched, allow} | {matched, deny} | nomatch.
|
||||
|
||||
-type default_result() :: allow | deny.
|
||||
|
||||
-type authz_result_value() :: #{result := allow | deny, from => _}.
|
||||
-type authz_result() :: {stop, authz_result_value()} | {ok, authz_result_value()} | ignore.
|
||||
|
||||
-type source() :: emqx_authz_source:source().
|
||||
-type sources() :: [source()].
|
||||
|
||||
-define(METRIC_SUPERUSER, 'authorization.superuser').
|
||||
|
@ -75,51 +81,70 @@
|
|||
|
||||
-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.
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?METRICS).
|
||||
|
||||
init() ->
|
||||
ok = register_metrics(),
|
||||
ok = init_metrics(client_info_source()),
|
||||
emqx_conf:add_handler(?CONF_KEY_PATH, ?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, []),
|
||||
ok = check_dup_types(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() ->
|
||||
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(?ROOT_KEY),
|
||||
emqx_authz_utils:cleanup_resources().
|
||||
|
@ -163,7 +188,14 @@ pre_config_update(Path, Cmd, Sources) ->
|
|||
{error, Reason} -> {error, Reason};
|
||||
NSources -> {ok, NSources}
|
||||
catch
|
||||
_:Reason -> {error, Reason}
|
||||
Error:Reason:Stack ->
|
||||
?SLOG(info, #{
|
||||
msg => "error_in_pre_config_update",
|
||||
exception => Error,
|
||||
reason => Reason,
|
||||
stacktrace => Stack
|
||||
}),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
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_move(Cmd, Sources);
|
||||
do_pre_config_update({?CMD_PREPEND, Source}, Sources) ->
|
||||
NSource = maybe_write_files(Source),
|
||||
NSource = maybe_write_source_files(Source),
|
||||
NSources = [NSource] ++ Sources,
|
||||
ok = check_dup_types(NSources),
|
||||
NSources;
|
||||
do_pre_config_update({?CMD_APPEND, Source}, Sources) ->
|
||||
NSource = maybe_write_files(Source),
|
||||
NSource = maybe_write_source_files(Source),
|
||||
NSources = Sources ++ [NSource],
|
||||
ok = check_dup_types(NSources),
|
||||
NSources;
|
||||
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),
|
||||
NSources = Front ++ [NSource | Rear],
|
||||
ok = check_dup_types(NSources),
|
||||
|
@ -212,7 +244,7 @@ do_pre_config_update({{?CMD_DELETE, Type}, _Source}, Sources) ->
|
|||
NSources;
|
||||
do_pre_config_update({?CMD_REPLACE, Sources}, _OldSources) ->
|
||||
%% 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),
|
||||
NSources;
|
||||
do_pre_config_update({Op, Source}, Sources) ->
|
||||
|
@ -356,6 +388,32 @@ init_metrics(Source) ->
|
|||
%% 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
|
||||
-spec authorize(
|
||||
emqx_types:clientinfo(),
|
||||
|
@ -502,29 +560,23 @@ changed_paths(OldSources, NewSources) ->
|
|||
Changed = maps:get(changed, emqx_utils:diff_lists(NewSources, OldSources, fun type/1)),
|
||||
[?CONF_KEY_PATH ++ [type(OldSource)] || {OldSource, _} <- Changed].
|
||||
|
||||
maybe_read_acl_file(RawConf) ->
|
||||
maybe_convert_acl_file(RawConf, fun read_acl_file/1).
|
||||
maybe_read_files(RawConf) ->
|
||||
maybe_convert_sources(RawConf, fun maybe_read_source_files/1).
|
||||
|
||||
maybe_write_acl_file(RawConf) ->
|
||||
maybe_convert_acl_file(RawConf, fun write_acl_file/1).
|
||||
maybe_write_files(RawConf) ->
|
||||
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
|
||||
) ->
|
||||
Sources1 = lists:map(
|
||||
fun
|
||||
(#{<<"type">> := <<"file">>} = FileSource) -> Fun(FileSource);
|
||||
(Source) -> Source
|
||||
end,
|
||||
Sources
|
||||
),
|
||||
Sources1 = lists:map(Fun, Sources),
|
||||
RawConf#{?CONF_NS_BINARY => AuthRawConf#{<<"sources">> => Sources1}};
|
||||
maybe_convert_acl_file(RawConf, _Fun) ->
|
||||
maybe_convert_sources(RawConf, _Fun) ->
|
||||
RawConf.
|
||||
|
||||
read_acl_file(#{<<"path">> := Path} = Source) ->
|
||||
{ok, Rules} = emqx_authz_file:read_file(Path),
|
||||
maps:remove(<<"path">>, Source#{<<"rules">> => Rules}).
|
||||
% read_acl_file(#{<<"path">> := Path} = Source) ->
|
||||
% {ok, Rules} = emqx_authz_file:read_file(Path),
|
||||
% maps:remove(<<"path">>, Source#{<<"rules">> => Rules}).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Extended Features
|
||||
|
@ -566,59 +618,79 @@ take(Type, Sources) ->
|
|||
end.
|
||||
|
||||
find_action_in_hooks() ->
|
||||
Callbacks = emqx_hooks:lookup('client.authorize'),
|
||||
[Action] = [Action || {callback, {?MODULE, authorize, _} = Action, _, _} <- Callbacks],
|
||||
Action.
|
||||
|
||||
authz_module(built_in_database) ->
|
||||
emqx_authz_mnesia;
|
||||
authz_module(Type) ->
|
||||
case emqx_authz_enterprise:is_enterprise_module(Type) of
|
||||
{ok, Module} ->
|
||||
Module;
|
||||
_ ->
|
||||
list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type))
|
||||
Actions = lists:filtermap(
|
||||
fun(Callback) ->
|
||||
case emqx_hooks:callback_action(Callback) of
|
||||
{?MODULE, authorize, _} = Action -> {true, Action};
|
||||
_ -> false
|
||||
end
|
||||
end,
|
||||
emqx_hooks:lookup('client.authorize')
|
||||
),
|
||||
case Actions of
|
||||
[] ->
|
||||
?SLOG(error, #{
|
||||
msg => "authz_not_initialized",
|
||||
configured_types => configured_types(),
|
||||
registered_types => emqx_authz_source_registry:get()
|
||||
}),
|
||||
error(authz_not_initialized);
|
||||
[Action] ->
|
||||
Action
|
||||
end.
|
||||
|
||||
type(#{type := Type}) -> type(Type);
|
||||
type(#{<<"type">> := Type}) -> type(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).
|
||||
authz_module(Type) ->
|
||||
emqx_authz_source_registry:module(Type).
|
||||
|
||||
maybe_write_files(#{<<"type">> := <<"file">>} = Source) ->
|
||||
write_acl_file(Source);
|
||||
maybe_write_files(NewSource) ->
|
||||
maybe_write_certs(NewSource).
|
||||
type(#{type := Type}) ->
|
||||
type(Type);
|
||||
type(#{<<"type">> := Type}) ->
|
||||
type(Type);
|
||||
type(Type) when is_atom(Type) orelse is_binary(Type) ->
|
||||
emqx_authz_source_registry:get(Type).
|
||||
|
||||
write_acl_file(#{<<"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);
|
||||
write_acl_file(Source) ->
|
||||
Source.
|
||||
merge_defaults(Source) ->
|
||||
Type = type(Source),
|
||||
Mod = authz_module(Type),
|
||||
try
|
||||
Mod:merge_defaults(Source)
|
||||
catch
|
||||
error:undef ->
|
||||
Source
|
||||
end.
|
||||
|
||||
%% @doc where the acl.conf file is stored.
|
||||
acl_conf_file() ->
|
||||
filename:join([emqx:data_dir(), "authz", "acl.conf"]).
|
||||
maybe_write_source_files(Source) ->
|
||||
Module = authz_module(type(Source)),
|
||||
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) ->
|
||||
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) ->
|
||||
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) ->
|
||||
filename:join(["authz", Type]).
|
||||
|
||||
|
@ -652,18 +714,6 @@ get_source_by_type(Type, Sources) ->
|
|||
update_authz_chain(Actions) ->
|
||||
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) ->
|
||||
{OriginSource, NewSources} =
|
||||
lists:foldl(
|
|
@ -20,8 +20,6 @@
|
|||
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
|
||||
-import(hoconsc, [mk/1, ref/2]).
|
||||
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
|
@ -22,13 +22,11 @@
|
|||
-include_lib("emqx/include/logger.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(NOT_FOUND, 'NOT_FOUND').
|
||||
|
||||
-define(API_SCHEMA_MODULE, emqx_authz_api_schema).
|
||||
|
||||
-export([
|
||||
get_raw_sources/0,
|
||||
get_raw_source/1,
|
||||
|
@ -65,7 +63,19 @@ paths() ->
|
|||
].
|
||||
|
||||
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
|
||||
|
@ -87,7 +97,7 @@ schema("/authorization/sources") ->
|
|||
description => ?DESC(authorization_sources_post),
|
||||
tags => ?TAGS,
|
||||
'requestBody' => mk(
|
||||
hoconsc:union(authz_sources_type_refs()),
|
||||
emqx_authz_schema:api_source_type(),
|
||||
#{desc => ?DESC(source_config)}
|
||||
),
|
||||
responses =>
|
||||
|
@ -111,7 +121,7 @@ schema("/authorization/sources/:type") ->
|
|||
responses =>
|
||||
#{
|
||||
200 => mk(
|
||||
hoconsc:union(authz_sources_type_refs()),
|
||||
emqx_authz_schema:api_source_type(),
|
||||
#{desc => ?DESC(source)}
|
||||
),
|
||||
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
|
||||
|
@ -122,7 +132,7 @@ schema("/authorization/sources/:type") ->
|
|||
description => ?DESC(authorization_sources_type_put),
|
||||
tags => ?TAGS,
|
||||
parameters => parameters_field(),
|
||||
'requestBody' => mk(hoconsc:union(authz_sources_type_refs())),
|
||||
'requestBody' => mk(emqx_authz_schema:api_source_type()),
|
||||
responses =>
|
||||
#{
|
||||
204 => <<"Authorization source updated successfully">>,
|
||||
|
@ -172,7 +182,7 @@ schema("/authorization/sources/:type/move") ->
|
|||
parameters => parameters_field(),
|
||||
'requestBody' =>
|
||||
emqx_dashboard_swagger:schema_with_examples(
|
||||
ref(?API_SCHEMA_MODULE, position),
|
||||
ref(?MODULE, position),
|
||||
position_example()
|
||||
),
|
||||
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(get, _) ->
|
||||
Sources = lists:foldl(
|
||||
fun
|
||||
(
|
||||
#{
|
||||
<<"type">> := <<"file">>,
|
||||
<<"enable">> := Enable,
|
||||
<<"path">> := Path
|
||||
},
|
||||
AccIn
|
||||
) ->
|
||||
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])
|
||||
fun(Source0, AccIn) ->
|
||||
try emqx_authz:maybe_read_source_files(Source0) of
|
||||
Source1 ->
|
||||
lists:append(AccIn, [Source1])
|
||||
catch
|
||||
_Error:_Reason ->
|
||||
lists:append(AccIn, [Source0])
|
||||
end
|
||||
end,
|
||||
[],
|
||||
get_raw_sources()
|
||||
|
@ -240,23 +229,17 @@ source(Method, #{bindings := #{type := Type} = Bindings} = Req) when
|
|||
source(get, #{bindings := #{type := Type}}) ->
|
||||
with_source(
|
||||
Type,
|
||||
fun
|
||||
(#{<<"type">> := <<"file">>, <<"enable">> := Enable, <<"path">> := Path}) ->
|
||||
case emqx_authz_file:read_file(Path) of
|
||||
{ok, Rules} ->
|
||||
{200, #{
|
||||
type => file,
|
||||
enable => Enable,
|
||||
rules => Rules
|
||||
}};
|
||||
{error, Reason} ->
|
||||
fun(Source0) ->
|
||||
try emqx_authz:maybe_read_source_files(Source0) of
|
||||
Source1 ->
|
||||
{200, Source1}
|
||||
catch
|
||||
_Error:Reason ->
|
||||
{500, #{
|
||||
code => <<"INTERNAL_ERROR">>,
|
||||
message => bin(Reason)
|
||||
}}
|
||||
end;
|
||||
(Source) ->
|
||||
{200, Source}
|
||||
end
|
||||
end
|
||||
);
|
||||
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()),
|
||||
Conf = #{<<"sources">> => RawSources},
|
||||
#{<<"sources">> := Sources} = hocon_tconf:make_serializable(Schema, Conf, #{}),
|
||||
merge_default_headers(Sources).
|
||||
merge_defaults(Sources).
|
||||
|
||||
merge_default_headers(Sources) ->
|
||||
lists:map(
|
||||
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
|
||||
).
|
||||
merge_defaults(Sources) ->
|
||||
lists:map(fun emqx_authz:merge_defaults/1, Sources).
|
||||
|
||||
get_raw_source(Type) ->
|
||||
lists:filter(
|
||||
|
@ -546,7 +510,7 @@ parameters_field() ->
|
|||
[
|
||||
{type,
|
||||
mk(
|
||||
enum(?API_SCHEMA_MODULE:authz_sources_types(simple)),
|
||||
enum(emqx_authz_schema:source_types()),
|
||||
#{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])).
|
||||
|
||||
status_metrics_example() ->
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-behaviour(emqx_authz).
|
||||
-behaviour(emqx_authz_source).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
|
@ -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.
|
|
@ -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"})}.
|
|
@ -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
|
||||
]).
|
|
@ -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.
|
|
@ -24,6 +24,7 @@
|
|||
create_resource/2,
|
||||
create_resource/3,
|
||||
update_resource/2,
|
||||
remove_resource/1,
|
||||
update_config/2,
|
||||
parse_deep/2,
|
||||
parse_str/2,
|
||||
|
@ -59,7 +60,7 @@ create_resource(Module, Config) ->
|
|||
create_resource(ResourceId, Module, Config) ->
|
||||
Result = emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?RESOURCE_GROUP,
|
||||
?AUTHZ_RESOURCE_GROUP,
|
||||
Module,
|
||||
Config,
|
||||
?DEFAULT_RESOURCE_OPTS
|
||||
|
@ -81,6 +82,9 @@ update_resource(Module, #{annotations := #{id := ResourceId}} = Config) ->
|
|||
end,
|
||||
start_resource_if_enabled(Result, ResourceId, Config).
|
||||
|
||||
remove_resource(ResourceId) ->
|
||||
emqx_resource:remove_local(ResourceId).
|
||||
|
||||
start_resource_if_enabled({ok, _} = Result, ResourceId, #{enable := true}) ->
|
||||
_ = emqx_resource:start(ResourceId),
|
||||
Result;
|
||||
|
@ -90,7 +94,7 @@ start_resource_if_enabled(Result, _ResourceId, _Config) ->
|
|||
cleanup_resources() ->
|
||||
lists:foreach(
|
||||
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) ->
|
|
@ -53,9 +53,36 @@ end_per_testcase(Case, Config) ->
|
|||
%% 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}) ->
|
||||
emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]),
|
||||
mria:clear_table(emqx_authn_mnesia),
|
||||
emqx_common_test_helpers:start_apps([emqx_conf, emqx_auth, emqx_auth_file]),
|
||||
emqx_authn_test_lib:register_fake_providers([{password_based, built_in_database}]),
|
||||
AuthnConfig = #{
|
||||
<<"mechanism">> => <<"password_based">>,
|
||||
<<"backend">> => <<"built_in_database">>,
|
||||
|
@ -68,7 +95,7 @@ t_will_message_connection_denied({init, Config}) ->
|
|||
),
|
||||
User = #{user_id => <<"subscriber">>, password => <<"p">>},
|
||||
AuthenticatorID = <<"password_based:built_in_database">>,
|
||||
{ok, _} = emqx_authentication:add_user(
|
||||
{ok, _} = emqx_authn_chains:add_user(
|
||||
Chain,
|
||||
AuthenticatorID,
|
||||
User
|
||||
|
@ -79,10 +106,11 @@ t_will_message_connection_denied({'end', _Config}) ->
|
|||
[authentication],
|
||||
{delete_authenticator, 'mqtt:global', <<"password_based:built_in_database">>}
|
||||
),
|
||||
emqx_common_test_helpers:stop_apps([emqx_authn, emqx_conf]),
|
||||
mria:clear_table(emqx_authn_mnesia),
|
||||
emqx_common_test_helpers:stop_apps([emqx_auth_file, emqx_auth, emqx_conf]),
|
||||
ok;
|
||||
t_will_message_connection_denied(Config) when is_list(Config) ->
|
||||
process_flag(trap_exit, true),
|
||||
|
||||
{ok, Subscriber} = emqtt:start_link([
|
||||
{clientid, <<"subscriber">>},
|
||||
{password, <<"p">>}
|
||||
|
@ -90,8 +118,6 @@ t_will_message_connection_denied(Config) when is_list(Config) ->
|
|||
{ok, _} = emqtt:connect(Subscriber),
|
||||
{ok, _, [?RC_SUCCESS]} = emqtt:subscribe(Subscriber, <<"lwt">>),
|
||||
|
||||
process_flag(trap_exit, true),
|
||||
|
||||
{ok, Publisher} = emqtt:start_link([
|
||||
{clientid, <<"publisher">>},
|
||||
{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,
|
||||
%% expect CONNACK with reason_code=5 and socket close
|
||||
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 = #{
|
||||
<<"mechanism">> => <<"password_based">>,
|
||||
<<"backend">> => <<"built_in_database">>,
|
||||
|
@ -137,7 +166,7 @@ t_password_undefined({'end', _Config}) ->
|
|||
[authentication],
|
||||
{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;
|
||||
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>>,
|
||||
|
@ -168,45 +197,18 @@ t_password_undefined(Config) when is_list(Config) ->
|
|||
end,
|
||||
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}) ->
|
||||
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], []),
|
||||
Config;
|
||||
t_update_conf({'end', _Config}) ->
|
||||
{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;
|
||||
t_update_conf(Config) when is_list(Config) ->
|
||||
Authn1 = #{
|
||||
|
@ -242,11 +244,11 @@ t_update_conf(Config) when is_list(Config) ->
|
|||
#{
|
||||
enable := true,
|
||||
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]),
|
||||
|
@ -256,21 +258,21 @@ t_update_conf(Config) when is_list(Config) ->
|
|||
#{
|
||||
enable := true,
|
||||
id := <<"password_based:built_in_database">>,
|
||||
provider := emqx_authn_mnesia
|
||||
provider := emqx_authn_fake_provider
|
||||
},
|
||||
#{
|
||||
enable := true,
|
||||
id := <<"password_based:http">>,
|
||||
provider := emqx_authn_http
|
||||
provider := emqx_authn_fake_provider
|
||||
},
|
||||
#{
|
||||
enable := true,
|
||||
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]),
|
||||
?assertMatch(
|
||||
|
@ -279,16 +281,16 @@ t_update_conf(Config) when is_list(Config) ->
|
|||
#{
|
||||
enable := true,
|
||||
id := <<"password_based:http">>,
|
||||
provider := emqx_authn_http
|
||||
provider := emqx_authn_fake_provider
|
||||
},
|
||||
#{
|
||||
enable := true,
|
||||
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]),
|
||||
|
@ -298,26 +300,26 @@ t_update_conf(Config) when is_list(Config) ->
|
|||
#{
|
||||
enable := true,
|
||||
id := <<"jwt">>,
|
||||
provider := emqx_authn_jwt
|
||||
provider := emqx_authn_fake_provider
|
||||
},
|
||||
#{
|
||||
enable := true,
|
||||
id := <<"password_based:http">>,
|
||||
provider := emqx_authn_http
|
||||
provider := emqx_authn_fake_provider
|
||||
},
|
||||
#{
|
||||
enable := true,
|
||||
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], []),
|
||||
?assertMatch(
|
||||
{error, {not_found, {chain, Chain}}},
|
||||
emqx_authentication:lookup_chain(Chain)
|
||||
emqx_authn_chains:lookup_chain(Chain)
|
||||
),
|
||||
ok.
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
-compile(nowarn_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]).
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
|
@ -53,8 +52,6 @@ init_per_testcase(_Case, Config) ->
|
|||
[listeners, tcp, default, ?CONF_NS_ATOM],
|
||||
?TCP_DEFAULT
|
||||
),
|
||||
|
||||
{atomic, ok} = mria:clear_table(emqx_authn_mnesia),
|
||||
Config.
|
||||
|
||||
end_per_testcase(t_authenticator_fail, Config) ->
|
||||
|
@ -68,7 +65,7 @@ init_per_suite(Config) ->
|
|||
[
|
||||
emqx,
|
||||
emqx_conf,
|
||||
emqx_authn,
|
||||
emqx_auth,
|
||||
emqx_management,
|
||||
{emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"}
|
||||
],
|
||||
|
@ -77,6 +74,12 @@ init_per_suite(Config) ->
|
|||
}
|
||||
),
|
||||
_ = 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),
|
||||
{ok, Chains} = ?AUTHN:list_chains(),
|
||||
?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(_) ->
|
||||
test_authenticator_position([]).
|
||||
|
||||
t_authenticator_import_users(_) ->
|
||||
test_authenticator_import_users([]).
|
||||
|
||||
%t_listener_authenticators(_) ->
|
||||
% test_authenticators(["listeners", ?TCP_DEFAULT]).
|
||||
|
||||
%t_listener_authenticator(_) ->
|
||||
% 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(_) ->
|
||||
% test_authenticator_position(["listeners", ?TCP_DEFAULT]).
|
||||
|
||||
%t_listener_authenticator_import_users(_) ->
|
||||
% test_authenticator_import_users(["listeners", ?TCP_DEFAULT]).
|
||||
|
||||
t_aggregate_metrics(_) ->
|
||||
Metrics = #{
|
||||
'emqx@node1.emqx.io' => #{
|
||||
|
@ -329,218 +314,6 @@ test_authenticator(PathPrefix) ->
|
|||
|
||||
?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) ->
|
||||
AuthenticatorConfs = [
|
||||
emqx_authn_test_lib:http_example(),
|
||||
|
@ -660,37 +433,6 @@ test_authenticator_position(PathPrefix) ->
|
|||
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
|
||||
%% Don't support listener switch to global chain.
|
||||
ignore_switch_to_global_chain(_) ->
|
|
@ -14,10 +14,10 @@
|
|||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_authentication_SUITE).
|
||||
-module(emqx_authn_chains_SUITE).
|
||||
|
||||
-behaviour(hocon_schema).
|
||||
-behaviour(emqx_authentication).
|
||||
-behaviour(emqx_authn_provider).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
@ -26,9 +26,9 @@
|
|||
|
||||
-include_lib("common_test/include/ct.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),
|
||||
(fun() ->
|
||||
{KEY, _V_} = lists:keyfind(KEY, 1, Config),
|
||||
|
@ -98,7 +98,7 @@ init_per_suite(Config) ->
|
|||
[
|
||||
emqx,
|
||||
emqx_conf,
|
||||
emqx_authn
|
||||
emqx_auth
|
||||
],
|
||||
#{work_dir => ?config(priv_dir)}
|
||||
),
|
||||
|
@ -295,9 +295,9 @@ t_update_config({init, Config}) ->
|
|||
| 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(
|
||||
[listeners, '?', '?', ?CONF_ROOT], emqx_authentication_config
|
||||
[listeners, '?', '?', ?CONF_ROOT], emqx_authn_config
|
||||
),
|
||||
ok = register_provider(?config("auth1"), ?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()),
|
||||
|
||||
ok = supervisor:terminate_child(emqx_authentication_sup, ?AUTHN),
|
||||
{ok, _} = supervisor:restart_child(emqx_authentication_sup, ?AUTHN),
|
||||
ok = supervisor:terminate_child(emqx_authn_sup, ?AUTHN),
|
||||
{ok, _} = supervisor:restart_child(emqx_authn_sup, ?AUTHN),
|
||||
|
||||
?assertEqual({ok, [test_chain]}, ?AUTHN:list_chain_names());
|
||||
t_restart({'end', _Config}) ->
|
||||
|
@ -493,7 +493,7 @@ t_combine_authn_and_callback(Config) when is_list(Config) ->
|
|||
password => <<"any">>
|
||||
},
|
||||
|
||||
%% no emqx_authentication authenticators, anonymous is allowed
|
||||
%% no emqx_authn_chains authenticators, anonymous is allowed
|
||||
?assertAuthSuccessForUser(bad),
|
||||
|
||||
AuthNType = ?config(authn_type),
|
||||
|
@ -506,7 +506,7 @@ t_combine_authn_and_callback(Config) when is_list(Config) ->
|
|||
},
|
||||
{ok, _} = ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig),
|
||||
|
||||
%% emqx_authentication alone
|
||||
%% emqx_authn_chains alone
|
||||
?assertAuthSuccessForUser(good),
|
||||
?assertAuthFailureForUser(ignore),
|
||||
?assertAuthFailureForUser(bad),
|
||||
|
@ -520,12 +520,12 @@ t_combine_authn_and_callback(Config) when is_list(Config) ->
|
|||
?assertAuthFailureForUser(bad),
|
||||
|
||||
%% 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_bad),
|
||||
|
||||
%% 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),
|
||||
?assertAuthFailureForUser(hook_user_finally_bad),
|
||||
|
||||
|
@ -539,13 +539,13 @@ t_combine_authn_and_callback(Config) when is_list(Config) ->
|
|||
?assertAuthFailureForUser(bad),
|
||||
?assertAuthFailureForUser(ignore),
|
||||
|
||||
%% lower-priority hook can overrride emqx_authentication result
|
||||
%% lower-priority hook can overrride emqx_authn_chains result
|
||||
%% for ignored users
|
||||
?assertAuthSuccessForUser(emqx_authn_ignore_for_hook_good),
|
||||
?assertAuthFailureForUser(emqx_authn_ignore_for_hook_bad),
|
||||
|
||||
%% 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_bad),
|
||||
?assertAuthFailureForUser(hook_user_good),
|
|
@ -30,9 +30,10 @@ all() ->
|
|||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
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)
|
||||
}),
|
||||
ok = emqx_authn_test_lib:register_fake_providers([{password_based, built_in_database}]),
|
||||
[{apps, Apps} | Config].
|
||||
|
||||
end_per_suite(Config) ->
|
|
@ -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.
|
|
@ -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)
|
||||
).
|
|
@ -28,9 +28,13 @@ all() ->
|
|||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
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)
|
||||
}),
|
||||
ok = emqx_authn_test_lib:register_fake_providers([
|
||||
{password_based, built_in_database},
|
||||
{password_based, redis}
|
||||
]),
|
||||
[{apps, Apps} | Config].
|
||||
|
||||
end_per_suite(Config) ->
|
||||
|
@ -70,14 +74,14 @@ t_create_update_delete(Config) ->
|
|||
#{
|
||||
id := <<"password_based:built_in_database">>,
|
||||
state := #{
|
||||
user_id_type := clientid
|
||||
config := #{user_id_type := clientid}
|
||||
}
|
||||
}
|
||||
],
|
||||
name := 'tcp:listener0'
|
||||
}
|
||||
]},
|
||||
emqx_authentication:list_chains()
|
||||
emqx_authn_chains:list_chains()
|
||||
),
|
||||
|
||||
%% Drop old, create new
|
||||
|
@ -99,14 +103,14 @@ t_create_update_delete(Config) ->
|
|||
#{
|
||||
id := <<"password_based:built_in_database">>,
|
||||
state := #{
|
||||
user_id_type := clientid
|
||||
config := #{user_id_type := clientid}
|
||||
}
|
||||
}
|
||||
],
|
||||
name := 'tcp:listener1'
|
||||
}
|
||||
]},
|
||||
emqx_authentication:list_chains()
|
||||
emqx_authn_chains:list_chains()
|
||||
),
|
||||
|
||||
%% Update
|
||||
|
@ -128,14 +132,14 @@ t_create_update_delete(Config) ->
|
|||
#{
|
||||
id := <<"password_based:built_in_database">>,
|
||||
state := #{
|
||||
user_id_type := username
|
||||
config := #{user_id_type := username}
|
||||
}
|
||||
}
|
||||
],
|
||||
name := 'tcp:listener1'
|
||||
}
|
||||
]},
|
||||
emqx_authentication:list_chains()
|
||||
emqx_authn_chains:list_chains()
|
||||
),
|
||||
|
||||
%% Update by listener path
|
||||
|
@ -153,14 +157,14 @@ t_create_update_delete(Config) ->
|
|||
#{
|
||||
id := <<"password_based:built_in_database">>,
|
||||
state := #{
|
||||
user_id_type := clientid
|
||||
config := #{user_id_type := clientid}
|
||||
}
|
||||
}
|
||||
],
|
||||
name := 'tcp:listener1'
|
||||
}
|
||||
]},
|
||||
emqx_authentication:list_chains()
|
||||
emqx_authn_chains:list_chains()
|
||||
),
|
||||
|
||||
%% Delete
|
||||
|
@ -170,7 +174,7 @@ t_create_update_delete(Config) ->
|
|||
),
|
||||
?assertMatch(
|
||||
{ok, []},
|
||||
emqx_authentication:list_chains()
|
||||
emqx_authn_chains:list_chains()
|
||||
).
|
||||
|
||||
t_convert_certs(Config) ->
|
||||
|
@ -236,7 +240,7 @@ listener_mqtt_tcp_conf(Config) ->
|
|||
}.
|
||||
|
||||
some_pem() ->
|
||||
Dir = code:lib_dir(emqx_authn, test),
|
||||
Dir = code:lib_dir(emqx_auth, test),
|
||||
Path = filename:join([Dir, "data", "private_key.pem"]),
|
||||
{ok, Pem} = file:read_file(Path),
|
||||
Pem.
|
|
@ -12,7 +12,7 @@ all() ->
|
|||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
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)
|
||||
}),
|
||||
[{apps, Apps} | Config].
|
||||
|
@ -54,7 +54,7 @@ t_check_schema(_Config) ->
|
|||
?assertThrow(
|
||||
#{
|
||||
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
|
||||
},
|
||||
Check(ConfigNotOk)
|
||||
|
@ -73,7 +73,7 @@ t_check_schema(_Config) ->
|
|||
#{
|
||||
path := "authentication.1.password_hash_algorithm",
|
||||
reason := "algorithm_name_missing",
|
||||
matched_type := "authn:builtin_db"
|
||||
matched_type := "builtin_db"
|
||||
},
|
||||
Check(ConfigMissingAlgoName)
|
||||
).
|
||||
|
@ -107,7 +107,8 @@ t_union_member_selector(_) ->
|
|||
BadBackend = Base#{<<"mechanism">> => <<"password_based">>, <<"backend">> => <<"bar">>},
|
||||
?assertThrow(
|
||||
#{
|
||||
reason := "unknown_backend",
|
||||
reason := "unsupported_mechanism",
|
||||
mechanism := <<"password_based">>,
|
||||
backend := <<"bar">>
|
||||
},
|
||||
check(BadBackend)
|
||||
|
@ -124,9 +125,8 @@ t_union_member_selector(_) ->
|
|||
BadCombination = Base#{<<"mechanism">> => <<"scram">>, <<"backend">> => <<"http">>},
|
||||
?assertThrow(
|
||||
#{
|
||||
reason := "unsupported_mechanism",
|
||||
mechanism := <<"scram">>,
|
||||
backend := <<"http">>
|
||||
reason := "unknown_mechanism",
|
||||
expected := "password_based"
|
||||
},
|
||||
check(BadCombination)
|
||||
),
|
|
@ -35,7 +35,7 @@ jwt_example() ->
|
|||
authenticator_example(jwt).
|
||||
|
||||
delete_authenticators(Path, Chain) ->
|
||||
case emqx_authentication:list_authenticators(Chain) of
|
||||
case emqx_authn_chains:list_authenticators(Chain) of
|
||||
{error, _} ->
|
||||
ok;
|
||||
{ok, Authenticators} ->
|
||||
|
@ -60,9 +60,20 @@ delete_config(ID) ->
|
|||
).
|
||||
|
||||
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">>]),
|
||||
<<"certfile">> => filename:join([Dir, <<"data/certs">>, <<"client.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).
|
|
@ -39,20 +39,33 @@ init_per_suite(Config) ->
|
|||
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
|
||||
meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
|
||||
meck:expect(
|
||||
emqx_authz,
|
||||
emqx_authz_file,
|
||||
acl_conf_file,
|
||||
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
|
||||
),
|
||||
|
||||
ok = emqx_common_test_helpers:start_apps(
|
||||
[emqx_conf, emqx_authz],
|
||||
fun set_special_configs/1
|
||||
Apps = emqx_cth_suite:start(
|
||||
[
|
||||
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
|
||||
],
|
||||
#{
|
||||
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(
|
||||
[authorization],
|
||||
#{
|
||||
|
@ -61,7 +74,7 @@ end_per_suite(_Config) ->
|
|||
<<"sources">> => []
|
||||
}
|
||||
),
|
||||
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
|
||||
emqx_cth_suite:stop(?config(suite_apps, Config)),
|
||||
meck:unload(emqx_resource),
|
||||
ok.
|
||||
|
||||
|
@ -73,7 +86,7 @@ init_per_testcase(TestCase, Config) when
|
|||
{ok, _} = emqx_authz:update(?CMD_REPLACE, []),
|
||||
{ok, _} = emqx:update_config([authorization, deny_action], disconnect),
|
||||
Config;
|
||||
init_per_testcase(_, Config) ->
|
||||
init_per_testcase(_TestCase, Config) ->
|
||||
{ok, _} = emqx_authz:update(?CMD_REPLACE, []),
|
||||
Config.
|
||||
|
||||
|
@ -90,14 +103,6 @@ end_per_testcase(_TestCase, _Config) ->
|
|||
emqx_common_test_helpers:call_janitor(),
|
||||
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, #{
|
||||
<<"type">> => <<"http">>,
|
||||
<<"enable">> => true,
|
||||
|
@ -205,7 +210,7 @@ t_bad_file_source(_) ->
|
|||
BadActionErr = {invalid_authorization_action, pubsub},
|
||||
lists:foreach(
|
||||
fun({Source, Error}) ->
|
||||
File = emqx_authz:acl_conf_file(),
|
||||
File = emqx_authz_file:acl_conf_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_PREPEND, Source)),
|
|
@ -33,7 +33,7 @@ groups() ->
|
|||
|
||||
init_per_suite(Config) ->
|
||||
ok = emqx_mgmt_api_test_util:init_suite(
|
||||
[emqx_conf, emqx_authz],
|
||||
[emqx_conf, emqx_auth],
|
||||
fun set_special_configs/1
|
||||
),
|
||||
Config.
|
||||
|
@ -47,12 +47,12 @@ end_per_suite(_Config) ->
|
|||
<<"sources">> => []
|
||||
}
|
||||
),
|
||||
emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]),
|
||||
emqx_mgmt_api_test_util:end_suite([emqx_auth, emqx_conf]),
|
||||
ok.
|
||||
|
||||
set_special_configs(emqx_dashboard) ->
|
||||
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, no_match], deny),
|
||||
{ok, _} = emqx:update_config([authorization, sources], []),
|
|
@ -31,7 +31,7 @@ groups() ->
|
|||
|
||||
init_per_suite(Config) ->
|
||||
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
|
||||
),
|
||||
Config.
|
||||
|
@ -46,12 +46,12 @@ end_per_suite(_Config) ->
|
|||
}
|
||||
),
|
||||
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.
|
||||
|
||||
set_special_configs(emqx_dashboard) ->
|
||||
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, no_match], deny),
|
||||
{ok, _} = emqx:update_config([authorization, sources], []),
|
|
@ -21,6 +21,7 @@
|
|||
-import(emqx_mgmt_api_test_util, [request/3, uri/1]).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||
|
||||
-define(MONGO_SINGLE_HOST, "mongo").
|
||||
|
@ -101,27 +102,42 @@ groups() ->
|
|||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
ok = stop_apps([emqx_resource]),
|
||||
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, health_check, fun(St) -> {ok, St} end),
|
||||
meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
|
||||
meck:expect(
|
||||
emqx_authz,
|
||||
emqx_authz_file,
|
||||
acl_conf_file,
|
||||
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
|
||||
),
|
||||
|
||||
ok = emqx_mgmt_api_test_util:init_suite(
|
||||
[emqx_conf, emqx_authz],
|
||||
fun set_special_configs/1
|
||||
Apps = emqx_cth_suite:start(
|
||||
[
|
||||
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]),
|
||||
Config.
|
||||
_ = emqx_common_test_http:create_default_app(),
|
||||
[{suite_apps, Apps} | Config].
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
end_per_suite(Config) ->
|
||||
{ok, _} = emqx:update_config(
|
||||
[authorization],
|
||||
#{
|
||||
|
@ -130,23 +146,11 @@ end_per_suite(_Config) ->
|
|||
<<"sources">> => []
|
||||
}
|
||||
),
|
||||
%% resource and connector should be stop first,
|
||||
%% or authz_[mysql|pgsql|redis..]_SUITE would be failed
|
||||
ok = stop_apps([emqx_resource]),
|
||||
emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]),
|
||||
_ = emqx_common_test_http:delete_default_app(),
|
||||
emqx_cth_suite:stop(?config(suite_apps, Config)),
|
||||
meck:unload(emqx_resource),
|
||||
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) ->
|
||||
meck:new(emqx_utils, [non_strict, passthrough, no_history, no_link]),
|
||||
meck:expect(emqx_utils, gen_id, fun() -> "fake" end),
|
||||
|
@ -206,7 +210,7 @@ t_api(_) ->
|
|||
],
|
||||
Sources
|
||||
),
|
||||
?assert(filelib:is_file(emqx_authz:acl_conf_file())),
|
||||
?assert(filelib:is_file(emqx_authz_file:acl_conf_file())),
|
||||
|
||||
{ok, 204, _} = request(
|
||||
put,
|
|
@ -34,10 +34,12 @@ groups() ->
|
|||
init_per_testcase(TestCase, Config) ->
|
||||
Apps = emqx_cth_suite:start(
|
||||
[
|
||||
emqx,
|
||||
{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].
|
||||
|
|
@ -35,7 +35,7 @@ all() ->
|
|||
|
||||
init_per_suite(Config) ->
|
||||
ok = emqx_common_test_helpers:start_apps(
|
||||
[emqx_conf, emqx_authz],
|
||||
[emqx_conf, emqx_auth],
|
||||
fun set_special_configs/1
|
||||
),
|
||||
Config.
|
||||
|
@ -49,7 +49,7 @@ end_per_suite(_Config) ->
|
|||
<<"sources">> => []
|
||||
}
|
||||
),
|
||||
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
|
||||
emqx_common_test_helpers:stop_apps([emqx_auth, emqx_conf]),
|
||||
ok.
|
||||
|
||||
init_per_testcase(_TestCase, Config) ->
|
||||
|
@ -58,7 +58,7 @@ end_per_testcase(_TestCase, _Config) ->
|
|||
_ = emqx_authz:set_feature_available(rich_actions, true),
|
||||
ok.
|
||||
|
||||
set_special_configs(emqx_authz) ->
|
||||
set_special_configs(emqx_auth) ->
|
||||
{ok, _} = emqx:update_config([authorization, cache, enable], false),
|
||||
{ok, _} = emqx:update_config([authorization, no_match], deny),
|
||||
{ok, _} = emqx:update_config([authorization, sources], []),
|
|
@ -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.
|
|
@ -0,0 +1,7 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{deps, [
|
||||
{emqx, {path, "../emqx"}},
|
||||
{emqx_utils, {path, "../emqx_utils"}},
|
||||
{emqx_auth, {path, "../emqx_auth"}}
|
||||
|
||||
]}.
|
|
@ -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, []}
|
||||
]}.
|
|
@ -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.
|
|
@ -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}}.
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-behaviour(emqx_authz).
|
||||
-behaviour(emqx_authz_source).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
|
@ -31,14 +31,56 @@
|
|||
create/1,
|
||||
update/1,
|
||||
destroy/1,
|
||||
authorize/4,
|
||||
validate/1,
|
||||
read_file/1
|
||||
authorize/4
|
||||
]).
|
||||
|
||||
-export([
|
||||
validate/1,
|
||||
write_files/1,
|
||||
read_files/1
|
||||
]).
|
||||
|
||||
%% For testing
|
||||
-export([
|
||||
acl_conf_file/0
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Authz Source Callbacks
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
description() ->
|
||||
"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) ->
|
||||
Path = filename(Path0),
|
||||
Rules =
|
||||
|
@ -53,25 +95,39 @@ validate(Path0) ->
|
|||
}),
|
||||
throw(failed_to_read_acl_file);
|
||||
{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})
|
||||
end,
|
||||
{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) ->
|
||||
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) ->
|
||||
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.
|
|
@ -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.
|
|
@ -18,7 +18,7 @@
|
|||
-compile(nowarn_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("common_test/include/ct.hrl").
|
||||
|
||||
|
@ -42,9 +42,11 @@ init_per_testcase(TestCase, Config) ->
|
|||
Apps = emqx_cth_suite:start(
|
||||
[
|
||||
{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].
|
||||
|
|
@ -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.
|
|
@ -0,0 +1,6 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{deps, [
|
||||
{emqx, {path, "../emqx"}},
|
||||
{emqx_utils, {path, "../emqx_utils"}},
|
||||
{emqx_auth, {path, "../emqx_auth"}}
|
||||
]}.
|
|
@ -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, []}
|
||||
]}.
|
|
@ -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.
|
|
@ -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}}.
|
|
@ -16,188 +16,47 @@
|
|||
|
||||
-module(emqx_authn_http).
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
-include_lib("emqx_auth/include/emqx_authn.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_authentication).
|
||||
-behaviour(emqx_authn_provider).
|
||||
|
||||
-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,
|
||||
update/2,
|
||||
authenticate/2,
|
||||
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
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
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(Config).
|
||||
|
||||
create(Config0) ->
|
||||
with_validated_config(Config0, fun(Config, State) ->
|
||||
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(
|
||||
ResourceId,
|
||||
emqx_bridge_http_connector,
|
||||
Config
|
||||
),
|
||||
{ok, State#{resource_id => ResourceId}}.
|
||||
{ok, State#{resource_id => ResourceId}}
|
||||
end).
|
||||
|
||||
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
|
||||
{error, Reason} ->
|
||||
error({load_config_error, Reason});
|
||||
{ok, _} ->
|
||||
{ok, NState#{resource_id => ResourceId}}
|
||||
end.
|
||||
end
|
||||
end).
|
||||
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
|
@ -237,92 +96,35 @@ destroy(#{resource_id := ResourceId}) ->
|
|||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
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
|
||||
).
|
||||
|
||||
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
|
||||
with_validated_config(Config, Fun) ->
|
||||
Pipeline = [
|
||||
fun check_ssl_opts/1,
|
||||
fun check_headers/1,
|
||||
fun parse_config/1
|
||||
],
|
||||
case emqx_utils:pipeline(Pipeline, Config, undefined) of
|
||||
{ok, NConfig, ProviderState} ->
|
||||
Fun(NConfig, ProviderState);
|
||||
{error, Reason, _} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
check_headers(Conf) ->
|
||||
case is_backend_http(Conf) of
|
||||
true ->
|
||||
Headers = get_conf_val("headers", Conf),
|
||||
case to_bin(get_conf_val("method", Conf)) of
|
||||
<<"post">> ->
|
||||
ok;
|
||||
<<"get">> ->
|
||||
check_ssl_opts(#{url := <<"https://", _/binary>>, ssl := #{enable := false}}) ->
|
||||
{error,
|
||||
{invalid_ssl_opts,
|
||||
<<"it's required to enable the TLS option to establish a https connection">>}};
|
||||
check_ssl_opts(_) ->
|
||||
ok.
|
||||
|
||||
check_headers(#{headers := Headers, method := get}) ->
|
||||
case maps:is_key(<<"content-type">>, Headers) of
|
||||
false -> ok;
|
||||
true -> <<"HTTP GET requests cannot include content-type header.">>
|
||||
end
|
||||
end;
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
|
||||
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>>, <<>>}
|
||||
ok;
|
||||
true ->
|
||||
{error, {invalid_headers, <<"HTTP GET requests cannot include content-type header.">>}}
|
||||
end;
|
||||
[HostPort] ->
|
||||
{iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>}
|
||||
end;
|
||||
[Url] ->
|
||||
throw({invalid_url, Url})
|
||||
end.
|
||||
check_headers(_) ->
|
||||
ok.
|
||||
|
||||
parse_config(
|
||||
#{
|
||||
|
@ -332,7 +134,7 @@ parse_config(
|
|||
request_timeout := RequestTimeout
|
||||
} = Config
|
||||
) ->
|
||||
{BaseUrl0, Path, Query} = parse_url(RawUrl),
|
||||
{BaseUrl0, Path, Query} = emqx_authn_utils:parse_url(RawUrl),
|
||||
{ok, BaseUrl} = emqx_http_lib:uri_parse(BaseUrl0),
|
||||
State = #{
|
||||
method => Method,
|
||||
|
@ -346,7 +148,7 @@ parse_config(
|
|||
request_timeout => RequestTimeout,
|
||||
url => RawUrl
|
||||
},
|
||||
{Config#{base_url => BaseUrl, pool_type => random}, State}.
|
||||
{ok, Config#{base_url => BaseUrl, pool_type => random}, State}.
|
||||
|
||||
generate_request(Credential, #{
|
||||
method := Method,
|
||||
|
@ -469,16 +271,11 @@ to_list(B) when is_binary(B) ->
|
|||
to_list(L) when is_list(L) ->
|
||||
L.
|
||||
|
||||
to_bin(A) when is_atom(A) ->
|
||||
atom_to_binary(A);
|
||||
to_bin(B) when is_binary(B) ->
|
||||
B;
|
||||
to_bin(L) when is_list(L) ->
|
||||
list_to_binary(L).
|
||||
|
||||
get_conf_val(Name, Conf) ->
|
||||
hocon_maps:get(?CONF_NS ++ "." ++ Name, Conf).
|
||||
|
||||
ensure_header_name_type(Headers) ->
|
||||
Fun = fun
|
||||
(Key, _Val, Acc) when is_binary(Key) ->
|
|
@ -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.
|
|
@ -16,13 +16,11 @@
|
|||
|
||||
-module(emqx_authz_http).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
-behaviour(emqx_authz).
|
||||
-behaviour(emqx_authz_source).
|
||||
|
||||
%% AuthZ Callbacks
|
||||
-export([
|
||||
|
@ -31,6 +29,7 @@
|
|||
update/1,
|
||||
destroy/1,
|
||||
authorize/4,
|
||||
merge_defaults/1,
|
||||
parse_url/1
|
||||
]).
|
||||
|
||||
|
@ -73,7 +72,7 @@ update(Config) ->
|
|||
end.
|
||||
|
||||
destroy(#{annotations := #{id := Id}}) ->
|
||||
ok = emqx_resource:remove_local(Id).
|
||||
emqx_authz_utils:remove_resource(Id).
|
||||
|
||||
authorize(
|
||||
Client,
|
||||
|
@ -95,7 +94,7 @@ authorize(
|
|||
case emqx_authz_utils:parse_http_resp_body(ContentType, Body) of
|
||||
error ->
|
||||
?SLOG(error, #{
|
||||
msg => "authz_http_response_incorrect",
|
||||
msg => authz_http_response_incorrect,
|
||||
content_type => ContentType,
|
||||
body => Body
|
||||
}),
|
||||
|
@ -119,11 +118,25 @@ authorize(
|
|||
ignore
|
||||
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) ->
|
||||
?SLOG(
|
||||
debug,
|
||||
#{
|
||||
msg => "unexpected_authz_http_response",
|
||||
msg => unexpected_authz_http_response,
|
||||
status => Status,
|
||||
content_type => emqx_authz_utils:content_type(Headers),
|
||||
body => Body
|
|
@ -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).
|
|
@ -19,7 +19,7 @@
|
|||
-compile(nowarn_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("common_test/include/ct.hrl").
|
||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||
|
@ -65,7 +65,7 @@ all() ->
|
|||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
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)
|
||||
}),
|
||||
[{apps, Apps} | Config].
|
||||
|
@ -102,7 +102,7 @@ t_create(_Config) ->
|
|||
{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) ->
|
||||
AuthConfig = raw_http_auth_config(),
|
||||
|
@ -128,7 +128,7 @@ t_create_invalid(_Config) ->
|
|||
end,
|
||||
?assertEqual(
|
||||
{error, {not_found, {chain, ?GLOBAL}}},
|
||||
emqx_authentication:list_authenticators(?GLOBAL)
|
||||
emqx_authn_chains:list_authenticators(?GLOBAL)
|
||||
)
|
||||
end,
|
||||
InvalidConfigs
|
||||
|
@ -274,7 +274,7 @@ t_destroy(_Config) ->
|
|||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_http, state := State}]} =
|
||||
emqx_authentication:list_authenticators(?GLOBAL),
|
||||
emqx_authn_chains:list_authenticators(?GLOBAL),
|
||||
|
||||
Credentials = maps:with([username, password], ?CREDENTIALS),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue