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