From d9466eef6318d27b964a2ab0683cad72d2e7446c Mon Sep 17 00:00:00 2001 From: JimMoen Date: Tue, 19 Sep 2023 10:15:12 +0800 Subject: [PATCH 01/20] 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 08/20] 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 09/20] 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 10/20] 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 11/20] 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 12/20] 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 13/20] 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 14/20] 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 15/20] 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 16/20] 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 17/20] 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 18/20] 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 19/20] 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 20/20] 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">>).