Просмотр исходного кода

test(rbac): add test cases for RBAC && update changes

firest 2 лет назад
Родитель
Сommit
4b97d3f57d

+ 24 - 4
apps/emqx_dashboard/src/emqx_dashboard_admin.erl

@@ -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
@@ -228,7 +229,20 @@ remove_user(Username) when is_binary(Username) ->
 update_user(Username, Role, Desc) when is_binary(Username) ->
     case legal_role(Role) of
         ok ->
-            return(mria:transaction(?DASHBOARD_SHARD, fun update_user_/3, [Username, Role, Desc]));
+            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.
@@ -258,7 +272,7 @@ update_user_(Username, Role, Desc) ->
             mnesia:abort(<<"username_not_found">>);
         [Admin] ->
             mnesia:write(Admin#?ADMIN{role = Role, description = Desc}),
-            #{username => Username, role => Role, description => Desc}
+            {role(Admin) =:= Role, #{username => Username, role => Role, description => Desc}}
     end.
 
 change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) ->
@@ -381,8 +395,9 @@ add_default_user(Username, Password) ->
         _ -> {ok, default_user_exists}
     end.
 
-%% ensure the `role` is correct when it directly read from the table
+%% 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) ->
@@ -392,6 +407,9 @@ ensure_role(Role) when is_binary(Role) ->
 legal_role(Role) ->
     emqx_dashboard_rbac:legal_role(Role).
 
+role(Data) ->
+    emqx_dashboard_rbac:role(Data).
+
 -else.
 
 -dialyzer({no_match, [add_user/4, update_user/3]}).
@@ -399,6 +417,8 @@ legal_role(Role) ->
 legal_role(_) ->
     ok.
 
+role(_) ->
+    ?ROLE_DEFAULT.
 -endif.
 
 -ifdef(TEST).

+ 1 - 7
apps/emqx_dashboard/src/emqx_dashboard_token.erl

@@ -117,7 +117,7 @@ do_sign(#?ADMIN{username = Username} = User, Password) ->
     },
     Signed = jose_jwt:sign(JWK, JWS, JWT),
     {_, Token} = jose_jws:compact(Signed),
-    Role = role(User),
+    Role = emqx_dashboard_admin:role(User),
     JWTRec = format(Token, Username, Role, ExpTime),
     _ = mria:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]),
     {ok, Token}.
@@ -249,9 +249,6 @@ clean_expired_jwt(Now) ->
 check_rbac(Req, Extra) ->
     emqx_dashboard_rbac:check_rbac(Req, Extra).
 
-role(Data) ->
-    emqx_dashboard_rbac:role(Data).
-
 -else.
 
 -dialyzer({nowarn_function, [check_rbac/2]}).
@@ -260,7 +257,4 @@ role(Data) ->
 check_rbac(_Req, _Extra) ->
     true.
 
-role(_) ->
-    ?ROLE_DEFAULT.
-
 -endif.

+ 4 - 3
apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl

@@ -119,10 +119,11 @@ t_rest_api(_Config) ->
     {ok, 200, Res0} = http_get(["users"]),
     ?assertEqual(
         [
-            #{
+            filter_req(#{
                 <<"username">> => <<"admin">>,
-                <<"description">> => <<"administrator">>
-            }
+                <<"description">> => <<"administrator">>,
+                <<"role">> => ?ROLE_SUPERUSER
+            })
         ],
         get_http_data(Res0)
     ),

+ 1 - 1
apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl

@@ -175,7 +175,7 @@ t_clean_token(_) ->
     NewPassword = <<"public_www2">>,
     {ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>),
     {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
-    FakeReq = #{method => <<"get">>},
+    FakeReq = #{method => <<"GET">>},
     ok = emqx_dashboard_admin:verify_token(FakeReq, Token),
     %% change password
     {ok, _} = emqx_dashboard_admin:change_password(Username, Password, NewPassword),

+ 10 - 3
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)}.
 

+ 0 - 2
apps/emqx_dashboard_rbac/.gitignore

@@ -1,2 +0,0 @@
-src/emqx_ldap_filter_lexer.erl
-src/emqx_ldap_filter_parser.erl

+ 157 - 0
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
+    ).

+ 6 - 0
changes/ee/feat-11610.en.md

@@ -0,0 +1,6 @@
+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 only can access the `GET` resource.

+ 1 - 1
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},