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

Merge pull request #11811 from lafirest/fix/api_rbac

fix(rbac): for compatibility with old data schema, extend the existing field as an extra
lafirest 2 лет назад
Родитель
Сommit
e63602c4b0

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

@@ -34,7 +34,7 @@
 -define(DEFAULT_APP_SECRET, <<"default_app_secret">>).
 -define(DEFAULT_APP_SECRET, <<"default_app_secret">>).
 
 
 %% from emqx_dashboard/include/emqx_dashboard_rbac.hrl
 %% from emqx_dashboard/include/emqx_dashboard_rbac.hrl
--define(ROLE_API_SUPERUSER, <<"api_administrator">>).
+-define(ROLE_API_SUPERUSER, <<"administrator">>).
 
 
 request_api(Method, Url, Auth) ->
 request_api(Method, Url, Auth) ->
     request_api(Method, Url, [], Auth, []).
     request_api(Method, Url, [], Auth, []).

+ 3 - 3
apps/emqx_dashboard/include/emqx_dashboard_rbac.hrl

@@ -25,9 +25,9 @@
 -define(ROLE_SUPERUSER, <<"administrator">>).
 -define(ROLE_SUPERUSER, <<"administrator">>).
 -define(ROLE_DEFAULT, ?ROLE_SUPERUSER).
 -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_VIEWER, <<"viewer">>).
+-define(ROLE_API_SUPERUSER, <<"administrator">>).
+-define(ROLE_API_PUBLISHER, <<"publisher">>).
 -define(ROLE_API_DEFAULT, ?ROLE_API_SUPERUSER).
 -define(ROLE_API_DEFAULT, ?ROLE_API_SUPERUSER).
 
 
 -endif.
 -endif.

+ 0 - 4
apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl

@@ -59,12 +59,8 @@ valid_role(Type, Role) ->
 %% ===================================================================
 %% ===================================================================
 check_rbac(?ROLE_SUPERUSER, _, _, _) ->
 check_rbac(?ROLE_SUPERUSER, _, _, _) ->
     true;
     true;
-check_rbac(?ROLE_API_SUPERUSER, _, _, _) ->
-    true;
 check_rbac(?ROLE_VIEWER, <<"GET">>, _, _) ->
 check_rbac(?ROLE_VIEWER, <<"GET">>, _, _) ->
     true;
     true;
-check_rbac(?ROLE_API_VIEWER, <<"GET">>, _, _) ->
-    true;
 check_rbac(?ROLE_API_PUBLISHER, <<"POST">>, <<"/publish">>, _) ->
 check_rbac(?ROLE_API_PUBLISHER, <<"POST">>, <<"/publish">>, _) ->
     true;
     true;
 check_rbac(?ROLE_API_PUBLISHER, <<"POST">>, <<"/publish/bulk">>, _) ->
 check_rbac(?ROLE_API_PUBLISHER, <<"POST">>, <<"/publish/bulk">>, _) ->

+ 52 - 66
apps/emqx_management/src/emqx_mgmt_auth.erl

@@ -38,7 +38,7 @@
 -export([authorize/4]).
 -export([authorize/4]).
 -export([post_config_update/5]).
 -export([post_config_update/5]).
 
 
--export([backup_tables/0, validate_mnesia_backup/1, migrate_mnesia_backup/1]).
+-export([backup_tables/0, validate_mnesia_backup/1]).
 
 
 %% Internal exports (RPC)
 %% Internal exports (RPC)
 -export([
 -export([
@@ -53,18 +53,17 @@
 -endif.
 -endif.
 
 
 -define(APP, emqx_app).
 -define(APP, emqx_app).
--type api_user_role() :: binary().
 
 
 -record(?APP, {
 -record(?APP, {
     name = <<>> :: binary() | '_',
     name = <<>> :: binary() | '_',
     api_key = <<>> :: binary() | '_',
     api_key = <<>> :: binary() | '_',
     api_secret_hash = <<>> :: binary() | '_',
     api_secret_hash = <<>> :: binary() | '_',
     enable = true :: boolean() | '_',
     enable = true :: boolean() | '_',
-    desc = <<>> :: binary() | '_',
+    %% Since v5.4.0 the `desc` has changed to `extra`
+    %% desc = <<>> :: binary() | '_',
+    extra = #{} :: binary() | map() | '_',
     expired_at = 0 :: integer() | undefined | infinity | '_',
     expired_at = 0 :: integer() | undefined | infinity | '_',
-    created_at = 0 :: integer() | '_',
-    role = ?ROLE_DEFAULT :: api_user_role() | '_',
-    extra = #{} :: map() | '_'
+    created_at = 0 :: integer() | '_'
 }).
 }).
 
 
 mnesia(boot) ->
 mnesia(boot) ->
@@ -75,8 +74,7 @@ mnesia(boot) ->
         {storage, disc_copies},
         {storage, disc_copies},
         {record_name, ?APP},
         {record_name, ?APP},
         {attributes, Fields}
         {attributes, Fields}
-    ]),
-    maybe_migrate_table(Fields).
+    ]).
 
 
 %%--------------------------------------------------------------------
 %%--------------------------------------------------------------------
 %% Data backup
 %% Data backup
