./ct_report/coverage/mongoose_graphql_directive_protected.COVER.html

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.
Line Hits Source