Merge pull request #6441 from savonarola/refactor-acl
refactor(authz): hide mnesia authz implementation details
This commit is contained in:
commit
5538cd3708
|
@ -8,29 +8,6 @@
|
|||
(A =:= all) orelse (A =:= <<"all">>)
|
||||
)).
|
||||
|
||||
-define(ACL_SHARDED, emqx_acl_sharded).
|
||||
|
||||
-define(ACL_TABLE, emqx_acl).
|
||||
|
||||
%% To save some space, use an integer for label, 0 for 'all', {1, Username} and {2, ClientId}.
|
||||
-define(ACL_TABLE_ALL, 0).
|
||||
-define(ACL_TABLE_USERNAME, 1).
|
||||
-define(ACL_TABLE_CLIENTID, 2).
|
||||
|
||||
-type(action() :: subscribe | publish | all).
|
||||
-type(permission() :: allow | deny).
|
||||
|
||||
-record(emqx_acl, {
|
||||
who :: ?ACL_TABLE_ALL| {?ACL_TABLE_USERNAME, binary()} | {?ACL_TABLE_CLIENTID, binary()},
|
||||
rules :: [ {permission(), action(), emqx_topic:topic()} ]
|
||||
}).
|
||||
|
||||
-record(authz_metrics, {
|
||||
allow = 'client.authorize.allow',
|
||||
deny = 'client.authorize.deny',
|
||||
ignore = 'client.authorize.ignore'
|
||||
}).
|
||||
|
||||
-define(CMD_REPLACE, replace).
|
||||
-define(CMD_DELETE, delete).
|
||||
-define(CMD_PREPEND, prepend).
|
||||
|
@ -42,12 +19,6 @@
|
|||
-define(CMD_MOVE_BEFORE(Before), {<<"before">>, Before}).
|
||||
-define(CMD_MOVE_AFTER(After), {<<"after">>, After}).
|
||||
|
||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||
|
||||
-define(AUTHZ_METRICS, ?METRICS(authz_metrics)).
|
||||
-define(AUTHZ_METRICS(K), ?METRICS(authz_metrics, K)).
|
||||
|
||||
-define(CONF_KEY_PATH, [authorization, sources]).
|
||||
|
||||
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}").
|
||||
|
|
|
@ -53,15 +53,32 @@
|
|||
|
||||
-type(sources() :: [source()]).
|
||||
|
||||
-define(METRIC_ALLOW, 'client.authorize.allow').
|
||||
-define(METRIC_DENY, 'client.authorize.deny').
|
||||
-define(METRIC_NOMATCH, 'client.authorize.nomatch').
|
||||
|
||||
-define(METRICS, [?METRIC_ALLOW, ?METRIC_DENY, ?METRIC_NOMATCH]).
|
||||
|
||||
%% Initialize authz backend.
|
||||
%% Populate the passed configuration map with necessary data,
|
||||
%% like `ResourceID`s
|
||||
-callback(init(source()) -> source()).
|
||||
|
||||
%% Get authz text description.
|
||||
-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).
|
||||
|
||||
%% Check if a configuration map is valid for further
|
||||
%% authz backend initialization.
|
||||
%% The callback must deallocate all resources allocated
|
||||
%% during verification.
|
||||
-callback(dry_run(source()) -> ok | {error, term()}).
|
||||
|
||||
%% Authorize client action.
|
||||
-callback(authorize(
|
||||
emqx_types:clientinfo(),
|
||||
emqx_types:pubsub(),
|
||||
|
@ -70,7 +87,7 @@
|
|||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTHZ_METRICS).
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?METRICS).
|
||||
|
||||
init() ->
|
||||
ok = register_metrics(),
|
||||
|
@ -273,14 +290,14 @@ authorize(#{username := Username,
|
|||
username => Username,
|
||||
ipaddr => IpAddress,
|
||||
topic => Topic}),
|
||||
emqx_metrics:inc(?AUTHZ_METRICS(allow)),
|
||||
emqx_metrics:inc(?METRIC_ALLOW),
|
||||
{stop, allow};
|
||||
{matched, deny} ->
|
||||
?SLOG(info, #{msg => "authorization_permission_denied",
|
||||
username => Username,
|
||||
ipaddr => IpAddress,
|
||||
topic => Topic}),
|
||||
emqx_metrics:inc(?AUTHZ_METRICS(deny)),
|
||||
emqx_metrics:inc(?METRIC_DENY),
|
||||
{stop, deny};
|
||||
nomatch ->
|
||||
?SLOG(info, #{msg => "authorization_failed_nomatch",
|
||||
|
@ -288,6 +305,7 @@ authorize(#{username := Username,
|
|||
ipaddr => IpAddress,
|
||||
topic => Topic,
|
||||
reason => "no-match rule"}),
|
||||
emqx_metrics:inc(?METRIC_NOMATCH),
|
||||
{stop, DefaultResult}
|
||||
end.
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-define(FORMAT_USERNAME_FUN, {?MODULE, format_by_username}).
|
||||
|
@ -269,39 +268,27 @@ fields(meta) ->
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
users(get, #{query_string := PageParams}) ->
|
||||
MatchSpec = ets:fun2ms(
|
||||
fun({?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}, Rules}) ->
|
||||
[{username, Username}, {rules, Rules}]
|
||||
end),
|
||||
{200, emqx_mgmt_api:paginate(?ACL_TABLE, MatchSpec, PageParams, ?FORMAT_USERNAME_FUN)};
|
||||
{Table, MatchSpec} = emqx_authz_mnesia:list_username_rules(),
|
||||
{200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?FORMAT_USERNAME_FUN)};
|
||||
users(post, #{body := Body}) when is_list(Body) ->
|
||||
lists:foreach(fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
|
||||
mria:dirty_write(#emqx_acl{
|
||||
who = {?ACL_TABLE_USERNAME, Username},
|
||||
rules = format_rules(Rules)
|
||||
})
|
||||
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules))
|
||||
end, Body),
|
||||
{204}.
|
||||
|
||||
clients(get, #{query_string := PageParams}) ->
|
||||
MatchSpec = ets:fun2ms(
|
||||
fun({?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}, Rules}) ->
|
||||
[{clientid, Clientid}, {rules, Rules}]
|
||||
end),
|
||||
{200, emqx_mgmt_api:paginate(?ACL_TABLE, MatchSpec, PageParams, ?FORMAT_CLIENTID_FUN)};
|
||||
{Table, MatchSpec} = emqx_authz_mnesia:list_clientid_rules(),
|
||||
{200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?FORMAT_CLIENTID_FUN)};
|
||||
clients(post, #{body := Body}) when is_list(Body) ->
|
||||
lists:foreach(fun(#{<<"clientid">> := Clientid, <<"rules">> := Rules}) ->
|
||||
mria:dirty_write(#emqx_acl{
|
||||
who = {?ACL_TABLE_CLIENTID, Clientid},
|
||||
rules = format_rules(Rules)
|
||||
})
|
||||
emqx_authz_mnesia:store_rules({clientid, Clientid}, format_rules(Rules))
|
||||
end, Body),
|
||||
{204}.
|
||||
|
||||
user(get, #{bindings := #{username := Username}}) ->
|
||||
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}) of
|
||||
[] -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
||||
[#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}] ->
|
||||
case emqx_authz_mnesia:get_rules({username, Username}) of
|
||||
not_found -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
||||
{ok, Rules} ->
|
||||
{200, #{username => Username,
|
||||
rules => [ #{topic => Topic,
|
||||
action => Action,
|
||||
|
@ -311,19 +298,16 @@ user(get, #{bindings := #{username := Username}}) ->
|
|||
end;
|
||||
user(put, #{bindings := #{username := Username},
|
||||
body := #{<<"username">> := Username, <<"rules">> := Rules}}) ->
|
||||
mria:dirty_write(#emqx_acl{
|
||||
who = {?ACL_TABLE_USERNAME, Username},
|
||||
rules = format_rules(Rules)
|
||||
}),
|
||||
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)),
|
||||
{204};
|
||||
user(delete, #{bindings := #{username := Username}}) ->
|
||||
mria:dirty_delete({?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}}),
|
||||
emqx_authz_mnesia:delete_rules({username, Username}),
|
||||
{204}.
|
||||
|
||||
client(get, #{bindings := #{clientid := Clientid}}) ->
|
||||
case mnesia:dirty_read(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}) of
|
||||
[] -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
||||
[#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}] ->
|
||||
case emqx_authz_mnesia:get_rules({clientid, Clientid}) of
|
||||
not_found -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
||||
{ok, Rules} ->
|
||||
{200, #{clientid => Clientid,
|
||||
rules => [ #{topic => Topic,
|
||||
action => Action,
|
||||
|
@ -333,20 +317,17 @@ client(get, #{bindings := #{clientid := Clientid}}) ->
|
|||
end;
|
||||
client(put, #{bindings := #{clientid := Clientid},
|
||||
body := #{<<"clientid">> := Clientid, <<"rules">> := Rules}}) ->
|
||||
mria:dirty_write(#emqx_acl{
|
||||
who = {?ACL_TABLE_CLIENTID, Clientid},
|
||||
rules = format_rules(Rules)
|
||||
}),
|
||||
emqx_authz_mnesia:store_rules({clientid, Clientid}, format_rules(Rules)),
|
||||
{204};
|
||||
client(delete, #{bindings := #{clientid := Clientid}}) ->
|
||||
mria:dirty_delete({?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid}}),
|
||||
emqx_authz_mnesia:delete_rules({clientid, Clientid}),
|
||||
{204}.
|
||||
|
||||
all(get, _) ->
|
||||
case mnesia:dirty_read(?ACL_TABLE, ?ACL_TABLE_ALL) of
|
||||
[] ->
|
||||
case emqx_authz_mnesia:get_rules(all) of
|
||||
not_found ->
|
||||
{200, #{rules => []}};
|
||||
[#emqx_acl{who = ?ACL_TABLE_ALL, rules = Rules}] ->
|
||||
{ok, Rules} ->
|
||||
{200, #{rules => [ #{topic => Topic,
|
||||
action => Action,
|
||||
permission => Permission
|
||||
|
@ -354,18 +335,13 @@ all(get, _) ->
|
|||
}
|
||||
end;
|
||||
all(put, #{body := #{<<"rules">> := Rules}}) ->
|
||||
mria:dirty_write(#emqx_acl{
|
||||
who = ?ACL_TABLE_ALL,
|
||||
rules = format_rules(Rules)
|
||||
}),
|
||||
emqx_authz_mnesia:store_rules(all, format_rules(Rules)),
|
||||
{204}.
|
||||
|
||||
purge(delete, _) ->
|
||||
case emqx_authz_api_sources:get_raw_source(<<"built-in-database">>) of
|
||||
[#{<<"enable">> := false}] ->
|
||||
ok = lists:foreach(fun(Key) ->
|
||||
ok = mria:dirty_delete(?ACL_TABLE, Key)
|
||||
end, mnesia:dirty_all_keys(?ACL_TABLE)),
|
||||
ok = emqx_authz_mnesia:purge_rules(),
|
||||
{204};
|
||||
[#{<<"enable">> := true}] ->
|
||||
{400, #{code => <<"BAD_REQUEST">>,
|
||||
|
|
|
@ -23,12 +23,10 @@
|
|||
|
||||
-behaviour(application).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
ok = mria_rlog:wait_for_shards([?ACL_SHARDED], infinity),
|
||||
ok = emqx_authz_mnesia:init_tables(),
|
||||
{ok, Sup} = emqx_authz_sup:start_link(),
|
||||
ok = emqx_authz:init(),
|
||||
{ok, Sup}.
|
||||
|
@ -36,5 +34,3 @@ start(_StartType, _StartArgs) ->
|
|||
stop(_State) ->
|
||||
ok = emqx_authz:deinit(),
|
||||
ok.
|
||||
|
||||
%% internal functions
|
||||
|
|
|
@ -16,21 +16,53 @@
|
|||
|
||||
-module(emqx_authz_mnesia).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-define(ACL_SHARDED, emqx_acl_sharded).
|
||||
|
||||
-define(ACL_TABLE, emqx_acl).
|
||||
|
||||
%% To save some space, use an integer for label, 0 for 'all', {1, Username} and {2, ClientId}.
|
||||
-define(ACL_TABLE_ALL, 0).
|
||||
-define(ACL_TABLE_USERNAME, 1).
|
||||
-define(ACL_TABLE_CLIENTID, 2).
|
||||
|
||||
-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()]).
|
||||
|
||||
-record(emqx_acl, {
|
||||
who :: ?ACL_TABLE_ALL | {?ACL_TABLE_USERNAME, binary()} | {?ACL_TABLE_CLIENTID, binary()},
|
||||
rules :: rules()
|
||||
}).
|
||||
|
||||
-behaviour(emqx_authz).
|
||||
|
||||
%% AuthZ Callbacks
|
||||
-export([ mnesia/1
|
||||
, description/0
|
||||
-export([ description/0
|
||||
, init/1
|
||||
, destroy/1
|
||||
, dry_run/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
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
@ -47,6 +79,10 @@ mnesia(boot) ->
|
|||
{attributes, record_info(fields, ?ACL_TABLE)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% emqx_authz callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
description() ->
|
||||
"AuthZ with Mnesia".
|
||||
|
||||
|
@ -74,6 +110,78 @@ authorize(#{username := Username,
|
|||
end,
|
||||
do_authorize(Client, PubSub, Topic, Rules).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Management API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init_tables() ->
|
||||
ok = mria_rlog:wait_for_shards([?ACL_SHARDED], infinity).
|
||||
|
||||
-spec(store_rules(who(), rules()) -> ok).
|
||||
store_rules({username, Username}, Rules) ->
|
||||
Record = #emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules},
|
||||
mria:dirty_write(Record);
|
||||
store_rules({clientid, Clientid}, Rules) ->
|
||||
Record = #emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules},
|
||||
mria:dirty_write(Record);
|
||||
store_rules(all, Rules) ->
|
||||
Record = #emqx_acl{who = ?ACL_TABLE_ALL, rules = Rules},
|
||||
mria:dirty_write(Record).
|
||||
|
||||
-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)).
|
||||
|
||||
-spec(get_rules(who()) -> {ok, rules()} | not_found).
|
||||
get_rules({username, Username}) ->
|
||||
do_get_rules({?ACL_TABLE_USERNAME, Username});
|
||||
get_rules({clientid, Clientid}) ->
|
||||
do_get_rules({?ACL_TABLE_CLIENTID, Clientid});
|
||||
get_rules(all) ->
|
||||
do_get_rules(?ACL_TABLE_ALL).
|
||||
|
||||
-spec(delete_rules(who()) -> ok).
|
||||
delete_rules({username, Username}) ->
|
||||
mria:dirty_delete(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username});
|
||||
delete_rules({clientid, Clientid}) ->
|
||||
mria:dirty_delete(?ACL_TABLE, {?ACL_TABLE_CLIENTID, Clientid});
|
||||
delete_rules(all) ->
|
||||
mria:dirty_delete(?ACL_TABLE, ?ACL_TABLE_ALL).
|
||||
|
||||
-spec(list_username_rules() -> {mria:table(), ets:match_spec()}).
|
||||
list_username_rules() ->
|
||||
MatchSpec = ets:fun2ms(
|
||||
fun(#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}) ->
|
||||
[{username, Username}, {rules, Rules}]
|
||||
end),
|
||||
{?ACL_TABLE, MatchSpec}.
|
||||
|
||||
-spec(list_clientid_rules() -> {mria:table(), ets:match_spec()}).
|
||||
list_clientid_rules() ->
|
||||
MatchSpec = ets:fun2ms(
|
||||
fun(#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}) ->
|
||||
[{clientid, Clientid}, {rules, Rules}]
|
||||
end),
|
||||
{?ACL_TABLE, MatchSpec}.
|
||||
|
||||
-spec(record_count() -> non_neg_integer()).
|
||||
record_count() ->
|
||||
mnesia:table_info(?ACL_TABLE, size).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
do_get_rules(Key) ->
|
||||
case mnesia:dirty_read(?ACL_TABLE, Key) of
|
||||
[#emqx_acl{rules = Rules}] -> {ok, Rules};
|
||||
[] -> not_found
|
||||
end.
|
||||
|
||||
do_authorize(_Client, _PubSub, _Topic, []) -> nomatch;
|
||||
do_authorize(Client, PubSub, Topic, [ {Permission, Action, TopicFilter} | Tail]) ->
|
||||
case emqx_authz_rule:match(Client, PubSub, Topic,
|
||||
|
|
|
@ -43,9 +43,14 @@
|
|||
{'or', [ipaddress() | username() | clientid()]} |
|
||||
all).
|
||||
|
||||
-type(action() :: subscribe | publish | all).
|
||||
-type(permission() :: allow | deny).
|
||||
|
||||
-type(rule() :: {permission(), who(), action(), list(emqx_types:topic())}).
|
||||
|
||||
-export_type([rule/0]).
|
||||
-export_type([ action/0
|
||||
, permission/0
|
||||
]).
|
||||
|
||||
compile({Permission, all})
|
||||
when ?ALLOW_DENY(Permission) -> {Permission, all, all, [compile_topic(<<"#">>)]};
|
||||
|
|
|
@ -217,7 +217,7 @@ t_api(_) ->
|
|||
request( delete
|
||||
, uri(["authorization", "sources", "built-in-database", "purge-all"])
|
||||
, []),
|
||||
?assertEqual([], mnesia:dirty_all_keys(?ACL_TABLE)),
|
||||
?assertEqual(0, emqx_authz_mnesia:record_count()),
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -55,24 +55,25 @@ set_special_configs(_App) ->
|
|||
ok.
|
||||
|
||||
init_per_testcase(t_authz, Config) ->
|
||||
mria:dirty_write(#emqx_acl{who = {?ACL_TABLE_USERNAME, <<"test_username">>},
|
||||
rules = [{allow, publish, <<"test/", ?PH_S_USERNAME>>},
|
||||
{allow, subscribe, <<"eq #">>}
|
||||
]
|
||||
}),
|
||||
mria:dirty_write(#emqx_acl{who = {?ACL_TABLE_CLIENTID, <<"test_clientid">>},
|
||||
rules = [{allow, publish, <<"test/", ?PH_S_CLIENTID>>},
|
||||
{deny, subscribe, <<"eq #">>}
|
||||
]
|
||||
}),
|
||||
mria:dirty_write(#emqx_acl{who = ?ACL_TABLE_ALL,
|
||||
rules = [{deny, all, <<"#">>}]
|
||||
}),
|
||||
emqx_authz_mnesia:store_rules(
|
||||
{username, <<"test_username">>},
|
||||
[{allow, publish, <<"test/", ?PH_S_USERNAME>>},
|
||||
{allow, subscribe, <<"eq #">>}]),
|
||||
|
||||
emqx_authz_mnesia:store_rules(
|
||||
{clientid, <<"test_clientid">>},
|
||||
[{allow, publish, <<"test/", ?PH_S_CLIENTID>>},
|
||||
{deny, subscribe, <<"eq #">>}]),
|
||||
|
||||
emqx_authz_mnesia:store_rules(
|
||||
all,
|
||||
[{deny, all, <<"#">>}]),
|
||||
|
||||
Config;
|
||||
init_per_testcase(_, Config) -> Config.
|
||||
|
||||
end_per_testcase(t_authz, Config) ->
|
||||
[ mria:dirty_delete(?ACL_TABLE, K) || K <- mnesia:dirty_all_keys(?ACL_TABLE)],
|
||||
ok = emqx_authz_mnesia:purge_rules(),
|
||||
Config;
|
||||
end_per_testcase(_, Config) -> Config.
|
||||
|
||||
|
|
Loading…
Reference in New Issue