Merge pull request #7484 from JimMoen/erlfmt-authn-authz

Erlfmt authn and authz
This commit is contained in:
Zaiming (Stone) Shi 2022-04-01 00:24:53 +02:00 committed by GitHub
commit 4d74296fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 7556 additions and 5260 deletions

View File

@ -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_unused_vars,
warn_shadow_vars, warn_shadow_vars,
warnings_as_errors, warnings_as_errors,
warn_unused_import, warn_unused_import,
warn_obsolete_guard, warn_obsolete_guard,
debug_info, debug_info,
{parse_transform}]}. {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]}.

View File

@ -1,6 +1,6 @@
%% -*- 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]},

View File

@ -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,7 +54,8 @@ 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 =
case maps:get(<<"backend">>, Config, false) of
false -> atom(Mec); false -> atom(Mec);
Backend -> {atom(Mec), atom(Backend)} Backend -> {atom(Mec), atom(Backend)}
end, end,
@ -59,8 +63,11 @@ do_check_config(#{<<"mechanism">> := Mec} = Config, Opts) ->
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) ->

File diff suppressed because it is too large Load Diff

View File

@ -21,8 +21,9 @@
-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").
@ -55,9 +56,11 @@ initialize() ->
AuthConfig = emqx_authn:check_configs(RawAuthConfigs), AuthConfig = emqx_authn:check_configs(RawAuthConfigs),
?AUTHN:initialize_authentication( ?AUTHN:initialize_authentication(
ChainName, ChainName,
AuthConfig) AuthConfig
)
end, end,
chain_configs()). chain_configs()
).
deinitialize() -> deinitialize() ->
ok = ?AUTHN:deregister_providers(provider_types()), ok = ?AUTHN:deregister_providers(provider_types()),
@ -74,12 +77,13 @@ listener_chain_configs() ->
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()).

View File

@ -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() :: #{
name := pbkdf2,
mac_fun := emqx_passwd:pbkdf2_mac_fun(), mac_fun := emqx_passwd:pbkdf2_mac_fun(),
iterations := pos_integer()}). 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,17 +44,23 @@
-behaviour(hocon_schema). -behaviour(hocon_schema).
-export([roots/0, -export([
roots/0,
fields/1, fields/1,
namespace/0]). namespace/0
]).
-export([type_ro/1, -export([
type_rw/1]). type_ro/1,
type_rw/1
]).
-export([init/1, -export([
init/1,
gen_salt/1, gen_salt/1,
hash/2, hash/2,
check_password/4]). check_password/4
]).
namespace() -> "authn-hash". namespace() -> "authn-hash".
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms]. roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
@ -58,19 +68,20 @@ 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]}}, [
{name, {enum, [pbkdf2]}},
{mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}}, {mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}},
{iterations, integer()}, {iterations, integer()},
{dk_length, fun dk_length/1}]; {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(
#{
name := pbkdf2,
mac_fun := MacFun, mac_fun := MacFun,
iterations := Iterations} = Algorithm, Password) -> 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()) -> boolean()). emqx_passwd:password()
) -> 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(
#{
name := pbkdf2,
mac_fun := MacFun, mac_fun := MacFun,
iterations := Iterations} = Algorithm, iterations := Iterations
Salt, PasswordHash, Password) -> } = 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, bcrypt_rw),
hoconsc:ref(?MODULE, pbkdf2), hoconsc:ref(?MODULE, pbkdf2),
hoconsc:ref(?MODULE, other_algorithms)]. hoconsc:ref(?MODULE, other_algorithms)
].
ro_refs() -> ro_refs() ->
[hoconsc:ref(?MODULE, bcrypt), [
hoconsc:ref(?MODULE, bcrypt),
hoconsc:ref(?MODULE, pbkdf2), hoconsc:ref(?MODULE, pbkdf2),
hoconsc:ref(?MODULE, other_algorithms)]. hoconsc:ref(?MODULE, other_algorithms)
].

View File

@ -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() ->

View File

@ -18,8 +18,9 @@
-behaviour(supervisor). -behaviour(supervisor).
-export([ start_link/0 -export([
, init/1 start_link/0,
init/1
]). ]).
start_link() -> start_link() ->

View File

@ -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_USERNAME,
?PH_CLIENTID, ?PH_CLIENTID,
?PH_PASSWORD, ?PH_PASSWORD,
?PH_PEERHOST, ?PH_PEERHOST,
?PH_CERT_SUBJECT, ?PH_CERT_SUBJECT,
?PH_CERT_CN_NAME]). ?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) ->
@ -64,26 +67,32 @@ 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};
@ -115,7 +124,8 @@ 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),

View File

@ -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).
@ -43,11 +43,13 @@ 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.
@ -56,7 +58,8 @@ 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,48 +120,57 @@ 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) ->
@ -168,57 +180,72 @@ test_authenticator(PathPrefix) ->
end end
end, end,
LookFun = fun(List) -> LookupVal(List, RList) end, LookFun = fun(List) -> LookupVal(List, RList) end,
MetricsList = [{<<"failed">>, 0}, MetricsList = [
{<<"failed">>, 0},
{<<"matched">>, 0}, {<<"matched">>, 0},
{<<"rate">>, 0.0}, {<<"rate">>, 0.0},
{<<"rate_last5m">>, 0.0}, {<<"rate_last5m">>, 0.0},
{<<"rate_max">>, 0.0}, {<<"rate_max">>, 0.0},
{<<"success">>, 0}], {<<"success">>, 0}
],
EqualFun = fun({M, V}) -> EqualFun = fun({M, V}) ->
?assertEqual(V, LookFun([<<"metrics">>, ?assertEqual(
M] V,
LookFun([
<<"metrics">>,
M
])
) )
) end, 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]).
@ -228,22 +255,25 @@ test_authenticator_users(PathPrefix) ->
{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) ->
@ -251,31 +281,41 @@ test_authenticator_users(PathPrefix) ->
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, #{
<<"data">> := Page1Users,
<<"meta">> := <<"meta">> :=
#{<<"page">> := 1, #{
<<"page">> := 1,
<<"limit">> := 2, <<"limit">> := 2,
<<"count">> := 3}} = <<"count">> := 3
}
} =
jiffy:decode(Page1Data, [return_maps]), 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, #{
<<"data">> := Page2Users,
<<"meta">> := <<"meta">> :=
#{<<"page">> := 2, #{
<<"page">> := 2,
<<"limit">> := 2, <<"limit">> := 2,
<<"count">> := 3}} = jiffy:decode(Page2Data, [return_maps]), <<"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"]),
@ -283,7 +323,8 @@ test_authenticator_user(PathPrefix) ->
{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),
@ -300,17 +341,20 @@ test_authenticator_user(PathPrefix) ->
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").
@ -327,9 +371,11 @@ test_authenticator_move(PathPrefix) ->
{ok, 200, _} = request( {ok, 200, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
Conf) Conf
)
end, end,
AuthenticatorConfs), AuthenticatorConfs
),
?assertAuthenticatorsMatch( ?assertAuthenticatorsMatch(
[ [
@ -337,34 +383,40 @@ test_authenticator_move(PathPrefix) ->
#{<<"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
@ -372,7 +424,8 @@ test_authenticator_move(PathPrefix) ->
{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(
[ [
@ -380,13 +433,15 @@ test_authenticator_move(PathPrefix) ->
#{<<"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(
[ [
@ -394,13 +449,15 @@ test_authenticator_move(PathPrefix) ->
#{<<"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(
[ [
@ -408,13 +465,15 @@ test_authenticator_move(PathPrefix) ->
#{<<"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(
[ [
@ -422,18 +481,20 @@ test_authenticator_move(PathPrefix) ->
#{<<"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, #{}),

View File

@ -28,13 +28,13 @@
-define(HTTP_PORT, 33333). -define(HTTP_PORT, 33333).
-define(HTTP_PATH, "/auth"). -define(HTTP_PATH, "/auth").
-define(CREDENTIALS, #{username => <<"plain">>, -define(CREDENTIALS, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt protocol => mqtt
}). }).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -47,7 +47,8 @@ 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.
@ -56,7 +57,8 @@ 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.
@ -72,7 +74,8 @@ t_create(_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).
@ -92,14 +95,16 @@ t_create_invalid(_Config) ->
try try
emqx:update_config( emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}) {create_authenticator, ?GLOBAL, Config}
)
catch catch
throw:Error -> throw:Error ->
{error, Error} {error, Error}
end, end,
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL) {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
end, end,
InvalidConfigs). InvalidConfigs
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = lists:foreach( ok = lists:foreach(
@ -107,16 +112,20 @@ t_authenticate(_Config) ->
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(#{
handler := Handler,
config_params := SpecificConfgParams, config_params := SpecificConfgParams,
result := Result}) -> 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),
@ -124,40 +133,47 @@ test_user_auth(#{handler := Handler,
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(),
@ -166,20 +182,23 @@ t_update(_Config) ->
{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).
@ -187,7 +206,8 @@ 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},
@ -210,11 +230,10 @@ t_is_superuser(_Config) ->
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])}
@ -226,13 +245,16 @@ test_is_superuser({Kind, Value, ExpectedValue}) ->
200, 200,
#{<<"content-type">> => ContentType}, #{<<"content-type">> => ContentType},
Res, Res,
Req0), Req0
),
{ok, Req, State} {ok, Req, State}
end), end
),
?assertMatch( ?assertMatch(
{ok, #{is_superuser := ExpectedValue}}, {ok, #{is_superuser := ExpectedValue}},
emqx_access_control:authenticate(?CREDENTIALS)). emqx_access_control:authenticate(?CREDENTIALS)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -253,8 +275,10 @@ raw_http_auth_config() ->
samples() -> samples() ->
[ [
%% simple get request %% simple get request
#{handler => fun(Req0, State) -> #{
#{username := <<"plain">>, handler => fun(Req0, State) ->
#{
username := <<"plain">>,
password := <<"plain">> password := <<"plain">>
} = cowboy_req:match_qs([username, password], Req0), } = cowboy_req:match_qs([username, password], Req0),
@ -266,12 +290,14 @@ samples() ->
}, },
%% get request with json body response %% get request with json body response
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
Req = cowboy_req:reply( Req = cowboy_req:reply(
200, 200,
#{<<"content-type">> => <<"application/json">>}, #{<<"content-type">> => <<"application/json">>},
jiffy:encode(#{is_superuser => true}), jiffy:encode(#{is_superuser => true}),
Req0), Req0
),
{ok, Req, State} {ok, Req, State}
end, end,
config_params => #{}, config_params => #{},
@ -279,13 +305,17 @@ samples() ->
}, },
%% get request with url-form-encoded body response %% get request with url-form-encoded body response
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
Req = cowboy_req:reply( Req = cowboy_req:reply(
200, 200,
#{<<"content-type">> => #{
<<"application/x-www-form-urlencoded">>}, <<"content-type">> =>
<<"application/x-www-form-urlencoded">>
},
<<"is_superuser=true">>, <<"is_superuser=true">>,
Req0), Req0
),
{ok, Req, State} {ok, Req, State}
end, end,
config_params => #{}, config_params => #{},
@ -293,13 +323,17 @@ samples() ->
}, },
%% get request with response of unknown encoding %% get request with response of unknown encoding
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
Req = cowboy_req:reply( Req = cowboy_req:reply(
200, 200,
#{<<"content-type">> => #{
<<"test/plain">>}, <<"content-type">> =>
<<"test/plain">>
},
<<"is_superuser=true">>, <<"is_superuser=true">>,
Req0), Req0
),
{ok, Req, State} {ok, Req, State}
end, end,
config_params => #{}, config_params => #{},
@ -307,9 +341,11 @@ samples() ->
}, },
%% simple post request, application/json %% simple post request, application/json
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
{ok, RawBody, Req1} = cowboy_req:read_body(Req0), {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
#{<<"username">> := <<"plain">>, #{
<<"username">> := <<"plain">>,
<<"password">> := <<"plain">> <<"password">> := <<"plain">>
} = jiffy:decode(RawBody, [return_maps]), } = jiffy:decode(RawBody, [return_maps]),
Req = cowboy_req:reply(200, Req1), Req = cowboy_req:reply(200, Req1),
@ -323,9 +359,11 @@ samples() ->
}, },
%% simple post request, application/x-www-form-urlencoded %% simple post request, application/x-www-form-urlencoded
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
{ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0), {ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0),
#{<<"username">> := <<"plain">>, #{
<<"username">> := <<"plain">>,
<<"password">> := <<"plain">> <<"password">> := <<"plain">>
} = maps:from_list(PostVars), } = maps:from_list(PostVars),
Req = cowboy_req:reply(200, Req1), Req = cowboy_req:reply(200, Req1),
@ -333,14 +371,15 @@ samples() ->
end, end,
config_params => #{ config_params => #{
method => post, method => post,
headers => #{<<"content-type">> => headers => #{
<<"application/x-www-form-urlencoded">>} <<"content-type">> =>
<<"application/x-www-form-urlencoded">>
}
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
} }#{
%% 204 code %% 204 code
#{handler => fun(Req0, State) -> handler => fun(Req0, State) ->
Req = cowboy_req:reply(204, Req0), Req = cowboy_req:reply(204, Req0),
{ok, Req, State} {ok, Req, State}
end, end,
@ -349,7 +388,8 @@ samples() ->
}, },
%% custom headers %% custom headers
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
<<"Test Value">> = cowboy_req:header(<<"x-test-header">>, Req0), <<"Test Value">> = cowboy_req:header(<<"x-test-header">>, Req0),
Req = cowboy_req:reply(200, Req0), Req = cowboy_req:reply(200, Req0),
{ok, Req, State} {ok, Req, State}
@ -359,7 +399,8 @@ samples() ->
}, },
%% 400 code %% 400 code
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
Req = cowboy_req:reply(400, Req0), Req = cowboy_req:reply(400, Req0),
{ok, Req, State} {ok, Req, State}
end, end,
@ -368,7 +409,8 @@ samples() ->
}, },
%% 500 code %% 500 code
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
Req = cowboy_req:reply(500, Req0), Req = cowboy_req:reply(500, Req0),
{ok, Req, State} {ok, Req, State}
end, end,
@ -377,7 +419,8 @@ samples() ->
}, },
%% Handling error %% Handling error
#{handler => fun(Req0, State) -> #{
handler => fun(Req0, State) ->
error(woops), error(woops),
{ok, Req0, State} {ok, Req0, State}
end, end,

View File

