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}. |