diff --git a/apps/emqx_dashboard/include/emqx_dashboard.hrl b/apps/emqx_dashboard/include/emqx_dashboard.hrl index 095229c53..8ebb4d3d5 100644 --- a/apps/emqx_dashboard/include/emqx_dashboard.hrl +++ b/apps/emqx_dashboard/include/emqx_dashboard.hrl @@ -15,23 +15,34 @@ %%-------------------------------------------------------------------- -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, { username :: binary(), pwdhash :: binary(), description :: binary(), - role = undefined :: atom(), - %% not used so far, for future extension - extra = [] :: term() + role = ?ROLE_DEFAULT :: binary(), + extra = #{} :: map() }). +-type dashboard_user_role() :: binary(). +-type dashboard_user() :: #?ADMIN{}. + -define(ADMIN_JWT, emqx_admin_jwt). -record(?ADMIN_JWT, { token :: binary(), username :: binary(), exptime :: integer(), - %% not used so far, fur future extension - extra = [] :: term() + extra = #{} :: map() }). -define(TAB_COLLECT, emqx_collect). diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 8c56d8014..e6a9a2fd1 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -205,13 +205,16 @@ authorize(Req) -> {basic, Username, Password} -> api_key_authorize(Req, Username, Password); {bearer, Token} -> - case emqx_dashboard_admin:verify_token(Token) of + case emqx_dashboard_admin:verify_token(Req, Token) of ok -> ok; {error, token_timeout} -> {401, 'TOKEN_TIME_OUT', <<"Token expired, get new token by POST /login">>}; {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; _ -> return_unauthorized( diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index e8f95d609..06dac9a01 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -30,10 +30,10 @@ -export([mnesia/1]). -export([ - add_user/3, - force_add_user/3, + add_user/4, + force_add_user/4, remove_user/1, - update_user/2, + update_user/3, lookup_user/1, change_password/2, change_password/3, @@ -43,7 +43,7 @@ -export([ sign_token/2, - verify_token/1, + verify_token/2, destroy_token_by_username/2 ]). -export([ @@ -56,10 +56,11 @@ default_username/0 ]). +-export([role/1]). + -export([backup_tables/0]). -type emqx_admin() :: #?ADMIN{}. --define(BOOTSTRAP_USER_TAG, <<"bootstrap user">>). %%-------------------------------------------------------------------- %% Mnesia bootstrap @@ -98,18 +99,19 @@ add_default_user() -> %% API %%-------------------------------------------------------------------- --spec add_user(binary(), binary(), binary()) -> {ok, map()} | {error, any()}. -add_user(Username, Password, Desc) when +-spec add_user(binary(), binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, any()}. +add_user(Username, Password, Role, Desc) when is_binary(Username), is_binary(Password) -> - case {legal_username(Username), legal_password(Password)} of - {ok, ok} -> do_add_user(Username, Password, Desc); - {{error, Reason}, _} -> {error, Reason}; - {_, {error, Reason}} -> {error, Reason} + case {legal_username(Username), legal_password(Password), legal_role(Role)} of + {ok, ok, ok} -> do_add_user(Username, Password, Role, Desc); + {{error, Reason}, _, _} -> {error, Reason}; + {_, {error, Reason}, _} -> {error, Reason}; + {_, _, {error, Reason}} -> {error, Reason} end. -do_add_user(Username, Password, Desc) -> - Res = mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc]), +do_add_user(Username, Password, Role, Desc) -> + Res = mria:transaction(?DASHBOARD_SHARD, fun add_user_/4, [Username, Password, Role, Desc]), return(Res). %% 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). %% black-magic: force overwrite a user -force_add_user(Username, Password, Desc) -> +force_add_user(Username, Password, Role, Desc) -> AddFun = fun() -> mnesia:write(#?ADMIN{ username = Username, pwdhash = hash(Password), + role = Role, description = Desc }) end, @@ -191,12 +194,17 @@ force_add_user(Username, Password, Desc) -> end. %% @private -add_user_(Username, Password, Desc) -> +add_user_(Username, Password, Role, Desc) -> 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), - #{username => Username, description => Desc}; + #{username => Username, role => Role, description => Desc}; [_] -> mnesia:abort(<<"username_already_exist">>) end. @@ -217,9 +225,27 @@ remove_user(Username) when is_binary(Username) -> {error, Reason} end. --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])). +-spec update_user(binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, term()}. +update_user(Username, Role, Desc) when is_binary(Username) -> + 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) -> SaltBin = emqx_dashboard_token:salt(), @@ -240,18 +266,18 @@ sha256(SaltBin, Password) -> crypto:hash('sha256', <>). %% @private -update_user_(Username, Desc) -> +update_user_(Username, Role, Desc) -> case mnesia:wread({?ADMIN, Username}) of [] -> mnesia:abort(<<"username_not_found">>); [Admin] -> - mnesia:write(Admin#?ADMIN{description = Desc}), - #{username => Username, description => Desc} + mnesia:write(Admin#?ADMIN{role = Role, description = Desc}), + {role(Admin) =:= Role, #{username => Username, role => Role, description => Desc}} end. change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) -> case check(Username, OldPasswd) of - ok -> change_password(Username, NewPasswd); + {ok, _} -> change_password(Username, NewPasswd); Error -> Error end. @@ -298,12 +324,14 @@ all_users() -> fun( #?ADMIN{ username = Username, - description = Desc + description = Desc, + role = Role } ) -> #{ username => Username, - description => Desc + description => Desc, + role => ensure_role(Role) } end, ets:tab2list(?ADMIN) @@ -320,9 +348,9 @@ check(_, undefined) -> {error, <<"password_not_provided">>}; check(Username, Password) -> case lookup_user(Username) of - [#?ADMIN{pwdhash = PwdHash}] -> + [#?ADMIN{pwdhash = PwdHash} = User] -> case verify_hash(Password, PwdHash) of - ok -> ok; + ok -> {ok, User}; error -> {error, <<"password_error">>} end; [] -> @@ -333,14 +361,14 @@ check(Username, Password) -> %% token sign_token(Username, Password) -> case check(Username, Password) of - ok -> - emqx_dashboard_token:sign(Username, Password); + {ok, User} -> + emqx_dashboard_token:sign(User, Password); Error -> Error end. -verify_token(Token) -> - emqx_dashboard_token:verify(Token). +verify_token(Req, Token) -> + emqx_dashboard_token:verify(Req, Token). destroy_token_by_username(Username, Token) -> 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}; add_default_user(Username, Password) -> case lookup_user(Username) of - [] -> do_add_user(Username, Password, <<"administrator">>); + [] -> do_add_user(Username, Password, ?ROLE_SUPERUSER, <<"administrator">>); _ -> {ok, default_user_exists} 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). -include_lib("eunit/include/eunit.hrl"). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 108cde379..9ed6d1a77 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -18,6 +18,7 @@ -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"). @@ -111,9 +112,9 @@ schema("/users") -> post => #{ tags => [<<"dashboard">>], desc => ?DESC(create_user_api), - 'requestBody' => fields([username, password, description]), + 'requestBody' => fields([username, password, role, description]), responses => #{ - 200 => fields([username, description]) + 200 => fields([username, role, description]) } } }; @@ -124,9 +125,9 @@ schema("/users/:username") -> tags => [<<"dashboard">>], desc => ?DESC(update_user_api), parameters => fields([username_in_path]), - 'requestBody' => fields([description]), + 'requestBody' => fields([role, description]), responses => #{ - 200 => fields([username, description]), + 200 => fields([username, role, description]), 404 => response_schema(404) } }, @@ -170,7 +171,7 @@ response_schema(404) -> fields(user) -> fields([username, description]); fields(List) -> - [field(Key) || Key <- List]. + [field(Key) || Key <- List, field_filter(Key)]. field(username) -> {username, @@ -203,7 +204,9 @@ field(version) -> field(old_pwd) -> {old_pwd, mk(binary(), #{desc => ?DESC(old_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 @@ -239,19 +242,20 @@ logout(_, #{ end. users(get, _Request) -> - {200, emqx_dashboard_admin:all_users()}; + {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, Desc) of + case emqx_dashboard_admin:add_user(Username, Password, Role, Desc) of {ok, Result} -> ?SLOG(info, #{msg => "Create dashboard success", username => Username}), - {200, Result}; + {200, filter_result(Result)}; {error, Reason} -> ?SLOG(info, #{ msg => "Create dashboard failed", @@ -263,12 +267,15 @@ users(post, #{body := Params}) -> end. user(put, #{bindings := #{username := Username}, body := Params}) -> + Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT), 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} -> - {200, Result}; + {200, filter_result(Result)}; + {error, <<"username_not_found">> = Reason} -> + {404, ?USER_NOT_FOUND, Reason}; {error, Reason} -> - {404, ?USER_NOT_FOUND, Reason} + {400, ?BAD_REQUEST, Reason} end; user(delete, #{bindings := #{username := Username}, headers := Headers}) -> case Username == emqx_dashboard_admin:default_username() of @@ -347,3 +354,24 @@ change_pwd(post, #{bindings := #{username := Username}, body := Params}) -> {400, ?BAD_REQUEST, Reason} 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. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_cli.erl b/apps/emqx_dashboard/src/emqx_dashboard_cli.erl index 52a915ecb..3da3e822d 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_cli.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_cli.erl @@ -16,6 +16,8 @@ -module(emqx_dashboard_cli). +-include("emqx_dashboard.hrl"). + -export([ load/0, admins/1, @@ -25,15 +27,6 @@ load() -> 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]) -> case emqx_dashboard_admin:change_password(bin(Username), bin(Password)) of {ok, _} -> @@ -48,14 +41,8 @@ admins(["del", Username]) -> {error, Reason} -> print_error(Reason) end; -admins(_) -> - emqx_ctl:usage( - [ - {"admins add ", "Add dashboard user"}, - {"admins passwd ", "Reset dashboard user password"}, - {"admins del ", "Delete dashboard user"} - ] - ). +admins(Args) -> + inner_admins(Args). unload() -> 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. % print_error(Reason) -> % emqx_ctl:print("Error: ~p~n", [Reason]). + +-if(?EMQX_RELEASE_EDITION == ee). +usage() -> + [ + {"admins add ", "Add dashboard user"}, + {"admins passwd ", "Reset dashboard user password"}, + {"admins del ", "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 ", "Add dashboard user"}, + {"admins passwd ", "Reset dashboard user password"}, + {"admins del ", "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. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index e8357c458..f71df77bd 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -20,7 +20,7 @@ -export([ sign/2, - verify/1, + verify/2, lookup/1, owner/1, destroy/1, @@ -55,14 +55,17 @@ %%-------------------------------------------------------------------- %% jwt function --spec sign(Username :: binary(), Password :: binary()) -> +-spec sign(User :: dashboard_user(), Password :: binary()) -> {ok, Token :: binary()} | {error, Reason :: term()}. -sign(Username, Password) -> - do_sign(Username, Password). +sign(User, Password) -> + do_sign(User, Password). --spec verify(Token :: binary()) -> Result :: ok | {error, token_timeout | not_found}. -verify(Token) -> - do_verify(Token). +-spec verify(_, Token :: binary()) -> + Result :: + ok + | {error, token_timeout | not_found | unauthorized_role}. +verify(Req, Token) -> + do_verify(Req, Token). -spec destroy(KeyOrKeys :: list() | binary() | #?ADMIN_JWT{}) -> ok. destroy([]) -> @@ -101,7 +104,7 @@ mnesia(boot) -> %%-------------------------------------------------------------------- %% jwt apply -do_sign(Username, Password) -> +do_sign(#?ADMIN{username = Username} = User, Password) -> ExpTime = jwt_expiration_time(), Salt = salt(), JWK = jwk(Username, Password, Salt), @@ -114,22 +117,28 @@ do_sign(Username, Password) -> }, Signed = jose_jwt:sign(JWK, JWS, JWT), {_, 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]), {ok, Token}. -do_verify(Token) -> +do_verify(Req, Token) -> 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 true -> - NewJWT = JWT#?ADMIN_JWT{exptime = jwt_expiration_time()}, - {atomic, Res} = mria:transaction( - ?DASHBOARD_SHARD, - fun mnesia:write/1, - [NewJWT] - ), - Res; + case check_rbac(Req, Extra) of + true -> + NewJWT = JWT#?ADMIN_JWT{exptime = jwt_expiration_time()}, + {atomic, Res} = mria:transaction( + ?DASHBOARD_SHARD, + fun mnesia:write/1, + [NewJWT] + ), + Res; + _ -> + {error, unauthorized_role} + end; _ -> {error, token_timeout} end; @@ -183,11 +192,12 @@ jwt_expiration_time() -> token_ttl() -> emqx_conf:get([dashboard, token_expired_time], ?EXPTIME). -format(Token, Username, ExpTime) -> +format(Token, Username, Role, ExpTime) -> #?ADMIN_JWT{ token = Token, username = Username, - exptime = ExpTime + exptime = ExpTime, + extra = #{role => Role} }. %%-------------------------------------------------------------------- @@ -234,3 +244,17 @@ clean_expired_jwt(Now) -> fun() -> mnesia:select(?TAB, Spec) end ), 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. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index f0b6db8ea..141044836 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -67,7 +67,9 @@ end_per_suite(_Config) -> t_overview(_) -> 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">>), [ {ok, _} = request_dashboard(get, api_path([Overview]), Headers) @@ -77,8 +79,12 @@ t_overview(_) -> t_admins_add_delete(_) -> mnesia:clear_table(?ADMIN), Desc = <<"simple description">>, - {ok, _} = emqx_dashboard_admin:add_user(<<"username">>, <<"password_0">>, Desc), - {ok, _} = emqx_dashboard_admin:add_user(<<"username1">>, <<"password1">>, Desc), + {ok, _} = emqx_dashboard_admin:add_user( + <<"username">>, <<"password_0">>, ?ROLE_SUPERUSER, Desc + ), + {ok, _} = emqx_dashboard_admin:add_user( + <<"username1">>, <<"password1">>, ?ROLE_SUPERUSER, Desc + ), Admins = emqx_dashboard_admin:all_users(), ?assertEqual(2, length(Admins)), {ok, _} = emqx_dashboard_admin:remove_user(<<"username1">>), @@ -95,7 +101,7 @@ t_admins_add_delete(_) -> t_admin_delete_self_failed(_) -> mnesia:clear_table(?ADMIN), 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(), ?assertEqual(1, length(Admins)), Header = auth_header_(<<"username1">>, <<"password_1">>), @@ -109,23 +115,34 @@ t_rest_api(_Config) -> mnesia:clear_table(?ADMIN), Desc = <<"administrator">>, 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"]), ?assertEqual( [ - #{ + filter_req(#{ <<"username">> => <<"admin">>, - <<"description">> => <<"administrator">> - } + <<"description">> => <<"administrator">>, + <<"role">> => ?ROLE_SUPERUSER + }) ], get_http_data(Res0) ), - {ok, 200, _} = http_put(["users", "admin"], #{<<"description">> => <<"a_new_description">>}), - {ok, 200, _} = http_post(["users"], #{ - <<"username">> => <<"usera">>, - <<"password">> => <<"passwd_01234">>, - <<"description">> => Desc - }), + {ok, 200, _} = http_put( + ["users", "admin"], + filter_req(#{ + <<"role">> => ?ROLE_SUPERUSER, + <<"description">> => <<"a_new_description">> + }) + ), + {ok, 200, _} = http_post( + ["users"], + filter_req(#{ + <<"username">> => <<"usera">>, + <<"password">> => <<"passwd_01234">>, + <<"role">> => ?ROLE_SUPERUSER, + <<"description">> => Desc + }) + ), {ok, 204, _} = http_delete(["users", "usera"]), {ok, 404, _} = http_delete(["users", "usera"]), {ok, 204, _} = http_post( @@ -136,7 +153,7 @@ t_rest_api(_Config) -> } ), mnesia:clear_table(?ADMIN), - emqx_dashboard_admin:add_user(<<"admin">>, Password, <<"administrator">>), + emqx_dashboard_admin:add_user(<<"admin">>, Password, ?ROLE_SUPERUSER, <<"administrator">>), ok. t_swagger_json(_Config) -> @@ -180,7 +197,7 @@ t_cli(_Config) -> t_lookup_by_username_jwt(_Config) -> User = bin(["user-", 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( [#?ADMIN_JWT{username = User}], emqx_dashboard_token:lookup_by_username(User) @@ -194,7 +211,7 @@ t_lookup_by_username_jwt(_Config) -> t_clean_expired_jwt(_Config) -> User = bin(["user-", 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}] = emqx_dashboard_token:lookup_by_username(User), ok = emqx_dashboard_token:clean_expired_jwt(_Now1 = ExpTime), @@ -261,3 +278,14 @@ api_path(Parts) -> json(Data) -> {ok, Jsx} = emqx_utils_json:safe_decode(Data, [return_maps]), Jsx. + +-if(?EMQX_RELEASE_EDITION == ee). +filter_req(Req) -> + Req. + +-else. + +filter_req(Req) -> + maps:without([role, <<"role">>], Req). + +-endif. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl index c12849ac7..45684012b 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl @@ -42,8 +42,8 @@ t_check_user(_) -> BadPassword = <<"public_bad">>, EmptyUsername = <<>>, EmptyPassword = <<>>, - {ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>), - ok = emqx_dashboard_admin:check(Username, Password), + {ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>), + {ok, _} = emqx_dashboard_admin:check(Username, Password), {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, BadPassword), @@ -61,19 +61,23 @@ t_add_user(_) -> BadAddUser = <<"***add_user_bad">>, %% 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), AddDescription = maps:get(description, NewUser), false = maps:is_key(password, NewUser), %% add again {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 BadNameError = <<"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. t_lookup_user(_) -> @@ -84,7 +88,9 @@ t_lookup_user(_) -> BadLookupUser = <<"***lookup_user_bad">>, {ok, _} = - emqx_dashboard_admin:add_user(LookupUser, LookupPassword, LookupDescription), + emqx_dashboard_admin:add_user( + LookupUser, LookupPassword, ?ROLE_SUPERUSER, LookupDescription + ), %% lookup success. not return password [#emqx_admin{username = LookupUser, description = LookupDescription}] = emqx_dashboard_admin:lookup_user(LookupUser), @@ -95,7 +101,7 @@ t_lookup_user(_) -> t_all_users(_) -> Username = <<"admin_all">>, 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(), ?assert(erlang:length(All) >= 1), ok. @@ -108,7 +114,9 @@ t_delete_user(_) -> DeleteBadUser = <<"delete_user_bad">>, {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), %% remove again {error, <<"username_not_found">>} = emqx_dashboard_admin:remove_user(DeleteUser), @@ -124,13 +132,17 @@ t_update_user(_) -> 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} = - emqx_dashboard_admin:update_user(UpdateUser, NewDesc), + emqx_dashboard_admin:update_user(UpdateUser, ?ROLE_SUPERUSER, NewDesc), UpdateUser = maps:get(username, 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. t_change_password(_) -> @@ -143,7 +155,7 @@ t_change_password(_) -> 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), %% change pwd again @@ -161,17 +173,18 @@ t_clean_token(_) -> Username = <<"admin_token">>, Password = <<"public_www1">>, 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 = emqx_dashboard_admin:verify_token(Token), + FakeReq = #{method => <<"GET">>}, + ok = emqx_dashboard_admin:verify_token(FakeReq, Token), %% change password {ok, _} = emqx_dashboard_admin:change_password(Username, Password, NewPassword), timer:sleep(5), - {error, not_found} = emqx_dashboard_admin:verify_token(Token), + {error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token), %% remove user {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), timer:sleep(5), - {error, not_found} = emqx_dashboard_admin:verify_token(Token2), + {error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token2), ok. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl index 5e8c61e15..cf022d65d 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl @@ -24,6 +24,7 @@ request/2, request/3, request/4, + request/5, multipart_formdata_request/3, multipart_formdata_request/4, host/0, @@ -73,6 +74,9 @@ request(Method, Url, Body) -> request(<<"admin">>, Method, Url, Body). request(Username, Method, Url, Body) -> + request(Username, <<"public">>, Method, Url, Body). + +request(Username, Password, Method, Url, Body) -> Request = case Body of [] when @@ -80,9 +84,10 @@ request(Username, Method, Url, Body) -> Method =:= head orelse Method =:= delete orelse 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, ct:pal("Method: ~p, Request: ~p", [Method, Request]), 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])). auth_header(Username) -> - Password = <<"public">>, + auth_header(Username, <<"public">>). + +auth_header(Username, Password) -> {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), {"Authorization", "Bearer " ++ binary_to_list(Token)}. diff --git a/apps/emqx_dashboard_rbac/BSL.txt b/apps/emqx_dashboard_rbac/BSL.txt new file mode 100644 index 000000000..0acc0e696 --- /dev/null +++ b/apps/emqx_dashboard_rbac/BSL.txt @@ -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. diff --git a/apps/emqx_dashboard_rbac/README.md b/apps/emqx_dashboard_rbac/README.md new file mode 100644 index 000000000..9d854d29d --- /dev/null +++ b/apps/emqx_dashboard_rbac/README.md @@ -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). diff --git a/apps/emqx_dashboard_rbac/docker-ct b/apps/emqx_dashboard_rbac/docker-ct new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/emqx_dashboard_rbac/docker-ct @@ -0,0 +1 @@ + diff --git a/apps/emqx_dashboard_rbac/rebar.config b/apps/emqx_dashboard_rbac/rebar.config new file mode 100644 index 000000000..03d877a31 --- /dev/null +++ b/apps/emqx_dashboard_rbac/rebar.config @@ -0,0 +1,6 @@ +%% -*- mode: erlang; -*- + +{erl_opts, [debug_info]}. +{deps, [ + {emqx_connector, {path, "../../apps/emqx_dashboard"}} +]}. diff --git a/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.app.src b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.app.src new file mode 100644 index 000000000..190764e2f --- /dev/null +++ b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.app.src @@ -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, []} +]}. diff --git a/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl new file mode 100644 index 000000000..74f6312ea --- /dev/null +++ b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl @@ -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]. diff --git a/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl new file mode 100644 index 000000000..607cb710d --- /dev/null +++ b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl @@ -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 + ). diff --git a/apps/emqx_machine/priv/reboot_lists.eterm b/apps/emqx_machine/priv/reboot_lists.eterm index 0e2ecb799..d794dabd9 100644 --- a/apps/emqx_machine/priv/reboot_lists.eterm +++ b/apps/emqx_machine/priv/reboot_lists.eterm @@ -114,7 +114,8 @@ emqx_node_rebalance, emqx_ft, emqx_ldap, - emqx_gcp_device + emqx_gcp_device, + emqx_dashboard_rbac ], %% must always be of type `load' ce_business_apps => diff --git a/apps/emqx_management/test/emqx_mgmt_data_backup_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_data_backup_SUITE.erl index 7cb2c9cf1..e737c12b9 100644 --- a/apps/emqx_management/test/emqx_mgmt_data_backup_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_data_backup_SUITE.erl @@ -22,6 +22,7 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). +-define(ROLE_SUPERUSER, <<"superuser">>). -define(BOOTSTRAP_BACKUP, "emqx-export-test-bootstrap-ce.tar.gz"). all() -> @@ -297,7 +298,7 @@ t_import_on_cluster(Config) -> t_verify_imported_mnesia_tab_on_cluster(Config) -> UsersToExport = users(<<"user_to_export_">>), 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, Cwd} = file:get_cwd(), AbsFilePath = filename:join(Cwd, FileName), @@ -305,7 +306,7 @@ t_verify_imported_mnesia_tab_on_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 ], diff --git a/changes/ee/feat-11610.en.md b/changes/ee/feat-11610.en.md new file mode 100644 index 000000000..db63d6cc7 --- /dev/null +++ b/changes/ee/feat-11610.en.md @@ -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. diff --git a/mix.exs b/mix.exs index 749258f3e..68365f44c 100644 --- a/mix.exs +++ b/mix.exs @@ -58,7 +58,7 @@ defmodule EMQXUmbrella.MixProject do {:ekka, github: "emqx/ekka", tag: "0.15.13", 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}, - {: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}, {:replayq, github: "emqx/replayq", tag: "0.3.7", 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_azure_event_hub, :emqx_ldap, - :emqx_gcp_device + :emqx_gcp_device, + :emqx_dashboard_rbac ]) end diff --git a/rebar.config b/rebar.config index 7a27a59a1..935c5be03 100644 --- a/rebar.config +++ b/rebar.config @@ -65,7 +65,7 @@ , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.15.13"}}} , {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"}}} - , {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"}}} , {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"}}} diff --git a/rebar.config.erl b/rebar.config.erl index 3efdfe079..255a8e3cc 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -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_ldap") -> 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_jq_supported() ->