@ -26,7 +26,8 @@
-export([init/1]). -export([init/1]).
% API % API
-export([start_link/2, -export([
start_link/2,
start_link/3, start_link/3,
stop/0, stop/0,
set_handler/1 set_handler/1
@ -57,7 +58,8 @@ init([Port, Path, SSLOpts]) ->
Dispatch = cowboy_router:compile( Dispatch = cowboy_router:compile(
[ [
{'_', [{Path, ?MODULE, []}]} {'_', [{Path, ?MODULE, []}]}
]), ]
),
ProtoOpts = #{env => #{dispatch => Dispatch}}, ProtoOpts = #{env => #{dispatch => Dispatch}},
@ -83,16 +85,21 @@ 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 = #{
socket_opts => [
{port, Port},
{next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]}, {next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]} {alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
| SSLOpts], | SSLOpts
connection_type => supervisor}, ],
connection_type => supervisor
},
{ranch_ssl, TransOpts, cowboy_tls}. {ranch_ssl, TransOpts, cowboy_tls}.
default_handler(Req0, State) -> default_handler(Req0, State) ->
@ -100,6 +107,6 @@ default_handler(Req0, State) ->
400, 400,
#{<<"content-type">> => <<"text/plain">>}, #{<<"content-type">> => <<"text/plain">>},
<<"">>, <<"">>,
Req0), Req0
),
{ok, Req, State}. {ok, Req, State}.

View File

@ -28,13 +28,13 @@
-define(HTTPS_PORT, 33333). -define(HTTPS_PORT, 33333).
-define(HTTPS_PATH, "/auth"). -define(HTTPS_PATH, "/auth").
-define(CREDENTIALS, #{username => <<"plain">>, -define(CREDENTIALS, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt protocol => mqtt
}). }).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -47,7 +47,8 @@ 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.
@ -56,7 +57,8 @@ 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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}), <<"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">>, #{
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}), <<"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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>]}), <<"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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-ECDSA-AES256-SHA384">>]}), <<"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
@ -122,7 +140,8 @@ 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">>,
@ -148,11 +167,13 @@ 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")}, [
{keyfile, cert_path("server.key")},
{certfile, cert_path("server.crt")}, {certfile, cert_path("server.crt")},
{cacertfile, cert_path("ca.crt")}, {cacertfile, cert_path("ca.crt")},
{verify, verify_none}, {verify, verify_none},

View File

@ -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 = #{
mechanism => jwt,
use_jwks => false, use_jwks => false,
algorithm => 'hmac-based', algorithm => 'hmac-based',
secret => Secret, secret => Secret,
secret_base64_encoded => false, secret_base64_encoded => false,
verify_claims => []}, 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 = #{
mechanism => jwt,
use_jwks => false, use_jwks => false,
algorithm => 'public-key', algorithm => 'public-key',
certificate => PublicKey, certificate => PublicKey,
verify_claims => []}, 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,10 +196,13 @@ 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, BadConfig0 = #{
mechanism => jwt,
algorithm => 'public-key', algorithm => 'public-key',
ssl => #{enable => false}, ssl => #{enable => false},
verify_claims => [], verify_claims => [],
@ -178,31 +217,39 @@ t_jwks_renewal(_Config) ->
{{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#{
endpoint =>
"https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH, "https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH,
ssl => BadClientSSLOpts}, 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},
@ -211,12 +258,15 @@ t_jwks_renewal(_Config) ->
{{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().
@ -232,12 +282,12 @@ jwks_handler(Req0, State) ->
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,16 +300,18 @@ 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),
@ -268,13 +320,16 @@ generate_jws('public-key', Payload, PrivateKey) ->
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, #{
enable => true,
verify => verify_peer, verify => verify_peer,
server_name_indication => "authn-server" server_name_indication => "authn-server"
}). }
).
server_ssl_opts() -> server_ssl_opts() ->
[{keyfile, cert_file("server.key")}, [
{keyfile, cert_file("server.key")},
{certfile, cert_file("server.crt")}, {certfile, cert_file("server.crt")},
{cacertfile, cert_file("ca.crt")}, {cacertfile, cert_file("ca.crt")},
{verify, verify_none} {verify, verify_none}

View File

@ -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(),
@ -122,13 +123,16 @@ t_authenticate(_) ->
{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
}} = emqx_authn_mnesia:update_user(
<<"u">>, <<"u">>,
#{password => <<"p1">>, is_superuser => true}, #{password => <<"p1">>, is_superuser => true},
State), 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 => <<"u1">>, password => <<"p">>},
#{user_id => <<"u2">>, password => <<"p">>}, #{user_id => <<"u2">>, password => <<"p">>},
#{user_id => <<"u3">>, 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 := _},
#{is_superuser := false, user_id := _}
],
meta := #{page := 1, limit := 2, count := 3}
} = emqx_authn_mnesia:list_users(
#{<<"page">> => 1, <<"limit">> => 2}, #{<<"page">> => 1, <<"limit">> => 2},
State), 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 := _}],
meta := #{page := 2, limit := 2, count := 3}
} = emqx_authn_mnesia:list_users(
#{<<"page">> => 2, <<"limit">> => 2}, #{<<"page">> => 2, <<"limit">> => 2},
State), 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(),
@ -207,35 +230,43 @@ t_import_users(_) ->
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
}
}. }.

View File

@ -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').
@ -38,7 +37,8 @@ init_per_testcase(_TestCase, Config) ->
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.
@ -59,7 +59,8 @@ 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,7 +73,8 @@ t_create(_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).
@ -90,11 +92,13 @@ t_create_invalid(_Config) ->
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(),
@ -103,17 +107,21 @@ t_authenticate(_Config) ->
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(#{
credentials := Credentials0,
config_params := SpecificConfigParams, config_params := SpecificConfigParams,
result := Result}) -> 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',
@ -123,7 +131,8 @@ test_user_auth(#{credentials := Credentials0,
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(),
@ -131,29 +140,36 @@ t_destroy(_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">>, #{
username => <<"plain">>,
password => <<"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">>, #{
username => <<"plain">>,
password => <<"plain">> password => <<"plain">>
}, },
State)), State
)
),
ok = drop_seeds(). ok = drop_seeds().
@ -165,33 +181,40 @@ t_update(_Config) ->
{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">>, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt 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">>, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt 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},
@ -230,7 +253,8 @@ test_is_superuser({Value, ExpectedValue}) ->
?assertEqual( ?assertEqual(
{ok, #{is_superuser => ExpectedValue}}, {ok, #{is_superuser => ExpectedValue}},
emqx_access_control:authenticate(Credentials)). emqx_access_control:authenticate(Credentials)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -239,8 +263,10 @@ 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">>,
salt_position => <<"suffix">>
},
enable => <<"true">>, enable => <<"true">>,
backend => <<"mongodb">>, backend => <<"mongodb">>,
@ -257,7 +283,9 @@ raw_mongo_auth_config() ->
}. }.
user_seeds() -> user_seeds() ->
[#{data => #{ [
#{
data => #{
username => <<"plain">>, username => <<"plain">>,
password_hash => <<"plainsalt">>, password_hash => <<"plainsalt">>,
salt => <<"salt">>, salt => <<"salt">>,
@ -267,12 +295,12 @@ user_seeds() ->
username => <<"plain">>, username => <<"plain">>,
password => <<"plain">> password => <<"plain">>
}, },
config_params => #{ config_params => #{},
},
result => {ok, #{is_superuser => true}} result => {ok, #{is_superuser => true}}
}, },
#{data => #{ #{
data => #{
username => <<"md5">>, username => <<"md5">>,
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>, password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"salt">>, salt => <<"salt">>,
@ -283,15 +311,19 @@ user_seeds() ->
password => <<"md5">> password => <<"md5">>
}, },
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"md5">>, password_hash_algorithm => #{
salt_position => <<"suffix">> } name => <<"md5">>,
salt_position => <<"suffix">>
}
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
username => <<"sha256">>, username => <<"sha256">>,
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>, password_hash =>
<<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
salt => <<"salt">>, salt => <<"salt">>,
is_superuser => 1 is_superuser => 1
}, },
@ -301,13 +333,16 @@ user_seeds() ->
}, },
config_params => #{ config_params => #{
selector => #{<<"username">> => <<"${clientid}">>}, selector => #{<<"username">> => <<"${clientid}">>},
password_hash_algorithm => #{name => <<"sha256">>, password_hash_algorithm => #{
salt_position => <<"prefix">>} name => <<"sha256">>,
salt_position => <<"prefix">>
}
}, },
result => {ok, #{is_superuser => true}} result => {ok, #{is_superuser => true}}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt">>, username => <<"bcrypt">>,
password_hash => password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
@ -324,7 +359,8 @@ user_seeds() ->
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt0">>, username => <<"bcrypt0">>,
password_hash => password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
@ -343,7 +379,8 @@ user_seeds() ->
result => {error, not_authorized} result => {error, not_authorized}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt1">>, username => <<"bcrypt1">>,
password_hash => password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
@ -361,7 +398,8 @@ user_seeds() ->
result => {error, not_authorized} result => {error, not_authorized}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt2">>, username => <<"bcrypt2">>,
password_hash => password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,

View File

@ -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]).
@ -38,7 +37,8 @@ init_per_testcase(_TestCase, Config) ->
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) ->
@ -55,7 +55,8 @@ 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]).
@ -73,32 +74,42 @@ 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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}), <<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
}
),
fun({ok, _}, Trace) -> 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">>,
<<"verify">> => <<"verify_peer">>
}
),
fun(_, Trace) -> fun(_, Trace) ->
?assertNotEqual( ?assertNotEqual(
[ok], [ok],
?projection( ?projection(
status, status,
?of_kind(emqx_connector_mongo_health_check, Trace))) ?of_kind(emqx_connector_mongo_health_check, Trace)
end). )
)
end
).
%% docker-compose-mongo-single-tls.yaml: %% docker-compose-mongo-single-tls.yaml:
%% --tlsDisabledProtocols TLS1_0,TLS1_1 %% --tlsDisabledProtocols TLS1_0,TLS1_1
@ -106,17 +117,22 @@ t_create_invalid_server_name(_Config) ->
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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>]}), <<"versions">> => [<<"tlsv1.1">>]
}
),
fun(_, Trace) -> fun(_, Trace) ->
?assertNotEqual( ?assertNotEqual(
[ok], [ok],
?projection( ?projection(
status, status,
?of_kind(emqx_connector_mongo_health_check, Trace))) ?of_kind(emqx_connector_mongo_health_check, Trace)
end). )
)
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'
@ -124,17 +140,23 @@ t_create_invalid_version(_Config) ->
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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"DHE-RSA-AES256-GCM-SHA384">>]}), <<"ciphers">> => [<<"DHE-RSA-AES256-GCM-SHA384">>]
}
),
fun(_, Trace) -> fun(_, Trace) ->
?assertNotEqual( ?assertNotEqual(
[ok], [ok],
?projection( ?projection(
status, status,
?of_kind(emqx_connector_mongo_health_check, Trace))) ?of_kind(emqx_connector_mongo_health_check, Trace)
end). )
)
end
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -149,11 +171,14 @@ 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">>,
salt_position => <<"suffix">>
},
enable => <<"true">>, enable => <<"true">>,
backend => <<"mongodb">>, backend => <<"mongodb">>,

View File

@ -21,25 +21,33 @@
-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([
init/1,
handle_call/3, handle_call/3,
handle_cast/2, handle_cast/2,
handle_info/2, handle_info/2,
terminate/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
}). }).
@ -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, #{
owner => Owner,
socket => Socket, socket => Socket,
parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS) parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS)
}}. }}.
handle_info({tcp, _Sock, Data}, #{parse_state := PSt, handle_info(
{tcp, _Sock, Data},
#{
parse_state := PSt,
owner := Owner, owner := Owner,
socket := Socket} = St) -> 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}.
@ -105,11 +117,11 @@ process_incoming(PSt, Data, Packets) ->
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}]).

View File

