feat: support extracting initial client attrs from clientinfo
This commit is contained in:
parent
4f71c97854
commit
2fd0a2cd4d
|
@ -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}) ->
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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).
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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."""
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue