1%% This Source Code Form is subject to the terms of the Mozilla Public 2%% License, v. 2.0. If a copy of the MPL was not distributed with this 3%% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4%% 5%% Copyright (c) 2007-2021 VMware, Inc. or its affiliates. All rights reserved. 6%% 7 8-module(rabbit_auth_backend_http). 9 10-include_lib("rabbit_common/include/rabbit.hrl"). 11 12-behaviour(rabbit_authn_backend). 13-behaviour(rabbit_authz_backend). 14 15-export([description/0, p/1, q/1, join_tags/1]). 16-export([user_login_authentication/2, user_login_authorization/2, 17 check_vhost_access/3, check_resource_access/4, check_topic_access/4, 18 state_can_expire/0]). 19 20%% If keepalive connection is closed, retry N times before failing. 21-define(RETRY_ON_KEEPALIVE_CLOSED, 3). 22 23-define(RESOURCE_REQUEST_PARAMETERS, [username, vhost, resource, name, permission]). 24 25-define(SUCCESSFUL_RESPONSE_CODES, [200, 201]). 26 27%%-------------------------------------------------------------------- 28 29description() -> 30 [{name, <<"HTTP">>}, 31 {description, <<"HTTP authentication / authorisation">>}]. 32 33%%-------------------------------------------------------------------- 34 35user_login_authentication(Username, AuthProps) -> 36 case http_req(p(user_path), q([{username, Username}|AuthProps])) of 37 {error, _} = E -> E; 38 "deny" -> {refused, "Denied by the backing HTTP service", []}; 39 "allow" ++ Rest -> Tags = [rabbit_data_coercion:to_atom(T) || 40 T <- string:tokens(Rest, " ")], 41 {ok, #auth_user{username = Username, 42 tags = Tags, 43 impl = none}}; 44 Other -> {error, {bad_response, Other}} 45 end. 46 47user_login_authorization(Username, AuthProps) -> 48 case user_login_authentication(Username, AuthProps) of 49 {ok, #auth_user{impl = Impl}} -> {ok, Impl}; 50 Else -> Else 51 end. 52 53check_vhost_access(#auth_user{username = Username, tags = Tags}, VHost, undefined) -> 54 do_check_vhost_access(Username, Tags, VHost, "", undefined); 55check_vhost_access(#auth_user{username = Username, tags = Tags}, VHost, 56 AuthzData = #{peeraddr := PeerAddr}) when is_map(AuthzData) -> 57 AuthzData1 = maps:remove(peeraddr, AuthzData), 58 Ip = parse_peeraddr(PeerAddr), 59 do_check_vhost_access(Username, Tags, VHost, Ip, AuthzData1). 60 61do_check_vhost_access(Username, Tags, VHost, Ip, AuthzData) -> 62 OptionsParameters = context_as_parameters(AuthzData), 63 bool_req(vhost_path, [{username, Username}, 64 {vhost, VHost}, 65 {ip, Ip}, 66 {tags, join_tags(Tags)}] ++ OptionsParameters). 67 68check_resource_access(#auth_user{username = Username, tags = Tags}, 69 #resource{virtual_host = VHost, kind = Type, name = Name}, 70 Permission, 71 AuthzContext) -> 72 OptionsParameters = context_as_parameters(AuthzContext), 73 bool_req(resource_path, [{username, Username}, 74 {vhost, VHost}, 75 {resource, Type}, 76 {name, Name}, 77 {permission, Permission}, 78 {tags, join_tags(Tags)}] ++ OptionsParameters). 79 80check_topic_access(#auth_user{username = Username, tags = Tags}, 81 #resource{virtual_host = VHost, kind = topic = Type, name = Name}, 82 Permission, 83 Context) -> 84 OptionsParameters = context_as_parameters(Context), 85 bool_req(topic_path, [{username, Username}, 86 {vhost, VHost}, 87 {resource, Type}, 88 {name, Name}, 89 {permission, Permission}, 90 {tags, join_tags(Tags)}] ++ OptionsParameters). 91 92state_can_expire() -> false. 93 94%%-------------------------------------------------------------------- 95 96context_as_parameters(Options) when is_map(Options) -> 97 % filter keys that would erase fixed parameters 98 [{rabbit_data_coercion:to_atom(Key), maps:get(Key, Options)} 99 || Key <- maps:keys(Options), 100 lists:member( 101 rabbit_data_coercion:to_atom(Key), 102 ?RESOURCE_REQUEST_PARAMETERS) =:= false]; 103context_as_parameters(_) -> 104 []. 105 106bool_req(PathName, Props) -> 107 case http_req(p(PathName), q(Props)) of 108 "deny" -> false; 109 "allow" -> true; 110 E -> E 111 end. 112 113http_req(Path, Query) -> http_req(Path, Query, ?RETRY_ON_KEEPALIVE_CLOSED). 114 115http_req(Path, Query, Retry) -> 116 case do_http_req(Path, Query) of 117 {error, socket_closed_remotely} -> 118 %% HTTP keepalive connection can no longer be used. Retry the request. 119 case Retry > 0 of 120 true -> http_req(Path, Query, Retry - 1); 121 false -> {error, socket_closed_remotely} 122 end; 123 Other -> Other 124 end. 125 126 127do_http_req(Path0, Query) -> 128 URI = uri_parser:parse(Path0, [{port, 80}]), 129 {host, Host} = lists:keyfind(host, 1, URI), 130 {port, Port} = lists:keyfind(port, 1, URI), 131 HostHdr = rabbit_misc:format("~s:~b", [Host, Port]), 132 {ok, Method} = application:get_env(rabbitmq_auth_backend_http, http_method), 133 Request = case rabbit_data_coercion:to_atom(Method) of 134 get -> 135 Path = Path0 ++ "?" ++ Query, 136 rabbit_log:debug("auth_backend_http: GET ~s", [Path]), 137 {Path, [{"Host", HostHdr}]}; 138 post -> 139 rabbit_log:debug("auth_backend_http: POST ~s", [Path0]), 140 {Path0, [{"Host", HostHdr}], "application/x-www-form-urlencoded", Query} 141 end, 142 HttpOpts = case application:get_env(rabbitmq_auth_backend_http, 143 ssl_options) of 144 {ok, Opts} when is_list(Opts) -> [{ssl, Opts}]; 145 _ -> [] 146 end, 147 148 case httpc:request(Method, Request, HttpOpts, []) of 149 {ok, {{_HTTP, Code, _}, _Headers, Body}} -> 150 rabbit_log:debug("auth_backend_http: response code is ~p, body: ~p", [Code, Body]), 151 case lists:member(Code, ?SUCCESSFUL_RESPONSE_CODES) of 152 true -> case parse_resp(Body) of 153 {error, _} = E -> E; 154 Resp -> Resp 155 end; 156 false -> {error, {Code, Body}} 157 end; 158 {error, _} = E -> 159 E 160 end. 161 162p(PathName) -> 163 {ok, Path} = application:get_env(rabbitmq_auth_backend_http, PathName), 164 Path. 165 166q(Args) -> 167 string:join([escape(K, V) || {K, V} <- Args], "&"). 168 169escape(K, Map) when is_map(Map) -> 170 string:join([escape(rabbit_data_coercion:to_list(K) ++ "." ++ rabbit_data_coercion:to_list(Key), Value) 171 || {Key, Value} <- maps:to_list(Map)], "&"); 172escape(K, V) -> 173 rabbit_data_coercion:to_list(K) ++ "=" ++ rabbit_http_util:quote_plus(V). 174 175parse_resp(Resp) -> string:to_lower(string:strip(Resp)). 176 177join_tags([]) -> ""; 178join_tags(Tags) -> 179 Strings = [rabbit_data_coercion:to_list(T) || T <- Tags], 180 string:join(Strings, " "). 181 182parse_peeraddr(PeerAddr) -> 183 handle_inet_ntoa_peeraddr(inet:ntoa(PeerAddr), PeerAddr). 184 185handle_inet_ntoa_peeraddr({error, einval}, PeerAddr) -> 186 rabbit_data_coercion:to_list(PeerAddr); 187handle_inet_ntoa_peeraddr(PeerAddrStr, _PeerAddr0) -> 188 PeerAddrStr. 189