@ -41,7 +41,8 @@ init_per_testcase(_, Config) ->
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) ->
@ -63,7 +64,8 @@ init_per_suite(Config) ->
?RESOURCE_GROUP, ?RESOURCE_GROUP,
emqx_connector_mysql, emqx_connector_mysql,
mysql_config(), mysql_config(),
#{}), #{}
),
Config; Config;
false -> false ->
{skip, no_mysql} {skip, no_mysql}
@ -72,7 +74,8 @@ 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]).
@ -86,7 +89,8 @@ t_create(_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).
@ -106,11 +110,13 @@ t_create_invalid(_Config) ->
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), emqx_authn_test_lib:delete_config(?ResourceID),
{ok, _} = emqx_authentication:list_authenticators(?GLOBAL) {ok, _} = emqx_authentication:list_authenticators(?GLOBAL)
end, end,
InvalidConfigs). InvalidConfigs
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = lists:foreach( ok = lists:foreach(
@ -118,16 +124,20 @@ t_authenticate(_Config) ->
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(#{
credentials := Credentials0,
config_params := SpecificConfigParams, config_params := SpecificConfigParams,
result := Result}) -> 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',
@ -138,66 +148,84 @@ test_user_auth(#{credentials := Credentials0,
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">>, #{
username => <<"plain">>,
password => <<"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">>, #{
username => <<"plain">>,
password => <<"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">>, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt 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">>, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt protocol => mqtt
}). }
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -206,8 +234,10 @@ 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">>,
salt_position => <<"suffix">>
},
enable => <<"true">>, enable => <<"true">>,
backend => <<"mysql">>, backend => <<"mysql">>,
@ -215,13 +245,18 @@ raw_mysql_auth_config() ->
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">>, <<
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
" FROM users where username = ${username} LIMIT 1"
>>,
server => mysql_server() server => mysql_server()
}. }.
user_seeds() -> user_seeds() ->
[#{data => #{ [
#{
data => #{
username => "plain", username => "plain",
password_hash => "plainsalt", password_hash => "plainsalt",
salt => "salt", salt => "salt",
@ -229,12 +264,14 @@ user_seeds() ->
}, },
credentials => #{ credentials => #{
username => <<"plain">>, username => <<"plain">>,
password => <<"plain">>}, password => <<"plain">>
},
config_params => #{}, config_params => #{},
result => {ok, #{is_superuser => true}} result => {ok, #{is_superuser => true}}
}, },
#{data => #{ #{
data => #{
username => "md5", username => "md5",
password_hash => "9b4d0c43d206d48279e69b9ad7132e22", password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
salt => "salt", salt => "salt",
@ -245,13 +282,16 @@ user_seeds() ->
password => <<"md5">> password => <<"md5">>
}, },
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"md5">>, password_hash_algorithm => #{
salt_position => <<"suffix">>} name => <<"md5">>,
salt_position => <<"suffix">>
}
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
username => "sha256", username => "sha256",
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf", password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
salt => "salt", salt => "salt",
@ -262,15 +302,21 @@ user_seeds() ->
password => <<"sha256">> password => <<"sha256">>
}, },
config_params => #{ 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 => <<"sha256">>, "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
salt_position => <<"prefix">>} " FROM users where username = ${clientid} LIMIT 1"
>>,
password_hash_algorithm => #{
name => <<"sha256">>,
salt_position => <<"prefix">>
}
}, },
result => {ok, #{is_superuser => true}} result => {ok, #{is_superuser => true}}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt">>, username => <<"bcrypt">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
@ -281,14 +327,18 @@ user_seeds() ->
password => <<"bcrypt">> password => <<"bcrypt">>
}, },
config_params => #{ config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser query =>
FROM users where username = ${username} LIMIT 1">>, <<
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
" FROM users where username = ${username} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt">>, username => <<"bcrypt">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve" salt => "$2b$12$wtY3h20mUjjmeaClpqZVve"
@ -298,14 +348,18 @@ user_seeds() ->
password => <<"bcrypt">> password => <<"bcrypt">>
}, },
config_params => #{ config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser query =>
FROM users where username = ${username} LIMIT 1">>, <<
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
" FROM users where username = ${username} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt0">>, username => <<"bcrypt0">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
@ -317,14 +371,18 @@ user_seeds() ->
}, },
config_params => #{ config_params => #{
% clientid variable & username credentials % clientid variable & username credentials
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser query =>
FROM users where username = ${clientid} LIMIT 1">>, <<
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
" FROM users where username = ${clientid} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {error, not_authorized} result => {error, not_authorized}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt1">>, username => <<"bcrypt1">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
@ -336,14 +394,18 @@ user_seeds() ->
}, },
config_params => #{ config_params => #{
% Bad keys in query % Bad keys in query
query => <<"SELECT 1 AS unknown_field query =>
FROM users where username = ${username} LIMIT 1">>, <<
"SELECT 1 AS unknown_field\n"
" FROM users where username = ${username} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {error, not_authorized} result => {error, not_authorized}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt2">>, username => <<"bcrypt2">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
@ -363,15 +425,18 @@ user_seeds() ->
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 =
"INSERT INTO users(username, password_hash, salt, "
" is_superuser_str, is_superuser_int) VALUES(?, ?, ?, ?, ?)", " is_superuser_str, is_superuser_int) VALUES(?, ?, ?, ?, ?)",
lists:foreach( lists:foreach(
@ -379,17 +444,20 @@ init_seeds() ->
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").
@ -398,7 +466,8 @@ 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, #{
auto_reconnect => true,
database => <<"mqtt">>, database => <<"mqtt">>,
username => <<"root">>, username => <<"root">>,
password => <<"public">>, password => <<"public">>,

View File

@ -40,7 +40,8 @@ init_per_testcase(_, Config) ->
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) ->
@ -57,7 +58,8 @@ 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,36 +74,51 @@ t_create(_Config) ->
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_mysql_auth_with_ssl_opts( create_mysql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]})). <<"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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>]})), <<"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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]})). <<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]
}
)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -114,11 +131,14 @@ 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">>,
salt_position => <<"suffix">>
},
enable => <<"true">>, enable => <<"true">>,
backend => <<"mysql">>, backend => <<"mysql">>,
@ -126,8 +146,11 @@ raw_mysql_auth_config(SpecificSSLOpts) ->
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">>, <<
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
" FROM users where username = ${username} LIMIT 1"
>>,
server => mysql_server(), server => mysql_server(),
ssl => maps:merge(SSLOpts, SpecificSSLOpts) ssl => maps:merge(SSLOpts, SpecificSSLOpts)
}. }.

View File

@ -38,8 +38,9 @@ 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) ->
@ -47,29 +48,35 @@ t_gen_salt(_Config) ->
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, password_hash := Hash,
salt := Salt, salt := Salt,
password := Password, password := Password,
password_hash_algorithm := Algorithm password_hash_algorithm := Algorithm
} = Sample) -> } = 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">>).
@ -77,79 +84,104 @@ test_check_password(#{
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 := Password,
password_hash_algorithm := Algorithm password_hash_algorithm := Algorithm
} = Sample) -> } = 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">>, password_hash => <<"plainsalt">>,
salt => <<"salt">>, salt => <<"salt">>,
password => <<"plain">>, password => <<"plain">>,
password_hash_algorithm => #{name => plain, password_hash_algorithm => #{
salt_position => suffix} name => plain,
salt_position => suffix
}
}, },
#{ #{
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>, password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"salt">>, salt => <<"salt">>,
password => <<"md5">>, password => <<"md5">>,
password_hash_algorithm => #{name => md5, password_hash_algorithm => #{
salt_position => suffix} name => md5,
salt_position => suffix
}
}, },
#{ #{
password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>, password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>,
salt => <<"salt">>, salt => <<"salt">>,
password => <<"sha">>, password => <<"sha">>,
password_hash_algorithm => #{name => sha, password_hash_algorithm => #{
salt_position => prefix} name => sha,
salt_position => prefix
}
}, },
#{ #{
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>, password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
salt => <<"salt">>, salt => <<"salt">>,
password => <<"sha256">>, password => <<"sha256">>,
password_hash_algorithm => #{name => sha256, password_hash_algorithm => #{
salt_position => prefix} name => sha256,
salt_position => prefix
}
}, },
#{ #{
password_hash => <<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8" password_hash => <<
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>, "a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8"
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd"
>>,
salt => <<"salt">>, salt => <<"salt">>,
password => <<"sha512">>, password => <<"sha512">>,
password_hash_algorithm => #{name => sha512, password_hash_algorithm => #{
salt_position => prefix} name => sha512,
salt_position => prefix
}
}, },
#{ #{
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
password => <<"bcrypt">>, 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"
"a05378b93244ec8f48a99e61ad799d86"
>>,
salt => <<"ATHENA.MIT.EDUraeburn">>, salt => <<"ATHENA.MIT.EDUraeburn">>,
password => <<"password">>, password => <<"password">>,
password_hash_algorithm => #{name => pbkdf2, password_hash_algorithm => #{
name => pbkdf2,
iterations => 2, iterations => 2,
dk_length => 32, dk_length => 32,
mac_fun => sha} mac_fun => sha
}
}, },
#{ #{
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>, password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
salt => <<"ATHENA.MIT.EDUraeburn">>, salt => <<"ATHENA.MIT.EDUraeburn">>,
password => <<"password">>, password => <<"password">>,
password_hash_algorithm => #{name => pbkdf2, password_hash_algorithm => #{
name => pbkdf2,
iterations => 2, iterations => 2,
mac_fun => sha} mac_fun => sha
}
} }
]. ].

View File

@ -42,7 +42,8 @@ init_per_testcase(_, Config) ->
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) ->
@ -64,7 +65,8 @@ init_per_suite(Config) ->
?RESOURCE_GROUP, ?RESOURCE_GROUP,
emqx_connector_pgsql, emqx_connector_pgsql,
pgsql_config(), pgsql_config(),
#{}), #{}
),
Config; Config;
false -> false ->
{skip, no_pgsql} {skip, no_pgsql}
@ -73,7 +75,8 @@ 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]).
@ -87,7 +90,8 @@ t_create(_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).
@ -107,11 +111,13 @@ t_create_invalid(_Config) ->
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), emqx_authn_test_lib:delete_config(?ResourceID),
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL) {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
end, end,
InvalidConfigs). InvalidConfigs
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = lists:foreach( ok = lists:foreach(
@ -119,16 +125,20 @@ t_authenticate(_Config) ->
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(#{
credentials := Credentials0,
config_params := SpecificConfigParams, config_params := SpecificConfigParams,
result := Result}) -> 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',
@ -139,72 +149,91 @@ test_user_auth(#{credentials := Credentials0,
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">>, #{
username => <<"plain">>,
password => <<"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">>, #{
username => <<"plain">>,
password => <<"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">>, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt 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">>, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt 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},
@ -237,13 +266,16 @@ test_is_superuser({Field, Value, ExpectedValue}) ->
ok = create_user(UserData), ok = create_user(UserData),
Query = "SELECT password_hash, salt, " ++ atom_to_list(Field) ++ " as is_superuser " Query =
"SELECT password_hash, salt, " ++ atom_to_list(Field) ++
" as is_superuser "
"FROM users where username = ${username} LIMIT 1", "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',
@ -254,7 +286,8 @@ test_is_superuser({Field, Value, ExpectedValue}) ->
?assertEqual( ?assertEqual(
{ok, #{is_superuser => ExpectedValue}}, {ok, #{is_superuser => ExpectedValue}},
emqx_access_control:authenticate(Credentials)). emqx_access_control:authenticate(Credentials)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -263,8 +296,10 @@ 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">>,
salt_position => <<"suffix">>
},
enable => <<"true">>, enable => <<"true">>,
backend => <<"postgresql">>, backend => <<"postgresql">>,
@ -272,13 +307,18 @@ raw_pgsql_auth_config() ->
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">>, <<
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
" FROM users where username = ${username} LIMIT 1"
>>,
server => pgsql_server() server => pgsql_server()
}. }.
user_seeds() -> user_seeds() ->
[#{data => #{ [
#{
data => #{
username => "plain", username => "plain",
password_hash => "plainsalt", password_hash => "plainsalt",
salt => "salt", salt => "salt",
@ -286,12 +326,14 @@ user_seeds() ->
}, },
credentials => #{ credentials => #{
username => <<"plain">>, username => <<"plain">>,
password => <<"plain">>}, password => <<"plain">>
},
config_params => #{}, config_params => #{},
result => {ok, #{is_superuser => true}} result => {ok, #{is_superuser => true}}
}, },
#{data => #{ #{
data => #{
username => "md5", username => "md5",
password_hash => "9b4d0c43d206d48279e69b9ad7132e22", password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
salt => "salt", salt => "salt",
@ -302,13 +344,16 @@ user_seeds() ->
password => <<"md5">> password => <<"md5">>
}, },
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"md5">>, password_hash_algorithm => #{
salt_position => <<"suffix">>} name => <<"md5">>,
salt_position => <<"suffix">>
}
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
username => "sha256", username => "sha256",
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf", password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
salt => "salt", salt => "salt",
@ -319,15 +364,21 @@ user_seeds() ->
password => <<"sha256">> password => <<"sha256">>
}, },
config_params => #{ 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 => <<"sha256">>, "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
salt_position => <<"prefix">>} " FROM users where username = ${clientid} LIMIT 1"
>>,
password_hash_algorithm => #{
name => <<"sha256">>,
salt_position => <<"prefix">>
}
}, },
result => {ok, #{is_superuser => true}} result => {ok, #{is_superuser => true}}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt">>, username => <<"bcrypt">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
@ -338,14 +389,18 @@ user_seeds() ->
password => <<"bcrypt">> password => <<"bcrypt">>
}, },
config_params => #{ config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser query =>
FROM users where username = ${username} LIMIT 1">>, <<
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
" FROM users where username = ${username} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt0">>, username => <<"bcrypt0">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
@ -357,14 +412,18 @@ user_seeds() ->
}, },
config_params => #{ config_params => #{
% clientid variable & username credentials % clientid variable & username credentials
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser query =>
FROM users where username = ${clientid} LIMIT 1">>, <<
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
" FROM users where username = ${clientid} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {error, not_authorized} result => {error, not_authorized}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt1">>, username => <<"bcrypt1">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
@ -376,14 +435,18 @@ user_seeds() ->
}, },
config_params => #{ config_params => #{
% Bad keys in query % Bad keys in query
query => <<"SELECT 1 AS unknown_field query =>
FROM users where username = ${username} LIMIT 1">>, <<
"SELECT 1 AS unknown_field\n"
" FROM users where username = ${username} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {error, not_authorized} result => {error, not_authorized}
}, },
#{data => #{ #{
data => #{
username => <<"bcrypt2">>, username => <<"bcrypt2">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
@ -403,24 +466,28 @@ user_seeds() ->
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 =
"INSERT INTO users(username, password_hash, salt,"
"is_superuser_str, is_superuser_int, is_superuser_bool) " "is_superuser_str, is_superuser_int, is_superuser_bool) "
"VALUES($1, $2, $3, $4, $5, $6)", "VALUES($1, $2, $3, $4, $5, $6)",
@ -431,12 +498,14 @@ 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"),
@ -446,7 +515,8 @@ 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, #{
auto_reconnect => true,
database => <<"mqtt">>, database => <<"mqtt">>,
username => <<"root">>, username => <<"root">>,
password => <<"public">>, password => <<"public">>,

View File

@ -40,7 +40,8 @@ init_per_testcase(_, Config) ->
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) ->
@ -57,7 +58,8 @@ 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,36 +74,51 @@ t_create(_Config) ->
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_pgsql_auth_with_ssl_opts( create_pgsql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]})). <<"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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>]})), <<"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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.2">>], <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]})). <<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]
}
)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -114,11 +131,14 @@ 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">>,
salt_position => <<"suffix">>
},
enable => <<"true">>, enable => <<"true">>,
backend => <<"postgresql">>, backend => <<"postgresql">>,

View File

@ -41,7 +41,8 @@ init_per_testcase(_, Config) ->
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) ->
@ -63,7 +64,8 @@ init_per_suite(Config) ->
?RESOURCE_GROUP, ?RESOURCE_GROUP,
emqx_connector_redis, emqx_connector_redis,
redis_config(), redis_config(),
#{}), #{}
),
Config; Config;
false -> false ->
{skip, no_redis} {skip, no_redis}
@ -72,7 +74,8 @@ 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]).
@ -87,7 +90,8 @@ 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).
@ -96,21 +100,26 @@ t_create_invalid(_Config) ->
InvalidConfigs = InvalidConfigs =
[ [
AuthConfig#{ AuthConfig#{
cmd => <<"MGET password_hash:${username} salt:${username}">>}, cmd => <<"MGET password_hash:${username} salt:${username}">>
},
AuthConfig#{ AuthConfig#{
cmd => <<"HMGET mqtt_user:${username} password_hash invalid_field">>}, cmd => <<"HMGET mqtt_user:${username} password_hash invalid_field">>
},
AuthConfig#{ AuthConfig#{
cmd => <<"HMGET mqtt_user:${username} salt is_superuser">>} 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 =
[ [
@ -124,11 +133,13 @@ t_create_invalid(_Config) ->
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), emqx_authn_test_lib:delete_config(?ResourceID),
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL) {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
end, end,
InvalidConfigs1). InvalidConfigs1
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = lists:foreach( ok = lists:foreach(
@ -136,16 +147,20 @@ t_authenticate(_Config) ->
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(#{
credentials := Credentials0,
config_params := SpecificConfigParams, config_params := SpecificConfigParams,
result := Result}) -> 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',
@ -156,65 +171,80 @@ test_user_auth(#{credentials := Credentials0,
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">>, #{
username => <<"plain">>,
password => <<"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">>, #{
username => <<"plain">>,
password => <<"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">>, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt 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">>, #{
username => <<"plain">>,
password => <<"plain">>, password => <<"plain">>,
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt protocol => mqtt
}). }
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -223,8 +253,10 @@ 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">>,
salt_position => <<"suffix">>
},
enable => <<"true">>, enable => <<"true">>,
backend => <<"redis">>, backend => <<"redis">>,
@ -235,20 +267,24 @@ raw_redis_auth_config() ->
}. }.
user_seeds() -> user_seeds() ->
[#{data => #{ [
#{
data => #{
password_hash => <<"plainsalt">>, password_hash => <<"plainsalt">>,
salt => <<"salt">>, salt => <<"salt">>,
is_superuser => <<"1">> is_superuser => <<"1">>
}, },
credentials => #{ credentials => #{
username => <<"plain">>, username => <<"plain">>,
password => <<"plain">>}, password => <<"plain">>
},
key => <<"mqtt_user:plain">>, key => <<"mqtt_user:plain">>,
config_params => #{}, config_params => #{},
result => {ok, #{is_superuser => true}} result => {ok, #{is_superuser => true}}
}, },
#{data => #{ #{
data => #{
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>, password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"salt">>, salt => <<"salt">>,
is_superuser => <<"0">> is_superuser => <<"0">>
@ -259,14 +295,18 @@ user_seeds() ->
}, },
key => <<"mqtt_user:md5">>, key => <<"mqtt_user:md5">>,
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"md5">>, password_hash_algorithm => #{
salt_position => <<"suffix">>} name => <<"md5">>,
salt_position => <<"suffix">>
}
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>, data => #{
password_hash =>
<<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
salt => <<"salt">>, salt => <<"salt">>,
is_superuser => <<"1">> is_superuser => <<"1">>
}, },
@ -277,13 +317,16 @@ user_seeds() ->
key => <<"mqtt_user:sha256">>, key => <<"mqtt_user:sha256">>,
config_params => #{ config_params => #{
cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>, cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"sha256">>, password_hash_algorithm => #{
salt_position => <<"prefix">>} name => <<"sha256">>,
salt_position => <<"prefix">>
}
}, },
result => {ok, #{is_superuser => true}} result => {ok, #{is_superuser => true}}
}, },
#{data => #{ #{
data => #{
password_hash => password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
@ -299,7 +342,8 @@ user_seeds() ->
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>, password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
salt => <<"ATHENA.MIT.EDUraeburn">>, salt => <<"ATHENA.MIT.EDUraeburn">>,
is_superuser => <<"0">> is_superuser => <<"0">>
@ -310,14 +354,16 @@ user_seeds() ->
}, },
key => <<"mqtt_user:pbkdf2">>, key => <<"mqtt_user:pbkdf2">>,
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"pbkdf2">>, password_hash_algorithm => #{
name => <<"pbkdf2">>,
iterations => 2, iterations => 2,
mac_fun => sha mac_fun => sha
} }
}, },
result => {ok, #{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
data => #{
password_hash => password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
@ -336,7 +382,8 @@ user_seeds() ->
result => {error, not_authorized} result => {error, not_authorized}
}, },
#{data => #{ #{
data => #{
password_hash => password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
@ -355,7 +402,8 @@ user_seeds() ->
result => {error, not_authorized} result => {error, not_authorized}
}, },
#{data => #{ #{
data => #{
password_hash => password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
@ -379,30 +427,36 @@ 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(
fun({Key, Value}) ->
q(["HSET", UserKey, atom_to_list(Key), Value]) q(["HSET", UserKey, atom_to_list(Key), Value])
end, end,
maps:to_list(Values)) maps:to_list(Values)
)
end, end,
user_seeds()). 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, #{
auto_reconnect => true,
database => 1, database => 1,
pool_size => 8, pool_size => 8,
redis_type => single, redis_type => single,

View File

@ -40,7 +40,8 @@ init_per_testcase(_, Config) ->
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) ->
@ -57,7 +58,8 @@ 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]).
@ -69,37 +71,53 @@ t_create(_Config) ->
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_redis_auth_with_ssl_opts( create_redis_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.3">>], <<"versions">> => [<<"tlsv1.3">>],
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]})). <<"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">>, #{
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.3">>], <<"versions">> => [<<"tlsv1.3">>],
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]})), <<"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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>, <<"tlsv1.2">>]})), <<"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">>, #{
<<"server_name_indication">> => <<"authn-server">>,
<<"verify">> => <<"verify_peer">>, <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.3">>], <<"versions">> => [<<"tlsv1.3">>],
<<"ciphers">> => [<<"TLS_AES_128_GCM_SHA256">>]})). <<"ciphers">> => [<<"TLS_AES_128_GCM_SHA256">>]
}
)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -112,11 +130,14 @@ 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">>,
salt_position => <<"suffix">>
},
enable => <<"true">>, enable => <<"true">>,
backend => <<"redis">>, backend => <<"redis">>,

View File

@ -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"]), #{
keyfile => filename:join([Dir, "data/certs", "client.key"]),
certfile => filename:join([Dir, "data/certs", "client.crt"]), certfile => filename:join([Dir, "data/certs", "client.crt"]),
cacertfile => filename:join([Dir, "data/certs", "ca.crt"])}. cacertfile => filename:join([Dir, "data/certs", "ca.crt"])
}.

View File

@ -26,8 +26,10 @@
-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).
@ -45,7 +47,8 @@ init_per_testcase(_Case, Config) ->
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) ->
@ -65,10 +68,11 @@ 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 = #{
@ -80,7 +84,8 @@ t_create_invalid(_Config) ->
{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).
@ -102,33 +107,41 @@ t_authenticate(_Config) ->
'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, #{
client_first_message => ClientFirstMessage,
password => Password, password => Password,
algorithm => Algorithm} 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}
@ -152,7 +165,8 @@ t_authenticate_bad_username(_Config) ->
'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),
@ -176,26 +190,33 @@ t_authenticate_bad_password(_Config) ->
'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, #{
client_first_message => ClientFirstMessage,
password => <<"badpassword">>, password => <<"badpassword">>,
algorithm => Algorithm} 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),
@ -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
}} = emqx_enhanced_authn_scram_mnesia:update_user(
<<"u">>, <<"u">>,
#{password => <<"p1">>, is_superuser => true}, #{password => <<"p1">>, is_superuser => true},
State), 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 => <<"u1">>, password => <<"p">>},
#{user_id => <<"u2">>, password => <<"p">>}, #{user_id => <<"u2">>, password => <<"p">>},
#{user_id => <<"u3">>, 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],
meta := #{page := 1, limit := 2, count := 3}
} = emqx_enhanced_authn_scram_mnesia:list_users(
#{<<"page">> => 1, <<"limit">> => 2}, #{<<"page">> => 1, <<"limit">> => 2},
State), State
#{data := [?USER_MAP], ),
meta := #{page := 2, limit := 2, count := 3}} = emqx_enhanced_authn_scram_mnesia:list_users( #{
data := [?USER_MAP],
meta := #{page := 2, limit := 2, count := 3}
} = emqx_enhanced_authn_scram_mnesia:list_users(
#{<<"page">> => 2, <<"limit">> => 2}, #{<<"page">> => 2, <<"limit">> => 2},
State), State
#{data := [#{user_id := <<"u1">>, ),
is_superuser := _}], #{
meta := #{page := 1, limit := 3, count := 1}} = emqx_enhanced_authn_scram_mnesia:list_users( data := [
#{ <<"page">> => 1 #{
, <<"limit">> => 3 user_id := <<"u1">>,
, <<"like_username">> => <<"1">>}, is_superuser := _
State). }
],
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_method => <<"SCRAM-SHA-512">>,
auth_data => ClientFirstMessage, auth_data => ClientFirstMessage,
auth_cache => #{} 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, #{
client_first_message => ClientFirstMessage,
password => Password, password => Password,
algorithm => sha512} 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_method => <<"SCRAM-SHA-512">>,
auth_data => ClientFinalMessage, auth_data => ClientFinalMessage,
auth_cache => ServerCache 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,7 +385,6 @@ test_is_superuser(UserInfo, ExpectedIsSuperuser) ->
ok = emqx_enhanced_authn_scram_mnesia:destroy(State). ok = emqx_enhanced_authn_scram_mnesia:destroy(State).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -362,13 +410,15 @@ init_auth(Username, Password, 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

View File

@ -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 ).
-define(PUBSUB(A),
((A =:= subscribe) orelse (A =:= <<"subscribe">>) orelse
(A =:= publish) orelse (A =:= <<"publish">>) orelse (A =:= publish) orelse (A =:= <<"publish">>) orelse
(A =:= all) orelse (A =:= <<"all">>) (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,
rules => [
#{
topic => <<"test/toopic/1">>,
permission => <<"allow">>, permission => <<"allow">>,
action => <<"publish">> action => <<"publish">>
} },
, #{topic => <<"test/toopic/2">>, #{
topic => <<"test/toopic/2">>,
permission => <<"allow">>, permission => <<"allow">>,
action => <<"subscribe">> action => <<"subscribe">>
} },
, #{topic => <<"eq test/#">>, #{
topic => <<"eq test/#">>,
permission => <<"deny">>, permission => <<"deny">>,
action => <<"all">> action => <<"all">>
} }
] ]
}). }).
-define(CLIENTID_RULES_EXAMPLE, #{clientid => client1, -define(CLIENTID_RULES_EXAMPLE, #{
rules => [ #{topic => <<"test/toopic/1">>, clientid => client1,
rules => [
#{
topic => <<"test/toopic/1">>,
permission => <<"allow">>, permission => <<"allow">>,
action => <<"publish">> action => <<"publish">>
} },
, #{topic => <<"test/toopic/2">>, #{
topic => <<"test/toopic/2">>,
permission => <<"allow">>, permission => <<"allow">>,
action => <<"subscribe">> action => <<"subscribe">>
} },
, #{topic => <<"eq test/#">>, #{
topic => <<"eq test/#">>,
permission => <<"deny">>, permission => <<"deny">>,
action => <<"all">> action => <<"all">>
} }
] ]
}). }).
-define(ALL_RULES_EXAMPLE, #{rules => [ #{topic => <<"test/toopic/1">>, -define(ALL_RULES_EXAMPLE, #{
rules => [
#{
topic => <<"test/toopic/1">>,
permission => <<"allow">>, permission => <<"allow">>,
action => <<"publish">> action => <<"publish">>
} },
, #{topic => <<"test/toopic/2">>, #{
topic => <<"test/toopic/2">>,
permission => <<"allow">>, permission => <<"allow">>,
action => <<"subscribe">> action => <<"subscribe">>
} },
, #{topic => <<"eq test/#">>, #{
topic => <<"eq test/#">>,
permission => <<"deny">>, permission => <<"deny">>,
action => <<"all">> action => <<"all">>
} }
] ]
}). }).
-define(META_EXAMPLE, #{ page => 1 -define(META_EXAMPLE, #{
, limit => 100 page => 1,
, count => 1 limit => 100,
count => 1
}). }).
-define(RESOURCE_GROUP, <<"emqx_authz">>). -define(RESOURCE_GROUP, <<"emqx_authz">>).

View File

@ -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]}.

View File

@ -1,11 +1,11 @@
%% -*- 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

View File

@ -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,11 +219,13 @@ 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 =
case maps:get(<<"type">>, Source, maps:get(type, Source, undefined)) of
undefined -> undefined ->
%% this should never happen if the value is type checked by honcon schema %% this should never happen if the value is type checked by honcon schema
throw({bad_source_input, Source}); throw({bad_source_input, Source});
@ -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, ) ->
authz_result().
authorize(
#{
username := Username,
peerhost := IpAddress peerhost := IpAddress
} = Client, PubSub, Topic, DefaultResult, Sources) -> } = 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]
),
?SLOG(info, #{
msg => "authorization_permission_allowed",
username => Username, username => Username,
ipaddr => IpAddress, ipaddr => IpAddress,
topic => Topic}), 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]
),
?SLOG(info, #{
msg => "authorization_permission_denied",
username => Username, username => Username,
ipaddr => IpAddress, ipaddr => IpAddress,
topic => Topic}), 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]
),
?SLOG(info, #{
msg => "authorization_failed_nomatch",
username => Username, username => Username,
ipaddr => IpAddress, ipaddr => IpAddress,
topic => Topic, topic => Topic,
reason => "no-match rule"}), 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);
@ -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)

View File