@@ -87,11 +85,12 @@ backup_tables() -> [?APP].
 validate_mnesia_backup({schema, _Tab, CreateList} = Schema) ->
 validate_mnesia_backup({schema, _Tab, CreateList} = Schema) ->
     case emqx_mgmt_data_backup:default_validate_mnesia_backup(Schema) of
     case emqx_mgmt_data_backup:default_validate_mnesia_backup(Schema) of
         ok ->
         ok ->
-            {ok, over};
+            ok;
         _ ->
         _ ->
             case proplists:get_value(attributes, CreateList) of
             case proplists:get_value(attributes, CreateList) of
+                %% Since v5.4.0 the `desc` has changed to `extra`
                 [name, api_key, api_secret_hash, enable, desc, expired_at, created_at] ->
                 [name, api_key, api_secret_hash, enable, desc, expired_at, created_at] ->
-                    {ok, migrate};
+                    ok;
                 Fields ->
                 Fields ->
                     {error, {unknow_fields, Fields}}
                     {error, {unknow_fields, Fields}}
             end
             end
@@ -99,20 +98,6 @@ validate_mnesia_backup({schema, _Tab, CreateList} = Schema) ->
 validate_mnesia_backup(_Other) ->
 validate_mnesia_backup(_Other) ->
     ok.
     ok.
 
 
-migrate_mnesia_backup({schema, Tab, CreateList}) ->
-    case proplists:get_value(attributes, CreateList) of
-        [name, api_key, api_secret_hash, enable, desc, expired_at, created_at] = Fields ->
-            NewFields = Fields ++ [role, extra],
-            CreateList2 = lists:keyreplace(
-                attributes, 1, CreateList, {attributes, NewFields}
-            ),
-            {ok, {schema, Tab, CreateList2}};
-        Fields ->
-            {error, {unknow_fields, Fields}}
-    end;
-migrate_mnesia_backup(Data) ->
-    {ok, do_table_migrate(Data)}.
-
 post_config_update([api_key], _Req, NewConf, _OldConf, _AppEnvs) ->
 post_config_update([api_key], _Req, NewConf, _OldConf, _AppEnvs) ->
     #{bootstrap_file := File} = NewConf,
     #{bootstrap_file := File} = NewConf,
     case init_bootstrap_file(File) of
     case init_bootstrap_file(File) of
@@ -158,13 +143,13 @@ do_update(Name, Enable, ExpiredAt, Desc, Role) ->
     case mnesia:read(?APP, Name, write) of
     case mnesia:read(?APP, Name, write) of
         [] ->
         [] ->
             mnesia:abort(not_found);
             mnesia:abort(not_found);
-        [App0 = #?APP{enable = Enable0, desc = Desc0}] ->
+        [App0 = #?APP{enable = Enable0, extra = Extra0}] ->
+            #{desc := Desc0} = Extra = normalize_extra(Extra0),
             App =
             App =
                 App0#?APP{
                 App0#?APP{
                     expired_at = ExpiredAt,
                     expired_at = ExpiredAt,
                     enable = ensure_not_undefined(Enable, Enable0),
                     enable = ensure_not_undefined(Enable, Enable0),
-                    desc = ensure_not_undefined(Desc, Desc0),
-                    role = Role
+                    extra = Extra#{desc := ensure_not_undefined(Desc, Desc0), role := Role}
                 },
                 },
             ok = mnesia:write(App),
             ok = mnesia:write(App),
             to_map(App)
             to_map(App)
@@ -220,10 +205,10 @@ find_by_api_key(ApiKey) ->
     case mria:ro_transaction(?COMMON_SHARD, Fun) of
     case mria:ro_transaction(?COMMON_SHARD, Fun) of
         {atomic, [
         {atomic, [
             #?APP{
             #?APP{
-                api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt, role = Role
+                api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt, extra = Extra
             }
             }
         ]} ->
         ]} ->
