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