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