./ct_report/coverage/mongoose_graphql_permissions.COVER.html

1 %% @doc Checks if a requested query can be executed with provided permissions.
2 %%
3 %% GraphQL has directives that allow attaching additional information to schema,
4 %% to objects, to fields, and more. The custom directive `@protected' is created
5 %% to mark which objects or fields could be accessed only by an authorized request.
6 %% This module analyzes the AST and tries to find if there is at least one protected
7 %% resource. The `@protected' directive can be attached to <b>field definitions</b>
8 %% to <b>objects</b>, or to <b>interfaces</b>.
9 %%
10 %% Interfaces and objects permissions are checked independently. This means that when
11 %% an interface is protected or has protected fields, then all implementing objects
12 %% should be protected or have the same fields protected. <strong>This demands to mark all
13 %% protected resources at every occurrence with the directive</strong>. Otherwise permissions
14 %% will be different for interface and implementing objects.
15 %%
16 %% If an unauthorized request wants to execute a query that contains protected resources,
17 %% an error is thrown.
18 %%
19 %% Directives can have arguments, so if needed this functionality can be easily
20 %% extended. For example, to allow access to resources only to the user that belongs
21 %% to a specific group.
22 %% @end
23 -module(mongoose_graphql_permissions).
24
25 -export([check_permissions/2]).
26
27 -include_lib("graphql/src/graphql_schema.hrl").
28 -include_lib("graphql/src/graphql_internal.hrl").
29 -include_lib("graphql/include/graphql.hrl").
30 -include_lib("jid/include/jid.hrl").
31
32 -type auth_status() :: boolean().
33 -type auth_role() :: user | admin | domain_admin.
34 -type params() :: map().
35 -type auth_ctx() :: #{operation_name := binary(),
36 params := params(),
37 authorized := auth_status(),
38 authorized_as => auth_role(),
39 user => jid:jid(),
40 admin => jid:jid(),
41 atom() => any()}.
42 -type no_access_info() :: #{path := [binary()],
43 type := atom(),
44 invalid_args => [binary()]}.
45 -type field_check_result() :: ok | no_access_info().
46 -type document() :: #document{}.
47 -type definitions() :: [any()].
48
49 %% @doc Checks if query can be executed by unauthorized request or authorized as one
50 %% of the roles (USER, ADMIN, DOMAIN_ADMIN). If not, throw an error.
51 %%
52 %% The USER and ADMIN can execute each query because they are on separated GraphQL
53 %% instances that serves different queries.
54 %%
55 %% The DOMAIN_ADMIN use the same GraphQL instance as ADMIN, but have permissions
56 %% only to administrate own domain.
57 %% @end
58 -spec check_permissions(auth_ctx(), document()) -> ok.
59 check_permissions(#{operation_name := OpName, authorized := false},
60 #document{definitions = Definitions}) ->
61 3 check_unauthorized_request_permissions(OpName, Definitions);
62 check_permissions(#{operation_name := OpName, authorized_as := domain_admin,
63 admin := #jid{lserver = Domain}, params := Params},
64 #document{definitions = Definitions}) ->
65 1 check_domain_authorized_request_permissions(OpName, Domain, Params, Definitions);
66 check_permissions(#{authorized := true}, _) ->
67 373 ok.
68
69 -spec check_unauthorized_request_permissions(binary(), definitions()) -> ok.
70 check_unauthorized_request_permissions(OpName, Definitions) ->
71 3 Op = lists:filter(fun(D) -> is_req_operation(D, OpName) end, Definitions),
72 3 case Op of
73 [#op{schema = Schema, selection_set = Set} = Op1] ->
74 3 case is_object_protected(Schema, Set, Definitions) of
75 true ->
76 % Seems that the introspection fields belong to the query object.
77 % When an object is protected we need to ensure that the request
78 % query contains only introspection fields to execute it without
79 % authorization. Otherwise, a user couldn't access documentation
80 % without logging in.
81
:-(
case is_introspection_op(Op1) of
82 true ->
83
:-(
ok;
84 false ->
85
:-(
OpName2 = op_name(OpName),
86
:-(
graphql_err:abort([OpName2], authorize, {no_permissions, OpName2})
87 end;
88 false ->
89 3 ok
90 end;
91 _ ->
92
:-(
ok
93 end.
94
95 -spec check_domain_authorized_request_permissions(binary(), binary(),
96 params(), definitions()) -> ok.
97 check_domain_authorized_request_permissions(OpName, Domain, Params, Definitions) ->
98 1 Op = lists:filter(fun(D) -> is_req_operation(D, OpName) end, Definitions),
99 1 case Op of
100 [#op{selection_set = Set}] ->
101 1 case check_fields(#{domain => Domain}, Params, Set) of
102 ok ->
103 1 ok;
104 #{path := Path} = NoAccessInfo ->
105
:-(
OpName2 = op_name(OpName),
106
:-(
Error = {no_permissions, OpName2, NoAccessInfo},
107
:-(
Path2 = lists:reverse([OpName2 | Path]),
108
:-(
graphql_err:abort(Path2, authorize, Error)
109 end;
110 _ ->
111
:-(
ok
112 end.
113
114 % Internal
115
116 -spec check_fields(map(), map(), [any()]) -> field_check_result().
117 check_fields(Ctx, Params, Fields) ->
118 5 Fun = fun(F, ok) -> check_field(F, Ctx, Params);
119
:-(
(_, NoAccessInfo) -> NoAccessInfo
120 end,
121 5 lists:foldl(Fun, ok, Fields).
122
123 -spec check_field(field() | any(), map(), map()) -> field_check_result().
124 check_field(#field{id = Name, selection_set = Set, args = Args,
125 schema = #schema_field{directives = Directives}}, Ctx, Params) ->
126 4 Args2 = maps:from_list([prepare_arg(ArgName, Type, Params) || {ArgName, Type} <- Args]),
127 4 Res = check_field_args(Ctx, Args2, Directives),
128 4 Res2 = check_field_type(Res, Ctx, Params, Set),
129 4 add_path(Res2, name(Name));
130
:-(
check_field(_, _, _) -> ok.
131
132 -spec check_field_args(map(), map(), [graphql:directive()]) -> field_check_result().
133 check_field_args(Ctx, Args, Directives) ->
134 4 case lists:filter(fun is_protected_directive/1, Directives) of
135 [#directive{} = Dir] ->
136
:-(
#{type := {enum, Type}, args := PArgs} = protected_dir_args_to_map(Dir),
137
:-(
check_field_args(Type, Ctx, PArgs, Args);
138 [] ->
139 4 ok
140 end.
141
142 -spec check_field_args(binary(), map(), [binary()], map()) -> field_check_result().
143 check_field_args(<<"DOMAIN">>, #{domain := Domain}, ProtectedArgs, Args) ->
144
:-(
case lists:filter(fun(N) -> not arg_eq(get_arg(N, Args), Domain) end, ProtectedArgs) of
145 [] ->
146
:-(
ok;
147 InvalidArgs ->
148
:-(
#{type => domain, path => [], invalid_args => InvalidArgs}
149 end;
150 check_field_args(<<"GLOBAL">>, #{domain := _}, _, _Args) ->
151
:-(
#{type => global, path => []};
152 check_field_args(<<"DEFAULT">>, _Ctx, _ProtectedArgs, _Args) ->
153
:-(
ok.
154
155 -spec check_field_type(field_check_result(), map(), map(), [any()]) -> field_check_result().
156 check_field_type(ok, Ctx, Params, Set) ->
157 4 check_fields(Ctx, Params, Set);
158 check_field_type(NoAccessInfo, _, _, _) ->
159
:-(
NoAccessInfo.
160
161 prepare_arg(ArgName, #{value := #var{id = Name}}, Vars) ->
162
:-(
{ArgName, maps:get(name(Name), Vars)};
163 prepare_arg(ArgName, #{value := Val}, _) ->
164
:-(
{ArgName, Val}.
165
166 get_arg(Name, Args) when is_binary(Name)->
167
:-(
Path = binary:split(Name, <<".">>, [global]),
168
:-(
get_arg(Path, Args);
169
:-(
get_arg([], Value) -> Value;
170
:-(
get_arg(_, undefined) -> undefined;
171 get_arg(Path, List) when is_list(List) ->
172
:-(
[get_arg(Path, ArgsMap) || ArgsMap <- List];
173 get_arg([Name | Path], ArgsMap) ->
174
:-(
get_arg(Path, maps:get(Name, ArgsMap, undefined)).
175
176 arg_eq(Args, Domain) when is_list(Args) ->
177
:-(
lists:all(fun(Arg) -> arg_eq(Arg, Domain) end, Args);
178 arg_eq(Domain, Domain) ->
179
:-(
true;
180 arg_eq(Subdomain, Domain) when is_binary(Subdomain), is_binary(Domain) ->
181
:-(
check_subdomain(Subdomain, Domain);
182 arg_eq(#jid{lserver = Domain1}, Domain2) ->
183
:-(
arg_eq(Domain1, Domain2);
184 arg_eq(undefined, _) ->
185 % The arg is optional, and the value is not present, so we assume that
186 % the domain admin has access.
187
:-(
true;
188 arg_eq(_, _) ->
189
:-(
false.
190
191 check_subdomain(Subdomain, Domain) ->
192
:-(
case mongoose_domain_api:get_subdomain_info(Subdomain) of
193
:-(
{ok, #{parent_domain := ParentDomain}} -> ParentDomain =:= Domain;
194
:-(
{error, not_found} -> false
195 end.
196
197 4 add_path(ok, _) -> ok;
198 add_path(#{path := Path} = Acc, FieldName) ->
199
:-(
Acc#{path => [FieldName | Path]}.
200
201 protected_dir_args_to_map(#directive{args = Args}) ->
202
:-(
Default = #{type => {enum, <<"DEFAULT">>}, args => []},
203
:-(
ArgsMap = maps:from_list([{binary_to_atom(name(N)), V} || {N, V} <- Args]),
204
:-(
maps:merge(Default, ArgsMap).
205
206 4 name({name, _, N}) -> N;
207
:-(
name(N) when is_binary(N) -> N.
208
209 op_name(undefined) ->
210
:-(
<<"ROOT">>;
211 op_name(Name) ->
212
:-(
Name.
213
214 is_req_operation(#op{id = 'ROOT'}, undefined) ->
215 4 true;
216 is_req_operation(#op{id = {name, _, Name}}, Name) ->
217
:-(
true;
218 is_req_operation(_, _) ->
219
:-(
false.
220
221 is_protected_directive(#directive{id = {name, _, <<"protected">>}}) ->
222
:-(
true;
223 is_protected_directive(_) ->
224
:-(
false.
225
226 is_introspection_op(#op{selection_set = Set}) ->
227
:-(
lists:all(fun is_introspection_field/1, Set).
228
229 is_introspection_field(#field{id = {name, _, <<"__schema">>}}) ->
230
:-(
true;
231 is_introspection_field(#field{id = {name, _, <<"__type">>}}) ->
232
:-(
true;
233 is_introspection_field(_) ->
234
:-(
false.
235
236 is_object_protected(_, [], _) ->
237 8 false;
238 is_object_protected(#schema_field{ty = Ty}, Set, Definitions) ->
239 3 is_object_protected(Ty, Set, Definitions);
240 is_object_protected({non_null, Obj}, Set, Definitions) ->
241
:-(
is_object_protected(Obj, Set, Definitions);
242 is_object_protected({list, Obj}, Set, Definitions) ->
243
:-(
is_object_protected(Obj, Set, Definitions);
244 is_object_protected(Object, Set, Definitions) ->
245 6 case is_object_protected(Object) of
246 false ->
247 6 lists:any(fun(S) -> is_field_protected(Object, S, Definitions) end, Set);
248 true ->
249
:-(
true
250 end.
251
252 is_object_protected(#interface_type{directives = Directives}) ->
253
:-(
lists:any(fun is_protected_directive/1, Directives);
254 is_object_protected(#object_type{directives = Directives}) ->
255 6 lists:any(fun is_protected_directive/1, Directives);
256 is_object_protected(_) ->
257
:-(
false.
258
259 is_field_protected(_, #frag_spread{id = {name, _, Name}}, Definitions) ->
260
:-(
[#frag{schema = Schema, selection_set = Set}] =
261
:-(
lists:filter(fun(#frag{id = {name, _, Name2}}) -> Name == Name2;
262
:-(
(_) -> false end, Definitions),
263
:-(
is_object_protected(Schema, Set, Definitions);
264 is_field_protected(_, #frag{schema = Object, selection_set = Set}, Definitions) ->
265
:-(
is_object_protected(Object, Set, Definitions);
266 is_field_protected(Parent,
267 #field{id = {name, _, Name}, schema = Object, selection_set = Set},
268 Definitions) ->
269 11 {ok, #schema_field{directives = Directives}} = maps:find(Name, fields(Parent)),
270 11 case lists:any(fun is_protected_directive/1, Directives) of
271 false ->
272 11 is_object_protected(Object, Set, Definitions);
273 true ->
274
:-(
true
275 end.
276
277 11 fields(#object_type{fields = Fields}) -> Fields;
278
:-(
fields(#interface_type{fields = Fields}) -> Fields.
Line Hits Source