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 |
:-( |
PrepDomain = jid:nameprep(Domain), |
49 |
:-( |
case mongoose_domain_api:get_domain_host_type(PrepDomain) of |
50 |
|
{ok, _} -> |
51 |
:-( |
Users = ejabberd_auth:get_vh_registered_users(PrepDomain), |
52 |
:-( |
SUsers = lists:sort(Users), |
53 |
:-( |
{ok, [jid:to_binary(US) || US <- SUsers]}; |
54 |
|
{error, not_found} -> |
55 |
:-( |
{domain_not_found, "Domain does not exist"} |
56 |
|
end. |
57 |
|
|
58 |
|
-spec count_users(jid:server()) -> count_user_result(). |
59 |
|
count_users(Domain) -> |
60 |
:-( |
PrepDomain = jid:nameprep(Domain), |
61 |
:-( |
case mongoose_domain_api:get_domain_host_type(PrepDomain) of |
62 |
|
{ok, _} -> |
63 |
:-( |
UserCount = ejabberd_auth:get_vh_registered_users_number(PrepDomain), |
64 |
:-( |
{ok, UserCount}; |
65 |
|
{error, not_found} -> |
66 |
:-( |
{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 |
:-( |
Username = generate_username(), |
72 |
:-( |
JID = jid:to_binary({Username, Host}), |
73 |
:-( |
{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 |
552 |
JID = jid:make_bare(User, Host), |
78 |
552 |
case ejabberd_auth:try_register(JID, Password) of |
79 |
|
{error, exists} -> |
80 |
:-( |
String = |
81 |
|
io_lib:format("User ~s already registered at node ~p", |
82 |
|
[jid:to_binary(JID), node()]), |
83 |
:-( |
{exists, String}; |
84 |
|
{error, invalid_jid} -> |
85 |
:-( |
String = io_lib:format("Invalid JID ~s@~s", [User, Host]), |
86 |
:-( |
{invalid_jid, String}; |
87 |
|
{error, limit_per_domain_exceeded} -> |
88 |
:-( |
String = io_lib:format("User limit has been exceeded for domain ~s", [Host]), |
89 |
:-( |
{limit_per_domain_exceeded, String}; |
90 |
|
{error, Reason} -> |
91 |
:-( |
String = |
92 |
|
io_lib:format("Can't register user ~s at node ~p: ~p", |
93 |
|
[jid:to_binary(JID), node(), Reason]), |
94 |
:-( |
{cannot_register, String}; |
95 |
|
_ -> |
96 |
552 |
{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 |
556 |
JID = jid:make_bare(User, Host), |
102 |
556 |
unregister_user(JID). |
103 |
|
|
104 |
|
-spec unregister_user(jid:jid()) -> unregister_result(). |
105 |
|
unregister_user(JID) -> |
106 |
570 |
case ejabberd_auth:remove_user(JID) of |
107 |
|
ok -> |
108 |
552 |
{ok, io_lib:format("User ~s successfully unregistered", [jid:to_binary(JID)])}; |
109 |
|
error -> |
110 |
:-( |
{invalid_jid, "Invalid JID"}; |
111 |
|
{error, _} -> |
112 |
18 |
{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 |
:-( |
JID = jid:make_bare(User, Host), |
118 |
:-( |
change_password(JID, Password). |
119 |
|
|
120 |
|
-spec change_password(jid:jid(), binary()) -> change_password_result(). |
121 |
|
change_password(JID, Password) -> |
122 |
:-( |
Result = ejabberd_auth:set_password(JID, Password), |
123 |
:-( |
format_change_password(Result). |
124 |
|
|
125 |
|
-spec check_account(jid:jid()) -> check_account_result(). |
126 |
|
check_account(JID) -> |
127 |
:-( |
case ejabberd_auth:does_user_exist(JID) of |
128 |
|
true -> |
129 |
:-( |
{ok, io_lib:format("User ~s exists", [jid:to_binary(JID)])}; |
130 |
|
false -> |
131 |
:-( |
{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 |
:-( |
case ejabberd_auth:does_user_exist(JID) of |
137 |
|
true -> |
138 |
:-( |
case ejabberd_auth:check_password(JID, Password) of |
139 |
|
true -> |
140 |
:-( |
{ok, io_lib:format("Password '~s' for user ~s is correct", |
141 |
|
[Password, jid:to_binary(JID)])}; |
142 |
|
false -> |
143 |
:-( |
{incorrect, io_lib:format("Password '~s' for user ~s is incorrect", |
144 |
|
[Password, jid:to_binary(JID)])} |
145 |
|
end; |
146 |
|
false -> |
147 |
:-( |
{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 |
:-( |
AccountPass = ejabberd_auth:get_password_s(JID), |
155 |
:-( |
AccountPassHash = case HashMethod of |
156 |
:-( |
"md5" -> get_md5(AccountPass); |
157 |
:-( |
"sha" -> get_sha(AccountPass); |
158 |
:-( |
_ -> undefined |
159 |
|
end, |
160 |
:-( |
case {AccountPass, AccountPassHash} of |
161 |
|
{<<>>, _} -> |
162 |
:-( |
{wrong_user, "User does not exist or using SCRAM password"}; |
163 |
|
{_, undefined} -> |
164 |
:-( |
Msg = io_lib:format("Given hash method `~s` is not supported. Try `md5` or `sha`", |
165 |
|
[HashMethod]), |
166 |
:-( |
{wrong_method, Msg}; |
167 |
|
{_, PasswordHash} -> |
168 |
:-( |
{ok, "Password hash is correct"}; |
169 |
|
_-> |
170 |
:-( |
{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 |
:-( |
case mongoose_import_users:run(Filename) of |
177 |
|
{ok, Summary} -> |
178 |
:-( |
{ok, maps:fold( |
179 |
|
fun(Reason, List, Map) -> |
180 |
:-( |
List2 = [{ok, El} || El <- List], |
181 |
:-( |
maps:put(from_reason(Reason), List2, Map) |
182 |
|
end, |
183 |
|
#{<<"status">> => <<"Completed">>}, |
184 |
|
Summary)}; |
185 |
|
{error, file_not_found} -> |
186 |
:-( |
{file_not_found, <<"File not found">>} |
187 |
|
end. |
188 |
|
|
189 |
|
-spec from_reason(mongoose_import_users:reason()) -> binary(). |
190 |
:-( |
from_reason(ok) -> <<"created">>; |
191 |
:-( |
from_reason(exists) -> <<"existing">>; |
192 |
:-( |
from_reason(not_allowed) -> <<"notAllowed">>; |
193 |
:-( |
from_reason(invalid_jid) -> <<"invalidJID">>; |
194 |
:-( |
from_reason(null_password) -> <<"emptyPassword">>; |
195 |
:-( |
from_reason(bad_csv) -> <<"invalidRecord">>. |
196 |
|
|
197 |
|
-spec ban_account(jid:jid(), binary()) -> change_password_result(). |
198 |
|
ban_account(JID, Reason) -> |
199 |
:-( |
case ejabberd_auth:does_user_exist(JID) of |
200 |
|
true -> |
201 |
:-( |
mongoose_session_api:kick_sessions(JID, Reason), |
202 |
:-( |
case set_random_password(JID, Reason) of |
203 |
|
ok -> |
204 |
:-( |
{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 |
:-( |
{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 |
:-( |
{ok, "Password changed"}; |
217 |
|
format_change_password({error, empty_password}) -> |
218 |
:-( |
{empty_password, "Empty password"}; |
219 |
|
format_change_password({error, not_allowed}) -> |
220 |
:-( |
{not_allowed, "User does not exist or you are not authorized properly"}; |
221 |
|
format_change_password({error, invalid_jid}) -> |
222 |
:-( |
{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 |
:-( |
NewPass = build_random_password(Reason), |
230 |
:-( |
ejabberd_auth:set_password(JID, NewPass). |
231 |
|
|
232 |
|
-spec build_random_password(Reason :: binary()) -> binary(). |
233 |
|
build_random_password(Reason) -> |
234 |
:-( |
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(), |
235 |
:-( |
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 |
:-( |
RandomString = mongoose_bin:gen_from_crypto(), |
238 |
:-( |
<<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>. |
239 |
|
|
240 |
|
-spec generate_username() -> binary(). |
241 |
|
generate_username() -> |
242 |
:-( |
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 |
:-( |
lists:flatten([io_lib:format("~.16B", [X]) |
248 |
:-( |
|| X <- binary_to_list(crypto:hash(md5, AccountPass))]). |
249 |
|
|
250 |
|
-spec get_sha(binary()) -> string(). |
251 |
|
get_sha(AccountPass) -> |
252 |
:-( |
lists:flatten([io_lib:format("~.16B", [X]) |
253 |
:-( |
|| X <- binary_to_list(crypto:hash(sha, AccountPass))]). |