./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 4 #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 12 case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of
61 true ->
62 12 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 12 [{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
:-(
IndexLocation = {priv_file, mongooseim, "graphql/wsite/index.html"},
77
:-(
{cowboy_rest,
78 Req,
79 Opts#{index_location => IndexLocation}
80 }.
81
82 allowed_methods(Req, State) ->
83
:-(
{[<<"GET">>, <<"POST">>], Req, State}.
84
85 content_types_accepted(Req, State) ->
86
:-(
{[
87 {{<<"application">>, <<"json">>, []}, from_json}
88 ], Req, State}.
89
90 content_types_provided(Req, State) ->
91
:-(
{[
92 {{<<"application">>, <<"json">>, []}, to_json},
93 {{<<"text">>, <<"html">>, []}, to_html}
94 ], Req, State}.
95
96 charsets_provided(Req, State) ->
97
:-(
{[<<"utf-8">>], Req, State}.
98
99 is_authorized(Req, State) ->
100
:-(
case check_auth_header(Req, State) of
101 {ok, State2} ->
102
:-(
{true, Req, State2};
103 {error, Error} ->
104
:-(
reply_error(Error, Req, State)
105 end.
106
107 resource_exists(#{method := <<"GET">>} = Req, State) ->
108
:-(
{true, Req, State};
109 resource_exists(#{method := <<"POST">>} = Req, State) ->
110
:-(
{false, Req, State}.
111
112 to_html(Req, #{index_location := {priv_file, App, FileLocation}} = State) ->
113
:-(
Filename = filename:join(code:priv_dir(App), FileLocation),
114
:-(
{ok, Data} = file:read_file(Filename),
115
:-(
{Data, Req, State}.
116
117 json_request(Req, State) ->
118
:-(
case gather(Req) of
119 {error, Reason} ->
120
:-(
reply_error(Reason, Req, State);
121 {ok, Req2, Decoded} ->
122
:-(
run_request(Decoded, Req2, State)
123 end.
124
125
:-(
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
:-(
try cowboy_req:parse_header(<<"authorization">>, Req) of
132 Auth ->
133
:-(
case check_auth(Auth, State) of
134 {ok, State2} ->
135
:-(
{ok, State2};
136 error ->
137
:-(
{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
:-(
QueryDocument = document(Params),
146
:-(
case variables(Params) of
147 {ok, Vars} ->
148
:-(
Operation = operation_name(Params),
149
:-(
{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
:-(
auth_domain_admin(Auth, State);
160 check_auth(Auth, #{schema_endpoint := admin} = State) ->
161
:-(
auth_admin(Auth, State);
162 check_auth(Auth, #{schema_endpoint := user} = State) ->
163
:-(
auth_user(Auth, State).
164
165 auth_user({basic, User, Password}, State) ->
166
:-(
JID = jid:from_binary(User),
167
:-(
case mongoose_api_common:check_password(JID, Password) of
168
:-(
{true, _} -> {ok, State#{authorized => true,
169 authorized_as => user,
170 schema_ctx => #{user => JID}}};
171
:-(
_ -> error
172 end;
173 auth_user(_, State) ->
174
:-(
{ok, State#{authorized => false}}.
175
176 auth_admin({basic, Username, Password}, #{username := Username, password := Password} = State) ->
177
:-(
{ok, State#{authorized => true,
178 schema_ctx => #{authorized_as => admin}
179 }};
180 auth_admin({basic, _, _}, _) ->
181
:-(
error;
182 auth_admin(_, #{username := _, password := _} = State) ->
183
:-(
{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
:-(
case jid:to_lus(jid:from_binary(Username)) of
191 {<<"admin">>, Domain} ->
192
:-(
case mongoose_domain_api:check_domain_password(Domain, Password) of
193 ok ->
194
:-(
{ok, State#{authorized => true,
195 schema_ctx => #{authorized_as => domain_admin,
196 admin => jid:from_binary(Username)}}};
197 {error, _} ->
198
:-(
error
199 end;
200 _ ->
201
:-(
error
202 end;
203 auth_domain_admin(_, State) ->
204
:-(
{ok, State#{authorized => false}}.
205
206 run_request(#{document := undefined}, Req, State) ->
207
:-(
reply_error(make_error(decode, no_query_supplied), Req, State);
208 run_request(#{} = ReqCtx, Req, #{schema_endpoint := EpName,
209 authorized := AuthStatus} = State) ->
210
:-(
Ep = mongoose_graphql:get_endpoint(EpName),
211
:-(
Ctx = maps:get(schema_ctx, State, #{}),
212
:-(
AllowedCategories = maps:get(allowed_categories, State, []),
213
:-(
ReqCtx2 = ReqCtx#{authorized => AuthStatus,
214 ctx => Ctx#{method => http,
215 allowed_categories => AllowedCategories}},
216
:-(
case mongoose_graphql:execute(Ep, ReqCtx2) of
217 {ok, Response} ->
218
:-(
ResponseBody = mongoose_graphql_response:term_to_json(Response),
219
:-(
Req2 = cowboy_req:set_resp_body(ResponseBody, Req),
220
:-(
Reply = cowboy_req:reply(200, Req2),
221
:-(
{stop, Reply, State};
222 {error, Reason} ->
223
:-(
reply_error(Reason, Req, State)
224 end.
225
226 gather(Req) ->
227
:-(
case get_params(cowboy_req:method(Req), Req) of
228
:-(
{error, _} = Error -> Error;
229
:-(
Params -> gather(Req, Params)
230 end.
231
232 get_params(<<"GET">>, Req) ->
233
:-(
try maps:from_list(cowboy_req:parse_qs(Req))
234
:-(
catch _:_ -> {error, make_error(decode, invalid_query_parameters)}
235 end;
236 get_params(<<"POST">>, Req) ->
237
:-(
{ok, Body, _} = cowboy_req:read_body(Req),
238
:-(
try jiffy:decode(Body, [return_maps])
239
:-(
catch _:_ -> {error, make_error(decode, invalid_json_body)}
240 end.
241
242
:-(
document(#{<<"query">> := Q}) -> Q;
243
:-(
document(#{}) -> undefined.
244
245 variables(#{<<"variables">> := Vars}) ->
246
:-(
if
247 is_binary(Vars) ->
248
:-(
try jiffy:decode(Vars, [return_maps]) of
249
:-(
null -> {ok, #{}};
250
:-(
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
:-(
{ok, Vars};
258 Vars == null ->
259
:-(
{ok, #{}}
260 end;
261 variables(#{}) ->
262
:-(
{ok, #{}}.
263
264 operation_name(#{<<"operationName">> := OpName}) ->
265
:-(
OpName;
266 operation_name(#{}) ->
267
:-(
undefined.
268
269 make_error(Phase, Term) ->
270
:-(
#{error_term => Term, phase => Phase}.
271
272 reply_error(Msg, Req, State) ->
273
:-(
{Code, Error} = mongoose_graphql_errors:format_error(Msg),
274
:-(
Body = jiffy:encode(#{errors => [Error]}),
275
:-(
Req2 = cowboy_req:set_resp_body(Body, Req),
276
:-(
Reply = cowboy_req:reply(Code, Req2),
277
:-(
{stop, Reply, State}.
278
279 allowed_categories() ->
280 4 [<<"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