./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 -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 5632 JID = jid:make_bare(User, Host),
87 5632 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 5611 {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 5686 JID = jid:make_bare(User, Host),
111 5686 unregister_user(JID).
112
113 -spec unregister_user(jid:jid()) -> unregister_result().
114 unregister_user(JID) ->
115 5710 case ejabberd_auth:remove_user(JID) of
116 ok ->
117 5577 {ok, io_lib:format("User ~s successfully unregistered", [jid:to_binary(JID)])};
118 error ->
119 1 {invalid_jid, "Invalid JID"};
120 {error, _} ->
121 132 {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))]).
Line Hits Source