فهرست منبع

feat(authn): support ${cert_pem} placeholder

zmstone 1 سال پیش
والد
کامیت
751f7a24e9

+ 2 - 0
apps/emqx/include/emqx_placeholder.hrl

@@ -28,8 +28,10 @@
 %% cert
 %% cert
 -define(VAR_CERT_SUBJECT, "cert_subject").
 -define(VAR_CERT_SUBJECT, "cert_subject").
 -define(VAR_CERT_CN_NAME, "cert_common_name").
 -define(VAR_CERT_CN_NAME, "cert_common_name").
+-define(VAR_CERT_PEM, "cert_pem").
 -define(PH_CERT_SUBJECT, ?PH(?VAR_CERT_SUBJECT)).
 -define(PH_CERT_SUBJECT, ?PH(?VAR_CERT_SUBJECT)).
 -define(PH_CERT_CN_NAME, ?PH(?VAR_CERT_CN_NAME)).
 -define(PH_CERT_CN_NAME, ?PH(?VAR_CERT_CN_NAME)).
+-define(PH_CERT_PEM, ?PH(?VAR_CERT_PEM)).
 
 
 %% MQTT/Gateway
 %% MQTT/Gateway
 -define(VAR_PASSWORD, "password").
 -define(VAR_PASSWORD, "password").

+ 18 - 4
apps/emqx/src/emqx_channel.erl

@@ -1722,6 +1722,16 @@ count_flapping_event(_ConnPkt, #channel{clientinfo = ClientInfo}) ->
 %%--------------------------------------------------------------------
 %%--------------------------------------------------------------------
 %% Authenticate
 %% Authenticate
 
 
+%% If peercert exists, add it as `cert_pem` credential field.
+maybe_add_cert(Map, #channel{conninfo = ConnInfo}) ->
+    maybe_add_cert(Map, ConnInfo);
+maybe_add_cert(Map, #{peercert := PeerCert}) when is_binary(PeerCert) ->
+    %% NOTE: it's raw binary at this point,
+    %% encoding to PEM (base64) is done lazy in emqx_authn_utils:render_var
+    Map#{cert_pem => PeerCert};
+maybe_add_cert(Map, _) ->
+    Map.
+
 authenticate(
 authenticate(
     ?CONNECT_PACKET(
     ?CONNECT_PACKET(
         #mqtt_packet_connect{
         #mqtt_packet_connect{
@@ -1734,20 +1744,23 @@ authenticate(
         auth_cache = AuthCache
         auth_cache = AuthCache
     } = Channel
     } = Channel
 ) ->
 ) ->
+    %% Auth with CONNECT packet for MQTT v5
     AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
     AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
-    do_authenticate(
+    Credential0 =
         ClientInfo#{
         ClientInfo#{
             auth_method => AuthMethod,
             auth_method => AuthMethod,
             auth_data => AuthData,
             auth_data => AuthData,
             auth_cache => AuthCache
             auth_cache => AuthCache
         },
         },
-        Channel
-    );
+    Credential = maybe_add_cert(Credential0, Channel),
+    do_authenticate(Credential, Channel);
 authenticate(
 authenticate(
     ?CONNECT_PACKET(#mqtt_packet_connect{password = Password}),
     ?CONNECT_PACKET(#mqtt_packet_connect{password = Password}),
     #channel{clientinfo = ClientInfo} = Channel
     #channel{clientinfo = ClientInfo} = Channel
 ) ->
 ) ->
-    do_authenticate(ClientInfo#{password => Password}, Channel);
+    %% Auth with CONNECT packet for MQTT v3
+    Credential = maybe_add_cert(ClientInfo#{password => Password}, Channel),
+    do_authenticate(Credential, Channel);
 authenticate(
 authenticate(
     ?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properties),
     ?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properties),
     #channel{
     #channel{
@@ -1756,6 +1769,7 @@ authenticate(
         auth_cache = AuthCache
         auth_cache = AuthCache
     } = Channel
     } = Channel
 ) ->
 ) ->
+    %% Enhanced auth
     case emqx_mqtt_props:get('Authentication-Method', ConnProps, undefined) of
     case emqx_mqtt_props:get('Authentication-Method', ConnProps, undefined) of
         AuthMethod ->
         AuthMethod ->
             AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
             AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),

+ 3 - 0
apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl

@@ -55,6 +55,7 @@
     ?VAR_PEERHOST,
     ?VAR_PEERHOST,
     ?VAR_CERT_SUBJECT,
     ?VAR_CERT_SUBJECT,
     ?VAR_CERT_CN_NAME,
     ?VAR_CERT_CN_NAME,
+    ?VAR_CERT_PEM,
     ?VAR_NS_CLIENT_ATTRS
     ?VAR_NS_CLIENT_ATTRS
 ]).
 ]).
 
 
