feat: support initialize client attribute from user property

This commit is contained in:
zmstone 2024-03-21 20:13:41 +01:00
parent 2fd0a2cd4d
commit 8254b801ae
4 changed files with 99 additions and 30 deletions

View File

@ -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
}) ->

View File

@ -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.

View File

@ -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).

View File

@ -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."""
}
}