emqx/apps/emqx_dashboard/src/emqx_dashboard_admin.erl

242 lines
7.8 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Web dashboard admin authentication with username and password.
-module(emqx_dashboard_admin).
-include("emqx_dashboard.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-boot_mnesia({mnesia, [boot]}).
%% Mnesia bootstrap
-export([mnesia/1]).
-export([ add_user/3
, force_add_user/3
, remove_user/1
, update_user/2
, lookup_user/1
, change_password/2
, change_password/3
, all_users/0
, check/2
]).
-export([ sign_token/2
, verify_token/1
, destroy_token_by_username/2
]).
-export([ hash/1
, verify_hash/2
]).
-export([add_default_user/0]).
-type emqx_admin() :: #?ADMIN{}.
%%--------------------------------------------------------------------
%% Mnesia bootstrap
%%--------------------------------------------------------------------
mnesia(boot) ->
ok = mria:create_table(?ADMIN, [
{type, set},
{rlog_shard, ?DASHBOARD_SHARD},
{storage, disc_copies},
{record_name, ?ADMIN},
{attributes, record_info(fields, ?ADMIN)},
{storage_properties, [{ets, [{read_concurrency, true},
{write_concurrency, true}]}]}]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
-spec(add_user(binary(), binary(), binary()) -> {ok, map()} | {error, any()}).
add_user(Username, Password, Desc)
when is_binary(Username), is_binary(Password) ->
return(mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc])).
%% black-magic: force overwrite a user
force_add_user(Username, Password, Desc) ->
AddFun = fun() ->
mnesia:write(#?ADMIN{username = Username,
pwdhash = hash(Password),
description = Desc})
end,
case mria:transaction(?DASHBOARD_SHARD, AddFun) of
{atomic, ok} -> ok;
{aborted, Reason} -> {error, Reason}
end.
%% @private
add_user_(Username, Password, Desc) ->
case mnesia:wread({?ADMIN, Username}) of
[] ->
Admin = #?ADMIN{username = Username, pwdhash = hash(Password), description = Desc},
mnesia:write(Admin),
#{username => Username, description => Desc};
[_] ->
mnesia:abort(<<"Username Already Exist">>)
end.
-spec(remove_user(binary()) -> {ok, any()} | {error, any()}).
remove_user(Username) when is_binary(Username) ->
Trans = fun() ->
case lookup_user(Username) of
[] -> mnesia:abort(<<"Username Not Found">>);
_ -> mnesia:delete({?ADMIN, Username})
end
end,
return(mria:transaction(?DASHBOARD_SHARD, Trans)).
-spec(update_user(binary(), binary()) -> {ok, map()} | {error, term()}).
update_user(Username, Desc) when is_binary(Username) ->
return(mria:transaction(?DASHBOARD_SHARD, fun update_user_/2, [Username, Desc])).
hash(Password) ->
SaltBin = emqx_dashboard_token:salt(),
<<SaltBin/binary, (sha256(SaltBin, Password))/binary>>.
verify_hash(Origin, SaltHash) ->
case SaltHash of
<<Salt:4/binary, Hash/binary>> ->
case Hash =:= sha256(Salt, Origin) of
true -> ok;
false -> error
end;
_ -> error
end.
sha256(SaltBin, Password) ->
crypto:hash('sha256', <<SaltBin/binary, Password/binary>>).
%% @private
update_user_(Username, Desc) ->
case mnesia:wread({?ADMIN, Username}) of
[] ->
mnesia:abort(<<"Username Not Found">>);
[Admin] ->
mnesia:write(Admin#?ADMIN{description = Desc}),
#{username => Username, description => Desc}
end.
change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) ->
case check(Username, OldPasswd) of
ok -> change_password(Username, NewPasswd);
Error -> Error
end.
change_password(Username, Password) when is_binary(Username), is_binary(Password) ->
change_password_hash(Username, hash(Password)).
change_password_hash(Username, PasswordHash) ->
update_pwd(Username, fun(User) ->
User#?ADMIN{pwdhash = PasswordHash}
end).
update_pwd(Username, Fun) ->
Trans = fun() ->
User =
case lookup_user(Username) of
[Admin] -> Admin;
[] ->
mnesia:abort(<<"Username Not Found">>)
end,
mnesia:write(Fun(User))
end,
return(mria:transaction(?DASHBOARD_SHARD, Trans)).
-spec(lookup_user(binary()) -> [emqx_admin()]).
lookup_user(Username) when is_binary(Username) ->
Fun = fun() -> mnesia:read(?ADMIN, Username) end,
{atomic, User} = mria:ro_transaction(?DASHBOARD_SHARD, Fun),
User.
-spec(all_users() -> [map()]).
all_users() ->
lists:map(fun(#?ADMIN{username = Username,
description = Desc
}) ->
#{username => Username,
description => Desc
}
end, ets:tab2list(?ADMIN)).
return({atomic, Result}) ->
{ok, Result};
return({aborted, Reason}) ->
{error, Reason}.
check(undefined, _) ->
{error, <<"username_not_provided">>};
check(_, undefined) ->
{error, <<"password_not_provided">>};
check(Username, Password) ->
case lookup_user(Username) of
[#?ADMIN{pwdhash = PwdHash}] ->
case verify_hash(Password, PwdHash) of
ok -> ok;
error -> {error, <<"password_error">>}
end;
[] ->
{error, <<"username_not_found">>}
end.
%%--------------------------------------------------------------------
%% token
sign_token(Username, Password) ->
case check(Username, Password) of
ok ->
emqx_dashboard_token:sign(Username, Password);
Error ->
Error
end.
verify_token(Token) ->
emqx_dashboard_token:verify(Token).
destroy_token_by_username(Username, Token) ->
case emqx_dashboard_token:lookup(Token) of
{ok, #?ADMIN_JWT{username = Username}} ->
emqx_dashboard_token:destroy(Token);
_ ->
{error, not_found}
end.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
-spec(add_default_user() -> {ok, map() | empty | default_user_exists } | {error, any()}).
add_default_user() ->
add_default_user(binenv(default_username), binenv(default_password)).
binenv(Key) ->
iolist_to_binary(emqx_conf:get([emqx_dashboard, Key], "")).
add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY(Password) ->
{ok, empty};
add_default_user(Username, Password) ->
case lookup_user(Username) of
[] -> add_user(Username, Password, <<"administrator">>);
_ -> {ok, default_user_exists}
end.