1 |
|
%% @doc The custom directive `@protected' is created to mark which objects or fields |
2 |
|
%% could be accessed only by an authorized request. |
3 |
|
%% This module analyzes the AST and tries to find if there is at least one protected |
4 |
|
%% resource. The `@protected' directive can be attached to <b>field definitions</b> |
5 |
|
%% to <b>objects</b>, or to <b>interfaces</b>. |
6 |
|
%% |
7 |
|
%% Interfaces and objects permissions are checked independently. This means that when |
8 |
|
%% an interface is protected or has protected fields, then all implementing objects |
9 |
|
%% should be protected or have the same fields protected. <strong>This demands to mark all |
10 |
|
%% protected resources at every occurrence with the directive</strong>. Otherwise, permissions |
11 |
|
%% will be different for interface and implementing objects. |
12 |
|
%% |
13 |
|
%% If an unauthorized request wants to execute a query that contains protected resources, |
14 |
|
%% an error is thrown. |
15 |
|
|
16 |
|
-module(mongoose_graphql_directive_protected). |
17 |
|
|
18 |
|
-behaviour(mongoose_graphql_directive). |
19 |
|
|
20 |
|
-export([handle_directive/3, handle_object_directive/3]). |
21 |
|
|
22 |
|
-include_lib("graphql/src/graphql_schema.hrl"). |
23 |
|
-include_lib("graphql/include/graphql.hrl"). |
24 |
|
-include_lib("jid/include/jid.hrl"). |
25 |
|
|
26 |
|
-import(mongoose_graphql_directive_helper, [name/1, op_name/1, get_arg/2]). |
27 |
|
|
28 |
|
-type error_type() :: global | domain. |
29 |
|
|
30 |
|
%% @doc Checks if command can be executed by unauthorized request or authorized as one |
31 |
|
%% of the roles (USER, ADMIN, DOMAIN_ADMIN). If not, throw an error. |
32 |
|
%% |
33 |
|
%% The USER and ADMIN can execute each query because they are on separated GraphQL |
34 |
|
%% instances that serves different queries. |
35 |
|
%% |
36 |
|
%% The DOMAIN_ADMIN use the same GraphQL instance as ADMIN, but have permissions |
37 |
|
%% only to administrate own domain. |
38 |
|
handle_directive(#directive{id = <<"protected">>}, |
39 |
|
_Field, |
40 |
|
#{authorized := false, operation_name := OpName}) -> |
41 |
:-( |
OpName2 = op_name(OpName), |
42 |
:-( |
graphql_err:abort([OpName2], authorize, {no_permissions, OpName2}); |
43 |
|
handle_directive(#directive{id = <<"protected">>} = Dir, |
44 |
|
#schema_field{} = Field, |
45 |
|
#{field_args := FieldArgs, |
46 |
|
operation_name := OpName, |
47 |
|
authorized_as := domain_admin, |
48 |
|
path := Path, |
49 |
|
admin := #jid{lserver = Domain}}) -> |
50 |
416 |
#{type := {enum, Type}, args := PArgs} = protected_dir_args_to_map(Dir), |
51 |
416 |
Ctx = #{domain => Domain, |
52 |
|
path => Path, |
53 |
|
operation_name => OpName}, |
54 |
416 |
check_field_args(Type, Ctx, PArgs, FieldArgs), |
55 |
241 |
Field; |
56 |
|
handle_directive(#directive{id = <<"protected">>}, Field, #{authorized := true}) -> |
57 |
1082 |
Field. |
58 |
|
|
59 |
|
%% @doc Checks if category can be executed by unauthorized request. |
60 |
|
handle_object_directive(#directive{id = <<"protected">>}, |
61 |
|
_Object, |
62 |
|
#{authorized := false, operation_name := OpName}) -> |
63 |
2 |
OpName2 = op_name(OpName), |
64 |
2 |
graphql_err:abort([OpName2], authorize, {no_permissions, OpName2}); |
65 |
|
handle_object_directive(#directive{id = <<"protected">>}, |
66 |
|
Object, |
67 |
|
#{authorized := true} = Ctx) -> |
68 |
2717 |
{Object, Ctx}. |
69 |
|
|
70 |
|
%% Internal |
71 |
|
|
72 |
|
-spec protected_dir_args_to_map(graphql:directive()) -> map(). |
73 |
|
protected_dir_args_to_map(#directive{args = Args}) -> |
74 |
416 |
Default = #{type => {enum, <<"DEFAULT">>}, args => []}, |
75 |
416 |
ArgsMap = maps:from_list([{binary_to_atom(name(N)), V} || {N, V} <- Args]), |
76 |
416 |
maps:merge(Default, ArgsMap). |
77 |
|
|
78 |
|
-spec check_field_args(binary(), map(), [binary()], map()) -> ok. |
79 |
|
check_field_args(<<"DOMAIN">>, #{domain := Domain} = Ctx, ProtectedArgs, Args) -> |
80 |
397 |
case lists:filter(fun(N) -> not arg_eq(get_arg(N, Args), Domain) end, ProtectedArgs) of |
81 |
|
[] -> |
82 |
241 |
ok; |
83 |
|
InvalidArgs -> |
84 |
156 |
raise_authorize_error(Ctx, domain, InvalidArgs) |
85 |
|
end; |
86 |
|
check_field_args(<<"GLOBAL">>, Ctx, _, _Args) -> |
87 |
19 |
raise_authorize_error(Ctx, global, undefined); |
88 |
|
check_field_args(<<"DEFAULT">>, _Ctx, _ProtectedArgs, _Args) -> |
89 |
:-( |
ok. |
90 |
|
|
91 |
|
-spec raise_authorize_error(map(), error_type(), [binary()]) -> no_return(). |
92 |
|
raise_authorize_error(Ctx, Type, InvalidArgs) -> |
93 |
175 |
#{path := Path, operation_name := OpName} = Ctx, |
94 |
175 |
Error = {no_permissions, op_name(OpName), #{type => Type, invalid_args => InvalidArgs}}, |
95 |
175 |
graphql_err:abort(Path, authorize, Error). |
96 |
|
|
97 |
|
-spec arg_eq(ToMatchDomain, Domain) -> boolean() |
98 |
|
when Domain :: jid:lserver(), |
99 |
|
ToMatchDomain :: [jid:jid() | jid:lserver()] | jid:jid() | jid:lserver(). |
100 |
|
arg_eq(Args, Domain) when is_list(Args) -> |
101 |
4 |
lists:all(fun(Arg) -> arg_eq(Arg, Domain) end, Args); |
102 |
|
arg_eq(Domain, Domain) -> |
103 |
215 |
true; |
104 |
|
arg_eq(Subdomain, Domain) when is_binary(Subdomain), is_binary(Domain) -> |
105 |
197 |
check_subdomain(Subdomain, Domain); |
106 |
|
arg_eq(#jid{lserver = Domain1}, Domain2) -> |
107 |
315 |
arg_eq(Domain1, Domain2); |
108 |
|
arg_eq(_, _) -> |
109 |
11 |
false. |
110 |
|
|
111 |
|
-spec check_subdomain(jid:lserver(), jid:lserver()) -> boolean(). |
112 |
|
check_subdomain(Subdomain, Domain) -> |
113 |
197 |
case mongoose_domain_api:get_subdomain_info(Subdomain) of |
114 |
|
{ok, #{parent_domain := ParentDomain}} -> |
115 |
68 |
ParentDomain =:= Domain; |
116 |
|
{error, not_found} -> |
117 |
129 |
false |
118 |
|
end. |