-            {ok, Enable, ExpiredAt, SecretHash, Role};
+            {ok, Enable, ExpiredAt, SecretHash, get_role(Extra)};
         _ ->
         _ ->
             {error, "not_found"}
             {error, "not_found"}
     end.
     end.
@@ -234,15 +219,16 @@ ensure_not_undefined(New, _Old) -> New.
 to_map(Apps) when is_list(Apps) ->
 to_map(Apps) when is_list(Apps) ->
     [to_map(App) || App <- Apps];
     [to_map(App) || App <- Apps];
 to_map(#?APP{
 to_map(#?APP{
-    name = N, api_key = K, enable = E, expired_at = ET, created_at = CT, desc = D, role = Role
+    name = N, api_key = K, enable = E, expired_at = ET, created_at = CT, extra = Extra0
 }) ->
 }) ->
+    #{role := Role, desc := Desc} = normalize_extra(Extra0),
     #{
     #{
         name => N,
         name => N,
         api_key => K,
         api_key => K,
         enable => E,
         enable => E,
         expired_at => ET,
         expired_at => ET,
         created_at => CT,
         created_at => CT,
-        desc => D,
+        desc => Desc,
         expired => is_expired(ET),
         expired => is_expired(ET),
         role => Role
         role => Role
     }.
     }.
@@ -256,11 +242,10 @@ create_app(Name, ApiSecret, Enable, ExpiredAt, Desc, Role) ->
             name = Name,
             name = Name,
             enable = Enable,
             enable = Enable,
             expired_at = ExpiredAt,
             expired_at = ExpiredAt,
-            desc = Desc,
+            extra = #{desc => Desc, role => Role},
             created_at = erlang:system_time(second),
             created_at = erlang:system_time(second),
             api_secret_hash = emqx_dashboard_admin:hash(ApiSecret),
             api_secret_hash = emqx_dashboard_admin:hash(ApiSecret),
-            api_key = list_to_binary(emqx_utils:gen_id(16)),
-            role = Role
+            api_key = list_to_binary(emqx_utils:gen_id(16))
         },
         },
     case create_app(App) of
     case create_app(App) of
         {ok, Res} ->
         {ok, Res} ->
@@ -269,7 +254,7 @@ create_app(Name, ApiSecret, Enable, ExpiredAt, Desc, Role) ->
             Error
             Error
     end.
     end.
 
 
