./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/3]).
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
31 -type auth_status() :: boolean().
32 -type document() :: #document{}.
33
34 %% @doc Checks if query can be executed by unauthorized request. If not, throws
35 %% an error. When request is authorized, just skip.
36 %% @end
37 -spec check_permissions(binary(), auth_status(), document()) -> ok.
38 check_permissions(OpName, false, #document{definitions = Definitions}) ->
39 2 Op = lists:filter(fun(D) -> is_req_operation(D, OpName) end, Definitions),
40 2 case Op of
41 [#op{schema = Schema, selection_set = Set} = Op1] ->
42 2 case is_object_protected(Schema, Set, Definitions) of
43 true ->
44 % Seems that the introspection fields belong to the query object.
45 % When an object is protected we need to ensure that the request
46 % query contains only introspection fields to execute it without
47 % authorization. Otherwise, a user couldn't access documentation
48 % without logging in.
49
:-(
case is_introspection_op(Op1) of
50 true ->
51
:-(
ok;
52 false ->
53
:-(
OpName2 = op_name(OpName),
54
:-(
graphql_err:abort([OpName2], authorize, {no_permissions, OpName2})
55 end;
56 false ->
57 2 ok
58 end;
59 _ ->
60
:-(
ok
61 end;
62 check_permissions(_, true, _) ->
63 206 ok.
64
65 % Internal
66
67 op_name(undefined) ->
68
:-(
<<"ROOT">>;
69 op_name(Name) ->
70
:-(
Name.
71
72 is_req_operation(#op{id = 'ROOT'}, undefined) ->
73 2 true;
74 is_req_operation(#op{id = {name, _, Name}}, Name) ->
75
:-(
true;
76 is_req_operation(_, _) ->
77
:-(
false.
78
79 is_protected_directive(#directive{id = {name, _, <<"protected">>}}) ->
80
:-(
true;
81 is_protected_directive(_) ->
82
:-(
false.
83
84 is_introspection_op(#op{selection_set = Set}) ->
85
:-(
lists:all(fun is_introspection_field/1, Set).
86
87 is_introspection_field(#field{id = {name, _, <<"__schema">>}}) ->
88
:-(
true;
89 is_introspection_field(#field{id = {name, _, <<"__type">>}}) ->
90
:-(
true;
91 is_introspection_field(_) ->
92
:-(
false.
93
94 is_object_protected(_, [], _) ->
95 3 false;
96 is_object_protected(#schema_field{ty = Ty}, Set, Definitions) ->
97 1 is_object_protected(Ty, Set, Definitions);
98 is_object_protected({non_null, Obj}, Set, Definitions) ->
99
:-(
is_object_protected(Obj, Set, Definitions);
100 is_object_protected({list, Obj}, Set, Definitions) ->
101
:-(
is_object_protected(Obj, Set, Definitions);
102 is_object_protected(Object, Set, Definitions) ->
103 3 case is_object_protected(Object) of
104 false ->
105 3 lists:any(fun(S) -> is_field_protected(Object, S, Definitions) end, Set);
106 true ->
107
:-(
true
108 end.
109
110 is_object_protected(#interface_type{directives = Directives}) ->
111
:-(
lists:any(fun is_protected_directive/1, Directives);
112 is_object_protected(#object_type{directives = Directives}) ->
113 3 lists:any(fun is_protected_directive/1, Directives);
114 is_object_protected(_) ->
115
:-(
false.
116
117 is_field_protected(_, #frag_spread{id = {name, _, Name}}, Definitions) ->
118
:-(
[#frag{schema = Schema, selection_set = Set}] =
119
:-(
lists:filter(fun(#frag{id = {name, _, Name2}}) -> Name == Name2;
120
:-(
(_) -> false end, Definitions),
121
:-(
is_object_protected(Schema, Set, Definitions);
122 is_field_protected(_, #frag{schema = Object, selection_set = Set}, Definitions) ->
123
:-(
is_object_protected(Object, Set, Definitions);
124 is_field_protected(Parent,
125 #field{id = {name, _, Name}, schema = Object, selection_set = Set},
126 Definitions) ->
127 4 {ok, #schema_field{directives = Directives}} = maps:find(Name, fields(Parent)),
128 4 case lists:any(fun is_protected_directive/1, Directives) of
129 false ->
130 4 is_object_protected(Object, Set, Definitions);
131 true ->
132
:-(
true
133 end.
134
135 4 fields(#object_type{fields = Fields}) -> Fields;
136
:-(
fields(#interface_type{fields = Fields}) -> Fields.
Line Hits Source