Skip to content

Commit

Permalink
Merge pull request #3822 from esl/graphql-allowed-categories
Browse files Browse the repository at this point in the history
Adding allowed_categories option to graphql_cowboy_handler
  • Loading branch information
chrzaszcz authored Nov 10, 2022
2 parents c5cd29b + c1dd1d7 commit 68efb98
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 8 deletions.
61 changes: 58 additions & 3 deletions big_tests/tests/graphql_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
-compile([export_all, nowarn_export_all]).

-import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]).
-import(graphql_helper, [execute/3, execute_auth/2, execute_user/3]).
-import(graphql_helper, [execute/3, execute_auth/2, execute_user/3,
get_value/2, get_bad_request/1]).

-define(assertAdminAuth(Domain, Type, Auth, Data),
assert_auth(#{<<"domain">> => Domain,
Expand All @@ -24,13 +25,15 @@ all() ->
[{group, cowboy_handler},
{group, admin_handler},
{group, domain_admin_handler},
{group, user_handler}].
{group, user_handler},
{group, categories_disabled}].

groups() ->
[{cowboy_handler, [parallel], cowboy_handler()},
{user_handler, [parallel], user_handler()},
{domain_admin_handler, [parallel], domain_admin_handler()},
{admin_handler, [parallel], admin_handler()}].
{admin_handler, [parallel], admin_handler()},
{categories_disabled, [parallel], categories_disabled_tests()}].

