diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 016386011..e3c730cd5 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -56,31 +56,31 @@ authenticate(Credential) -> NotSuperUser = #{is_superuser => false}, case pre_hook_authenticate(Credential) of ok -> - inc_authn_metrics(anonymous), + on_authentication_complete(Credential, NotSuperUser, anonymous), {ok, NotSuperUser}; continue -> case run_hooks('client.authenticate', [Credential], ignore) of ignore -> - inc_authn_metrics(anonymous), + on_authentication_complete(Credential, NotSuperUser, anonymous), {ok, NotSuperUser}; ok -> - inc_authn_metrics(ok), + on_authentication_complete(Credential, NotSuperUser, ok), {ok, NotSuperUser}; - {ok, _AuthResult} = OkResult -> - inc_authn_metrics(ok), + {ok, AuthResult} = OkResult -> + on_authentication_complete(Credential, AuthResult, ok), OkResult; - {ok, _AuthResult, _AuthData} = OkResult -> - inc_authn_metrics(ok), + {ok, AuthResult, _AuthData} = OkResult -> + on_authentication_complete(Credential, AuthResult, ok), OkResult; - {error, _Reason} = Error -> - inc_authn_metrics(error), + {error, Reason} = Error -> + on_authentication_complete(Credential, Reason, error), Error; %% {continue, AuthCache} | {continue, AuthData, AuthCache} Other -> Other end; - {error, _Reason} = Error -> - inc_authn_metrics(error), + {error, Reason} = Error -> + on_authentication_complete(Credential, Reason, error), Error end. @@ -240,3 +240,27 @@ inc_authn_metrics(ok) -> inc_authn_metrics(anonymous) -> emqx_metrics:inc('authentication.success.anonymous'), emqx_metrics:inc('authentication.success'). + +on_authentication_complete(Credential, Reason, error) -> + emqx_hooks:run( + 'client.check_authn_complete', + [ + Credential, + #{ + reason_code => Reason + } + ] + ), + inc_authn_metrics(error); +on_authentication_complete(Credential, Result, Type) -> + emqx_hooks:run( + 'client.check_authn_complete', + [ + Credential, + Result#{ + reason_code => success, + is_anonymous => (Type =:= anonymous) + } + ] + ), + inc_authn_metrics(Type). diff --git a/apps/emqx/src/emqx_hookpoints.erl b/apps/emqx/src/emqx_hookpoints.erl index 0fcf76f3f..e33896719 100644 --- a/apps/emqx/src/emqx_hookpoints.erl +++ b/apps/emqx/src/emqx_hookpoints.erl @@ -44,6 +44,7 @@ 'client.disconnected', 'client.authorize', 'client.check_authz_complete', + 'client.check_authn_complete', 'client.authenticate', 'client.subscribe', 'client.unsubscribe', diff --git a/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl index a9f65b0fa..2450253c1 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl @@ -282,6 +282,18 @@ fields("ctx_check_authz_complete") -> {"authz_source", sc(binary(), #{desc => ?DESC("event_authz_source")})}, {"result", sc(binary(), #{desc => ?DESC("event_result")})} ]; +fields("ctx_check_authn_complete") -> + Event = 'client.check_authn_complete', + [ + {"event_type", event_type_sc(Event)}, + {"event", event_sc(Event)}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"reason_code", sc(binary(), #{desc => ?DESC("event_ctx_authn_reason_code")})}, + {"peername", sc(binary(), #{desc => ?DESC("event_peername")})}, + {"is_anonymous", sc(boolean(), #{desc => ?DESC("event_is_anonymous"), required => false})}, + {"is_superuser", sc(boolean(), #{desc => ?DESC("event_is_superuser"), required => false})} + ]; fields("ctx_bridge_mqtt") -> Event = '$bridges/mqtt:*', EventBin = atom_to_binary(Event), @@ -330,6 +342,7 @@ rule_input_message_context() -> ref("ctx_disconnected"), ref("ctx_connack"), ref("ctx_check_authz_complete"), + ref("ctx_check_authn_complete"), ref("ctx_bridge_mqtt"), ref("ctx_delivery_dropped"), ref("ctx_schema_validation_failed") diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 45085caf8..4f0214a9d 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -40,6 +40,7 @@ on_client_disconnected/4, on_client_connack/4, on_client_check_authz_complete/6, + on_client_check_authn_complete/3, on_session_subscribed/4, on_session_unsubscribed/4, on_message_publish/2, @@ -182,6 +183,18 @@ on_client_check_authz_complete( Conf ). +on_client_check_authn_complete(ClientInfo, Result, Conf) -> + apply_event( + 'client.check_authn_complete', + fun() -> + eventmsg_check_authn_complete( + ClientInfo, + Result + ) + end, + Conf + ). + on_client_disconnected(ClientInfo, Reason, ConnInfo, Conf) -> apply_event( 'client.disconnected', @@ -438,6 +451,35 @@ eventmsg_check_authz_complete( #{} ). +eventmsg_check_authn_complete( + _ClientInfo = #{ + clientid := ClientId, + username := Username, + peerhost := PeerHost, + peerport := PeerPort + }, + Result +) -> + #{ + reason_code := Reason, + is_superuser := IsSuperuser, + is_anonymous := IsAnonymous + } = maps:merge( + #{is_anonymous => false, is_superuser => false}, Result + ), + with_basic_columns( + 'client.check_authn_complete', + #{ + clientid => ClientId, + username => Username, + peername => ntoa({PeerHost, PeerPort}), + reason_code => force_to_bin(Reason), + is_anonymous => IsAnonymous, + is_superuser => IsSuperuser + }, + #{} + ). + eventmsg_sub_or_unsub( Event, _ClientInfo = #{ @@ -679,6 +721,7 @@ event_info() -> event_info_client_disconnected(), event_info_client_connack(), event_info_client_check_authz_complete(), + event_info_client_check_authn_complete(), event_info_session_subscribed(), event_info_session_unsubscribed(), event_info_delivery_dropped(), @@ -770,6 +813,13 @@ event_info_client_check_authz_complete() -> {<<"client check authz complete">>, <<"授权结果"/utf8>>}, <<"SELECT * FROM \"$events/client_check_authz_complete\"">> ). +event_info_client_check_authn_complete() -> + event_info_common( + 'client.check_authn_complete', + {<<"client check authn complete">>, <<"认证结果"/utf8>>}, + {<<"client check authn complete">>, <<"认证结果"/utf8>>}, + <<"SELECT * FROM \"$events/client_check_authn_complete\"">> + ). event_info_session_subscribed() -> event_info_common( 'session.subscribed', @@ -854,6 +904,14 @@ test_columns('client.check_authz_complete') -> {<<"action">>, [<<"publish">>, <<"the action of publish or subscribe">>]}, {<<"result">>, [<<"allow">>, <<"the authz check complete result">>]} ]; +test_columns('client.check_authn_complete') -> + [ + {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]}, + {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]}, + {<<"reason_code">>, [<<"sucess">>, <<"the reason code">>]}, + {<<"is_superuser">>, [true, <<"Whether this is a superuser">>]}, + {<<"is_anonymous">>, [false, <<"Whether this is a superuser">>]} + ]; test_columns('session.unsubscribed') -> test_columns('session.subscribed'); test_columns('session.subscribed') -> @@ -1023,6 +1081,18 @@ columns_with_exam('client.check_authz_complete') -> {<<"timestamp">>, erlang:system_time(millisecond)}, {<<"node">>, node()} ]; +columns_with_exam('client.check_authn_complete') -> + [ + {<<"event">>, 'client.check_authz_complete'}, + {<<"clientid">>, <<"c_emqx">>}, + {<<"username">>, <<"u_emqx">>}, + {<<"peername">>, <<"192.168.0.10:56431">>}, + {<<"reason_code">>, <<"sucess">>}, + {<<"is_superuser">>, true}, + {<<"is_anonymous">>, false}, + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} + ]; columns_with_exam('session.subscribed') -> [columns_example_props(sub_props)] ++ columns_message_sub_unsub('session.subscribed'); columns_with_exam('session.unsubscribed') -> @@ -1124,6 +1194,7 @@ hook_fun('client.connected') -> fun ?MODULE:on_client_connected/3; hook_fun('client.disconnected') -> fun ?MODULE:on_client_disconnected/4; hook_fun('client.connack') -> fun ?MODULE:on_client_connack/4; hook_fun('client.check_authz_complete') -> fun ?MODULE:on_client_check_authz_complete/6; +hook_fun('client.check_authn_complete') -> fun ?MODULE:on_client_check_authn_complete/3; hook_fun('session.subscribed') -> fun ?MODULE:on_session_subscribed/4; hook_fun('session.unsubscribed') -> fun ?MODULE:on_session_unsubscribed/4; hook_fun('message.delivered') -> fun ?MODULE:on_message_delivered/3; @@ -1139,6 +1210,11 @@ reason({shutdown, Reason}) when is_atom(Reason) -> Reason; reason({Error, _}) when is_atom(Error) -> Error; reason(_) -> internal_error. +force_to_bin(Bin) when is_binary(Bin) -> + Bin; +force_to_bin(Term) -> + emqx_utils_conv:bin(io_lib:format("~p", [Term])). + ntoa(undefined) -> undefined; ntoa(IpOrIpPort) -> @@ -1149,6 +1225,7 @@ event_name(<<"$events/client_connected">>) -> 'client.connected'; event_name(<<"$events/client_disconnected">>) -> 'client.disconnected'; event_name(<<"$events/client_connack">>) -> 'client.connack'; event_name(<<"$events/client_check_authz_complete">>) -> 'client.check_authz_complete'; +event_name(<<"$events/client_check_authn_complete">>) -> 'client.check_authn_complete'; event_name(<<"$events/session_subscribed">>) -> 'session.subscribed'; event_name(<<"$events/session_unsubscribed">>) -> 'session.unsubscribed'; event_name(<<"$events/message_delivered">>) -> 'message.delivered'; @@ -1163,6 +1240,7 @@ event_topic('client.connected') -> <<"$events/client_connected">>; event_topic('client.disconnected') -> <<"$events/client_disconnected">>; event_topic('client.connack') -> <<"$events/client_connack">>; event_topic('client.check_authz_complete') -> <<"$events/client_check_authz_complete">>; +event_topic('client.check_authn_complete') -> <<"$events/client_check_authn_complete">>; event_topic('session.subscribed') -> <<"$events/session_subscribed">>; event_topic('session.unsubscribed') -> <<"$events/session_unsubscribed">>; event_topic('message.delivered') -> <<"$events/message_delivered">>; diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index a84ead1c2..395883e7b 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -237,6 +237,7 @@ init_per_testcase(t_events, Config) -> "\"$events/client_disconnected\", " "\"$events/client_connack\", " "\"$events/client_check_authz_complete\", " + "\"$events/client_check_authn_complete\", " "\"$events/session_subscribed\", " "\"$events/session_unsubscribed\", " "\"$events/message_acked\", " @@ -1084,6 +1085,7 @@ client_connected(Client, Client2) -> {ok, _} = emqtt:connect(Client2), verify_event('client.connack'), verify_event('client.connected'), + verify_event('client.check_authn_complete'), ok. client_disconnected(Client, Client2) -> ok = emqtt:disconnect(Client, 0, #{'User-Property' => {<<"reason">>, <<"normal">>}}), @@ -4196,7 +4198,18 @@ verify_event_fields('client.check_authz_complete', Fields) -> ]) ), ?assert(lists:member(ClientId, [<<"c_event">>, <<"c_event2">>])), - ?assert(lists:member(Username, [<<"u_event">>, <<"u_event2">>])). + ?assert(lists:member(Username, [<<"u_event">>, <<"u_event2">>])); +verify_event_fields('client.check_authn_complete', Fields) -> + #{ + clientid := ClientId, + username := Username, + is_anonymous := IsAnonymous, + is_superuser := IsSuperuser + } = Fields, + ?assert(lists:member(ClientId, [<<"c_event">>, <<"c_event2">>])), + ?assert(lists:member(Username, [<<"u_event">>, <<"u_event2">>])), + ?assert(erlang:is_boolean(IsAnonymous)), + ?assert(erlang:is_boolean(IsSuperuser)). verify_peername(PeerName) -> case string:split(PeerName, ":") of diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_api_2_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_api_2_SUITE.erl index f761197f0..7ebb673a8 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_api_2_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_api_2_SUITE.erl @@ -265,6 +265,22 @@ t_rule_test_smoke(_Config) -> <<"sql">> => <<"SELECT\n *\nFROM\n \"t/#\"">> } }, + #{ + expected => #{code => 412}, + input => + #{ + <<"context">> => + #{ + <<"clientid">> => <<"c_emqx">>, + <<"event_type">> => <<"client_check_authn_complete">>, + <<"reason_code">> => <<"sucess">>, + <<"is_superuser">> => true, + <<"is_anonymous">> => false, + <<"username">> => <<"u_emqx">> + }, + <<"sql">> => <<"SELECT\n *\nFROM\n \"t/#\"">> + } + }, #{ expected => #{code => 412}, input => diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_api_rule_test_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_api_rule_test_SUITE.erl index 5282e3e0e..3d3dabae0 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_api_rule_test_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_api_rule_test_SUITE.erl @@ -195,6 +195,29 @@ t_ctx_check_authz_complete(_) -> do_test(SQL, Context, Expected). +t_ctx_check_authn_complete(_) -> + SQL = + << + "SELECT clientid, username, is_superuser, is_anonymous\n" + "FROM \"$events/client_check_authn_complete\"" + >>, + + Context = + #{ + clientid => <<"c_emqx">>, + event_type => client_check_authn_complete, + reason_code => <<"sucess">>, + is_superuser => true, + is_anonymous => false + }, + Expected = check_result( + [clientid, username, is_superuser, is_anonymous], + [], + Context + ), + + do_test(SQL, Context, Expected). + t_ctx_delivery_dropped(_) -> SQL = <<"SELECT from_clientid, from_username, reason, topic, qos FROM \"$events/delivery_dropped\"">>, diff --git a/changes/ce/feat-12983.en.md b/changes/ce/feat-12983.en.md new file mode 100644 index 000000000..b531bfa89 --- /dev/null +++ b/changes/ce/feat-12983.en.md @@ -0,0 +1 @@ +Add new rule engine event `$events/client_check_authn_complete` for authentication completion event. diff --git a/rel/i18n/emqx_rule_api_schema.hocon b/rel/i18n/emqx_rule_api_schema.hocon index 668e2581a..18d0990a2 100644 --- a/rel/i18n/emqx_rule_api_schema.hocon +++ b/rel/i18n/emqx_rule_api_schema.hocon @@ -390,4 +390,13 @@ event_ctx_disconnected_reason.desc: event_ctx_disconnected_reason.label: """Disconnect Reason""" +event_is_anonymous.desc: +"""True if this user is anonymous.""" + +event_is_superuser.desc: +"""True if this is a super user.""" + +event_ctx_authn_reason_code.desc: +"""The reason code""" + }