diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e42000489..8bb31ab71 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,9 +5,11 @@ /apps/emqx/ @emqx/emqx-review-board @lafirest /apps/emqx_authn/ @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_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_gateway/ @emqx/emqx-review-board @lafirest /apps/emqx_management/ @emqx/emqx-review-board @lafirest @sstrigler @@ -18,7 +20,7 @@ /apps/emqx_rule_engine/ @emqx/emqx-review-board @kjellwinblad /apps/emqx_slow_subs/ @emqx/emqx-review-board @lafirest /apps/emqx_statsd/ @emqx/emqx-review-board @JimMoen -/apps/emqx_durable_storage/ @emqx/emqx-review-board @ieQu1 @keynslug +/apps/emqx_durable_storage/ @emqx/emqx-review-board @ieQu1 @keynslug ## CI /deploy/ @emqx/emqx-review-board @Rory-Z diff --git a/.github/workflows/_push-entrypoint.yaml b/.github/workflows/_push-entrypoint.yaml index afdf2a050..4a9dbee24 100644 --- a/.github/workflows/_push-entrypoint.yaml +++ b/.github/workflows/_push-entrypoint.yaml @@ -13,6 +13,7 @@ on: - 'master' - 'release-51' - 'release-52' + - 'release-53' - 'ci/**' env: diff --git a/.github/workflows/build_packages_cron.yaml b/.github/workflows/build_packages_cron.yaml index a67ab81d2..d14e41ff6 100644 --- a/.github/workflows/build_packages_cron.yaml +++ b/.github/workflows/build_packages_cron.yaml @@ -21,8 +21,8 @@ jobs: matrix: profile: - ['emqx', 'master'] - - ['emqx-enterprise', 'release-51'] - ['emqx-enterprise', 'release-52'] + - ['emqx-enterprise', 'release-53'] otp: - 25.3.2-2 arch: diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index a406c00fb..9b6252efb 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -35,7 +35,7 @@ -define(EMQX_RELEASE_CE, "5.2.1"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.2.1"). +-define(EMQX_RELEASE_EE, "5.3.0-alpha.1"). %% The HTTP API version -define(EMQX_API_VERSION, "5.0"). diff --git a/apps/emqx/test/emqx_bpapi_static_checks.erl b/apps/emqx/test/emqx_bpapi_static_checks.erl index b44e564c7..6766912c0 100644 --- a/apps/emqx/test/emqx_bpapi_static_checks.erl +++ b/apps/emqx/test/emqx_bpapi_static_checks.erl @@ -48,7 +48,7 @@ %% Applications and modules we wish to ignore in the analysis: -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(FORCE_DELETED_MODULES, [ diff --git a/apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl b/apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl index 1dd5adab2..4450030e0 100644 --- a/apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl +++ b/apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl @@ -1260,7 +1260,7 @@ auth_header_() -> auth_header_(<<"admin">>, <<"public">>). 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)}. api_path(Parts) -> diff --git a/apps/emqx_ctl/src/emqx_ctl.erl b/apps/emqx_ctl/src/emqx_ctl.erl index 3aca1bb54..4947a9715 100644 --- a/apps/emqx_ctl/src/emqx_ctl.erl +++ b/apps/emqx_ctl/src/emqx_ctl.erl @@ -44,6 +44,10 @@ usage/2 ]). +-export([ + eval_erl/1 +]). + %% Exports mainly for test cases -export([ format/2, @@ -119,7 +123,7 @@ run_command(Cmd, Args) when is_atom(Cmd) -> Start = erlang:monotonic_time(), Result = case lookup_command(Cmd) of - [{Mod, Fun}] -> + {ok, {Mod, Fun}} -> try apply(Mod, Fun, [Args]) catch @@ -127,13 +131,15 @@ run_command(Cmd, Args) when is_atom(Cmd) -> ?LOG_ERROR(#{ msg => "ctl_command_crashed", stacktrace => Stacktrace, - reason => Reason + reason => Reason, + module => Mod, + function => Fun }), {error, Reason} end; - Error -> + {error, Reason} -> help(), - Error + {error, Reason} end, Duration = erlang:convert_time_unit(erlang:monotonic_time() - Start, native, millisecond), @@ -144,12 +150,22 @@ run_command(Cmd, Args) when is_atom(Cmd) -> ), 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) -> case is_initialized() of true -> case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of - [El] -> El; + [[{M, F}]] -> {ok, {M, F}}; [] -> {error, cmd_not_found} end; false -> @@ -319,7 +335,7 @@ audit_log(Level, From, Log) -> case lookup_command(audit) of {error, _} -> ignore; - [{Mod, Fun}] -> + {ok, {Mod, Fun}} -> try apply(Mod, Fun, [Level, From, Log]) 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(_, _) -> 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}. diff --git a/apps/emqx_ctl/test/emqx_ctl_SUITE.erl b/apps/emqx_ctl/test/emqx_ctl_SUITE.erl index c11a1d5cb..7e556137b 100644 --- a/apps/emqx_ctl/test/emqx_ctl_SUITE.erl +++ b/apps/emqx_ctl/test/emqx_ctl_SUITE.erl @@ -40,8 +40,8 @@ t_reg_unreg_command(_) -> fun(_CtlSrv) -> emqx_ctl:register_command(cmd1, {?MODULE, cmd1_fun}), emqx_ctl:register_command(cmd2, {?MODULE, cmd2_fun}), - ?assertEqual([{?MODULE, cmd1_fun}], emqx_ctl:lookup_command(cmd1)), - ?assertEqual([{?MODULE, cmd2_fun}], emqx_ctl:lookup_command(cmd2)), + ?assertEqual({?MODULE, cmd1_fun}, lookup_command(cmd1)), + ?assertEqual({?MODULE, cmd2_fun}, lookup_command(cmd2)), ?assertEqual( [{cmd1, ?MODULE, cmd1_fun}, {cmd2, ?MODULE, cmd2_fun}], emqx_ctl:get_commands() @@ -49,8 +49,8 @@ t_reg_unreg_command(_) -> emqx_ctl:unregister_command(cmd1), emqx_ctl:unregister_command(cmd2), ct:sleep(100), - ?assertEqual({error, cmd_not_found}, emqx_ctl:lookup_command(cmd1)), - ?assertEqual({error, cmd_not_found}, emqx_ctl:lookup_command(cmd2)), + ?assertEqual({error, cmd_not_found}, lookup_command(cmd1)), + ?assertEqual({error, cmd_not_found}, lookup_command(cmd2)), ?assertEqual([], emqx_ctl:get_commands()) end ). @@ -79,6 +79,12 @@ t_print(_) -> ?assertEqual("~!@#$%^&*()", emqx_ctl:print("~ts", [<<"~!@#$%^&*()">>])), 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(_) -> CmdParams1 = "emqx_cmd_1 param1 param2", CmdDescr1 = "emqx_cmd_1 is a test command means nothing", @@ -129,3 +135,9 @@ mock_print() -> unmock_print() -> meck:unload(emqx_ctl). + +lookup_command(Cmd) -> + case emqx_ctl:lookup_command(Cmd) of + {ok, {Mod, Fun}} -> {Mod, Fun}; + Error -> Error + end. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 029e35e3b..70d6accb4 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -77,7 +77,7 @@ schema("/login") -> summary => <<"Dashboard authentication">>, 'requestBody' => fields([username, password]), responses => #{ - 200 => fields([token, version, license]), + 200 => fields([role, token, version, license]), 401 => response_schema(401) }, security => [] @@ -219,14 +219,16 @@ login(post, #{body := Params}) -> Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), case emqx_dashboard_admin:sign_token(Username, Password) of - {ok, Token} -> + {ok, Role, Token} -> ?SLOG(info, #{msg => "dashboard_login_successful", username => Username}), Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), - {200, #{ - token => Token, - version => Version, - license => #{edition => emqx_release:edition()} - }}; + {200, + filter_result(#{ + role => Role, + token => Token, + version => Version, + license => #{edition => emqx_release:edition()} + })}; {error, R} -> ?SLOG(info, #{msg => "dashboard_login_failed", username => Username, reason => R}), {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>} diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index a86c30893..75e93fdd1 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -28,7 +28,7 @@ -export([error_codes/1, error_codes/2]). -export([file_schema/1]). -export([base_path/0]). --export([relative_uri/1]). +-export([relative_uri/1, get_relative_uri/1]). -export([compose_filters/2]). -export([ @@ -212,6 +212,12 @@ base_path() -> relative_uri(Uri) -> base_path() ++ Uri. +-spec get_relative_uri(uri_string:uri_string()) -> {ok, uri_string:uri_string()} | error. +get_relative_uri(<>) -> + {ok, Path}; +get_relative_uri(_Path) -> + error. + file_schema(FileName) -> #{ content => #{ diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index 38856e7c7..866da971b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -56,7 +56,7 @@ %%-------------------------------------------------------------------- %% jwt function -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) -> do_sign(User, Password). @@ -120,7 +120,7 @@ do_sign(#?ADMIN{username = Username} = User, Password) -> Role = emqx_dashboard_admin:role(User), JWTRec = format(Token, Username, Role, ExpTime), _ = mria:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]), - {ok, Token}. + {ok, Role, Token}. -spec do_verify(_, Token :: binary()) -> Result :: diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index 141044836..5d2e4f721 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -120,6 +120,7 @@ t_rest_api(_Config) -> ?assertEqual( [ filter_req(#{ + <<"backend">> => <<"local">>, <<"username">> => <<"admin">>, <<"description">> => <<"administrator">>, <<"role">> => ?ROLE_SUPERUSER @@ -269,7 +270,7 @@ auth_header_() -> auth_header_(<<"admin">>, <<"public">>). 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)}. api_path(Parts) -> @@ -286,6 +287,6 @@ filter_req(Req) -> -else. filter_req(Req) -> - maps:without([role, <<"role">>], Req). + maps:without([role, <<"role">>, backend, <<"backend">>], Req). -endif. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl index c5ee02099..8a6e21fa3 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl @@ -174,15 +174,16 @@ t_clean_token(_) -> Password = <<"public_www1">>, NewPassword = <<"public_www2">>, {ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>), - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), - FakeReq = #{method => <<"GET">>}, + {ok, _, Token} = emqx_dashboard_admin:sign_token(Username, Password), + 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), %% change password {ok, _} = emqx_dashboard_admin:change_password(Username, Password, NewPassword), timer:sleep(5), {error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token), %% 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, _} = emqx_dashboard_admin:remove_user(Username), timer:sleep(5), diff --git a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl index cf022d65d..5a48ef72f 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl @@ -116,7 +116,7 @@ auth_header(Username) -> auth_header(Username, <<"public">>). 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)}. multipart_formdata_request(Url, Fields, Files) -> diff --git a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl index cb6a5a9fd..5323c019e 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl @@ -55,7 +55,7 @@ t_status(_Config) -> [binary, {active, false}, {packet, raw}] ), 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( Socket, "GET /status HTTP/1.1\r\n" diff --git a/apps/emqx_dashboard_rbac/README.md b/apps/emqx_dashboard_rbac/README.md index 9d854d29d..70c3a8c85 100644 --- a/apps/emqx_dashboard_rbac/README.md +++ b/apps/emqx_dashboard_rbac/README.md @@ -12,4 +12,4 @@ Please see our [contributing.md](../../CONTRIBUTING.md). ## License -See [APL](../../APL.txt). +EMQ Business Source License 1.1, refer to [LICENSE](BSL.txt). diff --git a/apps/emqx_dashboard_rbac/rebar.config b/apps/emqx_dashboard_rbac/rebar.config index 03d877a31..fbd100693 100644 --- a/apps/emqx_dashboard_rbac/rebar.config +++ b/apps/emqx_dashboard_rbac/rebar.config @@ -1,6 +1,6 @@ %% -*- mode: erlang; -*- - {erl_opts, [debug_info]}. + {deps, [ - {emqx_connector, {path, "../../apps/emqx_dashboard"}} + {emqx_dashboard, {path, "../../apps/emqx_dashboard"}} ]}. diff --git a/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl index 74f6312ea..28bd8960e 100644 --- a/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl +++ b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl @@ -12,9 +12,15 @@ %%===================================================================== %% API check_rbac(Req, Extra) -> - Method = cowboy_req:method(Req), 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 role(#?ADMIN{role = undefined}) -> @@ -35,11 +41,14 @@ valid_role(Role) -> {error, <<"Role does not exist">>} end. %% =================================================================== -check_rbac_with_method(?ROLE_SUPERUSER, _) -> +check_rbac(?ROLE_SUPERUSER, _, _) -> true; -check_rbac_with_method(?ROLE_VIEWER, <<"GET">>) -> +check_rbac(?ROLE_VIEWER, <<"GET">>, _) -> true; -check_rbac_with_method(_, _) -> +%% this API is a special case +check_rbac(?ROLE_VIEWER, <<"POST">>, <<"/logout">>) -> + true; +check_rbac(_, _, _) -> false. role_list() -> diff --git a/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl index f5e723a3d..b1a51a3c9 100644 --- a/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl +++ b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl @@ -135,8 +135,9 @@ t_clean_token(_) -> Desc = <<"desc">>, NewDesc = <<"new desc">>, {ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, Desc), - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), - FakeReq = #{method => <<"GET">>}, + {ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password), + 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), %% change description {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), 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() -> {ok, _NewUser} = emqx_dashboard_admin:add_user( ?DEFAULT_SUPERUSER, diff --git a/apps/emqx_dashboard_sso/rebar.config b/apps/emqx_dashboard_sso/rebar.config index efc4a4539..46f26fd99 100644 --- a/apps/emqx_dashboard_sso/rebar.config +++ b/apps/emqx_dashboard_sso/rebar.config @@ -3,5 +3,6 @@ {erl_opts, [debug_info]}. {deps, [ {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"}}} ]}. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.app.src b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.app.src index f60273590..e00a3cbfa 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.app.src +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.app.src @@ -6,7 +6,8 @@ kernel, stdlib, emqx_dashboard, - emqx_ldap + emqx_ldap, + esaml ]}, {mod, {emqx_dashboard_sso_app, []}}, {env, []}, diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl index 8fbf220f5..5abfa3d33 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -5,6 +5,7 @@ -module(emqx_dashboard_sso). -include_lib("hocon/include/hoconsc.hrl"). +-include_lib("emqx_dashboard/include/emqx_dashboard.hrl"). -export([ hocon_ref/1, @@ -38,7 +39,9 @@ {ok, NewState :: state()} | {error, Reason :: term()}. -callback destroy(State :: state()) -> ok. -callback login(request(), State :: state()) -> - {ok, Token :: binary()} | {error, Reason :: term()}. + {ok, dashboard_user_role(), Token :: binary()} + | {redirect, tuple()} + | {error, Reason :: term()}. %%------------------------------------------------------------------------------ %% Callback Interface @@ -76,4 +79,7 @@ provider(Backend) -> maps:get(Backend, backends()). backends() -> - #{ldap => emqx_dashboard_sso_ldap}. + #{ + ldap => emqx_dashboard_sso_ldap, + saml => emqx_dashboard_sso_saml + }. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl index 26e2f132f..c19d2b66e 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -16,6 +16,8 @@ ref/1 ]). +-import(emqx_dashboard_sso, [provider/1]). + -export([ api_spec/0, fields/1, @@ -31,8 +33,9 @@ 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_REQUEST, 'BAD_REQUEST'). -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") -> #{ 'operationId' => login, @@ -83,7 +89,9 @@ schema("/sso/login/:backend") -> parameters => backend_name_in_path(), 'requestBody' => login_union(), 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), 404 => response_schema(404) }, @@ -126,8 +134,10 @@ schema("/sso/:backend") -> fields(backend_status) -> emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()). -%% ------------------------------------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% API +%%-------------------------------------------------------------------- + running(get, _Request) -> SSO = emqx:get_config([dashboard_sso], #{}), {200, @@ -141,28 +151,25 @@ running(get, _Request) -> 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 undefined -> - {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; + {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; State -> - Provider = emqx_dashboard_sso:provider(Backend), - case emqx_dashboard_sso:login(Provider, Sign, State) of - {ok, Token} -> - ?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Sign}), - Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), - {200, #{ - token => Token, - version => Version, - license => #{edition => emqx_release:edition()} - }}; + case emqx_dashboard_sso:login(provider(Backend), Request, State) of + {ok, Role, Token} -> + ?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Request}), + {200, login_reply(Role, Token)}; + {redirect, Redirect} -> + ?SLOG(info, #{msg => "dashboard_sso_login_redirect", request => Request}), + Redirect; {error, Reason} -> ?SLOG(info, #{ msg => "dashboard_sso_login_failed", - request => Sign, + request => Request, reason => Reason }), - {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>} + {401, #{code => ?BAD_USERNAME_OR_PWD, message => <<"Auth failed">>}} end end. @@ -179,7 +186,7 @@ sso(get, _Request) -> backend(get, #{bindings := #{backend := Type}}) -> case emqx:get_config([dashboard_sso, Type], undefined) of undefined -> - {404, ?BACKEND_NOT_FOUND}; + {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; Backend -> {200, to_json(Backend)} end; @@ -193,8 +200,12 @@ backend(delete, #{bindings := #{backend := Backend}}) -> sso_parameters(Params) -> backend_name_as_arg(query, [local], <<"local">>) ++ Params. -%% ------------------------------------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% 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) -> @@ -227,24 +238,25 @@ on_backend_update(Backend, Config, Fun) -> Result = valid_config(Backend, Config, Fun), handle_backend_update_result(Result, Config). -valid_config(Backend, Config, Fun) -> - case maps:get(<<"backend">>, Config, undefined) of - Backend -> - Fun(Backend, Config); - _ -> - {error, invalid_config} - end. +valid_config(Backend, #{<<"backend">> := Backend} = Config, Fun) -> + Fun(Backend, Config); +valid_config(_, _, _) -> + {error, invalid_config}. -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)}; handle_backend_update_result(ok, _) -> 204; 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}, _) -> - {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}, _) -> - {400, ?BAD_REQUEST, Reason}. + {400, #{code => ?BAD_REQUEST, message => Reason}}. to_json(Data) -> emqx_utils_maps:jsonable_map( @@ -253,3 +265,11 @@ to_json(Data) -> {K, emqx_utils_maps:binary_string(V)} 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()} + }. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl index 395deea7d..bea8ef7c6 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl @@ -106,26 +106,22 @@ ensure_bind_password(Config) -> Config#{bind_password => <<"${password}">>}. 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_fields( - T, - [ - {filter, Meta#{ - default => <<"(objectClass=user)">>, - example => <<"(objectClass=user)">> - }} - | Acc - ] - ); -adjust_ldap_fields([Any | T], Acc) -> - adjust_ldap_fields(T, [Any | Acc]); -adjust_ldap_fields([], Acc) -> - lists:reverse(Acc). +adjust_ldap_field({base_dn, Meta}) -> + {base_dn, maps:remove(example, Meta)}; +adjust_ldap_field({filter, Meta}) -> + Default = <<"(& (objectClass=person) (uid=${username}))">>, + {filter, Meta#{ + desc => ?DESC(filter), + default => Default, + example => Default + }}; +adjust_ldap_field(Any) -> + Any. login( - #{<<"username">> := Username} = Req, + #{body := #{<<"username">> := Username} = Sign} = _Req, #{ query_timeout := Timeout, resource_id := ResourceId @@ -134,7 +130,7 @@ login( case emqx_resource:simple_sync_query( ResourceId, - {query, Req, [], Timeout} + {query, Sign, [], Timeout} ) of {ok, []} -> @@ -143,7 +139,7 @@ login( case emqx_resource:simple_sync_query( ResourceId, - {bind, Req} + {bind, Sign} ) of ok -> diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl new file mode 100644 index 000000000..edfb51712 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -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. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl new file mode 100644 index 000000000..105b69141 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl @@ -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">> + } + } + } + }. diff --git a/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl b/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl index 605be7fd1..85cb23693 100644 --- a/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl +++ b/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl @@ -101,7 +101,7 @@ t_first_login(_) -> }, %% this API is authorization-free {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( [#?ADMIN{username = ?SSO_USERNAME(ldap, ?LDAP_USER)}], emqx_dashboard_admin:lookup_user(ldap, ?LDAP_USER) diff --git a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl index 61ed94bdc..dbf034bf8 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl @@ -252,7 +252,7 @@ describe_plugins(Name) -> end. 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"]), case emqx_mgmt_api_test_util:upload_request( diff --git a/bin/nodetool b/bin/nodetool index 3af3bd21a..d00d14a12 100755 --- a/bin/nodetool +++ b/bin/nodetool @@ -142,9 +142,9 @@ do(Args) -> ["eval" | ListOfArgs] -> Parsed = parse_eval_args(ListOfArgs), % and evaluate it on the remote node - case rpc:call(TargetNode, erl_eval, exprs, [Parsed, [] ]) of - {value, Value, _} -> - io:format ("~p~n",[Value]); + case rpc:call(TargetNode, emqx_ctl, eval_erl, [Parsed]) of + {ok, Value} -> + io:format("~p~n",[Value]); {badrpc, Reason} -> io:format("RPC to ~p failed: ~p~n", [TargetNode, Reason]), halt(1) diff --git a/deploy/charts/emqx-enterprise/Chart.yaml b/deploy/charts/emqx-enterprise/Chart.yaml index 3ceec9806..e5ab02dfc 100644 --- a/deploy/charts/emqx-enterprise/Chart.yaml +++ b/deploy/charts/emqx-enterprise/Chart.yaml @@ -14,8 +14,8 @@ type: application # 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. -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 # incremented each time you make changes to the application. -appVersion: 5.2.1 +appVersion: 5.3.0-alpha.1 diff --git a/dev b/dev index 7768fbcf6..c6a3aaf21 100755 --- a/dev +++ b/dev @@ -37,6 +37,7 @@ COMMANDS: ctl: Equivalent to 'emqx ctl'. ctl command arguments should be passed after '--' e.g. $0 ctl -- help + eval: Evaluate an Erlang expression OPTIONS: -p|--profile: emqx | emqx-enterprise, defaults to 'PROFILE' env. @@ -83,6 +84,10 @@ case "${1:-novalue}" in COMMAND='ctl' shift ;; + eval) + COMMAND='eval' + shift + ;; help) usage exit 0 @@ -425,14 +430,22 @@ remsh() { $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() { if [ -z "${PASSTHROUGH_ARGS:-}" ]; then logerr "Need at least one argument for ctl command" logerr "e.g. $0 ctl -- help" exit 1 fi - local tmpnode args rpc_code output result - tmpnode="$(gen_tmp_node_name)" + local args rpc_code output result args="$(make_erlang_args "${PASSTHROUGH_ARGS[@]}")" rpc_code=" case rpc:call('$EMQX_NODE_NAME', emqx_ctl, run_command, [[$args]]) of @@ -443,8 +456,7 @@ ctl() { init:stop(1) end" set +e - # shellcheck disable=SC2086 - output="$(erl -name "$tmpnode" -setcookie "$COOKIE" -hidden -noshell $EPMD_ARGS -eval "$rpc_code" 2>&1)" + output="$(eval_remsh_erl "$rpc_code")" result=$? if [ $result -eq 0 ]; then echo -e "$output" @@ -464,4 +476,8 @@ case "$COMMAND" in ctl) ctl ;; + eval) + PASSTHROUGH_ARGS=('eval_erl' "${PASSTHROUGH_ARGS[@]}") + ctl + ;; esac diff --git a/rebar.config b/rebar.config index 8cd65d04a..8b893d6d1 100644 --- a/rebar.config +++ b/rebar.config @@ -84,14 +84,14 @@ %% in conflict by erlavro and rocketmq , {jsone, {git, "https://github.com/emqx/jsone.git", {tag, "1.7.1"}}} , {uuid, {git, "https://github.com/okeuday/uuid.git", {tag, "v2.0.6"}}} -%% trace - , {opentelemetry_api, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry_api"}} - , {opentelemetry, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry"}} - %% log metrics - , {opentelemetry_experimental, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry_experimental"}} - , {opentelemetry_api_experimental, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry_api_experimental"}} - %% export - , {opentelemetry_exporter, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry_exporter"}} + %% trace + , {opentelemetry_api, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry_api"}} + , {opentelemetry, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry"}} + %% log metrics + , {opentelemetry_experimental, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry_experimental"}} + , {opentelemetry_api_experimental, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry_api_experimental"}} + %% export + , {opentelemetry_exporter, {git_subdir, "https://github.com/emqx/opentelemetry-erlang", {tag, "v1.3.0-emqx"}, "apps/opentelemetry_exporter"}} ]}. {xref_ignores, diff --git a/rel/i18n/emqx_dashboard_sso_api.hocon b/rel/i18n/emqx_dashboard_sso_api.hocon index e14039156..a7ce1f97e 100644 --- a/rel/i18n/emqx_dashboard_sso_api.hocon +++ b/rel/i18n/emqx_dashboard_sso_api.hocon @@ -30,6 +30,15 @@ delete_backend.desc: delete_backend.label: """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 failed. Bad username or password""" diff --git a/rel/i18n/emqx_dashboard_sso_ldap.hocon b/rel/i18n/emqx_dashboard_sso_ldap.hocon index f15975416..db837c81b 100644 --- a/rel/i18n/emqx_dashboard_sso_ldap.hocon +++ b/rel/i18n/emqx_dashboard_sso_ldap.hocon @@ -8,4 +8,11 @@ query_timeout.desc: query_timeout.label: """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""" + } diff --git a/rel/i18n/emqx_dashboard_sso_saml.hocon b/rel/i18n/emqx_dashboard_sso_saml.hocon new file mode 100644 index 000000000..c4bb57a27 --- /dev/null +++ b/rel/i18n/emqx_dashboard_sso_saml.hocon @@ -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""" + +} diff --git a/scripts/rel/cut.sh b/scripts/rel/cut.sh index a7d4408b1..4fafbd3ef 100755 --- a/scripts/rel/cut.sh +++ b/scripts/rel/cut.sh @@ -22,6 +22,7 @@ options: -b|--base: Specify the current release base branch, can be one of release-51 release-52 + release-53 NOTE: this option should be used when --dryrun. --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 -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 ]---------------------------.-----------.--- \\ / - \`---[release-51]----(v5.1.1 | e5.1.1) - -For 5.2 series the current working branch must be 'release-52' - --.--[ master ]---------------------------.-----------.--- - \\ / - \`---[release-52]----(v5.2.1 | e5.2.1) + \`---[release-53]----(v5.3.1 | e5.3.1) EOF } @@ -134,6 +130,12 @@ rel_branch() { e5.2.*) echo 'release-52' ;; + v5.3.*) + echo 'release-53' + ;; + e5.3.*) + echo 'release-53' + ;; *) logerr "Unsupported version tag $TAG" exit 1 diff --git a/scripts/rel/sync-remotes.sh b/scripts/rel/sync-remotes.sh index dddc10638..9d3da2715 100755 --- a/scripts/rel/sync-remotes.sh +++ b/scripts/rel/sync-remotes.sh @@ -5,7 +5,7 @@ set -euo pipefail # ensure dir cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." -BASE_BRANCHES=( 'release-52' 'release-51' 'master' ) +BASE_BRANCHES=( 'release-53' 'release-52' 'release-51' 'master' ) usage() { cat <>>= 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 >>>2 Unrecognized git ref: v5.2.0 @@ -18,6 +23,21 @@ Unrecognized git ref: v5.2.0-1 Unrecognized git ref: e5.2.0-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 >>> {"profile": "emqx", "release": true, "latest": false} @@ -33,6 +53,11 @@ Unrecognized git ref: e5.2.0-1 {"profile": "emqx", "release": true, "latest": false} >>>= 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 >>>2 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} >>>= 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 >>> {"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} >>>= 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 >>> {"profile": "emqx-enterprise", "release": true, "latest": false} >>>= 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 >>> {"profile": "emqx-enterprise", "release": true, "latest": false} >>>= 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 >>> {"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} >>>= 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 >>> {"profile": "emqx", "release": false, "latest": false} diff --git a/scripts/spellcheck/dicts/emqx.txt b/scripts/spellcheck/dicts/emqx.txt index b515a0010..83edb22d1 100644 --- a/scripts/spellcheck/dicts/emqx.txt +++ b/scripts/spellcheck/dicts/emqx.txt @@ -286,3 +286,5 @@ FormatType RocketMQ Keyspace OpenTSDB +saml +idp