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