From c6c73eac588a381088f570d20014b47878ff28a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 23 Dec 2021 10:36:25 +0100 Subject: [PATCH 1/8] Prepare mongoose_graphql_SUITE for testing gql listeners --- test/mongoose_graphql_SUITE.erl | 109 +++++++++++++++++- .../listener_schema.gql | 21 ++++ 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 test/mongoose_graphql_SUITE_data/listener_schema.gql diff --git a/test/mongoose_graphql_SUITE.erl b/test/mongoose_graphql_SUITE.erl index 51bc1c94b60..fa795a8fd4f 100644 --- a/test/mongoose_graphql_SUITE.erl +++ b/test/mongoose_graphql_SUITE.erl @@ -5,6 +5,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). -include_lib("graphql/src/graphql_schema.hrl"). +-include_lib("jid/include/jid.hrl"). -define(assertPermissionsFailed(Config, Doc), ?assertThrow({error, #{error_term := {no_permissions, _}}}, @@ -24,14 +25,18 @@ all() -> {group, protected_graphql}, {group, error_handling}, {group, error_formatting}, - {group, permissions}]. + {group, permissions}, + {group, user_listener}, + {group, admin_listener}]. groups() -> [{protected_graphql, [parallel], protected_graphql()}, {unprotected_graphql, [parallel], unprotected_graphql()}, {error_handling, [parallel], error_handling()}, {error_formatting, [parallel], error_formatting()}, - {permissions, [parallel], permissions()}]. + {permissions, [parallel], permissions()}, + {admin_listener, [parallel], admin_listener()}, + {user_listener, [parallel], user_listener()}]. protected_graphql() -> [auth_can_execute_protected_query, @@ -76,6 +81,49 @@ permissions() -> check_union_permissions ]. +user_listener() -> + []. +admin_listener() -> + []. + +init_per_suite(Config) -> + application:ensure_all_started(cowboy), + application:ensure_all_started(jid), + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(user_listener, Config) -> + meck:new(mongoose_api_common, [no_link]), + meck:expect(mongoose_api_common, check_password, + fun + (#jid{user = <<"alice">>}, <<"makota">>) -> {true, {}}; + (_, _) -> false + end), + ListenerOpts = [{schema_endpoint, <<"user">>}], + init_ep_listener(5557, user_schema_ep, ListenerOpts, Config); +init_per_group(admin_listener, Config) -> + ListenerOpts = [{username, <<"admin">>}, + {password, <<"secret">>}, + {schema_endpoint, <<"admin">>}], + init_ep_listener(5558, admin_schema_ep, ListenerOpts, Config); +init_per_group(no_creds_admin_listener, Config) -> + ListenerOpts = [{schema_endpoint, <<"admin">>}], + init_ep_listener(5559, admin_schema_ep, ListenerOpts, Config); +init_per_group(_G, Config) -> + Config. + +end_per_group(user_listener, Config) -> + meck:unload(mongoose_api_common), + ?config(test_process, Config) ! stop, + Config; +end_per_group(admin_listener, Config) -> + ?config(test_process, Config) ! stop, + Config; +end_per_group(_, Config) -> + Config. + init_per_testcase(C, Config) when C =:= auth_can_execute_protected_query; C =:= auth_can_execute_protected_mutation; C =:= unauth_cannot_execute_protected_query; @@ -468,3 +516,60 @@ example_permissions_schema_data(Config) -> interfaces => #{default => mongoose_graphql_default_resolver}, unions => #{default => mongoose_graphql_default_resolver}}, {Mapping, Pattern}. + +example_listener_schema_data(Config) -> + Pattern = filename:join([proplists:get_value(data_dir, Config), "listener_schema.gql"]), + Mapping = + #{objects => + #{'UserQuery' => mongoose_graphql_default_resolver, + 'UserMutation' => mongoose_graphql_default_resolver, + default => mongoose_graphql_default_resolver}}, + {Mapping, Pattern}. + +-spec init_ep_listener(integer(), atom(), [{atom(), term()}], [{atom(), term()}]) -> + [{atom(), term()}]. +init_ep_listener(Port, EpName, ListenerOpts, Config) -> + Pid = spawn(fun() -> + Name = list_to_atom("gql_listener_" ++ atom_to_list(EpName)), + ok = start_listener(Name, Port, ListenerOpts), + {Mapping, Pattern} = example_listener_schema_data(Config), + {ok, _} = mongoose_graphql:create_endpoint(EpName, Mapping, [Pattern]), + receive + stop -> + ok + end + end), + [{test_process, Pid}, {endpoint_addr, "http://localhost:" ++ integer_to_list(Port)} | Config]. + +-spec start_listener(atom(), integer(), [{atom(), term()}]) -> ok. +start_listener(Ref, Port, Opts) -> + Dispatch = cowboy_router:compile([ + {'_', [{"/graphql", mongoose_graphql_cowboy_handler, Opts}]} + ]), + {ok, _} = cowboy:start_clear(Ref, + [{port, Port}], + #{env => #{dispatch => Dispatch}}), + ok. + +-spec execute(binary(), map(), undefined | {binary(), binary()}) -> {{binary(), binary()}, map()}. +execute(EpAddr, Body, undefined) -> + post_request(EpAddr, [], Body); +execute(EpAddr, Body, {Username, Password}) -> + Creds = base64:encode(<>), + Headers = [{<<"Authorization">>, <<"Basic ", Creds/binary>>}], + post_request(EpAddr, Headers, Body). + +post_request(EpAddr, HeadersIn, Body) when is_binary(Body) -> + {ok, Client} = fusco:start(EpAddr, []), + Headers = [{<<"Content-Type">>, <<"application/json">>}, + {<<"Request-Id">>, random_request_id()} | HeadersIn], + {ok, {ResStatus, _, ResBody, _, _}} = Res = + fusco:request(Client, <<"/graphql">>, <<"POST">>, Headers, Body, 5000), + fusco:disconnect(Client), + ct:log("~p", [Res]), + {ResStatus, jiffy:decode(ResBody, [return_maps])}; +post_request(Ep, HeadersIn, Body) -> + post_request(Ep, HeadersIn, jiffy:encode(Body)). + +random_request_id() -> + base16:encode(crypto:strong_rand_bytes(8)). diff --git a/test/mongoose_graphql_SUITE_data/listener_schema.gql b/test/mongoose_graphql_SUITE_data/listener_schema.gql new file mode 100644 index 00000000000..e34b79826c5 --- /dev/null +++ b/test/mongoose_graphql_SUITE_data/listener_schema.gql @@ -0,0 +1,21 @@ +schema{ + query: Query, + mutation: Mutation +} + +directive @protected on FIELD_DEFINITION | OBJECT + +""" +Contains all available queries. +""" +type Query @protected{ + field: String +} + +""" +Contains all available mutations. +""" +type Mutation{ + field: String + id(value: String!): String +} From a6605f4e7bcc0eebddb15eaa3313b3cacbb80ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 23 Dec 2021 10:38:28 +0100 Subject: [PATCH 2/8] Move listener tests to small tests and adapt them --- test/mongoose_graphql_SUITE.erl | 147 +++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/test/mongoose_graphql_SUITE.erl b/test/mongoose_graphql_SUITE.erl index fa795a8fd4f..27ec8a4674a 100644 --- a/test/mongoose_graphql_SUITE.erl +++ b/test/mongoose_graphql_SUITE.erl @@ -82,9 +82,23 @@ permissions() -> ]. user_listener() -> - []. + [auth_user_can_access_protected_types | common_tests()]. admin_listener() -> - []. + [no_creds_defined_admin_can_access_protected, + auth_admin_can_access_protected_types | common_tests()]. + +common_tests() -> + [malformed_auth_header_error, + auth_wrong_creds_error, + invalid_json_body_error, + no_query_supplied_error, + variables_invalid_json_error, + listener_reply_with_parsing_error, + listener_reply_with_type_check_error, + listener_reply_with_validation_error, + listener_unauth_cannot_access_protected_types, + listener_unauth_can_access_unprotected_types, + listener_can_execute_query_with_variables]. init_per_suite(Config) -> application:ensure_all_started(cowboy), @@ -207,6 +221,8 @@ admin_and_user_load_global_types(_Config) -> ?assertMatch(#directive_type{id = <<"protected">>}, graphql_schema:get(UserEp, <<"protected">>)). +%% Protected graphql + auth_can_execute_protected_query(Config) -> Ep = ?config(endpoint, Config), Doc = <<"{ field }">>, @@ -251,6 +267,8 @@ unauth_can_access_introspection(Config) -> }, ?assertEqual(Expected, Res). +%% Unprotected graphql + can_execute_query_with_vars(Config) -> Ep = ?config(endpoint, Config), Doc = <<"query Q1($value: String!) { id(value: $value)}">>, @@ -287,6 +305,8 @@ auth_can_execute_mutation(Config) -> Res = mongoose_graphql:execute(Ep, request(Doc, true)), ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res). +%% Error handling + should_catch_parsing_error(Config) -> Ep = ?config(endpoint, Config), Doc = <<"query { field ">>, @@ -315,6 +335,8 @@ should_catch_validation_error(Config) -> Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)), ?assertMatch({error, #{phase := validate, error_term := {not_unique, _}}}, Res). +%% Permissions + check_object_permissions(Config) -> Doc = <<"query { field }">>, FDoc = <<"mutation { field }">>, @@ -374,6 +396,8 @@ check_union_permissions(Config) -> ?assertPermissionsFailed(Config, FDoc), ?assertPermissionsFailed(Config, FDoc2). +%% Error formatting + format_internal_crash(_Config) -> {Code, Res} = mongoose_graphql_errors:format_error(internal_crash), ?assertEqual(500, Code), @@ -448,8 +472,127 @@ format_any_error(_Config) -> ?assertErrMsg(uncategorized, <<"any_error">>, Msg3), ?assertErrMsg(uncategorized, <<"any_error">>, Msg4). +%% Listeners + +auth_user_can_access_protected_types(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{query => "{ field }"}, + {Status, Data} = execute(Ep, Body, {<<"alice@localhost">>, <<"makota">>}), + assert_access_granted(Status, Data). + +no_creds_defined_admin_can_access_protected(_Config) -> + Port = 5559, + Ep = "http://localhost:" ++ integer_to_list(Port), + start_listener(no_creds_admin_listener, Port, [{schema_endpoint, <<"admin">>}]), + Body = #{<<"query">> => <<"{ field }">>}, + {Status, Data} = execute(Ep, Body, undefined), + assert_access_granted(Status, Data). + +auth_admin_can_access_protected_types(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{query => "{ field }"}, + {Status, Data} = execute(Ep, Body, {<<"admin">>, <<"secret">>}), + assert_access_granted(Status, Data). + +malformed_auth_header_error(Config) -> + Ep = ?config(endpoint_addr, Config), + % The encoded credentials value is malformed and cannot be decoded. + Headers = [{<<"Authorization">>, <<"Basic YWRtaW46c2VjcmV">>}], + {Status, Data} = post_request(Ep, Headers, <<"">>), + assert_no_permissions(request_error, Status, Data). + +auth_wrong_creds_error(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{query => "{ field }"}, + {Status, Data} = execute(Ep, Body, {<<"user">>, <<"wrong_password">>}), + assert_no_permissions(wrong_credentials, Status, Data). + +invalid_json_body_error(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = <<"">>, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(invalid_json_body, Data). + +no_query_supplied_error(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(no_query_supplied, Data). + +variables_invalid_json_error(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{<<"query">> => <<"{ field }">>, <<"variables">> => <<"{1: 2}">>}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(variables_invalid_json, Data). + +listener_reply_with_parsing_error(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{<<"query">> => <<"{ field ">>}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(parser_error, Data), + + BodyScanner = #{<<"query">> => <<"mutation { id(value: \"asdfsad) } ">>}, + {StatusScanner, DataScanner} = execute(Ep, BodyScanner, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, StatusScanner), + assert_code(scanner_error, DataScanner). + +listener_reply_with_type_check_error(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{<<"query">> => <<"mutation { id(value: 12) }">>}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(input_coercion, Data). + +listener_reply_with_validation_error(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{<<"query">> => <<"query Q1 { field } query Q1 { field }">>, + <<"operationName">> => <<"Q1">>}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(not_unique, Data). + +listener_can_execute_query_with_variables(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{query => "mutation M1($value: String!){ id(value: $value) } query Q1{ field }", + variables => #{value => <<"Hello">>}, + operationName => <<"M1">> + }, + {Status, Data} = execute(Ep, Body, undefined), + assert_access_granted(Status, Data), + ?assertMatch(#{<<"data">> := #{<<"id">> := <<"Hello">>}}, Data). + +listener_unauth_cannot_access_protected_types(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{query => "{ field }"}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertMatch(#{<<"errors">> := [#{<<"path">> := [<<"ROOT">>]}]}, Data), + assert_no_permissions(no_permissions, Status, Data). + +listener_unauth_can_access_unprotected_types(Config) -> + Ep = ?config(endpoint_addr, Config), + Body = #{query => "mutation { field }"}, + {Status, Data} = execute(Ep, Body, undefined), + assert_access_granted(Status, Data). + %% Helpers +assert_code(Code, Data) -> + BinCode = atom_to_binary(Code), + ?assertMatch(#{<<"errors">> := [#{<<"extensions">> := #{<<"code">> := BinCode}}]}, Data). + +assert_no_permissions(ExpectedCode, Status, Data) -> + ?assertEqual({<<"401">>,<<"Unauthorized">>}, Status), + assert_code(ExpectedCode, Data). + +assert_access_granted(Status, Data) -> + ?assertEqual({<<"200">>,<<"OK">>}, Status), + % access was granted, no error was returned + ?assertNotMatch(#{<<"errors">> := _}, Data). + assert_err_msg(Code, MsgContains, #{message := Msg} = ErrorMsg) -> ?assertMatch(#{extensions := #{code := Code}}, ErrorMsg), ?assertNotEqual(nomatch, binary:match(Msg, MsgContains)). From 8a3f468fde191e94b631e7be91f44f209b974d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 23 Dec 2021 12:16:28 +0100 Subject: [PATCH 3/8] Clean graphql big tests --- big_tests/tests/graphql_SUITE.erl | 123 +----------------- big_tests/tests/graphql_SUITE_data/schema.gql | 21 --- big_tests/tests/graphql_helper.erl | 20 +-- 3 files changed, 7 insertions(+), 157 deletions(-) delete mode 100644 big_tests/tests/graphql_SUITE_data/schema.gql diff --git a/big_tests/tests/graphql_SUITE.erl b/big_tests/tests/graphql_SUITE.erl index aa035af641c..658767ce6db 100644 --- a/big_tests/tests/graphql_SUITE.erl +++ b/big_tests/tests/graphql_SUITE.erl @@ -5,7 +5,7 @@ -compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). --import(graphql_helper, [load_test_schema/2]). +-import(graphql_helper, []). suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). @@ -30,28 +30,12 @@ admin_handler() -> [auth_admin_can_access_protected_types | common_tests()]. common_tests() -> - [malformed_auth_header_error, - document_parse_error, - document_type_check_error, - document_validate_error, - wrong_creds_cannot_access_protected_types, - unauth_cannot_access_protected_types, - unauth_can_access_unprotected_types, - can_execute_query_with_variables, - invalid_json_body_error, - no_query_supplied_error, - variables_invalid_json_error, - can_load_graphiql]. + [can_load_graphiql]. init_per_suite(Config) -> - % reset endpoints and load test schema - ok = load_test_schema(admin, Config), - ok = load_test_schema(user, Config), escalus:init_per_suite(Config). end_per_suite(Config) -> - % reinit endpoints with original schemas - ok = rpc(mim(), mongoose_graphql, init, []), escalus:end_per_suite(Config). init_per_group(admin_handler, Config) -> @@ -86,63 +70,11 @@ can_connect_to_admin(_Config) -> can_connect_to_user(_Config) -> ?assertMatch({{<<"400">>,<<"Bad Request">>}, _}, execute(user, #{}, undefined)). -unauth_cannot_access_protected_types(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = #{query => "{ field }"}, - {Status, Data} = execute(Ep, Body, undefined), - ?assertMatch(#{<<"errors">> := [#{<<"path">> := [<<"ROOT">>]}]}, Data), - assert_no_permissions(no_permissions, Status, Data). - -unauth_can_access_unprotected_types(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = #{query => "mutation { field }"}, - {Status, Data} = execute(Ep, Body, undefined), - assert_access_granted(Status, Data). - -wrong_creds_cannot_access_protected_types(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = #{query => "{ field }"}, - {Status, Data} = execute(Ep, Body, {<<"user">>, <<"wrong_password">>}), - assert_no_permissions(wrong_credentials, Status, Data). - -malformed_auth_header_error(Config) -> - EpName = ?config(schema_endpoint, Config), - Request = - #{port => get_port(EpName), - role => {graphql, atom_to_binary(EpName)}, - method => <<"POST">>, - headers => [{<<"Authorization">>, <<"Basic YWRtaW46c2VjcmV">>}], - return_maps => true, - path => "/graphql"}, - % The encoded credentials value is malformed and cannot be decoded. - {Status, Data} = rest_helper:make_request(Request), - assert_no_permissions(request_error, Status, Data). - -document_parse_error(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = #{<<"query">> => <<"{ field ">>}, - {Status, Data} = execute(Ep, Body, undefined), - ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - assert_code(parser_error, Data), - - BodyScanner = #{<<"query">> => <<"mutation { id(value: \"asdfsad) } ">>}, - {StatusScanner, DataScanner} = execute(Ep, BodyScanner, undefined), - ?assertEqual({<<"400">>,<<"Bad Request">>}, StatusScanner), - assert_code(scanner_error, DataScanner). - -document_type_check_error(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = #{<<"query">> => <<"mutation { id(value: 12) }">>}, - {Status, Data} = execute(Ep, Body, undefined), - ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - assert_code(input_coercion, Data). - -document_validate_error(Config) -> +can_load_graphiql(Config) -> Ep = ?config(schema_endpoint, Config), - Body = #{<<"query">> => <<"query Q1 { field } query Q1 { field }">>, <<"operationName">> => <<"Q1">>}, - {Status, Data} = execute(Ep, Body, undefined), - ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - assert_code(not_unique, Data). + {Status, Html} = get_graphiql_website(Ep), + ?assertEqual({<<"200">>,<<"OK">>}, Status), + ?assertNotEqual(nomatch, binary:match(Html, <<"Loading...">>)). auth_user_can_access_protected_types(Config) -> escalus:fresh_story( @@ -165,49 +97,6 @@ auth_admin_can_access_protected_types(Config) -> {Status, Data} = execute(Ep, Body, {User, Password}), assert_access_granted(Status, Data). -can_execute_query_with_variables(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = #{query => "mutation M1($value: String!){ id(value: $value) } query Q1{ field }", - variables => #{value => <<"Hello">>}, - operationName => <<"M1">> - }, - {Status, Data} = execute(Ep, Body, undefined), - ?assertEqual({<<"200">>,<<"OK">>}, Status), - % operation M1 was executed, because id is in path - % access was granted, an error was returned because valid resolver was not defined - ?assertMatch(#{<<"data">> := #{<<"id">> := null}, - <<"errors">> := - [#{<<"extensions">> := #{<<"code">> := <<"resolver_crash">>}, - <<"path">> := [<<"id">>]}]}, - Data). - -can_load_graphiql(Config) -> - Ep = ?config(schema_endpoint, Config), - {Status, Html} = get_graphiql_website(Ep), - ?assertEqual({<<"200">>,<<"OK">>}, Status), - ?assertNotEqual(nomatch, binary:match(Html, <<"Loading...">>)). - -invalid_json_body_error(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = <<"">>, - {Status, Data} = execute(Ep, Body, undefined), - ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - assert_code(invalid_json_body, Data). - -no_query_supplied_error(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = #{}, - {Status, Data} = execute(Ep, Body, undefined), - ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - assert_code(no_query_supplied, Data). - -variables_invalid_json_error(Config) -> - Ep = ?config(schema_endpoint, Config), - Body = #{<<"query">> => <<"{ field }">>, <<"variables">> => <<"{1: 2}">>}, - {Status, Data} = execute(Ep, Body, undefined), - ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - assert_code(variables_invalid_json, Data). - %% Helpers assert_code(Code, Data) -> diff --git a/big_tests/tests/graphql_SUITE_data/schema.gql b/big_tests/tests/graphql_SUITE_data/schema.gql deleted file mode 100644 index e34b79826c5..00000000000 --- a/big_tests/tests/graphql_SUITE_data/schema.gql +++ /dev/null @@ -1,21 +0,0 @@ -schema{ - query: Query, - mutation: Mutation -} - -directive @protected on FIELD_DEFINITION | OBJECT - -""" -Contains all available queries. -""" -type Query @protected{ - field: String -} - -""" -Contains all available mutations. -""" -type Mutation{ - field: String - id(value: String!): String -} diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index cbfefcbe29c..d0d0b1f1d47 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -2,23 +2,5 @@ -include_lib("common_test/include/ct.hrl"). --import(distributed_helper, [mim/0, rpc/4]). +-export([]). --export([load_test_schema/2]). - -load_test_schema(Name, Config) -> - Path = filename:join([?config(mim_data_dir, Config), "schema.gql"]), - {ok, SchemaData} = file:read_file(Path), - Ep = rpc(mim(), mongoose_graphql, get_endpoint, [Name]), - ok = rpc(mim(), graphql_schema, reset, [Ep]), - ok = rpc(mim(), graphql, load_schema, [Ep, test_schema_mapping(), SchemaData]), - ok = rpc(mim(), graphql, validate_schema, [Ep]), - ok. - -test_schema_mapping() -> - #{objects => #{ - 'Query' => mongoose_graphql_default, - 'Mutation' => mongoose_graphql_default, - default => mongoose_graphql_default - } - }. From 9309d9ac0987e16cf33f9b6047a8291c4be8627d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 23 Dec 2021 13:45:44 +0100 Subject: [PATCH 4/8] Extract some helper functions to the graphql helper module --- big_tests/tests/graphql_SUITE.erl | 34 ++------------------------ big_tests/tests/graphql_helper.erl | 39 ++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/big_tests/tests/graphql_SUITE.erl b/big_tests/tests/graphql_SUITE.erl index 658767ce6db..a972887a2f5 100644 --- a/big_tests/tests/graphql_SUITE.erl +++ b/big_tests/tests/graphql_SUITE.erl @@ -5,7 +5,7 @@ -compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). --import(graphql_helper, []). +-import(graphql_helper, [execute/3, get_listener_port/1, get_listener_config/1]). suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). @@ -118,10 +118,6 @@ user_password(User) -> [{User, Props}] = escalus:get_users([User]), proplists:get_value(password, Props). -get_port(EpName) -> - {PortIpNet, ejabberd_cowboy, _Opts} = get_listener_config(EpName), - element(1, PortIpNet). - get_listener_opts(EpName) -> {_, ejabberd_cowboy, Opts} = get_listener_config(EpName), {value, {modules, Modules}} = lists:keysearch(modules, 1, Opts), @@ -134,35 +130,9 @@ get_listener_opts(EpName) -> end, Modules), Opts2. -get_listener_config(EpName) -> - Listeners = rpc(mim(), mongoose_config, get_opt, [listen]), - [{_, ejabberd_cowboy, _} = Config] = - lists:filter(fun(Config) -> is_graphql_config(Config, EpName) end, Listeners), - Config. - -is_graphql_config({_PortIpNet, ejabberd_cowboy, Opts}, EpName) -> - {value, {modules, Modules}} = lists:keysearch(modules, 1, Opts), - lists:any(fun({_, _Path, mongoose_graphql_cowboy_handler, Args}) -> - atom_to_binary(EpName) == proplists:get_value(schema_endpoint, Args); - (_) -> false - end, Modules); -is_graphql_config(_, _EpName) -> - false. - -execute(EpName, Body, Creds) -> - Request = - #{port => get_port(EpName), - role => {graphql, atom_to_binary(EpName)}, - method => <<"POST">>, - return_maps => true, - creds => Creds, - path => "/graphql", - body => Body}, - rest_helper:make_request(Request). - get_graphiql_website(EpName) -> Request = - #{port => get_port(EpName), + #{port => get_listener_port(EpName), role => {graphql, atom_to_binary(EpName)}, method => <<"GET">>, headers => [{<<"Accept">>, <<"text/html">>}], diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index d0d0b1f1d47..fe698662f51 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -1,6 +1,41 @@ -module(graphql_helper). --include_lib("common_test/include/ct.hrl"). +-import(distributed_helper, [mim/0, rpc/4]). --export([]). +-export([execute/3, get_listener_port/1, get_listener_config/1]). +-spec execute(atom(), binary(), {binary(), binary()} | undefined) -> + {Status :: tuple(), Data :: map()}. +execute(EpName, Body, Creds) -> + Request = + #{port => get_listener_port(EpName), + role => {graphql, atom_to_binary(EpName)}, + method => <<"POST">>, + return_maps => true, + creds => Creds, + path => "/graphql", + body => Body}, + rest_helper:make_request(Request). + +-spec get_listener_port(binary()) -> integer(). +get_listener_port(EpName) -> + {PortIpNet, ejabberd_cowboy, _Opts} = get_listener_config(EpName), + element(1, PortIpNet). + +-spec get_listener_config(binary()) -> tuple(). +get_listener_config(EpName) -> + Listeners = rpc(mim(), mongoose_config, get_opt, [listen]), + [{_, ejabberd_cowboy, _} = Config] = + lists:filter(fun(Config) -> is_graphql_config(Config, EpName) end, Listeners), + Config. + +%% Internal + +is_graphql_config({_PortIpNet, ejabberd_cowboy, Opts}, EpName) -> + {value, {modules, Modules}} = lists:keysearch(modules, 1, Opts), + lists:any(fun({_, _Path, mongoose_graphql_cowboy_handler, Args}) -> + atom_to_binary(EpName) == proplists:get_value(schema_endpoint, Args); + (_) -> false + end, Modules); +is_graphql_config(_, _EpName) -> + false. From 5937beaaa7b0256ff604841955107b3033fc49f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 23 Dec 2021 13:58:16 +0100 Subject: [PATCH 5/8] Add query that displays authorization info This will be useful in big tests to check if authorization works correctly. Also, users can easily check their own authorization this way. --- priv/graphql/schemas/admin/admin_schema.gql | 8 +++++--- priv/graphql/schemas/global/auth_status.gql | 7 +++++++ priv/graphql/schemas/user/user_auth_status.gql | 7 +++++++ priv/graphql/schemas/user/user_schema.gql | 6 +++--- src/mongoose_graphql.erl | 8 ++++++-- .../admin/mongoose_graphql_admin_query.erl | 7 +++++++ src/mongoose_graphql/mongoose_graphql_enum.erl | 11 +++++++++++ .../user/mongoose_graphql_user_auth_info.erl | 16 ++++++++++++++++ .../user/mongoose_graphql_user_query.erl | 7 +++++++ 9 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 priv/graphql/schemas/global/auth_status.gql create mode 100644 priv/graphql/schemas/user/user_auth_status.gql create mode 100644 src/mongoose_graphql/mongoose_graphql_enum.erl create mode 100644 src/mongoose_graphql/user/mongoose_graphql_user_auth_info.erl diff --git a/priv/graphql/schemas/admin/admin_schema.gql b/priv/graphql/schemas/admin/admin_schema.gql index aac625079c1..8f20dc8e387 100644 --- a/priv/graphql/schemas/admin/admin_schema.gql +++ b/priv/graphql/schemas/admin/admin_schema.gql @@ -7,11 +7,13 @@ schema{ Contains all admin available queries. Only an authenticated admin can execute these queries. """ -type AdminQuery @protected{ +type AdminQuery{ "Get all enabled domains by hostType" - domainsByHostType(hostType: String!): [String!] + domainsByHostType(hostType: String!): [String!] @protected "Get information about the domain" - domainDetails(domain: String!): Domain + domainDetails(domain: String!): Domain @protected + "Check authorization status" + checkAuth: AuthStatus } """ diff --git a/priv/graphql/schemas/global/auth_status.gql b/priv/graphql/schemas/global/auth_status.gql new file mode 100644 index 00000000000..e7f6bf9ef77 --- /dev/null +++ b/priv/graphql/schemas/global/auth_status.gql @@ -0,0 +1,7 @@ +"All authorization statuses" +enum AuthStatus{ + "Request is authorized" + AUTHORIZED, + "Request is unauthorized" + UNAUTHORIZED +} diff --git a/priv/graphql/schemas/user/user_auth_status.gql b/priv/graphql/schemas/user/user_auth_status.gql new file mode 100644 index 00000000000..0f15e6a9d62 --- /dev/null +++ b/priv/graphql/schemas/user/user_auth_status.gql @@ -0,0 +1,7 @@ +"Inforamtion about user request authorization" +type UserAuthInfo{ + "Authorized as user with name" + username: String + "Authorization status" + authStatus: AuthStatus +} diff --git a/priv/graphql/schemas/user/user_schema.gql b/priv/graphql/schemas/user/user_schema.gql index 1e735c7367e..55102144a1a 100644 --- a/priv/graphql/schemas/user/user_schema.gql +++ b/priv/graphql/schemas/user/user_schema.gql @@ -7,9 +7,9 @@ schema{ Contains all user available queries. Only an authenticated user can execute these queries. """ -type UserQuery @protected{ - "Something to not leave type without fields" - field: String +type UserQuery{ + "Check authorization status" + checkAuth: UserAuthInfo } """ diff --git a/src/mongoose_graphql.erl b/src/mongoose_graphql.erl index c0832736bbb..2e6622afaaa 100644 --- a/src/mongoose_graphql.erl +++ b/src/mongoose_graphql.erl @@ -68,6 +68,7 @@ execute(Ep, #{document := Doc, Coerced = graphql:type_check_params(Ep, FunEnv, OpName, Vars), Ctx2 = Ctx#{params => Coerced, operation_name => OpName, + authorized => AuthStatus, error_module => mongoose_graphql_errors}, {ok, graphql:execute(Ep, Ctx2, Ast2)} catch @@ -120,14 +121,17 @@ admin_mapping_rules() -> 'AdminMutation' => mongoose_graphql_admin_mutation, 'Domain' => mongoose_graphql_domain, default => mongoose_graphql_default}, - interfaces => #{default => mongoose_graphql_default}}. + interfaces => #{default => mongoose_graphql_default}, + enums => #{default => mongoose_graphql_enum}}. user_mapping_rules() -> #{objects => #{ 'UserQuery' => mongoose_graphql_user_query, 'UserMutation' => mongoose_graphql_user_mutation, + 'UserAuthInfo' => mongoose_graphql_user_auth_info, default => mongoose_graphql_default}, - interfaces => #{default => mongoose_graphql_default}}. + interfaces => #{default => mongoose_graphql_default}, + enums => #{default => mongoose_graphql_enum}}. load_multiple_file_schema(Patterns) -> Paths = lists:flatmap(fun(P) -> filelib:wildcard(P) end, Patterns), diff --git a/src/mongoose_graphql/admin/mongoose_graphql_admin_query.erl b/src/mongoose_graphql/admin/mongoose_graphql_admin_query.erl index 5b57eb1ed25..d8fa18d3d61 100644 --- a/src/mongoose_graphql/admin/mongoose_graphql_admin_query.erl +++ b/src/mongoose_graphql/admin/mongoose_graphql_admin_query.erl @@ -17,4 +17,11 @@ execute(_Ctx, _Obj, <<"domainDetails">>, #{<<"domain">> := Domain}) -> enabled = Enabled}}; {error, not_found} -> {error, domain_not_found} + end; +execute(#{authorized := Authorized}, _Obj, <<"checkAuth">>, _Args) -> + case Authorized of + true -> + {ok, 'AUTHORIZED'}; + false -> + {ok, 'UNAUTHORIZED'} end. diff --git a/src/mongoose_graphql/mongoose_graphql_enum.erl b/src/mongoose_graphql/mongoose_graphql_enum.erl new file mode 100644 index 00000000000..feb2c0c0a93 --- /dev/null +++ b/src/mongoose_graphql/mongoose_graphql_enum.erl @@ -0,0 +1,11 @@ +-module(mongoose_graphql_enum). + +-export([input/2, output/2]). + +-ignore_xref([input/2, output/2]). + +input(<<"AuthStatus">>, <<"AUTHORIZED">>) -> {ok, 'AUTHORIZED'}; +input(<<"AuthStatus">>, <<"UNAUTHORIZED">>) -> {ok, 'UNAUTHORIZED'}. + +output(<<"AuthStatus">>, Status) -> + {ok, atom_to_binary(Status, utf8)}. diff --git a/src/mongoose_graphql/user/mongoose_graphql_user_auth_info.erl b/src/mongoose_graphql/user/mongoose_graphql_user_auth_info.erl new file mode 100644 index 00000000000..58c229d1031 --- /dev/null +++ b/src/mongoose_graphql/user/mongoose_graphql_user_auth_info.erl @@ -0,0 +1,16 @@ +-module(mongoose_graphql_user_auth_info). + +-export([execute/4]). + +-ignore_xref([execute/4]). + +execute(#{authorized := Authorized}, user, <<"authStatus">>, _Args) -> + case Authorized of + true -> + {ok, 'AUTHORIZED'}; + false -> + {ok, 'UNAUTHORIZED'} + end; +execute(Ctx, user, <<"username">>, _Args) -> + Username = maps:get(username, Ctx, null), + {ok, Username}. diff --git a/src/mongoose_graphql/user/mongoose_graphql_user_query.erl b/src/mongoose_graphql/user/mongoose_graphql_user_query.erl index 9ec57ee3477..8aa00416d44 100644 --- a/src/mongoose_graphql/user/mongoose_graphql_user_query.erl +++ b/src/mongoose_graphql/user/mongoose_graphql_user_query.erl @@ -1 +1,8 @@ -module(mongoose_graphql_user_query). + +-export([execute/4]). + +-ignore_xref([execute/4]). + +execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> + {ok, user}. From 20ef8f7417ba696e9d733e5180a9f3076553e38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 23 Dec 2021 14:49:06 +0100 Subject: [PATCH 6/8] Test checkAuth in big tests These tests ensure that GraphQL works correctly on the default MongooseIM configuration --- big_tests/tests/graphql_SUITE.erl | 63 ++++++++++++++++++------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/big_tests/tests/graphql_SUITE.erl b/big_tests/tests/graphql_SUITE.erl index a972887a2f5..6f6c8d96004 100644 --- a/big_tests/tests/graphql_SUITE.erl +++ b/big_tests/tests/graphql_SUITE.erl @@ -4,9 +4,15 @@ -include_lib("eunit/include/eunit.hrl"). -compile([export_all, nowarn_export_all]). + -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute/3, get_listener_port/1, get_listener_config/1]). +-define(assertAdminAuth(Auth, Data), assert_auth(atom_to_binary(Auth), Data)). +-define(assertUserAuth(Username, Auth, Data), + assert_auth(#{<<"username">> => Username, + <<"authStatus">> => atom_to_binary(Auth)}, Data)). + suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). @@ -25,9 +31,11 @@ cowboy_handler() -> can_connect_to_user]. user_handler() -> - [auth_user_can_access_protected_types | common_tests()]. + [user_checks_auth, + auth_user_checks_auth | common_tests()]. admin_handler() -> - [auth_admin_can_access_protected_types | common_tests()]. + [admin_checks_auth, + auth_admin_checks_auth | common_tests()]. common_tests() -> [can_load_graphiql]. @@ -65,54 +73,55 @@ end_per_testcase(CaseName, Config) -> escalus:end_per_testcase(CaseName, Config). can_connect_to_admin(_Config) -> - ?assertMatch({{<<"400">>,<<"Bad Request">>}, _}, execute(admin, #{}, undefined)). + ?assertMatch({{<<"400">>, <<"Bad Request">>}, _}, execute(admin, #{}, undefined)). can_connect_to_user(_Config) -> - ?assertMatch({{<<"400">>,<<"Bad Request">>}, _}, execute(user, #{}, undefined)). + ?assertMatch({{<<"400">>, <<"Bad Request">>}, _}, execute(user, #{}, undefined)). can_load_graphiql(Config) -> Ep = ?config(schema_endpoint, Config), {Status, Html} = get_graphiql_website(Ep), - ?assertEqual({<<"200">>,<<"OK">>}, Status), + ?assertEqual({<<"200">>, <<"OK">>}, Status), ?assertNotEqual(nomatch, binary:match(Html, <<"Loading...">>)). -auth_user_can_access_protected_types(Config) -> +user_checks_auth(Config) -> + Ep = ?config(schema_endpoint, Config), + Body = #{query => "{ checkAuth { username authStatus } }"}, + StatusData = execute(Ep, Body, undefined), + ?assertUserAuth(null, 'UNAUTHORIZED', StatusData). + +auth_user_checks_auth(Config) -> escalus:fresh_story( Config, [{alice, 1}], fun(Alice) -> Password = user_password(alice), AliceJID = escalus_client:short_jid(Alice), Ep = ?config(schema_endpoint, Config), - Body = #{query => "{ field }"}, - {Status, Data} = execute(Ep, Body, {AliceJID, Password}), - assert_access_granted(Status, Data) + Body = #{query => "{ checkAuth { username authStatus } }"}, + StatusData = execute(Ep, Body, {AliceJID, Password}), + ?assertUserAuth(AliceJID, 'AUTHORIZED', StatusData) end). -auth_admin_can_access_protected_types(Config) -> +admin_checks_auth(Config) -> + Ep = ?config(schema_endpoint, Config), + Body = #{query => "{ checkAuth }"}, + StatusData = execute(Ep, Body, undefined), + ?assertAdminAuth('UNAUTHORIZED', StatusData). + +auth_admin_checks_auth(Config) -> Ep = ?config(schema_endpoint, Config), Opts = get_listener_opts(Ep), User = proplists:get_value(username, Opts), Password = proplists:get_value(password, Opts), - Body = #{query => "{ field }"}, - {Status, Data} = execute(Ep, Body, {User, Password}), - assert_access_granted(Status, Data). + Body = #{query => "{ checkAuth }"}, + StatusData = execute(Ep, Body, {User, Password}), + ?assertAdminAuth('AUTHORIZED', StatusData). %% Helpers -assert_code(Code, Data) -> - BinCode = atom_to_binary(Code), - ?assertMatch(#{<<"errors">> := [#{<<"extensions">> := #{<<"code">> := BinCode}}]}, Data). - -assert_no_permissions(ExpectedCode, Status, Data) -> - ?assertEqual({<<"401">>,<<"Unauthorized">>}, Status), - assert_code(ExpectedCode, Data). - -assert_access_granted(Status, Data) -> - ?assertEqual({<<"200">>,<<"OK">>}, Status), - % access was granted, an error was returned because valid resolver was not defined - ?assertMatch(#{<<"errors">> := - [#{<<"extensions">> := - #{<<"code">> := <<"resolver_crash">>}}]}, Data). +assert_auth(Auth, {Status, Data}) -> + ?assertEqual({<<"200">>, <<"OK">>}, Status), + ?assertMatch(#{<<"data">> := #{<<"checkAuth">> := Auth}}, Data). user_password(User) -> [{User, Props}] = escalus:get_users([User]), From a215df05d957bbe1ea30d40a8e99d0cf567a6598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 23 Dec 2021 15:24:25 +0100 Subject: [PATCH 7/8] Stop mocking graphql schema in CTLw tests --- big_tests/tests/mongooseimctl_SUITE.erl | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/big_tests/tests/mongooseimctl_SUITE.erl b/big_tests/tests/mongooseimctl_SUITE.erl index 99870dd8e62..50a7f571d43 100644 --- a/big_tests/tests/mongooseimctl_SUITE.erl +++ b/big_tests/tests/mongooseimctl_SUITE.erl @@ -187,8 +187,7 @@ upload_enabled() -> graphql() -> [graphql_wrong_arguments_number, - can_execute_admin_protected_query, - can_execute_admin_unprotected_query, + can_execute_admin_queries_with_permissions, can_handle_execution_error]. suite() -> @@ -249,10 +248,6 @@ init_per_group(upload_without_acl, Config) -> init_per_group(upload_with_acl, Config) -> dynamic_modules:start(host_type(), mod_http_upload, ?MINIO_OPTS(true)), [{with_acl, true} | Config]; -init_per_group(graphql, Config) -> - % reset admin endpoint and load test schema - ok = graphql_helper:load_test_schema(admin, Config), - Config; init_per_group(_GroupName, Config) -> Config. @@ -283,9 +278,6 @@ end_per_group(UploadGroup, Config) when UploadGroup =:= upload_without_acl; UploadGroup =:= upload_with_acl -> dynamic_modules:stop(host_type(), mod_http_upload), Config; -end_per_group(graphql, _Config) -> - % reinit endpoints with original schemas - ok = rpc(mim(), mongoose_graphql, init, []); end_per_group(_GroupName, Config) -> Config. @@ -1112,21 +1104,12 @@ stats_host(Config) -> %% mongoose_graphql tests %%-------------------------------------------------------------------- -can_execute_admin_protected_query(Config) -> - Query = "query { field }", - Res = mongooseimctl("graphql", [Query], Config), - ?assertMatch({_, 0}, Res), - Data = element(1, Res), - % We expect resolver to crash, because valid resolver was not defined. - ?assertNotEqual(nomatch, string:find(Data, "resolver_crash")). - -can_execute_admin_unprotected_query(Config) -> - Query = "mutation { field }", +can_execute_admin_queries_with_permissions(Config) -> + Query = "query { checkAuth }", Res = mongooseimctl("graphql", [Query], Config), ?assertMatch({_, 0}, Res), Data = element(1, Res), - % We expect resolver to crash, because valid resolver was not defined. - ?assertNotEqual(nomatch, string:find(Data, "resolver_crash")). + ?assertNotEqual(nomatch, string:find(Data, "AUTHORIZED")). can_handle_execution_error(Config) -> Query = "{}", From 1ec7a69b66e93597415a4ab924a2c57817f0181f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 23 Dec 2021 15:26:25 +0100 Subject: [PATCH 8/8] Remove default auth header data from GraphiQL --- priv/graphql/wsite/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/graphql/wsite/index.html b/priv/graphql/wsite/index.html index 81c1477a0c3..f61d32f9820 100644 --- a/priv/graphql/wsite/index.html +++ b/priv/graphql/wsite/index.html @@ -130,7 +130,7 @@ onEditQuery: onEditQuery, onEditVariables: onEditVariables, headerEditorEnabled: true, - headers: '{\n "Authorization": "Basic YWRtaW46c2VjcmV0"\n}', + headers: '{}', onEditOperationName: onEditOperationName }), document.getElementById('graphiql')