diff --git a/apps/emqx_authn/i18n/emqx_authn_http_i18n.conf b/apps/emqx_authn/i18n/emqx_authn_http_i18n.conf index 35a5b4973..129db5054 100644 --- a/apps/emqx_authn/i18n/emqx_authn_http_i18n.conf +++ b/apps/emqx_authn/i18n/emqx_authn_http_i18n.conf @@ -18,7 +18,7 @@ emqx_authn_http { en: """HTTP request method.""" zh: """HTTP 请求方法。""" } - label: { + label { en: """Request Method""" zh: """请求方法""" } @@ -27,9 +27,9 @@ emqx_authn_http { url { desc { en: """URL of the HTTP server.""" - zh: """HTTP 服务器地址。""" + zh: """认证 HTTP 服务器地址。""" } - label: { + label { en: """URL""" zh: """URL""" } @@ -37,23 +37,23 @@ emqx_authn_http { headers { desc { - en: """HTTP request headers.""" - zh: """HTTP request headers。""" + en: """List of HTTP Headers.""" + zh: """HTTP Headers 列表""" } - label: { - en: """Request Headers""" - zh: """Request Headers""" + label { + en: """Headers""" + zh: """请求头""" } } headers_no_content_type { desc { - en: """HTTP request headers (without content-type).""" - zh: """HTTP request headers(无 content-type)。""" + en: """List of HTTP headers (without content-type).""" + zh: """HTTP Headers 列表 (无 content-type) 。""" } - label: { - en: """Request Headers""" - zh: """Request Headers""" + label { + en: """headers_no_content_type""" + zh: """请求头(无 content-type)""" } } @@ -62,7 +62,7 @@ emqx_authn_http { en: """HTTP request body.""" zh: """HTTP request body。""" } - label: { + label { en: """Request Body""" zh: """Request Body""" } @@ -73,7 +73,7 @@ emqx_authn_http { en: """HTTP request timeout.""" zh: """HTTP 请求超时时长。""" } - label: { + label { en: """Request Timeout""" zh: """请求超时时间""" } diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index b03a7f1b1..9e25f5a7a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -83,7 +83,10 @@ common_fields() -> {mechanism, emqx_authn_schema:mechanism(password_based)}, {backend, emqx_authn_schema:backend(http)}, {url, fun url/1}, - {body, hoconsc:mk(map([{fuzzy, term(), binary()}]), #{desc => ?DESC(body)})}, + {body, + hoconsc:mk(map([{fuzzy, term(), binary()}]), #{ + required => false, desc => ?DESC(body) + })}, {request_timeout, fun request_timeout/1} ] ++ emqx_authn_schema:common_fields() ++ maps:to_list( @@ -127,7 +130,10 @@ headers_no_content_type(desc) -> ?DESC(?FUNCTION_NAME); headers_no_content_type(converter) -> fun(Headers) -> - maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) + maps:without( + [<<"content-type">>], + maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) + ) end; headers_no_content_type(default) -> default_headers_no_content_type(); @@ -155,26 +161,23 @@ create(_AuthenticatorID, Config) -> create( #{ method := Method, - url := RawURL, - headers := HeadersT, - body := Body, + url := RawUrl, + headers := Headers, request_timeout := RequestTimeout } = Config ) -> - Headers = ensure_header_name_type(HeadersT), - {BsaeUrlWithPath, Query} = parse_fullpath(RawURL), - URIMap = parse_url(BsaeUrlWithPath), + {BaseUrl0, Path, Query} = parse_url(RawUrl), + {ok, BaseUrl} = emqx_http_lib:uri_parse(BaseUrl0), ResourceId = emqx_authn_utils:make_resource_id(?MODULE), State = #{ method => Method, - path => maps:get(path, URIMap), + path => Path, + headers => Headers, + base_path_templete => emqx_authn_utils:parse_str(Path), base_query_template => emqx_authn_utils:parse_deep( cow_qs:parse_qs(to_bin(Query)) ), - headers => maps:to_list(Headers), - body_template => emqx_authn_utils:parse_deep( - maps:to_list(Body) - ), + body_template => emqx_authn_utils:parse_deep(maps:get(body, Config, #{})), request_timeout => RequestTimeout, resource_id => ResourceId }, @@ -184,7 +187,7 @@ create( ?RESOURCE_GROUP, emqx_connector_http, Config#{ - base_url => maps:remove(query, URIMap), + base_url => BaseUrl, pool_type => random }, #{} @@ -273,9 +276,6 @@ destroy(#{resource_id := ResourceId}) -> %% Internal functions %%-------------------------------------------------------------------- -parse_fullpath(RawURL) -> - cow_http:parse_fullpath(to_bin(RawURL)). - default_headers() -> maps:put( <<"content-type">>, @@ -302,14 +302,14 @@ transform_header_name(Headers) -> ). check_ssl_opts(Conf) -> - {BaseUrlWithPath, _Query} = parse_fullpath(get_conf_val("url", Conf)), - case parse_url(BaseUrlWithPath) of - #{scheme := https} -> + {BaseUrl, _Path, _Query} = parse_url(get_conf_val("url", Conf)), + case BaseUrl of + <<"https://", _/binary>> -> case get_conf_val("ssl.enable", Conf) of true -> ok; false -> false end; - #{scheme := http} -> + <<"http://", _/binary>> -> ok end. @@ -318,39 +318,51 @@ check_headers(Conf) -> Headers = get_conf_val("headers", Conf), Method =:= <<"post">> orelse (not maps:is_key(<<"content-type">>, Headers)). -parse_url(URL) -> - {ok, URIMap} = emqx_http_lib:uri_parse(URL), - case maps:get(query, URIMap, undefined) of - undefined -> - URIMap#{query => ""}; - _ -> - URIMap +parse_url(Url) -> + case string:split(Url, "//", leading) of + [Scheme, UrlRem] -> + case string:split(UrlRem, "/", leading) of + [HostPort, Remaining] -> + BaseUrl = iolist_to_binary([Scheme, "//", HostPort]), + case string:split(Remaining, "?", leading) of + [Path, QueryString] -> + {BaseUrl, Path, QueryString}; + [Path] -> + {BaseUrl, Path, <<>>} + end; + [HostPort] -> + {iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>} + end; + [Url] -> + throw({invalid_url, Url}) end. generate_request(Credential, #{ method := Method, - path := Path, + headers := Headers0, + base_path_templete := BasePathTemplate, base_query_template := BaseQueryTemplate, - headers := Headers, body_template := BodyTemplate }) -> + Headers = maps:to_list(Headers0), + Path = emqx_authn_utils:render_str(BasePathTemplate, Credential), + Query = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential), Body = emqx_authn_utils:render_deep(BodyTemplate, Credential), - NBaseQuery = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential), case Method of get -> - NPath = append_query(Path, NBaseQuery ++ Body), - {NPath, Headers}; + NPathQuery = append_query(to_list(Path), to_list(Query) ++ maps:to_list(Body)), + {NPathQuery, Headers}; post -> - NPath = append_query(Path, NBaseQuery), + NPathQuery = append_query(to_list(Path), to_list(Query)), ContentType = proplists:get_value(<<"content-type">>, Headers), NBody = serialize_body(ContentType, Body), - {NPath, Headers, NBody} + {NPathQuery, Headers, NBody} end. append_query(Path, []) -> - Path; + encode_path(Path); append_query(Path, Query) -> - Path ++ "?" ++ binary_to_list(qs(Query)). + encode_path(Path) ++ "?" ++ binary_to_list(qs(Query)). qs(KVs) -> qs(KVs, []). @@ -387,12 +399,18 @@ may_append_body(Output, {ok, _, _}) -> Output. uri_encode(T) -> - emqx_http_lib:uri_encode(to_bin(T)). + emqx_http_lib:uri_encode(to_list(T)). + +encode_path(Path) -> + Parts = string:split(Path, "/", all), + lists:flatten(["/" ++ Part || Part <- lists:map(fun uri_encode/1, Parts)]). to_list(A) when is_atom(A) -> atom_to_list(A); to_list(B) when is_binary(B) -> - binary_to_list(B). + binary_to_list(B); +to_list(L) when is_list(L) -> + L. to_bin(A) when is_atom(A) -> atom_to_binary(A); @@ -403,14 +421,3 @@ to_bin(L) when is_list(L) -> get_conf_val(Name, Conf) -> hocon_maps:get(?CONF_NS ++ "." ++ Name, Conf). - -ensure_header_name_type(Headers) -> - Fun = fun - (Key, _Val, Acc) when is_binary(Key) -> - Acc; - (Key, Val, Acc) when is_atom(Key) -> - Acc2 = maps:remove(Key, Acc), - BinKey = erlang:atom_to_binary(Key), - Acc2#{BinKey => Val} - end, - maps:fold(Fun, Headers, Headers). diff --git a/apps/emqx_authn/test/emqx_authn_api_SUITE.erl b/apps/emqx_authn/test/emqx_authn_api_SUITE.erl index 5fe1f9b17..fe4a8af7e 100644 --- a/apps/emqx_authn/test/emqx_authn_api_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_api_SUITE.erl @@ -184,14 +184,14 @@ test_authenticators(PathPrefix) -> InvalidConfig0 ), - InvalidConfig1 = ValidConfig#{ + ValidConfig1 = ValidConfig#{ method => <<"get">>, headers => #{<<"content-type">> => <<"application/json">>} }, - {ok, 400, _} = request( - post, - uri(PathPrefix ++ [?CONF_NS]), - InvalidConfig1 + {ok, 200, _} = request( + put, + uri(PathPrefix ++ [?CONF_NS, "password_based:http"]), + ValidConfig1 ), ?assertAuthenticatorsMatch( @@ -264,23 +264,23 @@ test_authenticator(PathPrefix) -> InvalidConfig0 ), - InvalidConfig1 = ValidConfig0#{ + ValidConfig1 = ValidConfig0#{ method => <<"get">>, headers => #{<<"content-type">> => <<"application/json">>} }, - {ok, 400, _} = request( - put, - uri(PathPrefix ++ [?CONF_NS, "password_based:http"]), - InvalidConfig1 - ), - - ValidConfig1 = ValidConfig0#{pool_size => 9}, {ok, 200, _} = request( put, uri(PathPrefix ++ [?CONF_NS, "password_based:http"]), ValidConfig1 ), + ValidConfig2 = ValidConfig0#{pool_size => 9}, + {ok, 200, _} = request( + put, + uri(PathPrefix ++ [?CONF_NS, "password_based:http"]), + ValidConfig2 + ), + {ok, 404, _} = request( delete, uri(PathPrefix ++ [?CONF_NS, "password_based:redis"]) diff --git a/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf b/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf index d5c86ba59..6d2c66b79 100644 --- a/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf +++ b/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf @@ -60,19 +60,19 @@ emqx_authz_api_schema { headers { desc { - en: """List of HTTP headers.""" - zh: """""" + en: """List of HTTP Headers.""" + zh: """HTTP Headers 列表""" } label { - en: """headers""" + en: """Headers""" zh: """请求头""" } } headers_no_content_type { desc { - en: """List of HTTP headers (without `content_type`).""" - zh: """""" + en: """List of HTTP headers (without content-type).""" + zh: """HTTP Headers 列表(无 content-type)""" } label { en: """headers_no_content_type""" diff --git a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf b/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf index a05cb3fa1..4f1c35ee8 100644 --- a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf +++ b/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf @@ -122,7 +122,7 @@ and the new rules will override all rules from the old config file. http_get { desc { en: """Authorization using an external HTTP server (via GET requests).""" - zh: """使用外部 HTTP 服务器鉴权(GET 请求).""" + zh: """使用外部 HTTP 服务器鉴权(GET 请求)。""" } label { en: """http_get""" @@ -133,7 +133,7 @@ and the new rules will override all rules from the old config file. http_post { desc { en: """Authorization using an external HTTP server (via POST requests).""" - zh: """使用外部 HTTP 服务器鉴权(POST 请求).""" + zh: """使用外部 HTTP 服务器鉴权(POST 请求)。""" } label { en: """http_post""" @@ -155,29 +155,29 @@ and the new rules will override all rules from the old config file. url { desc { en: """URL of the auth server.""" - zh: """认证服务器 URL""" + zh: """鉴权 HTTP 服务器地址。""" } label { - en: """url""" - zh: """url""" + en: """URL""" + zh: """URL""" } } headers { desc { - en: """List of HTTP headers.""" - zh: """""" + en: """List of HTTP Headers.""" + zh: """HTTP Headers 列表""" } label { - en: """headers""" + en: """Headers""" zh: """请求头""" } } headers_no_content_type { desc { - en: """List of HTTP headers (without `content_type`).""" - zh: """""" + en: """List of HTTP headers (without content-type).""" + zh: """HTTP Headers 列表 (无 content-type) 。""" } label { en: """headers_no_content_type""" @@ -188,22 +188,22 @@ and the new rules will override all rules from the old config file. body { desc { en: """HTTP request body.""" - zh: """HTTP 请求体""" + zh: """HTTP request body。""" } label { - en: """body""" - zh: """请求体""" + en: """Request Body""" + zh: """Request Body""" } } request_timeout { desc { - en: """Request timeout.""" - zh: """请求超时时间""" + en: """HTTP request timeout.""" + zh: """HTTP 请求超时时长。""" } label { - en: """request_timeout""" - zh: """请求超时""" + en: """Request Timeout""" + zh: """请求超时时间""" } } diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index ff015cee2..b971faa77 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -102,9 +102,14 @@ authz_http_common_fields() -> authz_common_fields(http) ++ [ {url, fun url/1}, - {body, map([{fuzzy, term(), binary()}])}, + {body, + hoconsc:mk(map([{fuzzy, term(), binary()}]), #{ + required => false, desc => ?DESC(body) + })}, {request_timeout, - mk_duration("Request timeout", #{default => "30s", desc => ?DESC(request_timeout)})} + mk_duration("Request timeout", #{ + required => false, default => "30s", desc => ?DESC(request_timeout) + })} ] ++ maps:to_list( maps:without( @@ -141,7 +146,10 @@ headers_no_content_type(desc) -> ?DESC(?FUNCTION_NAME); headers_no_content_type(converter) -> fun(Headers) -> - maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) + maps:without( + [<<"content-type">>], + maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) + ) end; headers_no_content_type(default) -> default_headers_no_content_type(); diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index 94dfcecf3..319928670 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -94,45 +94,49 @@ authorize( parse_config( #{ - url := URL, + url := RawUrl, method := Method, headers := Headers, request_timeout := ReqTimeout } = Conf ) -> - {BaseURLWithPath, Query} = parse_fullpath(URL), - BaseURLMap = parse_url(BaseURLWithPath), + {BaseUrl0, Path, Query} = parse_url(RawUrl), + {ok, BaseUrl} = emqx_http_lib:uri_parse(BaseUrl0), Conf#{ method => Method, - base_url => maps:remove(query, BaseURLMap), + base_url => BaseUrl, + headers => Headers, + base_path_templete => emqx_authz_utils:parse_str(Path, ?PLACEHOLDERS), base_query_template => emqx_authz_utils:parse_deep( - cow_qs:parse_qs(bin(Query)), + cow_qs:parse_qs(to_bin(Query)), ?PLACEHOLDERS ), body_template => emqx_authz_utils:parse_deep( maps:to_list(maps:get(body, Conf, #{})), ?PLACEHOLDERS ), - headers => Headers, request_timeout => ReqTimeout, %% pool_type default value `random` pool_type => random }. -parse_fullpath(RawURL) -> - cow_http:parse_fullpath(bin(RawURL)). - -parse_url(URL) when - URL =:= undefined --> - #{}; -parse_url(URL) -> - {ok, URIMap} = emqx_http_lib:uri_parse(URL), - case maps:get(query, URIMap, undefined) of - undefined -> - URIMap#{query => ""}; - _ -> - URIMap +parse_url(Url) -> + case string:split(Url, "//", leading) of + [Scheme, UrlRem] -> + case string:split(UrlRem, "/", leading) of + [HostPort, Remaining] -> + BaseUrl = iolist_to_binary([Scheme, "//", HostPort]), + case string:split(Remaining, "?", leading) of + [Path, QueryString] -> + {BaseUrl, Path, QueryString}; + [Path] -> + {BaseUrl, Path, <<>>} + end; + [HostPort] -> + {iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>} + end; + [Url] -> + throw({invalid_url, Url}) end. generate_request( @@ -141,32 +145,33 @@ generate_request( Client, #{ method := Method, - base_url := #{path := Path}, - base_query_template := BaseQueryTemplate, headers := Headers, + base_path_templete := BasePathTemplate, + base_query_template := BaseQueryTemplate, body_template := BodyTemplate } ) -> Values = client_vars(Client, PubSub, Topic), + Path = emqx_authz_utils:render_str(BasePathTemplate, Values), + Query = emqx_authz_utils:render_deep(BaseQueryTemplate, Values), Body = emqx_authz_utils:render_deep(BodyTemplate, Values), - NBaseQuery = emqx_authz_utils:render_deep(BaseQueryTemplate, Values), case Method of get -> - NPath = append_query(Path, NBaseQuery ++ Body), + NPath = append_query(Path, Query ++ Body), {NPath, Headers}; _ -> - NPath = append_query(Path, NBaseQuery), + NPath = append_query(Path, Query), NBody = serialize_body( - proplists:get_value(<<"Accept">>, Headers, <<"application/json">>), + proplists:get_value(<<"accept">>, Headers, <<"application/json">>), Body ), {NPath, Headers, NBody} end. append_query(Path, []) -> - Path; + encode_path(Path); append_query(Path, Query) -> - Path ++ "?" ++ binary_to_list(query_string(Query)). + encode_path(Path) ++ "?" ++ to_list(query_string(Query)). query_string(Body) -> query_string(Body, []). @@ -179,13 +184,14 @@ query_string([], Acc) -> <<>> end; query_string([{K, V} | More], Acc) -> - query_string( - More, - [ - ["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] - | Acc - ] - ). + query_string(More, [["&", uri_encode(K), "=", uri_encode(V)] | Acc]). + +uri_encode(T) -> + emqx_http_lib:uri_encode(to_list(T)). + +encode_path(Path) -> + Parts = string:split(Path, "/", all), + lists:flatten(["/" ++ Part || Part <- lists:map(fun uri_encode/1, Parts)]). serialize_body(<<"application/json">>, Body) -> jsx:encode(Body); @@ -198,7 +204,13 @@ client_vars(Client, PubSub, Topic) -> topic => Topic }. -bin(A) when is_atom(A) -> atom_to_binary(A, utf8); -bin(B) when is_binary(B) -> B; -bin(L) when is_list(L) -> list_to_binary(L); -bin(X) -> X. +to_list(A) when is_atom(A) -> + atom_to_list(A); +to_list(B) when is_binary(B) -> + binary_to_list(B); +to_list(L) when is_list(L) -> + L. + +to_bin(B) when is_binary(B) -> B; +to_bin(L) when is_list(L) -> list_to_binary(L); +to_bin(X) -> X. diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 4946eb2a2..e8e0a7f3e 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -29,6 +29,8 @@ -type action() :: publish | subscribe | all. -type permission() :: allow | deny. +-import(emqx_schema, [mk_duration/2]). + -export([ namespace/0, roots/0, @@ -249,8 +251,8 @@ http_common_fields() -> [ {url, fun url/1}, {request_timeout, - emqx_schema:mk_duration("Request timeout", #{ - default => "30s", desc => ?DESC(request_timeout) + mk_duration("Request timeout", #{ + required => false, default => "30s", desc => ?DESC(request_timeout) })}, {body, #{type => map(), required => false, desc => ?DESC(body)}} ] ++ @@ -303,7 +305,12 @@ headers_no_content_type(desc) -> ?DESC(?FUNCTION_NAME); headers_no_content_type(converter) -> fun(Headers) -> - maps:to_list(maps:merge(default_headers_no_content_type(), transform_header_name(Headers))) + maps:to_list( + maps:without( + [<<"content-type">>], + maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) + ) + ) end; headers_no_content_type(default) -> default_headers_no_content_type(); @@ -359,12 +366,12 @@ check_ssl_opts(Conf) -> true; Url -> case emqx_authz_http:parse_url(Url) of - #{scheme := https} -> + {<<"https", _>>, _, _} -> case hocon_maps:get("config.ssl.enable", Conf) of true -> true; _ -> {error, ssl_not_enable} end; - #{scheme := http} -> + {<<"http", _>>, _, _} -> true; Bad -> {bad_scheme, Url, Bad} diff --git a/apps/emqx_authz/src/emqx_authz_utils.erl b/apps/emqx_authz/src/emqx_authz_utils.erl index c153dbc1f..4babb373b 100644 --- a/apps/emqx_authz/src/emqx_authz_utils.erl +++ b/apps/emqx_authz/src/emqx_authz_utils.erl @@ -25,8 +25,10 @@ create_resource/2, update_config/2, parse_deep/2, + parse_str/2, parse_sql/3, render_deep/2, + render_str/2, render_sql_params/2 ]). @@ -69,6 +71,9 @@ update_config(Path, ConfigRequest) -> parse_deep(Template, PlaceHolders) -> emqx_placeholder:preproc_tmpl_deep(Template, #{placeholders => PlaceHolders}). +parse_str(Template, PlaceHolders) -> + emqx_placeholder:preproc_tmpl(Template, #{placeholders => PlaceHolders}). + parse_sql(Template, ReplaceWith, PlaceHolders) -> emqx_placeholder:preproc_sql( Template, @@ -85,6 +90,13 @@ render_deep(Template, Values) -> #{return => full_binary, var_trans => fun handle_var/2} ). +render_str(Template, Values) -> + emqx_placeholder:proc_tmpl( + Template, + client_vars(Values), + #{return => full_binary, var_trans => fun handle_var/2} + ). + render_sql_params(ParamList, Values) -> emqx_placeholder:proc_tmpl( ParamList, diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index 03016f7e2..9c00c5966 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -201,6 +201,54 @@ t_query_params(_Config) -> emqx_access_control:authorize(ClientInfo, publish, <<"t">>) ). +t_path(_Config) -> + ok = setup_handler_and_config( + fun(Req0, State) -> + ?assertEqual( + << + "/authz/users/" + "user%20name/" + "client%20id/" + "127.0.0.1/" + "MQTT/" + "MOUNTPOINT/" + "t/1/" + "publish" + >>, + cowboy_req:path(Req0) + ), + Req = cowboy_req:reply(200, Req0), + {ok, Req, State} + end, + #{ + <<"url">> => << + "http://127.0.0.1:33333/authz/users/" + "${username}/" + "${clientid}/" + "${peerhost}/" + "${proto_name}/" + "${mountpoint}/" + "${topic}/" + "${action}" + >> + } + ), + + ClientInfo = #{ + clientid => <<"client id">>, + username => <<"user name">>, + peerhost => {127, 0, 0, 1}, + protocol => <<"MQTT">>, + mountpoint => <<"MOUNTPOINT">>, + zone => default, + listener => {tcp, default} + }, + + ?assertEqual( + allow, + emqx_access_control:authorize(ClientInfo, publish, <<"t/1">>) + ). + t_json_body(_Config) -> ok = setup_handler_and_config( fun(Req0, State) ->