style: erlfmt apps/emqx_authz

This commit is contained in:
JimMoen 2022-04-01 02:11:51 +08:00
parent aae2d01582
commit 82559b9b08
33 changed files with 4042 additions and 2663 deletions

View File

@ -16,13 +16,15 @@
-define(APP, emqx_authz).
-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse
(A =:= deny) orelse (A =:= <<"deny">>)
)).
-define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= <<"subscribe">>) orelse
(A =:= publish) orelse (A =:= <<"publish">>) orelse
(A =:= all) orelse (A =:= <<"all">>)
)).
-define(ALLOW_DENY(A),
((A =:= allow) orelse (A =:= <<"allow">>) orelse
(A =:= deny) orelse (A =:= <<"deny">>))
).
-define(PUBSUB(A),
((A =:= subscribe) orelse (A =:= <<"subscribe">>) orelse
(A =:= publish) orelse (A =:= <<"publish">>) orelse
(A =:= all) orelse (A =:= <<"all">>))
).
%% authz_mnesia
-define(ACL_TABLE, emqx_acl).
@ -44,53 +46,69 @@
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9_]+\\}").
%% API examples
-define(USERNAME_RULES_EXAMPLE, #{username => user1,
rules => [ #{topic => <<"test/toopic/1">>,
permission => <<"allow">>,
action => <<"publish">>
}
, #{topic => <<"test/toopic/2">>,
permission => <<"allow">>,
action => <<"subscribe">>
}
, #{topic => <<"eq test/#">>,
permission => <<"deny">>,
action => <<"all">>
}
]
}).
-define(CLIENTID_RULES_EXAMPLE, #{clientid => client1,
rules => [ #{topic => <<"test/toopic/1">>,
permission => <<"allow">>,
action => <<"publish">>
}
, #{topic => <<"test/toopic/2">>,
permission => <<"allow">>,
action => <<"subscribe">>
}
, #{topic => <<"eq test/#">>,
permission => <<"deny">>,
action => <<"all">>
}
]
}).
-define(ALL_RULES_EXAMPLE, #{rules => [ #{topic => <<"test/toopic/1">>,
permission => <<"allow">>,
action => <<"publish">>
}
, #{topic => <<"test/toopic/2">>,
permission => <<"allow">>,
action => <<"subscribe">>
}
, #{topic => <<"eq test/#">>,
permission => <<"deny">>,
action => <<"all">>
}
]
}).
-define(META_EXAMPLE, #{ page => 1
, limit => 100
, count => 1
}).
-define(USERNAME_RULES_EXAMPLE, #{
username => user1,
rules => [
#{
topic => <<"test/toopic/1">>,
permission => <<"allow">>,
action => <<"publish">>
},
#{
topic => <<"test/toopic/2">>,
permission => <<"allow">>,
action => <<"subscribe">>
},
#{
topic => <<"eq test/#">>,
permission => <<"deny">>,
action => <<"all">>
}
]
}).
-define(CLIENTID_RULES_EXAMPLE, #{
clientid => client1,
rules => [
#{
topic => <<"test/toopic/1">>,
permission => <<"allow">>,
action => <<"publish">>
},
#{
topic => <<"test/toopic/2">>,
permission => <<"allow">>,
action => <<"subscribe">>
},
#{
topic => <<"eq test/#">>,
permission => <<"deny">>,
action => <<"all">>
}
]
}).
-define(ALL_RULES_EXAMPLE, #{
rules => [
#{
topic => <<"test/toopic/1">>,
permission => <<"allow">>,
action => <<"publish">>
},
#{
topic => <<"test/toopic/2">>,
permission => <<"allow">>,
action => <<"subscribe">>
},
#{
topic => <<"eq test/#">>,
permission => <<"deny">>,
action => <<"all">>
}
]
}).
-define(META_EXAMPLE, #{
page => 1,
limit => 100,
count => 1
}).
-define(RESOURCE_GROUP, <<"emqx_authz">>).

View File

@ -1,11 +1,14 @@
%% -*- mode: erlang -*-
{erl_opts, [debug_info, nowarn_unused_import]}.
{deps, [ {emqx, {path, "../emqx"}}
, {emqx_connector, {path, "../emqx_connector"}}
]}.
{deps, [
{emqx, {path, "../emqx"}},
{emqx_connector, {path, "../emqx_connector"}}
]}.
{shell, [
% {config, "config/sys.config"},
% {config, "config/sys.config"},
{apps, [emqx_authz]}
]}.
{project_plugins, [erlfmt]}.

View File

@ -1,18 +1,18 @@
%% -*- mode: erlang -*-
{application, emqx_authz,
[{description, "An OTP application"},
{vsn, "0.1.1"},
{registered, []},
{mod, {emqx_authz_app, []}},
{applications,
[kernel,
stdlib,
crypto,
emqx_connector
]},
{env,[]},
{modules, []},
{application, emqx_authz, [
{description, "An OTP application"},
{vsn, "0.1.1"},
{registered, []},
{mod, {emqx_authz_app, []}},
{applications, [
kernel,
stdlib,
crypto,
emqx_connector
]},
{env, []},
{modules, []},
{licenses, ["Apache 2.0"]},
{links, []}
]}.
{licenses, ["Apache 2.0"]},
{links, []}
]}.

View File

@ -25,29 +25,30 @@
-compile(nowarn_export_all).
-endif.
-export([ register_metrics/0
, init/0
, deinit/0
, lookup/0
, lookup/1
, move/2
, update/2
, authorize/5
]).
-export([
register_metrics/0,
init/0,
deinit/0,
lookup/0,
lookup/1,
move/2,
update/2,
authorize/5
]).
-export([post_config_update/5, pre_config_update/3]).
-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_DENY, 'client.authorize.deny').
@ -60,24 +61,25 @@
%% Initialize authz backend.
%% Populate the passed configuration map with necessary data,
%% like `ResourceID`s
-callback(init(source()) -> source()).
-callback init(source()) -> source().
%% Get authz text description.
-callback(description() -> string()).
-callback description() -> string().
%% Destroy authz backend.
%% Make cleanup of all allocated data.
%% An authz backend will not be used after `destroy`.
-callback(destroy(source()) -> ok).
-callback destroy(source()) -> ok.
%% Authorize client action.
-callback(authorize(
emqx_types:clientinfo(),
emqx_types:pubsub(),
emqx_types:topic(),
source()) -> match_result()).
-callback authorize(
emqx_types:clientinfo(),
emqx_types:pubsub(),
emqx_types:topic(),
source()
) -> match_result().
-spec(register_metrics() -> ok).
-spec register_metrics() -> ok.
register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?METRICS).
@ -104,13 +106,16 @@ lookup(Type) ->
move(Type, ?CMD_MOVE_BEFORE(Before)) ->
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)) ->
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) ->
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) ->
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),
Front2 ++ [S2, S1] ++ Rear2.
ensure_resource_deleted(#{enable := false}) -> ok;
ensure_resource_deleted(#{enable := false}) ->
ok;
ensure_resource_deleted(#{type := Type} = Source) ->
Module = authz_module(Type),
Module:destroy(Source).
@ -213,17 +219,19 @@ ensure_resource_deleted(#{type := Type} = Source) ->
check_dup_types(Sources) ->
check_dup_types(Sources, []).
check_dup_types([], _Checked) -> ok;
check_dup_types([], _Checked) ->
ok;
check_dup_types([Source | Sources], Checked) ->
%% the input might be raw or type-checked result, so lookup both 'type' and <<"type">>
%% TODO: check: really?
Type = case maps:get(<<"type">>, Source, maps:get(type, Source, undefined)) of
undefined ->
%% this should never happen if the value is type checked by honcon schema
throw({bad_source_input, Source});
Type0 ->
type(Type0)
end,
Type =
case maps:get(<<"type">>, Source, maps:get(type, Source, undefined)) of
undefined ->
%% this should never happen if the value is type checked by honcon schema
throw({bad_source_input, Source});
Type0 ->
type(Type0)
end,
case lists:member(Type, Checked) of
true ->
%% we have made it clear not to support more than one authz instance for each type
@ -240,7 +248,8 @@ init_sources(Sources) ->
end,
lists:map(fun init_source/1, Sources).
init_source(#{enable := false} = Source) -> Source;
init_source(#{enable := false} = Source) ->
Source;
init_source(#{type := Type} = Source) ->
Module = authz_module(Type),
Module:init(Source).
@ -250,42 +259,63 @@ init_source(#{type := Type} = Source) ->
%%--------------------------------------------------------------------
%% @doc Check AuthZ
-spec(authorize( emqx_types:clientinfo()
, emqx_types:pubsub()
, emqx_types:topic()
, default_result()
, sources())
-> authz_result()).
authorize(#{username := Username,
peerhost := IpAddress
} = Client, PubSub, Topic, DefaultResult, Sources) ->
-spec authorize(
emqx_types:clientinfo(),
emqx_types:pubsub(),
emqx_types:topic(),
default_result(),
sources()
) ->
authz_result().
authorize(
#{
username := Username,
peerhost := IpAddress
} = Client,
PubSub,
Topic,
DefaultResult,
Sources
) ->
case do_authorize(Client, PubSub, Topic, Sources) of
{{matched, allow}, AuthzSource}->
emqx:run_hook('client.check_authz_complete',
[Client, PubSub, Topic, allow, AuthzSource]),
?SLOG(info, #{msg => "authorization_permission_allowed",
username => Username,
ipaddr => IpAddress,
topic => Topic}),
{{matched, allow}, AuthzSource} ->
emqx:run_hook(
'client.check_authz_complete',
[Client, PubSub, Topic, allow, AuthzSource]
),
?SLOG(info, #{
msg => "authorization_permission_allowed",
username => Username,
ipaddr => IpAddress,
topic => Topic
}),
emqx_metrics:inc(?METRIC_ALLOW),
{stop, allow};
{{matched, deny}, AuthzSource}->
emqx:run_hook('client.check_authz_complete',
[Client, PubSub, Topic, deny, AuthzSource]),
?SLOG(info, #{msg => "authorization_permission_denied",
username => Username,
ipaddr => IpAddress,
topic => Topic}),
{{matched, deny}, AuthzSource} ->
emqx:run_hook(
'client.check_authz_complete',
[Client, PubSub, Topic, deny, AuthzSource]
),
?SLOG(info, #{
msg => "authorization_permission_denied",
username => Username,
ipaddr => IpAddress,
topic => Topic
}),
emqx_metrics:inc(?METRIC_DENY),
{stop, deny};
nomatch ->
emqx:run_hook('client.check_authz_complete',
[Client, PubSub, Topic, DefaultResult, default]),
?SLOG(info, #{msg => "authorization_failed_nomatch",
username => Username,
ipaddr => IpAddress,
topic => Topic,
reason => "no-match rule"}),
emqx:run_hook(
'client.check_authz_complete',
[Client, PubSub, Topic, DefaultResult, default]
),
?SLOG(info, #{
msg => "authorization_failed_nomatch",
username => Username,
ipaddr => IpAddress,
topic => Topic,
reason => "no-match rule"
}),
emqx_metrics:inc(?METRIC_NOMATCH),
{stop, DefaultResult}
end.
@ -294,8 +324,12 @@ do_authorize(_Client, _PubSub, _Topic, []) ->
nomatch;
do_authorize(Client, PubSub, Topic, [#{enable := false} | Rest]) ->
do_authorize(Client, PubSub, Topic, Rest);
do_authorize(Client, PubSub, Topic,
[Connector = #{type := Type} | Tail] ) ->
do_authorize(
Client,
PubSub,
Topic,
[Connector = #{type := Type} | Tail]
) ->
Module = authz_module(Type),
case Module:authorize(Client, PubSub, Topic, Connector) of
nomatch -> do_authorize(Client, PubSub, Topic, Tail);
@ -311,7 +345,7 @@ take(Type) -> take(Type, lookup()).
%% Take the source of give type, the sources list is split into two parts
%% front part and rear part.
take(Type, Sources) ->
{Front, Rear} = lists:splitwith(fun(T) -> type(T) =/= type(Type) end, Sources),
{Front, Rear} = lists:splitwith(fun(T) -> type(T) =/= type(Type) end, Sources),
case Rear =:= [] of
true ->
throw({not_found_source, Type});
@ -321,7 +355,7 @@ take(Type, Sources) ->
find_action_in_hooks() ->
Callbacks = emqx_hooks:lookup('client.authorize'),
[Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ],
[Action] = [Action || {callback, {?MODULE, authorize, _} = Action, _, _} <- Callbacks],
Action.
authz_module('built_in_database') ->
@ -364,8 +398,11 @@ acl_conf_file() ->
filename:join([emqx:data_dir(), "authz", "acl.conf"]).
maybe_write_certs(#{<<"type">> := Type} = Source) ->
case emqx_tls_lib:ensure_ssl_files(
ssl_file_path(Type), maps:get(<<"ssl">>, Source, undefined)) of
case
emqx_tls_lib:ensure_ssl_files(
ssl_file_path(Type), maps:get(<<"ssl">>, Source, undefined)
)
of
{ok, SSL} ->
new_ssl_source(Source, SSL);
{error, Reason} ->
@ -380,7 +417,8 @@ clear_certs(OldSource) ->
write_file(Filename, Bytes) ->
ok = filelib:ensure_dir(Filename),
case file:write_file(Filename, Bytes) of
ok -> {ok, iolist_to_binary(Filename)};
ok ->
{ok, iolist_to_binary(Filename)};
{error, Reason} ->
?SLOG(error, #{filename => Filename, msg => "write_file_error", reason => Reason}),
throw(Reason)

View File

@ -30,25 +30,28 @@
-define(ACL_USERNAME_QSCHEMA, [{<<"like_username">>, binary}]).
-define(ACL_CLIENTID_QSCHEMA, [{<<"like_clientid">>, binary}]).
-export([ api_spec/0
, paths/0
, schema/1
, fields/1
]).
-export([
api_spec/0,
paths/0,
schema/1,
fields/1
]).
%% operation funs
-export([ users/2
, clients/2
, user/2
, client/2
, all/2
, purge/2
]).
-export([
users/2,
clients/2,
user/2,
client/2,
all/2,
purge/2
]).
%% query funs
-export([ query_username/4
, query_clientid/4]).
-export([
query_username/4,
query_clientid/4
]).
-export([format_result/1]).
@ -65,279 +68,399 @@ api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
paths() ->
[ "/authorization/sources/built_in_database/username"
, "/authorization/sources/built_in_database/clientid"
, "/authorization/sources/built_in_database/username/:username"
, "/authorization/sources/built_in_database/clientid/:clientid"
, "/authorization/sources/built_in_database/all"
, "/authorization/sources/built_in_database/purge-all"].
[
"/authorization/sources/built_in_database/username",
"/authorization/sources/built_in_database/clientid",
"/authorization/sources/built_in_database/username/:username",
"/authorization/sources/built_in_database/clientid/:clientid",
"/authorization/sources/built_in_database/all",
"/authorization/sources/built_in_database/purge-all"
].
%%--------------------------------------------------------------------
%% Schema for each URI
%%--------------------------------------------------------------------
schema("/authorization/sources/built_in_database/username") ->
#{ 'operationId' => users
, get =>
#{ tags => [<<"authorization">>]
, description => <<"Show the list of record for username">>
, parameters =>
[ ref(emqx_dashboard_swagger, page)
, ref(emqx_dashboard_swagger, limit)
, { like_username
, mk( binary(), #{ in => query
, required => false
, desc => <<"Fuzzy search `username` as substring">>})}
]
, responses =>
#{ 200 => swagger_with_example( {username_response_data, ?TYPE_REF}
, {username, ?PAGE_QUERY_EXAMPLE})
#{
'operationId' => users,
get =>
#{
tags => [<<"authorization">>],
description => <<"Show the list of record for username">>,
parameters =>
[
ref(emqx_dashboard_swagger, page),
ref(emqx_dashboard_swagger, limit),
{like_username,
mk(binary(), #{
in => query,
required => false,
desc => <<"Fuzzy search `username` as substring">>
})}
],
responses =>
#{
200 => swagger_with_example(
{username_response_data, ?TYPE_REF},
{username, ?PAGE_QUERY_EXAMPLE}
)
}
},
post =>
#{
tags => [<<"authorization">>],
description => <<"Add new records for username">>,
'requestBody' => swagger_with_example(
{rules_for_username, ?TYPE_ARRAY},
{username, ?POST_ARRAY_EXAMPLE}
),
responses =>
#{
204 => <<"Created">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad username or bad rule schema">>
)
}
}
}
, post =>
#{ tags => [<<"authorization">>]
, description => <<"Add new records for username">>
, 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_ARRAY}
, {username, ?POST_ARRAY_EXAMPLE})
, responses =>
#{ 204 => <<"Created">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad username or bad rule schema">>)
}
}
};
schema("/authorization/sources/built_in_database/clientid") ->
#{ 'operationId' => clients
, get =>
#{ tags => [<<"authorization">>]
, description => <<"Show the list of record for clientid">>
, parameters =>
[ ref(emqx_dashboard_swagger, page)
, ref(emqx_dashboard_swagger, limit)
, { like_clientid
, mk( binary()
, #{ in => query
, required => false
, desc => <<"Fuzzy search `clientid` as substring">>})
#{
'operationId' => clients,
get =>
#{
tags => [<<"authorization">>],
description => <<"Show the list of record for clientid">>,
parameters =>
[
ref(emqx_dashboard_swagger, page),
ref(emqx_dashboard_swagger, limit),
{like_clientid,
mk(
binary(),
#{
in => query,
required => false,
desc => <<"Fuzzy search `clientid` as substring">>
}
)}
],
responses =>
#{
200 => swagger_with_example(
{clientid_response_data, ?TYPE_REF},
{clientid, ?PAGE_QUERY_EXAMPLE}
)
}
]
, responses =>
#{ 200 => swagger_with_example( {clientid_response_data, ?TYPE_REF}
, {clientid, ?PAGE_QUERY_EXAMPLE})
}
}
, post =>
#{ tags => [<<"authorization">>]
, description => <<"Add new records for clientid">>
, 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_ARRAY}
, {clientid, ?POST_ARRAY_EXAMPLE})
, responses =>
#{ 204 => <<"Created">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>)
}
}
};
schema("/authorization/sources/built_in_database/username/:username") ->
#{ 'operationId' => user
, get =>
#{ tags => [<<"authorization">>]
, description => <<"Get record info for username">>
, parameters => [ref(username)]
, responses =>
#{ 200 => swagger_with_example( {rules_for_username, ?TYPE_REF}
, {username, ?PUT_MAP_EXAMPLE})
, 404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"Not Found">>)
}
}
, put =>
#{ tags => [<<"authorization">>]
, description => <<"Set record for username">>
, parameters => [ref(username)]
, 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_REF}
, {username, ?PUT_MAP_EXAMPLE})
, responses =>
#{ 204 => <<"Updated">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad username or bad rule schema">>)
}
}
, delete =>
#{ tags => [<<"authorization">>]
, description => <<"Delete one record for username">>
, parameters => [ref(username)]
, responses =>
#{ 204 => <<"Deleted">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad username">>)
, 404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"Username Not Found">>)
}
}
};
schema("/authorization/sources/built_in_database/clientid/:clientid") ->
#{ 'operationId' => client
, get =>
#{ tags => [<<"authorization">>]
, description => <<"Get record info for clientid">>
, parameters => [ref(clientid)]
, responses =>
#{ 200 => swagger_with_example( {rules_for_clientid, ?TYPE_REF}
, {clientid, ?PUT_MAP_EXAMPLE})
, 404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"Not Found">>)
}
},
put =>
#{ tags => [<<"authorization">>]
, description => <<"Set record for clientid">>
, parameters => [ref(clientid)]
, 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_REF}
, {clientid, ?PUT_MAP_EXAMPLE})
, responses =>
#{ 204 => <<"Updated">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>)
}
post =>
#{
tags => [<<"authorization">>],
description => <<"Add new records for clientid">>,
'requestBody' => swagger_with_example(
{rules_for_clientid, ?TYPE_ARRAY},
{clientid, ?POST_ARRAY_EXAMPLE}
),
responses =>
#{
204 => <<"Created">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>
)
}
}
, delete =>
#{ tags => [<<"authorization">>]
, description => <<"Delete one record for clientid">>
, parameters => [ref(clientid)]
, responses =>
#{ 204 => <<"Deleted">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid">>)
, 404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"ClientID Not Found">>)
}
};
schema("/authorization/sources/built_in_database/username/:username") ->
#{
'operationId' => user,
get =>
#{
tags => [<<"authorization">>],
description => <<"Get record info for username">>,
parameters => [ref(username)],
responses =>
#{
200 => swagger_with_example(
{rules_for_username, ?TYPE_REF},
{username, ?PUT_MAP_EXAMPLE}
),
404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"Not Found">>
)
}
},
put =>
#{
tags => [<<"authorization">>],
description => <<"Set record for username">>,
parameters => [ref(username)],
'requestBody' => swagger_with_example(
{rules_for_username, ?TYPE_REF},
{username, ?PUT_MAP_EXAMPLE}
),
responses =>
#{
204 => <<"Updated">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad username or bad rule schema">>
)
}
},
delete =>
#{
tags => [<<"authorization">>],
description => <<"Delete one record for username">>,
parameters => [ref(username)],
responses =>
#{
204 => <<"Deleted">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad username">>
),
404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"Username Not Found">>
)
}
}
};
};
schema("/authorization/sources/built_in_database/clientid/:clientid") ->
#{
'operationId' => client,
get =>
#{
tags => [<<"authorization">>],
description => <<"Get record info for clientid">>,
parameters => [ref(clientid)],
responses =>
#{
200 => swagger_with_example(
{rules_for_clientid, ?TYPE_REF},
{clientid, ?PUT_MAP_EXAMPLE}
),
404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"Not Found">>
)
}
},
put =>
#{
tags => [<<"authorization">>],
description => <<"Set record for clientid">>,
parameters => [ref(clientid)],
'requestBody' => swagger_with_example(
{rules_for_clientid, ?TYPE_REF},
{clientid, ?PUT_MAP_EXAMPLE}
),
responses =>
#{
204 => <<"Updated">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>
)
}
},
delete =>
#{
tags => [<<"authorization">>],
description => <<"Delete one record for clientid">>,
parameters => [ref(clientid)],
responses =>
#{
204 => <<"Deleted">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid">>
),
404 => emqx_dashboard_swagger:error_codes(
[?NOT_FOUND], <<"ClientID Not Found">>
)
}
}
};
schema("/authorization/sources/built_in_database/all") ->
#{ 'operationId' => all
, get =>
#{ tags => [<<"authorization">>]
, description => <<"Show the list of rules for all">>
, responses =>
#{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})}
#{
'operationId' => all,
get =>
#{
tags => [<<"authorization">>],
description => <<"Show the list of rules for all">>,
responses =>
#{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})}
},
post =>
#{
tags => [<<"authorization">>],
description => <<
"Create/Update the list of rules for all. "
"Set a empty list to clean up rules"
>>,
'requestBody' =>
swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}),
responses =>
#{
204 => <<"Updated">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad rule schema">>
)
}
}
, post =>
#{ tags => [<<"authorization">>]
, description => <<"Create/Update the list of rules for all. "
"Set a empty list to clean up rules">>
, 'requestBody' =>
swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})
, responses =>
#{ 204 => <<"Updated">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad rule schema">>)
}
}
};
};
schema("/authorization/sources/built_in_database/purge-all") ->
#{ 'operationId' => purge
, delete =>
#{ tags => [<<"authorization">>]
, description => <<"Purge all records">>
, responses =>
#{ 204 => <<"Deleted">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad Request">>)
}
#{
'operationId' => purge,
delete =>
#{
tags => [<<"authorization">>],
description => <<"Purge all records">>,
responses =>
#{
204 => <<"Deleted">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad Request">>
)
}
}
}.
}.
fields(rule_item) ->
[ {topic, mk(string(),
#{ required => true
, desc => <<"Rule on specific topic">>
, example => <<"test/topic/1">>
})}
, {permission, mk(enum([allow, deny]),
#{ desc => <<"Permission">>
, required => true
, example => allow
})}
, {action, mk(enum([publish, subscribe, all]),
#{ required => true
, example => publish
, desc => <<"Authorized action">>
})}
[
{topic,
mk(
string(),
#{
required => true,
desc => <<"Rule on specific topic">>,
example => <<"test/topic/1">>
}
)},
{permission,
mk(
enum([allow, deny]),
#{
desc => <<"Permission">>,
required => true,
example => allow
}
)},
{action,
mk(
enum([publish, subscribe, all]),
#{
required => true,
example => publish,
desc => <<"Authorized action">>
}
)}
];
fields(clientid) ->
[ {clientid, mk(binary(),
#{ in => path
, required => true
, desc => <<"ClientID">>
, example => <<"client1">>
})}
[
{clientid,
mk(
binary(),
#{
in => path,
required => true,
desc => <<"ClientID">>,
example => <<"client1">>
}
)}
];
fields(username) ->
[ {username, mk(binary(),
#{ in => path
, required => true
, desc => <<"Username">>
, example => <<"user1">>})}
[
{username,
mk(
binary(),
#{
in => path,
required => true,
desc => <<"Username">>,
example => <<"user1">>
}
)}
];
fields(rules_for_username) ->
fields(rules)
++ fields(username);
fields(rules) ++
fields(username);
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)
++ fields(clientid);
fields(rules) ++
fields(clientid);
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) ->
[{rules, mk(array(ref(rule_item)))}];
fields(meta) ->
emqx_dashboard_swagger:fields(page)
++ emqx_dashboard_swagger:fields(limit)
++ [{count, mk(integer(), #{example => 1})}].
emqx_dashboard_swagger:fields(page) ++
emqx_dashboard_swagger:fields(limit) ++
[{count, mk(integer(), #{example => 1})}].
%%--------------------------------------------------------------------
%% HTTP API
%%--------------------------------------------------------------------
users(get, #{query_string := QueryString}) ->
Response = emqx_mgmt_api:node_query(node(), QueryString,
?ACL_TABLE, ?ACL_USERNAME_QSCHEMA, ?QUERY_USERNAME_FUN),
Response = emqx_mgmt_api:node_query(
node(),
QueryString,
?ACL_TABLE,
?ACL_USERNAME_QSCHEMA,
?QUERY_USERNAME_FUN
),
emqx_mgmt_util:generate_response(Response);
users(post, #{body := Body}) when is_list(Body) ->
lists:foreach(fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules))
end, Body),
lists:foreach(
fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules))
end,
Body
),
{204}.
clients(get, #{query_string := QueryString}) ->
Response = emqx_mgmt_api:node_query(node(), QueryString,
?ACL_TABLE, ?ACL_CLIENTID_QSCHEMA, ?QUERY_CLIENTID_FUN),
Response = emqx_mgmt_api:node_query(
node(),
QueryString,
?ACL_TABLE,
?ACL_CLIENTID_QSCHEMA,
?QUERY_CLIENTID_FUN
),
emqx_mgmt_util:generate_response(Response);
clients(post, #{body := Body}) when is_list(Body) ->
lists:foreach(fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules))
end, Body),
lists:foreach(
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules))
end,
Body
),
{204}.
user(get, #{bindings := #{username := Username}}) ->
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} ->
{200, #{username => Username,
rules => [ #{topic => Topic,
action => Action,
permission => Permission
} || {Permission, Action, Topic} <- Rules]}
}
{200, #{
username => Username,
rules => [
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}}
end;
user(put, #{bindings := #{username := Username},
body := #{<<"username">> := Username, <<"rules">> := Rules}}) ->
user(put, #{
bindings := #{username := Username},
body := #{<<"username">> := Username, <<"rules">> := Rules}
}) ->
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)),
{204};
user(delete, #{bindings := #{username := Username}}) ->
@ -351,17 +474,25 @@ user(delete, #{bindings := #{username := Username}}) ->
client(get, #{bindings := #{clientid := ClientID}}) ->
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} ->
{200, #{clientid => ClientID,
rules => [ #{topic => Topic,
action => Action,
permission => Permission
} || {Permission, Action, Topic} <- Rules]}
}
{200, #{
clientid => ClientID,
rules => [
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}}
end;
client(put, #{bindings := #{clientid := ClientID},
body := #{<<"clientid">> := ClientID, <<"rules">> := Rules}}) ->
client(put, #{
bindings := #{clientid := ClientID},
body := #{<<"clientid">> := ClientID, <<"rules">> := Rules}
}) ->
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules)),
{204};
client(delete, #{bindings := #{clientid := ClientID}}) ->
@ -378,11 +509,16 @@ all(get, _) ->
not_found ->
{200, #{rules => []}};
{ok, Rules} ->
{200, #{rules => [ #{topic => Topic,
action => Action,
permission => Permission
} || {Permission, Action, Topic} <- Rules]}
}
{200, #{
rules => [
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}}
end;
all(post, #{body := #{<<"rules">> := Rules}}) ->
emqx_authz_mnesia:store_rules(all, format_rules(Rules)),
@ -394,13 +530,16 @@ purge(delete, _) ->
ok = emqx_authz_mnesia:purge_rules(),
{204};
[#{<<"enable">> := true}] ->
{400, #{code => <<"BAD_REQUEST">>,
message =>
<<"'built_in_database' type source must be disabled before purge.">>}};
{400, #{
code => <<"BAD_REQUEST">>,
message =>
<<"'built_in_database' type source must be disabled before purge.">>
}};
[] ->
{404, #{code => <<"BAD_REQUEST">>,
message => <<"'built_in_database' type source is not found.">>
}}
{404, #{
code => <<"BAD_REQUEST">>,
message => <<"'built_in_database' type source is not found.">>
}}
end.
%%--------------------------------------------------------------------
@ -408,25 +547,43 @@ purge(delete, _) ->
query_username(Tab, {_QString, []}, Continuation, Limit) ->
Ms = emqx_authz_mnesia:list_username_rules(),
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
fun format_result/1);
emqx_mgmt_api:select_table_with_count(
Tab,
Ms,
Continuation,
Limit,
fun format_result/1
);
query_username(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
Ms = emqx_authz_mnesia:list_username_rules(),
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
fun format_result/1).
emqx_mgmt_api:select_table_with_count(
Tab,
{Ms, FuzzyFilterFun},
Continuation,
Limit,
fun format_result/1
).
query_clientid(Tab, {_QString, []}, Continuation, Limit) ->
Ms = emqx_authz_mnesia:list_clientid_rules(),
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
fun format_result/1);
emqx_mgmt_api:select_table_with_count(
Tab,
Ms,
Continuation,
Limit,
fun format_result/1
);
query_clientid(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
Ms = emqx_authz_mnesia:list_clientid_rules(),
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
fun format_result/1).
emqx_mgmt_api:select_table_with_count(
Tab,
{Ms, FuzzyFilterFun},
Continuation,
Limit,
fun format_result/1
).
%%--------------------------------------------------------------------
%% Match funcs
@ -434,17 +591,23 @@ query_clientid(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
%% Fuzzy username funcs
fuzzy_filter_fun(Fuzzy) ->
fun(MsRaws) when is_list(MsRaws) ->
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
, MsRaws)
lists:filter(
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
MsRaws
)
end.
run_fuzzy_filter(_, []) ->
true;
run_fuzzy_filter( E = [{username, Username}, _Rule]
, [{username, like, UsernameSubStr} | Fuzzy]) ->
run_fuzzy_filter(
E = [{username, Username}, _Rule],
[{username, like, UsernameSubStr} | Fuzzy]
) ->
binary:match(Username, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy);
run_fuzzy_filter( E = [{clientid, ClientId}, _Rule]
, [{clientid, like, ClientIdSubStr} | Fuzzy]) ->
run_fuzzy_filter(
E = [{clientid, ClientId}, _Rule],
[{clientid, like, ClientIdSubStr} | 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_rules(Rules) when is_list(Rules) ->
lists:foldl(fun(#{<<"topic">> := Topic,
<<"action">> := Action,
<<"permission">> := Permission
}, AccIn) when ?PUBSUB(Action)
andalso ?ALLOW_DENY(Permission) ->
AccIn ++ [{ atom(Permission), atom(Action), Topic }]
end, [], Rules).
lists:foldl(
fun(
#{
<<"topic">> := Topic,
<<"action">> := Action,
<<"permission">> := Permission
},
AccIn
) when
?PUBSUB(Action) andalso
?ALLOW_DENY(Permission)
->
AccIn ++ [{atom(Permission), atom(Action), Topic}]
end,
[],
Rules
).
%% format result from mnesia tab
format_result([{username, Username}, {rules, Rules}]) ->
#{username => Username,
rules => [ #{topic => Topic,
action => Action,
permission => Permission
} || {Permission, Action, Topic} <- Rules]
};
#{
username => Username,
rules => [
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
};
format_result([{clientid, ClientID}, {rules, Rules}]) ->
#{clientid => ClientID,
rules => [ #{topic => Topic,
action => Action,
permission => Permission
} || {Permission, Action, Topic} <- Rules]
}.
#{
clientid => ClientID,
rules => [
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}.
atom(B) when is_binary(B) ->
try binary_to_existing_atom(B, utf8)
try
binary_to_existing_atom(B, utf8)
catch
_Error:_Expection -> binary_to_atom(B)
end;
@ -488,25 +672,27 @@ atom(A) when is_atom(A) -> A.
swagger_with_example({Ref, TypeP}, {_Name, _Type} = Example) ->
emqx_dashboard_swagger:schema_with_examples(
case TypeP of
?TYPE_REF -> ref(?MODULE, Ref);
?TYPE_ARRAY -> array(ref(?MODULE, Ref))
end,
rules_example(Example)).
case TypeP of
?TYPE_REF -> ref(?MODULE, Ref);
?TYPE_ARRAY -> array(ref(?MODULE, Ref))
end,
rules_example(Example)
).
rules_example({ExampleName, ExampleType}) ->
{Summary, Example} =
case ExampleName of
username -> {<<"Username">>, ?USERNAME_RULES_EXAMPLE};
clientid -> {<<"ClientID">>, ?CLIENTID_RULES_EXAMPLE};
all -> {<<"All">>, ?ALL_RULES_EXAMPLE}
all -> {<<"All">>, ?ALL_RULES_EXAMPLE}
end,
Value =
case ExampleType of
?PAGE_QUERY_EXAMPLE -> #{
data => [Example],
meta => ?META_EXAMPLE
};
?PAGE_QUERY_EXAMPLE ->
#{
data => [Example],
meta => ?META_EXAMPLE
};
?PUT_MAP_EXAMPLE ->
Example;
?POST_ARRAY_EXAMPLE ->
@ -515,6 +701,6 @@ rules_example({ExampleName, ExampleType}) ->
#{
'password_based:built_in_database' => #{
summary => Summary,
value => Value
value => Value
}
}.

View File

@ -25,58 +25,77 @@
-export([fields/1, authz_sources_types/1]).
fields(http) ->
authz_common_fields(http)
++ [ {url, fun url/1}
, {method, #{ type => enum([get, post])
, default => get}}
, {headers, fun headers/1}
, {body, map([{fuzzy, term(), binary()}])}
, {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))));
authz_common_fields(http) ++
[
{url, fun url/1},
{method, #{
type => enum([get, post]),
default => get
}},
{headers, fun headers/1},
{body, map([{fuzzy, term(), binary()}])},
{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') ->
authz_common_fields('built_in_database');
fields(mongo_single) ->
authz_mongo_common_fields()
++ emqx_connector_mongo:fields(single);
authz_mongo_common_fields() ++
emqx_connector_mongo:fields(single);
fields(mongo_rs) ->
authz_mongo_common_fields()
++ emqx_connector_mongo:fields(rs);
authz_mongo_common_fields() ++
emqx_connector_mongo:fields(rs);
fields(mongo_sharded) ->
authz_mongo_common_fields()
++ emqx_connector_mongo:fields(sharded);
authz_mongo_common_fields() ++
emqx_connector_mongo:fields(sharded);
fields(mysql) ->
authz_common_fields(mysql)
++ [ {query, #{type => binary()}}]
++ emqx_connector_mysql:fields(config);
authz_common_fields(mysql) ++
[{query, #{type => binary()}}] ++
emqx_connector_mysql:fields(config);
fields(postgresql) ->
authz_common_fields(postgresql)
++ [ {query, #{type => binary()}}]
++ proplists:delete(named_queries, emqx_connector_pgsql:fields(config));
authz_common_fields(postgresql) ++
[{query, #{type => binary()}}] ++
proplists:delete(named_queries, emqx_connector_pgsql:fields(config));
fields(redis_single) ->
authz_redis_common_fields()
++ emqx_connector_redis:fields(single);
authz_redis_common_fields() ++
emqx_connector_redis:fields(single);
fields(redis_sentinel) ->
authz_redis_common_fields()
++ emqx_connector_redis:fields(sentinel);
authz_redis_common_fields() ++
emqx_connector_redis:fields(sentinel);
fields(redis_cluster) ->
authz_redis_common_fields()
++ emqx_connector_redis:fields(cluster);
authz_redis_common_fields() ++
emqx_connector_redis:fields(cluster);
fields(file) ->
authz_common_fields(file)
++ [ { rules, #{ type => binary()
, required => true
, example =>
<<"{allow,{username,\"^dashboard?\"},","subscribe,[\"$SYS/#\"]}.\n",
"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>}}
];
authz_common_fields(file) ++
[
{rules, #{
type => binary(),
required => true,
example =>
<<"{allow,{username,\"^dashboard?\"},", "subscribe,[\"$SYS/#\"]}.\n",
"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
}}
];
fields(position) ->
[ { position
, mk( string()
, #{ desc => <<"Where to place the source">>
, required => true
, in => body})}].
[
{position,
mk(
string(),
#{
desc => <<"Where to place the source">>,
required => true,
in => body
}
)}
].
%%------------------------------------------------------------------------------
%% http type funcs
@ -86,41 +105,52 @@ url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")];
url(required) -> true;
url(_) -> undefined.
headers(type) -> map();
headers(type) ->
map();
headers(converter) ->
fun(Headers) ->
maps:merge(default_headers(), transform_header_name(Headers))
maps:merge(default_headers(), transform_header_name(Headers))
end;
headers(default) -> default_headers();
headers(_) -> undefined.
headers(default) ->
default_headers();
headers(_) ->
undefined.
%% headers
default_headers() ->
maps:put(<<"content-type">>,
<<"application/json">>,
default_headers_no_content_type()).
maps:put(
<<"content-type">>,
<<"application/json">>,
default_headers_no_content_type()
).
default_headers_no_content_type() ->
#{ <<"accept">> => <<"application/json">>
, <<"cache-control">> => <<"no-cache">>
, <<"connection">> => <<"keep-alive">>
, <<"keep-alive">> => <<"timeout=30, max=1000">>
}.
#{
<<"accept">> => <<"application/json">>,
<<"cache-control">> => <<"no-cache">>,
<<"connection">> => <<"keep-alive">>,
<<"keep-alive">> => <<"timeout=30, max=1000">>
}.
transform_header_name(Headers) ->
maps:fold(fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc)
end, #{}, Headers).
maps:fold(
fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc)
end,
#{},
Headers
).
%%------------------------------------------------------------------------------
%% MonogDB type funcs
authz_mongo_common_fields() ->
authz_common_fields(mongodb) ++
[ {collection, fun collection/1}
, {selector, fun selector/1}
].
[
{collection, fun collection/1},
{selector, fun selector/1}
].
collection(type) -> binary();
collection(_) -> undefined.
@ -133,19 +163,24 @@ selector(_) -> undefined.
authz_redis_common_fields() ->
authz_common_fields(redis) ++
[ {cmd, #{ type => binary()
, example => <<"HGETALL mqtt_authz">>}}].
[
{cmd, #{
type => binary(),
example => <<"HGETALL mqtt_authz">>
}}
].
%%------------------------------------------------------------------------------
%% Authz api type funcs
authz_common_fields(Type) when is_atom(Type)->
[ {enable, fun enable/1}
, {type, #{ type => enum([Type])
, default => Type
, in => body
}
}
authz_common_fields(Type) when is_atom(Type) ->
[
{enable, fun enable/1},
{type, #{
type => enum([Type]),
default => Type,
in => body
}}
].
enable(type) -> boolean();
@ -158,20 +193,25 @@ enable(_) -> undefined.
authz_sources_types(Type) ->
case Type of
simple -> [mongodb, redis];
detailed -> [ mongo_single
, mongo_rs
, mongo_sharded
, redis_single
, redis_sentinel
, redis_cluster]
end
++
[ http
, 'built_in_database'
, mysql
, postgresql
, file].
simple ->
[mongodb, redis];
detailed ->
[
mongo_single,
mongo_rs,
mongo_sharded,
redis_single,
redis_sentinel,
redis_cluster
]
end ++
[
http,
'built_in_database',
mysql,
postgresql,
file
].
to_list(A) when is_atom(A) ->
atom_to_list(A);

View File

@ -20,10 +20,11 @@
-import(hoconsc, [mk/1, ref/2]).
-export([ api_spec/0
, paths/0
, schema/1
]).
-export([
api_spec/0,
paths/0,
schema/1
]).
-export([settings/2]).
@ -40,33 +41,42 @@ paths() ->
%%--------------------------------------------------------------------
schema("/authorization/settings") ->
#{ 'operationId' => settings
, get =>
#{ description => <<"Get authorization settings">>
, responses =>
#{200 => ref_authz_schema()}
#{
'operationId' => settings,
get =>
#{
description => <<"Get authorization settings">>,
responses =>
#{200 => ref_authz_schema()}
},
put =>
#{
description => <<"Update authorization settings">>,
'requestBody' => ref_authz_schema(),
responses =>
#{
200 => ref_authz_schema(),
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
}
}
, put =>
#{ description => <<"Update authorization settings">>
, 'requestBody' => ref_authz_schema()
, responses =>
#{ 200 => ref_authz_schema()
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)}
}
}.
}.
ref_authz_schema() ->
proplists:delete(sources, emqx_conf_schema:fields("authorization")).
settings(get, _Params) ->
{200, authorization_settings()};
settings(put, #{body := #{<<"no_match">> := NoMatch,
<<"deny_action">> := DenyAction,
<<"cache">> := Cache}}) ->
settings(put, #{
body := #{
<<"no_match">> := NoMatch,
<<"deny_action">> := DenyAction,
<<"cache">> := Cache
}
}) ->
{ok, _} = emqx_authz_utils:update_config([authorization, no_match], NoMatch),
{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_cache:drain_cache(),
{200, authorization_settings()}.

View File

@ -29,167 +29,226 @@
-define(API_SCHEMA_MODULE, emqx_authz_api_schema).
-export([ get_raw_sources/0
, get_raw_source/1
, source_status/2
, lookup_from_local_node/1
, lookup_from_all_nodes/1
]).
-export([
get_raw_sources/0,
get_raw_source/1,
source_status/2,
lookup_from_local_node/1,
lookup_from_all_nodes/1
]).
-export([ api_spec/0
, paths/0
, schema/1
]).
-export([
api_spec/0,
paths/0,
schema/1
]).
-export([ sources/2
, source/2
, move_source/2
]).
-export([
sources/2,
source/2,
move_source/2
]).
api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
paths() ->
[ "/authorization/sources"
, "/authorization/sources/:type"
, "/authorization/sources/:type/status"
, "/authorization/sources/:type/move"].
[
"/authorization/sources",
"/authorization/sources/:type",
"/authorization/sources/:type/status",
"/authorization/sources/:type/move"
].
%%--------------------------------------------------------------------
%% Schema for each URI
%%--------------------------------------------------------------------
schema("/authorization/sources") ->
#{ 'operationId' => sources
, get =>
#{ description => <<"List all authorization sources">>
, responses =>
#{ 200 => mk( array(hoconsc:union(authz_sources_type_refs()))
, #{desc => <<"Authorization source">>})
}
#{
'operationId' => sources,
get =>
#{
description => <<"List all authorization sources">>,
responses =>
#{
200 => mk(
array(hoconsc:union(authz_sources_type_refs())),
#{desc => <<"Authorization source">>}
)
}
},
post =>
#{
description => <<"Add a new source">>,
'requestBody' => mk(
hoconsc:union(authz_sources_type_refs()),
#{desc => <<"Source config">>}
),
responses =>
#{
204 => <<"Authorization source created successfully">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST],
<<"Bad Request">>
)
}
}
, post =>
#{ description => <<"Add a new source">>
, 'requestBody' => mk( hoconsc:union(authz_sources_type_refs())
, #{desc => <<"Source config">>})
, responses =>
#{ 204 => <<"Authorization source created successfully">>
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST],
<<"Bad Request">>)
}
}
};
};
schema("/authorization/sources/:type") ->
#{ 'operationId' => source
, get =>
#{ description => <<"Get a authorization source">>
, parameters => parameters_field()
, responses =>
#{ 200 => mk( hoconsc:union(authz_sources_type_refs())
, #{desc => <<"Authorization source">>})
, 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
}
#{
'operationId' => source,
get =>
#{
description => <<"Get a authorization source">>,
parameters => parameters_field(),
responses =>
#{
200 => mk(
hoconsc:union(authz_sources_type_refs()),
#{desc => <<"Authorization source">>}
),
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
}
},
put =>
#{
description => <<"Update source">>,
parameters => parameters_field(),
'requestBody' => mk(hoconsc:union(authz_sources_type_refs())),
responses =>
#{
204 => <<"Authorization source updated successfully">>,
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
}
},
delete =>
#{
description => <<"Delete source">>,
parameters => parameters_field(),
responses =>
#{
204 => <<"Deleted successfully">>,
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
}
}
, put =>
#{ description => <<"Update source">>
, parameters => parameters_field()
, 'requestBody' => mk(hoconsc:union(authz_sources_type_refs()))
, responses =>
#{ 204 => <<"Authorization source updated successfully">>
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
}
}
, delete =>
#{ description => <<"Delete source">>
, parameters => parameters_field()
, responses =>
#{ 204 => <<"Deleted successfully">>
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
}
}
};
};
schema("/authorization/sources/:type/status") ->
#{ 'operationId' => source_status
, get =>
#{ description => <<"Get a authorization source">>
, parameters => parameters_field()
, responses =>
#{ 200 => emqx_dashboard_swagger:schema_with_examples(
hoconsc:ref(emqx_authn_schema, "metrics_status_fields"),
status_metrics_example())
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad request">>)
, 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
}
#{
'operationId' => source_status,
get =>
#{
description => <<"Get a authorization source">>,
parameters => parameters_field(),
responses =>
#{
200 => emqx_dashboard_swagger:schema_with_examples(
hoconsc:ref(emqx_authn_schema, "metrics_status_fields"),
status_metrics_example()
),
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad request">>
),
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
}
}
};
};
schema("/authorization/sources/:type/move") ->
#{ 'operationId' => move_source
, post =>
#{ description => <<"Change the order of sources">>
, parameters => parameters_field()
, 'requestBody' =>
emqx_dashboard_swagger:schema_with_examples(
ref(?API_SCHEMA_MODULE, position),
position_example())
, responses =>
#{ 204 => <<"No Content">>
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
, 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
}
#{
'operationId' => move_source,
post =>
#{
description => <<"Change the order of sources">>,
parameters => parameters_field(),
'requestBody' =>
emqx_dashboard_swagger:schema_with_examples(
ref(?API_SCHEMA_MODULE, position),
position_example()
),
responses =>
#{
204 => <<"No Content">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad Request">>
),
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
}
}
}.
}.
%%--------------------------------------------------------------------
%% Operation functions
%%--------------------------------------------------------------------
sources(Method, #{bindings := #{type := Type} = Bindings } = Req)
when is_atom(Type) ->
sources(Method, #{bindings := #{type := Type} = Bindings} = Req) when
is_atom(Type)
->
sources(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
sources(get, _) ->
Sources = lists:foldl(fun (#{<<"type">> := <<"file">>,
<<"enable">> := Enable, <<"path">> := Path}, AccIn) ->
case file:read_file(Path) of
{ok, Rules} ->
lists:append(AccIn, [#{type => file,
enable => Enable,
rules => Rules
}]);
{error, _} ->
lists:append(AccIn, [#{type => file,
enable => Enable,
rules => <<"">>
}])
end;
(Source, AccIn) ->
lists:append(AccIn, [read_certs(Source)])
end, [], get_raw_sources()),
Sources = lists:foldl(
fun
(
#{
<<"type">> := <<"file">>,
<<"enable">> := Enable,
<<"path">> := Path
},
AccIn
) ->
case file:read_file(Path) of
{ok, Rules} ->
lists:append(AccIn, [
#{
type => file,
enable => Enable,
rules => Rules
}
]);
{error, _} ->
lists:append(AccIn, [
#{
type => file,
enable => Enable,
rules => <<"">>
}
])
end;
(Source, AccIn) ->
lists:append(AccIn, [read_certs(Source)])
end,
[],
get_raw_sources()
),
{200, #{sources => Sources}};
sources(post, #{body := #{<<"type">> := <<"file">>} = Body}) ->
create_authz_file(Body);
sources(post, #{body := Body}) ->
update_config(?CMD_PREPEND, Body).
source(Method, #{bindings := #{type := Type} = Bindings } = Req)
when is_atom(Type) ->
source(Method, #{bindings := #{type := Type} = Bindings} = Req) when
is_atom(Type)
->
source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
source(get, #{bindings := #{type := Type}}) ->
case get_raw_source(Type) of
[] -> {404, #{message => <<"Not found ", Type/binary>>}};
[] ->
{404, #{message => <<"Not found ", Type/binary>>}};
[#{<<"type">> := <<"file">>, <<"enable">> := Enable, <<"path">> := Path}] ->
case file:read_file(Path) of
{ok, Rules} ->
{200, #{type => file,
enable => Enable,
rules => Rules
}
};
{200, #{
type => file,
enable => Enable,
rules => Rules
}};
{error, Reason} ->
{500, #{code => <<"INTERNAL_ERROR">>,
message => bin(Reason)}}
{500, #{
code => <<"INTERNAL_ERROR">>,
message => bin(Reason)
}}
end;
[Source] -> {200, read_certs(Source)}
[Source] ->
{200, read_certs(Source)}
end;
source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>} = Body}) ->
update_authz_file(Body);
@ -201,44 +260,61 @@ source(delete, #{bindings := #{type := Type}}) ->
source_status(get, #{bindings := #{type := Type}}) ->
BinType = atom_to_binary(Type, utf8),
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">>}] ->
{400, #{code => <<"BAD_REQUEST">>,
message => <<"Not Support Status">>}};
{400, #{
code => <<"BAD_REQUEST">>,
message => <<"Not Support Status">>
}};
[_] ->
case emqx_authz:lookup(Type) of
#{annotations := #{id := ResourceId }} -> lookup_from_all_nodes(ResourceId);
#{annotations := #{id := ResourceId}} -> lookup_from_all_nodes(ResourceId);
_ -> {400, #{code => <<"BAD_REQUEST">>, message => <<"Resource Disable">>}}
end
end.
move_source(Method, #{bindings := #{type := Type} = Bindings } = Req)
when is_atom(Type) ->
move_source(Method, #{bindings := #{type := Type} = Bindings} = Req) when
is_atom(Type)
->
move_source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) ->
case parse_position(Position) of
case parse_position(Position) of
{ok, NPosition} ->
try emqx_authz:move(Type, NPosition) of
{ok, _} -> {204};
{ok, _} ->
{204};
{error, {not_found_source, _Type}} ->
{404, #{code => <<"NOT_FOUND">>,
message => <<"source ", Type/binary, " not found">>}};
{404, #{
code => <<"NOT_FOUND">>,
message => <<"source ", Type/binary, " not found">>
}};
{error, {emqx_conf_schema, _}} ->
{400, #{code => <<"BAD_REQUEST">>,
message => <<"BAD_SCHEMA">>}};
{400, #{
code => <<"BAD_REQUEST">>,
message => <<"BAD_SCHEMA">>
}};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
message => bin(Reason)}}
{400, #{
code => <<"BAD_REQUEST">>,
message => bin(Reason)
}}
catch
error : {unknown_authz_source_type, Unknown} ->
error:{unknown_authz_source_type, Unknown} ->
NUnknown = bin(Unknown),
{400, #{code => <<"BAD_REQUEST">>,
message => <<"Unknown authz Source Type: ", NUnknown/binary>>}}
{400, #{
code => <<"BAD_REQUEST">>,
message => <<"Unknown authz Source Type: ", NUnknown/binary>>
}}
end;
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
message => bin(Reason)}}
{400, #{
code => <<"BAD_REQUEST">>,
message => bin(Reason)
}}
end.
%%--------------------------------------------------------------------
@ -249,46 +325,53 @@ lookup_from_local_node(ResourceId) ->
NodeId = node(self()),
case emqx_resource:get_instance(ResourceId) of
{error, not_found} -> {error, {NodeId, not_found_resource}};
{ok, _, #{ status := Status, metrics := Metrics }} ->
{ok, {NodeId, Status, Metrics}}
{ok, _, #{status := Status, metrics := Metrics}} -> {ok, {NodeId, Status, Metrics}}
end.
lookup_from_all_nodes(ResourceId) ->
Nodes = mria_mnesia:running_nodes(),
case is_ok(emqx_authz_proto_v1:lookup_from_all_nodes(Nodes, ResourceId)) of
{ok, ResList} ->
{StatusMap, MetricsMap, _} = make_result_map(ResList),
AggregateStatus = aggregate_status(maps:values(StatusMap)),
AggregateMetrics = aggregate_metrics(maps:values(MetricsMap)),
Fun = fun (_, V1) -> restructure_map(V1) 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,
case AggregateStatus of
empty_metrics_and_status -> {400, #{code => <<"BAD_REQUEST">>,
message => <<"Resource Not Support Status">>}};
_ -> {200, #{node_status => HelpFun(StatusMap, status),
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
status => AggregateStatus,
metrics => restructure_map(AggregateMetrics)
}
}
end;
{StatusMap, MetricsMap, _} = make_result_map(ResList),
AggregateStatus = aggregate_status(maps:values(StatusMap)),
AggregateMetrics = aggregate_metrics(maps:values(MetricsMap)),
Fun = fun(_, V1) -> restructure_map(V1) 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,
case AggregateStatus of
empty_metrics_and_status ->
{400, #{
code => <<"BAD_REQUEST">>,
message => <<"Resource Not Support Status">>
}};
_ ->
{200, #{
node_status => HelpFun(StatusMap, status),
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
status => AggregateStatus,
metrics => restructure_map(AggregateMetrics)
}}
end;
{error, ErrL} ->
{500, #{code => <<"INTERNAL_ERROR">>,
message => bin_t(io_lib:format("~p", [ErrL]))}}
{500, #{
code => <<"INTERNAL_ERROR">>,
message => bin_t(io_lib:format("~p", [ErrL]))
}}
end.
aggregate_status([]) -> empty_metrics_and_status;
aggregate_status([]) ->
empty_metrics_and_status;
aggregate_status(AllStatus) ->
Head = fun ([A | _]) -> A end,
Head = fun([A | _]) -> A end,
HeadVal = Head(AllStatus),
AllRes = lists:all(fun (Val) -> Val == HeadVal end, AllStatus),
AllRes = lists:all(fun(Val) -> Val == HeadVal end, AllStatus),
case AllRes of
true -> HeadVal;
false -> inconsistent
end.
aggregate_metrics([]) -> empty_metrics_and_status;
aggregate_metrics([]) ->
empty_metrics_and_status;
aggregate_metrics([HeadMetrics | AllMetrics]) ->
CombinerFun =
fun ComFun(Val1, Val2) ->
@ -297,8 +380,9 @@ aggregate_metrics([HeadMetrics | AllMetrics]) ->
false -> Val1 + Val2
end
end,
Fun = fun (ElemMap, AccMap) ->
emqx_map_lib:merge_with(CombinerFun, ElemMap, AccMap) end,
Fun = fun(ElemMap, AccMap) ->
emqx_map_lib:merge_with(CombinerFun, ElemMap, AccMap)
end,
lists:foldl(Fun, HeadMetrics, AllMetrics).
make_result_map(ResList) ->
@ -306,39 +390,45 @@ make_result_map(ResList) ->
fun(Elem, {StatusMap, MetricsMap, ErrorMap}) ->
case Elem of
{ok, {NodeId, Status, Metrics}} ->
{maps:put(NodeId, Status, StatusMap),
maps:put(NodeId, Metrics, MetricsMap),
ErrorMap
{
maps:put(NodeId, Status, StatusMap),
maps:put(NodeId, Metrics, MetricsMap),
ErrorMap
};
{error, {NodeId, Reason}} ->
{StatusMap,
MetricsMap,
maps:put(NodeId, Reason, ErrorMap)
}
{StatusMap, MetricsMap, maps:put(NodeId, Reason, ErrorMap)}
end
end,
lists:foldl(Fun, {maps:new(), maps:new(), maps:new()}, ResList).
restructure_map(#{counters := #{failed := Failed, matched := Match, success := Succ},
rate := #{matched := #{current := Rate, last5m := Rate5m, max := RateMax}
}
}
) ->
#{matched => Match,
success => Succ,
failed => Failed,
rate => Rate,
rate_last5m => Rate5m,
rate_max => RateMax
};
restructure_map(#{
counters := #{failed := Failed, matched := Match, success := Succ},
rate := #{matched := #{current := Rate, last5m := Rate5m, max := RateMax}}
}) ->
#{
matched => Match,
success => Succ,
failed => Failed,
rate => Rate,
rate_last5m => Rate5m,
rate_max => RateMax
};
restructure_map(Error) ->
Error.
Error.
bin_t(S) when is_list(S) ->
list_to_binary(S).
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]};
ErrL -> {error, ErrL}
end.
@ -352,43 +442,60 @@ get_raw_sources() ->
merge_default_headers(Sources).
merge_default_headers(Sources) ->
lists:map(fun(Source) ->
case maps:find(<<"headers">>, Source) of
{ok, Headers} ->
NewHeaders =
case Source of
#{<<"method">> := <<"get">>} ->
(emqx_authz_schema:headers_no_content_type(converter))(Headers);
#{<<"method">> := <<"post">>} ->
(emqx_authz_schema:headers(converter))(Headers);
_ -> Headers
end,
Source#{<<"headers">> => NewHeaders};
error -> Source
end
end, Sources).
lists:map(
fun(Source) ->
case maps:find(<<"headers">>, Source) of
{ok, Headers} ->
NewHeaders =
case Source of
#{<<"method">> := <<"get">>} ->
(emqx_authz_schema:headers_no_content_type(converter))(Headers);
#{<<"method">> := <<"post">>} ->
(emqx_authz_schema:headers(converter))(Headers);
_ ->
Headers
end,
Source#{<<"headers">> => NewHeaders};
error ->
Source
end
end,
Sources
).
get_raw_source(Type) ->
lists:filter(fun (#{<<"type">> := T}) ->
T =:= Type
end, get_raw_sources()).
lists:filter(
fun(#{<<"type">> := T}) ->
T =:= Type
end,
get_raw_sources()
).
update_config(Cmd, Sources) ->
case emqx_authz:update(Cmd, Sources) of
{ok, _} -> {204};
{ok, _} ->
{204};
{error, {pre_config_update, emqx_authz, Reason}} ->
{400, #{code => <<"BAD_REQUEST">>,
message => bin(Reason)}};
{400, #{
code => <<"BAD_REQUEST">>,
message => bin(Reason)
}};
{error, {post_config_update, emqx_authz, Reason}} ->
{400, #{code => <<"BAD_REQUEST">>,
message => bin(Reason)}};
{400, #{
code => <<"BAD_REQUEST">>,
message => bin(Reason)
}};
%% TODO: The `Reason` may cann't be trans to json term. (i.e. ecpool start failed)
{error, {emqx_conf_schema, _}} ->
{400, #{code => <<"BAD_REQUEST">>,
message => <<"BAD_SCHEMA">>}};
{400, #{
code => <<"BAD_REQUEST">>,
message => <<"BAD_SCHEMA">>
}};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
message => bin(Reason)}}
{400, #{
code => <<"BAD_REQUEST">>,
message => bin(Reason)
}}
end.
read_certs(#{<<"ssl">> := SSL} = Source) ->
@ -399,12 +506,16 @@ read_certs(#{<<"ssl">> := SSL} = Source) ->
{ok, NewSSL} ->
Source#{<<"ssl">> => NewSSL}
end;
read_certs(Source) -> Source.
read_certs(Source) ->
Source.
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">>) ->
@ -423,50 +534,68 @@ parse_position(_) ->
{error, <<"Invalid parameter. Unknow position">>}.
position_example() ->
#{ front =>
#{ summary => <<"front example">>
, value => #{<<"position">> => <<"front">>}}
, rear =>
#{ summary => <<"rear example">>
, value => #{<<"position">> => <<"rear">>}}
, relative_before =>
#{ summary => <<"relative example">>
, value => #{<<"position">> => <<"before:file">>}}
, relative_after =>
#{ summary => <<"relative example">>
, value => #{<<"position">> => <<"after:file">>}}
}.
#{
front =>
#{
summary => <<"front example">>,
value => #{<<"position">> => <<"front">>}
},
rear =>
#{
summary => <<"rear example">>,
value => #{<<"position">> => <<"rear">>}
},
relative_before =>
#{
summary => <<"relative example">>,
value => #{<<"position">> => <<"before:file">>}
},
relative_after =>
#{
summary => <<"relative example">>,
value => #{<<"position">> => <<"after:file">>}
}
}.
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])).
status_metrics_example() ->
#{ metrics => #{ matched => 0,
success => 0,
failed => 0,
rate => 0.0,
rate_last5m => 0.0,
rate_max => 0.0
},
node_metrics => [ #{node => node(),
metrics => #{ matched => 0,
success => 0,
failed => 0,
rate => 0.0,
rate_last5m => 0.0,
rate_max => 0.0
}
}
],
status => connected,
node_status => [ #{node => node(),
status => connected
}
]
}.
#{
metrics => #{
matched => 0,
success => 0,
failed => 0,
rate => 0.0,
rate_last5m => 0.0,
rate_max => 0.0
},
node_metrics => [
#{
node => node(),
metrics => #{
matched => 0,
success => 0,
failed => 0,
rate => 0.0,
rate_last5m => 0.0,
rate_max => 0.0
}
}
],
status => connected,
node_status => [
#{
node => node(),
status => connected
}
]
}.
create_authz_file(Body) ->
do_update_authz_file(?CMD_PREPEND, Body).
@ -476,4 +605,4 @@ update_authz_file(Body) ->
do_update_authz_file(Cmd, Body) ->
%% API update will placed in `authz` subdirectory inside EMQX's `data_dir`
update_config(Cmd, Body).
update_config(Cmd, Body).

