./ct_report/coverage/mongoose_graphql_handler.COVER.html

1 %% @doc A cowboy handler for graphql listeners. It supports both admin and user
2 %% schemas. The `schema_endpoint' config option must be set to decide which
3 %% schema to use.
4 %%
5 %% The graphql request is authorized, processed and then passed for execution.
6 %% @end
7 -module(mongoose_graphql_handler).
8
9 -behaviour(mongoose_http_handler).
10 -behavior(cowboy_rest).
11
12 %% mongoose_http_handler callbacks
13 -export([config_spec/0,
14 routes/1]).
15
16 %% config processing callbacks
17 -export([process_config/1]).
18
19 %% Cowboy Handler Interface
20 -export([init/2]).
21
22 %% REST callbacks
23 -export([allowed_methods/2,
24 resource_exists/2,
25 content_types_provided/2,
26 content_types_accepted/2,
27 charsets_provided/2,
28 is_authorized/2]).
29
30 %% Data input/output callbacks
31 -export([from_json/2,
32 to_json/2,
33 to_html/2]).
34
35 %% Utilities used by the SSE handler
36 -export([check_auth_header/2,
37 gather/1]).
38
39 -ignore_xref([from_json/2, to_html/2, to_json/2]).
40
41 -include("mongoose_config_spec.hrl").
42
43 %% mongoose_http_handler callbacks
44
45 -spec config_spec() -> mongoose_config_spec:config_section().
46 config_spec() ->
47 100 #section{
48 items = #{<<"username">> => #option{type = binary},
49 <<"password">> => #option{type = binary},
50 <<"schema_endpoint">> => #option{type = atom,
51 validate = {enum, [admin, domain_admin, user]}},
52 <<"allowed_categories">> => #list{items = #option{type = binary,
53 validate = {enum, allowed_categories()}},
54 validate = unique_non_empty},
55 <<"sse_idle_timeout">> => #option{type = int_or_infinity,
56 validate = positive}},
57 defaults = #{<<"sse_idle_timeout">> => 3600000}, % 1h
58 format_items = map,
59 required = [<<"schema_endpoint">>],
60 process = fun ?MODULE:process_config/1}.
61
62 process_config(Opts) ->
63 300 case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of
64 true ->
65 300 Opts;
66 false ->
67
:-(
error(#{what => both_username_and_password_required, opts => Opts})
68 end.
69
70 -spec routes(mongoose_http_handler:options()) -> mongoose_http_handler:routes().
71 routes(Opts = #{path := Path}) ->
72 316 [{Path, ?MODULE, Opts},
73 {Path ++ "/sse", lasse_handler, #{module => mongoose_graphql_sse_handler,
74 init_args => Opts}}].
75
76 %% cowboy_rest callbacks
77
78 init(Req, Opts) ->
79 721 IndexLocation = {priv_file, mongooseim, "graphql/wsite/index.html"},
80 721 {cowboy_rest,
81 Req,
82 Opts#{index_location => IndexLocation}
83 }.
84
85 allowed_methods(Req, State) ->
86 721 {[<<"GET">>, <<"POST">>], Req, State}.
87
88 content_types_accepted(Req, State) ->
89 719 {[
90 {{<<"application">>, <<"json">>, []}, from_json}
91 ], Req, State}.
92
93 content_types_provided(Req, State) ->
94 721 {[
95 {{<<"application">>, <<"json">>, []}, to_json},
96 {{<<"text">>, <<"html">>, []}, to_html}
97 ], Req, State}.
98
99 charsets_provided(Req, State) ->
100 721 {[<<"utf-8">>], Req, State}.
101
102 is_authorized(Req, State) ->
103 721 case check_auth_header(Req, State) of
104 {ok, State2} ->
105 721 {true, Req, State2};
106 {error, Error} ->
107
:-(
reply_error(Error, Req, State)
108 end.
109
110 resource_exists(#{method := <<"GET">>} = Req, State) ->
111 2 {true, Req, State};
112 resource_exists(#{method := <<"POST">>} = Req, State) ->
113 719 {false, Req, State}.
114
115 to_html(Req, #{index_location := {priv_file, App, FileLocation}} = State) ->
116 2 Filename = filename:join(code:priv_dir(App), FileLocation),
117 2 {ok, Data} = file:read_file(Filename),
118 2 {Data, Req, State}.
119
120 json_request(Req, State) ->
121 719 case gather(Req) of
122 {error, Reason} ->
123 4 reply_error(Reason, Req, State);
124 {ok, Req2, Decoded} ->
125 715 run_request(Decoded, Req2, State)
126 end.
127
128 719 from_json(Req, State) -> json_request(Req, State).
129
:-(
to_json(Req, State) -> json_request(Req, State).
130
131 %% Utils used also by the SSE handler
132
133 check_auth_header(Req, State) ->
134 735 try cowboy_req:parse_header(<<"authorization">>, Req) of
135 Auth ->
136 735 case check_auth(Auth, State) of
137 {ok, State2} ->
138 733 {ok, State2};
139 error ->
140 2 {error, make_error(authorize, wrong_credentials)}
141 end
142 catch
143 exit:Err ->
144
:-(
{error, make_error(authorize, Err)}
145 end.
146
147 gather(Req, Params) ->
148 725 QueryDocument = document(Params),
149 725 case variables(Params) of
150 {ok, Vars} ->
151 725 Operation = operation_name(Params),
152 725 {ok, Req, #{document => QueryDocument,
153 vars => Vars,
154 operation_name => Operation}};
155 {error, Reason} ->
156
:-(
{error, Reason}
157 end.
158
159 %% Internal
160
161 check_auth(Auth, #{schema_endpoint := domain_admin} = State) ->
162 2 auth_domain_admin(Auth, State);
163 check_auth(Auth, #{schema_endpoint := admin} = State) ->
164 505 auth_admin(Auth, State);
165 check_auth(Auth, #{schema_endpoint := user} = State) ->
166 228 auth_user(Auth, State).
167
168 auth_user({basic, User, Password}, State) ->
169 222 JID = jid:from_binary(User),
170 222 case mongoose_api_common:check_password(JID, Password) of
171 221 {true, _} -> {ok, State#{authorized => true,
172 authorized_as => user,
173 schema_ctx => #{user => JID}}};
174 1 _ -> error
175 end;
176 auth_user(_, State) ->
177 6 {ok, State#{authorized => false}}.
178
179 auth_admin({basic, Username, Password}, #{username := Username, password := Password} = State) ->
180 495 {ok, State#{authorized => true,
181 schema_ctx => #{authorized_as => admin}
182 }};
183 auth_admin({basic, _, _}, _) ->
184 1 error;
185 auth_admin(_, #{username := _, password := _} = State) ->
186 9 {ok, State#{authorized => false}};
187 auth_admin(_, State) ->
188 % auth credentials not provided in config
189
:-(
{ok, State#{authorized => true,
190 schema_ctx => #{authorized_as => admin}}}.
191
192 auth_domain_admin({basic, Username, Password}, State) ->
193
:-(
case jid:to_lus(jid:from_binary(Username)) of
194 {<<"admin">>, Domain} ->
195
:-(
case mongoose_domain_api:check_domain_password(Domain, Password) of
196 ok ->
197
:-(
{ok, State#{authorized => true,
198 schema_ctx => #{authorized_as => domain_admin,
199 admin => jid:from_binary(Username)}}};
200 {error, _} ->
201
:-(
error
202 end;
203 _ ->
204
:-(
error
205 end;
206 auth_domain_admin(_, State) ->
207 2 {ok, State#{authorized => false}}.
208
209 run_request(#{document := undefined}, Req, State) ->
210 3 reply_error(make_error(decode, no_query_supplied), Req, State);
211 run_request(#{} = ReqCtx, Req, #{schema_endpoint := EpName,
212 authorized := AuthStatus} = State) ->
213 712 Ep = mongoose_graphql:get_endpoint(EpName),
214 712 Ctx = maps:get(schema_ctx, State, #{}),
215 712 AllowedCategories = maps:get(allowed_categories, State, []),
216 712 ReqCtx2 = ReqCtx#{authorized => AuthStatus,
217 ctx => Ctx#{method => http,
218 allowed_categories => AllowedCategories}},
219 712 case mongoose_graphql:execute(Ep, ReqCtx2) of
220 {ok, Response} ->
221 656 ResponseBody = mongoose_graphql_response:term_to_json(Response),
222 656 Req2 = cowboy_req:set_resp_body(ResponseBody, Req),
223 656 Reply = cowboy_req:reply(200, Req2),
224 656 {stop, Reply, State};
225 {error, Reason} ->
226 56 reply_error(Reason, Req, State)
227 end.
228
229 gather(Req) ->
230 731 case get_params(cowboy_req:method(Req), Req) of
231 6 {error, _} = Error -> Error;
232 725 Params -> gather(Req, Params)
233 end.
234
235 get_params(<<"GET">>, Req) ->
236 12 try maps:from_list(cowboy_req:parse_qs(Req))
237 2 catch _:_ -> {error, make_error(decode, invalid_query_parameters)}
238 end;
239 get_params(<<"POST">>, Req) ->
240 719 {ok, Body, _} = cowboy_req:read_body(Req),
241 719 try jiffy:decode(Body, [return_maps])
242 4 catch _:_ -> {error, make_error(decode, invalid_json_body)}
243 end.
244
245 720 document(#{<<"query">> := Q}) -> Q;
246 5 document(#{}) -> undefined.
247
248 variables(#{<<"variables">> := Vars}) ->
249 709 if
250 is_binary(Vars) ->
251 6 try jiffy:decode(Vars, [return_maps]) of
252
:-(
null -> {ok, #{}};
253 6 JSON when is_map(JSON) -> {ok, JSON};
254
:-(
_ -> {error, make_error(decode, variables_invalid_json)}
255 catch
256 _:_ ->
257
:-(
{error, make_error(decode, variables_invalid_json)}
258 end;
259 is_map(Vars) ->
260 703 {ok, Vars};
261 Vars == null ->
262
:-(
{ok, #{}}
263 end;
264 variables(#{}) ->
265 16 {ok, #{}}.
266
267 operation_name(#{<<"operationName">> := OpName}) ->
268 1 OpName;
269 operation_name(#{}) ->
270 724 undefined.
271
272 make_error(Phase, Term) ->
273 11 #{error_term => Term, phase => Phase}.
274
275 reply_error(Msg, Req, State) ->
276 63 {Code, Error} = mongoose_graphql_errors:format_error(Msg),
277 63 Body = jiffy:encode(#{errors => [Error]}),
278 63 Req2 = cowboy_req:set_resp_body(Body, Req),
279 63 Reply = cowboy_req:reply(Code, Req2),
280 63 {stop, Reply, State}.
281
282 allowed_categories() ->
283 100 [<<"checkAuth">>, <<"account">>, <<"domain">>, <<"last">>, <<"muc">>, <<"muc_light">>,
284 <<"session">>, <<"stanza">>, <<"roster">>, <<"vcard">>, <<"private">>, <<"metric">>,
285 <<"stat">>, <<"gdpr">>, <<"mnesia">>, <<"server">>, <<"inbox">>, <<"http_upload">>,
286 <<"offline">>, <<"token">>].
Line Hits Source