@@ -357,6 +358,8 @@ render_var(_, undefined) ->
     % Any allowed but undefined binding will be replaced with empty string, even when
     % Any allowed but undefined binding will be replaced with empty string, even when
     % rendering SQL values.
     % rendering SQL values.
     <<>>;
     <<>>;
+render_var(?VAR_CERT_PEM, Value) ->
+    base64:encode(Value);
 render_var(?VAR_PEERHOST, Value) ->
 render_var(?VAR_PEERHOST, Value) ->
     inet:ntoa(Value);
     inet:ntoa(Value);
 render_var(_Name, Value) ->
 render_var(_Name, Value) ->

+ 9 - 3
apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl

@@ -37,6 +37,7 @@
     protocol => mqtt,
     protocol => mqtt,
     cert_subject => <<"cert_subject_data">>,
     cert_subject => <<"cert_subject_data">>,
     cert_common_name => <<"cert_common_name_data">>,
     cert_common_name => <<"cert_common_name_data">>,
+    cert_pem => <<"fake_raw_cert_to_be_base64_encoded">>,
     client_attrs => #{<<"group">> => <<"g1">>}
     client_attrs => #{<<"group">> => <<"g1">>}
 }).
 }).
 
 
@@ -222,7 +223,8 @@ t_no_value_for_placeholder(_Config) ->
         {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
         {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
         #{
         #{
             <<"cert_subject">> := <<"">>,
             <<"cert_subject">> := <<"">>,
-            <<"cert_common_name">> := <<"">>
+            <<"cert_common_name">> := <<"">>,
+            <<"cert_pem">> := <<"">>
         } = emqx_utils_json:decode(RawBody, [return_maps]),
         } = emqx_utils_json:decode(RawBody, [return_maps]),
         Req = cowboy_req:reply(
         Req = cowboy_req:reply(
             200,
             200,
@@ -238,7 +240,8 @@ t_no_value_for_placeholder(_Config) ->
         <<"headers">> => #{<<"content-type">> => <<"application/json">>},
         <<"headers">> => #{<<"content-type">> => <<"application/json">>},
         <<"body">> => #{
         <<"body">> => #{
             <<"cert_subject">> => ?PH_CERT_SUBJECT,
             <<"cert_subject">> => ?PH_CERT_SUBJECT,
-            <<"cert_common_name">> => ?PH_CERT_CN_NAME
+            <<"cert_common_name">> => ?PH_CERT_CN_NAME,
+            <<"cert_pem">> => ?PH_CERT_PEM
         }
         }
     },
     },
 
 
@@ -251,7 +254,7 @@ t_no_value_for_placeholder(_Config) ->
 
 
     ok = emqx_authn_http_test_server:set_handler(Handler),
     ok = emqx_authn_http_test_server:set_handler(Handler),
 
 
-    Credentials = maps:without([cert_subject, cert_common_name], ?CREDENTIALS),
+    Credentials = maps:without([cert_subject, cert_common_name, cert_pem], ?CREDENTIALS),
 
 
     ?assertMatch({ok, _}, emqx_access_control:authenticate(Credentials)),
     ?assertMatch({ok, _}, emqx_access_control:authenticate(Credentials)),
 
 
@@ -656,8 +659,10 @@ samples() ->
                     <<"peerhost">> := <<"127.0.0.1">>,
                     <<"peerhost">> := <<"127.0.0.1">>,
                     <<"cert_subject">> := <<"cert_subject_data">>,
                     <<"cert_subject">> := <<"cert_subject_data">>,
                     <<"cert_common_name">> := <<"cert_common_name_data">>,
                     <<"cert_common_name">> := <<"cert_common_name_data">>,
+                    <<"cert_pem">> := CertPem,
                     <<"the_group">> := <<"g1">>
                     <<"the_group">> := <<"g1">>
                 } = emqx_utils_json:decode(RawBody, [return_maps]),
                 } = emqx_utils_json:decode(RawBody, [return_maps]),
+                <<"fake_raw_cert_to_be_base64_encoded">> = base64:decode(CertPem),
                 Req = cowboy_req:reply(
                 Req = cowboy_req:reply(
                     200,
                     200,
                     #{<<"content-type">> => <<"application/json">>},
                     #{<<"content-type">> => <<"application/json">>},
@@ -676,6 +681,7 @@ samples() ->
                     <<"peerhost">> => ?PH_PEERHOST,
                     <<"peerhost">> => ?PH_PEERHOST,
                     <<"cert_subject">> => ?PH_CERT_SUBJECT,
                     <<"cert_subject">> => ?PH_CERT_SUBJECT,
                     <<"cert_common_name">> => ?PH_CERT_CN_NAME,
                     <<"cert_common_name">> => ?PH_CERT_CN_NAME,
+                    <<"cert_pem">> => ?PH_CERT_PEM,
                     <<"the_group">> => <<"${client_attrs.group}">>
                     <<"the_group">> => <<"${client_attrs.group}">>
                 }
                 }
             },
             },