From ec43268eeefafce89875cf57278ffe31010b7fc5 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 21 Sep 2023 12:49:01 +0200 Subject: [PATCH 01/26] chore: update scripts and CI to work with 5.3.X --- .github/workflows/_push-entrypoint.yaml | 1 + .github/workflows/build_packages_cron.yaml | 2 +- scripts/rel/cut.sh | 16 ++++--- scripts/rel/sync-remotes.sh | 10 +++-- scripts/shelltest/parse-git-ref.test | 50 ++++++++++++++++++++++ 5 files changed, 68 insertions(+), 11 deletions(-) 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/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} From 7cf60c5a910a81d71101f62e8b788aff81f19bd5 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 21 Sep 2023 12:54:40 +0200 Subject: [PATCH 02/26] chore: e5.3.0-alpha.1 --- apps/emqx/include/emqx_release.hrl | 2 +- deploy/charts/emqx-enterprise/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 From 681e57dee6836168f05c52bddf9bf815128559a6 Mon Sep 17 00:00:00 2001 From: firest Date: Fri, 22 Sep 2023 11:06:24 +0800 Subject: [PATCH 03/26] fix(RBAC): allow read-only users to logout --- .../src/emqx_dashboard_swagger.erl | 8 +++++++- .../src/emqx_dashboard_rbac.erl | 19 ++++++++++++++----- .../test/emqx_dashboard_rbac_SUITE.erl | 14 +++++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) 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_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..2cbe4bc1c 100644 --- a/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl +++ b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl @@ -136,7 +136,8 @@ t_clean_token(_) -> 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">>}, + 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, 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, From 9e55ae240a3a10106e7aaac914d41d7cb3714909 Mon Sep 17 00:00:00 2001 From: firest Date: Fri, 22 Sep 2023 11:46:58 +0800 Subject: [PATCH 04/26] feat(sso): add `role` into the result of login endpoints --- .../emqx_bridge_kafka_impl_producer_SUITE.erl | 2 +- apps/emqx_dashboard/src/emqx_dashboard_api.erl | 16 +++++++++------- apps/emqx_dashboard/src/emqx_dashboard_token.erl | 4 ++-- .../emqx_dashboard/test/emqx_dashboard_SUITE.erl | 5 +++-- .../test/emqx_dashboard_admin_SUITE.erl | 7 ++++--- .../test/emqx_dashboard_api_test_helpers.erl | 2 +- .../test/emqx_dashboard_haproxy_SUITE.erl | 2 +- .../test/emqx_dashboard_rbac_SUITE.erl | 4 ++-- .../src/emqx_dashboard_sso.erl | 3 ++- .../src/emqx_dashboard_sso_api.erl | 5 +++-- .../test/emqx_dashboard_sso_ldap_SUITE.erl | 2 +- .../test/emqx_mgmt_api_plugins_SUITE.erl | 2 +- 12 files changed, 30 insertions(+), 24 deletions(-) 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_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_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/test/emqx_dashboard_rbac_SUITE.erl b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl index 2cbe4bc1c..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,7 +135,7 @@ 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), + {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), @@ -154,7 +154,7 @@ t_login_out(_) -> Password = <<"public_www1">>, Desc = <<"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), 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), diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl index 8fbf220f5..ecc6f40d2 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,7 @@ {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()} | {error, Reason :: term()}. %%------------------------------------------------------------------------------ %% Callback Interface 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..7bcc65d2e 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -83,7 +83,7 @@ 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]), 401 => response_schema(401), 404 => response_schema(404) }, @@ -148,10 +148,11 @@ login(post, #{bindings := #{backend := Backend}, body := Sign}) -> State -> Provider = emqx_dashboard_sso:provider(Backend), case emqx_dashboard_sso:login(Provider, Sign, State) of - {ok, Token} -> + {ok, Role, Token} -> ?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Sign}), Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), {200, #{ + role => Role, token => Token, version => Version, license => #{edition => emqx_release:edition()} 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( From 7286d15ca680da7e8d5451529297ae5b8485df2f Mon Sep 17 00:00:00 2001 From: firest Date: Fri, 22 Sep 2023 11:58:27 +0800 Subject: [PATCH 05/26] chore(sso): adjust the schema of the SSO LDAP backend --- .../src/emqx_dashboard_sso_ldap.erl | 28 ++++++++----------- rel/i18n/emqx_dashboard_sso_ldap.hocon | 7 +++++ 2 files changed, 19 insertions(+), 16 deletions(-) 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..d6acbb164 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl @@ -106,23 +106,19 @@ 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, 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""" + } From a34ab19d93d13c45ab19d9302a2d68ca2361a9aa Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 22 Sep 2023 11:22:15 +0200 Subject: [PATCH 06/26] fix(audit): make emqx eval command auditable --- apps/emqx_ctl/src/emqx_ctl.erl | 50 +++++++++++++++++++++++---- apps/emqx_ctl/test/emqx_ctl_SUITE.erl | 20 ++++++++--- bin/nodetool | 6 ++-- dev | 24 ++++++++++--- 4 files changed, 82 insertions(+), 18 deletions(-) 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/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/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 From d9466eef6318d27b964a2ab0683cad72d2e7446c Mon Sep 17 00:00:00 2001 From: JimMoen Date: Tue, 19 Sep 2023 10:15:12 +0800 Subject: [PATCH 07/26] chore: fix Dashboard RBAC license and rebar.config --- .github/CODEOWNERS | 8 +++++--- apps/emqx_dashboard_rbac/README.md | 2 +- apps/emqx_dashboard_rbac/rebar.config | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) 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/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"}} ]}. From c9e0d4fc308a327969696c1d16b4ef0d4a95f3e9 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Thu, 21 Sep 2023 05:09:06 +0800 Subject: [PATCH 08/26] feat: saml integration for dashboard sso --- apps/emqx_dashboard_sso/rebar.config | 3 +- .../src/emqx_dashboard_sso.app.src | 3 +- .../src/emqx_dashboard_sso.erl | 5 +- .../src/emqx_dashboard_sso_api.erl | 126 ++++++++++-- .../src/emqx_dashboard_sso_app.erl | 1 + .../src/emqx_dashboard_sso_manager.erl | 2 +- .../src/emqx_dashboard_sso_saml.erl | 194 ++++++++++++++++++ rebar.config | 1 + rel/i18n/emqx_dashboard_sso_api.hocon | 9 + rel/i18n/emqx_dashboard_sso_saml.hocon | 28 +++ 10 files changed, 350 insertions(+), 22 deletions(-) create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl create mode 100644 rel/i18n/emqx_dashboard_sso_saml.hocon diff --git a/apps/emqx_dashboard_sso/rebar.config b/apps/emqx_dashboard_sso/rebar.config index efc4a4539..a26bc8fe7 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/JimMoen/esaml", {branch, "master"}}} ]}. 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 ecc6f40d2..8a0bb18e8 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -77,4 +77,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 7bcc65d2e..a6041c6af 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, @@ -28,11 +30,14 @@ running/2, login/2, sso/2, - backend/2 + backend/2, + sp_saml_metadata/2, + sp_saml_callback/2 ]). -export([sso_parameters/1]). +-define(REDIRECT, 'REDIRECT'). -define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). -define(BAD_REQUEST, 'BAD_REQUEST'). -define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND'). @@ -48,7 +53,10 @@ paths() -> "/sso", "/sso/:backend", "/sso/running", - "/sso/login/:backend" + "/sso/login/:backend", + "/sso_saml/acs", + "/sso_saml/metadata" + %% "/sso_saml/logout" ]. schema("/sso/running") -> @@ -74,6 +82,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, @@ -84,6 +95,8 @@ schema("/sso/login/:backend") -> 'requestBody' => login_union(), responses => #{ 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) }, @@ -121,8 +134,41 @@ schema("/sso/:backend") -> 404 => response_schema(404) } } + }; +%% 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' => saml_response(), + %% SAMLResponse and RelayState + %% should return 302 to redirect to dashboard + responses => #{ + 302 => response_schema(302), + 401 => response_schema(401), + 404 => response_schema(404) + } + } + }; +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) + } + } }. +%% TODO: +%% schema("/sso_saml/logout") -> + fields(backend_status) -> emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()). @@ -141,22 +187,19 @@ running(get, _Request) -> maps:values(SSO) )}. -login(post, #{bindings := #{backend := Backend}, body := Sign}) -> +login(post, #{bindings := #{backend := Backend}, body := Sign, headers := Headers}) -> case emqx_dashboard_sso_manager:lookup_state(Backend) of undefined -> {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; State -> - Provider = emqx_dashboard_sso:provider(Backend), + Provider = provider(Backend), case emqx_dashboard_sso:login(Provider, Sign, State) of {ok, Role, Token} -> ?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Sign}), - Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), - {200, #{ - role => Role, - token => Token, - version => Version, - license => #{edition => emqx_release:edition()} - }}; + {200, login_reply(Role, Token)}; + {redirect, RedirectFun} -> + ?SLOG(info, #{msg => "dashboard_sso_login_redirect", request => Sign}), + RedirectFun(Headers); {error, Reason} -> ?SLOG(info, #{ msg => "dashboard_sso_login_failed", @@ -191,11 +234,41 @@ backend(delete, #{bindings := #{backend := Backend}}) -> ?SLOG(info, #{msg => "Delete SSO backend", backend => Backend}), handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined). +sp_saml_metadata(get, _Req) -> + case emqx_dashboard_sso_manager:lookup_state(saml) of + undefined -> + {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; + #{sp := SP} = _State -> + SignedXml = SP:generate_metadata(), + Metadata = xmerl:export([SignedXml], xmerl_xml), + {200, [{<<"Content-Type">>, <<"text/xml">>}], Metadata} + end. + +sp_saml_callback(post, Req) -> + case emqx_dashboard_sso_manager:lookup_state(saml) of + undefined -> + {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; + State -> + case (provider(saml)):callback(Req, State) of + {ok, Token} -> + {200, [{<<"Content-Type">>, <<"text/html">>}], login_reply(Token)}; + {error, Reason} -> + ?SLOG(info, #{ + msg => "dashboard_saml_sso_login_failed", + request => Req, + reason => Reason + }), + {403, #{code => <<"UNAUTHORIZED">>, message => Reason}} + end + end. + 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) -> @@ -207,6 +280,18 @@ backend_union() -> login_union() -> hoconsc:union([emqx_dashboard_sso:login_ref(Mod) || Mod <- emqx_dashboard_sso:modules()]). +saml_metadata_response() -> + #{ + 'content' => #{ + 'application/xml' => #{ + schema => #{ + type => <<"string">>, + format => <<"binary">> + } + } + } + }. + backend_name_in_path() -> backend_name_as_arg(path, [], <<"ldap">>). @@ -228,13 +313,10 @@ 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) -> {200, to_json(Config)}; @@ -254,3 +336,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_app.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl index 2ad280b5e..feef9d4e3 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl @@ -12,6 +12,7 @@ ]). start(_StartType, _StartArgs) -> + {ok, _} = application:ensure_all_started(esaml), emqx_dashboard_sso_sup:start_link(). stop(_State) -> diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl index afa27cb47..43ebbfa72 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl @@ -77,7 +77,7 @@ delete(Backend) -> lookup_state(Backend) -> case ets:lookup(dashboard_sso, Backend) of [Data] -> - Data#dashboard_sso.state; + Data; [] -> undefined end. 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..4b1dad0c8 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -0,0 +1,194 @@ +%%-------------------------------------------------------------------- +%% 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([ + fields/1, + desc/1 +]). + +-export([ + hocon_ref/0, + login_ref/0, + login/2, + create/1, + update/2, + destroy/1 +]). + +-export([callback/2]). + +%%------------------------------------------------------------------------------ +%% 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( + #{ + dashboard_addr := DashboardAddr, + idp_metadata_url := IDPMetadataURL, + sp_sign_request := SignRequest + } = Config +) -> + BaseURL = binary_to_list(DashboardAddr) ++ "/api/v5", + %% {Config, State} = parse_config(Config), + SP = esaml_sp:setup(#esaml_sp{ + %% TODO: save cert and key then return path + %% TODO: #esaml_sp.key #esaml_sp.certificate support + %% key = PrivKey, + %% certificate = Cert, + sp_sign_requests = SignRequest, + trusted_fingerprints = [], + consume_uri = BaseURL ++ "/sso_saml/acs", + metadata_uri = BaseURL ++ "/sso_saml/metadata", + org = #esaml_org{ + name = "EMQX Team", + displayname = "EMQX Dashboard", + url = DashboardAddr + }, + tech = #esaml_contact{ + name = "EMQX Team", + email = "contact@emqx.io" + } + }), + IdpMeta = esaml_util:load_metadata(binary_to_list(IDPMetadataURL)), + + {ok, Config#{idp_meta => IdpMeta, sp => SP}}. + +update(_Config0, State) -> + {ok, State}. + +destroy(#{resource_id := ResourceId}) -> + _ = emqx_resource:remove_local(ResourceId), + ok. + +login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State) -> + SignedXml = SP:generate_authn_request(IDP), + Target = esaml_binding:encode_http_redirect(IDP, SignedXml, <<>>), + %% TODO: _Req acutally is HTTP request body, not fully request + RedirectFun = fun(Headers) -> + case is_msie(Headers) of + true -> + Html = esaml_binding:encode_http_post(IDP, SignedXml, <<>>), + {200, + [ + {<<"Cache-Control">>, <<"no-cache">>}, + {<<"Pragma">>, <<"no-cache">>} + ], + Html}; + false -> + {302, redirect_header(Target), <<"Redirecting...">>} + end + end, + {redirect, RedirectFun}. + +callback(Req, #{sp := SP} = _State) -> + case esaml_cowboy:validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Req) of + {ok, Assertion, _RelayState, _Req2} -> + Subject = Assertion#esaml_assertion.subject, + Username = Subject#esaml_subject.name, + ensure_user_exists(Username); + {error, Reason0, _Req2} -> + Reason = [ + "Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0]) + ], + {error, Reason} + end. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +%% -define(DIR, <<"SAML_SSO_sp_certs">>). +%% -define(RSA_KEYS_A, [sp_public_key, sp_private_key]). + +is_msie(Headers) -> + UA = maps:get(<<"user-agent">>, Headers, <<"">>), + not (binary:match(UA, <<"MSIE">>) =:= nomatch). + +redirect_header(TargetUrl) -> + [ + {<<"Cache-Control">>, <<"no-cache">>}, + {<<"Pragma">>, <<"no-cache">>}, + {<<"Location">>, TargetUrl} + ]. + +%% 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/rebar.config b/rebar.config index fd3f9820d..6a06cb532 100644 --- a/rebar.config +++ b/rebar.config @@ -84,6 +84,7 @@ %% 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"}}} + %% , {esaml, {git, " %% 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"}} 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_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""" + +} From 13666fa9f9ecbfdf7560b65a93db12eb9e739660 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 09:29:15 +0800 Subject: [PATCH 09/26] refactor: avoid dynamic call --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl | 2 +- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 a6041c6af..e649eb87d 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -239,7 +239,7 @@ sp_saml_metadata(get, _Req) -> undefined -> {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; #{sp := SP} = _State -> - SignedXml = SP:generate_metadata(), + SignedXml = esaml_sp:generate_metadata(SP), Metadata = xmerl:export([SignedXml], xmerl_xml), {200, [{<<"Content-Type">>, <<"text/xml">>}], Metadata} end. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index 4b1dad0c8..16ae600cf 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -129,7 +129,7 @@ destroy(#{resource_id := ResourceId}) -> ok. login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State) -> - SignedXml = SP:generate_authn_request(IDP), + SignedXml = esaml_sp:generate_authn_request(IDP, SP), Target = esaml_binding:encode_http_redirect(IDP, SignedXml, <<>>), %% TODO: _Req acutally is HTTP request body, not fully request RedirectFun = fun(Headers) -> From 44836ef5ee8af20d7040a988aec597149a7e17eb Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 09:37:44 +0800 Subject: [PATCH 10/26] chore: bump esaml vsn to v1.1.1 --- apps/emqx_dashboard_sso/rebar.config | 2 +- rebar.config | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/emqx_dashboard_sso/rebar.config b/apps/emqx_dashboard_sso/rebar.config index a26bc8fe7..494f22df7 100644 --- a/apps/emqx_dashboard_sso/rebar.config +++ b/apps/emqx_dashboard_sso/rebar.config @@ -4,5 +4,5 @@ {deps, [ {emqx_ldap, {path, "../../apps/emqx_ldap"}}, {emqx_dashboard, {path, "../../apps/emqx_dashboard"}}, - {esaml, {git, "https://github.com/JimMoen/esaml", {branch, "master"}}} + {esaml, {git, "https://github.com/JimMoen/esaml", {tag, "v1.1.1"}}} ]}. diff --git a/rebar.config b/rebar.config index 6a06cb532..860951744 100644 --- a/rebar.config +++ b/rebar.config @@ -84,15 +84,15 @@ %% 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"}}} - %% , {esaml, {git, " -%% 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"}} + , {esaml, {git, "https://github.com/JimMoen/esaml", {tag, "v1.1.1"}}} + %% 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, From 8300cd42d401419a59e66da32d0d03abe58d76cb Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 09:53:20 +0800 Subject: [PATCH 11/26] fix: acl url ignore auth check --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 e649eb87d..248266a14 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -149,7 +149,8 @@ schema("/sso_saml/acs") -> 302 => response_schema(302), 401 => response_schema(401), 404 => response_schema(404) - } + }, + security => [] } }; schema("/sso_saml/metadata") -> From bba5cc44a80bbeeaa3021fbc62c5b1e1ec5f45f1 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 09:59:54 +0800 Subject: [PATCH 12/26] fix: keep same API path style --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 248266a14..d1f5fc096 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -54,8 +54,8 @@ paths() -> "/sso/:backend", "/sso/running", "/sso/login/:backend", - "/sso_saml/acs", - "/sso_saml/metadata" + "/sso/saml/acs", + "/sso/saml/metadata" %% "/sso_saml/logout" ]. @@ -136,7 +136,7 @@ schema("/sso/:backend") -> } }; %% Handles HTTP-POST bound assertions coming back from the IDP. -schema("/sso_saml/acs") -> +schema("/sso/saml/acs") -> #{ 'operationId' => sp_saml_callback, post => #{ @@ -153,7 +153,7 @@ schema("/sso_saml/acs") -> security => [] } }; -schema("/sso_saml/metadata") -> +schema("/sso/saml/metadata") -> #{ 'operationId' => sp_saml_metadata, get => #{ From b4fb5196cb864799c16bf0fa85b6c4081a8fb0b9 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 10:22:53 +0800 Subject: [PATCH 13/26] fix(sso): SSO management API 500 --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl index 43ebbfa72..afa27cb47 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl @@ -77,7 +77,7 @@ delete(Backend) -> lookup_state(Backend) -> case ets:lookup(dashboard_sso, Backend) of [Data] -> - Data; + Data#dashboard_sso.state; [] -> undefined end. From 1c78c6bf6d41597de1aa5c8bc78ba1feddcf392b Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 22 Sep 2023 11:28:16 +0800 Subject: [PATCH 14/26] chore: fix 500 crashes when backend not existed --- .../src/emqx_dashboard_sso_api.erl | 26 ++++++++++++------- .../src/emqx_dashboard_sso_saml.erl | 14 ++++++---- 2 files changed, 25 insertions(+), 15 deletions(-) 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 d1f5fc096..91373d93b 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -173,8 +173,10 @@ schema("/sso/saml/metadata") -> 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, @@ -191,7 +193,7 @@ running(get, _Request) -> login(post, #{bindings := #{backend := Backend}, body := Sign, headers := Headers}) -> 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 = provider(Backend), case emqx_dashboard_sso:login(Provider, Sign, State) of @@ -207,7 +209,7 @@ login(post, #{bindings := #{backend := Backend}, body := Sign, headers := Header request => Sign, reason => Reason }), - {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>} + {401, #{code => ?BAD_USERNAME_OR_PWD, message => <<"Auth failed">>}} end end. @@ -224,7 +226,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; @@ -238,7 +240,7 @@ backend(delete, #{bindings := #{backend := Backend}}) -> sp_saml_metadata(get, _Req) -> case emqx_dashboard_sso_manager:lookup_state(saml) of undefined -> - {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; + {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; #{sp := SP} = _State -> SignedXml = esaml_sp:generate_metadata(SP), Metadata = xmerl:export([SignedXml], xmerl_xml), @@ -248,7 +250,7 @@ sp_saml_metadata(get, _Req) -> sp_saml_callback(post, Req) -> case emqx_dashboard_sso_manager:lookup_state(saml) of undefined -> - {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; + {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; State -> case (provider(saml)):callback(Req, State) of {ok, Token} -> @@ -266,8 +268,10 @@ sp_saml_callback(post, Req) -> 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) -> @@ -324,11 +328,13 @@ handle_backend_update_result({ok, _}, 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( diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index 16ae600cf..26dc7d5be 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -117,15 +117,19 @@ create( email = "contact@emqx.io" } }), - IdpMeta = esaml_util:load_metadata(binary_to_list(IDPMetadataURL)), - - {ok, Config#{idp_meta => IdpMeta, sp => SP}}. + try + IdpMeta = esaml_util:load_metadata(binary_to_list(IDPMetadataURL)), + {ok, Config#{idp_meta => IdpMeta, sp => SP}} + catch + Kind:Error -> + ?SLOG(error, #{msg => failed_to_load_metadata, kind => Kind, error => Error}), + {error, failed_to_load_metadata} + end. update(_Config0, State) -> {ok, State}. -destroy(#{resource_id := ResourceId}) -> - _ = emqx_resource:remove_local(ResourceId), +destroy(_State) -> ok. login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State) -> From 47badc3181340613fcb6e5b1ba4f1b2b6ccfb5c8 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 22 Sep 2023 11:45:19 +0800 Subject: [PATCH 15/26] chore: make dialyzer happy --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl | 2 +- .../src/emqx_dashboard_sso_saml.erl | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl index 8a0bb18e8..a47f01199 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -39,7 +39,7 @@ {ok, NewState :: state()} | {error, Reason :: term()}. -callback destroy(State :: state()) -> ok. -callback login(request(), State :: state()) -> - {ok, dashboard_user_role(), Token :: binary()} | {error, Reason :: term()}. + {ok, dashboard_user_role(), Token :: binary()} | {redirect, fun()} | {error, Reason :: term()}. %%------------------------------------------------------------------------------ %% Callback Interface diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index 26dc7d5be..1f21dc042 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -12,20 +12,22 @@ -behaviour(emqx_dashboard_sso). -export([ + hocon_ref/0, + login_ref/0, fields/1, desc/1 ]). +%% emqx_dashboard_sso callbacks -export([ - hocon_ref/0, - login_ref/0, - login/2, create/1, update/2, destroy/1 ]). --export([callback/2]). +-export([login/2, callback/2]). + +-dialyzer({nowarn_function, create/1}). %%------------------------------------------------------------------------------ %% Hocon Schema @@ -156,7 +158,7 @@ callback(Req, #{sp := SP} = _State) -> case esaml_cowboy:validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Req) of {ok, Assertion, _RelayState, _Req2} -> Subject = Assertion#esaml_assertion.subject, - Username = Subject#esaml_subject.name, + Username = iolist_to_binary(Subject#esaml_subject.name), ensure_user_exists(Username); {error, Reason0, _Req2} -> Reason = [ From 4a26f63bd63471e28a75e01a68b5dff1611ee422 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 22 Sep 2023 11:56:53 +0800 Subject: [PATCH 16/26] chore: fix bugs --- .../src/emqx_dashboard_sso_saml.erl | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index 1f21dc042..4d172dcdb 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -107,8 +107,8 @@ create( %% certificate = Cert, sp_sign_requests = SignRequest, trusted_fingerprints = [], - consume_uri = BaseURL ++ "/sso_saml/acs", - metadata_uri = BaseURL ++ "/sso_saml/metadata", + consume_uri = BaseURL ++ "/sso/saml/acs", + metadata_uri = BaseURL ++ "/sso/saml/metadata", org = #esaml_org{ name = "EMQX Team", displayname = "EMQX Dashboard", @@ -139,17 +139,14 @@ login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = Target = esaml_binding:encode_http_redirect(IDP, SignedXml, <<>>), %% TODO: _Req acutally is HTTP request body, not fully request RedirectFun = fun(Headers) -> + RespHeaders = #{<<"Cache-Control">> => <<"no-cache">>, <<"Pragma">> => <<"no-cache">>}, case is_msie(Headers) of true -> Html = esaml_binding:encode_http_post(IDP, SignedXml, <<>>), - {200, - [ - {<<"Cache-Control">>, <<"no-cache">>}, - {<<"Pragma">>, <<"no-cache">>} - ], - Html}; + {200, RespHeaders, Html}; false -> - {302, redirect_header(Target), <<"Redirecting...">>} + RespHeaders1 = RespHeaders#{<<"Location">> => Target}, + {302, RespHeaders1, <<"Redirecting...">>} end end, {redirect, RedirectFun}. @@ -178,13 +175,6 @@ is_msie(Headers) -> UA = maps:get(<<"user-agent">>, Headers, <<"">>), not (binary:match(UA, <<"MSIE">>) =:= nomatch). -redirect_header(TargetUrl) -> - [ - {<<"Cache-Control">>, <<"no-cache">>}, - {<<"Pragma">>, <<"no-cache">>}, - {<<"Location">>, TargetUrl} - ]. - %% TODO: unify with emqx_dashboard_sso_manager:ensure_user_exists/1 ensure_user_exists(Username) -> case emqx_dashboard_admin:lookup_user(saml, Username) of From ec0894ca0b89e91598f5bcfcc6b784e1d747a2a5 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 22 Sep 2023 12:48:16 +0800 Subject: [PATCH 17/26] chore: update esaml vsn --- apps/emqx_dashboard_sso/rebar.config | 2 +- rebar.config | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/emqx_dashboard_sso/rebar.config b/apps/emqx_dashboard_sso/rebar.config index 494f22df7..46f26fd99 100644 --- a/apps/emqx_dashboard_sso/rebar.config +++ b/apps/emqx_dashboard_sso/rebar.config @@ -4,5 +4,5 @@ {deps, [ {emqx_ldap, {path, "../../apps/emqx_ldap"}}, {emqx_dashboard, {path, "../../apps/emqx_dashboard"}}, - {esaml, {git, "https://github.com/JimMoen/esaml", {tag, "v1.1.1"}}} + {esaml, {git, "https://github.com/emqx/esaml", {tag, "v1.1.1"}}} ]}. diff --git a/rebar.config b/rebar.config index 860951744..900353c35 100644 --- a/rebar.config +++ b/rebar.config @@ -84,7 +84,6 @@ %% 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"}}} - , {esaml, {git, "https://github.com/JimMoen/esaml", {tag, "v1.1.1"}}} %% 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"}} From df94426ee3fb9806ce25a23e39280209da5599cd Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 22 Sep 2023 14:11:26 +0800 Subject: [PATCH 18/26] chore: make static_check happy --- apps/emqx/test/emqx_bpapi_static_checks.erl | 2 +- scripts/spellcheck/dicts/emqx.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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/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 From 9181ec844f0ba57fcbac5a238eef21492ff22fab Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 22 Sep 2023 16:31:55 +0800 Subject: [PATCH 19/26] chore: split out sso_saml_api module --- .../src/emqx_dashboard_sso_api.erl | 85 +---------- .../src/emqx_dashboard_sso_saml.erl | 25 +++- .../src/emqx_dashboard_sso_saml_api.erl | 132 ++++++++++++++++++ 3 files changed, 155 insertions(+), 87 deletions(-) create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl 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 91373d93b..f2cd02ecb 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -30,12 +30,10 @@ running/2, login/2, sso/2, - backend/2, - sp_saml_metadata/2, - sp_saml_callback/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'). @@ -53,10 +51,7 @@ paths() -> "/sso", "/sso/:backend", "/sso/running", - "/sso/login/:backend", - "/sso/saml/acs", - "/sso/saml/metadata" - %% "/sso_saml/logout" + "/sso/login/:backend" ]. schema("/sso/running") -> @@ -134,42 +129,8 @@ schema("/sso/:backend") -> 404 => response_schema(404) } } - }; -%% 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' => saml_response(), - %% SAMLResponse and RelayState - %% should return 302 to redirect to dashboard - 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) - } - } }. -%% TODO: -%% schema("/sso_saml/logout") -> - fields(backend_status) -> emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()). @@ -237,34 +198,6 @@ backend(delete, #{bindings := #{backend := Backend}}) -> ?SLOG(info, #{msg => "Delete SSO backend", backend => Backend}), handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined). -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">>}], 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, Token} -> - {200, [{<<"Content-Type">>, <<"text/html">>}], login_reply(Token)}; - {error, Reason} -> - ?SLOG(info, #{ - msg => "dashboard_saml_sso_login_failed", - request => Req, - reason => Reason - }), - {403, #{code => <<"UNAUTHORIZED">>, message => Reason}} - end - end. - sso_parameters(Params) -> backend_name_as_arg(query, [local], <<"local">>) ++ Params. @@ -285,18 +218,6 @@ backend_union() -> login_union() -> hoconsc:union([emqx_dashboard_sso:login_ref(Mod) || Mod <- emqx_dashboard_sso:modules()]). -saml_metadata_response() -> - #{ - 'content' => #{ - 'application/xml' => #{ - schema => #{ - type => <<"string">>, - format => <<"binary">> - } - } - } - }. - backend_name_in_path() -> backend_name_as_arg(path, [], <<"ldap">>). diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index 4d172dcdb..9f2b5cc48 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -151,17 +151,32 @@ login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = end, {redirect, RedirectFun}. -callback(Req, #{sp := SP} = _State) -> - case esaml_cowboy:validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Req) of - {ok, Assertion, _RelayState, _Req2} -> +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, _Req2} -> + {error, Reason0} -> Reason = [ "Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0]) ], - {error, Reason} + {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. %%------------------------------------------------------------------------------ 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..492012153 --- /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">>}], 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, Token} -> + {200, emqx_dashboard_sso_api:login_reply(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">> + } + } + } + }. From a318ad486a368fba6d26e1222a05df2226155d1e Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 16:43:12 +0800 Subject: [PATCH 20/26] refactor: behavior login/2 use all http request --- .../emqx_dashboard_sso/src/emqx_dashboard_sso.erl | 4 +++- .../src/emqx_dashboard_sso_api.erl | 15 +++++++-------- .../src/emqx_dashboard_sso_ldap.erl | 6 +++--- .../src/emqx_dashboard_sso_saml.erl | 15 ++++++++------- .../src/emqx_dashboard_sso_saml_api.erl | 2 +- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl index a47f01199..5abfa3d33 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -39,7 +39,9 @@ {ok, NewState :: state()} | {error, Reason :: term()}. -callback destroy(State :: state()) -> ok. -callback login(request(), State :: state()) -> - {ok, dashboard_user_role(), Token :: binary()} | {redirect, fun()} | {error, Reason :: term()}. + {ok, dashboard_user_role(), Token :: binary()} + | {redirect, tuple()} + | {error, Reason :: term()}. %%------------------------------------------------------------------------------ %% Callback Interface 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 f2cd02ecb..6674db3a8 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -151,23 +151,22 @@ running(get, _Request) -> maps:values(SSO) )}. -login(post, #{bindings := #{backend := Backend}, body := Sign, headers := Headers}) -> +login(post, #{bindings := #{backend := Backend}} = Request) -> case emqx_dashboard_sso_manager:lookup_state(Backend) of undefined -> {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; State -> - Provider = provider(Backend), - case emqx_dashboard_sso:login(Provider, Sign, State) of + case emqx_dashboard_sso:login(provider(Backend), Request, State) of {ok, Role, Token} -> - ?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Sign}), + ?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Request}), {200, login_reply(Role, Token)}; - {redirect, RedirectFun} -> - ?SLOG(info, #{msg => "dashboard_sso_login_redirect", request => Sign}), - RedirectFun(Headers); + {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, #{code => ?BAD_USERNAME_OR_PWD, message => <<"Auth failed">>}} 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 d6acbb164..bea8ef7c6 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl @@ -121,7 +121,7 @@ adjust_ldap_field(Any) -> Any. login( - #{<<"username">> := Username} = Req, + #{body := #{<<"username">> := Username} = Sign} = _Req, #{ query_timeout := Timeout, resource_id := ResourceId @@ -130,7 +130,7 @@ login( case emqx_resource:simple_sync_query( ResourceId, - {query, Req, [], Timeout} + {query, Sign, [], Timeout} ) of {ok, []} -> @@ -139,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 index 9f2b5cc48..455fc5686 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -134,12 +134,14 @@ update(_Config0, State) -> destroy(_State) -> ok. -login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State) -> +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, <<>>), - %% TODO: _Req acutally is HTTP request body, not fully request - RedirectFun = fun(Headers) -> - RespHeaders = #{<<"Cache-Control">> => <<"no-cache">>, <<"Pragma">> => <<"no-cache">>}, + RespHeaders = #{<<"Cache-Control">> => <<"no-cache">>, <<"Pragma">> => <<"no-cache">>}, + Redirect = case is_msie(Headers) of true -> Html = esaml_binding:encode_http_post(IDP, SignedXml, <<>>), @@ -147,9 +149,8 @@ login(_Req, #{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = false -> RespHeaders1 = RespHeaders#{<<"Location">> => Target}, {302, RespHeaders1, <<"Redirecting...">>} - end - end, - {redirect, RedirectFun}. + end, + {redirect, Redirect}. callback(_Req = #{body := Body}, #{sp := SP} = _State) -> case do_validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Body) of 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 index 492012153..0163ab9a8 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl @@ -87,7 +87,7 @@ sp_saml_metadata(get, _Req) -> #{sp := SP} = _State -> SignedXml = esaml_sp:generate_metadata(SP), Metadata = xmerl:export([SignedXml], xmerl_xml), - {200, [{<<"Content-Type">>, <<"text/xml">>}], Metadata} + {200, #{<<"Content-Type">> => <<"text/xml">>}, Metadata} end. sp_saml_callback(post, Req) -> From 2a8f3f9eaaa0349b64096b7cf4f31fe396ebf75e Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 16:58:44 +0800 Subject: [PATCH 21/26] fix: saml xml metedata format --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 0163ab9a8..fb5e27fa4 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl @@ -87,7 +87,7 @@ sp_saml_metadata(get, _Req) -> #{sp := SP} = _State -> SignedXml = esaml_sp:generate_metadata(SP), Metadata = xmerl:export([SignedXml], xmerl_xml), - {200, #{<<"Content-Type">> => <<"text/xml">>}, Metadata} + {200, #{<<"Content-Type">> => <<"text/xml">>}, erlang:iolist_to_binary(Metadata)} end. sp_saml_callback(post, Req) -> From 6349cd3910870e19550c55e58bdb17ff262af130 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 21:13:04 +0800 Subject: [PATCH 22/26] fix(saml): sp sign request --- .../src/emqx_dashboard_sso_app.erl | 1 - .../src/emqx_dashboard_sso_saml.erl | 59 ++++++++++++++----- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl index feef9d4e3..2ad280b5e 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl @@ -12,7 +12,6 @@ ]). start(_StartType, _StartArgs) -> - {ok, _} = application:ensure_all_started(esaml), emqx_dashboard_sso_sup:start_link(). stop(_State) -> diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index 455fc5686..bceb064f6 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -91,31 +91,45 @@ desc(_) -> %% APIs %%------------------------------------------------------------------------------ -create( +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 := SignRequest + key := KeyPath, + certificate := CertPath } = Config ) -> + {ok, _} = application:ensure_all_started(esaml), BaseURL = binary_to_list(DashboardAddr) ++ "/api/v5", - %% {Config, State} = parse_config(Config), + Key = esaml_util:load_private_key(KeyPath), + Cert = esaml_util:load_certificate(CertPath), SP = esaml_sp:setup(#esaml_sp{ - %% TODO: save cert and key then return path - %% TODO: #esaml_sp.key #esaml_sp.certificate support - %% key = PrivKey, - %% certificate = Cert, - sp_sign_requests = SignRequest, + key = Key, + certificate = Cert, + sp_sign_requests = true, 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 Team", + name = "EMQX", displayname = "EMQX Dashboard", url = DashboardAddr }, tech = #esaml_contact{ - name = "EMQX Team", + name = "EMQX", email = "contact@emqx.io" } }), @@ -124,14 +138,17 @@ create( {ok, Config#{idp_meta => IdpMeta, sp => SP}} catch Kind:Error -> - ?SLOG(error, #{msg => failed_to_load_metadata, kind => Kind, error => Error}), - {error, failed_to_load_metadata} + Reason = failed_to_load_metadata, + ?SLOG(error, #{msg => Reason, kind => Kind, error => Error}), + {error, Reason} end. -update(_Config0, State) -> - {ok, State}. +update(Config0, State) -> + destroy(State), + create(Config0). destroy(_State) -> + _ = application:stop(esaml), ok. login( @@ -184,8 +201,18 @@ do_validate_assertion(SP, DuplicateFun, Body) -> %% Internal functions %%------------------------------------------------------------------------------ -%% -define(DIR, <<"SAML_SSO_sp_certs">>). -%% -define(RSA_KEYS_A, [sp_public_key, sp_private_key]). +-define(DIR, <<"SAML_SSO_sp_certs">>). +-define(RSA_KEYS_A, [sp_public_key, sp_private_key]). + +ensure_cert_and_key(Config) -> + case + emqx_tls_lib:ensure_ssl_files(?DIR, Config#{enable => ture}, #{required_keys => ?RSA_KEYS_A}) + of + {ok, NConfig} -> + NConfig; + {error, #{which_options := [KeyPath | _]}} -> + error({missing_key, KeyPath}) + end. is_msie(Headers) -> UA = maps:get(<<"user-agent">>, Headers, <<"">>), From cc3e4e4dc58b34da102e8291b3d78b8c4175cef0 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 22:37:04 +0800 Subject: [PATCH 23/26] fix(saml): drop cert and key content and return path --- .../src/emqx_dashboard_sso_api.erl | 4 ++- .../src/emqx_dashboard_sso_saml.erl | 36 +++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) 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 6674db3a8..c19d2b66e 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -243,7 +243,9 @@ valid_config(Backend, #{<<"backend">> := Backend} = Config, Fun) -> 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; diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index bceb064f6..aa9f482c1 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -107,18 +107,17 @@ do_create( #{ dashboard_addr := DashboardAddr, idp_metadata_url := IDPMetadataURL, - key := KeyPath, - certificate := CertPath + 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", - Key = esaml_util:load_private_key(KeyPath), - Cert = esaml_util:load_certificate(CertPath), SP = esaml_sp:setup(#esaml_sp{ - key = Key, - certificate = Cert, - sp_sign_requests = true, + 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", @@ -135,7 +134,8 @@ do_create( }), try IdpMeta = esaml_util:load_metadata(binary_to_list(IDPMetadataURL)), - {ok, Config#{idp_meta => IdpMeta, sp => SP}} + State = Config, + {ok, State#{idp_meta => IdpMeta, sp => SP}} catch Kind:Error -> Reason = failed_to_load_metadata, @@ -202,18 +202,24 @@ do_validate_assertion(SP, DuplicateFun, Body) -> %%------------------------------------------------------------------------------ -define(DIR, <<"SAML_SSO_sp_certs">>). --define(RSA_KEYS_A, [sp_public_key, sp_private_key]). -ensure_cert_and_key(Config) -> +ensure_cert_and_key(#{sp_public_key := Cert, sp_private_key := Key} = Config) -> case - emqx_tls_lib:ensure_ssl_files(?DIR, Config#{enable => ture}, #{required_keys => ?RSA_KEYS_A}) + emqx_tls_lib:ensure_ssl_files( + ?DIR, #{enable => ture, certfile => Cert, keyfile => Key}, #{} + ) of - {ok, NConfig} -> - NConfig; - {error, #{which_options := [KeyPath | _]}} -> - error({missing_key, KeyPath}) + {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). From 80a6c1150d8537872911f94882df67c0fbe90da2 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 22:39:02 +0800 Subject: [PATCH 24/26] fix(saml): saml login reply role `viewer` as default --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index fb5e27fa4..105b69141 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml_api.erl @@ -96,8 +96,8 @@ sp_saml_callback(post, Req) -> {404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}; State -> case (provider(saml)):callback(Req, State) of - {ok, Token} -> - {200, emqx_dashboard_sso_api:login_reply(Token)}; + {ok, Role, Token} -> + {200, emqx_dashboard_sso_api:login_reply(Role, Token)}; {error, Reason} -> ?SLOG(info, #{ msg => "dashboard_saml_sso_login_failed", From 1dddccb448f8521dd37c9cdf3190af9f4fcd454e Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 22 Sep 2023 22:49:08 +0800 Subject: [PATCH 25/26] fix(saml): cert files cleanup when destroy --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index aa9f482c1..96654b7f7 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -29,6 +29,8 @@ -dialyzer({nowarn_function, create/1}). +-define(DIR, <<"saml_sp_certs">>). + %%------------------------------------------------------------------------------ %% Hocon Schema %%------------------------------------------------------------------------------ @@ -148,6 +150,7 @@ update(Config0, State) -> create(Config0). destroy(_State) -> + _ = file:del_dir_r(emqx_tls_lib:pem_dir(?DIR)), _ = application:stop(esaml), ok. @@ -201,8 +204,6 @@ do_validate_assertion(SP, DuplicateFun, Body) -> %% Internal functions %%------------------------------------------------------------------------------ --define(DIR, <<"SAML_SSO_sp_certs">>). - ensure_cert_and_key(#{sp_public_key := Cert, sp_private_key := Key} = Config) -> case emqx_tls_lib:ensure_ssl_files( From f8d06614c07945d44cbdbd081ebda0d78a92c6a4 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Sat, 23 Sep 2023 07:34:04 +0800 Subject: [PATCH 26/26] chore: fix dialyzer warnings --- apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl index 96654b7f7..edfb51712 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_saml.erl @@ -27,7 +27,7 @@ -export([login/2, callback/2]). --dialyzer({nowarn_function, create/1}). +-dialyzer({nowarn_function, do_create/1}). -define(DIR, <<"saml_sp_certs">>).