Merge pull request #12191 from zmstone/1217-improve-authz-logging

1217 improve authz logging
This commit is contained in:
Zaiming (Stone) Shi 2023-12-18 12:12:25 +01:00 committed by GitHub
commit e9a91881cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 134 additions and 96 deletions

View File

@ -24,9 +24,13 @@
-define(DEFAULT_ACTION_QOS, 0). -define(DEFAULT_ACTION_QOS, 0).
-define(DEFAULT_ACTION_RETAIN, false). -define(DEFAULT_ACTION_RETAIN, false).
-define(AUTHZ_SUBSCRIBE_MATCH_MAP(QOS), #{action_type := subscribe, qos := QOS}).
-define(AUTHZ_SUBSCRIBE(QOS), #{action_type => subscribe, qos => QOS}). -define(AUTHZ_SUBSCRIBE(QOS), #{action_type => subscribe, qos => QOS}).
-define(AUTHZ_SUBSCRIBE, ?AUTHZ_SUBSCRIBE(?DEFAULT_ACTION_QOS)). -define(AUTHZ_SUBSCRIBE, ?AUTHZ_SUBSCRIBE(?DEFAULT_ACTION_QOS)).
-define(AUTHZ_PUBLISH_MATCH_MAP(QOS, RETAIN), #{
action_type := publish, qos := QOS, retain := RETAIN
}).
-define(AUTHZ_PUBLISH(QOS, RETAIN), #{action_type => publish, qos => QOS, retain => RETAIN}). -define(AUTHZ_PUBLISH(QOS, RETAIN), #{action_type => publish, qos => QOS, retain => RETAIN}).
-define(AUTHZ_PUBLISH(QOS), ?AUTHZ_PUBLISH(QOS, ?DEFAULT_ACTION_RETAIN)). -define(AUTHZ_PUBLISH(QOS), ?AUTHZ_PUBLISH(QOS, ?DEFAULT_ACTION_RETAIN)).
-define(AUTHZ_PUBLISH, ?AUTHZ_PUBLISH(?DEFAULT_ACTION_QOS)). -define(AUTHZ_PUBLISH, ?AUTHZ_PUBLISH(?DEFAULT_ACTION_QOS)).

View File

@ -22,7 +22,8 @@
-export([ -export([
authenticate/1, authenticate/1,
authorize/3 authorize/3,
format_action/1
]). ]).
-ifdef(TEST). -ifdef(TEST).
@ -152,6 +153,7 @@ do_authorize(ClientInfo, Action, Topic) ->
case run_hooks('client.authorize', [ClientInfo, Action, Topic], Default) of case run_hooks('client.authorize', [ClientInfo, Action, Topic], Default) of
AuthzResult = #{result := Result} when Result == allow; Result == deny -> AuthzResult = #{result := Result} when Result == allow; Result == deny ->
From = maps:get(from, AuthzResult, unknown), From = maps:get(from, AuthzResult, unknown),
ok = log_result(ClientInfo, Topic, Action, From, NoMatch),
emqx_hooks:run( emqx_hooks:run(
'client.check_authz_complete', 'client.check_authz_complete',
[ClientInfo, Action, Topic, Result, From] [ClientInfo, Action, Topic, Result, From]
@ -170,6 +172,42 @@ do_authorize(ClientInfo, Action, Topic) ->
deny deny
end. end.
log_result(#{username := Username}, Topic, Action, From, Result) ->
LogMeta = fun() ->
#{
username => Username,
topic => Topic,
action => format_action(Action),
source => format_from(From)
}
end,
case Result of
allow -> ?SLOG(info, (LogMeta())#{msg => "authorization_permission_allowed"});
deny -> ?SLOG(warning, (LogMeta())#{msg => "authorization_permission_denied"})
end.
%% @private Format authorization rules source.
format_from(default) ->
"'authorization.no_match' config";
format_from(unknown) ->
"'client.authorize' hook callback";
format_from(Type) ->
Type.
%% @doc Format enriched action info for logging.
format_action(?AUTHZ_SUBSCRIBE_MATCH_MAP(QoS)) ->
"SUBSCRIBE(" ++ format_qos(QoS) ++ ")";
format_action(?AUTHZ_PUBLISH_MATCH_MAP(QoS, Retain)) ->
"PUBLISH(" ++ format_qos(QoS) ++ "," ++ format_retain_flag(Retain) ++ ")".
format_qos(QoS) ->
"Q" ++ integer_to_list(QoS).
format_retain_flag(true) ->
"R1";
format_retain_flag(false) ->
"R0".
-compile({inline, [run_hooks/3]}). -compile({inline, [run_hooks/3]}).
run_hooks(Name, Args, Acc) -> run_hooks(Name, Args, Acc) ->
ok = emqx_metrics:inc(Name), ok = emqx_metrics:inc(Name),

View File

@ -408,8 +408,7 @@ init_metrics(Source) ->
{stop, #{result => deny, from => ?MODULE}}. {stop, #{result => deny, from => ?MODULE}}.
authorize_deny( authorize_deny(
#{ #{
username := Username, username := Username
peerhost := IpAddress
} = _Client, } = _Client,
_PubSub, _PubSub,
Topic, Topic,
@ -419,13 +418,15 @@ authorize_deny(
?SLOG(warning, #{ ?SLOG(warning, #{
msg => "authorization_not_initialized", msg => "authorization_not_initialized",
username => Username, username => Username,
ipaddr => IpAddress,
topic => Topic, topic => Topic,
source => ?MODULE source => ?MODULE
}), }),
{stop, #{result => deny, from => ?MODULE}}. {stop, #{result => deny, from => ?MODULE}}.
%% @doc Check AuthZ %% @doc Check AuthZ.
%% DefaultResult is always ignored in this callback because the final decision
%% is to be made by `emqx_access_control' module after all authorization
%% sources are exhausted.
-spec authorize( -spec authorize(
emqx_types:clientinfo(), emqx_types:clientinfo(),
emqx_types:pubsub(), emqx_types:pubsub(),
@ -434,77 +435,36 @@ authorize_deny(
sources() sources()
) -> ) ->
authz_result(). authz_result().
authorize( authorize(Client, PubSub, Topic, _DefaultResult, Sources) ->
#{
username := Username,
peerhost := IpAddress
} = Client,
PubSub,
Topic,
DefaultResult,
Sources
) ->
case maps:get(is_superuser, Client, false) of case maps:get(is_superuser, Client, false) of
true -> true ->
log_allowed(#{
username => Username,
ipaddr => IpAddress,
topic => Topic,
is_superuser => true
}),
emqx_metrics:inc(?METRIC_SUPERUSER), emqx_metrics:inc(?METRIC_SUPERUSER),
{stop, #{result => allow, from => superuser}}; {stop, #{result => allow, from => superuser}};
false -> false ->
authorize_non_superuser(Client, PubSub, Topic, DefaultResult, Sources) authorize_non_superuser(Client, PubSub, Topic, Sources)
end. end.
authorize_non_superuser( authorize_non_superuser(Client, PubSub, Topic, Sources) ->
#{
username := Username,
peerhost := IpAddress
} = Client,
PubSub,
Topic,
_DefaultResult,
Sources
) ->
case do_authorize(Client, PubSub, Topic, sources_with_defaults(Sources)) of case do_authorize(Client, PubSub, Topic, sources_with_defaults(Sources)) of
{{matched, allow}, AuthzSource} -> {{matched, allow}, AuthzSource} ->
log_allowed(#{
username => Username,
ipaddr => IpAddress,
topic => Topic,
source => AuthzSource
}),
emqx_metrics_worker:inc(authz_metrics, AuthzSource, allow), emqx_metrics_worker:inc(authz_metrics, AuthzSource, allow),
emqx_metrics:inc(?METRIC_ALLOW), emqx_metrics:inc(?METRIC_ALLOW),
{stop, #{result => allow, from => AuthzSource}}; {stop, #{result => allow, from => source_for_logging(AuthzSource, Client)}};
{{matched, deny}, AuthzSource} -> {{matched, deny}, AuthzSource} ->
?SLOG(warning, #{
msg => "authorization_permission_denied",
username => Username,
ipaddr => IpAddress,
topic => Topic,
source => AuthzSource
}),
emqx_metrics_worker:inc(authz_metrics, AuthzSource, deny), emqx_metrics_worker:inc(authz_metrics, AuthzSource, deny),
emqx_metrics:inc(?METRIC_DENY), emqx_metrics:inc(?METRIC_DENY),
{stop, #{result => deny, from => AuthzSource}}; {stop, #{result => deny, from => source_for_logging(AuthzSource, Client)}};
nomatch -> nomatch ->
?tp(authz_non_superuser, #{result => nomatch}), ?tp(authz_non_superuser, #{result => nomatch}),
?SLOG(info, #{
msg => "authorization_failed_nomatch",
username => Username,
ipaddr => IpAddress,
topic => Topic,
reason => "no-match rule"
}),
emqx_metrics:inc(?METRIC_NOMATCH), emqx_metrics:inc(?METRIC_NOMATCH),
%% return ignore here because there might be other hook callbacks
ignore ignore
end. end.
log_allowed(Meta) -> source_for_logging(client_info, #{acl := Acl}) ->
?SLOG(info, Meta#{msg => "authorization_permission_allowed"}). maps:get(source_for_logging, Acl, client_info);
source_for_logging(Type, _) ->
Type.
do_authorize(_Client, _PubSub, _Topic, []) -> do_authorize(_Client, _PubSub, _Topic, []) ->
nomatch; nomatch;
@ -512,8 +472,7 @@ do_authorize(Client, PubSub, Topic, [#{enable := false} | Rest]) ->
do_authorize(Client, PubSub, Topic, Rest); do_authorize(Client, PubSub, Topic, Rest);
do_authorize( do_authorize(
#{ #{
username := Username, username := Username
peerhost := IpAddress
} = Client, } = Client,
PubSub, PubSub,
Topic, Topic,
@ -527,9 +486,8 @@ do_authorize(
?TRACE("AUTHZ", "authorization_module_nomatch", #{ ?TRACE("AUTHZ", "authorization_module_nomatch", #{
module => Module, module => Module,
username => Username, username => Username,
ipaddr => IpAddress,
topic => Topic, topic => Topic,
pub_sub => PubSub action => emqx_access_control:format_action(PubSub)
}), }),
do_authorize(Client, PubSub, Topic, Tail); do_authorize(Client, PubSub, Topic, Tail);
%% {matched, allow | deny | ignore} %% {matched, allow | deny | ignore}
@ -537,18 +495,16 @@ do_authorize(
?TRACE("AUTHZ", "authorization_module_match_ignore", #{ ?TRACE("AUTHZ", "authorization_module_match_ignore", #{
module => Module, module => Module,
username => Username, username => Username,
ipaddr => IpAddress,
topic => Topic, topic => Topic,
pub_sub => PubSub action => emqx_access_control:format_action(PubSub)
}), }),
do_authorize(Client, PubSub, Topic, Tail); do_authorize(Client, PubSub, Topic, Tail);
ignore -> ignore ->
?TRACE("AUTHZ", "authorization_module_ignore", #{ ?TRACE("AUTHZ", "authorization_module_ignore", #{
module => Module, module => Module,
username => Username, username => Username,
ipaddr => IpAddress,
topic => Topic, topic => Topic,
pub_sub => PubSub action => emqx_access_control:format_action(PubSub)
}), }),
do_authorize(Client, PubSub, Topic, Tail); do_authorize(Client, PubSub, Topic, Tail);
%% {matched, allow | deny} %% {matched, allow | deny}

View File

@ -106,9 +106,15 @@ compile({Permission, Who, Action, TopicFilters}) when
|| Topic <- TopicFilters || Topic <- TopicFilters
]}; ]};
compile({Permission, _Who, _Action, _TopicFilter}) when not ?IS_PERMISSION(Permission) -> compile({Permission, _Who, _Action, _TopicFilter}) when not ?IS_PERMISSION(Permission) ->
throw({invalid_authorization_permission, Permission}); throw(#{
reason => invalid_authorization_permission,
value => Permission
});
compile(BadRule) -> compile(BadRule) ->
throw({invalid_authorization_rule, BadRule}). throw(#{
reason => invalid_authorization_rule,
value => BadRule
}).
compile_action(Action) -> compile_action(Action) ->
compile_action(emqx_authz:feature_available(rich_actions), Action). compile_action(emqx_authz:feature_available(rich_actions), Action).
@ -133,7 +139,10 @@ compile_action(true = _RichActionsOn, {Action, Opts}) when
retain => retain_from_opts(Opts) retain => retain_from_opts(Opts)
}; };
compile_action(_RichActionsOn, Action) -> compile_action(_RichActionsOn, Action) ->
throw({invalid_authorization_action, Action}). throw(#{
reason => invalid_authorization_action,
value => Action
}).
qos_from_opts(Opts) -> qos_from_opts(Opts) ->
try try
@ -152,20 +161,29 @@ qos_from_opts(Opts) ->
) )
end end
catch catch
bad_qos -> {bad_qos, QoS} ->
throw({invalid_authorization_qos, Opts}) throw(#{
reason => invalid_authorization_qos,
qos => QoS
})
end. end.
validate_qos(QoS) when is_integer(QoS), QoS >= 0, QoS =< 2 -> validate_qos(QoS) when is_integer(QoS), QoS >= 0, QoS =< 2 ->
QoS; QoS;
validate_qos(_) -> validate_qos(QoS) ->
throw(bad_qos). throw({bad_qos, QoS}).
retain_from_opts(Opts) -> retain_from_opts(Opts) ->
case proplists:get_value(retain, Opts, ?DEFAULT_RULE_RETAIN) of case proplists:get_value(retain, Opts, ?DEFAULT_RULE_RETAIN) of
all -> all; all ->
Retain when is_boolean(Retain) -> Retain; all;
_ -> throw({invalid_authorization_retain, Opts}) Retain when is_boolean(Retain) ->
Retain;
Value ->
throw(#{
reason => invalid_authorization_retain,
value => Value
})
end. end.
compile_who(all) -> compile_who(all) ->
@ -193,7 +211,10 @@ compile_who({'and', L}) when is_list(L) ->
compile_who({'or', L}) when is_list(L) -> compile_who({'or', L}) when is_list(L) ->
{'or', [compile_who(Who) || Who <- L]}; {'or', [compile_who(Who) || Who <- L]};
compile_who(Who) -> compile_who(Who) ->
throw({invalid_who, Who}). throw(#{
reason => invalid_client_match_condition,
identifier => Who
}).
compile_topic("eq " ++ Topic) -> compile_topic("eq " ++ Topic) ->
{eq, emqx_topic:words(bin(Topic))}; {eq, emqx_topic:words(bin(Topic))};
@ -254,9 +275,17 @@ match_action(#{action_type := subscribe, qos := QoS}, #{action_type := subscribe
match_qos(QoS, QoSCond); match_qos(QoS, QoSCond);
match_action(#{action_type := subscribe, qos := QoS}, #{action_type := all, qos := QoSCond}) -> match_action(#{action_type := subscribe, qos := QoS}, #{action_type := all, qos := QoSCond}) ->
match_qos(QoS, QoSCond); match_qos(QoS, QoSCond);
match_action(_, _) -> match_action(_, PubSubCond) ->
true = is_pubsub_cond(PubSubCond),
false. false.
is_pubsub_cond(publish) ->
true;
is_pubsub_cond(subscribe) ->
true;
is_pubsub_cond(#{action_type := A}) ->
is_pubsub_cond(A).
match_pubsub(publish, publish) -> true; match_pubsub(publish, publish) -> true;
match_pubsub(subscribe, subscribe) -> true; match_pubsub(subscribe, subscribe) -> true;
match_pubsub(_, all) -> true; match_pubsub(_, all) -> true;

View File

@ -37,7 +37,7 @@
emqx_authz_rule:action_condition(), emqx_authz_rule:action_condition(),
emqx_authz_rule:topic_condition() emqx_authz_rule:topic_condition()
}} }}
| {error, term()}. | {error, map()}.
parse_rule( parse_rule(
#{ #{
<<"permission">> := PermissionRaw, <<"permission">> := PermissionRaw,

View File

@ -178,11 +178,13 @@ t_bad_file_source(_) ->
BadContent = ?SOURCE_FILE(<<"{allow,{username,\"bar\"}, publish, [\"test\"]}">>), BadContent = ?SOURCE_FILE(<<"{allow,{username,\"bar\"}, publish, [\"test\"]}">>),
BadContentErr = {bad_acl_file_content, {1, erl_parse, ["syntax error before: ", []]}}, BadContentErr = {bad_acl_file_content, {1, erl_parse, ["syntax error before: ", []]}},
BadRule = ?SOURCE_FILE(<<"{allow,{username,\"bar\"},publish}.">>), BadRule = ?SOURCE_FILE(<<"{allow,{username,\"bar\"},publish}.">>),
BadRuleErr = {invalid_authorization_rule, {allow, {username, "bar"}, publish}}, BadRuleErr = #{
reason => invalid_authorization_rule, value => {allow, {username, "bar"}, publish}
},
BadPermission = ?SOURCE_FILE(<<"{not_allow,{username,\"bar\"},publish,[\"test\"]}.">>), BadPermission = ?SOURCE_FILE(<<"{not_allow,{username,\"bar\"},publish,[\"test\"]}.">>),
BadPermissionErr = {invalid_authorization_permission, not_allow}, BadPermissionErr = #{reason => invalid_authorization_permission, value => not_allow},
BadAction = ?SOURCE_FILE(<<"{allow,{username,\"bar\"},pubsub,[\"test\"]}.">>), BadAction = ?SOURCE_FILE(<<"{allow,{username,\"bar\"},pubsub,[\"test\"]}.">>),
BadActionErr = {invalid_authorization_action, pubsub}, BadActionErr = #{reason => invalid_authorization_action, value => pubsub},
lists:foreach( lists:foreach(
fun({Source, Error}) -> fun({Source, Error}) ->
File = emqx_authz_file:acl_conf_file(), File = emqx_authz_file:acl_conf_file(),

View File

@ -100,7 +100,7 @@ t_rich_actions(_Config) ->
t_no_rich_actions(_Config) -> t_no_rich_actions(_Config) ->
_ = emqx_authz:set_feature_available(rich_actions, false), _ = emqx_authz:set_feature_available(rich_actions, false),
?assertMatch( ?assertMatch(
{error, {pre_config_update, emqx_authz, {invalid_authorization_action, _}}}, {error, {pre_config_update, emqx_authz, #{reason := invalid_authorization_action}}},
emqx_authz:update(?CMD_REPLACE, [ emqx_authz:update(?CMD_REPLACE, [
?RAW_SOURCE#{ ?RAW_SOURCE#{
<<"rules">> => <<"rules">> =>

View File

@ -176,7 +176,7 @@ t_compile_ce(_Config) ->
_ = emqx_authz:set_feature_available(rich_actions, false), _ = emqx_authz:set_feature_available(rich_actions, false),
?assertThrow( ?assertThrow(
{invalid_authorization_action, _}, #{reason := invalid_authorization_action},
emqx_authz_rule:compile( emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]} {allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
) )
@ -676,34 +676,34 @@ t_match(_) ->
t_invalid_rule(_) -> t_invalid_rule(_) ->
?assertThrow( ?assertThrow(
{invalid_authorization_permission, _}, #{reason := invalid_authorization_permission},
emqx_authz_rule:compile({allawww, all, all, ["topic/test"]}) emqx_authz_rule:compile({allawww, all, all, ["topic/test"]})
), ),
?assertThrow( ?assertThrow(
{invalid_authorization_rule, _}, #{reason := invalid_authorization_rule},
emqx_authz_rule:compile(ooops) emqx_authz_rule:compile(ooops)
), ),
?assertThrow( ?assertThrow(
{invalid_authorization_qos, _}, #{reason := invalid_authorization_qos},
emqx_authz_rule:compile({allow, {username, "test"}, {publish, [{qos, 3}]}, ["topic/test"]}) emqx_authz_rule:compile({allow, {username, "test"}, {publish, [{qos, 3}]}, ["topic/test"]})
), ),
?assertThrow( ?assertThrow(
{invalid_authorization_retain, _}, #{reason := invalid_authorization_retain},
emqx_authz_rule:compile( emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{retain, 'FALSE'}]}, ["topic/test"]} {allow, {username, "test"}, {publish, [{retain, 'FALSE'}]}, ["topic/test"]}
) )
), ),
?assertThrow( ?assertThrow(
{invalid_authorization_action, _}, #{reason := invalid_authorization_action},
emqx_authz_rule:compile({allow, all, unsubscribe, ["topic/test"]}) emqx_authz_rule:compile({allow, all, unsubscribe, ["topic/test"]})
), ),
?assertThrow( ?assertThrow(
{invalid_who, _}, #{reason := invalid_client_match_condition},
emqx_authz_rule:compile({allow, who, all, ["topic/test"]}) emqx_authz_rule:compile({allow, who, all, ["topic/test"]})
). ).

View File

@ -61,10 +61,8 @@ kmg(Byte) ->
kmg(F, S) -> kmg(F, S) ->
iolist_to_binary(io_lib:format("~.2f~ts", [F, S])). iolist_to_binary(io_lib:format("~.2f~ts", [F, S])).
ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) -> ntoa(Ip) ->
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); emqx_utils:ntoa(Ip).
ntoa(IP) ->
inet_parse:ntoa(IP).
merge_maps(Default, New) -> merge_maps(Default, New) ->
maps:fold( maps:fold(

View File

@ -1039,9 +1039,10 @@ reason({shutdown, Reason}) when is_atom(Reason) -> Reason;
reason({Error, _}) when is_atom(Error) -> Error; reason({Error, _}) when is_atom(Error) -> Error;
reason(_) -> internal_error. reason(_) -> internal_error.
ntoa(undefined) -> undefined; ntoa(undefined) ->
ntoa({IpAddr, Port}) -> iolist_to_binary([inet:ntoa(IpAddr), ":", integer_to_list(Port)]); undefined;
ntoa(IpAddr) -> iolist_to_binary(inet:ntoa(IpAddr)). ntoa(IpOrIpPort) ->
iolist_to_binary(emqx_utils:ntoa(IpOrIpPort)).
event_name(?BRIDGE_HOOKPOINT(_) = Bridge) -> Bridge; event_name(?BRIDGE_HOOKPOINT(_) = Bridge) -> Bridge;
event_name(<<"$events/client_connected">>) -> 'client.connected'; event_name(<<"$events/client_connected">>) -> 'client.connected';

View File

@ -64,7 +64,8 @@
tcp_keepalive_opts/4, tcp_keepalive_opts/4,
format/1, format/1,
format_mfal/1, format_mfal/1,
call_first_defined/1 call_first_defined/1,
ntoa/1
]). ]).
-export([ -export([
@ -1031,6 +1032,15 @@ flatcomb(Ys = [_ | _], Zs = [_ | _]) ->
flatcomb(Y, Zs) -> flatcomb(Y, Zs) ->
[Y | Zs]. [Y | Zs].
%% @doc Format IP address tuple or {IP, Port} tuple to string.
ntoa({IP, Port}) ->
ntoa(IP) ++ ":" ++ integer_to_list(Port);
ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) ->
%% v6 piggyback v4
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
ntoa(IP) ->
inet_parse:ntoa(IP).
-ifdef(TEST). -ifdef(TEST).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").