Merge pull request #7484 from JimMoen/erlfmt-authn-authz
Erlfmt authn and authz
This commit is contained in:
commit
4d74296fb9
|
|
@ -1,23 +1,32 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{deps,
|
{deps, [
|
||||||
[ {emqx, {path, "../emqx"}}
|
{emqx, {path, "../emqx"}},
|
||||||
, {emqx_connector, {path, "../emqx_connector"}}
|
{emqx_connector, {path, "../emqx_connector"}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{edoc_opts, [{preprocess, true}]}.
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
{erl_opts, [warn_unused_vars,
|
{erl_opts, [
|
||||||
warn_shadow_vars,
|
warn_unused_vars,
|
||||||
warnings_as_errors,
|
warn_shadow_vars,
|
||||||
warn_unused_import,
|
warnings_as_errors,
|
||||||
warn_obsolete_guard,
|
warn_unused_import,
|
||||||
debug_info,
|
warn_obsolete_guard,
|
||||||
{parse_transform}]}.
|
debug_info,
|
||||||
|
{parse_transform}
|
||||||
|
]}.
|
||||||
|
|
||||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
{xref_checks, [
|
||||||
locals_not_used, deprecated_function_calls,
|
undefined_function_calls,
|
||||||
warnings_as_errors, deprecated_functions]}.
|
undefined_functions,
|
||||||
|
locals_not_used,
|
||||||
|
deprecated_function_calls,
|
||||||
|
warnings_as_errors,
|
||||||
|
deprecated_functions
|
||||||
|
]}.
|
||||||
|
|
||||||
{cover_enabled, true}.
|
{cover_enabled, true}.
|
||||||
{cover_opts, [verbose]}.
|
{cover_opts, [verbose]}.
|
||||||
{cover_export_enabled, true}.
|
{cover_export_enabled, true}.
|
||||||
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_authn,
|
{application, emqx_authn, [
|
||||||
[{description, "EMQX Authentication"},
|
{description, "EMQX Authentication"},
|
||||||
{vsn, "0.1.0"},
|
{vsn, "0.1.0"},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
{registered, [emqx_authn_sup, emqx_authn_registry]},
|
{registered, [emqx_authn_sup, emqx_authn_registry]},
|
||||||
{applications, [kernel,stdlib,emqx_resource,ehttpc,epgsql,mysql,jose]},
|
{applications, [kernel, stdlib, emqx_resource, ehttpc, epgsql, mysql, jose]},
|
||||||
{mod, {emqx_authn_app,[]}},
|
{mod, {emqx_authn_app, []}},
|
||||||
{env, []},
|
{env, []},
|
||||||
{licenses, ["Apache-2.0"]},
|
{licenses, ["Apache-2.0"]},
|
||||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||||
{links, [{"Homepage", "https://emqx.io/"}]}
|
{links, [{"Homepage", "https://emqx.io/"}]}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -16,28 +16,31 @@
|
||||||
|
|
||||||
-module(emqx_authn).
|
-module(emqx_authn).
|
||||||
|
|
||||||
-export([ providers/0
|
-export([
|
||||||
, check_config/1
|
providers/0,
|
||||||
, check_config/2
|
check_config/1,
|
||||||
, check_configs/1
|
check_config/2,
|
||||||
]).
|
check_configs/1
|
||||||
|
]).
|
||||||
|
|
||||||
-include("emqx_authn.hrl").
|
-include("emqx_authn.hrl").
|
||||||
|
|
||||||
providers() ->
|
providers() ->
|
||||||
[ {{'password_based', 'built_in_database'}, emqx_authn_mnesia}
|
[
|
||||||
, {{'password_based', mysql}, emqx_authn_mysql}
|
{{'password_based', 'built_in_database'}, emqx_authn_mnesia},
|
||||||
, {{'password_based', postgresql}, emqx_authn_pgsql}
|
{{'password_based', mysql}, emqx_authn_mysql},
|
||||||
, {{'password_based', mongodb}, emqx_authn_mongodb}
|
{{'password_based', postgresql}, emqx_authn_pgsql},
|
||||||
, {{'password_based', redis}, emqx_authn_redis}
|
{{'password_based', mongodb}, emqx_authn_mongodb},
|
||||||
, {{'password_based', 'http'}, emqx_authn_http}
|
{{'password_based', redis}, emqx_authn_redis},
|
||||||
, {jwt, emqx_authn_jwt}
|
{{'password_based', 'http'}, emqx_authn_http},
|
||||||
, {{scram, 'built_in_database'}, emqx_enhanced_authn_scram_mnesia}
|
{jwt, emqx_authn_jwt},
|
||||||
|
{{scram, 'built_in_database'}, emqx_enhanced_authn_scram_mnesia}
|
||||||
].
|
].
|
||||||
|
|
||||||
check_configs(C) when is_map(C) ->
|
check_configs(C) when is_map(C) ->
|
||||||
check_configs([C]);
|
check_configs([C]);
|
||||||
check_configs([]) -> [];
|
check_configs([]) ->
|
||||||
|
[];
|
||||||
check_configs([Config | Configs]) ->
|
check_configs([Config | Configs]) ->
|
||||||
[check_config(Config) | check_configs(Configs)].
|
[check_config(Config) | check_configs(Configs)].
|
||||||
|
|
||||||
|
|
@ -51,22 +54,26 @@ check_config(Config, Opts) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_check_config(#{<<"mechanism">> := Mec} = Config, Opts) ->
|
do_check_config(#{<<"mechanism">> := Mec} = Config, Opts) ->
|
||||||
Key = case maps:get(<<"backend">>, Config, false) of
|
Key =
|
||||||
false -> atom(Mec);
|
case maps:get(<<"backend">>, Config, false) of
|
||||||
Backend -> {atom(Mec), atom(Backend)}
|
false -> atom(Mec);
|
||||||
end,
|
Backend -> {atom(Mec), atom(Backend)}
|
||||||
|
end,
|
||||||
case lists:keyfind(Key, 1, providers()) of
|
case lists:keyfind(Key, 1, providers()) of
|
||||||
false ->
|
false ->
|
||||||
throw({unknown_handler, Key});
|
throw({unknown_handler, Key});
|
||||||
{_, ProviderModule} ->
|
{_, ProviderModule} ->
|
||||||
hocon_tconf:check_plain(ProviderModule, #{?CONF_NS_BINARY => Config},
|
hocon_tconf:check_plain(
|
||||||
Opts#{atom_key => true})
|
ProviderModule,
|
||||||
|
#{?CONF_NS_BINARY => Config},
|
||||||
|
Opts#{atom_key => true}
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
atom(Bin) ->
|
atom(Bin) ->
|
||||||
try
|
try
|
||||||
binary_to_existing_atom(Bin, utf8)
|
binary_to_existing_atom(Bin, utf8)
|
||||||
catch
|
catch
|
||||||
_ : _ ->
|
_:_ ->
|
||||||
throw({unknown_auth_provider, Bin})
|
throw({unknown_auth_provider, Bin})
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -21,9 +21,10 @@
|
||||||
-behaviour(application).
|
-behaviour(application).
|
||||||
|
|
||||||
%% Application callbacks
|
%% Application callbacks
|
||||||
-export([ start/2
|
-export([
|
||||||
, stop/1
|
start/2,
|
||||||
]).
|
stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/emqx_authentication.hrl").
|
-include_lib("emqx/include/emqx_authentication.hrl").
|
||||||
|
|
||||||
|
|
@ -51,13 +52,15 @@ initialize() ->
|
||||||
ok = ?AUTHN:register_providers(emqx_authn:providers()),
|
ok = ?AUTHN:register_providers(emqx_authn:providers()),
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun({ChainName, RawAuthConfigs}) ->
|
fun({ChainName, RawAuthConfigs}) ->
|
||||||
AuthConfig = emqx_authn:check_configs(RawAuthConfigs),
|
AuthConfig = emqx_authn:check_configs(RawAuthConfigs),
|
||||||
?AUTHN:initialize_authentication(
|
?AUTHN:initialize_authentication(
|
||||||
ChainName,
|
ChainName,
|
||||||
AuthConfig)
|
AuthConfig
|
||||||
end,
|
)
|
||||||
chain_configs()).
|
end,
|
||||||
|
chain_configs()
|
||||||
|
).
|
||||||
|
|
||||||
deinitialize() ->
|
deinitialize() ->
|
||||||
ok = ?AUTHN:deregister_providers(provider_types()),
|
ok = ?AUTHN:deregister_providers(provider_types()),
|
||||||
|
|
@ -71,15 +74,16 @@ global_chain_config() ->
|
||||||
|
|
||||||
listener_chain_configs() ->
|
listener_chain_configs() ->
|
||||||
lists:map(
|
lists:map(
|
||||||
fun({ListenerID, _}) ->
|
fun({ListenerID, _}) ->
|
||||||
{ListenerID, emqx:get_raw_config(auth_config_path(ListenerID), [])}
|
{ListenerID, emqx:get_raw_config(auth_config_path(ListenerID), [])}
|
||||||
end,
|
end,
|
||||||
emqx_listeners:list()).
|
emqx_listeners:list()
|
||||||
|
).
|
||||||
|
|
||||||
auth_config_path(ListenerID) ->
|
auth_config_path(ListenerID) ->
|
||||||
[<<"listeners">>]
|
[<<"listeners">>] ++
|
||||||
++ binary:split(atom_to_binary(ListenerID), <<":">>)
|
binary:split(atom_to_binary(ListenerID), <<":">>) ++
|
||||||
++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY].
|
[?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY].
|
||||||
|
|
||||||
provider_types() ->
|
provider_types() ->
|
||||||
lists:map(fun({Type, _Module}) -> Type end, emqx_authn:providers()).
|
lists:map(fun({Type, _Module}) -> Type end, emqx_authn:providers()).
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,25 @@
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-type(simple_algorithm_name() :: plain | md5 | sha | sha256 | sha512).
|
-type simple_algorithm_name() :: plain | md5 | sha | sha256 | sha512.
|
||||||
-type(salt_position() :: prefix | suffix).
|
-type salt_position() :: prefix | suffix.
|
||||||
|
|
||||||
-type(simple_algorithm() :: #{name := simple_algorithm_name(),
|
-type simple_algorithm() :: #{
|
||||||
salt_position := salt_position()}).
|
name := simple_algorithm_name(),
|
||||||
|
salt_position := salt_position()
|
||||||
|
}.
|
||||||
|
|
||||||
-type(bcrypt_algorithm() :: #{name := bcrypt}).
|
-type bcrypt_algorithm() :: #{name := bcrypt}.
|
||||||
-type(bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}).
|
-type bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}.
|
||||||
|
|
||||||
-type(pbkdf2_algorithm() :: #{name := pbkdf2,
|
-type pbkdf2_algorithm() :: #{
|
||||||
mac_fun := emqx_passwd:pbkdf2_mac_fun(),
|
name := pbkdf2,
|
||||||
iterations := pos_integer()}).
|
mac_fun := emqx_passwd:pbkdf2_mac_fun(),
|
||||||
|
iterations := pos_integer()
|
||||||
|
}.
|
||||||
|
|
||||||
-type(algorithm() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm()).
|
-type algorithm() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm().
|
||||||
-type(algorithm_rw() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm_rw()).
|
-type algorithm_rw() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm_rw().
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Hocon Schema
|
%% Hocon Schema
|
||||||
|
|
@ -40,37 +44,44 @@
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
-export([roots/0,
|
-export([
|
||||||
fields/1,
|
roots/0,
|
||||||
namespace/0]).
|
fields/1,
|
||||||
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([type_ro/1,
|
-export([
|
||||||
type_rw/1]).
|
type_ro/1,
|
||||||
|
type_rw/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([init/1,
|
-export([
|
||||||
gen_salt/1,
|
init/1,
|
||||||
hash/2,
|
gen_salt/1,
|
||||||
check_password/4]).
|
hash/2,
|
||||||
|
check_password/4
|
||||||
|
]).
|
||||||
|
|
||||||
namespace() -> "authn-hash".
|
namespace() -> "authn-hash".
|
||||||
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
|
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
|
||||||
|
|
||||||
fields(bcrypt_rw) ->
|
fields(bcrypt_rw) ->
|
||||||
fields(bcrypt) ++
|
fields(bcrypt) ++
|
||||||
[{salt_rounds, fun salt_rounds/1}];
|
[{salt_rounds, fun salt_rounds/1}];
|
||||||
|
|
||||||
fields(bcrypt) ->
|
fields(bcrypt) ->
|
||||||
[{name, {enum, [bcrypt]}}];
|
[{name, {enum, [bcrypt]}}];
|
||||||
|
|
||||||
fields(pbkdf2) ->
|
fields(pbkdf2) ->
|
||||||
[{name, {enum, [pbkdf2]}},
|
[
|
||||||
{mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}},
|
{name, {enum, [pbkdf2]}},
|
||||||
{iterations, integer()},
|
{mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}},
|
||||||
{dk_length, fun dk_length/1}];
|
{iterations, integer()},
|
||||||
|
{dk_length, fun dk_length/1}
|
||||||
|
];
|
||||||
fields(other_algorithms) ->
|
fields(other_algorithms) ->
|
||||||
[{name, {enum, [plain, md5, sha, sha256, sha512]}},
|
[
|
||||||
{salt_position, fun salt_position/1}].
|
{name, {enum, [plain, md5, sha, sha256, sha512]}},
|
||||||
|
{salt_position, fun salt_position/1}
|
||||||
|
].
|
||||||
|
|
||||||
salt_position(type) -> {enum, [prefix, suffix]};
|
salt_position(type) -> {enum, [prefix, suffix]};
|
||||||
salt_position(desc) -> "Specifies whether the password salt is stored as a prefix or the suffix.";
|
salt_position(desc) -> "Specifies whether the password salt is stored as a prefix or the suffix.";
|
||||||
|
|
@ -89,47 +100,56 @@ dk_length(_) -> undefined.
|
||||||
|
|
||||||
type_rw(type) ->
|
type_rw(type) ->
|
||||||
hoconsc:union(rw_refs());
|
hoconsc:union(rw_refs());
|
||||||
type_rw(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
|
type_rw(default) ->
|
||||||
type_rw(_) -> undefined.
|
#{<<"name">> => sha256, <<"salt_position">> => prefix};
|
||||||
|
type_rw(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
type_ro(type) ->
|
type_ro(type) ->
|
||||||
hoconsc:union(ro_refs());
|
hoconsc:union(ro_refs());
|
||||||
type_ro(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
|
type_ro(default) ->
|
||||||
type_ro(_) -> undefined.
|
#{<<"name">> => sha256, <<"salt_position">> => prefix};
|
||||||
|
type_ro(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
-spec(init(algorithm()) -> ok).
|
-spec init(algorithm()) -> ok.
|
||||||
init(#{name := bcrypt}) ->
|
init(#{name := bcrypt}) ->
|
||||||
{ok, _} = application:ensure_all_started(bcrypt),
|
{ok, _} = application:ensure_all_started(bcrypt),
|
||||||
ok;
|
ok;
|
||||||
init(#{name := _Other}) ->
|
init(#{name := _Other}) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
-spec gen_salt(algorithm_rw()) -> emqx_passwd:salt().
|
||||||
-spec(gen_salt(algorithm_rw()) -> emqx_passwd:salt()).
|
|
||||||
gen_salt(#{name := plain}) ->
|
gen_salt(#{name := plain}) ->
|
||||||
<<>>;
|
<<>>;
|
||||||
gen_salt(#{name := bcrypt,
|
gen_salt(#{
|
||||||
salt_rounds := Rounds}) ->
|
name := bcrypt,
|
||||||
|
salt_rounds := Rounds
|
||||||
|
}) ->
|
||||||
{ok, Salt} = bcrypt:gen_salt(Rounds),
|
{ok, Salt} = bcrypt:gen_salt(Rounds),
|
||||||
list_to_binary(Salt);
|
list_to_binary(Salt);
|
||||||
gen_salt(#{name := Other}) when Other =/= plain, Other =/= bcrypt ->
|
gen_salt(#{name := Other}) when Other =/= plain, Other =/= bcrypt ->
|
||||||
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
||||||
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
|
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
|
||||||
|
|
||||||
|
-spec hash(algorithm_rw(), emqx_passwd:password()) -> {emqx_passwd:hash(), emqx_passwd:salt()}.
|
||||||
-spec(hash(algorithm_rw(), emqx_passwd:password()) -> {emqx_passwd:hash(), emqx_passwd:salt()}).
|
|
||||||
hash(#{name := bcrypt, salt_rounds := _} = Algorithm, Password) ->
|
hash(#{name := bcrypt, salt_rounds := _} = Algorithm, Password) ->
|
||||||
Salt0 = gen_salt(Algorithm),
|
Salt0 = gen_salt(Algorithm),
|
||||||
Hash = emqx_passwd:hash({bcrypt, Salt0}, Password),
|
Hash = emqx_passwd:hash({bcrypt, Salt0}, Password),
|
||||||
Salt = Hash,
|
Salt = Hash,
|
||||||
{Hash, Salt};
|
{Hash, Salt};
|
||||||
hash(#{name := pbkdf2,
|
hash(
|
||||||
mac_fun := MacFun,
|
#{
|
||||||
iterations := Iterations} = Algorithm, Password) ->
|
name := pbkdf2,
|
||||||
|
mac_fun := MacFun,
|
||||||
|
iterations := Iterations
|
||||||
|
} = Algorithm,
|
||||||
|
Password
|
||||||
|
) ->
|
||||||
Salt = gen_salt(Algorithm),
|
Salt = gen_salt(Algorithm),
|
||||||
DKLength = maps:get(dk_length, Algorithm, undefined),
|
DKLength = maps:get(dk_length, Algorithm, undefined),
|
||||||
Hash = emqx_passwd:hash({pbkdf2, MacFun, Salt, Iterations, DKLength}, Password),
|
Hash = emqx_passwd:hash({pbkdf2, MacFun, Salt, Iterations, DKLength}, Password),
|
||||||
|
|
@ -139,18 +159,24 @@ hash(#{name := Other, salt_position := SaltPosition} = Algorithm, Password) ->
|
||||||
Hash = emqx_passwd:hash({Other, Salt, SaltPosition}, Password),
|
Hash = emqx_passwd:hash({Other, Salt, SaltPosition}, Password),
|
||||||
{Hash, Salt}.
|
{Hash, Salt}.
|
||||||
|
|
||||||
|
-spec check_password(
|
||||||
-spec(check_password(
|
algorithm(),
|
||||||
algorithm(),
|
emqx_passwd:salt(),
|
||||||
emqx_passwd:salt(),
|
emqx_passwd:hash(),
|
||||||
emqx_passwd:hash(),
|
emqx_passwd:password()
|
||||||
emqx_passwd:password()) -> boolean()).
|
) -> boolean().
|
||||||
check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) ->
|
check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) ->
|
||||||
emqx_passwd:check_pass({bcrypt, PasswordHash}, PasswordHash, Password);
|
emqx_passwd:check_pass({bcrypt, PasswordHash}, PasswordHash, Password);
|
||||||
check_password(#{name := pbkdf2,
|
check_password(
|
||||||
mac_fun := MacFun,
|
#{
|
||||||
iterations := Iterations} = Algorithm,
|
name := pbkdf2,
|
||||||
Salt, PasswordHash, Password) ->
|
mac_fun := MacFun,
|
||||||
|
iterations := Iterations
|
||||||
|
} = Algorithm,
|
||||||
|
Salt,
|
||||||
|
PasswordHash,
|
||||||
|
Password
|
||||||
|
) ->
|
||||||
DKLength = maps:get(dk_length, Algorithm, undefined),
|
DKLength = maps:get(dk_length, Algorithm, undefined),
|
||||||
emqx_passwd:check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password);
|
emqx_passwd:check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password);
|
||||||
check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHash, Password) ->
|
check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHash, Password) ->
|
||||||
|
|
@ -161,11 +187,15 @@ check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHa
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
rw_refs() ->
|
rw_refs() ->
|
||||||
[hoconsc:ref(?MODULE, bcrypt_rw),
|
[
|
||||||
hoconsc:ref(?MODULE, pbkdf2),
|
hoconsc:ref(?MODULE, bcrypt_rw),
|
||||||
hoconsc:ref(?MODULE, other_algorithms)].
|
hoconsc:ref(?MODULE, pbkdf2),
|
||||||
|
hoconsc:ref(?MODULE, other_algorithms)
|
||||||
|
].
|
||||||
|
|
||||||
ro_refs() ->
|
ro_refs() ->
|
||||||
[hoconsc:ref(?MODULE, bcrypt),
|
[
|
||||||
hoconsc:ref(?MODULE, pbkdf2),
|
hoconsc:ref(?MODULE, bcrypt),
|
||||||
hoconsc:ref(?MODULE, other_algorithms)].
|
hoconsc:ref(?MODULE, pbkdf2),
|
||||||
|
hoconsc:ref(?MODULE, other_algorithms)
|
||||||
|
].
|
||||||
|
|
|
||||||
|
|
@ -20,21 +20,20 @@
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-import(hoconsc, [mk/2, ref/2]).
|
-import(hoconsc, [mk/2, ref/2]).
|
||||||
|
|
||||||
-export([ common_fields/0
|
-export([
|
||||||
, roots/0
|
common_fields/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
, authenticator_type/0
|
fields/1,
|
||||||
, root_type/0
|
authenticator_type/0,
|
||||||
, mechanism/1
|
root_type/0,
|
||||||
, backend/1
|
mechanism/1,
|
||||||
]).
|
backend/1
|
||||||
|
]).
|
||||||
|
|
||||||
roots() -> [].
|
roots() -> [].
|
||||||
|
|
||||||
|
|
||||||
common_fields() ->
|
common_fields() ->
|
||||||
[ {enable, fun enable/1}
|
[{enable, fun enable/1}].
|
||||||
].
|
|
||||||
|
|
||||||
enable(type) -> boolean();
|
enable(type) -> boolean();
|
||||||
enable(default) -> true;
|
enable(default) -> true;
|
||||||
|
|
@ -54,46 +53,60 @@ root_type() ->
|
||||||
hoconsc:array(authenticator_type()).
|
hoconsc:array(authenticator_type()).
|
||||||
|
|
||||||
mechanism(Name) ->
|
mechanism(Name) ->
|
||||||
hoconsc:mk(hoconsc:enum([Name]),
|
hoconsc:mk(
|
||||||
#{ required => true
|
hoconsc:enum([Name]),
|
||||||
, desc => "Authentication mechanism."
|
#{
|
||||||
}).
|
required => true,
|
||||||
|
desc => "Authentication mechanism."
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
backend(Name) ->
|
backend(Name) ->
|
||||||
hoconsc:mk(hoconsc:enum([Name]),
|
hoconsc:mk(
|
||||||
#{ required => true
|
hoconsc:enum([Name]),
|
||||||
, desc => "Backend type."
|
#{
|
||||||
}).
|
required => true,
|
||||||
|
desc => "Backend type."
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
fields("metrics_status_fields") ->
|
fields("metrics_status_fields") ->
|
||||||
[ {"metrics", mk(ref(?MODULE, "metrics"), #{desc => "The metrics of the resource"})}
|
[
|
||||||
, {"node_metrics", mk(hoconsc:array(ref(?MODULE, "node_metrics")),
|
{"metrics", mk(ref(?MODULE, "metrics"), #{desc => "The metrics of the resource"})},
|
||||||
#{ desc => "The metrics of the resource for each node"
|
{"node_metrics",
|
||||||
})}
|
mk(
|
||||||
, {"status", mk(status(), #{desc => "The status of the resource"})}
|
hoconsc:array(ref(?MODULE, "node_metrics")),
|
||||||
, {"node_status", mk(hoconsc:array(ref(?MODULE, "node_status")),
|
#{desc => "The metrics of the resource for each node"}
|
||||||
#{ desc => "The status of the resource for each node"
|
)},
|
||||||
})}
|
{"status", mk(status(), #{desc => "The status of the resource"})},
|
||||||
|
{"node_status",
|
||||||
|
mk(
|
||||||
|
hoconsc:array(ref(?MODULE, "node_status")),
|
||||||
|
#{desc => "The status of the resource for each node"}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("metrics") ->
|
fields("metrics") ->
|
||||||
[ {"matched", mk(integer(), #{desc => "Count of this resource is queried"})}
|
[
|
||||||
, {"success", mk(integer(), #{desc => "Count of query success"})}
|
{"matched", mk(integer(), #{desc => "Count of this resource is queried"})},
|
||||||
, {"failed", mk(integer(), #{desc => "Count of query failed"})}
|
{"success", mk(integer(), #{desc => "Count of query success"})},
|
||||||
, {"rate", mk(float(), #{desc => "The rate of matched, times/second"})}
|
{"failed", mk(integer(), #{desc => "Count of query failed"})},
|
||||||
, {"rate_max", mk(float(), #{desc => "The max rate of matched, times/second"})}
|
{"rate", mk(float(), #{desc => "The rate of matched, times/second"})},
|
||||||
, {"rate_last5m", mk(float(),
|
{"rate_max", mk(float(), #{desc => "The max rate of matched, times/second"})},
|
||||||
#{desc => "The average rate of matched in the last 5 minutes, times/second"})}
|
{"rate_last5m",
|
||||||
|
mk(
|
||||||
|
float(),
|
||||||
|
#{desc => "The average rate of matched in the last 5 minutes, times/second"}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("node_metrics") ->
|
fields("node_metrics") ->
|
||||||
[ node_name()
|
[
|
||||||
, {"metrics", mk(ref(?MODULE, "metrics"), #{})}
|
node_name(),
|
||||||
|
{"metrics", mk(ref(?MODULE, "metrics"), #{})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("node_status") ->
|
fields("node_status") ->
|
||||||
[ node_name()
|
[
|
||||||
, {"status", mk(status(), #{desc => "Status of the node."})}
|
node_name(),
|
||||||
|
{"status", mk(status(), #{desc => "Status of the node."})}
|
||||||
].
|
].
|
||||||
|
|
||||||
status() ->
|
status() ->
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@
|
||||||
|
|
||||||
-behaviour(supervisor).
|
-behaviour(supervisor).
|
||||||
|
|
||||||
-export([ start_link/0
|
-export([
|
||||||
, init/1
|
start_link/0,
|
||||||
]).
|
init/1
|
||||||
|
]).
|
||||||
|
|
||||||
start_link() ->
|
start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
|
||||||
|
|
@ -19,26 +19,29 @@
|
||||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||||
-include_lib("emqx_authn.hrl").
|
-include_lib("emqx_authn.hrl").
|
||||||
|
|
||||||
-export([ check_password_from_selected_map/3
|
-export([
|
||||||
, parse_deep/1
|
check_password_from_selected_map/3,
|
||||||
, parse_str/1
|
parse_deep/1,
|
||||||
, parse_sql/2
|
parse_str/1,
|
||||||
, render_deep/2
|
parse_sql/2,
|
||||||
, render_str/2
|
render_deep/2,
|
||||||
, render_sql_params/2
|
render_str/2,
|
||||||
, is_superuser/1
|
render_sql_params/2,
|
||||||
, bin/1
|
is_superuser/1,
|
||||||
, ensure_apps_started/1
|
bin/1,
|
||||||
, cleanup_resources/0
|
ensure_apps_started/1,
|
||||||
, make_resource_id/1
|
cleanup_resources/0,
|
||||||
]).
|
make_resource_id/1
|
||||||
|
]).
|
||||||
|
|
||||||
-define(AUTHN_PLACEHOLDERS, [?PH_USERNAME,
|
-define(AUTHN_PLACEHOLDERS, [
|
||||||
?PH_CLIENTID,
|
?PH_USERNAME,
|
||||||
?PH_PASSWORD,
|
?PH_CLIENTID,
|
||||||
?PH_PEERHOST,
|
?PH_PASSWORD,
|
||||||
?PH_CERT_SUBJECT,
|
?PH_PEERHOST,
|
||||||
?PH_CERT_CN_NAME]).
|
?PH_CERT_SUBJECT,
|
||||||
|
?PH_CERT_CN_NAME
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
|
|
@ -47,12 +50,12 @@
|
||||||
check_password_from_selected_map(_Algorithm, _Selected, undefined) ->
|
check_password_from_selected_map(_Algorithm, _Selected, undefined) ->
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
check_password_from_selected_map(
|
check_password_from_selected_map(
|
||||||
Algorithm, #{<<"password_hash">> := Hash} = Selected, Password) ->
|
Algorithm, #{<<"password_hash">> := Hash} = Selected, Password
|
||||||
|
) ->
|
||||||
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
||||||
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
|
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false ->
|
false -> {error, bad_username_or_password}
|
||||||
{error, bad_username_or_password}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_deep(Template) ->
|
parse_deep(Template) ->
|
||||||
|
|
@ -63,27 +66,33 @@ parse_str(Template) ->
|
||||||
|
|
||||||
parse_sql(Template, ReplaceWith) ->
|
parse_sql(Template, ReplaceWith) ->
|
||||||
emqx_placeholder:preproc_sql(
|
emqx_placeholder:preproc_sql(
|
||||||
Template,
|
Template,
|
||||||
#{replace_with => ReplaceWith,
|
#{
|
||||||
placeholders => ?AUTHN_PLACEHOLDERS}).
|
replace_with => ReplaceWith,
|
||||||
|
placeholders => ?AUTHN_PLACEHOLDERS
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
render_deep(Template, Credential) ->
|
render_deep(Template, Credential) ->
|
||||||
emqx_placeholder:proc_tmpl_deep(
|
emqx_placeholder:proc_tmpl_deep(
|
||||||
Template,
|
Template,
|
||||||
Credential,
|
Credential,
|
||||||
#{return => full_binary, var_trans => fun handle_var/2}).
|
#{return => full_binary, var_trans => fun handle_var/2}
|
||||||
|
).
|
||||||
|
|
||||||
render_str(Template, Credential) ->
|
render_str(Template, Credential) ->
|
||||||
emqx_placeholder:proc_tmpl(
|
emqx_placeholder:proc_tmpl(
|
||||||
Template,
|
Template,
|
||||||
Credential,
|
Credential,
|
||||||
#{return => full_binary, var_trans => fun handle_var/2}).
|
#{return => full_binary, var_trans => fun handle_var/2}
|
||||||
|
).
|
||||||
|
|
||||||
render_sql_params(ParamList, Credential) ->
|
render_sql_params(ParamList, Credential) ->
|
||||||
emqx_placeholder:proc_tmpl(
|
emqx_placeholder:proc_tmpl(
|
||||||
ParamList,
|
ParamList,
|
||||||
Credential,
|
Credential,
|
||||||
#{return => rawlist, var_trans => fun handle_sql_var/2}).
|
#{return => rawlist, var_trans => fun handle_sql_var/2}
|
||||||
|
).
|
||||||
|
|
||||||
is_superuser(#{<<"is_superuser">> := <<"">>}) ->
|
is_superuser(#{<<"is_superuser">> := <<"">>}) ->
|
||||||
#{is_superuser => false};
|
#{is_superuser => false};
|
||||||
|
|
@ -114,8 +123,9 @@ bin(X) -> X.
|
||||||
|
|
||||||
cleanup_resources() ->
|
cleanup_resources() ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun emqx_resource:remove_local/1,
|
fun emqx_resource:remove_local/1,
|
||||||
emqx_resource:list_group_instances(?RESOURCE_GROUP)).
|
emqx_resource:list_group_instances(?RESOURCE_GROUP)
|
||||||
|
).
|
||||||
|
|
||||||
make_resource_id(Name) ->
|
make_resource_id(Name) ->
|
||||||
NameBin = bin(Name),
|
NameBin = bin(Name),
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,12 @@
|
||||||
|
|
||||||
-define(TCP_DEFAULT, 'tcp:default').
|
-define(TCP_DEFAULT, 'tcp:default').
|
||||||
|
|
||||||
-define(
|
-define(assertAuthenticatorsMatch(Guard, Path),
|
||||||
assertAuthenticatorsMatch(Guard, Path),
|
|
||||||
(fun() ->
|
(fun() ->
|
||||||
{ok, 200, Response} = request(get, uri(Path)),
|
{ok, 200, Response} = request(get, uri(Path)),
|
||||||
?assertMatch(Guard, jiffy:decode(Response, [return_maps]))
|
?assertMatch(Guard, jiffy:decode(Response, [return_maps]))
|
||||||
end)()).
|
end)()
|
||||||
|
).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
@ -42,12 +42,14 @@ groups() ->
|
||||||
init_per_testcase(_, Config) ->
|
init_per_testcase(_, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[?CONF_NS_ATOM],
|
[?CONF_NS_ATOM],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[listeners, tcp, default, ?CONF_NS_ATOM],
|
[listeners, tcp, default, ?CONF_NS_ATOM],
|
||||||
?TCP_DEFAULT),
|
?TCP_DEFAULT
|
||||||
|
),
|
||||||
|
|
||||||
{atomic, ok} = mria:clear_table(emqx_authn_mnesia),
|
{atomic, ok} = mria:clear_table(emqx_authn_mnesia),
|
||||||
Config.
|
Config.
|
||||||
|
|
@ -55,8 +57,9 @@ init_per_testcase(_, Config) ->
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
_ = application:load(emqx_conf),
|
_ = application:load(emqx_conf),
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_authn, emqx_dashboard],
|
[emqx_authn, emqx_dashboard],
|
||||||
fun set_special_configs/1),
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
|
|
||||||
?AUTHN:delete_chain(?GLOBAL),
|
?AUTHN:delete_chain(?GLOBAL),
|
||||||
{ok, Chains} = ?AUTHN:list_chains(),
|
{ok, Chains} = ?AUTHN:list_chains(),
|
||||||
|
|
@ -117,108 +120,132 @@ t_listener_authenticator_import_users(_) ->
|
||||||
test_authenticator_import_users(["listeners", ?TCP_DEFAULT]).
|
test_authenticator_import_users(["listeners", ?TCP_DEFAULT]).
|
||||||
|
|
||||||
test_authenticators(PathPrefix) ->
|
test_authenticators(PathPrefix) ->
|
||||||
|
|
||||||
ValidConfig = emqx_authn_test_lib:http_example(),
|
ValidConfig = emqx_authn_test_lib:http_example(),
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
ValidConfig),
|
ValidConfig
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 409, _} = request(
|
{ok, 409, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
ValidConfig),
|
ValidConfig
|
||||||
|
),
|
||||||
|
|
||||||
InvalidConfig0 = ValidConfig#{method => <<"delete">>},
|
InvalidConfig0 = ValidConfig#{method => <<"delete">>},
|
||||||
{ok, 400, _} = request(
|
{ok, 400, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
InvalidConfig0),
|
InvalidConfig0
|
||||||
|
),
|
||||||
|
|
||||||
InvalidConfig1 = ValidConfig#{method => <<"get">>,
|
InvalidConfig1 = ValidConfig#{
|
||||||
headers => #{<<"content-type">> => <<"application/json">>}},
|
method => <<"get">>,
|
||||||
|
headers => #{<<"content-type">> => <<"application/json">>}
|
||||||
|
},
|
||||||
{ok, 400, _} = request(
|
{ok, 400, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
InvalidConfig1),
|
InvalidConfig1
|
||||||
|
),
|
||||||
|
|
||||||
?assertAuthenticatorsMatch(
|
?assertAuthenticatorsMatch(
|
||||||
[#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}],
|
[#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}],
|
||||||
PathPrefix ++ [?CONF_NS]).
|
PathPrefix ++ [?CONF_NS]
|
||||||
|
).
|
||||||
|
|
||||||
test_authenticator(PathPrefix) ->
|
test_authenticator(PathPrefix) ->
|
||||||
ValidConfig0 = emqx_authn_test_lib:http_example(),
|
ValidConfig0 = emqx_authn_test_lib:http_example(),
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
ValidConfig0),
|
ValidConfig0
|
||||||
|
),
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
get,
|
get,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 200, Res} = request(
|
{ok, 200, Res} = request(
|
||||||
get,
|
get,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])
|
||||||
|
),
|
||||||
{ok, RList} = emqx_json:safe_decode(Res),
|
{ok, RList} = emqx_json:safe_decode(Res),
|
||||||
Snd = fun ({_, Val}) -> Val end,
|
Snd = fun({_, Val}) -> Val end,
|
||||||
LookupVal = fun LookupV(List, RestJson) ->
|
LookupVal = fun LookupV(List, RestJson) ->
|
||||||
case List of
|
case List of
|
||||||
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
|
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
|
||||||
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
|
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
LookFun = fun (List) -> LookupVal(List, RList) end,
|
LookFun = fun(List) -> LookupVal(List, RList) end,
|
||||||
MetricsList = [{<<"failed">>, 0},
|
MetricsList = [
|
||||||
{<<"matched">>, 0},
|
{<<"failed">>, 0},
|
||||||
{<<"rate">>, 0.0},
|
{<<"matched">>, 0},
|
||||||
{<<"rate_last5m">>, 0.0},
|
{<<"rate">>, 0.0},
|
||||||
{<<"rate_max">>, 0.0},
|
{<<"rate_last5m">>, 0.0},
|
||||||
{<<"success">>, 0}],
|
{<<"rate_max">>, 0.0},
|
||||||
EqualFun = fun ({M, V}) ->
|
{<<"success">>, 0}
|
||||||
?assertEqual(V, LookFun([<<"metrics">>,
|
],
|
||||||
M]
|
EqualFun = fun({M, V}) ->
|
||||||
)
|
?assertEqual(
|
||||||
) end,
|
V,
|
||||||
|
LookFun([
|
||||||
|
<<"metrics">>,
|
||||||
|
M
|
||||||
|
])
|
||||||
|
)
|
||||||
|
end,
|
||||||
lists:map(EqualFun, MetricsList),
|
lists:map(EqualFun, MetricsList),
|
||||||
?assertEqual(<<"connected">>,
|
?assertEqual(
|
||||||
LookFun([<<"status">>
|
<<"connected">>,
|
||||||
])),
|
LookFun([<<"status">>])
|
||||||
|
),
|
||||||
{ok, 404, _} = request(
|
{ok, 404, _} = request(
|
||||||
get,
|
get,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 404, _} = request(
|
{ok, 404, _} = request(
|
||||||
put,
|
put,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database"]),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database"]),
|
||||||
emqx_authn_test_lib:built_in_database_example()),
|
emqx_authn_test_lib:built_in_database_example()
|
||||||
|
),
|
||||||
|
|
||||||
InvalidConfig0 = ValidConfig0#{method => <<"delete">>},
|
InvalidConfig0 = ValidConfig0#{method => <<"delete">>},
|
||||||
{ok, 400, _} = request(
|
{ok, 400, _} = request(
|
||||||
put,
|
put,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||||
InvalidConfig0),
|
InvalidConfig0
|
||||||
|
),
|
||||||
|
|
||||||
InvalidConfig1 = ValidConfig0#{method => <<"get">>,
|
InvalidConfig1 = ValidConfig0#{
|
||||||
headers => #{<<"content-type">> => <<"application/json">>}},
|
method => <<"get">>,
|
||||||
|
headers => #{<<"content-type">> => <<"application/json">>}
|
||||||
|
},
|
||||||
{ok, 400, _} = request(
|
{ok, 400, _} = request(
|
||||||
put,
|
put,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||||
InvalidConfig1),
|
InvalidConfig1
|
||||||
|
),
|
||||||
|
|
||||||
ValidConfig1 = ValidConfig0#{pool_size => 9},
|
ValidConfig1 = ValidConfig0#{pool_size => 9},
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
put,
|
put,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||||
ValidConfig1),
|
ValidConfig1
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 404, _} = request(
|
{ok, 404, _} = request(
|
||||||
delete,
|
delete,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
delete,
|
delete,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])
|
||||||
|
),
|
||||||
|
|
||||||
?assertAuthenticatorsMatch([], PathPrefix ++ [?CONF_NS]).
|
?assertAuthenticatorsMatch([], PathPrefix ++ [?CONF_NS]).
|
||||||
|
|
||||||
|
|
@ -226,64 +253,78 @@ test_authenticator_users(PathPrefix) ->
|
||||||
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
|
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
|
||||||
|
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
emqx_authn_test_lib:built_in_database_example()),
|
emqx_authn_test_lib:built_in_database_example()
|
||||||
|
),
|
||||||
|
|
||||||
InvalidUsers = [
|
InvalidUsers = [
|
||||||
#{clientid => <<"u1">>, password => <<"p1">>},
|
#{clientid => <<"u1">>, password => <<"p1">>},
|
||||||
#{user_id => <<"u2">>},
|
#{user_id => <<"u2">>},
|
||||||
#{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}],
|
#{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}
|
||||||
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(User) -> {ok, 400, _} = request(post, UsersUri, User) end,
|
fun(User) -> {ok, 400, _} = request(post, UsersUri, User) end,
|
||||||
InvalidUsers),
|
InvalidUsers
|
||||||
|
),
|
||||||
|
|
||||||
ValidUsers = [
|
ValidUsers = [
|
||||||
#{user_id => <<"u1">>, password => <<"p1">>},
|
#{user_id => <<"u1">>, password => <<"p1">>},
|
||||||
#{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true},
|
#{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true},
|
||||||
#{user_id => <<"u3">>, password => <<"p3">>}],
|
#{user_id => <<"u3">>, password => <<"p3">>}
|
||||||
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(User) ->
|
fun(User) ->
|
||||||
{ok, 201, UserData} = request(post, UsersUri, User),
|
{ok, 201, UserData} = request(post, UsersUri, User),
|
||||||
CreatedUser = jiffy:decode(UserData, [return_maps]),
|
CreatedUser = jiffy:decode(UserData, [return_maps]),
|
||||||
?assertMatch(#{<<"user_id">> := _}, CreatedUser)
|
?assertMatch(#{<<"user_id">> := _}, CreatedUser)
|
||||||
end,
|
end,
|
||||||
ValidUsers),
|
ValidUsers
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 200, Page1Data} = request(get, UsersUri ++ "?page=1&limit=2"),
|
{ok, 200, Page1Data} = request(get, UsersUri ++ "?page=1&limit=2"),
|
||||||
|
|
||||||
#{<<"data">> := Page1Users,
|
#{
|
||||||
<<"meta">> :=
|
<<"data">> := Page1Users,
|
||||||
#{<<"page">> := 1,
|
<<"meta">> :=
|
||||||
<<"limit">> := 2,
|
#{
|
||||||
<<"count">> := 3}} =
|
<<"page">> := 1,
|
||||||
jiffy:decode(Page1Data, [return_maps]),
|
<<"limit">> := 2,
|
||||||
|
<<"count">> := 3
|
||||||
|
}
|
||||||
|
} =
|
||||||
|
jiffy:decode(Page1Data, [return_maps]),
|
||||||
|
|
||||||
{ok, 200, Page2Data} = request(get, UsersUri ++ "?page=2&limit=2"),
|
{ok, 200, Page2Data} = request(get, UsersUri ++ "?page=2&limit=2"),
|
||||||
|
|
||||||
#{<<"data">> := Page2Users,
|
#{
|
||||||
<<"meta">> :=
|
<<"data">> := Page2Users,
|
||||||
#{<<"page">> := 2,
|
<<"meta">> :=
|
||||||
<<"limit">> := 2,
|
#{
|
||||||
<<"count">> := 3}} = jiffy:decode(Page2Data, [return_maps]),
|
<<"page">> := 2,
|
||||||
|
<<"limit">> := 2,
|
||||||
|
<<"count">> := 3
|
||||||
|
}
|
||||||
|
} = jiffy:decode(Page2Data, [return_maps]),
|
||||||
|
|
||||||
?assertEqual(2, length(Page1Users)),
|
?assertEqual(2, length(Page1Users)),
|
||||||
?assertEqual(1, length(Page2Users)),
|
?assertEqual(1, length(Page2Users)),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
[<<"u1">>, <<"u2">>, <<"u3">>],
|
[<<"u1">>, <<"u2">>, <<"u3">>],
|
||||||
lists:usort([ UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])).
|
lists:usort([UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])
|
||||||
|
).
|
||||||
|
|
||||||
test_authenticator_user(PathPrefix) ->
|
test_authenticator_user(PathPrefix) ->
|
||||||
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
|
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
|
||||||
|
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
emqx_authn_test_lib:built_in_database_example()),
|
emqx_authn_test_lib:built_in_database_example()
|
||||||
|
),
|
||||||
|
|
||||||
User = #{user_id => <<"u1">>, password => <<"p1">>},
|
User = #{user_id => <<"u1">>, password => <<"p1">>},
|
||||||
{ok, 201, _} = request(post, UsersUri, User),
|
{ok, 201, _} = request(post, UsersUri, User),
|
||||||
|
|
@ -299,141 +340,161 @@ test_authenticator_user(PathPrefix) ->
|
||||||
?assertNotMatch(#{<<"password">> := _}, FetchedUser),
|
?assertNotMatch(#{<<"password">> := _}, FetchedUser),
|
||||||
|
|
||||||
ValidUserUpdates = [
|
ValidUserUpdates = [
|
||||||
#{password => <<"p1">>},
|
#{password => <<"p1">>},
|
||||||
#{password => <<"p1">>, is_superuser => true}],
|
#{password => <<"p1">>, is_superuser => true}
|
||||||
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(UserUpdate) -> {ok, 200, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
|
fun(UserUpdate) -> {ok, 200, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
|
||||||
ValidUserUpdates),
|
ValidUserUpdates
|
||||||
|
),
|
||||||
|
|
||||||
InvalidUserUpdates = [#{user_id => <<"u1">>, password => <<"p1">>}],
|
InvalidUserUpdates = [#{user_id => <<"u1">>, password => <<"p1">>}],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(UserUpdate) -> {ok, 400, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
|
fun(UserUpdate) -> {ok, 400, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
|
||||||
InvalidUserUpdates),
|
InvalidUserUpdates
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 404, _} = request(delete, UsersUri ++ "/u123"),
|
{ok, 404, _} = request(delete, UsersUri ++ "/u123"),
|
||||||
{ok, 204, _} = request(delete, UsersUri ++ "/u1").
|
{ok, 204, _} = request(delete, UsersUri ++ "/u1").
|
||||||
|
|
||||||
test_authenticator_move(PathPrefix) ->
|
test_authenticator_move(PathPrefix) ->
|
||||||
AuthenticatorConfs = [
|
AuthenticatorConfs = [
|
||||||
emqx_authn_test_lib:http_example(),
|
emqx_authn_test_lib:http_example(),
|
||||||
emqx_authn_test_lib:jwt_example(),
|
emqx_authn_test_lib:jwt_example(),
|
||||||
emqx_authn_test_lib:built_in_database_example()
|
emqx_authn_test_lib:built_in_database_example()
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Conf) ->
|
fun(Conf) ->
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
Conf)
|
Conf
|
||||||
end,
|
)
|
||||||
AuthenticatorConfs),
|
end,
|
||||||
|
AuthenticatorConfs
|
||||||
|
),
|
||||||
|
|
||||||
?assertAuthenticatorsMatch(
|
?assertAuthenticatorsMatch(
|
||||||
[
|
[
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||||
#{<<"mechanism">> := <<"jwt">>},
|
#{<<"mechanism">> := <<"jwt">>},
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||||
],
|
],
|
||||||
PathPrefix ++ [?CONF_NS]),
|
PathPrefix ++ [?CONF_NS]
|
||||||
|
),
|
||||||
|
|
||||||
%% Invalid moves
|
%% Invalid moves
|
||||||
|
|
||||||
{ok, 400, _} = request(
|
{ok, 400, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||||
#{position => <<"up">>}),
|
#{position => <<"up">>}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 400, _} = request(
|
{ok, 400, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 404, _} = request(
|
{ok, 404, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||||
#{position => <<"before:invalid">>}),
|
#{position => <<"before:invalid">>}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 404, _} = request(
|
{ok, 404, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||||
#{position => <<"before:password_based:redis">>}),
|
#{position => <<"before:password_based:redis">>}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 404, _} = request(
|
{ok, 404, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||||
#{position => <<"before:password_based:redis">>}),
|
#{position => <<"before:password_based:redis">>}
|
||||||
|
),
|
||||||
|
|
||||||
%% Valid moves
|
%% Valid moves
|
||||||
|
|
||||||
%% test front
|
%% test front
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||||
#{position => <<"front">>}),
|
#{position => <<"front">>}
|
||||||
|
),
|
||||||
|
|
||||||
?assertAuthenticatorsMatch(
|
?assertAuthenticatorsMatch(
|
||||||
[
|
[
|
||||||
#{<<"mechanism">> := <<"jwt">>},
|
#{<<"mechanism">> := <<"jwt">>},
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||||
],
|
],
|
||||||
PathPrefix ++ [?CONF_NS]),
|
PathPrefix ++ [?CONF_NS]
|
||||||
|
),
|
||||||
|
|
||||||
%% test rear
|
%% test rear
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||||
#{position => <<"rear">>}),
|
#{position => <<"rear">>}
|
||||||
|
),
|
||||||
|
|
||||||
?assertAuthenticatorsMatch(
|
?assertAuthenticatorsMatch(
|
||||||
[
|
[
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
|
||||||
#{<<"mechanism">> := <<"jwt">>}
|
#{<<"mechanism">> := <<"jwt">>}
|
||||||
],
|
],
|
||||||
PathPrefix ++ [?CONF_NS]),
|
PathPrefix ++ [?CONF_NS]
|
||||||
|
),
|
||||||
|
|
||||||
%% test before
|
%% test before
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||||
#{position => <<"before:password_based:built_in_database">>}),
|
#{position => <<"before:password_based:built_in_database">>}
|
||||||
|
),
|
||||||
|
|
||||||
?assertAuthenticatorsMatch(
|
?assertAuthenticatorsMatch(
|
||||||
[
|
[
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||||
#{<<"mechanism">> := <<"jwt">>},
|
#{<<"mechanism">> := <<"jwt">>},
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||||
],
|
],
|
||||||
PathPrefix ++ [?CONF_NS]),
|
PathPrefix ++ [?CONF_NS]
|
||||||
|
),
|
||||||
|
|
||||||
%% test after
|
%% test after
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based%3Abuilt_in_database", "move"]),
|
uri(PathPrefix ++ [?CONF_NS, "password_based%3Abuilt_in_database", "move"]),
|
||||||
#{position => <<"after:password_based:http">>}),
|
#{position => <<"after:password_based:http">>}
|
||||||
|
),
|
||||||
|
|
||||||
?assertAuthenticatorsMatch(
|
?assertAuthenticatorsMatch(
|
||||||
[
|
[
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
|
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
|
||||||
#{<<"mechanism">> := <<"jwt">>}
|
#{<<"mechanism">> := <<"jwt">>}
|
||||||
],
|
],
|
||||||
PathPrefix ++ [?CONF_NS]).
|
PathPrefix ++ [?CONF_NS]
|
||||||
|
).
|
||||||
|
|
||||||
test_authenticator_import_users(PathPrefix) ->
|
test_authenticator_import_users(PathPrefix) ->
|
||||||
ImportUri = uri(
|
ImportUri = uri(
|
||||||
PathPrefix ++
|
PathPrefix ++
|
||||||
[?CONF_NS, "password_based:built_in_database", "import_users"]),
|
[?CONF_NS, "password_based:built_in_database", "import_users"]
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
post,
|
post,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS]),
|
||||||
emqx_authn_test_lib:built_in_database_example()),
|
emqx_authn_test_lib:built_in_database_example()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 400, _} = request(post, ImportUri, #{}),
|
{ok, 400, _} = request(post, ImportUri, #{}),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@
|
||||||
|
|
||||||
-define(HTTP_PORT, 33333).
|
-define(HTTP_PORT, 33333).
|
||||||
-define(HTTP_PATH, "/auth").
|
-define(HTTP_PATH, "/auth").
|
||||||
-define(CREDENTIALS, #{username => <<"plain">>,
|
-define(CREDENTIALS, #{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}).
|
protocol => mqtt
|
||||||
|
}).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
@ -46,8 +46,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_) ->
|
end_per_suite(_) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
emqx_common_test_helpers:stop_apps([emqx_authn]),
|
emqx_common_test_helpers:stop_apps([emqx_authn]),
|
||||||
application:stop(cowboy),
|
application:stop(cowboy),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -55,8 +56,9 @@ end_per_suite(_) ->
|
||||||
init_per_testcase(_Case, Config) ->
|
init_per_testcase(_Case, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
{ok, _} = emqx_authn_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH),
|
{ok, _} = emqx_authn_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
|
@ -71,8 +73,9 @@ t_create(_Config) ->
|
||||||
AuthConfig = raw_http_auth_config(),
|
AuthConfig = raw_http_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_http}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
{ok, [#{provider := emqx_authn_http}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
||||||
|
|
||||||
|
|
@ -81,83 +84,96 @@ t_create_invalid(_Config) ->
|
||||||
|
|
||||||
InvalidConfigs =
|
InvalidConfigs =
|
||||||
[
|
[
|
||||||
AuthConfig#{headers => []},
|
AuthConfig#{headers => []},
|
||||||
AuthConfig#{method => delete}
|
AuthConfig#{method => delete}
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Config) ->
|
fun(Config) ->
|
||||||
ct:pal("creating authenticator with invalid config: ~p", [Config]),
|
ct:pal("creating authenticator with invalid config: ~p", [Config]),
|
||||||
{error, _} =
|
{error, _} =
|
||||||
try
|
try
|
||||||
emqx:update_config(
|
emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config})
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
catch
|
)
|
||||||
throw:Error ->
|
catch
|
||||||
{error, Error}
|
throw:Error ->
|
||||||
end,
|
{error, Error}
|
||||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
end,
|
||||||
end,
|
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||||
InvalidConfigs).
|
end,
|
||||||
|
InvalidConfigs
|
||||||
|
).
|
||||||
|
|
||||||
t_authenticate(_Config) ->
|
t_authenticate(_Config) ->
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(Sample) ->
|
fun(Sample) ->
|
||||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||||
test_user_auth(Sample)
|
test_user_auth(Sample)
|
||||||
end,
|
end,
|
||||||
samples()).
|
samples()
|
||||||
|
).
|
||||||
|
|
||||||
test_user_auth(#{handler := Handler,
|
test_user_auth(#{
|
||||||
config_params := SpecificConfgParams,
|
handler := Handler,
|
||||||
result := Result}) ->
|
config_params := SpecificConfgParams,
|
||||||
|
result := Result
|
||||||
|
}) ->
|
||||||
AuthConfig = maps:merge(raw_http_auth_config(), SpecificConfgParams),
|
AuthConfig = maps:merge(raw_http_auth_config(), SpecificConfgParams),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_http_test_server:set_handler(Handler),
|
ok = emqx_authn_http_test_server:set_handler(Handler),
|
||||||
|
|
||||||
?assertEqual(Result, emqx_access_control:authenticate(?CREDENTIALS)),
|
?assertEqual(Result, emqx_access_control:authenticate(?CREDENTIALS)),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL).
|
?GLOBAL
|
||||||
|
).
|
||||||
|
|
||||||
t_destroy(_Config) ->
|
t_destroy(_Config) ->
|
||||||
AuthConfig = raw_http_auth_config(),
|
AuthConfig = raw_http_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_http_test_server:set_handler(
|
ok = emqx_authn_http_test_server:set_handler(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(200, Req0),
|
Req = cowboy_req:reply(200, Req0),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_http, state := State}]}
|
{ok, [#{provider := emqx_authn_http, state := State}]} =
|
||||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
emqx_authentication:list_authenticators(?GLOBAL),
|
||||||
|
|
||||||
Credentials = maps:with([username, password], ?CREDENTIALS),
|
Credentials = maps:with([username, password], ?CREDENTIALS),
|
||||||
|
|
||||||
{ok, _} = emqx_authn_http:authenticate(
|
{ok, _} = emqx_authn_http:authenticate(
|
||||||
Credentials,
|
Credentials,
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
|
|
||||||
% Authenticator should not be usable anymore
|
% Authenticator should not be usable anymore
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
ignore,
|
ignore,
|
||||||
emqx_authn_http:authenticate(
|
emqx_authn_http:authenticate(
|
||||||
Credentials,
|
Credentials,
|
||||||
State)).
|
State
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
t_update(_Config) ->
|
t_update(_Config) ->
|
||||||
CorrectConfig = raw_http_auth_config(),
|
CorrectConfig = raw_http_auth_config(),
|
||||||
|
|
@ -165,74 +181,80 @@ t_update(_Config) ->
|
||||||
CorrectConfig#{url => <<"http://127.0.0.1:33333/invalid">>},
|
CorrectConfig#{url => <<"http://127.0.0.1:33333/invalid">>},
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_http_test_server:set_handler(
|
ok = emqx_authn_http_test_server:set_handler(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(200, Req0),
|
Req = cowboy_req:reply(200, Req0),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
|
|
||||||
{error, not_authorized} = emqx_access_control:authenticate(?CREDENTIALS),
|
{error, not_authorized} = emqx_access_control:authenticate(?CREDENTIALS),
|
||||||
|
|
||||||
% We update with config with correct query, provider should update and work properly
|
% We update with config with correct query, provider should update and work properly
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{update_authenticator, ?GLOBAL, <<"password_based:http">>, CorrectConfig}),
|
{update_authenticator, ?GLOBAL, <<"password_based:http">>, CorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok,_} = emqx_access_control:authenticate(?CREDENTIALS).
|
{ok, _} = emqx_access_control:authenticate(?CREDENTIALS).
|
||||||
|
|
||||||
t_is_superuser(_Config) ->
|
t_is_superuser(_Config) ->
|
||||||
Config = raw_http_auth_config(),
|
Config = raw_http_auth_config(),
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
|
),
|
||||||
|
|
||||||
Checks = [
|
Checks = [
|
||||||
{json, <<"0">>, false},
|
{json, <<"0">>, false},
|
||||||
{json, <<"">>, false},
|
{json, <<"">>, false},
|
||||||
{json, null, false},
|
{json, null, false},
|
||||||
{json, 0, false},
|
{json, 0, false},
|
||||||
|
|
||||||
{json, <<"1">>, true},
|
{json, <<"1">>, true},
|
||||||
{json, <<"val">>, true},
|
{json, <<"val">>, true},
|
||||||
{json, 1, true},
|
{json, 1, true},
|
||||||
{json, 123, true},
|
{json, 123, true},
|
||||||
|
|
||||||
{form, <<"0">>, false},
|
{form, <<"0">>, false},
|
||||||
{form, <<"">>, false},
|
{form, <<"">>, false},
|
||||||
|
|
||||||
{form, <<"1">>, true},
|
{form, <<"1">>, true},
|
||||||
{form, <<"val">>, true}
|
{form, <<"val">>, true}
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(fun test_is_superuser/1, Checks).
|
lists:foreach(fun test_is_superuser/1, Checks).
|
||||||
|
|
||||||
test_is_superuser({Kind, Value, ExpectedValue}) ->
|
test_is_superuser({Kind, Value, ExpectedValue}) ->
|
||||||
|
{ContentType, Res} =
|
||||||
{ContentType, Res} = case Kind of
|
case Kind of
|
||||||
json ->
|
json ->
|
||||||
{<<"application/json">>,
|
{<<"application/json">>, jiffy:encode(#{is_superuser => Value})};
|
||||||
jiffy:encode(#{is_superuser => Value})};
|
form ->
|
||||||
form ->
|
{<<"application/x-www-form-urlencoded">>,
|
||||||
{<<"application/x-www-form-urlencoded">>,
|
iolist_to_binary([<<"is_superuser=">>, Value])}
|
||||||
iolist_to_binary([<<"is_superuser=">>, Value])}
|
end,
|
||||||
end,
|
|
||||||
|
|
||||||
ok = emqx_authn_http_test_server:set_handler(
|
ok = emqx_authn_http_test_server:set_handler(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(
|
Req = cowboy_req:reply(
|
||||||
200,
|
200,
|
||||||
#{<<"content-type">> => ContentType},
|
#{<<"content-type">> => ContentType},
|
||||||
Res,
|
Res,
|
||||||
Req0),
|
Req0
|
||||||
{ok, Req, State}
|
),
|
||||||
end),
|
{ok, Req, State}
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, #{is_superuser := ExpectedValue}},
|
{ok, #{is_superuser := ExpectedValue}},
|
||||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
emqx_access_control:authenticate(?CREDENTIALS)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -252,138 +274,159 @@ raw_http_auth_config() ->
|
||||||
|
|
||||||
samples() ->
|
samples() ->
|
||||||
[
|
[
|
||||||
%% simple get request
|
%% simple get request
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
#{username := <<"plain">>,
|
handler => fun(Req0, State) ->
|
||||||
password := <<"plain">>
|
#{
|
||||||
} = cowboy_req:match_qs([username, password], Req0),
|
username := <<"plain">>,
|
||||||
|
password := <<"plain">>
|
||||||
|
} = cowboy_req:match_qs([username, password], Req0),
|
||||||
|
|
||||||
Req = cowboy_req:reply(200, Req0),
|
Req = cowboy_req:reply(200, Req0),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end,
|
end,
|
||||||
config_params => #{},
|
config_params => #{},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {ok, #{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
|
||||||
%% get request with json body response
|
%% get request with json body response
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
Req = cowboy_req:reply(
|
handler => fun(Req0, State) ->
|
||||||
200,
|
Req = cowboy_req:reply(
|
||||||
#{<<"content-type">> => <<"application/json">>},
|
200,
|
||||||
jiffy:encode(#{is_superuser => true}),
|
#{<<"content-type">> => <<"application/json">>},
|
||||||
Req0),
|
jiffy:encode(#{is_superuser => true}),
|
||||||
{ok, Req, State}
|
Req0
|
||||||
end,
|
),
|
||||||
config_params => #{},
|
{ok, Req, State}
|
||||||
result => {ok,#{is_superuser => true, user_property => #{}}}
|
end,
|
||||||
},
|
config_params => #{},
|
||||||
|
result => {ok, #{is_superuser => true, user_property => #{}}}
|
||||||
|
},
|
||||||
|
|
||||||
%% get request with url-form-encoded body response
|
%% get request with url-form-encoded body response
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
Req = cowboy_req:reply(
|
handler => fun(Req0, State) ->
|
||||||
200,
|
Req = cowboy_req:reply(
|
||||||
#{<<"content-type">> =>
|
200,
|
||||||
<<"application/x-www-form-urlencoded">>},
|
#{
|
||||||
<<"is_superuser=true">>,
|
<<"content-type">> =>
|
||||||
Req0),
|
<<"application/x-www-form-urlencoded">>
|
||||||
{ok, Req, State}
|
},
|
||||||
end,
|
<<"is_superuser=true">>,
|
||||||
config_params => #{},
|
Req0
|
||||||
result => {ok,#{is_superuser => true, user_property => #{}}}
|
),
|
||||||
},
|
{ok, Req, State}
|
||||||
|
end,
|
||||||
|
config_params => #{},
|
||||||
|
result => {ok, #{is_superuser => true, user_property => #{}}}
|
||||||
|
},
|
||||||
|
|
||||||
%% get request with response of unknown encoding
|
%% get request with response of unknown encoding
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
Req = cowboy_req:reply(
|
handler => fun(Req0, State) ->
|
||||||
200,
|
Req = cowboy_req:reply(
|
||||||
#{<<"content-type">> =>
|
200,
|
||||||
<<"test/plain">>},
|
#{
|
||||||
<<"is_superuser=true">>,
|
<<"content-type">> =>
|
||||||
Req0),
|
<<"test/plain">>
|
||||||
{ok, Req, State}
|
},
|
||||||
end,
|
<<"is_superuser=true">>,
|
||||||
config_params => #{},
|
Req0
|
||||||
result => {ok,#{is_superuser => false}}
|
),
|
||||||
},
|
{ok, Req, State}
|
||||||
|
end,
|
||||||
|
config_params => #{},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
%% simple post request, application/json
|
%% simple post request, application/json
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
|
handler => fun(Req0, State) ->
|
||||||
#{<<"username">> := <<"plain">>,
|
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
|
||||||
<<"password">> := <<"plain">>
|
#{
|
||||||
} = jiffy:decode(RawBody, [return_maps]),
|
<<"username">> := <<"plain">>,
|
||||||
Req = cowboy_req:reply(200, Req1),
|
<<"password">> := <<"plain">>
|
||||||
{ok, Req, State}
|
} = jiffy:decode(RawBody, [return_maps]),
|
||||||
end,
|
Req = cowboy_req:reply(200, Req1),
|
||||||
config_params => #{
|
{ok, Req, State}
|
||||||
method => post,
|
end,
|
||||||
headers => #{<<"content-type">> => <<"application/json">>}
|
config_params => #{
|
||||||
},
|
method => post,
|
||||||
result => {ok,#{is_superuser => false}}
|
headers => #{<<"content-type">> => <<"application/json">>}
|
||||||
},
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
%% simple post request, application/x-www-form-urlencoded
|
%% simple post request, application/x-www-form-urlencoded
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
{ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0),
|
handler => fun(Req0, State) ->
|
||||||
#{<<"username">> := <<"plain">>,
|
{ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0),
|
||||||
<<"password">> := <<"plain">>
|
#{
|
||||||
} = maps:from_list(PostVars),
|
<<"username">> := <<"plain">>,
|
||||||
Req = cowboy_req:reply(200, Req1),
|
<<"password">> := <<"plain">>
|
||||||
{ok, Req, State}
|
} = maps:from_list(PostVars),
|
||||||
end,
|
Req = cowboy_req:reply(200, Req1),
|
||||||
config_params => #{
|
{ok, Req, State}
|
||||||
method => post,
|
end,
|
||||||
headers => #{<<"content-type">> =>
|
config_params => #{
|
||||||
<<"application/x-www-form-urlencoded">>}
|
method => post,
|
||||||
},
|
headers => #{
|
||||||
result => {ok,#{is_superuser => false}}
|
<<"content-type">> =>
|
||||||
}
|
<<"application/x-www-form-urlencoded">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
}#{
|
||||||
|
%% 204 code
|
||||||
|
handler => fun(Req0, State) ->
|
||||||
|
Req = cowboy_req:reply(204, Req0),
|
||||||
|
{ok, Req, State}
|
||||||
|
end,
|
||||||
|
config_params => #{},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
%% 204 code
|
%% custom headers
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
Req = cowboy_req:reply(204, Req0),
|
handler => fun(Req0, State) ->
|
||||||
{ok, Req, State}
|
<<"Test Value">> = cowboy_req:header(<<"x-test-header">>, Req0),
|
||||||
end,
|
Req = cowboy_req:reply(200, Req0),
|
||||||
config_params => #{},
|
{ok, Req, State}
|
||||||
result => {ok,#{is_superuser => false}}
|
end,
|
||||||
},
|
config_params => #{},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
%% custom headers
|
%% 400 code
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
<<"Test Value">> = cowboy_req:header(<<"x-test-header">>, Req0),
|
handler => fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(200, Req0),
|
Req = cowboy_req:reply(400, Req0),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end,
|
end,
|
||||||
config_params => #{},
|
config_params => #{},
|
||||||
result => {ok,#{is_superuser => false}}
|
result => {error, not_authorized}
|
||||||
},
|
},
|
||||||
|
|
||||||
%% 400 code
|
%% 500 code
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
Req = cowboy_req:reply(400, Req0),
|
handler => fun(Req0, State) ->
|
||||||
{ok, Req, State}
|
Req = cowboy_req:reply(500, Req0),
|
||||||
end,
|
{ok, Req, State}
|
||||||
config_params => #{},
|
end,
|
||||||
result => {error,not_authorized}
|
config_params => #{},
|
||||||
},
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
%% 500 code
|
%% Handling error
|
||||||
#{handler => fun(Req0, State) ->
|
#{
|
||||||
Req = cowboy_req:reply(500, Req0),
|
handler => fun(Req0, State) ->
|
||||||
{ok, Req, State}
|
error(woops),
|
||||||
end,
|
{ok, Req0, State}
|
||||||
config_params => #{},
|
end,
|
||||||
result => {error,not_authorized}
|
config_params => #{},
|
||||||
},
|
result => {error, not_authorized}
|
||||||
|
}
|
||||||
%% Handling error
|
|
||||||
#{handler => fun(Req0, State) ->
|
|
||||||
error(woops),
|
|
||||||
{ok, Req0, State}
|
|
||||||
end,
|
|
||||||
config_params => #{},
|
|
||||||
result => {error,not_authorized}
|
|
||||||
}
|
|
||||||
].
|
].
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,12 @@
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
% API
|
% API
|
||||||
-export([start_link/2,
|
-export([
|
||||||
start_link/3,
|
start_link/2,
|
||||||
stop/0,
|
start_link/3,
|
||||||
set_handler/1
|
stop/0,
|
||||||
]).
|
set_handler/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% API
|
%% API
|
||||||
|
|
@ -55,10 +56,11 @@ set_handler(F) when is_function(F, 2) ->
|
||||||
|
|
||||||
init([Port, Path, SSLOpts]) ->
|
init([Port, Path, SSLOpts]) ->
|
||||||
Dispatch = cowboy_router:compile(
|
Dispatch = cowboy_router:compile(
|
||||||
[
|
[
|
||||||
{'_', [{Path, ?MODULE, []}]}
|
{'_', [{Path, ?MODULE, []}]}
|
||||||
]),
|
]
|
||||||
|
),
|
||||||
|
|
||||||
ProtoOpts = #{env => #{dispatch => Dispatch}},
|
ProtoOpts = #{env => #{dispatch => Dispatch}},
|
||||||
|
|
||||||
Tab = ets:new(?MODULE, [set, named_table, public]),
|
Tab = ets:new(?MODULE, [set, named_table, public]),
|
||||||
|
|
@ -83,23 +85,28 @@ init(Req, State) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
transport_settings(Port, false) ->
|
transport_settings(Port, false) ->
|
||||||
TransOpts = #{socket_opts => [{port, Port}],
|
TransOpts = #{
|
||||||
connection_type => supervisor},
|
socket_opts => [{port, Port}],
|
||||||
|
connection_type => supervisor
|
||||||
|
},
|
||||||
{ranch_tcp, TransOpts, cowboy_clear};
|
{ranch_tcp, TransOpts, cowboy_clear};
|
||||||
|
|
||||||
transport_settings(Port, SSLOpts) ->
|
transport_settings(Port, SSLOpts) ->
|
||||||
TransOpts = #{socket_opts => [{port, Port},
|
TransOpts = #{
|
||||||
{next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
|
socket_opts => [
|
||||||
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|
{port, Port},
|
||||||
| SSLOpts],
|
{next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
|
||||||
connection_type => supervisor},
|
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|
||||||
|
| SSLOpts
|
||||||
|
],
|
||||||
|
connection_type => supervisor
|
||||||
|
},
|
||||||
{ranch_ssl, TransOpts, cowboy_tls}.
|
{ranch_ssl, TransOpts, cowboy_tls}.
|
||||||
|
|
||||||
default_handler(Req0, State) ->
|
default_handler(Req0, State) ->
|
||||||
Req = cowboy_req:reply(
|
Req = cowboy_req:reply(
|
||||||
400,
|
400,
|
||||||
#{<<"content-type">> => <<"text/plain">>},
|
#{<<"content-type">> => <<"text/plain">>},
|
||||||
<<"">>,
|
<<"">>,
|
||||||
Req0),
|
Req0
|
||||||
|
),
|
||||||
{ok, Req, State}.
|
{ok, Req, State}.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@
|
||||||
|
|
||||||
-define(HTTPS_PORT, 33333).
|
-define(HTTPS_PORT, 33333).
|
||||||
-define(HTTPS_PATH, "/auth").
|
-define(HTTPS_PATH, "/auth").
|
||||||
-define(CREDENTIALS, #{username => <<"plain">>,
|
-define(CREDENTIALS, #{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}).
|
protocol => mqtt
|
||||||
|
}).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
@ -46,8 +46,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_) ->
|
end_per_suite(_) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
emqx_common_test_helpers:stop_apps([emqx_authn]),
|
emqx_common_test_helpers:stop_apps([emqx_authn]),
|
||||||
application:stop(cowboy),
|
application:stop(cowboy),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -55,8 +56,9 @@ end_per_suite(_) ->
|
||||||
init_per_testcase(_Case, Config) ->
|
init_per_testcase(_Case, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
{ok, _} = emqx_authn_http_test_server:start_link(?HTTPS_PORT, ?HTTPS_PATH, server_ssl_opts()),
|
{ok, _} = emqx_authn_http_test_server:start_link(?HTTPS_PORT, ?HTTPS_PATH, server_ssl_opts()),
|
||||||
ok = emqx_authn_http_test_server:set_handler(fun cowboy_handler/2),
|
ok = emqx_authn_http_test_server:set_handler(fun cowboy_handler/2),
|
||||||
Config.
|
Config.
|
||||||
|
|
@ -70,46 +72,62 @@ end_per_testcase(_Case, _Config) ->
|
||||||
|
|
||||||
t_create(_Config) ->
|
t_create(_Config) ->
|
||||||
{ok, _} = create_https_auth_with_ssl_opts(
|
{ok, _} = create_https_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}),
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
|
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
emqx_access_control:authenticate(?CREDENTIALS)
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid_domain(_Config) ->
|
t_create_invalid_domain(_Config) ->
|
||||||
{ok, _} = create_https_auth_with_ssl_opts(
|
{ok, _} = create_https_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}),
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
|
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
{error, not_authorized},
|
{error, not_authorized},
|
||||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
emqx_access_control:authenticate(?CREDENTIALS)
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid_version(_Config) ->
|
t_create_invalid_version(_Config) ->
|
||||||
{ok, _} = create_https_auth_with_ssl_opts(
|
{ok, _} = create_https_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.1">>]}),
|
<<"verify">> => <<"verify_peer">>,
|
||||||
|
<<"versions">> => [<<"tlsv1.1">>]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
{error, not_authorized},
|
{error, not_authorized},
|
||||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
emqx_access_control:authenticate(?CREDENTIALS)
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid_ciphers(_Config) ->
|
t_create_invalid_ciphers(_Config) ->
|
||||||
{ok, _} = create_https_auth_with_ssl_opts(
|
{ok, _} = create_https_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES256-SHA384">>]}),
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
|
<<"ciphers">> => [<<"ECDHE-ECDSA-AES256-SHA384">>]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
{error, not_authorized},
|
{error, not_authorized},
|
||||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
emqx_access_control:authenticate(?CREDENTIALS)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -121,8 +139,9 @@ create_https_auth_with_ssl_opts(SpecificSSLOpts) ->
|
||||||
|
|
||||||
raw_https_auth_config(SpecificSSLOpts) ->
|
raw_https_auth_config(SpecificSSLOpts) ->
|
||||||
SSLOpts = maps:merge(
|
SSLOpts = maps:merge(
|
||||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||||
#{enable => <<"true">>}),
|
#{enable => <<"true">>}
|
||||||
|
),
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
enable => <<"true">>,
|
enable => <<"true">>,
|
||||||
|
|
@ -133,7 +152,7 @@ raw_https_auth_config(SpecificSSLOpts) ->
|
||||||
body => #{<<"username">> => ?PH_USERNAME, <<"password">> => ?PH_PASSWORD},
|
body => #{<<"username">> => ?PH_USERNAME, <<"password">> => ?PH_PASSWORD},
|
||||||
headers => #{<<"X-Test-Header">> => <<"Test Value">>},
|
headers => #{<<"X-Test-Header">> => <<"Test Value">>},
|
||||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
@ -147,15 +166,17 @@ cert_path(FileName) ->
|
||||||
|
|
||||||
cowboy_handler(Req0, State) ->
|
cowboy_handler(Req0, State) ->
|
||||||
Req = cowboy_req:reply(
|
Req = cowboy_req:reply(
|
||||||
200,
|
200,
|
||||||
Req0),
|
Req0
|
||||||
|
),
|
||||||
{ok, Req, State}.
|
{ok, Req, State}.
|
||||||
|
|
||||||
server_ssl_opts() ->
|
server_ssl_opts() ->
|
||||||
[{keyfile, cert_path("server.key")},
|
[
|
||||||
{certfile, cert_path("server.crt")},
|
{keyfile, cert_path("server.key")},
|
||||||
{cacertfile, cert_path("ca.crt")},
|
{certfile, cert_path("server.crt")},
|
||||||
{verify, verify_none},
|
{cacertfile, cert_path("ca.crt")},
|
||||||
{versions, ['tlsv1.2', 'tlsv1.3']},
|
{verify, verify_none},
|
||||||
{ciphers, ["ECDHE-RSA-AES256-GCM-SHA384", "TLS_CHACHA20_POLY1305_SHA256"]}
|
{versions, ['tlsv1.2', 'tlsv1.3']},
|
||||||
|
{ciphers, ["ECDHE-RSA-AES256-GCM-SHA384", "TLS_CHACHA20_POLY1305_SHA256"]}
|
||||||
].
|
].
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@
|
||||||
-define(JWKS_PORT, 33333).
|
-define(JWKS_PORT, 33333).
|
||||||
-define(JWKS_PATH, "/jwks.json").
|
-define(JWKS_PATH, "/jwks.json").
|
||||||
|
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
|
@ -51,24 +50,30 @@ end_per_suite(_) ->
|
||||||
|
|
||||||
t_jwt_authenticator_hmac_based(_) ->
|
t_jwt_authenticator_hmac_based(_) ->
|
||||||
Secret = <<"abcdef">>,
|
Secret = <<"abcdef">>,
|
||||||
Config = #{mechanism => jwt,
|
Config = #{
|
||||||
use_jwks => false,
|
mechanism => jwt,
|
||||||
algorithm => 'hmac-based',
|
use_jwks => false,
|
||||||
secret => Secret,
|
algorithm => 'hmac-based',
|
||||||
secret_base64_encoded => false,
|
secret => Secret,
|
||||||
verify_claims => []},
|
secret_base64_encoded => false,
|
||||||
|
verify_claims => []
|
||||||
|
},
|
||||||
{ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config),
|
{ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config),
|
||||||
|
|
||||||
Payload = #{<<"username">> => <<"myuser">>},
|
Payload = #{<<"username">> => <<"myuser">>},
|
||||||
JWS = generate_jws('hmac-based', Payload, Secret),
|
JWS = generate_jws('hmac-based', Payload, Secret),
|
||||||
Credential = #{username => <<"myuser">>,
|
Credential = #{
|
||||||
password => JWS},
|
username => <<"myuser">>,
|
||||||
|
password => JWS
|
||||||
|
},
|
||||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
||||||
|
|
||||||
Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true},
|
Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true},
|
||||||
JWS1 = generate_jws('hmac-based', Payload1, Secret),
|
JWS1 = generate_jws('hmac-based', Payload1, Secret),
|
||||||
Credential1 = #{username => <<"myuser">>,
|
Credential1 = #{
|
||||||
password => JWS1},
|
username => <<"myuser">>,
|
||||||
|
password => JWS1
|
||||||
|
},
|
||||||
?assertEqual({ok, #{is_superuser => true}}, emqx_authn_jwt:authenticate(Credential1, State)),
|
?assertEqual({ok, #{is_superuser => true}}, emqx_authn_jwt:authenticate(Credential1, State)),
|
||||||
|
|
||||||
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
|
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
|
||||||
|
|
@ -76,59 +81,84 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential2, State)),
|
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential2, State)),
|
||||||
|
|
||||||
%% secret_base64_encoded
|
%% secret_base64_encoded
|
||||||
Config2 = Config#{secret => base64:encode(Secret),
|
Config2 = Config#{
|
||||||
secret_base64_encoded => true},
|
secret => base64:encode(Secret),
|
||||||
|
secret_base64_encoded => true
|
||||||
|
},
|
||||||
{ok, State2} = emqx_authn_jwt:update(Config2, State),
|
{ok, State2} = emqx_authn_jwt:update(Config2, State),
|
||||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)),
|
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)),
|
||||||
|
|
||||||
%% invalid secret
|
%% invalid secret
|
||||||
BadConfig = Config#{secret => <<"emqxsecret">>,
|
BadConfig = Config#{
|
||||||
secret_base64_encoded => true},
|
secret => <<"emqxsecret">>,
|
||||||
|
secret_base64_encoded => true
|
||||||
|
},
|
||||||
{error, {invalid_parameter, secret}} = emqx_authn_jwt:create(?AUTHN_ID, BadConfig),
|
{error, {invalid_parameter, secret}} = emqx_authn_jwt:create(?AUTHN_ID, BadConfig),
|
||||||
|
|
||||||
Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]},
|
Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]},
|
||||||
{ok, State3} = emqx_authn_jwt:update(Config3, State2),
|
{ok, State3} = emqx_authn_jwt:update(Config3, State2),
|
||||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State3)),
|
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State3)),
|
||||||
?assertEqual({error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential#{username => <<"otheruser">>}, State3)),
|
?assertEqual(
|
||||||
|
{error, bad_username_or_password},
|
||||||
|
emqx_authn_jwt:authenticate(Credential#{username => <<"otheruser">>}, State3)
|
||||||
|
),
|
||||||
|
|
||||||
%% Expiration
|
%% Expiration
|
||||||
Payload3 = #{ <<"username">> => <<"myuser">>
|
Payload3 = #{
|
||||||
, <<"exp">> => erlang:system_time(second) - 60},
|
<<"username">> => <<"myuser">>,
|
||||||
|
<<"exp">> => erlang:system_time(second) - 60
|
||||||
|
},
|
||||||
JWS3 = generate_jws('hmac-based', Payload3, Secret),
|
JWS3 = generate_jws('hmac-based', Payload3, Secret),
|
||||||
Credential3 = Credential#{password => JWS3},
|
Credential3 = Credential#{password => JWS3},
|
||||||
?assertEqual({error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential3, State3)),
|
?assertEqual(
|
||||||
|
{error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential3, State3)
|
||||||
|
),
|
||||||
|
|
||||||
Payload4 = #{ <<"username">> => <<"myuser">>
|
Payload4 = #{
|
||||||
, <<"exp">> => erlang:system_time(second) + 60},
|
<<"username">> => <<"myuser">>,
|
||||||
|
<<"exp">> => erlang:system_time(second) + 60
|
||||||
|
},
|
||||||
JWS4 = generate_jws('hmac-based', Payload4, Secret),
|
JWS4 = generate_jws('hmac-based', Payload4, Secret),
|
||||||
Credential4 = Credential#{password => JWS4},
|
Credential4 = Credential#{password => JWS4},
|
||||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential4, State3)),
|
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential4, State3)),
|
||||||
|
|
||||||
%% Issued At
|
%% Issued At
|
||||||
Payload5 = #{ <<"username">> => <<"myuser">>
|
Payload5 = #{
|
||||||
, <<"iat">> => erlang:system_time(second) - 60},
|
<<"username">> => <<"myuser">>,
|
||||||
|
<<"iat">> => erlang:system_time(second) - 60
|
||||||
|
},
|
||||||
JWS5 = generate_jws('hmac-based', Payload5, Secret),
|
JWS5 = generate_jws('hmac-based', Payload5, Secret),
|
||||||
Credential5 = Credential#{password => JWS5},
|
Credential5 = Credential#{password => JWS5},
|
||||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential5, State3)),
|
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential5, State3)),
|
||||||
|
|
||||||
Payload6 = #{ <<"username">> => <<"myuser">>
|
Payload6 = #{
|
||||||
, <<"iat">> => erlang:system_time(second) + 60},
|
<<"username">> => <<"myuser">>,
|
||||||
|
<<"iat">> => erlang:system_time(second) + 60
|
||||||
|
},
|
||||||
JWS6 = generate_jws('hmac-based', Payload6, Secret),
|
JWS6 = generate_jws('hmac-based', Payload6, Secret),
|
||||||
Credential6 = Credential#{password => JWS6},
|
Credential6 = Credential#{password => JWS6},
|
||||||
?assertEqual({error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential6, State3)),
|
?assertEqual(
|
||||||
|
{error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential6, State3)
|
||||||
|
),
|
||||||
|
|
||||||
%% Not Before
|
%% Not Before
|
||||||
Payload7 = #{ <<"username">> => <<"myuser">>
|
Payload7 = #{
|
||||||
, <<"nbf">> => erlang:system_time(second) - 60},
|
<<"username">> => <<"myuser">>,
|
||||||
|
<<"nbf">> => erlang:system_time(second) - 60
|
||||||
|
},
|
||||||
JWS7 = generate_jws('hmac-based', Payload7, Secret),
|
JWS7 = generate_jws('hmac-based', Payload7, Secret),
|
||||||
Credential7 = Credential6#{password => JWS7},
|
Credential7 = Credential6#{password => JWS7},
|
||||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential7, State3)),
|
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential7, State3)),
|
||||||
|
|
||||||
Payload8 = #{ <<"username">> => <<"myuser">>
|
Payload8 = #{
|
||||||
, <<"nbf">> => erlang:system_time(second) + 60},
|
<<"username">> => <<"myuser">>,
|
||||||
|
<<"nbf">> => erlang:system_time(second) + 60
|
||||||
|
},
|
||||||
JWS8 = generate_jws('hmac-based', Payload8, Secret),
|
JWS8 = generate_jws('hmac-based', Payload8, Secret),
|
||||||
Credential8 = Credential#{password => JWS8},
|
Credential8 = Credential#{password => JWS8},
|
||||||
?assertEqual({error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential8, State3)),
|
?assertEqual(
|
||||||
|
{error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential8, State3)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(ok, emqx_authn_jwt:destroy(State3)),
|
?assertEqual(ok, emqx_authn_jwt:destroy(State3)),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -136,19 +166,25 @@ t_jwt_authenticator_hmac_based(_) ->
|
||||||
t_jwt_authenticator_public_key(_) ->
|
t_jwt_authenticator_public_key(_) ->
|
||||||
PublicKey = test_rsa_key(public),
|
PublicKey = test_rsa_key(public),
|
||||||
PrivateKey = test_rsa_key(private),
|
PrivateKey = test_rsa_key(private),
|
||||||
Config = #{mechanism => jwt,
|
Config = #{
|
||||||
use_jwks => false,
|
mechanism => jwt,
|
||||||
algorithm => 'public-key',
|
use_jwks => false,
|
||||||
certificate => PublicKey,
|
algorithm => 'public-key',
|
||||||
verify_claims => []},
|
certificate => PublicKey,
|
||||||
|
verify_claims => []
|
||||||
|
},
|
||||||
{ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config),
|
{ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config),
|
||||||
|
|
||||||
Payload = #{<<"username">> => <<"myuser">>},
|
Payload = #{<<"username">> => <<"myuser">>},
|
||||||
JWS = generate_jws('public-key', Payload, PrivateKey),
|
JWS = generate_jws('public-key', Payload, PrivateKey),
|
||||||
Credential = #{username => <<"myuser">>,
|
Credential = #{
|
||||||
password => JWS},
|
username => <<"myuser">>,
|
||||||
|
password => JWS
|
||||||
|
},
|
||||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
||||||
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State)),
|
?assertEqual(
|
||||||
|
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(ok, emqx_authn_jwt:destroy(State)),
|
?assertEqual(ok, emqx_authn_jwt:destroy(State)),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -160,63 +196,77 @@ t_jwks_renewal(_Config) ->
|
||||||
PrivateKey = test_rsa_key(private),
|
PrivateKey = test_rsa_key(private),
|
||||||
Payload = #{<<"username">> => <<"myuser">>},
|
Payload = #{<<"username">> => <<"myuser">>},
|
||||||
JWS = generate_jws('public-key', Payload, PrivateKey),
|
JWS = generate_jws('public-key', Payload, PrivateKey),
|
||||||
Credential = #{username => <<"myuser">>,
|
Credential = #{
|
||||||
password => JWS},
|
username => <<"myuser">>,
|
||||||
|
password => JWS
|
||||||
BadConfig0 = #{mechanism => jwt,
|
},
|
||||||
algorithm => 'public-key',
|
|
||||||
ssl => #{enable => false},
|
|
||||||
verify_claims => [],
|
|
||||||
|
|
||||||
use_jwks => true,
|
BadConfig0 = #{
|
||||||
endpoint => "https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT + 1) ++ ?JWKS_PATH,
|
mechanism => jwt,
|
||||||
refresh_interval => 1000
|
algorithm => 'public-key',
|
||||||
},
|
ssl => #{enable => false},
|
||||||
|
verify_claims => [],
|
||||||
|
|
||||||
|
use_jwks => true,
|
||||||
|
endpoint => "https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT + 1) ++ ?JWKS_PATH,
|
||||||
|
refresh_interval => 1000
|
||||||
|
},
|
||||||
|
|
||||||
ok = snabbkaffe:start_trace(),
|
ok = snabbkaffe:start_trace(),
|
||||||
|
|
||||||
{{ok, State0}, _} = ?wait_async_action(
|
{{ok, State0}, _} = ?wait_async_action(
|
||||||
emqx_authn_jwt:create(?AUTHN_ID, BadConfig0),
|
emqx_authn_jwt:create(?AUTHN_ID, BadConfig0),
|
||||||
#{?snk_kind := jwks_endpoint_response},
|
#{?snk_kind := jwks_endpoint_response},
|
||||||
10000),
|
10000
|
||||||
|
),
|
||||||
|
|
||||||
ok = snabbkaffe:stop(),
|
ok = snabbkaffe:stop(),
|
||||||
|
|
||||||
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State0)),
|
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State0)),
|
||||||
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)),
|
?assertEqual(
|
||||||
|
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)
|
||||||
|
),
|
||||||
|
|
||||||
ClientSSLOpts = client_ssl_opts(),
|
ClientSSLOpts = client_ssl_opts(),
|
||||||
BadClientSSLOpts = ClientSSLOpts#{server_name_indication => "authn-server-unknown-host"},
|
BadClientSSLOpts = ClientSSLOpts#{server_name_indication => "authn-server-unknown-host"},
|
||||||
|
|
||||||
BadConfig1 = BadConfig0#{endpoint =>
|
BadConfig1 = BadConfig0#{
|
||||||
"https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH,
|
endpoint =>
|
||||||
ssl => BadClientSSLOpts},
|
"https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH,
|
||||||
|
ssl => BadClientSSLOpts
|
||||||
|
},
|
||||||
|
|
||||||
ok = snabbkaffe:start_trace(),
|
ok = snabbkaffe:start_trace(),
|
||||||
|
|
||||||
{{ok, State1}, _} = ?wait_async_action(
|
{{ok, State1}, _} = ?wait_async_action(
|
||||||
emqx_authn_jwt:create(?AUTHN_ID, BadConfig1),
|
emqx_authn_jwt:create(?AUTHN_ID, BadConfig1),
|
||||||
#{?snk_kind := jwks_endpoint_response},
|
#{?snk_kind := jwks_endpoint_response},
|
||||||
10000),
|
10000
|
||||||
|
),
|
||||||
|
|
||||||
ok = snabbkaffe:stop(),
|
ok = snabbkaffe:stop(),
|
||||||
|
|
||||||
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State1)),
|
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State1)),
|
||||||
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)),
|
?assertEqual(
|
||||||
|
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)
|
||||||
|
),
|
||||||
|
|
||||||
GoodConfig = BadConfig1#{ssl => ClientSSLOpts},
|
GoodConfig = BadConfig1#{ssl => ClientSSLOpts},
|
||||||
|
|
||||||
ok = snabbkaffe:start_trace(),
|
ok = snabbkaffe:start_trace(),
|
||||||
|
|
||||||
{{ok, State2}, _} = ?wait_async_action(
|
{{ok, State2}, _} = ?wait_async_action(
|
||||||
emqx_authn_jwt:update(GoodConfig, State1),
|
emqx_authn_jwt:update(GoodConfig, State1),
|
||||||
#{?snk_kind := jwks_endpoint_response},
|
#{?snk_kind := jwks_endpoint_response},
|
||||||
10000),
|
10000
|
||||||
|
),
|
||||||
|
|
||||||
ok = snabbkaffe:stop(),
|
ok = snabbkaffe:stop(),
|
||||||
|
|
||||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)),
|
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)),
|
||||||
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State2)),
|
?assertEqual(
|
||||||
|
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State2)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(ok, emqx_authn_jwt:destroy(State2)),
|
?assertEqual(ok, emqx_authn_jwt:destroy(State2)),
|
||||||
ok = emqx_authn_http_test_server:stop().
|
ok = emqx_authn_http_test_server:stop().
|
||||||
|
|
@ -229,15 +279,15 @@ jwks_handler(Req0, State) ->
|
||||||
JWK = jose_jwk:from_pem_file(test_rsa_key(public)),
|
JWK = jose_jwk:from_pem_file(test_rsa_key(public)),
|
||||||
JWKS = jose_jwk_set:to_map([JWK], #{}),
|
JWKS = jose_jwk_set:to_map([JWK], #{}),
|
||||||
Req = cowboy_req:reply(
|
Req = cowboy_req:reply(
|
||||||
200,
|
200,
|
||||||
#{<<"content-type">> => <<"application/json">>},
|
#{<<"content-type">> => <<"application/json">>},
|
||||||
jiffy:encode(JWKS),
|
jiffy:encode(JWKS),
|
||||||
Req0),
|
Req0
|
||||||
|
),
|
||||||
{ok, Req, State}.
|
{ok, Req, State}.
|
||||||
|
|
||||||
test_rsa_key(public) ->
|
test_rsa_key(public) ->
|
||||||
data_file("public_key.pem");
|
data_file("public_key.pem");
|
||||||
|
|
||||||
test_rsa_key(private) ->
|
test_rsa_key(private) ->
|
||||||
data_file("private_key.pem").
|
data_file("private_key.pem").
|
||||||
|
|
||||||
|
|
@ -250,32 +300,37 @@ cert_file(Name) ->
|
||||||
|
|
||||||
generate_jws('hmac-based', Payload, Secret) ->
|
generate_jws('hmac-based', Payload, Secret) ->
|
||||||
JWK = jose_jwk:from_oct(Secret),
|
JWK = jose_jwk:from_oct(Secret),
|
||||||
Header = #{ <<"alg">> => <<"HS256">>
|
Header = #{
|
||||||
, <<"typ">> => <<"JWT">>
|
<<"alg">> => <<"HS256">>,
|
||||||
},
|
<<"typ">> => <<"JWT">>
|
||||||
|
},
|
||||||
Signed = jose_jwt:sign(JWK, Header, Payload),
|
Signed = jose_jwt:sign(JWK, Header, Payload),
|
||||||
{_, JWS} = jose_jws:compact(Signed),
|
{_, JWS} = jose_jws:compact(Signed),
|
||||||
JWS;
|
JWS;
|
||||||
generate_jws('public-key', Payload, PrivateKey) ->
|
generate_jws('public-key', Payload, PrivateKey) ->
|
||||||
JWK = jose_jwk:from_pem_file(PrivateKey),
|
JWK = jose_jwk:from_pem_file(PrivateKey),
|
||||||
Header = #{ <<"alg">> => <<"RS256">>
|
Header = #{
|
||||||
, <<"typ">> => <<"JWT">>
|
<<"alg">> => <<"RS256">>,
|
||||||
},
|
<<"typ">> => <<"JWT">>
|
||||||
|
},
|
||||||
Signed = jose_jwt:sign(JWK, Header, Payload),
|
Signed = jose_jwt:sign(JWK, Header, Payload),
|
||||||
{_, JWS} = jose_jws:compact(Signed),
|
{_, JWS} = jose_jws:compact(Signed),
|
||||||
JWS.
|
JWS.
|
||||||
|
|
||||||
client_ssl_opts() ->
|
client_ssl_opts() ->
|
||||||
maps:merge(
|
maps:merge(
|
||||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||||
#{enable => true,
|
#{
|
||||||
verify => verify_peer,
|
enable => true,
|
||||||
server_name_indication => "authn-server"
|
verify => verify_peer,
|
||||||
}).
|
server_name_indication => "authn-server"
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
server_ssl_opts() ->
|
server_ssl_opts() ->
|
||||||
[{keyfile, cert_file("server.key")},
|
[
|
||||||
{certfile, cert_file("server.crt")},
|
{keyfile, cert_file("server.key")},
|
||||||
{cacertfile, cert_file("ca.crt")},
|
{certfile, cert_file("server.crt")},
|
||||||
{verify, verify_none}
|
{cacertfile, cert_file("ca.crt")},
|
||||||
|
{verify, verify_none}
|
||||||
].
|
].
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,8 @@ t_check_schema(_Config) ->
|
||||||
?assertException(
|
?assertException(
|
||||||
throw,
|
throw,
|
||||||
{emqx_authn_mnesia, _},
|
{emqx_authn_mnesia, _},
|
||||||
hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigNotOk))).
|
hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigNotOk))
|
||||||
|
).
|
||||||
|
|
||||||
t_create(_) ->
|
t_create(_) ->
|
||||||
Config0 = config(),
|
Config0 = config(),
|
||||||
|
|
@ -110,7 +111,7 @@ t_destroy(_) ->
|
||||||
ok = emqx_authn_mnesia:destroy(State0),
|
ok = emqx_authn_mnesia:destroy(State0),
|
||||||
|
|
||||||
{ok, State1} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
|
{ok, State1} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
|
||||||
{error,not_found} = emqx_authn_mnesia:lookup_user(<<"u">>, State1),
|
{error, not_found} = emqx_authn_mnesia:lookup_user(<<"u">>, State1),
|
||||||
{ok, _} = emqx_authn_mnesia:lookup_user(<<"u">>, StateOther).
|
{ok, _} = emqx_authn_mnesia:lookup_user(<<"u">>, StateOther).
|
||||||
|
|
||||||
t_authenticate(_) ->
|
t_authenticate(_) ->
|
||||||
|
|
@ -121,14 +122,17 @@ t_authenticate(_) ->
|
||||||
{ok, _} = emqx_authn_mnesia:add_user(User, State),
|
{ok, _} = emqx_authn_mnesia:add_user(User, State),
|
||||||
|
|
||||||
{ok, _} = emqx_authn_mnesia:authenticate(
|
{ok, _} = emqx_authn_mnesia:authenticate(
|
||||||
#{username => <<"u">>, password => <<"p">>},
|
#{username => <<"u">>, password => <<"p">>},
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
{error, bad_username_or_password} = emqx_authn_mnesia:authenticate(
|
{error, bad_username_or_password} = emqx_authn_mnesia:authenticate(
|
||||||
#{username => <<"u">>, password => <<"badpass">>},
|
#{username => <<"u">>, password => <<"badpass">>},
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
ignore = emqx_authn_mnesia:authenticate(
|
ignore = emqx_authn_mnesia:authenticate(
|
||||||
#{clientid => <<"u">>, password => <<"p">>},
|
#{clientid => <<"u">>, password => <<"p">>},
|
||||||
State).
|
State
|
||||||
|
).
|
||||||
|
|
||||||
t_add_user(_) ->
|
t_add_user(_) ->
|
||||||
Config = config(),
|
Config = config(),
|
||||||
|
|
@ -157,16 +161,19 @@ t_update_user(_) ->
|
||||||
{ok, _} = emqx_authn_mnesia:add_user(User, State),
|
{ok, _} = emqx_authn_mnesia:add_user(User, State),
|
||||||
|
|
||||||
{error, not_found} = emqx_authn_mnesia:update_user(<<"u1">>, #{password => <<"p1">>}, State),
|
{error, not_found} = emqx_authn_mnesia:update_user(<<"u1">>, #{password => <<"p1">>}, State),
|
||||||
{ok,
|
{ok, #{
|
||||||
#{user_id := <<"u">>,
|
user_id := <<"u">>,
|
||||||
is_superuser := true}} = emqx_authn_mnesia:update_user(
|
is_superuser := true
|
||||||
<<"u">>,
|
}} = emqx_authn_mnesia:update_user(
|
||||||
#{password => <<"p1">>, is_superuser => true},
|
<<"u">>,
|
||||||
State),
|
#{password => <<"p1">>, is_superuser => true},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_authn_mnesia:authenticate(
|
{ok, _} = emqx_authn_mnesia:authenticate(
|
||||||
#{username => <<"u">>, password => <<"p1">>},
|
#{username => <<"u">>, password => <<"p1">>},
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{ok, #{is_superuser := true}} = emqx_authn_mnesia:lookup_user(<<"u">>, State).
|
{ok, #{is_superuser := true}} = emqx_authn_mnesia:lookup_user(<<"u">>, State).
|
||||||
|
|
||||||
|
|
@ -174,31 +181,47 @@ t_list_users(_) ->
|
||||||
Config = config(),
|
Config = config(),
|
||||||
{ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
|
{ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
|
||||||
|
|
||||||
Users = [#{user_id => <<"u1">>, password => <<"p">>},
|
Users = [
|
||||||
#{user_id => <<"u2">>, password => <<"p">>},
|
#{user_id => <<"u1">>, password => <<"p">>},
|
||||||
#{user_id => <<"u3">>, password => <<"p">>}],
|
#{user_id => <<"u2">>, password => <<"p">>},
|
||||||
|
#{user_id => <<"u3">>, password => <<"p">>}
|
||||||
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(U) -> {ok, _} = emqx_authn_mnesia:add_user(U, State) end,
|
fun(U) -> {ok, _} = emqx_authn_mnesia:add_user(U, State) end,
|
||||||
Users),
|
Users
|
||||||
|
),
|
||||||
|
|
||||||
#{data := [#{is_superuser := false,user_id := _},
|
#{
|
||||||
#{is_superuser := false,user_id := _}],
|
data := [
|
||||||
meta := #{page := 1, limit := 2, count := 3}} = emqx_authn_mnesia:list_users(
|
#{is_superuser := false, user_id := _},
|
||||||
#{<<"page">> => 1, <<"limit">> => 2},
|
#{is_superuser := false, user_id := _}
|
||||||
State),
|
],
|
||||||
|
meta := #{page := 1, limit := 2, count := 3}
|
||||||
|
} = emqx_authn_mnesia:list_users(
|
||||||
|
#{<<"page">> => 1, <<"limit">> => 2},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
#{data := [#{is_superuser := false,user_id := _}],
|
#{
|
||||||
meta := #{page := 2, limit := 2, count := 3}} = emqx_authn_mnesia:list_users(
|
data := [#{is_superuser := false, user_id := _}],
|
||||||
#{<<"page">> => 2, <<"limit">> => 2},
|
meta := #{page := 2, limit := 2, count := 3}
|
||||||
State),
|
} = emqx_authn_mnesia:list_users(
|
||||||
|
#{<<"page">> => 2, <<"limit">> => 2},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
#{data := [#{is_superuser := false,user_id := <<"u3">>}],
|
#{
|
||||||
meta := #{page := 1, limit := 20, count := 1}} = emqx_authn_mnesia:list_users(
|
data := [#{is_superuser := false, user_id := <<"u3">>}],
|
||||||
#{ <<"page">> => 1
|
meta := #{page := 1, limit := 20, count := 1}
|
||||||
, <<"limit">> => 20
|
} = emqx_authn_mnesia:list_users(
|
||||||
, <<"like_username">> => <<"3">>},
|
#{
|
||||||
State).
|
<<"page">> => 1,
|
||||||
|
<<"limit">> => 20,
|
||||||
|
<<"like_username">> => <<"3">>
|
||||||
|
},
|
||||||
|
State
|
||||||
|
).
|
||||||
|
|
||||||
t_import_users(_) ->
|
t_import_users(_) ->
|
||||||
Config0 = config(),
|
Config0 = config(),
|
||||||
|
|
@ -206,36 +229,44 @@ t_import_users(_) ->
|
||||||
{ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
|
{ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
|
||||||
|
|
||||||
ok = emqx_authn_mnesia:import_users(
|
ok = emqx_authn_mnesia:import_users(
|
||||||
data_filename(<<"user-credentials.json">>),
|
data_filename(<<"user-credentials.json">>),
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_mnesia:import_users(
|
ok = emqx_authn_mnesia:import_users(
|
||||||
data_filename(<<"user-credentials.csv">>),
|
data_filename(<<"user-credentials.csv">>),
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{error, {unsupported_file_format, _}} = emqx_authn_mnesia:import_users(
|
{error, {unsupported_file_format, _}} = emqx_authn_mnesia:import_users(
|
||||||
<<"/file/with/unknown.extension">>,
|
<<"/file/with/unknown.extension">>,
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{error, unknown_file_format} = emqx_authn_mnesia:import_users(
|
{error, unknown_file_format} = emqx_authn_mnesia:import_users(
|
||||||
<<"/file/with/no/extension">>,
|
<<"/file/with/no/extension">>,
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{error, enoent} = emqx_authn_mnesia:import_users(
|
{error, enoent} = emqx_authn_mnesia:import_users(
|
||||||
<<"/file/that/not/exist.json">>,
|
<<"/file/that/not/exist.json">>,
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{error, bad_format} = emqx_authn_mnesia:import_users(
|
{error, bad_format} = emqx_authn_mnesia:import_users(
|
||||||
data_filename(<<"user-credentials-malformed-0.json">>),
|
data_filename(<<"user-credentials-malformed-0.json">>),
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{error, {_, invalid_json}} = emqx_authn_mnesia:import_users(
|
{error, {_, invalid_json}} = emqx_authn_mnesia:import_users(
|
||||||
data_filename(<<"user-credentials-malformed-1.json">>),
|
data_filename(<<"user-credentials-malformed-1.json">>),
|
||||||
State),
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{error, bad_format} = emqx_authn_mnesia:import_users(
|
{error, bad_format} = emqx_authn_mnesia:import_users(
|
||||||
data_filename(<<"user-credentials-malformed.csv">>),
|
data_filename(<<"user-credentials-malformed.csv">>),
|
||||||
State).
|
State
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -246,7 +277,10 @@ data_filename(Name) ->
|
||||||
filename:join([Dir, <<"data">>, Name]).
|
filename:join([Dir, <<"data">>, Name]).
|
||||||
|
|
||||||
config() ->
|
config() ->
|
||||||
#{user_id_type => username,
|
#{
|
||||||
password_hash_algorithm => #{name => bcrypt,
|
user_id_type => username,
|
||||||
salt_rounds => 8}
|
password_hash_algorithm => #{
|
||||||
}.
|
name => bcrypt,
|
||||||
|
salt_rounds => 8
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
|
||||||
-define(MONGO_HOST, "mongo").
|
-define(MONGO_HOST, "mongo").
|
||||||
-define(MONGO_CLIENT, 'emqx_authn_mongo_SUITE_client').
|
-define(MONGO_CLIENT, 'emqx_authn_mongo_SUITE_client').
|
||||||
|
|
||||||
|
|
@ -37,8 +36,9 @@ init_per_testcase(_TestCase, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
{ok, _} = mc_worker_api:connect(mongo_config()),
|
{ok, _} = mc_worker_api:connect(mongo_config()),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
|
@ -58,8 +58,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||||
|
|
||||||
|
|
@ -71,8 +72,9 @@ t_create(_Config) ->
|
||||||
AuthConfig = raw_mongo_auth_config(),
|
AuthConfig = raw_mongo_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_mongodb}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
{ok, [#{provider := emqx_authn_mongodb}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
||||||
|
|
||||||
|
|
@ -81,79 +83,93 @@ t_create_invalid(_Config) ->
|
||||||
|
|
||||||
InvalidConfigs =
|
InvalidConfigs =
|
||||||
[
|
[
|
||||||
AuthConfig#{mongo_type => <<"unknown">>},
|
AuthConfig#{mongo_type => <<"unknown">>},
|
||||||
AuthConfig#{selector => <<"{ \"username\": \"${username}\" }">>},
|
AuthConfig#{selector => <<"{ \"username\": \"${username}\" }">>},
|
||||||
AuthConfig#{w_mode => <<"unknown">>}
|
AuthConfig#{w_mode => <<"unknown">>}
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Config) ->
|
fun(Config) ->
|
||||||
{error, _} = emqx:update_config(
|
{error, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||||
end,
|
end,
|
||||||
InvalidConfigs).
|
InvalidConfigs
|
||||||
|
).
|
||||||
|
|
||||||
t_authenticate(_Config) ->
|
t_authenticate(_Config) ->
|
||||||
ok = init_seeds(),
|
ok = init_seeds(),
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(Sample) ->
|
fun(Sample) ->
|
||||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||||
test_user_auth(Sample)
|
test_user_auth(Sample)
|
||||||
end,
|
end,
|
||||||
user_seeds()),
|
user_seeds()
|
||||||
|
),
|
||||||
ok = drop_seeds().
|
ok = drop_seeds().
|
||||||
|
|
||||||
test_user_auth(#{credentials := Credentials0,
|
test_user_auth(#{
|
||||||
config_params := SpecificConfigParams,
|
credentials := Credentials0,
|
||||||
result := Result}) ->
|
config_params := SpecificConfigParams,
|
||||||
|
result := Result
|
||||||
|
}) ->
|
||||||
AuthConfig = maps:merge(raw_mongo_auth_config(), SpecificConfigParams),
|
AuthConfig = maps:merge(raw_mongo_auth_config(), SpecificConfigParams),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
Credentials = Credentials0#{
|
Credentials = Credentials0#{
|
||||||
listener => 'tcp:default',
|
listener => 'tcp:default',
|
||||||
protocol => mqtt
|
protocol => mqtt
|
||||||
},
|
},
|
||||||
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL).
|
?GLOBAL
|
||||||
|
).
|
||||||
|
|
||||||
t_destroy(_Config) ->
|
t_destroy(_Config) ->
|
||||||
ok = init_seeds(),
|
ok = init_seeds(),
|
||||||
AuthConfig = raw_mongo_auth_config(),
|
AuthConfig = raw_mongo_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_mongodb, state := State}]}
|
{ok, [#{provider := emqx_authn_mongodb, state := State}]} =
|
||||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
emqx_authentication:list_authenticators(?GLOBAL),
|
||||||
|
|
||||||
{ok, _} = emqx_authn_mongodb:authenticate(
|
{ok, _} = emqx_authn_mongodb:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>
|
username => <<"plain">>,
|
||||||
},
|
password => <<"plain">>
|
||||||
State),
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
|
|
||||||
% Authenticator should not be usable anymore
|
% Authenticator should not be usable anymore
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
ignore,
|
ignore,
|
||||||
emqx_authn_mongodb:authenticate(
|
emqx_authn_mongodb:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>
|
username => <<"plain">>,
|
||||||
},
|
password => <<"plain">>
|
||||||
State)),
|
},
|
||||||
|
State
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
ok = drop_seeds().
|
ok = drop_seeds().
|
||||||
|
|
||||||
|
|
@ -164,48 +180,55 @@ t_update(_Config) ->
|
||||||
CorrectConfig#{selector => #{<<"wrongfield">> => <<"wrongvalue">>}},
|
CorrectConfig#{selector => #{<<"wrongfield">> => <<"wrongvalue">>}},
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{error, not_authorized} = emqx_access_control:authenticate(
|
{error, not_authorized} = emqx_access_control:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}),
|
protocol => mqtt
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
% We update with config with correct selector, provider should update and work properly
|
% We update with config with correct selector, provider should update and work properly
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{update_authenticator, ?GLOBAL, <<"password_based:mongodb">>, CorrectConfig}),
|
{update_authenticator, ?GLOBAL, <<"password_based:mongodb">>, CorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok,_} = emqx_access_control:authenticate(
|
{ok, _} = emqx_access_control:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}),
|
protocol => mqtt
|
||||||
|
}
|
||||||
|
),
|
||||||
ok = drop_seeds().
|
ok = drop_seeds().
|
||||||
|
|
||||||
t_is_superuser(_Config) ->
|
t_is_superuser(_Config) ->
|
||||||
Config = raw_mongo_auth_config(),
|
Config = raw_mongo_auth_config(),
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
|
),
|
||||||
|
|
||||||
Checks = [
|
Checks = [
|
||||||
{<<"0">>, false},
|
{<<"0">>, false},
|
||||||
{<<"">>, false},
|
{<<"">>, false},
|
||||||
{null, false},
|
{null, false},
|
||||||
{false, false},
|
{false, false},
|
||||||
{0, false},
|
{0, false},
|
||||||
|
|
||||||
{<<"1">>, true},
|
{<<"1">>, true},
|
||||||
{<<"val">>, true},
|
{<<"val">>, true},
|
||||||
{1, true},
|
{1, true},
|
||||||
{123, true},
|
{123, true},
|
||||||
{true, true}
|
{true, true}
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(fun test_is_superuser/1, Checks).
|
lists:foreach(fun test_is_superuser/1, Checks).
|
||||||
|
|
||||||
|
|
@ -213,24 +236,25 @@ test_is_superuser({Value, ExpectedValue}) ->
|
||||||
{true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"users">>, #{}),
|
{true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"users">>, #{}),
|
||||||
|
|
||||||
UserData = #{
|
UserData = #{
|
||||||
username => <<"user">>,
|
username => <<"user">>,
|
||||||
password_hash => <<"plainsalt">>,
|
password_hash => <<"plainsalt">>,
|
||||||
salt => <<"salt">>,
|
salt => <<"salt">>,
|
||||||
is_superuser => Value
|
is_superuser => Value
|
||||||
},
|
},
|
||||||
|
|
||||||
{{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, [UserData]),
|
{{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, [UserData]),
|
||||||
|
|
||||||
Credentials = #{
|
Credentials = #{
|
||||||
listener => 'tcp:default',
|
listener => 'tcp:default',
|
||||||
protocol => mqtt,
|
protocol => mqtt,
|
||||||
username => <<"user">>,
|
username => <<"user">>,
|
||||||
password => <<"plain">>
|
password => <<"plain">>
|
||||||
},
|
},
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
{ok, #{is_superuser => ExpectedValue}},
|
{ok, #{is_superuser => ExpectedValue}},
|
||||||
emqx_access_control:authenticate(Credentials)).
|
emqx_access_control:authenticate(Credentials)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -238,146 +262,160 @@ test_is_superuser({Value, ExpectedValue}) ->
|
||||||
|
|
||||||
raw_mongo_auth_config() ->
|
raw_mongo_auth_config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
password_hash_algorithm => #{name => <<"plain">>,
|
password_hash_algorithm => #{
|
||||||
salt_position => <<"suffix">>},
|
name => <<"plain">>,
|
||||||
enable => <<"true">>,
|
salt_position => <<"suffix">>
|
||||||
|
},
|
||||||
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"mongodb">>,
|
backend => <<"mongodb">>,
|
||||||
mongo_type => <<"single">>,
|
mongo_type => <<"single">>,
|
||||||
database => <<"mqtt">>,
|
database => <<"mqtt">>,
|
||||||
collection => <<"users">>,
|
collection => <<"users">>,
|
||||||
server => mongo_server(),
|
server => mongo_server(),
|
||||||
w_mode => <<"unsafe">>,
|
w_mode => <<"unsafe">>,
|
||||||
|
|
||||||
selector => #{<<"username">> => <<"${username}">>},
|
selector => #{<<"username">> => <<"${username}">>},
|
||||||
password_hash_field => <<"password_hash">>,
|
password_hash_field => <<"password_hash">>,
|
||||||
salt_field => <<"salt">>,
|
salt_field => <<"salt">>,
|
||||||
is_superuser_field => <<"is_superuser">>
|
is_superuser_field => <<"is_superuser">>
|
||||||
}.
|
}.
|
||||||
|
|
||||||
user_seeds() ->
|
user_seeds() ->
|
||||||
[#{data => #{
|
[
|
||||||
username => <<"plain">>,
|
#{
|
||||||
password_hash => <<"plainsalt">>,
|
data => #{
|
||||||
salt => <<"salt">>,
|
username => <<"plain">>,
|
||||||
is_superuser => <<"1">>
|
password_hash => <<"plainsalt">>,
|
||||||
},
|
salt => <<"salt">>,
|
||||||
credentials => #{
|
is_superuser => <<"1">>
|
||||||
username => <<"plain">>,
|
},
|
||||||
password => <<"plain">>
|
credentials => #{
|
||||||
},
|
username => <<"plain">>,
|
||||||
config_params => #{
|
password => <<"plain">>
|
||||||
},
|
},
|
||||||
result => {ok,#{is_superuser => true}}
|
config_params => #{},
|
||||||
},
|
result => {ok, #{is_superuser => true}}
|
||||||
|
|
||||||
#{data => #{
|
|
||||||
username => <<"md5">>,
|
|
||||||
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
|
||||||
salt => <<"salt">>,
|
|
||||||
is_superuser => <<"0">>
|
|
||||||
},
|
|
||||||
credentials => #{
|
|
||||||
username => <<"md5">>,
|
|
||||||
password => <<"md5">>
|
|
||||||
},
|
|
||||||
config_params => #{
|
|
||||||
password_hash_algorithm => #{name => <<"md5">>,
|
|
||||||
salt_position => <<"suffix">> }
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => false}}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
|
||||||
username => <<"sha256">>,
|
|
||||||
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
|
||||||
salt => <<"salt">>,
|
|
||||||
is_superuser => 1
|
|
||||||
},
|
},
|
||||||
credentials => #{
|
|
||||||
clientid => <<"sha256">>,
|
|
||||||
password => <<"sha256">>
|
|
||||||
},
|
|
||||||
config_params => #{
|
|
||||||
selector => #{<<"username">> => <<"${clientid}">>},
|
|
||||||
password_hash_algorithm => #{name => <<"sha256">>,
|
|
||||||
salt_position => <<"prefix">>}
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => true}}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt">>,
|
data => #{
|
||||||
password_hash =>
|
username => <<"md5">>,
|
||||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
salt => <<"salt">>,
|
||||||
is_superuser => 0
|
is_superuser => <<"0">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"bcrypt">>,
|
username => <<"md5">>,
|
||||||
password => <<"bcrypt">>
|
password => <<"md5">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
password_hash_algorithm => #{
|
||||||
},
|
name => <<"md5">>,
|
||||||
result => {ok,#{is_superuser => false}}
|
salt_position => <<"suffix">>
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt0">>,
|
data => #{
|
||||||
password_hash =>
|
username => <<"sha256">>,
|
||||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
password_hash =>
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
<<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
||||||
is_superuser => <<"0">>
|
salt => <<"salt">>,
|
||||||
},
|
is_superuser => 1
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt0">>,
|
credentials => #{
|
||||||
password => <<"bcrypt">>
|
clientid => <<"sha256">>,
|
||||||
},
|
password => <<"sha256">>
|
||||||
config_params => #{
|
},
|
||||||
% clientid variable & username credentials
|
config_params => #{
|
||||||
selector => #{<<"username">> => <<"${clientid}">>},
|
selector => #{<<"username">> => <<"${clientid}">>},
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
password_hash_algorithm => #{
|
||||||
},
|
name => <<"sha256">>,
|
||||||
result => {error,not_authorized}
|
salt_position => <<"prefix">>
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => true}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt1">>,
|
data => #{
|
||||||
password_hash =>
|
username => <<"bcrypt">>,
|
||||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
password_hash =>
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
is_superuser => <<"0">>
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
},
|
is_superuser => 0
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt1">>,
|
credentials => #{
|
||||||
password => <<"bcrypt">>
|
username => <<"bcrypt">>,
|
||||||
},
|
password => <<"bcrypt">>
|
||||||
config_params => #{
|
},
|
||||||
selector => #{<<"userid">> => <<"${clientid}">>},
|
config_params => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
},
|
},
|
||||||
result => {error,not_authorized}
|
result => {ok, #{is_superuser => false}}
|
||||||
},
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt2">>,
|
data => #{
|
||||||
password_hash =>
|
username => <<"bcrypt0">>,
|
||||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
password_hash =>
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
is_superuser => <<"0">>
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
},
|
is_superuser => <<"0">>
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt2">>,
|
credentials => #{
|
||||||
% Wrong password
|
username => <<"bcrypt0">>,
|
||||||
password => <<"wrongpass">>
|
password => <<"bcrypt">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
% clientid variable & username credentials
|
||||||
},
|
selector => #{<<"username">> => <<"${clientid}">>},
|
||||||
result => {error,bad_username_or_password}
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
}
|
},
|
||||||
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
username => <<"bcrypt1">>,
|
||||||
|
password_hash =>
|
||||||
|
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
is_superuser => <<"0">>
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt1">>,
|
||||||
|
password => <<"bcrypt">>
|
||||||
|
},
|
||||||
|
config_params => #{
|
||||||
|
selector => #{<<"userid">> => <<"${clientid}">>},
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
username => <<"bcrypt2">>,
|
||||||
|
password_hash =>
|
||||||
|
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
is_superuser => <<"0">>
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt2">>,
|
||||||
|
% Wrong password
|
||||||
|
password => <<"wrongpass">>
|
||||||
|
},
|
||||||
|
config_params => #{
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, bad_username_or_password}
|
||||||
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
init_seeds() ->
|
init_seeds() ->
|
||||||
|
|
@ -390,14 +428,14 @@ drop_seeds() ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
mongo_server() ->
|
mongo_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?MONGO_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
|
||||||
|
|
||||||
mongo_config() ->
|
mongo_config() ->
|
||||||
[
|
[
|
||||||
{database, <<"mqtt">>},
|
{database, <<"mqtt">>},
|
||||||
{host, ?MONGO_HOST},
|
{host, ?MONGO_HOST},
|
||||||
{port, ?MONGO_DEFAULT_PORT},
|
{port, ?MONGO_DEFAULT_PORT},
|
||||||
{register, ?MONGO_CLIENT}
|
{register, ?MONGO_CLIENT}
|
||||||
].
|
].
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
|
|
||||||
-define(MONGO_HOST, "mongo-tls").
|
-define(MONGO_HOST, "mongo-tls").
|
||||||
|
|
||||||
-define(PATH, [authentication]).
|
-define(PATH, [authentication]).
|
||||||
|
|
@ -37,8 +36,9 @@ init_per_testcase(_TestCase, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
|
@ -54,8 +54,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||||
|
|
||||||
|
|
@ -72,69 +73,90 @@ end_per_suite(_Config) ->
|
||||||
|
|
||||||
t_create(_Config) ->
|
t_create(_Config) ->
|
||||||
?check_trace(
|
?check_trace(
|
||||||
create_mongo_auth_with_ssl_opts(
|
create_mongo_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}),
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
fun({ok, _}, Trace) ->
|
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
fun({ok, _}, Trace) ->
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
[ok | _],
|
[ok | _],
|
||||||
?projection(
|
?projection(
|
||||||
status,
|
status,
|
||||||
?of_kind(emqx_connector_mongo_health_check, Trace)))
|
?of_kind(emqx_connector_mongo_health_check, Trace)
|
||||||
end).
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid_server_name(_Config) ->
|
t_create_invalid_server_name(_Config) ->
|
||||||
?check_trace(
|
?check_trace(
|
||||||
create_mongo_auth_with_ssl_opts(
|
create_mongo_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>}),
|
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||||
fun(_, Trace) ->
|
<<"verify">> => <<"verify_peer">>
|
||||||
?assertNotEqual(
|
}
|
||||||
[ok],
|
),
|
||||||
?projection(
|
fun(_, Trace) ->
|
||||||
status,
|
?assertNotEqual(
|
||||||
?of_kind(emqx_connector_mongo_health_check, Trace)))
|
[ok],
|
||||||
end).
|
?projection(
|
||||||
|
status,
|
||||||
|
?of_kind(emqx_connector_mongo_health_check, Trace)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%% docker-compose-mongo-single-tls.yaml:
|
%% docker-compose-mongo-single-tls.yaml:
|
||||||
%% --tlsDisabledProtocols TLS1_0,TLS1_1
|
%% --tlsDisabledProtocols TLS1_0,TLS1_1
|
||||||
|
|
||||||
t_create_invalid_version(_Config) ->
|
t_create_invalid_version(_Config) ->
|
||||||
?check_trace(
|
?check_trace(
|
||||||
create_mongo_auth_with_ssl_opts(
|
create_mongo_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.1">>]}),
|
<<"verify">> => <<"verify_peer">>,
|
||||||
fun(_, Trace) ->
|
<<"versions">> => [<<"tlsv1.1">>]
|
||||||
?assertNotEqual(
|
}
|
||||||
[ok],
|
),
|
||||||
?projection(
|
fun(_, Trace) ->
|
||||||
status,
|
?assertNotEqual(
|
||||||
?of_kind(emqx_connector_mongo_health_check, Trace)))
|
[ok],
|
||||||
end).
|
?projection(
|
||||||
|
status,
|
||||||
|
?of_kind(emqx_connector_mongo_health_check, Trace)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%% docker-compose-mongo-single-tls.yaml:
|
%% docker-compose-mongo-single-tls.yaml:
|
||||||
%% --setParameter opensslCipherConfig='HIGH:!EXPORT:!aNULL:!DHE:!kDHE@STRENGTH'
|
%% --setParameter opensslCipherConfig='HIGH:!EXPORT:!aNULL:!DHE:!kDHE@STRENGTH'
|
||||||
|
|
||||||
t_invalid_ciphers(_Config) ->
|
t_invalid_ciphers(_Config) ->
|
||||||
?check_trace(
|
?check_trace(
|
||||||
create_mongo_auth_with_ssl_opts(
|
create_mongo_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"DHE-RSA-AES256-GCM-SHA384">>]}),
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
fun(_, Trace) ->
|
<<"ciphers">> => [<<"DHE-RSA-AES256-GCM-SHA384">>]
|
||||||
?assertNotEqual(
|
}
|
||||||
[ok],
|
),
|
||||||
?projection(
|
fun(_, Trace) ->
|
||||||
status,
|
?assertNotEqual(
|
||||||
?of_kind(emqx_connector_mongo_health_check, Trace)))
|
[ok],
|
||||||
end).
|
?projection(
|
||||||
|
status,
|
||||||
|
?of_kind(emqx_connector_mongo_health_check, Trace)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -148,35 +170,38 @@ create_mongo_auth_with_ssl_opts(SpecificSSLOpts) ->
|
||||||
|
|
||||||
raw_mongo_auth_config(SpecificSSLOpts) ->
|
raw_mongo_auth_config(SpecificSSLOpts) ->
|
||||||
SSLOpts = maps:merge(
|
SSLOpts = maps:merge(
|
||||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||||
#{enable => <<"true">>}),
|
#{enable => <<"true">>}
|
||||||
|
),
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
password_hash_algorithm => #{name => <<"plain">>,
|
password_hash_algorithm => #{
|
||||||
salt_position => <<"suffix">>},
|
name => <<"plain">>,
|
||||||
enable => <<"true">>,
|
salt_position => <<"suffix">>
|
||||||
|
},
|
||||||
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"mongodb">>,
|
backend => <<"mongodb">>,
|
||||||
pool_size => 2,
|
pool_size => 2,
|
||||||
mongo_type => <<"single">>,
|
mongo_type => <<"single">>,
|
||||||
database => <<"mqtt">>,
|
database => <<"mqtt">>,
|
||||||
collection => <<"users">>,
|
collection => <<"users">>,
|
||||||
server => mongo_server(),
|
server => mongo_server(),
|
||||||
w_mode => <<"unsafe">>,
|
w_mode => <<"unsafe">>,
|
||||||
|
|
||||||
selector => #{<<"username">> => <<"${username}">>},
|
selector => #{<<"username">> => <<"${username}">>},
|
||||||
password_hash_field => <<"password_hash">>,
|
password_hash_field => <<"password_hash">>,
|
||||||
salt_field => <<"salt">>,
|
salt_field => <<"salt">>,
|
||||||
is_superuser_field => <<"is_superuser">>,
|
is_superuser_field => <<"is_superuser">>,
|
||||||
topology => #{
|
topology => #{
|
||||||
server_selection_timeout_ms => <<"10000ms">>
|
server_selection_timeout_ms => <<"10000ms">>
|
||||||
},
|
},
|
||||||
|
|
||||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
mongo_server() ->
|
mongo_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?MONGO_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -21,28 +21,36 @@
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([start_link/2,
|
-export([
|
||||||
stop/1]).
|
start_link/2,
|
||||||
|
stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([send/2]).
|
-export([send/2]).
|
||||||
|
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
|
|
||||||
-export([init/1,
|
-export([
|
||||||
handle_call/3,
|
init/1,
|
||||||
handle_cast/2,
|
handle_call/3,
|
||||||
handle_info/2,
|
handle_cast/2,
|
||||||
terminate/2]).
|
handle_info/2,
|
||||||
|
terminate/2
|
||||||
|
]).
|
||||||
|
|
||||||
-define(TIMEOUT, 1000).
|
-define(TIMEOUT, 1000).
|
||||||
-define(TCP_OPTIONS, [binary, {packet, raw}, {active, once},
|
-define(TCP_OPTIONS, [
|
||||||
{nodelay, true}]).
|
binary,
|
||||||
|
{packet, raw},
|
||||||
|
{active, once},
|
||||||
|
{nodelay, true}
|
||||||
|
]).
|
||||||
|
|
||||||
-define(PARSE_OPTIONS,
|
-define(PARSE_OPTIONS, #{
|
||||||
#{strict_mode => false,
|
strict_mode => false,
|
||||||
max_size => ?MAX_PACKET_SIZE,
|
max_size => ?MAX_PACKET_SIZE,
|
||||||
version => ?MQTT_PROTO_V5
|
version => ?MQTT_PROTO_V5
|
||||||
}).
|
}).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% API
|
%% API
|
||||||
|
|
@ -63,26 +71,30 @@ send(Pid, Packet) ->
|
||||||
|
|
||||||
init([Host, Port, Owner]) ->
|
init([Host, Port, Owner]) ->
|
||||||
{ok, Socket} = gen_tcp:connect(Host, Port, ?TCP_OPTIONS, ?TIMEOUT),
|
{ok, Socket} = gen_tcp:connect(Host, Port, ?TCP_OPTIONS, ?TIMEOUT),
|
||||||
{ok, #{owner => Owner,
|
{ok, #{
|
||||||
socket => Socket,
|
owner => Owner,
|
||||||
parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS)
|
socket => Socket,
|
||||||
}}.
|
parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS)
|
||||||
|
}}.
|
||||||
|
|
||||||
handle_info({tcp, _Sock, Data}, #{parse_state := PSt,
|
handle_info(
|
||||||
owner := Owner,
|
{tcp, _Sock, Data},
|
||||||
socket := Socket} = St) ->
|
#{
|
||||||
|
parse_state := PSt,
|
||||||
|
owner := Owner,
|
||||||
|
socket := Socket
|
||||||
|
} = St
|
||||||
|
) ->
|
||||||
{NewPSt, Packets} = process_incoming(PSt, Data, []),
|
{NewPSt, Packets} = process_incoming(PSt, Data, []),
|
||||||
ok = deliver(Owner, Packets),
|
ok = deliver(Owner, Packets),
|
||||||
ok = run_sock(Socket),
|
ok = run_sock(Socket),
|
||||||
{noreply, St#{parse_state => NewPSt}};
|
{noreply, St#{parse_state => NewPSt}};
|
||||||
|
|
||||||
handle_info({tcp_closed, _Sock}, St) ->
|
handle_info({tcp_closed, _Sock}, St) ->
|
||||||
{stop, normal, St}.
|
{stop, normal, St}.
|
||||||
|
|
||||||
handle_call({send, Packet}, _From, #{socket := Socket} = St) ->
|
handle_call({send, Packet}, _From, #{socket := Socket} = St) ->
|
||||||
ok = gen_tcp:send(Socket, emqx_frame:serialize(Packet, ?MQTT_PROTO_V5)),
|
ok = gen_tcp:send(Socket, emqx_frame:serialize(Packet, ?MQTT_PROTO_V5)),
|
||||||
{reply, ok, St};
|
{reply, ok, St};
|
||||||
|
|
||||||
handle_call(stop, _From, #{socket := Socket} = St) ->
|
handle_call(stop, _From, #{socket := Socket} = St) ->
|
||||||
ok = gen_tcp:close(Socket),
|
ok = gen_tcp:close(Socket),
|
||||||
{stop, normal, ok, St}.
|
{stop, normal, ok, St}.
|
||||||
|
|
@ -100,16 +112,16 @@ terminate(_Reason, _St) ->
|
||||||
process_incoming(PSt, Data, Packets) ->
|
process_incoming(PSt, Data, Packets) ->
|
||||||
case emqx_frame:parse(Data, PSt) of
|
case emqx_frame:parse(Data, PSt) of
|
||||||
{more, NewPSt} ->
|
{more, NewPSt} ->
|
||||||
{NewPSt, lists:reverse(Packets)};
|
{NewPSt, lists:reverse(Packets)};
|
||||||
{ok, Packet, Rest, NewPSt} ->
|
{ok, Packet, Rest, NewPSt} ->
|
||||||
process_incoming(NewPSt, Rest, [Packet | Packets])
|
process_incoming(NewPSt, Rest, [Packet | Packets])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
deliver(_Owner, []) -> ok;
|
deliver(_Owner, []) ->
|
||||||
|
ok;
|
||||||
deliver(Owner, [Packet | Packets]) ->
|
deliver(Owner, [Packet | Packets]) ->
|
||||||
Owner ! {packet, Packet},
|
Owner ! {packet, Packet},
|
||||||
deliver(Owner, Packets).
|
deliver(Owner, Packets).
|
||||||
|
|
||||||
|
|
||||||
run_sock(Socket) ->
|
run_sock(Socket) ->
|
||||||
inet:setopts(Socket, [{active, once}]).
|
inet:setopts(Socket, [{active, once}]).
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,9 @@ init_per_testcase(_, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
init_per_group(require_seeds, Config) ->
|
init_per_group(require_seeds, Config) ->
|
||||||
|
|
@ -59,11 +60,12 @@ init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
||||||
ok = start_apps([emqx_resource, emqx_connector]),
|
ok = start_apps([emqx_resource, emqx_connector]),
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?MYSQL_RESOURCE,
|
?MYSQL_RESOURCE,
|
||||||
?RESOURCE_GROUP,
|
?RESOURCE_GROUP,
|
||||||
emqx_connector_mysql,
|
emqx_connector_mysql,
|
||||||
mysql_config(),
|
mysql_config(),
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
Config;
|
Config;
|
||||||
false ->
|
false ->
|
||||||
{skip, no_mysql}
|
{skip, no_mysql}
|
||||||
|
|
@ -71,8 +73,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
|
ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||||
|
|
@ -85,8 +88,9 @@ t_create(_Config) ->
|
||||||
AuthConfig = raw_mysql_auth_config(),
|
AuthConfig = raw_mysql_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_mysql}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
{ok, [#{provider := emqx_authn_mysql}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID).
|
emqx_authn_test_lib:delete_config(?ResourceID).
|
||||||
|
|
@ -96,108 +100,132 @@ t_create_invalid(_Config) ->
|
||||||
|
|
||||||
InvalidConfigs =
|
InvalidConfigs =
|
||||||
[
|
[
|
||||||
maps:without([server], AuthConfig),
|
maps:without([server], AuthConfig),
|
||||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||||
AuthConfig#{password => <<"wrongpass">>},
|
AuthConfig#{password => <<"wrongpass">>},
|
||||||
AuthConfig#{database => <<"wrongdatabase">>}
|
AuthConfig#{database => <<"wrongdatabase">>}
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Config) ->
|
fun(Config) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
),
|
||||||
{ok, _} = emqx_authentication:list_authenticators(?GLOBAL)
|
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||||
end,
|
{ok, _} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||||
InvalidConfigs).
|
end,
|
||||||
|
InvalidConfigs
|
||||||
|
).
|
||||||
|
|
||||||
t_authenticate(_Config) ->
|
t_authenticate(_Config) ->
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(Sample) ->
|
fun(Sample) ->
|
||||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||||
test_user_auth(Sample)
|
test_user_auth(Sample)
|
||||||
end,
|
end,
|
||||||
user_seeds()).
|
user_seeds()
|
||||||
|
).
|
||||||
|
|
||||||
test_user_auth(#{credentials := Credentials0,
|
test_user_auth(#{
|
||||||
config_params := SpecificConfigParams,
|
credentials := Credentials0,
|
||||||
result := Result}) ->
|
config_params := SpecificConfigParams,
|
||||||
|
result := Result
|
||||||
|
}) ->
|
||||||
AuthConfig = maps:merge(raw_mysql_auth_config(), SpecificConfigParams),
|
AuthConfig = maps:merge(raw_mysql_auth_config(), SpecificConfigParams),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
Credentials = Credentials0#{
|
Credentials = Credentials0#{
|
||||||
listener => 'tcp:default',
|
listener => 'tcp:default',
|
||||||
protocol => mqtt
|
protocol => mqtt
|
||||||
},
|
},
|
||||||
|
|
||||||
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL).
|
?GLOBAL
|
||||||
|
).
|
||||||
|
|
||||||
t_destroy(_Config) ->
|
t_destroy(_Config) ->
|
||||||
AuthConfig = raw_mysql_auth_config(),
|
AuthConfig = raw_mysql_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_mysql, state := State}]}
|
{ok, [#{provider := emqx_authn_mysql, state := State}]} =
|
||||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
emqx_authentication:list_authenticators(?GLOBAL),
|
||||||
|
|
||||||
{ok, _} = emqx_authn_mysql:authenticate(
|
{ok, _} = emqx_authn_mysql:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>
|
username => <<"plain">>,
|
||||||
},
|
password => <<"plain">>
|
||||||
State),
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
|
|
||||||
% Authenticator should not be usable anymore
|
% Authenticator should not be usable anymore
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
ignore,
|
ignore,
|
||||||
emqx_authn_mysql:authenticate(
|
emqx_authn_mysql:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>
|
username => <<"plain">>,
|
||||||
},
|
password => <<"plain">>
|
||||||
State)).
|
},
|
||||||
|
State
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
t_update(_Config) ->
|
t_update(_Config) ->
|
||||||
CorrectConfig = raw_mysql_auth_config(),
|
CorrectConfig = raw_mysql_auth_config(),
|
||||||
IncorrectConfig =
|
IncorrectConfig =
|
||||||
CorrectConfig#{
|
CorrectConfig#{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
query =>
|
||||||
FROM wrong_table where username = ${username} LIMIT 1">>},
|
<<
|
||||||
|
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||||
|
" FROM wrong_table where username = ${username} LIMIT 1"
|
||||||
|
>>
|
||||||
|
},
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{error, not_authorized} = emqx_access_control:authenticate(
|
{error, not_authorized} = emqx_access_control:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}),
|
protocol => mqtt
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
% We update with config with correct query, provider should update and work properly
|
% We update with config with correct query, provider should update and work properly
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{update_authenticator, ?GLOBAL, <<"password_based:mysql">>, CorrectConfig}),
|
{update_authenticator, ?GLOBAL, <<"password_based:mysql">>, CorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok,_} = emqx_access_control:authenticate(
|
{ok, _} = emqx_access_control:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}).
|
protocol => mqtt
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -205,207 +233,248 @@ t_update(_Config) ->
|
||||||
|
|
||||||
raw_mysql_auth_config() ->
|
raw_mysql_auth_config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
password_hash_algorithm => #{name => <<"plain">>,
|
password_hash_algorithm => #{
|
||||||
salt_position => <<"suffix">>},
|
name => <<"plain">>,
|
||||||
enable => <<"true">>,
|
salt_position => <<"suffix">>
|
||||||
|
},
|
||||||
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"mysql">>,
|
backend => <<"mysql">>,
|
||||||
database => <<"mqtt">>,
|
database => <<"mqtt">>,
|
||||||
username => <<"root">>,
|
username => <<"root">>,
|
||||||
password => <<"public">>,
|
password => <<"public">>,
|
||||||
|
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
query =>
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
<<
|
||||||
server => mysql_server()
|
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||||
}.
|
" FROM users where username = ${username} LIMIT 1"
|
||||||
|
>>,
|
||||||
|
server => mysql_server()
|
||||||
|
}.
|
||||||
|
|
||||||
user_seeds() ->
|
user_seeds() ->
|
||||||
[#{data => #{
|
[
|
||||||
username => "plain",
|
#{
|
||||||
password_hash => "plainsalt",
|
data => #{
|
||||||
salt => "salt",
|
username => "plain",
|
||||||
is_superuser_str => "1"
|
password_hash => "plainsalt",
|
||||||
},
|
salt => "salt",
|
||||||
credentials => #{
|
is_superuser_str => "1"
|
||||||
username => <<"plain">>,
|
},
|
||||||
password => <<"plain">>},
|
credentials => #{
|
||||||
config_params => #{},
|
username => <<"plain">>,
|
||||||
result => {ok,#{is_superuser => true}}
|
password => <<"plain">>
|
||||||
},
|
},
|
||||||
|
config_params => #{},
|
||||||
#{data => #{
|
result => {ok, #{is_superuser => true}}
|
||||||
username => "md5",
|
|
||||||
password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
|
|
||||||
salt => "salt",
|
|
||||||
is_superuser_str => "0"
|
|
||||||
},
|
|
||||||
credentials => #{
|
|
||||||
username => <<"md5">>,
|
|
||||||
password => <<"md5">>
|
|
||||||
},
|
|
||||||
config_params => #{
|
|
||||||
password_hash_algorithm => #{name => <<"md5">>,
|
|
||||||
salt_position => <<"suffix">>}
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => false}}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
|
||||||
username => "sha256",
|
|
||||||
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
|
|
||||||
salt => "salt",
|
|
||||||
is_superuser_int => 1
|
|
||||||
},
|
},
|
||||||
credentials => #{
|
|
||||||
clientid => <<"sha256">>,
|
|
||||||
password => <<"sha256">>
|
|
||||||
},
|
|
||||||
config_params => #{
|
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
|
||||||
FROM users where username = ${clientid} LIMIT 1">>,
|
|
||||||
password_hash_algorithm => #{name => <<"sha256">>,
|
|
||||||
salt_position => <<"prefix">>}
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => true}}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => "md5",
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
|
||||||
is_superuser_int => 0
|
salt => "salt",
|
||||||
},
|
is_superuser_str => "0"
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt">>,
|
credentials => #{
|
||||||
password => <<"bcrypt">>
|
username => <<"md5">>,
|
||||||
},
|
password => <<"md5">>
|
||||||
config_params => #{
|
},
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
config_params => #{
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
password_hash_algorithm => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
name => <<"md5">>,
|
||||||
},
|
salt_position => <<"suffix">>
|
||||||
result => {ok,#{is_superuser => false}}
|
}
|
||||||
},
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => "sha256",
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve"
|
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
|
||||||
},
|
salt => "salt",
|
||||||
credentials => #{
|
is_superuser_int => 1
|
||||||
username => <<"bcrypt">>,
|
},
|
||||||
password => <<"bcrypt">>
|
credentials => #{
|
||||||
},
|
clientid => <<"sha256">>,
|
||||||
config_params => #{
|
password => <<"sha256">>
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
},
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
config_params => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
query =>
|
||||||
},
|
<<
|
||||||
result => {ok,#{is_superuser => false}}
|
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||||
},
|
" FROM users where username = ${clientid} LIMIT 1"
|
||||||
|
>>,
|
||||||
|
password_hash_algorithm => #{
|
||||||
|
name => <<"sha256">>,
|
||||||
|
salt_position => <<"prefix">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => true}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt0">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => <<"bcrypt">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
is_superuser_str => "0"
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||||
},
|
is_superuser_int => 0
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt0">>,
|
credentials => #{
|
||||||
password => <<"bcrypt">>
|
username => <<"bcrypt">>,
|
||||||
},
|
password => <<"bcrypt">>
|
||||||
config_params => #{
|
},
|
||||||
% clientid variable & username credentials
|
config_params => #{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query =>
|
||||||
FROM users where username = ${clientid} LIMIT 1">>,
|
<<
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||||
},
|
" FROM users where username = ${username} LIMIT 1"
|
||||||
result => {error,not_authorized}
|
>>,
|
||||||
},
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt1">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => <<"bcrypt">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
is_superuser_str => "0"
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve"
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"bcrypt1">>,
|
username => <<"bcrypt">>,
|
||||||
password => <<"bcrypt">>
|
password => <<"bcrypt">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
% Bad keys in query
|
query =>
|
||||||
query => <<"SELECT 1 AS unknown_field
|
<<
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
" FROM users where username = ${username} LIMIT 1"
|
||||||
},
|
>>,
|
||||||
result => {error,not_authorized}
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
},
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt2">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => <<"bcrypt0">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
is_superuser => "0"
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||||
},
|
is_superuser_str => "0"
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt2">>,
|
credentials => #{
|
||||||
% Wrong password
|
username => <<"bcrypt0">>,
|
||||||
password => <<"wrongpass">>
|
password => <<"bcrypt">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
% clientid variable & username credentials
|
||||||
},
|
query =>
|
||||||
result => {error,bad_username_or_password}
|
<<
|
||||||
}
|
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||||
|
" FROM users where username = ${clientid} LIMIT 1"
|
||||||
|
>>,
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
username => <<"bcrypt1">>,
|
||||||
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||||
|
is_superuser_str => "0"
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt1">>,
|
||||||
|
password => <<"bcrypt">>
|
||||||
|
},
|
||||||
|
config_params => #{
|
||||||
|
% Bad keys in query
|
||||||
|
query =>
|
||||||
|
<<
|
||||||
|
"SELECT 1 AS unknown_field\n"
|
||||||
|
" FROM users where username = ${username} LIMIT 1"
|
||||||
|
>>,
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
username => <<"bcrypt2">>,
|
||||||
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||||
|
is_superuser => "0"
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt2">>,
|
||||||
|
% Wrong password
|
||||||
|
password => <<"wrongpass">>
|
||||||
|
},
|
||||||
|
config_params => #{
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, bad_username_or_password}
|
||||||
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
init_seeds() ->
|
init_seeds() ->
|
||||||
ok = drop_seeds(),
|
ok = drop_seeds(),
|
||||||
ok = q("CREATE TABLE users(
|
ok = q(
|
||||||
username VARCHAR(255),
|
"CREATE TABLE users(\n"
|
||||||
password_hash VARCHAR(255),
|
" username VARCHAR(255),\n"
|
||||||
salt VARCHAR(255),
|
" password_hash VARCHAR(255),\n"
|
||||||
is_superuser_str VARCHAR(255),
|
" salt VARCHAR(255),\n"
|
||||||
is_superuser_int TINYINT)"),
|
" is_superuser_str VARCHAR(255),\n"
|
||||||
|
" is_superuser_int TINYINT)"
|
||||||
|
),
|
||||||
|
|
||||||
Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int],
|
Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int],
|
||||||
InsertQuery = "INSERT INTO users(username, password_hash, salt, "
|
InsertQuery =
|
||||||
" is_superuser_str, is_superuser_int) VALUES(?, ?, ?, ?, ?)",
|
"INSERT INTO users(username, password_hash, salt, "
|
||||||
|
" is_superuser_str, is_superuser_int) VALUES(?, ?, ?, ?, ?)",
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(#{data := Values}) ->
|
fun(#{data := Values}) ->
|
||||||
Params = [maps:get(F, Values, null) || F <- Fields],
|
Params = [maps:get(F, Values, null) || F <- Fields],
|
||||||
ok = q(InsertQuery, Params)
|
ok = q(InsertQuery, Params)
|
||||||
end,
|
end,
|
||||||
user_seeds()).
|
user_seeds()
|
||||||
|
).
|
||||||
|
|
||||||
q(Sql) ->
|
q(Sql) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?MYSQL_RESOURCE,
|
?MYSQL_RESOURCE,
|
||||||
{sql, Sql}).
|
{sql, Sql}
|
||||||
|
).
|
||||||
|
|
||||||
q(Sql, Params) ->
|
q(Sql, Params) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?MYSQL_RESOURCE,
|
?MYSQL_RESOURCE,
|
||||||
{sql, Sql, Params}).
|
{sql, Sql, Params}
|
||||||
|
).
|
||||||
|
|
||||||
drop_seeds() ->
|
drop_seeds() ->
|
||||||
ok = q("DROP TABLE IF EXISTS users").
|
ok = q("DROP TABLE IF EXISTS users").
|
||||||
|
|
||||||
mysql_server() ->
|
mysql_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?MYSQL_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?MYSQL_HOST])).
|
||||||
|
|
||||||
mysql_config() ->
|
mysql_config() ->
|
||||||
#{auto_reconnect => true,
|
#{
|
||||||
database => <<"mqtt">>,
|
auto_reconnect => true,
|
||||||
username => <<"root">>,
|
database => <<"mqtt">>,
|
||||||
password => <<"public">>,
|
username => <<"root">>,
|
||||||
pool_size => 8,
|
password => <<"public">>,
|
||||||
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
|
pool_size => 8,
|
||||||
ssl => #{enable => false}
|
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
|
||||||
}.
|
ssl => #{enable => false}
|
||||||
|
}.
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
|
@ -56,8 +57,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||||
|
|
||||||
|
|
@ -70,38 +72,53 @@ t_create(_Config) ->
|
||||||
%% -connect authn-server:3306 -starttls mysql \
|
%% -connect authn-server:3306 -starttls mysql \
|
||||||
%% -cert client.crt -key client.key -CAfile ca.crt
|
%% -cert client.crt -key client.key -CAfile ca.crt
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_mysql_auth_with_ssl_opts(
|
create_mysql_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]})).
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
|
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid(_Config) ->
|
t_create_invalid(_Config) ->
|
||||||
|
|
||||||
%% invalid server_name
|
%% invalid server_name
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_mysql_auth_with_ssl_opts(
|
create_mysql_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>})),
|
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||||
|
<<"verify">> => <<"verify_peer">>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||||
%% incompatible versions
|
%% incompatible versions
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_mysql_auth_with_ssl_opts(
|
create_mysql_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.1">>]})),
|
<<"verify">> => <<"verify_peer">>,
|
||||||
|
<<"versions">> => [<<"tlsv1.1">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||||
%% incompatible ciphers
|
%% incompatible ciphers
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_mysql_auth_with_ssl_opts(
|
create_mysql_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]})).
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
|
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -113,27 +130,33 @@ create_mysql_auth_with_ssl_opts(SpecificSSLOpts) ->
|
||||||
|
|
||||||
raw_mysql_auth_config(SpecificSSLOpts) ->
|
raw_mysql_auth_config(SpecificSSLOpts) ->
|
||||||
SSLOpts = maps:merge(
|
SSLOpts = maps:merge(
|
||||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||||
#{enable => <<"true">>}),
|
#{enable => <<"true">>}
|
||||||
|
),
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
password_hash_algorithm => #{name => <<"plain">>,
|
password_hash_algorithm => #{
|
||||||
salt_position => <<"suffix">>},
|
name => <<"plain">>,
|
||||||
enable => <<"true">>,
|
salt_position => <<"suffix">>
|
||||||
|
},
|
||||||
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"mysql">>,
|
backend => <<"mysql">>,
|
||||||
database => <<"mqtt">>,
|
database => <<"mqtt">>,
|
||||||
username => <<"root">>,
|
username => <<"root">>,
|
||||||
password => <<"public">>,
|
password => <<"public">>,
|
||||||
|
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
query =>
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
<<
|
||||||
server => mysql_server(),
|
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
" FROM users where username = ${username} LIMIT 1"
|
||||||
}.
|
>>,
|
||||||
|
server => mysql_server(),
|
||||||
|
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||||
|
}.
|
||||||
|
|
||||||
mysql_server() ->
|
mysql_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?MYSQL_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?MYSQL_HOST])).
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -38,118 +38,150 @@ end_per_suite(_Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_gen_salt(_Config) ->
|
t_gen_salt(_Config) ->
|
||||||
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES]
|
Algorithms =
|
||||||
++ [#{name => bcrypt, salt_rounds => 10}],
|
[#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES] ++
|
||||||
|
[#{name => bcrypt, salt_rounds => 10}],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Algorithm) ->
|
fun(Algorithm) ->
|
||||||
Salt = emqx_authn_password_hashing:gen_salt(Algorithm),
|
Salt = emqx_authn_password_hashing:gen_salt(Algorithm),
|
||||||
ct:pal("gen_salt(~p): ~p", [Algorithm, Salt]),
|
ct:pal("gen_salt(~p): ~p", [Algorithm, Salt]),
|
||||||
?assert(is_binary(Salt))
|
?assert(is_binary(Salt))
|
||||||
end,
|
end,
|
||||||
Algorithms).
|
Algorithms
|
||||||
|
).
|
||||||
|
|
||||||
t_init(_Config) ->
|
t_init(_Config) ->
|
||||||
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES]
|
Algorithms =
|
||||||
++ [#{name => bcrypt, salt_rounds => 10}],
|
[#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES] ++
|
||||||
|
[#{name => bcrypt, salt_rounds => 10}],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Algorithm) ->
|
fun(Algorithm) ->
|
||||||
ok = emqx_authn_password_hashing:init(Algorithm)
|
ok = emqx_authn_password_hashing:init(Algorithm)
|
||||||
end,
|
end,
|
||||||
Algorithms).
|
Algorithms
|
||||||
|
).
|
||||||
|
|
||||||
t_check_password(_Config) ->
|
t_check_password(_Config) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun test_check_password/1,
|
fun test_check_password/1,
|
||||||
hash_examples()).
|
hash_examples()
|
||||||
|
).
|
||||||
|
|
||||||
test_check_password(#{
|
test_check_password(
|
||||||
password_hash := Hash,
|
#{
|
||||||
salt := Salt,
|
password_hash := Hash,
|
||||||
password := Password,
|
salt := Salt,
|
||||||
password_hash_algorithm := Algorithm
|
password := Password,
|
||||||
} = Sample) ->
|
password_hash_algorithm := Algorithm
|
||||||
|
} = Sample
|
||||||
|
) ->
|
||||||
ct:pal("t_check_password sample: ~p", [Sample]),
|
ct:pal("t_check_password sample: ~p", [Sample]),
|
||||||
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password),
|
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password),
|
||||||
false = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, <<"wrongpass">>).
|
false = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, <<"wrongpass">>).
|
||||||
|
|
||||||
t_hash(_Config) ->
|
t_hash(_Config) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun test_hash/1,
|
fun test_hash/1,
|
||||||
hash_examples()).
|
hash_examples()
|
||||||
|
).
|
||||||
|
|
||||||
test_hash(#{password := Password,
|
test_hash(
|
||||||
password_hash_algorithm := Algorithm
|
#{
|
||||||
} = Sample) ->
|
password := Password,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
} = Sample
|
||||||
|
) ->
|
||||||
ct:pal("t_hash sample: ~p", [Sample]),
|
ct:pal("t_hash sample: ~p", [Sample]),
|
||||||
{Hash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password),
|
{Hash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password),
|
||||||
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password).
|
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password).
|
||||||
|
|
||||||
hash_examples() ->
|
hash_examples() ->
|
||||||
[#{
|
[
|
||||||
password_hash => <<"plainsalt">>,
|
#{
|
||||||
salt => <<"salt">>,
|
password_hash => <<"plainsalt">>,
|
||||||
password => <<"plain">>,
|
salt => <<"salt">>,
|
||||||
password_hash_algorithm => #{name => plain,
|
password => <<"plain">>,
|
||||||
salt_position => suffix}
|
password_hash_algorithm => #{
|
||||||
},
|
name => plain,
|
||||||
#{
|
salt_position => suffix
|
||||||
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
}
|
||||||
salt => <<"salt">>,
|
},
|
||||||
password => <<"md5">>,
|
#{
|
||||||
password_hash_algorithm => #{name => md5,
|
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
||||||
salt_position => suffix}
|
salt => <<"salt">>,
|
||||||
},
|
password => <<"md5">>,
|
||||||
#{
|
password_hash_algorithm => #{
|
||||||
password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>,
|
name => md5,
|
||||||
salt => <<"salt">>,
|
salt_position => suffix
|
||||||
password => <<"sha">>,
|
}
|
||||||
password_hash_algorithm => #{name => sha,
|
},
|
||||||
salt_position => prefix}
|
#{
|
||||||
},
|
password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>,
|
||||||
#{
|
salt => <<"salt">>,
|
||||||
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
password => <<"sha">>,
|
||||||
salt => <<"salt">>,
|
password_hash_algorithm => #{
|
||||||
password => <<"sha256">>,
|
name => sha,
|
||||||
password_hash_algorithm => #{name => sha256,
|
salt_position => prefix
|
||||||
salt_position => prefix}
|
}
|
||||||
},
|
},
|
||||||
#{
|
#{
|
||||||
password_hash => <<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8"
|
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
||||||
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>,
|
salt => <<"salt">>,
|
||||||
salt => <<"salt">>,
|
password => <<"sha256">>,
|
||||||
password => <<"sha512">>,
|
password_hash_algorithm => #{
|
||||||
password_hash_algorithm => #{name => sha512,
|
name => sha256,
|
||||||
salt_position => prefix}
|
salt_position => prefix
|
||||||
},
|
}
|
||||||
#{
|
},
|
||||||
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
#{
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
password_hash => <<
|
||||||
password => <<"bcrypt">>,
|
"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8"
|
||||||
|
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd"
|
||||||
|
>>,
|
||||||
|
salt => <<"salt">>,
|
||||||
|
password => <<"sha512">>,
|
||||||
|
password_hash_algorithm => #{
|
||||||
|
name => sha512,
|
||||||
|
salt_position => prefix
|
||||||
|
}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
password => <<"bcrypt">>,
|
||||||
|
|
||||||
password_hash_algorithm => #{name => bcrypt,
|
password_hash_algorithm => #{
|
||||||
salt_rounds => 10}
|
name => bcrypt,
|
||||||
},
|
salt_rounds => 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
#{
|
#{
|
||||||
password_hash => <<"01dbee7f4a9e243e988b62c73cda935d"
|
password_hash => <<
|
||||||
"a05378b93244ec8f48a99e61ad799d86">>,
|
"01dbee7f4a9e243e988b62c73cda935d"
|
||||||
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
"a05378b93244ec8f48a99e61ad799d86"
|
||||||
password => <<"password">>,
|
>>,
|
||||||
|
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||||
|
password => <<"password">>,
|
||||||
|
|
||||||
password_hash_algorithm => #{name => pbkdf2,
|
password_hash_algorithm => #{
|
||||||
iterations => 2,
|
name => pbkdf2,
|
||||||
dk_length => 32,
|
iterations => 2,
|
||||||
mac_fun => sha}
|
dk_length => 32,
|
||||||
},
|
mac_fun => sha
|
||||||
#{
|
}
|
||||||
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
|
},
|
||||||
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
#{
|
||||||
password => <<"password">>,
|
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
|
||||||
|
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||||
|
password => <<"password">>,
|
||||||
|
|
||||||
password_hash_algorithm => #{name => pbkdf2,
|
password_hash_algorithm => #{
|
||||||
iterations => 2,
|
name => pbkdf2,
|
||||||
mac_fun => sha}
|
iterations => 2,
|
||||||
}
|
mac_fun => sha
|
||||||
|
}
|
||||||
|
}
|
||||||
].
|
].
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,9 @@ init_per_testcase(_, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
init_per_group(require_seeds, Config) ->
|
init_per_group(require_seeds, Config) ->
|
||||||
|
|
@ -60,11 +61,12 @@ init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
||||||
ok = start_apps([emqx_resource, emqx_connector]),
|
ok = start_apps([emqx_resource, emqx_connector]),
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?PGSQL_RESOURCE,
|
?PGSQL_RESOURCE,
|
||||||
?RESOURCE_GROUP,
|
?RESOURCE_GROUP,
|
||||||
emqx_connector_pgsql,
|
emqx_connector_pgsql,
|
||||||
pgsql_config(),
|
pgsql_config(),
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
Config;
|
Config;
|
||||||
false ->
|
false ->
|
||||||
{skip, no_pgsql}
|
{skip, no_pgsql}
|
||||||
|
|
@ -72,8 +74,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
|
ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||||
|
|
@ -86,8 +89,9 @@ t_create(_Config) ->
|
||||||
AuthConfig = raw_pgsql_auth_config(),
|
AuthConfig = raw_pgsql_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_pgsql}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
{ok, [#{provider := emqx_authn_pgsql}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID).
|
emqx_authn_test_lib:delete_config(?ResourceID).
|
||||||
|
|
@ -97,131 +101,156 @@ t_create_invalid(_Config) ->
|
||||||
|
|
||||||
InvalidConfigs =
|
InvalidConfigs =
|
||||||
[
|
[
|
||||||
maps:without([server], AuthConfig),
|
maps:without([server], AuthConfig),
|
||||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||||
AuthConfig#{password => <<"wrongpass">>},
|
AuthConfig#{password => <<"wrongpass">>},
|
||||||
AuthConfig#{database => <<"wrongdatabase">>}
|
AuthConfig#{database => <<"wrongdatabase">>}
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Config) ->
|
fun(Config) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
),
|
||||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||||
end,
|
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||||
InvalidConfigs).
|
end,
|
||||||
|
InvalidConfigs
|
||||||
|
).
|
||||||
|
|
||||||
t_authenticate(_Config) ->
|
t_authenticate(_Config) ->
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(Sample) ->
|
fun(Sample) ->
|
||||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||||
test_user_auth(Sample)
|
test_user_auth(Sample)
|
||||||
end,
|
end,
|
||||||
user_seeds()).
|
user_seeds()
|
||||||
|
).
|
||||||
|
|
||||||
test_user_auth(#{credentials := Credentials0,
|
test_user_auth(#{
|
||||||
config_params := SpecificConfigParams,
|
credentials := Credentials0,
|
||||||
result := Result}) ->
|
config_params := SpecificConfigParams,
|
||||||
|
result := Result
|
||||||
|
}) ->
|
||||||
AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfigParams),
|
AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfigParams),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
Credentials = Credentials0#{
|
Credentials = Credentials0#{
|
||||||
listener => 'tcp:default',
|
listener => 'tcp:default',
|
||||||
protocol => mqtt
|
protocol => mqtt
|
||||||
},
|
},
|
||||||
|
|
||||||
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL).
|
?GLOBAL
|
||||||
|
).
|
||||||
|
|
||||||
t_destroy(_Config) ->
|
t_destroy(_Config) ->
|
||||||
AuthConfig = raw_pgsql_auth_config(),
|
AuthConfig = raw_pgsql_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_pgsql, state := State}]}
|
{ok, [#{provider := emqx_authn_pgsql, state := State}]} =
|
||||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
emqx_authentication:list_authenticators(?GLOBAL),
|
||||||
|
|
||||||
{ok, _} = emqx_authn_pgsql:authenticate(
|
{ok, _} = emqx_authn_pgsql:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>
|
username => <<"plain">>,
|
||||||
},
|
password => <<"plain">>
|
||||||
State),
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
|
|
||||||
% Authenticator should not be usable anymore
|
% Authenticator should not be usable anymore
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
ignore,
|
ignore,
|
||||||
emqx_authn_pgsql:authenticate(
|
emqx_authn_pgsql:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>
|
username => <<"plain">>,
|
||||||
},
|
password => <<"plain">>
|
||||||
State)).
|
},
|
||||||
|
State
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
t_update(_Config) ->
|
t_update(_Config) ->
|
||||||
CorrectConfig = raw_pgsql_auth_config(),
|
CorrectConfig = raw_pgsql_auth_config(),
|
||||||
IncorrectConfig =
|
IncorrectConfig =
|
||||||
CorrectConfig#{
|
CorrectConfig#{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
query =>
|
||||||
FROM users where username = ${username} LIMIT 0">>},
|
<<
|
||||||
|
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||||
|
" FROM users where username = ${username} LIMIT 0"
|
||||||
|
>>
|
||||||
|
},
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{error, not_authorized} = emqx_access_control:authenticate(
|
{error, not_authorized} = emqx_access_control:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}),
|
protocol => mqtt
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
% We update with config with correct query, provider should update and work properly
|
% We update with config with correct query, provider should update and work properly
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, CorrectConfig}),
|
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, CorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok,_} = emqx_access_control:authenticate(
|
{ok, _} = emqx_access_control:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}).
|
protocol => mqtt
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
t_is_superuser(_Config) ->
|
t_is_superuser(_Config) ->
|
||||||
Config = raw_pgsql_auth_config(),
|
Config = raw_pgsql_auth_config(),
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
|
),
|
||||||
|
|
||||||
Checks = [
|
Checks = [
|
||||||
{is_superuser_str, "0", false},
|
{is_superuser_str, "0", false},
|
||||||
{is_superuser_str, "", false},
|
{is_superuser_str, "", false},
|
||||||
{is_superuser_str, null, false},
|
{is_superuser_str, null, false},
|
||||||
{is_superuser_str, "1", true},
|
{is_superuser_str, "1", true},
|
||||||
{is_superuser_str, "val", true},
|
{is_superuser_str, "val", true},
|
||||||
|
|
||||||
{is_superuser_int, 0, false},
|
{is_superuser_int, 0, false},
|
||||||
{is_superuser_int, null, false},
|
{is_superuser_int, null, false},
|
||||||
{is_superuser_int, 1, true},
|
{is_superuser_int, 1, true},
|
||||||
{is_superuser_int, 123, true},
|
{is_superuser_int, 123, true},
|
||||||
|
|
||||||
{is_superuser_bool, false, false},
|
{is_superuser_bool, false, false},
|
||||||
{is_superuser_bool, null, false},
|
{is_superuser_bool, null, false},
|
||||||
{is_superuser_bool, true, true}
|
{is_superuser_bool, true, true}
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(fun test_is_superuser/1, Checks).
|
lists:foreach(fun test_is_superuser/1, Checks).
|
||||||
|
|
||||||
|
|
@ -229,32 +258,36 @@ test_is_superuser({Field, Value, ExpectedValue}) ->
|
||||||
{ok, _} = q("DELETE FROM users"),
|
{ok, _} = q("DELETE FROM users"),
|
||||||
|
|
||||||
UserData = #{
|
UserData = #{
|
||||||
username => "user",
|
username => "user",
|
||||||
password_hash => "plainsalt",
|
password_hash => "plainsalt",
|
||||||
salt => "salt",
|
salt => "salt",
|
||||||
Field => Value
|
Field => Value
|
||||||
},
|
},
|
||||||
|
|
||||||
ok = create_user(UserData),
|
ok = create_user(UserData),
|
||||||
|
|
||||||
Query = "SELECT password_hash, salt, " ++ atom_to_list(Field) ++ " as is_superuser "
|
Query =
|
||||||
"FROM users where username = ${username} LIMIT 1",
|
"SELECT password_hash, salt, " ++ atom_to_list(Field) ++
|
||||||
|
" as is_superuser "
|
||||||
|
"FROM users where username = ${username} LIMIT 1",
|
||||||
|
|
||||||
Config = maps:put(query, Query, raw_pgsql_auth_config()),
|
Config = maps:put(query, Query, raw_pgsql_auth_config()),
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, Config}),
|
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, Config}
|
||||||
|
),
|
||||||
|
|
||||||
Credentials = #{
|
Credentials = #{
|
||||||
listener => 'tcp:default',
|
listener => 'tcp:default',
|
||||||
protocol => mqtt,
|
protocol => mqtt,
|
||||||
username => <<"user">>,
|
username => <<"user">>,
|
||||||
password => <<"plain">>
|
password => <<"plain">>
|
||||||
},
|
},
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
{ok, #{is_superuser => ExpectedValue}},
|
{ok, #{is_superuser => ExpectedValue}},
|
||||||
emqx_access_control:authenticate(Credentials)).
|
emqx_access_control:authenticate(Credentials)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -262,167 +295,201 @@ test_is_superuser({Field, Value, ExpectedValue}) ->
|
||||||
|
|
||||||
raw_pgsql_auth_config() ->
|
raw_pgsql_auth_config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
password_hash_algorithm => #{name => <<"plain">>,
|
password_hash_algorithm => #{
|
||||||
salt_position => <<"suffix">>},
|
name => <<"plain">>,
|
||||||
enable => <<"true">>,
|
salt_position => <<"suffix">>
|
||||||
|
},
|
||||||
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"postgresql">>,
|
backend => <<"postgresql">>,
|
||||||
database => <<"mqtt">>,
|
database => <<"mqtt">>,
|
||||||
username => <<"root">>,
|
username => <<"root">>,
|
||||||
password => <<"public">>,
|
password => <<"public">>,
|
||||||
|
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
query =>
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
<<
|
||||||
server => pgsql_server()
|
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||||
}.
|
" FROM users where username = ${username} LIMIT 1"
|
||||||
|
>>,
|
||||||
|
server => pgsql_server()
|
||||||
|
}.
|
||||||
|
|
||||||
user_seeds() ->
|
user_seeds() ->
|
||||||
[#{data => #{
|
[
|
||||||
username => "plain",
|
#{
|
||||||
password_hash => "plainsalt",
|
data => #{
|
||||||
salt => "salt",
|
username => "plain",
|
||||||
is_superuser_str => "1"
|
password_hash => "plainsalt",
|
||||||
},
|
salt => "salt",
|
||||||
credentials => #{
|
is_superuser_str => "1"
|
||||||
username => <<"plain">>,
|
},
|
||||||
password => <<"plain">>},
|
credentials => #{
|
||||||
config_params => #{},
|
username => <<"plain">>,
|
||||||
result => {ok,#{is_superuser => true}}
|
password => <<"plain">>
|
||||||
},
|
},
|
||||||
|
config_params => #{},
|
||||||
#{data => #{
|
result => {ok, #{is_superuser => true}}
|
||||||
username => "md5",
|
|
||||||
password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
|
|
||||||
salt => "salt",
|
|
||||||
is_superuser_str => "0"
|
|
||||||
},
|
|
||||||
credentials => #{
|
|
||||||
username => <<"md5">>,
|
|
||||||
password => <<"md5">>
|
|
||||||
},
|
|
||||||
config_params => #{
|
|
||||||
password_hash_algorithm => #{name => <<"md5">>,
|
|
||||||
salt_position => <<"suffix">>}
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => false}}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
|
||||||
username => "sha256",
|
|
||||||
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
|
|
||||||
salt => "salt",
|
|
||||||
is_superuser_int => 1
|
|
||||||
},
|
},
|
||||||
credentials => #{
|
|
||||||
clientid => <<"sha256">>,
|
|
||||||
password => <<"sha256">>
|
|
||||||
},
|
|
||||||
config_params => #{
|
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
|
||||||
FROM users where username = ${clientid} LIMIT 1">>,
|
|
||||||
password_hash_algorithm => #{name => <<"sha256">>,
|
|
||||||
salt_position => <<"prefix">>}
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => true}}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => "md5",
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
|
||||||
is_superuser_int => 0
|
salt => "salt",
|
||||||
},
|
is_superuser_str => "0"
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt">>,
|
credentials => #{
|
||||||
password => <<"bcrypt">>
|
username => <<"md5">>,
|
||||||
},
|
password => <<"md5">>
|
||||||
config_params => #{
|
},
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
config_params => #{
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
password_hash_algorithm => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
name => <<"md5">>,
|
||||||
},
|
salt_position => <<"suffix">>
|
||||||
result => {ok,#{is_superuser => false}}
|
}
|
||||||
},
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt0">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => "sha256",
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
|
||||||
is_superuser_str => "0"
|
salt => "salt",
|
||||||
},
|
is_superuser_int => 1
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt0">>,
|
credentials => #{
|
||||||
password => <<"bcrypt">>
|
clientid => <<"sha256">>,
|
||||||
},
|
password => <<"sha256">>
|
||||||
config_params => #{
|
},
|
||||||
% clientid variable & username credentials
|
config_params => #{
|
||||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
query =>
|
||||||
FROM users where username = ${clientid} LIMIT 1">>,
|
<<
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||||
},
|
" FROM users where username = ${clientid} LIMIT 1"
|
||||||
result => {error,not_authorized}
|
>>,
|
||||||
},
|
password_hash_algorithm => #{
|
||||||
|
name => <<"sha256">>,
|
||||||
|
salt_position => <<"prefix">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => true}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt1">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => <<"bcrypt">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
is_superuser_str => "0"
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||||
},
|
is_superuser_int => 0
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt1">>,
|
credentials => #{
|
||||||
password => <<"bcrypt">>
|
username => <<"bcrypt">>,
|
||||||
},
|
password => <<"bcrypt">>
|
||||||
config_params => #{
|
},
|
||||||
% Bad keys in query
|
config_params => #{
|
||||||
query => <<"SELECT 1 AS unknown_field
|
query =>
|
||||||
FROM users where username = ${username} LIMIT 1">>,
|
<<
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||||
},
|
" FROM users where username = ${username} LIMIT 1"
|
||||||
result => {error,not_authorized}
|
>>,
|
||||||
},
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
username => <<"bcrypt2">>,
|
data => #{
|
||||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
username => <<"bcrypt0">>,
|
||||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
is_superuser => "0"
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||||
},
|
is_superuser_str => "0"
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt2">>,
|
credentials => #{
|
||||||
% Wrong password
|
username => <<"bcrypt0">>,
|
||||||
password => <<"wrongpass">>
|
password => <<"bcrypt">>
|
||||||
},
|
},
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
% clientid variable & username credentials
|
||||||
},
|
query =>
|
||||||
result => {error,bad_username_or_password}
|
<<
|
||||||
}
|
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||||
|
" FROM users where username = ${clientid} LIMIT 1"
|
||||||
|
>>,
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
username => <<"bcrypt1">>,
|
||||||
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||||
|
is_superuser_str => "0"
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt1">>,
|
||||||
|
password => <<"bcrypt">>
|
||||||
|
},
|
||||||
|
config_params => #{
|
||||||
|
% Bad keys in query
|
||||||
|
query =>
|
||||||
|
<<
|
||||||
|
"SELECT 1 AS unknown_field\n"
|
||||||
|
" FROM users where username = ${username} LIMIT 1"
|
||||||
|
>>,
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
username => <<"bcrypt2">>,
|
||||||
|
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||||
|
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||||
|
is_superuser => "0"
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt2">>,
|
||||||
|
% Wrong password
|
||||||
|
password => <<"wrongpass">>
|
||||||
|
},
|
||||||
|
config_params => #{
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, bad_username_or_password}
|
||||||
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
init_seeds() ->
|
init_seeds() ->
|
||||||
ok = drop_seeds(),
|
ok = drop_seeds(),
|
||||||
{ok, _, _} = q("CREATE TABLE users(
|
{ok, _, _} = q(
|
||||||
username varchar(255),
|
"CREATE TABLE users(\n"
|
||||||
password_hash varchar(255),
|
" username varchar(255),\n"
|
||||||
salt varchar(255),
|
" password_hash varchar(255),\n"
|
||||||
is_superuser_str varchar(255),
|
" salt varchar(255),\n"
|
||||||
is_superuser_int smallint,
|
" is_superuser_str varchar(255),\n"
|
||||||
is_superuser_bool boolean)"),
|
" is_superuser_int smallint,\n"
|
||||||
|
" is_superuser_bool boolean)"
|
||||||
|
),
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(#{data := Values}) ->
|
fun(#{data := Values}) ->
|
||||||
ok = create_user(Values)
|
ok = create_user(Values)
|
||||||
end,
|
end,
|
||||||
user_seeds()).
|
user_seeds()
|
||||||
|
).
|
||||||
|
|
||||||
create_user(Values) ->
|
create_user(Values) ->
|
||||||
Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int, is_superuser_bool],
|
Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int, is_superuser_bool],
|
||||||
|
|
||||||
InsertQuery = "INSERT INTO users(username, password_hash, salt,"
|
InsertQuery =
|
||||||
"is_superuser_str, is_superuser_int, is_superuser_bool) "
|
"INSERT INTO users(username, password_hash, salt,"
|
||||||
"VALUES($1, $2, $3, $4, $5, $6)",
|
"is_superuser_str, is_superuser_int, is_superuser_bool) "
|
||||||
|
"VALUES($1, $2, $3, $4, $5, $6)",
|
||||||
|
|
||||||
Params = [maps:get(F, Values, null) || F <- Fields],
|
Params = [maps:get(F, Values, null) || F <- Fields],
|
||||||
{ok, 1} = q(InsertQuery, Params),
|
{ok, 1} = q(InsertQuery, Params),
|
||||||
|
|
@ -430,30 +497,33 @@ create_user(Values) ->
|
||||||
|
|
||||||
q(Sql) ->
|
q(Sql) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?PGSQL_RESOURCE,
|
?PGSQL_RESOURCE,
|
||||||
{query, Sql}).
|
{query, Sql}
|
||||||
|
).
|
||||||
|
|
||||||
q(Sql, Params) ->
|
q(Sql, Params) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?PGSQL_RESOURCE,
|
?PGSQL_RESOURCE,
|
||||||
{query, Sql, Params}).
|
{query, Sql, Params}
|
||||||
|
).
|
||||||
|
|
||||||
drop_seeds() ->
|
drop_seeds() ->
|
||||||
{ok, _, _} = q("DROP TABLE IF EXISTS users"),
|
{ok, _, _} = q("DROP TABLE IF EXISTS users"),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
pgsql_server() ->
|
pgsql_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
|
||||||
|
|
||||||
pgsql_config() ->
|
pgsql_config() ->
|
||||||
#{auto_reconnect => true,
|
#{
|
||||||
database => <<"mqtt">>,
|
auto_reconnect => true,
|
||||||
username => <<"root">>,
|
database => <<"mqtt">>,
|
||||||
password => <<"public">>,
|
username => <<"root">>,
|
||||||
pool_size => 8,
|
password => <<"public">>,
|
||||||
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
|
pool_size => 8,
|
||||||
ssl => #{enable => false}
|
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
|
||||||
}.
|
ssl => #{enable => false}
|
||||||
|
}.
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
|
@ -56,8 +57,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||||
|
|
||||||
|
|
@ -70,38 +72,53 @@ t_create(_Config) ->
|
||||||
%% -starttls postgres -connect authn-server:5432 \
|
%% -starttls postgres -connect authn-server:5432 \
|
||||||
%% -cert client.crt -key client.key -CAfile ca.crt
|
%% -cert client.crt -key client.key -CAfile ca.crt
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_pgsql_auth_with_ssl_opts(
|
create_pgsql_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]})).
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
|
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid(_Config) ->
|
t_create_invalid(_Config) ->
|
||||||
|
|
||||||
%% invalid server_name
|
%% invalid server_name
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_pgsql_auth_with_ssl_opts(
|
create_pgsql_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>})),
|
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||||
|
<<"verify">> => <<"verify_peer">>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||||
%% incompatible versions
|
%% incompatible versions
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_pgsql_auth_with_ssl_opts(
|
create_pgsql_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.1">>]})),
|
<<"verify">> => <<"verify_peer">>,
|
||||||
|
<<"versions">> => [<<"tlsv1.1">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||||
%% incompatible ciphers
|
%% incompatible ciphers
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_pgsql_auth_with_ssl_opts(
|
create_pgsql_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.2">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]})).
|
<<"versions">> => [<<"tlsv1.2">>],
|
||||||
|
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -113,26 +130,29 @@ create_pgsql_auth_with_ssl_opts(SpecificSSLOpts) ->
|
||||||
|
|
||||||
raw_pgsql_auth_config(SpecificSSLOpts) ->
|
raw_pgsql_auth_config(SpecificSSLOpts) ->
|
||||||
SSLOpts = maps:merge(
|
SSLOpts = maps:merge(
|
||||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||||
#{enable => <<"true">>}),
|
#{enable => <<"true">>}
|
||||||
|
),
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
password_hash_algorithm => #{name => <<"plain">>,
|
password_hash_algorithm => #{
|
||||||
salt_position => <<"suffix">>},
|
name => <<"plain">>,
|
||||||
enable => <<"true">>,
|
salt_position => <<"suffix">>
|
||||||
|
},
|
||||||
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"postgresql">>,
|
backend => <<"postgresql">>,
|
||||||
database => <<"mqtt">>,
|
database => <<"mqtt">>,
|
||||||
username => <<"root">>,
|
username => <<"root">>,
|
||||||
password => <<"public">>,
|
password => <<"public">>,
|
||||||
|
|
||||||
query => <<"SELECT 1">>,
|
query => <<"SELECT 1">>,
|
||||||
server => pgsql_server(),
|
server => pgsql_server(),
|
||||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
pgsql_server() ->
|
pgsql_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,9 @@ init_per_testcase(_, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
init_per_group(require_seeds, Config) ->
|
init_per_group(require_seeds, Config) ->
|
||||||
|
|
@ -59,11 +60,12 @@ init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
||||||
ok = start_apps([emqx_resource, emqx_connector]),
|
ok = start_apps([emqx_resource, emqx_connector]),
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?REDIS_RESOURCE,
|
?REDIS_RESOURCE,
|
||||||
?RESOURCE_GROUP,
|
?RESOURCE_GROUP,
|
||||||
emqx_connector_redis,
|
emqx_connector_redis,
|
||||||
redis_config(),
|
redis_config(),
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
Config;
|
Config;
|
||||||
false ->
|
false ->
|
||||||
{skip, no_redis}
|
{skip, no_redis}
|
||||||
|
|
@ -71,8 +73,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
ok = emqx_resource:remove_local(?REDIS_RESOURCE),
|
ok = emqx_resource:remove_local(?REDIS_RESOURCE),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||||
|
|
@ -86,8 +89,9 @@ t_create(_Config) ->
|
||||||
|
|
||||||
AuthConfig = raw_redis_auth_config(),
|
AuthConfig = raw_redis_auth_config(),
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_redis}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
{ok, [#{provider := emqx_authn_redis}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
||||||
|
|
||||||
|
|
@ -95,126 +99,152 @@ t_create_invalid(_Config) ->
|
||||||
AuthConfig = raw_redis_auth_config(),
|
AuthConfig = raw_redis_auth_config(),
|
||||||
InvalidConfigs =
|
InvalidConfigs =
|
||||||
[
|
[
|
||||||
AuthConfig#{
|
AuthConfig#{
|
||||||
cmd => <<"MGET password_hash:${username} salt:${username}">>},
|
cmd => <<"MGET password_hash:${username} salt:${username}">>
|
||||||
AuthConfig#{
|
},
|
||||||
cmd => <<"HMGET mqtt_user:${username} password_hash invalid_field">>},
|
AuthConfig#{
|
||||||
AuthConfig#{
|
cmd => <<"HMGET mqtt_user:${username} password_hash invalid_field">>
|
||||||
cmd => <<"HMGET mqtt_user:${username} salt is_superuser">>}
|
},
|
||||||
|
AuthConfig#{
|
||||||
|
cmd => <<"HMGET mqtt_user:${username} salt is_superuser">>
|
||||||
|
}
|
||||||
],
|
],
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Config) ->
|
fun(Config) ->
|
||||||
{error, _} = emqx:update_config(
|
{error, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||||
end,
|
end,
|
||||||
InvalidConfigs),
|
InvalidConfigs
|
||||||
|
),
|
||||||
|
|
||||||
InvalidConfigs1 =
|
InvalidConfigs1 =
|
||||||
[
|
[
|
||||||
maps:without([server], AuthConfig),
|
maps:without([server], AuthConfig),
|
||||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||||
AuthConfig#{password => <<"wrongpass">>},
|
AuthConfig#{password => <<"wrongpass">>},
|
||||||
AuthConfig#{database => <<"5678">>}
|
AuthConfig#{database => <<"5678">>}
|
||||||
],
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Config) ->
|
fun(Config) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
),
|
||||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||||
end,
|
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||||
InvalidConfigs1).
|
end,
|
||||||
|
InvalidConfigs1
|
||||||
|
).
|
||||||
|
|
||||||
t_authenticate(_Config) ->
|
t_authenticate(_Config) ->
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(Sample) ->
|
fun(Sample) ->
|
||||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||||
test_user_auth(Sample)
|
test_user_auth(Sample)
|
||||||
end,
|
end,
|
||||||
user_seeds()).
|
user_seeds()
|
||||||
|
).
|
||||||
|
|
||||||
test_user_auth(#{credentials := Credentials0,
|
test_user_auth(#{
|
||||||
config_params := SpecificConfigParams,
|
credentials := Credentials0,
|
||||||
result := Result}) ->
|
config_params := SpecificConfigParams,
|
||||||
|
result := Result
|
||||||
|
}) ->
|
||||||
AuthConfig = maps:merge(raw_redis_auth_config(), SpecificConfigParams),
|
AuthConfig = maps:merge(raw_redis_auth_config(), SpecificConfigParams),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
Credentials = Credentials0#{
|
Credentials = Credentials0#{
|
||||||
listener => 'tcp:default',
|
listener => 'tcp:default',
|
||||||
protocol => mqtt
|
protocol => mqtt
|
||||||
},
|
},
|
||||||
|
|
||||||
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL).
|
?GLOBAL
|
||||||
|
).
|
||||||
|
|
||||||
t_destroy(_Config) ->
|
t_destroy(_Config) ->
|
||||||
AuthConfig = raw_redis_auth_config(),
|
AuthConfig = raw_redis_auth_config(),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_authn_redis, state := State}]}
|
{ok, [#{provider := emqx_authn_redis, state := State}]} =
|
||||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
emqx_authentication:list_authenticators(?GLOBAL),
|
||||||
|
|
||||||
{ok, _} = emqx_authn_redis:authenticate(
|
{ok, _} = emqx_authn_redis:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>
|
username => <<"plain">>,
|
||||||
},
|
password => <<"plain">>
|
||||||
State),
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
|
|
||||||
% Authenticator should not be usable anymore
|
% Authenticator should not be usable anymore
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
ignore,
|
ignore,
|
||||||
emqx_authn_redis:authenticate(
|
emqx_authn_redis:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>
|
username => <<"plain">>,
|
||||||
},
|
password => <<"plain">>
|
||||||
State)).
|
},
|
||||||
|
State
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
t_update(_Config) ->
|
t_update(_Config) ->
|
||||||
CorrectConfig = raw_redis_auth_config(),
|
CorrectConfig = raw_redis_auth_config(),
|
||||||
IncorrectConfig =
|
IncorrectConfig =
|
||||||
CorrectConfig#{
|
CorrectConfig#{
|
||||||
cmd => <<"HMGET invalid_key:${username} password_hash salt is_superuser">>},
|
cmd => <<"HMGET invalid_key:${username} password_hash salt is_superuser">>
|
||||||
|
},
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{error, not_authorized} = emqx_access_control:authenticate(
|
{error, not_authorized} = emqx_access_control:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}),
|
protocol => mqtt
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
% We update with config with correct query, provider should update and work properly
|
% We update with config with correct query, provider should update and work properly
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{update_authenticator, ?GLOBAL, <<"password_based:redis">>, CorrectConfig}),
|
{update_authenticator, ?GLOBAL, <<"password_based:redis">>, CorrectConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok,_} = emqx_access_control:authenticate(
|
{ok, _} = emqx_access_control:authenticate(
|
||||||
#{username => <<"plain">>,
|
#{
|
||||||
password => <<"plain">>,
|
username => <<"plain">>,
|
||||||
listener => 'tcp:default',
|
password => <<"plain">>,
|
||||||
protocol => mqtt
|
listener => 'tcp:default',
|
||||||
}).
|
protocol => mqtt
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -222,194 +252,218 @@ t_update(_Config) ->
|
||||||
|
|
||||||
raw_redis_auth_config() ->
|
raw_redis_auth_config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
password_hash_algorithm => #{name => <<"plain">>,
|
password_hash_algorithm => #{
|
||||||
salt_position => <<"suffix">>},
|
name => <<"plain">>,
|
||||||
enable => <<"true">>,
|
salt_position => <<"suffix">>
|
||||||
|
},
|
||||||
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"redis">>,
|
backend => <<"redis">>,
|
||||||
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
||||||
database => <<"1">>,
|
database => <<"1">>,
|
||||||
password => <<"public">>,
|
password => <<"public">>,
|
||||||
server => redis_server()
|
server => redis_server()
|
||||||
}.
|
}.
|
||||||
|
|
||||||
user_seeds() ->
|
user_seeds() ->
|
||||||
[#{data => #{
|
[
|
||||||
password_hash => <<"plainsalt">>,
|
#{
|
||||||
salt => <<"salt">>,
|
data => #{
|
||||||
is_superuser => <<"1">>
|
password_hash => <<"plainsalt">>,
|
||||||
},
|
salt => <<"salt">>,
|
||||||
credentials => #{
|
is_superuser => <<"1">>
|
||||||
username => <<"plain">>,
|
},
|
||||||
password => <<"plain">>},
|
credentials => #{
|
||||||
key => <<"mqtt_user:plain">>,
|
username => <<"plain">>,
|
||||||
config_params => #{},
|
password => <<"plain">>
|
||||||
result => {ok,#{is_superuser => true}}
|
},
|
||||||
},
|
key => <<"mqtt_user:plain">>,
|
||||||
|
config_params => #{},
|
||||||
#{data => #{
|
result => {ok, #{is_superuser => true}}
|
||||||
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
|
||||||
salt => <<"salt">>,
|
|
||||||
is_superuser => <<"0">>
|
|
||||||
},
|
|
||||||
credentials => #{
|
|
||||||
username => <<"md5">>,
|
|
||||||
password => <<"md5">>
|
|
||||||
},
|
|
||||||
key => <<"mqtt_user:md5">>,
|
|
||||||
config_params => #{
|
|
||||||
password_hash_algorithm => #{name => <<"md5">>,
|
|
||||||
salt_position => <<"suffix">>}
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => false}}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
|
||||||
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
|
||||||
salt => <<"salt">>,
|
|
||||||
is_superuser => <<"1">>
|
|
||||||
},
|
},
|
||||||
credentials => #{
|
|
||||||
clientid => <<"sha256">>,
|
|
||||||
password => <<"sha256">>
|
|
||||||
},
|
|
||||||
key => <<"mqtt_user:sha256">>,
|
|
||||||
config_params => #{
|
|
||||||
cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
|
|
||||||
password_hash_algorithm => #{name => <<"sha256">>,
|
|
||||||
salt_position => <<"prefix">>}
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => true}}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
password_hash =>
|
data => #{
|
||||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
salt => <<"salt">>,
|
||||||
is_superuser => <<"0">>
|
is_superuser => <<"0">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"bcrypt">>,
|
username => <<"md5">>,
|
||||||
password => <<"bcrypt">>
|
password => <<"md5">>
|
||||||
},
|
},
|
||||||
key => <<"mqtt_user:bcrypt">>,
|
key => <<"mqtt_user:md5">>,
|
||||||
config_params => #{
|
config_params => #{
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
password_hash_algorithm => #{
|
||||||
},
|
name => <<"md5">>,
|
||||||
result => {ok,#{is_superuser => false}}
|
salt_position => <<"suffix">>
|
||||||
},
|
}
|
||||||
#{data => #{
|
},
|
||||||
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
|
result => {ok, #{is_superuser => false}}
|
||||||
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
},
|
||||||
is_superuser => <<"0">>
|
|
||||||
},
|
|
||||||
credentials => #{
|
|
||||||
username => <<"pbkdf2">>,
|
|
||||||
password => <<"password">>
|
|
||||||
},
|
|
||||||
key => <<"mqtt_user:pbkdf2">>,
|
|
||||||
config_params => #{
|
|
||||||
password_hash_algorithm => #{name => <<"pbkdf2">>,
|
|
||||||
iterations => 2,
|
|
||||||
mac_fun => sha
|
|
||||||
}
|
|
||||||
},
|
|
||||||
result => {ok,#{is_superuser => false}}
|
|
||||||
},
|
|
||||||
#{data => #{
|
|
||||||
password_hash =>
|
|
||||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
|
||||||
is_superuser => <<"0">>
|
|
||||||
},
|
|
||||||
credentials => #{
|
|
||||||
username => <<"bcrypt0">>,
|
|
||||||
password => <<"bcrypt">>
|
|
||||||
},
|
|
||||||
key => <<"mqtt_user:bcrypt0">>,
|
|
||||||
config_params => #{
|
|
||||||
% clientid variable & username credentials
|
|
||||||
cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>,
|
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
|
||||||
},
|
|
||||||
result => {error,not_authorized}
|
|
||||||
},
|
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
password_hash =>
|
data => #{
|
||||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
password_hash =>
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
<<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
||||||
is_superuser => <<"0">>
|
salt => <<"salt">>,
|
||||||
},
|
is_superuser => <<"1">>
|
||||||
credentials => #{
|
},
|
||||||
username => <<"bcrypt1">>,
|
credentials => #{
|
||||||
password => <<"bcrypt">>
|
clientid => <<"sha256">>,
|
||||||
},
|
password => <<"sha256">>
|
||||||
key => <<"mqtt_user:bcrypt1">>,
|
},
|
||||||
config_params => #{
|
key => <<"mqtt_user:sha256">>,
|
||||||
% Bad key in cmd
|
config_params => #{
|
||||||
cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>,
|
cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
password_hash_algorithm => #{
|
||||||
},
|
name => <<"sha256">>,
|
||||||
result => {error,not_authorized}
|
salt_position => <<"prefix">>
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => true}}
|
||||||
|
},
|
||||||
|
|
||||||
#{data => #{
|
#{
|
||||||
password_hash =>
|
data => #{
|
||||||
|
password_hash =>
|
||||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
is_superuser => <<"0">>
|
is_superuser => <<"0">>
|
||||||
},
|
},
|
||||||
credentials => #{
|
credentials => #{
|
||||||
username => <<"bcrypt2">>,
|
username => <<"bcrypt">>,
|
||||||
% Wrong password
|
password => <<"bcrypt">>
|
||||||
password => <<"wrongpass">>
|
},
|
||||||
},
|
key => <<"mqtt_user:bcrypt">>,
|
||||||
key => <<"mqtt_user:bcrypt2">>,
|
config_params => #{
|
||||||
config_params => #{
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
},
|
||||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
result => {ok, #{is_superuser => false}}
|
||||||
},
|
},
|
||||||
result => {error,bad_username_or_password}
|
#{
|
||||||
}
|
data => #{
|
||||||
|
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
|
||||||
|
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||||
|
is_superuser => <<"0">>
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"pbkdf2">>,
|
||||||
|
password => <<"password">>
|
||||||
|
},
|
||||||
|
key => <<"mqtt_user:pbkdf2">>,
|
||||||
|
config_params => #{
|
||||||
|
password_hash_algorithm => #{
|
||||||
|
name => <<"pbkdf2">>,
|
||||||
|
iterations => 2,
|
||||||
|
mac_fun => sha
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result => {ok, #{is_superuser => false}}
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
password_hash =>
|
||||||
|
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
is_superuser => <<"0">>
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt0">>,
|
||||||
|
password => <<"bcrypt">>
|
||||||
|
},
|
||||||
|
key => <<"mqtt_user:bcrypt0">>,
|
||||||
|
config_params => #{
|
||||||
|
% clientid variable & username credentials
|
||||||
|
cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>,
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
password_hash =>
|
||||||
|
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
is_superuser => <<"0">>
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt1">>,
|
||||||
|
password => <<"bcrypt">>
|
||||||
|
},
|
||||||
|
key => <<"mqtt_user:bcrypt1">>,
|
||||||
|
config_params => #{
|
||||||
|
% Bad key in cmd
|
||||||
|
cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>,
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, not_authorized}
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
data => #{
|
||||||
|
password_hash =>
|
||||||
|
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||||
|
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||||
|
is_superuser => <<"0">>
|
||||||
|
},
|
||||||
|
credentials => #{
|
||||||
|
username => <<"bcrypt2">>,
|
||||||
|
% Wrong password
|
||||||
|
password => <<"wrongpass">>
|
||||||
|
},
|
||||||
|
key => <<"mqtt_user:bcrypt2">>,
|
||||||
|
config_params => #{
|
||||||
|
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
||||||
|
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||||
|
},
|
||||||
|
result => {error, bad_username_or_password}
|
||||||
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
init_seeds() ->
|
init_seeds() ->
|
||||||
ok = drop_seeds(),
|
ok = drop_seeds(),
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(#{key := UserKey, data := Values}) ->
|
fun(#{key := UserKey, data := Values}) ->
|
||||||
lists:foreach(fun({Key, Value}) ->
|
lists:foreach(
|
||||||
q(["HSET", UserKey, atom_to_list(Key), Value])
|
fun({Key, Value}) ->
|
||||||
end,
|
q(["HSET", UserKey, atom_to_list(Key), Value])
|
||||||
maps:to_list(Values))
|
end,
|
||||||
end,
|
maps:to_list(Values)
|
||||||
user_seeds()).
|
)
|
||||||
|
end,
|
||||||
|
user_seeds()
|
||||||
|
).
|
||||||
|
|
||||||
q(Command) ->
|
q(Command) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?REDIS_RESOURCE,
|
?REDIS_RESOURCE,
|
||||||
{cmd, Command}).
|
{cmd, Command}
|
||||||
|
).
|
||||||
|
|
||||||
drop_seeds() ->
|
drop_seeds() ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(#{key := UserKey}) ->
|
fun(#{key := UserKey}) ->
|
||||||
q(["DEL", UserKey])
|
q(["DEL", UserKey])
|
||||||
end,
|
end,
|
||||||
user_seeds()).
|
user_seeds()
|
||||||
|
).
|
||||||
|
|
||||||
redis_server() ->
|
redis_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?REDIS_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?REDIS_HOST])).
|
||||||
|
|
||||||
redis_config() ->
|
redis_config() ->
|
||||||
#{auto_reconnect => true,
|
#{
|
||||||
database => 1,
|
auto_reconnect => true,
|
||||||
pool_size => 8,
|
database => 1,
|
||||||
redis_type => single,
|
pool_size => 8,
|
||||||
password => "public",
|
redis_type => single,
|
||||||
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
|
password => "public",
|
||||||
ssl => #{enable => false}
|
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
|
||||||
}.
|
ssl => #{enable => false}
|
||||||
|
}.
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
|
@ -56,8 +57,9 @@ init_per_suite(Config) ->
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||||
|
|
||||||
|
|
@ -67,39 +69,55 @@ end_per_suite(_Config) ->
|
||||||
|
|
||||||
t_create(_Config) ->
|
t_create(_Config) ->
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_redis_auth_with_ssl_opts(
|
create_redis_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.3">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]})).
|
<<"versions">> => [<<"tlsv1.3">>],
|
||||||
|
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid(_Config) ->
|
t_create_invalid(_Config) ->
|
||||||
%% invalid server_name
|
%% invalid server_name
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
create_redis_auth_with_ssl_opts(
|
create_redis_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||||
<<"versions">> => [<<"tlsv1.3">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]})),
|
<<"versions">> => [<<"tlsv1.3">>],
|
||||||
|
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
%% incompatible versions
|
%% incompatible versions
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, _},
|
{error, _},
|
||||||
create_redis_auth_with_ssl_opts(
|
create_redis_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.1">>, <<"tlsv1.2">>]})),
|
<<"verify">> => <<"verify_peer">>,
|
||||||
|
<<"versions">> => [<<"tlsv1.1">>, <<"tlsv1.2">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
%% incompatible ciphers
|
%% incompatible ciphers
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, _},
|
{error, _},
|
||||||
create_redis_auth_with_ssl_opts(
|
create_redis_auth_with_ssl_opts(
|
||||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
#{
|
||||||
<<"verify">> => <<"verify_peer">>,
|
<<"server_name_indication">> => <<"authn-server">>,
|
||||||
<<"versions">> => [<<"tlsv1.3">>],
|
<<"verify">> => <<"verify_peer">>,
|
||||||
<<"ciphers">> => [<<"TLS_AES_128_GCM_SHA256">>]})).
|
<<"versions">> => [<<"tlsv1.3">>],
|
||||||
|
<<"ciphers">> => [<<"TLS_AES_128_GCM_SHA256">>]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -111,24 +129,27 @@ create_redis_auth_with_ssl_opts(SpecificSSLOpts) ->
|
||||||
|
|
||||||
raw_redis_auth_config(SpecificSSLOpts) ->
|
raw_redis_auth_config(SpecificSSLOpts) ->
|
||||||
SSLOpts = maps:merge(
|
SSLOpts = maps:merge(
|
||||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||||
#{enable => <<"true">>}),
|
#{enable => <<"true">>}
|
||||||
|
),
|
||||||
#{
|
#{
|
||||||
mechanism => <<"password_based">>,
|
mechanism => <<"password_based">>,
|
||||||
password_hash_algorithm => #{name => <<"plain">>,
|
password_hash_algorithm => #{
|
||||||
salt_position => <<"suffix">>},
|
name => <<"plain">>,
|
||||||
enable => <<"true">>,
|
salt_position => <<"suffix">>
|
||||||
|
},
|
||||||
|
enable => <<"true">>,
|
||||||
|
|
||||||
backend => <<"redis">>,
|
backend => <<"redis">>,
|
||||||
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
||||||
database => <<"1">>,
|
database => <<"1">>,
|
||||||
password => <<"public">>,
|
password => <<"public">>,
|
||||||
server => redis_server(),
|
server => redis_server(),
|
||||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
redis_server() ->
|
redis_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s:~b",[?REDIS_HOST, ?REDIS_TLS_PORT])).
|
iolist_to_binary(io_lib:format("~s:~b", [?REDIS_HOST, ?REDIS_TLS_PORT])).
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -36,16 +36,19 @@ jwt_example() ->
|
||||||
|
|
||||||
delete_authenticators(Path, Chain) ->
|
delete_authenticators(Path, Chain) ->
|
||||||
case emqx_authentication:list_authenticators(Chain) of
|
case emqx_authentication:list_authenticators(Chain) of
|
||||||
{error, _} -> ok;
|
{error, _} ->
|
||||||
|
ok;
|
||||||
{ok, Authenticators} ->
|
{ok, Authenticators} ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(#{id := ID}) ->
|
fun(#{id := ID}) ->
|
||||||
emqx:update_config(
|
emqx:update_config(
|
||||||
Path,
|
Path,
|
||||||
{delete_authenticator, Chain, ID},
|
{delete_authenticator, Chain, ID},
|
||||||
#{rawconf_with_defaults => true})
|
#{rawconf_with_defaults => true}
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
Authenticators)
|
Authenticators
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete_config(ID) ->
|
delete_config(ID) ->
|
||||||
|
|
@ -53,10 +56,13 @@ delete_config(ID) ->
|
||||||
emqx:update_config(
|
emqx:update_config(
|
||||||
[authentication],
|
[authentication],
|
||||||
{delete_authenticator, ?GLOBAL, ID},
|
{delete_authenticator, ?GLOBAL, ID},
|
||||||
#{rawconf_with_defaults => false}).
|
#{rawconf_with_defaults => false}
|
||||||
|
).
|
||||||
|
|
||||||
client_ssl_cert_opts() ->
|
client_ssl_cert_opts() ->
|
||||||
Dir = code:lib_dir(emqx_authn, test),
|
Dir = code:lib_dir(emqx_authn, test),
|
||||||
#{keyfile => filename:join([Dir, "data/certs", "client.key"]),
|
#{
|
||||||
certfile => filename:join([Dir, "data/certs", "client.crt"]),
|
keyfile => filename:join([Dir, "data/certs", "client.key"]),
|
||||||
cacertfile => filename:join([Dir, "data/certs", "ca.crt"])}.
|
certfile => filename:join([Dir, "data/certs", "client.crt"]),
|
||||||
|
cacertfile => filename:join([Dir, "data/certs", "ca.crt"])
|
||||||
|
}.
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,16 @@
|
||||||
|
|
||||||
-define(PATH, [authentication]).
|
-define(PATH, [authentication]).
|
||||||
|
|
||||||
-define(USER_MAP, #{user_id := _,
|
-define(USER_MAP, #{
|
||||||
is_superuser := _}).
|
user_id := _,
|
||||||
|
is_superuser := _
|
||||||
|
}).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
_ = application:load(emqx_conf),
|
_ = application:load(emqx_conf),
|
||||||
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
|
@ -44,8 +46,9 @@ init_per_testcase(_Case, Config) ->
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
mria:clear_table(emqx_enhanced_authn_scram_mnesia),
|
mria:clear_table(emqx_enhanced_authn_scram_mnesia),
|
||||||
emqx_authn_test_lib:delete_authenticators(
|
emqx_authn_test_lib:delete_authenticators(
|
||||||
[authentication],
|
[authentication],
|
||||||
?GLOBAL),
|
?GLOBAL
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_testcase(_Case, Config) ->
|
end_per_testcase(_Case, Config) ->
|
||||||
|
|
@ -64,23 +67,25 @@ t_create(_Config) ->
|
||||||
},
|
},
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, ValidConfig}),
|
{create_authenticator, ?GLOBAL, ValidConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{provider := emqx_enhanced_authn_scram_mnesia}]}
|
{ok, [#{provider := emqx_enhanced_authn_scram_mnesia}]} =
|
||||||
= emqx_authentication:list_authenticators(?GLOBAL).
|
emqx_authentication:list_authenticators(?GLOBAL).
|
||||||
|
|
||||||
t_create_invalid(_Config) ->
|
t_create_invalid(_Config) ->
|
||||||
InvalidConfig = #{
|
InvalidConfig = #{
|
||||||
<<"mechanism">> => <<"scram">>,
|
<<"mechanism">> => <<"scram">>,
|
||||||
<<"backend">> => <<"built_in_database">>,
|
<<"backend">> => <<"built_in_database">>,
|
||||||
<<"algorithm">> => <<"sha271828">>,
|
<<"algorithm">> => <<"sha271828">>,
|
||||||
<<"iteration_count">> => <<"4096">>
|
<<"iteration_count">> => <<"4096">>
|
||||||
},
|
},
|
||||||
|
|
||||||
{error, _} = emqx:update_config(
|
{error, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, InvalidConfig}),
|
{create_authenticator, ?GLOBAL, InvalidConfig}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL).
|
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL).
|
||||||
|
|
||||||
|
|
@ -96,39 +101,47 @@ t_authenticate(_Config) ->
|
||||||
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
||||||
|
|
||||||
ConnectPacket = ?CONNECT_PACKET(
|
ConnectPacket = ?CONNECT_PACKET(
|
||||||
#mqtt_packet_connect{
|
#mqtt_packet_connect{
|
||||||
proto_ver = ?MQTT_PROTO_V5,
|
proto_ver = ?MQTT_PROTO_V5,
|
||||||
properties = #{
|
properties = #{
|
||||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||||
'Authentication-Data' => ClientFirstMessage
|
'Authentication-Data' => ClientFirstMessage
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
||||||
|
|
||||||
?AUTH_PACKET(
|
?AUTH_PACKET(
|
||||||
?RC_CONTINUE_AUTHENTICATION,
|
?RC_CONTINUE_AUTHENTICATION,
|
||||||
#{'Authentication-Data' := ServerFirstMessage}) = receive_packet(),
|
#{'Authentication-Data' := ServerFirstMessage}
|
||||||
|
) = receive_packet(),
|
||||||
|
|
||||||
{continue, ClientFinalMessage, ClientCache} =
|
{continue, ClientFinalMessage, ClientCache} =
|
||||||
esasl_scram:check_server_first_message(
|
esasl_scram:check_server_first_message(
|
||||||
ServerFirstMessage,
|
ServerFirstMessage,
|
||||||
#{client_first_message => ClientFirstMessage,
|
#{
|
||||||
password => Password,
|
client_first_message => ClientFirstMessage,
|
||||||
algorithm => Algorithm}
|
password => Password,
|
||||||
|
algorithm => Algorithm
|
||||||
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
AuthContinuePacket = ?AUTH_PACKET(
|
AuthContinuePacket = ?AUTH_PACKET(
|
||||||
?RC_CONTINUE_AUTHENTICATION,
|
?RC_CONTINUE_AUTHENTICATION,
|
||||||
#{'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
#{
|
||||||
'Authentication-Data' => ClientFinalMessage}),
|
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||||
|
'Authentication-Data' => ClientFinalMessage
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket),
|
ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket),
|
||||||
|
|
||||||
?CONNACK_PACKET(
|
?CONNACK_PACKET(
|
||||||
?RC_SUCCESS,
|
?RC_SUCCESS,
|
||||||
_,
|
_,
|
||||||
#{'Authentication-Data' := ServerFinalMessage}) = receive_packet(),
|
#{'Authentication-Data' := ServerFinalMessage}
|
||||||
|
) = receive_packet(),
|
||||||
|
|
||||||
ok = esasl_scram:check_server_final_message(
|
ok = esasl_scram:check_server_final_message(
|
||||||
ServerFinalMessage, ClientCache#{algorithm => Algorithm}
|
ServerFinalMessage, ClientCache#{algorithm => Algorithm}
|
||||||
|
|
@ -146,13 +159,14 @@ t_authenticate_bad_username(_Config) ->
|
||||||
ClientFirstMessage = esasl_scram:client_first_message(<<"badusername">>),
|
ClientFirstMessage = esasl_scram:client_first_message(<<"badusername">>),
|
||||||
|
|
||||||
ConnectPacket = ?CONNECT_PACKET(
|
ConnectPacket = ?CONNECT_PACKET(
|
||||||
#mqtt_packet_connect{
|
#mqtt_packet_connect{
|
||||||
proto_ver = ?MQTT_PROTO_V5,
|
proto_ver = ?MQTT_PROTO_V5,
|
||||||
properties = #{
|
properties = #{
|
||||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||||
'Authentication-Data' => ClientFirstMessage
|
'Authentication-Data' => ClientFirstMessage
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
||||||
|
|
||||||
|
|
@ -170,32 +184,39 @@ t_authenticate_bad_password(_Config) ->
|
||||||
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
||||||
|
|
||||||
ConnectPacket = ?CONNECT_PACKET(
|
ConnectPacket = ?CONNECT_PACKET(
|
||||||
#mqtt_packet_connect{
|
#mqtt_packet_connect{
|
||||||
proto_ver = ?MQTT_PROTO_V5,
|
proto_ver = ?MQTT_PROTO_V5,
|
||||||
properties = #{
|
properties = #{
|
||||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||||
'Authentication-Data' => ClientFirstMessage
|
'Authentication-Data' => ClientFirstMessage
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
||||||
|
|
||||||
?AUTH_PACKET(
|
?AUTH_PACKET(
|
||||||
?RC_CONTINUE_AUTHENTICATION,
|
?RC_CONTINUE_AUTHENTICATION,
|
||||||
#{'Authentication-Data' := ServerFirstMessage}) = receive_packet(),
|
#{'Authentication-Data' := ServerFirstMessage}
|
||||||
|
) = receive_packet(),
|
||||||
|
|
||||||
{continue, ClientFinalMessage, _ClientCache} =
|
{continue, ClientFinalMessage, _ClientCache} =
|
||||||
esasl_scram:check_server_first_message(
|
esasl_scram:check_server_first_message(
|
||||||
ServerFirstMessage,
|
ServerFirstMessage,
|
||||||
#{client_first_message => ClientFirstMessage,
|
#{
|
||||||
password => <<"badpassword">>,
|
client_first_message => ClientFirstMessage,
|
||||||
algorithm => Algorithm}
|
password => <<"badpassword">>,
|
||||||
|
algorithm => Algorithm
|
||||||
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
AuthContinuePacket = ?AUTH_PACKET(
|
AuthContinuePacket = ?AUTH_PACKET(
|
||||||
?RC_CONTINUE_AUTHENTICATION,
|
?RC_CONTINUE_AUTHENTICATION,
|
||||||
#{'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
#{
|
||||||
'Authentication-Data' => ClientFinalMessage}),
|
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||||
|
'Authentication-Data' => ClientFinalMessage
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket),
|
ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket),
|
||||||
|
|
||||||
|
|
@ -218,7 +239,7 @@ t_destroy(_) ->
|
||||||
ok = emqx_enhanced_authn_scram_mnesia:destroy(State0),
|
ok = emqx_enhanced_authn_scram_mnesia:destroy(State0),
|
||||||
|
|
||||||
{ok, State1} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config),
|
{ok, State1} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config),
|
||||||
{error,not_found} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State1),
|
{error, not_found} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State1),
|
||||||
{ok, _} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, StateOther).
|
{ok, _} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, StateOther).
|
||||||
|
|
||||||
t_add_user(_) ->
|
t_add_user(_) ->
|
||||||
|
|
@ -248,12 +269,14 @@ t_update_user(_) ->
|
||||||
{ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State),
|
{ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State),
|
||||||
{ok, #{is_superuser := false}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State),
|
{ok, #{is_superuser := false}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State),
|
||||||
|
|
||||||
{ok,
|
{ok, #{
|
||||||
#{user_id := <<"u">>,
|
user_id := <<"u">>,
|
||||||
is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:update_user(
|
is_superuser := true
|
||||||
<<"u">>,
|
}} = emqx_enhanced_authn_scram_mnesia:update_user(
|
||||||
#{password => <<"p1">>, is_superuser => true},
|
<<"u">>,
|
||||||
State),
|
#{password => <<"p1">>, is_superuser => true},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{ok, #{is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State).
|
{ok, #{is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State).
|
||||||
|
|
||||||
|
|
@ -261,29 +284,47 @@ t_list_users(_) ->
|
||||||
Config = config(),
|
Config = config(),
|
||||||
{ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config),
|
{ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config),
|
||||||
|
|
||||||
Users = [#{user_id => <<"u1">>, password => <<"p">>},
|
Users = [
|
||||||
#{user_id => <<"u2">>, password => <<"p">>},
|
#{user_id => <<"u1">>, password => <<"p">>},
|
||||||
#{user_id => <<"u3">>, password => <<"p">>}],
|
#{user_id => <<"u2">>, password => <<"p">>},
|
||||||
|
#{user_id => <<"u3">>, password => <<"p">>}
|
||||||
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(U) -> {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(U, State) end,
|
fun(U) -> {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(U, State) end,
|
||||||
Users),
|
Users
|
||||||
|
),
|
||||||
|
|
||||||
#{data := [?USER_MAP, ?USER_MAP],
|
#{
|
||||||
meta := #{page := 1, limit := 2, count := 3}} = emqx_enhanced_authn_scram_mnesia:list_users(
|
data := [?USER_MAP, ?USER_MAP],
|
||||||
#{<<"page">> => 1, <<"limit">> => 2},
|
meta := #{page := 1, limit := 2, count := 3}
|
||||||
State),
|
} = emqx_enhanced_authn_scram_mnesia:list_users(
|
||||||
#{data := [?USER_MAP],
|
#{<<"page">> => 1, <<"limit">> => 2},
|
||||||
meta := #{page := 2, limit := 2, count := 3}} = emqx_enhanced_authn_scram_mnesia:list_users(
|
State
|
||||||
#{<<"page">> => 2, <<"limit">> => 2},
|
),
|
||||||
State),
|
#{
|
||||||
#{data := [#{user_id := <<"u1">>,
|
data := [?USER_MAP],
|
||||||
is_superuser := _}],
|
meta := #{page := 2, limit := 2, count := 3}
|
||||||
meta := #{page := 1, limit := 3, count := 1}} = emqx_enhanced_authn_scram_mnesia:list_users(
|
} = emqx_enhanced_authn_scram_mnesia:list_users(
|
||||||
#{ <<"page">> => 1
|
#{<<"page">> => 2, <<"limit">> => 2},
|
||||||
, <<"limit">> => 3
|
State
|
||||||
, <<"like_username">> => <<"1">>},
|
),
|
||||||
State).
|
#{
|
||||||
|
data := [
|
||||||
|
#{
|
||||||
|
user_id := <<"u1">>,
|
||||||
|
is_superuser := _
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta := #{page := 1, limit := 3, count := 1}
|
||||||
|
} = emqx_enhanced_authn_scram_mnesia:list_users(
|
||||||
|
#{
|
||||||
|
<<"page">> => 1,
|
||||||
|
<<"limit">> => 3,
|
||||||
|
<<"like_username">> => <<"1">>
|
||||||
|
},
|
||||||
|
State
|
||||||
|
).
|
||||||
|
|
||||||
t_is_superuser(_Config) ->
|
t_is_superuser(_Config) ->
|
||||||
ok = test_is_superuser(#{is_superuser => false}, false),
|
ok = test_is_superuser(#{is_superuser => false}, false),
|
||||||
|
|
@ -297,36 +338,44 @@ test_is_superuser(UserInfo, ExpectedIsSuperuser) ->
|
||||||
Username = <<"u">>,
|
Username = <<"u">>,
|
||||||
Password = <<"p">>,
|
Password = <<"p">>,
|
||||||
|
|
||||||
UserInfo0 = UserInfo#{user_id => Username,
|
UserInfo0 = UserInfo#{
|
||||||
password => Password},
|
user_id => Username,
|
||||||
|
password => Password
|
||||||
|
},
|
||||||
|
|
||||||
{ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(UserInfo0, State),
|
{ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(UserInfo0, State),
|
||||||
|
|
||||||
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
||||||
|
|
||||||
{continue, ServerFirstMessage, ServerCache}
|
{continue, ServerFirstMessage, ServerCache} =
|
||||||
= emqx_enhanced_authn_scram_mnesia:authenticate(
|
emqx_enhanced_authn_scram_mnesia:authenticate(
|
||||||
#{auth_method => <<"SCRAM-SHA-512">>,
|
#{
|
||||||
auth_data => ClientFirstMessage,
|
auth_method => <<"SCRAM-SHA-512">>,
|
||||||
auth_cache => #{}
|
auth_data => ClientFirstMessage,
|
||||||
},
|
auth_cache => #{}
|
||||||
State),
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
{continue, ClientFinalMessage, ClientCache} =
|
{continue, ClientFinalMessage, ClientCache} =
|
||||||
esasl_scram:check_server_first_message(
|
esasl_scram:check_server_first_message(
|
||||||
ServerFirstMessage,
|
ServerFirstMessage,
|
||||||
#{client_first_message => ClientFirstMessage,
|
#{
|
||||||
password => Password,
|
client_first_message => ClientFirstMessage,
|
||||||
algorithm => sha512}
|
password => Password,
|
||||||
|
algorithm => sha512
|
||||||
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
{ok, UserInfo1, ServerFinalMessage}
|
{ok, UserInfo1, ServerFinalMessage} =
|
||||||
= emqx_enhanced_authn_scram_mnesia:authenticate(
|
emqx_enhanced_authn_scram_mnesia:authenticate(
|
||||||
#{auth_method => <<"SCRAM-SHA-512">>,
|
#{
|
||||||
auth_data => ClientFinalMessage,
|
auth_method => <<"SCRAM-SHA-512">>,
|
||||||
auth_cache => ServerCache
|
auth_data => ClientFinalMessage,
|
||||||
},
|
auth_cache => ServerCache
|
||||||
State),
|
},
|
||||||
|
State
|
||||||
|
),
|
||||||
|
|
||||||
ok = esasl_scram:check_server_final_message(
|
ok = esasl_scram:check_server_final_message(
|
||||||
ServerFinalMessage, ClientCache#{algorithm => sha512}
|
ServerFinalMessage, ClientCache#{algorithm => sha512}
|
||||||
|
|
@ -336,18 +385,17 @@ test_is_superuser(UserInfo, ExpectedIsSuperuser) ->
|
||||||
|
|
||||||
ok = emqx_enhanced_authn_scram_mnesia:destroy(State).
|
ok = emqx_enhanced_authn_scram_mnesia:destroy(State).
|
||||||
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
config() ->
|
config() ->
|
||||||
#{
|
#{
|
||||||
mechanism => <<"scram">>,
|
mechanism => <<"scram">>,
|
||||||
backend => <<"built_in_database">>,
|
backend => <<"built_in_database">>,
|
||||||
algorithm => sha512,
|
algorithm => sha512,
|
||||||
iteration_count => 4096
|
iteration_count => 4096
|
||||||
}.
|
}.
|
||||||
|
|
||||||
raw_config(Algorithm) ->
|
raw_config(Algorithm) ->
|
||||||
#{
|
#{
|
||||||
|
|
@ -361,14 +409,16 @@ init_auth(Username, Password, Algorithm) ->
|
||||||
Config = raw_config(Algorithm),
|
Config = raw_config(Algorithm),
|
||||||
|
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
?PATH,
|
?PATH,
|
||||||
{create_authenticator, ?GLOBAL, Config}),
|
{create_authenticator, ?GLOBAL, Config}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [#{state := State}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
{ok, [#{state := State}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
||||||
|
|
||||||
emqx_enhanced_authn_scram_mnesia:add_user(
|
emqx_enhanced_authn_scram_mnesia:add_user(
|
||||||
#{user_id => Username, password => Password},
|
#{user_id => Username, password => Password},
|
||||||
State).
|
State
|
||||||
|
).
|
||||||
|
|
||||||
receive_packet() ->
|
receive_packet() ->
|
||||||
receive
|
receive
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,15 @@
|
||||||
|
|
||||||
-define(APP, emqx_authz).
|
-define(APP, emqx_authz).
|
||||||
|
|
||||||
-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse
|
-define(ALLOW_DENY(A),
|
||||||
(A =:= deny) orelse (A =:= <<"deny">>)
|
((A =:= allow) orelse (A =:= <<"allow">>) orelse
|
||||||
)).
|
(A =:= deny) orelse (A =:= <<"deny">>))
|
||||||
-define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= <<"subscribe">>) orelse
|
).
|
||||||
(A =:= publish) orelse (A =:= <<"publish">>) orelse
|
-define(PUBSUB(A),
|
||||||
(A =:= all) orelse (A =:= <<"all">>)
|
((A =:= subscribe) orelse (A =:= <<"subscribe">>) orelse
|
||||||
)).
|
(A =:= publish) orelse (A =:= <<"publish">>) orelse
|
||||||
|
(A =:= all) orelse (A =:= <<"all">>))
|
||||||
|
).
|
||||||
|
|
||||||
%% authz_mnesia
|
%% authz_mnesia
|
||||||
-define(ACL_TABLE, emqx_acl).
|
-define(ACL_TABLE, emqx_acl).
|
||||||
|
|
@ -44,53 +46,69 @@
|
||||||
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9_]+\\}").
|
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9_]+\\}").
|
||||||
|
|
||||||
%% API examples
|
%% API examples
|
||||||
-define(USERNAME_RULES_EXAMPLE, #{username => user1,
|
-define(USERNAME_RULES_EXAMPLE, #{
|
||||||
rules => [ #{topic => <<"test/toopic/1">>,
|
username => user1,
|
||||||
permission => <<"allow">>,
|
rules => [
|
||||||
action => <<"publish">>
|
#{
|
||||||
}
|
topic => <<"test/toopic/1">>,
|
||||||
, #{topic => <<"test/toopic/2">>,
|
permission => <<"allow">>,
|
||||||
permission => <<"allow">>,
|
action => <<"publish">>
|
||||||
action => <<"subscribe">>
|
},
|
||||||
}
|
#{
|
||||||
, #{topic => <<"eq test/#">>,
|
topic => <<"test/toopic/2">>,
|
||||||
permission => <<"deny">>,
|
permission => <<"allow">>,
|
||||||
action => <<"all">>
|
action => <<"subscribe">>
|
||||||
}
|
},
|
||||||
]
|
#{
|
||||||
}).
|
topic => <<"eq test/#">>,
|
||||||
-define(CLIENTID_RULES_EXAMPLE, #{clientid => client1,
|
permission => <<"deny">>,
|
||||||
rules => [ #{topic => <<"test/toopic/1">>,
|
action => <<"all">>
|
||||||
permission => <<"allow">>,
|
}
|
||||||
action => <<"publish">>
|
]
|
||||||
}
|
}).
|
||||||
, #{topic => <<"test/toopic/2">>,
|
-define(CLIENTID_RULES_EXAMPLE, #{
|
||||||
permission => <<"allow">>,
|
clientid => client1,
|
||||||
action => <<"subscribe">>
|
rules => [
|
||||||
}
|
#{
|
||||||
, #{topic => <<"eq test/#">>,
|
topic => <<"test/toopic/1">>,
|
||||||
permission => <<"deny">>,
|
permission => <<"allow">>,
|
||||||
action => <<"all">>
|
action => <<"publish">>
|
||||||
}
|
},
|
||||||
]
|
#{
|
||||||
}).
|
topic => <<"test/toopic/2">>,
|
||||||
-define(ALL_RULES_EXAMPLE, #{rules => [ #{topic => <<"test/toopic/1">>,
|
permission => <<"allow">>,
|
||||||
permission => <<"allow">>,
|
action => <<"subscribe">>
|
||||||
action => <<"publish">>
|
},
|
||||||
}
|
#{
|
||||||
, #{topic => <<"test/toopic/2">>,
|
topic => <<"eq test/#">>,
|
||||||
permission => <<"allow">>,
|
permission => <<"deny">>,
|
||||||
action => <<"subscribe">>
|
action => <<"all">>
|
||||||
}
|
}
|
||||||
, #{topic => <<"eq test/#">>,
|
]
|
||||||
permission => <<"deny">>,
|
}).
|
||||||
action => <<"all">>
|
-define(ALL_RULES_EXAMPLE, #{
|
||||||
}
|
rules => [
|
||||||
]
|
#{
|
||||||
}).
|
topic => <<"test/toopic/1">>,
|
||||||
-define(META_EXAMPLE, #{ page => 1
|
permission => <<"allow">>,
|
||||||
, limit => 100
|
action => <<"publish">>
|
||||||
, count => 1
|
},
|
||||||
}).
|
#{
|
||||||
|
topic => <<"test/toopic/2">>,
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
topic => <<"eq test/#">>,
|
||||||
|
permission => <<"deny">>,
|
||||||
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).
|
||||||
|
-define(META_EXAMPLE, #{
|
||||||
|
page => 1,
|
||||||
|
limit => 100,
|
||||||
|
count => 1
|
||||||
|
}).
|
||||||
|
|
||||||
-define(RESOURCE_GROUP, <<"emqx_authz">>).
|
-define(RESOURCE_GROUP, <<"emqx_authz">>).
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{erl_opts, [debug_info, nowarn_unused_import]}.
|
{erl_opts, [debug_info, nowarn_unused_import]}.
|
||||||
{deps, [ {emqx, {path, "../emqx"}}
|
{deps, [
|
||||||
, {emqx_connector, {path, "../emqx_connector"}}
|
{emqx, {path, "../emqx"}},
|
||||||
]}.
|
{emqx_connector, {path, "../emqx_connector"}}
|
||||||
|
]}.
|
||||||
|
|
||||||
{shell, [
|
{shell, [
|
||||||
% {config, "config/sys.config"},
|
% {config, "config/sys.config"},
|
||||||
{apps, [emqx_authz]}
|
{apps, [emqx_authz]}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_authz,
|
{application, emqx_authz, [
|
||||||
[{description, "An OTP application"},
|
{description, "An OTP application"},
|
||||||
{vsn, "0.1.1"},
|
{vsn, "0.1.1"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{mod, {emqx_authz_app, []}},
|
{mod, {emqx_authz_app, []}},
|
||||||
{applications,
|
{applications, [
|
||||||
[kernel,
|
kernel,
|
||||||
stdlib,
|
stdlib,
|
||||||
crypto,
|
crypto,
|
||||||
emqx_connector
|
emqx_connector
|
||||||
]},
|
]},
|
||||||
{env,[]},
|
{env, []},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
|
|
||||||
{licenses, ["Apache 2.0"]},
|
{licenses, ["Apache 2.0"]},
|
||||||
{links, []}
|
{links, []}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -25,29 +25,30 @@
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-export([ register_metrics/0
|
-export([
|
||||||
, init/0
|
register_metrics/0,
|
||||||
, deinit/0
|
init/0,
|
||||||
, lookup/0
|
deinit/0,
|
||||||
, lookup/1
|
lookup/0,
|
||||||
, move/2
|
lookup/1,
|
||||||
, update/2
|
move/2,
|
||||||
, authorize/5
|
update/2,
|
||||||
]).
|
authorize/5
|
||||||
|
]).
|
||||||
|
|
||||||
-export([post_config_update/5, pre_config_update/3]).
|
-export([post_config_update/5, pre_config_update/3]).
|
||||||
|
|
||||||
-export([acl_conf_file/0]).
|
-export([acl_conf_file/0]).
|
||||||
|
|
||||||
-type(source() :: map()).
|
-type source() :: map().
|
||||||
|
|
||||||
-type(match_result() :: {matched, allow} | {matched, deny} | nomatch).
|
-type match_result() :: {matched, allow} | {matched, deny} | nomatch.
|
||||||
|
|
||||||
-type(default_result() :: allow | deny).
|
-type default_result() :: allow | deny.
|
||||||
|
|
||||||
-type(authz_result() :: {stop, allow} | {ok, deny}).
|
-type authz_result() :: {stop, allow} | {ok, deny}.
|
||||||
|
|
||||||
-type(sources() :: [source()]).
|
-type sources() :: [source()].
|
||||||
|
|
||||||
-define(METRIC_ALLOW, 'client.authorize.allow').
|
-define(METRIC_ALLOW, 'client.authorize.allow').
|
||||||
-define(METRIC_DENY, 'client.authorize.deny').
|
-define(METRIC_DENY, 'client.authorize.deny').
|
||||||
|
|
@ -60,24 +61,25 @@
|
||||||
%% Initialize authz backend.
|
%% Initialize authz backend.
|
||||||
%% Populate the passed configuration map with necessary data,
|
%% Populate the passed configuration map with necessary data,
|
||||||
%% like `ResourceID`s
|
%% like `ResourceID`s
|
||||||
-callback(init(source()) -> source()).
|
-callback init(source()) -> source().
|
||||||
|
|
||||||
%% Get authz text description.
|
%% Get authz text description.
|
||||||
-callback(description() -> string()).
|
-callback description() -> string().
|
||||||
|
|
||||||
%% Destroy authz backend.
|
%% Destroy authz backend.
|
||||||
%% Make cleanup of all allocated data.
|
%% Make cleanup of all allocated data.
|
||||||
%% An authz backend will not be used after `destroy`.
|
%% An authz backend will not be used after `destroy`.
|
||||||
-callback(destroy(source()) -> ok).
|
-callback destroy(source()) -> ok.
|
||||||
|
|
||||||
%% Authorize client action.
|
%% Authorize client action.
|
||||||
-callback(authorize(
|
-callback authorize(
|
||||||
emqx_types:clientinfo(),
|
emqx_types:clientinfo(),
|
||||||
emqx_types:pubsub(),
|
emqx_types:pubsub(),
|
||||||
emqx_types:topic(),
|
emqx_types:topic(),
|
||||||
source()) -> match_result()).
|
source()
|
||||||
|
) -> match_result().
|
||||||
|
|
||||||
-spec(register_metrics() -> ok).
|
-spec register_metrics() -> ok.
|
||||||
register_metrics() ->
|
register_metrics() ->
|
||||||
lists:foreach(fun emqx_metrics:ensure/1, ?METRICS).
|
lists:foreach(fun emqx_metrics:ensure/1, ?METRICS).
|
||||||
|
|
||||||
|
|
@ -104,13 +106,16 @@ lookup(Type) ->
|
||||||
|
|
||||||
move(Type, ?CMD_MOVE_BEFORE(Before)) ->
|
move(Type, ?CMD_MOVE_BEFORE(Before)) ->
|
||||||
emqx_authz_utils:update_config(
|
emqx_authz_utils:update_config(
|
||||||
?CONF_KEY_PATH, {?CMD_MOVE, type(Type), ?CMD_MOVE_BEFORE(type(Before))});
|
?CONF_KEY_PATH, {?CMD_MOVE, type(Type), ?CMD_MOVE_BEFORE(type(Before))}
|
||||||
|
);
|
||||||
move(Type, ?CMD_MOVE_AFTER(After)) ->
|
move(Type, ?CMD_MOVE_AFTER(After)) ->
|
||||||
emqx_authz_utils:update_config(
|
emqx_authz_utils:update_config(
|
||||||
?CONF_KEY_PATH, {?CMD_MOVE, type(Type), ?CMD_MOVE_AFTER(type(After))});
|
?CONF_KEY_PATH, {?CMD_MOVE, type(Type), ?CMD_MOVE_AFTER(type(After))}
|
||||||
|
);
|
||||||
move(Type, Position) ->
|
move(Type, Position) ->
|
||||||
emqx_authz_utils:update_config(
|
emqx_authz_utils:update_config(
|
||||||
?CONF_KEY_PATH, {?CMD_MOVE, type(Type), Position}).
|
?CONF_KEY_PATH, {?CMD_MOVE, type(Type), Position}
|
||||||
|
).
|
||||||
|
|
||||||
update({?CMD_REPLACE, Type}, Sources) ->
|
update({?CMD_REPLACE, Type}, Sources) ->
|
||||||
emqx_authz_utils:update_config(?CONF_KEY_PATH, {{?CMD_REPLACE, type(Type)}, Sources});
|
emqx_authz_utils:update_config(?CONF_KEY_PATH, {{?CMD_REPLACE, type(Type)}, Sources});
|
||||||
|
|
@ -205,7 +210,8 @@ do_move({?CMD_MOVE, Type, ?CMD_MOVE_AFTER(After)}, Sources) ->
|
||||||
{S2, Front2, Rear2} = take(After, Front1 ++ Rear1),
|
{S2, Front2, Rear2} = take(After, Front1 ++ Rear1),
|
||||||
Front2 ++ [S2, S1] ++ Rear2.
|
Front2 ++ [S2, S1] ++ Rear2.
|
||||||
|
|
||||||
ensure_resource_deleted(#{enable := false}) -> ok;
|
ensure_resource_deleted(#{enable := false}) ->
|
||||||
|
ok;
|
||||||
ensure_resource_deleted(#{type := Type} = Source) ->
|
ensure_resource_deleted(#{type := Type} = Source) ->
|
||||||
Module = authz_module(Type),
|
Module = authz_module(Type),
|
||||||
Module:destroy(Source).
|
Module:destroy(Source).
|
||||||
|
|
@ -213,17 +219,19 @@ ensure_resource_deleted(#{type := Type} = Source) ->
|
||||||
check_dup_types(Sources) ->
|
check_dup_types(Sources) ->
|
||||||
check_dup_types(Sources, []).
|
check_dup_types(Sources, []).
|
||||||
|
|
||||||
check_dup_types([], _Checked) -> ok;
|
check_dup_types([], _Checked) ->
|
||||||
|
ok;
|
||||||
check_dup_types([Source | Sources], Checked) ->
|
check_dup_types([Source | Sources], Checked) ->
|
||||||
%% the input might be raw or type-checked result, so lookup both 'type' and <<"type">>
|
%% the input might be raw or type-checked result, so lookup both 'type' and <<"type">>
|
||||||
%% TODO: check: really?
|
%% TODO: check: really?
|
||||||
Type = case maps:get(<<"type">>, Source, maps:get(type, Source, undefined)) of
|
Type =
|
||||||
undefined ->
|
case maps:get(<<"type">>, Source, maps:get(type, Source, undefined)) of
|
||||||
%% this should never happen if the value is type checked by honcon schema
|
undefined ->
|
||||||
throw({bad_source_input, Source});
|
%% this should never happen if the value is type checked by honcon schema
|
||||||
Type0 ->
|
throw({bad_source_input, Source});
|
||||||
type(Type0)
|
Type0 ->
|
||||||
end,
|
type(Type0)
|
||||||
|
end,
|
||||||
case lists:member(Type, Checked) of
|
case lists:member(Type, Checked) of
|
||||||
true ->
|
true ->
|
||||||
%% we have made it clear not to support more than one authz instance for each type
|
%% we have made it clear not to support more than one authz instance for each type
|
||||||
|
|
@ -240,7 +248,8 @@ init_sources(Sources) ->
|
||||||
end,
|
end,
|
||||||
lists:map(fun init_source/1, Sources).
|
lists:map(fun init_source/1, Sources).
|
||||||
|
|
||||||
init_source(#{enable := false} = Source) -> Source;
|
init_source(#{enable := false} = Source) ->
|
||||||
|
Source;
|
||||||
init_source(#{type := Type} = Source) ->
|
init_source(#{type := Type} = Source) ->
|
||||||
Module = authz_module(Type),
|
Module = authz_module(Type),
|
||||||
Module:init(Source).
|
Module:init(Source).
|
||||||
|
|
@ -250,42 +259,63 @@ init_source(#{type := Type} = Source) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
%% @doc Check AuthZ
|
%% @doc Check AuthZ
|
||||||
-spec(authorize( emqx_types:clientinfo()
|
-spec authorize(
|
||||||
, emqx_types:pubsub()
|
emqx_types:clientinfo(),
|
||||||
, emqx_types:topic()
|
emqx_types:pubsub(),
|
||||||
, default_result()
|
emqx_types:topic(),
|
||||||
, sources())
|
default_result(),
|
||||||
-> authz_result()).
|
sources()
|
||||||
authorize(#{username := Username,
|
) ->
|
||||||
peerhost := IpAddress
|
authz_result().
|
||||||
} = Client, PubSub, Topic, DefaultResult, Sources) ->
|
authorize(
|
||||||
|
#{
|
||||||
|
username := Username,
|
||||||
|
peerhost := IpAddress
|
||||||
|
} = Client,
|
||||||
|
PubSub,
|
||||||
|
Topic,
|
||||||
|
DefaultResult,
|
||||||
|
Sources
|
||||||
|
) ->
|
||||||
case do_authorize(Client, PubSub, Topic, Sources) of
|
case do_authorize(Client, PubSub, Topic, Sources) of
|
||||||
{{matched, allow}, AuthzSource}->
|
{{matched, allow}, AuthzSource} ->
|
||||||
emqx:run_hook('client.check_authz_complete',
|
emqx:run_hook(
|
||||||
[Client, PubSub, Topic, allow, AuthzSource]),
|
'client.check_authz_complete',
|
||||||
?SLOG(info, #{msg => "authorization_permission_allowed",
|
[Client, PubSub, Topic, allow, AuthzSource]
|
||||||
username => Username,
|
),
|
||||||
ipaddr => IpAddress,
|
?SLOG(info, #{
|
||||||
topic => Topic}),
|
msg => "authorization_permission_allowed",
|
||||||
|
username => Username,
|
||||||
|
ipaddr => IpAddress,
|
||||||
|
topic => Topic
|
||||||
|
}),
|
||||||
emqx_metrics:inc(?METRIC_ALLOW),
|
emqx_metrics:inc(?METRIC_ALLOW),
|
||||||
{stop, allow};
|
{stop, allow};
|
||||||
{{matched, deny}, AuthzSource}->
|
{{matched, deny}, AuthzSource} ->
|
||||||
emqx:run_hook('client.check_authz_complete',
|
emqx:run_hook(
|
||||||
[Client, PubSub, Topic, deny, AuthzSource]),
|
'client.check_authz_complete',
|
||||||
?SLOG(info, #{msg => "authorization_permission_denied",
|
[Client, PubSub, Topic, deny, AuthzSource]
|
||||||
username => Username,
|
),
|
||||||
ipaddr => IpAddress,
|
?SLOG(info, #{
|
||||||
topic => Topic}),
|
msg => "authorization_permission_denied",
|
||||||
|
username => Username,
|
||||||
|
ipaddr => IpAddress,
|
||||||
|
topic => Topic
|
||||||
|
}),
|
||||||
emqx_metrics:inc(?METRIC_DENY),
|
emqx_metrics:inc(?METRIC_DENY),
|
||||||
{stop, deny};
|
{stop, deny};
|
||||||
nomatch ->
|
nomatch ->
|
||||||
emqx:run_hook('client.check_authz_complete',
|
emqx:run_hook(
|
||||||
[Client, PubSub, Topic, DefaultResult, default]),
|
'client.check_authz_complete',
|
||||||
?SLOG(info, #{msg => "authorization_failed_nomatch",
|
[Client, PubSub, Topic, DefaultResult, default]
|
||||||
username => Username,
|
),
|
||||||
ipaddr => IpAddress,
|
?SLOG(info, #{
|
||||||
topic => Topic,
|
msg => "authorization_failed_nomatch",
|
||||||
reason => "no-match rule"}),
|
username => Username,
|
||||||
|
ipaddr => IpAddress,
|
||||||
|
topic => Topic,
|
||||||
|
reason => "no-match rule"
|
||||||
|
}),
|
||||||
emqx_metrics:inc(?METRIC_NOMATCH),
|
emqx_metrics:inc(?METRIC_NOMATCH),
|
||||||
{stop, DefaultResult}
|
{stop, DefaultResult}
|
||||||
end.
|
end.
|
||||||
|
|
@ -294,8 +324,12 @@ do_authorize(_Client, _PubSub, _Topic, []) ->
|
||||||
nomatch;
|
nomatch;
|
||||||
do_authorize(Client, PubSub, Topic, [#{enable := false} | Rest]) ->
|
do_authorize(Client, PubSub, Topic, [#{enable := false} | Rest]) ->
|
||||||
do_authorize(Client, PubSub, Topic, Rest);
|
do_authorize(Client, PubSub, Topic, Rest);
|
||||||
do_authorize(Client, PubSub, Topic,
|
do_authorize(
|
||||||
[Connector = #{type := Type} | Tail] ) ->
|
Client,
|
||||||
|
PubSub,
|
||||||
|
Topic,
|
||||||
|
[Connector = #{type := Type} | Tail]
|
||||||
|
) ->
|
||||||
Module = authz_module(Type),
|
Module = authz_module(Type),
|
||||||
case Module:authorize(Client, PubSub, Topic, Connector) of
|
case Module:authorize(Client, PubSub, Topic, Connector) of
|
||||||
nomatch -> do_authorize(Client, PubSub, Topic, Tail);
|
nomatch -> do_authorize(Client, PubSub, Topic, Tail);
|
||||||
|
|
@ -311,7 +345,7 @@ take(Type) -> take(Type, lookup()).
|
||||||
%% Take the source of give type, the sources list is split into two parts
|
%% Take the source of give type, the sources list is split into two parts
|
||||||
%% front part and rear part.
|
%% front part and rear part.
|
||||||
take(Type, Sources) ->
|
take(Type, Sources) ->
|
||||||
{Front, Rear} = lists:splitwith(fun(T) -> type(T) =/= type(Type) end, Sources),
|
{Front, Rear} = lists:splitwith(fun(T) -> type(T) =/= type(Type) end, Sources),
|
||||||
case Rear =:= [] of
|
case Rear =:= [] of
|
||||||
true ->
|
true ->
|
||||||
throw({not_found_source, Type});
|
throw({not_found_source, Type});
|
||||||
|
|
@ -321,7 +355,7 @@ take(Type, Sources) ->
|
||||||
|
|
||||||
find_action_in_hooks() ->
|
find_action_in_hooks() ->
|
||||||
Callbacks = emqx_hooks:lookup('client.authorize'),
|
Callbacks = emqx_hooks:lookup('client.authorize'),
|
||||||
[Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ],
|
[Action] = [Action || {callback, {?MODULE, authorize, _} = Action, _, _} <- Callbacks],
|
||||||
Action.
|
Action.
|
||||||
|
|
||||||
authz_module('built_in_database') ->
|
authz_module('built_in_database') ->
|
||||||
|
|
@ -364,8 +398,11 @@ acl_conf_file() ->
|
||||||
filename:join([emqx:data_dir(), "authz", "acl.conf"]).
|
filename:join([emqx:data_dir(), "authz", "acl.conf"]).
|
||||||
|
|
||||||
maybe_write_certs(#{<<"type">> := Type} = Source) ->
|
maybe_write_certs(#{<<"type">> := Type} = Source) ->
|
||||||
case emqx_tls_lib:ensure_ssl_files(
|
case
|
||||||
ssl_file_path(Type), maps:get(<<"ssl">>, Source, undefined)) of
|
emqx_tls_lib:ensure_ssl_files(
|
||||||
|
ssl_file_path(Type), maps:get(<<"ssl">>, Source, undefined)
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, SSL} ->
|
{ok, SSL} ->
|
||||||
new_ssl_source(Source, SSL);
|
new_ssl_source(Source, SSL);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
@ -380,7 +417,8 @@ clear_certs(OldSource) ->
|
||||||
write_file(Filename, Bytes) ->
|
write_file(Filename, Bytes) ->
|
||||||
ok = filelib:ensure_dir(Filename),
|
ok = filelib:ensure_dir(Filename),
|
||||||
case file:write_file(Filename, Bytes) of
|
case file:write_file(Filename, Bytes) of
|
||||||
ok -> {ok, iolist_to_binary(Filename)};
|
ok ->
|
||||||
|
{ok, iolist_to_binary(Filename)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{filename => Filename, msg => "write_file_error", reason => Reason}),
|
?SLOG(error, #{filename => Filename, msg => "write_file_error", reason => Reason}),
|
||||||
throw(Reason)
|
throw(Reason)
|
||||||
|
|
|
||||||
|
|
@ -30,25 +30,28 @@
|
||||||
-define(ACL_USERNAME_QSCHEMA, [{<<"like_username">>, binary}]).
|
-define(ACL_USERNAME_QSCHEMA, [{<<"like_username">>, binary}]).
|
||||||
-define(ACL_CLIENTID_QSCHEMA, [{<<"like_clientid">>, binary}]).
|
-define(ACL_CLIENTID_QSCHEMA, [{<<"like_clientid">>, binary}]).
|
||||||
|
|
||||||
|
-export([
|
||||||
-export([ api_spec/0
|
api_spec/0,
|
||||||
, paths/0
|
paths/0,
|
||||||
, schema/1
|
schema/1,
|
||||||
, fields/1
|
fields/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% operation funs
|
%% operation funs
|
||||||
-export([ users/2
|
-export([
|
||||||
, clients/2
|
users/2,
|
||||||
, user/2
|
clients/2,
|
||||||
, client/2
|
user/2,
|
||||||
, all/2
|
client/2,
|
||||||
, purge/2
|
all/2,
|
||||||
]).
|
purge/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% query funs
|
%% query funs
|
||||||
-export([ query_username/4
|
-export([
|
||||||
, query_clientid/4]).
|
query_username/4,
|
||||||
|
query_clientid/4
|
||||||
|
]).
|
||||||
|
|
||||||
-export([format_result/1]).
|
-export([format_result/1]).
|
||||||
|
|
||||||
|
|
@ -65,279 +68,399 @@ api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[ "/authorization/sources/built_in_database/username"
|
[
|
||||||
, "/authorization/sources/built_in_database/clientid"
|
"/authorization/sources/built_in_database/username",
|
||||||
, "/authorization/sources/built_in_database/username/:username"
|
"/authorization/sources/built_in_database/clientid",
|
||||||
, "/authorization/sources/built_in_database/clientid/:clientid"
|
"/authorization/sources/built_in_database/username/:username",
|
||||||
, "/authorization/sources/built_in_database/all"
|
"/authorization/sources/built_in_database/clientid/:clientid",
|
||||||
, "/authorization/sources/built_in_database/purge-all"].
|
"/authorization/sources/built_in_database/all",
|
||||||
|
"/authorization/sources/built_in_database/purge-all"
|
||||||
|
].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Schema for each URI
|
%% Schema for each URI
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
schema("/authorization/sources/built_in_database/username") ->
|
schema("/authorization/sources/built_in_database/username") ->
|
||||||
#{ 'operationId' => users
|
#{
|
||||||
, get =>
|
'operationId' => users,
|
||||||
#{ tags => [<<"authorization">>]
|
get =>
|
||||||
, description => <<"Show the list of record for username">>
|
#{
|
||||||
, parameters =>
|
tags => [<<"authorization">>],
|
||||||
[ ref(emqx_dashboard_swagger, page)
|
description => <<"Show the list of record for username">>,
|
||||||
, ref(emqx_dashboard_swagger, limit)
|
parameters =>
|
||||||
, { like_username
|
[
|
||||||
, mk( binary(), #{ in => query
|
ref(emqx_dashboard_swagger, page),
|
||||||
, required => false
|
ref(emqx_dashboard_swagger, limit),
|
||||||
, desc => <<"Fuzzy search `username` as substring">>})}
|
{like_username,
|
||||||
]
|
mk(binary(), #{
|
||||||
, responses =>
|
in => query,
|
||||||
#{ 200 => swagger_with_example( {username_response_data, ?TYPE_REF}
|
required => false,
|
||||||
, {username, ?PAGE_QUERY_EXAMPLE})
|
desc => <<"Fuzzy search `username` as substring">>
|
||||||
|
})}
|
||||||
|
],
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => swagger_with_example(
|
||||||
|
{username_response_data, ?TYPE_REF},
|
||||||
|
{username, ?PAGE_QUERY_EXAMPLE}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
post =>
|
||||||
|
#{
|
||||||
|
tags => [<<"authorization">>],
|
||||||
|
description => <<"Add new records for username">>,
|
||||||
|
'requestBody' => swagger_with_example(
|
||||||
|
{rules_for_username, ?TYPE_ARRAY},
|
||||||
|
{username, ?POST_ARRAY_EXAMPLE}
|
||||||
|
),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Created">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad username or bad rule schema">>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
, post =>
|
|
||||||
#{ tags => [<<"authorization">>]
|
|
||||||
, description => <<"Add new records for username">>
|
|
||||||
, 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_ARRAY}
|
|
||||||
, {username, ?POST_ARRAY_EXAMPLE})
|
|
||||||
, responses =>
|
|
||||||
#{ 204 => <<"Created">>
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes(
|
|
||||||
[?BAD_REQUEST], <<"Bad username or bad rule schema">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
schema("/authorization/sources/built_in_database/clientid") ->
|
schema("/authorization/sources/built_in_database/clientid") ->
|
||||||
#{ 'operationId' => clients
|
#{
|
||||||
, get =>
|
'operationId' => clients,
|
||||||
#{ tags => [<<"authorization">>]
|
get =>
|
||||||
, description => <<"Show the list of record for clientid">>
|
#{
|
||||||
, parameters =>
|
tags => [<<"authorization">>],
|
||||||
[ ref(emqx_dashboard_swagger, page)
|
description => <<"Show the list of record for clientid">>,
|
||||||
, ref(emqx_dashboard_swagger, limit)
|
parameters =>
|
||||||
, { like_clientid
|
[
|
||||||
, mk( binary()
|
ref(emqx_dashboard_swagger, page),
|
||||||
, #{ in => query
|
ref(emqx_dashboard_swagger, limit),
|
||||||
, required => false
|
{like_clientid,
|
||||||
, desc => <<"Fuzzy search `clientid` as substring">>})
|
mk(
|
||||||
|
binary(),
|
||||||
|
#{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc => <<"Fuzzy search `clientid` as substring">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
],
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => swagger_with_example(
|
||||||
|
{clientid_response_data, ?TYPE_REF},
|
||||||
|
{clientid, ?PAGE_QUERY_EXAMPLE}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]
|
|
||||||
, responses =>
|
|
||||||
#{ 200 => swagger_with_example( {clientid_response_data, ?TYPE_REF}
|
|
||||||
, {clientid, ?PAGE_QUERY_EXAMPLE})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, post =>
|
|
||||||
#{ tags => [<<"authorization">>]
|
|
||||||
, description => <<"Add new records for clientid">>
|
|
||||||
, 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_ARRAY}
|
|
||||||
, {clientid, ?POST_ARRAY_EXAMPLE})
|
|
||||||
, responses =>
|
|
||||||
#{ 204 => <<"Created">>
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes(
|
|
||||||
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
schema("/authorization/sources/built_in_database/username/:username") ->
|
|
||||||
#{ 'operationId' => user
|
|
||||||
, get =>
|
|
||||||
#{ tags => [<<"authorization">>]
|
|
||||||
, description => <<"Get record info for username">>
|
|
||||||
, parameters => [ref(username)]
|
|
||||||
, responses =>
|
|
||||||
#{ 200 => swagger_with_example( {rules_for_username, ?TYPE_REF}
|
|
||||||
, {username, ?PUT_MAP_EXAMPLE})
|
|
||||||
, 404 => emqx_dashboard_swagger:error_codes(
|
|
||||||
[?NOT_FOUND], <<"Not Found">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, put =>
|
|
||||||
#{ tags => [<<"authorization">>]
|
|
||||||
, description => <<"Set record for username">>
|
|
||||||
, parameters => [ref(username)]
|
|
||||||
, 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_REF}
|
|
||||||
, {username, ?PUT_MAP_EXAMPLE})
|
|
||||||
, responses =>
|
|
||||||
#{ 204 => <<"Updated">>
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes(
|
|
||||||
[?BAD_REQUEST], <<"Bad username or bad rule schema">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, delete =>
|
|
||||||
#{ tags => [<<"authorization">>]
|
|
||||||
, description => <<"Delete one record for username">>
|
|
||||||
, parameters => [ref(username)]
|
|
||||||
, responses =>
|
|
||||||
#{ 204 => <<"Deleted">>
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes(
|
|
||||||
[?BAD_REQUEST], <<"Bad username">>)
|
|
||||||
, 404 => emqx_dashboard_swagger:error_codes(
|
|
||||||
[?NOT_FOUND], <<"Username Not Found">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
schema("/authorization/sources/built_in_database/clientid/:clientid") ->
|
|
||||||
#{ 'operationId' => client
|
|
||||||
, get =>
|
|
||||||
#{ tags => [<<"authorization">>]
|
|
||||||
, description => <<"Get record info for clientid">>
|
|
||||||
, parameters => [ref(clientid)]
|
|
||||||
, responses =>
|
|
||||||
#{ 200 => swagger_with_example( {rules_for_clientid, ?TYPE_REF}
|
|
||||||
, {clientid, ?PUT_MAP_EXAMPLE})
|
|
||||||
, 404 => emqx_dashboard_swagger:error_codes(
|
|
||||||
[?NOT_FOUND], <<"Not Found">>)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
put =>
|
post =>
|
||||||
#{ tags => [<<"authorization">>]
|
#{
|
||||||
, description => <<"Set record for clientid">>
|
tags => [<<"authorization">>],
|
||||||
, parameters => [ref(clientid)]
|
description => <<"Add new records for clientid">>,
|
||||||
, 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_REF}
|
'requestBody' => swagger_with_example(
|
||||||
, {clientid, ?PUT_MAP_EXAMPLE})
|
{rules_for_clientid, ?TYPE_ARRAY},
|
||||||
, responses =>
|
{clientid, ?POST_ARRAY_EXAMPLE}
|
||||||
#{ 204 => <<"Updated">>
|
),
|
||||||
, 400 => emqx_dashboard_swagger:error_codes(
|
responses =>
|
||||||
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>)
|
#{
|
||||||
}
|
204 => <<"Created">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, delete =>
|
};
|
||||||
#{ tags => [<<"authorization">>]
|
schema("/authorization/sources/built_in_database/username/:username") ->
|
||||||
, description => <<"Delete one record for clientid">>
|
#{
|
||||||
, parameters => [ref(clientid)]
|
'operationId' => user,
|
||||||
, responses =>
|
get =>
|
||||||
#{ 204 => <<"Deleted">>
|
#{
|
||||||
, 400 => emqx_dashboard_swagger:error_codes(
|
tags => [<<"authorization">>],
|
||||||
[?BAD_REQUEST], <<"Bad clientid">>)
|
description => <<"Get record info for username">>,
|
||||||
, 404 => emqx_dashboard_swagger:error_codes(
|
parameters => [ref(username)],
|
||||||
[?NOT_FOUND], <<"ClientID Not Found">>)
|
responses =>
|
||||||
}
|
#{
|
||||||
|
200 => swagger_with_example(
|
||||||
|
{rules_for_username, ?TYPE_REF},
|
||||||
|
{username, ?PUT_MAP_EXAMPLE}
|
||||||
|
),
|
||||||
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?NOT_FOUND], <<"Not Found">>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
put =>
|
||||||
|
#{
|
||||||
|
tags => [<<"authorization">>],
|
||||||
|
description => <<"Set record for username">>,
|
||||||
|
parameters => [ref(username)],
|
||||||
|
'requestBody' => swagger_with_example(
|
||||||
|
{rules_for_username, ?TYPE_REF},
|
||||||
|
{username, ?PUT_MAP_EXAMPLE}
|
||||||
|
),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Updated">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad username or bad rule schema">>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete =>
|
||||||
|
#{
|
||||||
|
tags => [<<"authorization">>],
|
||||||
|
description => <<"Delete one record for username">>,
|
||||||
|
parameters => [ref(username)],
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Deleted">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad username">>
|
||||||
|
),
|
||||||
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?NOT_FOUND], <<"Username Not Found">>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
schema("/authorization/sources/built_in_database/clientid/:clientid") ->
|
||||||
|
#{
|
||||||
|
'operationId' => client,
|
||||||
|
get =>
|
||||||
|
#{
|
||||||
|
tags => [<<"authorization">>],
|
||||||
|
description => <<"Get record info for clientid">>,
|
||||||
|
parameters => [ref(clientid)],
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => swagger_with_example(
|
||||||
|
{rules_for_clientid, ?TYPE_REF},
|
||||||
|
{clientid, ?PUT_MAP_EXAMPLE}
|
||||||
|
),
|
||||||
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?NOT_FOUND], <<"Not Found">>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
put =>
|
||||||
|
#{
|
||||||
|
tags => [<<"authorization">>],
|
||||||
|
description => <<"Set record for clientid">>,
|
||||||
|
parameters => [ref(clientid)],
|
||||||
|
'requestBody' => swagger_with_example(
|
||||||
|
{rules_for_clientid, ?TYPE_REF},
|
||||||
|
{clientid, ?PUT_MAP_EXAMPLE}
|
||||||
|
),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Updated">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete =>
|
||||||
|
#{
|
||||||
|
tags => [<<"authorization">>],
|
||||||
|
description => <<"Delete one record for clientid">>,
|
||||||
|
parameters => [ref(clientid)],
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Deleted">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad clientid">>
|
||||||
|
),
|
||||||
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?NOT_FOUND], <<"ClientID Not Found">>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
schema("/authorization/sources/built_in_database/all") ->
|
schema("/authorization/sources/built_in_database/all") ->
|
||||||
#{ 'operationId' => all
|
#{
|
||||||
, get =>
|
'operationId' => all,
|
||||||
#{ tags => [<<"authorization">>]
|
get =>
|
||||||
, description => <<"Show the list of rules for all">>
|
#{
|
||||||
, responses =>
|
tags => [<<"authorization">>],
|
||||||
#{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})}
|
description => <<"Show the list of rules for all">>,
|
||||||
|
responses =>
|
||||||
|
#{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})}
|
||||||
|
},
|
||||||
|
post =>
|
||||||
|
#{
|
||||||
|
tags => [<<"authorization">>],
|
||||||
|
description => <<
|
||||||
|
"Create/Update the list of rules for all. "
|
||||||
|
"Set a empty list to clean up rules"
|
||||||
|
>>,
|
||||||
|
'requestBody' =>
|
||||||
|
swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Updated">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad rule schema">>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, post =>
|
};
|
||||||
#{ tags => [<<"authorization">>]
|
|
||||||
, description => <<"Create/Update the list of rules for all. "
|
|
||||||
"Set a empty list to clean up rules">>
|
|
||||||
, 'requestBody' =>
|
|
||||||
swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})
|
|
||||||
, responses =>
|
|
||||||
#{ 204 => <<"Updated">>
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes(
|
|
||||||
[?BAD_REQUEST], <<"Bad rule schema">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
schema("/authorization/sources/built_in_database/purge-all") ->
|
schema("/authorization/sources/built_in_database/purge-all") ->
|
||||||
#{ 'operationId' => purge
|
#{
|
||||||
, delete =>
|
'operationId' => purge,
|
||||||
#{ tags => [<<"authorization">>]
|
delete =>
|
||||||
, description => <<"Purge all records">>
|
#{
|
||||||
, responses =>
|
tags => [<<"authorization">>],
|
||||||
#{ 204 => <<"Deleted">>
|
description => <<"Purge all records">>,
|
||||||
, 400 => emqx_dashboard_swagger:error_codes(
|
responses =>
|
||||||
[?BAD_REQUEST], <<"Bad Request">>)
|
#{
|
||||||
}
|
204 => <<"Deleted">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad Request">>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(rule_item) ->
|
fields(rule_item) ->
|
||||||
[ {topic, mk(string(),
|
[
|
||||||
#{ required => true
|
{topic,
|
||||||
, desc => <<"Rule on specific topic">>
|
mk(
|
||||||
, example => <<"test/topic/1">>
|
string(),
|
||||||
})}
|
#{
|
||||||
, {permission, mk(enum([allow, deny]),
|
required => true,
|
||||||
#{ desc => <<"Permission">>
|
desc => <<"Rule on specific topic">>,
|
||||||
, required => true
|
example => <<"test/topic/1">>
|
||||||
, example => allow
|
}
|
||||||
})}
|
)},
|
||||||
, {action, mk(enum([publish, subscribe, all]),
|
{permission,
|
||||||
#{ required => true
|
mk(
|
||||||
, example => publish
|
enum([allow, deny]),
|
||||||
, desc => <<"Authorized action">>
|
#{
|
||||||
})}
|
desc => <<"Permission">>,
|
||||||
|
required => true,
|
||||||
|
example => allow
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{action,
|
||||||
|
mk(
|
||||||
|
enum([publish, subscribe, all]),
|
||||||
|
#{
|
||||||
|
required => true,
|
||||||
|
example => publish,
|
||||||
|
desc => <<"Authorized action">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(clientid) ->
|
fields(clientid) ->
|
||||||
[ {clientid, mk(binary(),
|
[
|
||||||
#{ in => path
|
{clientid,
|
||||||
, required => true
|
mk(
|
||||||
, desc => <<"ClientID">>
|
binary(),
|
||||||
, example => <<"client1">>
|
#{
|
||||||
})}
|
in => path,
|
||||||
|
required => true,
|
||||||
|
desc => <<"ClientID">>,
|
||||||
|
example => <<"client1">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(username) ->
|
fields(username) ->
|
||||||
[ {username, mk(binary(),
|
[
|
||||||
#{ in => path
|
{username,
|
||||||
, required => true
|
mk(
|
||||||
, desc => <<"Username">>
|
binary(),
|
||||||
, example => <<"user1">>})}
|
#{
|
||||||
|
in => path,
|
||||||
|
required => true,
|
||||||
|
desc => <<"Username">>,
|
||||||
|
example => <<"user1">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(rules_for_username) ->
|
fields(rules_for_username) ->
|
||||||
fields(rules)
|
fields(rules) ++
|
||||||
++ fields(username);
|
fields(username);
|
||||||
fields(username_response_data) ->
|
fields(username_response_data) ->
|
||||||
[ {data, mk(array(ref(rules_for_username)), #{})}
|
[
|
||||||
, {meta, ref(meta)}
|
{data, mk(array(ref(rules_for_username)), #{})},
|
||||||
|
{meta, ref(meta)}
|
||||||
];
|
];
|
||||||
fields(rules_for_clientid) ->
|
fields(rules_for_clientid) ->
|
||||||
fields(rules)
|
fields(rules) ++
|
||||||
++ fields(clientid);
|
fields(clientid);
|
||||||
fields(clientid_response_data) ->
|
fields(clientid_response_data) ->
|
||||||
[ {data, mk(array(ref(rules_for_clientid)), #{})}
|
[
|
||||||
, {meta, ref(meta)}
|
{data, mk(array(ref(rules_for_clientid)), #{})},
|
||||||
|
{meta, ref(meta)}
|
||||||
];
|
];
|
||||||
fields(rules) ->
|
fields(rules) ->
|
||||||
[{rules, mk(array(ref(rule_item)))}];
|
[{rules, mk(array(ref(rule_item)))}];
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page)
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
++ emqx_dashboard_swagger:fields(limit)
|
emqx_dashboard_swagger:fields(limit) ++
|
||||||
++ [{count, mk(integer(), #{example => 1})}].
|
[{count, mk(integer(), #{example => 1})}].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% HTTP API
|
%% HTTP API
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
users(get, #{query_string := QueryString}) ->
|
users(get, #{query_string := QueryString}) ->
|
||||||
Response = emqx_mgmt_api:node_query(node(), QueryString,
|
Response = emqx_mgmt_api:node_query(
|
||||||
?ACL_TABLE, ?ACL_USERNAME_QSCHEMA, ?QUERY_USERNAME_FUN),
|
node(),
|
||||||
|
QueryString,
|
||||||
|
?ACL_TABLE,
|
||||||
|
?ACL_USERNAME_QSCHEMA,
|
||||||
|
?QUERY_USERNAME_FUN
|
||||||
|
),
|
||||||
emqx_mgmt_util:generate_response(Response);
|
emqx_mgmt_util:generate_response(Response);
|
||||||
users(post, #{body := Body}) when is_list(Body) ->
|
users(post, #{body := Body}) when is_list(Body) ->
|
||||||
lists:foreach(fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
|
lists:foreach(
|
||||||
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules))
|
fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
|
||||||
end, Body),
|
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules))
|
||||||
|
end,
|
||||||
|
Body
|
||||||
|
),
|
||||||
{204}.
|
{204}.
|
||||||
|
|
||||||
clients(get, #{query_string := QueryString}) ->
|
clients(get, #{query_string := QueryString}) ->
|
||||||
Response = emqx_mgmt_api:node_query(node(), QueryString,
|
Response = emqx_mgmt_api:node_query(
|
||||||
?ACL_TABLE, ?ACL_CLIENTID_QSCHEMA, ?QUERY_CLIENTID_FUN),
|
node(),
|
||||||
|
QueryString,
|
||||||
|
?ACL_TABLE,
|
||||||
|
?ACL_CLIENTID_QSCHEMA,
|
||||||
|
?QUERY_CLIENTID_FUN
|
||||||
|
),
|
||||||
emqx_mgmt_util:generate_response(Response);
|
emqx_mgmt_util:generate_response(Response);
|
||||||
clients(post, #{body := Body}) when is_list(Body) ->
|
clients(post, #{body := Body}) when is_list(Body) ->
|
||||||
lists:foreach(fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
|
lists:foreach(
|
||||||
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules))
|
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
|
||||||
end, Body),
|
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules))
|
||||||
|
end,
|
||||||
|
Body
|
||||||
|
),
|
||||||
{204}.
|
{204}.
|
||||||
|
|
||||||
user(get, #{bindings := #{username := Username}}) ->
|
user(get, #{bindings := #{username := Username}}) ->
|
||||||
case emqx_authz_mnesia:get_rules({username, Username}) of
|
case emqx_authz_mnesia:get_rules({username, Username}) of
|
||||||
not_found -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
not_found ->
|
||||||
|
{404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
||||||
{ok, Rules} ->
|
{ok, Rules} ->
|
||||||
{200, #{username => Username,
|
{200, #{
|
||||||
rules => [ #{topic => Topic,
|
username => Username,
|
||||||
action => Action,
|
rules => [
|
||||||
permission => Permission
|
#{
|
||||||
} || {Permission, Action, Topic} <- Rules]}
|
topic => Topic,
|
||||||
}
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
}
|
||||||
|
|| {Permission, Action, Topic} <- Rules
|
||||||
|
]
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
user(put, #{bindings := #{username := Username},
|
user(put, #{
|
||||||
body := #{<<"username">> := Username, <<"rules">> := Rules}}) ->
|
bindings := #{username := Username},
|
||||||
|
body := #{<<"username">> := Username, <<"rules">> := Rules}
|
||||||
|
}) ->
|
||||||
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)),
|
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)),
|
||||||
{204};
|
{204};
|
||||||
user(delete, #{bindings := #{username := Username}}) ->
|
user(delete, #{bindings := #{username := Username}}) ->
|
||||||
|
|
@ -351,17 +474,25 @@ user(delete, #{bindings := #{username := Username}}) ->
|
||||||
|
|
||||||
client(get, #{bindings := #{clientid := ClientID}}) ->
|
client(get, #{bindings := #{clientid := ClientID}}) ->
|
||||||
case emqx_authz_mnesia:get_rules({clientid, ClientID}) of
|
case emqx_authz_mnesia:get_rules({clientid, ClientID}) of
|
||||||
not_found -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
not_found ->
|
||||||
|
{404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
||||||
{ok, Rules} ->
|
{ok, Rules} ->
|
||||||
{200, #{clientid => ClientID,
|
{200, #{
|
||||||
rules => [ #{topic => Topic,
|
clientid => ClientID,
|
||||||
action => Action,
|
rules => [
|
||||||
permission => Permission
|
#{
|
||||||
} || {Permission, Action, Topic} <- Rules]}
|
topic => Topic,
|
||||||
}
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
}
|
||||||
|
|| {Permission, Action, Topic} <- Rules
|
||||||
|
]
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
client(put, #{bindings := #{clientid := ClientID},
|
client(put, #{
|
||||||
body := #{<<"clientid">> := ClientID, <<"rules">> := Rules}}) ->
|
bindings := #{clientid := ClientID},
|
||||||
|
body := #{<<"clientid">> := ClientID, <<"rules">> := Rules}
|
||||||
|
}) ->
|
||||||
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules)),
|
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules)),
|
||||||
{204};
|
{204};
|
||||||
client(delete, #{bindings := #{clientid := ClientID}}) ->
|
client(delete, #{bindings := #{clientid := ClientID}}) ->
|
||||||
|
|
@ -378,11 +509,16 @@ all(get, _) ->
|
||||||
not_found ->
|
not_found ->
|
||||||
{200, #{rules => []}};
|
{200, #{rules => []}};
|
||||||
{ok, Rules} ->
|
{ok, Rules} ->
|
||||||
{200, #{rules => [ #{topic => Topic,
|
{200, #{
|
||||||
action => Action,
|
rules => [
|
||||||
permission => Permission
|
#{
|
||||||
} || {Permission, Action, Topic} <- Rules]}
|
topic => Topic,
|
||||||
}
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
}
|
||||||
|
|| {Permission, Action, Topic} <- Rules
|
||||||
|
]
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
all(post, #{body := #{<<"rules">> := Rules}}) ->
|
all(post, #{body := #{<<"rules">> := Rules}}) ->
|
||||||
emqx_authz_mnesia:store_rules(all, format_rules(Rules)),
|
emqx_authz_mnesia:store_rules(all, format_rules(Rules)),
|
||||||
|
|
@ -394,13 +530,16 @@ purge(delete, _) ->
|
||||||
ok = emqx_authz_mnesia:purge_rules(),
|
ok = emqx_authz_mnesia:purge_rules(),
|
||||||
{204};
|
{204};
|
||||||
[#{<<"enable">> := true}] ->
|
[#{<<"enable">> := true}] ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message =>
|
code => <<"BAD_REQUEST">>,
|
||||||
<<"'built_in_database' type source must be disabled before purge.">>}};
|
message =>
|
||||||
|
<<"'built_in_database' type source must be disabled before purge.">>
|
||||||
|
}};
|
||||||
[] ->
|
[] ->
|
||||||
{404, #{code => <<"BAD_REQUEST">>,
|
{404, #{
|
||||||
message => <<"'built_in_database' type source is not found.">>
|
code => <<"BAD_REQUEST">>,
|
||||||
}}
|
message => <<"'built_in_database' type source is not found.">>
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -408,25 +547,43 @@ purge(delete, _) ->
|
||||||
|
|
||||||
query_username(Tab, {_QString, []}, Continuation, Limit) ->
|
query_username(Tab, {_QString, []}, Continuation, Limit) ->
|
||||||
Ms = emqx_authz_mnesia:list_username_rules(),
|
Ms = emqx_authz_mnesia:list_username_rules(),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_result/1);
|
Tab,
|
||||||
|
Ms,
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_result/1
|
||||||
|
);
|
||||||
query_username(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
|
query_username(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
Ms = emqx_authz_mnesia:list_username_rules(),
|
Ms = emqx_authz_mnesia:list_username_rules(),
|
||||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_result/1).
|
Tab,
|
||||||
|
{Ms, FuzzyFilterFun},
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_result/1
|
||||||
|
).
|
||||||
|
|
||||||
query_clientid(Tab, {_QString, []}, Continuation, Limit) ->
|
query_clientid(Tab, {_QString, []}, Continuation, Limit) ->
|
||||||
Ms = emqx_authz_mnesia:list_clientid_rules(),
|
Ms = emqx_authz_mnesia:list_clientid_rules(),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_result/1);
|
Tab,
|
||||||
|
Ms,
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_result/1
|
||||||
|
);
|
||||||
query_clientid(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
|
query_clientid(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
Ms = emqx_authz_mnesia:list_clientid_rules(),
|
Ms = emqx_authz_mnesia:list_clientid_rules(),
|
||||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_result/1).
|
Tab,
|
||||||
|
{Ms, FuzzyFilterFun},
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_result/1
|
||||||
|
).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Match funcs
|
%% Match funcs
|
||||||
|
|
@ -434,17 +591,23 @@ query_clientid(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
%% Fuzzy username funcs
|
%% Fuzzy username funcs
|
||||||
fuzzy_filter_fun(Fuzzy) ->
|
fuzzy_filter_fun(Fuzzy) ->
|
||||||
fun(MsRaws) when is_list(MsRaws) ->
|
fun(MsRaws) when is_list(MsRaws) ->
|
||||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
lists:filter(
|
||||||
, MsRaws)
|
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||||
|
MsRaws
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run_fuzzy_filter(_, []) ->
|
run_fuzzy_filter(_, []) ->
|
||||||
true;
|
true;
|
||||||
run_fuzzy_filter( E = [{username, Username}, _Rule]
|
run_fuzzy_filter(
|
||||||
, [{username, like, UsernameSubStr} | Fuzzy]) ->
|
E = [{username, Username}, _Rule],
|
||||||
|
[{username, like, UsernameSubStr} | Fuzzy]
|
||||||
|
) ->
|
||||||
binary:match(Username, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy);
|
binary:match(Username, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy);
|
||||||
run_fuzzy_filter( E = [{clientid, ClientId}, _Rule]
|
run_fuzzy_filter(
|
||||||
, [{clientid, like, ClientIdSubStr} | Fuzzy]) ->
|
E = [{clientid, ClientId}, _Rule],
|
||||||
|
[{clientid, like, ClientIdSubStr} | Fuzzy]
|
||||||
|
) ->
|
||||||
binary:match(ClientId, ClientIdSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
binary:match(ClientId, ClientIdSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -452,31 +615,52 @@ run_fuzzy_filter( E = [{clientid, ClientId}, _Rule]
|
||||||
|
|
||||||
%% format rule from api
|
%% format rule from api
|
||||||
format_rules(Rules) when is_list(Rules) ->
|
format_rules(Rules) when is_list(Rules) ->
|
||||||
lists:foldl(fun(#{<<"topic">> := Topic,
|
lists:foldl(
|
||||||
<<"action">> := Action,
|
fun(
|
||||||
<<"permission">> := Permission
|
#{
|
||||||
}, AccIn) when ?PUBSUB(Action)
|
<<"topic">> := Topic,
|
||||||
andalso ?ALLOW_DENY(Permission) ->
|
<<"action">> := Action,
|
||||||
AccIn ++ [{ atom(Permission), atom(Action), Topic }]
|
<<"permission">> := Permission
|
||||||
end, [], Rules).
|
},
|
||||||
|
AccIn
|
||||||
|
) when
|
||||||
|
?PUBSUB(Action) andalso
|
||||||
|
?ALLOW_DENY(Permission)
|
||||||
|
->
|
||||||
|
AccIn ++ [{atom(Permission), atom(Action), Topic}]
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
Rules
|
||||||
|
).
|
||||||
|
|
||||||
%% format result from mnesia tab
|
%% format result from mnesia tab
|
||||||
format_result([{username, Username}, {rules, Rules}]) ->
|
format_result([{username, Username}, {rules, Rules}]) ->
|
||||||
#{username => Username,
|
#{
|
||||||
rules => [ #{topic => Topic,
|
username => Username,
|
||||||
action => Action,
|
rules => [
|
||||||
permission => Permission
|
#{
|
||||||
} || {Permission, Action, Topic} <- Rules]
|
topic => Topic,
|
||||||
};
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
}
|
||||||
|
|| {Permission, Action, Topic} <- Rules
|
||||||
|
]
|
||||||
|
};
|
||||||
format_result([{clientid, ClientID}, {rules, Rules}]) ->
|
format_result([{clientid, ClientID}, {rules, Rules}]) ->
|
||||||
#{clientid => ClientID,
|
#{
|
||||||
rules => [ #{topic => Topic,
|
clientid => ClientID,
|
||||||
action => Action,
|
rules => [
|
||||||
permission => Permission
|
#{
|
||||||
} || {Permission, Action, Topic} <- Rules]
|
topic => Topic,
|
||||||
}.
|
action => Action,
|
||||||
|
permission => Permission
|
||||||
|
}
|
||||||
|
|| {Permission, Action, Topic} <- Rules
|
||||||
|
]
|
||||||
|
}.
|
||||||
atom(B) when is_binary(B) ->
|
atom(B) when is_binary(B) ->
|
||||||
try binary_to_existing_atom(B, utf8)
|
try
|
||||||
|
binary_to_existing_atom(B, utf8)
|
||||||
catch
|
catch
|
||||||
_Error:_Expection -> binary_to_atom(B)
|
_Error:_Expection -> binary_to_atom(B)
|
||||||
end;
|
end;
|
||||||
|
|
@ -488,25 +672,27 @@ atom(A) when is_atom(A) -> A.
|
||||||
|
|
||||||
swagger_with_example({Ref, TypeP}, {_Name, _Type} = Example) ->
|
swagger_with_example({Ref, TypeP}, {_Name, _Type} = Example) ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
case TypeP of
|
case TypeP of
|
||||||
?TYPE_REF -> ref(?MODULE, Ref);
|
?TYPE_REF -> ref(?MODULE, Ref);
|
||||||
?TYPE_ARRAY -> array(ref(?MODULE, Ref))
|
?TYPE_ARRAY -> array(ref(?MODULE, Ref))
|
||||||
end,
|
end,
|
||||||
rules_example(Example)).
|
rules_example(Example)
|
||||||
|
).
|
||||||
|
|
||||||
rules_example({ExampleName, ExampleType}) ->
|
rules_example({ExampleName, ExampleType}) ->
|
||||||
{Summary, Example} =
|
{Summary, Example} =
|
||||||
case ExampleName of
|
case ExampleName of
|
||||||
username -> {<<"Username">>, ?USERNAME_RULES_EXAMPLE};
|
username -> {<<"Username">>, ?USERNAME_RULES_EXAMPLE};
|
||||||
clientid -> {<<"ClientID">>, ?CLIENTID_RULES_EXAMPLE};
|
clientid -> {<<"ClientID">>, ?CLIENTID_RULES_EXAMPLE};
|
||||||
all -> {<<"All">>, ?ALL_RULES_EXAMPLE}
|
all -> {<<"All">>, ?ALL_RULES_EXAMPLE}
|
||||||
end,
|
end,
|
||||||
Value =
|
Value =
|
||||||
case ExampleType of
|
case ExampleType of
|
||||||
?PAGE_QUERY_EXAMPLE -> #{
|
?PAGE_QUERY_EXAMPLE ->
|
||||||
data => [Example],
|
#{
|
||||||
meta => ?META_EXAMPLE
|
data => [Example],
|
||||||
};
|
meta => ?META_EXAMPLE
|
||||||
|
};
|
||||||
?PUT_MAP_EXAMPLE ->
|
?PUT_MAP_EXAMPLE ->
|
||||||
Example;
|
Example;
|
||||||
?POST_ARRAY_EXAMPLE ->
|
?POST_ARRAY_EXAMPLE ->
|
||||||
|
|
@ -515,6 +701,6 @@ rules_example({ExampleName, ExampleType}) ->
|
||||||
#{
|
#{
|
||||||
'password_based:built_in_database' => #{
|
'password_based:built_in_database' => #{
|
||||||
summary => Summary,
|
summary => Summary,
|
||||||
value => Value
|
value => Value
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
|
||||||
|
|
@ -25,58 +25,77 @@
|
||||||
-export([fields/1, authz_sources_types/1]).
|
-export([fields/1, authz_sources_types/1]).
|
||||||
|
|
||||||
fields(http) ->
|
fields(http) ->
|
||||||
authz_common_fields(http)
|
authz_common_fields(http) ++
|
||||||
++ [ {url, fun url/1}
|
[
|
||||||
, {method, #{ type => enum([get, post])
|
{url, fun url/1},
|
||||||
, default => get}}
|
{method, #{
|
||||||
, {headers, fun headers/1}
|
type => enum([get, post]),
|
||||||
, {body, map([{fuzzy, term(), binary()}])}
|
default => get
|
||||||
, {request_timeout, mk_duration("Request timeout", #{default => "30s"})}]
|
}},
|
||||||
++ maps:to_list(maps:without([ base_url
|
{headers, fun headers/1},
|
||||||
, pool_type],
|
{body, map([{fuzzy, term(), binary()}])},
|
||||||
maps:from_list(emqx_connector_http:fields(config))));
|
{request_timeout, mk_duration("Request timeout", #{default => "30s"})}
|
||||||
|
] ++
|
||||||
|
maps:to_list(
|
||||||
|
maps:without(
|
||||||
|
[
|
||||||
|
base_url,
|
||||||
|
pool_type
|
||||||
|
],
|
||||||
|
maps:from_list(emqx_connector_http:fields(config))
|
||||||
|
)
|
||||||
|
);
|
||||||
fields('built_in_database') ->
|
fields('built_in_database') ->
|
||||||
authz_common_fields('built_in_database');
|
authz_common_fields('built_in_database');
|
||||||
fields(mongo_single) ->
|
fields(mongo_single) ->
|
||||||
authz_mongo_common_fields()
|
authz_mongo_common_fields() ++
|
||||||
++ emqx_connector_mongo:fields(single);
|
emqx_connector_mongo:fields(single);
|
||||||
fields(mongo_rs) ->
|
fields(mongo_rs) ->
|
||||||
authz_mongo_common_fields()
|
authz_mongo_common_fields() ++
|
||||||
++ emqx_connector_mongo:fields(rs);
|
emqx_connector_mongo:fields(rs);
|
||||||
fields(mongo_sharded) ->
|
fields(mongo_sharded) ->
|
||||||
authz_mongo_common_fields()
|
authz_mongo_common_fields() ++
|
||||||
++ emqx_connector_mongo:fields(sharded);
|
emqx_connector_mongo:fields(sharded);
|
||||||
fields(mysql) ->
|
fields(mysql) ->
|
||||||
authz_common_fields(mysql)
|
authz_common_fields(mysql) ++
|
||||||
++ [ {query, #{type => binary()}}]
|
[{query, #{type => binary()}}] ++
|
||||||
++ emqx_connector_mysql:fields(config);
|
emqx_connector_mysql:fields(config);
|
||||||
fields(postgresql) ->
|
fields(postgresql) ->
|
||||||
authz_common_fields(postgresql)
|
authz_common_fields(postgresql) ++
|
||||||
++ [ {query, #{type => binary()}}]
|
[{query, #{type => binary()}}] ++
|
||||||
++ proplists:delete(named_queries, emqx_connector_pgsql:fields(config));
|
proplists:delete(named_queries, emqx_connector_pgsql:fields(config));
|
||||||
fields(redis_single) ->
|
fields(redis_single) ->
|
||||||
authz_redis_common_fields()
|
authz_redis_common_fields() ++
|
||||||
++ emqx_connector_redis:fields(single);
|
emqx_connector_redis:fields(single);
|
||||||
fields(redis_sentinel) ->
|
fields(redis_sentinel) ->
|
||||||
authz_redis_common_fields()
|
authz_redis_common_fields() ++
|
||||||
++ emqx_connector_redis:fields(sentinel);
|
emqx_connector_redis:fields(sentinel);
|
||||||
fields(redis_cluster) ->
|
fields(redis_cluster) ->
|
||||||
authz_redis_common_fields()
|
authz_redis_common_fields() ++
|
||||||
++ emqx_connector_redis:fields(cluster);
|
emqx_connector_redis:fields(cluster);
|
||||||
fields(file) ->
|
fields(file) ->
|
||||||
authz_common_fields(file)
|
authz_common_fields(file) ++
|
||||||
++ [ { rules, #{ type => binary()
|
[
|
||||||
, required => true
|
{rules, #{
|
||||||
, example =>
|
type => binary(),
|
||||||
<<"{allow,{username,\"^dashboard?\"},","subscribe,[\"$SYS/#\"]}.\n",
|
required => true,
|
||||||
"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>}}
|
example =>
|
||||||
];
|
<<"{allow,{username,\"^dashboard?\"},", "subscribe,[\"$SYS/#\"]}.\n",
|
||||||
|
"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
|
||||||
|
}}
|
||||||
|
];
|
||||||
fields(position) ->
|
fields(position) ->
|
||||||
[ { position
|
[
|
||||||
, mk( string()
|
{position,
|
||||||
, #{ desc => <<"Where to place the source">>
|
mk(
|
||||||
, required => true
|
string(),
|
||||||
, in => body})}].
|
#{
|
||||||
|
desc => <<"Where to place the source">>,
|
||||||
|
required => true,
|
||||||
|
in => body
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
].
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% http type funcs
|
%% http type funcs
|
||||||
|
|
@ -86,41 +105,52 @@ url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")];
|
||||||
url(required) -> true;
|
url(required) -> true;
|
||||||
url(_) -> undefined.
|
url(_) -> undefined.
|
||||||
|
|
||||||
headers(type) -> map();
|
headers(type) ->
|
||||||
|
map();
|
||||||
headers(converter) ->
|
headers(converter) ->
|
||||||
fun(Headers) ->
|
fun(Headers) ->
|
||||||
maps:merge(default_headers(), transform_header_name(Headers))
|
maps:merge(default_headers(), transform_header_name(Headers))
|
||||||
end;
|
end;
|
||||||
headers(default) -> default_headers();
|
headers(default) ->
|
||||||
headers(_) -> undefined.
|
default_headers();
|
||||||
|
headers(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
%% headers
|
%% headers
|
||||||
default_headers() ->
|
default_headers() ->
|
||||||
maps:put(<<"content-type">>,
|
maps:put(
|
||||||
<<"application/json">>,
|
<<"content-type">>,
|
||||||
default_headers_no_content_type()).
|
<<"application/json">>,
|
||||||
|
default_headers_no_content_type()
|
||||||
|
).
|
||||||
|
|
||||||
default_headers_no_content_type() ->
|
default_headers_no_content_type() ->
|
||||||
#{ <<"accept">> => <<"application/json">>
|
#{
|
||||||
, <<"cache-control">> => <<"no-cache">>
|
<<"accept">> => <<"application/json">>,
|
||||||
, <<"connection">> => <<"keep-alive">>
|
<<"cache-control">> => <<"no-cache">>,
|
||||||
, <<"keep-alive">> => <<"timeout=30, max=1000">>
|
<<"connection">> => <<"keep-alive">>,
|
||||||
}.
|
<<"keep-alive">> => <<"timeout=30, max=1000">>
|
||||||
|
}.
|
||||||
|
|
||||||
transform_header_name(Headers) ->
|
transform_header_name(Headers) ->
|
||||||
maps:fold(fun(K0, V, Acc) ->
|
maps:fold(
|
||||||
K = list_to_binary(string:to_lower(to_list(K0))),
|
fun(K0, V, Acc) ->
|
||||||
maps:put(K, V, Acc)
|
K = list_to_binary(string:to_lower(to_list(K0))),
|
||||||
end, #{}, Headers).
|
maps:put(K, V, Acc)
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Headers
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% MonogDB type funcs
|
%% MonogDB type funcs
|
||||||
|
|
||||||
authz_mongo_common_fields() ->
|
authz_mongo_common_fields() ->
|
||||||
authz_common_fields(mongodb) ++
|
authz_common_fields(mongodb) ++
|
||||||
[ {collection, fun collection/1}
|
[
|
||||||
, {selector, fun selector/1}
|
{collection, fun collection/1},
|
||||||
].
|
{selector, fun selector/1}
|
||||||
|
].
|
||||||
|
|
||||||
collection(type) -> binary();
|
collection(type) -> binary();
|
||||||
collection(_) -> undefined.
|
collection(_) -> undefined.
|
||||||
|
|
@ -133,19 +163,24 @@ selector(_) -> undefined.
|
||||||
|
|
||||||
authz_redis_common_fields() ->
|
authz_redis_common_fields() ->
|
||||||
authz_common_fields(redis) ++
|
authz_common_fields(redis) ++
|
||||||
[ {cmd, #{ type => binary()
|
[
|
||||||
, example => <<"HGETALL mqtt_authz">>}}].
|
{cmd, #{
|
||||||
|
type => binary(),
|
||||||
|
example => <<"HGETALL mqtt_authz">>
|
||||||
|
}}
|
||||||
|
].
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Authz api type funcs
|
%% Authz api type funcs
|
||||||
|
|
||||||
authz_common_fields(Type) when is_atom(Type)->
|
authz_common_fields(Type) when is_atom(Type) ->
|
||||||
[ {enable, fun enable/1}
|
[
|
||||||
, {type, #{ type => enum([Type])
|
{enable, fun enable/1},
|
||||||
, default => Type
|
{type, #{
|
||||||
, in => body
|
type => enum([Type]),
|
||||||
}
|
default => Type,
|
||||||
}
|
in => body
|
||||||
|
}}
|
||||||
].
|
].
|
||||||
|
|
||||||
enable(type) -> boolean();
|
enable(type) -> boolean();
|
||||||
|
|
@ -158,20 +193,25 @@ enable(_) -> undefined.
|
||||||
|
|
||||||
authz_sources_types(Type) ->
|
authz_sources_types(Type) ->
|
||||||
case Type of
|
case Type of
|
||||||
simple -> [mongodb, redis];
|
simple ->
|
||||||
detailed -> [ mongo_single
|
[mongodb, redis];
|
||||||
, mongo_rs
|
detailed ->
|
||||||
, mongo_sharded
|
[
|
||||||
, redis_single
|
mongo_single,
|
||||||
, redis_sentinel
|
mongo_rs,
|
||||||
, redis_cluster]
|
mongo_sharded,
|
||||||
end
|
redis_single,
|
||||||
++
|
redis_sentinel,
|
||||||
[ http
|
redis_cluster
|
||||||
, 'built_in_database'
|
]
|
||||||
, mysql
|
end ++
|
||||||
, postgresql
|
[
|
||||||
, file].
|
http,
|
||||||
|
'built_in_database',
|
||||||
|
mysql,
|
||||||
|
postgresql,
|
||||||
|
file
|
||||||
|
].
|
||||||
|
|
||||||
to_list(A) when is_atom(A) ->
|
to_list(A) when is_atom(A) ->
|
||||||
atom_to_list(A);
|
atom_to_list(A);
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,11 @@
|
||||||
|
|
||||||
-import(hoconsc, [mk/1, ref/2]).
|
-import(hoconsc, [mk/1, ref/2]).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
]).
|
schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([settings/2]).
|
-export([settings/2]).
|
||||||
|
|
||||||
|
|
@ -40,33 +41,42 @@ paths() ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
schema("/authorization/settings") ->
|
schema("/authorization/settings") ->
|
||||||
#{ 'operationId' => settings
|
#{
|
||||||
, get =>
|
'operationId' => settings,
|
||||||
#{ description => <<"Get authorization settings">>
|
get =>
|
||||||
, responses =>
|
#{
|
||||||
#{200 => ref_authz_schema()}
|
description => <<"Get authorization settings">>,
|
||||||
|
responses =>
|
||||||
|
#{200 => ref_authz_schema()}
|
||||||
|
},
|
||||||
|
put =>
|
||||||
|
#{
|
||||||
|
description => <<"Update authorization settings">>,
|
||||||
|
'requestBody' => ref_authz_schema(),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => ref_authz_schema(),
|
||||||
|
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, put =>
|
}.
|
||||||
#{ description => <<"Update authorization settings">>
|
|
||||||
, 'requestBody' => ref_authz_schema()
|
|
||||||
, responses =>
|
|
||||||
#{ 200 => ref_authz_schema()
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)}
|
|
||||||
}
|
|
||||||
}.
|
|
||||||
|
|
||||||
ref_authz_schema() ->
|
ref_authz_schema() ->
|
||||||
proplists:delete(sources, emqx_conf_schema:fields("authorization")).
|
proplists:delete(sources, emqx_conf_schema:fields("authorization")).
|
||||||
|
|
||||||
settings(get, _Params) ->
|
settings(get, _Params) ->
|
||||||
{200, authorization_settings()};
|
{200, authorization_settings()};
|
||||||
|
settings(put, #{
|
||||||
settings(put, #{body := #{<<"no_match">> := NoMatch,
|
body := #{
|
||||||
<<"deny_action">> := DenyAction,
|
<<"no_match">> := NoMatch,
|
||||||
<<"cache">> := Cache}}) ->
|
<<"deny_action">> := DenyAction,
|
||||||
|
<<"cache">> := Cache
|
||||||
|
}
|
||||||
|
}) ->
|
||||||
{ok, _} = emqx_authz_utils:update_config([authorization, no_match], NoMatch),
|
{ok, _} = emqx_authz_utils:update_config([authorization, no_match], NoMatch),
|
||||||
{ok, _} = emqx_authz_utils:update_config(
|
{ok, _} = emqx_authz_utils:update_config(
|
||||||
[authorization, deny_action], DenyAction),
|
[authorization, deny_action], DenyAction
|
||||||
|
),
|
||||||
{ok, _} = emqx_authz_utils:update_config([authorization, cache], Cache),
|
{ok, _} = emqx_authz_utils:update_config([authorization, cache], Cache),
|
||||||
ok = emqx_authz_cache:drain_cache(),
|
ok = emqx_authz_cache:drain_cache(),
|
||||||
{200, authorization_settings()}.
|
{200, authorization_settings()}.
|
||||||
|
|
|
||||||
|
|
@ -29,167 +29,226 @@
|
||||||
|
|
||||||
-define(API_SCHEMA_MODULE, emqx_authz_api_schema).
|
-define(API_SCHEMA_MODULE, emqx_authz_api_schema).
|
||||||
|
|
||||||
-export([ get_raw_sources/0
|
-export([
|
||||||
, get_raw_source/1
|
get_raw_sources/0,
|
||||||
, source_status/2
|
get_raw_source/1,
|
||||||
, lookup_from_local_node/1
|
source_status/2,
|
||||||
, lookup_from_all_nodes/1
|
lookup_from_local_node/1,
|
||||||
]).
|
lookup_from_all_nodes/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
]).
|
schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ sources/2
|
-export([
|
||||||
, source/2
|
sources/2,
|
||||||
, move_source/2
|
source/2,
|
||||||
]).
|
move_source/2
|
||||||
|
]).
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[ "/authorization/sources"
|
[
|
||||||
, "/authorization/sources/:type"
|
"/authorization/sources",
|
||||||
, "/authorization/sources/:type/status"
|
"/authorization/sources/:type",
|
||||||
, "/authorization/sources/:type/move"].
|
"/authorization/sources/:type/status",
|
||||||
|
"/authorization/sources/:type/move"
|
||||||
|
].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Schema for each URI
|
%% Schema for each URI
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
schema("/authorization/sources") ->
|
schema("/authorization/sources") ->
|
||||||
#{ 'operationId' => sources
|
#{
|
||||||
, get =>
|
'operationId' => sources,
|
||||||
#{ description => <<"List all authorization sources">>
|
get =>
|
||||||
, responses =>
|
#{
|
||||||
#{ 200 => mk( array(hoconsc:union(authz_sources_type_refs()))
|
description => <<"List all authorization sources">>,
|
||||||
, #{desc => <<"Authorization source">>})
|
responses =>
|
||||||
}
|
#{
|
||||||
|
200 => mk(
|
||||||
|
array(hoconsc:union(authz_sources_type_refs())),
|
||||||
|
#{desc => <<"Authorization source">>}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
post =>
|
||||||
|
#{
|
||||||
|
description => <<"Add a new source">>,
|
||||||
|
'requestBody' => mk(
|
||||||
|
hoconsc:union(authz_sources_type_refs()),
|
||||||
|
#{desc => <<"Source config">>}
|
||||||
|
),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Authorization source created successfully">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST],
|
||||||
|
<<"Bad Request">>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, post =>
|
};
|
||||||
#{ description => <<"Add a new source">>
|
|
||||||
, 'requestBody' => mk( hoconsc:union(authz_sources_type_refs())
|
|
||||||
, #{desc => <<"Source config">>})
|
|
||||||
, responses =>
|
|
||||||
#{ 204 => <<"Authorization source created successfully">>
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST],
|
|
||||||
<<"Bad Request">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
schema("/authorization/sources/:type") ->
|
schema("/authorization/sources/:type") ->
|
||||||
#{ 'operationId' => source
|
#{
|
||||||
, get =>
|
'operationId' => source,
|
||||||
#{ description => <<"Get a authorization source">>
|
get =>
|
||||||
, parameters => parameters_field()
|
#{
|
||||||
, responses =>
|
description => <<"Get a authorization source">>,
|
||||||
#{ 200 => mk( hoconsc:union(authz_sources_type_refs())
|
parameters => parameters_field(),
|
||||||
, #{desc => <<"Authorization source">>})
|
responses =>
|
||||||
, 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
|
#{
|
||||||
}
|
200 => mk(
|
||||||
|
hoconsc:union(authz_sources_type_refs()),
|
||||||
|
#{desc => <<"Authorization source">>}
|
||||||
|
),
|
||||||
|
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
put =>
|
||||||
|
#{
|
||||||
|
description => <<"Update source">>,
|
||||||
|
parameters => parameters_field(),
|
||||||
|
'requestBody' => mk(hoconsc:union(authz_sources_type_refs())),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Authorization source updated successfully">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete =>
|
||||||
|
#{
|
||||||
|
description => <<"Delete source">>,
|
||||||
|
parameters => parameters_field(),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
204 => <<"Deleted successfully">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, put =>
|
};
|
||||||
#{ description => <<"Update source">>
|
|
||||||
, parameters => parameters_field()
|
|
||||||
, 'requestBody' => mk(hoconsc:union(authz_sources_type_refs()))
|
|
||||||
, responses =>
|
|
||||||
#{ 204 => <<"Authorization source updated successfully">>
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, delete =>
|
|
||||||
#{ description => <<"Delete source">>
|
|
||||||
, parameters => parameters_field()
|
|
||||||
, responses =>
|
|
||||||
#{ 204 => <<"Deleted successfully">>
|
|
||||||
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
schema("/authorization/sources/:type/status") ->
|
schema("/authorization/sources/:type/status") ->
|
||||||
#{ 'operationId' => source_status
|
#{
|
||||||
, get =>
|
'operationId' => source_status,
|
||||||
#{ description => <<"Get a authorization source">>
|
get =>
|
||||||
, parameters => parameters_field()
|
#{
|
||||||
, responses =>
|
description => <<"Get a authorization source">>,
|
||||||
#{ 200 => emqx_dashboard_swagger:schema_with_examples(
|
parameters => parameters_field(),
|
||||||
hoconsc:ref(emqx_authn_schema, "metrics_status_fields"),
|
responses =>
|
||||||
status_metrics_example())
|
#{
|
||||||
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad request">>)
|
200 => emqx_dashboard_swagger:schema_with_examples(
|
||||||
, 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
|
hoconsc:ref(emqx_authn_schema, "metrics_status_fields"),
|
||||||
}
|
status_metrics_example()
|
||||||
|
),
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad request">>
|
||||||
|
),
|
||||||
|
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
schema("/authorization/sources/:type/move") ->
|
schema("/authorization/sources/:type/move") ->
|
||||||
#{ 'operationId' => move_source
|
#{
|
||||||
, post =>
|
'operationId' => move_source,
|
||||||
#{ description => <<"Change the order of sources">>
|
post =>
|
||||||
, parameters => parameters_field()
|
#{
|
||||||
, 'requestBody' =>
|
description => <<"Change the order of sources">>,
|
||||||
emqx_dashboard_swagger:schema_with_examples(
|
parameters => parameters_field(),
|
||||||
ref(?API_SCHEMA_MODULE, position),
|
'requestBody' =>
|
||||||
position_example())
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
, responses =>
|
ref(?API_SCHEMA_MODULE, position),
|
||||||
#{ 204 => <<"No Content">>
|
position_example()
|
||||||
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
|
),
|
||||||
, 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
|
responses =>
|
||||||
}
|
#{
|
||||||
|
204 => <<"No Content">>,
|
||||||
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
[?BAD_REQUEST], <<"Bad Request">>
|
||||||
|
),
|
||||||
|
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Operation functions
|
%% Operation functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
sources(Method, #{bindings := #{type := Type} = Bindings } = Req)
|
sources(Method, #{bindings := #{type := Type} = Bindings} = Req) when
|
||||||
when is_atom(Type) ->
|
is_atom(Type)
|
||||||
|
->
|
||||||
sources(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
|
sources(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
|
||||||
sources(get, _) ->
|
sources(get, _) ->
|
||||||
Sources = lists:foldl(fun (#{<<"type">> := <<"file">>,
|
Sources = lists:foldl(
|
||||||
<<"enable">> := Enable, <<"path">> := Path}, AccIn) ->
|
fun
|
||||||
case file:read_file(Path) of
|
(
|
||||||
{ok, Rules} ->
|
#{
|
||||||
lists:append(AccIn, [#{type => file,
|
<<"type">> := <<"file">>,
|
||||||
enable => Enable,
|
<<"enable">> := Enable,
|
||||||
rules => Rules
|
<<"path">> := Path
|
||||||
}]);
|
},
|
||||||
{error, _} ->
|
AccIn
|
||||||
lists:append(AccIn, [#{type => file,
|
) ->
|
||||||
enable => Enable,
|
case file:read_file(Path) of
|
||||||
rules => <<"">>
|
{ok, Rules} ->
|
||||||
}])
|
lists:append(AccIn, [
|
||||||
end;
|
#{
|
||||||
(Source, AccIn) ->
|
type => file,
|
||||||
lists:append(AccIn, [read_certs(Source)])
|
enable => Enable,
|
||||||
end, [], get_raw_sources()),
|
rules => Rules
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
{error, _} ->
|
||||||
|
lists:append(AccIn, [
|
||||||
|
#{
|
||||||
|
type => file,
|
||||||
|
enable => Enable,
|
||||||
|
rules => <<"">>
|
||||||
|
}
|
||||||
|
])
|
||||||
|
end;
|
||||||
|
(Source, AccIn) ->
|
||||||
|
lists:append(AccIn, [read_certs(Source)])
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
get_raw_sources()
|
||||||
|
),
|
||||||
{200, #{sources => Sources}};
|
{200, #{sources => Sources}};
|
||||||
sources(post, #{body := #{<<"type">> := <<"file">>} = Body}) ->
|
sources(post, #{body := #{<<"type">> := <<"file">>} = Body}) ->
|
||||||
create_authz_file(Body);
|
create_authz_file(Body);
|
||||||
sources(post, #{body := Body}) ->
|
sources(post, #{body := Body}) ->
|
||||||
update_config(?CMD_PREPEND, Body).
|
update_config(?CMD_PREPEND, Body).
|
||||||
|
|
||||||
source(Method, #{bindings := #{type := Type} = Bindings } = Req)
|
source(Method, #{bindings := #{type := Type} = Bindings} = Req) when
|
||||||
when is_atom(Type) ->
|
is_atom(Type)
|
||||||
|
->
|
||||||
source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
|
source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
|
||||||
source(get, #{bindings := #{type := Type}}) ->
|
source(get, #{bindings := #{type := Type}}) ->
|
||||||
case get_raw_source(Type) of
|
case get_raw_source(Type) of
|
||||||
[] -> {404, #{message => <<"Not found ", Type/binary>>}};
|
[] ->
|
||||||
|
{404, #{message => <<"Not found ", Type/binary>>}};
|
||||||
[#{<<"type">> := <<"file">>, <<"enable">> := Enable, <<"path">> := Path}] ->
|
[#{<<"type">> := <<"file">>, <<"enable">> := Enable, <<"path">> := Path}] ->
|
||||||
case file:read_file(Path) of
|
case file:read_file(Path) of
|
||||||
{ok, Rules} ->
|
{ok, Rules} ->
|
||||||
{200, #{type => file,
|
{200, #{
|
||||||
enable => Enable,
|
type => file,
|
||||||
rules => Rules
|
enable => Enable,
|
||||||
}
|
rules => Rules
|
||||||
};
|
}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{500, #{code => <<"INTERNAL_ERROR">>,
|
{500, #{
|
||||||
message => bin(Reason)}}
|
code => <<"INTERNAL_ERROR">>,
|
||||||
|
message => bin(Reason)
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
[Source] -> {200, read_certs(Source)}
|
[Source] ->
|
||||||
|
{200, read_certs(Source)}
|
||||||
end;
|
end;
|
||||||
source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>} = Body}) ->
|
source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>} = Body}) ->
|
||||||
update_authz_file(Body);
|
update_authz_file(Body);
|
||||||
|
|
@ -201,44 +260,61 @@ source(delete, #{bindings := #{type := Type}}) ->
|
||||||
source_status(get, #{bindings := #{type := Type}}) ->
|
source_status(get, #{bindings := #{type := Type}}) ->
|
||||||
BinType = atom_to_binary(Type, utf8),
|
BinType = atom_to_binary(Type, utf8),
|
||||||
case get_raw_source(BinType) of
|
case get_raw_source(BinType) of
|
||||||
[] -> {404, #{code => <<"NOT_FOUND">>,
|
[] ->
|
||||||
message => <<"Not found", BinType/binary>>}};
|
{404, #{
|
||||||
|
code => <<"NOT_FOUND">>,
|
||||||
|
message => <<"Not found", BinType/binary>>
|
||||||
|
}};
|
||||||
[#{<<"type">> := <<"file">>}] ->
|
[#{<<"type">> := <<"file">>}] ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => <<"Not Support Status">>}};
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => <<"Not Support Status">>
|
||||||
|
}};
|
||||||
[_] ->
|
[_] ->
|
||||||
case emqx_authz:lookup(Type) of
|
case emqx_authz:lookup(Type) of
|
||||||
#{annotations := #{id := ResourceId }} -> lookup_from_all_nodes(ResourceId);
|
#{annotations := #{id := ResourceId}} -> lookup_from_all_nodes(ResourceId);
|
||||||
_ -> {400, #{code => <<"BAD_REQUEST">>, message => <<"Resource Disable">>}}
|
_ -> {400, #{code => <<"BAD_REQUEST">>, message => <<"Resource Disable">>}}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
move_source(Method, #{bindings := #{type := Type} = Bindings } = Req)
|
move_source(Method, #{bindings := #{type := Type} = Bindings} = Req) when
|
||||||
when is_atom(Type) ->
|
is_atom(Type)
|
||||||
|
->
|
||||||
move_source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
|
move_source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
|
||||||
move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) ->
|
move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) ->
|
||||||
case parse_position(Position) of
|
case parse_position(Position) of
|
||||||
{ok, NPosition} ->
|
{ok, NPosition} ->
|
||||||
try emqx_authz:move(Type, NPosition) of
|
try emqx_authz:move(Type, NPosition) of
|
||||||
{ok, _} -> {204};
|
{ok, _} ->
|
||||||
|
{204};
|
||||||
{error, {not_found_source, _Type}} ->
|
{error, {not_found_source, _Type}} ->
|
||||||
{404, #{code => <<"NOT_FOUND">>,
|
{404, #{
|
||||||
message => <<"source ", Type/binary, " not found">>}};
|
code => <<"NOT_FOUND">>,
|
||||||
|
message => <<"source ", Type/binary, " not found">>
|
||||||
|
}};
|
||||||
{error, {emqx_conf_schema, _}} ->
|
{error, {emqx_conf_schema, _}} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => <<"BAD_SCHEMA">>}};
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => <<"BAD_SCHEMA">>
|
||||||
|
}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => bin(Reason)}}
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => bin(Reason)
|
||||||
|
}}
|
||||||
catch
|
catch
|
||||||
error : {unknown_authz_source_type, Unknown} ->
|
error:{unknown_authz_source_type, Unknown} ->
|
||||||
NUnknown = bin(Unknown),
|
NUnknown = bin(Unknown),
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => <<"Unknown authz Source Type: ", NUnknown/binary>>}}
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => <<"Unknown authz Source Type: ", NUnknown/binary>>
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => bin(Reason)}}
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => bin(Reason)
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -249,46 +325,53 @@ lookup_from_local_node(ResourceId) ->
|
||||||
NodeId = node(self()),
|
NodeId = node(self()),
|
||||||
case emqx_resource:get_instance(ResourceId) of
|
case emqx_resource:get_instance(ResourceId) of
|
||||||
{error, not_found} -> {error, {NodeId, not_found_resource}};
|
{error, not_found} -> {error, {NodeId, not_found_resource}};
|
||||||
{ok, _, #{ status := Status, metrics := Metrics }} ->
|
{ok, _, #{status := Status, metrics := Metrics}} -> {ok, {NodeId, Status, Metrics}}
|
||||||
{ok, {NodeId, Status, Metrics}}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
lookup_from_all_nodes(ResourceId) ->
|
lookup_from_all_nodes(ResourceId) ->
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
Nodes = mria_mnesia:running_nodes(),
|
||||||
case is_ok(emqx_authz_proto_v1:lookup_from_all_nodes(Nodes, ResourceId)) of
|
case is_ok(emqx_authz_proto_v1:lookup_from_all_nodes(Nodes, ResourceId)) of
|
||||||
{ok, ResList} ->
|
{ok, ResList} ->
|
||||||
{StatusMap, MetricsMap, _} = make_result_map(ResList),
|
{StatusMap, MetricsMap, _} = make_result_map(ResList),
|
||||||
AggregateStatus = aggregate_status(maps:values(StatusMap)),
|
AggregateStatus = aggregate_status(maps:values(StatusMap)),
|
||||||
AggregateMetrics = aggregate_metrics(maps:values(MetricsMap)),
|
AggregateMetrics = aggregate_metrics(maps:values(MetricsMap)),
|
||||||
Fun = fun (_, V1) -> restructure_map(V1) end,
|
Fun = fun(_, V1) -> restructure_map(V1) end,
|
||||||
MKMap = fun (Name) -> fun ({Key, Val}) -> #{ node => Key, Name => Val } end end,
|
MKMap = fun(Name) -> fun({Key, Val}) -> #{node => Key, Name => Val} end end,
|
||||||
HelpFun = fun (M, Name) -> lists:map(MKMap(Name), maps:to_list(M)) end,
|
HelpFun = fun(M, Name) -> lists:map(MKMap(Name), maps:to_list(M)) end,
|
||||||
case AggregateStatus of
|
case AggregateStatus of
|
||||||
empty_metrics_and_status -> {400, #{code => <<"BAD_REQUEST">>,
|
empty_metrics_and_status ->
|
||||||
message => <<"Resource Not Support Status">>}};
|
{400, #{
|
||||||
_ -> {200, #{node_status => HelpFun(StatusMap, status),
|
code => <<"BAD_REQUEST">>,
|
||||||
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
|
message => <<"Resource Not Support Status">>
|
||||||
status => AggregateStatus,
|
}};
|
||||||
metrics => restructure_map(AggregateMetrics)
|
_ ->
|
||||||
}
|
{200, #{
|
||||||
}
|
node_status => HelpFun(StatusMap, status),
|
||||||
end;
|
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
|
||||||
|
status => AggregateStatus,
|
||||||
|
metrics => restructure_map(AggregateMetrics)
|
||||||
|
}}
|
||||||
|
end;
|
||||||
{error, ErrL} ->
|
{error, ErrL} ->
|
||||||
{500, #{code => <<"INTERNAL_ERROR">>,
|
{500, #{
|
||||||
message => bin_t(io_lib:format("~p", [ErrL]))}}
|
code => <<"INTERNAL_ERROR">>,
|
||||||
|
message => bin_t(io_lib:format("~p", [ErrL]))
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
aggregate_status([]) -> empty_metrics_and_status;
|
aggregate_status([]) ->
|
||||||
|
empty_metrics_and_status;
|
||||||
aggregate_status(AllStatus) ->
|
aggregate_status(AllStatus) ->
|
||||||
Head = fun ([A | _]) -> A end,
|
Head = fun([A | _]) -> A end,
|
||||||
HeadVal = Head(AllStatus),
|
HeadVal = Head(AllStatus),
|
||||||
AllRes = lists:all(fun (Val) -> Val == HeadVal end, AllStatus),
|
AllRes = lists:all(fun(Val) -> Val == HeadVal end, AllStatus),
|
||||||
case AllRes of
|
case AllRes of
|
||||||
true -> HeadVal;
|
true -> HeadVal;
|
||||||
false -> inconsistent
|
false -> inconsistent
|
||||||
end.
|
end.
|
||||||
|
|
||||||
aggregate_metrics([]) -> empty_metrics_and_status;
|
aggregate_metrics([]) ->
|
||||||
|
empty_metrics_and_status;
|
||||||
aggregate_metrics([HeadMetrics | AllMetrics]) ->
|
aggregate_metrics([HeadMetrics | AllMetrics]) ->
|
||||||
CombinerFun =
|
CombinerFun =
|
||||||
fun ComFun(Val1, Val2) ->
|
fun ComFun(Val1, Val2) ->
|
||||||
|
|
@ -297,8 +380,9 @@ aggregate_metrics([HeadMetrics | AllMetrics]) ->
|
||||||
false -> Val1 + Val2
|
false -> Val1 + Val2
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
Fun = fun (ElemMap, AccMap) ->
|
Fun = fun(ElemMap, AccMap) ->
|
||||||
emqx_map_lib:merge_with(CombinerFun, ElemMap, AccMap) end,
|
emqx_map_lib:merge_with(CombinerFun, ElemMap, AccMap)
|
||||||
|
end,
|
||||||
lists:foldl(Fun, HeadMetrics, AllMetrics).
|
lists:foldl(Fun, HeadMetrics, AllMetrics).
|
||||||
|
|
||||||
make_result_map(ResList) ->
|
make_result_map(ResList) ->
|
||||||
|
|
@ -306,39 +390,45 @@ make_result_map(ResList) ->
|
||||||
fun(Elem, {StatusMap, MetricsMap, ErrorMap}) ->
|
fun(Elem, {StatusMap, MetricsMap, ErrorMap}) ->
|
||||||
case Elem of
|
case Elem of
|
||||||
{ok, {NodeId, Status, Metrics}} ->
|
{ok, {NodeId, Status, Metrics}} ->
|
||||||
{maps:put(NodeId, Status, StatusMap),
|
{
|
||||||
maps:put(NodeId, Metrics, MetricsMap),
|
maps:put(NodeId, Status, StatusMap),
|
||||||
ErrorMap
|
maps:put(NodeId, Metrics, MetricsMap),
|
||||||
|
ErrorMap
|
||||||
};
|
};
|
||||||
{error, {NodeId, Reason}} ->
|
{error, {NodeId, Reason}} ->
|
||||||
{StatusMap,
|
{StatusMap, MetricsMap, maps:put(NodeId, Reason, ErrorMap)}
|
||||||
MetricsMap,
|
|
||||||
maps:put(NodeId, Reason, ErrorMap)
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
lists:foldl(Fun, {maps:new(), maps:new(), maps:new()}, ResList).
|
lists:foldl(Fun, {maps:new(), maps:new(), maps:new()}, ResList).
|
||||||
|
|
||||||
restructure_map(#{counters := #{failed := Failed, matched := Match, success := Succ},
|
restructure_map(#{
|
||||||
rate := #{matched := #{current := Rate, last5m := Rate5m, max := RateMax}
|
counters := #{failed := Failed, matched := Match, success := Succ},
|
||||||
}
|
rate := #{matched := #{current := Rate, last5m := Rate5m, max := RateMax}}
|
||||||
}
|
}) ->
|
||||||
) ->
|
#{
|
||||||
#{matched => Match,
|
matched => Match,
|
||||||
success => Succ,
|
success => Succ,
|
||||||
failed => Failed,
|
failed => Failed,
|
||||||
rate => Rate,
|
rate => Rate,
|
||||||
rate_last5m => Rate5m,
|
rate_last5m => Rate5m,
|
||||||
rate_max => RateMax
|
rate_max => RateMax
|
||||||
};
|
};
|
||||||
restructure_map(Error) ->
|
restructure_map(Error) ->
|
||||||
Error.
|
Error.
|
||||||
|
|
||||||
bin_t(S) when is_list(S) ->
|
bin_t(S) when is_list(S) ->
|
||||||
list_to_binary(S).
|
list_to_binary(S).
|
||||||
|
|
||||||
is_ok(ResL) ->
|
is_ok(ResL) ->
|
||||||
case lists:filter(fun({ok, _}) -> false; (_) -> true end, ResL) of
|
case
|
||||||
|
lists:filter(
|
||||||
|
fun
|
||||||
|
({ok, _}) -> false;
|
||||||
|
(_) -> true
|
||||||
|
end,
|
||||||
|
ResL
|
||||||
|
)
|
||||||
|
of
|
||||||
[] -> {ok, [Res || {ok, Res} <- ResL]};
|
[] -> {ok, [Res || {ok, Res} <- ResL]};
|
||||||
ErrL -> {error, ErrL}
|
ErrL -> {error, ErrL}
|
||||||
end.
|
end.
|
||||||
|
|
@ -352,43 +442,60 @@ get_raw_sources() ->
|
||||||
merge_default_headers(Sources).
|
merge_default_headers(Sources).
|
||||||
|
|
||||||
merge_default_headers(Sources) ->
|
merge_default_headers(Sources) ->
|
||||||
lists:map(fun(Source) ->
|
lists:map(
|
||||||
case maps:find(<<"headers">>, Source) of
|
fun(Source) ->
|
||||||
{ok, Headers} ->
|
case maps:find(<<"headers">>, Source) of
|
||||||
NewHeaders =
|
{ok, Headers} ->
|
||||||
case Source of
|
NewHeaders =
|
||||||
#{<<"method">> := <<"get">>} ->
|
case Source of
|
||||||
(emqx_authz_schema:headers_no_content_type(converter))(Headers);
|
#{<<"method">> := <<"get">>} ->
|
||||||
#{<<"method">> := <<"post">>} ->
|
(emqx_authz_schema:headers_no_content_type(converter))(Headers);
|
||||||
(emqx_authz_schema:headers(converter))(Headers);
|
#{<<"method">> := <<"post">>} ->
|
||||||
_ -> Headers
|
(emqx_authz_schema:headers(converter))(Headers);
|
||||||
end,
|
_ ->
|
||||||
Source#{<<"headers">> => NewHeaders};
|
Headers
|
||||||
error -> Source
|
end,
|
||||||
end
|
Source#{<<"headers">> => NewHeaders};
|
||||||
end, Sources).
|
error ->
|
||||||
|
Source
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Sources
|
||||||
|
).
|
||||||
|
|
||||||
get_raw_source(Type) ->
|
get_raw_source(Type) ->
|
||||||
lists:filter(fun (#{<<"type">> := T}) ->
|
lists:filter(
|
||||||
T =:= Type
|
fun(#{<<"type">> := T}) ->
|
||||||
end, get_raw_sources()).
|
T =:= Type
|
||||||
|
end,
|
||||||
|
get_raw_sources()
|
||||||
|
).
|
||||||
|
|
||||||
update_config(Cmd, Sources) ->
|
update_config(Cmd, Sources) ->
|
||||||
case emqx_authz:update(Cmd, Sources) of
|
case emqx_authz:update(Cmd, Sources) of
|
||||||
{ok, _} -> {204};
|
{ok, _} ->
|
||||||
|
{204};
|
||||||
{error, {pre_config_update, emqx_authz, Reason}} ->
|
{error, {pre_config_update, emqx_authz, Reason}} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => bin(Reason)}};
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => bin(Reason)
|
||||||
|
}};
|
||||||
{error, {post_config_update, emqx_authz, Reason}} ->
|
{error, {post_config_update, emqx_authz, Reason}} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => bin(Reason)}};
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => bin(Reason)
|
||||||
|
}};
|
||||||
%% TODO: The `Reason` may cann't be trans to json term. (i.e. ecpool start failed)
|
%% TODO: The `Reason` may cann't be trans to json term. (i.e. ecpool start failed)
|
||||||
{error, {emqx_conf_schema, _}} ->
|
{error, {emqx_conf_schema, _}} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => <<"BAD_SCHEMA">>}};
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => <<"BAD_SCHEMA">>
|
||||||
|
}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => <<"BAD_REQUEST">>,
|
{400, #{
|
||||||
message => bin(Reason)}}
|
code => <<"BAD_REQUEST">>,
|
||||||
|
message => bin(Reason)
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
read_certs(#{<<"ssl">> := SSL} = Source) ->
|
read_certs(#{<<"ssl">> := SSL} = Source) ->
|
||||||
|
|
@ -399,12 +506,16 @@ read_certs(#{<<"ssl">> := SSL} = Source) ->
|
||||||
{ok, NewSSL} ->
|
{ok, NewSSL} ->
|
||||||
Source#{<<"ssl">> => NewSSL}
|
Source#{<<"ssl">> => NewSSL}
|
||||||
end;
|
end;
|
||||||
read_certs(Source) -> Source.
|
read_certs(Source) ->
|
||||||
|
Source.
|
||||||
|
|
||||||
parameters_field() ->
|
parameters_field() ->
|
||||||
[ {type, mk( enum(?API_SCHEMA_MODULE:authz_sources_types(simple))
|
[
|
||||||
, #{in => path, desc => <<"Authorization type">>})
|
{type,
|
||||||
}
|
mk(
|
||||||
|
enum(?API_SCHEMA_MODULE:authz_sources_types(simple)),
|
||||||
|
#{in => path, desc => <<"Authorization type">>}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
parse_position(<<"front">>) ->
|
parse_position(<<"front">>) ->
|
||||||
|
|
@ -423,50 +534,68 @@ parse_position(_) ->
|
||||||
{error, <<"Invalid parameter. Unknow position">>}.
|
{error, <<"Invalid parameter. Unknow position">>}.
|
||||||
|
|
||||||
position_example() ->
|
position_example() ->
|
||||||
#{ front =>
|
#{
|
||||||
#{ summary => <<"front example">>
|
front =>
|
||||||
, value => #{<<"position">> => <<"front">>}}
|
#{
|
||||||
, rear =>
|
summary => <<"front example">>,
|
||||||
#{ summary => <<"rear example">>
|
value => #{<<"position">> => <<"front">>}
|
||||||
, value => #{<<"position">> => <<"rear">>}}
|
},
|
||||||
, relative_before =>
|
rear =>
|
||||||
#{ summary => <<"relative example">>
|
#{
|
||||||
, value => #{<<"position">> => <<"before:file">>}}
|
summary => <<"rear example">>,
|
||||||
, relative_after =>
|
value => #{<<"position">> => <<"rear">>}
|
||||||
#{ summary => <<"relative example">>
|
},
|
||||||
, value => #{<<"position">> => <<"after:file">>}}
|
relative_before =>
|
||||||
}.
|
#{
|
||||||
|
summary => <<"relative example">>,
|
||||||
|
value => #{<<"position">> => <<"before:file">>}
|
||||||
|
},
|
||||||
|
relative_after =>
|
||||||
|
#{
|
||||||
|
summary => <<"relative example">>,
|
||||||
|
value => #{<<"position">> => <<"after:file">>}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
authz_sources_type_refs() ->
|
authz_sources_type_refs() ->
|
||||||
[ref(?API_SCHEMA_MODULE, Type)
|
[
|
||||||
|| Type <- emqx_authz_api_schema:authz_sources_types(detailed)].
|
ref(?API_SCHEMA_MODULE, Type)
|
||||||
|
|| Type <- emqx_authz_api_schema:authz_sources_types(detailed)
|
||||||
|
].
|
||||||
|
|
||||||
bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])).
|
bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])).
|
||||||
|
|
||||||
status_metrics_example() ->
|
status_metrics_example() ->
|
||||||
#{ metrics => #{ matched => 0,
|
#{
|
||||||
success => 0,
|
metrics => #{
|
||||||
failed => 0,
|
matched => 0,
|
||||||
rate => 0.0,
|
success => 0,
|
||||||
rate_last5m => 0.0,
|
failed => 0,
|
||||||
rate_max => 0.0
|
rate => 0.0,
|
||||||
},
|
rate_last5m => 0.0,
|
||||||
node_metrics => [ #{node => node(),
|
rate_max => 0.0
|
||||||
metrics => #{ matched => 0,
|
},
|
||||||
success => 0,
|
node_metrics => [
|
||||||
failed => 0,
|
#{
|
||||||
rate => 0.0,
|
node => node(),
|
||||||
rate_last5m => 0.0,
|
metrics => #{
|
||||||
rate_max => 0.0
|
matched => 0,
|
||||||
}
|
success => 0,
|
||||||
}
|
failed => 0,
|
||||||
],
|
rate => 0.0,
|
||||||
status => connected,
|
rate_last5m => 0.0,
|
||||||
node_status => [ #{node => node(),
|
rate_max => 0.0
|
||||||
status => connected
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}.
|
status => connected,
|
||||||
|
node_status => [
|
||||||
|
#{
|
||||||
|
node => node(),
|
||||||
|
status => connected
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}.
|
||||||
|
|
||||||
create_authz_file(Body) ->
|
create_authz_file(Body) ->
|
||||||
do_update_authz_file(?CMD_PREPEND, Body).
|
do_update_authz_file(?CMD_PREPEND, Body).
|
||||||
|
|
@ -476,4 +605,4 @@ update_authz_file(Body) ->
|
||||||
|
|
||||||
do_update_authz_file(Cmd, Body) ->
|
do_update_authz_file(Cmd, Body) ->
|
||||||
%% API update will placed in `authz` subdirectory inside EMQX's `data_dir`
|
%% API update will placed in `authz` subdirectory inside EMQX's `data_dir`
|
||||||
update_config(Cmd, Body).
|
update_config(Cmd, Body).
|
||||||
|
|
|
||||||
|
|
@ -27,28 +27,32 @@
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
%% APIs
|
%% APIs
|
||||||
-export([ description/0
|
-export([
|
||||||
, init/1
|
description/0,
|
||||||
, destroy/1
|
init/1,
|
||||||
, authorize/4
|
destroy/1,
|
||||||
]).
|
authorize/4
|
||||||
|
]).
|
||||||
|
|
||||||
description() ->
|
description() ->
|
||||||
"AuthZ with static rules".
|
"AuthZ with static rules".
|
||||||
|
|
||||||
init(#{path := Path} = Source) ->
|
init(#{path := Path} = Source) ->
|
||||||
Rules = case file:consult(Path) of
|
Rules =
|
||||||
{ok, Terms} ->
|
case file:consult(Path) of
|
||||||
[emqx_authz_rule:compile(Term) || Term <- Terms];
|
{ok, Terms} ->
|
||||||
{error, Reason} when is_atom(Reason) ->
|
[emqx_authz_rule:compile(Term) || Term <- Terms];
|
||||||
?SLOG(alert, #{msg => failed_to_read_acl_file,
|
{error, Reason} when is_atom(Reason) ->
|
||||||
path => Path,
|
?SLOG(alert, #{
|
||||||
explain => emqx_misc:explain_posix(Reason)}),
|
msg => failed_to_read_acl_file,
|
||||||
throw(failed_to_read_acl_file);
|
path => Path,
|
||||||
{error, Reason} ->
|
explain => emqx_misc:explain_posix(Reason)
|
||||||
?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),
|
}),
|
||||||
throw(bad_acl_file_content)
|
throw(failed_to_read_acl_file);
|
||||||
end,
|
{error, Reason} ->
|
||||||
|
?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),
|
||||||
|
throw(bad_acl_file_content)
|
||||||
|
end,
|
||||||
Source#{annotations => #{rules => Rules}}.
|
Source#{annotations => #{rules => Rules}}.
|
||||||
|
|
||||||
destroy(_Source) -> ok.
|
destroy(_Source) -> ok.
|
||||||
|
|
|
||||||
|
|
@ -24,25 +24,28 @@
|
||||||
-behaviour(emqx_authz).
|
-behaviour(emqx_authz).
|
||||||
|
|
||||||
%% AuthZ Callbacks
|
%% AuthZ Callbacks
|
||||||
-export([ description/0
|
-export([
|
||||||
, init/1
|
description/0,
|
||||||
, destroy/1
|
init/1,
|
||||||
, authorize/4
|
destroy/1,
|
||||||
, parse_url/1
|
authorize/4,
|
||||||
]).
|
parse_url/1
|
||||||
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-define(PLACEHOLDERS, [?PH_USERNAME,
|
-define(PLACEHOLDERS, [
|
||||||
?PH_CLIENTID,
|
?PH_USERNAME,
|
||||||
?PH_PEERHOST,
|
?PH_CLIENTID,
|
||||||
?PH_PROTONAME,
|
?PH_PEERHOST,
|
||||||
?PH_MOUNTPOINT,
|
?PH_PROTONAME,
|
||||||
?PH_TOPIC,
|
?PH_MOUNTPOINT,
|
||||||
?PH_ACTION]).
|
?PH_TOPIC,
|
||||||
|
?PH_ACTION
|
||||||
|
]).
|
||||||
|
|
||||||
description() ->
|
description() ->
|
||||||
"AuthZ with http".
|
"AuthZ with http".
|
||||||
|
|
@ -57,14 +60,17 @@ init(Config) ->
|
||||||
destroy(#{annotations := #{id := Id}}) ->
|
destroy(#{annotations := #{id := Id}}) ->
|
||||||
ok = emqx_resource:remove_local(Id).
|
ok = emqx_resource:remove_local(Id).
|
||||||
|
|
||||||
authorize( Client
|
authorize(
|
||||||
, PubSub
|
Client,
|
||||||
, Topic
|
PubSub,
|
||||||
, #{ type := http
|
Topic,
|
||||||
, annotations := #{id := ResourceID}
|
#{
|
||||||
, method := Method
|
type := http,
|
||||||
, request_timeout := RequestTimeout
|
annotations := #{id := ResourceID},
|
||||||
} = Config) ->
|
method := Method,
|
||||||
|
request_timeout := RequestTimeout
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
Request = generate_request(PubSub, Topic, Client, Config),
|
Request = generate_request(PubSub, Topic, Client, Config),
|
||||||
case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of
|
case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of
|
||||||
{ok, 200, _Headers} ->
|
{ok, 200, _Headers} ->
|
||||||
|
|
@ -78,38 +84,47 @@ authorize( Client
|
||||||
{ok, _Status, _Headers, _Body} ->
|
{ok, _Status, _Headers, _Body} ->
|
||||||
nomatch;
|
nomatch;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "http_server_query_failed",
|
?SLOG(error, #{
|
||||||
resource => ResourceID,
|
msg => "http_server_query_failed",
|
||||||
reason => Reason}),
|
resource => ResourceID,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_config(#{ url := URL
|
parse_config(
|
||||||
, method := Method
|
#{
|
||||||
, headers := Headers
|
url := URL,
|
||||||
, request_timeout := ReqTimeout
|
method := Method,
|
||||||
} = Conf) ->
|
headers := Headers,
|
||||||
|
request_timeout := ReqTimeout
|
||||||
|
} = Conf
|
||||||
|
) ->
|
||||||
{BaseURLWithPath, Query} = parse_fullpath(URL),
|
{BaseURLWithPath, Query} = parse_fullpath(URL),
|
||||||
BaseURLMap = parse_url(BaseURLWithPath),
|
BaseURLMap = parse_url(BaseURLWithPath),
|
||||||
Conf#{ method => Method
|
Conf#{
|
||||||
, base_url => maps:remove(query, BaseURLMap)
|
method => Method,
|
||||||
, base_query_template => emqx_authz_utils:parse_deep(
|
base_url => maps:remove(query, BaseURLMap),
|
||||||
cow_qs:parse_qs(bin(Query)),
|
base_query_template => emqx_authz_utils:parse_deep(
|
||||||
?PLACEHOLDERS)
|
cow_qs:parse_qs(bin(Query)),
|
||||||
, body_template => emqx_authz_utils:parse_deep(
|
?PLACEHOLDERS
|
||||||
maps:to_list(maps:get(body, Conf, #{})),
|
),
|
||||||
?PLACEHOLDERS)
|
body_template => emqx_authz_utils:parse_deep(
|
||||||
, headers => Headers
|
maps:to_list(maps:get(body, Conf, #{})),
|
||||||
, request_timeout => ReqTimeout
|
?PLACEHOLDERS
|
||||||
%% pool_type default value `random`
|
),
|
||||||
, pool_type => random
|
headers => Headers,
|
||||||
}.
|
request_timeout => ReqTimeout,
|
||||||
|
%% pool_type default value `random`
|
||||||
|
pool_type => random
|
||||||
|
}.
|
||||||
|
|
||||||
parse_fullpath(RawURL) ->
|
parse_fullpath(RawURL) ->
|
||||||
cow_http:parse_fullpath(bin(RawURL)).
|
cow_http:parse_fullpath(bin(RawURL)).
|
||||||
|
|
||||||
parse_url(URL)
|
parse_url(URL) when
|
||||||
when URL =:= undefined ->
|
URL =:= undefined
|
||||||
|
->
|
||||||
#{};
|
#{};
|
||||||
parse_url(URL) ->
|
parse_url(URL) ->
|
||||||
{ok, URIMap} = emqx_http_lib:uri_parse(URL),
|
{ok, URIMap} = emqx_http_lib:uri_parse(URL),
|
||||||
|
|
@ -120,28 +135,31 @@ parse_url(URL) ->
|
||||||
URIMap
|
URIMap
|
||||||
end.
|
end.
|
||||||
|
|
||||||
generate_request( PubSub
|
generate_request(
|
||||||
, Topic
|
PubSub,
|
||||||
, Client
|
Topic,
|
||||||
, #{ method := Method
|
Client,
|
||||||
, base_url := #{path := Path}
|
#{
|
||||||
, base_query_template := BaseQueryTemplate
|
method := Method,
|
||||||
, headers := Headers
|
base_url := #{path := Path},
|
||||||
, body_template := BodyTemplate
|
base_query_template := BaseQueryTemplate,
|
||||||
}) ->
|
headers := Headers,
|
||||||
|
body_template := BodyTemplate
|
||||||
|
}
|
||||||
|
) ->
|
||||||
Values = client_vars(Client, PubSub, Topic),
|
Values = client_vars(Client, PubSub, Topic),
|
||||||
Body = emqx_authz_utils:render_deep(BodyTemplate, Values),
|
Body = emqx_authz_utils:render_deep(BodyTemplate, Values),
|
||||||
NBaseQuery = emqx_authz_utils:render_deep(BaseQueryTemplate, Values),
|
NBaseQuery = emqx_authz_utils:render_deep(BaseQueryTemplate, Values),
|
||||||
case Method of
|
case Method of
|
||||||
get ->
|
get ->
|
||||||
NPath = append_query(Path, NBaseQuery ++ Body),
|
NPath = append_query(Path, NBaseQuery ++ Body),
|
||||||
{NPath, Headers};
|
{NPath, Headers};
|
||||||
_ ->
|
_ ->
|
||||||
NPath = append_query(Path, NBaseQuery),
|
NPath = append_query(Path, NBaseQuery),
|
||||||
NBody = serialize_body(
|
NBody = serialize_body(
|
||||||
proplists:get_value(<<"Accept">>, Headers, <<"application/json">>),
|
proplists:get_value(<<"Accept">>, Headers, <<"application/json">>),
|
||||||
Body
|
Body
|
||||||
),
|
),
|
||||||
{NPath, Headers, NBody}
|
{NPath, Headers, NBody}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -161,9 +179,13 @@ query_string([], Acc) ->
|
||||||
<<>>
|
<<>>
|
||||||
end;
|
end;
|
||||||
query_string([{K, V} | More], Acc) ->
|
query_string([{K, V} | More], Acc) ->
|
||||||
query_string( More
|
query_string(
|
||||||
, [ ["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)]
|
More,
|
||||||
| Acc]).
|
[
|
||||||
|
["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)]
|
||||||
|
| Acc
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
serialize_body(<<"application/json">>, Body) ->
|
serialize_body(<<"application/json">>, Body) ->
|
||||||
jsx:encode(Body);
|
jsx:encode(Body);
|
||||||
|
|
@ -172,9 +194,9 @@ serialize_body(<<"application/x-www-form-urlencoded">>, Body) ->
|
||||||
|
|
||||||
client_vars(Client, PubSub, Topic) ->
|
client_vars(Client, PubSub, Topic) ->
|
||||||
Client#{
|
Client#{
|
||||||
action => PubSub,
|
action => PubSub,
|
||||||
topic => Topic
|
topic => Topic
|
||||||
}.
|
}.
|
||||||
|
|
||||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||||
bin(B) when is_binary(B) -> B;
|
bin(B) when is_binary(B) -> B;
|
||||||
|
|
|
||||||
|
|
@ -29,38 +29,40 @@
|
||||||
-define(ACL_TABLE_USERNAME, 1).
|
-define(ACL_TABLE_USERNAME, 1).
|
||||||
-define(ACL_TABLE_CLIENTID, 2).
|
-define(ACL_TABLE_CLIENTID, 2).
|
||||||
|
|
||||||
-type(username() :: {username, binary()}).
|
-type username() :: {username, binary()}.
|
||||||
-type(clientid() :: {clientid, binary()}).
|
-type clientid() :: {clientid, binary()}.
|
||||||
-type(who() :: username() | clientid() | all).
|
-type who() :: username() | clientid() | all.
|
||||||
|
|
||||||
-type(rule() :: {emqx_authz_rule:permission(), emqx_authz_rule:action(), emqx_topic:topic()}).
|
-type rule() :: {emqx_authz_rule:permission(), emqx_authz_rule:action(), emqx_topic:topic()}.
|
||||||
-type(rules() :: [rule()]).
|
-type rules() :: [rule()].
|
||||||
|
|
||||||
-record(emqx_acl, {
|
-record(emqx_acl, {
|
||||||
who :: ?ACL_TABLE_ALL | {?ACL_TABLE_USERNAME, binary()} | {?ACL_TABLE_CLIENTID, binary()},
|
who :: ?ACL_TABLE_ALL | {?ACL_TABLE_USERNAME, binary()} | {?ACL_TABLE_CLIENTID, binary()},
|
||||||
rules :: rules()
|
rules :: rules()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-behaviour(emqx_authz).
|
-behaviour(emqx_authz).
|
||||||
|
|
||||||
%% AuthZ Callbacks
|
%% AuthZ Callbacks
|
||||||
-export([ description/0
|
-export([
|
||||||
, init/1
|
description/0,
|
||||||
, destroy/1
|
init/1,
|
||||||
, authorize/4
|
destroy/1,
|
||||||
]).
|
authorize/4
|
||||||
|
]).
|
||||||
|
|
||||||
%% Management API
|
%% Management API
|
||||||
-export([ mnesia/1
|
-export([
|
||||||
, init_tables/0
|
mnesia/1,
|
||||||
, store_rules/2
|
init_tables/0,
|
||||||
, purge_rules/0
|
store_rules/2,
|
||||||
, get_rules/1
|
purge_rules/0,
|
||||||
, delete_rules/1
|
get_rules/1,
|
||||||
, list_clientid_rules/0
|
delete_rules/1,
|
||||||
, list_username_rules/0
|
list_clientid_rules/0,
|
||||||
, record_count/0
|
list_username_rules/0,
|
||||||
]).
|
record_count/0
|
||||||
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
@ -69,14 +71,15 @@
|
||||||
|
|
||||||
-boot_mnesia({mnesia, [boot]}).
|
-boot_mnesia({mnesia, [boot]}).
|
||||||
|
|
||||||
-spec(mnesia(boot | copy) -> ok).
|
-spec mnesia(boot | copy) -> ok.
|
||||||
mnesia(boot) ->
|
mnesia(boot) ->
|
||||||
ok = mria:create_table(?ACL_TABLE, [
|
ok = mria:create_table(?ACL_TABLE, [
|
||||||
{type, ordered_set},
|
{type, ordered_set},
|
||||||
{rlog_shard, ?ACL_SHARDED},
|
{rlog_shard, ?ACL_SHARDED},
|
||||||
{storage, disc_copies},
|
{storage, disc_copies},
|
||||||
{attributes, record_info(fields, ?ACL_TABLE)},
|
{attributes, record_info(fields, ?ACL_TABLE)},
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]).
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}
|
||||||
|
]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% emqx_authz callbacks
|
%% emqx_authz callbacks
|
||||||
|
|
@ -89,19 +92,25 @@ init(Source) -> Source.
|
||||||
|
|
||||||
destroy(_Source) -> ok.
|
destroy(_Source) -> ok.
|
||||||
|
|
||||||
authorize(#{username := Username,
|
authorize(
|
||||||
clientid := Clientid
|
#{
|
||||||
} = Client, PubSub, Topic, #{type := 'built_in_database'}) ->
|
username := Username,
|
||||||
|
clientid := Clientid
|
||||||
Rules = case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}) of
|
} = Client,
|
||||||
[] -> [];
|
PubSub,
|
||||||
[#emqx_acl{rules = Rules0}] when is_list(Rules0) -> Rules0
|
Topic,
|
||||||
end
|
#{type := 'built_in_database'}
|
||||||
++ case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}) of
|
) ->
|
||||||
|
Rules =
|
||||||
|
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}) of
|
||||||
|
[] -> [];
|
||||||
|
[#emqx_acl{rules = Rules0}] when is_list(Rules0) -> Rules0
|
||||||
|
end ++
|
||||||
|
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}) of
|
||||||
[] -> [];
|
[] -> [];
|
||||||
[#emqx_acl{rules = Rules1}] when is_list(Rules1) -> Rules1
|
[#emqx_acl{rules = Rules1}] when is_list(Rules1) -> Rules1
|
||||||
end
|
end ++
|
||||||
++ case mnesia:dirty_read(?ACL_TABLE, ?ACL_TABLE_ALL) of
|
case mnesia:dirty_read(?ACL_TABLE, ?ACL_TABLE_ALL) of
|
||||||
[] -> [];
|
[] -> [];
|
||||||
[#emqx_acl{rules = Rules2}] when is_list(Rules2) -> Rules2
|
[#emqx_acl{rules = Rules2}] when is_list(Rules2) -> Rules2
|
||||||
end,
|
end,
|
||||||
|
|
@ -112,12 +121,12 @@ authorize(#{username := Username,
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
%% Init
|
%% Init
|
||||||
-spec(init_tables() -> ok).
|
-spec init_tables() -> ok.
|
||||||
init_tables() ->
|
init_tables() ->
|
||||||
ok = mria_rlog:wait_for_shards([?ACL_SHARDED], infinity).
|
ok = mria_rlog:wait_for_shards([?ACL_SHARDED], infinity).
|
||||||
|
|
||||||
%% @doc Update authz rules
|
%% @doc Update authz rules
|
||||||
-spec(store_rules(who(), rules()) -> ok).
|
-spec store_rules(who(), rules()) -> ok.
|
||||||
store_rules({username, Username}, Rules) ->
|
store_rules({username, Username}, Rules) ->
|
||||||
Record = #emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = normalize_rules(Rules)},
|
Record = #emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = normalize_rules(Rules)},
|
||||||
mria:dirty_write(Record);
|
mria:dirty_write(Record);
|
||||||
|
|
@ -129,16 +138,17 @@ store_rules(all, Rules) ->
|
||||||
mria:dirty_write(Record).
|
mria:dirty_write(Record).
|
||||||
|
|
||||||
%% @doc Clean all authz rules for (username & clientid & all)
|
%% @doc Clean all authz rules for (username & clientid & all)
|
||||||
-spec(purge_rules() -> ok).
|
-spec purge_rules() -> ok.
|
||||||
purge_rules() ->
|
purge_rules() ->
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(Key) ->
|
fun(Key) ->
|
||||||
ok = mria:dirty_delete(?ACL_TABLE, Key)
|
ok = mria:dirty_delete(?ACL_TABLE, Key)
|
||||||
end,
|
end,
|
||||||
mnesia:dirty_all_keys(?ACL_TABLE)).
|
mnesia:dirty_all_keys(?ACL_TABLE)
|
||||||
|
).
|
||||||
|
|
||||||
%% @doc Get one record
|
%% @doc Get one record
|
||||||
-spec(get_rules(who()) -> {ok, rules()} | not_found).
|
-spec get_rules(who()) -> {ok, rules()} | not_found.
|
||||||
get_rules({username, Username}) ->
|
get_rules({username, Username}) ->
|
||||||
do_get_rules({?ACL_TABLE_USERNAME, Username});
|
do_get_rules({?ACL_TABLE_USERNAME, Username});
|
||||||
get_rules({clientid, Clientid}) ->
|
get_rules({clientid, Clientid}) ->
|
||||||
|
|
@ -147,7 +157,7 @@ get_rules(all) ->
|
||||||
do_get_rules(?ACL_TABLE_ALL).
|
do_get_rules(?ACL_TABLE_ALL).
|
||||||
|
|
||||||
%% @doc Delete one record
|
%% @doc Delete one record
|
||||||
-spec(delete_rules(who()) -> ok).
|
-spec delete_rules(who()) -> ok.
|
||||||
delete_rules({username, Username}) ->
|
delete_rules({username, Username}) ->
|
||||||
mria:dirty_delete(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username});
|
mria:dirty_delete(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username});
|
||||||
delete_rules({clientid, Clientid}) ->
|
delete_rules({clientid, Clientid}) ->
|
||||||
|
|
@ -155,21 +165,23 @@ delete_rules({clientid, Clientid}) ->
|
||||||
delete_rules(all) ->
|
delete_rules(all) ->
|
||||||
mria:dirty_delete(?ACL_TABLE, ?ACL_TABLE_ALL).
|
mria:dirty_delete(?ACL_TABLE, ?ACL_TABLE_ALL).
|
||||||
|
|
||||||
-spec(list_username_rules() -> ets:match_spec()).
|
-spec list_username_rules() -> ets:match_spec().
|
||||||
list_username_rules() ->
|
list_username_rules() ->
|
||||||
ets:fun2ms(
|
ets:fun2ms(
|
||||||
fun(#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}) ->
|
fun(#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}) ->
|
||||||
[{username, Username}, {rules, Rules}]
|
[{username, Username}, {rules, Rules}]
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
-spec(list_clientid_rules() -> ets:match_spec()).
|
-spec list_clientid_rules() -> ets:match_spec().
|
||||||
list_clientid_rules() ->
|
list_clientid_rules() ->
|
||||||
ets:fun2ms(
|
ets:fun2ms(
|
||||||
fun(#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}) ->
|
fun(#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}) ->
|
||||||
[{clientid, Clientid}, {rules, Rules}]
|
[{clientid, Clientid}, {rules, Rules}]
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
-spec(record_count() -> non_neg_integer()).
|
-spec record_count() -> non_neg_integer().
|
||||||
record_count() ->
|
record_count() ->
|
||||||
mnesia:table_info(?ACL_TABLE, size).
|
mnesia:table_info(?ACL_TABLE, size).
|
||||||
|
|
||||||
|
|
@ -181,9 +193,7 @@ normalize_rules(Rules) ->
|
||||||
lists:map(fun normalize_rule/1, Rules).
|
lists:map(fun normalize_rule/1, Rules).
|
||||||
|
|
||||||
normalize_rule({Permission, Action, Topic}) ->
|
normalize_rule({Permission, Action, Topic}) ->
|
||||||
{normalize_permission(Permission),
|
{normalize_permission(Permission), normalize_action(Action), normalize_topic(Topic)};
|
||||||
normalize_action(Action),
|
|
||||||
normalize_topic(Topic)};
|
|
||||||
normalize_rule(Rule) ->
|
normalize_rule(Rule) ->
|
||||||
error({invalid_rule, Rule}).
|
error({invalid_rule, Rule}).
|
||||||
|
|
||||||
|
|
@ -206,8 +216,9 @@ do_get_rules(Key) ->
|
||||||
[] -> not_found
|
[] -> not_found
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_authorize(_Client, _PubSub, _Topic, []) -> nomatch;
|
do_authorize(_Client, _PubSub, _Topic, []) ->
|
||||||
do_authorize(Client, PubSub, Topic, [ {Permission, Action, TopicFilter} | Tail]) ->
|
nomatch;
|
||||||
|
do_authorize(Client, PubSub, Topic, [{Permission, Action, TopicFilter} | Tail]) ->
|
||||||
Rule = emqx_authz_rule:compile({Permission, all, Action, [TopicFilter]}),
|
Rule = emqx_authz_rule:compile({Permission, all, Action, [TopicFilter]}),
|
||||||
case emqx_authz_rule:match(Client, PubSub, Topic, Rule) of
|
case emqx_authz_rule:match(Client, PubSub, Topic, Rule) of
|
||||||
{matched, Permission} -> {matched, Permission};
|
{matched, Permission} -> {matched, Permission};
|
||||||
|
|
|
||||||
|
|
@ -24,62 +24,83 @@
|
||||||
-behaviour(emqx_authz).
|
-behaviour(emqx_authz).
|
||||||
|
|
||||||
%% AuthZ Callbacks
|
%% AuthZ Callbacks
|
||||||
-export([ description/0
|
-export([
|
||||||
, init/1
|
description/0,
|
||||||
, destroy/1
|
init/1,
|
||||||
, authorize/4
|
destroy/1,
|
||||||
]).
|
authorize/4
|
||||||
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-define(PLACEHOLDERS, [?PH_USERNAME,
|
-define(PLACEHOLDERS, [
|
||||||
?PH_CLIENTID,
|
?PH_USERNAME,
|
||||||
?PH_PEERHOST]).
|
?PH_CLIENTID,
|
||||||
|
?PH_PEERHOST
|
||||||
|
]).
|
||||||
|
|
||||||
description() ->
|
description() ->
|
||||||
"AuthZ with MongoDB".
|
"AuthZ with MongoDB".
|
||||||
|
|
||||||
init(#{selector := Selector} = Source) ->
|
init(#{selector := Selector} = Source) ->
|
||||||
case emqx_authz_utils:create_resource(emqx_connector_mongo, Source) of
|
case emqx_authz_utils:create_resource(emqx_connector_mongo, Source) of
|
||||||
{error, Reason} -> error({load_config_error, Reason});
|
{error, Reason} ->
|
||||||
{ok, Id} -> Source#{annotations => #{id => Id},
|
error({load_config_error, Reason});
|
||||||
selector_template => emqx_authz_utils:parse_deep(
|
{ok, Id} ->
|
||||||
Selector,
|
Source#{
|
||||||
?PLACEHOLDERS)}
|
annotations => #{id => Id},
|
||||||
|
selector_template => emqx_authz_utils:parse_deep(
|
||||||
|
Selector,
|
||||||
|
?PLACEHOLDERS
|
||||||
|
)
|
||||||
|
}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
destroy(#{annotations := #{id := Id}}) ->
|
destroy(#{annotations := #{id := Id}}) ->
|
||||||
ok = emqx_resource:remove_local(Id).
|
ok = emqx_resource:remove_local(Id).
|
||||||
|
|
||||||
authorize(Client, PubSub, Topic,
|
authorize(
|
||||||
#{collection := Collection,
|
Client,
|
||||||
selector_template := SelectorTemplate,
|
PubSub,
|
||||||
annotations := #{id := ResourceID}
|
Topic,
|
||||||
}) ->
|
#{
|
||||||
|
collection := Collection,
|
||||||
|
selector_template := SelectorTemplate,
|
||||||
|
annotations := #{id := ResourceID}
|
||||||
|
}
|
||||||
|
) ->
|
||||||
RenderedSelector = emqx_authz_utils:render_deep(SelectorTemplate, Client),
|
RenderedSelector = emqx_authz_utils:render_deep(SelectorTemplate, Client),
|
||||||
Result = try
|
Result =
|
||||||
emqx_resource:query(ResourceID, {find, Collection, RenderedSelector, #{}})
|
try
|
||||||
catch
|
emqx_resource:query(ResourceID, {find, Collection, RenderedSelector, #{}})
|
||||||
error:Error -> {error, Error}
|
catch
|
||||||
end,
|
error:Error -> {error, Error}
|
||||||
|
end,
|
||||||
|
|
||||||
case Result of
|
case Result of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "query_mongo_error",
|
?SLOG(error, #{
|
||||||
reason => Reason,
|
msg => "query_mongo_error",
|
||||||
collection => Collection,
|
reason => Reason,
|
||||||
selector => RenderedSelector,
|
collection => Collection,
|
||||||
resource_id => ResourceID}),
|
selector => RenderedSelector,
|
||||||
|
resource_id => ResourceID
|
||||||
|
}),
|
||||||
|
nomatch;
|
||||||
|
[] ->
|
||||||
nomatch;
|
nomatch;
|
||||||
[] -> nomatch;
|
|
||||||
Rows ->
|
Rows ->
|
||||||
Rules = [ emqx_authz_rule:compile({Permission, all, Action, Topics})
|
Rules = [
|
||||||
|| #{<<"topics">> := Topics,
|
emqx_authz_rule:compile({Permission, all, Action, Topics})
|
||||||
<<"permission">> := Permission,
|
|| #{
|
||||||
<<"action">> := Action} <- Rows],
|
<<"topics">> := Topics,
|
||||||
|
<<"permission">> := Permission,
|
||||||
|
<<"action">> := Action
|
||||||
|
} <- Rows
|
||||||
|
],
|
||||||
do_authorize(Client, PubSub, Topic, Rules)
|
do_authorize(Client, PubSub, Topic, Rules)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,66 +24,89 @@
|
||||||
-behaviour(emqx_authz).
|
-behaviour(emqx_authz).
|
||||||
|
|
||||||
%% AuthZ Callbacks
|
%% AuthZ Callbacks
|
||||||
-export([ description/0
|
-export([
|
||||||
, init/1
|
description/0,
|
||||||
, destroy/1
|
init/1,
|
||||||
, authorize/4
|
destroy/1,
|
||||||
]).
|
authorize/4
|
||||||
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-define(PLACEHOLDERS, [?PH_USERNAME,
|
-define(PLACEHOLDERS, [
|
||||||
?PH_CLIENTID,
|
?PH_USERNAME,
|
||||||
?PH_PEERHOST,
|
?PH_CLIENTID,
|
||||||
?PH_CERT_CN_NAME,
|
?PH_PEERHOST,
|
||||||
?PH_CERT_SUBJECT]).
|
?PH_CERT_CN_NAME,
|
||||||
|
?PH_CERT_SUBJECT
|
||||||
|
]).
|
||||||
|
|
||||||
description() ->
|
description() ->
|
||||||
"AuthZ with Mysql".
|
"AuthZ with Mysql".
|
||||||
|
|
||||||
init(#{query := SQL} = Source) ->
|
init(#{query := SQL} = Source) ->
|
||||||
case emqx_authz_utils:create_resource(emqx_connector_mysql, Source) of
|
case emqx_authz_utils:create_resource(emqx_connector_mysql, Source) of
|
||||||
{error, Reason} -> error({load_config_error, Reason});
|
{error, Reason} ->
|
||||||
{ok, Id} -> Source#{annotations =>
|
error({load_config_error, Reason});
|
||||||
#{id => Id,
|
{ok, Id} ->
|
||||||
query => emqx_authz_utils:parse_sql(
|
Source#{
|
||||||
SQL,
|
annotations =>
|
||||||
'?',
|
#{
|
||||||
?PLACEHOLDERS)}}
|
id => Id,
|
||||||
|
query => emqx_authz_utils:parse_sql(
|
||||||
|
SQL,
|
||||||
|
'?',
|
||||||
|
?PLACEHOLDERS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
destroy(#{annotations := #{id := Id}}) ->
|
destroy(#{annotations := #{id := Id}}) ->
|
||||||
ok = emqx_resource:remove_local(Id).
|
ok = emqx_resource:remove_local(Id).
|
||||||
|
|
||||||
authorize(Client, PubSub, Topic,
|
authorize(
|
||||||
#{annotations := #{id := ResourceID,
|
Client,
|
||||||
query := {Query, Params}
|
PubSub,
|
||||||
}
|
Topic,
|
||||||
}) ->
|
#{
|
||||||
|
annotations := #{
|
||||||
|
id := ResourceID,
|
||||||
|
query := {Query, Params}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) ->
|
||||||
RenderParams = emqx_authz_utils:render_sql_params(Params, Client),
|
RenderParams = emqx_authz_utils:render_sql_params(Params, Client),
|
||||||
case emqx_resource:query(ResourceID, {sql, Query, RenderParams}) of
|
case emqx_resource:query(ResourceID, {sql, Query, RenderParams}) of
|
||||||
{ok, _Columns, []} -> nomatch;
|
{ok, _Columns, []} ->
|
||||||
|
nomatch;
|
||||||
{ok, Columns, Rows} ->
|
{ok, Columns, Rows} ->
|
||||||
do_authorize(Client, PubSub, Topic, Columns, Rows);
|
do_authorize(Client, PubSub, Topic, Columns, Rows);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{ msg => "query_mysql_error"
|
?SLOG(error, #{
|
||||||
, reason => Reason
|
msg => "query_mysql_error",
|
||||||
, query => Query
|
reason => Reason,
|
||||||
, params => RenderParams
|
query => Query,
|
||||||
, resource_id => ResourceID}),
|
params => RenderParams,
|
||||||
|
resource_id => ResourceID
|
||||||
|
}),
|
||||||
nomatch
|
nomatch
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
do_authorize(_Client, _PubSub, _Topic, _Columns, []) ->
|
do_authorize(_Client, _PubSub, _Topic, _Columns, []) ->
|
||||||
nomatch;
|
nomatch;
|
||||||
do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) ->
|
do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) ->
|
||||||
case emqx_authz_rule:match(Client, PubSub, Topic,
|
case
|
||||||
emqx_authz_rule:compile(format_result(Columns, Row))
|
emqx_authz_rule:match(
|
||||||
) of
|
Client,
|
||||||
|
PubSub,
|
||||||
|
Topic,
|
||||||
|
emqx_authz_rule:compile(format_result(Columns, Row))
|
||||||
|
)
|
||||||
|
of
|
||||||
{matched, Permission} -> {matched, Permission};
|
{matched, Permission} -> {matched, Permission};
|
||||||
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail)
|
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail)
|
||||||
end.
|
end.
|
||||||
|
|
@ -97,5 +120,5 @@ format_result(Columns, Row) ->
|
||||||
index(Elem, List) ->
|
index(Elem, List) ->
|
||||||
index(Elem, List, 1).
|
index(Elem, List, 1).
|
||||||
index(_Elem, [], _Index) -> {error, not_found};
|
index(_Elem, [], _Index) -> {error, not_found};
|
||||||
index(Elem, [ Elem | _List], Index) -> Index;
|
index(Elem, [Elem | _List], Index) -> Index;
|
||||||
index(Elem, [ _ | List], Index) -> index(Elem, List, Index + 1).
|
index(Elem, [_ | List], Index) -> index(Elem, List, Index + 1).
|
||||||
|
|
|
||||||
|
|
@ -24,42 +24,53 @@
|
||||||
-behaviour(emqx_authz).
|
-behaviour(emqx_authz).
|
||||||
|
|
||||||
%% AuthZ Callbacks
|
%% AuthZ Callbacks
|
||||||
-export([ description/0
|
-export([
|
||||||
, init/1
|
description/0,
|
||||||
, destroy/1
|
init/1,
|
||||||
, authorize/4
|
destroy/1,
|
||||||
]).
|
authorize/4
|
||||||
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-define(PLACEHOLDERS, [?PH_USERNAME,
|
-define(PLACEHOLDERS, [
|
||||||
?PH_CLIENTID,
|
?PH_USERNAME,
|
||||||
?PH_PEERHOST,
|
?PH_CLIENTID,
|
||||||
?PH_CERT_CN_NAME,
|
?PH_PEERHOST,
|
||||||
?PH_CERT_SUBJECT]).
|
?PH_CERT_CN_NAME,
|
||||||
|
?PH_CERT_SUBJECT
|
||||||
|
]).
|
||||||
|
|
||||||
description() ->
|
description() ->
|
||||||
"AuthZ with PostgreSQL".
|
"AuthZ with PostgreSQL".
|
||||||
|
|
||||||
init(#{query := SQL0} = Source) ->
|
init(#{query := SQL0} = Source) ->
|
||||||
{SQL, PlaceHolders} = emqx_authz_utils:parse_sql(
|
{SQL, PlaceHolders} = emqx_authz_utils:parse_sql(
|
||||||
SQL0,
|
SQL0,
|
||||||
'$n',
|
'$n',
|
||||||
?PLACEHOLDERS),
|
?PLACEHOLDERS
|
||||||
|
),
|
||||||
ResourceID = emqx_authz_utils:make_resource_id(emqx_connector_pgsql),
|
ResourceID = emqx_authz_utils:make_resource_id(emqx_connector_pgsql),
|
||||||
case emqx_resource:create_local(
|
case
|
||||||
|
emqx_resource:create_local(
|
||||||
ResourceID,
|
ResourceID,
|
||||||
?RESOURCE_GROUP,
|
?RESOURCE_GROUP,
|
||||||
emqx_connector_pgsql,
|
emqx_connector_pgsql,
|
||||||
Source#{named_queries => #{ResourceID => SQL}},
|
Source#{named_queries => #{ResourceID => SQL}},
|
||||||
#{}) of
|
#{}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
Source#{annotations =>
|
Source#{
|
||||||
#{id => ResourceID,
|
annotations =>
|
||||||
placeholders => PlaceHolders}};
|
#{
|
||||||
|
id => ResourceID,
|
||||||
|
placeholders => PlaceHolders
|
||||||
|
}
|
||||||
|
};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
error({load_config_error, Reason})
|
error({load_config_error, Reason})
|
||||||
end.
|
end.
|
||||||
|
|
@ -67,30 +78,44 @@ init(#{query := SQL0} = Source) ->
|
||||||
destroy(#{annotations := #{id := Id}}) ->
|
destroy(#{annotations := #{id := Id}}) ->
|
||||||
ok = emqx_resource:remove_local(Id).
|
ok = emqx_resource:remove_local(Id).
|
||||||
|
|
||||||
authorize(Client, PubSub, Topic,
|
authorize(
|
||||||
#{annotations := #{id := ResourceID,
|
Client,
|
||||||
placeholders := Placeholders
|
PubSub,
|
||||||
}
|
Topic,
|
||||||
}) ->
|
#{
|
||||||
|
annotations := #{
|
||||||
|
id := ResourceID,
|
||||||
|
placeholders := Placeholders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) ->
|
||||||
RenderedParams = emqx_authz_utils:render_sql_params(Placeholders, Client),
|
RenderedParams = emqx_authz_utils:render_sql_params(Placeholders, Client),
|
||||||
case emqx_resource:query(ResourceID, {prepared_query, ResourceID, RenderedParams}) of
|
case emqx_resource:query(ResourceID, {prepared_query, ResourceID, RenderedParams}) of
|
||||||
{ok, _Columns, []} -> nomatch;
|
{ok, _Columns, []} ->
|
||||||
|
nomatch;
|
||||||
{ok, Columns, Rows} ->
|
{ok, Columns, Rows} ->
|
||||||
do_authorize(Client, PubSub, Topic, Columns, Rows);
|
do_authorize(Client, PubSub, Topic, Columns, Rows);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{ msg => "query_postgresql_error"
|
?SLOG(error, #{
|
||||||
, reason => Reason
|
msg => "query_postgresql_error",
|
||||||
, params => RenderedParams
|
reason => Reason,
|
||||||
, resource_id => ResourceID}),
|
params => RenderedParams,
|
||||||
|
resource_id => ResourceID
|
||||||
|
}),
|
||||||
nomatch
|
nomatch
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_authorize(_Client, _PubSub, _Topic, _Columns, []) ->
|
do_authorize(_Client, _PubSub, _Topic, _Columns, []) ->
|
||||||
nomatch;
|
nomatch;
|
||||||
do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) ->
|
do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) ->
|
||||||
case emqx_authz_rule:match(Client, PubSub, Topic,
|
case
|
||||||
emqx_authz_rule:compile(format_result(Columns, Row))
|
emqx_authz_rule:match(
|
||||||
) of
|
Client,
|
||||||
|
PubSub,
|
||||||
|
Topic,
|
||||||
|
emqx_authz_rule:compile(format_result(Columns, Row))
|
||||||
|
)
|
||||||
|
of
|
||||||
{matched, Permission} -> {matched, Permission};
|
{matched, Permission} -> {matched, Permission};
|
||||||
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail)
|
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail)
|
||||||
end.
|
end.
|
||||||
|
|
@ -104,6 +129,9 @@ format_result(Columns, Row) ->
|
||||||
index(Key, N, TupleList) when is_integer(N) ->
|
index(Key, N, TupleList) when is_integer(N) ->
|
||||||
Tuple = lists:keyfind(Key, N, TupleList),
|
Tuple = lists:keyfind(Key, N, TupleList),
|
||||||
index(Tuple, TupleList, 1);
|
index(Tuple, TupleList, 1);
|
||||||
index(_Tuple, [], _Index) -> {error, not_found};
|
index(_Tuple, [], _Index) ->
|
||||||
index(Tuple, [Tuple | _TupleList], Index) -> Index;
|
{error, not_found};
|
||||||
index(Tuple, [_ | TupleList], Index) -> index(Tuple, TupleList, Index + 1).
|
index(Tuple, [Tuple | _TupleList], Index) ->
|
||||||
|
Index;
|
||||||
|
index(Tuple, [_ | TupleList], Index) ->
|
||||||
|
index(Tuple, TupleList, Index + 1).
|
||||||
|
|
|
||||||
|
|
@ -24,22 +24,25 @@
|
||||||
-behaviour(emqx_authz).
|
-behaviour(emqx_authz).
|
||||||
|
|
||||||
%% AuthZ Callbacks
|
%% AuthZ Callbacks
|
||||||
-export([ description/0
|
-export([
|
||||||
, init/1
|
description/0,
|
||||||
, destroy/1
|
init/1,
|
||||||
, authorize/4
|
destroy/1,
|
||||||
]).
|
authorize/4
|
||||||
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-define(PLACEHOLDERS, [?PH_CERT_CN_NAME,
|
-define(PLACEHOLDERS, [
|
||||||
?PH_CERT_SUBJECT,
|
?PH_CERT_CN_NAME,
|
||||||
?PH_PEERHOST,
|
?PH_CERT_SUBJECT,
|
||||||
?PH_CLIENTID,
|
?PH_PEERHOST,
|
||||||
?PH_USERNAME]).
|
?PH_CLIENTID,
|
||||||
|
?PH_USERNAME
|
||||||
|
]).
|
||||||
|
|
||||||
description() ->
|
description() ->
|
||||||
"AuthZ with Redis".
|
"AuthZ with Redis".
|
||||||
|
|
@ -48,38 +51,54 @@ init(#{cmd := CmdStr} = Source) ->
|
||||||
Cmd = tokens(CmdStr),
|
Cmd = tokens(CmdStr),
|
||||||
CmdTemplate = emqx_authz_utils:parse_deep(Cmd, ?PLACEHOLDERS),
|
CmdTemplate = emqx_authz_utils:parse_deep(Cmd, ?PLACEHOLDERS),
|
||||||
case emqx_authz_utils:create_resource(emqx_connector_redis, Source) of
|
case emqx_authz_utils:create_resource(emqx_connector_redis, Source) of
|
||||||
{error, Reason} -> error({load_config_error, Reason});
|
{error, Reason} ->
|
||||||
{ok, Id} -> Source#{annotations => #{id => Id},
|
error({load_config_error, Reason});
|
||||||
cmd_template => CmdTemplate}
|
{ok, Id} ->
|
||||||
|
Source#{
|
||||||
|
annotations => #{id => Id},
|
||||||
|
cmd_template => CmdTemplate
|
||||||
|
}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
destroy(#{annotations := #{id := Id}}) ->
|
destroy(#{annotations := #{id := Id}}) ->
|
||||||
ok = emqx_resource:remove_local(Id).
|
ok = emqx_resource:remove_local(Id).
|
||||||
|
|
||||||
authorize(Client, PubSub, Topic,
|
authorize(
|
||||||
#{cmd_template := CmdTemplate,
|
Client,
|
||||||
annotations := #{id := ResourceID}
|
PubSub,
|
||||||
}) ->
|
Topic,
|
||||||
|
#{
|
||||||
|
cmd_template := CmdTemplate,
|
||||||
|
annotations := #{id := ResourceID}
|
||||||
|
}
|
||||||
|
) ->
|
||||||
Cmd = emqx_authz_utils:render_deep(CmdTemplate, Client),
|
Cmd = emqx_authz_utils:render_deep(CmdTemplate, Client),
|
||||||
case emqx_resource:query(ResourceID, {cmd, Cmd}) of
|
case emqx_resource:query(ResourceID, {cmd, Cmd}) of
|
||||||
{ok, []} -> nomatch;
|
{ok, []} ->
|
||||||
|
nomatch;
|
||||||
{ok, Rows} ->
|
{ok, Rows} ->
|
||||||
do_authorize(Client, PubSub, Topic, Rows);
|
do_authorize(Client, PubSub, Topic, Rows);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{ msg => "query_redis_error"
|
?SLOG(error, #{
|
||||||
, reason => Reason
|
msg => "query_redis_error",
|
||||||
, cmd => Cmd
|
reason => Reason,
|
||||||
, resource_id => ResourceID}),
|
cmd => Cmd,
|
||||||
|
resource_id => ResourceID
|
||||||
|
}),
|
||||||
nomatch
|
nomatch
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_authorize(_Client, _PubSub, _Topic, []) ->
|
do_authorize(_Client, _PubSub, _Topic, []) ->
|
||||||
nomatch;
|
nomatch;
|
||||||
do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) ->
|
do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) ->
|
||||||
case emqx_authz_rule:match(
|
case
|
||||||
Client, PubSub, Topic,
|
emqx_authz_rule:match(
|
||||||
emqx_authz_rule:compile({allow, all, Action, [TopicFilter]})
|
Client,
|
||||||
) of
|
PubSub,
|
||||||
|
Topic,
|
||||||
|
emqx_authz_rule:compile({allow, all, Action, [TopicFilter]})
|
||||||
|
)
|
||||||
|
of
|
||||||
{matched, Permission} -> {matched, Permission};
|
{matched, Permission} -> {matched, Permission};
|
||||||
nomatch -> do_authorize(Client, PubSub, Topic, Tail)
|
nomatch -> do_authorize(Client, PubSub, Topic, Tail)
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -26,50 +26,66 @@
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
%% APIs
|
%% APIs
|
||||||
-export([ match/4
|
-export([
|
||||||
, matches/4
|
match/4,
|
||||||
, compile/1
|
matches/4,
|
||||||
]).
|
compile/1
|
||||||
|
]).
|
||||||
|
|
||||||
-type(ipaddress() :: {ipaddr, esockd_cidr:cidr_string()} |
|
-type ipaddress() ::
|
||||||
{ipaddrs, list(esockd_cidr:cidr_string())}).
|
{ipaddr, esockd_cidr:cidr_string()}
|
||||||
|
| {ipaddrs, list(esockd_cidr:cidr_string())}.
|
||||||
|
|
||||||
-type(username() :: {username, binary()}).
|
-type username() :: {username, binary()}.
|
||||||
|
|
||||||
-type(clientid() :: {clientid, binary()}).
|
-type clientid() :: {clientid, binary()}.
|
||||||
|
|
||||||
-type(who() :: ipaddress() | username() | clientid() |
|
-type who() ::
|
||||||
{'and', [ipaddress() | username() | clientid()]} |
|
ipaddress()
|
||||||
{'or', [ipaddress() | username() | clientid()]} |
|
| username()
|
||||||
all).
|
| clientid()
|
||||||
|
| {'and', [ipaddress() | username() | clientid()]}
|
||||||
|
| {'or', [ipaddress() | username() | clientid()]}
|
||||||
|
| all.
|
||||||
|
|
||||||
-type(action() :: subscribe | publish | all).
|
-type action() :: subscribe | publish | all.
|
||||||
-type(permission() :: allow | deny).
|
-type permission() :: allow | deny.
|
||||||
|
|
||||||
-type(rule() :: {permission(), who(), action(), list(emqx_types:topic())}).
|
-type rule() :: {permission(), who(), action(), list(emqx_types:topic())}.
|
||||||
|
|
||||||
-export_type([ action/0
|
-export_type([
|
||||||
, permission/0
|
action/0,
|
||||||
]).
|
permission/0
|
||||||
|
]).
|
||||||
|
|
||||||
compile({Permission, all})
|
compile({Permission, all}) when
|
||||||
when ?ALLOW_DENY(Permission) -> {Permission, all, all, [compile_topic(<<"#">>)]};
|
?ALLOW_DENY(Permission)
|
||||||
compile({Permission, Who, Action, TopicFilters})
|
->
|
||||||
when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) ->
|
{Permission, all, all, [compile_topic(<<"#">>)]};
|
||||||
{ atom(Permission), compile_who(Who), atom(Action)
|
compile({Permission, Who, Action, TopicFilters}) when
|
||||||
, [compile_topic(Topic) || Topic <- TopicFilters]}.
|
?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters)
|
||||||
|
->
|
||||||
|
{atom(Permission), compile_who(Who), atom(Action), [
|
||||||
|
compile_topic(Topic)
|
||||||
|
|| Topic <- TopicFilters
|
||||||
|
]}.
|
||||||
|
|
||||||
compile_who(all) -> all;
|
compile_who(all) ->
|
||||||
compile_who({user, Username}) -> compile_who({username, Username});
|
all;
|
||||||
|
compile_who({user, Username}) ->
|
||||||
|
compile_who({username, Username});
|
||||||
compile_who({username, {re, Username}}) ->
|
compile_who({username, {re, Username}}) ->
|
||||||
{ok, MP} = re:compile(bin(Username)),
|
{ok, MP} = re:compile(bin(Username)),
|
||||||
{username, MP};
|
{username, MP};
|
||||||
compile_who({username, Username}) -> {username, {eq, bin(Username)}};
|
compile_who({username, Username}) ->
|
||||||
compile_who({client, Clientid}) -> compile_who({clientid, Clientid});
|
{username, {eq, bin(Username)}};
|
||||||
|
compile_who({client, Clientid}) ->
|
||||||
|
compile_who({clientid, Clientid});
|
||||||
compile_who({clientid, {re, Clientid}}) ->
|
compile_who({clientid, {re, Clientid}}) ->
|
||||||
{ok, MP} = re:compile(bin(Clientid)),
|
{ok, MP} = re:compile(bin(Clientid)),
|
||||||
{clientid, MP};
|
{clientid, MP};
|
||||||
compile_who({clientid, Clientid}) -> {clientid, {eq, bin(Clientid)}};
|
compile_who({clientid, Clientid}) ->
|
||||||
|
{clientid, {eq, bin(Clientid)}};
|
||||||
compile_who({ipaddr, CIDR}) ->
|
compile_who({ipaddr, CIDR}) ->
|
||||||
{ipaddr, esockd_cidr:parse(CIDR, true)};
|
{ipaddr, esockd_cidr:parse(CIDR, true)};
|
||||||
compile_who({ipaddrs, CIDRs}) ->
|
compile_who({ipaddrs, CIDRs}) ->
|
||||||
|
|
@ -86,7 +102,7 @@ compile_topic({eq, Topic}) ->
|
||||||
compile_topic(Topic) ->
|
compile_topic(Topic) ->
|
||||||
Words = emqx_topic:words(bin(Topic)),
|
Words = emqx_topic:words(bin(Topic)),
|
||||||
case pattern(Words) of
|
case pattern(Words) of
|
||||||
true -> {pattern, Words};
|
true -> {pattern, Words};
|
||||||
false -> Words
|
false -> Words
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -94,7 +110,8 @@ pattern(Words) ->
|
||||||
lists:member(?PH_USERNAME, Words) orelse lists:member(?PH_CLIENTID, Words).
|
lists:member(?PH_USERNAME, Words) orelse lists:member(?PH_CLIENTID, Words).
|
||||||
|
|
||||||
atom(B) when is_binary(B) ->
|
atom(B) when is_binary(B) ->
|
||||||
try binary_to_existing_atom(B, utf8)
|
try
|
||||||
|
binary_to_existing_atom(B, utf8)
|
||||||
catch
|
catch
|
||||||
_E:_S -> binary_to_atom(B)
|
_E:_S -> binary_to_atom(B)
|
||||||
end;
|
end;
|
||||||
|
|
@ -105,21 +122,24 @@ bin(L) when is_list(L) ->
|
||||||
bin(B) when is_binary(B) ->
|
bin(B) when is_binary(B) ->
|
||||||
B.
|
B.
|
||||||
|
|
||||||
-spec(matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()])
|
-spec matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()]) ->
|
||||||
-> {matched, allow} | {matched, deny} | nomatch).
|
{matched, allow} | {matched, deny} | nomatch.
|
||||||
matches(_Client, _PubSub, _Topic, []) -> nomatch;
|
matches(_Client, _PubSub, _Topic, []) ->
|
||||||
|
nomatch;
|
||||||
matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) ->
|
matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) ->
|
||||||
case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of
|
case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of
|
||||||
nomatch -> matches(Client, PubSub, Topic, Tail);
|
nomatch -> matches(Client, PubSub, Topic, Tail);
|
||||||
Matched -> Matched
|
Matched -> Matched
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec(match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule())
|
-spec match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule()) ->
|
||||||
-> {matched, allow} | {matched, deny} | nomatch).
|
{matched, allow} | {matched, deny} | nomatch.
|
||||||
match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) ->
|
match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) ->
|
||||||
case match_action(PubSub, Action) andalso
|
case
|
||||||
match_who(Client, Who) andalso
|
match_action(PubSub, Action) andalso
|
||||||
match_topics(Client, Topic, TopicFilters) of
|
match_who(Client, Who) andalso
|
||||||
|
match_topics(Client, Topic, TopicFilters)
|
||||||
|
of
|
||||||
true -> {matched, Permission};
|
true -> {matched, Permission};
|
||||||
_ -> nomatch
|
_ -> nomatch
|
||||||
end.
|
end.
|
||||||
|
|
@ -129,16 +149,19 @@ match_action(subscribe, subscribe) -> true;
|
||||||
match_action(_, all) -> true;
|
match_action(_, all) -> true;
|
||||||
match_action(_, _) -> false.
|
match_action(_, _) -> false.
|
||||||
|
|
||||||
match_who(_, all) -> true;
|
match_who(_, all) ->
|
||||||
|
true;
|
||||||
match_who(#{username := undefined}, {username, _}) ->
|
match_who(#{username := undefined}, {username, _}) ->
|
||||||
false;
|
false;
|
||||||
match_who(#{username := Username}, {username, {eq, Username}}) -> true;
|
match_who(#{username := Username}, {username, {eq, Username}}) ->
|
||||||
|
true;
|
||||||
match_who(#{username := Username}, {username, {re_pattern, _, _, _, _} = MP}) ->
|
match_who(#{username := Username}, {username, {re_pattern, _, _, _, _} = MP}) ->
|
||||||
case re:run(Username, MP) of
|
case re:run(Username, MP) of
|
||||||
{match, _} -> true;
|
{match, _} -> true;
|
||||||
_ -> false
|
_ -> false
|
||||||
end;
|
end;
|
||||||
match_who(#{clientid := Clientid}, {clientid, {eq, Clientid}}) -> true;
|
match_who(#{clientid := Clientid}, {clientid, {eq, Clientid}}) ->
|
||||||
|
true;
|
||||||
match_who(#{clientid := Clientid}, {clientid, {re_pattern, _, _, _, _} = MP}) ->
|
match_who(#{clientid := Clientid}, {clientid, {re_pattern, _, _, _, _} = MP}) ->
|
||||||
case re:run(Clientid, MP) of
|
case re:run(Clientid, MP) of
|
||||||
{match, _} -> true;
|
{match, _} -> true;
|
||||||
|
|
@ -151,28 +174,40 @@ match_who(#{peerhost := IpAddress}, {ipaddr, CIDR}) ->
|
||||||
match_who(#{peerhost := undefined}, {ipaddrs, _CIDR}) ->
|
match_who(#{peerhost := undefined}, {ipaddrs, _CIDR}) ->
|
||||||
false;
|
false;
|
||||||
match_who(#{peerhost := IpAddress}, {ipaddrs, CIDRs}) ->
|
match_who(#{peerhost := IpAddress}, {ipaddrs, CIDRs}) ->
|
||||||
lists:any(fun(CIDR) ->
|
lists:any(
|
||||||
esockd_cidr:match(IpAddress, CIDR)
|
fun(CIDR) ->
|
||||||
end, CIDRs);
|
esockd_cidr:match(IpAddress, CIDR)
|
||||||
|
end,
|
||||||
|
CIDRs
|
||||||
|
);
|
||||||
match_who(ClientInfo, {'and', Principals}) when is_list(Principals) ->
|
match_who(ClientInfo, {'and', Principals}) when is_list(Principals) ->
|
||||||
lists:foldl(fun(Principal, Permission) ->
|
lists:foldl(
|
||||||
match_who(ClientInfo, Principal) andalso Permission
|
fun(Principal, Permission) ->
|
||||||
end, true, Principals);
|
match_who(ClientInfo, Principal) andalso Permission
|
||||||
|
end,
|
||||||
|
true,
|
||||||
|
Principals
|
||||||
|
);
|
||||||
match_who(ClientInfo, {'or', Principals}) when is_list(Principals) ->
|
match_who(ClientInfo, {'or', Principals}) when is_list(Principals) ->
|
||||||
lists:foldl(fun(Principal, Permission) ->
|
lists:foldl(
|
||||||
match_who(ClientInfo, Principal) orelse Permission
|
fun(Principal, Permission) ->
|
||||||
end, false, Principals);
|
match_who(ClientInfo, Principal) orelse Permission
|
||||||
match_who(_, _) -> false.
|
end,
|
||||||
|
false,
|
||||||
|
Principals
|
||||||
|
);
|
||||||
|
match_who(_, _) ->
|
||||||
|
false.
|
||||||
|
|
||||||
match_topics(_ClientInfo, _Topic, []) ->
|
match_topics(_ClientInfo, _Topic, []) ->
|
||||||
false;
|
false;
|
||||||
match_topics(ClientInfo, Topic, [{pattern, PatternFilter} | Filters]) ->
|
match_topics(ClientInfo, Topic, [{pattern, PatternFilter} | Filters]) ->
|
||||||
TopicFilter = feed_var(ClientInfo, PatternFilter),
|
TopicFilter = feed_var(ClientInfo, PatternFilter),
|
||||||
match_topic(emqx_topic:words(Topic), TopicFilter)
|
match_topic(emqx_topic:words(Topic), TopicFilter) orelse
|
||||||
orelse match_topics(ClientInfo, Topic, Filters);
|
match_topics(ClientInfo, Topic, Filters);
|
||||||
match_topics(ClientInfo, Topic, [TopicFilter | Filters]) ->
|
match_topics(ClientInfo, Topic, [TopicFilter | Filters]) ->
|
||||||
match_topic(emqx_topic:words(Topic), TopicFilter)
|
match_topic(emqx_topic:words(Topic), TopicFilter) orelse
|
||||||
orelse match_topics(ClientInfo, Topic, Filters).
|
match_topics(ClientInfo, Topic, Filters).
|
||||||
|
|
||||||
match_topic(Topic, {'eq', TopicFilter}) ->
|
match_topic(Topic, {'eq', TopicFilter}) ->
|
||||||
Topic =:= TopicFilter;
|
Topic =:= TopicFilter;
|
||||||
|
|
|
||||||
|
|
@ -19,22 +19,25 @@
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||||
|
|
||||||
-reflect_type([ permission/0
|
-reflect_type([
|
||||||
, action/0
|
permission/0,
|
||||||
]).
|
action/0
|
||||||
|
]).
|
||||||
|
|
||||||
-type action() :: publish | subscribe | all.
|
-type action() :: publish | subscribe | all.
|
||||||
-type permission() :: allow | deny.
|
-type permission() :: allow | deny.
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
, validations/0
|
fields/1,
|
||||||
]).
|
validations/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ headers_no_content_type/1
|
-export([
|
||||||
, headers/1
|
headers_no_content_type/1,
|
||||||
]).
|
headers/1
|
||||||
|
]).
|
||||||
|
|
||||||
-import(emqx_schema, [mk_duration/2]).
|
-import(emqx_schema, [mk_duration/2]).
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
|
@ -50,73 +53,84 @@ namespace() -> authz.
|
||||||
roots() -> [].
|
roots() -> [].
|
||||||
|
|
||||||
fields("authorization") ->
|
fields("authorization") ->
|
||||||
[ {sources, #{type => union_array(
|
[
|
||||||
[ hoconsc:ref(?MODULE, file)
|
{sources, #{
|
||||||
, hoconsc:ref(?MODULE, http_get)
|
type => union_array(
|
||||||
, hoconsc:ref(?MODULE, http_post)
|
[
|
||||||
, hoconsc:ref(?MODULE, mnesia)
|
hoconsc:ref(?MODULE, file),
|
||||||
, hoconsc:ref(?MODULE, mongo_single)
|
hoconsc:ref(?MODULE, http_get),
|
||||||
, hoconsc:ref(?MODULE, mongo_rs)
|
hoconsc:ref(?MODULE, http_post),
|
||||||
, hoconsc:ref(?MODULE, mongo_sharded)
|
hoconsc:ref(?MODULE, mnesia),
|
||||||
, hoconsc:ref(?MODULE, mysql)
|
hoconsc:ref(?MODULE, mongo_single),
|
||||||
, hoconsc:ref(?MODULE, postgresql)
|
hoconsc:ref(?MODULE, mongo_rs),
|
||||||
, hoconsc:ref(?MODULE, redis_single)
|
hoconsc:ref(?MODULE, mongo_sharded),
|
||||||
, hoconsc:ref(?MODULE, redis_sentinel)
|
hoconsc:ref(?MODULE, mysql),
|
||||||
, hoconsc:ref(?MODULE, redis_cluster)
|
hoconsc:ref(?MODULE, postgresql),
|
||||||
]),
|
hoconsc:ref(?MODULE, redis_single),
|
||||||
default => [],
|
hoconsc:ref(?MODULE, redis_sentinel),
|
||||||
desc =>
|
hoconsc:ref(?MODULE, redis_cluster)
|
||||||
"
|
]
|
||||||
Authorization data sources.<br>
|
),
|
||||||
An array of authorization (ACL) data providers.
|
default => [],
|
||||||
It is designed as an array, not a hash-map, so the sources can be
|
desc =>
|
||||||
ordered to form a chain of access controls.<br>
|
"\n"
|
||||||
|
"Authorization data sources.<br>\n"
|
||||||
When authorizing a 'publish' or 'subscribe' action, the configured
|
"An array of authorization (ACL) data providers.\n"
|
||||||
sources are checked in order. When checking an ACL source,
|
"It is designed as an array, not a hash-map, so the sources can be\n"
|
||||||
in case the client (identified by username or client ID) is not found,
|
"ordered to form a chain of access controls.<br>\n"
|
||||||
it moves on to the next source. And it stops immediately
|
"\n"
|
||||||
once an 'allow' or 'deny' decision is returned.<br>
|
"When authorizing a 'publish' or 'subscribe' action, the configured\n"
|
||||||
|
"sources are checked in order. When checking an ACL source,\n"
|
||||||
If the client is not found in any of the sources,
|
"in case the client (identified by username or client ID) is not found,\n"
|
||||||
the default action configured in 'authorization.no_match' is applied.<br>
|
"it moves on to the next source. And it stops immediately\n"
|
||||||
|
"once an 'allow' or 'deny' decision is returned.<br>\n"
|
||||||
NOTE:
|
"\n"
|
||||||
The source elements are identified by their 'type'.
|
"If the client is not found in any of the sources,\n"
|
||||||
It is NOT allowed to configure two or more sources of the same type.
|
"the default action configured in 'authorization.no_match' is applied.<br>\n"
|
||||||
"
|
"\n"
|
||||||
}
|
"NOTE:\n"
|
||||||
}
|
"The source elements are identified by their 'type'.\n"
|
||||||
|
"It is NOT allowed to configure two or more sources of the same type.\n"
|
||||||
|
}}
|
||||||
];
|
];
|
||||||
fields(file) ->
|
fields(file) ->
|
||||||
[ {type, #{type => file}}
|
[
|
||||||
, {enable, #{type => boolean(),
|
{type, #{type => file}},
|
||||||
default => true}}
|
{enable, #{
|
||||||
, {path, #{type => string(),
|
type => boolean(),
|
||||||
required => true,
|
default => true
|
||||||
desc => "
|
}},
|
||||||
Path to the file which contains the ACL rules.<br>
|
{path, #{
|
||||||
If the file provisioned before starting EMQX node,
|
type => string(),
|
||||||
it can be placed anywhere as long as EMQX has read access to it.
|
required => true,
|
||||||
|
desc =>
|
||||||
In case the rule-set is created from EMQX dashboard or management API,
|
"\n"
|
||||||
the file will be placed in `authz` subdirectory inside EMQX's `data_dir`,
|
"Path to the file which contains the ACL rules.<br>\n"
|
||||||
and the new rules will override all rules from the old config file.
|
"If the file provisioned before starting EMQX node,\n"
|
||||||
"
|
"it can be placed anywhere as long as EMQX has read access to it.\n"
|
||||||
}}
|
"\n"
|
||||||
|
"In case the rule-set is created from EMQX dashboard or management API,\n"
|
||||||
|
"the file will be placed in `authz` subdirectory inside EMQX's `data_dir`,\n"
|
||||||
|
"and the new rules will override all rules from the old config file.\n"
|
||||||
|
}}
|
||||||
];
|
];
|
||||||
fields(http_get) ->
|
fields(http_get) ->
|
||||||
[ {method, #{type => get, default => post}}
|
[
|
||||||
, {headers, fun headers_no_content_type/1}
|
{method, #{type => get, default => post}},
|
||||||
|
{headers, fun headers_no_content_type/1}
|
||||||
] ++ http_common_fields();
|
] ++ http_common_fields();
|
||||||
fields(http_post) ->
|
fields(http_post) ->
|
||||||
[ {method, #{type => post, default => post}}
|
[
|
||||||
, {headers, fun headers/1}
|
{method, #{type => post, default => post}},
|
||||||
|
{headers, fun headers/1}
|
||||||
] ++ http_common_fields();
|
] ++ http_common_fields();
|
||||||
fields(mnesia) ->
|
fields(mnesia) ->
|
||||||
[ {type, #{type => 'built_in_database'}}
|
[
|
||||||
, {enable, #{type => boolean(),
|
{type, #{type => 'built_in_database'}},
|
||||||
default => true}}
|
{enable, #{
|
||||||
|
type => boolean(),
|
||||||
|
default => true
|
||||||
|
}}
|
||||||
];
|
];
|
||||||
fields(mongo_single) ->
|
fields(mongo_single) ->
|
||||||
mongo_common_fields() ++ emqx_connector_mongo:fields(single);
|
mongo_common_fields() ++ emqx_connector_mongo:fields(single);
|
||||||
|
|
@ -126,62 +140,88 @@ fields(mongo_sharded) ->
|
||||||
mongo_common_fields() ++ emqx_connector_mongo:fields(sharded);
|
mongo_common_fields() ++ emqx_connector_mongo:fields(sharded);
|
||||||
fields(mysql) ->
|
fields(mysql) ->
|
||||||
connector_fields(mysql) ++
|
connector_fields(mysql) ++
|
||||||
[ {query, query()} ];
|
[{query, query()}];
|
||||||
fields(postgresql) ->
|
fields(postgresql) ->
|
||||||
[ {query, query()}
|
[
|
||||||
, {type, #{type => postgresql}}
|
{query, query()},
|
||||||
, {enable, #{type => boolean(),
|
{type, #{type => postgresql}},
|
||||||
default => true}}
|
{enable, #{
|
||||||
|
type => boolean(),
|
||||||
|
default => true
|
||||||
|
}}
|
||||||
] ++ emqx_connector_pgsql:fields(config);
|
] ++ emqx_connector_pgsql:fields(config);
|
||||||
fields(redis_single) ->
|
fields(redis_single) ->
|
||||||
connector_fields(redis, single) ++
|
connector_fields(redis, single) ++
|
||||||
[ {cmd, query()} ];
|
[{cmd, query()}];
|
||||||
fields(redis_sentinel) ->
|
fields(redis_sentinel) ->
|
||||||
connector_fields(redis, sentinel) ++
|
connector_fields(redis, sentinel) ++
|
||||||
[ {cmd, query()} ];
|
[{cmd, query()}];
|
||||||
fields(redis_cluster) ->
|
fields(redis_cluster) ->
|
||||||
connector_fields(redis, cluster) ++
|
connector_fields(redis, cluster) ++
|
||||||
[ {cmd, query()} ].
|
[{cmd, query()}].
|
||||||
|
|
||||||
http_common_fields() ->
|
http_common_fields() ->
|
||||||
[ {url, fun url/1}
|
[
|
||||||
, {request_timeout, mk_duration("Request timeout", #{default => "30s", desc => "Request timeout."})}
|
{url, fun url/1},
|
||||||
, {body, #{type => map(), required => false, desc => "HTTP request body."}}
|
{request_timeout,
|
||||||
] ++ maps:to_list(maps:without([ base_url
|
mk_duration("Request timeout", #{default => "30s", desc => "Request timeout."})},
|
||||||
, pool_type],
|
{body, #{type => map(), required => false, desc => "HTTP request body."}}
|
||||||
maps:from_list(connector_fields(http)))).
|
] ++
|
||||||
|
maps:to_list(
|
||||||
|
maps:without(
|
||||||
|
[
|
||||||
|
base_url,
|
||||||
|
pool_type
|
||||||
|
],
|
||||||
|
maps:from_list(connector_fields(http))
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
mongo_common_fields() ->
|
mongo_common_fields() ->
|
||||||
[ {collection, #{type => atom(), desc => "`MongoDB` collection containing the authorization data."}}
|
[
|
||||||
, {selector, #{type => map(), desc => "MQL query used to select the authorization record."}}
|
{collection, #{
|
||||||
, {type, #{type => mongodb, desc => "Database backend."}}
|
type => atom(), desc => "`MongoDB` collection containing the authorization data."
|
||||||
, {enable, #{type => boolean(),
|
}},
|
||||||
default => true,
|
{selector, #{type => map(), desc => "MQL query used to select the authorization record."}},
|
||||||
desc => "Enable or disable the backend."}}
|
{type, #{type => mongodb, desc => "Database backend."}},
|
||||||
|
{enable, #{
|
||||||
|
type => boolean(),
|
||||||
|
default => true,
|
||||||
|
desc => "Enable or disable the backend."
|
||||||
|
}}
|
||||||
].
|
].
|
||||||
|
|
||||||
validations() ->
|
validations() ->
|
||||||
[ {check_ssl_opts, fun check_ssl_opts/1}
|
[
|
||||||
, {check_headers, fun check_headers/1}
|
{check_ssl_opts, fun check_ssl_opts/1},
|
||||||
|
{check_headers, fun check_headers/1}
|
||||||
].
|
].
|
||||||
|
|
||||||
headers(type) -> list({binary(), binary()});
|
headers(type) ->
|
||||||
headers(desc) -> "List of HTTP headers.";
|
list({binary(), binary()});
|
||||||
|
headers(desc) ->
|
||||||
|
"List of HTTP headers.";
|
||||||
headers(converter) ->
|
headers(converter) ->
|
||||||
fun(Headers) ->
|
fun(Headers) ->
|
||||||
maps:to_list(maps:merge(default_headers(), transform_header_name(Headers)))
|
maps:to_list(maps:merge(default_headers(), transform_header_name(Headers)))
|
||||||
end;
|
end;
|
||||||
headers(default) -> default_headers();
|
headers(default) ->
|
||||||
headers(_) -> undefined.
|
default_headers();
|
||||||
|
headers(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
headers_no_content_type(type) -> list({binary(), binary()});
|
headers_no_content_type(type) ->
|
||||||
headers_no_content_type(desc) -> "List of HTTP headers.";
|
list({binary(), binary()});
|
||||||
|
headers_no_content_type(desc) ->
|
||||||
|
"List of HTTP headers.";
|
||||||
headers_no_content_type(converter) ->
|
headers_no_content_type(converter) ->
|
||||||
fun(Headers) ->
|
fun(Headers) ->
|
||||||
maps:to_list(maps:merge(default_headers_no_content_type(), transform_header_name(Headers)))
|
maps:to_list(maps:merge(default_headers_no_content_type(), transform_header_name(Headers)))
|
||||||
end;
|
end;
|
||||||
headers_no_content_type(default) -> default_headers_no_content_type();
|
headers_no_content_type(default) ->
|
||||||
headers_no_content_type(_) -> undefined.
|
default_headers_no_content_type();
|
||||||
|
headers_no_content_type(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
url(type) -> binary();
|
url(type) -> binary();
|
||||||
url(desc) -> "URL of the auth server.";
|
url(desc) -> "URL of the auth server.";
|
||||||
|
|
@ -194,26 +234,34 @@ url(_) -> undefined.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
default_headers() ->
|
default_headers() ->
|
||||||
maps:put(<<"content-type">>,
|
maps:put(
|
||||||
<<"application/json">>,
|
<<"content-type">>,
|
||||||
default_headers_no_content_type()).
|
<<"application/json">>,
|
||||||
|
default_headers_no_content_type()
|
||||||
|
).
|
||||||
|
|
||||||
default_headers_no_content_type() ->
|
default_headers_no_content_type() ->
|
||||||
#{ <<"accept">> => <<"application/json">>
|
#{
|
||||||
, <<"cache-control">> => <<"no-cache">>
|
<<"accept">> => <<"application/json">>,
|
||||||
, <<"connection">> => <<"keep-alive">>
|
<<"cache-control">> => <<"no-cache">>,
|
||||||
, <<"keep-alive">> => <<"timeout=30, max=1000">>
|
<<"connection">> => <<"keep-alive">>,
|
||||||
}.
|
<<"keep-alive">> => <<"timeout=30, max=1000">>
|
||||||
|
}.
|
||||||
|
|
||||||
transform_header_name(Headers) ->
|
transform_header_name(Headers) ->
|
||||||
maps:fold(fun(K0, V, Acc) ->
|
maps:fold(
|
||||||
K = list_to_binary(string:to_lower(to_list(K0))),
|
fun(K0, V, Acc) ->
|
||||||
maps:put(K, V, Acc)
|
K = list_to_binary(string:to_lower(to_list(K0))),
|
||||||
end, #{}, Headers).
|
maps:put(K, V, Acc)
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Headers
|
||||||
|
).
|
||||||
|
|
||||||
check_ssl_opts(Conf) ->
|
check_ssl_opts(Conf) ->
|
||||||
case hocon_maps:get("config.url", Conf) of
|
case hocon_maps:get("config.url", Conf) of
|
||||||
undefined -> true;
|
undefined ->
|
||||||
|
true;
|
||||||
Url ->
|
Url ->
|
||||||
case emqx_authz_http:parse_url(Url) of
|
case emqx_authz_http:parse_url(Url) of
|
||||||
#{scheme := https} ->
|
#{scheme := https} ->
|
||||||
|
|
@ -221,19 +269,23 @@ check_ssl_opts(Conf) ->
|
||||||
true -> true;
|
true -> true;
|
||||||
_ -> {error, ssl_not_enable}
|
_ -> {error, ssl_not_enable}
|
||||||
end;
|
end;
|
||||||
#{scheme := http} -> true;
|
#{scheme := http} ->
|
||||||
Bad -> {bad_scheme, Url, Bad}
|
true;
|
||||||
|
Bad ->
|
||||||
|
{bad_scheme, Url, Bad}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_headers(Conf) ->
|
check_headers(Conf) ->
|
||||||
case hocon_maps:get("config.method", Conf) of
|
case hocon_maps:get("config.method", Conf) of
|
||||||
undefined -> true;
|
undefined ->
|
||||||
|
true;
|
||||||
Method0 ->
|
Method0 ->
|
||||||
Method = to_bin(Method0),
|
Method = to_bin(Method0),
|
||||||
Headers = hocon_maps:get("config.headers", Conf),
|
Headers = hocon_maps:get("config.headers", Conf),
|
||||||
case Method of
|
case Method of
|
||||||
<<"post">> -> true;
|
<<"post">> ->
|
||||||
|
true;
|
||||||
_ when Headers =:= undefined -> true;
|
_ when Headers =:= undefined -> true;
|
||||||
_ when is_list(Headers) ->
|
_ when is_list(Headers) ->
|
||||||
case lists:member(<<"content-type">>, Headers) of
|
case lists:member(<<"content-type">>, Headers) of
|
||||||
|
|
@ -247,32 +299,37 @@ union_array(Item) when is_list(Item) ->
|
||||||
hoconsc:array(hoconsc:union(Item)).
|
hoconsc:array(hoconsc:union(Item)).
|
||||||
|
|
||||||
query() ->
|
query() ->
|
||||||
#{type => binary(),
|
#{
|
||||||
desc => "",
|
type => binary(),
|
||||||
validator => fun(S) ->
|
desc => "",
|
||||||
case size(S) > 0 of
|
validator => fun(S) ->
|
||||||
true -> ok;
|
case size(S) > 0 of
|
||||||
_ -> {error, "Request query"}
|
true -> ok;
|
||||||
end
|
_ -> {error, "Request query"}
|
||||||
end
|
end
|
||||||
}.
|
end
|
||||||
|
}.
|
||||||
|
|
||||||
connector_fields(DB) ->
|
connector_fields(DB) ->
|
||||||
connector_fields(DB, config).
|
connector_fields(DB, config).
|
||||||
connector_fields(DB, Fields) ->
|
connector_fields(DB, Fields) ->
|
||||||
Mod0 = io_lib:format("~ts_~ts",[emqx_connector, DB]),
|
Mod0 = io_lib:format("~ts_~ts", [emqx_connector, DB]),
|
||||||
Mod = try
|
Mod =
|
||||||
list_to_existing_atom(Mod0)
|
try
|
||||||
catch
|
list_to_existing_atom(Mod0)
|
||||||
error:badarg ->
|
catch
|
||||||
list_to_atom(Mod0);
|
error:badarg ->
|
||||||
error:Reason ->
|
list_to_atom(Mod0);
|
||||||
erlang:error(Reason)
|
error:Reason ->
|
||||||
end,
|
erlang:error(Reason)
|
||||||
[ {type, #{type => DB, desc => "Database backend."}}
|
end,
|
||||||
, {enable, #{type => boolean(),
|
[
|
||||||
default => true,
|
{type, #{type => DB, desc => "Database backend."}},
|
||||||
desc => "Enable or disable the backend."}}
|
{enable, #{
|
||||||
|
type => boolean(),
|
||||||
|
default => true,
|
||||||
|
desc => "Enable or disable the backend."
|
||||||
|
}}
|
||||||
] ++ erlang:apply(Mod, fields, [Fields]).
|
] ++ erlang:apply(Mod, fields, [Fields]).
|
||||||
|
|
||||||
to_list(A) when is_atom(A) ->
|
to_list(A) when is_atom(A) ->
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,10 @@ start_link() ->
|
||||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
SupFlags = #{strategy => one_for_all,
|
SupFlags = #{
|
||||||
intensity => 0,
|
strategy => one_for_all,
|
||||||
period => 1},
|
intensity => 0,
|
||||||
|
period => 1
|
||||||
|
},
|
||||||
ChildSpecs = [],
|
ChildSpecs = [],
|
||||||
{ok, {SupFlags, ChildSpecs}}.
|
{ok, {SupFlags, ChildSpecs}}.
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,16 @@
|
||||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||||
-include_lib("emqx_authz.hrl").
|
-include_lib("emqx_authz.hrl").
|
||||||
|
|
||||||
-export([ cleanup_resources/0
|
-export([
|
||||||
, make_resource_id/1
|
cleanup_resources/0,
|
||||||
, create_resource/2
|
make_resource_id/1,
|
||||||
, update_config/2
|
create_resource/2,
|
||||||
, parse_deep/2
|
update_config/2,
|
||||||
, parse_sql/3
|
parse_deep/2,
|
||||||
, render_deep/2
|
parse_sql/3,
|
||||||
, render_sql_params/2
|
render_deep/2,
|
||||||
]).
|
render_sql_params/2
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
|
|
@ -35,10 +36,15 @@
|
||||||
|
|
||||||
create_resource(Module, Config) ->
|
create_resource(Module, Config) ->
|
||||||
ResourceID = make_resource_id(Module),
|
ResourceID = make_resource_id(Module),
|
||||||
case emqx_resource:create_local(ResourceID,
|
case
|
||||||
?RESOURCE_GROUP,
|
emqx_resource:create_local(
|
||||||
Module, Config,
|
ResourceID,
|
||||||
#{}) of
|
?RESOURCE_GROUP,
|
||||||
|
Module,
|
||||||
|
Config,
|
||||||
|
#{}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, already_created} -> {ok, ResourceID};
|
{ok, already_created} -> {ok, ResourceID};
|
||||||
{ok, _} -> {ok, ResourceID};
|
{ok, _} -> {ok, ResourceID};
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
|
|
@ -46,37 +52,45 @@ create_resource(Module, Config) ->
|
||||||
|
|
||||||
cleanup_resources() ->
|
cleanup_resources() ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun emqx_resource:remove_local/1,
|
fun emqx_resource:remove_local/1,
|
||||||
emqx_resource:list_group_instances(?RESOURCE_GROUP)).
|
emqx_resource:list_group_instances(?RESOURCE_GROUP)
|
||||||
|
).
|
||||||
|
|
||||||
make_resource_id(Name) ->
|
make_resource_id(Name) ->
|
||||||
NameBin = bin(Name),
|
NameBin = bin(Name),
|
||||||
emqx_resource:generate_id(NameBin).
|
emqx_resource:generate_id(NameBin).
|
||||||
|
|
||||||
update_config(Path, ConfigRequest) ->
|
update_config(Path, ConfigRequest) ->
|
||||||
emqx_conf:update(Path, ConfigRequest, #{rawconf_with_defaults => true,
|
emqx_conf:update(Path, ConfigRequest, #{
|
||||||
override_to => cluster}).
|
rawconf_with_defaults => true,
|
||||||
|
override_to => cluster
|
||||||
|
}).
|
||||||
|
|
||||||
parse_deep(Template, PlaceHolders) ->
|
parse_deep(Template, PlaceHolders) ->
|
||||||
emqx_placeholder:preproc_tmpl_deep(Template, #{placeholders => PlaceHolders}).
|
emqx_placeholder:preproc_tmpl_deep(Template, #{placeholders => PlaceHolders}).
|
||||||
|
|
||||||
parse_sql(Template, ReplaceWith, PlaceHolders) ->
|
parse_sql(Template, ReplaceWith, PlaceHolders) ->
|
||||||
emqx_placeholder:preproc_sql(
|
emqx_placeholder:preproc_sql(
|
||||||
Template,
|
Template,
|
||||||
#{replace_with => ReplaceWith,
|
#{
|
||||||
placeholders => PlaceHolders}).
|
replace_with => ReplaceWith,
|
||||||
|
placeholders => PlaceHolders
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
render_deep(Template, Values) ->
|
render_deep(Template, Values) ->
|
||||||
emqx_placeholder:proc_tmpl_deep(
|
emqx_placeholder:proc_tmpl_deep(
|
||||||
Template,
|
Template,
|
||||||
client_vars(Values),
|
client_vars(Values),
|
||||||
#{return => full_binary, var_trans => fun handle_var/2}).
|
#{return => full_binary, var_trans => fun handle_var/2}
|
||||||
|
).
|
||||||
|
|
||||||
render_sql_params(ParamList, Values) ->
|
render_sql_params(ParamList, Values) ->
|
||||||
emqx_placeholder:proc_tmpl(
|
emqx_placeholder:proc_tmpl(
|
||||||
ParamList,
|
ParamList,
|
||||||
client_vars(Values),
|
client_vars(Values),
|
||||||
#{return => rawlist, var_trans => fun handle_sql_var/2}).
|
#{return => rawlist, var_trans => fun handle_sql_var/2}
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
|
|
@ -84,9 +98,11 @@ render_sql_params(ParamList, Values) ->
|
||||||
|
|
||||||
client_vars(ClientInfo) ->
|
client_vars(ClientInfo) ->
|
||||||
maps:from_list(
|
maps:from_list(
|
||||||
lists:map(
|
lists:map(
|
||||||
fun convert_client_var/1,
|
fun convert_client_var/1,
|
||||||
maps:to_list(ClientInfo))).
|
maps:to_list(ClientInfo)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
convert_client_var({cn, CN}) -> {cert_common_name, CN};
|
convert_client_var({cn, CN}) -> {cert_common_name, CN};
|
||||||
convert_client_var({dn, DN}) -> {cert_subject, DN};
|
convert_client_var({dn, DN}) -> {cert_subject, DN};
|
||||||
|
|
|
||||||
|
|
@ -33,22 +33,29 @@ init_per_suite(Config) ->
|
||||||
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
||||||
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
|
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
|
||||||
meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
|
meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
|
||||||
meck:expect(emqx_authz, acl_conf_file,
|
meck:expect(
|
||||||
fun() ->
|
emqx_authz,
|
||||||
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
|
acl_conf_file,
|
||||||
end),
|
fun() ->
|
||||||
|
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_connector, emqx_conf, emqx_authz],
|
[emqx_connector, emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1),
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
[authorization],
|
[authorization],
|
||||||
#{<<"no_match">> => <<"allow">>,
|
#{
|
||||||
<<"cache">> => #{<<"enable">> => <<"true">>},
|
<<"no_match">> => <<"allow">>,
|
||||||
<<"sources">> => []}),
|
<<"cache">> => #{<<"enable">> => <<"true">>},
|
||||||
|
<<"sources">> => []
|
||||||
|
}
|
||||||
|
),
|
||||||
ok = stop_apps([emqx_resource]),
|
ok = stop_apps([emqx_resource]),
|
||||||
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
|
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
|
||||||
meck:unload(emqx_resource),
|
meck:unload(emqx_resource),
|
||||||
|
|
@ -66,64 +73,71 @@ set_special_configs(emqx_authz) ->
|
||||||
set_special_configs(_App) ->
|
set_special_configs(_App) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
-define(SOURCE1, #{<<"type">> => <<"http">>,
|
-define(SOURCE1, #{
|
||||||
<<"enable">> => true,
|
<<"type">> => <<"http">>,
|
||||||
<<"url">> => <<"https://example.com:443/a/b?c=d">>,
|
<<"enable">> => true,
|
||||||
<<"headers">> => #{},
|
<<"url">> => <<"https://example.com:443/a/b?c=d">>,
|
||||||
<<"method">> => <<"get">>,
|
<<"headers">> => #{},
|
||||||
<<"request_timeout">> => 5000
|
<<"method">> => <<"get">>,
|
||||||
}).
|
<<"request_timeout">> => 5000
|
||||||
-define(SOURCE2, #{<<"type">> => <<"mongodb">>,
|
}).
|
||||||
<<"enable">> => true,
|
-define(SOURCE2, #{
|
||||||
<<"mongo_type">> => <<"single">>,
|
<<"type">> => <<"mongodb">>,
|
||||||
<<"server">> => <<"127.0.0.1:27017">>,
|
<<"enable">> => true,
|
||||||
<<"w_mode">> => <<"unsafe">>,
|
<<"mongo_type">> => <<"single">>,
|
||||||
<<"pool_size">> => 1,
|
<<"server">> => <<"127.0.0.1:27017">>,
|
||||||
<<"database">> => <<"mqtt">>,
|
<<"w_mode">> => <<"unsafe">>,
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"pool_size">> => 1,
|
||||||
<<"collection">> => <<"authz">>,
|
<<"database">> => <<"mqtt">>,
|
||||||
<<"selector">> => #{<<"a">> => <<"b">>}
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
}).
|
<<"collection">> => <<"authz">>,
|
||||||
-define(SOURCE3, #{<<"type">> => <<"mysql">>,
|
<<"selector">> => #{<<"a">> => <<"b">>}
|
||||||
<<"enable">> => true,
|
}).
|
||||||
<<"server">> => <<"127.0.0.1:27017">>,
|
-define(SOURCE3, #{
|
||||||
<<"pool_size">> => 1,
|
<<"type">> => <<"mysql">>,
|
||||||
<<"database">> => <<"mqtt">>,
|
<<"enable">> => true,
|
||||||
<<"username">> => <<"xx">>,
|
<<"server">> => <<"127.0.0.1:27017">>,
|
||||||
<<"password">> => <<"ee">>,
|
<<"pool_size">> => 1,
|
||||||
<<"auto_reconnect">> => true,
|
<<"database">> => <<"mqtt">>,
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"username">> => <<"xx">>,
|
||||||
<<"query">> => <<"abcb">>
|
<<"password">> => <<"ee">>,
|
||||||
}).
|
<<"auto_reconnect">> => true,
|
||||||
-define(SOURCE4, #{<<"type">> => <<"postgresql">>,
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
<<"enable">> => true,
|
<<"query">> => <<"abcb">>
|
||||||
<<"server">> => <<"127.0.0.1:27017">>,
|
}).
|
||||||
<<"pool_size">> => 1,
|
-define(SOURCE4, #{
|
||||||
<<"database">> => <<"mqtt">>,
|
<<"type">> => <<"postgresql">>,
|
||||||
<<"username">> => <<"xx">>,
|
<<"enable">> => true,
|
||||||
<<"password">> => <<"ee">>,
|
<<"server">> => <<"127.0.0.1:27017">>,
|
||||||
<<"auto_reconnect">> => true,
|
<<"pool_size">> => 1,
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"database">> => <<"mqtt">>,
|
||||||
<<"query">> => <<"abcb">>
|
<<"username">> => <<"xx">>,
|
||||||
}).
|
<<"password">> => <<"ee">>,
|
||||||
-define(SOURCE5, #{<<"type">> => <<"redis">>,
|
<<"auto_reconnect">> => true,
|
||||||
<<"redis_type">> => <<"single">>,
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
<<"enable">> => true,
|
<<"query">> => <<"abcb">>
|
||||||
<<"server">> => <<"127.0.0.1:27017">>,
|
}).
|
||||||
<<"pool_size">> => 1,
|
-define(SOURCE5, #{
|
||||||
<<"database">> => 0,
|
<<"type">> => <<"redis">>,
|
||||||
<<"password">> => <<"ee">>,
|
<<"redis_type">> => <<"single">>,
|
||||||
<<"auto_reconnect">> => true,
|
<<"enable">> => true,
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"server">> => <<"127.0.0.1:27017">>,
|
||||||
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
|
<<"pool_size">> => 1,
|
||||||
}).
|
<<"database">> => 0,
|
||||||
-define(SOURCE6, #{<<"type">> => <<"file">>,
|
<<"password">> => <<"ee">>,
|
||||||
<<"enable">> => true,
|
<<"auto_reconnect">> => true,
|
||||||
<<"rules">> =>
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
|
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
|
||||||
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
|
}).
|
||||||
}).
|
-define(SOURCE6, #{
|
||||||
|
<<"type">> => <<"file">>,
|
||||||
|
<<"enable">> => true,
|
||||||
|
<<"rules">> =>
|
||||||
|
<<
|
||||||
|
"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
|
||||||
|
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
|
||||||
|
>>
|
||||||
|
}).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Testcases
|
%% Testcases
|
||||||
|
|
@ -138,95 +152,130 @@ t_update_source(_) ->
|
||||||
{ok, _} = emqx_authz:update(?CMD_APPEND, ?SOURCE5),
|
{ok, _} = emqx_authz:update(?CMD_APPEND, ?SOURCE5),
|
||||||
{ok, _} = emqx_authz:update(?CMD_APPEND, ?SOURCE6),
|
{ok, _} = emqx_authz:update(?CMD_APPEND, ?SOURCE6),
|
||||||
|
|
||||||
?assertMatch([ #{type := http, enable := true}
|
?assertMatch(
|
||||||
, #{type := mongodb, enable := true}
|
[
|
||||||
, #{type := mysql, enable := true}
|
#{type := http, enable := true},
|
||||||
, #{type := postgresql, enable := true}
|
#{type := mongodb, enable := true},
|
||||||
, #{type := redis, enable := true}
|
#{type := mysql, enable := true},
|
||||||
, #{type := file, enable := true}
|
#{type := postgresql, enable := true},
|
||||||
], emqx_conf:get([authorization, sources], [])),
|
#{type := redis, enable := true},
|
||||||
|
#{type := file, enable := true}
|
||||||
|
],
|
||||||
|
emqx_conf:get([authorization, sources], [])
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, http}, ?SOURCE1#{<<"enable">> := true}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, http}, ?SOURCE1#{<<"enable">> := true}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, mongodb}, ?SOURCE2#{<<"enable">> := true}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, mongodb}, ?SOURCE2#{<<"enable">> := true}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, mysql}, ?SOURCE3#{<<"enable">> := true}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, mysql}, ?SOURCE3#{<<"enable">> := true}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, postgresql}, ?SOURCE4#{<<"enable">> := true}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, postgresql}, ?SOURCE4#{<<"enable">> := true}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, redis}, ?SOURCE5#{<<"enable">> := true}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, redis}, ?SOURCE5#{<<"enable">> := true}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, file}, ?SOURCE6#{<<"enable">> := true}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, file}, ?SOURCE6#{<<"enable">> := true}),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, http}, ?SOURCE1#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, http}, ?SOURCE1#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, mongodb}, ?SOURCE2#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, mongodb}, ?SOURCE2#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, mysql}, ?SOURCE3#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, mysql}, ?SOURCE3#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, postgresql}, ?SOURCE4#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, postgresql}, ?SOURCE4#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, redis}, ?SOURCE5#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, redis}, ?SOURCE5#{<<"enable">> := false}),
|
||||||
{ok, _} = emqx_authz:update({?CMD_REPLACE, file}, ?SOURCE6#{<<"enable">> := false}),
|
{ok, _} = emqx_authz:update({?CMD_REPLACE, file}, ?SOURCE6#{<<"enable">> := false}),
|
||||||
|
|
||||||
?assertMatch([ #{type := http, enable := false}
|
?assertMatch(
|
||||||
, #{type := mongodb, enable := false}
|
[
|
||||||
, #{type := mysql, enable := false}
|
#{type := http, enable := false},
|
||||||
, #{type := postgresql, enable := false}
|
#{type := mongodb, enable := false},
|
||||||
, #{type := redis, enable := false}
|
#{type := mysql, enable := false},
|
||||||
, #{type := file, enable := false}
|
#{type := postgresql, enable := false},
|
||||||
], emqx_conf:get([authorization, sources], [])),
|
#{type := redis, enable := false},
|
||||||
|
#{type := file, enable := false}
|
||||||
|
],
|
||||||
|
emqx_conf:get([authorization, sources], [])
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:update(?CMD_REPLACE, []).
|
{ok, _} = emqx_authz:update(?CMD_REPLACE, []).
|
||||||
|
|
||||||
t_delete_source(_) ->
|
t_delete_source(_) ->
|
||||||
{ok, _} = emqx_authz:update(?CMD_REPLACE, [?SOURCE1]),
|
{ok, _} = emqx_authz:update(?CMD_REPLACE, [?SOURCE1]),
|
||||||
|
|
||||||
?assertMatch([ #{type := http, enable := true}
|
?assertMatch([#{type := http, enable := true}], emqx_conf:get([authorization, sources], [])),
|
||||||
], emqx_conf:get([authorization, sources], [])),
|
|
||||||
|
|
||||||
{ok, _} = emqx_authz:update({?CMD_DELETE, http}, #{}),
|
{ok, _} = emqx_authz:update({?CMD_DELETE, http}, #{}),
|
||||||
|
|
||||||
?assertMatch([], emqx_conf:get([authorization, sources], [])).
|
?assertMatch([], emqx_conf:get([authorization, sources], [])).
|
||||||
|
|
||||||
t_move_source(_) ->
|
t_move_source(_) ->
|
||||||
{ok, _} = emqx_authz:update(?CMD_REPLACE,
|
{ok, _} = emqx_authz:update(
|
||||||
[?SOURCE1, ?SOURCE2, ?SOURCE3,
|
?CMD_REPLACE,
|
||||||
?SOURCE4, ?SOURCE5, ?SOURCE6]),
|
[
|
||||||
?assertMatch([ #{type := http}
|
?SOURCE1,
|
||||||
, #{type := mongodb}
|
?SOURCE2,
|
||||||
, #{type := mysql}
|
?SOURCE3,
|
||||||
, #{type := postgresql}
|
?SOURCE4,
|
||||||
, #{type := redis}
|
?SOURCE5,
|
||||||
, #{type := file}
|
?SOURCE6
|
||||||
], emqx_authz:lookup()),
|
]
|
||||||
|
),
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
#{type := http},
|
||||||
|
#{type := mongodb},
|
||||||
|
#{type := mysql},
|
||||||
|
#{type := postgresql},
|
||||||
|
#{type := redis},
|
||||||
|
#{type := file}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:move(postgresql, ?CMD_MOVE_FRONT),
|
{ok, _} = emqx_authz:move(postgresql, ?CMD_MOVE_FRONT),
|
||||||
?assertMatch([ #{type := postgresql}
|
?assertMatch(
|
||||||
, #{type := http}
|
[
|
||||||
, #{type := mongodb}
|
#{type := postgresql},
|
||||||
, #{type := mysql}
|
#{type := http},
|
||||||
, #{type := redis}
|
#{type := mongodb},
|
||||||
, #{type := file}
|
#{type := mysql},
|
||||||
], emqx_authz:lookup()),
|
#{type := redis},
|
||||||
|
#{type := file}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:move(http, ?CMD_MOVE_REAR),
|
{ok, _} = emqx_authz:move(http, ?CMD_MOVE_REAR),
|
||||||
?assertMatch([ #{type := postgresql}
|
?assertMatch(
|
||||||
, #{type := mongodb}
|
[
|
||||||
, #{type := mysql}
|
#{type := postgresql},
|
||||||
, #{type := redis}
|
#{type := mongodb},
|
||||||
, #{type := file}
|
#{type := mysql},
|
||||||
, #{type := http}
|
#{type := redis},
|
||||||
], emqx_authz:lookup()),
|
#{type := file},
|
||||||
|
#{type := http}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:move(mysql, ?CMD_MOVE_BEFORE(postgresql)),
|
{ok, _} = emqx_authz:move(mysql, ?CMD_MOVE_BEFORE(postgresql)),
|
||||||
?assertMatch([ #{type := mysql}
|
?assertMatch(
|
||||||
, #{type := postgresql}
|
[
|
||||||
, #{type := mongodb}
|
#{type := mysql},
|
||||||
, #{type := redis}
|
#{type := postgresql},
|
||||||
, #{type := file}
|
#{type := mongodb},
|
||||||
, #{type := http}
|
#{type := redis},
|
||||||
], emqx_authz:lookup()),
|
#{type := file},
|
||||||
|
#{type := http}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_authz:move(mongodb, ?CMD_MOVE_AFTER(http)),
|
{ok, _} = emqx_authz:move(mongodb, ?CMD_MOVE_AFTER(http)),
|
||||||
?assertMatch([ #{type := mysql}
|
?assertMatch(
|
||||||
, #{type := postgresql}
|
[
|
||||||
, #{type := redis}
|
#{type := mysql},
|
||||||
, #{type := file}
|
#{type := postgresql},
|
||||||
, #{type := http}
|
#{type := redis},
|
||||||
, #{type := mongodb}
|
#{type := file},
|
||||||
], emqx_authz:lookup()),
|
#{type := http},
|
||||||
|
#{type := mongodb}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,20 @@ groups() ->
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz, emqx_dashboard],
|
[emqx_conf, emqx_authz, emqx_dashboard],
|
||||||
fun set_special_configs/1),
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
[authorization],
|
[authorization],
|
||||||
#{<<"no_match">> => <<"allow">>,
|
#{
|
||||||
<<"cache">> => #{<<"enable">> => <<"true">>},
|
<<"no_match">> => <<"allow">>,
|
||||||
<<"sources">> => []}),
|
<<"cache">> => #{<<"enable">> => <<"true">>},
|
||||||
|
<<"sources">> => []
|
||||||
|
}
|
||||||
|
),
|
||||||
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
|
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -50,8 +54,10 @@ set_special_configs(emqx_dashboard) ->
|
||||||
set_special_configs(emqx_authz) ->
|
set_special_configs(emqx_authz) ->
|
||||||
{ok, _} = emqx:update_config([authorization, cache, enable], false),
|
{ok, _} = emqx:update_config([authorization, cache, enable], false),
|
||||||
{ok, _} = emqx:update_config([authorization, no_match], deny),
|
{ok, _} = emqx:update_config([authorization, no_match], deny),
|
||||||
{ok, _} = emqx:update_config([authorization, sources],
|
{ok, _} = emqx:update_config(
|
||||||
[#{<<"type">> => <<"built_in_database">>}]),
|
[authorization, sources],
|
||||||
|
[#{<<"type">> => <<"built_in_database">>}]
|
||||||
|
),
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_App) ->
|
set_special_configs(_App) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -62,176 +68,248 @@ set_special_configs(_App) ->
|
||||||
|
|
||||||
t_api(_) ->
|
t_api(_) ->
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( post
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username"])
|
post,
|
||||||
, [?USERNAME_RULES_EXAMPLE]),
|
uri(["authorization", "sources", "built_in_database", "username"]),
|
||||||
|
[?USERNAME_RULES_EXAMPLE]
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 200, Request1} =
|
{ok, 200, Request1} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "username"]),
|
||||||
#{<<"data">> := [#{<<"username">> := <<"user1">>, <<"rules">> := Rules1}],
|
[]
|
||||||
<<"meta">> := #{<<"count">> := 1,
|
),
|
||||||
<<"limit">> := 100,
|
#{
|
||||||
<<"page">> := 1}} = jsx:decode(Request1),
|
<<"data">> := [#{<<"username">> := <<"user1">>, <<"rules">> := Rules1}],
|
||||||
|
<<"meta">> := #{
|
||||||
|
<<"count">> := 1,
|
||||||
|
<<"limit">> := 100,
|
||||||
|
<<"page">> := 1
|
||||||
|
}
|
||||||
|
} = jsx:decode(Request1),
|
||||||
?assertEqual(3, length(Rules1)),
|
?assertEqual(3, length(Rules1)),
|
||||||
|
|
||||||
{ok, 200, Request1_1} =
|
{ok, 200, Request1_1} =
|
||||||
request( get
|
request(
|
||||||
, uri([ "authorization"
|
get,
|
||||||
, "sources"
|
uri([
|
||||||
, "built_in_database"
|
"authorization",
|
||||||
, "username?page=1&limit=20&like_username=noexist"])
|
"sources",
|
||||||
, []),
|
"built_in_database",
|
||||||
#{<<"data">> := [],
|
"username?page=1&limit=20&like_username=noexist"
|
||||||
<<"meta">> := #{<<"count">> := 0,
|
]),
|
||||||
<<"limit">> := 20,
|
[]
|
||||||
<<"page">> := 1}} = jsx:decode(Request1_1),
|
),
|
||||||
|
#{
|
||||||
|
<<"data">> := [],
|
||||||
|
<<"meta">> := #{
|
||||||
|
<<"count">> := 0,
|
||||||
|
<<"limit">> := 20,
|
||||||
|
<<"page">> := 1
|
||||||
|
}
|
||||||
|
} = jsx:decode(Request1_1),
|
||||||
|
|
||||||
{ok, 200, Request2} =
|
{ok, 200, Request2} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
#{<<"username">> := <<"user1">>, <<"rules">> := Rules1} = jsx:decode(Request2),
|
#{<<"username">> := <<"user1">>, <<"rules">> := Rules1} = jsx:decode(Request2),
|
||||||
|
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( put
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
|
put,
|
||||||
, ?USERNAME_RULES_EXAMPLE#{rules => []}),
|
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
|
||||||
|
?USERNAME_RULES_EXAMPLE#{rules => []}
|
||||||
|
),
|
||||||
{ok, 200, Request3} =
|
{ok, 200, Request3} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
#{<<"username">> := <<"user1">>, <<"rules">> := Rules2} = jsx:decode(Request3),
|
#{<<"username">> := <<"user1">>, <<"rules">> := Rules2} = jsx:decode(Request3),
|
||||||
?assertEqual(0, length(Rules2)),
|
?assertEqual(0, length(Rules2)),
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( delete
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
|
delete,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
{ok, 404, _} =
|
{ok, 404, _} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
{ok, 404, _} =
|
{ok, 404, _} =
|
||||||
request( delete
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
|
delete,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( post
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid"])
|
post,
|
||||||
, [?CLIENTID_RULES_EXAMPLE]),
|
uri(["authorization", "sources", "built_in_database", "clientid"]),
|
||||||
|
[?CLIENTID_RULES_EXAMPLE]
|
||||||
|
),
|
||||||
{ok, 200, Request4} =
|
{ok, 200, Request4} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "clientid"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
{ok, 200, Request5} =
|
{ok, 200, Request5} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
|
||||||
#{<<"data">> := [#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3}],
|
[]
|
||||||
<<"meta">> := #{<<"count">> := 1, <<"limit">> := 100, <<"page">> := 1}}
|
),
|
||||||
= jsx:decode(Request4),
|
#{
|
||||||
|
<<"data">> := [#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3}],
|
||||||
|
<<"meta">> := #{<<"count">> := 1, <<"limit">> := 100, <<"page">> := 1}
|
||||||
|
} =
|
||||||
|
jsx:decode(Request4),
|
||||||
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3} = jsx:decode(Request5),
|
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3} = jsx:decode(Request5),
|
||||||
?assertEqual(3, length(Rules3)),
|
?assertEqual(3, length(Rules3)),
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( put
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
|
put,
|
||||||
, ?CLIENTID_RULES_EXAMPLE#{rules => []}),
|
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
|
||||||
|
?CLIENTID_RULES_EXAMPLE#{rules => []}
|
||||||
|
),
|
||||||
{ok, 200, Request6} =
|
{ok, 200, Request6} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules4} = jsx:decode(Request6),
|
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules4} = jsx:decode(Request6),
|
||||||
?assertEqual(0, length(Rules4)),
|
?assertEqual(0, length(Rules4)),
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( delete
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
|
delete,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
{ok, 404, _} =
|
{ok, 404, _} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
{ok, 404, _} =
|
{ok, 404, _} =
|
||||||
request( delete
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
|
delete,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( post
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "all"])
|
post,
|
||||||
, ?ALL_RULES_EXAMPLE),
|
uri(["authorization", "sources", "built_in_database", "all"]),
|
||||||
|
?ALL_RULES_EXAMPLE
|
||||||
|
),
|
||||||
{ok, 200, Request7} =
|
{ok, 200, Request7} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "all"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "all"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
#{<<"rules">> := Rules5} = jsx:decode(Request7),
|
#{<<"rules">> := Rules5} = jsx:decode(Request7),
|
||||||
?assertEqual(3, length(Rules5)),
|
?assertEqual(3, length(Rules5)),
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( post
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "all"])
|
post,
|
||||||
|
uri(["authorization", "sources", "built_in_database", "all"]),
|
||||||
|
|
||||||
, ?ALL_RULES_EXAMPLE#{rules => []}),
|
?ALL_RULES_EXAMPLE#{rules => []}
|
||||||
|
),
|
||||||
{ok, 200, Request8} =
|
{ok, 200, Request8} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "all"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "all"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
#{<<"rules">> := Rules6} = jsx:decode(Request8),
|
#{<<"rules">> := Rules6} = jsx:decode(Request8),
|
||||||
?assertEqual(0, length(Rules6)),
|
?assertEqual(0, length(Rules6)),
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( post
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username"])
|
post,
|
||||||
, [ #{username => erlang:integer_to_binary(N), rules => []}
|
uri(["authorization", "sources", "built_in_database", "username"]),
|
||||||
|| N <- lists:seq(1, 20) ]),
|
[
|
||||||
|
#{username => erlang:integer_to_binary(N), rules => []}
|
||||||
|
|| N <- lists:seq(1, 20)
|
||||||
|
]
|
||||||
|
),
|
||||||
{ok, 200, Request9} =
|
{ok, 200, Request9} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "username?page=2&limit=5"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "username?page=2&limit=5"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
#{<<"data">> := Data1} = jsx:decode(Request9),
|
#{<<"data">> := Data1} = jsx:decode(Request9),
|
||||||
?assertEqual(5, length(Data1)),
|
?assertEqual(5, length(Data1)),
|
||||||
|
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( post
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid"])
|
post,
|
||||||
, [ #{clientid => erlang:integer_to_binary(N), rules => []}
|
uri(["authorization", "sources", "built_in_database", "clientid"]),
|
||||||
|| N <- lists:seq(1, 20) ]),
|
[
|
||||||
|
#{clientid => erlang:integer_to_binary(N), rules => []}
|
||||||
|
|| N <- lists:seq(1, 20)
|
||||||
|
]
|
||||||
|
),
|
||||||
{ok, 200, Request10} =
|
{ok, 200, Request10} =
|
||||||
request( get
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "clientid?limit=5"])
|
get,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "clientid?limit=5"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
#{<<"data">> := Data2} = jsx:decode(Request10),
|
#{<<"data">> := Data2} = jsx:decode(Request10),
|
||||||
?assertEqual(5, length(Data2)),
|
?assertEqual(5, length(Data2)),
|
||||||
|
|
||||||
{ok, 400, Msg1} =
|
{ok, 400, Msg1} =
|
||||||
request( delete
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "purge-all"])
|
delete,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "purge-all"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
?assertMatch({match, _}, re:run(Msg1, "must\sbe\sdisabled\sbefore")),
|
?assertMatch({match, _}, re:run(Msg1, "must\sbe\sdisabled\sbefore")),
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( put
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database"])
|
put,
|
||||||
, #{<<"enable">> => true, <<"type">> => <<"built_in_database">>}),
|
uri(["authorization", "sources", "built_in_database"]),
|
||||||
|
#{<<"enable">> => true, <<"type">> => <<"built_in_database">>}
|
||||||
|
),
|
||||||
%% test idempotence
|
%% test idempotence
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( put
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database"])
|
put,
|
||||||
, #{<<"enable">> => true, <<"type">> => <<"built_in_database">>}),
|
uri(["authorization", "sources", "built_in_database"]),
|
||||||
|
#{<<"enable">> => true, <<"type">> => <<"built_in_database">>}
|
||||||
|
),
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( put
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database"])
|
put,
|
||||||
, #{<<"enable">> => false, <<"type">> => <<"built_in_database">>}),
|
uri(["authorization", "sources", "built_in_database"]),
|
||||||
|
#{<<"enable">> => false, <<"type">> => <<"built_in_database">>}
|
||||||
|
),
|
||||||
{ok, 204, _} =
|
{ok, 204, _} =
|
||||||
request( delete
|
request(
|
||||||
, uri(["authorization", "sources", "built_in_database", "purge-all"])
|
delete,
|
||||||
, []),
|
uri(["authorization", "sources", "built_in_database", "purge-all"]),
|
||||||
|
[]
|
||||||
|
),
|
||||||
?assertEqual(0, emqx_authz_mnesia:record_count()),
|
?assertEqual(0, emqx_authz_mnesia:record_count()),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,20 @@ groups() ->
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz, emqx_dashboard],
|
[emqx_conf, emqx_authz, emqx_dashboard],
|
||||||
fun set_special_configs/1),
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
[authorization],
|
[authorization],
|
||||||
#{<<"no_match">> => <<"allow">>,
|
#{
|
||||||
<<"cache">> => #{<<"enable">> => <<"true">>},
|
<<"no_match">> => <<"allow">>,
|
||||||
<<"sources">> => []}),
|
<<"cache">> => #{<<"enable">> => <<"true">>},
|
||||||
|
<<"sources">> => []
|
||||||
|
}
|
||||||
|
),
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
|
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -60,27 +64,29 @@ set_special_configs(_App) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_api(_) ->
|
t_api(_) ->
|
||||||
Settings1 = #{<<"no_match">> => <<"deny">>,
|
Settings1 = #{
|
||||||
<<"deny_action">> => <<"disconnect">>,
|
<<"no_match">> => <<"deny">>,
|
||||||
<<"cache">> => #{
|
<<"deny_action">> => <<"disconnect">>,
|
||||||
<<"enable">> => false,
|
<<"cache">> => #{
|
||||||
<<"max_size">> => 32,
|
<<"enable">> => false,
|
||||||
<<"ttl">> => 60000
|
<<"max_size">> => 32,
|
||||||
}
|
<<"ttl">> => 60000
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{ok, 200, Result1} = request(put, uri(["authorization", "settings"]), Settings1),
|
{ok, 200, Result1} = request(put, uri(["authorization", "settings"]), Settings1),
|
||||||
{ok, 200, Result1} = request(get, uri(["authorization", "settings"]), []),
|
{ok, 200, Result1} = request(get, uri(["authorization", "settings"]), []),
|
||||||
?assertEqual(Settings1, jsx:decode(Result1)),
|
?assertEqual(Settings1, jsx:decode(Result1)),
|
||||||
|
|
||||||
Settings2 = #{<<"no_match">> => <<"allow">>,
|
Settings2 = #{
|
||||||
<<"deny_action">> => <<"ignore">>,
|
<<"no_match">> => <<"allow">>,
|
||||||
<<"cache">> => #{
|
<<"deny_action">> => <<"ignore">>,
|
||||||
<<"enable">> => true,
|
<<"cache">> => #{
|
||||||
<<"max_size">> => 32,
|
<<"enable">> => true,
|
||||||
<<"ttl">> => 60000
|
<<"max_size">> => 32,
|
||||||
}
|
<<"ttl">> => 60000
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{ok, 200, Result2} = request(put, uri(["authorization", "settings"]), Settings2),
|
{ok, 200, Result2} = request(put, uri(["authorization", "settings"]), Settings2),
|
||||||
{ok, 200, Result2} = request(get, uri(["authorization", "settings"]), []),
|
{ok, 200, Result2} = request(get, uri(["authorization", "settings"]), []),
|
||||||
|
|
|
||||||
|
|
@ -29,62 +29,70 @@
|
||||||
-define(PGSQL_HOST, "pgsql").
|
-define(PGSQL_HOST, "pgsql").
|
||||||
-define(REDIS_SINGLE_HOST, "redis").
|
-define(REDIS_SINGLE_HOST, "redis").
|
||||||
|
|
||||||
-define(SOURCE1, #{<<"type">> => <<"http">>,
|
-define(SOURCE1, #{
|
||||||
<<"enable">> => true,
|
<<"type">> => <<"http">>,
|
||||||
<<"url">> => <<"https://fake.com:443/acl?username=", ?PH_USERNAME/binary>>,
|
<<"enable">> => true,
|
||||||
<<"headers">> => #{},
|
<<"url">> => <<"https://fake.com:443/acl?username=", ?PH_USERNAME/binary>>,
|
||||||
<<"method">> => <<"get">>,
|
<<"headers">> => #{},
|
||||||
<<"request_timeout">> => <<"5s">>
|
<<"method">> => <<"get">>,
|
||||||
}).
|
<<"request_timeout">> => <<"5s">>
|
||||||
-define(SOURCE2, #{<<"type">> => <<"mongodb">>,
|
}).
|
||||||
<<"enable">> => true,
|
-define(SOURCE2, #{
|
||||||
<<"mongo_type">> => <<"single">>,
|
<<"type">> => <<"mongodb">>,
|
||||||
<<"server">> => <<?MONGO_SINGLE_HOST>>,
|
<<"enable">> => true,
|
||||||
<<"w_mode">> => <<"unsafe">>,
|
<<"mongo_type">> => <<"single">>,
|
||||||
<<"pool_size">> => 1,
|
<<"server">> => <<?MONGO_SINGLE_HOST>>,
|
||||||
<<"database">> => <<"mqtt">>,
|
<<"w_mode">> => <<"unsafe">>,
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"pool_size">> => 1,
|
||||||
<<"collection">> => <<"fake">>,
|
<<"database">> => <<"mqtt">>,
|
||||||
<<"selector">> => #{<<"a">> => <<"b">>}
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
}).
|
<<"collection">> => <<"fake">>,
|
||||||
-define(SOURCE3, #{<<"type">> => <<"mysql">>,
|
<<"selector">> => #{<<"a">> => <<"b">>}
|
||||||
<<"enable">> => true,
|
}).
|
||||||
<<"server">> => <<?MYSQL_HOST>>,
|
-define(SOURCE3, #{
|
||||||
<<"pool_size">> => 1,
|
<<"type">> => <<"mysql">>,
|
||||||
<<"database">> => <<"mqtt">>,
|
<<"enable">> => true,
|
||||||
<<"username">> => <<"xx">>,
|
<<"server">> => <<?MYSQL_HOST>>,
|
||||||
<<"password">> => <<"ee">>,
|
<<"pool_size">> => 1,
|
||||||
<<"auto_reconnect">> => true,
|
<<"database">> => <<"mqtt">>,
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"username">> => <<"xx">>,
|
||||||
<<"query">> => <<"abcb">>
|
<<"password">> => <<"ee">>,
|
||||||
}).
|
<<"auto_reconnect">> => true,
|
||||||
-define(SOURCE4, #{<<"type">> => <<"postgresql">>,
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
<<"enable">> => true,
|
<<"query">> => <<"abcb">>
|
||||||
<<"server">> => <<?PGSQL_HOST>>,
|
}).
|
||||||
<<"pool_size">> => 1,
|
-define(SOURCE4, #{
|
||||||
<<"database">> => <<"mqtt">>,
|
<<"type">> => <<"postgresql">>,
|
||||||
<<"username">> => <<"xx">>,
|
<<"enable">> => true,
|
||||||
<<"password">> => <<"ee">>,
|
<<"server">> => <<?PGSQL_HOST>>,
|
||||||
<<"auto_reconnect">> => true,
|
<<"pool_size">> => 1,
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"database">> => <<"mqtt">>,
|
||||||
<<"query">> => <<"abcb">>
|
<<"username">> => <<"xx">>,
|
||||||
}).
|
<<"password">> => <<"ee">>,
|
||||||
-define(SOURCE5, #{<<"type">> => <<"redis">>,
|
<<"auto_reconnect">> => true,
|
||||||
<<"enable">> => true,
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
<<"servers">> => <<?REDIS_SINGLE_HOST, ",127.0.0.1:6380">>,
|
<<"query">> => <<"abcb">>
|
||||||
<<"pool_size">> => 1,
|
}).
|
||||||
<<"database">> => 0,
|
-define(SOURCE5, #{
|
||||||
<<"password">> => <<"ee">>,
|
<<"type">> => <<"redis">>,
|
||||||
<<"auto_reconnect">> => true,
|
<<"enable">> => true,
|
||||||
<<"ssl">> => #{<<"enable">> => false},
|
<<"servers">> => <<?REDIS_SINGLE_HOST, ",127.0.0.1:6380">>,
|
||||||
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
|
<<"pool_size">> => 1,
|
||||||
}).
|
<<"database">> => 0,
|
||||||
-define(SOURCE6, #{<<"type">> => <<"file">>,
|
<<"password">> => <<"ee">>,
|
||||||
<<"enable">> => true,
|
<<"auto_reconnect">> => true,
|
||||||
<<"rules">> =>
|
<<"ssl">> => #{<<"enable">> => false},
|
||||||
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
|
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
|
||||||
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
|
}).
|
||||||
}).
|
-define(SOURCE6, #{
|
||||||
|
<<"type">> => <<"file">>,
|
||||||
|
<<"enable">> => true,
|
||||||
|
<<"rules">> =>
|
||||||
|
<<
|
||||||
|
"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
|
||||||
|
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
|
||||||
|
>>
|
||||||
|
}).
|
||||||
|
|
||||||
-define(MATCH_RSA_KEY, <<"-----BEGIN RSA PRIVATE KEY", _/binary>>).
|
-define(MATCH_RSA_KEY, <<"-----BEGIN RSA PRIVATE KEY", _/binary>>).
|
||||||
-define(MATCH_CERT, <<"-----BEGIN CERTIFICATE", _/binary>>).
|
-define(MATCH_CERT, <<"-----BEGIN CERTIFICATE", _/binary>>).
|
||||||
|
|
@ -100,24 +108,31 @@ init_per_suite(Config) ->
|
||||||
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
||||||
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
|
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
|
||||||
meck:expect(emqx_resource, health_check, fun(St) -> {ok, St} end),
|
meck:expect(emqx_resource, health_check, fun(St) -> {ok, St} end),
|
||||||
meck:expect(emqx_resource, remove_local, fun(_) -> ok end ),
|
meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
|
||||||
meck:expect(emqx_authz, acl_conf_file,
|
meck:expect(
|
||||||
fun() ->
|
emqx_authz,
|
||||||
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
|
acl_conf_file,
|
||||||
end),
|
fun() ->
|
||||||
|
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz, emqx_dashboard],
|
[emqx_conf, emqx_authz, emqx_dashboard],
|
||||||
fun set_special_configs/1),
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
ok = start_apps([emqx_resource, emqx_connector]),
|
ok = start_apps([emqx_resource, emqx_connector]),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
[authorization],
|
[authorization],
|
||||||
#{<<"no_match">> => <<"allow">>,
|
#{
|
||||||
<<"cache">> => #{<<"enable">> => <<"true">>},
|
<<"no_match">> => <<"allow">>,
|
||||||
<<"sources">> => []}),
|
<<"cache">> => #{<<"enable">> => <<"true">>},
|
||||||
|
<<"sources">> => []
|
||||||
|
}
|
||||||
|
),
|
||||||
%% resource and connector should be stop first,
|
%% resource and connector should be stop first,
|
||||||
%% or authz_[mysql|pgsql|redis..]_SUITE would be failed
|
%% or authz_[mysql|pgsql|redis..]_SUITE would be failed
|
||||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||||
|
|
@ -140,19 +155,24 @@ init_per_testcase(t_api, Config) ->
|
||||||
meck:expect(emqx_misc, gen_id, fun() -> "fake" end),
|
meck:expect(emqx_misc, gen_id, fun() -> "fake" end),
|
||||||
|
|
||||||
meck:new(emqx, [non_strict, passthrough, no_history, no_link]),
|
meck:new(emqx, [non_strict, passthrough, no_history, no_link]),
|
||||||
meck:expect(emqx, data_dir,
|
meck:expect(
|
||||||
fun() ->
|
emqx,
|
||||||
{data_dir, Data} = lists:keyfind(data_dir, 1, Config),
|
data_dir,
|
||||||
Data
|
fun() ->
|
||||||
end),
|
{data_dir, Data} = lists:keyfind(data_dir, 1, Config),
|
||||||
|
Data
|
||||||
|
end
|
||||||
|
),
|
||||||
Config;
|
Config;
|
||||||
init_per_testcase(_, Config) -> Config.
|
init_per_testcase(_, Config) ->
|
||||||
|
Config.
|
||||||
|
|
||||||
end_per_testcase(t_api, _Config) ->
|
end_per_testcase(t_api, _Config) ->
|
||||||
meck:unload(emqx_misc),
|
meck:unload(emqx_misc),
|
||||||
meck:unload(emqx),
|
meck:unload(emqx),
|
||||||
ok;
|
ok;
|
||||||
end_per_testcase(_, _Config) -> ok.
|
end_per_testcase(_, _Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Testcases
|
%% Testcases
|
||||||
|
|
@ -162,139 +182,190 @@ t_api(_) ->
|
||||||
{ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []),
|
{ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []),
|
||||||
?assertEqual([], get_sources(Result1)),
|
?assertEqual([], get_sources(Result1)),
|
||||||
|
|
||||||
[ begin {ok, 204, _} = request(post, uri(["authorization", "sources"]), Source) end
|
[
|
||||||
|| Source <- lists:reverse([?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6])],
|
begin
|
||||||
|
{ok, 204, _} = request(post, uri(["authorization", "sources"]), Source)
|
||||||
|
end
|
||||||
|
|| Source <- lists:reverse([?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6])
|
||||||
|
],
|
||||||
{ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1),
|
{ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1),
|
||||||
|
|
||||||
Snd = fun ({_, Val}) -> Val end,
|
Snd = fun({_, Val}) -> Val end,
|
||||||
LookupVal = fun LookupV(List, RestJson) ->
|
LookupVal = fun LookupV(List, RestJson) ->
|
||||||
case List of
|
case List of
|
||||||
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
|
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
|
||||||
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
|
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
EqualFun = fun (RList) ->
|
EqualFun = fun(RList) ->
|
||||||
fun ({M, V}) ->
|
fun({M, V}) ->
|
||||||
?assertEqual(V,
|
?assertEqual(
|
||||||
LookupVal([<<"metrics">>, M],
|
V,
|
||||||
RList)
|
LookupVal(
|
||||||
)
|
[<<"metrics">>, M],
|
||||||
end
|
RList
|
||||||
end,
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end,
|
||||||
AssertFun =
|
AssertFun =
|
||||||
fun (ResultJson) ->
|
fun(ResultJson) ->
|
||||||
{ok, RList} = emqx_json:safe_decode(ResultJson),
|
{ok, RList} = emqx_json:safe_decode(ResultJson),
|
||||||
MetricsList = [{<<"failed">>, 0},
|
MetricsList = [
|
||||||
{<<"matched">>, 0},
|
{<<"failed">>, 0},
|
||||||
{<<"rate">>, 0.0},
|
{<<"matched">>, 0},
|
||||||
{<<"rate_last5m">>, 0.0},
|
{<<"rate">>, 0.0},
|
||||||
{<<"rate_max">>, 0.0},
|
{<<"rate_last5m">>, 0.0},
|
||||||
{<<"success">>, 0}],
|
{<<"rate_max">>, 0.0},
|
||||||
|
{<<"success">>, 0}
|
||||||
|
],
|
||||||
lists:map(EqualFun(RList), MetricsList)
|
lists:map(EqualFun(RList), MetricsList)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
{ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []),
|
{ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []),
|
||||||
Sources = get_sources(Result2),
|
Sources = get_sources(Result2),
|
||||||
?assertMatch([ #{<<"type">> := <<"http">>}
|
?assertMatch(
|
||||||
, #{<<"type">> := <<"mongodb">>}
|
[
|
||||||
, #{<<"type">> := <<"mysql">>}
|
#{<<"type">> := <<"http">>},
|
||||||
, #{<<"type">> := <<"postgresql">>}
|
#{<<"type">> := <<"mongodb">>},
|
||||||
, #{<<"type">> := <<"redis">>}
|
#{<<"type">> := <<"mysql">>},
|
||||||
, #{<<"type">> := <<"file">>}
|
#{<<"type">> := <<"postgresql">>},
|
||||||
], Sources),
|
#{<<"type">> := <<"redis">>},
|
||||||
|
#{<<"type">> := <<"file">>}
|
||||||
|
],
|
||||||
|
Sources
|
||||||
|
),
|
||||||
?assert(filelib:is_file(emqx_authz:acl_conf_file())),
|
?assert(filelib:is_file(emqx_authz:acl_conf_file())),
|
||||||
|
|
||||||
{ok, 204, _} = request(put, uri(["authorization", "sources", "http"]),
|
{ok, 204, _} = request(
|
||||||
?SOURCE1#{<<"enable">> := false}),
|
put,
|
||||||
|
uri(["authorization", "sources", "http"]),
|
||||||
|
?SOURCE1#{<<"enable">> := false}
|
||||||
|
),
|
||||||
{ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []),
|
{ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []),
|
||||||
?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result3)),
|
?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result3)),
|
||||||
|
|
||||||
Keyfile = emqx_common_test_helpers:app_path(
|
Keyfile = emqx_common_test_helpers:app_path(
|
||||||
emqx,
|
emqx,
|
||||||
filename:join(["etc", "certs", "key.pem"])),
|
filename:join(["etc", "certs", "key.pem"])
|
||||||
|
),
|
||||||
Certfile = emqx_common_test_helpers:app_path(
|
Certfile = emqx_common_test_helpers:app_path(
|
||||||
emqx,
|
emqx,
|
||||||
filename:join(["etc", "certs", "cert.pem"])),
|
filename:join(["etc", "certs", "cert.pem"])
|
||||||
|
),
|
||||||
Cacertfile = emqx_common_test_helpers:app_path(
|
Cacertfile = emqx_common_test_helpers:app_path(
|
||||||
emqx,
|
emqx,
|
||||||
filename:join(["etc", "certs", "cacert.pem"])),
|
filename:join(["etc", "certs", "cacert.pem"])
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} = request(put, uri(["authorization", "sources", "mongodb"]),
|
{ok, 204, _} = request(
|
||||||
?SOURCE2#{<<"ssl">> => #{
|
put,
|
||||||
<<"enable">> => <<"true">>,
|
uri(["authorization", "sources", "mongodb"]),
|
||||||
<<"cacertfile">> => Cacertfile,
|
?SOURCE2#{
|
||||||
<<"certfile">> => Certfile,
|
<<"ssl">> => #{
|
||||||
<<"keyfile">> => Keyfile,
|
<<"enable">> => <<"true">>,
|
||||||
<<"verify">> => <<"verify_none">>
|
<<"cacertfile">> => Cacertfile,
|
||||||
}}),
|
<<"certfile">> => Certfile,
|
||||||
|
<<"keyfile">> => Keyfile,
|
||||||
|
<<"verify">> => <<"verify_none">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
{ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongodb"]), []),
|
{ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongodb"]), []),
|
||||||
{ok, 200, Status4} = request(get, uri(["authorization", "sources", "mongodb", "status"]), []),
|
{ok, 200, Status4} = request(get, uri(["authorization", "sources", "mongodb", "status"]), []),
|
||||||
AssertFun(Status4),
|
AssertFun(Status4),
|
||||||
?assertMatch(#{<<"type">> := <<"mongodb">>,
|
?assertMatch(
|
||||||
<<"ssl">> := #{<<"enable">> := <<"true">>,
|
#{
|
||||||
<<"cacertfile">> := ?MATCH_CERT,
|
<<"type">> := <<"mongodb">>,
|
||||||
<<"certfile">> := ?MATCH_CERT,
|
<<"ssl">> := #{
|
||||||
<<"keyfile">> := ?MATCH_RSA_KEY,
|
<<"enable">> := <<"true">>,
|
||||||
<<"verify">> := <<"verify_none">>
|
<<"cacertfile">> := ?MATCH_CERT,
|
||||||
}
|
<<"certfile">> := ?MATCH_CERT,
|
||||||
}, jsx:decode(Result4)),
|
<<"keyfile">> := ?MATCH_RSA_KEY,
|
||||||
|
<<"verify">> := <<"verify_none">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
jsx:decode(Result4)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, Cacert} = file:read_file(Cacertfile),
|
{ok, Cacert} = file:read_file(Cacertfile),
|
||||||
{ok, Cert} = file:read_file(Certfile),
|
{ok, Cert} = file:read_file(Certfile),
|
||||||
{ok, Key} = file:read_file(Keyfile),
|
{ok, Key} = file:read_file(Keyfile),
|
||||||
|
|
||||||
{ok, 204, _} = request(put, uri(["authorization", "sources", "mongodb"]),
|
{ok, 204, _} = request(
|
||||||
?SOURCE2#{<<"ssl">> => #{
|
put,
|
||||||
<<"enable">> => <<"true">>,
|
uri(["authorization", "sources", "mongodb"]),
|
||||||
<<"cacertfile">> => Cacert,
|
?SOURCE2#{
|
||||||
<<"certfile">> => Cert,
|
<<"ssl">> => #{
|
||||||
<<"keyfile">> => Key,
|
<<"enable">> => <<"true">>,
|
||||||
<<"verify">> => <<"verify_none">>
|
<<"cacertfile">> => Cacert,
|
||||||
}}),
|
<<"certfile">> => Cert,
|
||||||
|
<<"keyfile">> => Key,
|
||||||
|
<<"verify">> => <<"verify_none">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
{ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongodb"]), []),
|
{ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongodb"]), []),
|
||||||
{ok, 200, Status5} = request(get, uri(["authorization", "sources", "mongodb", "status"]), []),
|
{ok, 200, Status5} = request(get, uri(["authorization", "sources", "mongodb", "status"]), []),
|
||||||
AssertFun(Status5),
|
AssertFun(Status5),
|
||||||
?assertMatch(#{<<"type">> := <<"mongodb">>,
|
?assertMatch(
|
||||||
<<"ssl">> := #{<<"enable">> := <<"true">>,
|
#{
|
||||||
<<"cacertfile">> := ?MATCH_CERT,
|
<<"type">> := <<"mongodb">>,
|
||||||
<<"certfile">> := ?MATCH_CERT,
|
<<"ssl">> := #{
|
||||||
<<"keyfile">> := ?MATCH_RSA_KEY,
|
<<"enable">> := <<"true">>,
|
||||||
<<"verify">> := <<"verify_none">>
|
<<"cacertfile">> := ?MATCH_CERT,
|
||||||
}
|
<<"certfile">> := ?MATCH_CERT,
|
||||||
}, jsx:decode(Result5)),
|
<<"keyfile">> := ?MATCH_RSA_KEY,
|
||||||
|
<<"verify">> := <<"verify_none">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
jsx:decode(Result5)
|
||||||
|
),
|
||||||
|
|
||||||
|
#{
|
||||||
#{ssl := #{cacertfile := SavedCacertfile,
|
ssl := #{
|
||||||
certfile := SavedCertfile,
|
cacertfile := SavedCacertfile,
|
||||||
keyfile := SavedKeyfile
|
certfile := SavedCertfile,
|
||||||
}} = emqx_authz:lookup(mongodb),
|
keyfile := SavedKeyfile
|
||||||
|
}
|
||||||
|
} = emqx_authz:lookup(mongodb),
|
||||||
|
|
||||||
?assert(filelib:is_file(SavedCacertfile)),
|
?assert(filelib:is_file(SavedCacertfile)),
|
||||||
?assert(filelib:is_file(SavedCertfile)),
|
?assert(filelib:is_file(SavedCertfile)),
|
||||||
?assert(filelib:is_file(SavedKeyfile)),
|
?assert(filelib:is_file(SavedKeyfile)),
|
||||||
|
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
put,
|
put,
|
||||||
uri(["authorization", "sources", "mysql"]),
|
uri(["authorization", "sources", "mysql"]),
|
||||||
?SOURCE3#{<<"server">> := <<"192.168.1.100:3306">>}),
|
?SOURCE3#{<<"server">> := <<"192.168.1.100:3306">>}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
put,
|
put,
|
||||||
uri(["authorization", "sources", "postgresql"]),
|
uri(["authorization", "sources", "postgresql"]),
|
||||||
?SOURCE4#{<<"server">> := <<"fake">>}),
|
?SOURCE4#{<<"server">> := <<"fake">>}
|
||||||
|
),
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
put,
|
put,
|
||||||
uri(["authorization", "sources", "redis"]),
|
uri(["authorization", "sources", "redis"]),
|
||||||
?SOURCE5#{<<"servers">> := [<<"192.168.1.100:6379">>,
|
?SOURCE5#{
|
||||||
<<"192.168.1.100:6380">>]}),
|
<<"servers">> := [
|
||||||
|
<<"192.168.1.100:6379">>,
|
||||||
|
<<"192.168.1.100:6380">>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(#{<<"type">> := Type}) ->
|
fun(#{<<"type">> := Type}) ->
|
||||||
{ok, 204, _} = request(
|
{ok, 204, _} = request(
|
||||||
delete,
|
delete,
|
||||||
uri(["authorization", "sources", binary_to_list(Type)]),
|
uri(["authorization", "sources", binary_to_list(Type)]),
|
||||||
[])
|
[]
|
||||||
end, Sources),
|
)
|
||||||
|
end,
|
||||||
|
Sources
|
||||||
|
),
|
||||||
{ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []),
|
{ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []),
|
||||||
?assertEqual([], get_sources(Result6)),
|
?assertEqual([], get_sources(Result6)),
|
||||||
?assertEqual([], emqx:get_config([authorization, sources])),
|
?assertEqual([], emqx:get_config([authorization, sources])),
|
||||||
|
|
@ -302,48 +373,80 @@ t_api(_) ->
|
||||||
|
|
||||||
t_move_source(_) ->
|
t_move_source(_) ->
|
||||||
{ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]),
|
{ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]),
|
||||||
?assertMatch([ #{type := http}
|
?assertMatch(
|
||||||
, #{type := mongodb}
|
[
|
||||||
, #{type := mysql}
|
#{type := http},
|
||||||
, #{type := postgresql}
|
#{type := mongodb},
|
||||||
, #{type := redis}
|
#{type := mysql},
|
||||||
], emqx_authz:lookup()),
|
#{type := postgresql},
|
||||||
|
#{type := redis}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} = request(post, uri(["authorization", "sources", "postgresql", "move"]),
|
{ok, 204, _} = request(
|
||||||
#{<<"position">> => <<"front">>}),
|
post,
|
||||||
?assertMatch([ #{type := postgresql}
|
uri(["authorization", "sources", "postgresql", "move"]),
|
||||||
, #{type := http}
|
#{<<"position">> => <<"front">>}
|
||||||
, #{type := mongodb}
|
),
|
||||||
, #{type := mysql}
|
?assertMatch(
|
||||||
, #{type := redis}
|
[
|
||||||
], emqx_authz:lookup()),
|
#{type := postgresql},
|
||||||
|
#{type := http},
|
||||||
|
#{type := mongodb},
|
||||||
|
#{type := mysql},
|
||||||
|
#{type := redis}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} = request(post, uri(["authorization", "sources", "http", "move"]),
|
{ok, 204, _} = request(
|
||||||
#{<<"position">> => <<"rear">>}),
|
post,
|
||||||
?assertMatch([ #{type := postgresql}
|
uri(["authorization", "sources", "http", "move"]),
|
||||||
, #{type := mongodb}
|
#{<<"position">> => <<"rear">>}
|
||||||
, #{type := mysql}
|
),
|
||||||
, #{type := redis}
|
?assertMatch(
|
||||||
, #{type := http}
|
[
|
||||||
], emqx_authz:lookup()),
|
#{type := postgresql},
|
||||||
|
#{type := mongodb},
|
||||||
|
#{type := mysql},
|
||||||
|
#{type := redis},
|
||||||
|
#{type := http}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} = request(post, uri(["authorization", "sources", "mysql", "move"]),
|
{ok, 204, _} = request(
|
||||||
#{<<"position">> => <<"before:postgresql">>}),
|
post,
|
||||||
?assertMatch([ #{type := mysql}
|
uri(["authorization", "sources", "mysql", "move"]),
|
||||||
, #{type := postgresql}
|
#{<<"position">> => <<"before:postgresql">>}
|
||||||
, #{type := mongodb}
|
),
|
||||||
, #{type := redis}
|
?assertMatch(
|
||||||
, #{type := http}
|
[
|
||||||
], emqx_authz:lookup()),
|
#{type := mysql},
|
||||||
|
#{type := postgresql},
|
||||||
|
#{type := mongodb},
|
||||||
|
#{type := redis},
|
||||||
|
#{type := http}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, _} = request(post, uri(["authorization", "sources", "mongodb", "move"]),
|
{ok, 204, _} = request(
|
||||||
#{<<"position">> => <<"after:http">>}),
|
post,
|
||||||
?assertMatch([ #{type := mysql}
|
uri(["authorization", "sources", "mongodb", "move"]),
|
||||||
, #{type := postgresql}
|
#{<<"position">> => <<"after:http">>}
|
||||||
, #{type := redis}
|
),
|
||||||
, #{type := http}
|
?assertMatch(
|
||||||
, #{type := mongodb}
|
[
|
||||||
], emqx_authz:lookup()),
|
#{type := mysql},
|
||||||
|
#{type := postgresql},
|
||||||
|
#{type := redis},
|
||||||
|
#{type := http},
|
||||||
|
#{type := mongodb}
|
||||||
|
],
|
||||||
|
emqx_authz:lookup()
|
||||||
|
),
|
||||||
|
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,15 @@
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
-define(RAW_SOURCE, #{<<"type">> => <<"file">>,
|
-define(RAW_SOURCE, #{
|
||||||
<<"enable">> => true,
|
<<"type">> => <<"file">>,
|
||||||
<<"rules">> =>
|
<<"enable">> => true,
|
||||||
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
|
<<"rules">> =>
|
||||||
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
|
<<
|
||||||
}).
|
"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
|
||||||
|
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
|
||||||
|
>>
|
||||||
|
}).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
@ -37,13 +40,17 @@ groups() ->
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz],
|
[emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1),
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
%% meck after authz started
|
%% meck after authz started
|
||||||
meck:expect(emqx_authz, acl_conf_file,
|
meck:expect(
|
||||||
fun() ->
|
emqx_authz,
|
||||||
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
|
acl_conf_file,
|
||||||
end),
|
fun() ->
|
||||||
|
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
|
||||||
|
end
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
|
|
@ -57,7 +64,6 @@ init_per_testcase(_TestCase, Config) ->
|
||||||
|
|
||||||
set_special_configs(emqx_authz) ->
|
set_special_configs(emqx_authz) ->
|
||||||
ok = emqx_authz_test_lib:reset_authorizers();
|
ok = emqx_authz_test_lib:reset_authorizers();
|
||||||
|
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -66,43 +72,55 @@ set_special_configs(_) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_ok(_Config) ->
|
t_ok(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_config(?RAW_SOURCE#{<<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">>}),
|
ok = setup_config(?RAW_SOURCE#{
|
||||||
|
<<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">>
|
||||||
|
}),
|
||||||
|
|
||||||
io:format("~p", [emqx_authz:acl_conf_file()]),
|
io:format("~p", [emqx_authz:acl_conf_file()]),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
deny,
|
deny,
|
||||||
emqx_access_control:authorize(ClientInfo, subscribe, <<"t">>)).
|
emqx_access_control:authorize(ClientInfo, subscribe, <<"t">>)
|
||||||
|
).
|
||||||
|
|
||||||
t_invalid_file(_Config) ->
|
t_invalid_file(_Config) ->
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, bad_acl_file_content},
|
{error, bad_acl_file_content},
|
||||||
emqx_authz:update(?CMD_REPLACE, [?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>}])).
|
emqx_authz:update(?CMD_REPLACE, [?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>}])
|
||||||
|
).
|
||||||
|
|
||||||
t_update(_Config) ->
|
t_update(_Config) ->
|
||||||
ok = setup_config(?RAW_SOURCE#{<<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">>}),
|
ok = setup_config(?RAW_SOURCE#{
|
||||||
|
<<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">>
|
||||||
|
}),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, _},
|
{error, _},
|
||||||
emqx_authz:update(
|
emqx_authz:update(
|
||||||
{?CMD_REPLACE, file},
|
{?CMD_REPLACE, file},
|
||||||
?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>})),
|
?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
emqx_authz:update(
|
emqx_authz:update(
|
||||||
{?CMD_REPLACE, file}, ?RAW_SOURCE)).
|
{?CMD_REPLACE, file}, ?RAW_SOURCE
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -110,8 +128,9 @@ t_update(_Config) ->
|
||||||
|
|
||||||
setup_config(SpecialParams) ->
|
setup_config(SpecialParams) ->
|
||||||
emqx_authz_test_lib:setup_config(
|
emqx_authz_test_lib:setup_config(
|
||||||
?RAW_SOURCE,
|
?RAW_SOURCE,
|
||||||
SpecialParams).
|
SpecialParams
|
||||||
|
).
|
||||||
|
|
||||||
stop_apps(Apps) ->
|
stop_apps(Apps) ->
|
||||||
lists:foreach(fun application:stop/1, Apps).
|
lists:foreach(fun application:stop/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ all() ->
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
ok = stop_apps([emqx_resource, emqx_connector, cowboy]),
|
ok = stop_apps([emqx_resource, emqx_connector, cowboy]),
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz],
|
[emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1
|
fun set_special_configs/1
|
||||||
),
|
),
|
||||||
ok = start_apps([emqx_resource, emqx_connector, cowboy]),
|
ok = start_apps([emqx_resource, emqx_connector, cowboy]),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
|
@ -45,7 +45,6 @@ end_per_suite(_Config) ->
|
||||||
|
|
||||||
set_special_configs(emqx_authz) ->
|
set_special_configs(emqx_authz) ->
|
||||||
ok = emqx_authz_test_lib:reset_authorizers();
|
ok = emqx_authz_test_lib:reset_authorizers();
|
||||||
|
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -62,251 +61,301 @@ end_per_testcase(_Case, _Config) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_response_handling(_Config) ->
|
t_response_handling(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
%% OK, get, no body
|
%% OK, get, no body
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(200, Req0),
|
Req = cowboy_req:reply(200, Req0),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end,
|
end,
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
allow = emqx_access_control:authorize(ClientInfo, publish, <<"t">>),
|
allow = emqx_access_control:authorize(ClientInfo, publish, <<"t">>),
|
||||||
|
|
||||||
%% OK, get, body & headers
|
%% OK, get, body & headers
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(
|
Req = cowboy_req:reply(
|
||||||
200,
|
200,
|
||||||
#{<<"content-type">> => <<"text/plain">>},
|
#{<<"content-type">> => <<"text/plain">>},
|
||||||
"Response body",
|
"Response body",
|
||||||
Req0),
|
Req0
|
||||||
{ok, Req, State}
|
),
|
||||||
end,
|
{ok, Req, State}
|
||||||
#{}),
|
end,
|
||||||
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
),
|
||||||
|
|
||||||
%% OK, get, 204
|
%% OK, get, 204
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(204, Req0),
|
Req = cowboy_req:reply(204, Req0),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end,
|
end,
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
),
|
||||||
|
|
||||||
%% Not OK, get, 400
|
%% Not OK, get, 400
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(400, Req0),
|
Req = cowboy_req:reply(400, Req0),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end,
|
end,
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
deny,
|
deny,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
),
|
||||||
|
|
||||||
%% Not OK, get, 400 + body & headers
|
%% Not OK, get, 400 + body & headers
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(
|
Req = cowboy_req:reply(
|
||||||
400,
|
400,
|
||||||
#{<<"content-type">> => <<"text/plain">>},
|
#{<<"content-type">> => <<"text/plain">>},
|
||||||
"Response body",
|
"Response body",
|
||||||
Req0),
|
Req0
|
||||||
{ok, Req, State}
|
),
|
||||||
end,
|
{ok, Req, State}
|
||||||
#{}),
|
end,
|
||||||
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
deny,
|
deny,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
).
|
||||||
|
|
||||||
t_query_params(_Config) ->
|
t_query_params(_Config) ->
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
#{username := <<"user name">>,
|
#{
|
||||||
clientid := <<"client id">>,
|
username := <<"user name">>,
|
||||||
peerhost := <<"127.0.0.1">>,
|
clientid := <<"client id">>,
|
||||||
proto_name := <<"MQTT">>,
|
peerhost := <<"127.0.0.1">>,
|
||||||
mountpoint := <<"MOUNTPOINT">>,
|
proto_name := <<"MQTT">>,
|
||||||
topic := <<"t">>,
|
mountpoint := <<"MOUNTPOINT">>,
|
||||||
action := <<"publish">>
|
topic := <<"t">>,
|
||||||
} = cowboy_req:match_qs(
|
action := <<"publish">>
|
||||||
[username,
|
} = cowboy_req:match_qs(
|
||||||
clientid,
|
[
|
||||||
peerhost,
|
username,
|
||||||
proto_name,
|
clientid,
|
||||||
mountpoint,
|
peerhost,
|
||||||
topic,
|
proto_name,
|
||||||
action],
|
mountpoint,
|
||||||
Req0),
|
topic,
|
||||||
Req = cowboy_req:reply(200, Req0),
|
action
|
||||||
{ok, Req, State}
|
],
|
||||||
end,
|
Req0
|
||||||
#{<<"url">> => <<"http://127.0.0.1:33333/authz/users/?"
|
),
|
||||||
"username=${username}&"
|
Req = cowboy_req:reply(200, Req0),
|
||||||
"clientid=${clientid}&"
|
{ok, Req, State}
|
||||||
"peerhost=${peerhost}&"
|
end,
|
||||||
"proto_name=${proto_name}&"
|
#{
|
||||||
"mountpoint=${mountpoint}&"
|
<<"url">> => <<
|
||||||
"topic=${topic}&"
|
"http://127.0.0.1:33333/authz/users/?"
|
||||||
"action=${action}">>
|
"username=${username}&"
|
||||||
}),
|
"clientid=${clientid}&"
|
||||||
|
"peerhost=${peerhost}&"
|
||||||
|
"proto_name=${proto_name}&"
|
||||||
|
"mountpoint=${mountpoint}&"
|
||||||
|
"topic=${topic}&"
|
||||||
|
"action=${action}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ClientInfo = #{clientid => <<"client id">>,
|
ClientInfo = #{
|
||||||
username => <<"user name">>,
|
clientid => <<"client id">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"user name">>,
|
||||||
protocol => <<"MQTT">>,
|
peerhost => {127, 0, 0, 1},
|
||||||
mountpoint => <<"MOUNTPOINT">>,
|
protocol => <<"MQTT">>,
|
||||||
zone => default,
|
mountpoint => <<"MOUNTPOINT">>,
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
).
|
||||||
|
|
||||||
t_json_body(_Config) ->
|
t_json_body(_Config) ->
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
<<"/authz/users/">>,
|
<<"/authz/users/">>,
|
||||||
cowboy_req:path(Req0)),
|
cowboy_req:path(Req0)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
|
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
#{<<"username">> := <<"user name">>,
|
#{
|
||||||
<<"CLIENT">> := <<"client id">>,
|
<<"username">> := <<"user name">>,
|
||||||
<<"peerhost">> := <<"127.0.0.1">>,
|
<<"CLIENT">> := <<"client id">>,
|
||||||
<<"proto_name">> := <<"MQTT">>,
|
<<"peerhost">> := <<"127.0.0.1">>,
|
||||||
<<"mountpoint">> := <<"MOUNTPOINT">>,
|
<<"proto_name">> := <<"MQTT">>,
|
||||||
<<"topic">> := <<"t">>,
|
<<"mountpoint">> := <<"MOUNTPOINT">>,
|
||||||
<<"action">> := <<"publish">>},
|
<<"topic">> := <<"t">>,
|
||||||
jiffy:decode(RawBody, [return_maps])),
|
<<"action">> := <<"publish">>
|
||||||
|
},
|
||||||
|
jiffy:decode(RawBody, [return_maps])
|
||||||
|
),
|
||||||
|
|
||||||
Req = cowboy_req:reply(200, Req1),
|
Req = cowboy_req:reply(200, Req1),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end,
|
end,
|
||||||
#{<<"method">> => <<"post">>,
|
#{
|
||||||
<<"body">> => #{<<"username">> => <<"${username}">>,
|
<<"method">> => <<"post">>,
|
||||||
<<"CLIENT">> => <<"${clientid}">>,
|
<<"body">> => #{
|
||||||
<<"peerhost">> => <<"${peerhost}">>,
|
<<"username">> => <<"${username}">>,
|
||||||
<<"proto_name">> => <<"${proto_name}">>,
|
<<"CLIENT">> => <<"${clientid}">>,
|
||||||
<<"mountpoint">> => <<"${mountpoint}">>,
|
<<"peerhost">> => <<"${peerhost}">>,
|
||||||
<<"topic">> => <<"${topic}">>,
|
<<"proto_name">> => <<"${proto_name}">>,
|
||||||
<<"action">> => <<"${action}">>}
|
<<"mountpoint">> => <<"${mountpoint}">>,
|
||||||
}),
|
<<"topic">> => <<"${topic}">>,
|
||||||
|
<<"action">> => <<"${action}">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ClientInfo = #{clientid => <<"client id">>,
|
ClientInfo = #{
|
||||||
username => <<"user name">>,
|
clientid => <<"client id">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"user name">>,
|
||||||
protocol => <<"MQTT">>,
|
peerhost => {127, 0, 0, 1},
|
||||||
mountpoint => <<"MOUNTPOINT">>,
|
protocol => <<"MQTT">>,
|
||||||
zone => default,
|
mountpoint => <<"MOUNTPOINT">>,
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
).
|
||||||
|
|
||||||
t_form_body(_Config) ->
|
t_form_body(_Config) ->
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
<<"/authz/users/">>,
|
<<"/authz/users/">>,
|
||||||
cowboy_req:path(Req0)),
|
cowboy_req:path(Req0)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, [{PostVars, true}], Req1} = cowboy_req:read_urlencoded_body(Req0),
|
{ok, [{PostVars, true}], Req1} = cowboy_req:read_urlencoded_body(Req0),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
#{<<"username">> := <<"user name">>,
|
#{
|
||||||
<<"clientid">> := <<"client id">>,
|
<<"username">> := <<"user name">>,
|
||||||
<<"peerhost">> := <<"127.0.0.1">>,
|
<<"clientid">> := <<"client id">>,
|
||||||
<<"proto_name">> := <<"MQTT">>,
|
<<"peerhost">> := <<"127.0.0.1">>,
|
||||||
<<"mountpoint">> := <<"MOUNTPOINT">>,
|
<<"proto_name">> := <<"MQTT">>,
|
||||||
<<"topic">> := <<"t">>,
|
<<"mountpoint">> := <<"MOUNTPOINT">>,
|
||||||
<<"action">> := <<"publish">>},
|
<<"topic">> := <<"t">>,
|
||||||
jiffy:decode(PostVars, [return_maps])),
|
<<"action">> := <<"publish">>
|
||||||
|
},
|
||||||
|
jiffy:decode(PostVars, [return_maps])
|
||||||
|
),
|
||||||
|
|
||||||
Req = cowboy_req:reply(200, Req1),
|
Req = cowboy_req:reply(200, Req1),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end,
|
end,
|
||||||
#{<<"method">> => <<"post">>,
|
#{
|
||||||
<<"body">> => #{<<"username">> => <<"${username}">>,
|
<<"method">> => <<"post">>,
|
||||||
<<"clientid">> => <<"${clientid}">>,
|
<<"body">> => #{
|
||||||
<<"peerhost">> => <<"${peerhost}">>,
|
<<"username">> => <<"${username}">>,
|
||||||
<<"proto_name">> => <<"${proto_name}">>,
|
<<"clientid">> => <<"${clientid}">>,
|
||||||
<<"mountpoint">> => <<"${mountpoint}">>,
|
<<"peerhost">> => <<"${peerhost}">>,
|
||||||
<<"topic">> => <<"${topic}">>,
|
<<"proto_name">> => <<"${proto_name}">>,
|
||||||
<<"action">> => <<"${action}">>},
|
<<"mountpoint">> => <<"${mountpoint}">>,
|
||||||
<<"headers">> => #{<<"content-type">> => <<"application/x-www-form-urlencoded">>}
|
<<"topic">> => <<"${topic}">>,
|
||||||
}),
|
<<"action">> => <<"${action}">>
|
||||||
|
},
|
||||||
|
<<"headers">> => #{<<"content-type">> => <<"application/x-www-form-urlencoded">>}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ClientInfo = #{clientid => <<"client id">>,
|
ClientInfo = #{
|
||||||
username => <<"user name">>,
|
clientid => <<"client id">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"user name">>,
|
||||||
protocol => <<"MQTT">>,
|
peerhost => {127, 0, 0, 1},
|
||||||
mountpoint => <<"MOUNTPOINT">>,
|
protocol => <<"MQTT">>,
|
||||||
zone => default,
|
mountpoint => <<"MOUNTPOINT">>,
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
).
|
||||||
|
|
||||||
t_create_replace(_Config) ->
|
t_create_replace(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
%% Create with valid URL
|
%% Create with valid URL
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
Req = cowboy_req:reply(200, Req0),
|
Req = cowboy_req:reply(200, Req0),
|
||||||
{ok, Req, State}
|
{ok, Req, State}
|
||||||
end,
|
end,
|
||||||
#{<<"url">> =>
|
#{
|
||||||
<<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>}),
|
<<"url">> =>
|
||||||
|
<<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
),
|
||||||
|
|
||||||
%% Changing to valid config
|
%% Changing to valid config
|
||||||
OkConfig = maps:merge(
|
OkConfig = maps:merge(
|
||||||
raw_http_authz_config(),
|
raw_http_authz_config(),
|
||||||
#{<<"url">> =>
|
#{
|
||||||
<<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>}),
|
<<"url">> =>
|
||||||
|
<<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
emqx_authz:update({?CMD_REPLACE, http}, OkConfig)),
|
emqx_authz:update({?CMD_REPLACE, http}, OkConfig)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -324,8 +373,9 @@ raw_http_authz_config() ->
|
||||||
setup_handler_and_config(Handler, Config) ->
|
setup_handler_and_config(Handler, Config) ->
|
||||||
ok = emqx_authz_http_test_server:set_handler(Handler),
|
ok = emqx_authz_http_test_server:set_handler(Handler),
|
||||||
ok = emqx_authz_test_lib:setup_config(
|
ok = emqx_authz_test_lib:setup_config(
|
||||||
raw_http_authz_config(),
|
raw_http_authz_config(),
|
||||||
Config).
|
Config
|
||||||
|
).
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
% API
|
% API
|
||||||
-export([start_link/2,
|
-export([
|
||||||
stop/0,
|
start_link/2,
|
||||||
set_handler/1
|
stop/0,
|
||||||
]).
|
set_handler/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% API
|
%% API
|
||||||
|
|
@ -51,11 +52,14 @@ set_handler(F) when is_function(F, 2) ->
|
||||||
|
|
||||||
init([Port, Path]) ->
|
init([Port, Path]) ->
|
||||||
Dispatch = cowboy_router:compile(
|
Dispatch = cowboy_router:compile(
|
||||||
[
|
[
|
||||||
{'_', [{Path, ?MODULE, []}]}
|
{'_', [{Path, ?MODULE, []}]}
|
||||||
]),
|
]
|
||||||
TransOpts = #{socket_opts => [{port, Port}],
|
),
|
||||||
connection_type => supervisor},
|
TransOpts = #{
|
||||||
|
socket_opts => [{port, Port}],
|
||||||
|
connection_type => supervisor
|
||||||
|
},
|
||||||
ProtoOpts = #{env => #{dispatch => Dispatch}},
|
ProtoOpts = #{env => #{dispatch => Dispatch}},
|
||||||
|
|
||||||
Tab = ets:new(?MODULE, [set, named_table, public]),
|
Tab = ets:new(?MODULE, [set, named_table, public]),
|
||||||
|
|
@ -78,9 +82,9 @@ init(Req, State) ->
|
||||||
|
|
||||||
default_handler(Req0, State) ->
|
default_handler(Req0, State) ->
|
||||||
Req = cowboy_req:reply(
|
Req = cowboy_req:reply(
|
||||||
400,
|
400,
|
||||||
#{<<"content-type">> => <<"text/plain">>},
|
#{<<"content-type">> => <<"text/plain">>},
|
||||||
<<"">>,
|
<<"">>,
|
||||||
Req0),
|
Req0
|
||||||
|
),
|
||||||
{ok, Req, State}.
|
{ok, Req, State}.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,9 @@ groups() ->
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz],
|
[emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1),
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
|
|
@ -47,7 +48,6 @@ end_per_testcase(_TestCase, _Config) ->
|
||||||
|
|
||||||
set_special_configs(emqx_authz) ->
|
set_special_configs(emqx_authz) ->
|
||||||
ok = emqx_authz_test_lib:reset_authorizers();
|
ok = emqx_authz_test_lib:reset_authorizers();
|
||||||
|
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -64,16 +64,17 @@ t_all_topic_rules(_Config) ->
|
||||||
ok = test_topic_rules(all).
|
ok = test_topic_rules(all).
|
||||||
|
|
||||||
test_topic_rules(Key) ->
|
test_topic_rules(Key) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
SetupSamples = fun(CInfo, Samples) ->
|
SetupSamples = fun(CInfo, Samples) ->
|
||||||
setup_client_samples(CInfo, Samples, Key)
|
setup_client_samples(CInfo, Samples, Key)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, SetupSamples),
|
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, SetupSamples),
|
||||||
|
|
||||||
|
|
@ -82,41 +83,50 @@ test_topic_rules(Key) ->
|
||||||
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, SetupSamples).
|
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, SetupSamples).
|
||||||
|
|
||||||
t_normalize_rules(_Config) ->
|
t_normalize_rules(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = emqx_authz_mnesia:store_rules(
|
ok = emqx_authz_mnesia:store_rules(
|
||||||
{username, <<"username">>},
|
{username, <<"username">>},
|
||||||
[{allow, publish, "t"}]),
|
[{allow, publish, "t"}]
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
allow,
|
allow,
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
|
),
|
||||||
|
|
||||||
?assertException(
|
?assertException(
|
||||||
error,
|
error,
|
||||||
{invalid_rule, _},
|
{invalid_rule, _},
|
||||||
emqx_authz_mnesia:store_rules(
|
emqx_authz_mnesia:store_rules(
|
||||||
{username, <<"username">>},
|
{username, <<"username">>},
|
||||||
[[allow, publish, <<"t">>]])),
|
[[allow, publish, <<"t">>]]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
?assertException(
|
?assertException(
|
||||||
error,
|
error,
|
||||||
{invalid_rule_action, _},
|
{invalid_rule_action, _},
|
||||||
emqx_authz_mnesia:store_rules(
|
emqx_authz_mnesia:store_rules(
|
||||||
{username, <<"username">>},
|
{username, <<"username">>},
|
||||||
[{allow, pub, <<"t">>}])),
|
[{allow, pub, <<"t">>}]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
?assertException(
|
?assertException(
|
||||||
error,
|
error,
|
||||||
{invalid_rule_permission, _},
|
{invalid_rule_permission, _},
|
||||||
emqx_authz_mnesia:store_rules(
|
emqx_authz_mnesia:store_rules(
|
||||||
{username, <<"username">>},
|
{username, <<"username">>},
|
||||||
[{accept, publish, <<"t">>}])).
|
[{accept, publish, <<"t">>}]
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -131,20 +141,23 @@ raw_mnesia_authz_config() ->
|
||||||
setup_client_samples(ClientInfo, Samples, Key) ->
|
setup_client_samples(ClientInfo, Samples, Key) ->
|
||||||
ok = emqx_authz_mnesia:purge_rules(),
|
ok = emqx_authz_mnesia:purge_rules(),
|
||||||
Rules = lists:flatmap(
|
Rules = lists:flatmap(
|
||||||
fun(#{topics := Topics, permission := Permission, action := Action}) ->
|
fun(#{topics := Topics, permission := Permission, action := Action}) ->
|
||||||
lists:map(
|
lists:map(
|
||||||
fun(Topic) ->
|
fun(Topic) ->
|
||||||
{binary_to_atom(Permission), binary_to_atom(Action), Topic}
|
{binary_to_atom(Permission), binary_to_atom(Action), Topic}
|
||||||
end,
|
end,
|
||||||
Topics)
|
Topics
|
||||||
end,
|
)
|
||||||
Samples),
|
end,
|
||||||
|
Samples
|
||||||
|
),
|
||||||
#{username := Username, clientid := ClientId} = ClientInfo,
|
#{username := Username, clientid := ClientId} = ClientInfo,
|
||||||
Who = case Key of
|
Who =
|
||||||
username -> {username, Username};
|
case Key of
|
||||||
clientid -> {clientid, ClientId};
|
username -> {username, Username};
|
||||||
all -> all
|
clientid -> {clientid, ClientId};
|
||||||
end,
|
all -> all
|
||||||
|
end,
|
||||||
ok = emqx_authz_mnesia:store_rules(Who, Rules).
|
ok = emqx_authz_mnesia:store_rules(Who, Rules).
|
||||||
|
|
||||||
setup_config() ->
|
setup_config() ->
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ init_per_suite(Config) ->
|
||||||
case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
|
case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
|
||||||
true ->
|
true ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz],
|
[emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1
|
fun set_special_configs/1
|
||||||
),
|
),
|
||||||
ok = start_apps([emqx_resource, emqx_connector]),
|
ok = start_apps([emqx_resource, emqx_connector]),
|
||||||
Config;
|
Config;
|
||||||
false ->
|
false ->
|
||||||
|
|
@ -54,7 +54,6 @@ end_per_suite(_Config) ->
|
||||||
|
|
||||||
set_special_configs(emqx_authz) ->
|
set_special_configs(emqx_authz) ->
|
||||||
ok = emqx_authz_test_lib:reset_authorizers();
|
ok = emqx_authz_test_lib:reset_authorizers();
|
||||||
|
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -72,12 +71,13 @@ end_per_testcase(_TestCase, _Config) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_topic_rules(_Config) ->
|
t_topic_rules(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
|
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
|
||||||
|
|
||||||
|
|
@ -87,104 +87,137 @@ t_topic_rules(_Config) ->
|
||||||
|
|
||||||
t_complex_selector(_) ->
|
t_complex_selector(_) ->
|
||||||
%% atom and string values also supported
|
%% atom and string values also supported
|
||||||
ClientInfo = #{clientid => clientid,
|
ClientInfo = #{
|
||||||
username => "username",
|
clientid => clientid,
|
||||||
peerhost => {127,0,0,1},
|
username => "username",
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
Samples = [#{<<"x">> => #{<<"u">> => <<"username">>,
|
Samples = [
|
||||||
<<"c">> => [#{<<"c">> => <<"clientid">>}],
|
#{
|
||||||
<<"y">> => 1},
|
<<"x">> => #{
|
||||||
<<"permission">> => <<"allow">>,
|
<<"u">> => <<"username">>,
|
||||||
<<"action">> => <<"publish">>,
|
<<"c">> => [#{<<"c">> => <<"clientid">>}],
|
||||||
<<"topics">> => [<<"t">>]
|
<<"y">> => 1
|
||||||
}],
|
},
|
||||||
|
<<"permission">> => <<"allow">>,
|
||||||
|
<<"action">> => <<"publish">>,
|
||||||
|
<<"topics">> => [<<"t">>]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
ok = setup_samples(Samples),
|
ok = setup_samples(Samples),
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"selector">> => #{<<"x">> => #{<<"u">> => <<"${username}">>,
|
#{
|
||||||
<<"c">> => [#{<<"c">> => <<"${clientid}">>}],
|
<<"selector">> => #{
|
||||||
<<"y">> => 1}
|
<<"x">> => #{
|
||||||
}
|
<<"u">> => <<"${username}">>,
|
||||||
}),
|
<<"c">> => [#{<<"c">> => <<"${clientid}">>}],
|
||||||
|
<<"y">> => 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, publish, <<"t">>}]).
|
[{allow, publish, <<"t">>}]
|
||||||
|
).
|
||||||
|
|
||||||
t_mongo_error(_Config) ->
|
t_mongo_error(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_samples([]),
|
ok = setup_samples([]),
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"selector">> => #{<<"$badoperator">> => <<"$badoperator">>}}),
|
#{<<"selector">> => #{<<"$badoperator">> => <<"$badoperator">>}}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{deny, publish, <<"t">>}]).
|
[{deny, publish, <<"t">>}]
|
||||||
|
).
|
||||||
|
|
||||||
t_lookups(_Config) ->
|
t_lookups(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
cn => <<"cn">>,
|
clientid => <<"clientid">>,
|
||||||
dn => <<"dn">>,
|
cn => <<"cn">>,
|
||||||
username => <<"username">>,
|
dn => <<"dn">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ByClientid = #{<<"clientid">> => <<"clientid">>,
|
ByClientid = #{
|
||||||
<<"topics">> => [<<"a">>],
|
<<"clientid">> => <<"clientid">>,
|
||||||
<<"action">> => <<"all">>,
|
<<"topics">> => [<<"a">>],
|
||||||
<<"permission">> => <<"allow">>},
|
<<"action">> => <<"all">>,
|
||||||
|
<<"permission">> => <<"allow">>
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_samples([ByClientid]),
|
ok = setup_samples([ByClientid]),
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"selector">> => #{<<"clientid">> => <<"${clientid}">>}}),
|
#{<<"selector">> => #{<<"clientid">> => <<"${clientid}">>}}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
ByPeerhost = #{<<"peerhost">> => <<"127.0.0.1">>,
|
ByPeerhost = #{
|
||||||
<<"topics">> => [<<"a">>],
|
<<"peerhost">> => <<"127.0.0.1">>,
|
||||||
<<"action">> => <<"all">>,
|
<<"topics">> => [<<"a">>],
|
||||||
<<"permission">> => <<"allow">>},
|
<<"action">> => <<"all">>,
|
||||||
|
<<"permission">> => <<"allow">>
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_samples([ByPeerhost]),
|
ok = setup_samples([ByPeerhost]),
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"selector">> => #{<<"peerhost">> => <<"${peerhost}">>}}),
|
#{<<"selector">> => #{<<"peerhost">> => <<"${peerhost}">>}}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]).
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
t_bad_selector(_Config) ->
|
t_bad_selector(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
cn => <<"cn">>,
|
clientid => <<"clientid">>,
|
||||||
dn => <<"dn">>,
|
cn => <<"cn">>,
|
||||||
username => <<"username">>,
|
dn => <<"dn">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"selector">> => #{<<"$in">> => #{<<"a">> => 1}}}),
|
#{<<"selector">> => #{<<"$in">> => #{<<"a">> => 1}}}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{deny, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]).
|
{deny, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -201,17 +234,22 @@ setup_samples(AclRecords) ->
|
||||||
setup_client_samples(ClientInfo, Samples) ->
|
setup_client_samples(ClientInfo, Samples) ->
|
||||||
#{username := Username} = ClientInfo,
|
#{username := Username} = ClientInfo,
|
||||||
Records = lists:map(
|
Records = lists:map(
|
||||||
fun(Sample) ->
|
fun(Sample) ->
|
||||||
#{topics := Topics,
|
#{
|
||||||
permission := Permission,
|
topics := Topics,
|
||||||
action := Action} = Sample,
|
permission := Permission,
|
||||||
|
action := Action
|
||||||
|
} = Sample,
|
||||||
|
|
||||||
#{<<"topics">> => Topics,
|
#{
|
||||||
<<"permission">> => Permission,
|
<<"topics">> => Topics,
|
||||||
<<"action">> => Action,
|
<<"permission">> => Permission,
|
||||||
<<"username">> => Username}
|
<<"action">> => Action,
|
||||||
end,
|
<<"username">> => Username
|
||||||
Samples),
|
}
|
||||||
|
end,
|
||||||
|
Samples
|
||||||
|
),
|
||||||
setup_samples(Records),
|
setup_samples(Records),
|
||||||
setup_config(#{<<"selector">> => #{<<"username">> => <<"${username}">>}}).
|
setup_config(#{<<"selector">> => #{<<"username">> => <<"${username}">>}}).
|
||||||
|
|
||||||
|
|
@ -221,8 +259,9 @@ reset_samples() ->
|
||||||
|
|
||||||
setup_config(SpecialParams) ->
|
setup_config(SpecialParams) ->
|
||||||
emqx_authz_test_lib:setup_config(
|
emqx_authz_test_lib:setup_config(
|
||||||
raw_mongo_authz_config(),
|
raw_mongo_authz_config(),
|
||||||
SpecialParams).
|
SpecialParams
|
||||||
|
).
|
||||||
|
|
||||||
raw_mongo_authz_config() ->
|
raw_mongo_authz_config() ->
|
||||||
#{
|
#{
|
||||||
|
|
@ -238,14 +277,14 @@ raw_mongo_authz_config() ->
|
||||||
}.
|
}.
|
||||||
|
|
||||||
mongo_server() ->
|
mongo_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?MONGO_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
|
||||||
|
|
||||||
mongo_config() ->
|
mongo_config() ->
|
||||||
[
|
[
|
||||||
{database, <<"mqtt">>},
|
{database, <<"mqtt">>},
|
||||||
{host, ?MONGO_HOST},
|
{host, ?MONGO_HOST},
|
||||||
{port, ?MONGO_DEFAULT_PORT},
|
{port, ?MONGO_DEFAULT_PORT},
|
||||||
{register, ?MONGO_CLIENT}
|
{register, ?MONGO_CLIENT}
|
||||||
].
|
].
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,17 @@ init_per_suite(Config) ->
|
||||||
case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
|
case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
|
||||||
true ->
|
true ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz],
|
[emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1
|
fun set_special_configs/1
|
||||||
),
|
),
|
||||||
ok = start_apps([emqx_resource, emqx_connector]),
|
ok = start_apps([emqx_resource, emqx_connector]),
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?MYSQL_RESOURCE,
|
?MYSQL_RESOURCE,
|
||||||
?RESOURCE_GROUP,
|
?RESOURCE_GROUP,
|
||||||
emqx_connector_mysql,
|
emqx_connector_mysql,
|
||||||
mysql_config(),
|
mysql_config(),
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
Config;
|
Config;
|
||||||
false ->
|
false ->
|
||||||
{skip, no_mysql}
|
{skip, no_mysql}
|
||||||
|
|
@ -64,7 +65,6 @@ init_per_testcase(_TestCase, Config) ->
|
||||||
|
|
||||||
set_special_configs(emqx_authz) ->
|
set_special_configs(emqx_authz) ->
|
||||||
ok = emqx_authz_test_lib:reset_authorizers();
|
ok = emqx_authz_test_lib:reset_authorizers();
|
||||||
|
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -73,12 +73,13 @@ set_special_configs(_) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_topic_rules(_Config) ->
|
t_topic_rules(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
|
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
|
||||||
|
|
||||||
|
|
@ -86,127 +87,190 @@ t_topic_rules(_Config) ->
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
|
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
|
||||||
|
|
||||||
|
|
||||||
t_lookups(_Config) ->
|
t_lookups(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
cn => <<"cn">>,
|
clientid => <<"clientid">>,
|
||||||
dn => <<"dn">>,
|
cn => <<"cn">>,
|
||||||
username => <<"username">>,
|
dn => <<"dn">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
%% by clientid
|
%% by clientid
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = q(<<"INSERT INTO acl(clientid, topic, permission, action)"
|
ok = q(
|
||||||
"VALUES(?, ?, ?, ?)">>,
|
<<
|
||||||
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(clientid, topic, permission, action)"
|
||||||
|
"VALUES(?, ?, ?, ?)"
|
||||||
|
>>,
|
||||||
|
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE clientid = ${clientid}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE clientid = ${clientid}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
%% by peerhost
|
%% by peerhost
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = q(<<"INSERT INTO acl(peerhost, topic, permission, action)"
|
ok = q(
|
||||||
"VALUES(?, ?, ?, ?)">>,
|
<<
|
||||||
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(peerhost, topic, permission, action)"
|
||||||
|
"VALUES(?, ?, ?, ?)"
|
||||||
|
>>,
|
||||||
|
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE peerhost = ${peerhost}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE peerhost = ${peerhost}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
%% by cn
|
%% by cn
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = q(<<"INSERT INTO acl(cn, topic, permission, action)"
|
ok = q(
|
||||||
"VALUES(?, ?, ?, ?)">>,
|
<<
|
||||||
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(cn, topic, permission, action)"
|
||||||
|
"VALUES(?, ?, ?, ?)"
|
||||||
|
>>,
|
||||||
|
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE cn = ${cert_common_name}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE cn = ${cert_common_name}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
%% by dn
|
%% by dn
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = q(<<"INSERT INTO acl(dn, topic, permission, action)"
|
ok = q(
|
||||||
"VALUES(?, ?, ?, ?)">>,
|
<<
|
||||||
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(dn, topic, permission, action)"
|
||||||
|
"VALUES(?, ?, ?, ?)"
|
||||||
|
>>,
|
||||||
|
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE dn = ${cert_subject}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE dn = ${cert_subject}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]).
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
t_mysql_error(_Config) ->
|
t_mysql_error(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SOME INVALID STATEMENT">>}),
|
#{<<"query">> => <<"SOME INVALID STATEMENT">>}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{deny, subscribe, <<"a">>}]).
|
[{deny, subscribe, <<"a">>}]
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid(_Config) ->
|
t_create_invalid(_Config) ->
|
||||||
BadConfig = maps:merge(
|
BadConfig = maps:merge(
|
||||||
raw_mysql_authz_config(),
|
raw_mysql_authz_config(),
|
||||||
#{<<"server">> => <<"255.255.255.255:33333">>}),
|
#{<<"server">> => <<"255.255.255.255:33333">>}
|
||||||
|
),
|
||||||
{ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
|
{ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
|
||||||
|
|
||||||
[_] = emqx_authz:lookup().
|
[_] = emqx_authz:lookup().
|
||||||
|
|
||||||
t_nonbinary_values(_Config) ->
|
t_nonbinary_values(_Config) ->
|
||||||
ClientInfo = #{clientid => clientid,
|
ClientInfo = #{
|
||||||
username => "username",
|
clientid => clientid,
|
||||||
peerhost => {127,0,0,1},
|
username => "username",
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = q(<<"INSERT INTO acl(clientid, username, topic, permission, action)"
|
ok = q(
|
||||||
"VALUES(?, ?, ?, ?, ?)">>,
|
<<
|
||||||
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(clientid, username, topic, permission, action)"
|
||||||
|
"VALUES(?, ?, ?, ?, ?)"
|
||||||
|
>>,
|
||||||
|
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE clientid = ${clientid} AND username = ${username}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE clientid = ${clientid} AND username = ${username}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]).
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -221,33 +285,39 @@ raw_mysql_authz_config() ->
|
||||||
<<"username">> => <<"root">>,
|
<<"username">> => <<"root">>,
|
||||||
<<"password">> => <<"public">>,
|
<<"password">> => <<"public">>,
|
||||||
|
|
||||||
<<"query">> => <<"SELECT permission, action, topic "
|
<<"query">> => <<
|
||||||
"FROM acl WHERE username = ${username}">>,
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE username = ${username}"
|
||||||
|
>>,
|
||||||
|
|
||||||
<<"server">> => mysql_server()
|
<<"server">> => mysql_server()
|
||||||
}.
|
}.
|
||||||
|
|
||||||
q(Sql) ->
|
q(Sql) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?MYSQL_RESOURCE,
|
?MYSQL_RESOURCE,
|
||||||
{sql, Sql}).
|
{sql, Sql}
|
||||||
|
).
|
||||||
|
|
||||||
q(Sql, Params) ->
|
q(Sql, Params) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?MYSQL_RESOURCE,
|
?MYSQL_RESOURCE,
|
||||||
{sql, Sql, Params}).
|
{sql, Sql, Params}
|
||||||
|
).
|
||||||
|
|
||||||
init_table() ->
|
init_table() ->
|
||||||
ok = drop_table(),
|
ok = drop_table(),
|
||||||
ok = q("CREATE TABLE acl(
|
ok = q(
|
||||||
username VARCHAR(255),
|
"CREATE TABLE acl(\n"
|
||||||
clientid VARCHAR(255),
|
" username VARCHAR(255),\n"
|
||||||
peerhost VARCHAR(255),
|
" clientid VARCHAR(255),\n"
|
||||||
cn VARCHAR(255),
|
" peerhost VARCHAR(255),\n"
|
||||||
dn VARCHAR(255),
|
" cn VARCHAR(255),\n"
|
||||||
topic VARCHAR(255),
|
" dn VARCHAR(255),\n"
|
||||||
permission VARCHAR(255),
|
" topic VARCHAR(255),\n"
|
||||||
action VARCHAR(255))").
|
" permission VARCHAR(255),\n"
|
||||||
|
" action VARCHAR(255))"
|
||||||
|
).
|
||||||
|
|
||||||
drop_table() ->
|
drop_table() ->
|
||||||
ok = q("DROP TABLE IF EXISTS acl").
|
ok = q("DROP TABLE IF EXISTS acl").
|
||||||
|
|
@ -256,37 +326,50 @@ setup_client_samples(ClientInfo, Samples) ->
|
||||||
#{username := Username} = ClientInfo,
|
#{username := Username} = ClientInfo,
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(#{topics := Topics, permission := Permission, action := Action}) ->
|
fun(#{topics := Topics, permission := Permission, action := Action}) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Topic) ->
|
fun(Topic) ->
|
||||||
q(<<"INSERT INTO acl(username, topic, permission, action)"
|
q(
|
||||||
"VALUES(?, ?, ?, ?)">>,
|
<<
|
||||||
[Username, Topic, Permission, Action])
|
"INSERT INTO acl(username, topic, permission, action)"
|
||||||
end,
|
"VALUES(?, ?, ?, ?)"
|
||||||
Topics)
|
>>,
|
||||||
end,
|
[Username, Topic, Permission, Action]
|
||||||
Samples),
|
)
|
||||||
|
end,
|
||||||
|
Topics
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
Samples
|
||||||
|
),
|
||||||
setup_config(
|
setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE username = ${username}">>}).
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE username = ${username}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
setup_config(SpecialParams) ->
|
setup_config(SpecialParams) ->
|
||||||
emqx_authz_test_lib:setup_config(
|
emqx_authz_test_lib:setup_config(
|
||||||
raw_mysql_authz_config(),
|
raw_mysql_authz_config(),
|
||||||
SpecialParams).
|
SpecialParams
|
||||||
|
).
|
||||||
|
|
||||||
mysql_server() ->
|
mysql_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?MYSQL_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?MYSQL_HOST])).
|
||||||
|
|
||||||
mysql_config() ->
|
mysql_config() ->
|
||||||
#{auto_reconnect => true,
|
#{
|
||||||
database => <<"mqtt">>,
|
auto_reconnect => true,
|
||||||
username => <<"root">>,
|
database => <<"mqtt">>,
|
||||||
password => <<"public">>,
|
username => <<"root">>,
|
||||||
pool_size => 8,
|
password => <<"public">>,
|
||||||
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
|
pool_size => 8,
|
||||||
ssl => #{enable => false}
|
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
|
||||||
}.
|
ssl => #{enable => false}
|
||||||
|
}.
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,17 @@ init_per_suite(Config) ->
|
||||||
case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
|
case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
|
||||||
true ->
|
true ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz],
|
[emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1
|
fun set_special_configs/1
|
||||||
),
|
),
|
||||||
ok = start_apps([emqx_resource, emqx_connector]),
|
ok = start_apps([emqx_resource, emqx_connector]),
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?PGSQL_RESOURCE,
|
?PGSQL_RESOURCE,
|
||||||
?RESOURCE_GROUP,
|
?RESOURCE_GROUP,
|
||||||
emqx_connector_pgsql,
|
emqx_connector_pgsql,
|
||||||
pgsql_config(),
|
pgsql_config(),
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
Config;
|
Config;
|
||||||
false ->
|
false ->
|
||||||
{skip, no_pgsql}
|
{skip, no_pgsql}
|
||||||
|
|
@ -64,7 +65,6 @@ init_per_testcase(_TestCase, Config) ->
|
||||||
|
|
||||||
set_special_configs(emqx_authz) ->
|
set_special_configs(emqx_authz) ->
|
||||||
ok = emqx_authz_test_lib:reset_authorizers();
|
ok = emqx_authz_test_lib:reset_authorizers();
|
||||||
|
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -73,12 +73,13 @@ set_special_configs(_) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_topic_rules(_Config) ->
|
t_topic_rules(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
|
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
|
||||||
|
|
||||||
|
|
@ -86,128 +87,195 @@ t_topic_rules(_Config) ->
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
|
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
|
||||||
|
|
||||||
|
|
||||||
t_lookups(_Config) ->
|
t_lookups(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
cn => <<"cn">>,
|
clientid => <<"clientid">>,
|
||||||
dn => <<"dn">>,
|
cn => <<"cn">>,
|
||||||
username => <<"username">>,
|
dn => <<"dn">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
%% by clientid
|
%% by clientid
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = insert(<<"INSERT INTO acl(clientid, topic, permission, action)"
|
ok = insert(
|
||||||
"VALUES($1, $2, $3, $4)">>,
|
<<
|
||||||
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(clientid, topic, permission, action)"
|
||||||
|
"VALUES($1, $2, $3, $4)"
|
||||||
|
>>,
|
||||||
|
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE clientid = ${clientid}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE clientid = ${clientid}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
%% by peerhost
|
%% by peerhost
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = insert(<<"INSERT INTO acl(peerhost, topic, permission, action)"
|
ok = insert(
|
||||||
"VALUES($1, $2, $3, $4)">>,
|
<<
|
||||||
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(peerhost, topic, permission, action)"
|
||||||
|
"VALUES($1, $2, $3, $4)"
|
||||||
|
>>,
|
||||||
|
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE peerhost = ${peerhost}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE peerhost = ${peerhost}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
%% by cn
|
%% by cn
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = insert(<<"INSERT INTO acl(cn, topic, permission, action)"
|
ok = insert(
|
||||||
"VALUES($1, $2, $3, $4)">>,
|
<<
|
||||||
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(cn, topic, permission, action)"
|
||||||
|
"VALUES($1, $2, $3, $4)"
|
||||||
|
>>,
|
||||||
|
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE cn = ${cert_common_name}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE cn = ${cert_common_name}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
%% by dn
|
%% by dn
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = insert(<<"INSERT INTO acl(dn, topic, permission, action)"
|
ok = insert(
|
||||||
"VALUES($1, $2, $3, $4)">>,
|
<<
|
||||||
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(dn, topic, permission, action)"
|
||||||
|
"VALUES($1, $2, $3, $4)"
|
||||||
|
>>,
|
||||||
|
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE dn = ${cert_subject}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE dn = ${cert_subject}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]).
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
t_pgsql_error(_Config) ->
|
t_pgsql_error(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE clientid = ${username}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE clientid = ${username}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{deny, subscribe, <<"a">>}]).
|
[{deny, subscribe, <<"a">>}]
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid(_Config) ->
|
t_create_invalid(_Config) ->
|
||||||
BadConfig = maps:merge(
|
BadConfig = maps:merge(
|
||||||
raw_pgsql_authz_config(),
|
raw_pgsql_authz_config(),
|
||||||
#{<<"server">> => <<"255.255.255.255:33333">>}),
|
#{<<"server">> => <<"255.255.255.255:33333">>}
|
||||||
|
),
|
||||||
{ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
|
{ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
|
||||||
|
|
||||||
[_] = emqx_authz:lookup().
|
[_] = emqx_authz:lookup().
|
||||||
|
|
||||||
t_nonbinary_values(_Config) ->
|
t_nonbinary_values(_Config) ->
|
||||||
ClientInfo = #{clientid => clientid,
|
ClientInfo = #{
|
||||||
username => "username",
|
clientid => clientid,
|
||||||
peerhost => {127,0,0,1},
|
username => "username",
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = insert(<<"INSERT INTO acl(clientid, username, topic, permission, action)"
|
ok = insert(
|
||||||
"VALUES($1, $2, $3, $4, $5)">>,
|
<<
|
||||||
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
|
"INSERT INTO acl(clientid, username, topic, permission, action)"
|
||||||
|
"VALUES($1, $2, $3, $4, $5)"
|
||||||
|
>>,
|
||||||
|
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]
|
||||||
|
),
|
||||||
|
|
||||||
ok = setup_config(
|
ok = setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE clientid = ${clientid} AND username = ${username}">>}),
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE clientid = ${clientid} AND username = ${username}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]).
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
@ -222,34 +290,40 @@ raw_pgsql_authz_config() ->
|
||||||
<<"username">> => <<"root">>,
|
<<"username">> => <<"root">>,
|
||||||
<<"password">> => <<"public">>,
|
<<"password">> => <<"public">>,
|
||||||
|
|
||||||
<<"query">> => <<"SELECT permission, action, topic "
|
<<"query">> => <<
|
||||||
"FROM acl WHERE username = ${username}">>,
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE username = ${username}"
|
||||||
|
>>,
|
||||||
|
|
||||||
<<"server">> => pgsql_server()
|
<<"server">> => pgsql_server()
|
||||||
}.
|
}.
|
||||||
|
|
||||||
q(Sql) ->
|
q(Sql) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?PGSQL_RESOURCE,
|
?PGSQL_RESOURCE,
|
||||||
{query, Sql}).
|
{query, Sql}
|
||||||
|
).
|
||||||
|
|
||||||
insert(Sql, Params) ->
|
insert(Sql, Params) ->
|
||||||
{ok, _} = emqx_resource:query(
|
{ok, _} = emqx_resource:query(
|
||||||
?PGSQL_RESOURCE,
|
?PGSQL_RESOURCE,
|
||||||
{query, Sql, Params}),
|
{query, Sql, Params}
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
init_table() ->
|
init_table() ->
|
||||||
ok = drop_table(),
|
ok = drop_table(),
|
||||||
{ok, _, _} = q("CREATE TABLE acl(
|
{ok, _, _} = q(
|
||||||
username VARCHAR(255),
|
"CREATE TABLE acl(\n"
|
||||||
clientid VARCHAR(255),
|
" username VARCHAR(255),\n"
|
||||||
peerhost VARCHAR(255),
|
" clientid VARCHAR(255),\n"
|
||||||
cn VARCHAR(255),
|
" peerhost VARCHAR(255),\n"
|
||||||
dn VARCHAR(255),
|
" cn VARCHAR(255),\n"
|
||||||
topic VARCHAR(255),
|
" dn VARCHAR(255),\n"
|
||||||
permission VARCHAR(255),
|
" topic VARCHAR(255),\n"
|
||||||
action VARCHAR(255))"),
|
" permission VARCHAR(255),\n"
|
||||||
|
" action VARCHAR(255))"
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
drop_table() ->
|
drop_table() ->
|
||||||
|
|
@ -260,37 +334,50 @@ setup_client_samples(ClientInfo, Samples) ->
|
||||||
#{username := Username} = ClientInfo,
|
#{username := Username} = ClientInfo,
|
||||||
ok = init_table(),
|
ok = init_table(),
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(#{topics := Topics, permission := Permission, action := Action}) ->
|
fun(#{topics := Topics, permission := Permission, action := Action}) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Topic) ->
|
fun(Topic) ->
|
||||||
insert(<<"INSERT INTO acl(username, topic, permission, action)"
|
insert(
|
||||||
"VALUES($1, $2, $3, $4)">>,
|
<<
|
||||||
[Username, Topic, Permission, Action])
|
"INSERT INTO acl(username, topic, permission, action)"
|
||||||
end,
|
"VALUES($1, $2, $3, $4)"
|
||||||
Topics)
|
>>,
|
||||||
end,
|
[Username, Topic, Permission, Action]
|
||||||
Samples),
|
)
|
||||||
|
end,
|
||||||
|
Topics
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
Samples
|
||||||
|
),
|
||||||
setup_config(
|
setup_config(
|
||||||
#{<<"query">> => <<"SELECT permission, action, topic "
|
#{
|
||||||
"FROM acl WHERE username = ${username}">>}).
|
<<"query">> => <<
|
||||||
|
"SELECT permission, action, topic "
|
||||||
|
"FROM acl WHERE username = ${username}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
setup_config(SpecialParams) ->
|
setup_config(SpecialParams) ->
|
||||||
emqx_authz_test_lib:setup_config(
|
emqx_authz_test_lib:setup_config(
|
||||||
raw_pgsql_authz_config(),
|
raw_pgsql_authz_config(),
|
||||||
SpecialParams).
|
SpecialParams
|
||||||
|
).
|
||||||
|
|
||||||
pgsql_server() ->
|
pgsql_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
|
||||||
|
|
||||||
pgsql_config() ->
|
pgsql_config() ->
|
||||||
#{auto_reconnect => true,
|
#{
|
||||||
database => <<"mqtt">>,
|
auto_reconnect => true,
|
||||||
username => <<"root">>,
|
database => <<"mqtt">>,
|
||||||
password => <<"public">>,
|
username => <<"root">>,
|
||||||
pool_size => 8,
|
password => <<"public">>,
|
||||||
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
|
pool_size => 8,
|
||||||
ssl => #{enable => false}
|
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
|
||||||
}.
|
ssl => #{enable => false}
|
||||||
|
}.
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,17 @@ init_per_suite(Config) ->
|
||||||
case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_DEFAULT_PORT) of
|
case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_DEFAULT_PORT) of
|
||||||
true ->
|
true ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz],
|
[emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1
|
fun set_special_configs/1
|
||||||
),
|
),
|
||||||
ok = start_apps([emqx_resource, emqx_connector]),
|
ok = start_apps([emqx_resource, emqx_connector]),
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?REDIS_RESOURCE,
|
?REDIS_RESOURCE,
|
||||||
?RESOURCE_GROUP,
|
?RESOURCE_GROUP,
|
||||||
emqx_connector_redis,
|
emqx_connector_redis,
|
||||||
redis_config(),
|
redis_config(),
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
Config;
|
Config;
|
||||||
false ->
|
false ->
|
||||||
{skip, no_redis}
|
{skip, no_redis}
|
||||||
|
|
@ -65,109 +66,130 @@ init_per_testcase(_TestCase, Config) ->
|
||||||
|
|
||||||
set_special_configs(emqx_authz) ->
|
set_special_configs(emqx_authz) ->
|
||||||
ok = emqx_authz_test_lib:reset_authorizers();
|
ok = emqx_authz_test_lib:reset_authorizers();
|
||||||
|
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Tests
|
%% Tests
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_topic_rules(_Config) ->
|
t_topic_rules(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
|
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, fun setup_client_samples/2).
|
ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, fun setup_client_samples/2).
|
||||||
|
|
||||||
|
|
||||||
t_lookups(_Config) ->
|
t_lookups(_Config) ->
|
||||||
ClientInfo = #{clientid => <<"client id">>,
|
ClientInfo = #{
|
||||||
cn => <<"cn">>,
|
clientid => <<"client id">>,
|
||||||
dn => <<"dn">>,
|
cn => <<"cn">>,
|
||||||
username => <<"username">>,
|
dn => <<"dn">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
ByClientid = #{<<"mqtt_user:client id">> =>
|
ByClientid = #{
|
||||||
#{<<"a">> => <<"all">>}},
|
<<"mqtt_user:client id">> =>
|
||||||
|
#{<<"a">> => <<"all">>}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_sample(ByClientid),
|
ok = setup_sample(ByClientid),
|
||||||
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${clientid}">>}),
|
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${clientid}">>}),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
ByPeerhost = #{<<"mqtt_user:127.0.0.1">> =>
|
ByPeerhost = #{
|
||||||
#{<<"a">> => <<"all">>}},
|
<<"mqtt_user:127.0.0.1">> =>
|
||||||
|
#{<<"a">> => <<"all">>}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_sample(ByPeerhost),
|
ok = setup_sample(ByPeerhost),
|
||||||
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${peerhost}">>}),
|
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${peerhost}">>}),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
ByCN = #{<<"mqtt_user:cn">> =>
|
ByCN = #{
|
||||||
#{<<"a">> => <<"all">>}},
|
<<"mqtt_user:cn">> =>
|
||||||
|
#{<<"a">> => <<"all">>}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_sample(ByCN),
|
ok = setup_sample(ByCN),
|
||||||
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${cert_common_name}">>}),
|
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${cert_common_name}">>}),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]),
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
|
ByDN = #{
|
||||||
ByDN = #{<<"mqtt_user:dn">> =>
|
<<"mqtt_user:dn">> =>
|
||||||
#{<<"a">> => <<"all">>}},
|
#{<<"a">> => <<"all">>}
|
||||||
|
},
|
||||||
|
|
||||||
ok = setup_sample(ByDN),
|
ok = setup_sample(ByDN),
|
||||||
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${cert_subject}">>}),
|
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${cert_subject}">>}),
|
||||||
|
|
||||||
ok = emqx_authz_test_lib:test_samples(
|
ok = emqx_authz_test_lib:test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{allow, subscribe, <<"a">>},
|
[
|
||||||
{deny, subscribe, <<"b">>}]).
|
{allow, subscribe, <<"a">>},
|
||||||
|
{deny, subscribe, <<"b">>}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
t_create_invalid(_Config) ->
|
t_create_invalid(_Config) ->
|
||||||
AuthzConfig = raw_redis_authz_config(),
|
AuthzConfig = raw_redis_authz_config(),
|
||||||
|
|
||||||
InvalidConfigs =
|
InvalidConfigs =
|
||||||
[maps:without([<<"server">>], AuthzConfig),
|
[
|
||||||
AuthzConfig#{<<"server">> => <<"unknownhost:3333">>},
|
maps:without([<<"server">>], AuthzConfig),
|
||||||
AuthzConfig#{<<"password">> => <<"wrongpass">>},
|
AuthzConfig#{<<"server">> => <<"unknownhost:3333">>},
|
||||||
AuthzConfig#{<<"database">> => <<"5678">>}],
|
AuthzConfig#{<<"password">> => <<"wrongpass">>},
|
||||||
|
AuthzConfig#{<<"database">> => <<"5678">>}
|
||||||
|
],
|
||||||
|
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Config) ->
|
fun(Config) ->
|
||||||
{ok, _} = emqx_authz:update(?CMD_REPLACE, [Config]),
|
{ok, _} = emqx_authz:update(?CMD_REPLACE, [Config]),
|
||||||
[_] = emqx_authz:lookup()
|
[_] = emqx_authz:lookup()
|
||||||
|
end,
|
||||||
end,
|
InvalidConfigs
|
||||||
InvalidConfigs).
|
).
|
||||||
|
|
||||||
t_redis_error(_Config) ->
|
t_redis_error(_Config) ->
|
||||||
ok = setup_config(#{<<"cmd">> => <<"INVALID COMMAND">>}),
|
ok = setup_config(#{<<"cmd">> => <<"INVALID COMMAND">>}),
|
||||||
|
|
||||||
ClientInfo = #{clientid => <<"clientid">>,
|
ClientInfo = #{
|
||||||
username => <<"username">>,
|
clientid => <<"clientid">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"username">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
deny = emqx_access_control:authorize(ClientInfo, subscribe, <<"a">>).
|
deny = emqx_access_control:authorize(ClientInfo, subscribe, <<"a">>).
|
||||||
|
|
||||||
|
|
@ -178,30 +200,36 @@ t_redis_error(_Config) ->
|
||||||
setup_sample(AuthzData) ->
|
setup_sample(AuthzData) ->
|
||||||
{ok, _} = q(["FLUSHDB"]),
|
{ok, _} = q(["FLUSHDB"]),
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun({Key, Values}) ->
|
fun({Key, Values}) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun({TopicFilter, Action}) ->
|
fun({TopicFilter, Action}) ->
|
||||||
q(["HSET", Key, TopicFilter, Action])
|
q(["HSET", Key, TopicFilter, Action])
|
||||||
end,
|
end,
|
||||||
maps:to_list(Values))
|
maps:to_list(Values)
|
||||||
end,
|
)
|
||||||
maps:to_list(AuthzData)).
|
end,
|
||||||
|
maps:to_list(AuthzData)
|
||||||
|
).
|
||||||
|
|
||||||
setup_client_samples(ClientInfo, Samples) ->
|
setup_client_samples(ClientInfo, Samples) ->
|
||||||
#{username := Username} = ClientInfo,
|
#{username := Username} = ClientInfo,
|
||||||
Key = <<"mqtt_user:", Username/binary>>,
|
Key = <<"mqtt_user:", Username/binary>>,
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Sample) ->
|
fun(Sample) ->
|
||||||
#{topics := Topics,
|
#{
|
||||||
|
topics := Topics,
|
||||||
permission := <<"allow">>,
|
permission := <<"allow">>,
|
||||||
action := Action} = Sample,
|
action := Action
|
||||||
lists:foreach(
|
} = Sample,
|
||||||
|
lists:foreach(
|
||||||
fun(Topic) ->
|
fun(Topic) ->
|
||||||
q(["HSET", Key, Topic, Action])
|
q(["HSET", Key, Topic, Action])
|
||||||
end,
|
end,
|
||||||
Topics)
|
Topics
|
||||||
end,
|
)
|
||||||
Samples),
|
end,
|
||||||
|
Samples
|
||||||
|
),
|
||||||
setup_config(#{}).
|
setup_config(#{}).
|
||||||
|
|
||||||
setup_config(SpecialParams) ->
|
setup_config(SpecialParams) ->
|
||||||
|
|
@ -221,22 +249,24 @@ raw_redis_authz_config() ->
|
||||||
}.
|
}.
|
||||||
|
|
||||||
redis_server() ->
|
redis_server() ->
|
||||||
iolist_to_binary(io_lib:format("~s",[?REDIS_HOST])).
|
iolist_to_binary(io_lib:format("~s", [?REDIS_HOST])).
|
||||||
|
|
||||||
q(Command) ->
|
q(Command) ->
|
||||||
emqx_resource:query(
|
emqx_resource:query(
|
||||||
?REDIS_RESOURCE,
|
?REDIS_RESOURCE,
|
||||||
{cmd, Command}).
|
{cmd, Command}
|
||||||
|
).
|
||||||
|
|
||||||
redis_config() ->
|
redis_config() ->
|
||||||
#{auto_reconnect => true,
|
#{
|
||||||
database => 1,
|
auto_reconnect => true,
|
||||||
pool_size => 8,
|
database => 1,
|
||||||
redis_type => single,
|
pool_size => 8,
|
||||||
password => "public",
|
redis_type => single,
|
||||||
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
|
password => "public",
|
||||||
ssl => #{enable => false}
|
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
|
||||||
}.
|
ssl => #{enable => false}
|
||||||
|
}.
|
||||||
|
|
||||||
start_apps(Apps) ->
|
start_apps(Apps) ->
|
||||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||||
|
|
|
||||||
|
|
@ -23,30 +23,38 @@
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||||
|
|
||||||
-define(SOURCE1, {deny, all}).
|
-define(SOURCE1, {deny, all}).
|
||||||
-define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}).
|
-define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}).
|
||||||
-define(SOURCE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, [?PH_S_CLIENTID]}).
|
-define(SOURCE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, [?PH_S_CLIENTID]}).
|
||||||
-define(SOURCE4, {allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}).
|
-define(SOURCE4, {allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}).
|
||||||
-define(SOURCE5, {allow, {'or',
|
-define(SOURCE5,
|
||||||
[{username, {re, "^test"}},
|
{allow,
|
||||||
{clientid, {re, "test?"}}]},
|
{'or', [
|
||||||
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}).
|
{username, {re, "^test"}},
|
||||||
|
{clientid, {re, "test?"}}
|
||||||
|
]},
|
||||||
|
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
|
||||||
|
).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps(
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
[emqx_conf, emqx_authz],
|
[emqx_conf, emqx_authz],
|
||||||
fun set_special_configs/1),
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
[authorization],
|
[authorization],
|
||||||
#{<<"no_match">> => <<"allow">>,
|
#{
|
||||||
<<"cache">> => #{<<"enable">> => <<"true">>},
|
<<"no_match">> => <<"allow">>,
|
||||||
<<"sources">> => []}),
|
<<"cache">> => #{<<"enable">> => <<"true">>},
|
||||||
|
<<"sources">> => []
|
||||||
|
}
|
||||||
|
),
|
||||||
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
|
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -61,115 +69,242 @@ set_special_configs(_App) ->
|
||||||
t_compile(_) ->
|
t_compile(_) ->
|
||||||
?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?SOURCE1)),
|
?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?SOURCE1)),
|
||||||
|
|
||||||
?assertEqual({allow, {ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}},
|
?assertEqual(
|
||||||
all, [{eq, ['#']}, {eq, ['+']}]}, emqx_authz_rule:compile(?SOURCE2)),
|
{allow, {ipaddr, {{127, 0, 0, 1}, {127, 0, 0, 1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]},
|
||||||
|
emqx_authz_rule:compile(?SOURCE2)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual({allow,
|
?assertEqual(
|
||||||
{ipaddrs,[{{127,0,0,1},{127,0,0,1},32},
|
{allow,
|
||||||
{{192,168,1,0},{192,168,1,255},24}]},
|
{ipaddrs, [
|
||||||
subscribe,
|
{{127, 0, 0, 1}, {127, 0, 0, 1}, 32},
|
||||||
[{pattern,[?PH_CLIENTID]}]
|
{{192, 168, 1, 0}, {192, 168, 1, 255}, 24}
|
||||||
}, emqx_authz_rule:compile(?SOURCE3)),
|
]},
|
||||||
|
subscribe, [{pattern, [?PH_CLIENTID]}]},
|
||||||
|
emqx_authz_rule:compile(?SOURCE3)
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch({allow,
|
?assertMatch(
|
||||||
{'and', [{clientid, {eq, <<"test">>}}, {username, {eq, <<"test">>}}]},
|
{allow, {'and', [{clientid, {eq, <<"test">>}}, {username, {eq, <<"test">>}}]}, publish, [
|
||||||
publish,
|
[<<"topic">>, <<"test">>]
|
||||||
[[<<"topic">>, <<"test">>]]
|
]},
|
||||||
}, emqx_authz_rule:compile(?SOURCE4)),
|
emqx_authz_rule:compile(?SOURCE4)
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch({allow,
|
?assertMatch(
|
||||||
{'or', [{username, {re_pattern, _, _, _, _}},
|
{allow,
|
||||||
{clientid, {re_pattern, _, _, _, _}}]},
|
{'or', [
|
||||||
publish, [{pattern, [?PH_USERNAME]}, {pattern, [?PH_CLIENTID]}]
|
{username, {re_pattern, _, _, _, _}},
|
||||||
}, emqx_authz_rule:compile(?SOURCE5)),
|
{clientid, {re_pattern, _, _, _, _}}
|
||||||
|
]},
|
||||||
|
publish, [{pattern, [?PH_USERNAME]}, {pattern, [?PH_CLIENTID]}]},
|
||||||
|
emqx_authz_rule:compile(?SOURCE5)
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
||||||
t_match(_) ->
|
t_match(_) ->
|
||||||
ClientInfo1 = #{clientid => <<"test">>,
|
ClientInfo1 = #{
|
||||||
username => <<"test">>,
|
clientid => <<"test">>,
|
||||||
peerhost => {127,0,0,1},
|
username => <<"test">>,
|
||||||
zone => default,
|
peerhost => {127, 0, 0, 1},
|
||||||
listener => {tcp, default}
|
zone => default,
|
||||||
},
|
listener => {tcp, default}
|
||||||
ClientInfo2 = #{clientid => <<"test">>,
|
},
|
||||||
username => <<"test">>,
|
ClientInfo2 = #{
|
||||||
peerhost => {192,168,1,10},
|
clientid => <<"test">>,
|
||||||
zone => default,
|
username => <<"test">>,
|
||||||
listener => {tcp, default}
|
peerhost => {192, 168, 1, 10},
|
||||||
},
|
zone => default,
|
||||||
ClientInfo3 = #{clientid => <<"test">>,
|
listener => {tcp, default}
|
||||||
username => <<"fake">>,
|
},
|
||||||
peerhost => {127,0,0,1},
|
ClientInfo3 = #{
|
||||||
zone => default,
|
clientid => <<"test">>,
|
||||||
listener => {tcp, default}
|
username => <<"fake">>,
|
||||||
},
|
peerhost => {127, 0, 0, 1},
|
||||||
ClientInfo4 = #{clientid => <<"fake">>,
|
zone => default,
|
||||||
username => <<"test">>,
|
listener => {tcp, default}
|
||||||
peerhost => {127,0,0,1},
|
},
|
||||||
zone => default,
|
ClientInfo4 = #{
|
||||||
listener => {tcp, default}
|
clientid => <<"fake">>,
|
||||||
},
|
username => <<"test">>,
|
||||||
|
peerhost => {127, 0, 0, 1},
|
||||||
|
zone => default,
|
||||||
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
?assertEqual({matched, deny},
|
?assertEqual(
|
||||||
emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>,
|
{matched, deny},
|
||||||
emqx_authz_rule:compile(?SOURCE1))),
|
emqx_authz_rule:match(
|
||||||
?assertEqual({matched, deny},
|
ClientInfo1,
|
||||||
emqx_authz_rule:match(ClientInfo2, subscribe, <<"+">>,
|
subscribe,
|
||||||
emqx_authz_rule:compile(?SOURCE1))),
|
<<"#">>,
|
||||||
?assertEqual({matched, deny},
|
emqx_authz_rule:compile(?SOURCE1)
|
||||||
emqx_authz_rule:match(ClientInfo3, subscribe, <<"topic/test">>,
|
)
|
||||||
emqx_authz_rule:compile(?SOURCE1))),
|
),
|
||||||
|
?assertEqual(
|
||||||
|
{matched, deny},
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo2,
|
||||||
|
subscribe,
|
||||||
|
<<"+">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE1)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
{matched, deny},
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo3,
|
||||||
|
subscribe,
|
||||||
|
<<"topic/test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE1)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual({matched, allow},
|
?assertEqual(
|
||||||
emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>,
|
{matched, allow},
|
||||||
emqx_authz_rule:compile(?SOURCE2))),
|
emqx_authz_rule:match(
|
||||||
?assertEqual(nomatch,
|
ClientInfo1,
|
||||||
emqx_authz_rule:match(ClientInfo1, subscribe, <<"topic/test">>,
|
subscribe,
|
||||||
emqx_authz_rule:compile(?SOURCE2))),
|
<<"#">>,
|
||||||
?assertEqual(nomatch,
|
emqx_authz_rule:compile(?SOURCE2)
|
||||||
emqx_authz_rule:match(ClientInfo2, subscribe, <<"#">>,
|
)
|
||||||
emqx_authz_rule:compile(?SOURCE2))),
|
),
|
||||||
|
?assertEqual(
|
||||||
|
nomatch,
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo1,
|
||||||
|
subscribe,
|
||||||
|
<<"topic/test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE2)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
nomatch,
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo2,
|
||||||
|
subscribe,
|
||||||
|
<<"#">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE2)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual({matched, allow},
|
?assertEqual(
|
||||||
emqx_authz_rule:match(ClientInfo1, subscribe, <<"test">>,
|
{matched, allow},
|
||||||
emqx_authz_rule:compile(?SOURCE3))),
|
emqx_authz_rule:match(
|
||||||
?assertEqual({matched, allow},
|
ClientInfo1,
|
||||||
emqx_authz_rule:match(ClientInfo2, subscribe, <<"test">>,
|
subscribe,
|
||||||
emqx_authz_rule:compile(?SOURCE3))),
|
<<"test">>,
|
||||||
?assertEqual(nomatch,
|
emqx_authz_rule:compile(?SOURCE3)
|
||||||
emqx_authz_rule:match(ClientInfo2, subscribe, <<"topic/test">>,
|
)
|
||||||
emqx_authz_rule:compile(?SOURCE3))),
|
),
|
||||||
|
?assertEqual(
|
||||||
|
{matched, allow},
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo2,
|
||||||
|
subscribe,
|
||||||
|
<<"test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE3)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
nomatch,
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo2,
|
||||||
|
subscribe,
|
||||||
|
<<"topic/test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE3)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual({matched, allow},
|
?assertEqual(
|
||||||
emqx_authz_rule:match(ClientInfo1, publish, <<"topic/test">>,
|
{matched, allow},
|
||||||
emqx_authz_rule:compile(?SOURCE4))),
|
emqx_authz_rule:match(
|
||||||
?assertEqual({matched, allow},
|
ClientInfo1,
|
||||||
emqx_authz_rule:match(ClientInfo2, publish, <<"topic/test">>,
|
publish,
|
||||||
emqx_authz_rule:compile(?SOURCE4))),
|
<<"topic/test">>,
|
||||||
?assertEqual(nomatch,
|
emqx_authz_rule:compile(?SOURCE4)
|
||||||
emqx_authz_rule:match(ClientInfo3, publish, <<"topic/test">>,
|
)
|
||||||
emqx_authz_rule:compile(?SOURCE4))),
|
),
|
||||||
?assertEqual(nomatch,
|
?assertEqual(
|
||||||
emqx_authz_rule:match(ClientInfo4, publish, <<"topic/test">>,
|
{matched, allow},
|
||||||
emqx_authz_rule:compile(?SOURCE4))),
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo2,
|
||||||
|
publish,
|
||||||
|
<<"topic/test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE4)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
nomatch,
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo3,
|
||||||
|
publish,
|
||||||
|
<<"topic/test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE4)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
nomatch,
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo4,
|
||||||
|
publish,
|
||||||
|
<<"topic/test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE4)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual({matched, allow},
|
?assertEqual(
|
||||||
emqx_authz_rule:match(ClientInfo1, publish, <<"test">>,
|
{matched, allow},
|
||||||
emqx_authz_rule:compile(?SOURCE5))),
|
emqx_authz_rule:match(
|
||||||
?assertEqual({matched, allow},
|
ClientInfo1,
|
||||||
emqx_authz_rule:match(ClientInfo2, publish, <<"test">>,
|
publish,
|
||||||
emqx_authz_rule:compile(?SOURCE5))),
|
<<"test">>,
|
||||||
?assertEqual({matched, allow},
|
emqx_authz_rule:compile(?SOURCE5)
|
||||||
emqx_authz_rule:match(ClientInfo3, publish, <<"test">>,
|
)
|
||||||
emqx_authz_rule:compile(?SOURCE5))),
|
),
|
||||||
?assertEqual({matched, allow},
|
?assertEqual(
|
||||||
emqx_authz_rule:match(ClientInfo3, publish, <<"fake">>,
|
{matched, allow},
|
||||||
emqx_authz_rule:compile(?SOURCE5))),
|
emqx_authz_rule:match(
|
||||||
?assertEqual({matched, allow},
|
ClientInfo2,
|
||||||
emqx_authz_rule:match(ClientInfo4, publish, <<"test">>,
|
publish,
|
||||||
emqx_authz_rule:compile(?SOURCE5))),
|
<<"test">>,
|
||||||
?assertEqual({matched, allow},
|
emqx_authz_rule:compile(?SOURCE5)
|
||||||
emqx_authz_rule:match(ClientInfo4, publish, <<"fake">>,
|
)
|
||||||
emqx_authz_rule:compile(?SOURCE5))),
|
),
|
||||||
|
?assertEqual(
|
||||||
|
{matched, allow},
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo3,
|
||||||
|
publish,
|
||||||
|
<<"test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE5)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
{matched, allow},
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo3,
|
||||||
|
publish,
|
||||||
|
<<"fake">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE5)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
{matched, allow},
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo4,
|
||||||
|
publish,
|
||||||
|
<<"test">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE5)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
{matched, allow},
|
||||||
|
emqx_authz_rule:match(
|
||||||
|
ClientInfo4,
|
||||||
|
publish,
|
||||||
|
<<"fake">>,
|
||||||
|
emqx_authz_rule:compile(?SOURCE5)
|
||||||
|
)
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -32,33 +32,40 @@ restore_authorizers() ->
|
||||||
|
|
||||||
reset_authorizers(Nomatch, ChacheEnabled) ->
|
reset_authorizers(Nomatch, ChacheEnabled) ->
|
||||||
{ok, _} = emqx:update_config(
|
{ok, _} = emqx:update_config(
|
||||||
[authorization],
|
[authorization],
|
||||||
#{<<"no_match">> => atom_to_binary(Nomatch),
|
#{
|
||||||
<<"cache">> => #{<<"enable">> => atom_to_binary(ChacheEnabled)},
|
<<"no_match">> => atom_to_binary(Nomatch),
|
||||||
<<"sources">> => []}),
|
<<"cache">> => #{<<"enable">> => atom_to_binary(ChacheEnabled)},
|
||||||
|
<<"sources">> => []
|
||||||
|
}
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
setup_config(BaseConfig, SpecialParams) ->
|
setup_config(BaseConfig, SpecialParams) ->
|
||||||
Config = maps:merge(BaseConfig, SpecialParams),
|
Config = maps:merge(BaseConfig, SpecialParams),
|
||||||
case emqx_authz:update(?CMD_REPLACE, [Config]) of
|
case emqx_authz:update(?CMD_REPLACE, [Config]) of
|
||||||
{ok, _} -> ok;
|
{ok, _} -> ok;
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
test_samples(ClientInfo, Samples) ->
|
test_samples(ClientInfo, Samples) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun({Expected, Action, Topic}) ->
|
fun({Expected, Action, Topic}) ->
|
||||||
ct:pal(
|
ct:pal(
|
||||||
"client_info: ~p, action: ~p, topic: ~p, expected: ~p",
|
"client_info: ~p, action: ~p, topic: ~p, expected: ~p",
|
||||||
[ClientInfo, Action, Topic, Expected]),
|
[ClientInfo, Action, Topic, Expected]
|
||||||
?assertEqual(
|
),
|
||||||
Expected,
|
?assertEqual(
|
||||||
emqx_access_control:authorize(
|
Expected,
|
||||||
ClientInfo,
|
emqx_access_control:authorize(
|
||||||
Action,
|
ClientInfo,
|
||||||
Topic))
|
Action,
|
||||||
end,
|
Topic
|
||||||
Samples).
|
)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
Samples
|
||||||
|
).
|
||||||
|
|
||||||
test_no_topic_rules(ClientInfo, SetupSamples) ->
|
test_no_topic_rules(ClientInfo, SetupSamples) ->
|
||||||
%% No rules
|
%% No rules
|
||||||
|
|
@ -67,43 +74,52 @@ test_no_topic_rules(ClientInfo, SetupSamples) ->
|
||||||
ok = SetupSamples(ClientInfo, []),
|
ok = SetupSamples(ClientInfo, []),
|
||||||
|
|
||||||
ok = test_samples(
|
ok = test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[{deny, subscribe, <<"#">>},
|
[
|
||||||
|
{deny, subscribe, <<"#">>},
|
||||||
{deny, subscribe, <<"subs">>},
|
{deny, subscribe, <<"subs">>},
|
||||||
{deny, publish, <<"pub">>}]).
|
{deny, publish, <<"pub">>}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
test_allow_topic_rules(ClientInfo, SetupSamples) ->
|
test_allow_topic_rules(ClientInfo, SetupSamples) ->
|
||||||
Samples = [#{
|
Samples = [
|
||||||
topics => [<<"eq testpub1/${username}">>,
|
#{
|
||||||
<<"testpub2/${clientid}">>,
|
topics => [
|
||||||
<<"testpub3/#">>],
|
<<"eq testpub1/${username}">>,
|
||||||
permission => <<"allow">>,
|
<<"testpub2/${clientid}">>,
|
||||||
action => <<"publish">>
|
<<"testpub3/#">>
|
||||||
},
|
],
|
||||||
#{
|
permission => <<"allow">>,
|
||||||
topics => [<<"eq testsub1/${username}">>,
|
action => <<"publish">>
|
||||||
<<"testsub2/${clientid}">>,
|
},
|
||||||
<<"testsub3/#">>],
|
#{
|
||||||
permission => <<"allow">>,
|
topics => [
|
||||||
action => <<"subscribe">>
|
<<"eq testsub1/${username}">>,
|
||||||
},
|
<<"testsub2/${clientid}">>,
|
||||||
|
<<"testsub3/#">>
|
||||||
|
],
|
||||||
|
permission => <<"allow">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
},
|
||||||
|
|
||||||
#{
|
#{
|
||||||
topics => [<<"eq testall1/${username}">>,
|
topics => [
|
||||||
<<"testall2/${clientid}">>,
|
<<"eq testall1/${username}">>,
|
||||||
<<"testall3/#">>],
|
<<"testall2/${clientid}">>,
|
||||||
permission => <<"allow">>,
|
<<"testall3/#">>
|
||||||
action => <<"all">>
|
],
|
||||||
}
|
permission => <<"allow">>,
|
||||||
],
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
ok = reset_authorizers(deny, false),
|
ok = reset_authorizers(deny, false),
|
||||||
ok = SetupSamples(ClientInfo, Samples),
|
ok = SetupSamples(ClientInfo, Samples),
|
||||||
|
|
||||||
ok = test_samples(
|
ok = test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[
|
[
|
||||||
|
|
||||||
%% Publish rules
|
%% Publish rules
|
||||||
|
|
||||||
{deny, publish, <<"testpub1/username">>},
|
{deny, publish, <<"testpub1/username">>},
|
||||||
|
|
@ -114,7 +130,6 @@ test_allow_topic_rules(ClientInfo, SetupSamples) ->
|
||||||
{deny, publish, <<"testpub2/username">>},
|
{deny, publish, <<"testpub2/username">>},
|
||||||
{deny, publish, <<"testpub1/clientid">>},
|
{deny, publish, <<"testpub1/clientid">>},
|
||||||
|
|
||||||
|
|
||||||
{deny, subscribe, <<"testpub1/username">>},
|
{deny, subscribe, <<"testpub1/username">>},
|
||||||
{deny, subscribe, <<"testpub2/clientid">>},
|
{deny, subscribe, <<"testpub2/clientid">>},
|
||||||
{deny, subscribe, <<"testpub3/foobar">>},
|
{deny, subscribe, <<"testpub3/foobar">>},
|
||||||
|
|
@ -154,41 +169,47 @@ test_allow_topic_rules(ClientInfo, SetupSamples) ->
|
||||||
{deny, publish, <<"testall2/username">>},
|
{deny, publish, <<"testall2/username">>},
|
||||||
{deny, publish, <<"testall1/clientid">>},
|
{deny, publish, <<"testall1/clientid">>},
|
||||||
{deny, publish, <<"testall4/foobar">>}
|
{deny, publish, <<"testall4/foobar">>}
|
||||||
]).
|
]
|
||||||
|
).
|
||||||
|
|
||||||
test_deny_topic_rules(ClientInfo, SetupSamples) ->
|
test_deny_topic_rules(ClientInfo, SetupSamples) ->
|
||||||
Samples = [
|
Samples = [
|
||||||
#{
|
#{
|
||||||
topics => [<<"eq testpub1/${username}">>,
|
topics => [
|
||||||
<<"testpub2/${clientid}">>,
|
<<"eq testpub1/${username}">>,
|
||||||
<<"testpub3/#">>],
|
<<"testpub2/${clientid}">>,
|
||||||
permission => <<"deny">>,
|
<<"testpub3/#">>
|
||||||
action => <<"publish">>
|
],
|
||||||
},
|
permission => <<"deny">>,
|
||||||
#{
|
action => <<"publish">>
|
||||||
topics => [<<"eq testsub1/${username}">>,
|
},
|
||||||
<<"testsub2/${clientid}">>,
|
#{
|
||||||
<<"testsub3/#">>],
|
topics => [
|
||||||
permission => <<"deny">>,
|
<<"eq testsub1/${username}">>,
|
||||||
action => <<"subscribe">>
|
<<"testsub2/${clientid}">>,
|
||||||
},
|
<<"testsub3/#">>
|
||||||
|
],
|
||||||
|
permission => <<"deny">>,
|
||||||
|
action => <<"subscribe">>
|
||||||
|
},
|
||||||
|
|
||||||
#{
|
#{
|
||||||
topics => [<<"eq testall1/${username}">>,
|
topics => [
|
||||||
<<"testall2/${clientid}">>,
|
<<"eq testall1/${username}">>,
|
||||||
<<"testall3/#">>],
|
<<"testall2/${clientid}">>,
|
||||||
permission => <<"deny">>,
|
<<"testall3/#">>
|
||||||
action => <<"all">>
|
],
|
||||||
}
|
permission => <<"deny">>,
|
||||||
],
|
action => <<"all">>
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
ok = reset_authorizers(allow, false),
|
ok = reset_authorizers(allow, false),
|
||||||
ok = SetupSamples(ClientInfo, Samples),
|
ok = SetupSamples(ClientInfo, Samples),
|
||||||
|
|
||||||
ok = test_samples(
|
ok = test_samples(
|
||||||
ClientInfo,
|
ClientInfo,
|
||||||
[
|
[
|
||||||
|
|
||||||
%% Publish rules
|
%% Publish rules
|
||||||
|
|
||||||
{allow, publish, <<"testpub1/username">>},
|
{allow, publish, <<"testpub1/username">>},
|
||||||
|
|
@ -199,7 +220,6 @@ test_deny_topic_rules(ClientInfo, SetupSamples) ->
|
||||||
{allow, publish, <<"testpub2/username">>},
|
{allow, publish, <<"testpub2/username">>},
|
||||||
{allow, publish, <<"testpub1/clientid">>},
|
{allow, publish, <<"testpub1/clientid">>},
|
||||||
|
|
||||||
|
|
||||||
{allow, subscribe, <<"testpub1/username">>},
|
{allow, subscribe, <<"testpub1/username">>},
|
||||||
{allow, subscribe, <<"testpub2/clientid">>},
|
{allow, subscribe, <<"testpub2/clientid">>},
|
||||||
{allow, subscribe, <<"testpub3/foobar">>},
|
{allow, subscribe, <<"testpub3/foobar">>},
|
||||||
|
|
@ -239,4 +259,5 @@ test_deny_topic_rules(ClientInfo, SetupSamples) ->
|
||||||
{allow, publish, <<"testall2/username">>},
|
{allow, publish, <<"testall2/username">>},
|
||||||
{allow, publish, <<"testall1/clientid">>},
|
{allow, publish, <<"testall1/clientid">>},
|
||||||
{allow, publish, <<"testall4/foobar">>}
|
{allow, publish, <<"testall4/foobar">>}
|
||||||
]).
|
]
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
91bcf02970bd1fe0dfe06744a24aa8a149ac0052
|
91bcf02970bd1fe0dfe06744a24aa8a149ac0052
|
||||||
# reformat apps/emqx_modules
|
# reformat apps/emqx_modules
|
||||||
4d3157743e0d3ac7e6c09f108f6e79c820fd9c00
|
4d3157743e0d3ac7e6c09f108f6e79c820fd9c00
|
||||||
|
# reformat apps/emqx_authn
|
||||||
|
aae2d01582b980302f30913522eedf1d9d3d217c
|
||||||
|
# reformat apps/emqx_authz
|
||||||
|
82559b9b089c6cfadf54b7ce5143156dd403876f
|
||||||
# reformat lib-ee/emqx_license
|
# reformat lib-ee/emqx_license
|
||||||
4f396cceb84d79d5ef540e91c1a8420e8de74a56
|
4f396cceb84d79d5ef540e91c1a8420e8de74a56
|
||||||
# reformat lib-ee/emqx_enterprise_conf
|
# reformat lib-ee/emqx_enterprise_conf
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ cd -P -- "$(dirname -- "$0")/.."
|
||||||
|
|
||||||
APPS=()
|
APPS=()
|
||||||
APPS+=( 'apps/emqx' 'apps/emqx_modules' )
|
APPS+=( 'apps/emqx' 'apps/emqx_modules' )
|
||||||
|
APPS+=( 'apps/emqx_authn' 'apps/emqx_authz' )
|
||||||
APPS+=( 'lib-ee/emqx_enterprise_conf' 'lib-ee/emqx_license' )
|
APPS+=( 'lib-ee/emqx_enterprise_conf' 'lib-ee/emqx_license' )
|
||||||
|
|
||||||
for app in "${APPS[@]}"; do
|
for app in "${APPS[@]}"; do
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue