Merge pull request #11670 from id/0925-sync-r53-to-master
sync r53 to master
This commit is contained in:
commit
cedd90e89f
|
@ -5,9 +5,11 @@
|
||||||
/apps/emqx/ @emqx/emqx-review-board @lafirest
|
/apps/emqx/ @emqx/emqx-review-board @lafirest
|
||||||
/apps/emqx_authn/ @emqx/emqx-review-board @JimMoen @savonarola
|
/apps/emqx_authn/ @emqx/emqx-review-board @JimMoen @savonarola
|
||||||
/apps/emqx_authz/ @emqx/emqx-review-board @JimMoen @savonarola
|
/apps/emqx_authz/ @emqx/emqx-review-board @JimMoen @savonarola
|
||||||
/apps/emqx_connector/ @emqx/emqx-review-board @JimMoen
|
/apps/emqx_connector/ @emqx/emqx-review-board
|
||||||
/apps/emqx_dashboard/ @emqx/emqx-review-board @JimMoen @lafirest
|
/apps/emqx_dashboard/ @emqx/emqx-review-board @JimMoen @lafirest
|
||||||
/apps/emqx_exhook/ @emqx/emqx-review-board @JimMoen @lafirest
|
/apps/emqx_dashboard_rbac/ @emqx/emqx-review-board @lafirest
|
||||||
|
/apps/emqx_dashboard_sso/ @emqx/emqx-review-board @JimMoen @lafirest
|
||||||
|
/apps/emqx_exhook/ @emqx/emqx-review-board @JimMoen @HJianBo
|
||||||
/apps/emqx_ft/ @emqx/emqx-review-board @savonarola @keynslug
|
/apps/emqx_ft/ @emqx/emqx-review-board @savonarola @keynslug
|
||||||
/apps/emqx_gateway/ @emqx/emqx-review-board @lafirest
|
/apps/emqx_gateway/ @emqx/emqx-review-board @lafirest
|
||||||
/apps/emqx_management/ @emqx/emqx-review-board @lafirest @sstrigler
|
/apps/emqx_management/ @emqx/emqx-review-board @lafirest @sstrigler
|
||||||
|
|
|
@ -13,6 +13,7 @@ on:
|
||||||
- 'master'
|
- 'master'
|
||||||
- 'release-51'
|
- 'release-51'
|
||||||
- 'release-52'
|
- 'release-52'
|
||||||
|
- 'release-53'
|
||||||
- 'ci/**'
|
- 'ci/**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -21,8 +21,8 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
profile:
|
profile:
|
||||||
- ['emqx', 'master']
|
- ['emqx', 'master']
|
||||||
- ['emqx-enterprise', 'release-51']
|
|
||||||
- ['emqx-enterprise', 'release-52']
|
- ['emqx-enterprise', 'release-52']
|
||||||
|
- ['emqx-enterprise', 'release-53']
|
||||||
otp:
|
otp:
|
||||||
- 25.3.2-2
|
- 25.3.2-2
|
||||||
arch:
|
arch:
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
-define(EMQX_RELEASE_CE, "5.2.1").
|
-define(EMQX_RELEASE_CE, "5.2.1").
|
||||||
|
|
||||||
%% Enterprise edition
|
%% Enterprise edition
|
||||||
-define(EMQX_RELEASE_EE, "5.2.1").
|
-define(EMQX_RELEASE_EE, "5.3.0-alpha.1").
|
||||||
|
|
||||||
%% The HTTP API version
|
%% The HTTP API version
|
||||||
-define(EMQX_API_VERSION, "5.0").
|
-define(EMQX_API_VERSION, "5.0").
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
|
|
||||||
%% Applications and modules we wish to ignore in the analysis:
|
%% Applications and modules we wish to ignore in the analysis:
|
||||||
-define(IGNORED_APPS,
|
-define(IGNORED_APPS,
|
||||||
"gen_rpc, recon, redbug, observer_cli, snabbkaffe, ekka, mria, amqp_client, rabbit_common"
|
"gen_rpc, recon, redbug, observer_cli, snabbkaffe, ekka, mria, amqp_client, rabbit_common, esaml"
|
||||||
).
|
).
|
||||||
-define(IGNORED_MODULES, "emqx_rpc").
|
-define(IGNORED_MODULES, "emqx_rpc").
|
||||||
-define(FORCE_DELETED_MODULES, [
|
-define(FORCE_DELETED_MODULES, [
|
||||||
|
|
|
@ -1260,7 +1260,7 @@ auth_header_() ->
|
||||||
auth_header_(<<"admin">>, <<"public">>).
|
auth_header_(<<"admin">>, <<"public">>).
|
||||||
|
|
||||||
auth_header_(Username, Password) ->
|
auth_header_(Username, Password) ->
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
{ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
||||||
|
|
||||||
api_path(Parts) ->
|
api_path(Parts) ->
|
||||||
|
|
|
@ -44,6 +44,10 @@
|
||||||
usage/2
|
usage/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
eval_erl/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Exports mainly for test cases
|
%% Exports mainly for test cases
|
||||||
-export([
|
-export([
|
||||||
format/2,
|
format/2,
|
||||||
|
@ -119,7 +123,7 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
|
||||||
Start = erlang:monotonic_time(),
|
Start = erlang:monotonic_time(),
|
||||||
Result =
|
Result =
|
||||||
case lookup_command(Cmd) of
|
case lookup_command(Cmd) of
|
||||||
[{Mod, Fun}] ->
|
{ok, {Mod, Fun}} ->
|
||||||
try
|
try
|
||||||
apply(Mod, Fun, [Args])
|
apply(Mod, Fun, [Args])
|
||||||
catch
|
catch
|
||||||
|
@ -127,13 +131,15 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
|
||||||
?LOG_ERROR(#{
|
?LOG_ERROR(#{
|
||||||
msg => "ctl_command_crashed",
|
msg => "ctl_command_crashed",
|
||||||
stacktrace => Stacktrace,
|
stacktrace => Stacktrace,
|
||||||
reason => Reason
|
reason => Reason,
|
||||||
|
module => Mod,
|
||||||
|
function => Fun
|
||||||
}),
|
}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
Error ->
|
{error, Reason} ->
|
||||||
help(),
|
help(),
|
||||||
Error
|
{error, Reason}
|
||||||
end,
|
end,
|
||||||
Duration = erlang:convert_time_unit(erlang:monotonic_time() - Start, native, millisecond),
|
Duration = erlang:convert_time_unit(erlang:monotonic_time() - Start, native, millisecond),
|
||||||
|
|
||||||
|
@ -144,12 +150,22 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
|
||||||
),
|
),
|
||||||
Result.
|
Result.
|
||||||
|
|
||||||
-spec lookup_command(cmd()) -> [{module(), atom()}] | {error, any()}.
|
-spec lookup_command(cmd()) -> {module(), atom()} | {error, any()}.
|
||||||
|
lookup_command(eval_erl) ->
|
||||||
|
%% So far 'emqx ctl eval_erl Expr' is a undocumented hidden command.
|
||||||
|
%% For backward compatibility,
|
||||||
|
%% the documented command 'emqx eval Expr' has the expression parsed
|
||||||
|
%% in the remsh node (nodetool).
|
||||||
|
%%
|
||||||
|
%% 'eval_erl' is added for two purposes
|
||||||
|
%% 1. 'emqx eval Expr' can be audited
|
||||||
|
%% 2. 'emqx ctl eval_erl Expr' simplifies the scripting part
|
||||||
|
{ok, {?MODULE, eval_erl}};
|
||||||
lookup_command(Cmd) when is_atom(Cmd) ->
|
lookup_command(Cmd) when is_atom(Cmd) ->
|
||||||
case is_initialized() of
|
case is_initialized() of
|
||||||
true ->
|
true ->
|
||||||
case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of
|
case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of
|
||||||
[El] -> El;
|
[[{M, F}]] -> {ok, {M, F}};
|
||||||
[] -> {error, cmd_not_found}
|
[] -> {error, cmd_not_found}
|
||||||
end;
|
end;
|
||||||
false ->
|
false ->
|
||||||
|
@ -319,7 +335,7 @@ audit_log(Level, From, Log) ->
|
||||||
case lookup_command(audit) of
|
case lookup_command(audit) of
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
ignore;
|
ignore;
|
||||||
[{Mod, Fun}] ->
|
{ok, {Mod, Fun}} ->
|
||||||
try
|
try
|
||||||
apply(Mod, Fun, [Level, From, Log])
|
apply(Mod, Fun, [Level, From, Log])
|
||||||
catch
|
catch
|
||||||
|
@ -339,3 +355,23 @@ audit_level({ok, _}, Duration) when Duration >= ?TOO_SLOW -> warning;
|
||||||
audit_level(ok, _Duration) -> info;
|
audit_level(ok, _Duration) -> info;
|
||||||
audit_level({ok, _}, _Duration) -> info;
|
audit_level({ok, _}, _Duration) -> info;
|
||||||
audit_level(_, _) -> error.
|
audit_level(_, _) -> error.
|
||||||
|
|
||||||
|
eval_erl([Parsed | _] = Expr) when is_tuple(Parsed) ->
|
||||||
|
eval_expr(Expr);
|
||||||
|
eval_erl([String]) ->
|
||||||
|
% convenience to users, if they forgot a trailing
|
||||||
|
% '.' add it for them.
|
||||||
|
Normalized =
|
||||||
|
case lists:reverse(String) of
|
||||||
|
[$. | _] -> String;
|
||||||
|
R -> lists:reverse([$. | R])
|
||||||
|
end,
|
||||||
|
% then scan and parse the string
|
||||||
|
{ok, Scanned, _} = erl_scan:string(Normalized),
|
||||||
|
{ok, Parsed} = erl_parse:parse_exprs(Scanned),
|
||||||
|
{ok, Value} = eval_expr(Parsed),
|
||||||
|
print("~p~n", [Value]).
|
||||||
|
|
||||||
|
eval_expr(Parsed) ->
|
||||||
|
{value, Value, _} = erl_eval:exprs(Parsed, []),
|
||||||
|
{ok, Value}.
|
||||||
|
|
|
@ -40,8 +40,8 @@ t_reg_unreg_command(_) ->
|
||||||
fun(_CtlSrv) ->
|
fun(_CtlSrv) ->
|
||||||
emqx_ctl:register_command(cmd1, {?MODULE, cmd1_fun}),
|
emqx_ctl:register_command(cmd1, {?MODULE, cmd1_fun}),
|
||||||
emqx_ctl:register_command(cmd2, {?MODULE, cmd2_fun}),
|
emqx_ctl:register_command(cmd2, {?MODULE, cmd2_fun}),
|
||||||
?assertEqual([{?MODULE, cmd1_fun}], emqx_ctl:lookup_command(cmd1)),
|
?assertEqual({?MODULE, cmd1_fun}, lookup_command(cmd1)),
|
||||||
?assertEqual([{?MODULE, cmd2_fun}], emqx_ctl:lookup_command(cmd2)),
|
?assertEqual({?MODULE, cmd2_fun}, lookup_command(cmd2)),
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
[{cmd1, ?MODULE, cmd1_fun}, {cmd2, ?MODULE, cmd2_fun}],
|
[{cmd1, ?MODULE, cmd1_fun}, {cmd2, ?MODULE, cmd2_fun}],
|
||||||
emqx_ctl:get_commands()
|
emqx_ctl:get_commands()
|
||||||
|
@ -49,8 +49,8 @@ t_reg_unreg_command(_) ->
|
||||||
emqx_ctl:unregister_command(cmd1),
|
emqx_ctl:unregister_command(cmd1),
|
||||||
emqx_ctl:unregister_command(cmd2),
|
emqx_ctl:unregister_command(cmd2),
|
||||||
ct:sleep(100),
|
ct:sleep(100),
|
||||||
?assertEqual({error, cmd_not_found}, emqx_ctl:lookup_command(cmd1)),
|
?assertEqual({error, cmd_not_found}, lookup_command(cmd1)),
|
||||||
?assertEqual({error, cmd_not_found}, emqx_ctl:lookup_command(cmd2)),
|
?assertEqual({error, cmd_not_found}, lookup_command(cmd2)),
|
||||||
?assertEqual([], emqx_ctl:get_commands())
|
?assertEqual([], emqx_ctl:get_commands())
|
||||||
end
|
end
|
||||||
).
|
).
|
||||||
|
@ -79,6 +79,12 @@ t_print(_) ->
|
||||||
?assertEqual("~!@#$%^&*()", emqx_ctl:print("~ts", [<<"~!@#$%^&*()">>])),
|
?assertEqual("~!@#$%^&*()", emqx_ctl:print("~ts", [<<"~!@#$%^&*()">>])),
|
||||||
unmock_print().
|
unmock_print().
|
||||||
|
|
||||||
|
t_eval_erl(_) ->
|
||||||
|
mock_print(),
|
||||||
|
Expected = atom_to_list(node()) ++ "\n",
|
||||||
|
?assertEqual(Expected, emqx_ctl:run_command(["eval_erl", "node()"])),
|
||||||
|
unmock_print().
|
||||||
|
|
||||||
t_usage(_) ->
|
t_usage(_) ->
|
||||||
CmdParams1 = "emqx_cmd_1 param1 param2",
|
CmdParams1 = "emqx_cmd_1 param1 param2",
|
||||||
CmdDescr1 = "emqx_cmd_1 is a test command means nothing",
|
CmdDescr1 = "emqx_cmd_1 is a test command means nothing",
|
||||||
|
@ -129,3 +135,9 @@ mock_print() ->
|
||||||
|
|
||||||
unmock_print() ->
|
unmock_print() ->
|
||||||
meck:unload(emqx_ctl).
|
meck:unload(emqx_ctl).
|
||||||
|
|
||||||
|
lookup_command(Cmd) ->
|
||||||
|
case emqx_ctl:lookup_command(Cmd) of
|
||||||
|
{ok, {Mod, Fun}} -> {Mod, Fun};
|
||||||
|
Error -> Error
|
||||||
|
end.
|
||||||
|
|
|
@ -77,7 +77,7 @@ schema("/login") ->
|
||||||
summary => <<"Dashboard authentication">>,
|
summary => <<"Dashboard authentication">>,
|
||||||
'requestBody' => fields([username, password]),
|
'requestBody' => fields([username, password]),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => fields([token, version, license]),
|
200 => fields([role, token, version, license]),
|
||||||
401 => response_schema(401)
|
401 => response_schema(401)
|
||||||
},
|
},
|
||||||
security => []
|
security => []
|
||||||
|
@ -219,14 +219,16 @@ login(post, #{body := Params}) ->
|
||||||
Username = maps:get(<<"username">>, Params),
|
Username = maps:get(<<"username">>, Params),
|
||||||
Password = maps:get(<<"password">>, Params),
|
Password = maps:get(<<"password">>, Params),
|
||||||
case emqx_dashboard_admin:sign_token(Username, Password) of
|
case emqx_dashboard_admin:sign_token(Username, Password) of
|
||||||
{ok, Token} ->
|
{ok, Role, Token} ->
|
||||||
?SLOG(info, #{msg => "dashboard_login_successful", username => Username}),
|
?SLOG(info, #{msg => "dashboard_login_successful", username => Username}),
|
||||||
Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
||||||
{200, #{
|
{200,
|
||||||
|
filter_result(#{
|
||||||
|
role => Role,
|
||||||
token => Token,
|
token => Token,
|
||||||
version => Version,
|
version => Version,
|
||||||
license => #{edition => emqx_release:edition()}
|
license => #{edition => emqx_release:edition()}
|
||||||
}};
|
})};
|
||||||
{error, R} ->
|
{error, R} ->
|
||||||
?SLOG(info, #{msg => "dashboard_login_failed", username => Username, reason => R}),
|
?SLOG(info, #{msg => "dashboard_login_failed", username => Username, reason => R}),
|
||||||
{401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>}
|
{401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
-export([error_codes/1, error_codes/2]).
|
-export([error_codes/1, error_codes/2]).
|
||||||
-export([file_schema/1]).
|
-export([file_schema/1]).
|
||||||
-export([base_path/0]).
|
-export([base_path/0]).
|
||||||
-export([relative_uri/1]).
|
-export([relative_uri/1, get_relative_uri/1]).
|
||||||
-export([compose_filters/2]).
|
-export([compose_filters/2]).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
|
@ -212,6 +212,12 @@ base_path() ->
|
||||||
relative_uri(Uri) ->
|
relative_uri(Uri) ->
|
||||||
base_path() ++ Uri.
|
base_path() ++ Uri.
|
||||||
|
|
||||||
|
-spec get_relative_uri(uri_string:uri_string()) -> {ok, uri_string:uri_string()} | error.
|
||||||
|
get_relative_uri(<<?BASE_PATH, Path/binary>>) ->
|
||||||
|
{ok, Path};
|
||||||
|
get_relative_uri(_Path) ->
|
||||||
|
error.
|
||||||
|
|
||||||
file_schema(FileName) ->
|
file_schema(FileName) ->
|
||||||
#{
|
#{
|
||||||
content => #{
|
content => #{
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% jwt function
|
%% jwt function
|
||||||
-spec sign(User :: dashboard_user(), Password :: binary()) ->
|
-spec sign(User :: dashboard_user(), Password :: binary()) ->
|
||||||
{ok, Token :: binary()} | {error, Reason :: term()}.
|
{ok, dashboard_user_role(), Token :: binary()} | {error, Reason :: term()}.
|
||||||
sign(User, Password) ->
|
sign(User, Password) ->
|
||||||
do_sign(User, Password).
|
do_sign(User, Password).
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ do_sign(#?ADMIN{username = Username} = User, Password) ->
|
||||||
Role = emqx_dashboard_admin:role(User),
|
Role = emqx_dashboard_admin:role(User),
|
||||||
JWTRec = format(Token, Username, Role, ExpTime),
|
JWTRec = format(Token, Username, Role, ExpTime),
|
||||||
_ = mria:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]),
|
_ = mria:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]),
|
||||||
{ok, Token}.
|
{ok, Role, Token}.
|
||||||
|
|
||||||
-spec do_verify(_, Token :: binary()) ->
|
-spec do_verify(_, Token :: binary()) ->
|
||||||
Result ::
|
Result ::
|
||||||
|
|
|
@ -120,6 +120,7 @@ t_rest_api(_Config) ->
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
[
|
[
|
||||||
filter_req(#{
|
filter_req(#{
|
||||||
|
<<"backend">> => <<"local">>,
|
||||||
<<"username">> => <<"admin">>,
|
<<"username">> => <<"admin">>,
|
||||||
<<"description">> => <<"administrator">>,
|
<<"description">> => <<"administrator">>,
|
||||||
<<"role">> => ?ROLE_SUPERUSER
|
<<"role">> => ?ROLE_SUPERUSER
|
||||||
|
@ -269,7 +270,7 @@ auth_header_() ->
|
||||||
auth_header_(<<"admin">>, <<"public">>).
|
auth_header_(<<"admin">>, <<"public">>).
|
||||||
|
|
||||||
auth_header_(Username, Password) ->
|
auth_header_(Username, Password) ->
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
{ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
||||||
|
|
||||||
api_path(Parts) ->
|
api_path(Parts) ->
|
||||||
|
@ -286,6 +287,6 @@ filter_req(Req) ->
|
||||||
-else.
|
-else.
|
||||||
|
|
||||||
filter_req(Req) ->
|
filter_req(Req) ->
|
||||||
maps:without([role, <<"role">>], Req).
|
maps:without([role, <<"role">>, backend, <<"backend">>], Req).
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
|
@ -174,15 +174,16 @@ t_clean_token(_) ->
|
||||||
Password = <<"public_www1">>,
|
Password = <<"public_www1">>,
|
||||||
NewPassword = <<"public_www2">>,
|
NewPassword = <<"public_www2">>,
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>),
|
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>),
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
{ok, _, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
FakeReq = #{method => <<"GET">>},
|
FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/fake")),
|
||||||
|
FakeReq = #{method => <<"GET">>, path => FakePath},
|
||||||
{ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
{ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
%% change password
|
%% change password
|
||||||
{ok, _} = emqx_dashboard_admin:change_password(Username, Password, NewPassword),
|
{ok, _} = emqx_dashboard_admin:change_password(Username, Password, NewPassword),
|
||||||
timer:sleep(5),
|
timer:sleep(5),
|
||||||
{error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
{error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
%% remove user
|
%% remove user
|
||||||
{ok, Token2} = emqx_dashboard_admin:sign_token(Username, NewPassword),
|
{ok, _, Token2} = emqx_dashboard_admin:sign_token(Username, NewPassword),
|
||||||
{ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token2),
|
{ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token2),
|
||||||
{ok, _} = emqx_dashboard_admin:remove_user(Username),
|
{ok, _} = emqx_dashboard_admin:remove_user(Username),
|
||||||
timer:sleep(5),
|
timer:sleep(5),
|
||||||
|
|
|
@ -116,7 +116,7 @@ auth_header(Username) ->
|
||||||
auth_header(Username, <<"public">>).
|
auth_header(Username, <<"public">>).
|
||||||
|
|
||||||
auth_header(Username, Password) ->
|
auth_header(Username, Password) ->
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
{ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
||||||
|
|
||||||
multipart_formdata_request(Url, Fields, Files) ->
|
multipart_formdata_request(Url, Fields, Files) ->
|
||||||
|
|
|
@ -55,7 +55,7 @@ t_status(_Config) ->
|
||||||
[binary, {active, false}, {packet, raw}]
|
[binary, {active, false}, {packet, raw}]
|
||||||
),
|
),
|
||||||
ok = gen_tcp:send(Socket, ranch_proxy_header:header(ProxyInfo)),
|
ok = gen_tcp:send(Socket, ranch_proxy_header:header(ProxyInfo)),
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
{ok, _Role, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
||||||
ok = gen_tcp:send(
|
ok = gen_tcp:send(
|
||||||
Socket,
|
Socket,
|
||||||
"GET /status HTTP/1.1\r\n"
|
"GET /status HTTP/1.1\r\n"
|
||||||
|
|
|
@ -12,4 +12,4 @@ Please see our [contributing.md](../../CONTRIBUTING.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
See [APL](../../APL.txt).
|
EMQ Business Source License 1.1, refer to [LICENSE](BSL.txt).
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
%% -*- mode: erlang; -*-
|
%% -*- mode: erlang; -*-
|
||||||
|
|
||||||
{erl_opts, [debug_info]}.
|
{erl_opts, [debug_info]}.
|
||||||
|
|
||||||
{deps, [
|
{deps, [
|
||||||
{emqx_connector, {path, "../../apps/emqx_dashboard"}}
|
{emqx_dashboard, {path, "../../apps/emqx_dashboard"}}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
@ -12,9 +12,15 @@
|
||||||
%%=====================================================================
|
%%=====================================================================
|
||||||
%% API
|
%% API
|
||||||
check_rbac(Req, Extra) ->
|
check_rbac(Req, Extra) ->
|
||||||
Method = cowboy_req:method(Req),
|
|
||||||
Role = role(Extra),
|
Role = role(Extra),
|
||||||
check_rbac_with_method(Role, Method).
|
Method = cowboy_req:method(Req),
|
||||||
|
AbsPath = cowboy_req:path(Req),
|
||||||
|
case emqx_dashboard_swagger:get_relative_uri(AbsPath) of
|
||||||
|
{ok, Path} ->
|
||||||
|
check_rbac(Role, Method, Path);
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end.
|
||||||
|
|
||||||
%% For compatibility
|
%% For compatibility
|
||||||
role(#?ADMIN{role = undefined}) ->
|
role(#?ADMIN{role = undefined}) ->
|
||||||
|
@ -35,11 +41,14 @@ valid_role(Role) ->
|
||||||
{error, <<"Role does not exist">>}
|
{error, <<"Role does not exist">>}
|
||||||
end.
|
end.
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
check_rbac_with_method(?ROLE_SUPERUSER, _) ->
|
check_rbac(?ROLE_SUPERUSER, _, _) ->
|
||||||
true;
|
true;
|
||||||
check_rbac_with_method(?ROLE_VIEWER, <<"GET">>) ->
|
check_rbac(?ROLE_VIEWER, <<"GET">>, _) ->
|
||||||
true;
|
true;
|
||||||
check_rbac_with_method(_, _) ->
|
%% this API is a special case
|
||||||
|
check_rbac(?ROLE_VIEWER, <<"POST">>, <<"/logout">>) ->
|
||||||
|
true;
|
||||||
|
check_rbac(_, _, _) ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
role_list() ->
|
role_list() ->
|
||||||
|
|
|
@ -135,8 +135,9 @@ t_clean_token(_) ->
|
||||||
Desc = <<"desc">>,
|
Desc = <<"desc">>,
|
||||||
NewDesc = <<"new desc">>,
|
NewDesc = <<"new desc">>,
|
||||||
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, Desc),
|
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, Desc),
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
{ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
FakeReq = #{method => <<"GET">>},
|
FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/fake")),
|
||||||
|
FakeReq = #{method => <<"GET">>, path => FakePath},
|
||||||
{ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
{ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
%% change description
|
%% change description
|
||||||
{ok, _} = emqx_dashboard_admin:update_user(Username, ?ROLE_SUPERUSER, NewDesc),
|
{ok, _} = emqx_dashboard_admin:update_user(Username, ?ROLE_SUPERUSER, NewDesc),
|
||||||
|
@ -148,6 +149,17 @@ t_clean_token(_) ->
|
||||||
{error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
{error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_login_out(_) ->
|
||||||
|
Username = <<"admin_token">>,
|
||||||
|
Password = <<"public_www1">>,
|
||||||
|
Desc = <<"desc">>,
|
||||||
|
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, Desc),
|
||||||
|
{ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
|
FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/logout")),
|
||||||
|
FakeReq = #{method => <<"POST">>, path => FakePath},
|
||||||
|
{ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token),
|
||||||
|
ok.
|
||||||
|
|
||||||
add_default_superuser() ->
|
add_default_superuser() ->
|
||||||
{ok, _NewUser} = emqx_dashboard_admin:add_user(
|
{ok, _NewUser} = emqx_dashboard_admin:add_user(
|
||||||
?DEFAULT_SUPERUSER,
|
?DEFAULT_SUPERUSER,
|
||||||
|
|
|
@ -3,5 +3,6 @@
|
||||||
{erl_opts, [debug_info]}.
|
{erl_opts, [debug_info]}.
|
||||||
{deps, [
|
{deps, [
|
||||||
{emqx_ldap, {path, "../../apps/emqx_ldap"}},
|
{emqx_ldap, {path, "../../apps/emqx_ldap"}},
|
||||||
{emqx_dashboard, {path, "../../apps/emqx_dashboard"}}
|
{emqx_dashboard, {path, "../../apps/emqx_dashboard"}},
|
||||||
|
{esaml, {git, "https://github.com/emqx/esaml", {tag, "v1.1.1"}}}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
kernel,
|
kernel,
|
||||||
stdlib,
|
stdlib,
|
||||||
emqx_dashboard,
|
emqx_dashboard,
|
||||||
emqx_ldap
|
emqx_ldap,
|
||||||
|
esaml
|
||||||
]},
|
]},
|
||||||
{mod, {emqx_dashboard_sso_app, []}},
|
{mod, {emqx_dashboard_sso_app, []}},
|
||||||
{env, []},
|
{env, []},
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
-module(emqx_dashboard_sso).
|
-module(emqx_dashboard_sso).
|
||||||
|
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
hocon_ref/1,
|
hocon_ref/1,
|
||||||
|
@ -38,7 +39,9 @@
|
||||||
{ok, NewState :: state()} | {error, Reason :: term()}.
|
{ok, NewState :: state()} | {error, Reason :: term()}.
|
||||||
-callback destroy(State :: state()) -> ok.
|
-callback destroy(State :: state()) -> ok.
|
||||||
-callback login(request(), State :: state()) ->
|
-callback login(request(), State :: state()) ->
|
||||||
{ok, Token :: binary()} | {error, Reason :: term()}.
|
{ok, dashboard_user_role(), Token :: binary()}
|
||||||
|
| {redirect, tuple()}
|
||||||
|
| {error, Reason :: term()}.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Callback Interface
|
%% Callback Interface
|
||||||
|
@ -76,4 +79,7 @@ provider(Backend) ->
|
||||||
maps:get(Backend, backends()).
|
maps:get(Backend, backends()).
|
||||||
|
|
||||||
backends() ->
|
backends() ->
|
||||||
#{ldap => emqx_dashboard_sso_ldap}.
|
#{
|
||||||
|
ldap => emqx_dashboard_sso_ldap,
|
||||||
|
saml => emqx_dashboard_sso_saml
|
||||||
|
}.
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
ref/1
|
ref/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
-import(emqx_dashboard_sso, [provider/1]).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
api_spec/0,
|
api_spec/0,
|
||||||
fields/1,
|
fields/1,
|
||||||
|
@ -31,8 +33,9 @@
|
||||||
backend/2
|
backend/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([sso_parameters/1]).
|
-export([sso_parameters/1, login_reply/2]).
|
||||||
|
|
||||||
|
-define(REDIRECT, 'REDIRECT').
|
||||||
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
||||||
-define(BAD_REQUEST, 'BAD_REQUEST').
|
-define(BAD_REQUEST, 'BAD_REQUEST').
|
||||||
-define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND').
|
-define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND').
|
||||||
|
@ -74,6 +77,9 @@ schema("/sso") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
%% Visit "/sso/login/saml" to start the saml authentication process -- first check to see if
|
||||||
|
%% we are already logged in, otherwise we will make an AuthnRequest and send it to
|
||||||
|
%% our IDP
|
||||||
schema("/sso/login/:backend") ->
|
schema("/sso/login/:backend") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => login,
|
'operationId' => login,
|
||||||
|
@ -83,7 +89,9 @@ schema("/sso/login/:backend") ->
|
||||||
parameters => backend_name_in_path(),
|
parameters => backend_name_in_path(),
|
||||||
'requestBody' => login_union(),
|
'requestBody' => login_union(),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => emqx_dashboard_api:fields([token, version, license]),
|
200 => emqx_dashboard_api:fields([role, token, version, license]),
|
||||||
|
%% Redirect to IDP for saml
|
||||||
|
302 => response_schema(302),
|
||||||
401 => response_schema(401),
|
401 => response_schema(401),
|
||||||
404 => response_schema(404)
|
404 => response_schema(404)
|
||||||
},
|
},
|
||||||
|
@ -126,8 +134,10 @@ schema("/sso/:backend") ->
|
||||||
fields(backend_status) ->
|
fields(backend_status) ->
|
||||||
emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()).
|
emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()).
|
||||||
|
|
||||||
%% -------------------------------------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% API
|
%% API
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
running(get, _Request) ->
|
running(get, _Request) ->
|
||||||
SSO = emqx:get_config([dashboard_sso], #{}),
|
SSO = emqx:get_config([dashboard_sso], #{}),
|
||||||
{200,
|
{200,
|
||||||
|
@ -141,28 +151,25 @@ running(get, _Request) ->
|
||||||
maps:values(SSO)
|
maps:values(SSO)
|
||||||
)}.
|
)}.
|
||||||
|
|
||||||
login(post, #{bindings := #{backend := Backend}, body := Sign}) ->
|
login(post, #{bindings := #{backend := Backend}} = Request) ->
|
||||||
case emqx_dashboard_sso_manager:lookup_state(Backend) of
|
case emqx_dashboard_sso_manager:lookup_state(Backend) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{404, ?BACKEND_NOT_FOUND, <<"Backend not found">>};
|
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||||
State ->
|
State ->
|
||||||
Provider = emqx_dashboard_sso:provider(Backend),
|
case emqx_dashboard_sso:login(provider(Backend), Request, State) of
|
||||||
case emqx_dashboard_sso:login(Provider, Sign, State) of
|
{ok, Role, Token} ->
|
||||||
{ok, Token} ->
|
?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Request}),
|
||||||
?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Sign}),
|
{200, login_reply(Role, Token)};
|
||||||
Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
{redirect, Redirect} ->
|
||||||
{200, #{
|
?SLOG(info, #{msg => "dashboard_sso_login_redirect", request => Request}),
|
||||||
token => Token,
|
Redirect;
|
||||||
version => Version,
|
|
||||||
license => #{edition => emqx_release:edition()}
|
|
||||||
}};
|
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(info, #{
|
?SLOG(info, #{
|
||||||
msg => "dashboard_sso_login_failed",
|
msg => "dashboard_sso_login_failed",
|
||||||
request => Sign,
|
request => Request,
|
||||||
reason => Reason
|
reason => Reason
|
||||||
}),
|
}),
|
||||||
{401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>}
|
{401, #{code => ?BAD_USERNAME_OR_PWD, message => <<"Auth failed">>}}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -179,7 +186,7 @@ sso(get, _Request) ->
|
||||||
backend(get, #{bindings := #{backend := Type}}) ->
|
backend(get, #{bindings := #{backend := Type}}) ->
|
||||||
case emqx:get_config([dashboard_sso, Type], undefined) of
|
case emqx:get_config([dashboard_sso, Type], undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{404, ?BACKEND_NOT_FOUND};
|
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||||
Backend ->
|
Backend ->
|
||||||
{200, to_json(Backend)}
|
{200, to_json(Backend)}
|
||||||
end;
|
end;
|
||||||
|
@ -193,8 +200,12 @@ backend(delete, #{bindings := #{backend := Backend}}) ->
|
||||||
sso_parameters(Params) ->
|
sso_parameters(Params) ->
|
||||||
backend_name_as_arg(query, [local], <<"local">>) ++ Params.
|
backend_name_as_arg(query, [local], <<"local">>) ++ Params.
|
||||||
|
|
||||||
%% -------------------------------------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% internal
|
%% internal
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
response_schema(302) ->
|
||||||
|
emqx_dashboard_swagger:error_codes([?REDIRECT], ?DESC(redirect));
|
||||||
response_schema(401) ->
|
response_schema(401) ->
|
||||||
emqx_dashboard_swagger:error_codes([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401));
|
emqx_dashboard_swagger:error_codes([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401));
|
||||||
response_schema(404) ->
|
response_schema(404) ->
|
||||||
|
@ -227,24 +238,25 @@ on_backend_update(Backend, Config, Fun) ->
|
||||||
Result = valid_config(Backend, Config, Fun),
|
Result = valid_config(Backend, Config, Fun),
|
||||||
handle_backend_update_result(Result, Config).
|
handle_backend_update_result(Result, Config).
|
||||||
|
|
||||||
valid_config(Backend, Config, Fun) ->
|
valid_config(Backend, #{<<"backend">> := Backend} = Config, Fun) ->
|
||||||
case maps:get(<<"backend">>, Config, undefined) of
|
|
||||||
Backend ->
|
|
||||||
Fun(Backend, Config);
|
Fun(Backend, Config);
|
||||||
_ ->
|
valid_config(_, _, _) ->
|
||||||
{error, invalid_config}
|
{error, invalid_config}.
|
||||||
end.
|
|
||||||
|
|
||||||
handle_backend_update_result({ok, _}, Config) ->
|
handle_backend_update_result({ok, #{backend := saml} = State}, _Config) ->
|
||||||
|
{200, to_json(maps:without([idp_meta, sp], State))};
|
||||||
|
handle_backend_update_result({ok, _State}, Config) ->
|
||||||
{200, to_json(Config)};
|
{200, to_json(Config)};
|
||||||
handle_backend_update_result(ok, _) ->
|
handle_backend_update_result(ok, _) ->
|
||||||
204;
|
204;
|
||||||
handle_backend_update_result({error, not_exists}, _) ->
|
handle_backend_update_result({error, not_exists}, _) ->
|
||||||
{404, ?BACKEND_NOT_FOUND, <<"Backend not found">>};
|
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||||
handle_backend_update_result({error, already_exists}, _) ->
|
handle_backend_update_result({error, already_exists}, _) ->
|
||||||
{400, ?BAD_REQUEST, <<"Backend already exists">>};
|
{400, #{code => ?BAD_REQUEST, message => <<"Backend already exists">>}};
|
||||||
|
handle_backend_update_result({error, failed_to_load_metadata}, _) ->
|
||||||
|
{400, #{code => ?BAD_REQUEST, message => <<"Failed to load metadata">>}};
|
||||||
handle_backend_update_result({error, Reason}, _) ->
|
handle_backend_update_result({error, Reason}, _) ->
|
||||||
{400, ?BAD_REQUEST, Reason}.
|
{400, #{code => ?BAD_REQUEST, message => Reason}}.
|
||||||
|
|
||||||
to_json(Data) ->
|
to_json(Data) ->
|
||||||
emqx_utils_maps:jsonable_map(
|
emqx_utils_maps:jsonable_map(
|
||||||
|
@ -253,3 +265,11 @@ to_json(Data) ->
|
||||||
{K, emqx_utils_maps:binary_string(V)}
|
{K, emqx_utils_maps:binary_string(V)}
|
||||||
end
|
end
|
||||||
).
|
).
|
||||||
|
|
||||||
|
login_reply(Role, Token) ->
|
||||||
|
#{
|
||||||
|
role => Role,
|
||||||
|
token => Token,
|
||||||
|
version => iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
||||||
|
license => #{edition => emqx_release:edition()}
|
||||||
|
}.
|
||||||
|
|
|
@ -106,26 +106,22 @@ ensure_bind_password(Config) ->
|
||||||
Config#{bind_password => <<"${password}">>}.
|
Config#{bind_password => <<"${password}">>}.
|
||||||
|
|
||||||
adjust_ldap_fields(Fields) ->
|
adjust_ldap_fields(Fields) ->
|
||||||
adjust_ldap_fields(Fields, []).
|
lists:map(fun adjust_ldap_field/1, Fields).
|
||||||
|
|
||||||
adjust_ldap_fields([{filter, Meta} | T], Acc) ->
|
adjust_ldap_field({base_dn, Meta}) ->
|
||||||
adjust_ldap_fields(
|
{base_dn, maps:remove(example, Meta)};
|
||||||
T,
|
adjust_ldap_field({filter, Meta}) ->
|
||||||
[
|
Default = <<"(& (objectClass=person) (uid=${username}))">>,
|
||||||
{filter, Meta#{
|
{filter, Meta#{
|
||||||
default => <<"(objectClass=user)">>,
|
desc => ?DESC(filter),
|
||||||
example => <<"(objectClass=user)">>
|
default => Default,
|
||||||
}}
|
example => Default
|
||||||
| Acc
|
}};
|
||||||
]
|
adjust_ldap_field(Any) ->
|
||||||
);
|
Any.
|
||||||
adjust_ldap_fields([Any | T], Acc) ->
|
|
||||||
adjust_ldap_fields(T, [Any | Acc]);
|
|
||||||
adjust_ldap_fields([], Acc) ->
|
|
||||||
lists:reverse(Acc).
|
|
||||||
|
|
||||||
login(
|
login(
|
||||||
#{<<"username">> := Username} = Req,
|
#{body := #{<<"username">> := Username} = Sign} = _Req,
|
||||||
#{
|
#{
|
||||||
query_timeout := Timeout,
|
query_timeout := Timeout,
|
||||||
resource_id := ResourceId
|
resource_id := ResourceId
|
||||||
|
@ -134,7 +130,7 @@ login(
|
||||||
case
|
case
|
||||||
emqx_resource:simple_sync_query(
|
emqx_resource:simple_sync_query(
|
||||||
ResourceId,
|
ResourceId,
|
||||||
{query, Req, [], Timeout}
|
{query, Sign, [], Timeout}
|
||||||
)
|
)
|
||||||
of
|
of
|
||||||
{ok, []} ->
|
{ok, []} ->
|
||||||
|
@ -143,7 +139,7 @@ login(
|
||||||
case
|
case
|
||||||
emqx_resource:simple_sync_query(
|
emqx_resource:simple_sync_query(
|
||||||
ResourceId,
|
ResourceId,
|
||||||
{bind, Req}
|
{bind, Sign}
|
||||||
)
|
)
|
||||||
of
|
of
|
||||||
ok ->
|
ok ->
|
||||||
|
|
|
@ -0,0 +1,240 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_dashboard_sso_saml).
|
||||||
|
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
-include_lib("esaml/include/esaml.hrl").
|
||||||
|
|
||||||
|
-behaviour(emqx_dashboard_sso).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
hocon_ref/0,
|
||||||
|
login_ref/0,
|
||||||
|
fields/1,
|
||||||
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% emqx_dashboard_sso callbacks
|
||||||
|
-export([
|
||||||
|
create/1,
|
||||||
|
update/2,
|
||||||
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([login/2, callback/2]).
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, do_create/1}).
|
||||||
|
|
||||||
|
-define(DIR, <<"saml_sp_certs">>).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Hocon Schema
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
hocon_ref() ->
|
||||||
|
hoconsc:ref(?MODULE, saml).
|
||||||
|
|
||||||
|
login_ref() ->
|
||||||
|
hoconsc:ref(?MODULE, login).
|
||||||
|
|
||||||
|
fields(saml) ->
|
||||||
|
emqx_dashboard_sso_schema:common_backend_schema([saml]) ++
|
||||||
|
[
|
||||||
|
{dashboard_addr, fun dashboard_addr/1},
|
||||||
|
{idp_metadata_url, fun idp_metadata_url/1},
|
||||||
|
{sp_sign_request, fun sp_sign_request/1},
|
||||||
|
{sp_public_key, fun sp_public_key/1},
|
||||||
|
{sp_private_key, fun sp_private_key/1}
|
||||||
|
];
|
||||||
|
fields(login) ->
|
||||||
|
[
|
||||||
|
emqx_dashboard_sso_schema:backend_schema([saml])
|
||||||
|
].
|
||||||
|
|
||||||
|
dashboard_addr(type) -> binary();
|
||||||
|
%% without any path
|
||||||
|
dashboard_addr(desc) -> ?DESC(dashboard_addr);
|
||||||
|
dashboard_addr(default) -> <<"https://127.0.0.1:18083">>;
|
||||||
|
dashboard_addr(_) -> undefined.
|
||||||
|
|
||||||
|
%% TOOD: support raw xml metadata in hocon (maybe?🤔)
|
||||||
|
idp_metadata_url(type) -> binary();
|
||||||
|
idp_metadata_url(desc) -> ?DESC(idp_metadata_url);
|
||||||
|
idp_metadata_url(default) -> <<"https://idp.example.com">>;
|
||||||
|
idp_metadata_url(_) -> undefined.
|
||||||
|
|
||||||
|
sp_sign_request(type) -> boolean();
|
||||||
|
sp_sign_request(desc) -> ?DESC(sign_request);
|
||||||
|
sp_sign_request(default) -> false;
|
||||||
|
sp_sign_request(_) -> undefined.
|
||||||
|
|
||||||
|
sp_public_key(type) -> binary();
|
||||||
|
sp_public_key(desc) -> ?DESC(sp_public_key);
|
||||||
|
sp_public_key(default) -> <<"Pub Key">>;
|
||||||
|
sp_public_key(_) -> undefined.
|
||||||
|
|
||||||
|
sp_private_key(type) -> binary();
|
||||||
|
sp_private_key(desc) -> ?DESC(sp_private_key);
|
||||||
|
sp_private_key(required) -> false;
|
||||||
|
sp_private_key(format) -> <<"password">>;
|
||||||
|
sp_private_key(sensitive) -> true;
|
||||||
|
sp_private_key(_) -> undefined.
|
||||||
|
|
||||||
|
desc(saml) ->
|
||||||
|
"saml";
|
||||||
|
desc(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
create(#{sp_sign_request := true} = Config) ->
|
||||||
|
try
|
||||||
|
do_create(ensure_cert_and_key(Config))
|
||||||
|
catch
|
||||||
|
Kind:Error ->
|
||||||
|
Msg = failed_to_ensure_cert_and_key,
|
||||||
|
?SLOG(error, #{msg => Msg, kind => Kind, error => Error}),
|
||||||
|
{error, Msg}
|
||||||
|
end;
|
||||||
|
create(#{sp_sign_request := false} = Config) ->
|
||||||
|
do_create(Config#{key => undefined, certificate => undefined}).
|
||||||
|
|
||||||
|
do_create(
|
||||||
|
#{
|
||||||
|
dashboard_addr := DashboardAddr,
|
||||||
|
idp_metadata_url := IDPMetadataURL,
|
||||||
|
sp_sign_request := SpSignRequest,
|
||||||
|
sp_private_key := KeyPath,
|
||||||
|
sp_public_key := CertPath
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
|
{ok, _} = application:ensure_all_started(esaml),
|
||||||
|
BaseURL = binary_to_list(DashboardAddr) ++ "/api/v5",
|
||||||
|
SP = esaml_sp:setup(#esaml_sp{
|
||||||
|
key = maybe_load_cert_or_key(KeyPath, fun esaml_util:load_private_key/1),
|
||||||
|
certificate = maybe_load_cert_or_key(CertPath, fun esaml_util:load_certificate/1),
|
||||||
|
sp_sign_requests = SpSignRequest,
|
||||||
|
trusted_fingerprints = [],
|
||||||
|
consume_uri = BaseURL ++ "/sso/saml/acs",
|
||||||
|
metadata_uri = BaseURL ++ "/sso/saml/metadata",
|
||||||
|
%% TODO: support conf org and contact
|
||||||
|
org = #esaml_org{
|
||||||
|
name = "EMQX",
|
||||||
|
displayname = "EMQX Dashboard",
|
||||||
|
url = DashboardAddr
|
||||||
|
},
|
||||||
|
tech = #esaml_contact{
|
||||||
|
name = "EMQX",
|
||||||
|
email = "contact@emqx.io"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
try
|
||||||
|
IdpMeta = esaml_util:load_metadata(binary_to_list(IDPMetadataURL)),
|
||||||
|
State = Config,
|
||||||
|
{ok, State#{idp_meta => IdpMeta, sp => SP}}
|
||||||
|
catch
|
||||||
|
Kind:Error ->
|
||||||
|
Reason = failed_to_load_metadata,
|
||||||
|
?SLOG(error, #{msg => Reason, kind => Kind, error => Error}),
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
update(Config0, State) ->
|
||||||
|
destroy(State),
|
||||||
|
create(Config0).
|
||||||
|
|
||||||
|
destroy(_State) ->
|
||||||
|
_ = file:del_dir_r(emqx_tls_lib:pem_dir(?DIR)),
|
||||||
|
_ = application:stop(esaml),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
login(
|
||||||
|
#{headers := Headers} = _Req,
|
||||||
|
#{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State
|
||||||
|
) ->
|
||||||
|
SignedXml = esaml_sp:generate_authn_request(IDP, SP),
|
||||||
|
Target = esaml_binding:encode_http_redirect(IDP, SignedXml, <<>>),
|
||||||
|
RespHeaders = #{<<"Cache-Control">> => <<"no-cache">>, <<"Pragma">> => <<"no-cache">>},
|
||||||
|
Redirect =
|
||||||
|
case is_msie(Headers) of
|
||||||
|
true ->
|
||||||
|
Html = esaml_binding:encode_http_post(IDP, SignedXml, <<>>),
|
||||||
|
{200, RespHeaders, Html};
|
||||||
|
false ->
|
||||||
|
RespHeaders1 = RespHeaders#{<<"Location">> => Target},
|
||||||
|
{302, RespHeaders1, <<"Redirecting...">>}
|
||||||
|
end,
|
||||||
|
{redirect, Redirect}.
|
||||||
|
|
||||||
|
callback(_Req = #{body := Body}, #{sp := SP} = _State) ->
|
||||||
|
case do_validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Body) of
|
||||||
|
{ok, Assertion, _RelayState} ->
|
||||||
|
Subject = Assertion#esaml_assertion.subject,
|
||||||
|
Username = iolist_to_binary(Subject#esaml_subject.name),
|
||||||
|
ensure_user_exists(Username);
|
||||||
|
{error, Reason0} ->
|
||||||
|
Reason = [
|
||||||
|
"Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0])
|
||||||
|
],
|
||||||
|
{error, iolist_to_binary(Reason)}
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_validate_assertion(SP, DuplicateFun, Body) ->
|
||||||
|
PostVals = cow_qs:parse_qs(Body),
|
||||||
|
SAMLEncoding = proplists:get_value(<<"SAMLEncoding">>, PostVals),
|
||||||
|
SAMLResponse = proplists:get_value(<<"SAMLResponse">>, PostVals),
|
||||||
|
RelayState = proplists:get_value(<<"RelayState">>, PostVals),
|
||||||
|
case (catch esaml_binding:decode_response(SAMLEncoding, SAMLResponse)) of
|
||||||
|
{'EXIT', Reason} ->
|
||||||
|
{error, {bad_decode, Reason}};
|
||||||
|
Xml ->
|
||||||
|
case esaml_sp:validate_assertion(Xml, DuplicateFun, SP) of
|
||||||
|
{ok, A} -> {ok, A, RelayState};
|
||||||
|
{error, E} -> {error, E}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ensure_cert_and_key(#{sp_public_key := Cert, sp_private_key := Key} = Config) ->
|
||||||
|
case
|
||||||
|
emqx_tls_lib:ensure_ssl_files(
|
||||||
|
?DIR, #{enable => ture, certfile => Cert, keyfile => Key}, #{}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, #{certfile := CertPath, keyfile := KeyPath} = _NSSL} ->
|
||||||
|
Config#{sp_public_key => CertPath, sp_private_key => KeyPath};
|
||||||
|
{error, #{which_options := KeyPath}} ->
|
||||||
|
error({missing_key, lists:flatten(KeyPath)})
|
||||||
|
end.
|
||||||
|
|
||||||
|
maybe_load_cert_or_key(undefined, _) ->
|
||||||
|
undefined;
|
||||||
|
maybe_load_cert_or_key(Path, Func) ->
|
||||||
|
Func(Path).
|
||||||
|
|
||||||
|
is_msie(Headers) ->
|
||||||
|
UA = maps:get(<<"user-agent">>, Headers, <<"">>),
|
||||||
|
not (binary:match(UA, <<"MSIE">>) =:= nomatch).
|
||||||
|
|
||||||
|
%% TODO: unify with emqx_dashboard_sso_manager:ensure_user_exists/1
|
||||||
|
ensure_user_exists(Username) ->
|
||||||
|
case emqx_dashboard_admin:lookup_user(saml, Username) of
|
||||||
|
[User] ->
|
||||||
|
emqx_dashboard_token:sign(User, <<>>);
|
||||||
|
[] ->
|
||||||
|
case emqx_dashboard_admin:add_sso_user(saml, Username, ?ROLE_VIEWER, <<>>) of
|
||||||
|
{ok, _} ->
|
||||||
|
ensure_user_exists(Username);
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end
|
||||||
|
end.
|
|
@ -0,0 +1,132 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_dashboard_sso_saml_api).
|
||||||
|
|
||||||
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-import(hoconsc, [
|
||||||
|
mk/2,
|
||||||
|
array/1,
|
||||||
|
enum/1,
|
||||||
|
ref/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-import(emqx_dashboard_sso, [provider/1]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
api_spec/0,
|
||||||
|
paths/0,
|
||||||
|
schema/1,
|
||||||
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
sp_saml_metadata/2,
|
||||||
|
sp_saml_callback/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(REDIRECT, 'REDIRECT').
|
||||||
|
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
||||||
|
-define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND').
|
||||||
|
-define(TAGS, <<"Dashboard Single Sign-On">>).
|
||||||
|
|
||||||
|
namespace() -> "dashboard_sso".
|
||||||
|
|
||||||
|
api_spec() ->
|
||||||
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false, translate_body => false}).
|
||||||
|
|
||||||
|
paths() ->
|
||||||
|
[
|
||||||
|
"/sso/saml/acs",
|
||||||
|
"/sso/saml/metadata"
|
||||||
|
].
|
||||||
|
|
||||||
|
%% Handles HTTP-POST bound assertions coming back from the IDP.
|
||||||
|
schema("/sso/saml/acs") ->
|
||||||
|
#{
|
||||||
|
'operationId' => sp_saml_callback,
|
||||||
|
post => #{
|
||||||
|
tags => [?TAGS],
|
||||||
|
desc => ?DESC(saml_sso_acs),
|
||||||
|
%% 'requestbody' => urlencoded_request_body(),
|
||||||
|
responses => #{
|
||||||
|
302 => response_schema(302),
|
||||||
|
401 => response_schema(401),
|
||||||
|
404 => response_schema(404)
|
||||||
|
},
|
||||||
|
security => []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
schema("/sso/saml/metadata") ->
|
||||||
|
#{
|
||||||
|
'operationId' => sp_saml_metadata,
|
||||||
|
get => #{
|
||||||
|
tags => [?TAGS],
|
||||||
|
desc => ?DESC(sp_saml_metadata),
|
||||||
|
'requestbody' => saml_metadata_response(),
|
||||||
|
responses => #{
|
||||||
|
200 => emqx_dashboard_api:fields([token, version, license]),
|
||||||
|
404 => response_schema(404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% API
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
sp_saml_metadata(get, _Req) ->
|
||||||
|
case emqx_dashboard_sso_manager:lookup_state(saml) of
|
||||||
|
undefined ->
|
||||||
|
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||||
|
#{sp := SP} = _State ->
|
||||||
|
SignedXml = esaml_sp:generate_metadata(SP),
|
||||||
|
Metadata = xmerl:export([SignedXml], xmerl_xml),
|
||||||
|
{200, #{<<"Content-Type">> => <<"text/xml">>}, erlang:iolist_to_binary(Metadata)}
|
||||||
|
end.
|
||||||
|
|
||||||
|
sp_saml_callback(post, Req) ->
|
||||||
|
case emqx_dashboard_sso_manager:lookup_state(saml) of
|
||||||
|
undefined ->
|
||||||
|
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||||
|
State ->
|
||||||
|
case (provider(saml)):callback(Req, State) of
|
||||||
|
{ok, Role, Token} ->
|
||||||
|
{200, emqx_dashboard_sso_api:login_reply(Role, Token)};
|
||||||
|
{error, Reason} ->
|
||||||
|
?SLOG(info, #{
|
||||||
|
msg => "dashboard_saml_sso_login_failed",
|
||||||
|
request => Req,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
{403, #{code => <<"UNAUTHORIZED">>, message => Reason}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% internal
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
response_schema(302) ->
|
||||||
|
emqx_dashboard_swagger:error_codes([?REDIRECT], ?DESC(redirect));
|
||||||
|
response_schema(401) ->
|
||||||
|
emqx_dashboard_swagger:error_codes([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401));
|
||||||
|
response_schema(404) ->
|
||||||
|
emqx_dashboard_swagger:error_codes([?BACKEND_NOT_FOUND], ?DESC(backend_not_found)).
|
||||||
|
|
||||||
|
saml_metadata_response() ->
|
||||||
|
#{
|
||||||
|
'content' => #{
|
||||||
|
'application/xml' => #{
|
||||||
|
schema => #{
|
||||||
|
type => <<"string">>,
|
||||||
|
format => <<"binary">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
|
@ -101,7 +101,7 @@ t_first_login(_) ->
|
||||||
},
|
},
|
||||||
%% this API is authorization-free
|
%% this API is authorization-free
|
||||||
{ok, 200, Result} = request_without_authorization(post, Path, Req),
|
{ok, 200, Result} = request_without_authorization(post, Path, Req),
|
||||||
?assertMatch(#{license := _, token := _}, decode_json(Result)),
|
?assertMatch(#{license := _, token := _, role := ?ROLE_VIEWER}, decode_json(Result)),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
[#?ADMIN{username = ?SSO_USERNAME(ldap, ?LDAP_USER)}],
|
[#?ADMIN{username = ?SSO_USERNAME(ldap, ?LDAP_USER)}],
|
||||||
emqx_dashboard_admin:lookup_user(ldap, ?LDAP_USER)
|
emqx_dashboard_admin:lookup_user(ldap, ?LDAP_USER)
|
||||||
|
|
|
@ -252,7 +252,7 @@ describe_plugins(Name) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
install_plugin(FilePath) ->
|
install_plugin(FilePath) ->
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
{ok, _Role, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]),
|
Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]),
|
||||||
case
|
case
|
||||||
emqx_mgmt_api_test_util:upload_request(
|
emqx_mgmt_api_test_util:upload_request(
|
||||||
|
|
|
@ -142,8 +142,8 @@ do(Args) ->
|
||||||
["eval" | ListOfArgs] ->
|
["eval" | ListOfArgs] ->
|
||||||
Parsed = parse_eval_args(ListOfArgs),
|
Parsed = parse_eval_args(ListOfArgs),
|
||||||
% and evaluate it on the remote node
|
% and evaluate it on the remote node
|
||||||
case rpc:call(TargetNode, erl_eval, exprs, [Parsed, [] ]) of
|
case rpc:call(TargetNode, emqx_ctl, eval_erl, [Parsed]) of
|
||||||
{value, Value, _} ->
|
{ok, Value} ->
|
||||||
io:format("~p~n",[Value]);
|
io:format("~p~n",[Value]);
|
||||||
{badrpc, Reason} ->
|
{badrpc, Reason} ->
|
||||||
io:format("RPC to ~p failed: ~p~n", [TargetNode, Reason]),
|
io:format("RPC to ~p failed: ~p~n", [TargetNode, Reason]),
|
||||||
|
|
|
@ -14,8 +14,8 @@ type: application
|
||||||
|
|
||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
version: 5.2.1
|
version: 5.3.0-alpha.1
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application.
|
# incremented each time you make changes to the application.
|
||||||
appVersion: 5.2.1
|
appVersion: 5.3.0-alpha.1
|
||||||
|
|
24
dev
24
dev
|
@ -37,6 +37,7 @@ COMMANDS:
|
||||||
ctl: Equivalent to 'emqx ctl'.
|
ctl: Equivalent to 'emqx ctl'.
|
||||||
ctl command arguments should be passed after '--'
|
ctl command arguments should be passed after '--'
|
||||||
e.g. $0 ctl -- help
|
e.g. $0 ctl -- help
|
||||||
|
eval: Evaluate an Erlang expression
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
-p|--profile: emqx | emqx-enterprise, defaults to 'PROFILE' env.
|
-p|--profile: emqx | emqx-enterprise, defaults to 'PROFILE' env.
|
||||||
|
@ -83,6 +84,10 @@ case "${1:-novalue}" in
|
||||||
COMMAND='ctl'
|
COMMAND='ctl'
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
eval)
|
||||||
|
COMMAND='eval'
|
||||||
|
shift
|
||||||
|
;;
|
||||||
help)
|
help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -425,14 +430,22 @@ remsh() {
|
||||||
$EPMD_ARGS
|
$EPMD_ARGS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# evaluate erlang expression in remsh node
|
||||||
|
eval_remsh_erl() {
|
||||||
|
local tmpnode erl_code
|
||||||
|
tmpnode="$(gen_tmp_node_name)"
|
||||||
|
erl_code="$1"
|
||||||
|
# shellcheck disable=SC2086 # need to expand EMQD_ARGS
|
||||||
|
erl -name "$tmpnode" -setcookie "$COOKIE" -hidden -noshell $EPMD_ARGS -eval "$erl_code" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
ctl() {
|
ctl() {
|
||||||
if [ -z "${PASSTHROUGH_ARGS:-}" ]; then
|
if [ -z "${PASSTHROUGH_ARGS:-}" ]; then
|
||||||
logerr "Need at least one argument for ctl command"
|
logerr "Need at least one argument for ctl command"
|
||||||
logerr "e.g. $0 ctl -- help"
|
logerr "e.g. $0 ctl -- help"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
local tmpnode args rpc_code output result
|
local args rpc_code output result
|
||||||
tmpnode="$(gen_tmp_node_name)"
|
|
||||||
args="$(make_erlang_args "${PASSTHROUGH_ARGS[@]}")"
|
args="$(make_erlang_args "${PASSTHROUGH_ARGS[@]}")"
|
||||||
rpc_code="
|
rpc_code="
|
||||||
case rpc:call('$EMQX_NODE_NAME', emqx_ctl, run_command, [[$args]]) of
|
case rpc:call('$EMQX_NODE_NAME', emqx_ctl, run_command, [[$args]]) of
|
||||||
|
@ -443,8 +456,7 @@ ctl() {
|
||||||
init:stop(1)
|
init:stop(1)
|
||||||
end"
|
end"
|
||||||
set +e
|
set +e
|
||||||
# shellcheck disable=SC2086
|
output="$(eval_remsh_erl "$rpc_code")"
|
||||||
output="$(erl -name "$tmpnode" -setcookie "$COOKIE" -hidden -noshell $EPMD_ARGS -eval "$rpc_code" 2>&1)"
|
|
||||||
result=$?
|
result=$?
|
||||||
if [ $result -eq 0 ]; then
|
if [ $result -eq 0 ]; then
|
||||||
echo -e "$output"
|
echo -e "$output"
|
||||||
|
@ -464,4 +476,8 @@ case "$COMMAND" in
|
||||||
ctl)
|
ctl)
|
||||||
ctl
|
ctl
|
||||||
;;
|
;;
|
||||||
|
eval)
|
||||||
|
PASSTHROUGH_ARGS=('eval_erl' "${PASSTHROUGH_ARGS[@]}")
|
||||||
|
ctl
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
@ -30,6 +30,15 @@ delete_backend.desc:
|
||||||
delete_backend.label:
|
delete_backend.label:
|
||||||
"""Delete Backend"""
|
"""Delete Backend"""
|
||||||
|
|
||||||
|
saml_sso_acs.desc:
|
||||||
|
"""SAML SSO ACS URL"""
|
||||||
|
|
||||||
|
sp_saml_metadata.desc:
|
||||||
|
"""SP SAML Metadata"""
|
||||||
|
|
||||||
|
redirect.desc:
|
||||||
|
"""Redirect to IDP SSO login page"""
|
||||||
|
|
||||||
login_failed401.desc:
|
login_failed401.desc:
|
||||||
"""Login failed. Bad username or password"""
|
"""Login failed. Bad username or password"""
|
||||||
|
|
||||||
|
|
|
@ -8,4 +8,11 @@ query_timeout.desc:
|
||||||
|
|
||||||
query_timeout.label:
|
query_timeout.label:
|
||||||
"""Query Timeout"""
|
"""Query Timeout"""
|
||||||
|
|
||||||
|
filter.desc:
|
||||||
|
"""The filter for matching users in LDAP is by default `(&(objectClass=person)(uid=${username}))`. For Active Directory, it should be set to `(&(objectClass=user)(sAMAccountName=${username}))` by default. Please refer to [LDAP Filters](https://ldap.com/ldap-filters/) for more details."""
|
||||||
|
|
||||||
|
filter.label:
|
||||||
|
"""Filter"""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
emqx_dashboard_sso_saml {
|
||||||
|
|
||||||
|
dashboard_addr.desc:
|
||||||
|
"""The address of the EMQX Dashboard."""
|
||||||
|
dashboard_addr.label:
|
||||||
|
"""Dashboard Address"""
|
||||||
|
|
||||||
|
idp_metadata_url.desc:
|
||||||
|
"""The URL of the IdP metadata."""
|
||||||
|
idp_metadata_url.label:
|
||||||
|
"""IdP Metadata URL"""
|
||||||
|
|
||||||
|
sign_request.desc:
|
||||||
|
"""Whether to sign the SAML request."""
|
||||||
|
sign_request.label:
|
||||||
|
"""Sign SAML Request"""
|
||||||
|
|
||||||
|
sp_public_key.desc:
|
||||||
|
"""The public key of the SP."""
|
||||||
|
sp_public_key.label:
|
||||||
|
"""SP Public Key"""
|
||||||
|
|
||||||
|
sp_private_key.desc:
|
||||||
|
"""The private key of the SP."""
|
||||||
|
sp_private_key.label:
|
||||||
|
"""SP Private Key"""
|
||||||
|
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ options:
|
||||||
-b|--base: Specify the current release base branch, can be one of
|
-b|--base: Specify the current release base branch, can be one of
|
||||||
release-51
|
release-51
|
||||||
release-52
|
release-52
|
||||||
|
release-53
|
||||||
NOTE: this option should be used when --dryrun.
|
NOTE: this option should be used when --dryrun.
|
||||||
|
|
||||||
--dryrun: Do not actually create the git tag.
|
--dryrun: Do not actually create the git tag.
|
||||||
|
@ -33,15 +34,10 @@ options:
|
||||||
If this option is absent, the tag found by git describe will be used
|
If this option is absent, the tag found by git describe will be used
|
||||||
|
|
||||||
|
|
||||||
For 5.1 series the current working branch must be 'release-51'
|
For 5.X series the current working branch must be 'release-5X'
|
||||||
--.--[ master ]---------------------------.-----------.---
|
--.--[ master ]---------------------------.-----------.---
|
||||||
\\ /
|
\\ /
|
||||||
\`---[release-51]----(v5.1.1 | e5.1.1)
|
\`---[release-53]----(v5.3.1 | e5.3.1)
|
||||||
|
|
||||||
For 5.2 series the current working branch must be 'release-52'
|
|
||||||
--.--[ master ]---------------------------.-----------.---
|
|
||||||
\\ /
|
|
||||||
\`---[release-52]----(v5.2.1 | e5.2.1)
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +130,12 @@ rel_branch() {
|
||||||
e5.2.*)
|
e5.2.*)
|
||||||
echo 'release-52'
|
echo 'release-52'
|
||||||
;;
|
;;
|
||||||
|
v5.3.*)
|
||||||
|
echo 'release-53'
|
||||||
|
;;
|
||||||
|
e5.3.*)
|
||||||
|
echo 'release-53'
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
logerr "Unsupported version tag $TAG"
|
logerr "Unsupported version tag $TAG"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
@ -5,7 +5,7 @@ set -euo pipefail
|
||||||
# ensure dir
|
# ensure dir
|
||||||
cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/../.."
|
cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/../.."
|
||||||
|
|
||||||
BASE_BRANCHES=( 'release-52' 'release-51' 'master' )
|
BASE_BRANCHES=( 'release-53' 'release-52' 'release-51' 'master' )
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
@ -18,9 +18,10 @@ options:
|
||||||
It tries to merge (by default with --ff-only option)
|
It tries to merge (by default with --ff-only option)
|
||||||
upstreams branches for the current working branch.
|
upstreams branches for the current working branch.
|
||||||
The uppstream branch of the current branch are as below:
|
The uppstream branch of the current branch are as below:
|
||||||
|
* release-53: [] # no upstream for 5.3 opensource edition
|
||||||
* release-52: [] # no upstream for 5.2 opensource edition
|
* release-52: [] # no upstream for 5.2 opensource edition
|
||||||
* release-51: [] # no upstream for 5.1 opensource edition
|
* release-51: [] # no upstream for 5.1 opensource edition
|
||||||
* master: [release-52] # sync release-52 to master
|
* master: [release-53] # sync release-53 to master
|
||||||
|
|
||||||
-b|--base:
|
-b|--base:
|
||||||
The base branch of current working branch if currently is not
|
The base branch of current working branch if currently is not
|
||||||
|
@ -152,6 +153,9 @@ remote_refs() {
|
||||||
upstream_branches() {
|
upstream_branches() {
|
||||||
local base="$1"
|
local base="$1"
|
||||||
case "$base" in
|
case "$base" in
|
||||||
|
release-53)
|
||||||
|
remote_ref "$base"
|
||||||
|
;;
|
||||||
release-52)
|
release-52)
|
||||||
remote_ref "$base"
|
remote_ref "$base"
|
||||||
;;
|
;;
|
||||||
|
@ -159,7 +163,7 @@ upstream_branches() {
|
||||||
remote_ref "$base"
|
remote_ref "$base"
|
||||||
;;
|
;;
|
||||||
master)
|
master)
|
||||||
remote_refs "$base" 'release-52'
|
remote_refs "$base" 'release-53'
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
Unrecognized tag: refs/tags/v5.2.0-foobar.1
|
Unrecognized tag: refs/tags/v5.2.0-foobar.1
|
||||||
>>>= 1
|
>>>= 1
|
||||||
|
|
||||||
|
./parse-git-ref.sh refs/tags/v5.3.0-foobar.1
|
||||||
|
>>>2
|
||||||
|
Unrecognized tag: refs/tags/v5.3.0-foobar.1
|
||||||
|
>>>= 1
|
||||||
|
|
||||||
./parse-git-ref.sh v5.2.0
|
./parse-git-ref.sh v5.2.0
|
||||||
>>>2
|
>>>2
|
||||||
Unrecognized git ref: v5.2.0
|
Unrecognized git ref: v5.2.0
|
||||||
|
@ -18,6 +23,21 @@ Unrecognized git ref: v5.2.0-1
|
||||||
Unrecognized git ref: e5.2.0-1
|
Unrecognized git ref: e5.2.0-1
|
||||||
>>>= 1
|
>>>= 1
|
||||||
|
|
||||||
|
./parse-git-ref.sh v5.3.0
|
||||||
|
>>>2
|
||||||
|
Unrecognized git ref: v5.3.0
|
||||||
|
>>>= 1
|
||||||
|
|
||||||
|
./parse-git-ref.sh v5.3.0-1
|
||||||
|
>>>2
|
||||||
|
Unrecognized git ref: v5.3.0-1
|
||||||
|
>>>= 1
|
||||||
|
|
||||||
|
./parse-git-ref.sh e5.3.0-1
|
||||||
|
>>>2
|
||||||
|
Unrecognized git ref: e5.3.0-1
|
||||||
|
>>>= 1
|
||||||
|
|
||||||
./parse-git-ref.sh refs/tags/v5.1.0
|
./parse-git-ref.sh refs/tags/v5.1.0
|
||||||
>>>
|
>>>
|
||||||
{"profile": "emqx", "release": true, "latest": false}
|
{"profile": "emqx", "release": true, "latest": false}
|
||||||
|
@ -33,6 +53,11 @@ Unrecognized git ref: e5.2.0-1
|
||||||
{"profile": "emqx", "release": true, "latest": false}
|
{"profile": "emqx", "release": true, "latest": false}
|
||||||
>>>= 0
|
>>>= 0
|
||||||
|
|
||||||
|
./parse-git-ref.sh refs/tags/v5.3.0-alpha.1
|
||||||
|
>>>
|
||||||
|
{"profile": "emqx", "release": true, "latest": false}
|
||||||
|
>>>= 0
|
||||||
|
|
||||||
./parse-git-ref.sh refs/tags/v5.2.0-alpha-1
|
./parse-git-ref.sh refs/tags/v5.2.0-alpha-1
|
||||||
>>>2
|
>>>2
|
||||||
Unrecognized tag: refs/tags/v5.2.0-alpha-1
|
Unrecognized tag: refs/tags/v5.2.0-alpha-1
|
||||||
|
@ -43,6 +68,11 @@ Unrecognized tag: refs/tags/v5.2.0-alpha-1
|
||||||
{"profile": "emqx", "release": true, "latest": false}
|
{"profile": "emqx", "release": true, "latest": false}
|
||||||
>>>= 0
|
>>>= 0
|
||||||
|
|
||||||
|
./parse-git-ref.sh refs/tags/v5.3.0-beta.1
|
||||||
|
>>>
|
||||||
|
{"profile": "emqx", "release": true, "latest": false}
|
||||||
|
>>>= 0
|
||||||
|
|
||||||
./parse-git-ref.sh refs/tags/v5.2.0-rc.1
|
./parse-git-ref.sh refs/tags/v5.2.0-rc.1
|
||||||
>>>
|
>>>
|
||||||
{"profile": "emqx", "release": true, "latest": false}
|
{"profile": "emqx", "release": true, "latest": false}
|
||||||
|
@ -63,16 +93,31 @@ Unrecognized tag: refs/tags/v5.2.0-alpha-1
|
||||||
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
||||||
>>>= 0
|
>>>= 0
|
||||||
|
|
||||||
|
./parse-git-ref.sh refs/tags/e5.3.0-alpha.1
|
||||||
|
>>>
|
||||||
|
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
||||||
|
>>>= 0
|
||||||
|
|
||||||
./parse-git-ref.sh refs/tags/e5.2.0-beta.1
|
./parse-git-ref.sh refs/tags/e5.2.0-beta.1
|
||||||
>>>
|
>>>
|
||||||
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
||||||
>>>= 0
|
>>>= 0
|
||||||
|
|
||||||
|
./parse-git-ref.sh refs/tags/e5.3.0-beta.1
|
||||||
|
>>>
|
||||||
|
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
||||||
|
>>>= 0
|
||||||
|
|
||||||
./parse-git-ref.sh refs/tags/e5.2.0-rc.1
|
./parse-git-ref.sh refs/tags/e5.2.0-rc.1
|
||||||
>>>
|
>>>
|
||||||
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
||||||
>>>= 0
|
>>>= 0
|
||||||
|
|
||||||
|
./parse-git-ref.sh refs/tags/e5.3.0-rc.1
|
||||||
|
>>>
|
||||||
|
{"profile": "emqx-enterprise", "release": true, "latest": false}
|
||||||
|
>>>= 0
|
||||||
|
|
||||||
./parse-git-ref.sh refs/tags/e5.1.99
|
./parse-git-ref.sh refs/tags/e5.1.99
|
||||||
>>>
|
>>>
|
||||||
{"profile": "emqx-enterprise", "release": true, "latest": true}
|
{"profile": "emqx-enterprise", "release": true, "latest": true}
|
||||||
|
@ -98,6 +143,11 @@ Unrecognized tag: refs/tags/v5.2.0-alpha-1
|
||||||
{"profile": "emqx-enterprise", "release": false, "latest": false}
|
{"profile": "emqx-enterprise", "release": false, "latest": false}
|
||||||
>>>= 0
|
>>>= 0
|
||||||
|
|
||||||
|
./parse-git-ref.sh refs/heads/release-53
|
||||||
|
>>>
|
||||||
|
{"profile": "emqx-enterprise", "release": false, "latest": false}
|
||||||
|
>>>= 0
|
||||||
|
|
||||||
./parse-git-ref.sh refs/heads/ci/foobar
|
./parse-git-ref.sh refs/heads/ci/foobar
|
||||||
>>>
|
>>>
|
||||||
{"profile": "emqx", "release": false, "latest": false}
|
{"profile": "emqx", "release": false, "latest": false}
|
||||||
|
|
|
@ -286,3 +286,5 @@ FormatType
|
||||||
RocketMQ
|
RocketMQ
|
||||||
Keyspace
|
Keyspace
|
||||||
OpenTSDB
|
OpenTSDB
|
||||||
|
saml
|
||||||
|
idp
|
||||||
|
|
Loading…
Reference in New Issue