1: -module(graphql_SUITE).
    2: -include_lib("common_test/include/ct.hrl").
    3: -include_lib("eunit/include/eunit.hrl").
    4: -include_lib("exml/include/exml.hrl").
    5: 
    6: -compile([export_all, nowarn_export_all]).
    7: 
    8: -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]).
    9: -import(graphql_helper, [execute/3, execute_auth/2, execute_user/3,
   10:                          get_value/2, get_bad_request/1]).
   11: 
   12: -define(assertAdminAuth(Domain, Type, Auth, Data),
   13:         assert_auth(#{<<"domain">> => Domain,
   14:                       <<"authStatus">> => atom_to_binary(Auth),
   15:                       <<"authType">> => maybe_atom_to_bin(Type)}, Data)).
   16: -define(assertUserAuth(Username, Auth, Data),
   17:         assert_auth(#{<<"username">> => Username,
   18:                       <<"authStatus">> => atom_to_binary(Auth)}, Data)).
   19: 
   20: suite() ->
   21:     require_rpc_nodes([mim]) ++ escalus:suite().
   22: 
   23: all() ->
   24:     [{group, cowboy_handler},
   25:      {group, admin_handler},
   26:      {group, domain_admin_handler},
   27:      {group, user_handler},
   28:      {group, tls_enabled},
   29:      {group, categories_disabled}].
   30: 
   31: groups() ->
   32:     [{cowboy_handler, [parallel], cowboy_handler()},
   33:      {user_handler, [parallel], user_handler()},
   34:      {domain_admin_handler, [parallel], domain_admin_handler()},
   35:      {admin_handler, [parallel], admin_handler()},
   36:      {tls_enabled, [parallel], tls_enabled()},
   37:      {categories_disabled, [parallel], categories_disabled_tests()}].
   38: 
   39: cowboy_handler() ->
   40:     [can_connect_to_admin,
   41:      can_connect_to_domain_admin,
   42:      can_connect_to_user].
   43: 
   44: user_handler() ->
   45:     [user_checks_auth,
   46:      auth_user_checks_auth | common_tests()].
   47: admin_handler() ->
   48:     [admin_checks_auth,
   49:      auth_admin_checks_auth | common_tests()].
   50: domain_admin_handler() ->
   51:     [domain_admin_checks_auth,
   52:      auth_domain_admin_checks_auth | common_tests()].
   53: 
   54: common_tests() ->
   55:     [can_load_graphiql].
   56: 
   57: tls_enabled() ->
   58:     [tls_connect_domain_admin_no_certificate,
   59:      tls_connect_user_no_certificate,
   60:      tls_connect_user_unknown_certificate,
   61:      tls_connect_user_selfsigned_certificate,
   62:      tls_connect_user_signed_certificate,
   63:      tls_connect_admin_no_certificate,
   64:      tls_connect_admin_unknown_certificate,
   65:      tls_connect_admin_selfsigned_certificate,
   66:      tls_connect_admin_signed_certificate].
   67: 
   68: categories_disabled_tests() ->
   69:     [category_disabled_error_test,
   70:      admin_checks_auth,
   71:      category_does_not_exist_error,
   72:      listener_reply_with_validation_error,
   73:      multiple_categories_query_test].
   74: 
   75: init_per_suite(Config) ->
   76:     Config1 = escalus:init_per_suite(Config),
   77:     dynamic_modules:save_modules(domain_helper:host_type(), Config1).
   78: 
   79: end_per_suite(Config) ->
   80:     dynamic_modules:restore_modules(Config),
   81:     escalus_fresh:clean(),
   82:     escalus:end_per_suite(Config).
   83: 
   84: init_per_group(admin_handler, Config) ->
   85:     graphql_helper:init_admin_handler(Config);
   86: init_per_group(domain_admin_handler, Config) ->
   87:     case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of
   88:         true ->
   89:             graphql_helper:init_domain_admin_handler(Config);
   90:         false ->
   91:             {skip, require_rdbms}
   92:     end;
   93: init_per_group(user_handler, Config) ->
   94:     Config1 = escalus:create_users(Config, escalus:get_users([alice])),
   95:     [{schema_endpoint, user} | Config1];
   96: init_per_group(categories_disabled, Config) ->
   97:     #{node := Node} = mim(),
   98:     CowboyGraphqlListenerConfig = graphql_helper:get_listener_config(Node, admin),
   99:     #{handlers := [SchemaConfig]} = CowboyGraphqlListenerConfig,
  100:     UpdatedSchemaConfig = maps:put(allowed_categories, [<<"vcard">>, <<"checkAuth">>], SchemaConfig),
  101:     UpdatedListenerConfig = maps:put(handlers, [UpdatedSchemaConfig], CowboyGraphqlListenerConfig),
  102:     mongoose_helper:restart_listener(mim(), UpdatedListenerConfig),
  103:     Config1 = [{admin_listener_config, CowboyGraphqlListenerConfig} | Config],
  104:     graphql_helper:init_admin_handler(Config1);
  105: init_per_group(tls_enabled, Config) ->
  106:     Config0 = add_tls_to_listener(Config, admin, admin_listener_config, peer),
  107:     Config1 = add_tls_to_listener(Config0, domain_admin, domain_admin_listener_config, none),
  108:     Config2 = add_tls_to_listener(Config1, user, user_listener_config, selfsigned_peer),
  109:     Config3 = generate_certificate_signed(Config2),
  110:     generate_certificate_selfsigned(Config3);
  111: init_per_group(cowboy_handler, Config) ->
  112:     Config.
  113: 
  114: end_per_group(user_handler, Config) ->
  115:     escalus:delete_users(Config, escalus:get_users([alice]));
  116: end_per_group(domain_admin_handler, Config) ->
  117:     graphql_helper:end_domain_admin_handler(Config);
  118: end_per_group(categories_disabled, Config) ->
  119:     ListenerConfig = ?config(admin_listener_config, Config),
  120:     mongoose_helper:restart_listener(mim(), ListenerConfig),
  121:     Config;
  122: end_per_group(tls_enabled, Config) ->
  123:     restore_listener(admin_listener_config, Config),
  124:     restore_listener(domain_admin_listener_config, Config),
  125:     restore_listener(user_listener_config, Config);
  126: end_per_group(_, _Config) ->
  127:     ok.
  128: 
  129: init_per_testcase(CaseName, Config) ->
  130:     escalus:init_per_testcase(CaseName, Config).
  131: 
  132: end_per_testcase(CaseName, Config) ->
  133:     escalus:end_per_testcase(CaseName, Config).
  134: 
  135: can_connect_to_admin(_Config) ->
  136:     ?assertMatch({{<<"400">>, <<"Bad Request">>}, _}, execute(admin, #{}, undefined)).
  137: 
  138: can_connect_to_domain_admin(_Config) ->
  139:     ?assertMatch({{<<"400">>, <<"Bad Request">>}, _}, execute(domain_admin, #{}, undefined)).
  140: 
  141: can_connect_to_user(_Config) ->
  142:     ?assertMatch({{<<"400">>, <<"Bad Request">>}, _}, execute(user, #{}, undefined)).
  143: 
  144: can_load_graphiql(Config) ->
  145:     Ep = ?config(schema_endpoint, Config),
  146:     {Status, Html} = get_graphiql_website(Ep),
  147:     ?assertEqual({<<"200">>, <<"OK">>}, Status),
  148:     ?assertNotEqual(nomatch, binary:match(Html, <<"Loading...">>)).
  149: 
  150: user_checks_auth(Config) ->
  151:     Ep = ?config(schema_endpoint, Config),
  152:     StatusData = execute(Ep, user_check_auth_body(), undefined),
  153:     ?assertUserAuth(null, 'UNAUTHORIZED', StatusData).
  154: 
  155: auth_user_checks_auth(Config) ->
  156:     escalus:fresh_story(
  157:         Config, [{alice, 1}], fun(Alice) ->
  158:             AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)),
  159:             StatusData = execute_user(user_check_auth_body(), Alice, Config),
  160:             ?assertUserAuth(AliceJID, 'AUTHORIZED', StatusData)
  161:         end).
  162: 
  163: admin_checks_auth(Config) ->
  164:     Ep = ?config(schema_endpoint, Config),
  165:     StatusData = execute(Ep, admin_check_auth_body(), undefined),
  166:     ?assertAdminAuth(null, null, 'UNAUTHORIZED', StatusData).
  167: 
  168: auth_admin_checks_auth(Config) ->
  169:     StatusData = execute_auth(admin_check_auth_body(), Config),
  170:     ?assertAdminAuth(null, 'ADMIN', 'AUTHORIZED', StatusData).
  171: 
  172: domain_admin_checks_auth(Config) ->
  173:     Ep = ?config(schema_endpoint, Config),
  174:     Res = execute(Ep, admin_check_auth_body(), undefined),
  175:     ?assertAdminAuth(null, null, 'UNAUTHORIZED', Res).
  176: 
  177: auth_domain_admin_checks_auth(Config) ->
  178:     {Username, _} = ?config(domain_admin, Config),
  179:     Domain = escalus_utils:get_server(Username),
  180:     Res = execute_auth(admin_check_auth_body(), Config),
  181:     ?assertAdminAuth(Domain, 'DOMAIN_ADMIN', 'AUTHORIZED', Res).
  182: 
  183: category_disabled_error_test(Config) ->
  184:     Status = execute_auth(admin_server_get_loglevel_body(), Config),
  185:     {_Code, #{<<"errors">> := [Msg]}} = Status,
  186:     ?assertEqual(<<"category_disabled">>, get_value([extensions, code], Msg)),
  187:     ?assertEqual([<<"server">>], get_value([path], Msg)).
  188: 
  189: category_does_not_exist_error(Config) ->
  190:     Ep = ?config(schema_endpoint, Config),
  191:     Status = execute(Ep, #{<<"query">> => <<"{ field ">>}, undefined),
  192:     get_bad_request(Status),
  193:     {_Code, #{<<"errors">> := [Msg]}} = Status,
  194:     ?assertEqual(<<"parser_error">>, get_value([extensions, code], Msg)).
  195: 
  196: listener_reply_with_validation_error(Config) ->
  197:     Ep = ?config(schema_endpoint, Config),
  198:     Body = #{<<"query">> => <<"query Q1 { field } query Q1 { field }">>,
  199:              <<"operationName">> => <<"Q1">>},
  200:     {Status, Data} = execute(Ep, Body, undefined).
  201: 
  202: multiple_categories_query_test(Config) ->
  203:     Status = execute_auth(user_check_auth_multiple(), Config),
  204:     {_Code, #{<<"errors">> := [ErrorMsg], <<"data">> := DataMsg}} = Status,
  205:     ?assertEqual(<<"category_disabled">>, get_value([extensions, code], ErrorMsg)),
  206:     ?assertEqual([<<"server">>], get_value([path], ErrorMsg)),
  207:     ?assertEqual(<<"AUTHORIZED">>, get_value([checkAuth, authStatus], DataMsg)).
  208: 
  209: tls_connect_domain_admin_no_certificate(Config) ->
  210:     Opts = [{connect_options, [{verify, verify_none}]}],
  211:     Port = get_listener_port(Config, domain_admin_listener_config),
  212:     {ok, Client} = fusco_cp:start_link({"localhost", Port, true}, Opts, 1),
  213:     Result = fusco_cp:request(Client, <<"/api/graphql">>, <<"POST">>, headers(), <<>>, 2, 10000),
  214:     fusco_cp:stop(Client),
  215:     ?assertMatch({ok, {{<<"400">>, <<"Bad Request">>}, _, _, _, _}}, Result).
  216: 
  217: tls_connect_user_no_certificate(Config) ->
  218:     Opts = [{connect_options, [{verify, verify_none}]}],
  219:     Port = get_listener_port(Config, user_listener_config),
  220:     {ok, Client} = fusco_cp:start_link({"localhost", Port, true}, Opts, 1),
  221:     Result = fusco_cp:request(Client, <<"/api/graphql">>, <<"POST">>, headers(), <<>>, 2, 10000),
  222:     assert_match_error_result(certificate_required, Result).
  223: 
  224: tls_connect_user_unknown_certificate(Config) ->
  225:     Cert = filename:join([path_helper:repo_dir(Config), "tools", "ssl", "mongooseim", "cert.pem"]),
  226:     Key = filename:join([path_helper:repo_dir(Config), "tools", "ssl", "mongooseim", "key.pem"]),
  227:     Result = send_request_with_cert(Cert, Key, get_listener_port(Config, user_listener_config)),
  228:     assert_match_error_result(unknown_ca, Result).
  229: 
  230: tls_connect_user_selfsigned_certificate(Config) ->
  231:     Cert = maps:get(cert, ?config(certificate_selfsigned, Config)),
  232:     Key = maps:get(key, ?config(certificate_selfsigned, Config)),
  233:     Result = send_request_with_cert(Cert, Key, get_listener_port(Config, user_listener_config)),
  234:     ?assertMatch({ok, {{<<"400">>, <<"Bad Request">>}, _, _, _, _}}, Result).
  235: 
  236: tls_connect_user_signed_certificate(Config) ->
  237:     Cert = maps:get(cert, ?config(certificate_signed, Config)),
  238:     Key = maps:get(key, ?config(certificate_signed, Config)),
  239:     Result = send_request_with_cert(Cert, Key, get_listener_port(Config, user_listener_config)),
  240:     ?assertMatch({ok, {{<<"400">>, <<"Bad Request">>}, _, _, _, _}}, Result).
  241: 
  242: tls_connect_admin_no_certificate(Config) ->
  243:     Opts = [{connect_options, [{verify, verify_none}]}],
  244:     Port = get_listener_port(Config, admin_listener_config),
  245:     {ok, Client} = fusco_cp:start_link({"localhost", Port, true}, Opts, 1),
  246:     Result = fusco_cp:request(Client, <<"/api/graphql">>, <<"POST">>, headers(), <<>>, 2, 10000),
  247:     assert_match_error_result(certificate_required, Result).
  248: 
  249: tls_connect_admin_unknown_certificate(Config) ->
  250:     Cert = filename:join([path_helper:repo_dir(Config), "tools", "ssl", "mongooseim", "cert.pem"]),
  251:     Key = filename:join([path_helper:repo_dir(Config), "tools", "ssl", "mongooseim", "key.pem"]),
  252:     Result = send_request_with_cert(Cert, Key, get_listener_port(Config, admin_listener_config)),
  253:     assert_match_error_result(unknown_ca, Result).
  254: 
  255: tls_connect_admin_selfsigned_certificate(Config) ->
  256:     Cert = maps:get(cert, ?config(certificate_selfsigned, Config)),
  257:     Key = maps:get(key, ?config(certificate_selfsigned, Config)),
  258:     Result = send_request_with_cert(Cert, Key, get_listener_port(Config, admin_listener_config)),
  259:     assert_match_error_result(bad_certificate, Result).
  260: 
  261: tls_connect_admin_signed_certificate(Config) ->
  262:     Cert = maps:get(cert, ?config(certificate_signed, Config)),
  263:     Key = maps:get(key, ?config(certificate_signed, Config)),
  264:     Result = send_request_with_cert(Cert, Key, get_listener_port(Config, admin_listener_config)),
  265:     ?assertMatch({ok, {{<<"400">>, <<"Bad Request">>}, _, _, _, _}}, Result).
  266: 
  267: %% Helpers
  268: 
  269: % The proper error should be the first one, {error, {tls_alert, {certificate_required, _}}}.
  270: % Sometimes for unknown reasons, the result is {error, connection_closed}. This test is important
  271: % to check if the server does not allow the connection when the certificate is not attached.
  272: %  Therefore, to prevent the creation of a flaky test, the function below was created.
  273: assert_match_error_result(_, {error, connection_closed}) ->
  274:     ok;
  275: assert_match_error_result(AssertedError, Error) ->
  276:     ?assertMatch({error, {tls_alert, {AssertedError, _}}}, Error).
  277: 
  278: send_request_with_cert(Cert, Key, Port) ->
  279:     Opts = [{connect_options, [{verify, verify_none}, {certfile, Cert}, {keyfile, Key}]}],
  280:     {ok, Client} = fusco_cp:start_link({"localhost", Port, true}, Opts, 1),
  281:     fusco_cp:request(Client, <<"/api/graphql">>, <<"POST">>, headers(), <<>>, 2, 10000).
  282: 
  283: get_listener_port(Config, Listener) ->
  284:     ListenerConfig = ?config(Listener, Config),
  285:     maps:get(port, ListenerConfig).
  286: 
  287: generate_certificate_signed(Config) ->
  288:     CertSpec =#{cn => "signed_cert", signed => ca}, 
  289:     Filenames = ca_certificate_helper:generate_cert(Config, CertSpec, #{}),
  290:     [{certificate_signed, Filenames} | Config].
  291: 
  292: generate_certificate_selfsigned(Config) ->
  293:     CertSpec =#{cn => "selfsigned_cert", signed => self}, 
  294:     Filenames = ca_certificate_helper:generate_cert(Config, CertSpec, #{}),
  295:     [{certificate_selfsigned, Filenames} | Config].
  296: 
  297: headers() ->
  298:     [{<<"Content-Type">>, <<"application/json">>},
  299:      {<<"Request-Id">>, rest_helper:random_request_id()}].
  300: 
  301: tls_config(VerifyMode, Config) ->
  302:     CACert = filename:join([path_helper:repo_dir(Config), "tools", "ssl", "ca-clients", "cacert.pem"]),
  303:     #{tls =>
  304:         #{password => [],
  305:           certfile => "priv/ssl/fake_cert.pem",
  306:           keyfile => "priv/ssl/fake_key.pem",
  307:           cacertfile => CACert,
  308:           verify_mode => VerifyMode}}.
  309: 
  310: add_tls_to_listener(Config, ListenerType, ListenerName, VerifyMode) ->
  311:     #{node := Node} = mim(),
  312:     Listener = graphql_helper:get_listener_config(Node, ListenerType),
  313:     NewListener = maps:merge(Listener, tls_config(VerifyMode, Config)),
  314:     mongoose_helper:restart_listener(mim(), NewListener),
  315:     [{ListenerName, Listener} | Config].
  316: 
  317: restore_listener(ListenerName, Config) ->
  318:     Listener = ?config(ListenerName, Config),
  319:     mongoose_helper:restart_listener(mim(), Listener).
  320: 
  321: assert_auth(Auth, {Status, Data}) ->
  322:     ?assertEqual({<<"200">>, <<"OK">>}, Status),
  323:     ?assertMatch(#{<<"data">> := #{<<"checkAuth">> := Auth}}, Data).
  324: 
  325: get_graphiql_website(EpName) ->
  326:     Request =
  327:       #{port => graphql_helper:get_listener_port(EpName),
  328:         role => {graphql, atom_to_binary(EpName)},
  329:         method => <<"GET">>,
  330:         headers => [{<<"Accept">>, <<"text/html">>}],
  331:         return_maps => true,
  332:         path => "/graphql"},
  333:     rest_helper:make_request(Request).
  334: 
  335: maybe_atom_to_bin(null) -> null;
  336: maybe_atom_to_bin(X) -> atom_to_binary(X).
  337: 
  338: admin_check_auth_body() ->
  339:     #{query => "{ checkAuth { domain authType authStatus } }"}.
  340: 
  341: admin_server_get_loglevel_body() ->
  342:     #{query => "{ server { getLoglevel } }"}.
  343: 
  344: user_check_auth_body() ->
  345:     #{query => "{ checkAuth { username authStatus } }"}.
  346: 
  347: user_check_auth_multiple() ->
  348:     #{query => "{ checkAuth { authStatus } server { getLoglevel } }"}.