Przeglądaj źródła

feat(api_key): add RBAC feature for the API key

firest 2 lat temu
rodzic
commit
e095de7367

+ 7 - 1
apps/emqx/test/emqx_common_test_http.erl

@@ -17,6 +17,7 @@
 -module(emqx_common_test_http).
 
 -include_lib("common_test/include/ct.hrl").
+-include_lib("emqx_dashboard/include/emqx_dashboard_rbac.hrl").
 
 -export([
     request_api/3,
@@ -90,7 +91,12 @@ create_default_app() ->
     Now = erlang:system_time(second),
     ExpiredAt = Now + timer:minutes(10),
     emqx_mgmt_auth:create(
-        ?DEFAULT_APP_ID, ?DEFAULT_APP_SECRET, true, ExpiredAt, <<"default app key for test">>
+        ?DEFAULT_APP_ID,
+        ?DEFAULT_APP_SECRET,
+        true,
+        ExpiredAt,
+        <<"default app key for test">>,
+        ?ROLE_API_SUPERUSER
     ).
 
 delete_default_app() ->

+ 2 - 9
apps/emqx_dashboard/include/emqx_dashboard.hrl

@@ -13,16 +13,9 @@
 %% See the License for the specific language governing permissions and
 %% limitations under the License.
 %%--------------------------------------------------------------------
--define(ADMIN, emqx_admin).
+-include("emqx_dashboard_rbac.hrl").
 
-%% 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, <<"administrator">>).
--define(ROLE_DEFAULT, ?ROLE_SUPERUSER).
+-define(ADMIN, emqx_admin).
 
 -define(BACKEND_LOCAL, local).
 -define(SSO_USERNAME(Backend, Name), {Backend, Name}).

+ 33 - 0
apps/emqx_dashboard/include/emqx_dashboard_rbac.hrl

@@ -0,0 +1,33 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+-ifndef(EMQX_DASHBOARD_RBAC).
+-define(EMQX_DASHBOARD_RBAC, true).
+
+%% 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, <<"administrator">>).
+-define(ROLE_DEFAULT, ?ROLE_SUPERUSER).
+
+-define(ROLE_API_VIEWER, <<"api_viewer">>).
+-define(ROLE_API_SUPERUSER, <<"api_administrator">>).
+-define(ROLE_API_PUBLISHER, <<"api_publisher">>).
+-define(ROLE_API_DEFAULT, ?ROLE_API_SUPERUSER).
+
+-endif.

+ 4 - 1
apps/emqx_dashboard/src/emqx_dashboard.erl

@@ -251,7 +251,7 @@ listeners() ->
 
 api_key_authorize(Req, Key, Secret) ->
     Path = cowboy_req:path(Req),
-    case emqx_mgmt_auth:authorize(Path, Key, Secret) of
+    case emqx_mgmt_auth:authorize(Path, Req, Key, Secret) of
         ok ->
             {ok, #{auth_type => api_key, api_key => Key}};
         {error, <<"not_allowed">>} ->
@@ -259,6 +259,9 @@ api_key_authorize(Req, Key, Secret) ->
                 ?BAD_API_KEY_OR_SECRET,
                 <<"Not allowed, Check api_key/api_secret">>
             );
