./ct_report/coverage/mongoose_account_api.COVER.html

1 %% @doc Provide an interface for frontends (like graphql or ctl) to manage accounts.
2 -module(mongoose_account_api).
3
4 -export([list_users/1,
5 count_users/1,
6 register_user/3,
7 register_generated_user/2,
8 unregister_user/1,
9 unregister_user/2,
10 ban_account/2,
11 change_password/2,
12 change_password/3,
13 check_account/1,
14 check_password/2,
15 check_password_hash/3,
16 import_users/1]).
17
18 -type register_result() :: {ok | exists | invalid_jid | cannot_register |
19 limit_per_domain_exceeded, iolist()}.
20
21 -type unregister_result() :: {ok | not_allowed | invalid_jid | user_does_not_exist, string()}.
22
23 -type change_password_result() :: {ok | empty_password | not_allowed | invalid_jid |
24 user_does_not_exist, string()}.
25
26 -type check_password_result() :: {ok | incorrect | user_does_not_exist, string()}.
27
28 -type check_password_hash_result() :: {ok | incorrect | wrong_user | wrong_method, string()}.
29
30 -type check_account_result() :: {ok | user_does_not_exist, string()}.
31
32 -type list_user_result() :: {ok, [jid:literal_jid()]} | {domain_not_found, string()}.
33
34 -type count_user_result() :: {ok, non_neg_integer()} | {domain_not_found, string()}.
35
36 -export_type([register_result/0,
37 unregister_result/0,
38 change_password_result/0,
39 check_password_result/0,
40 check_password_hash_result/0,
41 check_account_result/0,
42 list_user_result/0]).
43
44 %% API
45
46 -spec list_users(jid:server()) -> list_user_result().
47 list_users(Domain) ->
48 14 PrepDomain = jid:nameprep(Domain),
49 14 case mongoose_domain_api:get_domain_host_type(PrepDomain) of
50 {ok, _} ->
51 12 Users = ejabberd_auth:get_vh_registered_users(PrepDomain),
52 12 SUsers = lists:sort(Users),
53 12 {ok, [jid:to_binary(US) || US <- SUsers]};
54 {error, not_found} ->
55 2 {domain_not_found, "Domain does not exist"}
56 end.
57
58 -spec count_users(jid:server()) -> count_user_result().
59 count_users(Domain) ->
60 9 PrepDomain = jid:nameprep(Domain),
61 9 case mongoose_domain_api:get_domain_host_type(PrepDomain) of
62 {ok, _} ->
63 7 UserCount = ejabberd_auth:get_vh_registered_users_number(PrepDomain),
64 7 {ok, UserCount};
65 {error, not_found} ->
66 2 {domain_not_found, "Domain does not exist"}
67 end.
68
69 -spec register_generated_user(jid:server(), binary()) -> {register_result(), jid:literal_jid()}.
70 register_generated_user(Host, Password) ->
71 3 Username = generate_username(),
72 3 JID = jid:to_binary({Username, Host}),
73 3 {register_user(Username, Host, Password), JID}.
74
75 -spec register_user(jid:user(), jid:server(), binary()) -> register_result().
76 register_user(User, Host, Password) ->
77 5628 JID = jid:make_bare(User, Host),
78 5628 case ejabberd_auth:try_register(JID, Password) of
79 {error, exists} ->
80 13 String =
81 io_lib:format("User ~s already registered at node ~p",
82 [jid:to_binary(JID), node()]),
83 13 {exists, String};
84 {error, invalid_jid} ->
85 1 String = io_lib:format("Invalid JID ~s@~s", [User, Host]),
86 1 {invalid_jid, String};
87 {error, limit_per_domain_exceeded} ->
88 3 String = io_lib:format("User limit has been exceeded for domain ~s", [Host]),
89 3 {limit_per_domain_exceeded, String};
90 {error, Reason} ->
91 4 String =
92 io_lib:format("Can't register user ~s at node ~p: ~p",
93 [jid:to_binary(JID), node(), Reason]),
94 4 {cannot_register, String};
95 _ ->
96 5607 {ok, io_lib:format("User ~s successfully registered", [jid:to_binary(JID)])}
97 end.
98
99 -spec unregister_user(jid:user(), jid:server()) -> unregister_result().
100 unregister_user(User, Host) ->
101 5682 JID = jid:make_bare(User, Host),
102 5682 unregister_user(JID).
103
104 -spec unregister_user(jid:jid()) -> unregister_result().
105 unregister_user(JID) ->
106 5706 case ejabberd_auth:remove_user(JID) of
107 ok ->
108 5573 {ok, io_lib:format("User ~s successfully unregistered", [jid:to_binary(JID)])};
109 error ->
110 1 {invalid_jid, "Invalid JID"};
111 {error, _} ->
112 132 {not_allowed, "User does not exist or you are not authorized properly"}
113 end.
114
115 -spec change_password(jid:user(), jid:server(), binary()) -> change_password_result().
116 change_password(User, Host, Password) ->
117 5 JID = jid:make_bare(User, Host),
118 5 change_password(JID, Password).
119
120 -spec change_password(jid:jid(), binary()) -> change_password_result().
121 change_password(JID, Password) ->
122 17 Result = ejabberd_auth:set_password(JID, Password),
123 17 format_change_password(Result).
124
125 -spec check_account(jid:jid()) -> check_account_result().
126 check_account(JID) ->
127 25 case ejabberd_auth:does_user_exist(JID) of
128 true ->
129 10 {ok, io_lib:format("User ~s exists", [jid:to_binary(JID)])};
130 false ->
131 15 {user_does_not_exist, io_lib:format("User ~s does not exist", [jid:to_binary(JID)])}
132 end.
133
134 -spec check_password(jid:jid(), binary()) -> check_password_result().
135 check_password(JID, Password) ->
136 10 case ejabberd_auth:does_user_exist(JID) of
137 true ->
138 6 case ejabberd_auth:check_password(JID, Password) of
139 true ->
140 3 {ok, io_lib:format("Password '~s' for user ~s is correct",
141 [Password, jid:to_binary(JID)])};
142 false ->
143 3 {incorrect, io_lib:format("Password '~s' for user ~s is incorrect",
144 [Password, jid:to_binary(JID)])}
145 end;
146 false ->
147 4 {user_does_not_exist,
148 io_lib:format("Password ~s for user ~s is incorrect because this user does not"
149 " exist", [Password, jid:to_binary(JID)])}
150 end.
151
152 -spec check_password_hash(jid:jid(), string(), string()) -> check_password_hash_result().
153 check_password_hash(JID, PasswordHash, HashMethod) ->
154 19 AccountPass = ejabberd_auth:get_password_s(JID),
155 19 AccountPassHash = case HashMethod of
156 11 "md5" -> get_md5(AccountPass);
157 4 "sha" -> get_sha(AccountPass);
158 4 _ -> undefined
159 end,
160 19 case {AccountPass, AccountPassHash} of
161 {<<>>, _} ->
162 7 {wrong_user, "User does not exist or using SCRAM password"};
163 {_, undefined} ->
164 4 Msg = io_lib:format("Given hash method `~s` is not supported. Try `md5` or `sha`",
165 [HashMethod]),
166 4 {wrong_method, Msg};
167 {_, PasswordHash} ->
168 4 {ok, "Password hash is correct"};
169 _->
170 4 {incorrect, "Password hash is incorrect"}
171 end.
172
173 -spec import_users(file:filename()) -> {ok, #{binary() => [{ok, jid:jid() | binary()}]}}
174 | {file_not_found, binary()}.
175 import_users(Filename) ->
176 3 case mongoose_import_users:run(Filename) of
177 {ok, Summary} ->
178 2 {ok, maps:fold(
179 fun(Reason, List, Map) ->
180 10 List2 = [{ok, El} || El <- List],
181 10 maps:put(from_reason(Reason), List2, Map)
182 end,
183 #{<<"status">> => <<"Completed">>},
184 Summary)};
185 {error, file_not_found} ->
186 1 {file_not_found, <<"File not found">>}
187 end.
188
189 -spec from_reason(mongoose_import_users:reason()) -> binary().
190 2 from_reason(ok) -> <<"created">>;
191 2 from_reason(exists) -> <<"existing">>;
192
:-(
from_reason(not_allowed) -> <<"notAllowed">>;
193 2 from_reason(invalid_jid) -> <<"invalidJID">>;
194 2 from_reason(null_password) -> <<"emptyPassword">>;
195 2 from_reason(bad_csv) -> <<"invalidRecord">>.
196
197 -spec ban_account(jid:jid(), binary()) -> change_password_result().
198 ban_account(JID, Reason) ->
199 7 case ejabberd_auth:does_user_exist(JID) of
200 true ->
201 3 mongoose_session_api:kick_sessions(JID, Reason),
202 3 case set_random_password(JID, Reason) of
203 ok ->
204 3 {ok, io_lib:format("User ~s successfully banned with reason: ~s",
205 [jid:to_binary(JID), Reason])};
206 ErrResult ->
207
:-(
format_change_password(ErrResult)
208 end;
209 false ->
210 4 {user_does_not_exist, io_lib:format("User ~s does not exist", [jid:to_binary(JID)])}
211 end.
212
213 %% Internal
214
215 format_change_password(ok) ->
216 6 {ok, "Password changed"};
217 format_change_password({error, empty_password}) ->
218 5 {empty_password, "Empty password"};
219 format_change_password({error, not_allowed}) ->
220 5 {not_allowed, "User does not exist or you are not authorized properly"};
221 format_change_password({error, invalid_jid}) ->
222 1 {invalid_jid, "Invalid JID"}.
223
224 -spec set_random_password(JID, Reason) -> Result when
225 JID :: jid:jid(),
226 Reason :: binary(),
227 Result :: ok | {error, any()}.
228 set_random_password(JID, Reason) ->
229 3 NewPass = build_random_password(Reason),
230 3 ejabberd_auth:set_password(JID, NewPass).
231
232 -spec build_random_password(Reason :: binary()) -> binary().
233 build_random_password(Reason) ->
234 3 {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(),
235 3 Date = iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0wZ",
236 [Year, Month, Day, Hour, Minute, Second])),
237 3 RandomString = mongoose_bin:gen_from_crypto(),
238 3 <<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>.
239
240 -spec generate_username() -> binary().
241 generate_username() ->
242 3 mongoose_bin:join([mongoose_bin:gen_from_timestamp(),
243 mongoose_bin:gen_from_crypto()], $-).
244
245 -spec get_md5(binary()) -> string().
246 get_md5(AccountPass) ->
247 11 lists:flatten([io_lib:format("~.16B", [X])
248 11 || X <- binary_to_list(crypto:hash(md5, AccountPass))]).
249
250 -spec get_sha(binary()) -> string().
251 get_sha(AccountPass) ->
252 4 lists:flatten([io_lib:format("~.16B", [X])
253 4 || X <- binary_to_list(crypto:hash(sha, AccountPass))]).
Line Hits Source