From f9d51484cc81b17f27e7483c0a7a672f88eec5e2 Mon Sep 17 00:00:00 2001 From: zmstone Date: Sat, 16 Mar 2024 14:24:45 +0100 Subject: [PATCH 01/16] feat: introuce attrs field in clientinfo the 'attrs' field is to be used as extra attributes extracted from various client properties for example, HTTP auth result can return an extra client_attributes field in the result which will get merged into the original attrs field in clientinfo --- apps/emqx/src/emqx_channel.erl | 3 ++- apps/emqx/src/emqx_cm.erl | 2 +- apps/emqx/src/emqx_types.erl | 10 +++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index a3480e232..1f51c15b1 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -259,7 +259,8 @@ init( mountpoint => MountPoint, is_bridge => false, is_superuser => false, - enable_authn => maps:get(enable_authn, Opts, true) + enable_authn => maps:get(enable_authn, Opts, true), + attrs => #{} }, Zone ), diff --git a/apps/emqx/src/emqx_cm.erl b/apps/emqx/src/emqx_cm.erl index c14058f9a..ac41c2da4 100644 --- a/apps/emqx/src/emqx_cm.erl +++ b/apps/emqx/src/emqx_cm.erl @@ -221,7 +221,7 @@ get_chan_info(ClientId, ChanPid) -> wrap_rpc(emqx_cm_proto_v2:get_chan_info(ClientId, ChanPid)). %% @doc Update infos of the channel. --spec set_chan_info(emqx_types:clientid(), emqx_types:attrs()) -> boolean(). +-spec set_chan_info(emqx_types:clientid(), emqx_types:channel_attrs()) -> boolean(). set_chan_info(ClientId, Info) when ?IS_CLIENTID(ClientId) -> Chan = {ClientId, self()}, try diff --git a/apps/emqx/src/emqx_types.erl b/apps/emqx/src/emqx_types.erl index 99a71e20b..3883991b4 100644 --- a/apps/emqx/src/emqx_types.erl +++ b/apps/emqx/src/emqx_types.erl @@ -54,7 +54,8 @@ password/0, peerhost/0, peername/0, - protocol/0 + protocol/0, + client_attrs/0 ]). -export_type([ @@ -106,7 +107,7 @@ -export_type([ caps/0, - attrs/0, + channel_attrs/0, infos/0, stats/0 ]). @@ -189,8 +190,11 @@ anonymous => boolean(), cn => binary(), dn => binary(), + %% extra attributes + attrs => client_attrs(), atom() => term() }. +-type client_attrs() :: #{binary() => binary()}. -type clientid() :: binary() | atom(). -type username() :: option(binary()). -type password() :: option(binary()). @@ -270,7 +274,7 @@ -type command() :: #command{}. -type caps() :: emqx_mqtt_caps:caps(). --type attrs() :: #{atom() => term()}. +-type channel_attrs() :: #{atom() => term()}. -type infos() :: #{atom() => term()}. -type stats() :: [{atom(), term()}]. From cc4805b1ac9337c599c0f3daec7c88d0a5912daa Mon Sep 17 00:00:00 2001 From: zmstone Date: Tue, 19 Mar 2024 19:53:16 +0100 Subject: [PATCH 02/16] feat: extract attrs field from http and jwt auth response --- apps/emqx/src/emqx_channel.erl | 16 ++++++++++++++-- apps/emqx_auth_http/src/emqx_authn_http.erl | 7 ++++--- .../test/emqx_authn_http_SUITE.erl | 12 ++++++------ apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src | 2 +- apps/emqx_auth_jwt/src/emqx_authn_jwt.erl | 5 ++++- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 1f51c15b1..5ca26ba4e 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1726,9 +1726,21 @@ do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> {error, emqx_reason_codes:connack_error(Reason)} end. -merge_auth_result(ClientInfo, AuthResult) when is_map(ClientInfo) andalso is_map(AuthResult) -> +%% Merge authentication result into ClientInfo +%% Authentication result may include: +%% 1. `is_superuser': The superuser flag from various backends +%% 2. `acl': ACL rules from JWT, HTTP auth backend +%% 3. `attrs': Extra client attributes from JWT, HTTP auth backend +merge_auth_result(ClientInfo, AuthResult0) when is_map(ClientInfo) andalso is_map(AuthResult0) -> IsSuperuser = maps:get(is_superuser, AuthResult, false), - maps:merge(ClientInfo, AuthResult#{is_superuser => IsSuperuser}). + AuthResult = maps:without([attrs], AuthResult0), + Attrs0 = maps:get(attrs, ClientInfo, #{}), + Attrs1 = maps:get(attrs, AuthResult0, #{}), + Attrs = maps:merge(Attrs0, Attrs1), + maps:merge( + ClientInfo#{attrs => Attrs}, + AuthResult#{is_superuser => IsSuperuser} + ). %%-------------------------------------------------------------------- %% Process Topic Alias diff --git a/apps/emqx_auth_http/src/emqx_authn_http.erl b/apps/emqx_auth_http/src/emqx_authn_http.erl index 6e8bf0514..4efa69042 100644 --- a/apps/emqx_auth_http/src/emqx_authn_http.erl +++ b/apps/emqx_auth_http/src/emqx_authn_http.erl @@ -197,9 +197,10 @@ handle_response(Headers, Body) -> {ok, NBody} -> case maps:get(<<"result">>, NBody, <<"ignore">>) of <<"allow">> -> - Res = emqx_authn_utils:is_superuser(NBody), - %% TODO: Return by user property - {ok, Res#{user_property => maps:get(<<"user_property">>, NBody, #{})}}; + IsSuperuser = emqx_authn_utils:is_superuser(NBody), + Attrs = maps:get(<<"attrs">>, NBody, #{}), + Result = maps:merge(IsSuperuser, Attrs), + {ok, Result}; <<"deny">> -> {error, not_authorized}; <<"ignore">> -> diff --git a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl index 3be69641b..12211bab8 100644 --- a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl +++ b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl @@ -533,7 +533,7 @@ samples() -> {ok, Req, State} end, config_params => #{}, - result => {ok, #{is_superuser => false, user_property => #{}}} + result => {ok, #{is_superuser => false, attrs => #{}}} }, %% get request with json body response @@ -548,7 +548,7 @@ samples() -> {ok, Req, State} end, config_params => #{}, - result => {ok, #{is_superuser => true, user_property => #{}}} + result => {ok, #{is_superuser => true, attrs => #{}}} }, %% get request with url-form-encoded body response @@ -566,7 +566,7 @@ samples() -> {ok, Req, State} end, config_params => #{}, - result => {ok, #{is_superuser => true, user_property => #{}}} + result => {ok, #{is_superuser => true, attrs => #{}}} }, %% get request with response of unknown encoding @@ -608,7 +608,7 @@ samples() -> <<"method">> => <<"post">>, <<"headers">> => #{<<"content-type">> => <<"application/json">>} }, - result => {ok, #{is_superuser => false, user_property => #{}}} + result => {ok, #{is_superuser => false, attrs => #{}}} }, %% simple post request, application/x-www-form-urlencoded @@ -634,7 +634,7 @@ samples() -> <<"application/x-www-form-urlencoded">> } }, - result => {ok, #{is_superuser => false, user_property => #{}}} + result => {ok, #{is_superuser => false, attrs => #{}}} }, %% simple post request for placeholders, application/json @@ -669,7 +669,7 @@ samples() -> <<"cert_common_name">> => ?PH_CERT_CN_NAME } }, - result => {ok, #{is_superuser => false, user_property => #{}}} + result => {ok, #{is_superuser => false, attrs => #{}}} }, %% custom headers diff --git a/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src b/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src index 7e313881e..62d560d2f 100644 --- a/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src +++ b/apps/emqx_auth_jwt/src/emqx_auth_jwt.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_auth_jwt, [ {description, "EMQX JWT Authentication and Authorization"}, - {vsn, "0.2.0"}, + {vsn, "0.3.0"}, {registered, []}, {mod, {emqx_auth_jwt_app, []}}, {applications, [ diff --git a/apps/emqx_auth_jwt/src/emqx_authn_jwt.erl b/apps/emqx_auth_jwt/src/emqx_authn_jwt.erl index 5f89d076d..45fd1d60a 100644 --- a/apps/emqx_auth_jwt/src/emqx_authn_jwt.erl +++ b/apps/emqx_auth_jwt/src/emqx_authn_jwt.erl @@ -220,7 +220,10 @@ verify(JWT, JWKs, VerifyClaims, AclClaimName) -> case do_verify(JWT, JWKs, VerifyClaims) of {ok, Extra} -> try - {ok, acl(Extra, AclClaimName)} + ACL = acl(Extra, AclClaimName), + Attrs = maps:get(<<"attrs">>, Extra, #{}), + Result = maps:merge(Attrs, ACL), + {ok, Result} catch throw:{bad_acl_rule, Reason} -> %% it's a invalid token, so ok to log From e5816f5a1386c9c0ee40b1a87c902298a07e6584 Mon Sep 17 00:00:00 2001 From: zmstone Date: Tue, 19 Mar 2024 21:03:44 +0100 Subject: [PATCH 03/16] refactor: rename attr to client_attr client_attr is unique enough for all contexts so the name can be unified from external responses to internal template rendering, and rule-engine template rendering --- apps/emqx/src/emqx_channel.erl | 15 ++++----- apps/emqx/src/emqx_types.erl | 2 +- .../src/emqx_authn/emqx_authn_utils.erl | 6 ++++ apps/emqx_auth_http/src/emqx_authn_http.erl | 2 +- .../test/emqx_authn_http_SUITE.erl | 12 +++---- apps/emqx_auth_jwt/src/emqx_authn_jwt.erl | 31 +++++++++---------- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5ca26ba4e..871de163e 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -260,7 +260,7 @@ init( is_bridge => false, is_superuser => false, enable_authn => maps:get(enable_authn, Opts, true), - attrs => #{} + client_attrs => #{} }, Zone ), @@ -1730,15 +1730,16 @@ do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> %% Authentication result may include: %% 1. `is_superuser': The superuser flag from various backends %% 2. `acl': ACL rules from JWT, HTTP auth backend -%% 3. `attrs': Extra client attributes from JWT, HTTP auth backend +%% 3. `client_attrs': Extra client attributes from JWT, HTTP auth backend +%% 4. Maybe more non-standard fileds used by hook callbacks merge_auth_result(ClientInfo, AuthResult0) when is_map(ClientInfo) andalso is_map(AuthResult0) -> - IsSuperuser = maps:get(is_superuser, AuthResult, false), - AuthResult = maps:without([attrs], AuthResult0), - Attrs0 = maps:get(attrs, ClientInfo, #{}), - Attrs1 = maps:get(attrs, AuthResult0, #{}), + IsSuperuser = maps:get(is_superuser, AuthResult0, false), + AuthResult = maps:without([client_attrs], AuthResult0), + Attrs0 = maps:get(client_attrs, ClientInfo, #{}), + Attrs1 = maps:get(client_attrs, AuthResult0, #{}), Attrs = maps:merge(Attrs0, Attrs1), maps:merge( - ClientInfo#{attrs => Attrs}, + ClientInfo#{client_attrs => Attrs}, AuthResult#{is_superuser => IsSuperuser} ). diff --git a/apps/emqx/src/emqx_types.erl b/apps/emqx/src/emqx_types.erl index 3883991b4..3361798cf 100644 --- a/apps/emqx/src/emqx_types.erl +++ b/apps/emqx/src/emqx_types.erl @@ -191,7 +191,7 @@ cn => binary(), dn => binary(), %% extra attributes - attrs => client_attrs(), + client_attrs => client_attrs(), atom() => term() }. -type client_attrs() :: #{binary() => binary()}. diff --git a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl index bf17ca950..80da32b0b 100644 --- a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl +++ b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl @@ -32,6 +32,7 @@ render_urlencoded_str/2, render_sql_params/2, is_superuser/1, + client_attrs/1, bin/1, ensure_apps_started/1, cleanup_resources/0, @@ -204,6 +205,11 @@ is_superuser(#{<<"is_superuser">> := Value}) -> is_superuser(#{}) -> #{is_superuser => false}. +client_attrs(#{<<"client_attrs">> := Attrs}) -> + #{client_attrs => Attrs}; +client_attrs(_) -> + #{client_attrs => #{}}. + ensure_apps_started(bcrypt) -> {ok, _} = application:ensure_all_started(bcrypt), ok; diff --git a/apps/emqx_auth_http/src/emqx_authn_http.erl b/apps/emqx_auth_http/src/emqx_authn_http.erl index 4efa69042..5cbffb394 100644 --- a/apps/emqx_auth_http/src/emqx_authn_http.erl +++ b/apps/emqx_auth_http/src/emqx_authn_http.erl @@ -198,7 +198,7 @@ handle_response(Headers, Body) -> case maps:get(<<"result">>, NBody, <<"ignore">>) of <<"allow">> -> IsSuperuser = emqx_authn_utils:is_superuser(NBody), - Attrs = maps:get(<<"attrs">>, NBody, #{}), + Attrs = emqx_authn_utils:client_attrs(NBody), Result = maps:merge(IsSuperuser, Attrs), {ok, Result}; <<"deny">> -> diff --git a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl index 12211bab8..576a22203 100644 --- a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl +++ b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl @@ -533,7 +533,7 @@ samples() -> {ok, Req, State} end, config_params => #{}, - result => {ok, #{is_superuser => false, attrs => #{}}} + result => {ok, #{is_superuser => false, client_attrs => #{}}} }, %% get request with json body response @@ -548,7 +548,7 @@ samples() -> {ok, Req, State} end, config_params => #{}, - result => {ok, #{is_superuser => true, attrs => #{}}} + result => {ok, #{is_superuser => true, client_attrs => #{}}} }, %% get request with url-form-encoded body response @@ -566,7 +566,7 @@ samples() -> {ok, Req, State} end, config_params => #{}, - result => {ok, #{is_superuser => true, attrs => #{}}} + result => {ok, #{is_superuser => true, client_attrs => #{}}} }, %% get request with response of unknown encoding @@ -608,7 +608,7 @@ samples() -> <<"method">> => <<"post">>, <<"headers">> => #{<<"content-type">> => <<"application/json">>} }, - result => {ok, #{is_superuser => false, attrs => #{}}} + result => {ok, #{is_superuser => false, client_attrs => #{}}} }, %% simple post request, application/x-www-form-urlencoded @@ -634,7 +634,7 @@ samples() -> <<"application/x-www-form-urlencoded">> } }, - result => {ok, #{is_superuser => false, attrs => #{}}} + result => {ok, #{is_superuser => false, client_attrs => #{}}} }, %% simple post request for placeholders, application/json @@ -669,7 +669,7 @@ samples() -> <<"cert_common_name">> => ?PH_CERT_CN_NAME } }, - result => {ok, #{is_superuser => false, attrs => #{}}} + result => {ok, #{is_superuser => false, client_attrs => #{}}} }, %% custom headers diff --git a/apps/emqx_auth_jwt/src/emqx_authn_jwt.erl b/apps/emqx_auth_jwt/src/emqx_authn_jwt.erl index 45fd1d60a..2a12f5acf 100644 --- a/apps/emqx_auth_jwt/src/emqx_authn_jwt.erl +++ b/apps/emqx_auth_jwt/src/emqx_authn_jwt.erl @@ -219,10 +219,11 @@ verify(undefined, _, _, _) -> verify(JWT, JWKs, VerifyClaims, AclClaimName) -> case do_verify(JWT, JWKs, VerifyClaims) of {ok, Extra} -> + IsSuperuser = emqx_authn_utils:is_superuser(Extra), + Attrs = emqx_authn_utils:client_attrs(Extra), try ACL = acl(Extra, AclClaimName), - Attrs = maps:get(<<"attrs">>, Extra, #{}), - Result = maps:merge(Attrs, ACL), + Result = maps:merge(IsSuperuser, maps:merge(ACL, Attrs)), {ok, Result} catch throw:{bad_acl_rule, Reason} -> @@ -245,20 +246,18 @@ verify(JWT, JWKs, VerifyClaims, AclClaimName) -> end. acl(Claims, AclClaimName) -> - Acl = - case Claims of - #{AclClaimName := Rules} -> - #{ - acl => #{ - rules => parse_rules(Rules), - source_for_logging => jwt, - expire => maps:get(<<"exp">>, Claims, undefined) - } - }; - _ -> - #{} - end, - maps:merge(emqx_authn_utils:is_superuser(Claims), Acl). + case Claims of + #{AclClaimName := Rules} -> + #{ + acl => #{ + rules => parse_rules(Rules), + source_for_logging => jwt, + expire => maps:get(<<"exp">>, Claims, undefined) + } + }; + _ -> + #{} + end. do_verify(_JWT, [], _VerifyClaims) -> {error, invalid_signature}; From 9ec99fef4ae04cfcc5643177c11a38da41650ffb Mon Sep 17 00:00:00 2001 From: zmstone Date: Tue, 19 Mar 2024 21:51:21 +0100 Subject: [PATCH 04/16] feat: allow client_attr used in authz rules --- apps/emqx/include/emqx_placeholder.hrl | 1 + .../src/emqx_authn/emqx_authn_utils.erl | 1 + .../src/emqx_authz/emqx_authz_rule.erl | 4 ++- .../test/emqx_authz/emqx_authz_file_SUITE.erl | 24 ++++++++++++++++ apps/emqx_auth_http/src/emqx_authz_http.erl | 3 +- .../src/emqx_auth_mongodb.app.src | 2 +- .../src/emqx_authz_mongodb.erl | 3 +- .../src/emqx_auth_mysql.app.src | 2 +- apps/emqx_auth_mysql/src/emqx_authz_mysql.erl | 3 +- .../src/emqx_auth_postgresql.app.src | 2 +- .../src/emqx_authz_postgresql.erl | 3 +- .../src/emqx_auth_redis.app.src | 2 +- apps/emqx_auth_redis/src/emqx_authz_redis.erl | 3 +- apps/emqx_utils/src/emqx_template.erl | 28 +++++++++++++++++-- apps/emqx_utils/test/emqx_template_SUITE.erl | 12 ++++++++ 15 files changed, 81 insertions(+), 12 deletions(-) diff --git a/apps/emqx/include/emqx_placeholder.hrl b/apps/emqx/include/emqx_placeholder.hrl index 8e886fe68..e2711be69 100644 --- a/apps/emqx/include/emqx_placeholder.hrl +++ b/apps/emqx/include/emqx_placeholder.hrl @@ -36,6 +36,7 @@ -define(VAR_CLIENTID, "clientid"). -define(VAR_USERNAME, "username"). -define(VAR_TOPIC, "topic"). +-define(VAR_NS_CLIENT_ATTRS, {var_namespace, "client_attrs"}). -define(PH_PASSWORD, ?PH(?VAR_PASSWORD)). -define(PH_CLIENTID, ?PH(?VAR_CLIENTID)). -define(PH_FROM_CLIENTID, ?PH("from_clientid")). diff --git a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl index 80da32b0b..fb6bf98fa 100644 --- a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl +++ b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl @@ -46,6 +46,7 @@ default_headers_no_content_type/0 ]). +%% VAR_NS_CLIENT_ATTRS is not added to this list because client_attrs is to be initialized from authn result -define(ALLOWED_VARS, [ ?VAR_USERNAME, ?VAR_CLIENTID, diff --git a/apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl b/apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl index 9ecfa0f64..68ee05b52 100644 --- a/apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl +++ b/apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl @@ -223,7 +223,9 @@ compile_topic(<<"eq ", Topic/binary>>) -> compile_topic({eq, Topic}) -> {eq, emqx_topic:words(bin(Topic))}; compile_topic(Topic) -> - Template = emqx_authz_utils:parse_str(Topic, [?VAR_USERNAME, ?VAR_CLIENTID]), + Template = emqx_authz_utils:parse_str(Topic, [ + ?VAR_USERNAME, ?VAR_CLIENTID, ?VAR_NS_CLIENT_ATTRS + ]), case emqx_template:is_const(Template) of true -> emqx_topic:words(bin(Topic)); false -> {pattern, Template} diff --git a/apps/emqx_auth/test/emqx_authz/emqx_authz_file_SUITE.erl b/apps/emqx_auth/test/emqx_authz/emqx_authz_file_SUITE.erl index 3ed7c1f83..e51f6dd37 100644 --- a/apps/emqx_auth/test/emqx_authz/emqx_authz_file_SUITE.erl +++ b/apps/emqx_auth/test/emqx_authz/emqx_authz_file_SUITE.erl @@ -74,6 +74,30 @@ t_ok(_Config) -> emqx_access_control:authorize(ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t">>) ). +t_client_attrs(_Config) -> + ClientInfo0 = emqx_authz_test_lib:base_client_info(), + ClientInfo = ClientInfo0#{client_attrs => #{<<"device_id">> => <<"id1">>}}, + + ok = setup_config(?RAW_SOURCE#{ + <<"rules">> => <<"{allow, all, all, [\"t/${client_attrs.device_id}/#\"]}.">> + }), + + ?assertEqual( + allow, + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t/id1/1">>) + ), + + ?assertEqual( + allow, + emqx_access_control:authorize(ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t/id1/#">>) + ), + + ?assertEqual( + deny, + emqx_access_control:authorize(ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t/id2/#">>) + ), + ok. + t_rich_actions(_Config) -> ClientInfo = emqx_authz_test_lib:base_client_info(), diff --git a/apps/emqx_auth_http/src/emqx_authz_http.erl b/apps/emqx_auth_http/src/emqx_authz_http.erl index 721307099..ffc339393 100644 --- a/apps/emqx_auth_http/src/emqx_authz_http.erl +++ b/apps/emqx_auth_http/src/emqx_authz_http.erl @@ -47,7 +47,8 @@ ?VAR_TOPIC, ?VAR_ACTION, ?VAR_CERT_SUBJECT, - ?VAR_CERT_CN_NAME + ?VAR_CERT_CN_NAME, + ?VAR_NS_CLIENT_ATTRS ]). -define(ALLOWED_VARS_RICH_ACTIONS, [ diff --git a/apps/emqx_auth_mongodb/src/emqx_auth_mongodb.app.src b/apps/emqx_auth_mongodb/src/emqx_auth_mongodb.app.src index 8970329fe..df3cc1268 100644 --- a/apps/emqx_auth_mongodb/src/emqx_auth_mongodb.app.src +++ b/apps/emqx_auth_mongodb/src/emqx_auth_mongodb.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_auth_mongodb, [ {description, "EMQX MongoDB Authentication and Authorization"}, - {vsn, "0.1.1"}, + {vsn, "0.2.0"}, {registered, []}, {mod, {emqx_auth_mongodb_app, []}}, {applications, [ diff --git a/apps/emqx_auth_mongodb/src/emqx_authz_mongodb.erl b/apps/emqx_auth_mongodb/src/emqx_authz_mongodb.erl index 8f185c3bd..0bab6ef90 100644 --- a/apps/emqx_auth_mongodb/src/emqx_authz_mongodb.erl +++ b/apps/emqx_auth_mongodb/src/emqx_authz_mongodb.erl @@ -40,7 +40,8 @@ ?VAR_CLIENTID, ?VAR_PEERHOST, ?VAR_CERT_CN_NAME, - ?VAR_CERT_SUBJECT + ?VAR_CERT_SUBJECT, + ?VAR_NS_CLIENT_ATTRS ]). description() -> diff --git a/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src b/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src index 24fdd2648..29bf97ac8 100644 --- a/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src +++ b/apps/emqx_auth_mysql/src/emqx_auth_mysql.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_auth_mysql, [ {description, "EMQX MySQL Authentication and Authorization"}, - {vsn, "0.1.2"}, + {vsn, "0.2.0"}, {registered, []}, {mod, {emqx_auth_mysql_app, []}}, {applications, [ diff --git a/apps/emqx_auth_mysql/src/emqx_authz_mysql.erl b/apps/emqx_auth_mysql/src/emqx_authz_mysql.erl index 6356f0017..59ed878ab 100644 --- a/apps/emqx_auth_mysql/src/emqx_authz_mysql.erl +++ b/apps/emqx_auth_mysql/src/emqx_authz_mysql.erl @@ -42,7 +42,8 @@ ?VAR_CLIENTID, ?VAR_PEERHOST, ?VAR_CERT_CN_NAME, - ?VAR_CERT_SUBJECT + ?VAR_CERT_SUBJECT, + ?VAR_NS_CLIENT_ATTRS ]). description() -> diff --git a/apps/emqx_auth_postgresql/src/emqx_auth_postgresql.app.src b/apps/emqx_auth_postgresql/src/emqx_auth_postgresql.app.src index bae3da0cb..3978f7dbc 100644 --- a/apps/emqx_auth_postgresql/src/emqx_auth_postgresql.app.src +++ b/apps/emqx_auth_postgresql/src/emqx_auth_postgresql.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_auth_postgresql, [ {description, "EMQX PostgreSQL Authentication and Authorization"}, - {vsn, "0.1.1"}, + {vsn, "0.2.0"}, {registered, []}, {mod, {emqx_auth_postgresql_app, []}}, {applications, [ diff --git a/apps/emqx_auth_postgresql/src/emqx_authz_postgresql.erl b/apps/emqx_auth_postgresql/src/emqx_authz_postgresql.erl index f53307d24..a77f0a424 100644 --- a/apps/emqx_auth_postgresql/src/emqx_authz_postgresql.erl +++ b/apps/emqx_auth_postgresql/src/emqx_authz_postgresql.erl @@ -42,7 +42,8 @@ ?VAR_CLIENTID, ?VAR_PEERHOST, ?VAR_CERT_CN_NAME, - ?VAR_CERT_SUBJECT + ?VAR_CERT_SUBJECT, + ?VAR_NS_CLIENT_ATTRS ]). description() -> diff --git a/apps/emqx_auth_redis/src/emqx_auth_redis.app.src b/apps/emqx_auth_redis/src/emqx_auth_redis.app.src index b5669e706..74168495b 100644 --- a/apps/emqx_auth_redis/src/emqx_auth_redis.app.src +++ b/apps/emqx_auth_redis/src/emqx_auth_redis.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_auth_redis, [ {description, "EMQX Redis Authentication and Authorization"}, - {vsn, "0.1.2"}, + {vsn, "0.2.0"}, {registered, []}, {mod, {emqx_auth_redis_app, []}}, {applications, [ diff --git a/apps/emqx_auth_redis/src/emqx_authz_redis.erl b/apps/emqx_auth_redis/src/emqx_authz_redis.erl index a8f52da1b..a7f88f7c6 100644 --- a/apps/emqx_auth_redis/src/emqx_authz_redis.erl +++ b/apps/emqx_auth_redis/src/emqx_authz_redis.erl @@ -40,7 +40,8 @@ ?VAR_CERT_SUBJECT, ?VAR_PEERHOST, ?VAR_CLIENTID, - ?VAR_USERNAME + ?VAR_USERNAME, + ?VAR_NS_CLIENT_ATTRS ]). description() -> diff --git a/apps/emqx_utils/src/emqx_template.erl b/apps/emqx_utils/src/emqx_template.erl index 9a2ac1b8f..21c0e1d4f 100644 --- a/apps/emqx_utils/src/emqx_template.erl +++ b/apps/emqx_utils/src/emqx_template.erl @@ -145,18 +145,42 @@ parse_accessor(Var) -> %% @doc Validate a template against a set of allowed variables. %% If the given template contains any variable not in the allowed set, an error %% is returned. --spec validate([varname()], t()) -> +-spec validate([varname() | {var_namespace, varname()}], t()) -> ok | {error, [_Error :: {varname(), disallowed}]}. validate(Allowed, Template) -> {_, Errors} = render(Template, #{}), {Used, _} = lists:unzip(Errors), - case lists:usort(Used) -- Allowed of + case find_disallowed(lists:usort(Used), Allowed) of [] -> ok; Disallowed -> {error, [{Var, disallowed} || Var <- Disallowed]} end. +find_disallowed([], _Allowed) -> + []; +find_disallowed([Var | Rest], Allowed) -> + case is_allowed(Var, Allowed) of + true -> + find_disallowed(Rest, Allowed); + false -> + [Var | find_disallowed(Rest, Allowed)] + end. + +is_allowed(_Var, []) -> + false; +is_allowed(Var, [{var_namespace, VarPrefix} | Allowed]) -> + case lists:prefix(VarPrefix ++ ".", Var) of + true -> + true; + false -> + is_allowed(Var, Allowed) + end; +is_allowed(Var, [Var | _Allowed]) -> + true; +is_allowed(Var, [_ | Allowed]) -> + is_allowed(Var, Allowed). + %% @doc Check if a template is constant with respect to rendering, i.e. does not %% contain any placeholders. -spec is_const(t()) -> diff --git a/apps/emqx_utils/test/emqx_template_SUITE.erl b/apps/emqx_utils/test/emqx_template_SUITE.erl index 46b42e5db..0a3273170 100644 --- a/apps/emqx_utils/test/emqx_template_SUITE.erl +++ b/apps/emqx_utils/test/emqx_template_SUITE.erl @@ -337,6 +337,18 @@ t_unparse_tmpl_deep(_) -> Template = emqx_template:parse_deep(Term), ?assertEqual(Term, emqx_template:unparse(Template)). +t_allow_var_by_namespace(_) -> + Context = #{d => #{d1 => <<"hi">>}}, + Template = emqx_template:parse(<<"d.d1:${d.d1}">>), + ?assertEqual( + ok, + emqx_template:validate([{var_namespace, "d"}], Template) + ), + ?assertEqual( + {<<"d.d1:hi">>, []}, + render_string(Template, Context) + ). + %% render_string(Template, Context) -> From c75840306b07a84a8b9d5fc876422c70fac8b6f7 Mon Sep 17 00:00:00 2001 From: zmstone Date: Tue, 19 Mar 2024 22:41:28 +0100 Subject: [PATCH 05/16] fix: restrict client_attr key and value string format The keys and values are used to render templates for authz rules, such as topic names, and SQL statements etc. --- apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl | 12 +++++++++++- apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl | 12 ++++++++++-- apps/emqx_utils/src/emqx_utils.erl | 10 +++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl index fb6bf98fa..13f8f238b 100644 --- a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl +++ b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl @@ -207,10 +207,20 @@ is_superuser(#{}) -> #{is_superuser => false}. client_attrs(#{<<"client_attrs">> := Attrs}) -> - #{client_attrs => Attrs}; + #{client_attrs => drop_invalid_attr(Attrs)}; client_attrs(_) -> #{client_attrs => #{}}. +drop_invalid_attr(Map) when is_map(Map) -> + maps:from_list(do_drop_invalid_attr(maps:to_list(Map))). + +do_drop_invalid_attr(KVL) -> + F = fun({K, V}) -> + emqx_utils:is_restricted_str(K) andalso + emqx_utils:is_restricted_str(V) + end, + lists:filter(F, KVL). + ensure_apps_started(bcrypt) -> {ok, _} = application:ensure_all_started(bcrypt), ok; diff --git a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl index 576a22203..7b2aae9f1 100644 --- a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl +++ b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl @@ -542,13 +542,21 @@ samples() -> Req = cowboy_req:reply( 200, #{<<"content-type">> => <<"application/json">>}, - emqx_utils_json:encode(#{result => allow, is_superuser => true}), + emqx_utils_json:encode(#{ + result => allow, + is_superuser => true, + client_attrs => #{ + fid => <<"n11">>, + <<"_bad_key">> => <<"v">>, + <<"ok_key">> => <<"but bad value">> + } + }), Req0 ), {ok, Req, State} end, config_params => #{}, - result => {ok, #{is_superuser => true, client_attrs => #{}}} + result => {ok, #{is_superuser => true, client_attrs => #{<<"fid">> => <<"n11">>}}} }, %% get request with url-form-encoded body response diff --git a/apps/emqx_utils/src/emqx_utils.erl b/apps/emqx_utils/src/emqx_utils.erl index 12855b9b6..007c2b54b 100644 --- a/apps/emqx_utils/src/emqx_utils.erl +++ b/apps/emqx_utils/src/emqx_utils.erl @@ -67,7 +67,8 @@ format/1, call_first_defined/1, ntoa/1, - foldl_while/3 + foldl_while/3, + is_restricted_str/1 ]). -export([ @@ -861,6 +862,13 @@ ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) -> ntoa(IP) -> inet_parse:ntoa(IP). +%% @doc Return true if the provided string is a restricted string: +%% Start with a letter or a digit, +%% remaining characters can be '-' or '_' in addition to letters and digits +is_restricted_str(String) -> + RE = <<"^[A-Za-z0-9]+[A-Za-z0-9-_]*$">>, + match =:= re:run(String, RE, [{capture, none}]). + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). From 4f71c978546005eb8dc4dc89f6c78d38b292a83d Mon Sep 17 00:00:00 2001 From: zmstone Date: Thu, 21 Mar 2024 17:49:43 +0100 Subject: [PATCH 06/16] docs: add changelog for PR #12750 --- changes/ce/feat-12750.en.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 changes/ce/feat-12750.en.md diff --git a/changes/ce/feat-12750.en.md b/changes/ce/feat-12750.en.md new file mode 100644 index 000000000..92886d298 --- /dev/null +++ b/changes/ce/feat-12750.en.md @@ -0,0 +1,31 @@ +Customizable client attributes in `clientinfo`. + +Introduced a new field `client_attrs` in the `clientinfo` object. +This enhancement enables the initialization of `client_attrs` with specific +attributes derived from the `clientinfo` fields upon accepting an MQTT connection. + +### Initialization of `client_attrs` + +- The `client_attrs` field can be initially populated based on the configuration from one of the + following sources: + - `clientid`: The MQTT client ID provided by the client. + - `username`: The username provided by the client. + - `cn`: The common name from the TLS client's certificate. + - `dn`: The distinguish name from the TLS client's certificate, i.e., the certificate "Subject". + +### Extension through Authentication Responses + +- Additional attributes may be merged into `client_attrs` from authentication responses. Supported + authentication backends include: + - **HTTP**: Attributes can be included in the JSON object of the HTTP response body through a + `client_attrs` field. + - **JWT**: Attributes can be included via a `client_attrs` claim within the JWT. + +### Usage in Authorization + +- The `client_attrs` can be utilized in authorization configurations or request templates, enhancing + flexibility and control. Examples include: + - In `acl.conf`, use `{allow, all, all, ["${client_attrs.namespace}/#"]}` to apply permissions + based on the `namespace` attribute. + - In other authorization backends, `${client_attrs.namespace}` can be used within request templates + to dynamically include client attributes. From 2fd0a2cd4de42070faa65840f5116b0d57d1bc00 Mon Sep 17 00:00:00 2001 From: zmstone Date: Wed, 20 Mar 2024 17:04:20 +0100 Subject: [PATCH 07/16] feat: support extracting initial client attrs from clientinfo --- apps/emqx/src/emqx_channel.erl | 81 ++++++++++++++++++- apps/emqx/src/emqx_schema.erl | 33 +++++++- apps/emqx/test/emqx_client_SUITE.erl | 28 +++++++ .../test/emqx_authz/emqx_authz_SUITE.erl | 31 +++++++ rel/i18n/emqx_schema.hocon | 50 ++++++++++-- 5 files changed, 214 insertions(+), 9 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 871de163e..5d466f098 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -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}) -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 7889a13de..e71126dca 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -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. diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index d9218b0dd..077a48593 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -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). diff --git a/apps/emqx_auth/test/emqx_authz/emqx_authz_SUITE.erl b/apps/emqx_auth/test/emqx_authz/emqx_authz_SUITE.erl index c9d5933bb..c88dcc244 100644 --- a/apps/emqx_auth/test/emqx_authz/emqx_authz_SUITE.erl +++ b/apps/emqx_auth/test/emqx_authz/emqx_authz_SUITE.erl @@ -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. diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index 0a0f71cfe..a90ecdffc 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -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.label:"Durable storage" +durable_storage.desc: """~ + Configuration related to the EMQX durable storages. -durable_storage.desc: -"""Configuration related to the EMQX durable storages. + EMQX uses durable storages to offload various data, such as MQTT messages, to disc.""" -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.""" +} } From 8254b801ae6b910de5168e161ff43c88724ac042 Mon Sep 17 00:00:00 2001 From: zmstone Date: Thu, 21 Mar 2024 20:13:41 +0100 Subject: [PATCH 08/16] 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.""" } } From 0cf61932b6170bcf7f0b4da23f6baaf28cbfada8 Mon Sep 17 00:00:00 2001 From: zmstone Date: Thu, 21 Mar 2024 20:36:41 +0100 Subject: [PATCH 09/16] feat: allow using client_attrs in authentication templates --- apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl | 6 ++++-- apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl | 5 ++--- apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl | 9 ++++++--- rel/i18n/emqx_schema.hocon | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl index 13f8f238b..3e12e3992 100644 --- a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl +++ b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl @@ -46,14 +46,16 @@ default_headers_no_content_type/0 ]). -%% VAR_NS_CLIENT_ATTRS is not added to this list because client_attrs is to be initialized from authn result +%% VAR_NS_CLIENT_ATTRS is added here because it can be initialized before authn. +%% NOTE: authn return may add more to (or even overwrite) client_attrs. -define(ALLOWED_VARS, [ ?VAR_USERNAME, ?VAR_CLIENTID, ?VAR_PASSWORD, ?VAR_PEERHOST, ?VAR_CERT_SUBJECT, - ?VAR_CERT_CN_NAME + ?VAR_CERT_CN_NAME, + ?VAR_NS_CLIENT_ATTRS ]). -define(DEFAULT_RESOURCE_OPTS, #{ diff --git a/apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl b/apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl index 68ee05b52..9a877b5c8 100644 --- a/apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl +++ b/apps/emqx_auth/src/emqx_authz/emqx_authz_rule.erl @@ -88,6 +88,7 @@ -type rule_precompile() :: {permission(), who_condition(), action_precompile(), [topic_filter()]}. -define(IS_PERMISSION(Permission), (Permission =:= allow orelse Permission =:= deny)). +-define(ALLOWED_VARS, [?VAR_USERNAME, ?VAR_CLIENTID, ?VAR_NS_CLIENT_ATTRS]). -spec compile(permission(), who_condition(), action_precompile(), [topic_filter()]) -> rule(). compile(Permission, Who, Action, TopicFilters) -> @@ -223,9 +224,7 @@ compile_topic(<<"eq ", Topic/binary>>) -> compile_topic({eq, Topic}) -> {eq, emqx_topic:words(bin(Topic))}; compile_topic(Topic) -> - Template = emqx_authz_utils:parse_str(Topic, [ - ?VAR_USERNAME, ?VAR_CLIENTID, ?VAR_NS_CLIENT_ATTRS - ]), + Template = emqx_authz_utils:parse_str(Topic, ?ALLOWED_VARS), case emqx_template:is_const(Template) of true -> emqx_topic:words(bin(Topic)); false -> {pattern, Template} diff --git a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl index 7b2aae9f1..3f16c9e2c 100644 --- a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl +++ b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl @@ -36,7 +36,8 @@ listener => 'tcp:default', protocol => mqtt, cert_subject => <<"cert_subject_data">>, - cert_common_name => <<"cert_common_name_data">> + cert_common_name => <<"cert_common_name_data">>, + client_attrs => #{<<"group">> => <<"g1">>} }). -define(SERVER_RESPONSE_JSON(Result), ?SERVER_RESPONSE_JSON(Result, false)). @@ -655,7 +656,8 @@ samples() -> <<"clientid">> := <<"clienta">>, <<"peerhost">> := <<"127.0.0.1">>, <<"cert_subject">> := <<"cert_subject_data">>, - <<"cert_common_name">> := <<"cert_common_name_data">> + <<"cert_common_name">> := <<"cert_common_name_data">>, + <<"the_group">> := <<"g1">> } = emqx_utils_json:decode(RawBody, [return_maps]), Req = cowboy_req:reply( 200, @@ -674,7 +676,8 @@ samples() -> <<"password">> => ?PH_PASSWORD, <<"peerhost">> => ?PH_PEERHOST, <<"cert_subject">> => ?PH_CERT_SUBJECT, - <<"cert_common_name">> => ?PH_CERT_CN_NAME + <<"cert_common_name">> => ?PH_CERT_CN_NAME, + <<"the_group">> => <<"${client_attrs.group}">> } }, result => {ok, #{is_superuser => false, client_attrs => #{}}} diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index 8232b4fe7..b65cb76b3 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -1578,8 +1578,8 @@ client_attrs_init { 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`.""" + and can be used as a placeholder in a template for authentication and authorization. + For example, use `${client_attrs.alias}` to render a HTTP POST body when `extract_as = alias`.""" } client_attrs_init_extract_from { From 5e9814d171153c470afc85c510eb4477eaf5889f Mon Sep 17 00:00:00 2001 From: zmstone Date: Fri, 22 Mar 2024 10:07:48 +0100 Subject: [PATCH 10/16] fix: add debug level logging for invalid client attributes --- apps/emqx/src/emqx_channel.erl | 2 +- .../src/emqx_authn/emqx_authn_utils.erl | 18 ++++++++++++------ apps/emqx_utils/src/emqx_template.erl | 11 ++--------- changes/ce/feat-12750.en.md | 2 +- rel/i18n/emqx_schema.hocon | 6 +++--- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 99a991225..49a5b2226 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1846,7 +1846,7 @@ do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> %% 1. `is_superuser': The superuser flag from various backends %% 2. `acl': ACL rules from JWT, HTTP auth backend %% 3. `client_attrs': Extra client attributes from JWT, HTTP auth backend -%% 4. Maybe more non-standard fileds used by hook callbacks +%% 4. Maybe more non-standard fields used by hook callbacks merge_auth_result(ClientInfo, AuthResult0) when is_map(ClientInfo) andalso is_map(AuthResult0) -> IsSuperuser = maps:get(is_superuser, AuthResult0, false), AuthResult = maps:without([client_attrs], AuthResult0), diff --git a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl index 3e12e3992..9ce934a30 100644 --- a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl +++ b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl @@ -216,12 +216,18 @@ client_attrs(_) -> drop_invalid_attr(Map) when is_map(Map) -> maps:from_list(do_drop_invalid_attr(maps:to_list(Map))). -do_drop_invalid_attr(KVL) -> - F = fun({K, V}) -> - emqx_utils:is_restricted_str(K) andalso - emqx_utils:is_restricted_str(V) - end, - lists:filter(F, KVL). +do_drop_invalid_attr([]) -> + []; +do_drop_invalid_attr([{K, V} | More]) -> + case emqx_utils:is_restricted_str(K) andalso emqx_utils:is_restricted_str(V) of + true -> + [{iolist_to_binary(K), iolist_to_binary(V)} | do_drop_invalid_attr(More)]; + false -> + ?SLOG(debug, #{msg => "invalid_client_attr_dropped", key => K, value => V}, #{ + tag => "AUTHN" + }), + do_drop_invalid_attr(More) + end. ensure_apps_started(bcrypt) -> {ok, _} = application:ensure_all_started(bcrypt), diff --git a/apps/emqx_utils/src/emqx_template.erl b/apps/emqx_utils/src/emqx_template.erl index 21c0e1d4f..b0272b1bb 100644 --- a/apps/emqx_utils/src/emqx_template.erl +++ b/apps/emqx_utils/src/emqx_template.erl @@ -157,15 +157,8 @@ validate(Allowed, Template) -> {error, [{Var, disallowed} || Var <- Disallowed]} end. -find_disallowed([], _Allowed) -> - []; -find_disallowed([Var | Rest], Allowed) -> - case is_allowed(Var, Allowed) of - true -> - find_disallowed(Rest, Allowed); - false -> - [Var | find_disallowed(Rest, Allowed)] - end. +find_disallowed(Vars, Allowed) -> + lists:filter(fun(Var) -> not is_allowed(Var, Allowed) end, Vars). is_allowed(_Var, []) -> false; diff --git a/changes/ce/feat-12750.en.md b/changes/ce/feat-12750.en.md index 92886d298..33c1d0b4f 100644 --- a/changes/ce/feat-12750.en.md +++ b/changes/ce/feat-12750.en.md @@ -11,7 +11,7 @@ attributes derived from the `clientinfo` fields upon accepting an MQTT connectio - `clientid`: The MQTT client ID provided by the client. - `username`: The username provided by the client. - `cn`: The common name from the TLS client's certificate. - - `dn`: The distinguish name from the TLS client's certificate, i.e., the certificate "Subject". + - `dn`: The distinguished name from the TLS client's certificate, i.e., the certificate "Subject". ### Extension through Authentication Responses diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index b65cb76b3..32b8ff8e9 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -1577,7 +1577,7 @@ client_attrs_init { 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, + The initialized client attribute will be stored in the `client_attrs` property with the specified name, and can be used as a placeholder in a template for authentication and authorization. For example, use `${client_attrs.alias}` to render a HTTP POST body when `extract_as = alias`.""" } @@ -1605,14 +1605,14 @@ client_attrs_init_extract_regexp { 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.""" + Note that failure to match the regular expression will result in the client attribute being absent 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. + The extracted attribute will be stored in the `client_attrs` property with this name. In case `extract_from = user_property`, this should be the key of the user property.""" } From 8e8fc6a3d18033d9177d190cd34f45a4aa177219 Mon Sep 17 00:00:00 2001 From: zmstone Date: Fri, 22 Mar 2024 10:22:36 +0100 Subject: [PATCH 11/16] test: fix config test --- apps/emqx/src/emqx_types.erl | 5 +++-- apps/emqx/test/emqx_config_SUITE.erl | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/emqx/src/emqx_types.erl b/apps/emqx/src/emqx_types.erl index 3361798cf..322cc1c05 100644 --- a/apps/emqx/src/emqx_types.erl +++ b/apps/emqx/src/emqx_types.erl @@ -190,8 +190,9 @@ anonymous => boolean(), cn => binary(), dn => binary(), - %% extra attributes - client_attrs => client_attrs(), + %% Extra client attributes, commented out for bpapi spec backward compatibility. + %% This field is never used in RPC calls. + %% client_attrs => client_attrs(), atom() => term() }. -type client_attrs() :: #{binary() => binary()}. diff --git a/apps/emqx/test/emqx_config_SUITE.erl b/apps/emqx/test/emqx_config_SUITE.erl index 77fd28f11..01a5cc001 100644 --- a/apps/emqx/test/emqx_config_SUITE.erl +++ b/apps/emqx/test/emqx_config_SUITE.erl @@ -453,7 +453,8 @@ zone_global_defaults() -> strict_mode => false, upgrade_qos => false, use_username_as_clientid => false, - wildcard_subscription => true + wildcard_subscription => true, + client_attrs_init => disabled }, overload_protection => #{ From 3136ec595826b446a52e08bb8f5fb0e64d5e7cb2 Mon Sep 17 00:00:00 2001 From: zmstone Date: Fri, 22 Mar 2024 13:32:43 +0100 Subject: [PATCH 12/16] feat: allow mountpoint to use client_attrs --- apps/emqx/src/emqx_channel.erl | 17 +++++------ apps/emqx/src/emqx_mountpoint.erl | 24 ++++++--------- apps/emqx/test/emqx_listeners_SUITE.erl | 30 +++++++++++++++++++ .../src/emqx_authn/emqx_authn_utils.erl | 4 +-- .../test/emqx_authn_http_SUITE.erl | 3 +- rel/i18n/emqx_schema.hocon | 3 +- 6 files changed, 52 insertions(+), 29 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 49a5b2226..9707b49b0 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1563,9 +1563,7 @@ enrich_client(ConnPkt, Channel = #channel{clientinfo = ClientInfo}) -> 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 + fun maybe_set_client_initial_attr/2 ], ConnPkt, ClientInfo @@ -1728,11 +1726,11 @@ extract_attr_from_clientinfo(#{extract_from := username, extract_regexp := Regex extract_attr_from_clientinfo(_Config, _CLientInfo) -> ignored. -fix_mountpoint(_ConnPkt, #{mountpoint := undefined}) -> - ok; -fix_mountpoint(_ConnPkt, ClientInfo = #{mountpoint := MountPoint}) -> +fix_mountpoint(#{mountpoint := undefined} = ClientInfo) -> + ClientInfo; +fix_mountpoint(ClientInfo = #{mountpoint := MountPoint}) -> MountPoint1 = emqx_mountpoint:replvar(MountPoint, ClientInfo), - {ok, ClientInfo#{mountpoint := MountPoint1}}. + ClientInfo#{mountpoint := MountPoint1}. %%-------------------------------------------------------------------- %% Set log metadata @@ -1853,10 +1851,11 @@ merge_auth_result(ClientInfo, AuthResult0) when is_map(ClientInfo) andalso is_ma Attrs0 = maps:get(client_attrs, ClientInfo, #{}), Attrs1 = maps:get(client_attrs, AuthResult0, #{}), Attrs = maps:merge(Attrs0, Attrs1), - maps:merge( + NewClientInfo = maps:merge( ClientInfo#{client_attrs => Attrs}, AuthResult#{is_superuser => IsSuperuser} - ). + ), + fix_mountpoint(NewClientInfo). %%-------------------------------------------------------------------- %% Process Topic Alias diff --git a/apps/emqx/src/emqx_mountpoint.erl b/apps/emqx/src/emqx_mountpoint.erl index 42e44c176..c9ed9efc5 100644 --- a/apps/emqx/src/emqx_mountpoint.erl +++ b/apps/emqx/src/emqx_mountpoint.erl @@ -87,18 +87,12 @@ unmount_maybe_share(MountPoint, TopicFilter = #share{topic = Topic}) when -spec replvar(option(mountpoint()), map()) -> option(mountpoint()). replvar(undefined, _Vars) -> undefined; -replvar(MountPoint, Vars) -> - ClientID = maps:get(clientid, Vars, undefined), - UserName = maps:get(username, Vars, undefined), - EndpointName = maps:get(endpoint_name, Vars, undefined), - List = [ - {?PH_CLIENTID, ClientID}, - {?PH_USERNAME, UserName}, - {?PH_ENDPOINT_NAME, EndpointName} - ], - lists:foldl(fun feed_var/2, MountPoint, List). - -feed_var({_PlaceHolder, undefined}, MountPoint) -> - MountPoint; -feed_var({PlaceHolder, Value}, MountPoint) -> - emqx_topic:feed_var(PlaceHolder, Value, MountPoint). +replvar(MountPoint, Vars0) -> + Allowed = [clientid, username, endpoint_name, client_attrs], + Vars = maps:filter( + fun(K, V) -> V =/= undefined andalso lists:member(K, Allowed) end, + Vars0 + ), + Template = emqx_template:parse(MountPoint), + {String, _Errors} = emqx_template:render(Template, Vars), + unicode:characters_to_binary(String). diff --git a/apps/emqx/test/emqx_listeners_SUITE.erl b/apps/emqx/test/emqx_listeners_SUITE.erl index 78c2c2e3a..acd7656d7 100644 --- a/apps/emqx/test/emqx_listeners_SUITE.erl +++ b/apps/emqx/test/emqx_listeners_SUITE.erl @@ -143,6 +143,36 @@ t_max_conns_tcp(_Config) -> ) end). +t_client_attr_as_mountpoint(_Config) -> + Port = emqx_common_test_helpers:select_free_port(tcp), + ListenerConf = #{ + <<"bind">> => format_bind({"127.0.0.1", Port}), + <<"limiter">> => #{}, + <<"mountpoint">> => <<"groups/${client_attrs.ns}/">> + }, + emqx_config:put_zone_conf(default, [mqtt, client_attrs_init], #{ + extract_from => clientid, + extract_regexp => <<"^(.+)-.+$">>, + extract_as => <<"ns">> + }), + emqx_logger:set_log_level(debug), + with_listener(tcp, attr_as_moutpoint, ListenerConf, fun() -> + {ok, Client} = emqtt:start_link(#{ + hosts => [{"127.0.0.1", Port}], + clientid => <<"abc-123">> + }), + unlink(Client), + {ok, _} = emqtt:connect(Client), + TopicPrefix = atom_to_binary(?FUNCTION_NAME), + SubTopic = <>, + MatchTopic = <<"groups/abc/", TopicPrefix/binary, "/1">>, + {ok, _, [1]} = emqtt:subscribe(Client, SubTopic, 1), + ?assertMatch([_], emqx_router:match_routes(MatchTopic)), + emqtt:stop(Client) + end), + emqx_config:put_zone_conf(default, [mqtt, client_attrs_init], disabled), + ok. + t_current_conns_tcp(_Config) -> Port = emqx_common_test_helpers:select_free_port(tcp), Conf = #{ diff --git a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl index 9ce934a30..a2050fbf0 100644 --- a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl +++ b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl @@ -219,11 +219,11 @@ drop_invalid_attr(Map) when is_map(Map) -> do_drop_invalid_attr([]) -> []; do_drop_invalid_attr([{K, V} | More]) -> - case emqx_utils:is_restricted_str(K) andalso emqx_utils:is_restricted_str(V) of + case emqx_utils:is_restricted_str(K) of true -> [{iolist_to_binary(K), iolist_to_binary(V)} | do_drop_invalid_attr(More)]; false -> - ?SLOG(debug, #{msg => "invalid_client_attr_dropped", key => K, value => V}, #{ + ?SLOG(debug, #{msg => "invalid_client_attr_dropped", attr_name => K}, #{ tag => "AUTHN" }), do_drop_invalid_attr(More) diff --git a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl index 3f16c9e2c..54e1f4b11 100644 --- a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl +++ b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl @@ -548,8 +548,7 @@ samples() -> is_superuser => true, client_attrs => #{ fid => <<"n11">>, - <<"_bad_key">> => <<"v">>, - <<"ok_key">> => <<"but bad value">> + <<"#_bad_key">> => <<"v">> } }), Req0 diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index 32b8ff8e9..7539770db 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -1579,7 +1579,8 @@ client_attrs_init { where `NAME` is the name of the attribute specified in the config `extract_as`. The initialized client attribute will be stored in the `client_attrs` property with the specified name, and can be used as a placeholder in a template for authentication and authorization. - For example, use `${client_attrs.alias}` to render a HTTP POST body when `extract_as = alias`.""" + For example, use `${client_attrs.alias}` to render an HTTP POST body when `extract_as = alias`, + or render listener config `moutpoint = devices/${client_attrs.alias}/` to initialize a per-client topic namespace.""" } client_attrs_init_extract_from { From 22838f027ae2435ca27163fd6f84bb69a2aaf9d4 Mon Sep 17 00:00:00 2001 From: zmstone Date: Fri, 22 Mar 2024 20:57:36 +0100 Subject: [PATCH 13/16] fix: mountpoint template render should not replace unknown as undefined For backward compatibility, the unknown vars used in mountpoint is kept unchanged. e.g. '${unknown}/foo/bar' should be rendered as '${unknown}/foo/bar' but not 'undefined/foo/bar' --- apps/emqx/include/emqx_placeholder.hrl | 6 ++- apps/emqx/src/emqx_mountpoint.erl | 42 +++++++++++++++---- apps/emqx/test/emqx_mountpoint_SUITE.erl | 28 +++++++++++++ .../src/emqx_authz/emqx_authz_utils.erl | 16 +------ apps/emqx_utils/src/emqx_template.erl | 23 ++++++++++ 5 files changed, 90 insertions(+), 25 deletions(-) diff --git a/apps/emqx/include/emqx_placeholder.hrl b/apps/emqx/include/emqx_placeholder.hrl index e2711be69..31ce6a070 100644 --- a/apps/emqx/include/emqx_placeholder.hrl +++ b/apps/emqx/include/emqx_placeholder.hrl @@ -31,12 +31,14 @@ -define(PH_CERT_SUBJECT, ?PH(?VAR_CERT_SUBJECT)). -define(PH_CERT_CN_NAME, ?PH(?VAR_CERT_CN_NAME)). -%% MQTT +%% MQTT/Gateway -define(VAR_PASSWORD, "password"). -define(VAR_CLIENTID, "clientid"). -define(VAR_USERNAME, "username"). -define(VAR_TOPIC, "topic"). +-define(VAR_ENDPOINT_NAME, "endpoint_name"). -define(VAR_NS_CLIENT_ATTRS, {var_namespace, "client_attrs"}). + -define(PH_PASSWORD, ?PH(?VAR_PASSWORD)). -define(PH_CLIENTID, ?PH(?VAR_CLIENTID)). -define(PH_FROM_CLIENTID, ?PH("from_clientid")). @@ -90,7 +92,7 @@ -define(PH_NODE, ?PH("node")). -define(PH_REASON, ?PH("reason")). --define(PH_ENDPOINT_NAME, ?PH("endpoint_name")). +-define(PH_ENDPOINT_NAME, ?PH(?VAR_ENDPOINT_NAME)). -define(VAR_RETAIN, "retain"). -define(PH_RETAIN, ?PH(?VAR_RETAIN)). diff --git a/apps/emqx/src/emqx_mountpoint.erl b/apps/emqx/src/emqx_mountpoint.erl index c9ed9efc5..9d2790b9e 100644 --- a/apps/emqx/src/emqx_mountpoint.erl +++ b/apps/emqx/src/emqx_mountpoint.erl @@ -28,10 +28,19 @@ -export([replvar/2]). +-export([lookup/2]). + -export_type([mountpoint/0]). -type mountpoint() :: binary(). +-define(ALLOWED_VARS, [ + ?VAR_CLIENTID, + ?VAR_USERNAME, + ?VAR_ENDPOINT_NAME, + ?VAR_NS_CLIENT_ATTRS +]). + -spec mount(option(mountpoint()), Any) -> Any when Any :: emqx_types:topic() @@ -87,12 +96,29 @@ unmount_maybe_share(MountPoint, TopicFilter = #share{topic = Topic}) when -spec replvar(option(mountpoint()), map()) -> option(mountpoint()). replvar(undefined, _Vars) -> undefined; -replvar(MountPoint, Vars0) -> - Allowed = [clientid, username, endpoint_name, client_attrs], - Vars = maps:filter( - fun(K, V) -> V =/= undefined andalso lists:member(K, Allowed) end, - Vars0 - ), - Template = emqx_template:parse(MountPoint), - {String, _Errors} = emqx_template:render(Template, Vars), +replvar(MountPoint, Vars) -> + Template = parse(MountPoint), + {String, _Errors} = emqx_template:render(Template, {?MODULE, Vars}), unicode:characters_to_binary(String). + +lookup([<>], #{clientid := ClientId}) when is_binary(ClientId) -> + {ok, ClientId}; +lookup([<>], #{username := Username}) when is_binary(Username) -> + {ok, Username}; +lookup([<>], #{endpoint_name := Name}) when is_binary(Name) -> + {ok, Name}; +lookup([<<"client_attrs">>, AttrName], #{client_attrs := Attrs}) when is_map(Attrs) -> + Original = iolist_to_binary(["${client_attrs.", AttrName, "}"]), + {ok, maps:get(AttrName, Attrs, Original)}; +lookup(Accessor, _) -> + {ok, iolist_to_binary(["${", lists:join(".", Accessor), "}"])}. + +parse(Template) -> + Parsed = emqx_template:parse(Template), + case emqx_template:validate(?ALLOWED_VARS, Parsed) of + ok -> + Parsed; + {error, _Disallowed} -> + Escaped = emqx_template:escape_disallowed(Parsed, ?ALLOWED_VARS), + emqx_template:parse(Escaped) + end. diff --git a/apps/emqx/test/emqx_mountpoint_SUITE.erl b/apps/emqx/test/emqx_mountpoint_SUITE.erl index 06913a35c..756bfeb59 100644 --- a/apps/emqx/test/emqx_mountpoint_SUITE.erl +++ b/apps/emqx/test/emqx_mountpoint_SUITE.erl @@ -116,4 +116,32 @@ t_replvar(_) -> username => undefined } ) + ), + ?assertEqual( + <<"mount/g1/clientid/">>, + replvar( + <<"mount/${client_attrs.group}/${clientid}/">>, + #{ + clientid => <<"clientid">>, + client_attrs => #{<<"group">> => <<"g1">>} + } + ) + ), + ?assertEqual( + <<"mount/${client_attrs.group}/clientid/">>, + replvar( + <<"mount/${client_attrs.group}/${clientid}/">>, + #{ + clientid => <<"clientid">> + } + ) + ), + ?assertEqual( + <<"mount/${not.allowed}/clientid/">>, + replvar( + <<"mount/${not.allowed}/${clientid}/">>, + #{ + clientid => <<"clientid">> + } + ) ). diff --git a/apps/emqx_auth/src/emqx_authz/emqx_authz_utils.erl b/apps/emqx_auth/src/emqx_authz/emqx_authz_utils.erl index 9a795d434..26b8d000f 100644 --- a/apps/emqx_auth/src/emqx_authz/emqx_authz_utils.erl +++ b/apps/emqx_auth/src/emqx_authz/emqx_authz_utils.erl @@ -139,7 +139,7 @@ handle_disallowed_placeholders(Template, Source, Allowed) -> " However, consider using `${$}` escaping for literal `$` where" " needed to avoid unexpected results." }), - Result = prerender_disallowed_placeholders(Template, Allowed), + Result = emqx_template:escape_disallowed(Template, Allowed), case Source of {string, _} -> emqx_template:parse(Result); @@ -148,20 +148,6 @@ handle_disallowed_placeholders(Template, Source, Allowed) -> end end. -prerender_disallowed_placeholders(Template, Allowed) -> - {Result, _} = emqx_template:render(Template, #{}, #{ - var_trans => fun(Name, _) -> - % NOTE - % Rendering disallowed placeholders in escaped form, which will then - % parse as a literal string. - case lists:member(Name, Allowed) of - true -> "${" ++ Name ++ "}"; - false -> "${$}{" ++ Name ++ "}" - end - end - }), - Result. - render_deep(Template, Values) -> % NOTE % Ignoring errors here, undefined bindings will be replaced with empty string. diff --git a/apps/emqx_utils/src/emqx_template.erl b/apps/emqx_utils/src/emqx_template.erl index b0272b1bb..1383f90e1 100644 --- a/apps/emqx_utils/src/emqx_template.erl +++ b/apps/emqx_utils/src/emqx_template.erl @@ -32,6 +32,7 @@ -export([lookup/2]). -export([to_string/1]). +-export([escape_disallowed/2]). -export_type([t/0]). -export_type([str/0]). @@ -157,9 +158,31 @@ validate(Allowed, Template) -> {error, [{Var, disallowed} || Var <- Disallowed]} end. +%% @doc Escape `$' with `${$}' for the variable references +%% which are not allowed, so the original variable name +%% can be preserved instead of rendered as `undefined'. +%% E.g. to render `${var1}/${clientid}', if only `clientid' +%% is allowed, the rendering result should be `${var1}/client1' +%% but not `undefined/client1'. +escape_disallowed(Template, Allowed) -> + {Result, _} = render(Template, #{}, #{ + var_trans => fun(Name, _) -> + case is_allowed(Name, Allowed) of + true -> "${" ++ Name ++ "}"; + false -> "${$}{" ++ Name ++ "}" + end + end + }), + Result. + find_disallowed(Vars, Allowed) -> lists:filter(fun(Var) -> not is_allowed(Var, Allowed) end, Vars). +%% @private Return 'true' if a variable reference matches +%% at least one allowed variables. +%% For `"${var_name}"' kind of reference, its a `=:=' compare +%% for `{var_namespace, "namespace"}' kind of reference +%% it matches the `"namespace."' prefix. is_allowed(_Var, []) -> false; is_allowed(Var, [{var_namespace, VarPrefix} | Allowed]) -> From df913f245038a6a5fcab2e75ee0a84e74bdaabad Mon Sep 17 00:00:00 2001 From: zmstone Date: Sat, 23 Mar 2024 11:57:28 +0100 Subject: [PATCH 14/16] docs: refine changelog --- apps/emqx_conf/src/emqx_conf.erl | 4 ++-- changes/ce/feat-12750.en.md | 14 ++++++++++---- rel/i18n/emqx_schema.hocon | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index b685db278..122998eeb 100644 --- a/apps/emqx_conf/src/emqx_conf.erl +++ b/apps/emqx_conf/src/emqx_conf.erl @@ -424,14 +424,14 @@ is_missing_namespace(ShortName, FullName, RootNames) -> ShortName =:= FullName end. -%% Returns short name from full name, fullname delemited by colon(:). +%% Returns short name from full name, fullname delimited by colon(:). short_name(FullName) -> case string:split(FullName, ":") of [_, Name] -> to_bin(Name); _ -> to_bin(FullName) end. -%% Returns the hash-anchor from full name, fullname delemited by colon(:). +%% Returns the hash-anchor from full name, fullname delimited by colon(:). format_hash(FullName) -> case string:split(FullName, ":") of [Namespace, Name] -> diff --git a/changes/ce/feat-12750.en.md b/changes/ce/feat-12750.en.md index 33c1d0b4f..bd7375168 100644 --- a/changes/ce/feat-12750.en.md +++ b/changes/ce/feat-12750.en.md @@ -2,16 +2,18 @@ Customizable client attributes in `clientinfo`. Introduced a new field `client_attrs` in the `clientinfo` object. This enhancement enables the initialization of `client_attrs` with specific -attributes derived from the `clientinfo` fields upon accepting an MQTT connection. +attributes derived from the `clientinfo` fields, immediately up on accepting +an MQTT connection. ### Initialization of `client_attrs` - The `client_attrs` field can be initially populated based on the configuration from one of the following sources: + - `cn`: The common name from the TLS client's certificate. + - `dn`: The distinguished name from the TLS client's certificate, that is, the certificate "Subject". - `clientid`: The MQTT client ID provided by the client. - `username`: The username provided by the client. - - `cn`: The common name from the TLS client's certificate. - - `dn`: The distinguished name from the TLS client's certificate, i.e., the certificate "Subject". + - `user_property`: Extract a property value from 'User-Property' of the MQTT CONNECT packet. ### Extension through Authentication Responses @@ -21,7 +23,11 @@ attributes derived from the `clientinfo` fields upon accepting an MQTT connectio `client_attrs` field. - **JWT**: Attributes can be included via a `client_attrs` claim within the JWT. -### Usage in Authorization +### Usage in Authentication and Authorization + +- If `client_attrs` is initialized before authentication, it can be used in external authentication + requests. For instance, `${client_attrs.property1}` can be used within request templates + directed at an HTTP server for the purpose of authenticity validation. - The `client_attrs` can be utilized in authorization configurations or request templates, enhancing flexibility and control. Examples include: diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index 7539770db..62a908663 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -1592,7 +1592,7 @@ client_attrs_init_extract_from { - `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. + - `dn`: Extract from the Distinguished Name (DN) field of the client certificate. - `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. @@ -1605,7 +1605,7 @@ client_attrs_init_extract_regexp { 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 `^(.+?)-.*$`. + For example if the client attribute is the first part of the client ID delimited by a dash, the regular expression would be `^(.+?)-.*$`. Note that failure to match the regular expression will result in the client attribute being absent but not an empty string.""" } From b1c9bb63d7dcbf4237f7ec4a591c35dba3af908b Mon Sep 17 00:00:00 2001 From: zmstone Date: Tue, 26 Mar 2024 13:20:48 +0100 Subject: [PATCH 15/16] chore: delete dead code hocon schema validator never validates 'undefined' --- apps/emqx/src/emqx_schema.erl | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 4311e100e..427df5db0 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -1748,8 +1748,6 @@ fields("client_attrs_init") -> })} ]. -restricted_string(undefined) -> - undefined; restricted_string(Str) -> case emqx_utils:is_restricted_str(Str) of true -> ok; From 450ef2d5914084cd5c08112af43d60d2b44f0436 Mon Sep 17 00:00:00 2001 From: zmstone Date: Wed, 27 Mar 2024 09:37:38 +0100 Subject: [PATCH 16/16] docs: refine docs --- rel/i18n/emqx_mgmt_api_trace.hocon | 2 +- rel/i18n/emqx_schema.hocon | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/rel/i18n/emqx_mgmt_api_trace.hocon b/rel/i18n/emqx_mgmt_api_trace.hocon index a53dabae3..67462ab43 100644 --- a/rel/i18n/emqx_mgmt_api_trace.hocon +++ b/rel/i18n/emqx_mgmt_api_trace.hocon @@ -56,7 +56,7 @@ file_mtime.label: """file mtime""" trace_name.desc: -"""Unique name of the trace. Only ascii letters in a-z, A-Z, 0-9 and underscore '_' are allowed.""" +"""Unique name of the trace. Only ASCII letters in a-z, A-Z, 0-9 and underscore '_' are allowed.""" trace_name.label: """Unique name of the trace""" diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index 62a908663..0bd8c74d5 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -1572,7 +1572,7 @@ durable_storage.desc: """~ EMQX uses durable storages to offload various data, such as MQTT messages, to disc.""" client_attrs_init { - label: "Client attributes init" + label: "Client Attributes Initialization" desc: """~ Specify how to initialize client attributes. One initial client attribute can be initialized as `client_attrs.NAME`, @@ -1584,7 +1584,7 @@ client_attrs_init { } client_attrs_init_extract_from { - label: "Client property to extract attribute from" + label: "Client Property to Extract Attribute" desc: """~ Specify from which client property the client attribute should be extracted. @@ -1594,23 +1594,24 @@ client_attrs_init_extract_from { - `cn`: Extract from the Common Name (CN) field of the client certificate. - `dn`: Extract from the Distinguished Name (DN) field of the client certificate. - `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. + In this case, `extract_regexp` 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_regexp { - label: "Client attribute extract regex" + label: "Client Attribute Extraction Regular Expression" 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 delimited by a dash, the regular expression would be `^(.+?)-.*$`. - Note that failure to match the regular expression will result in the client attribute being absent but not an empty string.""" + Note that failure to match the regular expression will result in the client attribute being absent but not an empty string. + Note also that currently only printable ASCII characters are allowed as input for the regular expression extraction.""" } client_attrs_init_extract_as { - label: "Name the extracted attribute" + 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_attrs` property with this name.