@ -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 => post =>
#{ tags => [<<"authorization">>] #{
, description => <<"Add new records for username">> tags => [<<"authorization">>],
, 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_ARRAY} description => <<"Add new records for username">>,
, {username, ?POST_ARRAY_EXAMPLE}) 'requestBody' => swagger_with_example(
, responses => {rules_for_username, ?TYPE_ARRAY},
#{ 204 => <<"Created">> {username, ?POST_ARRAY_EXAMPLE}
, 400 => emqx_dashboard_swagger:error_codes( ),
[?BAD_REQUEST], <<"Bad username or bad rule schema">>) 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} responses =>
, {clientid, ?PAGE_QUERY_EXAMPLE}) #{
200 => swagger_with_example(
{clientid_response_data, ?TYPE_REF},
{clientid, ?PAGE_QUERY_EXAMPLE}
)
} }
} },
, post => post =>
#{ tags => [<<"authorization">>] #{
, description => <<"Add new records for clientid">> tags => [<<"authorization">>],
, 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_ARRAY} description => <<"Add new records for clientid">>,
, {clientid, ?POST_ARRAY_EXAMPLE}) 'requestBody' => swagger_with_example(
, responses => {rules_for_clientid, ?TYPE_ARRAY},
#{ 204 => <<"Created">> {clientid, ?POST_ARRAY_EXAMPLE}
, 400 => emqx_dashboard_swagger:error_codes( ),
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>) 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") -> schema("/authorization/sources/built_in_database/username/:username") ->
#{ 'operationId' => user #{
, get => 'operationId' => user,
#{ tags => [<<"authorization">>] get =>
, description => <<"Get record info for username">> #{
, parameters => [ref(username)] tags => [<<"authorization">>],
, responses => description => <<"Get record info for username">>,
#{ 200 => swagger_with_example( {rules_for_username, ?TYPE_REF} parameters => [ref(username)],
, {username, ?PUT_MAP_EXAMPLE}) responses =>
, 404 => emqx_dashboard_swagger:error_codes( #{
[?NOT_FOUND], <<"Not Found">>) 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">>
)
} }
, put => },
#{ tags => [<<"authorization">>] delete =>
, description => <<"Set record for username">> #{
, parameters => [ref(username)] tags => [<<"authorization">>],
, 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_REF} description => <<"Delete one record for username">>,
, {username, ?PUT_MAP_EXAMPLE}) parameters => [ref(username)],
, responses => responses =>
#{ 204 => <<"Updated">> #{
, 400 => emqx_dashboard_swagger:error_codes( 204 => <<"Deleted">>,
[?BAD_REQUEST], <<"Bad username or bad rule schema">>) 400 => emqx_dashboard_swagger:error_codes(
} [?BAD_REQUEST], <<"Bad username">>
} ),
, delete => 404 => emqx_dashboard_swagger:error_codes(
#{ tags => [<<"authorization">>] [?NOT_FOUND], <<"Username Not Found">>
, 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") -> schema("/authorization/sources/built_in_database/clientid/:clientid") ->
#{ 'operationId' => client #{
, get => 'operationId' => client,
#{ tags => [<<"authorization">>] get =>
, description => <<"Get record info for clientid">> #{
, parameters => [ref(clientid)] tags => [<<"authorization">>],
, responses => description => <<"Get record info for clientid">>,
#{ 200 => swagger_with_example( {rules_for_clientid, ?TYPE_REF} parameters => [ref(clientid)],
, {clientid, ?PUT_MAP_EXAMPLE}) responses =>
, 404 => emqx_dashboard_swagger:error_codes( #{
[?NOT_FOUND], <<"Not Found">>) 200 => swagger_with_example(
{rules_for_clientid, ?TYPE_REF},
{clientid, ?PUT_MAP_EXAMPLE}
),
404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"Not Found">>
)
} }
}, },
put => put =>
#{ tags => [<<"authorization">>] #{
, description => <<"Set record for clientid">> tags => [<<"authorization">>],
, parameters => [ref(clientid)] description => <<"Set record for clientid">>,
, 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_REF} parameters => [ref(clientid)],
, {clientid, ?PUT_MAP_EXAMPLE}) 'requestBody' => swagger_with_example(
, responses => {rules_for_clientid, ?TYPE_REF},
#{ 204 => <<"Updated">> {clientid, ?PUT_MAP_EXAMPLE}
, 400 => emqx_dashboard_swagger:error_codes( ),
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>) responses =>
#{
204 => <<"Updated">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>
)
} }
} },
, delete => delete =>
#{ tags => [<<"authorization">>] #{
, description => <<"Delete one record for clientid">> tags => [<<"authorization">>],
, parameters => [ref(clientid)] description => <<"Delete one record for clientid">>,
, responses => parameters => [ref(clientid)],
#{ 204 => <<"Deleted">> responses =>
, 400 => emqx_dashboard_swagger:error_codes( #{
[?BAD_REQUEST], <<"Bad clientid">>) 204 => <<"Deleted">>,
, 404 => emqx_dashboard_swagger:error_codes( 400 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"ClientID Not Found">>) [?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">>],
description => <<"Show the list of rules for all">>,
responses =>
#{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})} #{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})}
} },
, post => post =>
#{ tags => [<<"authorization">>] #{
, description => <<"Create/Update the list of rules for all. " tags => [<<"authorization">>],
"Set a empty list to clean up rules">> description => <<
, 'requestBody' => "Create/Update the list of rules for all. "
swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}) "Set a empty list to clean up rules"
, responses => >>,
#{ 204 => <<"Updated">> 'requestBody' =>
, 400 => emqx_dashboard_swagger:error_codes( swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}),
[?BAD_REQUEST], <<"Bad rule schema">>) 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(
fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)) emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules))
end, Body), 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(
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules)) emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules))
end, Body), 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,
rules => [
#{
topic => Topic,
action => Action, action => Action,
permission => Permission permission => Permission
} || {Permission, Action, Topic} <- Rules]}
} }
|| {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,
rules => [
#{
topic => Topic,
action => Action, action => Action,
permission => Permission permission => Permission
} || {Permission, Action, Topic} <- Rules]}
} }
|| {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, #{
rules => [
#{
topic => Topic,
action => Action, action => Action,
permission => Permission permission => Permission
} || {Permission, Action, Topic} <- Rules]}
} }
|| {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,11 +530,14 @@ 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, #{
code => <<"BAD_REQUEST">>,
message => message =>
<<"'built_in_database' type source must be disabled before purge.">>}}; <<"'built_in_database' type source must be disabled before purge.">>
}};
[] -> [] ->
{404, #{code => <<"BAD_REQUEST">>, {404, #{
code => <<"BAD_REQUEST">>,
message => <<"'built_in_database' type source is not found.">> 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(
fun(
#{
<<"topic">> := Topic,
<<"action">> := Action, <<"action">> := Action,
<<"permission">> := Permission <<"permission">> := Permission
}, AccIn) when ?PUBSUB(Action) },
andalso ?ALLOW_DENY(Permission) -> AccIn
) when
?PUBSUB(Action) andalso
?ALLOW_DENY(Permission)
->
AccIn ++ [{atom(Permission), atom(Action), Topic}] AccIn ++ [{atom(Permission), atom(Action), Topic}]
end, [], Rules). 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,
rules => [
#{
topic => Topic,
action => Action, action => Action,
permission => Permission permission => Permission
} || {Permission, Action, Topic} <- Rules] }
|| {Permission, Action, Topic} <- Rules
]
}; };
format_result([{clientid, ClientID}, {rules, Rules}]) -> format_result([{clientid, ClientID}, {rules, Rules}]) ->
#{clientid => ClientID, #{
rules => [ #{topic => Topic, clientid => ClientID,
rules => [
#{
topic => Topic,
action => Action, action => Action,
permission => Permission permission => Permission
} || {Permission, Action, Topic} <- Rules] }
|| {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;
@ -492,7 +676,8 @@ swagger_with_example({Ref, TypeP}, {_Name, _Type} = Example) ->
?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} =
@ -503,7 +688,8 @@ rules_example({ExampleName, ExampleType}) ->
end, end,
Value = Value =
case ExampleType of case ExampleType of
?PAGE_QUERY_EXAMPLE -> #{ ?PAGE_QUERY_EXAMPLE ->
#{
data => [Example], data => [Example],
meta => ?META_EXAMPLE meta => ?META_EXAMPLE
}; };

View File

@ -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(),
required => true,
example =>
<<"{allow,{username,\"^dashboard?\"},", "subscribe,[\"$SYS/#\"]}.\n", <<"{allow,{username,\"^dashboard?\"},", "subscribe,[\"$SYS/#\"]}.\n",
"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>}} "{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,40 +105,51 @@ 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(
<<"content-type">>,
<<"application/json">>, <<"application/json">>,
default_headers_no_content_type()). 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(
fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))), K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc) maps:put(K, V, Acc)
end, #{}, Headers). 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();
@ -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);

View File

@ -20,9 +20,10 @@
-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,18 +41,23 @@ paths() ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
schema("/authorization/settings") -> schema("/authorization/settings") ->
#{ 'operationId' => settings #{
, get => 'operationId' => settings,
#{ description => <<"Get authorization settings">> get =>
, responses => #{
description => <<"Get authorization settings">>,
responses =>
#{200 => ref_authz_schema()} #{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">>)}
} }
}. }.
@ -60,13 +66,17 @@ ref_authz_schema() ->
settings(get, _Params) -> settings(get, _Params) ->
{200, authorization_settings()}; {200, authorization_settings()};
settings(put, #{
settings(put, #{body := #{<<"no_match">> := NoMatch, body := #{
<<"no_match">> := NoMatch,
<<"deny_action">> := DenyAction, <<"deny_action">> := DenyAction,
<<"cache">> := Cache}}) -> <<"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()}.

View File

@ -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 => post =>
#{ description => <<"Add a new source">> #{
, 'requestBody' => mk( hoconsc:union(authz_sources_type_refs()) description => <<"Add a new source">>,
, #{desc => <<"Source config">>}) 'requestBody' => mk(
, responses => hoconsc:union(authz_sources_type_refs()),
#{ 204 => <<"Authorization source created successfully">> #{desc => <<"Source config">>}
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], ),
<<"Bad Request">>) 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">>)
} }
, put => },
#{ description => <<"Update source">> delete =>
, parameters => parameters_field() #{
, 'requestBody' => mk(hoconsc:union(authz_sources_type_refs())) description => <<"Delete source">>,
, responses => parameters => parameters_field(),
#{ 204 => <<"Authorization source updated successfully">> responses =>
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>) #{
} 204 => <<"Deleted 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(),
responses =>
#{
200 => emqx_dashboard_swagger:schema_with_examples(
hoconsc:ref(emqx_authn_schema, "metrics_status_fields"), hoconsc:ref(emqx_authn_schema, "metrics_status_fields"),
status_metrics_example()) status_metrics_example()
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad request">>) ),
, 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>) 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">>,
parameters => parameters_field(),
'requestBody' =>
emqx_dashboard_swagger:schema_with_examples( emqx_dashboard_swagger:schema_with_examples(
ref(?API_SCHEMA_MODULE, position), ref(?API_SCHEMA_MODULE, position),
position_example()) position_example()
, responses => ),
#{ 204 => <<"No Content">> responses =>
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>) #{
, 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>) 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
(
#{
<<"type">> := <<"file">>,
<<"enable">> := Enable,
<<"path">> := Path
},
AccIn
) ->
case file:read_file(Path) of case file:read_file(Path) of
{ok, Rules} -> {ok, Rules} ->
lists:append(AccIn, [#{type => file, lists:append(AccIn, [
#{
type => file,
enable => Enable, enable => Enable,
rules => Rules rules => Rules
}]); }
]);
{error, _} -> {error, _} ->
lists:append(AccIn, [#{type => file, lists:append(AccIn, [
#{
type => file,
enable => Enable, enable => Enable,
rules => <<"">> rules => <<"">>
}]) }
])
end; end;
(Source, AccIn) -> (Source, AccIn) ->
lists:append(AccIn, [read_certs(Source)]) lists:append(AccIn, [read_certs(Source)])
end, [], get_raw_sources()), 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, #{
type => file,
enable => Enable, enable => Enable,
rules => Rules 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,11 +260,16 @@ 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);
@ -213,32 +277,44 @@ source_status(get, #{bindings := #{type := Type}}) ->
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,8 +325,7 @@ 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) ->
@ -264,21 +339,28 @@ lookup_from_all_nodes(ResourceId) ->
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">>,
message => <<"Resource Not Support Status">>
}};
_ ->
{200, #{
node_status => HelpFun(StatusMap, status),
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics), node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
status => AggregateStatus, status => AggregateStatus,
metrics => restructure_map(AggregateMetrics) metrics => restructure_map(AggregateMetrics)
} }}
}
end; 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),
@ -288,7 +370,8 @@ aggregate_status(AllStatus) ->
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) ->
@ -298,7 +381,8 @@ aggregate_metrics([HeadMetrics | AllMetrics]) ->
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,25 +390,23 @@ 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, Status, StatusMap),
maps:put(NodeId, Metrics, MetricsMap), maps:put(NodeId, Metrics, MetricsMap),
ErrorMap 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,
@ -338,7 +420,15 @@ 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,7 +442,8 @@ 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(
fun(Source) ->
case maps:find(<<"headers">>, Source) of case maps:find(<<"headers">>, Source) of
{ok, Headers} -> {ok, Headers} ->
NewHeaders = NewHeaders =
@ -361,34 +452,50 @@ merge_default_headers(Sources) ->
(emqx_authz_schema:headers_no_content_type(converter))(Headers); (emqx_authz_schema:headers_no_content_type(converter))(Headers);
#{<<"method">> := <<"post">>} -> #{<<"method">> := <<"post">>} ->
(emqx_authz_schema:headers(converter))(Headers); (emqx_authz_schema:headers(converter))(Headers);
_ -> Headers _ ->
Headers
end, end,
Source#{<<"headers">> => NewHeaders}; Source#{<<"headers">> => NewHeaders};
error -> Source error ->
Source
end end
end, Sources). end,
Sources
).
get_raw_source(Type) -> get_raw_source(Type) ->
lists:filter(fun (#{<<"type">> := T}) -> lists:filter(
fun(#{<<"type">> := T}) ->
T =:= Type T =:= Type
end, get_raw_sources()). 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,36 +534,52 @@ 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, #{
metrics => #{
matched => 0,
success => 0, success => 0,
failed => 0, failed => 0,
rate => 0.0, rate => 0.0,
rate_last5m => 0.0, rate_last5m => 0.0,
rate_max => 0.0 rate_max => 0.0
}, },
node_metrics => [ #{node => node(), node_metrics => [
metrics => #{ matched => 0, #{
node => node(),
metrics => #{
matched => 0,
success => 0, success => 0,
failed => 0, failed => 0,
rate => 0.0, rate => 0.0,
@ -462,7 +589,9 @@ status_metrics_example() ->
} }
], ],
status => connected, status => connected,
node_status => [ #{node => node(), node_status => [
#{
node => node(),
status => connected status => connected
} }
] ]

View File

@ -27,23 +27,27 @@
-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 =
case file:consult(Path) of
{ok, Terms} -> {ok, Terms} ->
[emqx_authz_rule:compile(Term) || Term <- Terms]; [emqx_authz_rule:compile(Term) || Term <- Terms];
{error, Reason} when is_atom(Reason) -> {error, Reason} when is_atom(Reason) ->
?SLOG(alert, #{msg => failed_to_read_acl_file, ?SLOG(alert, #{
msg => failed_to_read_acl_file,
path => Path, path => Path,
explain => emqx_misc:explain_posix(Reason)}), explain => emqx_misc:explain_posix(Reason)
}),
throw(failed_to_read_acl_file); throw(failed_to_read_acl_file);
{error, Reason} -> {error, Reason} ->
?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}), ?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),

View File