View File

@ -27,28 +27,32 @@
-endif.
%% APIs
-export([ description/0
, init/1
, destroy/1
, authorize/4
]).
-export([
description/0,
init/1,
destroy/1,
authorize/4
]).
description() ->
"AuthZ with static rules".
init(#{path := Path} = Source) ->
Rules = case file:consult(Path) of
{ok, Terms} ->
[emqx_authz_rule:compile(Term) || Term <- Terms];
{error, Reason} when is_atom(Reason) ->
?SLOG(alert, #{msg => failed_to_read_acl_file,
path => Path,
explain => emqx_misc:explain_posix(Reason)}),
throw(failed_to_read_acl_file);
{error, Reason} ->
?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),
throw(bad_acl_file_content)
end,
Rules =
case file:consult(Path) of
{ok, Terms} ->
[emqx_authz_rule:compile(Term) || Term <- Terms];
{error, Reason} when is_atom(Reason) ->
?SLOG(alert, #{
msg => failed_to_read_acl_file,
path => Path,
explain => emqx_misc:explain_posix(Reason)
}),
throw(failed_to_read_acl_file);
{error, Reason} ->
?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),
throw(bad_acl_file_content)
end,
Source#{annotations => #{rules => Rules}}.
destroy(_Source) -> ok.

View File

@ -24,25 +24,28 @@
-behaviour(emqx_authz).
%% AuthZ Callbacks
-export([ description/0
, init/1
, destroy/1
, authorize/4
, parse_url/1
]).
-export([
description/0,
init/1,
destroy/1,
authorize/4,
parse_url/1
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
-define(PLACEHOLDERS, [?PH_USERNAME,
?PH_CLIENTID,
?PH_PEERHOST,
?PH_PROTONAME,
?PH_MOUNTPOINT,
?PH_TOPIC,
?PH_ACTION]).
-define(PLACEHOLDERS, [
?PH_USERNAME,
?PH_CLIENTID,
?PH_PEERHOST,
?PH_PROTONAME,
?PH_MOUNTPOINT,
?PH_TOPIC,
?PH_ACTION
]).
description() ->
"AuthZ with http".
@ -57,14 +60,17 @@ init(Config) ->
destroy(#{annotations := #{id := Id}}) ->
ok = emqx_resource:remove_local(Id).
authorize( Client
, PubSub
, Topic
, #{ type := http
, annotations := #{id := ResourceID}
, method := Method
, request_timeout := RequestTimeout
} = Config) ->
authorize(
Client,
PubSub,
Topic,
#{
type := http,
annotations := #{id := ResourceID},
method := Method,
request_timeout := RequestTimeout
} = Config
) ->
Request = generate_request(PubSub, Topic, Client, Config),
case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of
{ok, 200, _Headers} ->
@ -78,38 +84,47 @@ authorize( Client
{ok, _Status, _Headers, _Body} ->
nomatch;
{error, Reason} ->
?SLOG(error, #{msg => "http_server_query_failed",
resource => ResourceID,
reason => Reason}),
?SLOG(error, #{
msg => "http_server_query_failed",
resource => ResourceID,
reason => Reason
}),
ignore
end.
parse_config(#{ url := URL
, method := Method
, headers := Headers
, request_timeout := ReqTimeout
} = Conf) ->
parse_config(
#{
url := URL,
method := Method,
headers := Headers,
request_timeout := ReqTimeout
} = Conf
) ->
{BaseURLWithPath, Query} = parse_fullpath(URL),
BaseURLMap = parse_url(BaseURLWithPath),
Conf#{ method => Method
, base_url => maps:remove(query, BaseURLMap)
, base_query_template => emqx_authz_utils:parse_deep(
cow_qs:parse_qs(bin(Query)),
?PLACEHOLDERS)
, body_template => emqx_authz_utils:parse_deep(
maps:to_list(maps:get(body, Conf, #{})),
?PLACEHOLDERS)
, headers => Headers
, request_timeout => ReqTimeout
%% pool_type default value `random`
, pool_type => random
}.
Conf#{
method => Method,
base_url => maps:remove(query, BaseURLMap),
base_query_template => emqx_authz_utils:parse_deep(
cow_qs:parse_qs(bin(Query)),
?PLACEHOLDERS
),
body_template => emqx_authz_utils:parse_deep(
maps:to_list(maps:get(body, Conf, #{})),
?PLACEHOLDERS
),
headers => Headers,
request_timeout => ReqTimeout,
%% pool_type default value `random`
pool_type => random
}.
parse_fullpath(RawURL) ->
cow_http:parse_fullpath(bin(RawURL)).
parse_url(URL)
when URL =:= undefined ->
parse_url(URL) when
URL =:= undefined
->
#{};
parse_url(URL) ->
{ok, URIMap} = emqx_http_lib:uri_parse(URL),
@ -120,28 +135,31 @@ parse_url(URL) ->
URIMap
end.
generate_request( PubSub
, Topic
, Client
, #{ method := Method
, base_url := #{path := Path}
, base_query_template := BaseQueryTemplate
, headers := Headers
, body_template := BodyTemplate
}) ->
generate_request(
PubSub,
Topic,
Client,
#{
method := Method,
base_url := #{path := Path},
base_query_template := BaseQueryTemplate,
headers := Headers,
body_template := BodyTemplate
}
) ->
Values = client_vars(Client, PubSub, Topic),
Body = emqx_authz_utils:render_deep(BodyTemplate, Values),
NBaseQuery = emqx_authz_utils:render_deep(BaseQueryTemplate, Values),
case Method of
get ->
get ->
NPath = append_query(Path, NBaseQuery ++ Body),
{NPath, Headers};
_ ->
NPath = append_query(Path, NBaseQuery),
NBody = serialize_body(
proplists:get_value(<<"Accept">>, Headers, <<"application/json">>),
Body
),
proplists:get_value(<<"Accept">>, Headers, <<"application/json">>),
Body
),
{NPath, Headers, NBody}
end.
@ -161,9 +179,13 @@ query_string([], Acc) ->
<<>>
end;
query_string([{K, V} | More], Acc) ->
query_string( More
, [ ["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)]
| Acc]).
query_string(
More,
[
["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)]
| Acc
]
).
serialize_body(<<"application/json">>, Body) ->
jsx:encode(Body);
@ -172,9 +194,9 @@ serialize_body(<<"application/x-www-form-urlencoded">>, Body) ->
client_vars(Client, PubSub, Topic) ->
Client#{
action => PubSub,
topic => Topic
}.
action => PubSub,
topic => Topic
}.
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(B) when is_binary(B) -> B;

View File

@ -29,38 +29,40 @@
-define(ACL_TABLE_USERNAME, 1).
-define(ACL_TABLE_CLIENTID, 2).
-type(username() :: {username, binary()}).
-type(clientid() :: {clientid, binary()}).
-type(who() :: username() | clientid() | all).
-type username() :: {username, binary()}.
-type clientid() :: {clientid, binary()}.
-type who() :: username() | clientid() | all.
-type(rule() :: {emqx_authz_rule:permission(), emqx_authz_rule:action(), emqx_topic:topic()}).
-type(rules() :: [rule()]).
-type rule() :: {emqx_authz_rule:permission(), emqx_authz_rule:action(), emqx_topic:topic()}.
-type rules() :: [rule()].
-record(emqx_acl, {
who :: ?ACL_TABLE_ALL | {?ACL_TABLE_USERNAME, binary()} | {?ACL_TABLE_CLIENTID, binary()},
rules :: rules()
}).
who :: ?ACL_TABLE_ALL | {?ACL_TABLE_USERNAME, binary()} | {?ACL_TABLE_CLIENTID, binary()},
rules :: rules()
}).
-behaviour(emqx_authz).
%% AuthZ Callbacks
-export([ description/0
, init/1
, destroy/1
, authorize/4
]).
-export([
description/0,
init/1,
destroy/1,
authorize/4
]).
%% Management API
-export([ mnesia/1
, init_tables/0
, store_rules/2
, purge_rules/0
, get_rules/1
, delete_rules/1
, list_clientid_rules/0
, list_username_rules/0
, record_count/0
]).
-export([
mnesia/1,
init_tables/0,
store_rules/2,
purge_rules/0,
get_rules/1,
delete_rules/1,
list_clientid_rules/0,
list_username_rules/0,
record_count/0
]).
-ifdef(TEST).
-compile(export_all).
@ -69,14 +71,15 @@
-boot_mnesia({mnesia, [boot]}).
-spec(mnesia(boot | copy) -> ok).
-spec mnesia(boot | copy) -> ok.
mnesia(boot) ->
ok = mria:create_table(?ACL_TABLE, [
{type, ordered_set},
{rlog_shard, ?ACL_SHARDED},
{storage, disc_copies},
{attributes, record_info(fields, ?ACL_TABLE)},
{storage_properties, [{ets, [{read_concurrency, true}]}]}]).
{type, ordered_set},
{rlog_shard, ?ACL_SHARDED},
{storage, disc_copies},
{attributes, record_info(fields, ?ACL_TABLE)},
{storage_properties, [{ets, [{read_concurrency, true}]}]}
]).
%%--------------------------------------------------------------------
%% emqx_authz callbacks
@ -89,19 +92,25 @@ init(Source) -> Source.
destroy(_Source) -> ok.
authorize(#{username := Username,
clientid := Clientid
} = Client, PubSub, 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
end
++ case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}) of
authorize(
#{
username := Username,
clientid := Clientid
} = Client,
PubSub,
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
end ++
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}) of
[] -> [];
[#emqx_acl{rules = Rules1}] when is_list(Rules1) -> Rules1
end
++ case mnesia:dirty_read(?ACL_TABLE, ?ACL_TABLE_ALL) of
end ++
case mnesia:dirty_read(?ACL_TABLE, ?ACL_TABLE_ALL) of
[] -> [];
[#emqx_acl{rules = Rules2}] when is_list(Rules2) -> Rules2
end,
@ -112,12 +121,12 @@ authorize(#{username := Username,
%%--------------------------------------------------------------------
%% Init
-spec(init_tables() -> ok).
-spec init_tables() -> ok.
init_tables() ->
ok = mria_rlog:wait_for_shards([?ACL_SHARDED], infinity).
%% @doc Update authz rules
-spec(store_rules(who(), rules()) -> ok).
-spec store_rules(who(), rules()) -> ok.
store_rules({username, Username}, Rules) ->
Record = #emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = normalize_rules(Rules)},
mria:dirty_write(Record);
@ -129,16 +138,17 @@ store_rules(all, Rules) ->
mria:dirty_write(Record).
%% @doc Clean all authz rules for (username & clientid & all)
-spec(purge_rules() -> ok).
-spec purge_rules() -> ok.
purge_rules() ->
ok = lists:foreach(
fun(Key) ->
ok = mria:dirty_delete(?ACL_TABLE, Key)
end,
mnesia:dirty_all_keys(?ACL_TABLE)).
fun(Key) ->
ok = mria:dirty_delete(?ACL_TABLE, Key)
end,
mnesia:dirty_all_keys(?ACL_TABLE)
).
%% @doc Get one record
-spec(get_rules(who()) -> {ok, rules()} | not_found).
-spec get_rules(who()) -> {ok, rules()} | not_found.
get_rules({username, Username}) ->
do_get_rules({?ACL_TABLE_USERNAME, Username});
get_rules({clientid, Clientid}) ->
@ -147,7 +157,7 @@ get_rules(all) ->
do_get_rules(?ACL_TABLE_ALL).
%% @doc Delete one record
-spec(delete_rules(who()) -> ok).
-spec delete_rules(who()) -> ok.
delete_rules({username, Username}) ->
mria:dirty_delete(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username});
delete_rules({clientid, Clientid}) ->
@ -155,21 +165,23 @@ delete_rules({clientid, Clientid}) ->
delete_rules(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() ->
ets:fun2ms(
fun(#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}) ->
[{username, Username}, {rules, Rules}]
end).
fun(#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}) ->
[{username, Username}, {rules, Rules}]
end
).
-spec(list_clientid_rules() -> ets:match_spec()).
-spec list_clientid_rules() -> ets:match_spec().
list_clientid_rules() ->
ets:fun2ms(
fun(#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}) ->
[{clientid, Clientid}, {rules, Rules}]
end).
fun(#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}) ->
[{clientid, Clientid}, {rules, Rules}]
end
).
-spec(record_count() -> non_neg_integer()).
-spec record_count() -> non_neg_integer().
record_count() ->
mnesia:table_info(?ACL_TABLE, size).
@ -181,9 +193,7 @@ normalize_rules(Rules) ->
lists:map(fun normalize_rule/1, Rules).
normalize_rule({Permission, Action, Topic}) ->
{normalize_permission(Permission),
normalize_action(Action),
normalize_topic(Topic)};
{normalize_permission(Permission), normalize_action(Action), normalize_topic(Topic)};
normalize_rule(Rule) ->
error({invalid_rule, Rule}).
@ -206,8 +216,9 @@ do_get_rules(Key) ->
[] -> not_found
end.
do_authorize(_Client, _PubSub, _Topic, []) -> nomatch;
do_authorize(Client, PubSub, Topic, [ {Permission, Action, TopicFilter} | Tail]) ->
do_authorize(_Client, _PubSub, _Topic, []) ->
nomatch;
do_authorize(Client, PubSub, Topic, [{Permission, Action, TopicFilter} | Tail]) ->
Rule = emqx_authz_rule:compile({Permission, all, Action, [TopicFilter]}),
case emqx_authz_rule:match(Client, PubSub, Topic, Rule) of
{matched, Permission} -> {matched, Permission};

View File

@ -24,62 +24,83 @@
-behaviour(emqx_authz).
%% AuthZ Callbacks
-export([ description/0
, init/1
, destroy/1
, authorize/4
]).
-export([
description/0,
init/1,
destroy/1,
authorize/4
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
-define(PLACEHOLDERS, [?PH_USERNAME,
?PH_CLIENTID,
?PH_PEERHOST]).
-define(PLACEHOLDERS, [
?PH_USERNAME,
?PH_CLIENTID,
?PH_PEERHOST
]).
description() ->
"AuthZ with MongoDB".
init(#{selector := Selector} = Source) ->
case emqx_authz_utils:create_resource(emqx_connector_mongo, Source) of
{error, Reason} -> error({load_config_error, Reason});
{ok, Id} -> Source#{annotations => #{id => Id},
selector_template => emqx_authz_utils:parse_deep(
Selector,
?PLACEHOLDERS)}
{error, Reason} ->
error({load_config_error, Reason});
{ok, Id} ->
Source#{
annotations => #{id => Id},
selector_template => emqx_authz_utils:parse_deep(
Selector,
?PLACEHOLDERS
)
}
end.
destroy(#{annotations := #{id := Id}}) ->
ok = emqx_resource:remove_local(Id).
authorize(Client, PubSub, Topic,
#{collection := Collection,
selector_template := SelectorTemplate,
annotations := #{id := ResourceID}
}) ->
authorize(
Client,
PubSub,
Topic,
#{
collection := Collection,
selector_template := SelectorTemplate,
annotations := #{id := ResourceID}
}
) ->
RenderedSelector = emqx_authz_utils:render_deep(SelectorTemplate, Client),
Result = try
emqx_resource:query(ResourceID, {find, Collection, RenderedSelector, #{}})
catch
error:Error -> {error, Error}
end,
Result =
try
emqx_resource:query(ResourceID, {find, Collection, RenderedSelector, #{}})
catch
error:Error -> {error, Error}
end,
case Result of
{error, Reason} ->
?SLOG(error, #{msg => "query_mongo_error",
reason => Reason,
collection => Collection,
selector => RenderedSelector,
resource_id => ResourceID}),
?SLOG(error, #{
msg => "query_mongo_error",
reason => Reason,
collection => Collection,
selector => RenderedSelector,
resource_id => ResourceID
}),
nomatch;
[] ->
nomatch;
[] -> nomatch;
Rows ->
Rules = [ emqx_authz_rule:compile({Permission, all, Action, Topics})
|| #{<<"topics">> := Topics,
<<"permission">> := Permission,
<<"action">> := Action} <- Rows],
Rules = [
emqx_authz_rule:compile({Permission, all, Action, Topics})
|| #{
<<"topics">> := Topics,
<<"permission">> := Permission,
<<"action">> := Action
} <- Rows
],
do_authorize(Client, PubSub, Topic, Rules)
end.

View File

@ -24,66 +24,89 @@
-behaviour(emqx_authz).
%% AuthZ Callbacks
-export([ description/0
, init/1
, destroy/1
, authorize/4
]).
-export([
description/0,
init/1,
destroy/1,
authorize/4
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
-define(PLACEHOLDERS, [?PH_USERNAME,
?PH_CLIENTID,
?PH_PEERHOST,
?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT]).
-define(PLACEHOLDERS, [
?PH_USERNAME,
?PH_CLIENTID,
?PH_PEERHOST,
?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT
]).
description() ->
"AuthZ with Mysql".
init(#{query := SQL} = Source) ->
case emqx_authz_utils:create_resource(emqx_connector_mysql, Source) of
{error, Reason} -> error({load_config_error, Reason});
{ok, Id} -> Source#{annotations =>
#{id => Id,
query => emqx_authz_utils:parse_sql(
SQL,
'?',
?PLACEHOLDERS)}}
{error, Reason} ->
error({load_config_error, Reason});
{ok, Id} ->
Source#{
annotations =>
#{
id => Id,
query => emqx_authz_utils:parse_sql(
SQL,
'?',
?PLACEHOLDERS
)
}
}
end.
destroy(#{annotations := #{id := Id}}) ->
ok = emqx_resource:remove_local(Id).
authorize(Client, PubSub, Topic,
#{annotations := #{id := ResourceID,
query := {Query, Params}
}
}) ->
authorize(
Client,
PubSub,
Topic,
#{
annotations := #{
id := ResourceID,
query := {Query, Params}
}
}
) ->
RenderParams = emqx_authz_utils:render_sql_params(Params, Client),
case emqx_resource:query(ResourceID, {sql, Query, RenderParams}) of
{ok, _Columns, []} -> nomatch;
{ok, _Columns, []} ->
nomatch;
{ok, Columns, Rows} ->
do_authorize(Client, PubSub, Topic, Columns, Rows);
{error, Reason} ->
?SLOG(error, #{ msg => "query_mysql_error"
, reason => Reason
, query => Query
, params => RenderParams
, resource_id => ResourceID}),
?SLOG(error, #{
msg => "query_mysql_error",
reason => Reason,
query => Query,
params => RenderParams,
resource_id => ResourceID
}),
nomatch
end.
do_authorize(_Client, _PubSub, _Topic, _Columns, []) ->
nomatch;
do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) ->
case emqx_authz_rule:match(Client, PubSub, Topic,
emqx_authz_rule:compile(format_result(Columns, Row))
) of
case
emqx_authz_rule:match(
Client,
PubSub,
Topic,
emqx_authz_rule:compile(format_result(Columns, Row))
)
of
{matched, Permission} -> {matched, Permission};
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail)
end.
@ -97,5 +120,5 @@ format_result(Columns, Row) ->
index(Elem, List) ->
index(Elem, List, 1).
index(_Elem, [], _Index) -> {error, not_found};
index(Elem, [ Elem | _List], Index) -> Index;
index(Elem, [ _ | List], Index) -> index(Elem, List, Index + 1).
index(Elem, [Elem | _List], Index) -> Index;
index(Elem, [_ | List], Index) -> index(Elem, List, Index + 1).

View File

@ -24,42 +24,53 @@
-behaviour(emqx_authz).
%% AuthZ Callbacks
-export([ description/0
, init/1
, destroy/1
, authorize/4
]).
-export([
description/0,
init/1,
destroy/1,
authorize/4
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
-define(PLACEHOLDERS, [?PH_USERNAME,
?PH_CLIENTID,
?PH_PEERHOST,
?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT]).
-define(PLACEHOLDERS, [
?PH_USERNAME,
?PH_CLIENTID,
?PH_PEERHOST,
?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT
]).
description() ->
"AuthZ with PostgreSQL".
init(#{query := SQL0} = Source) ->
{SQL, PlaceHolders} = emqx_authz_utils:parse_sql(
SQL0,
'$n',
?PLACEHOLDERS),
SQL0,
'$n',
?PLACEHOLDERS
),
ResourceID = emqx_authz_utils:make_resource_id(emqx_connector_pgsql),
case emqx_resource:create_local(
case
emqx_resource:create_local(
ResourceID,
?RESOURCE_GROUP,
emqx_connector_pgsql,
Source#{named_queries => #{ResourceID => SQL}},
#{}) of
#{}
)
of
{ok, _} ->
Source#{annotations =>
#{id => ResourceID,
placeholders => PlaceHolders}};
Source#{
annotations =>
#{
id => ResourceID,
placeholders => PlaceHolders
}
};
{error, Reason} ->
error({load_config_error, Reason})
end.
@ -67,30 +78,44 @@ init(#{query := SQL0} = Source) ->
destroy(#{annotations := #{id := Id}}) ->
ok = emqx_resource:remove_local(Id).
authorize(Client, PubSub, Topic,
#{annotations := #{id := ResourceID,
placeholders := Placeholders
}
}) ->
authorize(
Client,
PubSub,
Topic,
#{
annotations := #{
id := ResourceID,
placeholders := Placeholders
}
}
) ->
RenderedParams = emqx_authz_utils:render_sql_params(Placeholders, Client),
case emqx_resource:query(ResourceID, {prepared_query, ResourceID, RenderedParams}) of
{ok, _Columns, []} -> nomatch;
{ok, _Columns, []} ->
nomatch;
{ok, Columns, Rows} ->
do_authorize(Client, PubSub, Topic, Columns, Rows);
{error, Reason} ->
?SLOG(error, #{ msg => "query_postgresql_error"
, reason => Reason
, params => RenderedParams
, resource_id => ResourceID}),
?SLOG(error, #{
msg => "query_postgresql_error",
reason => Reason,
params => RenderedParams,
resource_id => ResourceID
}),
nomatch
end.
do_authorize(_Client, _PubSub, _Topic, _Columns, []) ->
nomatch;
do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) ->
case emqx_authz_rule:match(Client, PubSub, Topic,
emqx_authz_rule:compile(format_result(Columns, Row))
) of
case
emqx_authz_rule:match(
Client,
PubSub,
Topic,
emqx_authz_rule:compile(format_result(Columns, Row))
)
of
{matched, Permission} -> {matched, Permission};
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail)
end.
@ -104,6 +129,9 @@ format_result(Columns, Row) ->
index(Key, N, TupleList) when is_integer(N) ->
Tuple = lists:keyfind(Key, N, TupleList),
index(Tuple, TupleList, 1);
index(_Tuple, [], _Index) -> {error, not_found};
index(Tuple, [Tuple | _TupleList], Index) -> Index;
index(Tuple, [_ | TupleList], Index) -> index(Tuple, TupleList, Index + 1).
index(_Tuple, [], _Index) ->
{error, not_found};
index(Tuple, [Tuple | _TupleList], Index) ->
Index;
index(Tuple, [_ | TupleList], Index) ->
index(Tuple, TupleList, Index + 1).

View File

@ -24,22 +24,25 @@
-behaviour(emqx_authz).
%% AuthZ Callbacks
-export([ description/0
, init/1
, destroy/1
, authorize/4
]).
-export([
description/0,
init/1,
destroy/1,
authorize/4
]).
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
-define(PLACEHOLDERS, [?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT,
?PH_PEERHOST,
?PH_CLIENTID,
?PH_USERNAME]).
-define(PLACEHOLDERS, [
?PH_CERT_CN_NAME,
?PH_CERT_SUBJECT,
?PH_PEERHOST,
?PH_CLIENTID,
?PH_USERNAME
]).
description() ->
"AuthZ with Redis".
@ -48,38 +51,54 @@ init(#{cmd := CmdStr} = Source) ->
Cmd = tokens(CmdStr),
CmdTemplate = emqx_authz_utils:parse_deep(Cmd, ?PLACEHOLDERS),
case emqx_authz_utils:create_resource(emqx_connector_redis, Source) of
{error, Reason} -> error({load_config_error, Reason});
{ok, Id} -> Source#{annotations => #{id => Id},
cmd_template => CmdTemplate}
{error, Reason} ->
error({load_config_error, Reason});
{ok, Id} ->
Source#{
annotations => #{id => Id},
cmd_template => CmdTemplate
}
end.
destroy(#{annotations := #{id := Id}}) ->
ok = emqx_resource:remove_local(Id).
authorize(Client, PubSub, Topic,
#{cmd_template := CmdTemplate,
annotations := #{id := ResourceID}
}) ->
authorize(
Client,
PubSub,
Topic,
#{
cmd_template := CmdTemplate,
annotations := #{id := ResourceID}
}
) ->
Cmd = emqx_authz_utils:render_deep(CmdTemplate, Client),
case emqx_resource:query(ResourceID, {cmd, Cmd}) of
{ok, []} -> nomatch;
{ok, []} ->
nomatch;
{ok, Rows} ->
do_authorize(Client, PubSub, Topic, Rows);
{error, Reason} ->
?SLOG(error, #{ msg => "query_redis_error"
, reason => Reason
, cmd => Cmd
, resource_id => ResourceID}),
?SLOG(error, #{
msg => "query_redis_error",
reason => Reason,
cmd => Cmd,
resource_id => ResourceID
}),
nomatch
end.
do_authorize(_Client, _PubSub, _Topic, []) ->
nomatch;
do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) ->
case emqx_authz_rule:match(
Client, PubSub, Topic,
emqx_authz_rule:compile({allow, all, Action, [TopicFilter]})
) of
case
emqx_authz_rule:match(
Client,
PubSub,
Topic,
emqx_authz_rule:compile({allow, all, Action, [TopicFilter]})
)
of
{matched, Permission} -> {matched, Permission};
nomatch -> do_authorize(Client, PubSub, Topic, Tail)
end.

View File

