feat: support extracting initial client attrs from clientinfo
This commit is contained in:
parent
4f71c97854
commit
2fd0a2cd4d
|
@ -245,7 +245,7 @@ init(
|
|||
MP -> MP
|
||||
end,
|
||||
ListenerId = emqx_listeners:listener_id(Type, Listener),
|
||||
ClientInfo = set_peercert_infos(
|
||||
ClientInfo0 = set_peercert_infos(
|
||||
Peercert,
|
||||
#{
|
||||
zone => Zone,
|
||||
|
@ -259,11 +259,11 @@ init(
|
|||
mountpoint => MountPoint,
|
||||
is_bridge => false,
|
||||
is_superuser => false,
|
||||
enable_authn => maps:get(enable_authn, Opts, true),
|
||||
client_attrs => #{}
|
||||
enable_authn => maps:get(enable_authn, Opts, true)
|
||||
},
|
||||
Zone
|
||||
),
|
||||
ClientInfo = initialize_client_attrs_from_cert(ClientInfo0, Peercert, Zone),
|
||||
{NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo),
|
||||
#channel{
|
||||
conninfo = NConnInfo,
|
||||
|
@ -1561,6 +1561,9 @@ enrich_client(ConnPkt, Channel = #channel{clientinfo = ClientInfo}) ->
|
|||
fun set_bridge_mode/2,
|
||||
fun maybe_username_as_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
|
||||
],
|
||||
ConnPkt,
|
||||
|
@ -1573,6 +1576,46 @@ 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;
|
||||
_ ->
|
||||
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(
|
||||
#mqtt_packet_connect{username = Username},
|
||||
ClientInfo = #{username := undefined}
|
||||
|
@ -1613,6 +1656,38 @@ 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, #{}),
|
||||
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}) ->
|
||||
ok;
|
||||
fix_mountpoint(_ConnPkt, ClientInfo = #{mountpoint := MountPoint}) ->
|
||||
|
|
|
@ -1731,7 +1731,30 @@ fields("session_persistence") ->
|
|||
)}
|
||||
];
|
||||
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) ->
|
||||
base_listener(Bind) ++
|
||||
|
@ -3526,6 +3549,14 @@ mqtt_general() ->
|
|||
default => disabled,
|
||||
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.
|
||||
|
|
|
@ -75,6 +75,8 @@ groups() ->
|
|||
{mqttv5, [non_parallel_tests], [t_basic_with_props_v5, t_v5_receive_maximim_in_connack]},
|
||||
{others, [non_parallel_tests], [
|
||||
t_username_as_clientid,
|
||||
t_certcn_as_alias,
|
||||
t_certdn_as_alias,
|
||||
t_certcn_as_clientid_default_config_tls,
|
||||
t_certcn_as_clientid_tlsv1_3,
|
||||
t_certcn_as_clientid_tlsv1_2,
|
||||
|
@ -384,6 +386,32 @@ t_username_as_clientid(_) ->
|
|||
end,
|
||||
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(_) ->
|
||||
tls_certcn_as_clientid(default).
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
-compile(export_all).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("eunit/include/eunit.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
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -544,6 +555,26 @@ t_publish_last_will_testament_denied_topic(_Config) ->
|
|||
|
||||
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,
|
||||
%% and then gets banned and kicked out while connected. Should not
|
||||
%% 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."""
|
||||
|
||||
durable_storage.label:
|
||||
"""Durable storage"""
|
||||
|
||||
durable_storage.desc:
|
||||
"""Configuration related to the EMQX durable storages.
|
||||
durable_storage.label:"Durable storage"
|
||||
durable_storage.desc: """~
|
||||
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