+        {error, unauthorized_role} ->
+            {403, 'UNAUTHORIZED_ROLE',
+                <<"This API Key don't have permission to access this resource">>};
         {error, _} ->
             return_unauthorized(
                 ?BAD_API_KEY_OR_SECRET,

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

@@ -416,7 +416,7 @@ ensure_role(Role) when is_binary(Role) ->
 
 -if(?EMQX_RELEASE_EDITION == ee).
 legal_role(Role) ->
-    emqx_dashboard_rbac:valid_role(Role).
+    emqx_dashboard_rbac:valid_dashboard_role(Role).
 
 role(Data) ->
     emqx_dashboard_rbac:role(Data).
@@ -447,8 +447,10 @@ lookup_user(Backend, Username) when is_atom(Backend) ->
 
 -dialyzer({no_match, [add_user/4, update_user/3]}).
 
+legal_role(?ROLE_DEFAULT) ->
+    ok;
 legal_role(_) ->
-    ok.
+    {error, <<"Role does not exist">>}.
 
 role(_) ->
     ?ROLE_DEFAULT.

+ 1 - 1
apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.app.src

@@ -1,6 +1,6 @@
 {application, emqx_dashboard_rbac, [
     {description, "EMQX Dashboard RBAC"},
-    {vsn, "0.1.0"},
+    {vsn, "0.1.1"},
     {registered, []},
     {applications, [
         kernel,

+ 34 - 10
apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl

@@ -6,7 +6,12 @@
 
 -include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
 
--export([check_rbac/2, role/1, valid_role/1]).
+-export([
+    check_rbac/2,
+    role/1,
+    valid_dashboard_role/1,
+    valid_api_role/1
+]).
 
 -dialyzer({nowarn_function, role/1}).
 %%=====================================================================
@@ -31,25 +36,44 @@ role(#?ADMIN{role = Role}) ->
 role([]) ->
     ?ROLE_SUPERUSER;
 role(#{role := Role}) ->
+    Role;
+role(Role) when is_binary(Role) ->
     Role.
 
-valid_role(Role) ->
-    case lists:member(Role, role_list()) of
-        true ->
-            ok;
-        _ ->
-            {error, <<"Role does not exist">>}
-    end.
+valid_dashboard_role(Role) ->
+    valid_role(dashboard, Role).
+
+valid_api_role(Role) ->
+    valid_role(api, Role).
+
 %% ===================================================================
 check_rbac(?ROLE_SUPERUSER, _, _) ->
     true;
+check_rbac(?ROLE_API_SUPERUSER, _, _) ->
+    true;
 check_rbac(?ROLE_VIEWER, <<"GET">>, _) ->
     true;
+check_rbac(?ROLE_API_VIEWER, <<"GET">>, _) ->
+    true;
 %% this API is a special case
 check_rbac(?ROLE_VIEWER, <<"POST">>, <<"/logout">>) ->
     true;
+check_rbac(?ROLE_API_PUBLISHER, <<"POST">>, <<"/publish">>) ->
+    true;
+check_rbac(?ROLE_API_PUBLISHER, <<"POST">>, <<"/publish/bulk">>) ->
+    true;
 check_rbac(_, _, _) ->
     false.
 
-role_list() ->
-    [?ROLE_VIEWER, ?ROLE_SUPERUSER].
+valid_role(Type, Role) ->
+    case lists:member(Role, role_list(Type)) of
+        true ->
+            ok;
+        _ ->
+            {error, <<"Role does not exist">>}
+    end.
+
+role_list(dashboard) ->
+    [?ROLE_VIEWER, ?ROLE_SUPERUSER];
+role_list(api) ->
+    [?ROLE_API_VIEWER, ?ROLE_API_PUBLISHER, ?ROLE_API_SUPERUSER].

+ 35 - 5
apps/emqx_management/src/emqx_mgmt_api_api_keys.erl

@@ -19,6 +19,7 @@
 
 -include_lib("typerefl/include/types.hrl").
 -include_lib("hocon/include/hoconsc.hrl").
+-include_lib("emqx_dashboard/include/emqx_dashboard_rbac.hrl").
 
 -export([api_spec/0, fields/1, paths/0, schema/1, namespace/0]).
 -export([api_key/2, api_key_by_name/2]).
@@ -150,7 +151,7 @@ fields(app) ->
             )},
         {enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})},
         {expired, hoconsc:mk(boolean(), #{desc => "Expired", required => false})}
-    ];
+    ] ++ app_extend_fields();
 fields(name) ->
     [
         {name,
@@ -192,7 +193,8 @@ api_key(post, #{body := App}) ->
     } = App,
     ExpiredAt = ensure_expired_at(App),
     Desc = unicode:characters_to_binary(Desc0, unicode),
-    case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of
+    Role = maps:get(<<"role">>, App, ?ROLE_API_DEFAULT),
+    case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc, Role) of
         {ok, NewApp} ->
             {200, emqx_mgmt_auth:format(NewApp)};
         {error, Reason} ->
@@ -218,10 +220,38 @@ api_key_by_name(put, #{bindings := #{name := Name}, body := Body}) ->
     Enable = maps:get(<<"enable">>, Body, undefined),
     ExpiredAt = ensure_expired_at(Body),
     Desc = maps:get(<<"desc">>, Body, undefined),
-    case emqx_mgmt_auth:update(Name, Enable, ExpiredAt, Desc) of
-        {ok, App} -> {200, emqx_mgmt_auth:format(App)};
-        {error, not_found} -> {404, ?NOT_FOUND_RESPONSE}
+    Role = maps:get(<<"role">>, Body, ?ROLE_API_DEFAULT),
+    case emqx_mgmt_auth:update(Name, Enable, ExpiredAt, Desc, Role) of
+        {ok, App} ->
+            {200, emqx_mgmt_auth:format(App)};
+        {error, not_found} ->
+            {404, ?NOT_FOUND_RESPONSE};
+        {error, Reason} ->
+            {400, #{
+                code => 'BAD_REQUEST',
+                message => iolist_to_binary(io_lib:format("~p", [Reason]))
+            }}
     end.
 
 ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt;
 ensure_expired_at(_) -> infinity.
+
+-if(?EMQX_RELEASE_EDITION == ee).
+
+app_extend_fields() ->
+    [
+        {role,
+            hoconsc:mk(binary(), #{
+                desc => ?DESC(role),
+                default => ?ROLE_API_DEFAULT,
+                example => ?ROLE_API_DEFAULT,
+                validator => fun emqx_dashboard_rbac:valid_api_role/1
+            })}
+    ].
+
+-else.
+
+app_extend_fields() ->
+    [].
+
+-endif.

+ 115 - 37
apps/emqx_management/src/emqx_mgmt_auth.erl

@@ -16,6 +16,7 @@
 -module(emqx_mgmt_auth).
 -include_lib("emqx/include/emqx.hrl").
 -include_lib("emqx/include/logger.hrl").
+-include_lib("emqx_dashboard/include/emqx_dashboard_rbac.hrl").
 
 -behaviour(emqx_db_backup).
 
@@ -25,16 +26,16 @@
 -behaviour(emqx_config_handler).
 
 -export([
-    create/4,
+    create/5,
     read/1,
-    update/4,
+    update/5,
     delete/1,
     list/0,
     init_bootstrap_file/0,
     format/1
 ]).
 
--export([authorize/3]).
+-export([authorize/4]).
 -export([post_config_update/5]).
 
 -export([backup_tables/0]).
@@ -48,10 +49,11 @@
 ]).
 
 -ifdef(TEST).
--export([create/5]).
+-export([create/6]).
 -endif.
 
 -define(APP, emqx_app).
+-type api_user_role() :: binary().
 
 -record(?APP, {
     name = <<>> :: binary() | '_',
@@ -60,17 +62,21 @@
     enable = true :: boolean() | '_',
     desc = <<>> :: binary() | '_',
     expired_at = 0 :: integer() | undefined | infinity | '_',
-    created_at = 0 :: integer() | '_'
+    created_at = 0 :: integer() | '_',
+    role = ?ROLE_DEFAULT :: api_user_role() | '_',
+    extra = #{} :: map() | '_'
 }).
 
 mnesia(boot) ->
+    Fields = record_info(fields, ?APP),
     ok = mria:create_table(?APP, [
         {type, set},
         {rlog_shard, ?COMMON_SHARD},
         {storage, disc_copies},
         {record_name, ?APP},
-        {attributes, record_info(fields, ?APP)}
-    ]).
+        {attributes, Fields}
+    ]),
+    maybe_migrate_table(Fields).
 
 %%--------------------------------------------------------------------
 %% Data backup
