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 |
|
list_old_users/1, |
7 |
|
list_old_users_for_domain/2, |
8 |
|
register_user/3, |
9 |
|
register_generated_user/2, |
10 |
|
unregister_user/1, |
11 |
|
unregister_user/2, |
12 |
|
delete_old_users/1, |
13 |
|
delete_old_users_for_domain/2, |
14 |
|
ban_account/2, |
15 |
|
ban_account/3, |
16 |
|
change_password/2, |
17 |
|
change_password/3, |
18 |
|
check_account/1, |
19 |
|
check_account/2, |
20 |
|
check_password/2, |
21 |
|
check_password/3, |
22 |
|
check_password_hash/3, |
23 |
|
check_password_hash/4, |
24 |
|
num_active_users/2]). |
25 |
|
|
26 |
|
-include("mongoose.hrl"). |
27 |
|
|
28 |
|
-type register_result() :: {ok | exists | invalid_jid | cannot_register, iolist()}. |
29 |
|
|
30 |
|
-type unregister_result() :: {ok | not_allowed | invalid_jid, string()}. |
31 |
|
|
32 |
|
-type change_password_result() :: {ok | empty_password | not_allowed | invalid_jid, string()}. |
33 |
|
|
34 |
|
-type check_password_result() :: {ok | incorrect | user_does_not_exist, string()}. |
35 |
|
|
36 |
|
-type check_password_hash_result() :: {ok | incorrect | wrong_user | wrong_method, string()}. |
37 |
|
|
38 |
|
-type check_account_result() :: {ok | user_does_not_exist, string()}. |
39 |
|
|
40 |
|
-type num_active_users_result() :: {ok, non_neg_integer()} | {cannot_count, string()}. |
41 |
|
|
42 |
|
-type delete_old_users_result() :: {ok, iolist()}. |
43 |
|
|
44 |
|
-export_type([register_result/0, |
45 |
|
unregister_result/0, |
46 |
|
change_password_result/0, |
47 |
|
check_password_result/0, |
48 |
|
check_password_hash_result/0, |
49 |
|
check_account_result/0, |
50 |
|
num_active_users_result/0, |
51 |
|
delete_old_users_result/0]). |
52 |
|
|
53 |
|
%% API |
54 |
|
|
55 |
|
-spec list_users(jid:server()) -> [jid:literal_jid()]. |
56 |
|
list_users(Domain) -> |
57 |
9 |
Users = ejabberd_auth:get_vh_registered_users(Domain), |
58 |
9 |
SUsers = lists:sort(Users), |
59 |
9 |
[jid:to_binary(US) || US <- SUsers]. |
60 |
|
|
61 |
|
-spec count_users(jid:server()) -> integer(). |
62 |
|
count_users(Domain) -> |
63 |
2 |
ejabberd_auth:get_vh_registered_users_number(Domain). |
64 |
|
|
65 |
|
-spec register_generated_user(jid:server(), binary()) -> {register_result(), jid:literal_jid()}. |
66 |
|
register_generated_user(Host, Password) -> |
67 |
2 |
Username = generate_username(), |
68 |
2 |
JID = jid:to_binary({Username, Host}), |
69 |
2 |
{register_user(Username, Host, Password), JID}. |
70 |
|
|
71 |
|
-spec register_user(jid:user(), jid:server(), binary()) -> register_result(). |
72 |
|
register_user(User, Host, Password) -> |
73 |
2123 |
JID = jid:make(User, Host, <<>>), |
74 |
2123 |
case ejabberd_auth:try_register(JID, Password) of |
75 |
|
{error, exists} -> |
76 |
13 |
String = |
77 |
|
io_lib:format("User ~s already registered at node ~p", |
78 |
|
[jid:to_binary(JID), node()]), |
79 |
13 |
{exists, String}; |
80 |
|
{error, invalid_jid} -> |
81 |
1 |
String = io_lib:format("Invalid JID ~s@~s", [User, Host]), |
82 |
1 |
{invalid_jid, String}; |
83 |
|
{error, Reason} -> |
84 |
:-( |
String = |
85 |
|
io_lib:format("Can't register user ~s at node ~p: ~p", |
86 |
|
[jid:to_binary(JID), node(), Reason]), |
87 |
:-( |
{cannot_register, String}; |
88 |
|
_ -> |
89 |
2109 |
{ok, io_lib:format("User ~s successfully registered", [jid:to_binary(JID)])} |
90 |
|
end. |
91 |
|
|
92 |
|
-spec unregister_user(jid:user(), jid:server()) -> unregister_result(). |
93 |
|
unregister_user(User, Host) -> |
94 |
2157 |
JID = jid:make(User, Host, <<>>), |
95 |
2157 |
unregister_user(JID). |
96 |
|
|
97 |
|
-spec unregister_user(jid:jid()) -> unregister_result(). |
98 |
|
unregister_user(JID) -> |
99 |
2160 |
case ejabberd_auth:remove_user(JID) of |
100 |
|
ok -> |
101 |
2099 |
{ok, io_lib:format("User ~s successfully unregistered", [jid:to_binary(JID)])}; |
102 |
|
error -> |
103 |
1 |
{invalid_jid, "Invalid JID"}; |
104 |
|
{error, not_allowed} -> |
105 |
60 |
{not_allowed, "User does not exist or you are not authorised properly"} |
106 |
|
end. |
107 |
|
|
108 |
|
-spec change_password(jid:user(), jid:server(), binary()) -> change_password_result(). |
109 |
|
change_password(User, Host, Password) -> |
110 |
7 |
JID = jid:make(User, Host, <<>>), |
111 |
7 |
change_password(JID, Password). |
112 |
|
|
113 |
|
-spec change_password(jid:jid(), binary()) -> change_password_result(). |
114 |
|
change_password(JID, Password) -> |
115 |
12 |
Result = ejabberd_auth:set_password(JID, Password), |
116 |
12 |
format_change_password(Result). |
117 |
|
|
118 |
|
-spec check_account(jid:user(), jid:server()) -> check_account_result(). |
119 |
|
check_account(User, Host) -> |
120 |
2 |
JID = jid:make(User, Host, <<>>), |
121 |
2 |
check_account(JID). |
122 |
|
|
123 |
|
-spec check_account(jid:jid()) -> check_account_result(). |
124 |
|
check_account(JID) -> |
125 |
4 |
case ejabberd_auth:does_user_exist(JID) of |
126 |
|
true -> |
127 |
2 |
{ok, io_lib:format("User ~s exists", [jid:to_binary(JID)])}; |
128 |
|
false -> |
129 |
2 |
{user_does_not_exist, io_lib:format("User ~s does not exist", [jid:to_binary(JID)])} |
130 |
|
end. |
131 |
|
|
132 |
|
-spec check_password(jid:user(), jid:server(), binary()) -> check_password_result(). |
133 |
|
check_password(User, Host, Password) -> |
134 |
2 |
JID = jid:make(User, Host, <<>>), |
135 |
2 |
check_password(JID, Password). |
136 |
|
|
137 |
|
-spec check_password(jid:jid(), binary()) -> check_password_result(). |
138 |
|
check_password(JID, Password) -> |
139 |
5 |
case ejabberd_auth:does_user_exist(JID) of |
140 |
|
true -> |
141 |
4 |
case ejabberd_auth:check_password(JID, Password) of |
142 |
|
true -> |
143 |
2 |
{ok, io_lib:format("Password '~s' for user ~s is correct", |
144 |
|
[Password, jid:to_binary(JID)])}; |
145 |
|
false -> |
146 |
2 |
{incorrect, io_lib:format("Password '~s' for user ~s is incorrect", |
147 |
|
[Password, jid:to_binary(JID)])} |
148 |
|
end; |
149 |
|
false -> |
150 |
1 |
{user_does_not_exist, |
151 |
|
io_lib:format("Password ~s for user ~s is incorrect because this user does not" |
152 |
|
" exist", [Password, jid:to_binary(JID)])} |
153 |
|
end. |
154 |
|
|
155 |
|
-spec check_password_hash(jid:user(), jid:server(), string(), string()) -> |
156 |
|
check_password_hash_result(). |
157 |
|
check_password_hash(User, Host, PasswordHash, HashMethod) -> |
158 |
:-( |
JID = jid:make(User, Host, <<>>), |
159 |
:-( |
check_password_hash(JID, PasswordHash, HashMethod). |
160 |
|
|
161 |
|
-spec check_password_hash(jid:jid(), string(), string()) -> check_password_hash_result(). |
162 |
|
check_password_hash(JID, PasswordHash, HashMethod) -> |
163 |
2 |
AccountPass = ejabberd_auth:get_password_s(JID), |
164 |
2 |
AccountPassHash = case HashMethod of |
165 |
2 |
"md5" -> get_md5(AccountPass); |
166 |
:-( |
"sha" -> get_sha(AccountPass); |
167 |
:-( |
_ -> undefined |
168 |
|
end, |
169 |
2 |
case {AccountPass, AccountPassHash} of |
170 |
|
{<<>>, _} -> |
171 |
2 |
{wrong_user, "User does not exist or using SCRAM password"}; |
172 |
|
{_, undefined} -> |
173 |
:-( |
Msg = io_lib:format("Given hash method `~s` is not supported. Try `md5` or `sha`", |
174 |
|
[HashMethod]), |
175 |
:-( |
{wrong_method, Msg}; |
176 |
|
{_, PasswordHash} -> |
177 |
:-( |
{ok, "Password hash is correct"}; |
178 |
|
_-> |
179 |
:-( |
{incorrect, "Password hash is incorrect"} |
180 |
|
end. |
181 |
|
|
182 |
|
-spec num_active_users(jid:lserver(), integer()) -> num_active_users_result(). |
183 |
|
num_active_users(Domain, Days) -> |
184 |
4 |
TimeStamp = erlang:system_time(second), |
185 |
4 |
TS = TimeStamp - Days * 86400, |
186 |
4 |
try |
187 |
4 |
{ok, HostType} = mongoose_domain_api:get_domain_host_type(Domain), |
188 |
3 |
Num = mod_last:count_active_users(HostType, Domain, TS), |
189 |
3 |
{ok, Num} |
190 |
|
catch |
191 |
|
_:_ -> |
192 |
1 |
Message = io_lib:format("Cannot count active users for domain ~s", [Domain]), |
193 |
1 |
{cannot_count, Message} |
194 |
|
end. |
195 |
|
|
196 |
|
-spec ban_account(jid:user(), jid:server(), binary()) -> change_password_result(). |
197 |
|
ban_account(User, Host, ReasonText) -> |
198 |
1 |
JID = jid:make(User, Host, <<>>), |
199 |
1 |
ban_account(JID, ReasonText). |
200 |
|
|
201 |
|
-spec ban_account(jid:jid(), binary()) -> change_password_result(). |
202 |
|
ban_account(JID, ReasonText) -> |
203 |
3 |
Reason = mongoose_session_api:prepare_reason(ReasonText), |
204 |
3 |
mongoose_session_api:kick_sessions(JID, Reason), |
205 |
3 |
case set_random_password(JID, Reason) of |
206 |
|
ok -> |
207 |
2 |
{ok, io_lib:format("User ~s successfully banned with reason: ~s", |
208 |
|
[jid:to_binary(JID), ReasonText])}; |
209 |
|
ErrResult -> |
210 |
1 |
format_change_password(ErrResult) |
211 |
|
end. |
212 |
|
|
213 |
|
-spec delete_old_users(non_neg_integer()) -> {delete_old_users_result(), [jid:literal_jid()]}. |
214 |
|
delete_old_users(Days) -> |
215 |
:-( |
Users = list_old_users_raw(Days), |
216 |
:-( |
DeletedUsers = delete_users(Users), |
217 |
:-( |
{{ok, format_deleted_users(DeletedUsers)}, DeletedUsers}. |
218 |
|
|
219 |
|
-spec delete_old_users_for_domain(binary(), non_neg_integer()) -> |
220 |
|
{delete_old_users_result(), [jid:literal_jid()]}. |
221 |
|
delete_old_users_for_domain(Domain, Days) -> |
222 |
:-( |
Users = list_old_users_raw(Domain, Days), |
223 |
:-( |
DeletedUsers = delete_users(Users), |
224 |
:-( |
{{ok, format_deleted_users(DeletedUsers)}, DeletedUsers}. |
225 |
|
|
226 |
|
-spec list_old_users(non_neg_integer()) -> {ok, [jid:literal_jid()]}. |
227 |
|
list_old_users(Days) -> |
228 |
:-( |
Users = list_old_users_raw(Days), |
229 |
:-( |
{ok, lists:map(fun jid:to_binary/1, Users)}. |
230 |
|
|
231 |
|
-spec list_old_users_for_domain(binary(), non_neg_integer()) -> {ok, [jid:literal_jid()]}. |
232 |
|
list_old_users_for_domain(Domain, Days) -> |
233 |
:-( |
Users = list_old_users_raw(Domain, Days), |
234 |
:-( |
{ok, lists:map(fun jid:to_binary/1, Users)}. |
235 |
|
|
236 |
|
%% Internal |
237 |
|
|
238 |
|
-spec delete_users([jid:simple_bare_jid()]) -> [jid:literal_jid()]. |
239 |
|
delete_users(Users) -> |
240 |
:-( |
lists:map(fun({LUser, LServer}) -> |
241 |
:-( |
JID = jid:make(LUser, LServer, <<>>), |
242 |
:-( |
ok = ejabberd_auth:remove_user(JID), |
243 |
:-( |
jid:to_binary(JID) |
244 |
|
end, Users). |
245 |
|
|
246 |
|
-spec list_old_users_raw(non_neg_integer()) -> [jid:simple_bare_jid()]. |
247 |
|
list_old_users_raw(Days) -> |
248 |
:-( |
lists:append([list_old_users_raw(Domain, Days) || |
249 |
:-( |
HostType <- ?ALL_HOST_TYPES, |
250 |
:-( |
Domain <- mongoose_domain_api:get_domains_by_host_type(HostType)]). |
251 |
|
|
252 |
|
-spec list_old_users_raw(jid:lserver(), non_neg_integer()) -> [jid:simple_bare_jid()]. |
253 |
|
list_old_users_raw(Domain, Days) -> |
254 |
:-( |
Users = ejabberd_auth:get_vh_registered_users(Domain), |
255 |
|
% Convert older time |
256 |
:-( |
SecOlder = Days * 24 * 60 * 60, |
257 |
|
% Get current time |
258 |
:-( |
TimeStampNow = erlang:system_time(second), |
259 |
|
% Filter old users |
260 |
:-( |
lists:filter(fun(User) -> is_old_user(User, TimeStampNow, SecOlder) end, Users). |
261 |
|
|
262 |
|
-spec is_old_user(jid:simple_bare_jid(), non_neg_integer(), non_neg_integer()) -> boolean(). |
263 |
|
is_old_user({LUser, LServer}, TimeStampNow, SecOlder) -> |
264 |
:-( |
JID = jid:make(LUser, LServer, <<>>), |
265 |
|
% Check if the user is logged |
266 |
:-( |
case ejabberd_sm:get_user_resources(JID) of |
267 |
:-( |
[] -> is_user_nonactive_long_enough(JID, TimeStampNow, SecOlder); |
268 |
:-( |
_ -> false |
269 |
|
end. |
270 |
|
|
271 |
|
-spec is_user_nonactive_long_enough(jid:jid(), non_neg_integer(), non_neg_integer()) -> boolean(). |
272 |
|
is_user_nonactive_long_enough(JID, TimeStampNow, SecOlder) -> |
273 |
:-( |
{LUser, LServer} = jid:to_lus(JID), |
274 |
:-( |
{ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer), |
275 |
:-( |
case mod_last:get_last_info(HostType, LUser, LServer) of |
276 |
|
{ok, TimeStamp, _Status} -> |
277 |
|
% Get user age |
278 |
:-( |
Sec = TimeStampNow - TimeStamp, |
279 |
:-( |
case Sec < SecOlder of |
280 |
|
% Younger than SecOlder |
281 |
|
true -> |
282 |
:-( |
false; |
283 |
|
%% Older |
284 |
|
false -> |
285 |
:-( |
true |
286 |
|
end; |
287 |
|
not_found -> |
288 |
:-( |
true |
289 |
|
end. |
290 |
|
|
291 |
|
format_deleted_users(Users) -> |
292 |
:-( |
io_lib:format("Deleted ~p users: ~p", [length(Users), Users]). |
293 |
|
|
294 |
|
format_change_password(ok) -> |
295 |
7 |
{ok, "Password changed"}; |
296 |
|
format_change_password({error, empty_password}) -> |
297 |
3 |
{empty_password, "Empty password"}; |
298 |
|
format_change_password({error, not_allowed}) -> |
299 |
2 |
{not_allowed, "Password change not allowed"}; |
300 |
|
format_change_password({error, invalid_jid}) -> |
301 |
1 |
{invalid_jid, "Invalid JID"}. |
302 |
|
|
303 |
|
-spec set_random_password(JID, Reason) -> Result when |
304 |
|
JID :: jid:jid(), |
305 |
|
Reason :: binary(), |
306 |
|
Result :: ok | {error, any()}. |
307 |
|
set_random_password(JID, Reason) -> |
308 |
3 |
NewPass = build_random_password(Reason), |
309 |
3 |
ejabberd_auth:set_password(JID, NewPass). |
310 |
|
|
311 |
|
-spec build_random_password(Reason :: binary()) -> binary(). |
312 |
|
build_random_password(Reason) -> |
313 |
3 |
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(), |
314 |
3 |
Date = iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0wZ", |
315 |
|
[Year, Month, Day, Hour, Minute, Second])), |
316 |
3 |
RandomString = mongoose_bin:gen_from_crypto(), |
317 |
3 |
<<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>. |
318 |
|
|
319 |
|
-spec generate_username() -> binary(). |
320 |
|
generate_username() -> |
321 |
2 |
mongoose_bin:join([mongoose_bin:gen_from_timestamp(), |
322 |
|
mongoose_bin:gen_from_crypto()], $-). |
323 |
|
|
324 |
|
-spec get_md5(binary()) -> string(). |
325 |
|
get_md5(AccountPass) -> |
326 |
2 |
lists:flatten([io_lib:format("~.16B", [X]) |
327 |
2 |
|| X <- binary_to_list(crypto:hash(md5, AccountPass))]). |
328 |
|
|
329 |
|
-spec get_sha(binary()) -> string(). |
330 |
|
get_sha(AccountPass) -> |
331 |
:-( |
lists:flatten([io_lib:format("~.16B", [X]) |
332 |
:-( |
|| X <- binary_to_list(crypto:hash(sha, AccountPass))]). |