@ -26,50 +26,66 @@
-endif.
%% APIs
-export([ match/4
, matches/4
, compile/1
]).
-export([
match/4,
matches/4,
compile/1
]).
-type(ipaddress() :: {ipaddr, esockd_cidr:cidr_string()} |
{ipaddrs, list(esockd_cidr:cidr_string())}).
-type ipaddress() ::
{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() |
{'and', [ipaddress() | username() | clientid()]} |
{'or', [ipaddress() | username() | clientid()]} |
all).
-type who() ::
ipaddress()
| username()
| clientid()
| {'and', [ipaddress() | username() | clientid()]}
| {'or', [ipaddress() | username() | clientid()]}
| all.
-type(action() :: subscribe | publish | all).
-type(permission() :: allow | deny).
-type action() :: subscribe | publish | all.
-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
, permission/0
]).
-export_type([
action/0,
permission/0
]).
compile({Permission, all})
when ?ALLOW_DENY(Permission) -> {Permission, all, all, [compile_topic(<<"#">>)]};
compile({Permission, Who, Action, TopicFilters})
when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) ->
{ atom(Permission), compile_who(Who), atom(Action)
, [compile_topic(Topic) || Topic <- TopicFilters]}.
compile({Permission, all}) when
?ALLOW_DENY(Permission)
->
{Permission, all, all, [compile_topic(<<"#">>)]};
compile({Permission, Who, Action, TopicFilters}) when
?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({user, Username}) -> compile_who({username, Username});
compile_who(all) ->
all;
compile_who({user, Username}) ->
compile_who({username, Username});
compile_who({username, {re, Username}}) ->
{ok, MP} = re:compile(bin(Username)),
{username, MP};
compile_who({username, Username}) -> {username, {eq, bin(Username)}};
compile_who({client, Clientid}) -> compile_who({clientid, Clientid});
compile_who({username, Username}) ->
{username, {eq, bin(Username)}};
compile_who({client, Clientid}) ->
compile_who({clientid, Clientid});
compile_who({clientid, {re, Clientid}}) ->
{ok, MP} = re:compile(bin(Clientid)),
{clientid, MP};
compile_who({clientid, Clientid}) -> {clientid, {eq, bin(Clientid)}};
compile_who({clientid, Clientid}) ->
{clientid, {eq, bin(Clientid)}};
compile_who({ipaddr, CIDR}) ->
{ipaddr, esockd_cidr:parse(CIDR, true)};
compile_who({ipaddrs, CIDRs}) ->
@ -86,7 +102,7 @@ compile_topic({eq, Topic}) ->
compile_topic(Topic) ->
Words = emqx_topic:words(bin(Topic)),
case pattern(Words) of
true -> {pattern, Words};
true -> {pattern, Words};
false -> Words
end.
@ -94,7 +110,8 @@ pattern(Words) ->
lists:member(?PH_USERNAME, Words) orelse lists:member(?PH_CLIENTID, Words).
atom(B) when is_binary(B) ->
try binary_to_existing_atom(B, utf8)
try
binary_to_existing_atom(B, utf8)
catch
_E:_S -> binary_to_atom(B)
end;
@ -105,21 +122,24 @@ bin(L) when is_list(L) ->
bin(B) when is_binary(B) ->
B.
-spec(matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()])
-> {matched, allow} | {matched, deny} | nomatch).
matches(_Client, _PubSub, _Topic, []) -> nomatch;
-spec matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()]) ->
{matched, allow} | {matched, deny} | nomatch.
matches(_Client, _PubSub, _Topic, []) ->
nomatch;
matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) ->
case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of
nomatch -> matches(Client, PubSub, Topic, Tail);
Matched -> Matched
end.
-spec(match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule())
-> {matched, allow} | {matched, deny} | nomatch).
-spec match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule()) ->
{matched, allow} | {matched, deny} | nomatch.
match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) ->
case match_action(PubSub, Action) andalso
match_who(Client, Who) andalso
match_topics(Client, Topic, TopicFilters) of
case
match_action(PubSub, Action) andalso
match_who(Client, Who) andalso
match_topics(Client, Topic, TopicFilters)
of
true -> {matched, Permission};
_ -> nomatch
end.
@ -129,16 +149,19 @@ match_action(subscribe, subscribe) -> true;
match_action(_, all) -> true;
match_action(_, _) -> false.
match_who(_, all) -> true;
match_who(_, all) ->
true;
match_who(#{username := undefined}, {username, _}) ->
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}) ->
case re:run(Username, MP) of
{match, _} -> true;
_ -> false
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}) ->
case re:run(Clientid, MP) of
{match, _} -> true;
@ -151,28 +174,40 @@ match_who(#{peerhost := IpAddress}, {ipaddr, CIDR}) ->
match_who(#{peerhost := undefined}, {ipaddrs, _CIDR}) ->
false;
match_who(#{peerhost := IpAddress}, {ipaddrs, CIDRs}) ->
lists:any(fun(CIDR) ->
esockd_cidr:match(IpAddress, CIDR)
end, CIDRs);
lists:any(
fun(CIDR) ->
esockd_cidr:match(IpAddress, CIDR)
end,
CIDRs
);
match_who(ClientInfo, {'and', Principals}) when is_list(Principals) ->
lists:foldl(fun(Principal, Permission) ->
match_who(ClientInfo, Principal) andalso Permission
end, true, Principals);
lists:foldl(
fun(Principal, Permission) ->
match_who(ClientInfo, Principal) andalso Permission
end,
true,
Principals
);
match_who(ClientInfo, {'or', Principals}) when is_list(Principals) ->
lists:foldl(fun(Principal, Permission) ->
match_who(ClientInfo, Principal) orelse Permission
end, false, Principals);
match_who(_, _) -> false.
lists:foldl(
fun(Principal, Permission) ->
match_who(ClientInfo, Principal) orelse Permission
end,
false,
Principals
);
match_who(_, _) ->
false.
match_topics(_ClientInfo, _Topic, []) ->
false;
match_topics(ClientInfo, Topic, [{pattern, PatternFilter} | Filters]) ->
TopicFilter = feed_var(ClientInfo, PatternFilter),
match_topic(emqx_topic:words(Topic), TopicFilter)
orelse match_topics(ClientInfo, Topic, Filters);
match_topic(emqx_topic:words(Topic), TopicFilter) orelse
match_topics(ClientInfo, Topic, Filters);
match_topics(ClientInfo, Topic, [TopicFilter | Filters]) ->
match_topic(emqx_topic:words(Topic), TopicFilter)
orelse match_topics(ClientInfo, Topic, Filters).
match_topic(emqx_topic:words(Topic), TopicFilter) orelse
match_topics(ClientInfo, Topic, Filters).
match_topic(Topic, {'eq', TopicFilter}) ->
Topic =:= TopicFilter;

View File

@ -19,22 +19,25 @@
-include_lib("typerefl/include/types.hrl").
-include_lib("emqx_connector/include/emqx_connector.hrl").
-reflect_type([ permission/0
, action/0
]).
-reflect_type([
permission/0,
action/0
]).
-type action() :: publish | subscribe | all.
-type permission() :: allow | deny.
-export([ namespace/0
, roots/0
, fields/1
, validations/0
]).
-export([
namespace/0,
roots/0,
fields/1,
validations/0
]).
-export([ headers_no_content_type/1
, headers/1
]).
-export([
headers_no_content_type/1,
headers/1
]).
-import(emqx_schema, [mk_duration/2]).
-include_lib("hocon/include/hoconsc.hrl").
@ -50,73 +53,84 @@ namespace() -> authz.
roots() -> [].
fields("authorization") ->
[ {sources, #{type => union_array(
[ hoconsc:ref(?MODULE, file)
, hoconsc:ref(?MODULE, http_get)
, hoconsc:ref(?MODULE, http_post)
, hoconsc:ref(?MODULE, mnesia)
, hoconsc:ref(?MODULE, mongo_single)
, hoconsc:ref(?MODULE, mongo_rs)
, hoconsc:ref(?MODULE, mongo_sharded)
, hoconsc:ref(?MODULE, mysql)
, hoconsc:ref(?MODULE, postgresql)
, hoconsc:ref(?MODULE, redis_single)
, hoconsc:ref(?MODULE, redis_sentinel)
, hoconsc:ref(?MODULE, redis_cluster)
]),
default => [],
desc =>
"
Authorization data sources.<br>
An array of authorization (ACL) data providers.
It is designed as an array, not a hash-map, so the sources can be
ordered to form a chain of access controls.<br>
When authorizing a 'publish' or 'subscribe' action, the configured
sources are checked in order. When checking an ACL source,
in case the client (identified by username or client ID) is not found,
it moves on to the next source. And it stops immediately
once an 'allow' or 'deny' decision is returned.<br>
If the client is not found in any of the sources,
the default action configured in 'authorization.no_match' is applied.<br>
NOTE:
The source elements are identified by their 'type'.
It is NOT allowed to configure two or more sources of the same type.
"
}
}
[
{sources, #{
type => union_array(
[
hoconsc:ref(?MODULE, file),
hoconsc:ref(?MODULE, http_get),
hoconsc:ref(?MODULE, http_post),
hoconsc:ref(?MODULE, mnesia),
hoconsc:ref(?MODULE, mongo_single),
hoconsc:ref(?MODULE, mongo_rs),
hoconsc:ref(?MODULE, mongo_sharded),
hoconsc:ref(?MODULE, mysql),
hoconsc:ref(?MODULE, postgresql),
hoconsc:ref(?MODULE, redis_single),
hoconsc:ref(?MODULE, redis_sentinel),
hoconsc:ref(?MODULE, redis_cluster)
]
),
default => [],
desc =>
"\n"
"Authorization data sources.<br>\n"
"An array of authorization (ACL) data providers.\n"
"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>\n"
"\n"
"When authorizing a 'publish' or 'subscribe' action, the configured\n"
"sources are checked in order. When checking an ACL source,\n"
"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\n"
"once an 'allow' or 'deny' decision is returned.<br>\n"
"\n"
"If the client is not found in any of the sources,\n"
"the default action configured in 'authorization.no_match' is applied.<br>\n"
"\n"
"NOTE:\n"
"The source elements are identified by their 'type'.\n"
"It is NOT allowed to configure two or more sources of the same type.\n"
}}
];
fields(file) ->
[ {type, #{type => file}}
, {enable, #{type => boolean(),
default => true}}
, {path, #{type => string(),
required => true,
desc => "
Path to the file which contains the ACL rules.<br>
If the file provisioned before starting EMQX node,
it can be placed anywhere as long as EMQX has read access to it.
In case the rule-set is created from EMQX dashboard or management API,
the file will be placed in `authz` subdirectory inside EMQX's `data_dir`,
and the new rules will override all rules from the old config file.
"
}}
[
{type, #{type => file}},
{enable, #{
type => boolean(),
default => true
}},
{path, #{
type => string(),
required => true,
desc =>
"\n"
"Path to the file which contains the ACL rules.<br>\n"
"If the file provisioned before starting EMQX node,\n"
"it can be placed anywhere as long as EMQX has read access to it.\n"
"\n"
"In case the rule-set is created from EMQX dashboard or management API,\n"
"the file will be placed in `authz` subdirectory inside EMQX's `data_dir`,\n"
"and the new rules will override all rules from the old config file.\n"
}}
];
fields(http_get) ->
[ {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();
fields(http_post) ->
[ {method, #{type => post, default => post}}
, {headers, fun headers/1}
[
{method, #{type => post, default => post}},
{headers, fun headers/1}
] ++ http_common_fields();
fields(mnesia) ->
[ {type, #{type => 'built_in_database'}}
, {enable, #{type => boolean(),
default => true}}
[
{type, #{type => 'built_in_database'}},
{enable, #{
type => boolean(),
default => true
}}
];
fields(mongo_single) ->
mongo_common_fields() ++ emqx_connector_mongo:fields(single);
@ -126,62 +140,88 @@ fields(mongo_sharded) ->
mongo_common_fields() ++ emqx_connector_mongo:fields(sharded);
fields(mysql) ->
connector_fields(mysql) ++
[ {query, query()} ];
[{query, query()}];
fields(postgresql) ->
[ {query, query()}
, {type, #{type => postgresql}}
, {enable, #{type => boolean(),
default => true}}
[
{query, query()},
{type, #{type => postgresql}},
{enable, #{
type => boolean(),
default => true
}}
] ++ emqx_connector_pgsql:fields(config);
fields(redis_single) ->
connector_fields(redis, single) ++
[ {cmd, query()} ];
[{cmd, query()}];
fields(redis_sentinel) ->
connector_fields(redis, sentinel) ++
[ {cmd, query()} ];
[{cmd, query()}];
fields(redis_cluster) ->
connector_fields(redis, cluster) ++
[ {cmd, query()} ].
[{cmd, query()}].
http_common_fields() ->
[ {url, fun url/1}
, {request_timeout, mk_duration("Request timeout", #{default => "30s", desc => "Request timeout."})}
, {body, #{type => map(), required => false, desc => "HTTP request body."}}
] ++ maps:to_list(maps:without([ base_url
, pool_type],
maps:from_list(connector_fields(http)))).
[
{url, fun url/1},
{request_timeout,
mk_duration("Request timeout", #{default => "30s", desc => "Request timeout."})},
{body, #{type => map(), required => false, desc => "HTTP request body."}}
] ++
maps:to_list(
maps:without(
[
base_url,
pool_type
],
maps:from_list(connector_fields(http))
)
).
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."}}
, {type, #{type => mongodb, desc => "Database backend."}}
, {enable, #{type => boolean(),
default => true,
desc => "Enable or disable the backend."}}
[
{collection, #{
type => atom(), desc => "`MongoDB` collection containing the authorization data."
}},
{selector, #{type => map(), desc => "MQL query used to select the authorization record."}},
{type, #{type => mongodb, desc => "Database backend."}},
{enable, #{
type => boolean(),
default => true,
desc => "Enable or disable the backend."
}}
].
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(desc) -> "List of HTTP headers.";
headers(type) ->
list({binary(), binary()});
headers(desc) ->
"List of HTTP headers.";
headers(converter) ->
fun(Headers) ->
maps:to_list(maps:merge(default_headers(), transform_header_name(Headers)))
end;
headers(default) -> default_headers();
headers(_) -> undefined.
headers(default) ->
default_headers();
headers(_) ->
undefined.
headers_no_content_type(type) -> list({binary(), binary()});
headers_no_content_type(desc) -> "List of HTTP headers.";
headers_no_content_type(type) ->
list({binary(), binary()});
headers_no_content_type(desc) ->
"List of HTTP headers.";
headers_no_content_type(converter) ->
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;
headers_no_content_type(default) -> default_headers_no_content_type();
headers_no_content_type(_) -> undefined.
headers_no_content_type(default) ->
default_headers_no_content_type();
headers_no_content_type(_) ->
undefined.
url(type) -> binary();
url(desc) -> "URL of the auth server.";
@ -194,26 +234,34 @@ url(_) -> undefined.
%%--------------------------------------------------------------------
default_headers() ->
maps:put(<<"content-type">>,
<<"application/json">>,
default_headers_no_content_type()).
maps:put(
<<"content-type">>,
<<"application/json">>,
default_headers_no_content_type()
).
default_headers_no_content_type() ->
#{ <<"accept">> => <<"application/json">>
, <<"cache-control">> => <<"no-cache">>
, <<"connection">> => <<"keep-alive">>
, <<"keep-alive">> => <<"timeout=30, max=1000">>
}.
#{
<<"accept">> => <<"application/json">>,
<<"cache-control">> => <<"no-cache">>,
<<"connection">> => <<"keep-alive">>,
<<"keep-alive">> => <<"timeout=30, max=1000">>
}.
transform_header_name(Headers) ->
maps:fold(fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc)
end, #{}, Headers).
maps:fold(
fun(K0, V, Acc) ->
K = list_to_binary(string:to_lower(to_list(K0))),
maps:put(K, V, Acc)
end,
#{},
Headers
).
check_ssl_opts(Conf) ->
case hocon_maps:get("config.url", Conf) of
undefined -> true;
undefined ->
true;
Url ->
case emqx_authz_http:parse_url(Url) of
#{scheme := https} ->
@ -221,19 +269,23 @@ check_ssl_opts(Conf) ->
true -> true;
_ -> {error, ssl_not_enable}
end;
#{scheme := http} -> true;
Bad -> {bad_scheme, Url, Bad}
#{scheme := http} ->
true;
Bad ->
{bad_scheme, Url, Bad}
end
end.
check_headers(Conf) ->
case hocon_maps:get("config.method", Conf) of
undefined -> true;
undefined ->
true;
Method0 ->
Method = to_bin(Method0),
Headers = hocon_maps:get("config.headers", Conf),
case Method of
<<"post">> -> true;
<<"post">> ->
true;
_ when Headers =:= undefined -> true;
_ when is_list(Headers) ->
case lists:member(<<"content-type">>, Headers) of
@ -247,32 +299,37 @@ union_array(Item) when is_list(Item) ->
hoconsc:array(hoconsc:union(Item)).
query() ->
#{type => binary(),
desc => "",
validator => fun(S) ->
case size(S) > 0 of
true -> ok;
_ -> {error, "Request query"}
end
end
}.
#{
type => binary(),
desc => "",
validator => fun(S) ->
case size(S) > 0 of
true -> ok;
_ -> {error, "Request query"}
end
end
}.
connector_fields(DB) ->
connector_fields(DB, config).
connector_fields(DB, Fields) ->
Mod0 = io_lib:format("~ts_~ts",[emqx_connector, DB]),
Mod = try
list_to_existing_atom(Mod0)
catch
error:badarg ->
list_to_atom(Mod0);
error:Reason ->
erlang:error(Reason)
end,
[ {type, #{type => DB, desc => "Database backend."}}
, {enable, #{type => boolean(),
default => true,
desc => "Enable or disable the backend."}}
Mod0 = io_lib:format("~ts_~ts", [emqx_connector, DB]),
Mod =
try
list_to_existing_atom(Mod0)
catch
error:badarg ->
list_to_atom(Mod0);
error:Reason ->
erlang:error(Reason)
end,
[
{type, #{type => DB, desc => "Database backend."}},
{enable, #{
type => boolean(),
default => true,
desc => "Enable or disable the backend."
}}
] ++ erlang:apply(Mod, fields, [Fields]).
to_list(A) when is_atom(A) ->

View File

@ -33,8 +33,10 @@ start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
init([]) ->
SupFlags = #{strategy => one_for_all,
intensity => 0,
period => 1},
SupFlags = #{
strategy => one_for_all,
intensity => 0,
period => 1
},
ChildSpecs = [],
{ok, {SupFlags, ChildSpecs}}.

View File

@ -19,15 +19,16 @@
-include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("emqx_authz.hrl").
-export([ cleanup_resources/0
, make_resource_id/1
, create_resource/2
, update_config/2
, parse_deep/2
, parse_sql/3
, render_deep/2
, render_sql_params/2
]).
-export([
cleanup_resources/0,
make_resource_id/1,
create_resource/2,
update_config/2,
parse_deep/2,
parse_sql/3,
render_deep/2,
render_sql_params/2
]).
%%------------------------------------------------------------------------------
%% APIs
@ -35,10 +36,15 @@
create_resource(Module, Config) ->
ResourceID = make_resource_id(Module),
case emqx_resource:create_local(ResourceID,
?RESOURCE_GROUP,
Module, Config,
#{}) of
case
emqx_resource:create_local(
ResourceID,
?RESOURCE_GROUP,
Module,
Config,
#{}
)
of
{ok, already_created} -> {ok, ResourceID};
{ok, _} -> {ok, ResourceID};
{error, Reason} -> {error, Reason}
@ -46,37 +52,45 @@ create_resource(Module, Config) ->
cleanup_resources() ->
lists:foreach(
fun emqx_resource:remove_local/1,
emqx_resource:list_group_instances(?RESOURCE_GROUP)).
fun emqx_resource:remove_local/1,
emqx_resource:list_group_instances(?RESOURCE_GROUP)
).
make_resource_id(Name) ->
NameBin = bin(Name),
emqx_resource:generate_id(NameBin).
update_config(Path, ConfigRequest) ->
emqx_conf:update(Path, ConfigRequest, #{rawconf_with_defaults => true,
override_to => cluster}).
emqx_conf:update(Path, ConfigRequest, #{
rawconf_with_defaults => true,
override_to => cluster
}).
parse_deep(Template, PlaceHolders) ->
emqx_placeholder:preproc_tmpl_deep(Template, #{placeholders => PlaceHolders}).
parse_sql(Template, ReplaceWith, PlaceHolders) ->
emqx_placeholder:preproc_sql(
Template,
#{replace_with => ReplaceWith,
placeholders => PlaceHolders}).
Template,
#{
replace_with => ReplaceWith,
placeholders => PlaceHolders
}
).
render_deep(Template, Values) ->
emqx_placeholder:proc_tmpl_deep(
Template,
client_vars(Values),
#{return => full_binary, var_trans => fun handle_var/2}).
Template,
client_vars(Values),
#{return => full_binary, var_trans => fun handle_var/2}
).
render_sql_params(ParamList, Values) ->
emqx_placeholder:proc_tmpl(
ParamList,
client_vars(Values),
#{return => rawlist, var_trans => fun handle_sql_var/2}).
ParamList,
client_vars(Values),
#{return => rawlist, var_trans => fun handle_sql_var/2}
).
%%------------------------------------------------------------------------------
%% Internal functions
@ -84,9 +98,11 @@ render_sql_params(ParamList, Values) ->
client_vars(ClientInfo) ->
maps:from_list(
lists:map(
fun convert_client_var/1,
maps:to_list(ClientInfo))).
lists:map(
fun convert_client_var/1,
maps:to_list(ClientInfo)
)
).
convert_client_var({cn, CN}) -> {cert_common_name, CN};
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:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
meck:expect(emqx_authz, acl_conf_file,
fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end),
meck:expect(
emqx_authz,
acl_conf_file,
fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end
),
ok = emqx_common_test_helpers:start_apps(
[emqx_connector, emqx_conf, emqx_authz],
fun set_special_configs/1),
[emqx_connector, emqx_conf, emqx_authz],
fun set_special_configs/1
),
Config.
end_per_suite(_Config) ->
{ok, _} = emqx:update_config(
[authorization],
#{<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}),
[authorization],
#{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []
}
),
ok = stop_apps([emqx_resource]),
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
meck:unload(emqx_resource),
@ -66,64 +73,71 @@ set_special_configs(emqx_authz) ->
set_special_configs(_App) ->
ok.
-define(SOURCE1, #{<<"type">> => <<"http">>,
<<"enable">> => true,
<<"url">> => <<"https://example.com:443/a/b?c=d">>,
<<"headers">> => #{},
<<"method">> => <<"get">>,
<<"request_timeout">> => 5000
}).
-define(SOURCE2, #{<<"type">> => <<"mongodb">>,
<<"enable">> => true,
<<"mongo_type">> => <<"single">>,
<<"server">> => <<"127.0.0.1:27017">>,
<<"w_mode">> => <<"unsafe">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"ssl">> => #{<<"enable">> => false},
<<"collection">> => <<"authz">>,
<<"selector">> => #{<<"a">> => <<"b">>}
}).
-define(SOURCE3, #{<<"type">> => <<"mysql">>,
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">>
}).
-define(SOURCE4, #{<<"type">> => <<"postgresql">>,
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">>
}).
-define(SOURCE5, #{<<"type">> => <<"redis">>,
<<"redis_type">> => <<"single">>,
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => 0,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
}).
-define(SOURCE6, #{<<"type">> => <<"file">>,
<<"enable">> => true,
<<"rules">> =>
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
}).
-define(SOURCE1, #{
<<"type">> => <<"http">>,
<<"enable">> => true,
<<"url">> => <<"https://example.com:443/a/b?c=d">>,
<<"headers">> => #{},
<<"method">> => <<"get">>,
<<"request_timeout">> => 5000
}).
-define(SOURCE2, #{
<<"type">> => <<"mongodb">>,
<<"enable">> => true,
<<"mongo_type">> => <<"single">>,
<<"server">> => <<"127.0.0.1:27017">>,
<<"w_mode">> => <<"unsafe">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"ssl">> => #{<<"enable">> => false},
<<"collection">> => <<"authz">>,
<<"selector">> => #{<<"a">> => <<"b">>}
}).
-define(SOURCE3, #{
<<"type">> => <<"mysql">>,
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">>
}).
-define(SOURCE4, #{
<<"type">> => <<"postgresql">>,
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">>
}).
-define(SOURCE5, #{
<<"type">> => <<"redis">>,
<<"redis_type">> => <<"single">>,
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => 0,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
}).
-define(SOURCE6, #{
<<"type">> => <<"file">>,
<<"enable">> => true,
<<"rules">> =>
<<
"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
>>
}).
%%------------------------------------------------------------------------------
%% Testcases
@ -138,95 +152,130 @@ t_update_source(_) ->
{ok, _} = emqx_authz:update(?CMD_APPEND, ?SOURCE5),
{ok, _} = emqx_authz:update(?CMD_APPEND, ?SOURCE6),
?assertMatch([ #{type := http, enable := true}
, #{type := mongodb, enable := true}
, #{type := mysql, enable := true}
, #{type := postgresql, enable := true}
, #{type := redis, enable := true}
, #{type := file, enable := true}
], emqx_conf:get([authorization, sources], [])),
?assertMatch(
[
#{type := http, enable := true},
#{type := mongodb, enable := true},
#{type := mysql, enable := true},
#{type := postgresql, enable := true},
#{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, mysql}, ?SOURCE3#{<<"enable">> := true}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, postgresql}, ?SOURCE4#{<<"enable">> := true}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, redis}, ?SOURCE5#{<<"enable">> := true}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, file}, ?SOURCE6#{<<"enable">> := true}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, file}, ?SOURCE6#{<<"enable">> := true}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, http}, ?SOURCE1#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, http}, ?SOURCE1#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, mongodb}, ?SOURCE2#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, mysql}, ?SOURCE3#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, postgresql}, ?SOURCE4#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, redis}, ?SOURCE5#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, file}, ?SOURCE6#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({?CMD_REPLACE, file}, ?SOURCE6#{<<"enable">> := false}),
?assertMatch([ #{type := http, enable := false}
, #{type := mongodb, enable := false}
, #{type := mysql, enable := false}
, #{type := postgresql, enable := false}
, #{type := redis, enable := false}
, #{type := file, enable := false}
], emqx_conf:get([authorization, sources], [])),
?assertMatch(
[
#{type := http, enable := false},
#{type := mongodb, enable := false},
#{type := mysql, enable := false},
#{type := postgresql, enable := false},
#{type := redis, enable := false},
#{type := file, enable := false}
],
emqx_conf:get([authorization, sources], [])
),
{ok, _} = emqx_authz:update(?CMD_REPLACE, []).
t_delete_source(_) ->
{ok, _} = emqx_authz:update(?CMD_REPLACE, [?SOURCE1]),
?assertMatch([ #{type := http, enable := true}
], emqx_conf:get([authorization, sources], [])),
?assertMatch([#{type := http, enable := true}], emqx_conf:get([authorization, sources], [])),
{ok, _} = emqx_authz:update({?CMD_DELETE, http}, #{}),
?assertMatch([], emqx_conf:get([authorization, sources], [])).
t_move_source(_) ->
{ok, _} = emqx_authz:update(?CMD_REPLACE,
[?SOURCE1, ?SOURCE2, ?SOURCE3,
?SOURCE4, ?SOURCE5, ?SOURCE6]),
?assertMatch([ #{type := http}
, #{type := mongodb}
, #{type := mysql}
, #{type := postgresql}
, #{type := redis}
, #{type := file}
], emqx_authz:lookup()),
{ok, _} = emqx_authz:update(
?CMD_REPLACE,
[
?SOURCE1,
?SOURCE2,
?SOURCE3,
?SOURCE4,
?SOURCE5,
?SOURCE6
]
),
?assertMatch(
[
#{type := http},
#{type := mongodb},
#{type := mysql},
#{type := postgresql},
#{type := redis},
#{type := file}
],
emqx_authz:lookup()
),
{ok, _} = emqx_authz:move(postgresql, ?CMD_MOVE_FRONT),
?assertMatch([ #{type := postgresql}
, #{type := http}
, #{type := mongodb}
, #{type := mysql}
, #{type := redis}
, #{type := file}
], emqx_authz:lookup()),
?assertMatch(
[
#{type := postgresql},
#{type := http},
#{type := mongodb},
#{type := mysql},
#{type := redis},
#{type := file}
],
emqx_authz:lookup()
),
{ok, _} = emqx_authz:move(http, ?CMD_MOVE_REAR),
?assertMatch([ #{type := postgresql}
, #{type := mongodb}
, #{type := mysql}
, #{type := redis}
, #{type := file}
, #{type := http}
], emqx_authz:lookup()),
?assertMatch(
[
#{type := postgresql},
#{type := mongodb},
#{type := mysql},
#{type := redis},
#{type := file},
#{type := http}
],
emqx_authz:lookup()
),
{ok, _} = emqx_authz:move(mysql, ?CMD_MOVE_BEFORE(postgresql)),
?assertMatch([ #{type := mysql}
, #{type := postgresql}
, #{type := mongodb}
, #{type := redis}
, #{type := file}
, #{type := http}
], emqx_authz:lookup()),
?assertMatch(
[
#{type := mysql},
#{type := postgresql},
#{type := mongodb},
#{type := redis},
#{type := file},
#{type := http}
],
emqx_authz:lookup()
),
{ok, _} = emqx_authz:move(mongodb, ?CMD_MOVE_AFTER(http)),
?assertMatch([ #{type := mysql}
, #{type := postgresql}
, #{type := redis}
, #{type := file}
, #{type := http}
, #{type := mongodb}
], emqx_authz:lookup()),
?assertMatch(
[
#{type := mysql},
#{type := postgresql},
#{type := redis},
#{type := file},
#{type := http},
#{type := mongodb}
],
emqx_authz:lookup()
),
ok.

View File

@ -32,16 +32,20 @@ groups() ->
init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz, emqx_dashboard],
fun set_special_configs/1),
[emqx_conf, emqx_authz, emqx_dashboard],
fun set_special_configs/1
),
Config.
end_per_suite(_Config) ->
{ok, _} = emqx:update_config(
[authorization],
#{<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}),
[authorization],
#{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []
}
),
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
ok.
@ -50,8 +54,10 @@ set_special_configs(emqx_dashboard) ->
set_special_configs(emqx_authz) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
{ok, _} = emqx:update_config([authorization, sources],
[#{<<"type">> => <<"built_in_database">>}]),
{ok, _} = emqx:update_config(
[authorization, sources],
[#{<<"type">> => <<"built_in_database">>}]
),
ok;
set_special_configs(_App) ->
ok.
@ -62,176 +68,248 @@ set_special_configs(_App) ->
t_api(_) ->
{ok, 204, _} =
request( post
, uri(["authorization", "sources", "built_in_database", "username"])
, [?USERNAME_RULES_EXAMPLE]),
request(
post,
uri(["authorization", "sources", "built_in_database", "username"]),
[?USERNAME_RULES_EXAMPLE]
),
{ok, 200, Request1} =
request( get
, uri(["authorization", "sources", "built_in_database", "username"])
, []),
#{<<"data">> := [#{<<"username">> := <<"user1">>, <<"rules">> := Rules1}],
<<"meta">> := #{<<"count">> := 1,
<<"limit">> := 100,
<<"page">> := 1}} = jsx:decode(Request1),
request(
get,
uri(["authorization", "sources", "built_in_database", "username"]),
[]
),
#{
<<"data">> := [#{<<"username">> := <<"user1">>, <<"rules">> := Rules1}],
<<"meta">> := #{
<<"count">> := 1,
<<"limit">> := 100,
<<"page">> := 1
}
} = jsx:decode(Request1),
?assertEqual(3, length(Rules1)),
{ok, 200, Request1_1} =
request( get
, uri([ "authorization"
, "sources"
, "built_in_database"
, "username?page=1&limit=20&like_username=noexist"])
, []),
#{<<"data">> := [],
<<"meta">> := #{<<"count">> := 0,
<<"limit">> := 20,
<<"page">> := 1}} = jsx:decode(Request1_1),
request(
get,
uri([
"authorization",
"sources",
"built_in_database",
"username?page=1&limit=20&like_username=noexist"
]),
[]
),
#{
<<"data">> := [],
<<"meta">> := #{
<<"count">> := 0,
<<"limit">> := 20,
<<"page">> := 1
}
} = jsx:decode(Request1_1),
{ok, 200, Request2} =
request( get
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
[]
),
#{<<"username">> := <<"user1">>, <<"rules">> := Rules1} = jsx:decode(Request2),
{ok, 204, _} =
request( put
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
, ?USERNAME_RULES_EXAMPLE#{rules => []}),
request(
put,
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
?USERNAME_RULES_EXAMPLE#{rules => []}
),
{ok, 200, Request3} =
request( get
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
[]
),
#{<<"username">> := <<"user1">>, <<"rules">> := Rules2} = jsx:decode(Request3),
?assertEqual(0, length(Rules2)),
{ok, 204, _} =
request( delete
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
, []),
request(
delete,
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
[]
),
{ok, 404, _} =
request( get
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
[]
),
{ok, 404, _} =
request( delete
, uri(["authorization", "sources", "built_in_database", "username", "user1"])
, []),
request(
delete,
uri(["authorization", "sources", "built_in_database", "username", "user1"]),
[]
),
{ok, 204, _} =
request( post
, uri(["authorization", "sources", "built_in_database", "clientid"])
, [?CLIENTID_RULES_EXAMPLE]),
request(
post,
uri(["authorization", "sources", "built_in_database", "clientid"]),
[?CLIENTID_RULES_EXAMPLE]
),
{ok, 200, Request4} =
request( get
, uri(["authorization", "sources", "built_in_database", "clientid"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "clientid"]),
[]
),
{ok, 200, Request5} =
request( get
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
, []),
#{<<"data">> := [#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3}],
<<"meta">> := #{<<"count">> := 1, <<"limit">> := 100, <<"page">> := 1}}
= jsx:decode(Request4),
request(
get,
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
[]
),
#{
<<"data">> := [#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3}],
<<"meta">> := #{<<"count">> := 1, <<"limit">> := 100, <<"page">> := 1}
} =
jsx:decode(Request4),
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3} = jsx:decode(Request5),
?assertEqual(3, length(Rules3)),
{ok, 204, _} =
request( put
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
, ?CLIENTID_RULES_EXAMPLE#{rules => []}),
request(
put,
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
?CLIENTID_RULES_EXAMPLE#{rules => []}
),
{ok, 200, Request6} =
request( get
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
[]
),
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules4} = jsx:decode(Request6),
?assertEqual(0, length(Rules4)),
{ok, 204, _} =
request( delete
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
, []),
request(
delete,
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
[]
),
{ok, 404, _} =
request( get
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
[]
),
{ok, 404, _} =
request( delete
, uri(["authorization", "sources", "built_in_database", "clientid", "client1"])
, []),
request(
delete,
uri(["authorization", "sources", "built_in_database", "clientid", "client1"]),
[]
),
{ok, 204, _} =
request( post
, uri(["authorization", "sources", "built_in_database", "all"])
, ?ALL_RULES_EXAMPLE),
request(
post,
uri(["authorization", "sources", "built_in_database", "all"]),
?ALL_RULES_EXAMPLE
),
{ok, 200, Request7} =
request( get
, uri(["authorization", "sources", "built_in_database", "all"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "all"]),
[]
),
#{<<"rules">> := Rules5} = jsx:decode(Request7),
?assertEqual(3, length(Rules5)),
{ok, 204, _} =
request( post
, uri(["authorization", "sources", "built_in_database", "all"])
request(
post,
uri(["authorization", "sources", "built_in_database", "all"]),
, ?ALL_RULES_EXAMPLE#{rules => []}),
?ALL_RULES_EXAMPLE#{rules => []}
),
{ok, 200, Request8} =
request( get
, uri(["authorization", "sources", "built_in_database", "all"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "all"]),
[]
),
#{<<"rules">> := Rules6} = jsx:decode(Request8),
?assertEqual(0, length(Rules6)),
{ok, 204, _} =
request( post
, uri(["authorization", "sources", "built_in_database", "username"])
, [ #{username => erlang:integer_to_binary(N), rules => []}
|| N <- lists:seq(1, 20) ]),
request(
post,
uri(["authorization", "sources", "built_in_database", "username"]),
[
#{username => erlang:integer_to_binary(N), rules => []}
|| N <- lists:seq(1, 20)
]
),
{ok, 200, Request9} =
request( get
, uri(["authorization", "sources", "built_in_database", "username?page=2&limit=5"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "username?page=2&limit=5"]),
[]
),
#{<<"data">> := Data1} = jsx:decode(Request9),
?assertEqual(5, length(Data1)),
{ok, 204, _} =
request( post
, uri(["authorization", "sources", "built_in_database", "clientid"])
, [ #{clientid => erlang:integer_to_binary(N), rules => []}
|| N <- lists:seq(1, 20) ]),
request(
post,
uri(["authorization", "sources", "built_in_database", "clientid"]),
[
#{clientid => erlang:integer_to_binary(N), rules => []}
|| N <- lists:seq(1, 20)
]
),
{ok, 200, Request10} =
request( get
, uri(["authorization", "sources", "built_in_database", "clientid?limit=5"])
, []),
request(
get,
uri(["authorization", "sources", "built_in_database", "clientid?limit=5"]),
[]
),
#{<<"data">> := Data2} = jsx:decode(Request10),
?assertEqual(5, length(Data2)),
{ok, 400, Msg1} =
request( delete
, uri(["authorization", "sources", "built_in_database", "purge-all"])
, []),
request(
delete,
uri(["authorization", "sources", "built_in_database", "purge-all"]),
[]
),
?assertMatch({match, _}, re:run(Msg1, "must\sbe\sdisabled\sbefore")),
{ok, 204, _} =
request( put
, uri(["authorization", "sources", "built_in_database"])
, #{<<"enable">> => true, <<"type">> => <<"built_in_database">>}),
request(
put,
uri(["authorization", "sources", "built_in_database"]),
#{<<"enable">> => true, <<"type">> => <<"built_in_database">>}
),
%% test idempotence
{ok, 204, _} =
request( put
, uri(["authorization", "sources", "built_in_database"])
, #{<<"enable">> => true, <<"type">> => <<"built_in_database">>}),
request(
put,
uri(["authorization", "sources", "built_in_database"]),
#{<<"enable">> => true, <<"type">> => <<"built_in_database">>}
),
{ok, 204, _} =
request( put
, uri(["authorization", "sources", "built_in_database"])
, #{<<"enable">> => false, <<"type">> => <<"built_in_database">>}),
request(
put,
uri(["authorization", "sources", "built_in_database"]),
#{<<"enable">> => false, <<"type">> => <<"built_in_database">>}
),
{ok, 204, _} =
request( delete
, uri(["authorization", "sources", "built_in_database", "purge-all"])
, []),
request(
delete,
uri(["authorization", "sources", "built_in_database", "purge-all"]),
[]
),
?assertEqual(0, emqx_authz_mnesia:record_count()),
ok.

View File

@ -31,16 +31,20 @@ groups() ->
init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz, emqx_dashboard],
fun set_special_configs/1),
[emqx_conf, emqx_authz, emqx_dashboard],
fun set_special_configs/1
),
Config.
end_per_suite(_Config) ->
{ok, _} = emqx:update_config(
[authorization],
#{<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}),
[authorization],
#{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []
}
),
ok = stop_apps([emqx_resource, emqx_connector]),
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
ok.
@ -60,27 +64,29 @@ set_special_configs(_App) ->
%%------------------------------------------------------------------------------
t_api(_) ->
Settings1 = #{<<"no_match">> => <<"deny">>,
<<"deny_action">> => <<"disconnect">>,
<<"cache">> => #{
<<"enable">> => false,
<<"max_size">> => 32,
<<"ttl">> => 60000
}
},
Settings1 = #{
<<"no_match">> => <<"deny">>,
<<"deny_action">> => <<"disconnect">>,
<<"cache">> => #{
<<"enable">> => false,
<<"max_size">> => 32,
<<"ttl">> => 60000
}
},
{ok, 200, Result1} = request(put, uri(["authorization", "settings"]), Settings1),
{ok, 200, Result1} = request(get, uri(["authorization", "settings"]), []),
?assertEqual(Settings1, jsx:decode(Result1)),
Settings2 = #{<<"no_match">> => <<"allow">>,
<<"deny_action">> => <<"ignore">>,
<<"cache">> => #{
<<"enable">> => true,
<<"max_size">> => 32,
<<"ttl">> => 60000
}
},
Settings2 = #{
<<"no_match">> => <<"allow">>,
<<"deny_action">> => <<"ignore">>,
<<"cache">> => #{
<<"enable">> => true,
<<"max_size">> => 32,
<<"ttl">> => 60000
}
},
{ok, 200, Result2} = request(put, uri(["authorization", "settings"]), Settings2),
{ok, 200, Result2} = request(get, uri(["authorization", "settings"]), []),

