emqx/apps/emqx_dashboard/src/emqx_dashboard_api.erl

351 lines
12 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.
%%--------------------------------------------------------------------
-module(emqx_dashboard_api).
-behaviour(minirest_api).
-include("emqx_dashboard.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("typerefl/include/types.hrl").
-import(hoconsc, [
mk/2,
array/1,
enum/1
]).
-export([
api_spec/0,
fields/1,
paths/0,
schema/1,
namespace/0
]).
-export([
login/2,
logout/2,
users/2,
user/2,
change_pwd/2
]).
-define(EMPTY(V), (V == undefined orelse V == <<>>)).
-define(WRONG_USERNAME_OR_PWD, 'WRONG_USERNAME_OR_PWD').
-define(WRONG_TOKEN_OR_USERNAME, 'WRONG_TOKEN_OR_USERNAME').
-define(USER_NOT_FOUND, 'USER_NOT_FOUND').
-define(ERROR_PWD_NOT_MATCH, 'ERROR_PWD_NOT_MATCH').
-define(NOT_ALLOWED, 'NOT_ALLOWED').
-define(BAD_REQUEST, 'BAD_REQUEST').
namespace() -> "dashboard".
api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
paths() ->
[
"/login",
"/logout",
"/users",
"/users/:username",
"/users/:username/change_pwd"
].
schema("/login") ->
#{
'operationId' => login,
post => #{
tags => [<<"dashboard">>],
desc => ?DESC(login_api),
summary => <<"Dashboard Auth">>,
'requestBody' => fields([username, password]),
responses => #{
200 => fields([token, version, license]),
401 => response_schema(401)
},
security => []
}
};
schema("/logout") ->
#{
'operationId' => logout,
post => #{
tags => [<<"dashboard">>],
desc => ?DESC(logout_api),
'requestBody' => fields([username]),
responses => #{
204 => <<"Dashboard logout successfully">>,
401 => response_schema(401)
}
}
};
schema("/users") ->
#{
'operationId' => users,
get => #{
tags => [<<"dashboard">>],
desc => ?DESC(list_users_api),
responses => #{
200 => mk(
array(hoconsc:ref(user)),
#{desc => ?DESC(list_users_api)}
)
}
},
post => #{
tags => [<<"dashboard">>],
desc => ?DESC(create_user_api),
'requestBody' => fields([username, password, description]),
responses => #{
200 => fields([username, description])
}
}
};
schema("/users/:username") ->
#{
'operationId' => user,
put => #{
tags => [<<"dashboard">>],
desc => ?DESC(update_user_api),
parameters => fields([username_in_path]),
'requestBody' => fields([description]),
responses => #{
200 => fields([username, description]),
404 => response_schema(404)
}
},
delete => #{
tags => [<<"dashboard">>],
desc => ?DESC(delete_user_api),
parameters => fields([username_in_path]),
responses => #{
204 => <<"Delete User successfully">>,
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST, ?NOT_ALLOWED], ?DESC(login_failed_response400)
),
404 => response_schema(404)
}
}
};
schema("/users/:username/change_pwd") ->
#{
'operationId' => change_pwd,
put => #{
tags => [<<"dashboard">>],
desc => ?DESC(change_pwd_api),
parameters => fields([username_in_path]),
'requestBody' => fields([old_pwd, new_pwd]),
responses => #{
204 => <<"Update user password successfully">>,
404 => response_schema(404),
400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST, ?ERROR_PWD_NOT_MATCH],
?DESC(login_failed_response400)
)
}
}
}.
response_schema(401) ->
emqx_dashboard_swagger:error_codes([?WRONG_USERNAME_OR_PWD], ?DESC(login_failed401));
response_schema(404) ->
emqx_dashboard_swagger:error_codes([?USER_NOT_FOUND], ?DESC(users_api404)).
fields(user) ->
fields([username, description]);
fields(List) ->
[field(Key) || Key <- List].
field(username) ->
{username,
mk(binary(), #{desc => ?DESC(username), 'maxLength' => 100, example => <<"admin">>})};
field(username_in_path) ->
{username,
mk(binary(), #{
desc => ?DESC(username),
example => <<"admin">>,
in => path,
required => true
})};
field(password) ->
{password,
mk(binary(), #{desc => ?DESC(password), 'maxLength' => 100, example => <<"public">>})};
field(description) ->
{description, mk(binary(), #{desc => ?DESC(user_description), example => <<"administrator">>})};
field(token) ->
{token, mk(binary(), #{desc => ?DESC(token)})};
field(license) ->
{license, [
{edition,
mk(
enum([opensource, enterprise]),
#{desc => ?DESC(license), example => opensource}
)}
]};
field(version) ->
{version, mk(string(), #{desc => ?DESC(version), example => <<"5.0.0">>})};
field(old_pwd) ->
{old_pwd, mk(binary(), #{desc => ?DESC(old_pwd)})};
field(new_pwd) ->
{new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})}.
%% -------------------------------------------------------------------------------------------------
%% API
login(post, #{body := Params}) ->
Username = maps:get(<<"username">>, Params),
Password = maps:get(<<"password">>, Params),
case emqx_dashboard_admin:sign_token(Username, Password) of
{ok, Token} ->
?SLOG(info, #{msg => "Dashboard login successfully", username => Username}),
Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
{200, #{
token => Token,
version => Version,
license => #{edition => emqx_release:edition()}
}};
{error, R} ->
?SLOG(info, #{msg => "Dashboard login failed", username => Username, reason => R}),
{401, ?WRONG_USERNAME_OR_PWD, <<"Auth failed">>}
end.
logout(_, #{
body := #{<<"username">> := Username},
headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}
}) ->
case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of
ok ->
?SLOG(info, #{msg => "Dashboard logout successfully", username => Username}),
204;
_R ->
?SLOG(info, #{msg => "Dashboard logout failed.", username => Username}),
{401, ?WRONG_TOKEN_OR_USERNAME, <<"Ensure your token & username">>}
end.
users(get, _Request) ->
{200, emqx_dashboard_admin:all_users()};
users(post, #{body := Params}) ->
Desc = maps:get(<<"description">>, Params, <<"">>),
Username = maps:get(<<"username">>, Params),
Password = maps:get(<<"password">>, Params),
case ?EMPTY(Username) orelse ?EMPTY(Password) of
true ->
{400, ?BAD_REQUEST, <<"Username or password undefined">>};
false ->
case emqx_dashboard_admin:add_user(Username, Password, Desc) of
{ok, Result} ->
?SLOG(info, #{msg => "Create dashboard success", username => Username}),
{200, Result};
{error, Reason} ->
?SLOG(info, #{
msg => "Create dashboard failed",
username => Username,
reason => Reason
}),
{400, ?BAD_REQUEST, Reason}
end
end.
user(put, #{bindings := #{username := Username}, body := Params}) ->
Desc = maps:get(<<"description">>, Params),
case emqx_dashboard_admin:update_user(Username, Desc) of
{ok, Result} ->
{200, Result};
{error, Reason} ->
{404, ?USER_NOT_FOUND, Reason}
end;
user(delete, #{bindings := #{username := Username}, headers := Headers}) ->
case Username == emqx_dashboard_admin:default_username() of
true ->
?SLOG(info, #{msg => "Dashboard delete admin user failed", username => Username}),
Message = list_to_binary(io_lib:format("Cannot delete user ~p", [Username])),
{400, ?NOT_ALLOWED, Message};
false ->
case is_self_auth(Username, Headers) of
true ->
{400, ?NOT_ALLOWED, <<"Cannot delete self">>};
false ->
case emqx_dashboard_admin:remove_user(Username) of
{error, Reason} ->
{404, ?USER_NOT_FOUND, Reason};
{ok, _} ->
?SLOG(info, #{
msg => "Dashboard delete admin user", username => Username
}),
{204}
end
end
end.
is_self_auth(Username, #{<<"authorization">> := Token}) ->
is_self_auth(Username, Token);
is_self_auth(Username, #{<<"Authorization">> := Token}) ->
is_self_auth(Username, Token);
is_self_auth(Username, <<"basic ", Token/binary>>) ->
is_self_auth_basic(Username, Token);
is_self_auth(Username, <<"Basic ", Token/binary>>) ->
is_self_auth_basic(Username, Token);
is_self_auth(Username, <<"bearer ", Token/binary>>) ->
is_self_auth_token(Username, Token);
is_self_auth(Username, <<"Bearer ", Token/binary>>) ->
is_self_auth_token(Username, Token).
is_self_auth_basic(Username, Token) ->
UP = base64:decode(Token),
case binary:match(UP, Username) of
{0, N} ->
binary:part(UP, {N, 1}) == <<":">>;
_ ->
false
end.
is_self_auth_token(Username, Token) ->
case emqx_dashboard_token:owner(Token) of
{ok, Owner} ->
Owner == Username;
{error, _NotFound} ->
false
end.
change_pwd(put, #{bindings := #{username := Username}, body := Params}) ->
LogMeta = #{msg => "Dashboard change password", username => Username},
OldPwd = maps:get(<<"old_pwd">>, Params),
NewPwd = maps:get(<<"new_pwd">>, Params),
case ?EMPTY(OldPwd) orelse ?EMPTY(NewPwd) of
true ->
?SLOG(error, LogMeta#{result => failed, reason => "password undefined or empty"}),
{400, ?BAD_REQUEST, <<"Old password or new password undefined">>};
false ->
case emqx_dashboard_admin:change_password(Username, OldPwd, NewPwd) of
{ok, _} ->
?SLOG(info, LogMeta#{result => success}),
{204};
{error, <<"username_not_found">>} ->
?SLOG(error, LogMeta#{result => failed, reason => "username not found"}),
{404, ?USER_NOT_FOUND, <<"User not found">>};
{error, <<"password_error">>} ->
?SLOG(error, LogMeta#{result => failed, reason => "error old pwd"}),
{400, ?ERROR_PWD_NOT_MATCH, <<"Old password not match">>};
{error, Reason} ->
?SLOG(error, LogMeta#{result => failed, reason => Reason}),
{400, ?BAD_REQUEST, Reason}
end
end.