./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 93 #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 279 case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of
64 true ->
65 279 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 295 [{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 1282 IndexLocation = {priv_file, mongooseim, "graphql/wsite/index.html"},
80 1282 {cowboy_rest,
81 Req,
82 Opts#{index_location => IndexLocation}
83 }.
84
85 allowed_methods(Req, State) ->
86 1282 {[<<"GET">>, <<"POST">>], Req, State}.
87
88 content_types_accepted(Req, State) ->
89 1263 {[
90 {{<<"application">>, <<"json">>, []}, from_json}
91 ], Req, State}.
92
93 content_types_provided(Req, State) ->
94 1266 {[
95 {{<<"application">>, <<"json">>, []}, to_json},
96 {{<<"text">>, <<"html">>, []}, to_html}
97 ], Req, State}.
98
99 charsets_provided(Req, State) ->
100 1266 {[<<"utf-8">>], Req, State}.
101
102 is_authorized(Req, State) ->
103 1282 case check_auth_header(Req, State) of
104 {ok, State2} ->
105 1266 {true, Req, State2};
106 {error, Error} ->
107 16 reply_error(Error, Req, State)
108 end.
109
110 resource_exists(#{method := <<"GET">>} = Req, State) ->
111 3 {true, Req, State};
112 resource_exists(#{method := <<"POST">>} = Req, State) ->
113 1263 {false, Req, State}.
114
115 to_html(Req, #{index_location := {priv_file, App, FileLocation}} = State) ->
116 3 Filename = filename:join(code:priv_dir(App), FileLocation),
117 3 {ok, Data} = file:read_file(Filename),
118 3 {Data, Req, State}.
119
120 json_request(Req, State) ->
121 1263 case gather(Req) of
122 {error, Reason} ->
123 1 reply_error(Reason, Req, State);
124 {ok, Req2, Decoded} ->
125 1262 run_request(Decoded, Req2, State)
126 end.
127
128 1263 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 1296 try cowboy_req:parse_header(<<"authorization">>, Req) of
135 Auth ->
136 1296 case check_auth(Auth, State) of
137 {ok, State2} ->
138 1278 {ok, State2};
139 error ->
140 18 {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 1272 QueryDocument = document(Params),
149 1272 case variables(Params) of
150 {ok, Vars} ->
151 1272 Operation = operation_name(Params),
152 1272 {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 465 auth_domain_admin(Auth, State);
163 check_auth(Auth, #{schema_endpoint := admin} = State) ->
164 581 auth_admin(Auth, State);
165 check_auth(Auth, #{schema_endpoint := user} = State) ->
166 250 auth_user(Auth, State).
167
168 auth_user({basic, User, Password}, State) ->
169 246 JID = jid:from_binary(User),
170 246 case mongoose_api_common:check_password(JID, Password) of
171 245 {true, _} -> {ok, State#{authorized => true,
172 authorized_as => user,
173 schema_ctx => #{user => JID}}};
174 1 _ -> error
175 end;
176 auth_user(_, State) ->
177 4 {ok, State#{authorized => false}}.
178
179 auth_admin({basic, Username, Password}, #{username := Username, password := Password} = State) ->
180 572 {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 8 {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 461 case jid:to_lus(jid:from_binary(Username)) of
194 {<<"admin">>, Domain} ->
195 461 case mongoose_domain_api:check_domain_password(Domain, Password) of
196 ok ->
197 445 {ok, State#{authorized => true,
198 schema_ctx => #{authorized_as => domain_admin,
199 admin => jid:from_binary(Username)}}};
200 {error, _} ->
201 16 error
202 end;
203 _ ->
204
:-(
error
205 end;
206 auth_domain_admin(_, State) ->
207 4 {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 1259 Ep = mongoose_graphql:get_endpoint(EpName),
214 1259 Ctx = maps:get(schema_ctx, State, #{}),
215 1259 AllowedCategories = maps:get(allowed_categories, State, []),
216 1259 ReqCtx2 = ReqCtx#{authorized => AuthStatus,
217 ctx => Ctx#{method => http,
218 allowed_categories => AllowedCategories}},
219 1259 case mongoose_graphql:execute(Ep, ReqCtx2) of
220 {ok, Response} ->
221 998 ResponseBody = mongoose_graphql_response:term_to_json(Response),
222 998 Req2 = cowboy_req:set_resp_body(ResponseBody, Req),
223 998 Reply = cowboy_req:reply(200, Req2),
224 998 {stop, Reply, State};
225 {error, Reason} ->
226 261 reply_error(Reason, Req, State)
227 end.
228
229 gather(Req) ->
230 1275 case get_params(cowboy_req:method(Req), Req) of
231 3 {error, _} = Error -> Error;
232 1272 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 1263 {ok, Body, _} = cowboy_req:read_body(Req),
241 1263 try jiffy:decode(Body, [return_maps])
242 1 catch _:_ -> {error, make_error(decode, invalid_json_body)}
243 end.
244
245 1267 document(#{<<"query">> := Q}) -> Q;
246 5 document(#{}) -> undefined.
247
248 variables(#{<<"variables">> := Vars}) ->
249 1254 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 1248 {ok, Vars};
261 Vars == null ->
262
:-(
{ok, #{}}
263 end;
264 variables(#{}) ->
265 18 {ok, #{}}.
266
267 operation_name(#{<<"operationName">> := OpName}) ->
268 1 OpName;
269 operation_name(#{}) ->
270 1271 undefined.
271
272 make_error(Phase, Term) ->
273 24 #{error_term => Term, phase => Phase}.
274
275 reply_error(Msg, Req, State) ->
276 281 {Code, Error} = mongoose_graphql_errors:format_error(Msg),
277 281 Body = jiffy:encode(#{errors => [Error]}),
278 281 Req2 = cowboy_req:set_resp_body(Body, Req),
279 281 Reply = cowboy_req:reply(Code, Req2),
280 281 {stop, Reply, State}.
281
282 allowed_categories() ->
283 93 [<<"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