View File

@ -29,62 +29,70 @@
-define(PGSQL_HOST, "pgsql").
-define(REDIS_SINGLE_HOST, "redis").
-define(SOURCE1, #{<<"type">> => <<"http">>,
<<"enable">> => true,
<<"url">> => <<"https://fake.com:443/acl?username=", ?PH_USERNAME/binary>>,
<<"headers">> => #{},
<<"method">> => <<"get">>,
<<"request_timeout">> => <<"5s">>
}).
-define(SOURCE2, #{<<"type">> => <<"mongodb">>,
<<"enable">> => true,
<<"mongo_type">> => <<"single">>,
<<"server">> => <<?MONGO_SINGLE_HOST>>,
<<"w_mode">> => <<"unsafe">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"ssl">> => #{<<"enable">> => false},
<<"collection">> => <<"fake">>,
<<"selector">> => #{<<"a">> => <<"b">>}
}).
-define(SOURCE3, #{<<"type">> => <<"mysql">>,
<<"enable">> => true,
<<"server">> => <<?MYSQL_HOST>>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">>
}).
-define(SOURCE4, #{<<"type">> => <<"postgresql">>,
<<"enable">> => true,
<<"server">> => <<?PGSQL_HOST>>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">>
}).
-define(SOURCE5, #{<<"type">> => <<"redis">>,
<<"enable">> => true,
<<"servers">> => <<?REDIS_SINGLE_HOST, ",127.0.0.1:6380">>,
<<"pool_size">> => 1,
<<"database">> => 0,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
}).
-define(SOURCE6, #{<<"type">> => <<"file">>,
<<"enable">> => true,
<<"rules">> =>
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
}).
-define(SOURCE1, #{
<<"type">> => <<"http">>,
<<"enable">> => true,
<<"url">> => <<"https://fake.com:443/acl?username=", ?PH_USERNAME/binary>>,
<<"headers">> => #{},
<<"method">> => <<"get">>,
<<"request_timeout">> => <<"5s">>
}).
-define(SOURCE2, #{
<<"type">> => <<"mongodb">>,
<<"enable">> => true,
<<"mongo_type">> => <<"single">>,
<<"server">> => <<?MONGO_SINGLE_HOST>>,
<<"w_mode">> => <<"unsafe">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"ssl">> => #{<<"enable">> => false},
<<"collection">> => <<"fake">>,
<<"selector">> => #{<<"a">> => <<"b">>}
}).
-define(SOURCE3, #{
<<"type">> => <<"mysql">>,
<<"enable">> => true,
<<"server">> => <<?MYSQL_HOST>>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">>
}).
-define(SOURCE4, #{
<<"type">> => <<"postgresql">>,
<<"enable">> => true,
<<"server">> => <<?PGSQL_HOST>>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"query">> => <<"abcb">>
}).
-define(SOURCE5, #{
<<"type">> => <<"redis">>,
<<"enable">> => true,
<<"servers">> => <<?REDIS_SINGLE_HOST, ",127.0.0.1:6380">>,
<<"pool_size">> => 1,
<<"database">> => 0,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
}).
-define(SOURCE6, #{
<<"type">> => <<"file">>,
<<"enable">> => true,
<<"rules">> =>
<<
"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
>>
}).
-define(MATCH_RSA_KEY, <<"-----BEGIN RSA PRIVATE KEY", _/binary>>).
-define(MATCH_CERT, <<"-----BEGIN CERTIFICATE", _/binary>>).
@ -100,24 +108,31 @@ init_per_suite(Config) ->
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, health_check, fun(St) -> {ok, St} end),
meck:expect(emqx_resource, remove_local, fun(_) -> ok end ),
meck:expect(emqx_authz, acl_conf_file,
fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end),
meck:expect(emqx_resource, remove_local, fun(_) -> ok end),
meck:expect(
emqx_authz,
acl_conf_file,
fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end
),
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz, emqx_dashboard],
fun set_special_configs/1),
[emqx_conf, emqx_authz, emqx_dashboard],
fun set_special_configs/1
),
ok = start_apps([emqx_resource, emqx_connector]),
Config.
end_per_suite(_Config) ->
{ok, _} = emqx:update_config(
[authorization],
#{<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}),
[authorization],
#{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []
}
),
%% resource and connector should be stop first,
%% or authz_[mysql|pgsql|redis..]_SUITE would be failed
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:new(emqx, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx, data_dir,
fun() ->
{data_dir, Data} = lists:keyfind(data_dir, 1, Config),
Data
end),
meck:expect(
emqx,
data_dir,
fun() ->
{data_dir, Data} = lists:keyfind(data_dir, 1, Config),
Data
end
),
Config;
init_per_testcase(_, Config) -> Config.
init_per_testcase(_, Config) ->
Config.
end_per_testcase(t_api, _Config) ->
meck:unload(emqx_misc),
meck:unload(emqx),
ok;
end_per_testcase(_, _Config) -> ok.
end_per_testcase(_, _Config) ->
ok.
%%------------------------------------------------------------------------------
%% Testcases
@ -162,139 +182,190 @@ t_api(_) ->
{ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []),
?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),
Snd = fun ({_, Val}) -> Val end,
Snd = fun({_, Val}) -> Val end,
LookupVal = fun LookupV(List, RestJson) ->
case List of
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
end
end,
EqualFun = fun (RList) ->
fun ({M, V}) ->
?assertEqual(V,
LookupVal([<<"metrics">>, M],
RList)
)
end
end,
case List of
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
end
end,
EqualFun = fun(RList) ->
fun({M, V}) ->
?assertEqual(
V,
LookupVal(
[<<"metrics">>, M],
RList
)
)
end
end,
AssertFun =
fun (ResultJson) ->
fun(ResultJson) ->
{ok, RList} = emqx_json:safe_decode(ResultJson),
MetricsList = [{<<"failed">>, 0},
{<<"matched">>, 0},
{<<"rate">>, 0.0},
{<<"rate_last5m">>, 0.0},
{<<"rate_max">>, 0.0},
{<<"success">>, 0}],
MetricsList = [
{<<"failed">>, 0},
{<<"matched">>, 0},
{<<"rate">>, 0.0},
{<<"rate_last5m">>, 0.0},
{<<"rate_max">>, 0.0},
{<<"success">>, 0}
],
lists:map(EqualFun(RList), MetricsList)
end,
{ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []),
Sources = get_sources(Result2),
?assertMatch([ #{<<"type">> := <<"http">>}
, #{<<"type">> := <<"mongodb">>}
, #{<<"type">> := <<"mysql">>}
, #{<<"type">> := <<"postgresql">>}
, #{<<"type">> := <<"redis">>}
, #{<<"type">> := <<"file">>}
], Sources),
?assertMatch(
[
#{<<"type">> := <<"http">>},
#{<<"type">> := <<"mongodb">>},
#{<<"type">> := <<"mysql">>},
#{<<"type">> := <<"postgresql">>},
#{<<"type">> := <<"redis">>},
#{<<"type">> := <<"file">>}
],
Sources
),
?assert(filelib:is_file(emqx_authz:acl_conf_file())),
{ok, 204, _} = request(put, uri(["authorization", "sources", "http"]),
?SOURCE1#{<<"enable">> := false}),
{ok, 204, _} = request(
put,
uri(["authorization", "sources", "http"]),
?SOURCE1#{<<"enable">> := false}
),
{ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []),
?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result3)),
Keyfile = emqx_common_test_helpers:app_path(
emqx,
filename:join(["etc", "certs", "key.pem"])),
emqx,
filename:join(["etc", "certs", "key.pem"])
),
Certfile = emqx_common_test_helpers:app_path(
emqx,
filename:join(["etc", "certs", "cert.pem"])),
emqx,
filename:join(["etc", "certs", "cert.pem"])
),
Cacertfile = emqx_common_test_helpers:app_path(
emqx,
filename:join(["etc", "certs", "cacert.pem"])),
emqx,
filename:join(["etc", "certs", "cacert.pem"])
),
{ok, 204, _} = request(put, uri(["authorization", "sources", "mongodb"]),
?SOURCE2#{<<"ssl">> => #{
<<"enable">> => <<"true">>,
<<"cacertfile">> => Cacertfile,
<<"certfile">> => Certfile,
<<"keyfile">> => Keyfile,
<<"verify">> => <<"verify_none">>
}}),
{ok, 204, _} = request(
put,
uri(["authorization", "sources", "mongodb"]),
?SOURCE2#{
<<"ssl">> => #{
<<"enable">> => <<"true">>,
<<"cacertfile">> => Cacertfile,
<<"certfile">> => Certfile,
<<"keyfile">> => Keyfile,
<<"verify">> => <<"verify_none">>
}
}
),
{ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongodb"]), []),
{ok, 200, Status4} = request(get, uri(["authorization", "sources", "mongodb", "status"]), []),
AssertFun(Status4),
?assertMatch(#{<<"type">> := <<"mongodb">>,
<<"ssl">> := #{<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT,
<<"certfile">> := ?MATCH_CERT,
<<"keyfile">> := ?MATCH_RSA_KEY,
<<"verify">> := <<"verify_none">>
}
}, jsx:decode(Result4)),
?assertMatch(
#{
<<"type">> := <<"mongodb">>,
<<"ssl">> := #{
<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT,
<<"certfile">> := ?MATCH_CERT,
<<"keyfile">> := ?MATCH_RSA_KEY,
<<"verify">> := <<"verify_none">>
}
},
jsx:decode(Result4)
),
{ok, Cacert} = file:read_file(Cacertfile),
{ok, Cert} = file:read_file(Certfile),
{ok, Key} = file:read_file(Keyfile),
{ok, 204, _} = request(put, uri(["authorization", "sources", "mongodb"]),
?SOURCE2#{<<"ssl">> => #{
<<"enable">> => <<"true">>,
<<"cacertfile">> => Cacert,
<<"certfile">> => Cert,
<<"keyfile">> => Key,
<<"verify">> => <<"verify_none">>
}}),
{ok, 204, _} = request(
put,
uri(["authorization", "sources", "mongodb"]),
?SOURCE2#{
<<"ssl">> => #{
<<"enable">> => <<"true">>,
<<"cacertfile">> => Cacert,
<<"certfile">> => Cert,
<<"keyfile">> => Key,
<<"verify">> => <<"verify_none">>
}
}
),
{ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongodb"]), []),
{ok, 200, Status5} = request(get, uri(["authorization", "sources", "mongodb", "status"]), []),
AssertFun(Status5),
?assertMatch(#{<<"type">> := <<"mongodb">>,
<<"ssl">> := #{<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT,
<<"certfile">> := ?MATCH_CERT,
<<"keyfile">> := ?MATCH_RSA_KEY,
<<"verify">> := <<"verify_none">>
}
}, jsx:decode(Result5)),
?assertMatch(
#{
<<"type">> := <<"mongodb">>,
<<"ssl">> := #{
<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT,
<<"certfile">> := ?MATCH_CERT,
<<"keyfile">> := ?MATCH_RSA_KEY,
<<"verify">> := <<"verify_none">>
}
},
jsx:decode(Result5)
),
#{ssl := #{cacertfile := SavedCacertfile,
certfile := SavedCertfile,
keyfile := SavedKeyfile
}} = emqx_authz:lookup(mongodb),
#{
ssl := #{
cacertfile := SavedCacertfile,
certfile := SavedCertfile,
keyfile := SavedKeyfile
}
} = emqx_authz:lookup(mongodb),
?assert(filelib:is_file(SavedCacertfile)),
?assert(filelib:is_file(SavedCertfile)),
?assert(filelib:is_file(SavedKeyfile)),
{ok, 204, _} = request(
put,
uri(["authorization", "sources", "mysql"]),
?SOURCE3#{<<"server">> := <<"192.168.1.100:3306">>}),
put,
uri(["authorization", "sources", "mysql"]),
?SOURCE3#{<<"server">> := <<"192.168.1.100:3306">>}
),
{ok, 204, _} = request(
put,
uri(["authorization", "sources", "postgresql"]),
?SOURCE4#{<<"server">> := <<"fake">>}),
put,
uri(["authorization", "sources", "postgresql"]),
?SOURCE4#{<<"server">> := <<"fake">>}
),
{ok, 204, _} = request(
put,
uri(["authorization", "sources", "redis"]),
?SOURCE5#{<<"servers">> := [<<"192.168.1.100:6379">>,
<<"192.168.1.100:6380">>]}),
put,
uri(["authorization", "sources", "redis"]),
?SOURCE5#{
<<"servers">> := [
<<"192.168.1.100:6379">>,
<<"192.168.1.100:6380">>
]
}
),
lists:foreach(
fun(#{<<"type">> := Type}) ->
{ok, 204, _} = request(
delete,
uri(["authorization", "sources", binary_to_list(Type)]),
[])
end, Sources),
fun(#{<<"type">> := Type}) ->
{ok, 204, _} = request(
delete,
uri(["authorization", "sources", binary_to_list(Type)]),
[]
)
end,
Sources
),
{ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []),
?assertEqual([], get_sources(Result6)),
?assertEqual([], emqx:get_config([authorization, sources])),
@ -302,48 +373,80 @@ t_api(_) ->
t_move_source(_) ->
{ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]),
?assertMatch([ #{type := http}
, #{type := mongodb}
, #{type := mysql}
, #{type := postgresql}
, #{type := redis}
], emqx_authz:lookup()),
?assertMatch(
[
#{type := http},
#{type := mongodb},
#{type := mysql},
#{type := postgresql},
#{type := redis}
],
emqx_authz:lookup()
),
{ok, 204, _} = request(post, uri(["authorization", "sources", "postgresql", "move"]),
#{<<"position">> => <<"front">>}),
?assertMatch([ #{type := postgresql}
, #{type := http}
, #{type := mongodb}
, #{type := mysql}
, #{type := redis}
], emqx_authz:lookup()),
{ok, 204, _} = request(
post,
uri(["authorization", "sources", "postgresql", "move"]),
#{<<"position">> => <<"front">>}
),
?assertMatch(
[
#{type := postgresql},
#{type := http},
#{type := mongodb},
#{type := mysql},
#{type := redis}
],
emqx_authz:lookup()
),
{ok, 204, _} = request(post, uri(["authorization", "sources", "http", "move"]),
#{<<"position">> => <<"rear">>}),
?assertMatch([ #{type := postgresql}
, #{type := mongodb}
, #{type := mysql}
, #{type := redis}
, #{type := http}
], emqx_authz:lookup()),
{ok, 204, _} = request(
post,
uri(["authorization", "sources", "http", "move"]),
#{<<"position">> => <<"rear">>}
),
?assertMatch(
[
#{type := postgresql},
#{type := mongodb},
#{type := mysql},
#{type := redis},
#{type := http}
],
emqx_authz:lookup()
),
{ok, 204, _} = request(post, uri(["authorization", "sources", "mysql", "move"]),
#{<<"position">> => <<"before:postgresql">>}),
?assertMatch([ #{type := mysql}
, #{type := postgresql}
, #{type := mongodb}
, #{type := redis}
, #{type := http}
], emqx_authz:lookup()),
{ok, 204, _} = request(
post,
uri(["authorization", "sources", "mysql", "move"]),
#{<<"position">> => <<"before:postgresql">>}
),
?assertMatch(
[
#{type := mysql},
#{type := postgresql},
#{type := mongodb},
#{type := redis},
#{type := http}
],
emqx_authz:lookup()
),
{ok, 204, _} = request(post, uri(["authorization", "sources", "mongodb", "move"]),
#{<<"position">> => <<"after:http">>}),
?assertMatch([ #{type := mysql}
, #{type := postgresql}
, #{type := redis}
, #{type := http}
, #{type := mongodb}
], emqx_authz:lookup()),
{ok, 204, _} = request(
post,
uri(["authorization", "sources", "mongodb", "move"]),
#{<<"position">> => <<"after:http">>}
),
?assertMatch(
[
#{type := mysql},
#{type := postgresql},
#{type := redis},
#{type := http},
#{type := mongodb}
],
emqx_authz:lookup()
),
ok.

View File

@ -22,12 +22,15 @@
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(RAW_SOURCE, #{<<"type">> => <<"file">>,
<<"enable">> => true,
<<"rules">> =>
<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
}).
-define(RAW_SOURCE, #{
<<"type">> => <<"file">>,
<<"enable">> => true,
<<"rules">> =>
<<
"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
"\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}."
>>
}).
all() ->
emqx_common_test_helpers:all(?MODULE).
@ -37,13 +40,17 @@ groups() ->
init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1),
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
%% meck after authz started
meck:expect(emqx_authz, acl_conf_file,
fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end),
meck:expect(
emqx_authz,
acl_conf_file,
fun() ->
emqx_common_test_helpers:deps_path(emqx_authz, "etc/acl.conf")
end
),
Config.
end_per_suite(_Config) ->
@ -57,7 +64,6 @@ init_per_testcase(_TestCase, Config) ->
set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
ok.
@ -66,43 +72,55 @@ set_special_configs(_) ->
%%------------------------------------------------------------------------------
t_ok(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_config(?RAW_SOURCE#{<<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">>}),
ok = setup_config(?RAW_SOURCE#{
<<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">>
}),
io:format("~p", [emqx_authz:acl_conf_file()]),
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
),
?assertEqual(
deny,
emqx_access_control:authorize(ClientInfo, subscribe, <<"t">>)).
deny,
emqx_access_control:authorize(ClientInfo, subscribe, <<"t">>)
).
t_invalid_file(_Config) ->
?assertMatch(
{error, bad_acl_file_content},
emqx_authz:update(?CMD_REPLACE, [?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>}])).
{error, bad_acl_file_content},
emqx_authz:update(?CMD_REPLACE, [?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>}])
).
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(
{error, _},
emqx_authz:update(
{?CMD_REPLACE, file},
?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>})),
{error, _},
emqx_authz:update(
{?CMD_REPLACE, file},
?RAW_SOURCE#{<<"rules">> => <<"{{invalid term">>}
)
),
?assertMatch(
{ok, _},
emqx_authz:update(
{?CMD_REPLACE, file}, ?RAW_SOURCE)).
{ok, _},
emqx_authz:update(
{?CMD_REPLACE, file}, ?RAW_SOURCE
)
).
%%------------------------------------------------------------------------------
%% Helpers
@ -110,8 +128,9 @@ t_update(_Config) ->
setup_config(SpecialParams) ->
emqx_authz_test_lib:setup_config(
?RAW_SOURCE,
SpecialParams).
?RAW_SOURCE,
SpecialParams
).
stop_apps(Apps) ->
lists:foreach(fun application:stop/1, Apps).

