1: -module(mongoose_graphql_SUITE). 2: 3: -compile([export_all, nowarn_export_all]). 4: 5: -include_lib("eunit/include/eunit.hrl"). 6: -include_lib("common_test/include/ct.hrl"). 7: -include_lib("graphql/src/graphql_schema.hrl"). 8: -include_lib("jid/include/jid.hrl"). 9: 10: -type listener_opts() :: #{endpoint_schema := binary(), 11: atom() => any()}. 12: 13: -define(assertPermissionsFailed(Config, Doc), 14: ?assertThrow({error, #{error_term := {no_permissions, _}}}, 15: check_permissions(Config, false, Doc))). 16: -define(assertPermissionsSuccess(Config, Doc), 17: ?assertMatch(ok, check_permissions(Config, false, Doc))). 18: 19: -define(assertDomainPermissionsFailed(Config, Domain, Args, Doc), 20: ?assertThrow({error, #{error_term := {no_permissions, _,#{type := domain, 21: invalid_args := Args}}}}, 22: check_domain_permissions(Config, Domain, Doc))). 23: -define(assertPermissionsSuccess(Config, Domain, Doc), 24: ?assertMatch(ok, check_domain_permissions(Config, Domain, Doc))). 25: 26: -define(assertErrMsg(Code, MsgContains, ErrorMsg), 27: assert_err_msg(Code, MsgContains, ErrorMsg)). 28: 29: all() -> 30: [can_create_endpoint, 31: can_load_split_schema, 32: unexpected_internal_error, 33: admin_and_user_load_global_types, 34: {group, unprotected_graphql}, 35: {group, protected_graphql}, 36: {group, error_handling}, 37: {group, error_formatting}, 38: {group, permissions}, 39: {group, domain_permissions}, 40: {group, user_listener}, 41: {group, admin_listener}, 42: {group, domain_admin_listener}]. 43: 44: groups() -> 45: [{protected_graphql, [parallel], protected_graphql()}, 46: {unprotected_graphql, [parallel], unprotected_graphql()}, 47: {error_handling, [parallel], error_handling()}, 48: {error_formatting, [parallel], error_formatting()}, 49: {permissions, [parallel], permissions()}, 50: {domain_permissions, [parallel], domain_permissions()}, 51: {admin_listener, [parallel], admin_listener()}, 52: {domain_admin_listener, [parallel], domain_admin_listener()}, 53: {user_listener, [parallel], user_listener()}]. 54: 55: protected_graphql() -> 56: [auth_can_execute_protected_query, 57: auth_can_execute_protected_mutation, 58: unauth_cannot_execute_protected_query, 59: unauth_cannot_execute_protected_mutation, 60: unauth_can_access_introspection]. 61: 62: unprotected_graphql() -> 63: [can_execute_query_with_vars, 64: auth_can_execute_query, 65: auth_can_execute_mutation, 66: unauth_can_execute_query, 67: unauth_can_execute_mutation]. 68: 69: error_handling() -> 70: [should_catch_parsing_error, 71: should_catch_type_check_params_error, 72: should_catch_type_check_error, 73: should_catch_validation_error]. 74: 75: error_formatting() -> 76: [format_internal_crash, 77: format_parse_errors, 78: format_decode_errors, 79: format_authorize_error, 80: format_validate_error, 81: format_type_check_error, 82: format_execute_error, 83: format_uncategorized_error, 84: format_any_error]. 85: 86: permissions() -> 87: [check_object_permissions, 88: check_field_permissions, 89: check_child_object_permissions, 90: check_child_object_field_permissions, 91: check_fragment_permissions, 92: check_interface_permissions, 93: check_interface_field_permissions, 94: check_inline_fragment_permissions, 95: check_union_permissions 96: ]. 97: 98: domain_permissions() -> 99: [check_field_domain_permissions, 100: check_field_input_arg_domain_permissions, 101: check_field_list_arg_domain_permissions, 102: check_field_null_arg_domain_permissions, 103: check_field_jid_arg_domain_permissions, 104: check_child_object_field_domain_permissions, 105: check_field_subdomain_permissions, 106: check_field_global_permissions, 107: check_interface_field_domain_permissions 108: ]. 109: 110: user_listener() -> 111: [auth_user_can_access_protected_types | common_tests()]. 112: 113: admin_listener() -> 114: [no_creds_defined_admin_can_access_protected, 115: auth_admin_can_access_protected_types | common_tests()]. 116: 117: domain_admin_listener() -> 118: [auth_domain_admin_can_access_protected_types, 119: auth_domain_admin_wrong_password_error, 120: auth_domain_admin_nonexistent_domain_error, 121: auth_domain_admin_cannot_access_other_domain, 122: auth_domain_admin_cannot_access_global, 123: auth_domain_admin_can_access_owned_domain 124: | common_tests()]. 125: 126: common_tests() -> 127: [malformed_auth_header_error, 128: auth_wrong_creds_error, 129: invalid_json_body_error, 130: no_query_supplied_error, 131: variables_invalid_json_error, 132: listener_reply_with_parsing_error, 133: listener_reply_with_type_check_error, 134: listener_reply_with_validation_error, 135: listener_unauth_cannot_access_protected_types, 136: listener_unauth_can_access_unprotected_types, 137: listener_can_execute_query_with_variables]. 138: 139: init_per_suite(Config) -> 140: application:ensure_all_started(cowboy), 141: application:ensure_all_started(jid), 142: Config. 143: 144: end_per_suite(_Config) -> 145: ok. 146: 147: init_per_group(user_listener, Config) -> 148: meck:new(mongoose_api_common, [no_link]), 149: meck:expect(mongoose_api_common, check_password, 150: fun 151: (#jid{luser = <<"alice">>}, <<"makota">>) -> {true, {}}; 152: (_, _) -> false 153: end), 154: ListenerOpts = #{schema_endpoint => <<"user">>}, 155: init_ep_listener(5557, user_schema_ep, ListenerOpts, Config); 156: init_per_group(admin_listener, Config) -> 157: ListenerOpts = #{username => <<"admin">>, 158: password => <<"secret">>, 159: schema_endpoint => <<"admin">>}, 160: init_ep_listener(5558, admin_schema_ep, ListenerOpts, Config); 161: init_per_group(domain_admin_listener, Config) -> 162: meck:new(mongoose_domain_api, [no_link]), 163: meck:expect(mongoose_domain_api, check_domain_password, 164: fun 165: (<<"localhost">>, <<"makota">>) -> ok; 166: (<<"localhost">>, _) -> {error, wrong_password}; 167: (_, _) -> {error, not_found} 168: end), 169: meck:expect(mongoose_domain_api, get_subdomain_info, 170: fun (_) -> {error, not_found} end), 171: ListenerOpts = #{schema_endpoint => <<"domain_admin">>}, 172: init_ep_listener(5560, domain_admin_schema_ep, ListenerOpts, Config); 173: init_per_group(domain_permissions, Config) -> 174: meck:new(mongoose_domain_api, [no_link]), 175: meck:expect(mongoose_domain_api, get_subdomain_info, 176: fun 177: (<<"subdomain.test-domain.com">>) -> 178: {ok, #{parent_domain => <<"test-domain.com">>}}; 179: (<<"subdomain.test-domain2.com">>) -> 180: {ok, #{parent_domain => <<"test-domain2.com">>}}; 181: (_) -> 182: {error, not_found} 183: end), 184: Domains = [{<<"subdomain.test-domain.com">>, <<"test-domain.com">>}, 185: {<<"subdomain.test-domain2.com">>, <<"test-domain2.com">>}], 186: [{domains, Domains} | Config]; 187: init_per_group(_G, Config) -> 188: Config. 189: 190: end_per_group(user_listener, Config) -> 191: meck:unload(mongoose_api_common), 192: ?config(test_process, Config) ! stop, 193: Config; 194: end_per_group(admin_listener, Config) -> 195: ?config(test_process, Config) ! stop, 196: Config; 197: end_per_group(domain_admin_listener, Config) -> 198: meck:unload(mongoose_domain_api), 199: ?config(test_process, Config) ! stop, 200: Config; 201: end_per_group(domain_permissions, _Config) -> 202: meck:unload(mongoose_domain_api); 203: end_per_group(_, Config) -> 204: Config. 205: 206: init_per_testcase(C, Config) when C =:= auth_can_execute_protected_query; 207: C =:= auth_can_execute_protected_mutation; 208: C =:= unauth_cannot_execute_protected_query; 209: C =:= unauth_cannot_execute_protected_mutation; 210: C =:= unauth_can_access_introspection -> 211: {Mapping, Pattern} = example_schema_protected_data(Config), 212: {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]), 213: Ep = mongoose_graphql:get_endpoint(C), 214: [{endpoint, Ep} | Config]; 215: init_per_testcase(C, Config) when C =:= can_execute_query_with_vars; 216: C =:= auth_can_execute_query; 217: C =:= auth_can_execute_mutation; 218: C =:= unauth_can_execute_query; 219: C =:= unauth_can_execute_mutation; 220: C =:= should_catch_type_check_params_error; 221: C =:= should_catch_type_check_error; 222: C =:= should_catch_parsing_error; 223: C =:= should_catch_validation_error -> 224: {Mapping, Pattern} = example_schema_data(Config), 225: {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]), 226: Ep = mongoose_graphql:get_endpoint(C), 227: [{endpoint, Ep} | Config]; 228: init_per_testcase(C, Config) when C =:= check_object_permissions; 229: C =:= check_field_permissions; 230: C =:= check_child_object_permissions; 231: C =:= check_child_object_field_permissions; 232: C =:= check_fragment_permissions; 233: C =:= check_interface_permissions; 234: C =:= check_interface_field_permissions; 235: C =:= check_inline_fragment_permissions; 236: C =:= check_union_permissions; 237: C =:= check_field_domain_permissions; 238: C =:= check_field_input_arg_domain_permissions; 239: C =:= check_field_list_arg_domain_permissions; 240: C =:= check_field_null_arg_domain_permissions; 241: C =:= check_field_jid_arg_domain_permissions; 242: C =:= check_field_subdomain_permissions; 243: C =:= check_field_global_permissions; 244: C =:= check_child_object_field_domain_permissions; 245: C =:= check_interface_field_domain_permissions -> 246: {Mapping, Pattern} = example_permissions_schema_data(Config), 247: {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]), 248: Ep = mongoose_graphql:get_endpoint(C), 249: [{endpoint, Ep} | Config]; 250: init_per_testcase(C, Config) -> 251: [{endpoint_name, C} | Config]. 252: 253: end_per_testcase(_, _Config) -> 254: ok. 255: 256: can_create_endpoint(Config) -> 257: Name = ?config(endpoint_name, Config), 258: {Mapping, Pattern} = example_schema_protected_data(Config), 259: {ok, Pid} = mongoose_graphql:create_endpoint(Name, Mapping, [Pattern]), 260: 261: Ep = mongoose_graphql:get_endpoint(Name), 262: ?assertMatch({endpoint_context, Name, Pid, _, _}, Ep), 263: ?assertMatch(#root_schema{id = 'ROOT', query = <<"UserQuery">>, 264: mutation = <<"UserMutation">>}, 265: graphql_schema:get(Ep, 'ROOT')). 266: 267: can_load_split_schema(Config) -> 268: Name = ?config(endpoint_name, Config), 269: {Mapping, Pattern} = example_split_schema_data(Config), 270: {ok, Pid} = mongoose_graphql:create_endpoint(Name, Mapping, [Pattern]), 271: 272: Ep = mongoose_graphql:get_endpoint(Name), 273: ?assertMatch({endpoint_context, Name, Pid, _, _}, Ep), 274: ?assertMatch(#root_schema{id = 'ROOT', query = <<"Query">>, 275: mutation = <<"Mutation">>}, 276: graphql_schema:get(Ep, 'ROOT')), 277: ?assertMatch(#object_type{id = <<"Query">>}, graphql_schema:get(Ep, <<"Query">>)), 278: ?assertMatch(#object_type{id = <<"Mutation">>}, graphql_schema:get(Ep, <<"Mutation">>)). 279: 280: unexpected_internal_error(Config) -> 281: Name = ?config(endpoint_name, Config), 282: Doc = <<"mutation { field }">>, 283: Res = mongoose_graphql:execute(Name, undefined, Doc), 284: ?assertEqual({error, internal_crash}, Res). 285: 286: admin_and_user_load_global_types(_Config) -> 287: mongoose_graphql:init(), 288: AdminEp = mongoose_graphql:get_endpoint(admin), 289: ?assertMatch(#scalar_type{id = <<"JID">>}, graphql_schema:get(AdminEp, <<"JID">>)), 290: ?assertMatch(#directive_type{id = <<"protected">>}, 291: graphql_schema:get(AdminEp, <<"protected">>)), 292: 293: UserEp = mongoose_graphql:get_endpoint(user), 294: ?assertMatch(#scalar_type{id = <<"JID">>}, graphql_schema:get(UserEp, <<"JID">>)), 295: ?assertMatch(#directive_type{id = <<"protected">>}, 296: graphql_schema:get(UserEp, <<"protected">>)). 297: 298: %% Protected graphql 299: 300: auth_can_execute_protected_query(Config) -> 301: Ep = ?config(endpoint, Config), 302: Doc = <<"{ field }">>, 303: Res = mongoose_graphql:execute(Ep, undefined, Doc), 304: ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res). 305: 306: auth_can_execute_protected_mutation(Config) -> 307: Ep = ?config(endpoint, Config), 308: Doc = <<"mutation { field }">>, 309: Res = mongoose_graphql:execute(Ep, undefined, Doc), 310: ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res). 311: 312: unauth_cannot_execute_protected_query(Config) -> 313: Ep = ?config(endpoint, Config), 314: Doc = <<"query Q1 { field }">>, 315: Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)), 316: ?assertMatch({error, #{error_term := {no_permissions, <<"Q1">>}, path := [<<"Q1">>]}}, Res). 317: 318: unauth_cannot_execute_protected_mutation(Config) -> 319: Ep = ?config(endpoint, Config), 320: Doc = <<"mutation { field }">>, 321: Res = mongoose_graphql:execute(Ep, request(Doc, false)), 322: ?assertMatch({error, #{error_term := {no_permissions, <<"ROOT">>}}}, Res). 323: 324: unauth_can_access_introspection(Config) -> 325: Ep = ?config(endpoint, Config), 326: Doc = <<"{ __schema { queryType { name } } __type(name: \"UserQuery\") { name } }">>, 327: Res = mongoose_graphql:execute(Ep, request(Doc, false)), 328: Expected = 329: {ok, 330: #{data => 331: #{<<"__schema">> => 332: #{<<"queryType">> => 333: #{<<"name">> => <<"UserQuery">>} 334: }, 335: <<"__type">> => 336: #{<<"name">> => 337: <<"UserQuery">> 338: } 339: } 340: } 341: }, 342: ?assertEqual(Expected, Res). 343: 344: %% Unprotected graphql 345: 346: can_execute_query_with_vars(Config) -> 347: Ep = ?config(endpoint, Config), 348: Doc = <<"query Q1($value: String!) { id(value: $value)}">>, 349: Req = 350: #{document => Doc, 351: operation_name => <<"Q1">>, 352: vars => #{<<"value">> => <<"Hello">>}, 353: authorized => false, 354: ctx => #{}}, 355: Res = mongoose_graphql:execute(Ep, Req), 356: ?assertEqual({ok, #{data => #{<<"id">> => <<"Hello">>}}}, Res). 357: 358: unauth_can_execute_query(Config) -> 359: Ep = ?config(endpoint, Config), 360: Doc = <<"query { field }">>, 361: Res = mongoose_graphql:execute(Ep, request(Doc, false)), 362: ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res). 363: 364: unauth_can_execute_mutation(Config) -> 365: Ep = ?config(endpoint, Config), 366: Doc = <<"mutation { field }">>, 367: Res = mongoose_graphql:execute(Ep, request(Doc, false)), 368: ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res). 369: 370: auth_can_execute_query(Config) -> 371: Ep = ?config(endpoint, Config), 372: Doc = <<"query { field }">>, 373: Res = mongoose_graphql:execute(Ep, request(Doc, true)), 374: ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res). 375: 376: auth_can_execute_mutation(Config) -> 377: Ep = ?config(endpoint, Config), 378: Doc = <<"mutation { field }">>, 379: Res = mongoose_graphql:execute(Ep, request(Doc, true)), 380: ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res). 381: 382: %% Error handling 383: 384: should_catch_parsing_error(Config) -> 385: Ep = ?config(endpoint, Config), 386: Doc = <<"query { field ">>, 387: DocScan = <<"query { id(value: \"ala) }">>, 388: ResParseErr = mongoose_graphql:execute(Ep, request(Doc, false)), 389: ?assertMatch({error, #{phase := parse, error_term := {parser_error, _}}}, ResParseErr), 390: ResScanErr = mongoose_graphql:execute(Ep, request(DocScan, false)), 391: ?assertMatch({error, #{phase := parse, error_term := {scanner_error, _}}}, ResScanErr). 392: 393: should_catch_type_check_error(Config) -> 394: Ep = ?config(endpoint, Config), 395: Doc = <<"query { notExistingField(value: \"Hello\") }">>, 396: Res = mongoose_graphql:execute(Ep, request(Doc, false)), 397: ?assertMatch({error, #{phase := type_check, error_term := unknown_field}}, Res). 398: 399: should_catch_type_check_params_error(Config) -> 400: Ep = ?config(endpoint, Config), 401: Doc = <<"query { id(value: 12) }">>, 402: Res = mongoose_graphql:execute(Ep, request(Doc, false)), 403: ?assertMatch({error, #{phase := type_check, error_term := {input_coercion, _, _, _}}}, Res). 404: 405: should_catch_validation_error(Config) -> 406: Ep = ?config(endpoint, Config), 407: Doc = <<"query Q1{ id(value: \"ok\") } query Q1{ id(value: \"ok\") }">>, 408: % Query name must be unique 409: Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)), 410: ?assertMatch({error, #{phase := validate, error_term := {not_unique, _}}}, Res). 411: 412: %% Permissions 413: 414: check_object_permissions(Config) -> 415: Doc = <<"query { field }">>, 416: FDoc = <<"mutation { field }">>, 417: ?assertPermissionsSuccess(Config, Doc), 418: ?assertPermissionsFailed(Config, FDoc). 419: 420: check_field_permissions(Config) -> 421: Doc = <<"{ field protectedField }">>, 422: ?assertPermissionsFailed(Config, Doc). 423: 424: check_child_object_permissions(Config) -> 425: Doc = <<"{ protectedObj{ type } }">>, 426: ?assertPermissionsFailed(Config, Doc). 427: 428: check_child_object_field_permissions(Config) -> 429: Doc = <<"{ obj { field } }">>, 430: FDoc = <<"{ obj { field protectedField } }">>, 431: ?assertPermissionsSuccess(Config, Doc), 432: ?assertPermissionsFailed(Config, FDoc). 433: 434: check_fragment_permissions(Config) -> 435: Config2 = [{op, <<"Q1">>} | Config], 436: Doc = <<"query Q1{ obj { ...body } } fragment body on Object { name field }">>, 437: FDoc = <<"query Q1{ obj { ...body } } fragment body on Object { name field protectedField }">>, 438: ?assertPermissionsSuccess(Config2, Doc), 439: ?assertPermissionsFailed(Config2, FDoc). 440: 441: check_interface_permissions(Config) -> 442: Doc = <<"{ interface { name } }">>, 443: FDoc = <<"{ protInterface { name } }">>, 444: ?assertPermissionsSuccess(Config, Doc), 445: ?assertPermissionsFailed(Config, FDoc). 446: 447: check_interface_field_permissions(Config) -> 448: Doc = <<"{ interface { protectedName } }">>, 449: FieldProtectedNotEnough = <<"{ obj { protectedName } }">>, 450: FieldProtectedEnough = <<"{ obj { otherName } }">>, 451: % Field is protected in interface and object, so it cannot be accessed. 452: ?assertPermissionsFailed(Config, Doc), 453: ?assertPermissionsFailed(Config, FieldProtectedEnough), 454: % Field is protected only in an interface, so it can be accessed from implementing objects. 455: ?assertPermissionsSuccess(Config, FieldProtectedNotEnough). 456: 457: check_inline_fragment_permissions(Config) -> 458: Doc = <<"{ interface { name otherName ... on Object { field } } }">>, 459: FDoc = <<"{ interface { name otherName ... on Object { field protectedField } } }">>, 460: FDoc2 = <<"{ interface { name ... on Object { field otherName} } }">>, 461: ?assertPermissionsSuccess(Config, Doc), 462: ?assertPermissionsFailed(Config, FDoc), 463: ?assertPermissionsFailed(Config, FDoc2). 464: 465: check_union_permissions(Config) -> 466: Doc = <<"{ union { ... on O1 { field1 } } }">>, 467: FDoc = <<"{ union { ... on O1 { field1 field1Protected } } }">>, 468: FDoc2 = <<"{ union { ... on O1 { field1 } ... on O2 { field2 } } }">>, 469: ?assertPermissionsSuccess(Config, Doc), 470: ?assertPermissionsFailed(Config, FDoc), 471: ?assertPermissionsFailed(Config, FDoc2). 472: 473: %% Domain permissions 474: 475: check_field_domain_permissions(Config) -> 476: Domain = <<"my-domain.com">>, 477: Config2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => Domain}} | Config], 478: Doc = <<"{ field protectedField }">>, 479: Doc2 = <<"query Q1($domain: String) { protectedField domainProtectedField(argA: $domain" 480: ", argB: \"domain\") }">>, 481: FDoc = <<"{protectedField domainProtectedField(argA: \"domain.com\"," 482: " argB: \"domain.com\") }">>, 483: ?assertPermissionsSuccess(Config, Domain, Doc), 484: ?assertPermissionsSuccess(Config2, Domain, Doc2), 485: ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>], FDoc). 486: 487: check_child_object_field_domain_permissions(Config) -> 488: Domain = <<"my-domain.com">>, 489: Config2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => Domain}} | Config], 490: Doc = <<"{ obj { field protectedField } }">>, 491: Doc2 = <<"query Q1($domain: String) { obj { protectedField domainProtectedField(argA: $domain" 492: ", argB: \"domain\") } }">>, 493: FDoc = <<"{ obj {protectedField domainProtectedField(argA: \"domain.com\"," 494: " argB: \"domain.com\") } }">>, 495: ?assertPermissionsSuccess(Config, Domain, Doc), 496: ?assertPermissionsSuccess(Config2, Domain, Doc2), 497: ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>], FDoc). 498: 499: check_interface_field_domain_permissions(Config) -> 500: Domain = <<"my-domain.com">>, 501: OkDomain = <<"{ interface { protectedDomainName(domain: \"my-domain.com\") } }">>, 502: OkDomain1 = <<"{ obj { protectedDomainName(domain: \"my-domain.com\") } }">>, 503: OkDomain2 = <<"{ obj { domainName(domain: \"my-domain.com\") } }">>, 504: WrongDomain = <<"{ interface { protectedDomainName(domain: \"domain.com\") } }">>, 505: WrongDomain1 = <<"{ obj { domainName(domain: \"domain.com\") } }">>, 506: ProtectedNotEnough = <<"{ obj { protectedDomainName(domain: \"domain.com\") } }">>, 507: ?assertPermissionsSuccess(Config, Domain, OkDomain), 508: ?assertPermissionsSuccess(Config, Domain, OkDomain1), 509: ?assertPermissionsSuccess(Config, Domain, OkDomain2), 510: % Field is protected in interface and object, so it cannot be accessed with the wrong domain. 511: ?assertDomainPermissionsFailed(Config, Domain, [<<"domain">>], WrongDomain), 512: ?assertDomainPermissionsFailed(Config, Domain, [<<"domain">>], WrongDomain1), 513: % Field is protected only in an interface, so it can be accessed from implementing objects 514: % with the wrong domain. 515: ?assertPermissionsSuccess(Config, Domain, ProtectedNotEnough). 516: 517: check_field_input_arg_domain_permissions(Config) -> 518: Domain = <<"my-domain.com">>, 519: DomainInput = #{<<"domain">> => Domain, <<"notDomain">> => <<"random text here">>}, 520: Config2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => Domain, 521: <<"domainInput">> => DomainInput}} | Config], 522: Doc = <<"query Q1($domain: String, $domainInput: DomainInput!) " 523: "{ domainInputProtectedField(argA: $domain, argB: $domainInput)" 524: " domainProtectedField(argA: $domain, argB: \"domain.com\") }">>, 525: 526: FDoc = <<"{ domainInputProtectedField(argA: \"do.com\", argB: { domain: \"do.com\" }) }">>, 527: ?assertPermissionsSuccess(Config2, Domain, Doc), 528: ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>, <<"argB.domain">>], FDoc). 529: 530: 531: check_field_list_arg_domain_permissions(Config) -> 532: [{Subdomain, Domain} | _] = ?config(domains, Config), 533: Domains = [#{<<"domain">> => Domain, <<"notDomain">> => <<"random text here">>}, 534: #{<<"domain">> => Subdomain}], 535: Config2 = [{op, <<"Q1">>}, {args, #{<<"domains">> => Domains}} | Config], 536: Doc = <<"query Q1($domains: [DomainInput!]) " 537: "{ domainListInputProtectedField(domains: $domains) }">>, 538: 539: FDoc = <<"{ domainListInputProtectedField(domains: [{ domain: \"do.com\" }]) }">>, 540: ?assertPermissionsSuccess(Config2, Domain, Doc), 541: ?assertDomainPermissionsFailed(Config, Domain, [<<"domains.domain">>], FDoc). 542: 543: check_field_null_arg_domain_permissions(Config) -> 544: [{_, Domain} | _] = ?config(domains, Config), 545: Doc = <<"{ domainProtectedField domainInputProtectedField }">>, 546: ?assertPermissionsSuccess(Config, Domain, Doc). 547: 548: check_field_jid_arg_domain_permissions(Config) -> 549: Domain = <<"my-domain.com">>, 550: Config2 = [{op, <<"Q1">>}, 551: {args, #{<<"jid">> => <<"bob@", Domain/binary>>}} | Config], 552: Doc = <<"query Q1($jid: JID) { domainJIDProtectedField(argA: $jid, argB: \"bob@bob\") }">>, 553: FDoc = <<"{ domainJIDProtectedField(argA: \"bob@do.com\", argB: \"bob@do.com\") }">>, 554: ?assertPermissionsSuccess(Config2, Domain, Doc), 555: ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>], FDoc). 556: 557: check_field_subdomain_permissions(Config) -> 558: [{Subdomain, Domain}, {FSubdomain, _Domain2}] = ?config(domains, Config), 559: Config2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => Subdomain}} | Config], 560: FConfig2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => FSubdomain}} | Config], 561: Doc = <<"query Q1($domain: String) " 562: "{ protectedField domainProtectedField(argA: $domain, argB: \"do.com\") }">>, 563: ?assertPermissionsSuccess(Config2, Domain, Doc), 564: ?assertDomainPermissionsFailed(FConfig2, Domain, [<<"argA">>], Doc). 565: 566: check_field_global_permissions(Config) -> 567: Domain = <<"my-domain.com">>, 568: Doc = <<"{ protectedField onlyForGlobalAdmin }">>, 569: ?assertMatch(ok, check_permissions(Config, true, Doc)), 570: ?assertThrow({error, #{error_term := {no_permissions, _, #{type := global}}}}, 571: check_domain_permissions(Config, Domain, Doc)). 572: 573: %% Error formatting 574: 575: format_internal_crash(_Config) -> 576: {Code, Res} = mongoose_graphql_errors:format_error(internal_crash), 577: ?assertEqual(500, Code), 578: ?assertMatch(#{extensions := #{code := internal_server_error}}, Res). 579: 580: format_parse_errors(_Config) -> 581: ParserError = make_error(parse, {parser_error, {0, graphql_parser, "parser_error_msg"}}), 582: ScannerError = make_error(parse, {scanner_error, 583: {0, graphql_scanner, {illegal, "illegal_characters"}}}), 584: ScannerError2 = make_error(parse, {scanner_error, 585: {0, graphql_scanner, {user, "user_scanner_err"}}}), 586: 587: {400, ResParser} = mongoose_graphql_errors:format_error(ParserError), 588: {400, ResScanner} = mongoose_graphql_errors:format_error(ScannerError), 589: {400, ResScanner2} = mongoose_graphql_errors:format_error(ScannerError2), 590: ?assertErrMsg(parser_error, <<"parser_error_msg">>, ResParser), 591: ?assertErrMsg(scanner_error, <<"illegal_characters">>, ResScanner), 592: ?assertErrMsg(scanner_error, <<"user_scanner_err">>, ResScanner2). 593: 594: format_decode_errors(_Config) -> 595: {400, Msg1} = mongoose_graphql_errors:format_error(make_error(decode, no_query_supplied)), 596: {400, Msg2} = mongoose_graphql_errors:format_error(make_error(decode, invalid_json_body)), 597: {400, Msg3} = mongoose_graphql_errors:format_error(make_error(decode, variables_invalid_json)), 598: 599: ?assertErrMsg(no_query_supplied, <<"The query was not supplied">>, Msg1), 600: ?assertErrMsg(invalid_json_body, <<"invalid">>, Msg2), 601: ?assertErrMsg(variables_invalid_json, <<"invalid">>, Msg3). 602: 603: format_authorize_error(_Config) -> 604: {401, Msg1} = mongoose_graphql_errors:format_error(make_error(authorize, wrong_credentials)), 605: {401, Msg2} = mongoose_graphql_errors:format_error( 606: make_error([<<"ROOT">>], authorize, {no_permissions, <<"ROOT">>})), 607: {401, Msg3} = mongoose_graphql_errors:format_error( 608: make_error(authorize, {request_error, {header, <<"authorization">>}, 'msg'})), 609: 610: ?assertErrMsg(wrong_credentials, <<"provided credentials are wrong">>, Msg1), 611: ?assertErrMsg(no_permissions, <<"without permissions">>, Msg2), 612: ?assertMatch(#{path := [<<"ROOT">>]}, Msg2), 613: ?assertErrMsg(request_error, <<"Malformed authorization header">>, Msg3). 614: 615: format_validate_error(_Config) -> 616: % Ensure the module can format this phase 617: {400, Msg} = mongoose_graphql_errors:format_error( 618: make_error(validate, {not_unique, <<"OpName">>})), 619: ?assertMatch(#{extensions := #{code := not_unique}}, Msg). 620: 621: format_type_check_error(_Config) -> 622: % Ensure the module can format this phase 623: {400, Msg} = mongoose_graphql_errors:format_error( 624: make_error(type_check, non_null)), 625: ?assertMatch(#{extensions := #{code := non_null}}, Msg). 626: 627: format_execute_error(_Config) -> 628: % Ensure the module can format this phase 629: {400, Msg} = mongoose_graphql_errors:format_error( 630: make_error(execute, {resolver_error, any_error})), 631: ?assertMatch(#{extensions := #{code := resolver_error}}, Msg). 632: 633: format_uncategorized_error(_Config) -> 634: % Ensure the module can format this phase 635: {400, Msg} = mongoose_graphql_errors:format_error( 636: make_error(uncategorized, any_error)), 637: ?assertMatch(#{extensions := #{code := any_error}}, Msg). 638: 639: format_any_error(_Config) -> 640: {400, Msg1} = mongoose_graphql_errors:format_error(any_error), 641: {400, Msg2} = mongoose_graphql_errors:format_error(<<"any_error">>), 642: {400, Msg3} = mongoose_graphql_errors:format_error({1, any_error}), 643: {400, Msg4} = mongoose_graphql_errors:format_error(#{msg => any_error}), 644: ?assertErrMsg(uncategorized, <<"any_error">>, Msg1), 645: ?assertErrMsg(uncategorized, <<"any_error">>, Msg2), 646: ?assertErrMsg(uncategorized, <<"any_error">>, Msg3), 647: ?assertErrMsg(uncategorized, <<"any_error">>, Msg4). 648: 649: %% Listeners 650: 651: auth_user_can_access_protected_types(Config) -> 652: Ep = ?config(endpoint_addr, Config), 653: Body = #{query => "{ field }"}, 654: {Status, Data} = execute(Ep, Body, {<<"alice@localhost">>, <<"makota">>}), 655: assert_access_granted(Status, Data). 656: 657: no_creds_defined_admin_can_access_protected(_Config) -> 658: Port = 5559, 659: Ep = "http://localhost:" ++ integer_to_list(Port), 660: start_listener(no_creds_admin_listener, Port, #{schema_endpoint => <<"admin">>}), 661: Body = #{<<"query">> => <<"{ field }">>}, 662: {Status, Data} = execute(Ep, Body, undefined), 663: assert_access_granted(Status, Data). 664: 665: auth_admin_can_access_protected_types(Config) -> 666: Ep = ?config(endpoint_addr, Config), 667: Body = #{query => "{ field }"}, 668: {Status, Data} = execute(Ep, Body, {<<"admin">>, <<"secret">>}), 669: assert_access_granted(Status, Data). 670: 671: auth_domain_admin_can_access_protected_types(Config) -> 672: Ep = ?config(endpoint_addr, Config), 673: Body = #{query => "{ field }"}, 674: {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"makota">>}), 675: assert_access_granted(Status, Data). 676: 677: auth_domain_admin_wrong_password_error(Config) -> 678: Ep = ?config(endpoint_addr, Config), 679: Body = #{query => "{ field }"}, 680: {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"mapsa">>}), 681: assert_no_permissions(wrong_credentials, Status, Data). 682: 683: auth_domain_admin_nonexistent_domain_error(Config) -> 684: Ep = ?config(endpoint_addr, Config), 685: Body = #{query => "{ field }"}, 686: {Status, Data} = execute(Ep, Body, {<<"admin@localhost2">>, <<"makota">>}), 687: assert_no_permissions(wrong_credentials, Status, Data). 688: 689: auth_domain_admin_can_access_owned_domain(Config) -> 690: Ep = ?config(endpoint_addr, Config), 691: Body = #{query => "{ fieldDP(argA: \"localhost\") }"}, 692: {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"makota">>}), 693: assert_access_granted(Status, Data). 694: 695: auth_domain_admin_cannot_access_other_domain(Config) -> 696: Ep = ?config(endpoint_addr, Config), 697: Body = #{query => "{ field fieldDP(argA: \"domain.com\") }"}, 698: {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"makota">>}), 699: assert_no_permissions(no_permissions, Status, Data). 700: 701: auth_domain_admin_cannot_access_global(Config) -> 702: Ep = ?config(endpoint_addr, Config), 703: Body = #{query => "{ fieldGlobal(argA: \"localhost\") }"}, 704: {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"makota">>}), 705: assert_no_permissions(no_permissions, Status, Data). 706: 707: malformed_auth_header_error(Config) -> 708: Ep = ?config(endpoint_addr, Config), 709: % The encoded credentials value is malformed and cannot be decoded. 710: Headers = [{<<"Authorization">>, <<"Basic YWRtaW46c2VjcmV">>}], 711: {Status, Data} = post_request(Ep, Headers, <<"">>), 712: assert_no_permissions(request_error, Status, Data). 713: 714: auth_wrong_creds_error(Config) -> 715: Ep = ?config(endpoint_addr, Config), 716: Body = #{query => "{ field }"}, 717: {Status, Data} = execute(Ep, Body, {<<"user">>, <<"wrong_password">>}), 718: assert_no_permissions(wrong_credentials, Status, Data). 719: 720: invalid_json_body_error(Config) -> 721: Ep = ?config(endpoint_addr, Config), 722: Body = <<"">>, 723: {Status, Data} = execute(Ep, Body, undefined), 724: ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), 725: assert_code(invalid_json_body, Data). 726: 727: no_query_supplied_error(Config) -> 728: Ep = ?config(endpoint_addr, Config), 729: Body = #{}, 730: {Status, Data} = execute(Ep, Body, undefined), 731: ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), 732: assert_code(no_query_supplied, Data). 733: 734: variables_invalid_json_error(Config) -> 735: Ep = ?config(endpoint_addr, Config), 736: Body = #{<<"query">> => <<"{ field }">>, <<"variables">> => <<"{1: 2}">>}, 737: {Status, Data} = execute(Ep, Body, undefined), 738: ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), 739: assert_code(variables_invalid_json, Data). 740: 741: listener_reply_with_parsing_error(Config) -> 742: Ep = ?config(endpoint_addr, Config), 743: Body = #{<<"query">> => <<"{ field ">>}, 744: {Status, Data} = execute(Ep, Body, undefined), 745: ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), 746: assert_code(parser_error, Data), 747: 748: BodyScanner = #{<<"query">> => <<"mutation { id(value: \"asdfsad) } ">>}, 749: {StatusScanner, DataScanner} = execute(Ep, BodyScanner, undefined), 750: ?assertEqual({<<"400">>,<<"Bad Request">>}, StatusScanner), 751: assert_code(scanner_error, DataScanner). 752: 753: listener_reply_with_type_check_error(Config) -> 754: Ep = ?config(endpoint_addr, Config), 755: Body = #{<<"query">> => <<"mutation { id(value: 12) }">>}, 756: {Status, Data} = execute(Ep, Body, undefined), 757: ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), 758: assert_code(input_coercion, Data). 759: 760: listener_reply_with_validation_error(Config) -> 761: Ep = ?config(endpoint_addr, Config), 762: Body = #{<<"query">> => <<"query Q1 { field } query Q1 { field }">>, 763: <<"operationName">> => <<"Q1">>}, 764: {Status, Data} = execute(Ep, Body, undefined), 765: ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), 766: assert_code(not_unique, Data). 767: 768: listener_can_execute_query_with_variables(Config) -> 769: Ep = ?config(endpoint_addr, Config), 770: Body = #{query => "mutation M1($value: String!){ id(value: $value) } query Q1{ field }", 771: variables => #{value => <<"Hello">>}, 772: operationName => <<"M1">> 773: }, 774: {Status, Data} = execute(Ep, Body, undefined), 775: assert_access_granted(Status, Data), 776: ?assertMatch(#{<<"data">> := #{<<"id">> := <<"Hello">>}}, Data). 777: 778: listener_unauth_cannot_access_protected_types(Config) -> 779: Ep = ?config(endpoint_addr, Config), 780: Body = #{query => "{ field }"}, 781: {Status, Data} = execute(Ep, Body, undefined), 782: ?assertMatch(#{<<"errors">> := [#{<<"path">> := [<<"ROOT">>]}]}, Data), 783: assert_no_permissions(no_permissions, Status, Data). 784: 785: listener_unauth_can_access_unprotected_types(Config) -> 786: Ep = ?config(endpoint_addr, Config), 787: Body = #{query => "mutation { field }"}, 788: {Status, Data} = execute(Ep, Body, undefined), 789: assert_access_granted(Status, Data). 790: 791: %% Helpers 792: 793: assert_code(Code, Data) -> 794: BinCode = atom_to_binary(Code), 795: ?assertMatch(#{<<"errors">> := [#{<<"extensions">> := #{<<"code">> := BinCode}}]}, Data). 796: 797: assert_no_permissions(ExpectedCode, Status, Data) -> 798: ?assertEqual({<<"401">>,<<"Unauthorized">>}, Status), 799: assert_code(ExpectedCode, Data). 800: 801: assert_access_granted(Status, Data) -> 802: ?assertEqual({<<"200">>,<<"OK">>}, Status), 803: % access was granted, no error was returned 804: ?assertNotMatch(#{<<"errors">> := _}, Data). 805: 806: assert_err_msg(Code, MsgContains, #{message := Msg} = ErrorMsg) -> 807: ?assertMatch(#{extensions := #{code := Code}}, ErrorMsg), 808: ?assertNotEqual(nomatch, binary:match(Msg, MsgContains)). 809: 810: make_error(Phase, Term) -> 811: #{phase => Phase, error_term => Term}. 812: 813: make_error(Path, Phase, Term) -> 814: #{path => Path, phase => Phase, error_term => Term}. 815: 816: check_permissions(Config, Auth, Doc) -> 817: Ep = ?config(endpoint, Config), 818: Op = proplists:get_value(op, Config, undefined), 819: {ok, Ast} = graphql:parse(Doc), 820: {ok, #{ast := Ast2}} = graphql:type_check(Ep, Ast), 821: ok = graphql:validate(Ast2), 822: Ctx = #{operation_name => Op, authorized => Auth, params => #{}}, 823: ok = mongoose_graphql_permissions:check_permissions(Ctx, Ast2). 824: 825: check_domain_permissions(Config, Domain, Doc) -> 826: Ep = ?config(endpoint, Config), 827: Args = proplists:get_value(args, Config, #{}), 828: Op = proplists:get_value(op, Config, undefined), 829: {ok, Ast} = graphql:parse(Doc), 830: {ok, #{ast := Ast2, fun_env := FunEnv}} = graphql:type_check(Ep, Ast), 831: ok = graphql:validate(Ast2), 832: Coerced = graphql:type_check_params(Ep, FunEnv, Op, Args), 833: Admin = jid:make_bare(<<"admin">>, Domain), 834: Ctx = #{operation_name => Op, authorized => true, authorized_as => domain_admin, 835: admin => Admin, params => Coerced}, 836: ok = mongoose_graphql_permissions:check_permissions(Ctx, Ast2). 837: 838: request(Doc, Authorized) -> 839: request(undefined, Doc, Authorized). 840: 841: request(Op, Doc, Authorized) -> 842: #{document => Doc, 843: operation_name => Op, 844: vars => #{}, 845: authorized => Authorized, 846: ctx => #{}}. 847: 848: example_split_schema_data(Config) -> 849: Pattern = filename:join([proplists:get_value(data_dir, Config), 850: "split_schema", "*.gql"]), 851: Mapping = 852: #{objects => 853: #{'Query' => mongoose_graphql_default_resolver, 854: 'Mutation' => mongoose_graphql_default_resolver, 855: default => mongoose_graphql_default_resolver}}, 856: {Mapping, Pattern}. 857: 858: example_schema_protected_data(Config) -> 859: Pattern = filename:join([proplists:get_value(data_dir, Config), "protected_schema.gql"]), 860: Mapping = 861: #{objects => 862: #{'UserQuery' => mongoose_graphql_default_resolver, 863: 'UserMutation' => mongoose_graphql_default_resolver, 864: default => mongoose_graphql_default_resolver}}, 865: {Mapping, Pattern}. 866: 867: example_schema_data(Config) -> 868: Pattern = filename:join([proplists:get_value(data_dir, Config), "schema.gql"]), 869: Mapping = 870: #{objects => 871: #{'UserQuery' => mongoose_graphql_default_resolver, 872: 'UserMutation' => mongoose_graphql_default_resolver, 873: default => mongoose_graphql_default_resolver}}, 874: {Mapping, Pattern}. 875: 876: example_permissions_schema_data(Config) -> 877: Pattern = filename:join([proplists:get_value(data_dir, Config), "permissions_schema.gql"]), 878: Mapping = 879: #{objects => 880: #{'UserQuery' => mongoose_graphql_default_resolver, 881: 'UserMutation' => mongoose_graphql_default_resolver, 882: default => mongoose_graphql_default_resolver}, 883: enums => #{default => mongoose_graphql_default_resolver}, 884: scalars => #{default => mongoose_graphql_scalar}, 885: interfaces => #{default => mongoose_graphql_default_resolver}, 886: unions => #{default => mongoose_graphql_default_resolver}}, 887: {Mapping, Pattern}. 888: 889: example_listener_schema_data(Config) -> 890: Pattern = filename:join([proplists:get_value(data_dir, Config), "listener_schema.gql"]), 891: Mapping = 892: #{objects => 893: #{'UserQuery' => mongoose_graphql_default_resolver, 894: 'UserMutation' => mongoose_graphql_default_resolver, 895: default => mongoose_graphql_default_resolver}, 896: enums => #{default => mongoose_graphql_default_resolver}}, 897: {Mapping, Pattern}. 898: 899: -spec init_ep_listener(integer(), atom(), listener_opts(), [{atom(), term()}]) -> 900: [{atom(), term()}]. 901: init_ep_listener(Port, EpName, ListenerOpts, Config) -> 902: Pid = spawn(fun() -> 903: Name = list_to_atom("gql_listener_" ++ atom_to_list(EpName)), 904: ok = start_listener(Name, Port, ListenerOpts), 905: {Mapping, Pattern} = example_listener_schema_data(Config), 906: {ok, _} = mongoose_graphql:create_endpoint(EpName, Mapping, [Pattern]), 907: receive 908: stop -> 909: ok 910: end 911: end), 912: [{test_process, Pid}, {endpoint_addr, "http://localhost:" ++ integer_to_list(Port)} | Config]. 913: 914: -spec start_listener(atom(), integer(), listener_opts()) -> ok. 915: start_listener(Ref, Port, Opts) -> 916: Dispatch = cowboy_router:compile([ 917: {'_', [{"/graphql", mongoose_graphql_cowboy_handler, Opts}]} 918: ]), 919: {ok, _} = cowboy:start_clear(Ref, 920: [{port, Port}], 921: #{env => #{dispatch => Dispatch}}), 922: ok. 923: 924: -spec execute(binary(), map(), undefined | {binary(), binary()}) -> {{binary(), binary()}, map()}. 925: execute(EpAddr, Body, undefined) -> 926: post_request(EpAddr, [], Body); 927: execute(EpAddr, Body, {Username, Password}) -> 928: Creds = base64:encode(<<Username/binary, ":", Password/binary>>), 929: Headers = [{<<"Authorization">>, <<"Basic ", Creds/binary>>}], 930: post_request(EpAddr, Headers, Body). 931: 932: post_request(EpAddr, HeadersIn, Body) when is_binary(Body) -> 933: {ok, Client} = fusco:start(EpAddr, []), 934: Headers = [{<<"Content-Type">>, <<"application/json">>}, 935: {<<"Request-Id">>, random_request_id()} | HeadersIn], 936: {ok, {ResStatus, _, ResBody, _, _}} = Res = 937: fusco:request(Client, <<"/graphql">>, <<"POST">>, Headers, Body, 5000), 938: fusco:disconnect(Client), 939: ct:log("~p", [Res]), 940: {ResStatus, jiffy:decode(ResBody, [return_maps])}; 941: post_request(Ep, HeadersIn, Body) -> 942: post_request(Ep, HeadersIn, jiffy:encode(Body)). 943: 944: random_request_id() -> 945: base16:encode(crypto:strong_rand_bytes(8)).