@@ -95,13 +101,13 @@ init_bootstrap_file() ->
     ?SLOG(debug, #{msg => "init_bootstrap_api_keys_from_file", file => File}),
     init_bootstrap_file(File).
 
-create(Name, Enable, ExpiredAt, Desc) ->
+create(Name, Enable, ExpiredAt, Desc, Role) ->
     ApiSecret = generate_api_secret(),
-    create(Name, ApiSecret, Enable, ExpiredAt, Desc).
+    create(Name, ApiSecret, Enable, ExpiredAt, Desc, Role).
 
-create(Name, ApiSecret, Enable, ExpiredAt, Desc) ->
+create(Name, ApiSecret, Enable, ExpiredAt, Desc, Role) ->
     case mnesia:table_info(?APP, size) < 100 of
-        true -> create_app(Name, ApiSecret, Enable, ExpiredAt, Desc);
+        true -> create_app(Name, ApiSecret, Enable, ExpiredAt, Desc, Role);
         false -> {error, "Maximum ApiKey"}
     end.
 
@@ -111,8 +117,13 @@ read(Name) ->
         [] -> {error, not_found}
     end.
 
-update(Name, Enable, ExpiredAt, Desc) ->
-    trans(fun ?MODULE:do_update/4, [Name, Enable, ExpiredAt, Desc]).
+update(Name, Enable, ExpiredAt, Desc, Role) ->
+    case valid_role(Role) of
+        ok ->
+            trans(fun ?MODULE:do_update/4, [Name, Enable, ExpiredAt, Desc]);
+        Error ->
+            Error
+    end.
 
 do_update(Name, Enable, ExpiredAt, Desc) ->
     case mnesia:read(?APP, Name, write) of
@@ -138,37 +149,37 @@ do_delete(Name) ->
         [_App] -> mnesia:delete({?APP, Name})
     end.
 
-format(App = #{expired_at := ExpiredAt0, created_at := CreateAt}) ->
-    ExpiredAt =
-        case ExpiredAt0 of
-            infinity -> <<"infinity">>;
-            _ -> emqx_utils_calendar:epoch_to_rfc3339(ExpiredAt0, second)
-        end,
-    App#{
-        expired_at => ExpiredAt,
-        created_at => emqx_utils_calendar:epoch_to_rfc3339(CreateAt, second)
-    }.
+format(App = #{expired_at := ExpiredAt, created_at := CreateAt}) ->
+    format_app_extend(App#{
+        expired_at => format_epoch(ExpiredAt),
+        created_at => format_epoch(CreateAt)
+    }).
+
+format_epoch(infinity) ->
+    <<"infinity">>;
+format_epoch(Epoch) ->
+    emqx_utils_calendar:epoch_to_rfc3339(Epoch, second).
 
 list() ->
     to_map(ets:match_object(?APP, #?APP{_ = '_'})).
 
-authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) ->
+authorize(<<"/api/v5/users", _/binary>>, _Req, _ApiKey, _ApiSecret) ->
     {error, <<"not_allowed">>};
-authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) ->
+authorize(<<"/api/v5/api_key", _/binary>>, _Req, _ApiKey, _ApiSecret) ->
     {error, <<"not_allowed">>};
-authorize(<<"/api/v5/logout", _/binary>>, _ApiKey, _ApiSecret) ->
+authorize(<<"/api/v5/logout", _/binary>>, _Req, _ApiKey, _ApiSecret) ->
     {error, <<"not_allowed">>};
-authorize(_Path, ApiKey, ApiSecret) ->
+authorize(_Path, Req, ApiKey, ApiSecret) ->
     Now = erlang:system_time(second),
     case find_by_api_key(ApiKey) of
-        {ok, true, ExpiredAt, SecretHash} when ExpiredAt >= Now ->
+        {ok, true, ExpiredAt, SecretHash, Role} when ExpiredAt >= Now ->
             case emqx_dashboard_admin:verify_hash(ApiSecret, SecretHash) of
-                ok -> ok;
+                ok -> check_rbac(Req, Role);
                 error -> {error, "secret_error"}
             end;
-        {ok, true, _ExpiredAt, _SecretHash} ->
+        {ok, true, _ExpiredAt, _SecretHash, _Role} ->
             {error, "secret_expired"};
-        {ok, false, _ExpiredAt, _SecretHash} ->
+        {ok, false, _ExpiredAt, _SecretHash, _Role} ->
             {error, "secret_disable"};
         {error, Reason} ->
             {error, Reason}
@@ -177,8 +188,12 @@ authorize(_Path, ApiKey, ApiSecret) ->
 find_by_api_key(ApiKey) ->
     Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
     case mria:ro_transaction(?COMMON_SHARD, Fun) of
-        {atomic, [#?APP{api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt}]} ->
-            {ok, Enable, ExpiredAt, SecretHash};
+        {atomic, [
+            #?APP{
+                api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt, role = Role
+            }
+        ]} ->
+            {ok, Enable, ExpiredAt, SecretHash, Role};
         _ ->
             {error, "not_found"}
     end.
@@ -202,7 +217,7 @@ to_map(#?APP{name = N, api_key = K, enable = E, expired_at = ET, created_at = CT
 is_expired(undefined) -> false;
 is_expired(ExpiredTime) -> ExpiredTime < erlang:system_time(second).
 
-create_app(Name, ApiSecret, Enable, ExpiredAt, Desc) ->
+create_app(Name, ApiSecret, Enable, ExpiredAt, Desc, Role) ->
     App =
         #?APP{
             name = Name,
@@ -211,7 +226,8 @@ create_app(Name, ApiSecret, Enable, ExpiredAt, Desc) ->
             desc = Desc,
             created_at = erlang:system_time(second),
             api_secret_hash = emqx_dashboard_admin:hash(ApiSecret),
-            api_key = list_to_binary(emqx_utils:gen_id(16))
+            api_key = list_to_binary(emqx_utils:gen_id(16)),
+            role = Role
         },
     case create_app(App) of
         {ok, Res} ->
@@ -220,8 +236,13 @@ create_app(Name, ApiSecret, Enable, ExpiredAt, Desc) ->
             Error
     end.
 
-create_app(App = #?APP{api_key = ApiKey, name = Name}) ->
-    trans(fun ?MODULE:do_create_app/3, [App, ApiKey, Name]).
+create_app(App = #?APP{api_key = ApiKey, name = Name, role = Role}) ->
+    case valid_role(Role) of
+        ok ->
+            trans(fun ?MODULE:do_create_app/3, [App, ApiKey, Name]);
+        Error ->
+            Error
+    end.
 
 force_create_app(NamePrefix, App = #?APP{api_key = ApiKey}) ->
     trans(fun ?MODULE:do_force_create_app/3, [App, ApiKey, NamePrefix]).
@@ -340,3 +361,60 @@ add_bootstrap_file(File, Dev, MP, Line) ->
         {error, Reason} ->
             throw(#{file => File, line => Line, reason => Reason})
     end.
+
+-if(?EMQX_RELEASE_EDITION == ee).
+check_rbac(Req, Role) ->
+    case emqx_dashboard_rbac:check_rbac(Req, Role) of
+        true ->
+            ok;
+        _ ->
+            {error, unauthorized_role}
+    end.
+
+format_app_extend(App) ->
+    App.
+
+valid_role(Role) ->
+    emqx_dashboard_rbac:valid_api_role(Role).
+
+-else.
+
+check_rbac(_Req, _Role) ->
+    ok.
+
+format_app_extend(App) ->
+    maps:remove(role, App).
+
+valid_role(?ROLE_API_DEFAULT) ->
+    ok;
+valid_role(_) ->
+    {error, <<"Role does not exist">>}.
+
+-endif.
+
+maybe_migrate_table(Fields) ->
+    case mnesia:table_info(?APP, attributes) =:= Fields of
+        true ->
+            ok;
+        false ->
+            TransFun = fun(App) ->
+                case App of
+                    {?APP, Name, Key, Hash, Enable, Desc, ExpiredAt, CreatedAt} ->
+                        #?APP{
+                            name = Name,
+                            api_key = Key,
+                            api_secret_hash = Hash,
+                            enable = Enable,
+                            desc = Desc,
+                            expired_at = ExpiredAt,
+                            created_at = CreatedAt,
+                            role = ?ROLE_API_VIEWER,
+                            extra = #{}
+                        };
+                    #?APP{} ->
+                        App
+                end
+            end,
+            {atomic, ok} = mnesia:transform_table(?APP, TransFun, Fields, ?APP),
+            ok
+    end.

+ 18 - 13
apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl

@@ -41,37 +41,42 @@ t_bootstrap_file(_) ->
     File = "./bootstrap_api_keys.txt",
     ok = file:write_file(File, Bin),
     update_file(File),
-    ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
-    ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
-    ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-1">>)),
+    ?assertEqual(ok, auth_authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
+    ?assertEqual(ok, auth_authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
+    ?assertMatch({error, _}, auth_authorize(TestPath, <<"test-2">>, <<"secret-1">>)),
 
     %% relaunch to check if the table is changed.
     Bin1 = <<"test-1:new-secret-1\ntest-2:new-secret-2">>,
     ok = file:write_file(File, Bin1),
     update_file(File),
-    ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
-    ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
-    ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)),
-    ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)),
+    ?assertMatch({error, _}, auth_authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
+    ?assertMatch({error, _}, auth_authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
+    ?assertEqual(ok, auth_authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)),
+    ?assertEqual(ok, auth_authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)),
 
     %% not error when bootstrap_file is empty
     update_file(<<>>),
     update_file("./bootstrap_apps_not_exist.txt"),
-    ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
-    ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
-    ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)),
-    ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)),
+    ?assertMatch({error, _}, auth_authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
+    ?assertMatch({error, _}, auth_authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
+    ?assertEqual(ok, auth_authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)),
+    ?assertEqual(ok, auth_authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)),
 
     %% bad format
     BadBin = <<"test-1:secret-11\ntest-2 secret-12">>,
     ok = file:write_file(File, BadBin),
     update_file(File),
     ?assertMatch({error, #{reason := "invalid_format"}}, emqx_mgmt_auth:init_bootstrap_file()),
-    ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-11">>)),
-    ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-12">>)),
+    ?assertEqual(ok, auth_authorize(TestPath, <<"test-1">>, <<"secret-11">>)),
+    ?assertMatch({error, _}, auth_authorize(TestPath, <<"test-2">>, <<"secret-12">>)),
     update_file(<<>>),
     ok.
 
+auth_authorize(Path, Key, Secret) ->
+    FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/fake")),
+    FakeReq = #{method => <<"GET">>, path => FakePath},
+    emqx_mgmt_auth:authorize(Path, FakeReq, Key, Secret).
+
 update_file(File) ->
     ?assertMatch({ok, _}, emqx:update_config([<<"api_key">>], #{<<"bootstrap_file">> => File})).
 

+ 3 - 0
rel/i18n/emqx_mgmt_api_api_keys.hocon

@@ -30,4 +30,7 @@ format.desc:
 format.label:
 """Unique and format by [a-zA-Z0-9-_]"""
 
+role.desc:
+"""Role for this API"""
+
 }