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: ?assertMatch(ok, assert_match_result(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: ?assertMatch({error, {tls_alert, {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: ?assertMatch(ok, assert_match_result(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: ?assertMatch({error, {tls_alert, {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: ?assertMatch({error, {tls_alert, {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_result({error, {tls_alert, {certificate_required, _}}}) -> 274: ok; 275: assert_match_result({error, connection_closed}) -> 276: ok; 277: assert_match_result(_) -> 278: wrong_pattern. 279: 280: send_request_with_cert(Cert, Key, Port) -> 281: Opts = [{connect_options, [{verify, verify_none}, {certfile, Cert}, {keyfile, Key}]}], 282: {ok, Client} = fusco_cp:start_link({"localhost", Port, true}, Opts, 1), 283: fusco_cp:request(Client, <<"/api/graphql">>, <<"POST">>, headers(), <<>>, 2, 10000). 284: 285: get_listener_port(Config, Listener) -> 286: ListenerConfig = ?config(Listener, Config), 287: maps:get(port, ListenerConfig). 288: 289: generate_certificate_signed(Config) -> 290: CertSpec =#{cn => "signed_cert", signed => ca}, 291: Filenames = ca_certificate_helper:generate_cert(Config, CertSpec, #{}), 292: [{certificate_signed, Filenames} | Config]. 293: 294: generate_certificate_selfsigned(Config) -> 295: CertSpec =#{cn => "selfsigned_cert", signed => self}, 296: Filenames = ca_certificate_helper:generate_cert(Config, CertSpec, #{}), 297: [{certificate_selfsigned, Filenames} | Config]. 298: 299: headers() -> 300: [{<<"Content-Type">>, <<"application/json">>}, 301: {<<"Request-Id">>, rest_helper:random_request_id()}]. 302: 303: tls_config(VerifyMode, Config) -> 304: CACert = filename:join([path_helper:repo_dir(Config), "tools", "ssl", "ca-clients", "cacert.pem"]), 305: #{tls => 306: #{password => [], 307: certfile => "priv/ssl/fake_cert.pem", 308: keyfile => "priv/ssl/fake_key.pem", 309: cacertfile => CACert, 310: verify_mode => VerifyMode}}. 311: 312: add_tls_to_listener(Config, ListenerType, ListenerName, VerifyMode) -> 313: #{node := Node} = mim(), 314: Listener = graphql_helper:get_listener_config(Node, ListenerType), 315: NewListener = maps:merge(Listener, tls_config(VerifyMode, Config)), 316: mongoose_helper:restart_listener(mim(), NewListener), 317: [{ListenerName, Listener} | Config]. 318: 319: restore_listener(ListenerName, Config) -> 320: Listener = ?config(ListenerName, Config), 321: mongoose_helper:restart_listener(mim(), Listener). 322: 323: assert_auth(Auth, {Status, Data}) -> 324: ?assertEqual({<<"200">>, <<"OK">>}, Status), 325: ?assertMatch(#{<<"data">> := #{<<"checkAuth">> := Auth}}, Data). 326: 327: get_graphiql_website(EpName) -> 328: Request = 329: #{port => graphql_helper:get_listener_port(EpName), 330: role => {graphql, atom_to_binary(EpName)}, 331: method => <<"GET">>, 332: headers => [{<<"Accept">>, <<"text/html">>}], 333: return_maps => true, 334: path => "/graphql"}, 335: rest_helper:make_request(Request). 336: 337: maybe_atom_to_bin(null) -> null; 338: maybe_atom_to_bin(X) -> atom_to_binary(X). 339: 340: admin_check_auth_body() -> 341: #{query => "{ checkAuth { domain authType authStatus } }"}. 342: 343: admin_server_get_loglevel_body() -> 344: #{query => "{ server { getLoglevel } }"}. 345: 346: user_check_auth_body() -> 347: #{query => "{ checkAuth { username authStatus } }"}. 348: 349: user_check_auth_multiple() -> 350: #{query => "{ checkAuth { authStatus } server { getLoglevel } }"}.