From 8254b801ae6b910de5168e161ff43c88724ac042 Mon Sep 17 00:00:00 2001 From: zmstone Date: Thu, 21 Mar 2024 20:13:41 +0100 Subject: [PATCH] feat: support initialize client attribute from user property --- apps/emqx/src/emqx_channel.erl | 90 ++++++++++++++++++++-------- apps/emqx/src/emqx_schema.erl | 6 +- apps/emqx/test/emqx_client_SUITE.erl | 26 +++++++- rel/i18n/emqx_schema.hocon | 7 ++- 4 files changed, 99 insertions(+), 30 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5d466f098..99a991225 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -263,7 +263,8 @@ init( }, Zone ), - ClientInfo = initialize_client_attrs_from_cert(ClientInfo0, Peercert, Zone), + AttrExtractionConfig = get_mqtt_conf(Zone, client_attrs_init), + ClientInfo = initialize_client_attrs_from_cert(AttrExtractionConfig, ClientInfo0, Peercert), {NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo), #channel{ conninfo = NConnInfo, @@ -1576,30 +1577,31 @@ enrich_client(ConnPkt, Channel = #channel{clientinfo = ClientInfo}) -> {error, ReasonCode, Channel#channel{clientinfo = NClientInfo}} end. -initialize_client_attrs_from_cert(ClientInfo, Peercert, Zone) -> - case get_mqtt_conf(Zone, client_attrs_init) of - #{ - extract_from := From, - extract_regexp := Regexp, - extract_as := AttrName - } when From =:= cn orelse From =:= dn -> - case extract_client_attr_from_cert(From, Regexp, Peercert) of - {ok, Value} -> - ?SLOG( - debug, - #{ - msg => "client_attr_init_from_cert", - extracted_as => AttrName, - extracted_value => Value - } - ), - ClientInfo#{client_attrs => #{AttrName => Value}}; - _ -> - ClientInfo#{client_attrs => #{}} - end; +initialize_client_attrs_from_cert( + #{ + extract_from := From, + extract_regexp := Regexp, + extract_as := AttrName + }, + ClientInfo, + Peercert +) when From =:= cn orelse From =:= dn -> + case extract_client_attr_from_cert(From, Regexp, Peercert) of + {ok, Value} -> + ?SLOG( + debug, + #{ + msg => "client_attr_init_from_cert", + extracted_as => AttrName, + extracted_value => Value + } + ), + ClientInfo#{client_attrs => #{AttrName => Value}}; _ -> ClientInfo#{client_attrs => #{}} - end. + end; +initialize_client_attrs_from_cert(_, ClientInfo, _Peercert) -> + ClientInfo. extract_client_attr_from_cert(cn, Regexp, Peercert) -> CN = esockd_peercert:common_name(Peercert), @@ -1656,9 +1658,10 @@ maybe_assign_clientid(#mqtt_packet_connect{clientid = <<>>}, ClientInfo) -> maybe_assign_clientid(#mqtt_packet_connect{clientid = ClientId}, ClientInfo) -> {ok, ClientInfo#{clientid => ClientId}}. -maybe_set_client_initial_attr(_, #{zone := Zone} = ClientInfo) -> - Attrs = maps:get(client_attrs, ClientInfo, #{}), +maybe_set_client_initial_attr(ConnPkt, #{zone := Zone} = ClientInfo0) -> Config = get_mqtt_conf(Zone, client_attrs_init), + ClientInfo = initialize_client_attrs_from_user_property(Config, ConnPkt, ClientInfo0), + Attrs = maps:get(client_attrs, ClientInfo, #{}), case extract_attr_from_clientinfo(Config, ClientInfo) of {ok, Value} -> #{extract_as := Name} = Config, @@ -1675,6 +1678,43 @@ maybe_set_client_initial_attr(_, #{zone := Zone} = ClientInfo) -> {ok, ClientInfo} end. +initialize_client_attrs_from_user_property( + #{ + extract_from := user_property, + extract_as := PropertyKey + }, + ConnPkt, + ClientInfo +) -> + case extract_client_attr_from_user_property(ConnPkt, PropertyKey) of + {ok, Value} -> + ?SLOG( + debug, + #{ + msg => "client_attr_init_from_user_property", + extracted_as => PropertyKey, + extracted_value => Value + } + ), + ClientInfo#{client_attrs => #{PropertyKey => Value}}; + _ -> + ClientInfo + end; +initialize_client_attrs_from_user_property(_, _ConnInfo, ClientInfo) -> + ClientInfo. + +extract_client_attr_from_user_property( + #mqtt_packet_connect{properties = #{'User-Property' := UserProperty}}, PropertyKey +) -> + case lists:keyfind(PropertyKey, 1, UserProperty) of + {_, Value} -> + {ok, Value}; + _ -> + not_found + end; +extract_client_attr_from_user_property(_ConnPkt, _PropertyKey) -> + ignored. + extract_attr_from_clientinfo(#{extract_from := clientid, extract_regexp := Regexp}, #{ clientid := ClientId }) -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index e71126dca..4311e100e 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -1736,8 +1736,8 @@ fields("client_attrs_init") -> [ {extract_from, sc( - hoconsc:enum([clientid, username, cn, dn]), - #{desc => ?DESC("client_atrs_init_extract_from")} + hoconsc:enum([clientid, username, cn, dn, user_property]), + #{desc => ?DESC("client_attrs_init_extract_from")} )}, {extract_regexp, sc(binary(), #{desc => ?DESC("client_attrs_init_extract_regexp")})}, {extract_as, @@ -2010,6 +2010,8 @@ desc("session_persistence") -> "Settings governing durable sessions persistence."; desc(durable_storage) -> ?DESC(durable_storage); +desc("client_attrs_init") -> + ?DESC(client_attrs_init); desc(_) -> undefined. diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index 077a48593..ba38d92ff 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -77,6 +77,7 @@ groups() -> t_username_as_clientid, t_certcn_as_alias, t_certdn_as_alias, + t_client_attr_from_user_property, t_certcn_as_clientid_default_config_tls, t_certcn_as_clientid_tlsv1_3, t_certcn_as_clientid_tlsv1_2, @@ -408,10 +409,33 @@ test_cert_extraction_as_alias(Which) -> {ok, _} = emqtt:connect(Client), %% assert only two chars are extracted ?assertMatch( - #{clientinfo := #{client_attrs := #{alias := <<_, _>>}}}, emqx_cm:get_chan_info(ClientId) + #{clientinfo := #{client_attrs := #{<<"alias">> := <<_, _>>}}}, + emqx_cm:get_chan_info(ClientId) ), emqtt:disconnect(Client). +t_client_attr_from_user_property(_Config) -> + ClientId = atom_to_binary(?FUNCTION_NAME), + emqx_config:put_zone_conf(default, [mqtt, client_attrs_init], #{ + extract_from => user_property, + extract_as => <<"group">> + }), + SslConf = emqx_common_test_helpers:client_mtls('tlsv1.3'), + {ok, Client} = emqtt:start_link([ + {clientid, ClientId}, + {port, 8883}, + {ssl, true}, + {ssl_opts, SslConf}, + {proto_ver, v5}, + {properties, #{'User-Property' => [{<<"group">>, <<"g1">>}]}} + ]), + {ok, _} = emqtt:connect(Client), + %% assert only two chars are extracted + ?assertMatch( + #{clientinfo := #{client_attrs := #{<<"group">> := <<"g1">>}}}, + emqx_cm:get_chan_info(ClientId) + ), + emqtt:disconnect(Client). t_certcn_as_clientid_default_config_tls(_) -> tls_certcn_as_clientid(default). diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index a90ecdffc..8232b4fe7 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -1592,12 +1592,14 @@ client_attrs_init_extract_from { - `username`: Extract from the username. - `cn`: Extract from the Common Name (CN) field of the client certificate. - `dn`: Extract from the Distinguished Name (DN) field of the client certficate. + - `user_property`: Extract from the user property sent in the MQTT v5 `CONNECT` packet. + In this case, `extract_regex` is not applicable, and `extract_as` should be the user property key. NOTE: this extraction happens **after** `clientid` or `username` is initialized from `peer_cert_as_clientid` or `peer_cert_as_username` config.""" } -client_attrs_init_extract_regex { +client_attrs_init_extract_regexp { label: "Client attribute extract regex" desc: """~ The regular expression to extract a client attribute from the client property specified by `client_attrs_init.extract_from` config. @@ -1610,7 +1612,8 @@ client_attrs_init_extract_as { label: "Name the extracted attribute" desc: """~ The name of the client attribute extracted from the client property specified by `client_attrs_init.extract_from` config. - The extracted attribute will be stored in the `client_attr` property with this name.""" + The extracted attribute will be stored in the `client_attr` property with this name. + In case `extract_from = user_property`, this should be the key of the user property.""" } }