@ -24,11 +24,12 @@
-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).
@ -36,13 +37,15 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-endif. -endif.
-define(PLACEHOLDERS, [?PH_USERNAME, -define(PLACEHOLDERS, [
?PH_USERNAME,
?PH_CLIENTID, ?PH_CLIENTID,
?PH_PEERHOST, ?PH_PEERHOST,
?PH_PROTONAME, ?PH_PROTONAME,
?PH_MOUNTPOINT, ?PH_MOUNTPOINT,
?PH_TOPIC, ?PH_TOPIC,
?PH_ACTION]). ?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, #{
msg => "http_server_query_failed",
resource => ResourceID, resource => ResourceID,
reason => Reason}), 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),
base_query_template => emqx_authz_utils:parse_deep(
cow_qs:parse_qs(bin(Query)), cow_qs:parse_qs(bin(Query)),
?PLACEHOLDERS) ?PLACEHOLDERS
, body_template => emqx_authz_utils:parse_deep( ),
body_template => emqx_authz_utils:parse_deep(
maps:to_list(maps:get(body, Conf, #{})), maps:to_list(maps:get(body, Conf, #{})),
?PLACEHOLDERS) ?PLACEHOLDERS
, headers => Headers ),
, request_timeout => ReqTimeout headers => Headers,
request_timeout => ReqTimeout,
%% pool_type default value `random` %% pool_type default value `random`
, pool_type => 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,15 +135,18 @@ 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),
@ -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);

View File

@ -29,12 +29,12 @@
-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()},
@ -44,22 +44,24 @@
-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).
@ -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(
#{
username := Username,
clientid := Clientid clientid := Clientid
} = Client, PubSub, Topic, #{type := 'built_in_database'}) -> } = Client,
PubSub,
Rules = case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}) of Topic,
#{type := 'built_in_database'}
) ->
Rules =
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}) of
[] -> []; [] -> [];
[#emqx_acl{rules = Rules0}] when is_list(Rules0) -> Rules0 [#emqx_acl{rules = Rules0}] when is_list(Rules0) -> Rules0
end end ++
++ case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}) of 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,7 +216,8 @@ do_get_rules(Key) ->
[] -> not_found [] -> not_found
end. end.
do_authorize(_Client, _PubSub, _Topic, []) -> nomatch; do_authorize(_Client, _PubSub, _Topic, []) ->
nomatch;
do_authorize(Client, PubSub, Topic, [{Permission, Action, TopicFilter} | Tail]) -> 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

View File

@ -24,10 +24,11 @@
-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).
@ -35,32 +36,45 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-endif. -endif.
-define(PLACEHOLDERS, [?PH_USERNAME, -define(PLACEHOLDERS, [
?PH_USERNAME,
?PH_CLIENTID, ?PH_CLIENTID,
?PH_PEERHOST]). ?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});
{ok, Id} ->
Source#{
annotations => #{id => Id},
selector_template => emqx_authz_utils:parse_deep( selector_template => emqx_authz_utils:parse_deep(
Selector, Selector,
?PLACEHOLDERS)} ?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,
PubSub,
Topic,
#{
collection := Collection,
selector_template := SelectorTemplate, selector_template := SelectorTemplate,
annotations := #{id := ResourceID} annotations := #{id := ResourceID}
}) -> }
) ->
RenderedSelector = emqx_authz_utils:render_deep(SelectorTemplate, Client), RenderedSelector = emqx_authz_utils:render_deep(SelectorTemplate, Client),
Result = try Result =
try
emqx_resource:query(ResourceID, {find, Collection, RenderedSelector, #{}}) emqx_resource:query(ResourceID, {find, Collection, RenderedSelector, #{}})
catch catch
error:Error -> {error, Error} error:Error -> {error, Error}
@ -68,18 +82,25 @@ authorize(Client, PubSub, Topic,
case Result of case Result of
{error, Reason} -> {error, Reason} ->
?SLOG(error, #{msg => "query_mongo_error", ?SLOG(error, #{
msg => "query_mongo_error",
reason => Reason, reason => Reason,
collection => Collection, collection => Collection,
selector => RenderedSelector, selector => RenderedSelector,
resource_id => ResourceID}), 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})
|| #{
<<"topics">> := Topics,
<<"permission">> := Permission, <<"permission">> := Permission,
<<"action">> := Action} <- Rows], <<"action">> := Action
} <- Rows
],
do_authorize(Client, PubSub, Topic, Rules) do_authorize(Client, PubSub, Topic, Rules)
end. end.

View File

@ -24,10 +24,11 @@
-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).
@ -35,55 +36,77 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-endif. -endif.
-define(PLACEHOLDERS, [?PH_USERNAME, -define(PLACEHOLDERS, [
?PH_USERNAME,
?PH_CLIENTID, ?PH_CLIENTID,
?PH_PEERHOST, ?PH_PEERHOST,
?PH_CERT_CN_NAME, ?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT]). ?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} ->
Source#{
annotations =>
#{
id => Id,
query => emqx_authz_utils:parse_sql( query => emqx_authz_utils:parse_sql(
SQL, SQL,
'?', '?',
?PLACEHOLDERS)}} ?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,
PubSub,
Topic,
#{
annotations := #{
id := ResourceID,
query := {Query, Params} 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:match(
Client,
PubSub,
Topic,
emqx_authz_rule:compile(format_result(Columns, Row)) emqx_authz_rule:compile(format_result(Columns, Row))
) of )
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.

View File

@ -24,10 +24,11 @@
-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).
@ -35,11 +36,13 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-endif. -endif.
-define(PLACEHOLDERS, [?PH_USERNAME, -define(PLACEHOLDERS, [
?PH_USERNAME,
?PH_CLIENTID, ?PH_CLIENTID,
?PH_PEERHOST, ?PH_PEERHOST,
?PH_CERT_CN_NAME, ?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT]). ?PH_CERT_SUBJECT
]).
description() -> description() ->
"AuthZ with PostgreSQL". "AuthZ with PostgreSQL".
@ -48,18 +51,26 @@ 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,
PubSub,
Topic,
#{
annotations := #{
id := ResourceID,
placeholders := Placeholders 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:match(
Client,
PubSub,
Topic,
emqx_authz_rule:compile(format_result(Columns, Row)) emqx_authz_rule:compile(format_result(Columns, Row))
) of )
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).

View File

@ -24,10 +24,11 @@
-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).
@ -35,11 +36,13 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-endif. -endif.
-define(PLACEHOLDERS, [?PH_CERT_CN_NAME, -define(PLACEHOLDERS, [
?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT, ?PH_CERT_SUBJECT,
?PH_PEERHOST, ?PH_PEERHOST,
?PH_CLIENTID, ?PH_CLIENTID,
?PH_USERNAME]). ?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,
PubSub,
Topic,
#{
cmd_template := CmdTemplate,
annotations := #{id := ResourceID} 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(
Client,
PubSub,
Topic,
emqx_authz_rule:compile({allow, all, Action, [TopicFilter]}) emqx_authz_rule:compile({allow, all, Action, [TopicFilter]})
) of )
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.

View File

@ -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}) ->
@ -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_action(PubSub, Action) andalso
match_who(Client, Who) andalso match_who(Client, Who) andalso
match_topics(Client, Topic, TopicFilters) of 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(
fun(CIDR) ->
esockd_cidr:match(IpAddress, CIDR) esockd_cidr:match(IpAddress, CIDR)
end, CIDRs); 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(
fun(Principal, Permission) ->
match_who(ClientInfo, Principal) andalso Permission match_who(ClientInfo, Principal) andalso Permission
end, true, Principals); 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(
fun(Principal, Permission) ->
match_who(ClientInfo, Principal) orelse Permission match_who(ClientInfo, Principal) orelse Permission
end, false, Principals); end,
match_who(_, _) -> false. 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;

View File

@ -19,21 +19,24 @@
-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]).
@ -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),
hoconsc:ref(?MODULE, redis_sentinel),
hoconsc:ref(?MODULE, redis_cluster)
]
),
default => [], default => [],
desc => desc =>
" "\n"
Authorization data sources.<br> "Authorization data sources.<br>\n"
An array of authorization (ACL) data providers. "An array of authorization (ACL) data providers.\n"
It is designed as an array, not a hash-map, so the sources can be "It is designed as an array, not a hash-map, so the sources can be\n"
ordered to form a chain of access controls.<br> "ordered to form a chain of access controls.<br>\n"
"\n"
When authorizing a 'publish' or 'subscribe' action, the configured "When authorizing a 'publish' or 'subscribe' action, the configured\n"
sources are checked in order. When checking an ACL source, "sources are checked in order. When checking an ACL source,\n"
in case the client (identified by username or client ID) is not found, "in case the client (identified by username or client ID) is not found,\n"
it moves on to the next source. And it stops immediately "it moves on to the next source. And it stops immediately\n"
once an 'allow' or 'deny' decision is returned.<br> "once an 'allow' or 'deny' decision is returned.<br>\n"
"\n"
If the client is not found in any of the sources, "If the client is not found in any of the sources,\n"
the default action configured in 'authorization.no_match' is applied.<br> "the default action configured in 'authorization.no_match' is applied.<br>\n"
"\n"
NOTE: "NOTE:\n"
The source elements are identified by their 'type'. "The source elements are identified by their 'type'.\n"
It is NOT allowed to configure two or more sources of the same type. "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(),
default => true
}},
{path, #{
type => string(),
required => true, required => true,
desc => " desc =>
Path to the file which contains the ACL rules.<br> "\n"
If the file provisioned before starting EMQX node, "Path to the file which contains the ACL rules.<br>\n"
it can be placed anywhere as long as EMQX has read access to it. "If the file provisioned before starting EMQX node,\n"
"it can be placed anywhere as long as EMQX has read access to it.\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`, "In case the rule-set is created from EMQX dashboard or management API,\n"
and the new rules will override all rules from the old config file. "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);
@ -128,10 +142,13 @@ 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) ++
@ -144,44 +161,67 @@ 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(), }},
{selector, #{type => map(), desc => "MQL query used to select the authorization record."}},
{type, #{type => mongodb, desc => "Database backend."}},
{enable, #{
type => boolean(),
default => true, default => true,
desc => "Enable or disable the backend."}} 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(
<<"content-type">>,
<<"application/json">>, <<"application/json">>,
default_headers_no_content_type()). 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(
fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))), K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc) maps:put(K, V, Acc)
end, #{}, Headers). 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,7 +299,8 @@ union_array(Item) when is_list(Item) ->
hoconsc:array(hoconsc:union(Item)). hoconsc:array(hoconsc:union(Item)).
query() -> query() ->
#{type => binary(), #{
type => binary(),
desc => "", desc => "",
validator => fun(S) -> validator => fun(S) ->
case size(S) > 0 of case size(S) > 0 of
@ -261,7 +314,8 @@ 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 =
try
list_to_existing_atom(Mod0) list_to_existing_atom(Mod0)
catch catch
error:badarg -> error:badarg ->
@ -269,10 +323,13 @@ connector_fields(DB, Fields) ->
error:Reason -> error:Reason ->
erlang:error(Reason) erlang:error(Reason)
end, end,
[ {type, #{type => DB, desc => "Database backend."}} [
, {enable, #{type => boolean(), {type, #{type => DB, desc => "Database backend."}},
{enable, #{
type => boolean(),
default => true, default => true,
desc => "Enable or disable the backend."}} 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) ->

View File

@ -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 = #{
strategy => one_for_all,
intensity => 0, intensity => 0,
period => 1}, period => 1
},
ChildSpecs = [], ChildSpecs = [],
{ok, {SupFlags, ChildSpecs}}. {ok, {SupFlags, ChildSpecs}}.

View File

@ -19,14 +19,15 @@
-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
]). ]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -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
emqx_resource:create_local(
ResourceID,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
Module, Config, Module,
#{}) of 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}
@ -47,15 +53,18 @@ 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}).
@ -63,20 +72,25 @@ parse_deep(Template, 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
@ -86,7 +100,9 @@ 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};

View File

@ -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(
emqx_authz,
acl_conf_file,
fun() -> fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf") emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end), 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">>, #{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>}, <<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}), <<"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,14 +73,16 @@ set_special_configs(emqx_authz) ->
set_special_configs(_App) -> set_special_configs(_App) ->
ok. ok.
-define(SOURCE1, #{<<"type">> => <<"http">>, -define(SOURCE1, #{
<<"type">> => <<"http">>,
<<"enable">> => true, <<"enable">> => true,
<<"url">> => <<"https://example.com:443/a/b?c=d">>, <<"url">> => <<"https://example.com:443/a/b?c=d">>,
<<"headers">> => #{}, <<"headers">> => #{},
<<"method">> => <<"get">>, <<"method">> => <<"get">>,
<<"request_timeout">> => 5000 <<"request_timeout">> => 5000
}). }).
-define(SOURCE2, #{<<"type">> => <<"mongodb">>, -define(SOURCE2, #{
<<"type">> => <<"mongodb">>,
<<"enable">> => true, <<"enable">> => true,
<<"mongo_type">> => <<"single">>, <<"mongo_type">> => <<"single">>,
<<"server">> => <<"127.0.0.1:27017">>, <<"server">> => <<"127.0.0.1:27017">>,
@ -84,7 +93,8 @@ set_special_configs(_App) ->
<<"collection">> => <<"authz">>, <<"collection">> => <<"authz">>,
<<"selector">> => #{<<"a">> => <<"b">>} <<"selector">> => #{<<"a">> => <<"b">>}
}). }).
-define(SOURCE3, #{<<"type">> => <<"mysql">>, -define(SOURCE3, #{
<<"type">> => <<"mysql">>,
<<"enable">> => true, <<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>, <<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1, <<"pool_size">> => 1,
@ -95,7 +105,8 @@ set_special_configs(_App) ->
<<"ssl">> => #{<<"enable">> => false}, <<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">> <<"query">> => <<"abcb">>
}). }).
-define(SOURCE4, #{<<"type">> => <<"postgresql">>, -define(SOURCE4, #{
<<"type">> => <<"postgresql">>,
<<"enable">> => true, <<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>, <<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1, <<"pool_size">> => 1,
@ -106,7 +117,8 @@ set_special_configs(_App) ->
<<"ssl">> => #{<<"enable">> => false}, <<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">> <<"query">> => <<"abcb">>
}). }).
-define(SOURCE5, #{<<"type">> => <<"redis">>, -define(SOURCE5, #{
<<"type">> => <<"redis">>,
<<"redis_type">> => <<"single">>, <<"redis_type">> => <<"single">>,
<<"enable">> => true, <<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>, <<"server">> => <<"127.0.0.1:27017">>,
@ -117,14 +129,16 @@ set_special_configs(_App) ->
<<"ssl">> => #{<<"enable">> => false}, <<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>> <<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
}). }).
-define(SOURCE6, #{<<"type">> => <<"file">>, -define(SOURCE6, #{
<<"type">> => <<"file">>,
<<"enable">> => true, <<"enable">> => true,
<<"rules">> => <<"rules">> =>
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}." <<
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> "{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
>>
}). }).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -138,13 +152,17 @@ 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}),
@ -160,73 +178,104 @@ t_update_source(_) ->
{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.

View File

@ -33,15 +33,19 @@ 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">>, #{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>}, <<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}), <<"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, ),
#{
<<"data">> := [#{<<"username">> := <<"user1">>, <<"rules">> := Rules1}],
<<"meta">> := #{
<<"count">> := 1,
<<"limit">> := 100, <<"limit">> := 100,
<<"page">> := 1}} = jsx:decode(Request1), <<"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, ]),
[]
),
#{
<<"data">> := [],
<<"meta">> := #{
<<"count">> := 0,
<<"limit">> := 20, <<"limit">> := 20,
<<"page">> := 1}} = jsx:decode(Request1_1), <<"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.

