|
|
@@ -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.
|