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 |
|
|
21 |
|
-type register_result() :: {ok | exists | invalid_jid | cannot_register, iolist()}. |
22 |
|
|
23 |
|
-type unregister_result() :: {ok | not_allowed | invalid_jid, string()}. |
24 |
|
|
25 |
|
-type change_password_result() :: {ok | empty_password | not_allowed | invalid_jid, string()}. |
26 |
|
|
27 |
|
-type check_password_result() :: {ok | incorrect | user_does_not_exist, string()}. |
28 |
|
|
29 |
|
-type check_password_hash_result() :: {ok | incorrect | wrong_user | wrong_method, string()}. |
30 |
|
|
31 |
|
-type check_account_result() :: {ok | user_does_not_exist, string()}. |
32 |
|
|
33 |
|
-export_type([register_result/0, |
34 |
|
unregister_result/0, |
35 |
|
change_password_result/0, |
36 |
|
check_password_result/0, |
37 |
|
check_password_hash_result/0, |
38 |
|
check_account_result/0]). |
39 |
|
|
40 |
|
%% API |
41 |
|
|
42 |
|
-spec list_users(jid:server()) -> [jid:literal_jid()]. |
43 |
|
list_users(Domain) -> |
44 |
9 |
Users = ejabberd_auth:get_vh_registered_users(Domain), |
45 |
9 |
SUsers = lists:sort(Users), |
46 |
9 |
[jid:to_binary(US) || US <- SUsers]. |
47 |
|
|
48 |
|
-spec count_users(jid:server()) -> integer(). |
49 |
|
count_users(Domain) -> |
50 |
2 |
ejabberd_auth:get_vh_registered_users_number(Domain). |
51 |
|
|
52 |
|
-spec register_generated_user(jid:server(), binary()) -> {register_result(), jid:literal_jid()}. |
53 |
|
register_generated_user(Host, Password) -> |
54 |
2 |
Username = generate_username(), |
55 |
2 |
JID = jid:to_binary({Username, Host}), |
56 |
2 |
{register_user(Username, Host, Password), JID}. |
57 |
|
|
58 |
|
-spec register_user(jid:user(), jid:server(), binary()) -> register_result(). |
59 |
|
register_user(User, Host, Password) -> |
60 |
2824 |
JID = jid:make(User, Host, <<>>), |
61 |
2824 |
case ejabberd_auth:try_register(JID, Password) of |
62 |
|
{error, exists} -> |
63 |
7 |
String = |
64 |
|
io_lib:format("User ~s already registered at node ~p", |
65 |
|
[jid:to_binary(JID), node()]), |
66 |
7 |
{exists, String}; |
67 |
|
{error, invalid_jid} -> |
68 |
1 |
String = io_lib:format("Invalid JID ~s@~s", [User, Host]), |
69 |
1 |
{invalid_jid, String}; |
70 |
|
{error, Reason} -> |
71 |
:-( |
String = |
72 |
|
io_lib:format("Can't register user ~s at node ~p: ~p", |
73 |
|
[jid:to_binary(JID), node(), Reason]), |
74 |
:-( |
{cannot_register, String}; |
75 |
|
_ -> |
76 |
2816 |
{ok, io_lib:format("User ~s successfully registered", [jid:to_binary(JID)])} |
77 |
|
end. |
78 |
|
|
79 |
|
-spec unregister_user(jid:user(), jid:server()) -> unregister_result(). |
80 |
|
unregister_user(User, Host) -> |
81 |
2851 |
JID = jid:make(User, Host, <<>>), |
82 |
2851 |
unregister_user(JID). |
83 |
|
|
84 |
|
-spec unregister_user(jid:jid()) -> unregister_result(). |
85 |
|
unregister_user(JID) -> |
86 |
2854 |
case ejabberd_auth:remove_user(JID) of |
87 |
|
ok -> |
88 |
2847 |
{ok, io_lib:format("User ~s successfully unregistered", [jid:to_binary(JID)])}; |
89 |
|
error -> |
90 |
1 |
{invalid_jid, "Invalid JID"}; |
91 |
|
{error, not_allowed} -> |
92 |
6 |
{not_allowed, "User does not exist or you are not authorised properly"} |
93 |
|
end. |
94 |
|
|
95 |
|
-spec change_password(jid:user(), jid:server(), binary()) -> change_password_result(). |
96 |
|
change_password(User, Host, Password) -> |
97 |
7 |
JID = jid:make(User, Host, <<>>), |
98 |
7 |
change_password(JID, Password). |
99 |
|
|
100 |
|
-spec change_password(jid:jid(), binary()) -> change_password_result(). |
101 |
|
change_password(JID, Password) -> |
102 |
12 |
Result = ejabberd_auth:set_password(JID, Password), |
103 |
12 |
format_change_password(Result). |
104 |
|
|
105 |
|
-spec check_account(jid:user(), jid:server()) -> check_account_result(). |
106 |
|
check_account(User, Host) -> |
107 |
13 |
JID = jid:make(User, Host, <<>>), |
108 |
13 |
check_account(JID). |
109 |
|
|
110 |
|
-spec check_account(jid:jid()) -> check_account_result(). |
111 |
|
check_account(JID) -> |
112 |
15 |
case ejabberd_auth:does_user_exist(JID) of |
113 |
|
true -> |
114 |
8 |
{ok, io_lib:format("User ~s exists", [jid:to_binary(JID)])}; |
115 |
|
false -> |
116 |
7 |
{user_does_not_exist, io_lib:format("User ~s does not exist", [jid:to_binary(JID)])} |
117 |
|
end. |
118 |
|
|
119 |
|
-spec check_password(jid:user(), jid:server(), binary()) -> check_password_result(). |
120 |
|
check_password(User, Host, Password) -> |
121 |
2 |
JID = jid:make(User, Host, <<>>), |
122 |
2 |
check_password(JID, Password). |
123 |
|
|
124 |
|
-spec check_password(jid:jid(), binary()) -> check_password_result(). |
125 |
|
check_password(JID, Password) -> |
126 |
5 |
case ejabberd_auth:does_user_exist(JID) of |
127 |
|
true -> |
128 |
4 |
case ejabberd_auth:check_password(JID, Password) of |
129 |
|
true -> |
130 |
2 |
{ok, io_lib:format("Password '~s' for user ~s is correct", |
131 |
|
[Password, jid:to_binary(JID)])}; |
132 |
|
false -> |
133 |
2 |
{incorrect, io_lib:format("Password '~s' for user ~s is incorrect", |
134 |
|
[Password, jid:to_binary(JID)])} |
135 |
|
end; |
136 |
|
false -> |
137 |
1 |
{user_does_not_exist, |
138 |
|
io_lib:format("Password ~s for user ~s is incorrect because this user does not" |
139 |
|
" exist", [Password, jid:to_binary(JID)])} |
140 |
|
end. |
141 |
|
|
142 |
|
-spec check_password_hash(jid:user(), jid:server(), string(), string()) -> |
143 |
|
check_password_hash_result(). |
144 |
|
check_password_hash(User, Host, PasswordHash, HashMethod) -> |
145 |
3 |
JID = jid:make(User, Host, <<>>), |
146 |
3 |
check_password_hash(JID, PasswordHash, HashMethod). |
147 |
|
|
148 |
|
-spec check_password_hash(jid:jid(), string(), string()) -> check_password_hash_result(). |
149 |
|
check_password_hash(JID, PasswordHash, HashMethod) -> |
150 |
8 |
AccountPass = ejabberd_auth:get_password_s(JID), |
151 |
8 |
AccountPassHash = case HashMethod of |
152 |
6 |
"md5" -> get_md5(AccountPass); |
153 |
1 |
"sha" -> get_sha(AccountPass); |
154 |
1 |
_ -> undefined |
155 |
|
end, |
156 |
8 |
case {AccountPass, AccountPassHash} of |
157 |
|
{<<>>, _} -> |
158 |
2 |
{wrong_user, "User does not exist or using SCRAM password"}; |
159 |
|
{_, undefined} -> |
160 |
1 |
Msg = io_lib:format("Given hash method `~s` is not supported. Try `md5` or `sha`", |
161 |
|
[HashMethod]), |
162 |
1 |
{wrong_method, Msg}; |
163 |
|
{_, PasswordHash} -> |
164 |
3 |
{ok, "Password hash is correct"}; |
165 |
|
_-> |
166 |
2 |
{incorrect, "Password hash is incorrect"} |
167 |
|
end. |
168 |
|
|
169 |
|
-spec ban_account(jid:user(), jid:server(), binary()) -> change_password_result(). |
170 |
|
ban_account(User, Host, ReasonText) -> |
171 |
1 |
JID = jid:make(User, Host, <<>>), |
172 |
1 |
ban_account(JID, ReasonText). |
173 |
|
|
174 |
|
-spec ban_account(jid:jid(), binary()) -> change_password_result(). |
175 |
|
ban_account(JID, ReasonText) -> |
176 |
3 |
Reason = mongoose_session_api:prepare_reason(ReasonText), |
177 |
3 |
mongoose_session_api:kick_sessions(JID, Reason), |
178 |
3 |
case set_random_password(JID, Reason) of |
179 |
|
ok -> |
180 |
2 |
{ok, io_lib:format("User ~s successfully banned with reason: ~s", |
181 |
|
[jid:to_binary(JID), ReasonText])}; |
182 |
|
ErrResult -> |
183 |
1 |
format_change_password(ErrResult) |
184 |
|
end. |
185 |
|
|
186 |
|
%% Internal |
187 |
|
|
188 |
|
format_change_password(ok) -> |
189 |
7 |
{ok, "Password changed"}; |
190 |
|
format_change_password({error, empty_password}) -> |
191 |
3 |
{empty_password, "Empty password"}; |
192 |
|
format_change_password({error, not_allowed}) -> |
193 |
2 |
{not_allowed, "Password change not allowed"}; |
194 |
|
format_change_password({error, invalid_jid}) -> |
195 |
1 |
{invalid_jid, "Invalid JID"}. |
196 |
|
|
197 |
|
-spec set_random_password(JID, Reason) -> Result when |
198 |
|
JID :: jid:jid(), |
199 |
|
Reason :: binary(), |
200 |
|
Result :: ok | {error, any()}. |
201 |
|
set_random_password(JID, Reason) -> |
202 |
3 |
NewPass = build_random_password(Reason), |
203 |
3 |
ejabberd_auth:set_password(JID, NewPass). |
204 |
|
|
205 |
|
-spec build_random_password(Reason :: binary()) -> binary(). |
206 |
|
build_random_password(Reason) -> |
207 |
3 |
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(), |
208 |
3 |
Date = iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0wZ", |
209 |
|
[Year, Month, Day, Hour, Minute, Second])), |
210 |
3 |
RandomString = mongoose_bin:gen_from_crypto(), |
211 |
3 |
<<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>. |
212 |
|
|
213 |
|
-spec generate_username() -> binary(). |
214 |
|
generate_username() -> |
215 |
2 |
mongoose_bin:join([mongoose_bin:gen_from_timestamp(), |
216 |
|
mongoose_bin:gen_from_crypto()], $-). |
217 |
|
|
218 |
|
-spec get_md5(binary()) -> string(). |
219 |
|
get_md5(AccountPass) -> |
220 |
6 |
lists:flatten([io_lib:format("~.16B", [X]) |
221 |
6 |
|| X <- binary_to_list(crypto:hash(md5, AccountPass))]). |
222 |
|
|
223 |
|
-spec get_sha(binary()) -> string(). |
224 |
|
get_sha(AccountPass) -> |
225 |
1 |
lists:flatten([io_lib:format("~.16B", [X]) |
226 |
1 |
|| X <- binary_to_list(crypto:hash(sha, AccountPass))]). |