feat: support extracting initial client attrs from clientinfo

This commit is contained in:
zmstone 2024-03-20 17:04:20 +01:00
parent 4f71c97854
commit 2fd0a2cd4d
5 changed files with 214 additions and 9 deletions

View File

@ -245,7 +245,7 @@ init(
MP -> MP MP -> MP
end, end,
ListenerId = emqx_listeners:listener_id(Type, Listener), ListenerId = emqx_listeners:listener_id(Type, Listener),
ClientInfo = set_peercert_infos( ClientInfo0 = set_peercert_infos(
Peercert, Peercert,
#{ #{
zone => Zone, zone => Zone,
@ -259,11 +259,11 @@ init(
mountpoint => MountPoint, mountpoint => MountPoint,
is_bridge => false, is_bridge => false,
is_superuser => false, is_superuser => false,
enable_authn => maps:get(enable_authn, Opts, true), enable_authn => maps:get(enable_authn, Opts, true)
client_attrs => #{}
}, },
Zone Zone
), ),
ClientInfo = initialize_client_attrs_from_cert(ClientInfo0, Peercert, Zone),
{NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo), {NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo),
#channel{ #channel{
conninfo = NConnInfo, conninfo = NConnInfo,
@ -1561,6 +1561,9 @@ enrich_client(ConnPkt, Channel = #channel{clientinfo = ClientInfo}) ->
fun set_bridge_mode/2, fun set_bridge_mode/2,
fun maybe_username_as_clientid/2, fun maybe_username_as_clientid/2,
fun maybe_assign_clientid/2, fun maybe_assign_clientid/2,
%% attr init should happen after clientid and username assign
fun maybe_set_client_initial_attr/2,
%% moutpoint fix should happen after attr init
fun fix_mountpoint/2 fun fix_mountpoint/2
], ],
ConnPkt, ConnPkt,
@ -1573,6 +1576,46 @@ enrich_client(ConnPkt, Channel = #channel{clientinfo = ClientInfo}) ->
{error, ReasonCode, Channel#channel{clientinfo = NClientInfo}} {error, ReasonCode, Channel#channel{clientinfo = NClientInfo}}
end. 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;
_ ->
ClientInfo#{client_attrs => #{}}
end.
extract_client_attr_from_cert(cn, Regexp, Peercert) ->
CN = esockd_peercert:common_name(Peercert),
re_extract(CN, Regexp);
extract_client_attr_from_cert(dn, Regexp, Peercert) ->
DN = esockd_peercert:subject(Peercert),
re_extract(DN, Regexp).
re_extract(Str, Regexp) when is_binary(Str) ->
case re:run(Str, Regexp, [{capture, all_but_first, list}]) of
{match, [_ | _] = List} -> {ok, iolist_to_binary(List)};
_ -> nomatch
end;
re_extract(_NotStr, _Regexp) ->
ignored.
set_username( set_username(
#mqtt_packet_connect{username = Username}, #mqtt_packet_connect{username = Username},
ClientInfo = #{username := undefined} ClientInfo = #{username := undefined}
@ -1613,6 +1656,38 @@ maybe_assign_clientid(#mqtt_packet_connect{clientid = <<>>}, ClientInfo) ->
maybe_assign_clientid(#mqtt_packet_connect{clientid = ClientId}, ClientInfo) -> maybe_assign_clientid(#mqtt_packet_connect{clientid = ClientId}, ClientInfo) ->
{ok, ClientInfo#{clientid => ClientId}}. {ok, ClientInfo#{clientid => ClientId}}.
maybe_set_client_initial_attr(_, #{zone := Zone} = ClientInfo) ->
Attrs = maps:get(client_attrs, ClientInfo, #{}),
Config = get_mqtt_conf(Zone, client_attrs_init),
case extract_attr_from_clientinfo(Config, ClientInfo) of
{ok, Value} ->
#{extract_as := Name} = Config,
?SLOG(
debug,
#{
msg => "client_attr_init_from_clientinfo",
extracted_as => Name,
extracted_value => Value
}
),
{ok, ClientInfo#{client_attrs => Attrs#{Name => Value}}};
_ ->
{ok, ClientInfo}
end.
extract_attr_from_clientinfo(#{extract_from := clientid, extract_regexp := Regexp}, #{
clientid := ClientId
}) ->
re_extract(ClientId, Regexp);
extract_attr_from_clientinfo(#{extract_from := username, extract_regexp := Regexp}, #{
username := Username
}) when
Username =/= undefined
->
re_extract(Username, Regexp);
extract_attr_from_clientinfo(_Config, _CLientInfo) ->
ignored.
fix_mountpoint(_ConnPkt, #{mountpoint := undefined}) -> fix_mountpoint(_ConnPkt, #{mountpoint := undefined}) ->
ok; ok;
fix_mountpoint(_ConnPkt, ClientInfo = #{mountpoint := MountPoint}) -> fix_mountpoint(_ConnPkt, ClientInfo = #{mountpoint := MountPoint}) ->

View File

@ -1731,7 +1731,30 @@ fields("session_persistence") ->
)} )}
]; ];
fields(durable_storage) -> fields(durable_storage) ->
emqx_ds_schema:schema(). emqx_ds_schema:schema();
fields("client_attrs_init") ->
[
{extract_from,
sc(
hoconsc:enum([clientid, username, cn, dn]),
#{desc => ?DESC("client_atrs_init_extract_from")}
)},
{extract_regexp, sc(binary(), #{desc => ?DESC("client_attrs_init_extract_regexp")})},
{extract_as,
sc(binary(), #{
default => <<"alias">>,
desc => ?DESC("client_attrs_init_extract_as"),
validator => fun restricted_string/1
})}
].
restricted_string(undefined) ->
undefined;
restricted_string(Str) ->
case emqx_utils:is_restricted_str(Str) of
true -> ok;
false -> {error, <<"Invalid string for attribute name">>}
end.
mqtt_listener(Bind) -> mqtt_listener(Bind) ->
base_listener(Bind) ++ base_listener(Bind) ++
@ -3526,6 +3549,14 @@ mqtt_general() ->
default => disabled, default => disabled,
desc => ?DESC(mqtt_peer_cert_as_clientid) desc => ?DESC(mqtt_peer_cert_as_clientid)
} }
)},
{"client_attrs_init",
sc(
hoconsc:union([disabled, ref("client_attrs_init")]),
#{
default => disabled,
desc => ?DESC("client_attrs_init")
}
)} )}
]. ].
%% All session's importance should be lower than general part to organize document. %% All session's importance should be lower than general part to organize document.