-create_app(App = #?APP{api_key = ApiKey, name = Name, role = Role}) ->
+create_app(App = #?APP{api_key = ApiKey, name = Name, extra = #{role := Role}}) ->
     case valid_role(Role) of
     case valid_role(Role) of
         ok ->
         ok ->
             trans(fun ?MODULE:do_create_app/3, [App, ApiKey, Name]);
             trans(fun ?MODULE:do_create_app/3, [App, ApiKey, Name]);
@@ -328,7 +313,7 @@ init_bootstrap_file(<<>>) ->
 init_bootstrap_file(File) ->
 init_bootstrap_file(File) ->
     case file:open(File, [read, binary]) of
     case file:open(File, [read, binary]) of
         {ok, Dev} ->
         {ok, Dev} ->
-            {ok, MP} = re:compile(<<"(\.+):(\.+$)">>, [ungreedy]),
+            {ok, MP} = re:compile(<<"(\.+):(\.+)(?::(\.+))?$">>, [ungreedy]),
             init_bootstrap_file(File, Dev, MP);
             init_bootstrap_file(File, Dev, MP);
         {error, Reason0} ->
         {error, Reason0} ->
             Reason = emqx_utils:explain_posix(Reason0),
             Reason = emqx_utils:explain_posix(Reason0),
@@ -358,13 +343,13 @@ init_bootstrap_file(File, Dev, MP) ->
 add_bootstrap_file(File, Dev, MP, Line) ->
 add_bootstrap_file(File, Dev, MP, Line) ->
     case file:read_line(Dev) of
     case file:read_line(Dev) of
         {ok, Bin} ->
         {ok, Bin} ->
-            case re:run(Bin, MP, [global, {capture, all_but_first, binary}]) of
-                {match, [[AppKey, ApiSecret]]} ->
+            case parse_bootstrap_line(Bin, MP) of
+                {ok, [AppKey, ApiSecret, Role]} ->
                     App =
                     App =
                         #?APP{
                         #?APP{
                             enable = true,
                             enable = true,
                             expired_at = infinity,
                             expired_at = infinity,
-                            desc = ?BOOTSTRAP_TAG,
+                            extra = #{desc => ?BOOTSTRAP_TAG, role => Role},
                             created_at = erlang:system_time(second),
                             created_at = erlang:system_time(second),
                             api_secret_hash = emqx_dashboard_admin:hash(ApiSecret),
                             api_secret_hash = emqx_dashboard_admin:hash(ApiSecret),
                             api_key = AppKey
                             api_key = AppKey
@@ -375,8 +360,7 @@ add_bootstrap_file(File, Dev, MP, Line) ->
                         {error, Reason} ->
                         {error, Reason} ->
                             throw(#{file => File, line => Line, content => Bin, reason => Reason})
                             throw(#{file => File, line => Line, content => Bin, reason => Reason})
                     end;
                     end;
-                _ ->
-                    Reason = "invalid_format",
+                {error, Reason} ->
                     ?SLOG(
                     ?SLOG(
                         error,
                         error,
                         #{
                         #{
@@ -395,6 +379,33 @@ add_bootstrap_file(File, Dev, MP, Line) ->
             throw(#{file => File, line => Line, reason => Reason})
             throw(#{file => File, line => Line, reason => Reason})
     end.
     end.
 
 
+parse_bootstrap_line(Bin, MP) ->
+    case re:run(Bin, MP, [global, {capture, all_but_first, binary}]) of
+        {match, [[_AppKey, _ApiSecret] = Args]} ->
+            {ok, Args ++ [?ROLE_API_DEFAULT]};
+        {match, [[_AppKey, _ApiSecret, Role] = Args]} ->
+            case valid_role(Role) of
+                ok ->
+                    {ok, Args};
+                _Error ->
+                    {error, {"invalid_role", Role}}
+            end;
+        _ ->
+            {error, "invalid_format"}
+    end.
+
+get_role(#{role := Role}) ->
+    Role;
+%% Before v5.4.0,
+%% the field in the position of the `extra` is `desc` which is a binary for description
+get_role(_Desc) ->
+    ?ROLE_API_DEFAULT.
+
+normalize_extra(Map) when is_map(Map) ->
+    Map;
+normalize_extra(Desc) ->
+    #{desc => Desc, role => ?ROLE_API_DEFAULT}.
+
 -if(?EMQX_RELEASE_EDITION == ee).
 -if(?EMQX_RELEASE_EDITION == ee).
 check_rbac(Req, ApiKey, Role) ->
 check_rbac(Req, ApiKey, Role) ->
     case emqx_dashboard_rbac:check_rbac(Req, ApiKey, Role) of
     case emqx_dashboard_rbac:check_rbac(Req, ApiKey, Role) of
@@ -424,28 +435,3 @@ valid_role(_) ->
     {error, <<"Role does not exist">>}.
     {error, <<"Role does not exist">>}.
 
 
 -endif.
 -endif.
-
-maybe_migrate_table(Fields) ->
-    case mnesia:table_info(?APP, attributes) =:= Fields of
-        true ->
-            ok;
-        false ->
-            TransFun = fun do_table_migrate/1,
-            {atomic, ok} = mnesia:transform_table(?APP, TransFun, Fields, ?APP),
-            ok
-    end.
-
-do_table_migrate({?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_DEFAULT,
-        extra = #{}
-    };
-do_table_migrate(#?APP{} = App) ->
-    App.

+ 87 - 1
apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl

@@ -39,7 +39,7 @@ groups() ->
     [
     [
         {parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
         {parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
         {parallel, [parallel], ?EE_CASES},
         {parallel, [parallel], ?EE_CASES},
-        {sequence, [], [t_bootstrap_file, t_create_failed]}
+        {sequence, [], [t_bootstrap_file, t_bootstrap_file_with_role, t_create_failed]}
     ].
     ].
 
 
 init_per_suite(Config) ->
 init_per_suite(Config) ->
@@ -86,6 +86,92 @@ t_bootstrap_file(_) ->
     update_file(<<>>),
     update_file(<<>>),
     ok.
     ok.
 
 
+-if(?EMQX_RELEASE_EDITION == ee).
+t_bootstrap_file_with_role(_) ->
+    Search = fun(Name) ->
+        lists:search(
+            fun(#{api_key := AppName}) ->
+                AppName =:= Name
+            end,
+            emqx_mgmt_auth:list()
+        )
+    end,
+
+    Bin = <<"role-1:role-1:viewer\nrole-2:role-2:administrator\nrole-3:role-3">>,
+    File = "./bootstrap_api_keys.txt",
+    ok = file:write_file(File, Bin),
+    update_file(File),
+
+    ?assertMatch(
+        {value, #{api_key := <<"role-1">>, role := <<"viewer">>}},
+        Search(<<"role-1">>)
+    ),
+
+    ?assertMatch(
+        {value, #{api_key := <<"role-2">>, role := <<"administrator">>}},
+        Search(<<"role-2">>)
+    ),
+
+    ?assertMatch(
+        {value, #{api_key := <<"role-3">>, role := <<"administrator">>}},
+        Search(<<"role-3">>)
+    ),
+
+    %% bad role
+    BadBin = <<"role-4:secret-11:bad\n">>,
+    ok = file:write_file(File, BadBin),
+    update_file(File),
+    ?assertEqual(
+        false,
+        Search(<<"role-4">>)
+    ),
+    ok.
+-else.
+t_bootstrap_file_with_role(_) ->
+    Search = fun(Name) ->
+        lists:search(
+            fun(#{api_key := AppName}) ->
+                AppName =:= Name
+            end,
+            emqx_mgmt_auth:list()
+        )
+    end,
+
+    Bin = <<"role-1:role-1:administrator\nrole-2:role-2">>,
+    File = "./bootstrap_api_keys.txt",
+    ok = file:write_file(File, Bin),
+    update_file(File),
+
+    ?assertMatch(
+        {value, #{api_key := <<"role-1">>, role := <<"administrator">>}},
+        Search(<<"role-1">>)
+    ),
+
+    ?assertMatch(
+        {value, #{api_key := <<"role-2">>, role := <<"administrator">>}},
+        Search(<<"role-2">>)
+    ),
+
+    %% only administrator
+    OtherRoleBin = <<"role-3:role-3:viewer\n">>,
+    ok = file:write_file(File, OtherRoleBin),
+    update_file(File),
+    ?assertEqual(
+        false,
+        Search(<<"role-3">>)
+    ),
+
+    %% bad role
+    BadBin = <<"role-4:secret-11:bad\n">>,
+    ok = file:write_file(File, BadBin),
+    update_file(File),
+    ?assertEqual(
+        false,
+        Search(<<"role-4">>)
+    ),
+    ok.
+-endif.
+
 auth_authorize(Path, Key, Secret) ->
 auth_authorize(Path, Key, Secret) ->
     FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/fake")),
     FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/fake")),
     FakeReq = #{method => <<"GET">>, path => FakePath},
     FakeReq = #{method => <<"GET">>, path => FakePath},

+ 1 - 1
apps/emqx_management/test/emqx_mgmt_data_backup_SUITE.erl

@@ -23,7 +23,7 @@
 -include_lib("snabbkaffe/include/snabbkaffe.hrl").
 -include_lib("snabbkaffe/include/snabbkaffe.hrl").
 
 
 -define(ROLE_SUPERUSER, <<"administrator">>).
 -define(ROLE_SUPERUSER, <<"administrator">>).
--define(ROLE_API_SUPERUSER, <<"api_administrator">>).
+-define(ROLE_API_SUPERUSER, <<"administrator">>).
 -define(BOOTSTRAP_BACKUP, "emqx-export-test-bootstrap-ce.tar.gz").
 -define(BOOTSTRAP_BACKUP, "emqx-export-test-bootstrap-ce.tar.gz").
 
 
 all() ->
 all() ->

+ 5 - 0
changes/ee/feat-11811.en.md

@@ -0,0 +1,5 @@
+Improve the format for the REST API key bootstrap file to support initialize key with a role.
+
+The new form is:`api_key:api_secret:role`.
+
+`role` is optional and its default value is `administrator`.

+ 5 - 2
rel/i18n/emqx_mgmt_api_key_schema.hocon

@@ -9,8 +9,11 @@ api_key.label:
 bootstrap_file.desc:
 bootstrap_file.desc:
 """The bootstrap file provides API keys for EMQX.
 """The bootstrap file provides API keys for EMQX.
 EMQX will load these keys on startup to authorize API requests.
 EMQX will load these keys on startup to authorize API requests.
-It contains key-value pairs in the format:`api_key:api_secret`.
-Each line specifies an API key and its associated secret."""
+It contains colon-separated values in the format: `api_key:api_secret:role`.
+Each line specifies an API key and its associated secret, and the role of this key.
+The 'role' part should be the pre-defined access scope group name,
+for example, `administrator` or `viewer`.
+The 'role' is introduced in 5.4, to be backward compatible, if it is missing, the key is implicitly granted `administrator` role."""
 
 
 bootstrap_file.label:
 bootstrap_file.label:
 """Initialize api_key file."""
 """Initialize api_key file."""