410 lines
14 KiB
Erlang
410 lines
14 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2020-2023 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(BAD_USERNAME_OR_PWD, 'BAD_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 authentication">>,
|
|
'requestBody' => fields([username, password]),
|
|
responses => #{
|
|
200 => fields([role, token, version, license]),
|
|
401 => response_schema(401)
|
|
},
|
|
security => []
|
|
}
|
|
};
|
|
schema("/logout") ->
|
|
#{
|
|
'operationId' => logout,
|
|
post => #{
|
|
tags => [<<"dashboard">>],
|
|
desc => ?DESC(logout_api),
|
|
parameters => sso_parameters(),
|
|
'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, role, description]),
|
|
responses => #{
|
|
200 => fields([username, role, description, backend])
|
|
}
|
|
}
|
|
};
|
|
schema("/users/:username") ->
|
|
#{
|
|
'operationId' => user,
|
|
put => #{
|
|
tags => [<<"dashboard">>],
|
|
desc => ?DESC(update_user_api),
|
|
parameters => sso_parameters(fields([username_in_path])),
|
|
'requestBody' => fields([role, description]),
|
|
responses => #{
|
|
200 => fields([username, role, description, backend]),
|
|
404 => response_schema(404)
|
|
}
|
|
},
|
|
delete => #{
|
|
tags => [<<"dashboard">>],
|
|
desc => ?DESC(delete_user_api),
|
|
parameters => sso_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,
|
|
post => #{
|
|
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([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401));
|
|
response_schema(404) ->
|
|
emqx_dashboard_swagger:error_codes([?USER_NOT_FOUND], ?DESC(users_api404)).
|
|
|
|
fields(user) ->
|
|
fields([username, role, description, backend]);
|
|
fields(List) ->
|
|
[field(Key) || Key <- List, field_filter(Key)].
|
|
|
|
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)})};
|
|
field(role) ->
|
|
{role,
|
|
mk(binary(), #{desc => ?DESC(role), default => ?ROLE_DEFAULT, example => ?ROLE_DEFAULT})};
|
|
field(backend) ->
|
|
{backend, mk(binary(), #{desc => ?DESC(backend), example => <<"local">>})}.
|
|
|
|
%% -------------------------------------------------------------------------------------------------
|
|
%% 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, Role, Token} ->
|
|
?SLOG(info, #{msg => "dashboard_login_successful", username => Username}),
|
|
Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
|
{200,
|
|
filter_result(#{
|
|
role => Role,
|
|
token => Token,
|
|
version => Version,
|
|
license => #{edition => emqx_release:edition()}
|
|
})};
|
|
{error, R} ->
|
|
?SLOG(info, #{msg => "dashboard_login_failed", username => Username, reason => R}),
|
|
{401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>}
|
|
end.
|
|
|
|
logout(_, #{
|
|
body := #{<<"username">> := Username0} = Req,
|
|
headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}
|
|
}) ->
|
|
Username = username(Req, Username0),
|
|
case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of
|
|
ok ->
|
|
?SLOG(info, #{msg => "dashboard_logout_successful", username => Username0}),
|
|
204;
|
|
_R ->
|
|
?SLOG(info, #{msg => "dashboard_logout_failed.", username => Username0}),
|
|
{401, ?WRONG_TOKEN_OR_USERNAME, <<"Ensure your token & username">>}
|
|
end.
|
|
|
|
users(get, _Request) ->
|
|
{200, filter_result(emqx_dashboard_admin:all_users())};
|
|
users(post, #{body := Params}) ->
|
|
Desc = maps:get(<<"description">>, Params, <<"">>),
|
|
Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT),
|
|
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, Role, Desc) of
|
|
{ok, Result} ->
|
|
?SLOG(info, #{msg => "create_dashboard_user_success", username => Username}),
|
|
{200, filter_result(Result)};
|
|
{error, Reason} ->
|
|
?SLOG(info, #{
|
|
msg => "create_dashboard_user_failed",
|
|
username => Username,
|
|
reason => Reason
|
|
}),
|
|
{400, ?BAD_REQUEST, Reason}
|
|
end
|
|
end.
|
|
|
|
user(put, #{bindings := #{username := Username0}, body := Params} = Req) ->
|
|
Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT),
|
|
Desc = maps:get(<<"description">>, Params),
|
|
Username = username(Req, Username0),
|
|
case emqx_dashboard_admin:update_user(Username, Role, Desc) of
|
|
{ok, Result} ->
|
|
{200, filter_result(Result)};
|
|
{error, <<"username_not_found">> = Reason} ->
|
|
{404, ?USER_NOT_FOUND, Reason};
|
|
{error, Reason} ->
|
|
{400, ?BAD_REQUEST, Reason}
|
|
end;
|
|
user(delete, #{bindings := #{username := Username0}, headers := Headers} = Req) ->
|
|
case Username0 == emqx_dashboard_admin:default_username() of
|
|
true ->
|
|
?SLOG(info, #{msg => "dashboard_delete_admin_user_failed", username => Username0}),
|
|
Message = list_to_binary(io_lib:format("Cannot delete user ~p", [Username0])),
|
|
{400, ?NOT_ALLOWED, Message};
|
|
false ->
|
|
Username = username(Req, Username0),
|
|
case is_self_auth(Username0, 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 => Username0
|
|
}),
|
|
{204}
|
|
end
|
|
end
|
|
end.
|
|
|
|
is_self_auth(?SSO_USERNAME(_, _), _) ->
|
|
fasle;
|
|
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(post, #{bindings := #{username := Username}, body := Params}) ->
|
|
LogMeta = #{msg => "dashboard_change_password", username => binary_to_list(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.
|
|
|
|
-if(?EMQX_RELEASE_EDITION == ee).
|
|
field_filter(_) ->
|
|
true.
|
|
|
|
filter_result(Result) ->
|
|
Result.
|
|
|
|
sso_parameters() ->
|
|
sso_parameters([]).
|
|
|
|
sso_parameters(Params) ->
|
|
emqx_dashboard_sso_api:sso_parameters(Params).
|
|
|
|
username(#{query_string := #{<<"backend">> := ?BACKEND_LOCAL}}, Username) ->
|
|
Username;
|
|
username(#{query_string := #{<<"backend">> := Backend}}, Username) ->
|
|
?SSO_USERNAME(Backend, Username);
|
|
username(_Req, Username) ->
|
|
Username.
|
|
|
|
-else.
|
|
|
|
field_filter(role) ->
|
|
false;
|
|
field_filter(_) ->
|
|
true.
|
|
|
|
filter_result(Result) when is_list(Result) ->
|
|
lists:map(fun filter_result/1, Result);
|
|
filter_result(Result) ->
|
|
maps:without([role, backend], Result).
|
|
|
|
sso_parameters() ->
|
|
sso_parameters([]).
|
|
|
|
sso_parameters(Any) ->
|
|
Any.
|
|
|
|
username(_Req, Username) ->
|
|
Username.
|
|
-endif.
|