feat(api_key): add RBAC feature for the API key
This commit is contained in:
parent
96c546c187
commit
e095de7367
|
@ -17,6 +17,7 @@
|
||||||
-module(emqx_common_test_http).
|
-module(emqx_common_test_http).
|
||||||
|
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard_rbac.hrl").
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
request_api/3,
|
request_api/3,
|
||||||
|
@ -90,7 +91,12 @@ create_default_app() ->
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
ExpiredAt = Now + timer:minutes(10),
|
ExpiredAt = Now + timer:minutes(10),
|
||||||
emqx_mgmt_auth:create(
|
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() ->
|
delete_default_app() ->
|
||||||
|
|
|
@ -13,16 +13,9 @@
|
||||||
%% See the License for the specific language governing permissions and
|
%% See the License for the specific language governing permissions and
|
||||||
%% limitations under the License.
|
%% limitations under the License.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-define(ADMIN, emqx_admin).
|
-include("emqx_dashboard_rbac.hrl").
|
||||||
|
|
||||||
%% TODO:
|
-define(ADMIN, emqx_admin).
|
||||||
%% 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(BACKEND_LOCAL, local).
|
-define(BACKEND_LOCAL, local).
|
||||||
-define(SSO_USERNAME(Backend, Name), {Backend, Name}).
|
-define(SSO_USERNAME(Backend, Name), {Backend, Name}).
|
||||||
|
|
|
@ -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.
|
|
@ -251,7 +251,7 @@ listeners() ->
|
||||||
|
|
||||||
api_key_authorize(Req, Key, Secret) ->
|
api_key_authorize(Req, Key, Secret) ->
|
||||||
Path = cowboy_req:path(Req),
|
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 ->
|
||||||
{ok, #{auth_type => api_key, api_key => Key}};
|
{ok, #{auth_type => api_key, api_key => Key}};
|
||||||
{error, <<"not_allowed">>} ->
|
{error, <<"not_allowed">>} ->
|
||||||
|
@ -259,6 +259,9 @@ api_key_authorize(Req, Key, Secret) ->
|
||||||
?BAD_API_KEY_OR_SECRET,
|
?BAD_API_KEY_OR_SECRET,
|
||||||
<<"Not allowed, Check api_key/api_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, _} ->
|
{error, _} ->
|
||||||
return_unauthorized(
|
return_unauthorized(
|
||||||
?BAD_API_KEY_OR_SECRET,
|
?BAD_API_KEY_OR_SECRET,
|
||||||
|
|
|
@ -416,7 +416,7 @@ ensure_role(Role) when is_binary(Role) ->
|
||||||
|
|
||||||
-if(?EMQX_RELEASE_EDITION == ee).
|
-if(?EMQX_RELEASE_EDITION == ee).
|
||||||
legal_role(Role) ->
|
legal_role(Role) ->
|
||||||
emqx_dashboard_rbac:valid_role(Role).
|
emqx_dashboard_rbac:valid_dashboard_role(Role).
|
||||||
|
|
||||||
role(Data) ->
|
role(Data) ->
|
||||||
emqx_dashboard_rbac: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]}).
|
-dialyzer({no_match, [add_user/4, update_user/3]}).
|
||||||
|
|
||||||
|
legal_role(?ROLE_DEFAULT) ->
|
||||||
|
ok;
|
||||||
legal_role(_) ->
|
legal_role(_) ->
|
||||||
ok.
|
{error, <<"Role does not exist">>}.
|
||||||
|
|
||||||
role(_) ->
|
role(_) ->
|
||||||
?ROLE_DEFAULT.
|
?ROLE_DEFAULT.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{application, emqx_dashboard_rbac, [
|
{application, emqx_dashboard_rbac, [
|
||||||
{description, "EMQX Dashboard RBAC"},
|
{description, "EMQX Dashboard RBAC"},
|
||||||
{vsn, "0.1.0"},
|
{vsn, "0.1.1"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{applications, [
|
{applications, [
|
||||||
kernel,
|
kernel,
|
||||||
|
|
|
@ -6,7 +6,12 @@
|
||||||
|
|
||||||
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
-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}).
|
-dialyzer({nowarn_function, role/1}).
|
||||||
%%=====================================================================
|
%%=====================================================================
|
||||||
|
@ -31,25 +36,44 @@ role(#?ADMIN{role = Role}) ->
|
||||||
role([]) ->
|
role([]) ->
|
||||||
?ROLE_SUPERUSER;
|
?ROLE_SUPERUSER;
|
||||||
role(#{role := Role}) ->
|
role(#{role := Role}) ->
|
||||||
|
Role;
|
||||||
|
role(Role) when is_binary(Role) ->
|
||||||
Role.
|
Role.
|
||||||
|
|
||||||
valid_role(Role) ->
|
valid_dashboard_role(Role) ->
|
||||||
case lists:member(Role, role_list()) of
|
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.
|
||||||
|
|
||||||
|
valid_role(Type, Role) ->
|
||||||
|
case lists:member(Role, role_list(Type)) of
|
||||||
true ->
|
true ->
|
||||||
ok;
|
ok;
|
||||||
_ ->
|
_ ->
|
||||||
{error, <<"Role does not exist">>}
|
{error, <<"Role does not exist">>}
|
||||||
end.
|
end.
|
||||||
%% ===================================================================
|
|
||||||
check_rbac(?ROLE_SUPERUSER, _, _) ->
|
|
||||||
true;
|
|
||||||
check_rbac(?ROLE_VIEWER, <<"GET">>, _) ->
|
|
||||||
true;
|
|
||||||
%% this API is a special case
|
|
||||||
check_rbac(?ROLE_VIEWER, <<"POST">>, <<"/logout">>) ->
|
|
||||||
true;
|
|
||||||
check_rbac(_, _, _) ->
|
|
||||||
false.
|
|
||||||
|
|
||||||
role_list() ->
|
role_list(dashboard) ->
|
||||||
[?ROLE_VIEWER, ?ROLE_SUPERUSER].
|
[?ROLE_VIEWER, ?ROLE_SUPERUSER];
|
||||||
|
role_list(api) ->
|
||||||
|
[?ROLE_API_VIEWER, ?ROLE_API_PUBLISHER, ?ROLE_API_SUPERUSER].
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("hocon/include/hoconsc.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_spec/0, fields/1, paths/0, schema/1, namespace/0]).
|
||||||
-export([api_key/2, api_key_by_name/2]).
|
-export([api_key/2, api_key_by_name/2]).
|
||||||
|
@ -150,7 +151,7 @@ fields(app) ->
|
||||||
)},
|
)},
|
||||||
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})},
|
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})},
|
||||||
{expired, hoconsc:mk(boolean(), #{desc => "Expired", required => false})}
|
{expired, hoconsc:mk(boolean(), #{desc => "Expired", required => false})}
|
||||||
];
|
] ++ app_extend_fields();
|
||||||
fields(name) ->
|
fields(name) ->
|
||||||
[
|
[
|
||||||
{name,
|
{name,
|
||||||
|
@ -192,7 +193,8 @@ api_key(post, #{body := App}) ->
|
||||||
} = App,
|
} = App,
|
||||||
ExpiredAt = ensure_expired_at(App),
|
ExpiredAt = ensure_expired_at(App),
|
||||||
Desc = unicode:characters_to_binary(Desc0, unicode),
|
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} ->
|
{ok, NewApp} ->
|
||||||
{200, emqx_mgmt_auth:format(NewApp)};
|
{200, emqx_mgmt_auth:format(NewApp)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
@ -218,10 +220,38 @@ api_key_by_name(put, #{bindings := #{name := Name}, body := Body}) ->
|
||||||
Enable = maps:get(<<"enable">>, Body, undefined),
|
Enable = maps:get(<<"enable">>, Body, undefined),
|
||||||
ExpiredAt = ensure_expired_at(Body),
|
ExpiredAt = ensure_expired_at(Body),
|
||||||
Desc = maps:get(<<"desc">>, Body, undefined),
|
Desc = maps:get(<<"desc">>, Body, undefined),
|
||||||
case emqx_mgmt_auth:update(Name, Enable, ExpiredAt, Desc) of
|
Role = maps:get(<<"role">>, Body, ?ROLE_API_DEFAULT),
|
||||||
{ok, App} -> {200, emqx_mgmt_auth:format(App)};
|
case emqx_mgmt_auth:update(Name, Enable, ExpiredAt, Desc, Role) of
|
||||||
{error, not_found} -> {404, ?NOT_FOUND_RESPONSE}
|
{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.
|
end.
|
||||||
|
|
||||||
ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt;
|
ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt;
|
||||||
ensure_expired_at(_) -> infinity.
|
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.
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
-module(emqx_mgmt_auth).
|
-module(emqx_mgmt_auth).
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard_rbac.hrl").
|
||||||
|
|
||||||
-behaviour(emqx_db_backup).
|
-behaviour(emqx_db_backup).
|
||||||
|
|
||||||
|
@ -25,16 +26,16 @@
|
||||||
-behaviour(emqx_config_handler).
|
-behaviour(emqx_config_handler).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
create/4,
|
create/5,
|
||||||
read/1,
|
read/1,
|
||||||
update/4,
|
update/5,
|
||||||
delete/1,
|
delete/1,
|
||||||
list/0,
|
list/0,
|
||||||
init_bootstrap_file/0,
|
init_bootstrap_file/0,
|
||||||
format/1
|
format/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([authorize/3]).
|
-export([authorize/4]).
|
||||||
-export([post_config_update/5]).
|
-export([post_config_update/5]).
|
||||||
|
|
||||||
-export([backup_tables/0]).
|
-export([backup_tables/0]).
|
||||||
|
@ -48,10 +49,11 @@
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-export([create/5]).
|
-export([create/6]).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-define(APP, emqx_app).
|
-define(APP, emqx_app).
|
||||||
|
-type api_user_role() :: binary().
|
||||||
|
|
||||||
-record(?APP, {
|
-record(?APP, {
|
||||||
name = <<>> :: binary() | '_',
|
name = <<>> :: binary() | '_',
|
||||||
|
@ -60,17 +62,21 @@
|
||||||
enable = true :: boolean() | '_',
|
enable = true :: boolean() | '_',
|
||||||
desc = <<>> :: binary() | '_',
|
desc = <<>> :: binary() | '_',
|
||||||
expired_at = 0 :: integer() | undefined | infinity | '_',
|
expired_at = 0 :: integer() | undefined | infinity | '_',
|
||||||
created_at = 0 :: integer() | '_'
|
created_at = 0 :: integer() | '_',
|
||||||
|
role = ?ROLE_DEFAULT :: api_user_role() | '_',
|
||||||
|
extra = #{} :: map() | '_'
|
||||||
}).
|
}).
|
||||||
|
|
||||||
mnesia(boot) ->
|
mnesia(boot) ->
|
||||||
|
Fields = record_info(fields, ?APP),
|
||||||
ok = mria:create_table(?APP, [
|
ok = mria:create_table(?APP, [
|
||||||
{type, set},
|
{type, set},
|
||||||
{rlog_shard, ?COMMON_SHARD},
|
{rlog_shard, ?COMMON_SHARD},
|
||||||
{storage, disc_copies},
|
{storage, disc_copies},
|
||||||
{record_name, ?APP},
|
{record_name, ?APP},
|
||||||
{attributes, record_info(fields, ?APP)}
|
{attributes, Fields}
|
||||||
]).
|
]),
|
||||||
|
maybe_migrate_table(Fields).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Data backup
|
%% Data backup
|
||||||
|
@ -95,13 +101,13 @@ init_bootstrap_file() ->
|
||||||
?SLOG(debug, #{msg => "init_bootstrap_api_keys_from_file", file => File}),
|
?SLOG(debug, #{msg => "init_bootstrap_api_keys_from_file", file => File}),
|
||||||
init_bootstrap_file(File).
|
init_bootstrap_file(File).
|
||||||
|
|
||||||
create(Name, Enable, ExpiredAt, Desc) ->
|
create(Name, Enable, ExpiredAt, Desc, Role) ->
|
||||||
ApiSecret = generate_api_secret(),
|
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
|
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"}
|
false -> {error, "Maximum ApiKey"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -111,8 +117,13 @@ read(Name) ->
|
||||||
[] -> {error, not_found}
|
[] -> {error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
update(Name, Enable, ExpiredAt, Desc) ->
|
update(Name, Enable, ExpiredAt, Desc, Role) ->
|
||||||
trans(fun ?MODULE:do_update/4, [Name, Enable, ExpiredAt, Desc]).
|
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) ->
|
do_update(Name, Enable, ExpiredAt, Desc) ->
|
||||||
case mnesia:read(?APP, Name, write) of
|
case mnesia:read(?APP, Name, write) of
|
||||||
|
@ -138,37 +149,37 @@ do_delete(Name) ->
|
||||||
[_App] -> mnesia:delete({?APP, Name})
|
[_App] -> mnesia:delete({?APP, Name})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
format(App = #{expired_at := ExpiredAt0, created_at := CreateAt}) ->
|
format(App = #{expired_at := ExpiredAt, created_at := CreateAt}) ->
|
||||||
ExpiredAt =
|
format_app_extend(App#{
|
||||||
case ExpiredAt0 of
|
expired_at => format_epoch(ExpiredAt),
|
||||||
infinity -> <<"infinity">>;
|
created_at => format_epoch(CreateAt)
|
||||||
_ -> emqx_utils_calendar:epoch_to_rfc3339(ExpiredAt0, second)
|
}).
|
||||||
end,
|
|
||||||
App#{
|
format_epoch(infinity) ->
|
||||||
expired_at => ExpiredAt,
|
<<"infinity">>;
|
||||||
created_at => emqx_utils_calendar:epoch_to_rfc3339(CreateAt, second)
|
format_epoch(Epoch) ->
|
||||||
}.
|
emqx_utils_calendar:epoch_to_rfc3339(Epoch, second).
|
||||||
|
|
||||||
list() ->
|
list() ->
|
||||||
to_map(ets:match_object(?APP, #?APP{_ = '_'})).
|
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">>};
|
{error, <<"not_allowed">>};
|
||||||
authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) ->
|
authorize(<<"/api/v5/api_key", _/binary>>, _Req, _ApiKey, _ApiSecret) ->
|
||||||
{error, <<"not_allowed">>};
|
{error, <<"not_allowed">>};
|
||||||
authorize(<<"/api/v5/logout", _/binary>>, _ApiKey, _ApiSecret) ->
|
authorize(<<"/api/v5/logout", _/binary>>, _Req, _ApiKey, _ApiSecret) ->
|
||||||
{error, <<"not_allowed">>};
|
{error, <<"not_allowed">>};
|
||||||
authorize(_Path, ApiKey, ApiSecret) ->
|
authorize(_Path, Req, ApiKey, ApiSecret) ->
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
case find_by_api_key(ApiKey) of
|
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
|
case emqx_dashboard_admin:verify_hash(ApiSecret, SecretHash) of
|
||||||
ok -> ok;
|
ok -> check_rbac(Req, Role);
|
||||||
error -> {error, "secret_error"}
|
error -> {error, "secret_error"}
|
||||||
end;
|
end;
|
||||||
{ok, true, _ExpiredAt, _SecretHash} ->
|
{ok, true, _ExpiredAt, _SecretHash, _Role} ->
|
||||||
{error, "secret_expired"};
|
{error, "secret_expired"};
|
||||||
{ok, false, _ExpiredAt, _SecretHash} ->
|
{ok, false, _ExpiredAt, _SecretHash, _Role} ->
|
||||||
{error, "secret_disable"};
|
{error, "secret_disable"};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
|
@ -177,8 +188,12 @@ authorize(_Path, ApiKey, ApiSecret) ->
|
||||||
find_by_api_key(ApiKey) ->
|
find_by_api_key(ApiKey) ->
|
||||||
Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
|
Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
|
||||||
case mria:ro_transaction(?COMMON_SHARD, Fun) of
|
case mria:ro_transaction(?COMMON_SHARD, Fun) of
|
||||||
{atomic, [#?APP{api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt}]} ->
|
{atomic, [
|
||||||
{ok, Enable, ExpiredAt, SecretHash};
|
#?APP{
|
||||||
|
api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt, role = Role
|
||||||
|
}
|
||||||
|
]} ->
|
||||||
|
{ok, Enable, ExpiredAt, SecretHash, Role};
|
||||||
_ ->
|
_ ->
|
||||||
{error, "not_found"}
|
{error, "not_found"}
|
||||||
end.
|
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(undefined) -> false;
|
||||||
is_expired(ExpiredTime) -> ExpiredTime < erlang:system_time(second).
|
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 =
|
||||||
#?APP{
|
#?APP{
|
||||||
name = Name,
|
name = Name,
|
||||||
|
@ -211,7 +226,8 @@ create_app(Name, ApiSecret, Enable, ExpiredAt, Desc) ->
|
||||||
desc = Desc,
|
desc = Desc,
|
||||||
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))
|
api_key = list_to_binary(emqx_utils:gen_id(16)),
|
||||||
|
role = Role
|
||||||
},
|
},
|
||||||
case create_app(App) of
|
case create_app(App) of
|
||||||
{ok, Res} ->
|
{ok, Res} ->
|
||||||
|
@ -220,8 +236,13 @@ create_app(Name, ApiSecret, Enable, ExpiredAt, Desc) ->
|
||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
create_app(App = #?APP{api_key = ApiKey, name = Name}) ->
|
create_app(App = #?APP{api_key = ApiKey, name = Name, role = Role}) ->
|
||||||
trans(fun ?MODULE:do_create_app/3, [App, ApiKey, Name]).
|
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}) ->
|
force_create_app(NamePrefix, App = #?APP{api_key = ApiKey}) ->
|
||||||
trans(fun ?MODULE:do_force_create_app/3, [App, ApiKey, NamePrefix]).
|
trans(fun ?MODULE:do_force_create_app/3, [App, ApiKey, NamePrefix]).
|
||||||
|
@ -340,3 +361,60 @@ add_bootstrap_file(File, Dev, MP, Line) ->
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
throw(#{file => File, line => Line, reason => Reason})
|
throw(#{file => File, line => Line, reason => Reason})
|
||||||
end.
|
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.
|
||||||
|
|
|
@ -41,37 +41,42 @@ t_bootstrap_file(_) ->
|
||||||
File = "./bootstrap_api_keys.txt",
|
File = "./bootstrap_api_keys.txt",
|
||||||
ok = file:write_file(File, Bin),
|
ok = file:write_file(File, Bin),
|
||||||
update_file(File),
|
update_file(File),
|
||||||
?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
|
?assertEqual(ok, auth_authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
|
||||||
?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
|
?assertEqual(ok, auth_authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
|
||||||
?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-1">>)),
|
?assertMatch({error, _}, auth_authorize(TestPath, <<"test-2">>, <<"secret-1">>)),
|
||||||
|
|
||||||
%% relaunch to check if the table is changed.
|
%% relaunch to check if the table is changed.
|
||||||
Bin1 = <<"test-1:new-secret-1\ntest-2:new-secret-2">>,
|
Bin1 = <<"test-1:new-secret-1\ntest-2:new-secret-2">>,
|
||||||
ok = file:write_file(File, Bin1),
|
ok = file:write_file(File, Bin1),
|
||||||
update_file(File),
|
update_file(File),
|
||||||
?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
|
?assertMatch({error, _}, auth_authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
|
||||||
?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
|
?assertMatch({error, _}, auth_authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
|
||||||
?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)),
|
?assertEqual(ok, auth_authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)),
|
||||||
?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)),
|
?assertEqual(ok, auth_authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)),
|
||||||
|
|
||||||
%% not error when bootstrap_file is empty
|
%% not error when bootstrap_file is empty
|
||||||
update_file(<<>>),
|
update_file(<<>>),
|
||||||
update_file("./bootstrap_apps_not_exist.txt"),
|
update_file("./bootstrap_apps_not_exist.txt"),
|
||||||
?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
|
?assertMatch({error, _}, auth_authorize(TestPath, <<"test-1">>, <<"secret-1">>)),
|
||||||
?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
|
?assertMatch({error, _}, auth_authorize(TestPath, <<"test-2">>, <<"secret-2">>)),
|
||||||
?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)),
|
?assertEqual(ok, auth_authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)),
|
||||||
?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)),
|
?assertEqual(ok, auth_authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)),
|
||||||
|
|
||||||
%% bad format
|
%% bad format
|
||||||
BadBin = <<"test-1:secret-11\ntest-2 secret-12">>,
|
BadBin = <<"test-1:secret-11\ntest-2 secret-12">>,
|
||||||
ok = file:write_file(File, BadBin),
|
ok = file:write_file(File, BadBin),
|
||||||
update_file(File),
|
update_file(File),
|
||||||
?assertMatch({error, #{reason := "invalid_format"}}, emqx_mgmt_auth:init_bootstrap_file()),
|
?assertMatch({error, #{reason := "invalid_format"}}, emqx_mgmt_auth:init_bootstrap_file()),
|
||||||
?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-11">>)),
|
?assertEqual(ok, auth_authorize(TestPath, <<"test-1">>, <<"secret-11">>)),
|
||||||
?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-12">>)),
|
?assertMatch({error, _}, auth_authorize(TestPath, <<"test-2">>, <<"secret-12">>)),
|
||||||
update_file(<<>>),
|
update_file(<<>>),
|
||||||
ok.
|
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) ->
|
update_file(File) ->
|
||||||
?assertMatch({ok, _}, emqx:update_config([<<"api_key">>], #{<<"bootstrap_file">> => File})).
|
?assertMatch({ok, _}, emqx:update_config([<<"api_key">>], #{<<"bootstrap_file">> => File})).
|
||||||
|
|
||||||
|
|
|
@ -30,4 +30,7 @@ format.desc:
|
||||||
format.label:
|
format.label:
|
||||||
"""Unique and format by [a-zA-Z0-9-_]"""
|
"""Unique and format by [a-zA-Z0-9-_]"""
|
||||||
|
|
||||||
|
role.desc:
|
||||||
|
"""Role for this API"""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue