Merge pull request #12750 from zmstone/0316-introduce-client-attrs
0316 introduce client_attrs
This commit is contained in:
commit
beb9152d50
|
@ -31,11 +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")).
|
||||
|
@ -89,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)).
|
||||
|
||||
|
|
|
@ -251,7 +251,7 @@ init(
|
|||
MP -> MP
|
||||
end,
|
||||
ListenerId = emqx_listeners:listener_id(Type, Listener),
|
||||
ClientInfo = set_peercert_infos(
|
||||
ClientInfo0 = set_peercert_infos(
|
||||
Peercert,
|
||||
#{
|
||||
zone => Zone,
|
||||
|
@ -269,6 +269,8 @@ init(
|
|||
},
|
||||
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,
|
||||
|
@ -1570,7 +1572,8 @@ enrich_client(ConnPkt, Channel = #channel{clientinfo = ClientInfo}) ->
|
|||
fun set_bridge_mode/2,
|
||||
fun maybe_username_as_clientid/2,
|
||||
fun maybe_assign_clientid/2,
|
||||
fun fix_mountpoint/2
|
||||
%% attr init should happen after clientid and username assign
|
||||
fun maybe_set_client_initial_attr/2
|
||||
],
|
||||
ConnPkt,
|
||||
ClientInfo
|
||||
|
@ -1582,6 +1585,47 @@ enrich_client(ConnPkt, Channel = #channel{clientinfo = ClientInfo}) ->
|
|||
{error, ReasonCode, Channel#channel{clientinfo = NClientInfo}}
|
||||
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;
|
||||
initialize_client_attrs_from_cert(_, ClientInfo, _Peercert) ->
|
||||
ClientInfo.
|
||||
|
||||
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}
|
||||
|
@ -1622,11 +1666,81 @@ maybe_assign_clientid(#mqtt_packet_connect{clientid = <<>>}, ClientInfo) ->
|
|||
maybe_assign_clientid(#mqtt_packet_connect{clientid = ClientId}, ClientInfo) ->
|
||||
{ok, ClientInfo#{clientid => ClientId}}.
|
||||
|
||||
fix_mountpoint(_ConnPkt, #{mountpoint := undefined}) ->
|
||||
ok;
|
||||
fix_mountpoint(_ConnPkt, ClientInfo = #{mountpoint := MountPoint}) ->
|
||||
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,
|
||||
?SLOG(
|
||||
debug,
|
||||
#{
|
||||
msg => "client_attr_init_from_clientinfo",
|
||||
extracted_as => Name,
|
||||
extracted_value => Value
|
||||
}
|
||||
),
|
||||
{ok, ClientInfo#{client_attrs => Attrs#{Name => Value}}};
|
||||
_ ->
|
||||
{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
|
||||
}) ->
|
||||
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(#{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
|
||||
|
@ -1735,9 +1849,23 @@ 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) ->
|
||||
IsSuperuser = maps:get(is_superuser, AuthResult, false),
|
||||
maps:merge(ClientInfo, AuthResult#{is_superuser => IsSuperuser}).
|
||||
%% 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. `client_attrs': Extra client attributes from JWT, HTTP auth backend
|
||||
%% 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),
|
||||
Attrs0 = maps:get(client_attrs, ClientInfo, #{}),
|
||||
Attrs1 = maps:get(client_attrs, AuthResult0, #{}),
|
||||
Attrs = maps:merge(Attrs0, Attrs1),
|
||||
NewClientInfo = maps:merge(
|
||||
ClientInfo#{client_attrs => Attrs},
|
||||
AuthResult#{is_superuser => IsSuperuser}
|
||||
),
|
||||
fix_mountpoint(NewClientInfo).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Process Topic Alias
|
||||
|
|
|
@ -227,7 +227,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
|
||||
|
|
|
@ -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()
|
||||
|
@ -88,17 +97,28 @@ unmount_maybe_share(MountPoint, TopicFilter = #share{topic = Topic}) when
|
|||
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).
|
||||
Template = parse(MountPoint),
|
||||
{String, _Errors} = emqx_template:render(Template, {?MODULE, Vars}),
|
||||
unicode:characters_to_binary(String).
|
||||
|
||||
feed_var({_PlaceHolder, undefined}, MountPoint) ->
|
||||
MountPoint;
|
||||
feed_var({PlaceHolder, Value}, MountPoint) ->
|
||||
emqx_topic:feed_var(PlaceHolder, Value, MountPoint).
|
||||
lookup([<<?VAR_CLIENTID>>], #{clientid := ClientId}) when is_binary(ClientId) ->
|
||||
{ok, ClientId};
|
||||
lookup([<<?VAR_USERNAME>>], #{username := Username}) when is_binary(Username) ->
|
||||
{ok, Username};
|
||||
lookup([<<?VAR_ENDPOINT_NAME>>], #{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.
|
||||
|
|
|
@ -1731,7 +1731,28 @@ 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, user_property]),
|
||||
#{desc => ?DESC("client_attrs_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(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) ++
|
||||
|
@ -1987,6 +2008,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.
|
||||
|
||||
|
@ -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.
|
||||
|
|
|
@ -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,12 @@
|
|||
anonymous => boolean(),
|
||||
cn => binary(),
|
||||
dn => binary(),
|
||||
%% 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()}.
|
||||
-type clientid() :: binary() | atom().
|
||||
-type username() :: option(binary()).
|
||||
-type password() :: option(binary()).
|
||||
|
@ -270,7 +275,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()}].
|
||||
|
||||
|
|
|
@ -75,6 +75,9 @@ 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_client_attr_from_user_property,
|
||||
t_certcn_as_clientid_default_config_tls,
|
||||
t_certcn_as_clientid_tlsv1_3,
|
||||
t_certcn_as_clientid_tlsv1_2,
|
||||
|
@ -384,6 +387,55 @@ 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_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).
|
||||
|
||||
|
|
|
@ -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 =>
|
||||
#{
|
||||
|
|
|
@ -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 = <<TopicPrefix/binary, "/#">>,
|
||||
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 = #{
|
||||
|
|
|
@ -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">>
|
||||
}
|
||||
)
|
||||
).
|
||||
|
|
|
@ -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,
|
||||
|
@ -45,13 +46,16 @@
|
|||
default_headers_no_content_type/0
|
||||
]).
|
||||
|
||||
%% 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, #{
|
||||
|
@ -204,6 +208,27 @@ is_superuser(#{<<"is_superuser">> := Value}) ->
|
|||
is_superuser(#{}) ->
|
||||
#{is_superuser => false}.
|
||||
|
||||
client_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([]) ->
|
||||
[];
|
||||
do_drop_invalid_attr([{K, V} | More]) ->
|
||||
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", attr_name => K}, #{
|
||||
tag => "AUTHN"
|
||||
}),
|
||||
do_drop_invalid_attr(More)
|
||||
end.
|
||||
|
||||
ensure_apps_started(bcrypt) ->
|
||||
{ok, _} = application:ensure_all_started(bcrypt),
|
||||
ok;
|
||||
|
|
|
@ -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,7 +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]),
|
||||
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}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(),
|
||||
|
||||
|
|
|
@ -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 = emqx_authn_utils:client_attrs(NBody),
|
||||
Result = maps:merge(IsSuperuser, Attrs),
|
||||
{ok, Result};
|
||||
<<"deny">> ->
|
||||
{error, not_authorized};
|
||||
<<"ignore">> ->
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -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)).
|
||||
|
@ -533,7 +534,7 @@ samples() ->
|
|||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => false, user_property => #{}}}
|
||||
result => {ok, #{is_superuser => false, client_attrs => #{}}}
|
||||
},
|
||||
|
||||
%% get request with json body response
|
||||
|
@ -542,13 +543,20 @@ 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">>
|
||||
}
|
||||
}),
|
||||
Req0
|
||||
),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => true, user_property => #{}}}
|
||||
result => {ok, #{is_superuser => true, client_attrs => #{<<"fid">> => <<"n11">>}}}
|
||||
},
|
||||
|
||||
%% get request with url-form-encoded body response
|
||||
|
@ -566,7 +574,7 @@ samples() ->
|
|||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => true, user_property => #{}}}
|
||||
result => {ok, #{is_superuser => true, client_attrs => #{}}}
|
||||
},
|
||||
|
||||
%% get request with response of unknown encoding
|
||||
|
@ -608,7 +616,7 @@ samples() ->
|
|||
<<"method">> => <<"post">>,
|
||||
<<"headers">> => #{<<"content-type">> => <<"application/json">>}
|
||||
},
|
||||
result => {ok, #{is_superuser => false, user_property => #{}}}
|
||||
result => {ok, #{is_superuser => false, client_attrs => #{}}}
|
||||
},
|
||||
|
||||
%% simple post request, application/x-www-form-urlencoded
|
||||
|
@ -634,7 +642,7 @@ samples() ->
|
|||
<<"application/x-www-form-urlencoded">>
|
||||
}
|
||||
},
|
||||
result => {ok, #{is_superuser => false, user_property => #{}}}
|
||||
result => {ok, #{is_superuser => false, client_attrs => #{}}}
|
||||
},
|
||||
|
||||
%% simple post request for placeholders, application/json
|
||||
|
@ -647,7 +655,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,
|
||||
|
@ -666,10 +675,11 @@ 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, user_property => #{}}}
|
||||
result => {ok, #{is_superuser => false, client_attrs => #{}}}
|
||||
},
|
||||
|
||||
%% custom headers
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -219,8 +219,12 @@ 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
|
||||
{ok, acl(Extra, AclClaimName)}
|
||||
ACL = acl(Extra, AclClaimName),
|
||||
Result = maps:merge(IsSuperuser, maps:merge(ACL, Attrs)),
|
||||
{ok, Result}
|
||||
catch
|
||||
throw:{bad_acl_rule, Reason} ->
|
||||
%% it's a invalid token, so ok to log
|
||||
|
@ -242,7 +246,6 @@ verify(JWT, JWKs, VerifyClaims, AclClaimName) ->
|
|||
end.
|
||||
|
||||
acl(Claims, AclClaimName) ->
|
||||
Acl =
|
||||
case Claims of
|
||||
#{AclClaimName := Rules} ->
|
||||
#{
|
||||
|
@ -254,8 +257,7 @@ acl(Claims, AclClaimName) ->
|
|||
};
|
||||
_ ->
|
||||
#{}
|
||||
end,
|
||||
maps:merge(emqx_authn_utils:is_superuser(Claims), Acl).
|
||||
end.
|
||||
|
||||
do_verify(_JWT, [], _VerifyClaims) ->
|
||||
{error, invalid_signature};
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
?VAR_CLIENTID,
|
||||
?VAR_PEERHOST,
|
||||
?VAR_CERT_CN_NAME,
|
||||
?VAR_CERT_SUBJECT
|
||||
?VAR_CERT_SUBJECT,
|
||||
?VAR_NS_CLIENT_ATTRS
|
||||
]).
|
||||
|
||||
description() ->
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -42,7 +42,8 @@
|
|||
?VAR_CLIENTID,
|
||||
?VAR_PEERHOST,
|
||||
?VAR_CERT_CN_NAME,
|
||||
?VAR_CERT_SUBJECT
|
||||
?VAR_CERT_SUBJECT,
|
||||
?VAR_NS_CLIENT_ATTRS
|
||||
]).
|
||||
|
||||
description() ->
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -42,7 +42,8 @@
|
|||
?VAR_CLIENTID,
|
||||
?VAR_PEERHOST,
|
||||
?VAR_CERT_CN_NAME,
|
||||
?VAR_CERT_SUBJECT
|
||||
?VAR_CERT_SUBJECT,
|
||||
?VAR_NS_CLIENT_ATTRS
|
||||
]).
|
||||
|
||||
description() ->
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
?VAR_CERT_SUBJECT,
|
||||
?VAR_PEERHOST,
|
||||
?VAR_CLIENTID,
|
||||
?VAR_USERNAME
|
||||
?VAR_USERNAME,
|
||||
?VAR_NS_CLIENT_ATTRS
|
||||
]).
|
||||
|
||||
description() ->
|
||||
|
|
|
@ -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] ->
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
-export([lookup/2]).
|
||||
|
||||
-export([to_string/1]).
|
||||
-export([escape_disallowed/2]).
|
||||
|
||||
-export_type([t/0]).
|
||||
-export_type([str/0]).
|
||||
|
@ -145,18 +146,57 @@ 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.
|
||||
|
||||
%% @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]) ->
|
||||
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()) ->
|
||||
|
|
|
@ -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").
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
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, 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.
|
||||
- `user_property`: Extract a property value from 'User-Property' of the MQTT CONNECT packet.
|
||||
|
||||
### 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 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:
|
||||
- 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.
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -1565,12 +1565,57 @@ 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 Initialization"
|
||||
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_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 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 {
|
||||
label: "Client Property to Extract Attribute"
|
||||
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 certificate.
|
||||
- `user_property`: Extract from the user property sent in the MQTT v5 `CONNECT` packet.
|
||||
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 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 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"
|
||||
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.
|
||||
In case `extract_from = user_property`, this should be the key of the user property."""
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue