./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 104 #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 format_items = map,
56 required = [<<"schema_endpoint">>],
57 process = fun ?MODULE:process_config/1}.
58
59 process_config(Opts) ->
60 312 case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of
61 true ->
62 312 Opts;
63 false ->
64
:-(
error(#{what => both_username_and_password_required, opts => Opts})
65 end.
66
67 -spec routes(mongoose_http_handler:options()) -> mongoose_http_handler:routes().
68 routes(Opts = #{path := Path}) ->
69 314 [{Path, ?MODULE, Opts},
70 {Path ++ "/sse", lasse_handler, #{module => mongoose_graphql_sse_handler,
71 init_args => Opts}}].
72
73 %% cowboy_rest callbacks
74
75 init(Req, Opts) ->
76 1279 IndexLocation = {priv_file, mongooseim, "graphql/wsite/index.html"},
77 1279 {cowboy_rest,
78 Req,
79 Opts#{index_location => IndexLocation}
80 }.
81
82 allowed_methods(Req, State) ->
83 1279 {[<<"GET">>, <<"POST">>], Req, State}.
84
85 content_types_accepted(Req, State) ->
86 1260 {[
87 {{<<"application">>, <<"json">>, []}, from_json}
88 ], Req, State}.
89
90 content_types_provided(Req, State) ->
91 1263 {[
92 {{<<"application">>, <<"json">>, []}, to_json},
93 {{<<"text">>, <<"html">>, []}, to_html}
94 ], Req, State}.
95
96 charsets_provided(Req, State) ->
97 1263 {[<<"utf-8">>], Req, State}.
98
99 is_authorized(Req, State) ->
100 1279 case check_auth_header(Req, State) of
101 {ok, State2} ->
102 1263 {true, Req, State2};
103 {error, Error} ->
104 16 reply_error(Error, Req, State)
105 end.
106
107 resource_exists(#{method := <<"GET">>} = Req, State) ->
108 3 {true, Req, State};
109 resource_exists(#{method := <<"POST">>} = Req, State) ->
110 1260 {false, Req, State}.
111
112 to_html(Req, #{index_location := {priv_file, App, FileLocation}} = State) ->
113 3 Filename = filename:join(code:priv_dir(App), FileLocation),
114 3 {ok, Data} = file:read_file(Filename),
115 3 {Data, Req, State}.
116
117 json_request(Req, State) ->
118 1260 case gather(Req) of
119 {error, Reason} ->
120
:-(
reply_error(Reason, Req, State);
121 {ok, Req2, Decoded} ->
122 1260 run_request(Decoded, Req2, State)
123 end.
124
125 1260 from_json(Req, State) -> json_request(Req, State).
126
:-(
to_json(Req, State) -> json_request(Req, State).
127
128 %% Utils used also by the SSE handler
129
130 check_auth_header(Req, State) ->
131 1292 try cowboy_req:parse_header(<<"authorization">>, Req) of
132 Auth ->
133 1292 case check_auth(Auth, State) of
134 {ok, State2} ->
135 1274 {ok, State2};
136 error ->
137 18 {error, make_error(authorize, wrong_credentials)}
138 end
139 catch
140 exit:Err ->
141
:-(
{error, make_error(authorize, Err)}
142 end.
143
144 gather(Req, Params) ->
145 1269 QueryDocument = document(Params),
146 1269 case variables(Params) of
147 {ok, Vars} ->
148 1269 Operation = operation_name(Params),
149 1269 {ok, Req, #{document => QueryDocument,
150 vars => Vars,
151 operation_name => Operation}};
152 {error, Reason} ->
153
:-(
{error, Reason}
154 end.
155
156 %% Internal
157
158 check_auth(Auth, #{schema_endpoint := domain_admin} = State) ->
159 464 auth_domain_admin(Auth, State);
160 check_auth(Auth, #{schema_endpoint := admin} = State) ->
161 579 auth_admin(Auth, State);
162 check_auth(Auth, #{schema_endpoint := user} = State) ->
163 249 auth_user(Auth, State).
164
165 auth_user({basic, User, Password}, State) ->
166 245 JID = jid:from_binary(User),
167 245 case mongoose_api_common:check_password(JID, Password) of
168 244 {true, _} -> {ok, State#{authorized => true,
169 authorized_as => user,
170 schema_ctx => #{user => JID}}};
171 1 _ -> error
172 end;
173 auth_user(_, State) ->
174 4 {ok, State#{authorized => false}}.
175
176 auth_admin({basic, Username, Password}, #{username := Username, password := Password} = State) ->
177 570 {ok, State#{authorized => true,
178 schema_ctx => #{authorized_as => admin}
179 }};
180 auth_admin({basic, _, _}, _) ->
181 1 error;
182 auth_admin(_, #{username := _, password := _} = State) ->
183 8 {ok, State#{authorized => false}};
184 auth_admin(_, State) ->
185 % auth credentials not provided in config
186
:-(
{ok, State#{authorized => true,
187 schema_ctx => #{authorized_as => admin}}}.
188
189 auth_domain_admin({basic, Username, Password}, State) ->
190 461 case jid:to_lus(jid:from_binary(Username)) of
191 {<<"admin">>, Domain} ->
192 461 case mongoose_domain_api:check_domain_password(Domain, Password) of
193 ok ->
194 445 {ok, State#{authorized => true,
195 schema_ctx => #{authorized_as => domain_admin,
196 admin => jid:from_binary(Username)}}};
197 {error, _} ->
198 16 error
199 end;
200 _ ->
201
:-(
error
202 end;
203 auth_domain_admin(_, State) ->
204 3 {ok, State#{authorized => false}}.
205
206 run_request(#{document := undefined}, Req, State) ->
207 3 reply_error(make_error(decode, no_query_supplied), Req, State);
208 run_request(#{} = ReqCtx, Req, #{schema_endpoint := EpName,
209 authorized := AuthStatus} = State) ->
210 1257 Ep = mongoose_graphql:get_endpoint(EpName),
211 1257 Ctx = maps:get(schema_ctx, State, #{}),
212 1257 AllowedCategories = maps:get(allowed_categories, State, []),
213 1257 ReqCtx2 = ReqCtx#{authorized => AuthStatus,
214 ctx => Ctx#{method => http,
215 allowed_categories => AllowedCategories}},
216 1257 case mongoose_graphql:execute(Ep, ReqCtx2) of
217 {ok, Response} ->
218 996 ResponseBody = mongoose_graphql_response:term_to_json(Response),
219 996 Req2 = cowboy_req:set_resp_body(ResponseBody, Req),
220 996 Reply = cowboy_req:reply(200, Req2),
221 996 {stop, Reply, State};
222 {error, Reason} ->
223 261 reply_error(Reason, Req, State)
224 end.
225
226 gather(Req) ->
227 1271 case get_params(cowboy_req:method(Req), Req) of
228 2 {error, _} = Error -> Error;
229 1269 Params -> gather(Req, Params)
230 end.
231
232 get_params(<<"GET">>, Req) ->
233 11 try maps:from_list(cowboy_req:parse_qs(Req))
234 2 catch _:_ -> {error, make_error(decode, invalid_query_parameters)}
235 end;
236 get_params(<<"POST">>, Req) ->
237 1260 {ok, Body, _} = cowboy_req:read_body(Req),
238 1260 try jiffy:decode(Body, [return_maps])
239
:-(
catch _:_ -> {error, make_error(decode, invalid_json_body)}
240 end.
241
242 1264 document(#{<<"query">> := Q}) -> Q;
243 5 document(#{}) -> undefined.
244
245 variables(#{<<"variables">> := Vars}) ->
246 1251 if
247 is_binary(Vars) ->
248 5 try jiffy:decode(Vars, [return_maps]) of
249
:-(
null -> {ok, #{}};
250 5 JSON when is_map(JSON) -> {ok, JSON};
251
:-(
_ -> {error, make_error(decode, variables_invalid_json)}
252 catch
253 _:_ ->
254
:-(
{error, make_error(decode, variables_invalid_json)}
255 end;
256 is_map(Vars) ->
257 1246 {ok, Vars};
258 Vars == null ->
259
:-(
{ok, #{}}
260 end;
261 variables(#{}) ->
262 18 {ok, #{}}.
263
264 operation_name(#{<<"operationName">> := OpName}) ->
265 1 OpName;
266 operation_name(#{}) ->
267 1268 undefined.
268
269 make_error(Phase, Term) ->
270 23 #{error_term => Term, phase => Phase}.
271
272 reply_error(Msg, Req, State) ->
273 280 {Code, Error} = mongoose_graphql_errors:format_error(Msg),
274 280 Body = jiffy:encode(#{errors => [Error]}),
275 280 Req2 = cowboy_req:set_resp_body(Body, Req),
276 280 Reply = cowboy_req:reply(Code, Req2),
277 280 {stop, Reply, State}.
278
279 allowed_categories() ->
280 104 [<<"checkAuth">>, <<"account">>, <<"domain">>, <<"last">>, <<"muc">>, <<"muc_light">>,
281 <<"session">>, <<"stanza">>, <<"roster">>, <<"vcard">>, <<"private">>, <<"metric">>,
282 <<"stat">>, <<"gdpr">>, <<"mnesia">>, <<"server">>, <<"inbox">>, <<"http_upload">>,
283 <<"offline">>, <<"token">>].
Line Hits Source