View File

@ -32,15 +32,19 @@ 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">>, #{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>}, <<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}), <<"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,7 +64,8 @@ set_special_configs(_App) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_api(_) -> t_api(_) ->
Settings1 = #{<<"no_match">> => <<"deny">>, Settings1 = #{
<<"no_match">> => <<"deny">>,
<<"deny_action">> => <<"disconnect">>, <<"deny_action">> => <<"disconnect">>,
<<"cache">> => #{ <<"cache">> => #{
<<"enable">> => false, <<"enable">> => false,
@ -73,7 +78,8 @@ t_api(_) ->
{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 = #{
<<"no_match">> => <<"allow">>,
<<"deny_action">> => <<"ignore">>, <<"deny_action">> => <<"ignore">>,
<<"cache">> => #{ <<"cache">> => #{
<<"enable">> => true, <<"enable">> => true,

View File

@ -29,14 +29,16 @@
-define(PGSQL_HOST, "pgsql"). -define(PGSQL_HOST, "pgsql").
-define(REDIS_SINGLE_HOST, "redis"). -define(REDIS_SINGLE_HOST, "redis").
-define(SOURCE1, #{<<"type">> => <<"http">>, -define(SOURCE1, #{
<<"type">> => <<"http">>,
<<"enable">> => true, <<"enable">> => true,
<<"url">> => <<"https://fake.com:443/acl?username=", ?PH_USERNAME/binary>>, <<"url">> => <<"https://fake.com:443/acl?username=", ?PH_USERNAME/binary>>,
<<"headers">> => #{}, <<"headers">> => #{},
<<"method">> => <<"get">>, <<"method">> => <<"get">>,
<<"request_timeout">> => <<"5s">> <<"request_timeout">> => <<"5s">>
}). }).
-define(SOURCE2, #{<<"type">> => <<"mongodb">>, -define(SOURCE2, #{
<<"type">> => <<"mongodb">>,
<<"enable">> => true, <<"enable">> => true,
<<"mongo_type">> => <<"single">>, <<"mongo_type">> => <<"single">>,
<<"server">> => <<?MONGO_SINGLE_HOST>>, <<"server">> => <<?MONGO_SINGLE_HOST>>,
@ -47,7 +49,8 @@
<<"collection">> => <<"fake">>, <<"collection">> => <<"fake">>,
<<"selector">> => #{<<"a">> => <<"b">>} <<"selector">> => #{<<"a">> => <<"b">>}
}). }).
-define(SOURCE3, #{<<"type">> => <<"mysql">>, -define(SOURCE3, #{
<<"type">> => <<"mysql">>,
<<"enable">> => true, <<"enable">> => true,
<<"server">> => <<?MYSQL_HOST>>, <<"server">> => <<?MYSQL_HOST>>,
<<"pool_size">> => 1, <<"pool_size">> => 1,
@ -58,7 +61,8 @@
<<"ssl">> => #{<<"enable">> => false}, <<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">> <<"query">> => <<"abcb">>
}). }).
-define(SOURCE4, #{<<"type">> => <<"postgresql">>, -define(SOURCE4, #{
<<"type">> => <<"postgresql">>,
<<"enable">> => true, <<"enable">> => true,
<<"server">> => <<?PGSQL_HOST>>, <<"server">> => <<?PGSQL_HOST>>,
<<"pool_size">> => 1, <<"pool_size">> => 1,
@ -69,7 +73,8 @@
<<"ssl">> => #{<<"enable">> => false}, <<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">> <<"query">> => <<"abcb">>
}). }).
-define(SOURCE5, #{<<"type">> => <<"redis">>, -define(SOURCE5, #{
<<"type">> => <<"redis">>,
<<"enable">> => true, <<"enable">> => true,
<<"servers">> => <<?REDIS_SINGLE_HOST, ",127.0.0.1:6380">>, <<"servers">> => <<?REDIS_SINGLE_HOST, ",127.0.0.1:6380">>,
<<"pool_size">> => 1, <<"pool_size">> => 1,
@ -79,11 +84,14 @@
<<"ssl">> => #{<<"enable">> => false}, <<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>> <<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
}). }).
-define(SOURCE6, #{<<"type">> => <<"file">>, -define(SOURCE6, #{
<<"type">> => <<"file">>,
<<"enable">> => true, <<"enable">> => true,
<<"rules">> => <<"rules">> =>
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}." <<
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> "{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>>).
@ -101,23 +109,30 @@ init_per_suite(Config) ->
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, 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(
emqx_authz,
acl_conf_file,
fun() -> fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf") emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end), 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">>, #{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>}, <<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}), <<"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(
emqx,
data_dir,
fun() -> fun() ->
{data_dir, Data} = lists:keyfind(data_dir, 1, Config), {data_dir, Data} = lists:keyfind(data_dir, 1, Config),
Data Data
end), 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,8 +182,12 @@ 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,
@ -175,99 +199,136 @@ t_api(_) ->
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],
RList
)
) )
end 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 = [
{<<"failed">>, 0},
{<<"matched">>, 0}, {<<"matched">>, 0},
{<<"rate">>, 0.0}, {<<"rate">>, 0.0},
{<<"rate_last5m">>, 0.0}, {<<"rate_last5m">>, 0.0},
{<<"rate_max">>, 0.0}, {<<"rate_max">>, 0.0},
{<<"success">>, 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,
uri(["authorization", "sources", "mongodb"]),
?SOURCE2#{
<<"ssl">> => #{
<<"enable">> => <<"true">>, <<"enable">> => <<"true">>,
<<"cacertfile">> => Cacertfile, <<"cacertfile">> => Cacertfile,
<<"certfile">> => Certfile, <<"certfile">> => Certfile,
<<"keyfile">> => Keyfile, <<"keyfile">> => Keyfile,
<<"verify">> => <<"verify_none">> <<"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">>, #{
<<"type">> := <<"mongodb">>,
<<"ssl">> := #{
<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT, <<"cacertfile">> := ?MATCH_CERT,
<<"certfile">> := ?MATCH_CERT, <<"certfile">> := ?MATCH_CERT,
<<"keyfile">> := ?MATCH_RSA_KEY, <<"keyfile">> := ?MATCH_RSA_KEY,
<<"verify">> := <<"verify_none">> <<"verify">> := <<"verify_none">>
} }
}, jsx:decode(Result4)), },
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,
uri(["authorization", "sources", "mongodb"]),
?SOURCE2#{
<<"ssl">> => #{
<<"enable">> => <<"true">>, <<"enable">> => <<"true">>,
<<"cacertfile">> => Cacert, <<"cacertfile">> => Cacert,
<<"certfile">> => Cert, <<"certfile">> => Cert,
<<"keyfile">> => Key, <<"keyfile">> => Key,
<<"verify">> => <<"verify_none">> <<"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">>, #{
<<"type">> := <<"mongodb">>,
<<"ssl">> := #{
<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT, <<"cacertfile">> := ?MATCH_CERT,
<<"certfile">> := ?MATCH_CERT, <<"certfile">> := ?MATCH_CERT,
<<"keyfile">> := ?MATCH_RSA_KEY, <<"keyfile">> := ?MATCH_RSA_KEY,
<<"verify">> := <<"verify_none">> <<"verify">> := <<"verify_none">>
} }
}, jsx:decode(Result5)), },
jsx:decode(Result5)
),
#{
#{ssl := #{cacertfile := SavedCacertfile, ssl := #{
cacertfile := SavedCacertfile,
certfile := SavedCertfile, certfile := SavedCertfile,
keyfile := SavedKeyfile keyfile := SavedKeyfile
}} = emqx_authz:lookup(mongodb), }
} = emqx_authz:lookup(mongodb),
?assert(filelib:is_file(SavedCacertfile)), ?assert(filelib:is_file(SavedCacertfile)),
?assert(filelib:is_file(SavedCertfile)), ?assert(filelib:is_file(SavedCertfile)),
@ -276,25 +337,35 @@ t_api(_) ->
{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.

View File

@ -22,11 +22,14 @@
-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, #{
<<"type">> => <<"file">>,
<<"enable">> => true, <<"enable">> => true,
<<"rules">> => <<"rules">> =>
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}." <<
"\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() ->
@ -38,12 +41,16 @@ 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(
emqx_authz,
acl_conf_file,
fun() -> fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf") emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end), 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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
listener => {tcp, 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
@ -111,7 +129,8 @@ 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).

View File

@ -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,7 +61,8 @@ end_per_testcase(_Case, _Config) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_response_handling(_Config) -> t_response_handling(_Config) ->
ClientInfo = #{clientid => <<"clientid">>, ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -75,7 +75,8 @@ t_response_handling(_Config) ->
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">>),
@ -86,14 +87,17 @@ t_response_handling(_Config) ->
200, 200,
#{<<"content-type">> => <<"text/plain">>}, #{<<"content-type">> => <<"text/plain">>},
"Response body", "Response body",
Req0), 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">>)
),
%% OK, get, 204 %% OK, get, 204
ok = setup_handler_and_config( ok = setup_handler_and_config(
@ -101,11 +105,13 @@ t_response_handling(_Config) ->
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(
@ -113,11 +119,13 @@ t_response_handling(_Config) ->
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(
@ -126,19 +134,23 @@ t_response_handling(_Config) ->
400, 400,
#{<<"content-type">> => <<"text/plain">>}, #{<<"content-type">> => <<"text/plain">>},
"Response body", "Response body",
Req0), 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">>)
).
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">>, #{
username := <<"user name">>,
clientid := <<"client id">>, clientid := <<"client id">>,
peerhost := <<"127.0.0.1">>, peerhost := <<"127.0.0.1">>,
proto_name := <<"MQTT">>, proto_name := <<"MQTT">>,
@ -146,28 +158,36 @@ t_query_params(_Config) ->
topic := <<"t">>, topic := <<"t">>,
action := <<"publish">> action := <<"publish">>
} = cowboy_req:match_qs( } = cowboy_req:match_qs(
[username, [
username,
clientid, clientid,
peerhost, peerhost,
proto_name, proto_name,
mountpoint, mountpoint,
topic, topic,
action], action
Req0), ],
Req0
),
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/?" #{
<<"url">> => <<
"http://127.0.0.1:33333/authz/users/?"
"username=${username}&" "username=${username}&"
"clientid=${clientid}&" "clientid=${clientid}&"
"peerhost=${peerhost}&" "peerhost=${peerhost}&"
"proto_name=${proto_name}&" "proto_name=${proto_name}&"
"mountpoint=${mountpoint}&" "mountpoint=${mountpoint}&"
"topic=${topic}&" "topic=${topic}&"
"action=${action}">> "action=${action}"
}), >>
}
),
ClientInfo = #{clientid => <<"client id">>, ClientInfo = #{
clientid => <<"client id">>,
username => <<"user name">>, username => <<"user name">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
protocol => <<"MQTT">>, protocol => <<"MQTT">>,
@ -178,41 +198,51 @@ t_query_params(_Config) ->
?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">>, #{
<<"username">> := <<"user name">>,
<<"CLIENT">> := <<"client id">>, <<"CLIENT">> := <<"client id">>,
<<"peerhost">> := <<"127.0.0.1">>, <<"peerhost">> := <<"127.0.0.1">>,
<<"proto_name">> := <<"MQTT">>, <<"proto_name">> := <<"MQTT">>,
<<"mountpoint">> := <<"MOUNTPOINT">>, <<"mountpoint">> := <<"MOUNTPOINT">>,
<<"topic">> := <<"t">>, <<"topic">> := <<"t">>,
<<"action">> := <<"publish">>}, <<"action">> := <<"publish">>
jiffy:decode(RawBody, [return_maps])), },
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">>,
<<"body">> => #{
<<"username">> => <<"${username}">>,
<<"CLIENT">> => <<"${clientid}">>, <<"CLIENT">> => <<"${clientid}">>,
<<"peerhost">> => <<"${peerhost}">>, <<"peerhost">> => <<"${peerhost}">>,
<<"proto_name">> => <<"${proto_name}">>, <<"proto_name">> => <<"${proto_name}">>,
<<"mountpoint">> => <<"${mountpoint}">>, <<"mountpoint">> => <<"${mountpoint}">>,
<<"topic">> => <<"${topic}">>, <<"topic">> => <<"${topic}">>,
<<"action">> => <<"${action}">>} <<"action">> => <<"${action}">>
}), }
}
),
ClientInfo = #{clientid => <<"client id">>, ClientInfo = #{
clientid => <<"client id">>,
username => <<"user name">>, username => <<"user name">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
protocol => <<"MQTT">>, protocol => <<"MQTT">>,
@ -223,43 +253,52 @@ t_json_body(_Config) ->
?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">>, #{
<<"username">> := <<"user name">>,
<<"clientid">> := <<"client id">>, <<"clientid">> := <<"client id">>,
<<"peerhost">> := <<"127.0.0.1">>, <<"peerhost">> := <<"127.0.0.1">>,
<<"proto_name">> := <<"MQTT">>, <<"proto_name">> := <<"MQTT">>,
<<"mountpoint">> := <<"MOUNTPOINT">>, <<"mountpoint">> := <<"MOUNTPOINT">>,
<<"topic">> := <<"t">>, <<"topic">> := <<"t">>,
<<"action">> := <<"publish">>}, <<"action">> := <<"publish">>
jiffy:decode(PostVars, [return_maps])), },
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">>,
<<"body">> => #{
<<"username">> => <<"${username}">>,
<<"clientid">> => <<"${clientid}">>, <<"clientid">> => <<"${clientid}">>,
<<"peerhost">> => <<"${peerhost}">>, <<"peerhost">> => <<"${peerhost}">>,
<<"proto_name">> => <<"${proto_name}">>, <<"proto_name">> => <<"${proto_name}">>,
<<"mountpoint">> => <<"${mountpoint}">>, <<"mountpoint">> => <<"${mountpoint}">>,
<<"topic">> => <<"${topic}">>, <<"topic">> => <<"${topic}">>,
<<"action">> => <<"${action}">>}, <<"action">> => <<"${action}">>
},
<<"headers">> => #{<<"content-type">> => <<"application/x-www-form-urlencoded">>} <<"headers">> => #{<<"content-type">> => <<"application/x-www-form-urlencoded">>}
}), }
),
ClientInfo = #{clientid => <<"client id">>, ClientInfo = #{
clientid => <<"client id">>,
username => <<"user name">>, username => <<"user name">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
protocol => <<"MQTT">>, protocol => <<"MQTT">>,
@ -270,11 +309,12 @@ t_form_body(_Config) ->
?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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -287,26 +327,35 @@ t_create_replace(_Config) ->
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
@ -325,7 +374,8 @@ 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).

View File

@ -26,7 +26,8 @@
-export([init/1]). -export([init/1]).
% API % API
-export([start_link/2, -export([
start_link/2,
stop/0, stop/0,
set_handler/1 set_handler/1
]). ]).
@ -53,9 +54,12 @@ 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]),
@ -81,6 +85,6 @@ default_handler(Req0, State) ->
400, 400,
#{<<"content-type">> => <<"text/plain">>}, #{<<"content-type">> => <<"text/plain">>},
<<"">>, <<"">>,
Req0), Req0
),
{ok, Req, State}. {ok, Req, State}.

View File

@ -30,7 +30,8 @@ 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,7 +64,8 @@ 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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -82,7 +83,8 @@ 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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -91,32 +93,40 @@ t_normalize_rules(_Config) ->
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
@ -136,11 +146,14 @@ setup_client_samples(ClientInfo, Samples, Key) ->
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, end,
Samples), Samples
),
#{username := Username, clientid := ClientId} = ClientInfo, #{username := Username, clientid := ClientId} = ClientInfo,
Who = case Key of Who =
case Key of
username -> {username, Username}; username -> {username, Username};
clientid -> {clientid, ClientId}; clientid -> {clientid, ClientId};
all -> all all -> all

View File

@ -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,7 +71,8 @@ end_per_testcase(_TestCase, _Config) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_topic_rules(_Config) -> t_topic_rules(_Config) ->
ClientInfo = #{clientid => <<"clientid">>, ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -87,35 +87,48 @@ 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 = #{
clientid => clientid,
username => "username", username => "username",
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
listener => {tcp, default} listener => {tcp, default}
}, },
Samples = [#{<<"x">> => #{<<"u">> => <<"username">>, Samples = [
#{
<<"x">> => #{
<<"u">> => <<"username">>,
<<"c">> => [#{<<"c">> => <<"clientid">>}], <<"c">> => [#{<<"c">> => <<"clientid">>}],
<<"y">> => 1}, <<"y">> => 1
},
<<"permission">> => <<"allow">>, <<"permission">> => <<"allow">>,
<<"action">> => <<"publish">>, <<"action">> => <<"publish">>,
<<"topics">> => [<<"t">>] <<"topics">> => [<<"t">>]
}], }
],
ok = setup_samples(Samples), ok = setup_samples(Samples),
ok = setup_config( ok = setup_config(
#{<<"selector">> => #{<<"x">> => #{<<"u">> => <<"${username}">>, #{
<<"selector">> => #{
<<"x">> => #{
<<"u">> => <<"${username}">>,
<<"c">> => [#{<<"c">> => <<"${clientid}">>}], <<"c">> => [#{<<"c">> => <<"${clientid}">>}],
<<"y">> => 1} <<"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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -124,14 +137,17 @@ t_mongo_error(_Config) ->
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 = #{
clientid => <<"clientid">>,
cn => <<"cn">>, cn => <<"cn">>,
dn => <<"dn">>, dn => <<"dn">>,
username => <<"username">>, username => <<"username">>,
@ -140,36 +156,49 @@ t_lookups(_Config) ->
listener => {tcp, default} listener => {tcp, default}
}, },
ByClientid = #{<<"clientid">> => <<"clientid">>, ByClientid = #{
<<"clientid">> => <<"clientid">>,
<<"topics">> => [<<"a">>], <<"topics">> => [<<"a">>],
<<"action">> => <<"all">>, <<"action">> => <<"all">>,
<<"permission">> => <<"allow">>}, <<"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 = #{
<<"peerhost">> => <<"127.0.0.1">>,
<<"topics">> => [<<"a">>], <<"topics">> => [<<"a">>],
<<"action">> => <<"all">>, <<"action">> => <<"all">>,
<<"permission">> => <<"allow">>}, <<"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 = #{
clientid => <<"clientid">>,
cn => <<"cn">>, cn => <<"cn">>,
dn => <<"dn">>, dn => <<"dn">>,
username => <<"username">>, username => <<"username">>,
@ -179,12 +208,16 @@ t_bad_selector(_Config) ->
}, },
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
@ -202,16 +235,21 @@ setup_client_samples(ClientInfo, Samples) ->
#{username := Username} = ClientInfo, #{username := Username} = ClientInfo,
Records = lists:map( Records = lists:map(
fun(Sample) -> fun(Sample) ->
#{topics := Topics, #{
topics := Topics,
permission := Permission, permission := Permission,
action := Action} = Sample, action := Action
} = Sample,
#{<<"topics">> => Topics, #{
<<"topics">> => Topics,
<<"permission">> => Permission, <<"permission">> => Permission,
<<"action">> => Action, <<"action">> => Action,
<<"username">> => Username} <<"username">> => Username
}
end, end,
Samples), Samples
),
setup_samples(Records), setup_samples(Records),
setup_config(#{<<"selector">> => #{<<"username">> => <<"${username}">>}}). setup_config(#{<<"selector">> => #{<<"username">> => <<"${username}">>}}).
@ -222,7 +260,8 @@ 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() ->
#{ #{

View File

@ -46,7 +46,8 @@ init_per_suite(Config) ->
?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,7 +73,8 @@ set_special_configs(_) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_topic_rules(_Config) -> t_topic_rules(_Config) ->
ClientInfo = #{clientid => <<"clientid">>, ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -86,9 +87,9 @@ 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 = #{
clientid => <<"clientid">>,
cn => <<"cn">>, cn => <<"cn">>,
dn => <<"dn">>, dn => <<"dn">>,
username => <<"username">>, username => <<"username">>,
@ -100,69 +101,118 @@ t_lookups(_Config) ->
%% 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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -170,43 +220,57 @@ t_mysql_error(_Config) ->
}, },
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 = #{
clientid => clientid,
username => "username", username => "username",
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
listener => {tcp, 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,8 +285,10 @@ 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()
}. }.
@ -230,24 +296,28 @@ raw_mysql_authz_config() ->
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").
@ -259,27 +329,40 @@ setup_client_samples(ClientInfo, Samples) ->
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)"
"VALUES(?, ?, ?, ?)"
>>,
[Username, Topic, Permission, Action]
)
end, end,
Topics) Topics
)
end, end,
Samples), 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, #{
auto_reconnect => true,
database => <<"mqtt">>, database => <<"mqtt">>,
username => <<"root">>, username => <<"root">>,
password => <<"public">>, password => <<"public">>,

View File

@ -46,7 +46,8 @@ init_per_suite(Config) ->
?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,7 +73,8 @@ set_special_configs(_) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_topic_rules(_Config) -> t_topic_rules(_Config) ->
ClientInfo = #{clientid => <<"clientid">>, ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -86,9 +87,9 @@ 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 = #{
clientid => <<"clientid">>,
cn => <<"cn">>, cn => <<"cn">>,
dn => <<"dn">>, dn => <<"dn">>,
username => <<"username">>, username => <<"username">>,
@ -100,69 +101,118 @@ t_lookups(_Config) ->
%% 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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -170,44 +220,62 @@ t_pgsql_error(_Config) ->
}, },
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 = #{
clientid => clientid,
username => "username", username => "username",
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
listener => {tcp, 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,8 +290,10 @@ 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()
}. }.
@ -231,25 +301,29 @@ raw_pgsql_authz_config() ->
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() ->
@ -263,27 +337,40 @@ setup_client_samples(ClientInfo, Samples) ->
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)"
"VALUES($1, $2, $3, $4)"
>>,
[Username, Topic, Permission, Action]
)
end, end,
Topics) Topics
)
end, end,
Samples), 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, #{
auto_reconnect => true,
database => <<"mqtt">>, database => <<"mqtt">>,
username => <<"root">>, username => <<"root">>,
password => <<"public">>, password => <<"public">>,

View File

@ -47,7 +47,8 @@ init_per_suite(Config) ->
?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,17 +66,16 @@ 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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -86,9 +86,9 @@ t_topic_rules(_Config) ->
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 = #{
clientid => <<"client id">>,
cn => <<"cn">>, cn => <<"cn">>,
dn => <<"dn">>, dn => <<"dn">>,
username => <<"username">>, username => <<"username">>,
@ -97,72 +97,94 @@ t_lookups(_Config) ->
listener => {tcp, 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), [
maps:without([<<"server">>], AuthzConfig),
AuthzConfig#{<<"server">> => <<"unknownhost:3333">>}, AuthzConfig#{<<"server">> => <<"unknownhost:3333">>},
AuthzConfig#{<<"password">> => <<"wrongpass">>}, AuthzConfig#{<<"password">> => <<"wrongpass">>},
AuthzConfig#{<<"database">> => <<"5678">>}], 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 = #{
clientid => <<"clientid">>,
username => <<"username">>, username => <<"username">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
@ -183,25 +205,31 @@ setup_sample(AuthzData) ->
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, end,
maps:to_list(AuthzData)). 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
} = Sample,
lists:foreach( lists:foreach(
fun(Topic) -> fun(Topic) ->
q(["HSET", Key, Topic, Action]) q(["HSET", Key, Topic, Action])
end, end,
Topics) Topics
)
end, end,
Samples), Samples
),
setup_config(#{}). setup_config(#{}).
setup_config(SpecialParams) -> setup_config(SpecialParams) ->
@ -226,10 +254,12 @@ redis_server() ->
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, #{
auto_reconnect => true,
database => 1, database => 1,
pool_size => 8, pool_size => 8,
redis_type => single, redis_type => single,

View File

@ -27,10 +27,14 @@
-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).
@ -38,15 +42,19 @@ all() ->
init_per_suite(Config) -> init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz], [emqx_conf, emqx_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">>, #{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>}, <<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}), <<"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 = #{
clientid => <<"test">>,
username => <<"test">>, username => <<"test">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
listener => {tcp, default} listener => {tcp, default}
}, },
ClientInfo2 = #{clientid => <<"test">>, ClientInfo2 = #{
clientid => <<"test">>,
username => <<"test">>, username => <<"test">>,
peerhost => {192, 168, 1, 10}, peerhost => {192, 168, 1, 10},
zone => default, zone => default,
listener => {tcp, default} listener => {tcp, default}
}, },
ClientInfo3 = #{clientid => <<"test">>, ClientInfo3 = #{
clientid => <<"test">>,
username => <<"fake">>, username => <<"fake">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
listener => {tcp, default} listener => {tcp, default}
}, },
ClientInfo4 = #{clientid => <<"fake">>, ClientInfo4 = #{
clientid => <<"fake">>,
username => <<"test">>, username => <<"test">>,
peerhost => {127, 0, 0, 1}, peerhost => {127, 0, 0, 1},
zone => default, zone => default,
listener => {tcp, 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.

View File

@ -33,9 +33,12 @@ 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), #{
<<"no_match">> => atom_to_binary(Nomatch),
<<"cache">> => #{<<"enable">> => atom_to_binary(ChacheEnabled)}, <<"cache">> => #{<<"enable">> => atom_to_binary(ChacheEnabled)},
<<"sources">> => []}), <<"sources">> => []
}
),
ok. ok.
setup_config(BaseConfig, SpecialParams) -> setup_config(BaseConfig, SpecialParams) ->
@ -50,15 +53,19 @@ test_samples(ClientInfo, Samples) ->
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( ?assertEqual(
Expected, Expected,
emqx_access_control:authorize( emqx_access_control:authorize(
ClientInfo, ClientInfo,
Action, Action,
Topic)) Topic
)
)
end, end,
Samples). Samples
).
test_no_topic_rules(ClientInfo, SetupSamples) -> test_no_topic_rules(ClientInfo, SetupSamples) ->
%% No rules %% No rules
@ -68,30 +75,40 @@ test_no_topic_rules(ClientInfo, SetupSamples) ->
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}">>, #{
topics => [
<<"eq testpub1/${username}">>,
<<"testpub2/${clientid}">>, <<"testpub2/${clientid}">>,
<<"testpub3/#">>], <<"testpub3/#">>
],
permission => <<"allow">>, permission => <<"allow">>,
action => <<"publish">> action => <<"publish">>
}, },
#{ #{
topics => [<<"eq testsub1/${username}">>, topics => [
<<"eq testsub1/${username}">>,
<<"testsub2/${clientid}">>, <<"testsub2/${clientid}">>,
<<"testsub3/#">>], <<"testsub3/#">>
],
permission => <<"allow">>, permission => <<"allow">>,
action => <<"subscribe">> action => <<"subscribe">>
}, },
#{ #{
topics => [<<"eq testall1/${username}">>, topics => [
<<"eq testall1/${username}">>,
<<"testall2/${clientid}">>, <<"testall2/${clientid}">>,
<<"testall3/#">>], <<"testall3/#">>
],
permission => <<"allow">>, permission => <<"allow">>,
action => <<"all">> action => <<"all">>
} }
@ -103,7 +120,6 @@ test_allow_topic_rules(ClientInfo, SetupSamples) ->
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,29 +169,36 @@ 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 => [
<<"eq testpub1/${username}">>,
<<"testpub2/${clientid}">>, <<"testpub2/${clientid}">>,
<<"testpub3/#">>], <<"testpub3/#">>
],
permission => <<"deny">>, permission => <<"deny">>,
action => <<"publish">> action => <<"publish">>
}, },
#{ #{
topics => [<<"eq testsub1/${username}">>, topics => [
<<"eq testsub1/${username}">>,
<<"testsub2/${clientid}">>, <<"testsub2/${clientid}">>,
<<"testsub3/#">>], <<"testsub3/#">>
],
permission => <<"deny">>, permission => <<"deny">>,
action => <<"subscribe">> action => <<"subscribe">>
}, },
#{ #{
topics => [<<"eq testall1/${username}">>, topics => [
<<"eq testall1/${username}">>,
<<"testall2/${clientid}">>, <<"testall2/${clientid}">>,
<<"testall3/#">>], <<"testall3/#">>
],
permission => <<"deny">>, permission => <<"deny">>,
action => <<"all">> action => <<"all">>
} }
@ -188,7 +210,6 @@ test_deny_topic_rules(ClientInfo, SetupSamples) ->
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">>}
]). ]
).

View File

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

View File

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