cowboy_handler() ->
[can_connect_to_admin,
Expand All @@ -50,6 +53,13 @@ domain_admin_handler() ->
common_tests() ->
[can_load_graphiql].

categories_disabled_tests() ->
[category_disabled_error_test,
admin_checks_auth,
category_does_not_exist_error,
listener_reply_with_validation_error,
multiple_categories_query_test].

init_per_suite(Config) ->
Config1 = escalus:init_per_suite(Config),
dynamic_modules:save_modules(domain_helper:host_type(), Config1).
Expand All @@ -71,13 +81,26 @@ init_per_group(domain_admin_handler, Config) ->
init_per_group(user_handler, Config) ->
Config1 = escalus:create_users(Config, escalus:get_users([alice])),
[{schema_endpoint, user} | Config1];
init_per_group(categories_disabled, Config) ->
#{node := Node} = mim(),
CowboyGraphqlListenerConfig = graphql_helper:get_listener_config(Node, admin),
#{handlers := [SchemaConfig]} = CowboyGraphqlListenerConfig,
UpdatedSchemaConfig = maps:put(allowed_categories, [<<"vcard">>, <<"checkAuth">>], SchemaConfig),
UpdatedListenerConfig = maps:put(handlers, [UpdatedSchemaConfig], CowboyGraphqlListenerConfig),
mongoose_helper:restart_listener(mim(), UpdatedListenerConfig),
Config1 = [{admin_listener_config, CowboyGraphqlListenerConfig} | Config],
graphql_helper:init_admin_handler(Config1);
init_per_group(cowboy_handler, Config) ->
Config.

end_per_group(user_handler, Config) ->
escalus:delete_users(Config, escalus:get_users([alice]));
end_per_group(domain_admin_handler, Config) ->
graphql_helper:end_domain_admin_handler(Config);
end_per_group(categories_disabled, Config) ->
ListenerConfig = ?config(admin_listener_config, Config),
mongoose_helper:restart_listener(mim(), ListenerConfig),
Config;
end_per_group(_, _Config) ->
ok.

Expand Down Expand Up @@ -136,6 +159,32 @@ auth_domain_admin_checks_auth(Config) ->
Res = execute_auth(admin_check_auth_body(), Config),
?assertAdminAuth(Domain, 'DOMAIN_ADMIN', 'AUTHORIZED', Res).

category_disabled_error_test(Config) ->
Status = execute_auth(admin_server_get_loglevel_body(), Config),
{_Code, #{<<"errors">> := [Msg]}} = Status,
?assertEqual(<<"category_disabled">>, get_value([extensions, code], Msg)),
?assertEqual([<<"server">>], get_value([path], Msg)).

category_does_not_exist_error(Config) ->
Ep = ?config(schema_endpoint, Config),
Status = execute(Ep, #{<<"query">> => <<"{ field ">>}, undefined),
get_bad_request(Status),
{_Code, #{<<"errors">> := [Msg]}} = Status,
?assertEqual(<<"parser_error">>, get_value([extensions, code], Msg)).

listener_reply_with_validation_error(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{<<"query">> => <<"query Q1 { field } query Q1 { field }">>,
<<"operationName">> => <<"Q1">>},
{Status, Data} = execute(Ep, Body, undefined).

multiple_categories_query_test(Config) ->
Status = execute_auth(user_check_auth_multiple(), Config),
{_Code, #{<<"errors">> := [ErrorMsg], <<"data">> := DataMsg}} = Status,
?assertEqual(<<"category_disabled">>, get_value([extensions, code], ErrorMsg)),
?assertEqual([<<"server">>], get_value([path], ErrorMsg)),
?assertEqual(<<"AUTHORIZED">>, get_value([checkAuth, authStatus], DataMsg)).

%% Helpers

assert_auth(Auth, {Status, Data}) ->
Expand All @@ -158,5 +207,11 @@ maybe_atom_to_bin(X) -> atom_to_binary(X).
admin_check_auth_body() ->
#{query => "{ checkAuth { domain authType authStatus } }"}.

admin_server_get_loglevel_body() ->
#{query => "{ server { getLoglevel } }"}.

user_check_auth_body() ->
#{query => "{ checkAuth { username authStatus } }"}.

user_check_auth_multiple() ->
#{query => "{ checkAuth { authStatus } server { getLoglevel } }"}.
9 changes: 8 additions & 1 deletion doc/configuration/listen.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,13 @@ When set, enables authentication for the admin API, otherwise it is disabled. Re
* **Default:** not set
* **Example:** `password = "secret"`

Required to enable authentication for the admin API.
#### `listen.http.handlers.mongoose_graphql_cowboy_handler.allowed_categories`
* **Syntax:** non-empty array of strings. Allowed values: `"checkAuth", "account", "domain", "last", "muc", "muc_light", "session", "stanza", "roster", "vcard", "private", "metric", "stat", "gdpr", "mnesia", "server", "inbox", "http_upload", "offline", "token"`
* **Default:** all GraphQL categories enabled
* **Example:** `allowed_categories = ["domain", "last"]`

By default, when the option is not included, all GraphQL categories are enabled, so you don't need to add this option.
When this option is added, only listed GraphQL categories will be processed. For others, the error "category disabled" will be returned.

### Handler types: REST API - Admin - `mongoose_admin_api`

Expand Down Expand Up @@ -653,6 +659,7 @@ GraphQL API for administration, the listener is bound to 127.0.0.1 for increased
schema_endpoint = "admin"
username = "admin"
password = "secret"
allowed_categories = ["server", "last", "vcard"]
```

#### Example 3. Domain Admin GraphQL API
Expand Down
4 changes: 3 additions & 1 deletion src/graphql/mongoose_graphql.erl
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ prepare_request(Ep, #{document := Doc,
error_module => mongoose_graphql_errors},
Ast3 = mongoose_graphql_directive:process_directives(Ctx2, Ast2),
mongoose_graphql_operations:verify_operations(Ctx2, Ast3),
Request#{ast => Ast3, ctx := Ctx2}.
AllowedCategories = maps:get(allowed_categories, Ctx2, []),
Ast4 = mongoose_graphql_check_categories:process_ast(Ast3, AllowedCategories),
Request#{ast => Ast4, ctx := Ctx2}.

execute_graphql(Ep, #{ast := Ast, ctx := Ctx}) ->
{ok, graphql:execute(Ep, Ctx, Ast)}.
Expand Down
50 changes: 50 additions & 0 deletions src/graphql/mongoose_graphql_check_categories.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
-module(mongoose_graphql_check_categories).

-export([process_ast/2]).

-include_lib("graphql/src/graphql_schema.hrl").
-include_lib("graphql/src/graphql_internal.hrl").
-include_lib("graphql/include/graphql.hrl").
-include_lib("jid/include/jid.hrl").

-type document() :: #document{}.
-type categories() :: [binary()].

-include("mongoose.hrl").

-spec process_ast(document(), categories()) -> document().
process_ast(#document{definitions = Definitions} = Document, Categories) ->
case Categories of
[] ->
Document;
_ ->
Definitions2 = lists:map(fun(#op{schema = Schema} = Op) ->
parse_schema(Schema, Op, Categories)
end, Definitions),
#document{definitions = Definitions2}
end.

parse_schema(#object_type{fields = Fields} = Schema, Op, Categories) ->
Fields2 = maps:map(fun(Key, Value) ->
case lists:member(Key, Categories) of
true -> Value;
false ->
case Value of
#schema_field{resolve = undefined} ->
Fun = category_disabled_fun(Key),
Value#schema_field{resolve = Fun};
_ ->
Value
end
end
end, Fields),
Schema2 = Schema#object_type{fields = Fields2},
Op#op{schema = Schema2};
parse_schema(_, Op, _) ->
Op.

-spec category_disabled_fun(binary()) -> resolver().
category_disabled_fun(Category) ->
Msg = <<"Category disabled">>,
Extra = #{category => Category},
fun(_, _, _, _) -> mongoose_graphql_helper:make_error(category_disabled, Msg, Extra) end.
17 changes: 14 additions & 3 deletions src/graphql/mongoose_graphql_cowboy_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ config_spec() ->
items = #{<<"username">> => #option{type = binary},
<<"password">> => #option{type = binary},
<<"schema_endpoint">> => #option{type = atom,
validate = {enum, [admin, domain_admin, user]}}
},
validate = {enum, [admin, domain_admin, user]}},
<<"allowed_categories">> => #list{items = #option{type = binary,
validate = {enum, allowed_categories()}},
validate = unique_non_empty}},
format_items = map,
required = [<<"schema_endpoint">>],
process = fun ?MODULE:process_config/1}.
Expand Down Expand Up @@ -207,7 +209,10 @@ run_request(#{} = ReqCtx, Req, #{schema_endpoint := EpName,
authorized := AuthStatus} = State) ->
Ep = mongoose_graphql:get_endpoint(EpName),
Ctx = maps:get(schema_ctx, State, #{}),
ReqCtx2 = ReqCtx#{authorized => AuthStatus, ctx => Ctx#{method => http}},
AllowedCategories = maps:get(allowed_categories, State, []),
ReqCtx2 = ReqCtx#{authorized => AuthStatus,
ctx => Ctx#{method => http,
allowed_categories => AllowedCategories}},
case mongoose_graphql:execute(Ep, ReqCtx2) of
{ok, Response} ->
ResponseBody = mongoose_graphql_response:term_to_json(Response),
Expand Down Expand Up @@ -270,3 +275,9 @@ reply_error(Msg, Req, State) ->
Req2 = cowboy_req:set_resp_body(Body, Req),
Reply = cowboy_req:reply(Code, Req2),
{stop, Reply, State}.

allowed_categories() ->
[<<"checkAuth">>, <<"account">>, <<"domain">>, <<"last">>, <<"muc">>, <<"muc_light">>,
<<"session">>, <<"stanza">>, <<"roster">>, <<"vcard">>, <<"private">>, <<"metric">>,
<<"stat">>, <<"gdpr">>, <<"mnesia">>, <<"server">>, <<"inbox">>, <<"http_upload">>,
<<"offline">>, <<"token">>].
3 changes: 3 additions & 0 deletions test/config_parser_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,9 @@ listen_http_handlers_graphql(_Config) ->
T = fun graphql_handler_raw/1,
{P, _} = test_listen_http_handler(mongoose_graphql_cowboy_handler, T),
test_listen_http_handler_creds(P, T),
?cfg(P ++ [allowed_categories], [<<"muc">>, <<"inbox">>],
T(#{<<"allowed_categories">> => [<<"muc">>, <<"inbox">>]})),
?err(T(#{<<"allowed_categories">> => [<<"invalid">>]})),
?err(T(#{<<"schema_endpoint">> => <<"wrong_endpoint">>})),
?err(http_handler_raw(mongoose_graphql_cowboy_handler, #{})).

Expand Down

0 comments on commit 68efb98

Please sign in to comment.