./ct_report/coverage/mongoose_account_api.COVER.html

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))]).
Line Hits Source