View File

@ -32,9 +32,9 @@ all() ->
init_per_suite(Config) ->
ok = stop_apps([emqx_resource, emqx_connector, cowboy]),
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
ok = start_apps([emqx_resource, emqx_connector, cowboy]),
Config.
@ -45,7 +45,6 @@ end_per_suite(_Config) ->
set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
ok.
@ -62,251 +61,301 @@ end_per_testcase(_Case, _Config) ->
%%------------------------------------------------------------------------------
t_response_handling(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
%% OK, get, no body
ok = setup_handler_and_config(
fun(Req0, State) ->
Req = cowboy_req:reply(200, Req0),
{ok, Req, State}
end,
#{}),
fun(Req0, State) ->
Req = cowboy_req:reply(200, Req0),
{ok, Req, State}
end,
#{}
),
allow = emqx_access_control:authorize(ClientInfo, publish, <<"t">>),
%% OK, get, body & headers
ok = setup_handler_and_config(
fun(Req0, State) ->
Req = cowboy_req:reply(
200,
#{<<"content-type">> => <<"text/plain">>},
"Response body",
Req0),
{ok, Req, State}
end,
#{}),
fun(Req0, State) ->
Req = cowboy_req:reply(
200,
#{<<"content-type">> => <<"text/plain">>},
"Response body",
Req0
),
{ok, Req, State}
end,
#{}
),
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
),
%% OK, get, 204
ok = setup_handler_and_config(
fun(Req0, State) ->
Req = cowboy_req:reply(204, Req0),
{ok, Req, State}
end,
#{}),
fun(Req0, State) ->
Req = cowboy_req:reply(204, Req0),
{ok, Req, State}
end,
#{}
),
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
),
%% Not OK, get, 400
ok = setup_handler_and_config(
fun(Req0, State) ->
Req = cowboy_req:reply(400, Req0),
{ok, Req, State}
end,
#{}),
fun(Req0, State) ->
Req = cowboy_req:reply(400, Req0),
{ok, Req, State}
end,
#{}
),
?assertEqual(
deny,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
),
%% Not OK, get, 400 + body & headers
ok = setup_handler_and_config(
fun(Req0, State) ->
Req = cowboy_req:reply(
400,
#{<<"content-type">> => <<"text/plain">>},
"Response body",
Req0),
{ok, Req, State}
end,
#{}),
fun(Req0, State) ->
Req = cowboy_req:reply(
400,
#{<<"content-type">> => <<"text/plain">>},
"Response body",
Req0
),
{ok, Req, State}
end,
#{}
),
?assertEqual(
deny,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
).
t_query_params(_Config) ->
ok = setup_handler_and_config(
fun(Req0, State) ->
#{username := <<"user name">>,
clientid := <<"client id">>,
peerhost := <<"127.0.0.1">>,
proto_name := <<"MQTT">>,
mountpoint := <<"MOUNTPOINT">>,
topic := <<"t">>,
action := <<"publish">>
} = cowboy_req:match_qs(
[username,
clientid,
peerhost,
proto_name,
mountpoint,
topic,
action],
Req0),
Req = cowboy_req:reply(200, Req0),
{ok, Req, State}
end,
#{<<"url">> => <<"http://127.0.0.1:33333/authz/users/?"
"username=${username}&"
"clientid=${clientid}&"
"peerhost=${peerhost}&"
"proto_name=${proto_name}&"
"mountpoint=${mountpoint}&"
"topic=${topic}&"
"action=${action}">>
}),
fun(Req0, State) ->
#{
username := <<"user name">>,
clientid := <<"client id">>,
peerhost := <<"127.0.0.1">>,
proto_name := <<"MQTT">>,
mountpoint := <<"MOUNTPOINT">>,
topic := <<"t">>,
action := <<"publish">>
} = cowboy_req:match_qs(
[
username,
clientid,
peerhost,
proto_name,
mountpoint,
topic,
action
],
Req0
),
Req = cowboy_req:reply(200, Req0),
{ok, Req, State}
end,
#{
<<"url">> => <<
"http://127.0.0.1:33333/authz/users/?"
"username=${username}&"
"clientid=${clientid}&"
"peerhost=${peerhost}&"
"proto_name=${proto_name}&"
"mountpoint=${mountpoint}&"
"topic=${topic}&"
"action=${action}"
>>
}
),
ClientInfo = #{clientid => <<"client id">>,
username => <<"user name">>,
peerhost => {127,0,0,1},
protocol => <<"MQTT">>,
mountpoint => <<"MOUNTPOINT">>,
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"client id">>,
username => <<"user name">>,
peerhost => {127, 0, 0, 1},
protocol => <<"MQTT">>,
mountpoint => <<"MOUNTPOINT">>,
zone => default,
listener => {tcp, default}
},
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
).
t_json_body(_Config) ->
ok = setup_handler_and_config(
fun(Req0, State) ->
?assertEqual(
<<"/authz/users/">>,
cowboy_req:path(Req0)),
fun(Req0, State) ->
?assertEqual(
<<"/authz/users/">>,
cowboy_req:path(Req0)
),
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
?assertMatch(
#{<<"username">> := <<"user name">>,
<<"CLIENT">> := <<"client id">>,
<<"peerhost">> := <<"127.0.0.1">>,
<<"proto_name">> := <<"MQTT">>,
<<"mountpoint">> := <<"MOUNTPOINT">>,
<<"topic">> := <<"t">>,
<<"action">> := <<"publish">>},
jiffy:decode(RawBody, [return_maps])),
?assertMatch(
#{
<<"username">> := <<"user name">>,
<<"CLIENT">> := <<"client id">>,
<<"peerhost">> := <<"127.0.0.1">>,
<<"proto_name">> := <<"MQTT">>,
<<"mountpoint">> := <<"MOUNTPOINT">>,
<<"topic">> := <<"t">>,
<<"action">> := <<"publish">>
},
jiffy:decode(RawBody, [return_maps])
),
Req = cowboy_req:reply(200, Req1),
{ok, Req, State}
end,
#{<<"method">> => <<"post">>,
<<"body">> => #{<<"username">> => <<"${username}">>,
<<"CLIENT">> => <<"${clientid}">>,
<<"peerhost">> => <<"${peerhost}">>,
<<"proto_name">> => <<"${proto_name}">>,
<<"mountpoint">> => <<"${mountpoint}">>,
<<"topic">> => <<"${topic}">>,
<<"action">> => <<"${action}">>}
}),
Req = cowboy_req:reply(200, Req1),
{ok, Req, State}
end,
#{
<<"method">> => <<"post">>,
<<"body">> => #{
<<"username">> => <<"${username}">>,
<<"CLIENT">> => <<"${clientid}">>,
<<"peerhost">> => <<"${peerhost}">>,
<<"proto_name">> => <<"${proto_name}">>,
<<"mountpoint">> => <<"${mountpoint}">>,
<<"topic">> => <<"${topic}">>,
<<"action">> => <<"${action}">>
}
}
),
ClientInfo = #{clientid => <<"client id">>,
username => <<"user name">>,
peerhost => {127,0,0,1},
protocol => <<"MQTT">>,
mountpoint => <<"MOUNTPOINT">>,
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"client id">>,
username => <<"user name">>,
peerhost => {127, 0, 0, 1},
protocol => <<"MQTT">>,
mountpoint => <<"MOUNTPOINT">>,
zone => default,
listener => {tcp, default}
},
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
).
t_form_body(_Config) ->
ok = setup_handler_and_config(
fun(Req0, State) ->
?assertEqual(
<<"/authz/users/">>,
cowboy_req:path(Req0)),
fun(Req0, State) ->
?assertEqual(
<<"/authz/users/">>,
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(
#{<<"username">> := <<"user name">>,
<<"clientid">> := <<"client id">>,
<<"peerhost">> := <<"127.0.0.1">>,
<<"proto_name">> := <<"MQTT">>,
<<"mountpoint">> := <<"MOUNTPOINT">>,
<<"topic">> := <<"t">>,
<<"action">> := <<"publish">>},
jiffy:decode(PostVars, [return_maps])),
?assertMatch(
#{
<<"username">> := <<"user name">>,
<<"clientid">> := <<"client id">>,
<<"peerhost">> := <<"127.0.0.1">>,
<<"proto_name">> := <<"MQTT">>,
<<"mountpoint">> := <<"MOUNTPOINT">>,
<<"topic">> := <<"t">>,
<<"action">> := <<"publish">>
},
jiffy:decode(PostVars, [return_maps])
),
Req = cowboy_req:reply(200, Req1),
{ok, Req, State}
end,
#{<<"method">> => <<"post">>,
<<"body">> => #{<<"username">> => <<"${username}">>,
<<"clientid">> => <<"${clientid}">>,
<<"peerhost">> => <<"${peerhost}">>,
<<"proto_name">> => <<"${proto_name}">>,
<<"mountpoint">> => <<"${mountpoint}">>,
<<"topic">> => <<"${topic}">>,
<<"action">> => <<"${action}">>},
<<"headers">> => #{<<"content-type">> => <<"application/x-www-form-urlencoded">>}
}),
Req = cowboy_req:reply(200, Req1),
{ok, Req, State}
end,
#{
<<"method">> => <<"post">>,
<<"body">> => #{
<<"username">> => <<"${username}">>,
<<"clientid">> => <<"${clientid}">>,
<<"peerhost">> => <<"${peerhost}">>,
<<"proto_name">> => <<"${proto_name}">>,
<<"mountpoint">> => <<"${mountpoint}">>,
<<"topic">> => <<"${topic}">>,
<<"action">> => <<"${action}">>
},
<<"headers">> => #{<<"content-type">> => <<"application/x-www-form-urlencoded">>}
}
),
ClientInfo = #{clientid => <<"client id">>,
username => <<"user name">>,
peerhost => {127,0,0,1},
protocol => <<"MQTT">>,
mountpoint => <<"MOUNTPOINT">>,
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"client id">>,
username => <<"user name">>,
peerhost => {127, 0, 0, 1},
protocol => <<"MQTT">>,
mountpoint => <<"MOUNTPOINT">>,
zone => default,
listener => {tcp, default}
},
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
).
t_create_replace(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
%% Create with valid URL
ok = setup_handler_and_config(
fun(Req0, State) ->
Req = cowboy_req:reply(200, Req0),
{ok, Req, State}
end,
#{<<"url">> =>
<<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>}),
fun(Req0, State) ->
Req = cowboy_req:reply(200, Req0),
{ok, Req, State}
end,
#{
<<"url">> =>
<<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>
}
),
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
),
%% Changing to valid config
OkConfig = maps:merge(
raw_http_authz_config(),
#{<<"url">> =>
<<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>}),
raw_http_authz_config(),
#{
<<"url">> =>
<<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>
}
),
?assertMatch(
{ok, _},
emqx_authz:update({?CMD_REPLACE, http}, OkConfig)),
emqx_authz:update({?CMD_REPLACE, http}, OkConfig)
),
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)).
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
).
%%------------------------------------------------------------------------------
%% Helpers
@ -324,8 +373,9 @@ raw_http_authz_config() ->
setup_handler_and_config(Handler, Config) ->
ok = emqx_authz_http_test_server:set_handler(Handler),
ok = emqx_authz_test_lib:setup_config(
raw_http_authz_config(),
Config).
raw_http_authz_config(),
Config
).
start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -26,10 +26,11 @@
-export([init/1]).
% API
-export([start_link/2,
stop/0,
set_handler/1
]).
-export([
start_link/2,
stop/0,
set_handler/1
]).
%%------------------------------------------------------------------------------
%% API
@ -51,11 +52,14 @@ set_handler(F) when is_function(F, 2) ->
init([Port, Path]) ->
Dispatch = cowboy_router:compile(
[
{'_', [{Path, ?MODULE, []}]}
]),
TransOpts = #{socket_opts => [{port, Port}],
connection_type => supervisor},
[
{'_', [{Path, ?MODULE, []}]}
]
),
TransOpts = #{
socket_opts => [{port, Port}],
connection_type => supervisor
},
ProtoOpts = #{env => #{dispatch => Dispatch}},
Tab = ets:new(?MODULE, [set, named_table, public]),
@ -78,9 +82,9 @@ init(Req, State) ->
default_handler(Req0, State) ->
Req = cowboy_req:reply(
400,
#{<<"content-type">> => <<"text/plain">>},
<<"">>,
Req0),
400,
#{<<"content-type">> => <<"text/plain">>},
<<"">>,
Req0
),
{ok, Req, State}.

View File

@ -29,8 +29,9 @@ groups() ->
init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1),
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
Config.
end_per_suite(_Config) ->
@ -47,7 +48,6 @@ end_per_testcase(_TestCase, _Config) ->
set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
ok.
@ -64,16 +64,17 @@ t_all_topic_rules(_Config) ->
ok = test_topic_rules(all).
test_topic_rules(Key) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
SetupSamples = fun(CInfo, Samples) ->
setup_client_samples(CInfo, Samples, Key)
end,
setup_client_samples(CInfo, Samples, Key)
end,
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, SetupSamples),
@ -82,41 +83,50 @@ test_topic_rules(Key) ->
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, SetupSamples).
t_normalize_rules(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_mnesia:store_rules(
{username, <<"username">>},
[{allow, publish, "t"}]),
{username, <<"username">>},
[{allow, publish, "t"}]
),
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)),
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
),
?assertException(
error,
{invalid_rule, _},
emqx_authz_mnesia:store_rules(
{username, <<"username">>},
[[allow, publish, <<"t">>]])),
error,
{invalid_rule, _},
emqx_authz_mnesia:store_rules(
{username, <<"username">>},
[[allow, publish, <<"t">>]]
)
),
?assertException(
error,
{invalid_rule_action, _},
emqx_authz_mnesia:store_rules(
{username, <<"username">>},
[{allow, pub, <<"t">>}])),
error,
{invalid_rule_action, _},
emqx_authz_mnesia:store_rules(
{username, <<"username">>},
[{allow, pub, <<"t">>}]
)
),
?assertException(
error,
{invalid_rule_permission, _},
emqx_authz_mnesia:store_rules(
{username, <<"username">>},
[{accept, publish, <<"t">>}])).
error,
{invalid_rule_permission, _},
emqx_authz_mnesia:store_rules(
{username, <<"username">>},
[{accept, publish, <<"t">>}]
)
).
%%------------------------------------------------------------------------------
%% Helpers
@ -131,20 +141,23 @@ raw_mnesia_authz_config() ->
setup_client_samples(ClientInfo, Samples, Key) ->
ok = emqx_authz_mnesia:purge_rules(),
Rules = lists:flatmap(
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:map(
fun(Topic) ->
{binary_to_atom(Permission), binary_to_atom(Action), Topic}
end,
Topics)
end,
Samples),
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:map(
fun(Topic) ->
{binary_to_atom(Permission), binary_to_atom(Action), Topic}
end,
Topics
)
end,
Samples
),
#{username := Username, clientid := ClientId} = ClientInfo,
Who = case Key of
username -> {username, Username};
clientid -> {clientid, ClientId};
all -> all
end,
Who =
case Key of
username -> {username, Username};
clientid -> {clientid, ClientId};
all -> all
end,
ok = emqx_authz_mnesia:store_rules(Who, Rules).
setup_config() ->

View File

@ -38,9 +38,9 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
true ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
ok = start_apps([emqx_resource, emqx_connector]),
Config;
false ->
@ -54,7 +54,6 @@ end_per_suite(_Config) ->
set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
ok.
@ -72,12 +71,13 @@ end_per_testcase(_TestCase, _Config) ->
%%------------------------------------------------------------------------------
t_topic_rules(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
@ -87,104 +87,137 @@ t_topic_rules(_Config) ->
t_complex_selector(_) ->
%% atom and string values also supported
ClientInfo = #{clientid => clientid,
username => "username",
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => clientid,
username => "username",
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
Samples = [#{<<"x">> => #{<<"u">> => <<"username">>,
<<"c">> => [#{<<"c">> => <<"clientid">>}],
<<"y">> => 1},
<<"permission">> => <<"allow">>,
<<"action">> => <<"publish">>,
<<"topics">> => [<<"t">>]
}],
Samples = [
#{
<<"x">> => #{
<<"u">> => <<"username">>,
<<"c">> => [#{<<"c">> => <<"clientid">>}],
<<"y">> => 1
},
<<"permission">> => <<"allow">>,
<<"action">> => <<"publish">>,
<<"topics">> => [<<"t">>]
}
],
ok = setup_samples(Samples),
ok = setup_config(
#{<<"selector">> => #{<<"x">> => #{<<"u">> => <<"${username}">>,
<<"c">> => [#{<<"c">> => <<"${clientid}">>}],
<<"y">> => 1}
}
}),
#{
<<"selector">> => #{
<<"x">> => #{
<<"u">> => <<"${username}">>,
<<"c">> => [#{<<"c">> => <<"${clientid}">>}],
<<"y">> => 1
}
}
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{allow, publish, <<"t">>}]).
ClientInfo,
[{allow, publish, <<"t">>}]
).
t_mongo_error(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_samples([]),
ok = setup_config(
#{<<"selector">> => #{<<"$badoperator">> => <<"$badoperator">>}}),
#{<<"selector">> => #{<<"$badoperator">> => <<"$badoperator">>}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{deny, publish, <<"t">>}]).
ClientInfo,
[{deny, publish, <<"t">>}]
).
t_lookups(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ByClientid = #{<<"clientid">> => <<"clientid">>,
<<"topics">> => [<<"a">>],
<<"action">> => <<"all">>,
<<"permission">> => <<"allow">>},
ByClientid = #{
<<"clientid">> => <<"clientid">>,
<<"topics">> => [<<"a">>],
<<"action">> => <<"all">>,
<<"permission">> => <<"allow">>
},
ok = setup_samples([ByClientid]),
ok = setup_config(
#{<<"selector">> => #{<<"clientid">> => <<"${clientid}">>}}),
#{<<"selector">> => #{<<"clientid">> => <<"${clientid}">>}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByPeerhost = #{<<"peerhost">> => <<"127.0.0.1">>,
<<"topics">> => [<<"a">>],
<<"action">> => <<"all">>,
<<"permission">> => <<"allow">>},
ByPeerhost = #{
<<"peerhost">> => <<"127.0.0.1">>,
<<"topics">> => [<<"a">>],
<<"action">> => <<"all">>,
<<"permission">> => <<"allow">>
},
ok = setup_samples([ByPeerhost]),
ok = setup_config(
#{<<"selector">> => #{<<"peerhost">> => <<"${peerhost}">>}}),
#{<<"selector">> => #{<<"peerhost">> => <<"${peerhost}">>}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]).
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
t_bad_selector(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_config(
#{<<"selector">> => #{<<"$in">> => #{<<"a">> => 1}}}),
#{<<"selector">> => #{<<"$in">> => #{<<"a">> => 1}}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{deny, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]).
ClientInfo,
[
{deny, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
%%------------------------------------------------------------------------------
%% Helpers
@ -201,17 +234,22 @@ setup_samples(AclRecords) ->
setup_client_samples(ClientInfo, Samples) ->
#{username := Username} = ClientInfo,
Records = lists:map(
fun(Sample) ->
#{topics := Topics,
permission := Permission,
action := Action} = Sample,
fun(Sample) ->
#{
topics := Topics,
permission := Permission,
action := Action
} = Sample,
#{<<"topics">> => Topics,
<<"permission">> => Permission,
<<"action">> => Action,
<<"username">> => Username}
end,
Samples),
#{
<<"topics">> => Topics,
<<"permission">> => Permission,
<<"action">> => Action,
<<"username">> => Username
}
end,
Samples
),
setup_samples(Records),
setup_config(#{<<"selector">> => #{<<"username">> => <<"${username}">>}}).
@ -221,8 +259,9 @@ reset_samples() ->
setup_config(SpecialParams) ->
emqx_authz_test_lib:setup_config(
raw_mongo_authz_config(),
SpecialParams).
raw_mongo_authz_config(),
SpecialParams
).
raw_mongo_authz_config() ->
#{
@ -238,14 +277,14 @@ raw_mongo_authz_config() ->
}.
mongo_server() ->
iolist_to_binary(io_lib:format("~s",[?MONGO_HOST])).
iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
mongo_config() ->
[
{database, <<"mqtt">>},
{host, ?MONGO_HOST},
{port, ?MONGO_DEFAULT_PORT},
{register, ?MONGO_CLIENT}
{database, <<"mqtt">>},
{host, ?MONGO_HOST},
{port, ?MONGO_DEFAULT_PORT},
{register, ?MONGO_CLIENT}
].
start_apps(Apps) ->

View File

