Merge pull request #11610 from lafirest/feat/rbac
feat(dashboard): add RBAC feature for Dashboard
This commit is contained in:
commit
6fe846bf0e
|
@ -15,23 +15,34 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-define(ADMIN, emqx_admin).
|
-define(ADMIN, emqx_admin).
|
||||||
|
|
||||||
|
%% TODO:
|
||||||
|
%% The predefined roles of the preliminary RBAC implementation,
|
||||||
|
%% these may be removed when developing the full RBAC feature.
|
||||||
|
%% In full RBAC feature, the role may be customised created and deleted,
|
||||||
|
%% a predefined configuration would replace these macros.
|
||||||
|
-define(ROLE_VIEWER, <<"viewer">>).
|
||||||
|
-define(ROLE_SUPERUSER, <<"superuser">>).
|
||||||
|
|
||||||
|
-define(ROLE_DEFAULT, ?ROLE_SUPERUSER).
|
||||||
|
|
||||||
-record(?ADMIN, {
|
-record(?ADMIN, {
|
||||||
username :: binary(),
|
username :: binary(),
|
||||||
pwdhash :: binary(),
|
pwdhash :: binary(),
|
||||||
description :: binary(),
|
description :: binary(),
|
||||||
role = undefined :: atom(),
|
role = ?ROLE_DEFAULT :: binary(),
|
||||||
%% not used so far, for future extension
|
extra = #{} :: map()
|
||||||
extra = [] :: term()
|
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-type dashboard_user_role() :: binary().
|
||||||
|
-type dashboard_user() :: #?ADMIN{}.
|
||||||
|
|
||||||
-define(ADMIN_JWT, emqx_admin_jwt).
|
-define(ADMIN_JWT, emqx_admin_jwt).
|
||||||
|
|
||||||
-record(?ADMIN_JWT, {
|
-record(?ADMIN_JWT, {
|
||||||
token :: binary(),
|
token :: binary(),
|
||||||
username :: binary(),
|
username :: binary(),
|
||||||
exptime :: integer(),
|
exptime :: integer(),
|
||||||
%% not used so far, fur future extension
|
extra = #{} :: map()
|
||||||
extra = [] :: term()
|
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-define(TAB_COLLECT, emqx_collect).
|
-define(TAB_COLLECT, emqx_collect).
|
||||||
|
|
|
@ -205,13 +205,16 @@ authorize(Req) ->
|
||||||
{basic, Username, Password} ->
|
{basic, Username, Password} ->
|
||||||
api_key_authorize(Req, Username, Password);
|
api_key_authorize(Req, Username, Password);
|
||||||
{bearer, Token} ->
|
{bearer, Token} ->
|
||||||
case emqx_dashboard_admin:verify_token(Token) of
|
case emqx_dashboard_admin:verify_token(Req, Token) of
|
||||||
ok ->
|
ok ->
|
||||||
ok;
|
ok;
|
||||||
{error, token_timeout} ->
|
{error, token_timeout} ->
|
||||||
{401, 'TOKEN_TIME_OUT', <<"Token expired, get new token by POST /login">>};
|
{401, 'TOKEN_TIME_OUT', <<"Token expired, get new token by POST /login">>};
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>}
|
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>};
|
||||||
|
{error, unauthorized_role} ->
|
||||||
|
{403, 'UNAUTHORIZED_ROLE',
|
||||||
|
<<"You don't have permission to access this resource">>}
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
return_unauthorized(
|
return_unauthorized(
|
||||||
|
|
|
@ -30,10 +30,10 @@
|
||||||
-export([mnesia/1]).
|
-export([mnesia/1]).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
add_user/3,
|
add_user/4,
|
||||||
force_add_user/3,
|
force_add_user/4,
|
||||||
remove_user/1,
|
remove_user/1,
|
||||||
update_user/2,
|
update_user/3,
|
||||||
lookup_user/1,
|
lookup_user/1,
|
||||||
change_password/2,
|
change_password/2,
|
||||||
change_password/3,
|
change_password/3,
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
sign_token/2,
|
sign_token/2,
|
||||||
verify_token/1,
|
verify_token/2,
|
||||||
destroy_token_by_username/2
|
destroy_token_by_username/2
|
||||||
]).
|
]).
|
||||||
-export([
|
-export([
|
||||||
|
@ -56,10 +56,11 @@
|
||||||
default_username/0
|
default_username/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-export([role/1]).
|
||||||
|
|
||||||
-export([backup_tables/0]).
|
-export([backup_tables/0]).
|
||||||
|
|
||||||
-type emqx_admin() :: #?ADMIN{}.
|
-type emqx_admin() :: #?ADMIN{}.
|
||||||
-define(BOOTSTRAP_USER_TAG, <<"bootstrap user">>).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Mnesia bootstrap
|
%% Mnesia bootstrap
|
||||||
|
@ -98,18 +99,19 @@ add_default_user() ->
|
||||||
%% API
|
%% API
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-spec add_user(binary(), binary(), binary()) -> {ok, map()} | {error, any()}.
|
-spec add_user(binary(), binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, any()}.
|
||||||
add_user(Username, Password, Desc) when
|
add_user(Username, Password, Role, Desc) when
|
||||||
is_binary(Username), is_binary(Password)
|
is_binary(Username), is_binary(Password)
|
||||||
->
|
->
|
||||||
case {legal_username(Username), legal_password(Password)} of
|
case {legal_username(Username), legal_password(Password), legal_role(Role)} of
|
||||||
{ok, ok} -> do_add_user(Username, Password, Desc);
|
{ok, ok, ok} -> do_add_user(Username, Password, Role, Desc);
|
||||||
{{error, Reason}, _} -> {error, Reason};
|
{{error, Reason}, _, _} -> {error, Reason};
|
||||||
{_, {error, Reason}} -> {error, Reason}
|
{_, {error, Reason}, _} -> {error, Reason};
|
||||||
|
{_, _, {error, Reason}} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_add_user(Username, Password, Desc) ->
|
do_add_user(Username, Password, Role, Desc) ->
|
||||||
Res = mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc]),
|
Res = mria:transaction(?DASHBOARD_SHARD, fun add_user_/4, [Username, Password, Role, Desc]),
|
||||||
return(Res).
|
return(Res).
|
||||||
|
|
||||||
%% 0-9 or A-Z or a-z or $_
|
%% 0-9 or A-Z or a-z or $_
|
||||||
|
@ -177,11 +179,12 @@ ascii_character_validate(Password) ->
|
||||||
contain(Xs, Spec) -> lists:any(fun(X) -> lists:member(X, Spec) end, Xs).
|
contain(Xs, Spec) -> lists:any(fun(X) -> lists:member(X, Spec) end, Xs).
|
||||||
|
|
||||||
%% black-magic: force overwrite a user
|
%% black-magic: force overwrite a user
|
||||||
force_add_user(Username, Password, Desc) ->
|
force_add_user(Username, Password, Role, Desc) ->
|
||||||
AddFun = fun() ->
|
AddFun = fun() ->
|
||||||
mnesia:write(#?ADMIN{
|
mnesia:write(#?ADMIN{
|
||||||
username = Username,
|
username = Username,
|
||||||
pwdhash = hash(Password),
|
pwdhash = hash(Password),
|
||||||
|
role = Role,
|
||||||
description = Desc
|
description = Desc
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
|
@ -191,12 +194,17 @@ force_add_user(Username, Password, Desc) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
add_user_(Username, Password, Desc) ->
|
add_user_(Username, Password, Role, Desc) ->
|
||||||
case mnesia:wread({?ADMIN, Username}) of
|
case mnesia:wread({?ADMIN, Username}) of
|
||||||
[] ->
|
[] ->
|
||||||
Admin = #?ADMIN{username = Username, pwdhash = hash(Password), description = Desc},
|
Admin = #?ADMIN{
|
||||||
|
username = Username,
|
||||||
|
pwdhash = hash(Password),
|
||||||
|
role = Role,
|
||||||
|
description = Desc
|
||||||
|
},
|
||||||
mnesia:write(Admin),
|
mnesia:write(Admin),
|
||||||
#{username => Username, description => Desc};
|
#{username => Username, role => Role, description => Desc};
|
||||||
[_] ->
|
[_] ->
|
||||||
mnesia:abort(<<"username_already_exist">>)
|
mnesia:abort(<<"username_already_exist">>)
|
||||||
end.
|
end.
|
||||||
|
@ -217,9 +225,27 @@ remove_user(Username) when is_binary(Username) ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec update_user(binary(), binary()) -> {ok, map()} | {error, term()}.
|
-spec update_user(binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, term()}.
|
||||||
update_user(Username, Desc) when is_binary(Username) ->
|
update_user(Username, Role, Desc) when is_binary(Username) ->
|
||||||
return(mria:transaction(?DASHBOARD_SHARD, fun update_user_/2, [Username, Desc])).
|
case legal_role(Role) of
|
||||||
|
ok ->
|
||||||
|
case
|
||||||
|
return(
|
||||||
|
mria:transaction(?DASHBOARD_SHARD, fun update_user_/3, [Username, Role, Desc])
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, {true, Result}} ->
|
||||||
|
{ok, Result};
|
||||||
|
{ok, {false, Result}} ->
|
||||||
|
%% role has changed, destroy the related token
|
||||||
|
_ = emqx_dashboard_token:destroy_by_username(Username),
|
||||||
|
{ok, Result};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end;
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
hash(Password) ->
|
hash(Password) ->
|
||||||
SaltBin = emqx_dashboard_token:salt(),
|
SaltBin = emqx_dashboard_token:salt(),
|
||||||
|
@ -240,18 +266,18 @@ sha256(SaltBin, Password) ->
|
||||||
crypto:hash('sha256', <<SaltBin/binary, Password/binary>>).
|
crypto:hash('sha256', <<SaltBin/binary, Password/binary>>).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
update_user_(Username, Desc) ->
|
update_user_(Username, Role, Desc) ->
|
||||||
case mnesia:wread({?ADMIN, Username}) of
|
case mnesia:wread({?ADMIN, Username}) of
|
||||||
[] ->
|
[] ->
|
||||||
mnesia:abort(<<"username_not_found">>);
|
mnesia:abort(<<"username_not_found">>);
|
||||||
[Admin] ->
|
[Admin] ->
|
||||||
mnesia:write(Admin#?ADMIN{description = Desc}),
|
mnesia:write(Admin#?ADMIN{role = Role, description = Desc}),
|
||||||
#{username => Username, description => Desc}
|
{role(Admin) =:= Role, #{username => Username, role => Role, description => Desc}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) ->
|
change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) ->
|
||||||
case check(Username, OldPasswd) of
|
case check(Username, OldPasswd) of
|
||||||
ok -> change_password(Username, NewPasswd);
|
{ok, _} -> change_password(Username, NewPasswd);
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -298,12 +324,14 @@ all_users() ->
|
||||||
fun(
|
fun(
|
||||||
#?ADMIN{
|
#?ADMIN{
|
||||||
username = Username,
|
username = Username,
|
||||||
description = Desc
|
description = Desc,
|
||||||
|
role = Role
|
||||||
}
|
}
|
||||||
) ->
|
) ->
|
||||||
#{
|
#{
|
||||||
username => Username,
|
username => Username,
|
||||||
description => Desc
|
description => Desc,
|
||||||
|
role => ensure_role(Role)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
ets:tab2list(?ADMIN)
|
ets:tab2list(?ADMIN)
|
||||||
|
@ -320,9 +348,9 @@ check(_, undefined) ->
|
||||||
{error, <<"password_not_provided">>};
|
{error, <<"password_not_provided">>};
|
||||||
check(Username, Password) ->
|
check(Username, Password) ->
|
||||||
case lookup_user(Username) of
|
case lookup_user(Username) of
|
||||||
[#?ADMIN{pwdhash = PwdHash}] ->
|
[#?ADMIN{pwdhash = PwdHash} = User] ->
|
||||||
case verify_hash(Password, PwdHash) of
|
case verify_hash(Password, PwdHash) of
|
||||||
ok -> ok;
|
ok -> {ok, User};
|
||||||
error -> {error, <<"password_error">>}
|
error -> {error, <<"password_error">>}
|
||||||
end;
|
end;
|
||||||
[] ->
|
[] ->
|
||||||
|
@ -333,14 +361,14 @@ check(Username, Password) ->
|
||||||
%% token
|
%% token
|
||||||
sign_token(Username, Password) ->
|
sign_token(Username, Password) ->
|
||||||
case check(Username, Password) of
|
case check(Username, Password) of
|
||||||
ok ->
|
{ok, User} ->
|
||||||
emqx_dashboard_token:sign(Username, Password);
|
emqx_dashboard_token:sign(User, Password);
|
||||||
Error ->
|
Error ->
|
||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
verify_token(Token) ->
|
verify_token(Req, Token) ->
|
||||||
emqx_dashboard_token:verify(Token).
|
emqx_dashboard_token:verify(Req, Token).
|
||||||
|
|
||||||
destroy_token_by_username(Username, Token) ->
|
destroy_token_by_username(Username, Token) ->
|
||||||
case emqx_dashboard_token:lookup(Token) of
|
case emqx_dashboard_token:lookup(Token) of
|
||||||
|
@ -363,10 +391,36 @@ add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY
|
||||||
{ok, empty};
|
{ok, empty};
|
||||||
add_default_user(Username, Password) ->
|
add_default_user(Username, Password) ->
|
||||||
case lookup_user(Username) of
|
case lookup_user(Username) of
|
||||||
[] -> do_add_user(Username, Password, <<"administrator">>);
|
[] -> do_add_user(Username, Password, ?ROLE_SUPERUSER, <<"administrator">>);
|
||||||
_ -> {ok, default_user_exists}
|
_ -> {ok, default_user_exists}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% ensure the `role` is correct when it is directly read from the table
|
||||||
|
%% this value in old data is `undefined`
|
||||||
|
-dialyzer({no_match, ensure_role/1}).
|
||||||
|
ensure_role(undefined) ->
|
||||||
|
?ROLE_SUPERUSER;
|
||||||
|
ensure_role(Role) when is_binary(Role) ->
|
||||||
|
Role.
|
||||||
|
|
||||||
|
-if(?EMQX_RELEASE_EDITION == ee).
|
||||||
|
legal_role(Role) ->
|
||||||
|
emqx_dashboard_rbac:valid_role(Role).
|
||||||
|
|
||||||
|
role(Data) ->
|
||||||
|
emqx_dashboard_rbac:role(Data).
|
||||||
|
|
||||||
|
-else.
|
||||||
|
|
||||||
|
-dialyzer({no_match, [add_user/4, update_user/3]}).
|
||||||
|
|
||||||
|
legal_role(_) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
role(_) ->
|
||||||
|
?ROLE_DEFAULT.
|
||||||
|
-endif.
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
|
-include("emqx_dashboard.hrl").
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
@ -111,9 +112,9 @@ schema("/users") ->
|
||||||
post => #{
|
post => #{
|
||||||
tags => [<<"dashboard">>],
|
tags => [<<"dashboard">>],
|
||||||
desc => ?DESC(create_user_api),
|
desc => ?DESC(create_user_api),
|
||||||
'requestBody' => fields([username, password, description]),
|
'requestBody' => fields([username, password, role, description]),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => fields([username, description])
|
200 => fields([username, role, description])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -124,9 +125,9 @@ schema("/users/:username") ->
|
||||||
tags => [<<"dashboard">>],
|
tags => [<<"dashboard">>],
|
||||||
desc => ?DESC(update_user_api),
|
desc => ?DESC(update_user_api),
|
||||||
parameters => fields([username_in_path]),
|
parameters => fields([username_in_path]),
|
||||||
'requestBody' => fields([description]),
|
'requestBody' => fields([role, description]),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => fields([username, description]),
|
200 => fields([username, role, description]),
|
||||||
404 => response_schema(404)
|
404 => response_schema(404)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -170,7 +171,7 @@ response_schema(404) ->
|
||||||
fields(user) ->
|
fields(user) ->
|
||||||
fields([username, description]);
|
fields([username, description]);
|
||||||
fields(List) ->
|
fields(List) ->
|
||||||
[field(Key) || Key <- List].
|
[field(Key) || Key <- List, field_filter(Key)].
|
||||||
|
|
||||||
field(username) ->
|
field(username) ->
|
||||||
{username,
|
{username,
|
||||||
|
@ -203,7 +204,9 @@ field(version) ->
|
||||||
field(old_pwd) ->
|
field(old_pwd) ->
|
||||||
{old_pwd, mk(binary(), #{desc => ?DESC(old_pwd)})};
|
{old_pwd, mk(binary(), #{desc => ?DESC(old_pwd)})};
|
||||||
field(new_pwd) ->
|
field(new_pwd) ->
|
||||||
{new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})}.
|
{new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})};
|
||||||
|
field(role) ->
|
||||||
|
{role, mk(binary(), #{desc => ?DESC(role), example => ?ROLE_DEFAULT})}.
|
||||||
|
|
||||||
%% -------------------------------------------------------------------------------------------------
|
%% -------------------------------------------------------------------------------------------------
|
||||||
%% API
|
%% API
|
||||||
|
@ -239,19 +242,20 @@ logout(_, #{
|
||||||
end.
|
end.
|
||||||
|
|
||||||
users(get, _Request) ->
|
users(get, _Request) ->
|
||||||
{200, emqx_dashboard_admin:all_users()};
|
{200, filter_result(emqx_dashboard_admin:all_users())};
|
||||||
users(post, #{body := Params}) ->
|
users(post, #{body := Params}) ->
|
||||||
Desc = maps:get(<<"description">>, Params, <<"">>),
|
Desc = maps:get(<<"description">>, Params, <<"">>),
|
||||||
|
Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT),
|
||||||
Username = maps:get(<<"username">>, Params),
|
Username = maps:get(<<"username">>, Params),
|
||||||
Password = maps:get(<<"password">>, Params),
|
Password = maps:get(<<"password">>, Params),
|
||||||
case ?EMPTY(Username) orelse ?EMPTY(Password) of
|
case ?EMPTY(Username) orelse ?EMPTY(Password) of
|
||||||
true ->
|
true ->
|
||||||
{400, ?BAD_REQUEST, <<"Username or password undefined">>};
|
{400, ?BAD_REQUEST, <<"Username or password undefined">>};
|
||||||
false ->
|
false ->
|
||||||
case emqx_dashboard_admin:add_user(Username, Password, Desc) of
|
case emqx_dashboard_admin:add_user(Username, Password, Role, Desc) of
|
||||||
{ok, Result} ->
|
{ok, Result} ->
|
||||||
?SLOG(info, #{msg => "Create dashboard success", username => Username}),
|
?SLOG(info, #{msg => "Create dashboard success", username => Username}),
|
||||||
{200, Result};
|
{200, filter_result(Result)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(info, #{
|
?SLOG(info, #{
|
||||||
msg => "Create dashboard failed",
|
msg => "Create dashboard failed",
|
||||||
|
@ -263,12 +267,15 @@ users(post, #{body := Params}) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
user(put, #{bindings := #{username := Username}, body := Params}) ->
|
user(put, #{bindings := #{username := Username}, body := Params}) ->
|
||||||
|
Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT),
|
||||||
Desc = maps:get(<<"description">>, Params),
|
Desc = maps:get(<<"description">>, Params),
|
||||||
case emqx_dashboard_admin:update_user(Username, Desc) of
|
case emqx_dashboard_admin:update_user(Username, Role, Desc) of
|
||||||
{ok, Result} ->
|
{ok, Result} ->
|
||||||
{200, Result};
|
{200, filter_result(Result)};
|
||||||
|
{error, <<"username_not_found">> = Reason} ->
|
||||||
|
{404, ?USER_NOT_FOUND, Reason};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{404, ?USER_NOT_FOUND, Reason}
|
{400, ?BAD_REQUEST, Reason}
|
||||||
end;
|
end;
|
||||||
user(delete, #{bindings := #{username := Username}, headers := Headers}) ->
|
user(delete, #{bindings := #{username := Username}, headers := Headers}) ->
|
||||||
case Username == emqx_dashboard_admin:default_username() of
|
case Username == emqx_dashboard_admin:default_username() of
|
||||||
|
@ -347,3 +354,24 @@ change_pwd(post, #{bindings := #{username := Username}, body := Params}) ->
|
||||||
{400, ?BAD_REQUEST, Reason}
|
{400, ?BAD_REQUEST, Reason}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-if(?EMQX_RELEASE_EDITION == ee).
|
||||||
|
field_filter(_) ->
|
||||||
|
true.
|
||||||
|
|
||||||
|
filter_result(Result) ->
|
||||||
|
Result.
|
||||||
|
|
||||||
|
-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], Result).
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
-module(emqx_dashboard_cli).
|
-module(emqx_dashboard_cli).
|
||||||
|
|
||||||
|
-include("emqx_dashboard.hrl").
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
load/0,
|
load/0,
|
||||||
admins/1,
|
admins/1,
|
||||||
|
@ -25,15 +27,6 @@
|
||||||
load() ->
|
load() ->
|
||||||
emqx_ctl:register_command(admins, {?MODULE, admins}, []).
|
emqx_ctl:register_command(admins, {?MODULE, admins}, []).
|
||||||
|
|
||||||
admins(["add", Username, Password]) ->
|
|
||||||
admins(["add", Username, Password, ""]);
|
|
||||||
admins(["add", Username, Password, Desc]) ->
|
|
||||||
case emqx_dashboard_admin:add_user(bin(Username), bin(Password), bin(Desc)) of
|
|
||||||
{ok, _} ->
|
|
||||||
emqx_ctl:print("ok~n");
|
|
||||||
{error, Reason} ->
|
|
||||||
print_error(Reason)
|
|
||||||
end;
|
|
||||||
admins(["passwd", Username, Password]) ->
|
admins(["passwd", Username, Password]) ->
|
||||||
case emqx_dashboard_admin:change_password(bin(Username), bin(Password)) of
|
case emqx_dashboard_admin:change_password(bin(Username), bin(Password)) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
@ -48,14 +41,8 @@ admins(["del", Username]) ->
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
print_error(Reason)
|
print_error(Reason)
|
||||||
end;
|
end;
|
||||||
admins(_) ->
|
admins(Args) ->
|
||||||
emqx_ctl:usage(
|
inner_admins(Args).
|
||||||
[
|
|
||||||
{"admins add <Username> <Password> <Description>", "Add dashboard user"},
|
|
||||||
{"admins passwd <Username> <Password>", "Reset dashboard user password"},
|
|
||||||
{"admins del <Username>", "Delete dashboard user"}
|
|
||||||
]
|
|
||||||
).
|
|
||||||
|
|
||||||
unload() ->
|
unload() ->
|
||||||
emqx_ctl:unregister_command(admins).
|
emqx_ctl:unregister_command(admins).
|
||||||
|
@ -67,3 +54,47 @@ print_error(Reason) when is_binary(Reason) ->
|
||||||
%% Maybe has more types of error, but there is only binary now. So close it for dialyzer.
|
%% Maybe has more types of error, but there is only binary now. So close it for dialyzer.
|
||||||
% print_error(Reason) ->
|
% print_error(Reason) ->
|
||||||
% emqx_ctl:print("Error: ~p~n", [Reason]).
|
% emqx_ctl:print("Error: ~p~n", [Reason]).
|
||||||
|
|
||||||
|
-if(?EMQX_RELEASE_EDITION == ee).
|
||||||
|
usage() ->
|
||||||
|
[
|
||||||
|
{"admins add <Username> <Password> <Role> <Description>", "Add dashboard user"},
|
||||||
|
{"admins passwd <Username> <Password>", "Reset dashboard user password"},
|
||||||
|
{"admins del <Username>", "Delete dashboard user"}
|
||||||
|
].
|
||||||
|
|
||||||
|
inner_admins(["add", Username, Password]) ->
|
||||||
|
inner_admins(["add", Username, Password, ?ROLE_SUPERUSER]);
|
||||||
|
inner_admins(["add", Username, Password, Role]) ->
|
||||||
|
inner_admins(["add", Username, Password, Role, ""]);
|
||||||
|
inner_admins(["add", Username, Password, Role, Desc]) ->
|
||||||
|
case emqx_dashboard_admin:add_user(bin(Username), bin(Password), bin(Role), bin(Desc)) of
|
||||||
|
{ok, _} ->
|
||||||
|
emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} ->
|
||||||
|
print_error(Reason)
|
||||||
|
end;
|
||||||
|
inner_admins(_) ->
|
||||||
|
emqx_ctl:usage(usage()).
|
||||||
|
-else.
|
||||||
|
|
||||||
|
usage() ->
|
||||||
|
[
|
||||||
|
{"admins add <Username> <Password> <Description>", "Add dashboard user"},
|
||||||
|
{"admins passwd <Username> <Password>", "Reset dashboard user password"},
|
||||||
|
{"admins del <Username>", "Delete dashboard user"}
|
||||||
|
].
|
||||||
|
|
||||||
|
inner_admins(["add", Username, Password]) ->
|
||||||
|
inner_admins(["add", Username, Password, ""]);
|
||||||
|
inner_admins(["add", Username, Password, Desc]) ->
|
||||||
|
case emqx_dashboard_admin:add_user(bin(Username), bin(Password), ?ROLE_SUPERUSER, bin(Desc)) of
|
||||||
|
{ok, _} ->
|
||||||
|
emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} ->
|
||||||
|
print_error(Reason)
|
||||||
|
end;
|
||||||
|
inner_admins(_) ->
|
||||||
|
emqx_ctl:usage(usage()).
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
sign/2,
|
sign/2,
|
||||||
verify/1,
|
verify/2,
|
||||||
lookup/1,
|
lookup/1,
|
||||||
owner/1,
|
owner/1,
|
||||||
destroy/1,
|
destroy/1,
|
||||||
|
@ -55,14 +55,17 @@
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% jwt function
|
%% jwt function
|
||||||
-spec sign(Username :: binary(), Password :: binary()) ->
|
-spec sign(User :: dashboard_user(), Password :: binary()) ->
|
||||||
{ok, Token :: binary()} | {error, Reason :: term()}.
|
{ok, Token :: binary()} | {error, Reason :: term()}.
|
||||||
sign(Username, Password) ->
|
sign(User, Password) ->
|
||||||
do_sign(Username, Password).
|
do_sign(User, Password).
|
||||||
|
|
||||||
-spec verify(Token :: binary()) -> Result :: ok | {error, token_timeout | not_found}.
|
-spec verify(_, Token :: binary()) ->
|
||||||
verify(Token) ->
|
Result ::
|
||||||
do_verify(Token).
|
ok
|
||||||
|
| {error, token_timeout | not_found | unauthorized_role}.
|
||||||
|
verify(Req, Token) ->
|
||||||
|
do_verify(Req, Token).
|
||||||
|
|
||||||
-spec destroy(KeyOrKeys :: list() | binary() | #?ADMIN_JWT{}) -> ok.
|
-spec destroy(KeyOrKeys :: list() | binary() | #?ADMIN_JWT{}) -> ok.
|
||||||
destroy([]) ->
|
destroy([]) ->
|
||||||
|
@ -101,7 +104,7 @@ mnesia(boot) ->
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% jwt apply
|
%% jwt apply
|
||||||
do_sign(Username, Password) ->
|
do_sign(#?ADMIN{username = Username} = User, Password) ->
|
||||||
ExpTime = jwt_expiration_time(),
|
ExpTime = jwt_expiration_time(),
|
||||||
Salt = salt(),
|
Salt = salt(),
|
||||||
JWK = jwk(Username, Password, Salt),
|
JWK = jwk(Username, Password, Salt),
|
||||||
|
@ -114,14 +117,17 @@ do_sign(Username, Password) ->
|
||||||
},
|
},
|
||||||
Signed = jose_jwt:sign(JWK, JWS, JWT),
|
Signed = jose_jwt:sign(JWK, JWS, JWT),
|
||||||
{_, Token} = jose_jws:compact(Signed),
|
{_, Token} = jose_jws:compact(Signed),
|
||||||
JWTRec = format(Token, Username, ExpTime),
|
Role = emqx_dashboard_admin:role(User),
|
||||||
|
JWTRec = format(Token, Username, Role, ExpTime),
|
||||||
_ = mria:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]),
|
_ = mria:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]),
|
||||||
{ok, Token}.
|
{ok, Token}.
|
||||||
|
|
||||||
do_verify(Token) ->
|
do_verify(Req, Token) ->
|
||||||
case lookup(Token) of
|
case lookup(Token) of
|
||||||
{ok, JWT = #?ADMIN_JWT{exptime = ExpTime}} ->
|
{ok, JWT = #?ADMIN_JWT{exptime = ExpTime, extra = Extra}} ->
|
||||||
case ExpTime > erlang:system_time(millisecond) of
|
case ExpTime > erlang:system_time(millisecond) of
|
||||||
|
true ->
|
||||||
|
case check_rbac(Req, Extra) of
|
||||||
true ->
|
true ->
|
||||||
NewJWT = JWT#?ADMIN_JWT{exptime = jwt_expiration_time()},
|
NewJWT = JWT#?ADMIN_JWT{exptime = jwt_expiration_time()},
|
||||||
{atomic, Res} = mria:transaction(
|
{atomic, Res} = mria:transaction(
|
||||||
|
@ -130,6 +136,9 @@ do_verify(Token) ->
|
||||||
[NewJWT]
|
[NewJWT]
|
||||||
),
|
),
|
||||||
Res;
|
Res;
|
||||||
|
_ ->
|
||||||
|
{error, unauthorized_role}
|
||||||
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
{error, token_timeout}
|
{error, token_timeout}
|
||||||
end;
|
end;
|
||||||
|
@ -183,11 +192,12 @@ jwt_expiration_time() ->
|
||||||
token_ttl() ->
|
token_ttl() ->
|
||||||
emqx_conf:get([dashboard, token_expired_time], ?EXPTIME).
|
emqx_conf:get([dashboard, token_expired_time], ?EXPTIME).
|
||||||
|
|
||||||
format(Token, Username, ExpTime) ->
|
format(Token, Username, Role, ExpTime) ->
|
||||||
#?ADMIN_JWT{
|
#?ADMIN_JWT{
|
||||||
token = Token,
|
token = Token,
|
||||||
username = Username,
|
username = Username,
|
||||||
exptime = ExpTime
|
exptime = ExpTime,
|
||||||
|
extra = #{role => Role}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -234,3 +244,17 @@ clean_expired_jwt(Now) ->
|
||||||
fun() -> mnesia:select(?TAB, Spec) end
|
fun() -> mnesia:select(?TAB, Spec) end
|
||||||
),
|
),
|
||||||
ok = destroy(JWTList).
|
ok = destroy(JWTList).
|
||||||
|
|
||||||
|
-if(?EMQX_RELEASE_EDITION == ee).
|
||||||
|
check_rbac(Req, Extra) ->
|
||||||
|
emqx_dashboard_rbac:check_rbac(Req, Extra).
|
||||||
|
|
||||||
|
-else.
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, [check_rbac/2]}).
|
||||||
|
-dialyzer({no_match, [do_verify/2]}).
|
||||||
|
|
||||||
|
check_rbac(_Req, _Extra) ->
|
||||||
|
true.
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
|
@ -67,7 +67,9 @@ end_per_suite(_Config) ->
|
||||||
|
|
||||||
t_overview(_) ->
|
t_overview(_) ->
|
||||||
mnesia:clear_table(?ADMIN),
|
mnesia:clear_table(?ADMIN),
|
||||||
emqx_dashboard_admin:add_user(<<"admin">>, <<"public_www1">>, <<"simple_description">>),
|
emqx_dashboard_admin:add_user(
|
||||||
|
<<"admin">>, <<"public_www1">>, ?ROLE_SUPERUSER, <<"simple_description">>
|
||||||
|
),
|
||||||
Headers = auth_header_(<<"admin">>, <<"public_www1">>),
|
Headers = auth_header_(<<"admin">>, <<"public_www1">>),
|
||||||
[
|
[
|
||||||
{ok, _} = request_dashboard(get, api_path([Overview]), Headers)
|
{ok, _} = request_dashboard(get, api_path([Overview]), Headers)
|
||||||
|
@ -77,8 +79,12 @@ t_overview(_) ->
|
||||||
t_admins_add_delete(_) ->
|
t_admins_add_delete(_) ->
|
||||||
mnesia:clear_table(?ADMIN),
|
mnesia:clear_table(?ADMIN),
|
||||||
Desc = <<"simple description">>,
|
Desc = <<"simple description">>,
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(<<"username">>, <<"password_0">>, Desc),
|
{ok, _} = emqx_dashboard_admin:add_user(
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(<<"username1">>, <<"password1">>, Desc),
|
<<"username">>, <<"password_0">>, ?ROLE_SUPERUSER, Desc
|
||||||
|
),
|
||||||
|
{ok, _} = emqx_dashboard_admin:add_user(
|
||||||
|
<<"username1">>, <<"password1">>, ?ROLE_SUPERUSER, Desc
|
||||||
|
),
|
||||||
Admins = emqx_dashboard_admin:all_users(),
|
Admins = emqx_dashboard_admin:all_users(),
|
||||||
?assertEqual(2, length(Admins)),
|
?assertEqual(2, length(Admins)),
|
||||||
{ok, _} = emqx_dashboard_admin:remove_user(<<"username1">>),
|
{ok, _} = emqx_dashboard_admin:remove_user(<<"username1">>),
|
||||||
|
@ -95,7 +101,7 @@ t_admins_add_delete(_) ->
|
||||||
t_admin_delete_self_failed(_) ->
|
t_admin_delete_self_failed(_) ->
|
||||||
mnesia:clear_table(?ADMIN),
|
mnesia:clear_table(?ADMIN),
|
||||||
Desc = <<"simple description">>,
|
Desc = <<"simple description">>,
|
||||||
_ = emqx_dashboard_admin:add_user(<<"username1">>, <<"password_1">>, Desc),
|
_ = emqx_dashboard_admin:add_user(<<"username1">>, <<"password_1">>, ?ROLE_SUPERUSER, Desc),
|
||||||
Admins = emqx_dashboard_admin:all_users(),
|
Admins = emqx_dashboard_admin:all_users(),
|
||||||
?assertEqual(1, length(Admins)),
|
?assertEqual(1, length(Admins)),
|
||||||
Header = auth_header_(<<"username1">>, <<"password_1">>),
|
Header = auth_header_(<<"username1">>, <<"password_1">>),
|
||||||
|
@ -109,23 +115,34 @@ t_rest_api(_Config) ->
|
||||||
mnesia:clear_table(?ADMIN),
|
mnesia:clear_table(?ADMIN),
|
||||||
Desc = <<"administrator">>,
|
Desc = <<"administrator">>,
|
||||||
Password = <<"public_www1">>,
|
Password = <<"public_www1">>,
|
||||||
emqx_dashboard_admin:add_user(<<"admin">>, Password, Desc),
|
emqx_dashboard_admin:add_user(<<"admin">>, Password, ?ROLE_SUPERUSER, Desc),
|
||||||
{ok, 200, Res0} = http_get(["users"]),
|
{ok, 200, Res0} = http_get(["users"]),
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
[
|
[
|
||||||
#{
|
filter_req(#{
|
||||||
<<"username">> => <<"admin">>,
|
<<"username">> => <<"admin">>,
|
||||||
<<"description">> => <<"administrator">>
|
<<"description">> => <<"administrator">>,
|
||||||
}
|
<<"role">> => ?ROLE_SUPERUSER
|
||||||
|
})
|
||||||
],
|
],
|
||||||
get_http_data(Res0)
|
get_http_data(Res0)
|
||||||
),
|
),
|
||||||
{ok, 200, _} = http_put(["users", "admin"], #{<<"description">> => <<"a_new_description">>}),
|
{ok, 200, _} = http_put(
|
||||||
{ok, 200, _} = http_post(["users"], #{
|
["users", "admin"],
|
||||||
|
filter_req(#{
|
||||||
|
<<"role">> => ?ROLE_SUPERUSER,
|
||||||
|
<<"description">> => <<"a_new_description">>
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ok, 200, _} = http_post(
|
||||||
|
["users"],
|
||||||
|
filter_req(#{
|
||||||
<<"username">> => <<"usera">>,
|
<<"username">> => <<"usera">>,
|
||||||
<<"password">> => <<"passwd_01234">>,
|
<<"password">> => <<"passwd_01234">>,
|
||||||
|
<<"role">> => ?ROLE_SUPERUSER,
|
||||||
<<"description">> => Desc
|
<<"description">> => Desc
|
||||||
}),
|
})
|
||||||
|
),
|
||||||
{ok, 204, _} = http_delete(["users", "usera"]),
|
{ok, 204, _} = http_delete(["users", "usera"]),
|
||||||
{ok, 404, _} = http_delete(["users", "usera"]),
|
{ok, 404, _} = http_delete(["users", "usera"]),
|
||||||
{ok, 204, _} = http_post(
|
{ok, 204, _} = http_post(
|
||||||
|
@ -136,7 +153,7 @@ t_rest_api(_Config) ->
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
mnesia:clear_table(?ADMIN),
|
mnesia:clear_table(?ADMIN),
|
||||||
emqx_dashboard_admin:add_user(<<"admin">>, Password, <<"administrator">>),
|
emqx_dashboard_admin:add_user(<<"admin">>, Password, ?ROLE_SUPERUSER, <<"administrator">>),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_swagger_json(_Config) ->
|
t_swagger_json(_Config) ->
|
||||||
|
@ -180,7 +197,7 @@ t_cli(_Config) ->
|
||||||
t_lookup_by_username_jwt(_Config) ->
|
t_lookup_by_username_jwt(_Config) ->
|
||||||
User = bin(["user-", integer_to_list(random_num())]),
|
User = bin(["user-", integer_to_list(random_num())]),
|
||||||
Pwd = bin("t_password" ++ integer_to_list(random_num())),
|
Pwd = bin("t_password" ++ integer_to_list(random_num())),
|
||||||
emqx_dashboard_token:sign(User, Pwd),
|
emqx_dashboard_token:sign(#?ADMIN{username = User}, Pwd),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
[#?ADMIN_JWT{username = User}],
|
[#?ADMIN_JWT{username = User}],
|
||||||
emqx_dashboard_token:lookup_by_username(User)
|
emqx_dashboard_token:lookup_by_username(User)
|
||||||
|
@ -194,7 +211,7 @@ t_lookup_by_username_jwt(_Config) ->
|
||||||
t_clean_expired_jwt(_Config) ->
|
t_clean_expired_jwt(_Config) ->
|
||||||
User = bin(["user-", integer_to_list(random_num())]),
|
User = bin(["user-", integer_to_list(random_num())]),
|
||||||
Pwd = bin("t_password" ++ integer_to_list(random_num())),
|
Pwd = bin("t_password" ++ integer_to_list(random_num())),
|
||||||
emqx_dashboard_token:sign(User, Pwd),
|
emqx_dashboard_token:sign(#?ADMIN{username = User}, Pwd),
|
||||||
[#?ADMIN_JWT{username = User, exptime = ExpTime}] =
|
[#?ADMIN_JWT{username = User, exptime = ExpTime}] =
|
||||||
emqx_dashboard_token:lookup_by_username(User),
|
emqx_dashboard_token:lookup_by_username(User),
|
||||||
ok = emqx_dashboard_token:clean_expired_jwt(_Now1 = ExpTime),
|
ok = emqx_dashboard_token:clean_expired_jwt(_Now1 = ExpTime),
|
||||||
|
@ -261,3 +278,14 @@ api_path(Parts) ->
|
||||||
json(Data) ->
|
json(Data) ->
|
||||||
{ok, Jsx} = emqx_utils_json:safe_decode(Data, [return_maps]),
|
{ok, Jsx} = emqx_utils_json:safe_decode(Data, [return_maps]),
|
||||||
Jsx.
|
Jsx.
|
||||||
|
|
||||||
|
-if(?EMQX_RELEASE_EDITION == ee).
|
||||||
|
filter_req(Req) ->
|
||||||
|
Req.
|
||||||
|
|
||||||
|
-else.
|
||||||
|
|
||||||
|
filter_req(Req) ->
|
||||||
|
maps:without([role, <<"role">>], Req).
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
|
@ -42,8 +42,8 @@ t_check_user(_) ->
|
||||||
BadPassword = <<"public_bad">>,
|
BadPassword = <<"public_bad">>,
|
||||||
EmptyUsername = <<>>,
|
EmptyUsername = <<>>,
|
||||||
EmptyPassword = <<>>,
|
EmptyPassword = <<>>,
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>),
|
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>),
|
||||||
ok = emqx_dashboard_admin:check(Username, Password),
|
{ok, _} = emqx_dashboard_admin:check(Username, Password),
|
||||||
{error, <<"password_error">>} = emqx_dashboard_admin:check(Username, BadPassword),
|
{error, <<"password_error">>} = emqx_dashboard_admin:check(Username, BadPassword),
|
||||||
{error, <<"username_not_found">>} = emqx_dashboard_admin:check(BadUsername, Password),
|
{error, <<"username_not_found">>} = emqx_dashboard_admin:check(BadUsername, Password),
|
||||||
{error, <<"username_not_found">>} = emqx_dashboard_admin:check(BadUsername, BadPassword),
|
{error, <<"username_not_found">>} = emqx_dashboard_admin:check(BadUsername, BadPassword),
|
||||||
|
@ -61,19 +61,23 @@ t_add_user(_) ->
|
||||||
BadAddUser = <<"***add_user_bad">>,
|
BadAddUser = <<"***add_user_bad">>,
|
||||||
|
|
||||||
%% add success. not return password
|
%% add success. not return password
|
||||||
{ok, NewUser} = emqx_dashboard_admin:add_user(AddUser, AddPassword, AddDescription),
|
{ok, NewUser} = emqx_dashboard_admin:add_user(
|
||||||
|
AddUser, AddPassword, ?ROLE_SUPERUSER, AddDescription
|
||||||
|
),
|
||||||
AddUser = maps:get(username, NewUser),
|
AddUser = maps:get(username, NewUser),
|
||||||
AddDescription = maps:get(description, NewUser),
|
AddDescription = maps:get(description, NewUser),
|
||||||
false = maps:is_key(password, NewUser),
|
false = maps:is_key(password, NewUser),
|
||||||
|
|
||||||
%% add again
|
%% add again
|
||||||
{error, <<"username_already_exist">>} =
|
{error, <<"username_already_exist">>} =
|
||||||
emqx_dashboard_admin:add_user(AddUser, AddPassword, AddDescription),
|
emqx_dashboard_admin:add_user(AddUser, AddPassword, ?ROLE_SUPERUSER, AddDescription),
|
||||||
|
|
||||||
%% add bad username
|
%% add bad username
|
||||||
BadNameError =
|
BadNameError =
|
||||||
<<"Bad Username. Only upper and lower case letters, numbers and underscores are supported">>,
|
<<"Bad Username. Only upper and lower case letters, numbers and underscores are supported">>,
|
||||||
{error, BadNameError} = emqx_dashboard_admin:add_user(BadAddUser, AddPassword, AddDescription),
|
{error, BadNameError} = emqx_dashboard_admin:add_user(
|
||||||
|
BadAddUser, AddPassword, ?ROLE_SUPERUSER, AddDescription
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_lookup_user(_) ->
|
t_lookup_user(_) ->
|
||||||
|
@ -84,7 +88,9 @@ t_lookup_user(_) ->
|
||||||
BadLookupUser = <<"***lookup_user_bad">>,
|
BadLookupUser = <<"***lookup_user_bad">>,
|
||||||
|
|
||||||
{ok, _} =
|
{ok, _} =
|
||||||
emqx_dashboard_admin:add_user(LookupUser, LookupPassword, LookupDescription),
|
emqx_dashboard_admin:add_user(
|
||||||
|
LookupUser, LookupPassword, ?ROLE_SUPERUSER, LookupDescription
|
||||||
|
),
|
||||||
%% lookup success. not return password
|
%% lookup success. not return password
|
||||||
[#emqx_admin{username = LookupUser, description = LookupDescription}] =
|
[#emqx_admin{username = LookupUser, description = LookupDescription}] =
|
||||||
emqx_dashboard_admin:lookup_user(LookupUser),
|
emqx_dashboard_admin:lookup_user(LookupUser),
|
||||||
|
@ -95,7 +101,7 @@ t_lookup_user(_) ->
|
||||||
t_all_users(_) ->
|
t_all_users(_) ->
|
||||||
Username = <<"admin_all">>,
|
Username = <<"admin_all">>,
|
||||||
Password = <<"public_2">>,
|
Password = <<"public_2">>,
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>),
|
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>),
|
||||||
All = emqx_dashboard_admin:all_users(),
|
All = emqx_dashboard_admin:all_users(),
|
||||||
?assert(erlang:length(All) >= 1),
|
?assert(erlang:length(All) >= 1),
|
||||||
ok.
|
ok.
|
||||||
|
@ -108,7 +114,9 @@ t_delete_user(_) ->
|
||||||
DeleteBadUser = <<"delete_user_bad">>,
|
DeleteBadUser = <<"delete_user_bad">>,
|
||||||
|
|
||||||
{ok, _NewUser} =
|
{ok, _NewUser} =
|
||||||
emqx_dashboard_admin:add_user(DeleteUser, DeletePassword, DeleteDescription),
|
emqx_dashboard_admin:add_user(
|
||||||
|
DeleteUser, DeletePassword, ?ROLE_SUPERUSER, DeleteDescription
|
||||||
|
),
|
||||||
{ok, ok} = emqx_dashboard_admin:remove_user(DeleteUser),
|
{ok, ok} = emqx_dashboard_admin:remove_user(DeleteUser),
|
||||||
%% remove again
|
%% remove again
|
||||||
{error, <<"username_not_found">>} = emqx_dashboard_admin:remove_user(DeleteUser),
|
{error, <<"username_not_found">>} = emqx_dashboard_admin:remove_user(DeleteUser),
|
||||||
|
@ -124,13 +132,17 @@ t_update_user(_) ->
|
||||||
|
|
||||||
BadUpdateUser = <<"update_user_bad">>,
|
BadUpdateUser = <<"update_user_bad">>,
|
||||||
|
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(UpdateUser, UpdatePassword, UpdateDescription),
|
{ok, _} = emqx_dashboard_admin:add_user(
|
||||||
|
UpdateUser, UpdatePassword, ?ROLE_SUPERUSER, UpdateDescription
|
||||||
|
),
|
||||||
{ok, NewUserInfo} =
|
{ok, NewUserInfo} =
|
||||||
emqx_dashboard_admin:update_user(UpdateUser, NewDesc),
|
emqx_dashboard_admin:update_user(UpdateUser, ?ROLE_SUPERUSER, NewDesc),
|
||||||
UpdateUser = maps:get(username, NewUserInfo),
|
UpdateUser = maps:get(username, NewUserInfo),
|
||||||
NewDesc = maps:get(description, NewUserInfo),
|
NewDesc = maps:get(description, NewUserInfo),
|
||||||
|
|
||||||
{error, <<"username_not_found">>} = emqx_dashboard_admin:update_user(BadUpdateUser, NewDesc),
|
{error, <<"username_not_found">>} = emqx_dashboard_admin:update_user(
|
||||||
|
BadUpdateUser, ?ROLE_SUPERUSER, NewDesc
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_change_password(_) ->
|
t_change_password(_) ->
|
||||||
|
@ -143,7 +155,7 @@ t_change_password(_) ->
|
||||||
|
|
||||||
BadChangeUser = <<"change_user_bad">>,
|
BadChangeUser = <<"change_user_bad">>,
|
||||||
|
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(User, OldPassword, Description),
|
{ok, _} = emqx_dashboard_admin:add_user(User, OldPassword, ?ROLE_SUPERUSER, Description),
|
||||||
|
|
||||||
{ok, ok} = emqx_dashboard_admin:change_password(User, OldPassword, NewPassword),
|
{ok, ok} = emqx_dashboard_admin:change_password(User, OldPassword, NewPassword),
|
||||||
%% change pwd again
|
%% change pwd again
|
||||||
|
@ -161,17 +173,18 @@ t_clean_token(_) ->
|
||||||
Username = <<"admin_token">>,
|
Username = <<"admin_token">>,
|
||||||
Password = <<"public_www1">>,
|
Password = <<"public_www1">>,
|
||||||
NewPassword = <<"public_www2">>,
|
NewPassword = <<"public_www2">>,
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>),
|
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>),
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
ok = emqx_dashboard_admin:verify_token(Token),
|
FakeReq = #{method => <<"GET">>},
|
||||||
|
ok = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
%% change password
|
%% change password
|
||||||
{ok, _} = emqx_dashboard_admin:change_password(Username, Password, NewPassword),
|
{ok, _} = emqx_dashboard_admin:change_password(Username, Password, NewPassword),
|
||||||
timer:sleep(5),
|
timer:sleep(5),
|
||||||
{error, not_found} = emqx_dashboard_admin:verify_token(Token),
|
{error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
%% remove user
|
%% remove user
|
||||||
{ok, Token2} = emqx_dashboard_admin:sign_token(Username, NewPassword),
|
{ok, Token2} = emqx_dashboard_admin:sign_token(Username, NewPassword),
|
||||||
ok = emqx_dashboard_admin:verify_token(Token2),
|
ok = emqx_dashboard_admin:verify_token(FakeReq, Token2),
|
||||||
{ok, _} = emqx_dashboard_admin:remove_user(Username),
|
{ok, _} = emqx_dashboard_admin:remove_user(Username),
|
||||||
timer:sleep(5),
|
timer:sleep(5),
|
||||||
{error, not_found} = emqx_dashboard_admin:verify_token(Token2),
|
{error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token2),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
request/2,
|
request/2,
|
||||||
request/3,
|
request/3,
|
||||||
request/4,
|
request/4,
|
||||||
|
request/5,
|
||||||
multipart_formdata_request/3,
|
multipart_formdata_request/3,
|
||||||
multipart_formdata_request/4,
|
multipart_formdata_request/4,
|
||||||
host/0,
|
host/0,
|
||||||
|
@ -73,6 +74,9 @@ request(Method, Url, Body) ->
|
||||||
request(<<"admin">>, Method, Url, Body).
|
request(<<"admin">>, Method, Url, Body).
|
||||||
|
|
||||||
request(Username, Method, Url, Body) ->
|
request(Username, Method, Url, Body) ->
|
||||||
|
request(Username, <<"public">>, Method, Url, Body).
|
||||||
|
|
||||||
|
request(Username, Password, Method, Url, Body) ->
|
||||||
Request =
|
Request =
|
||||||
case Body of
|
case Body of
|
||||||
[] when
|
[] when
|
||||||
|
@ -80,9 +84,10 @@ request(Username, Method, Url, Body) ->
|
||||||
Method =:= head orelse Method =:= delete orelse
|
Method =:= head orelse Method =:= delete orelse
|
||||||
Method =:= trace
|
Method =:= trace
|
||||||
->
|
->
|
||||||
{Url, [auth_header(Username)]};
|
{Url, [auth_header(Username, Password)]};
|
||||||
_ ->
|
_ ->
|
||||||
{Url, [auth_header(Username)], "application/json", emqx_utils_json:encode(Body)}
|
{Url, [auth_header(Username, Password)], "application/json",
|
||||||
|
emqx_utils_json:encode(Body)}
|
||||||
end,
|
end,
|
||||||
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
|
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
|
||||||
case httpc:request(Method, Request, [], [{body_format, binary}]) of
|
case httpc:request(Method, Request, [], [{body_format, binary}]) of
|
||||||
|
@ -108,7 +113,9 @@ uri(Host, Parts) when is_list(Host), is_list(Parts) ->
|
||||||
Host ++ "/" ++ to_list(filename:join([?BASE_PATH, ?API_VERSION | NParts])).
|
Host ++ "/" ++ to_list(filename:join([?BASE_PATH, ?API_VERSION | NParts])).
|
||||||
|
|
||||||
auth_header(Username) ->
|
auth_header(Username) ->
|
||||||
Password = <<"public">>,
|
auth_header(Username, <<"public">>).
|
||||||
|
|
||||||
|
auth_header(Username, Password) ->
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
Business Source License 1.1
|
||||||
|
|
||||||
|
Licensor: Hangzhou EMQ Technologies Co., Ltd.
|
||||||
|
Licensed Work: EMQX Enterprise Edition
|
||||||
|
The Licensed Work is (c) 2023
|
||||||
|
Hangzhou EMQ Technologies Co., Ltd.
|
||||||
|
Additional Use Grant: Students and educators are granted right to copy,
|
||||||
|
modify, and create derivative work for research
|
||||||
|
or education.
|
||||||
|
Change Date: 2027-02-01
|
||||||
|
Change License: Apache License, Version 2.0
|
||||||
|
|
||||||
|
For information about alternative licensing arrangements for the Software,
|
||||||
|
please contact Licensor: https://www.emqx.com/en/contact
|
||||||
|
|
||||||
|
Notice
|
||||||
|
|
||||||
|
The Business Source License (this document, or the “License”) is not an Open
|
||||||
|
Source license. However, the Licensed Work will eventually be made available
|
||||||
|
under an Open Source License, as stated in this License.
|
||||||
|
|
||||||
|
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
||||||
|
“Business Source License” is a trademark of MariaDB Corporation Ab.
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Business Source License 1.1
|
||||||
|
|
||||||
|
Terms
|
||||||
|
|
||||||
|
The Licensor hereby grants you the right to copy, modify, create derivative
|
||||||
|
works, redistribute, and make non-production use of the Licensed Work. The
|
||||||
|
Licensor may make an Additional Use Grant, above, permitting limited
|
||||||
|
production use.
|
||||||
|
|
||||||
|
Effective on the Change Date, or the fourth anniversary of the first publicly
|
||||||
|
available distribution of a specific version of the Licensed Work under this
|
||||||
|
License, whichever comes first, the Licensor hereby grants you rights under
|
||||||
|
the terms of the Change License, and the rights granted in the paragraph
|
||||||
|
above terminate.
|
||||||
|
|
||||||
|
If your use of the Licensed Work does not comply with the requirements
|
||||||
|
currently in effect as described in this License, you must purchase a
|
||||||
|
commercial license from the Licensor, its affiliated entities, or authorized
|
||||||
|
resellers, or you must refrain from using the Licensed Work.
|
||||||
|
|
||||||
|
All copies of the original and modified Licensed Work, and derivative works
|
||||||
|
of the Licensed Work, are subject to this License. This License applies
|
||||||
|
separately for each version of the Licensed Work and the Change Date may vary
|
||||||
|
for each version of the Licensed Work released by Licensor.
|
||||||
|
|
||||||
|
You must conspicuously display this License on each original or modified copy
|
||||||
|
of the Licensed Work. If you receive the Licensed Work in original or
|
||||||
|
modified form from a third party, the terms and conditions set forth in this
|
||||||
|
License apply to your use of that work.
|
||||||
|
|
||||||
|
Any use of the Licensed Work in violation of this License will automatically
|
||||||
|
terminate your rights under this License for the current and all other
|
||||||
|
versions of the Licensed Work.
|
||||||
|
|
||||||
|
This License does not grant you any right in any trademark or logo of
|
||||||
|
Licensor or its affiliates (provided that you may use a trademark or logo of
|
||||||
|
Licensor as expressly required by this License).
|
||||||
|
|
||||||
|
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
||||||
|
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
||||||
|
TITLE.
|
||||||
|
|
||||||
|
MariaDB hereby grants you permission to use this License’s text to license
|
||||||
|
your works, and to refer to it using the trademark “Business Source License”,
|
||||||
|
as long as you comply with the Covenants of Licensor below.
|
||||||
|
|
||||||
|
Covenants of Licensor
|
||||||
|
|
||||||
|
In consideration of the right to use this License’s text and the “Business
|
||||||
|
Source License” name and trademark, Licensor covenants to MariaDB, and to all
|
||||||
|
other recipients of the licensed work to be provided by Licensor:
|
||||||
|
|
||||||
|
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
||||||
|
or a license that is compatible with GPL Version 2.0 or a later version,
|
||||||
|
where “compatible” means that software provided under the Change License can
|
||||||
|
be included in a program with software provided under GPL Version 2.0 or a
|
||||||
|
later version. Licensor may specify additional Change Licenses without
|
||||||
|
limitation.
|
||||||
|
|
||||||
|
2. To either: (a) specify an additional grant of rights to use that does not
|
||||||
|
impose any additional restriction on the right granted in this License, as
|
||||||
|
the Additional Use Grant; or (b) insert the text “None”.
|
||||||
|
|
||||||
|
3. To specify a Change Date.
|
||||||
|
|
||||||
|
4. Not to modify this License in any other way.
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Dashboard Role-Based Access Control
|
||||||
|
|
||||||
|
RBAC (Role-Based Access Control) is a common access control model for managing user access to systems, applications or resources.
|
||||||
|
In the RBAC model, access permissions are assigned and managed based on user roles instead of being directly associated with individual users,
|
||||||
|
making management and usage simpler.
|
||||||
|
|
||||||
|
This application houses the RBAC feature for Dashboard.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please see our [contributing.md](../../CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See [APL](../../APL.txt).
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
%% -*- mode: erlang; -*-
|
||||||
|
|
||||||
|
{erl_opts, [debug_info]}.
|
||||||
|
{deps, [
|
||||||
|
{emqx_connector, {path, "../../apps/emqx_dashboard"}}
|
||||||
|
]}.
|
|
@ -0,0 +1,14 @@
|
||||||
|
{application, emqx_dashboard_rbac, [
|
||||||
|
{description, "EMQX Dashboard RBAC"},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [
|
||||||
|
kernel,
|
||||||
|
stdlib,
|
||||||
|
emqx_dashboard
|
||||||
|
]},
|
||||||
|
{env, []},
|
||||||
|
{modules, []},
|
||||||
|
|
||||||
|
{links, []}
|
||||||
|
]}.
|
|
@ -0,0 +1,46 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_dashboard_rbac).
|
||||||
|
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||||
|
|
||||||
|
-export([check_rbac/2, role/1, valid_role/1]).
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, role/1}).
|
||||||
|
%%=====================================================================
|
||||||
|
%% API
|
||||||
|
check_rbac(Req, Extra) ->
|
||||||
|
Method = cowboy_req:method(Req),
|
||||||
|
Role = role(Extra),
|
||||||
|
check_rbac_with_method(Role, Method).
|
||||||
|
|
||||||
|
%% For compatibility
|
||||||
|
role(#?ADMIN{role = undefined}) ->
|
||||||
|
?ROLE_SUPERUSER;
|
||||||
|
role(#?ADMIN{role = Role}) ->
|
||||||
|
Role;
|
||||||
|
%% For compatibility
|
||||||
|
role([]) ->
|
||||||
|
?ROLE_SUPERUSER;
|
||||||
|
role(#{role := Role}) ->
|
||||||
|
Role.
|
||||||
|
|
||||||
|
valid_role(Role) ->
|
||||||
|
case lists:member(Role, role_list()) of
|
||||||
|
true ->
|
||||||
|
ok;
|
||||||
|
_ ->
|
||||||
|
{error, <<"Role does not exist">>}
|
||||||
|
end.
|
||||||
|
%% ===================================================================
|
||||||
|
check_rbac_with_method(?ROLE_SUPERUSER, _) ->
|
||||||
|
true;
|
||||||
|
check_rbac_with_method(?ROLE_VIEWER, <<"GET">>) ->
|
||||||
|
true;
|
||||||
|
check_rbac_with_method(_, _) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
role_list() ->
|
||||||
|
[?ROLE_VIEWER, ?ROLE_SUPERUSER].
|
|
@ -0,0 +1,157 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_dashboard_rbac_SUITE).
|
||||||
|
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include("emqx_dashboard.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-import(emqx_dashboard_api_test_helpers, [request/4, uri/1]).
|
||||||
|
|
||||||
|
-define(DEFAULT_SUPERUSER, <<"admin_user">>).
|
||||||
|
-define(DEFAULT_SUPERUSER_PASS, <<"admin_password">>).
|
||||||
|
-define(ADD_DESCRIPTION, <<>>).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
emqx_mgmt_api_test_util:init_suite([emqx_conf]),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
emqx_mgmt_api_test_util:end_suite([emqx_conf]).
|
||||||
|
|
||||||
|
end_per_testcase(_, _Config) ->
|
||||||
|
All = emqx_dashboard_admin:all_users(),
|
||||||
|
[emqx_dashboard_admin:remove_user(Name) || #{username := Name} <- All].
|
||||||
|
|
||||||
|
t_create_bad_role(_) ->
|
||||||
|
?assertEqual(
|
||||||
|
{error, <<"Role does not exist">>},
|
||||||
|
emqx_dashboard_admin:add_user(
|
||||||
|
?DEFAULT_SUPERUSER,
|
||||||
|
?DEFAULT_SUPERUSER_PASS,
|
||||||
|
<<"bad_role">>,
|
||||||
|
?ADD_DESCRIPTION
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
|
t_permission(_) ->
|
||||||
|
add_default_superuser(),
|
||||||
|
|
||||||
|
ViewerUser = <<"viewer_user">>,
|
||||||
|
ViewerPassword = <<"add_password">>,
|
||||||
|
|
||||||
|
%% add by superuser
|
||||||
|
{ok, 200, Payload} = emqx_dashboard_api_test_helpers:request(
|
||||||
|
?DEFAULT_SUPERUSER,
|
||||||
|
?DEFAULT_SUPERUSER_PASS,
|
||||||
|
post,
|
||||||
|
uri([users]),
|
||||||
|
#{
|
||||||
|
username => ViewerUser,
|
||||||
|
password => ViewerPassword,
|
||||||
|
role => ?ROLE_VIEWER,
|
||||||
|
description => ?ADD_DESCRIPTION
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
?assertEqual(
|
||||||
|
#{
|
||||||
|
<<"username">> => ViewerUser,
|
||||||
|
<<"role">> => ?ROLE_VIEWER,
|
||||||
|
<<"description">> => ?ADD_DESCRIPTION
|
||||||
|
},
|
||||||
|
emqx_utils_json:decode(Payload, [return_maps])
|
||||||
|
),
|
||||||
|
|
||||||
|
%% add by viewer
|
||||||
|
?assertMatch(
|
||||||
|
{ok, 403, _},
|
||||||
|
emqx_dashboard_api_test_helpers:request(
|
||||||
|
ViewerUser,
|
||||||
|
ViewerPassword,
|
||||||
|
post,
|
||||||
|
uri([users]),
|
||||||
|
#{
|
||||||
|
username => ViewerUser,
|
||||||
|
password => ViewerPassword,
|
||||||
|
role => ?ROLE_VIEWER,
|
||||||
|
description => ?ADD_DESCRIPTION
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_update_role(_) ->
|
||||||
|
add_default_superuser(),
|
||||||
|
|
||||||
|
%% update role by superuser
|
||||||
|
{ok, 200, Payload} = emqx_dashboard_api_test_helpers:request(
|
||||||
|
?DEFAULT_SUPERUSER,
|
||||||
|
?DEFAULT_SUPERUSER_PASS,
|
||||||
|
put,
|
||||||
|
uri([users, ?DEFAULT_SUPERUSER]),
|
||||||
|
#{
|
||||||
|
role => ?ROLE_VIEWER,
|
||||||
|
description => ?ADD_DESCRIPTION
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
?assertEqual(
|
||||||
|
#{
|
||||||
|
<<"username">> => ?DEFAULT_SUPERUSER,
|
||||||
|
<<"role">> => ?ROLE_VIEWER,
|
||||||
|
<<"description">> => ?ADD_DESCRIPTION
|
||||||
|
},
|
||||||
|
emqx_utils_json:decode(Payload, [return_maps])
|
||||||
|
),
|
||||||
|
|
||||||
|
%% update role by viewer
|
||||||
|
?assertMatch(
|
||||||
|
{ok, 403, _},
|
||||||
|
emqx_dashboard_api_test_helpers:request(
|
||||||
|
?DEFAULT_SUPERUSER,
|
||||||
|
?DEFAULT_SUPERUSER_PASS,
|
||||||
|
put,
|
||||||
|
uri([users, ?DEFAULT_SUPERUSER]),
|
||||||
|
#{
|
||||||
|
role => ?ROLE_SUPERUSER,
|
||||||
|
description => ?ADD_DESCRIPTION
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_clean_token(_) ->
|
||||||
|
Username = <<"admin_token">>,
|
||||||
|
Password = <<"public_www1">>,
|
||||||
|
Desc = <<"desc">>,
|
||||||
|
NewDesc = <<"new desc">>,
|
||||||
|
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, Desc),
|
||||||
|
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
|
FakeReq = #{method => <<"GET">>},
|
||||||
|
ok = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
|
%% change description
|
||||||
|
{ok, _} = emqx_dashboard_admin:update_user(Username, ?ROLE_SUPERUSER, NewDesc),
|
||||||
|
timer:sleep(5),
|
||||||
|
ok = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
|
%% change role
|
||||||
|
{ok, _} = emqx_dashboard_admin:update_user(Username, ?ROLE_VIEWER, NewDesc),
|
||||||
|
timer:sleep(5),
|
||||||
|
{error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
add_default_superuser() ->
|
||||||
|
{ok, _NewUser} = emqx_dashboard_admin:add_user(
|
||||||
|
?DEFAULT_SUPERUSER,
|
||||||
|
?DEFAULT_SUPERUSER_PASS,
|
||||||
|
?ROLE_SUPERUSER,
|
||||||
|
?ADD_DESCRIPTION
|
||||||
|
).
|
|
@ -114,7 +114,8 @@
|
||||||
emqx_node_rebalance,
|
emqx_node_rebalance,
|
||||||
emqx_ft,
|
emqx_ft,
|
||||||
emqx_ldap,
|
emqx_ldap,
|
||||||
emqx_gcp_device
|
emqx_gcp_device,
|
||||||
|
emqx_dashboard_rbac
|
||||||
],
|
],
|
||||||
%% must always be of type `load'
|
%% must always be of type `load'
|
||||||
ce_business_apps =>
|
ce_business_apps =>
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
|
-define(ROLE_SUPERUSER, <<"superuser">>).
|
||||||
-define(BOOTSTRAP_BACKUP, "emqx-export-test-bootstrap-ce.tar.gz").
|
-define(BOOTSTRAP_BACKUP, "emqx-export-test-bootstrap-ce.tar.gz").
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
|
@ -297,7 +298,7 @@ t_import_on_cluster(Config) ->
|
||||||
t_verify_imported_mnesia_tab_on_cluster(Config) ->
|
t_verify_imported_mnesia_tab_on_cluster(Config) ->
|
||||||
UsersToExport = users(<<"user_to_export_">>),
|
UsersToExport = users(<<"user_to_export_">>),
|
||||||
UsersBeforeImport = users(<<"user_before_import_">>),
|
UsersBeforeImport = users(<<"user_before_import_">>),
|
||||||
[{ok, _} = emqx_dashboard_admin:add_user(U, U, U) || U <- UsersToExport],
|
[{ok, _} = emqx_dashboard_admin:add_user(U, U, ?ROLE_SUPERUSER, U) || U <- UsersToExport],
|
||||||
{ok, #{filename := FileName}} = emqx_mgmt_data_backup:export(),
|
{ok, #{filename := FileName}} = emqx_mgmt_data_backup:export(),
|
||||||
{ok, Cwd} = file:get_cwd(),
|
{ok, Cwd} = file:get_cwd(),
|
||||||
AbsFilePath = filename:join(Cwd, FileName),
|
AbsFilePath = filename:join(Cwd, FileName),
|
||||||
|
@ -305,7 +306,7 @@ t_verify_imported_mnesia_tab_on_cluster(Config) ->
|
||||||
[CoreNode1, CoreNode2, ReplicantNode] = ?config(cluster, Config),
|
[CoreNode1, CoreNode2, ReplicantNode] = ?config(cluster, Config),
|
||||||
|
|
||||||
[
|
[
|
||||||
{ok, _} = rpc:call(CoreNode1, emqx_dashboard_admin, add_user, [U, U, U])
|
{ok, _} = rpc:call(CoreNode1, emqx_dashboard_admin, add_user, [U, U, ?ROLE_SUPERUSER, U])
|
||||||
|| U <- UsersBeforeImport
|
|| U <- UsersBeforeImport
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
Implemented a preliminary Role-Based Access Control for the Dashboard.
|
||||||
|
|
||||||
|
In this version, there are two predefined roles:
|
||||||
|
- superuser
|
||||||
|
|
||||||
|
This role could access all resources.
|
||||||
|
- viewer
|
||||||
|
|
||||||
|
This role can only view resources and data, corresponding to all GET requests in the REST API.
|
5
mix.exs
5
mix.exs
|
@ -58,7 +58,7 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
{:ekka, github: "emqx/ekka", tag: "0.15.13", override: true},
|
{:ekka, github: "emqx/ekka", tag: "0.15.13", override: true},
|
||||||
{:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true},
|
{:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true},
|
||||||
{:grpc, github: "emqx/grpc-erl", tag: "0.6.8", override: true},
|
{:grpc, github: "emqx/grpc-erl", tag: "0.6.8", override: true},
|
||||||
{:minirest, github: "emqx/minirest", tag: "1.3.11", override: true},
|
{:minirest, github: "emqx/minirest", tag: "1.3.12", override: true},
|
||||||
{:ecpool, github: "emqx/ecpool", tag: "0.5.4", override: true},
|
{:ecpool, github: "emqx/ecpool", tag: "0.5.4", override: true},
|
||||||
{:replayq, github: "emqx/replayq", tag: "0.3.7", override: true},
|
{:replayq, github: "emqx/replayq", tag: "0.3.7", override: true},
|
||||||
{:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true},
|
{:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true},
|
||||||
|
@ -226,7 +226,8 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
:emqx_bridge_kinesis,
|
:emqx_bridge_kinesis,
|
||||||
:emqx_bridge_azure_event_hub,
|
:emqx_bridge_azure_event_hub,
|
||||||
:emqx_ldap,
|
:emqx_ldap,
|
||||||
:emqx_gcp_device
|
:emqx_gcp_device,
|
||||||
|
:emqx_dashboard_rbac
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.15.13"}}}
|
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.15.13"}}}
|
||||||
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}
|
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}
|
||||||
, {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.8"}}}
|
, {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.8"}}}
|
||||||
, {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.11"}}}
|
, {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.12"}}}
|
||||||
, {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.4"}}}
|
, {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.4"}}}
|
||||||
, {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.7"}}}
|
, {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.7"}}}
|
||||||
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
|
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
|
||||||
|
|
|
@ -109,6 +109,7 @@ is_community_umbrella_app("apps/emqx_bridge_kinesis") -> false;
|
||||||
is_community_umbrella_app("apps/emqx_bridge_azure_event_hub") -> false;
|
is_community_umbrella_app("apps/emqx_bridge_azure_event_hub") -> false;
|
||||||
is_community_umbrella_app("apps/emqx_ldap") -> false;
|
is_community_umbrella_app("apps/emqx_ldap") -> false;
|
||||||
is_community_umbrella_app("apps/emqx_gcp_device") -> false;
|
is_community_umbrella_app("apps/emqx_gcp_device") -> false;
|
||||||
|
is_community_umbrella_app("apps/emqx_dashboard_rbac") -> false;
|
||||||
is_community_umbrella_app(_) -> true.
|
is_community_umbrella_app(_) -> true.
|
||||||
|
|
||||||
is_jq_supported() ->
|
is_jq_supported() ->
|
||||||
|
|
Loading…
Reference in New Issue