View File

@ -75,6 +75,8 @@ groups() ->
{mqttv5, [non_parallel_tests], [t_basic_with_props_v5, t_v5_receive_maximim_in_connack]}, {mqttv5, [non_parallel_tests], [t_basic_with_props_v5, t_v5_receive_maximim_in_connack]},
{others, [non_parallel_tests], [ {others, [non_parallel_tests], [
t_username_as_clientid, t_username_as_clientid,
t_certcn_as_alias,
t_certdn_as_alias,
t_certcn_as_clientid_default_config_tls, t_certcn_as_clientid_default_config_tls,
t_certcn_as_clientid_tlsv1_3, t_certcn_as_clientid_tlsv1_3,
t_certcn_as_clientid_tlsv1_2, t_certcn_as_clientid_tlsv1_2,
@ -384,6 +386,32 @@ t_username_as_clientid(_) ->
end, end,
emqtt:disconnect(C). emqtt:disconnect(C).
t_certcn_as_alias(_) ->
test_cert_extraction_as_alias(cn).
t_certdn_as_alias(_) ->
test_cert_extraction_as_alias(dn).
test_cert_extraction_as_alias(Which) ->
%% extract the first two chars
Re = <<"^(..).*$">>,
ClientId = iolist_to_binary(["ClientIdFor_", atom_to_list(Which)]),
emqx_config:put_zone_conf(default, [mqtt, client_attrs_init], #{
extract_from => Which,
extract_regexp => Re,
extract_as => <<"alias">>
}),
SslConf = emqx_common_test_helpers:client_mtls('tlsv1.2'),
{ok, Client} = emqtt:start_link([
{clientid, ClientId}, {port, 8883}, {ssl, true}, {ssl_opts, SslConf}
]),
{ok, _} = emqtt:connect(Client),
%% assert only two chars are extracted
?assertMatch(
#{clientinfo := #{client_attrs := #{alias := <<_, _>>}}}, emqx_cm:get_chan_info(ClientId)
),
emqtt:disconnect(Client).
t_certcn_as_clientid_default_config_tls(_) -> t_certcn_as_clientid_default_config_tls(_) ->
tls_certcn_as_clientid(default). tls_certcn_as_clientid(default).

View File

@ -19,6 +19,7 @@
-compile(export_all). -compile(export_all).
-include("emqx_authz.hrl"). -include("emqx_authz.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
@ -168,6 +169,16 @@ end_per_testcase(_TestCase, _Config) ->
) )
). ).
%% Allow all clients to publish or subscribe to topics with their alias as prefix.
-define(SOURCE_FILE_CLIENT_ATTR,
?SOURCE_FILE(
<<
"{allow,all,all,[\"${client_attrs.alias}/#\"]}.\n"
"{deny, all}."
>>
)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -544,6 +555,26 @@ t_publish_last_will_testament_denied_topic(_Config) ->
ok. ok.
t_alias_prefix(_Config) ->
{ok, _} = emqx_authz:update(?CMD_REPLACE, [?SOURCE_FILE_CLIENT_ATTR]),
ExtractSuffix = <<"^.*-(.*)$">>,
emqx_config:put_zone_conf(default, [mqtt, client_attrs_init], #{
extract_from => clientid,
extract_regexp => ExtractSuffix,
extract_as => <<"alias">>
}),
ClientId = <<"org1-name2">>,
SubTopic = <<"name2/#">>,
SubTopicNotAllowed = <<"name3/#">>,
{ok, C} = emqtt:start_link([{clientid, ClientId}, {proto_ver, v5}]),
?assertMatch({ok, _}, emqtt:connect(C)),
?assertMatch({ok, _, [?RC_SUCCESS]}, emqtt:subscribe(C, SubTopic)),
?assertMatch({ok, _, [?RC_NOT_AUTHORIZED]}, emqtt:subscribe(C, SubTopicNotAllowed)),
unlink(C),
emqtt:stop(C),
emqx_config:put_zone_conf(default, [mqtt, client_attrs_init], disalbed),
ok.
%% client is allowed by ACL to publish to its LWT topic, is connected, %% client is allowed by ACL to publish to its LWT topic, is connected,
%% and then gets banned and kicked out while connected. Should not %% and then gets banned and kicked out while connected. Should not
%% publish LWT. %% publish LWT.

