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