./ct_report/coverage/mongoose_graphql_cowboy_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_cowboy_handler).
8
9 -behaviour(mongoose_http_handler).
10 -behavior(cowboy_rest).
11
12 %% mongoose_http_handler callbacks
13 -export([config_spec/0]).
14
15 %% config processing callbacks
16 -export([process_config/1]).
17
18 %% Cowboy Handler Interface
19 -export([init/2]).
20
21 %% REST callbacks
22 -export([allowed_methods/2,
23 resource_exists/2,
24 content_types_provided/2,
25 content_types_accepted/2,
26 charsets_provided/2,
27 is_authorized/2]).
28
29 %% Data input/output callbacks
30 -export([from_json/2,
31 to_json/2,
32 to_html/2]).
33
34 -ignore_xref([from_json/2, to_html/2, to_json/2]).
35
36 -include("mongoose_config_spec.hrl").
37
38 %% mongoose_http_handler callbacks
39
40 -spec config_spec() -> mongoose_config_spec:config_section().
41 config_spec() ->
42 83 #section{
43 items = #{<<"username">> => #option{type = binary},
44 <<"password">> => #option{type = binary},
45 <<"schema_endpoint">> => #option{type = atom,
46 validate = {enum, [admin, domain_admin, user]}}
47 },
48 format_items = map,
49 required = [<<"schema_endpoint">>],
50 process = fun ?MODULE:process_config/1}.
51
52 process_config(Opts) ->
53 249 case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of
54 true ->
55 249 Opts;
56 false ->
57
:-(
error(#{what => both_username_and_password_required, opts => Opts})
58 end.
59
60 %% cowboy_rest callbacks
61
62 init(Req, Opts) ->
63 441 IndexLocation = {priv_file, mongooseim, "graphql/wsite/index.html"},
64 441 {cowboy_rest,
65 Req,
66 Opts#{index_location => IndexLocation}
67 }.
68
69 allowed_methods(Req, State) ->
70 441 {[<<"GET">>, <<"POST">>], Req, State}.
71
72 content_types_accepted(Req, State) ->
73 439 {[
74 {{<<"application">>, <<"json">>, []}, from_json}
75 ], Req, State}.
76
77 content_types_provided(Req, State) ->
78 441 {[
79 {{<<"application">>, <<"json">>, []}, to_json},
80 {{<<"text">>, <<"html">>, []}, to_html}
81 ], Req, State}.
82
83 charsets_provided(Req, State) ->
84 441 {[<<"utf-8">>], Req, State}.
85
86 is_authorized(Req, State) ->
87 441 try cowboy_req:parse_header(<<"authorization">>, Req) of
88 Auth ->
89 441 case check_auth(Auth, State) of
90 {ok, State2} ->
91 441 {true, Req, State2};
92 error ->
93
:-(
Msg = make_error(authorize, wrong_credentials),
94
:-(
reply_error(Msg, Req, State)
95 end
96 catch
97 exit:Err ->
98
:-(
reply_error(make_error(authorize, Err), Req, State)
99 end.
100
101 resource_exists(#{method := <<"GET">>} = Req, State) ->
102 2 {true, Req, State};
103 resource_exists(#{method := <<"POST">>} = Req, State) ->
104 439 {false, Req, State}.
105
106 to_html(Req, #{index_location := {priv_file, App, FileLocation}} = State) ->
107 2 Filename = filename:join(code:priv_dir(App), FileLocation),
108 2 {ok, Data} = file:read_file(Filename),
109 2 {Data, Req, State}.
110
111 json_request(Req, State) ->
112 439 case gather(Req) of
113 {error, Reason} ->
114
:-(
reply_error(Reason, Req, State);
115 {ok, Req2, Decoded} ->
116 439 run_request(Decoded, Req2, State)
117 end.
118
119 439 from_json(Req, State) -> json_request(Req, State).
120
:-(
to_json(Req, State) -> json_request(Req, State).
121
122 %% Internal
123
124 check_auth(Auth, #{schema_endpoint := domain_admin} = State) ->
125 1 auth_domain_admin(Auth, State);
126 check_auth(Auth, #{schema_endpoint := admin} = State) ->
127 269 auth_admin(Auth, State);
128 check_auth(Auth, #{schema_endpoint := user} = State) ->
129 171 auth_user(Auth, State).
130
131 auth_user({basic, User, Password}, State) ->
132 168 JID = jid:from_binary(User),
133 168 case mongoose_api_common:check_password(JID, Password) of
134 168 {true, _} -> {ok, State#{authorized => true,
135 authorized_as => user,
136 schema_ctx => #{user => JID}}};
137
:-(
_ -> error
138 end;
139 auth_user(_, State) ->
140 3 {ok, State#{authorized => false}}.
141
142 auth_admin({basic, Username, Password}, #{username := Username, password := Password} = State) ->
143 266 {ok, State#{authorized => true,
144 schema_ctx => #{authorized_as => admin}
145 }};
146 auth_admin({basic, _, _}, _) ->
147
:-(
error;
148 auth_admin(_, #{username := _, password := _} = State) ->
149 3 {ok, State#{authorized => false}};
150 auth_admin(_, State) ->
151 % auth credentials not provided in config
152
:-(
{ok, State#{authorized => true,
153 schema_ctx => #{authorized_as => admin}}}.
154
155 auth_domain_admin({basic, Username, Password}, State) ->
156
:-(
case jid:to_lus(jid:from_binary(Username)) of
157 {<<"admin">>, Domain} ->
158
:-(
case mongoose_domain_api:check_domain_password(Domain, Password) of
159 ok ->
160
:-(
{ok, State#{authorized => true,
161 schema_ctx => #{authorized_as => domain_admin,
162 admin => jid:from_binary(Username)}}};
163 {error, _} ->
164
:-(
error
165 end;
166 _ ->
167
:-(
error
168 end;
169 auth_domain_admin(_, State) ->
170 1 {ok, State#{authorized => false}}.
171
172 run_request(#{document := undefined}, Req, State) ->
173 3 reply_error(make_error(decode, no_query_supplied), Req, State);
174 run_request(#{} = ReqCtx, Req, #{schema_endpoint := EpName,
175 authorized := AuthStatus} = State) ->
176 436 Ep = mongoose_graphql:get_endpoint(EpName),
177 436 Ctx = maps:get(schema_ctx, State, #{}),
178 436 ReqCtx2 = ReqCtx#{authorized => AuthStatus, ctx => Ctx},
179 436 case mongoose_graphql:execute(Ep, ReqCtx2) of
180 {ok, Response} ->
181 428 ResponseBody = mongoose_graphql_cowboy_response:term_to_json(Response),
182 428 Req2 = cowboy_req:set_resp_body(ResponseBody, Req),
183 428 Reply = cowboy_req:reply(200, Req2),
184 428 {stop, Reply, State};
185 {error, Reason} ->
186 8 reply_error(Reason, Req, State)
187 end.
188
189 gather(Req) ->
190 439 {ok, Body, Req2} = cowboy_req:read_body(Req),
191 439 Bindings = cowboy_req:bindings(Req2),
192 439 try jiffy:decode(Body, [return_maps]) of
193 JSON ->
194 439 gather(Req2, JSON, Bindings)
195 catch
196 _:_ ->
197
:-(
{error, make_error(decode, invalid_json_body)}
198 end.
199
200 gather(Req, Body, Params) ->
201 439 QueryDocument = document([Params, Body]),
202 439 case variables([Params, Body]) of
203 {ok, Vars} ->
204 439 Operation = operation_name([Params, Body]),
205 439 {ok, Req, #{document => QueryDocument,
206 vars => Vars,
207 operation_name => Operation}};
208 {error, Reason} ->
209
:-(
{error, Reason}
210 end.
211
212 436 document([#{<<"query">> := Q}|_]) -> Q;
213 442 document([_|Next]) -> document(Next);
214 3 document([]) -> undefined.
215
216 variables([#{<<"variables">> := Vars} | _]) ->
217 425 if
218 is_binary(Vars) ->
219
:-(
try jiffy:decode(Vars, [return_maps]) of
220
:-(
null -> {ok, #{}};
221
:-(
JSON when is_map(JSON) -> {ok, JSON};
222
:-(
_ -> {error, make_error(decode, variables_invalid_json)}
223 catch
224 _:_ ->
225
:-(
{error, make_error(decode, variables_invalid_json)}
226 end;
227 is_map(Vars) ->
228 425 {ok, Vars};
229 Vars == null ->
230
:-(
{ok, #{}}
231 end;
232 variables([_ | Next]) ->
233 453 variables(Next);
234 variables([]) ->
235 14 {ok, #{}}.
236
237 operation_name([#{<<"operationName">> := OpName} | _]) ->
238 428 OpName;
239 operation_name([_ | Next]) ->
240 450 operation_name(Next);
241 operation_name([]) ->
242 11 undefined.
243
244 make_error(Phase, Term) ->
245 3 #{error_term => Term, phase => Phase}.
246
247 reply_error(Msg, Req, State) ->
248 11 {Code, Error} = mongoose_graphql_errors:format_error(Msg),
249 11 Body = jiffy:encode(#{errors => [Error]}),
250 11 Req2 = cowboy_req:set_resp_body(Body, Req),
251 11 Reply = cowboy_req:reply(Code, Req2),
252 11 {stop, Reply, State}.
Line Hits Source