View File

@ -1565,12 +1565,52 @@ This value specifies size of the batch.
Note: larger batches generally improve the throughput and overall performance of the system, but increase RAM usage per client.""" Note: larger batches generally improve the throughput and overall performance of the system, but increase RAM usage per client."""
durable_storage.label: durable_storage.label:"Durable storage"
"""Durable storage""" durable_storage.desc: """~
Configuration related to the EMQX durable storages.
durable_storage.desc: EMQX uses durable storages to offload various data, such as MQTT messages, to disc."""
"""Configuration related to the EMQX durable storages.
EMQX uses durable storages to offload various data, such as MQTT messages, to disc.""" client_attrs_init {
label: "Client attributes init"
desc: """~
Specify how to initialize client attributes.
One initial client attribute can be initialized as `client_attrs.NAME`,
where `NAME` is the name of the attribute specified in the config `extract_as`.
The initialized client attribute will be stored in the `client_attr` property with the specified name,
and can be used as a placeholder in a template.
For example, `${client_attrs.alias}` if `extract_as` is set to `alias`."""
}
client_attrs_init_extract_from {
label: "Client property to extract attribute from"
desc: """~
Specify from which client property the client attribute should be extracted.
Supported values:
- `clientid`: Extract from the client ID.
- `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.
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 {
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.
The expression should match the entire client property value, and capturing groups are concatenated to make the client attribute.
For example if the client attribute is the first part of the client ID delemited by a dash, the regular expression would be `^(.+?)-.*$`.
Note that failure to match the regular expression will result in the client attribute being absence but not an empty string."""
}
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."""
}
} }