1: -module(graphql_account_SUITE).
    2: 
    3: -include_lib("common_test/include/ct.hrl").
    4: -include_lib("eunit/include/eunit.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, get_listener_port/1,
   10:                          get_listener_config/1, get_ok_value/2, get_err_msg/1]).
   11: 
   12: -define(NOT_EXISTING_JID, <<"unknown987@unknown">>).
   13: 
   14: suite() ->
   15:     require_rpc_nodes([mim]) ++ escalus:suite().
   16: 
   17: all() ->
   18:     [{group, user_account_handler},
   19:      {group, admin_account_handler}].
   20: 
   21: groups() ->
   22:     [{user_account_handler, [parallel], user_account_handler()},
   23:      {admin_account_handler, [], admin_account_handler()}].
   24: 
   25: user_account_handler() ->
   26:     [user_unregister,
   27:      user_change_password].
   28: 
   29: admin_account_handler() ->
   30:     [admin_list_users,
   31:      admin_count_users,
   32:      admin_check_password,
   33:      admin_check_password_hash,
   34:      admin_check_plain_password_hash,
   35:      admin_check_user,
   36:      admin_register_user,
   37:      admin_register_random_user,
   38:      admin_remove_non_existing_user,
   39:      admin_remove_existing_user,
   40:      admin_ban_user,
   41:      admin_change_user_password].
   42: 
   43: init_per_suite(Config) ->
   44:     Config1 = [{ctl_auth_mods, mongoose_helper:auth_modules()} | Config],
   45:     Config2 = escalus:init_per_suite(Config1),
   46:     dynamic_modules:save_modules(domain_helper:host_type(), Config2).
   47: 
   48: end_per_suite(Config) ->
   49:     dynamic_modules:restore_modules(Config),
   50:     escalus:end_per_suite(Config).
   51: 
   52: init_per_group(admin_account_handler, Config) ->
   53:     Config1 = escalus:create_users(Config, escalus:get_users([alice])),
   54:     graphql_helper:init_admin_handler(Config1);
   55: init_per_group(user_account_handler, Config) ->
   56:     [{schema_endpoint, user} | Config];
   57: init_per_group(_, Config) ->
   58:     Config.
   59: 
   60: end_per_group(admin_account_handler, Config) ->
   61:     escalus_fresh:clean(),
   62:     escalus:delete_users(Config, escalus:get_users([alice]));
   63: end_per_group(user_account_handler, _Config) ->
   64:     escalus_fresh:clean();
   65: end_per_group(_, _Config) ->
   66:     ok.
   67: 
   68: init_per_testcase(admin_register_user = C, Config) ->
   69:     Config1 = [{user, {<<"gql_admin_registration_test">>, domain_helper:domain()}} | Config],
   70:     escalus:init_per_testcase(C, Config1);
   71: init_per_testcase(admin_check_plain_password_hash = C, Config) ->
   72:     {_, AuthMods} = lists:keyfind(ctl_auth_mods, 1, Config),
   73:     case lists:member(ejabberd_auth_ldap, AuthMods) of
   74:         true ->
   75:             {skip, not_fully_supported_with_ldap};
   76:         false ->
   77:             AuthOpts = mongoose_helper:auth_opts_with_password_format(plain),
   78:             Config1 = mongoose_helper:backup_and_set_config_option(
   79:                         Config, {auth, domain_helper:host_type()}, AuthOpts),
   80:             Config2 = escalus:create_users(Config1, escalus:get_users([carol])),
   81:             escalus:init_per_testcase(C, Config2)
   82:     end;
   83: init_per_testcase(CaseName, Config) ->
   84:     escalus:init_per_testcase(CaseName, Config).
   85: 
   86: end_per_testcase(admin_register_user = C, Config) ->
   87:     {Username, Domain} = proplists:get_value(user, Config),
   88:     rpc(mim(), mongoose_account_api, unregister_user, [Username, Domain]),
   89:     escalus:end_per_testcase(C, Config);
   90: end_per_testcase(admin_check_plain_password_hash, Config) ->
   91:     mongoose_helper:restore_config(Config),
   92:     escalus:delete_users(Config, escalus:get_users([carol]));
   93: end_per_testcase(CaseName, Config) ->
   94:     escalus:end_per_testcase(CaseName, Config).
   95: 
   96: user_unregister(Config) ->
   97:     escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_unregister_story/2).
   98: 
   99: user_unregister_story(Config, Alice) ->
  100:     Ep = ?config(schema_endpoint, Config),
  101:     Password = lists:last(escalus_users:get_usp(Config, alice)),
  102:     BinJID = escalus_client:full_jid(Alice),
  103:     Creds = {BinJID, Password},
  104:     Query = <<"mutation { account { unregister } }">>,
  105: 
  106:     Path = [data, account, unregister],
  107:     Resp = execute(Ep, #{query => Query}, Creds),
  108:     ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp), <<"successfully unregistered">>)),
  109:     % Ensure the user is removed
  110:     AllUsers = rpc(mim(), mongoose_account_api, list_users, [domain_helper:domain()]),
  111:     LAliceJID = jid:to_binary(jid:to_lower((jid:binary_to_bare(BinJID)))),
  112:     ?assertNot(lists:member(LAliceJID, AllUsers)).
  113:     
  114: user_change_password(Config) ->
  115:     escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_change_password_story/2).
  116: 
  117: user_change_password_story(Config, Alice) ->
  118:     Ep = ?config(schema_endpoint, Config),
  119:     Password = lists:last(escalus_users:get_usp(Config, alice)),
  120:     Creds = {escalus_client:full_jid(Alice), Password},
  121:     % Set an empty password
  122:     Resp1 = execute(Ep, user_change_password_body(<<>>), Creds),
  123:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"Empty password">>)),
  124:     % Set a correct password
  125:     Path = [data, account, changePassword],
  126:     Resp2 = execute(Ep, user_change_password_body(<<"kaczka">>), Creds),
  127:     ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp2), <<"Password changed">>)).
  128: 
  129: admin_list_users(Config) ->
  130:     % An unknown domain
  131:     Resp = execute_auth(list_users_body(<<"unknown-domain">>), Config),
  132:     ?assertEqual([], get_ok_value([data, account, listUsers], Resp)),
  133:     % A domain with users
  134:     Domain = domain_helper:domain(),
  135:     Username = jid:nameprep(escalus_users:get_username(Config, alice)),
  136:     JID = <<Username/binary, "@", Domain/binary>>,
  137:     Resp2 = execute_auth(list_users_body(Domain), Config),
  138:     Users = get_ok_value([data, account, listUsers], Resp2),
  139:     ?assert(lists:member(JID, Users)).
  140:     
  141: admin_count_users(Config) ->
  142:     % An unknown domain
  143:     Resp = execute_auth(count_users_body(<<"unknown-domain">>), Config),
  144:     ?assertEqual(0, get_ok_value([data, account, countUsers], Resp)),
  145:     % A domain with at least one user
  146:     Domain = domain_helper:domain(),
  147:     Resp2 = execute_auth(count_users_body(Domain), Config),
  148:     ?assert(0 < get_ok_value([data, account, countUsers], Resp2)).
  149: 
  150: admin_check_password(Config) ->
  151:     Password = lists:last(escalus_users:get_usp(Config, alice)),
  152:     BinJID = escalus_users:get_jid(Config, alice),
  153:     Path = [data, account, checkPassword],
  154:     % A correct password
  155:     Resp1 = execute_auth(check_password_body(BinJID, Password), Config),
  156:     ?assertMatch(#{<<"correct">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)),
  157:     % An incorrect password
  158:     Resp2 = execute_auth(check_password_body(BinJID, <<"incorrect_pw">>), Config),
  159:     ?assertMatch(#{<<"correct">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)),
  160:     % A non-existing user
  161:     Resp3 = execute_auth(check_password_body(?NOT_EXISTING_JID, Password), Config),
  162:     ?assertEqual(null, get_ok_value(Path, Resp3)).
  163: 
  164: admin_check_password_hash(Config) ->
  165:     UserSCRAM = escalus_users:get_jid(Config, alice),
  166:     EmptyHash = list_to_binary(get_md5(<<>>)),
  167:     Method = <<"md5">>,
  168:     % SCRAM password user
  169:     Resp1 = execute_auth(check_password_hash_body(UserSCRAM, EmptyHash, Method), Config),
  170:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"SCRAM password">>)),
  171:     % A non-existing user
  172:     Resp2 = execute_auth(check_password_hash_body(?NOT_EXISTING_JID, EmptyHash, Method), Config),
  173:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"not exist">>)).
  174: 
  175: admin_check_plain_password_hash(Config) ->
  176:     UserJID = escalus_users:get_jid(Config, carol),
  177:     Password = lists:last(escalus_users:get_usp(Config, carol)),
  178:     Method = <<"md5">>,
  179:     Hash = list_to_binary(get_md5(Password)),
  180:     WrongHash = list_to_binary(get_md5(<<"wrong password">>)),
  181:     Path = [data, account, checkPasswordHash],
  182:     % A correct hash
  183:     Resp = execute_auth(check_password_hash_body(UserJID, Hash, Method), Config),
  184:     ?assertMatch(#{<<"correct">> := true, <<"message">> := _}, get_ok_value(Path, Resp)),
  185:     % An incorrect hash
  186:     Resp2 = execute_auth(check_password_hash_body(UserJID, WrongHash, Method), Config),
  187:     ?assertMatch(#{<<"correct">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)),
  188:     % A not-supported hash method
  189:     Resp3 = execute_auth(check_password_hash_body(UserJID, Hash, <<"a">>), Config),
  190:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp3), <<"not supported">>)).
  191: 
  192: admin_check_user(Config) ->
  193:     BinJID = escalus_users:get_jid(Config, alice),
  194:     Path = [data, account, checkUser],
  195:     % An existing user
  196:     Resp1 = execute_auth(check_user_body(BinJID), Config),
  197:     ?assertMatch(#{<<"exist">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)),
  198:     % A non-existing user
  199:     Resp2 = execute_auth(check_user_body(?NOT_EXISTING_JID), Config),
  200:     ?assertMatch(#{<<"exist">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)).
  201: 
  202: admin_register_user(Config) ->
  203:     Password = <<"my_password">>,
  204:     {Username, Domain} = proplists:get_value(user, Config),
  205:     Path = [data, account, registerUser, message],
  206:     % Register a new user
  207:     Resp1 = execute_auth(register_user_body(Domain, Username, Password), Config),
  208:     ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp1), <<"successfully registered">>)),
  209:     % Try to register a user with existing name
  210:     Resp2 = execute_auth(register_user_body(Domain, Username, Password), Config),
  211:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"already registered">>)).
  212: 
  213: admin_register_random_user(Config) ->
  214:     Password = <<"my_password">>,
  215:     Domain = domain_helper:domain(),
  216:     Path = [data, account, registerUser],
  217:     % Register a new user
  218:     Resp1 = execute_auth(register_user_body(Domain, null, Password), Config),
  219:     #{<<"message">> := Msg, <<"jid">> := JID} = get_ok_value(Path, Resp1),
  220:     {Username, Server} = jid:to_lus(jid:from_binary(JID)),
  221: 
  222:     ?assertNotEqual(nomatch, binary:match(Msg, <<"successfully registered">>)),
  223:     {ok, _} = rpc(mim(), mongoose_account_api, unregister_user, [Username, Server]).
  224: 
  225: admin_remove_non_existing_user(Config) ->
  226:     Resp = execute_auth(remove_user_body(?NOT_EXISTING_JID), Config),
  227:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp), <<"not exist">>)).
  228: 
  229: admin_remove_existing_user(Config) ->
  230:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  231:         Path = [data, account, removeUser, message],
  232:         BinJID = escalus_client:full_jid(Alice),
  233:         Resp4 = execute_auth(remove_user_body(BinJID), Config),
  234:         ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp4),
  235:                                               <<"successfully unregister">>))
  236:     end).
  237: 
  238: admin_ban_user(Config) ->
  239:     Path = [data, account, banUser, message],
  240:     Reason = <<"annoying">>,
  241:     % Ban not existing user
  242:     Resp1 = execute_auth(ban_user_body(?NOT_EXISTING_JID, Reason), Config),
  243:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"not allowed">>)),
  244:     % Ban an existing user
  245:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  246:         BinJID = escalus_client:full_jid(Alice),
  247:         Resp2 = execute_auth(ban_user_body(BinJID, Reason), Config),
  248:         ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp2), <<"successfully banned">>))
  249:     end).
  250: 
  251: admin_change_user_password(Config) ->
  252:     Path = [data, account, changeUserPassword, message],
  253:     NewPassword = <<"new password">>,
  254:     % Change password of not existing user
  255:     Resp1 = execute_auth(change_user_password_body(?NOT_EXISTING_JID, NewPassword), Config),
  256:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"not allowed">>)),
  257:     % Set an empty password
  258:     Resp2 = execute_auth(change_user_password_body(?NOT_EXISTING_JID, <<>>), Config),
  259:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"Empty password">>)),
  260:     % Change password of an existing user
  261:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  262:         BinJID = escalus_client:full_jid(Alice),
  263:         Resp3 = execute_auth(change_user_password_body(BinJID, NewPassword), Config),
  264:         ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp3), <<"Password changed">>))
  265:     end).
  266: 
  267: %% Helpers
  268: 
  269: get_md5(AccountPass) ->
  270:     lists:flatten([io_lib:format("~.16B", [X])
  271:                    || X <- binary_to_list(crypto:hash(md5, AccountPass))]).
  272: 
  273: %% Request bodies
  274: 
  275: list_users_body(Domain) ->
  276:     Query = <<"query Q1($domain: String!) { account { listUsers(domain: $domain) } }">>,
  277:     OpName = <<"Q1">>,
  278:     Vars = #{<<"domain">> => Domain},
  279:     #{query => Query, operationName => OpName, variables => Vars}.
  280: 
  281: count_users_body(Domain) ->
  282:     Query = <<"query Q1($domain: String!) { account { countUsers(domain: $domain) } }">>,
  283:     OpName = <<"Q1">>,
  284:     Vars = #{<<"domain">> => Domain},
  285:     #{query => Query, operationName => OpName, variables => Vars}.
  286: 
  287: check_password_body(User, Password) ->
  288:     Query = <<"query Q1($user: JID!, $password: String!) 
  289:               { account { checkPassword(user: $user, password: $password) {correct message} } }">>,
  290:     OpName = <<"Q1">>,
  291:     Vars = #{<<"user">> => User, <<"password">> => Password},
  292:     #{query => Query, operationName => OpName, variables => Vars}.
  293: 
  294: check_password_hash_body(User, PasswordHash, HashMethod) ->
  295:     Query = <<"query Q1($user: JID!, $hash: String!, $method: String!)
  296:               { account { checkPasswordHash(user: $user, passwordHash: $hash, hashMethod: $method)
  297:               {correct message} } }">>,
  298:     OpName = <<"Q1">>,
  299:     Vars = #{<<"user">> => User, <<"hash">> => PasswordHash, <<"method">> => HashMethod},
  300:     #{query => Query, operationName => OpName, variables => Vars}.
  301: 
  302: check_user_body(User) ->
  303:     Query = <<"query Q1($user: JID!) 
  304:               { account { checkUser(user: $user) {exist message} } }">>,
  305:     OpName = <<"Q1">>,
  306:     Vars = #{<<"user">> => User},
  307:     #{query => Query, operationName => OpName, variables => Vars}.
  308: 
  309: register_user_body(Domain, Username, Password) ->
  310:     Query = <<"mutation M1($domain: String!, $username: String, $password: String!)
  311:               { account { registerUser(domain: $domain, username: $username, password: $password) 
  312:               { jid message } } }">>,
  313:     OpName = <<"M1">>,
  314:     Vars = #{<<"domain">> => Domain, <<"username">> => Username, <<"password">> => Password},
  315:     #{query => Query, operationName => OpName, variables => Vars}.
  316: 
  317: remove_user_body(User) ->
  318:     Query = <<"mutation M1($user: JID!) 
  319:               { account { removeUser(user: $user) { jid message } } }">>,
  320:     OpName = <<"M1">>,
  321:     Vars = #{<<"user">> => User},
  322:     #{query => Query, operationName => OpName, variables => Vars}.
  323: 
  324: ban_user_body(JID, Reason) ->
  325:     Query = <<"mutation M1($user: JID!, $reason: String!) 
  326:               { account { banUser(user: $user, reason: $reason) { jid message } } }">>,
  327:     OpName = <<"M1">>,
  328:     Vars = #{<<"user">> => JID, <<"reason">> => Reason},
  329:     #{query => Query, operationName => OpName, variables => Vars}.
  330: 
  331: change_user_password_body(JID, NewPassword) ->
  332:     Query = <<"mutation M1($user: JID!, $newPassword: String!) 
  333:               { account { changeUserPassword(user: $user, newPassword: $newPassword) { jid message } } }">>,
  334:     OpName = <<"M1">>,
  335:     Vars = #{<<"user">> => JID, <<"newPassword">> => NewPassword},
  336:     #{query => Query, operationName => OpName, variables => Vars}.
  337: 
  338: user_change_password_body(NewPassword) ->
  339:     Query = <<"mutation M1($newPassword: String!)
  340:               { account { changePassword(newPassword: $newPassword) } }">>,
  341:     OpName = <<"M1">>,
  342:     Vars = #{<<"newPassword">> => NewPassword},
  343:     #{query => Query, operationName => OpName, variables => Vars}.