Merge pull request #11670 from id/0925-sync-r53-to-master

sync r53 to master
This commit is contained in:
Zaiming (Stone) Shi 2023-09-25 12:22:27 +02:00 committed by GitHub
commit cedd90e89f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 723 additions and 127 deletions

6
.github/CODEOWNERS vendored
View File

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

View File

@ -13,6 +13,7 @@ on:
- 'master' - 'master'
- 'release-51' - 'release-51'
- 'release-52' - 'release-52'
- 'release-53'
- 'ci/**' - 'ci/**'
env: env:

View File

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

View File

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

View File

@ -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, [

View File

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

View File

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

View File

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

View File

@ -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">>}

View File

@ -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 => #{

View File

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

View File

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

View File

@ -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),

View File

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

View File

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

View File

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

View File

@ -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"}}
]}. ]}.

View File

@ -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() ->

View File

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

View File

@ -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"}}}
]}. ]}.

View File

@ -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, []},

View File

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

View File

@ -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()}
}.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]),

View File

@ -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
View File

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

View File

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

View File

@ -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"""
} }

View File

@ -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"""
}

View File

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

View File

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

View File

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

View File

@ -286,3 +286,5 @@ FormatType
RocketMQ RocketMQ
Keyspace Keyspace
OpenTSDB OpenTSDB
saml
idp