@ -37,16 +37,17 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
true ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
ok = start_apps([emqx_resource, emqx_connector]),
{ok, _} = emqx_resource:create_local(
?MYSQL_RESOURCE,
?RESOURCE_GROUP,
emqx_connector_mysql,
mysql_config(),
#{}),
?MYSQL_RESOURCE,
?RESOURCE_GROUP,
emqx_connector_mysql,
mysql_config(),
#{}
),
Config;
false ->
{skip, no_mysql}
@ -64,7 +65,6 @@ init_per_testcase(_TestCase, Config) ->
set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
ok.
@ -73,12 +73,13 @@ set_special_configs(_) ->
%%------------------------------------------------------------------------------
t_topic_rules(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
@ -86,127 +87,190 @@ t_topic_rules(_Config) ->
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
t_lookups(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
%% by clientid
ok = init_table(),
ok = q(<<"INSERT INTO acl(clientid, topic, permission, action)"
"VALUES(?, ?, ?, ?)">>,
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = q(
<<
"INSERT INTO acl(clientid, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by peerhost
ok = init_table(),
ok = q(<<"INSERT INTO acl(peerhost, topic, permission, action)"
"VALUES(?, ?, ?, ?)">>,
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = q(
<<
"INSERT INTO acl(peerhost, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by cn
ok = init_table(),
ok = q(<<"INSERT INTO acl(cn, topic, permission, action)"
"VALUES(?, ?, ?, ?)">>,
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = q(
<<
"INSERT INTO acl(cn, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by dn
ok = init_table(),
ok = q(<<"INSERT INTO acl(dn, topic, permission, action)"
"VALUES(?, ?, ?, ?)">>,
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = q(
<<
"INSERT INTO acl(dn, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]).
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
t_mysql_error(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_config(
#{<<"query">> => <<"SOME INVALID STATEMENT">>}),
#{<<"query">> => <<"SOME INVALID STATEMENT">>}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{deny, subscribe, <<"a">>}]).
ClientInfo,
[{deny, subscribe, <<"a">>}]
).
t_create_invalid(_Config) ->
BadConfig = maps:merge(
raw_mysql_authz_config(),
#{<<"server">> => <<"255.255.255.255:33333">>}),
raw_mysql_authz_config(),
#{<<"server">> => <<"255.255.255.255:33333">>}
),
{ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
[_] = emqx_authz:lookup().
t_nonbinary_values(_Config) ->
ClientInfo = #{clientid => clientid,
username => "username",
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => clientid,
username => "username",
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = init_table(),
ok = q(<<"INSERT INTO acl(clientid, username, topic, permission, action)"
"VALUES(?, ?, ?, ?, ?)">>,
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = q(
<<
"INSERT INTO acl(clientid, username, topic, permission, action)"
"VALUES(?, ?, ?, ?, ?)"
>>,
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]).
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
%%------------------------------------------------------------------------------
%% Helpers
@ -221,33 +285,39 @@ raw_mysql_authz_config() ->
<<"username">> => <<"root">>,
<<"password">> => <<"public">>,
<<"query">> => <<"SELECT permission, action, topic "
"FROM acl WHERE username = ${username}">>,
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE username = ${username}"
>>,
<<"server">> => mysql_server()
}.
q(Sql) ->
emqx_resource:query(
?MYSQL_RESOURCE,
{sql, Sql}).
?MYSQL_RESOURCE,
{sql, Sql}
).
q(Sql, Params) ->
emqx_resource:query(
?MYSQL_RESOURCE,
{sql, Sql, Params}).
?MYSQL_RESOURCE,
{sql, Sql, Params}
).
init_table() ->
ok = drop_table(),
ok = q("CREATE TABLE acl(
username VARCHAR(255),
clientid VARCHAR(255),
peerhost VARCHAR(255),
cn VARCHAR(255),
dn VARCHAR(255),
topic VARCHAR(255),
permission VARCHAR(255),
action VARCHAR(255))").
ok = q(
"CREATE TABLE acl(\n"
" username VARCHAR(255),\n"
" clientid VARCHAR(255),\n"
" peerhost VARCHAR(255),\n"
" cn VARCHAR(255),\n"
" dn VARCHAR(255),\n"
" topic VARCHAR(255),\n"
" permission VARCHAR(255),\n"
" action VARCHAR(255))"
).
drop_table() ->
ok = q("DROP TABLE IF EXISTS acl").
@ -256,37 +326,50 @@ setup_client_samples(ClientInfo, Samples) ->
#{username := Username} = ClientInfo,
ok = init_table(),
ok = lists:foreach(
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:foreach(
fun(Topic) ->
q(<<"INSERT INTO acl(username, topic, permission, action)"
"VALUES(?, ?, ?, ?)">>,
[Username, Topic, Permission, Action])
end,
Topics)
end,
Samples),
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:foreach(
fun(Topic) ->
q(
<<
"INSERT INTO acl(username, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[Username, Topic, Permission, Action]
)
end,
Topics
)
end,
Samples
),
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) ->
emqx_authz_test_lib:setup_config(
raw_mysql_authz_config(),
SpecialParams).
raw_mysql_authz_config(),
SpecialParams
).
mysql_server() ->
iolist_to_binary(io_lib:format("~s",[?MYSQL_HOST])).
iolist_to_binary(io_lib:format("~s", [?MYSQL_HOST])).
mysql_config() ->
#{auto_reconnect => true,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
pool_size => 8,
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
ssl => #{enable => false}
}.
#{
auto_reconnect => true,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
pool_size => 8,
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
ssl => #{enable => false}
}.
start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -37,16 +37,17 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
true ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
ok = start_apps([emqx_resource, emqx_connector]),
{ok, _} = emqx_resource:create_local(
?PGSQL_RESOURCE,
?RESOURCE_GROUP,
emqx_connector_pgsql,
pgsql_config(),
#{}),
?PGSQL_RESOURCE,
?RESOURCE_GROUP,
emqx_connector_pgsql,
pgsql_config(),
#{}
),
Config;
false ->
{skip, no_pgsql}
@ -64,7 +65,6 @@ init_per_testcase(_TestCase, Config) ->
set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
ok.
@ -73,12 +73,13 @@ set_special_configs(_) ->
%%------------------------------------------------------------------------------
t_topic_rules(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
@ -86,128 +87,195 @@ t_topic_rules(_Config) ->
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
t_lookups(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
%% by clientid
ok = init_table(),
ok = insert(<<"INSERT INTO acl(clientid, topic, permission, action)"
"VALUES($1, $2, $3, $4)">>,
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = insert(
<<
"INSERT INTO acl(clientid, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by peerhost
ok = init_table(),
ok = insert(<<"INSERT INTO acl(peerhost, topic, permission, action)"
"VALUES($1, $2, $3, $4)">>,
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = insert(
<<
"INSERT INTO acl(peerhost, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by cn
ok = init_table(),
ok = insert(<<"INSERT INTO acl(cn, topic, permission, action)"
"VALUES($1, $2, $3, $4)">>,
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = insert(
<<
"INSERT INTO acl(cn, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by dn
ok = init_table(),
ok = insert(<<"INSERT INTO acl(dn, topic, permission, action)"
"VALUES($1, $2, $3, $4)">>,
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = insert(
<<
"INSERT INTO acl(dn, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]).
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
t_pgsql_error(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
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(
ClientInfo,
[{deny, subscribe, <<"a">>}]).
ClientInfo,
[{deny, subscribe, <<"a">>}]
).
t_create_invalid(_Config) ->
BadConfig = maps:merge(
raw_pgsql_authz_config(),
#{<<"server">> => <<"255.255.255.255:33333">>}),
raw_pgsql_authz_config(),
#{<<"server">> => <<"255.255.255.255:33333">>}
),
{ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
[_] = emqx_authz:lookup().
t_nonbinary_values(_Config) ->
ClientInfo = #{clientid => clientid,
username => "username",
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => clientid,
username => "username",
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = init_table(),
ok = insert(<<"INSERT INTO acl(clientid, username, topic, permission, action)"
"VALUES($1, $2, $3, $4, $5)">>,
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]),
ok = insert(
<<
"INSERT INTO acl(clientid, username, topic, permission, action)"
"VALUES($1, $2, $3, $4, $5)"
>>,
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
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(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]).
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
%%------------------------------------------------------------------------------
%% Helpers
@ -222,34 +290,40 @@ raw_pgsql_authz_config() ->
<<"username">> => <<"root">>,
<<"password">> => <<"public">>,
<<"query">> => <<"SELECT permission, action, topic "
"FROM acl WHERE username = ${username}">>,
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE username = ${username}"
>>,
<<"server">> => pgsql_server()
}.
q(Sql) ->
emqx_resource:query(
?PGSQL_RESOURCE,
{query, Sql}).
?PGSQL_RESOURCE,
{query, Sql}
).
insert(Sql, Params) ->
{ok, _} = emqx_resource:query(
?PGSQL_RESOURCE,
{query, Sql, Params}),
?PGSQL_RESOURCE,
{query, Sql, Params}
),
ok.
init_table() ->
ok = drop_table(),
{ok, _, _} = q("CREATE TABLE acl(
username VARCHAR(255),
clientid VARCHAR(255),
peerhost VARCHAR(255),
cn VARCHAR(255),
dn VARCHAR(255),
topic VARCHAR(255),
permission VARCHAR(255),
action VARCHAR(255))"),
{ok, _, _} = q(
"CREATE TABLE acl(\n"
" username VARCHAR(255),\n"
" clientid VARCHAR(255),\n"
" peerhost VARCHAR(255),\n"
" cn VARCHAR(255),\n"
" dn VARCHAR(255),\n"
" topic VARCHAR(255),\n"
" permission VARCHAR(255),\n"
" action VARCHAR(255))"
),
ok.
drop_table() ->
@ -260,37 +334,50 @@ setup_client_samples(ClientInfo, Samples) ->
#{username := Username} = ClientInfo,
ok = init_table(),
ok = lists:foreach(
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:foreach(
fun(Topic) ->
insert(<<"INSERT INTO acl(username, topic, permission, action)"
"VALUES($1, $2, $3, $4)">>,
[Username, Topic, Permission, Action])
end,
Topics)
end,
Samples),
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:foreach(
fun(Topic) ->
insert(
<<
"INSERT INTO acl(username, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[Username, Topic, Permission, Action]
)
end,
Topics
)
end,
Samples
),
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) ->
emqx_authz_test_lib:setup_config(
raw_pgsql_authz_config(),
SpecialParams).
raw_pgsql_authz_config(),
SpecialParams
).
pgsql_server() ->
iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])).
iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
pgsql_config() ->
#{auto_reconnect => true,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
pool_size => 8,
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
ssl => #{enable => false}
}.
#{
auto_reconnect => true,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
pool_size => 8,
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
ssl => #{enable => false}
}.
start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -38,16 +38,17 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_DEFAULT_PORT) of
true ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
ok = start_apps([emqx_resource, emqx_connector]),
{ok, _} = emqx_resource:create_local(
?REDIS_RESOURCE,
?RESOURCE_GROUP,
emqx_connector_redis,
redis_config(),
#{}),
?REDIS_RESOURCE,
?RESOURCE_GROUP,
emqx_connector_redis,
redis_config(),
#{}
),
Config;
false ->
{skip, no_redis}
@ -65,109 +66,130 @@ init_per_testcase(_TestCase, Config) ->
set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
ok.
%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------
t_topic_rules(_Config) ->
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, fun setup_client_samples/2).
t_lookups(_Config) ->
ClientInfo = #{clientid => <<"client id">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"client id">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ByClientid = #{<<"mqtt_user:client id">> =>
#{<<"a">> => <<"all">>}},
ByClientid = #{
<<"mqtt_user:client id">> =>
#{<<"a">> => <<"all">>}
},
ok = setup_sample(ByClientid),
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${clientid}">>}),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByPeerhost = #{<<"mqtt_user:127.0.0.1">> =>
#{<<"a">> => <<"all">>}},
ByPeerhost = #{
<<"mqtt_user:127.0.0.1">> =>
#{<<"a">> => <<"all">>}
},
ok = setup_sample(ByPeerhost),
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${peerhost}">>}),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByCN = #{<<"mqtt_user:cn">> =>
#{<<"a">> => <<"all">>}},
ByCN = #{
<<"mqtt_user:cn">> =>
#{<<"a">> => <<"all">>}
},
ok = setup_sample(ByCN),
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${cert_common_name}">>}),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]),
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByDN = #{<<"mqtt_user:dn">> =>
#{<<"a">> => <<"all">>}},
ByDN = #{
<<"mqtt_user:dn">> =>
#{<<"a">> => <<"all">>}
},
ok = setup_sample(ByDN),
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${cert_subject}">>}),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}]).
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
t_create_invalid(_Config) ->
AuthzConfig = raw_redis_authz_config(),
InvalidConfigs =
[maps:without([<<"server">>], AuthzConfig),
AuthzConfig#{<<"server">> => <<"unknownhost:3333">>},
AuthzConfig#{<<"password">> => <<"wrongpass">>},
AuthzConfig#{<<"database">> => <<"5678">>}],
[
maps:without([<<"server">>], AuthzConfig),
AuthzConfig#{<<"server">> => <<"unknownhost:3333">>},
AuthzConfig#{<<"password">> => <<"wrongpass">>},
AuthzConfig#{<<"database">> => <<"5678">>}
],
lists:foreach(
fun(Config) ->
fun(Config) ->
{ok, _} = emqx_authz:update(?CMD_REPLACE, [Config]),
[_] = emqx_authz:lookup()
end,
InvalidConfigs).
end,
InvalidConfigs
).
t_redis_error(_Config) ->
ok = setup_config(#{<<"cmd">> => <<"INVALID COMMAND">>}),
ClientInfo = #{clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
deny = emqx_access_control:authorize(ClientInfo, subscribe, <<"a">>).
@ -178,30 +200,36 @@ t_redis_error(_Config) ->
setup_sample(AuthzData) ->
{ok, _} = q(["FLUSHDB"]),
ok = lists:foreach(
fun({Key, Values}) ->
lists:foreach(
fun({TopicFilter, Action}) ->
q(["HSET", Key, TopicFilter, Action])
end,
maps:to_list(Values))
end,
maps:to_list(AuthzData)).
fun({Key, Values}) ->
lists:foreach(
fun({TopicFilter, Action}) ->
q(["HSET", Key, TopicFilter, Action])
end,
maps:to_list(Values)
)
end,
maps:to_list(AuthzData)
).
setup_client_samples(ClientInfo, Samples) ->
#{username := Username} = ClientInfo,
Key = <<"mqtt_user:", Username/binary>>,
lists:foreach(
fun(Sample) ->
#{topics := Topics,
fun(Sample) ->
#{
topics := Topics,
permission := <<"allow">>,
action := Action} = Sample,
lists:foreach(
action := Action
} = Sample,
lists:foreach(
fun(Topic) ->
q(["HSET", Key, Topic, Action])
q(["HSET", Key, Topic, Action])
end,
Topics)
end,
Samples),
Topics
)
end,
Samples
),
setup_config(#{}).
setup_config(SpecialParams) ->
@ -221,22 +249,24 @@ raw_redis_authz_config() ->
}.
redis_server() ->
iolist_to_binary(io_lib:format("~s",[?REDIS_HOST])).
iolist_to_binary(io_lib:format("~s", [?REDIS_HOST])).
q(Command) ->
emqx_resource:query(
?REDIS_RESOURCE,
{cmd, Command}).
?REDIS_RESOURCE,
{cmd, Command}
).
redis_config() ->
#{auto_reconnect => true,
database => 1,
pool_size => 8,
redis_type => single,
password => "public",
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
ssl => #{enable => false}
}.
#{
auto_reconnect => true,
database => 1,
pool_size => 8,
redis_type => single,
password => "public",
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
ssl => #{enable => false}
}.
start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -23,30 +23,38 @@
-include_lib("common_test/include/ct.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl").
-define(SOURCE1, {deny, all}).
-define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}).
-define(SOURCE1, {deny, all}).
-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(SOURCE4, {allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}).
-define(SOURCE5, {allow, {'or',
[{username, {re, "^test"}},
{clientid, {re, "test?"}}]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}).
-define(SOURCE5,
{allow,
{'or', [
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
).
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz],
fun set_special_configs/1),
[emqx_conf, emqx_authz],
fun set_special_configs/1
),
Config.
end_per_suite(_Config) ->
{ok, _} = emqx:update_config(
[authorization],
#{<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []}),
[authorization],
#{
<<"no_match">> => <<"allow">>,
<<"cache">> => #{<<"enable">> => <<"true">>},
<<"sources">> => []
}
),
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
ok.
@ -61,115 +69,242 @@ set_special_configs(_App) ->
t_compile(_) ->
?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?SOURCE1)),
?assertEqual({allow, {ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}},
all, [{eq, ['#']}, {eq, ['+']}]}, emqx_authz_rule:compile(?SOURCE2)),
?assertEqual(
{allow, {ipaddr, {{127, 0, 0, 1}, {127, 0, 0, 1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]},
emqx_authz_rule:compile(?SOURCE2)
),
?assertEqual({allow,
{ipaddrs,[{{127,0,0,1},{127,0,0,1},32},
{{192,168,1,0},{192,168,1,255},24}]},
subscribe,
[{pattern,[?PH_CLIENTID]}]
}, emqx_authz_rule:compile(?SOURCE3)),
?assertEqual(
{allow,
{ipaddrs, [
{{127, 0, 0, 1}, {127, 0, 0, 1}, 32},
{{192, 168, 1, 0}, {192, 168, 1, 255}, 24}
]},
subscribe, [{pattern, [?PH_CLIENTID]}]},
emqx_authz_rule:compile(?SOURCE3)
),
?assertMatch({allow,
{'and', [{clientid, {eq, <<"test">>}}, {username, {eq, <<"test">>}}]},
publish,
[[<<"topic">>, <<"test">>]]
}, emqx_authz_rule:compile(?SOURCE4)),
?assertMatch(
{allow, {'and', [{clientid, {eq, <<"test">>}}, {username, {eq, <<"test">>}}]}, publish, [
[<<"topic">>, <<"test">>]
]},
emqx_authz_rule:compile(?SOURCE4)
),
?assertMatch({allow,
{'or', [{username, {re_pattern, _, _, _, _}},
{clientid, {re_pattern, _, _, _, _}}]},
publish, [{pattern, [?PH_USERNAME]}, {pattern, [?PH_CLIENTID]}]
}, emqx_authz_rule:compile(?SOURCE5)),
?assertMatch(
{allow,
{'or', [
{username, {re_pattern, _, _, _, _}},
{clientid, {re_pattern, _, _, _, _}}
]},
publish, [{pattern, [?PH_USERNAME]}, {pattern, [?PH_CLIENTID]}]},
emqx_authz_rule:compile(?SOURCE5)
),
ok.
t_match(_) ->
ClientInfo1 = #{clientid => <<"test">>,
username => <<"test">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo2 = #{clientid => <<"test">>,
username => <<"test">>,
peerhost => {192,168,1,10},
zone => default,
listener => {tcp, default}
},
ClientInfo3 = #{clientid => <<"test">>,
username => <<"fake">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo4 = #{clientid => <<"fake">>,
username => <<"test">>,
peerhost => {127,0,0,1},
zone => default,
listener => {tcp, default}
},
ClientInfo1 = #{
clientid => <<"test">>,
username => <<"test">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ClientInfo2 = #{
clientid => <<"test">>,
username => <<"test">>,
peerhost => {192, 168, 1, 10},
zone => default,
listener => {tcp, default}
},
ClientInfo3 = #{
clientid => <<"test">>,
username => <<"fake">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ClientInfo4 = #{
clientid => <<"fake">>,
username => <<"test">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
?assertEqual({matched, deny},
emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>,
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, deny},
emqx_authz_rule:match(
ClientInfo1,
subscribe,
<<"#">>,
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},
emqx_authz_rule:match(ClientInfo1, 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},
emqx_authz_rule:match(
ClientInfo1,
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},
emqx_authz_rule:match(ClientInfo1, subscribe, <<"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},
emqx_authz_rule:match(
ClientInfo1,
subscribe,
<<"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},
emqx_authz_rule:match(ClientInfo1, publish, <<"topic/test">>,
emqx_authz_rule:compile(?SOURCE4))),
?assertEqual({matched, allow},
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},
emqx_authz_rule:match(
ClientInfo1,
publish,
<<"topic/test">>,
emqx_authz_rule:compile(?SOURCE4)
)
),
?assertEqual(
{matched, allow},
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},
emqx_authz_rule:match(ClientInfo1, publish, <<"test">>,
emqx_authz_rule:compile(?SOURCE5))),
?assertEqual({matched, allow},
emqx_authz_rule:match(ClientInfo2, publish, <<"test">>,
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))),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
ClientInfo1,
publish,
<<"test">>,
emqx_authz_rule:compile(?SOURCE5)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
ClientInfo2,
publish,
<<"test">>,
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.

View File

@ -32,33 +32,40 @@ restore_authorizers() ->
reset_authorizers(Nomatch, ChacheEnabled) ->
{ok, _} = emqx:update_config(
[authorization],
#{<<"no_match">> => atom_to_binary(Nomatch),
<<"cache">> => #{<<"enable">> => atom_to_binary(ChacheEnabled)},
<<"sources">> => []}),
[authorization],
#{
<<"no_match">> => atom_to_binary(Nomatch),
<<"cache">> => #{<<"enable">> => atom_to_binary(ChacheEnabled)},
<<"sources">> => []
}
),
ok.
setup_config(BaseConfig, SpecialParams) ->
Config = maps:merge(BaseConfig, SpecialParams),
case emqx_authz:update(?CMD_REPLACE, [Config]) of
{ok, _} -> ok;
{error, Reason} -> {error, Reason}
{ok, _} -> ok;
{error, Reason} -> {error, Reason}
end.
test_samples(ClientInfo, Samples) ->
lists:foreach(
fun({Expected, Action, Topic}) ->
ct:pal(
fun({Expected, Action, Topic}) ->
ct:pal(
"client_info: ~p, action: ~p, topic: ~p, expected: ~p",
[ClientInfo, Action, Topic, Expected]),
?assertEqual(
Expected,
emqx_access_control:authorize(
ClientInfo,
Action,
Topic))
end,
Samples).
[ClientInfo, Action, Topic, Expected]
),
?assertEqual(
Expected,
emqx_access_control:authorize(
ClientInfo,
Action,
Topic
)
)
end,
Samples
).
test_no_topic_rules(ClientInfo, SetupSamples) ->
%% No rules
@ -67,43 +74,52 @@ test_no_topic_rules(ClientInfo, SetupSamples) ->
ok = SetupSamples(ClientInfo, []),
ok = test_samples(
ClientInfo,
[{deny, subscribe, <<"#">>},
ClientInfo,
[
{deny, subscribe, <<"#">>},
{deny, subscribe, <<"subs">>},
{deny, publish, <<"pub">>}]).
{deny, publish, <<"pub">>}
]
).
test_allow_topic_rules(ClientInfo, SetupSamples) ->
Samples = [#{
topics => [<<"eq testpub1/${username}">>,
<<"testpub2/${clientid}">>,
<<"testpub3/#">>],
permission => <<"allow">>,
action => <<"publish">>
},
#{
topics => [<<"eq testsub1/${username}">>,
<<"testsub2/${clientid}">>,
<<"testsub3/#">>],
permission => <<"allow">>,
action => <<"subscribe">>
},
Samples = [
#{
topics => [
<<"eq testpub1/${username}">>,
<<"testpub2/${clientid}">>,
<<"testpub3/#">>
],
permission => <<"allow">>,
action => <<"publish">>
},
#{
topics => [
<<"eq testsub1/${username}">>,
<<"testsub2/${clientid}">>,
<<"testsub3/#">>
],
permission => <<"allow">>,
action => <<"subscribe">>
},
#{
topics => [<<"eq testall1/${username}">>,
<<"testall2/${clientid}">>,
<<"testall3/#">>],
permission => <<"allow">>,
action => <<"all">>
}
],
#{
topics => [
<<"eq testall1/${username}">>,
<<"testall2/${clientid}">>,
<<"testall3/#">>
],
permission => <<"allow">>,
action => <<"all">>
}
],
ok = reset_authorizers(deny, false),
ok = SetupSamples(ClientInfo, Samples),
ok = test_samples(
ClientInfo,
[
ClientInfo,
[
%% Publish rules
{deny, publish, <<"testpub1/username">>},
@ -114,7 +130,6 @@ test_allow_topic_rules(ClientInfo, SetupSamples) ->
{deny, publish, <<"testpub2/username">>},
{deny, publish, <<"testpub1/clientid">>},
{deny, subscribe, <<"testpub1/username">>},
{deny, subscribe, <<"testpub2/clientid">>},
{deny, subscribe, <<"testpub3/foobar">>},
@ -154,41 +169,47 @@ test_allow_topic_rules(ClientInfo, SetupSamples) ->
{deny, publish, <<"testall2/username">>},
{deny, publish, <<"testall1/clientid">>},
{deny, publish, <<"testall4/foobar">>}
]).
]
).
test_deny_topic_rules(ClientInfo, SetupSamples) ->
Samples = [
#{
topics => [<<"eq testpub1/${username}">>,
<<"testpub2/${clientid}">>,
<<"testpub3/#">>],
permission => <<"deny">>,
action => <<"publish">>
},
#{
topics => [<<"eq testsub1/${username}">>,
<<"testsub2/${clientid}">>,
<<"testsub3/#">>],
permission => <<"deny">>,
action => <<"subscribe">>
},
#{
topics => [
<<"eq testpub1/${username}">>,
<<"testpub2/${clientid}">>,
<<"testpub3/#">>
],
permission => <<"deny">>,
action => <<"publish">>
},
#{
topics => [
<<"eq testsub1/${username}">>,
<<"testsub2/${clientid}">>,
<<"testsub3/#">>
],
permission => <<"deny">>,
action => <<"subscribe">>
},
#{
topics => [<<"eq testall1/${username}">>,
<<"testall2/${clientid}">>,
<<"testall3/#">>],
permission => <<"deny">>,
action => <<"all">>
}
],
#{
topics => [
<<"eq testall1/${username}">>,
<<"testall2/${clientid}">>,
<<"testall3/#">>
],
permission => <<"deny">>,
action => <<"all">>
}
],
ok = reset_authorizers(allow, false),
ok = SetupSamples(ClientInfo, Samples),
ok = test_samples(
ClientInfo,
[
ClientInfo,
[
%% Publish rules
{allow, publish, <<"testpub1/username">>},
@ -199,7 +220,6 @@ test_deny_topic_rules(ClientInfo, SetupSamples) ->
{allow, publish, <<"testpub2/username">>},
{allow, publish, <<"testpub1/clientid">>},
{allow, subscribe, <<"testpub1/username">>},
{allow, subscribe, <<"testpub2/clientid">>},
{allow, subscribe, <<"testpub3/foobar">>},
@ -239,4 +259,5 @@ test_deny_topic_rules(ClientInfo, SetupSamples) ->
{allow, publish, <<"testall2/username">>},
{allow, publish, <<"testall1/clientid">>},
{allow, publish, <<"testall4/foobar">>}
]).
]
).