From 2cddce54795efca664cf453c5196716f2305c7fd Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 19 Sep 2023 14:10:29 +0800 Subject: [PATCH 1/4] feat(dashboard): add SSO feature and integrate with LDAP --- .../emqx_dashboard/include/emqx_dashboard.hrl | 13 +- .../src/emqx_dashboard_admin.erl | 27 +- apps/emqx_dashboard_sso/BSL.txt | 94 ++++++ apps/emqx_dashboard_sso/README.md | 11 + apps/emqx_dashboard_sso/docker-ct | 1 + apps/emqx_dashboard_sso/rebar.config | 7 + .../src/emqx_dashboard_sso.app.src | 19 ++ .../src/emqx_dashboard_sso.erl | 45 +++ .../src/emqx_dashboard_sso_api.erl | 235 +++++++++++++++ .../src/emqx_dashboard_sso_app.erl | 18 ++ .../src/emqx_dashboard_sso_ldap.erl | 134 +++++++++ .../src/emqx_dashboard_sso_manager.erl | 269 ++++++++++++++++++ .../src/emqx_dashboard_sso_schema.erl | 82 ++++++ .../src/emqx_dashboard_sso_sup.erl | 22 ++ .../src/emqx_enterprise_schema.erl | 3 +- apps/emqx_machine/priv/reboot_lists.eterm | 3 +- mix.exs | 3 +- rebar.config.erl | 1 + rel/i18n/emqx_dashboard_sso_api.hocon | 50 ++++ rel/i18n/emqx_dashboard_sso_ldap.hocon | 11 + rel/i18n/emqx_dashboard_sso_schema.hocon | 11 + 21 files changed, 1046 insertions(+), 13 deletions(-) create mode 100644 apps/emqx_dashboard_sso/BSL.txt create mode 100644 apps/emqx_dashboard_sso/README.md create mode 100644 apps/emqx_dashboard_sso/docker-ct create mode 100644 apps/emqx_dashboard_sso/rebar.config create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso.app.src create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl create mode 100644 apps/emqx_dashboard_sso/src/emqx_dashboard_sso_sup.erl create mode 100644 rel/i18n/emqx_dashboard_sso_api.hocon create mode 100644 rel/i18n/emqx_dashboard_sso_ldap.hocon create mode 100644 rel/i18n/emqx_dashboard_sso_schema.hocon diff --git a/apps/emqx_dashboard/include/emqx_dashboard.hrl b/apps/emqx_dashboard/include/emqx_dashboard.hrl index 8ebb4d3d5..8cb1853c9 100644 --- a/apps/emqx_dashboard/include/emqx_dashboard.hrl +++ b/apps/emqx_dashboard/include/emqx_dashboard.hrl @@ -22,18 +22,23 @@ %% a predefined configuration would replace these macros. -define(ROLE_VIEWER, <<"viewer">>). -define(ROLE_SUPERUSER, <<"superuser">>). - -define(ROLE_DEFAULT, ?ROLE_SUPERUSER). +-define(SSO_USERNAME(Backend, Name), {Backend, Name}). + +-type dashboard_sso_backend() :: atom(). +-type dashboard_sso_username() :: {dashboard_sso_backend(), binary()}. +-type dashboard_username() :: binary() | dashboard_sso_username(). +-type dashboard_user_role() :: binary(). + -record(?ADMIN, { - username :: binary(), + username :: dashboard_username(), pwdhash :: binary(), description :: binary(), - role = ?ROLE_DEFAULT :: binary(), + role = ?ROLE_DEFAULT :: dashboard_user_role(), extra = #{} :: map() }). --type dashboard_user_role() :: binary(). -type dashboard_user() :: #?ADMIN{}. -define(ADMIN_JWT, emqx_admin_jwt). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index 06dac9a01..d5e83b293 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -60,6 +60,10 @@ -export([backup_tables/0]). +-if(?EMQX_RELEASE_EDITION == ee). +-export([add_sso_user/4, lookup_user/2]). +-endif. + -type emqx_admin() :: #?ADMIN{}. %%-------------------------------------------------------------------- @@ -99,10 +103,11 @@ add_default_user() -> %% API %%-------------------------------------------------------------------- --spec add_user(binary(), binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, any()}. -add_user(Username, Password, Role, Desc) when - is_binary(Username), is_binary(Password) --> + + +-spec add_user(dashboard_username(), binary(), dashboard_user_role(), binary()) -> + {ok, map()} | {error, any()}. +add_user(Username, Password, Role, Desc) when is_binary(Password) -> case {legal_username(Username), legal_password(Password), legal_role(Role)} of {ok, ok, ok} -> do_add_user(Username, Password, Role, Desc); {{error, Reason}, _, _} -> {error, Reason}; @@ -115,6 +120,8 @@ do_add_user(Username, Password, Role, Desc) -> return(Res). %% 0-9 or A-Z or a-z or $_ +legal_username(?SSO_USERNAME(_, _)) -> + ok; legal_username(<<>>) -> {error, <<"Username cannot be empty">>}; legal_username(UserName) -> @@ -312,8 +319,8 @@ update_pwd(Username, Fun) -> end, return(mria:transaction(?DASHBOARD_SHARD, Trans)). --spec lookup_user(binary()) -> [emqx_admin()]. -lookup_user(Username) when is_binary(Username) -> +-spec lookup_user(dashboard_username()) -> [emqx_admin()]. +lookup_user(Username) -> Fun = fun() -> mnesia:read(?ADMIN, Username) end, {atomic, User} = mria:ro_transaction(?DASHBOARD_SHARD, Fun), User. @@ -410,6 +417,14 @@ legal_role(Role) -> role(Data) -> emqx_dashboard_rbac:role(Data). +-spec add_sso_user(atom(), binary(), dashboard_user_role(), binary()) -> + {ok, map()} | {error, any()}. +add_sso_user(Backend, Username, Role, Desc) -> + add_user(?SSO_USERNAME(Backend, Username), <<>>, Role, Desc). + +-spec lookup_user(dashboard_sso_backend(), binary()) -> [emqx_admin()]. +lookup_user(Backend, Username) when is_atom(Backend) -> + lookup_user(?SSO_USERNAME(Backend, Username)). -else. -dialyzer({no_match, [add_user/4, update_user/3]}). diff --git a/apps/emqx_dashboard_sso/BSL.txt b/apps/emqx_dashboard_sso/BSL.txt new file mode 100644 index 000000000..0acc0e696 --- /dev/null +++ b/apps/emqx_dashboard_sso/BSL.txt @@ -0,0 +1,94 @@ +Business Source License 1.1 + +Licensor: Hangzhou EMQ Technologies Co., Ltd. +Licensed Work: EMQX Enterprise Edition + The Licensed Work is (c) 2023 + Hangzhou EMQ Technologies Co., Ltd. +Additional Use Grant: Students and educators are granted right to copy, + modify, and create derivative work for research + or education. +Change Date: 2027-02-01 +Change License: Apache License, Version 2.0 + +For information about alternative licensing arrangements for the Software, +please contact Licensor: https://www.emqx.com/en/contact + +Notice + +The Business Source License (this document, or the “License”) is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. + +Covenants of Licensor + +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. + +3. To specify a Change Date. + +4. Not to modify this License in any other way. diff --git a/apps/emqx_dashboard_sso/README.md b/apps/emqx_dashboard_sso/README.md new file mode 100644 index 000000000..da4ff7c9a --- /dev/null +++ b/apps/emqx_dashboard_sso/README.md @@ -0,0 +1,11 @@ +# Dashboard Single sign-on + +Single Sign-On is a mechanism that allows a user to automatically sign in to multiple applications after signing in to one. This improves convenience and security. + +## Contributing + +Please see our [contributing.md](../../CONTRIBUTING.md). + +## License + +See [APL](../../APL.txt). diff --git a/apps/emqx_dashboard_sso/docker-ct b/apps/emqx_dashboard_sso/docker-ct new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/emqx_dashboard_sso/docker-ct @@ -0,0 +1 @@ + diff --git a/apps/emqx_dashboard_sso/rebar.config b/apps/emqx_dashboard_sso/rebar.config new file mode 100644 index 000000000..efc4a4539 --- /dev/null +++ b/apps/emqx_dashboard_sso/rebar.config @@ -0,0 +1,7 @@ +%% -*- mode: erlang; -*- + +{erl_opts, [debug_info]}. +{deps, [ + {emqx_ldap, {path, "../../apps/emqx_ldap"}}, + {emqx_dashboard, {path, "../../apps/emqx_dashboard"}} +]}. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.app.src b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.app.src new file mode 100644 index 000000000..b35f4e23a --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.app.src @@ -0,0 +1,19 @@ +{application, emqx_dashboard_sso, [ + {description, "EMQX Dashboard Single Sign-On"}, + {vsn, "0.1.0"}, + {registered, [emqx_dashboard_sso_sup]}, + {applications, [ + kernel, + stdlib, + emqx_dashboard, + emqx_ldap + ]}, + {mod, {emqx_dashboard_sso_app, []}}, + {env, []}, + {modules, []}, + {maintainers, ["EMQX Team "]}, + {links, [ + {"Homepage", "https://emqx.io/"}, + {"Github", "https://github.com/emqx/emqx-dashboard5"} + ]} +]}. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl new file mode 100644 index 000000000..957437161 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -0,0 +1,45 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso). + +-include_lib("hocon/include/hoconsc.hrl"). + +-export([types/0, modules/0, provider/1, backends/0]). +%%------------------------------------------------------------------------------ +%% Callbacks +%%------------------------------------------------------------------------------ +-type request() :: map(). +-type parsed_config() :: #{ + backend => atom(), + atom() => term() +}. +-type state() :: #{atom() => term()}. +-type raw_config() :: #{binary() => term()}. +-type config() :: parsed_config() | raw_config(). + +-callback hocon_ref() -> ?R_REF(Module :: atom(), Name :: atom() | binary()). +-callback login_ref() -> ?R_REF(Module :: atom(), Name :: atom() | binary()). +-callback create(Config :: config()) -> + {ok, State :: state()} | {error, Reason :: term()}. +-callback update(Config :: config(), State :: state()) -> + {ok, NewState :: state()} | {error, Reason :: term()}. +-callback destroy(State :: state()) -> ok. +-callback sign(request(), State :: state()) -> + {ok, Token :: binary()} | {error, Reason :: term()}. + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ +types() -> + maps:keys(backends()). + +modules() -> + maps:values(backends()). + +provider(Backend) -> + maps:get(Backend, backends()). + +backends() -> + #{ldap => emqx_dashboard_sso_ldap}. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl new file mode 100644 index 000000000..5bd6a9da3 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -0,0 +1,235 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso_api). + +-behaviour(minirest_api). + +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-import(hoconsc, [ + mk/2, + array/1, + enum/1, + ref/1, + union/1 +]). + +-export([ + api_spec/0, + fields/1, + paths/0, + schema/1, + namespace/0 +]). + +-export([ + running/2, + login/2, + sso/2, + backend/2 +]). + +-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). +-define(BAD_REQUEST, 'BAD_REQUEST'). + +-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 => true, translate_body => true}). + +paths() -> + [ + "/sso", + "/sso/login", + "/sso/running", + "/sso/:backend" + ]. + +schema("/sso") -> + #{ + 'operationId' => sso, + get => #{ + tags => [?TAGS], + desc => ?DESC(get_sso), + responses => #{ + 200 => array(ref(backend_status)) + } + } + }; +schema("/sso/login") -> + #{ + 'operationId' => login, + post => #{ + tags => [?TAGS], + desc => ?DESC(login), + 'requestBody' => login_union(), + responses => #{ + 200 => emqx_dashboard_api:fields([token, version, license]), + 401 => response_schema(401), + 404 => response_schema(404) + } + } + }; +schema("/sso/running") -> + #{ + 'operationId' => running, + get => #{ + tags => [?TAGS], + desc => ?DESC(get_running), + responses => #{ + 200 => array(enum(emqx_dashboard_sso:types())) + } + } + }; +schema("/sso/:backend") -> + #{ + 'operationId' => backend, + get => #{ + tags => [?TAGS], + desc => ?DESC(get_backend), + parameters => backend_name_in_path(), + responses => #{ + 200 => backend_union(), + 404 => response_schema(404) + } + }, + post => #{ + tags => [?TAGS], + desc => ?DESC(create_backend), + parameters => backend_name_in_path(), + 'requestBody' => backend_union(), + responses => #{ + 200 => backend_union() + } + }, + put => #{ + tags => [?TAGS], + desc => ?DESC(update_backend), + parameters => backend_name_in_path(), + 'requestBody' => backend_union(), + responses => #{ + 200 => backend_union(), + 404 => response_schema(404) + } + }, + delete => #{ + tags => [?TAGS], + desc => ?DESC(delete_backend), + parameters => backend_name_in_path(), + responses => #{ + 204 => <<"Delete successfully">>, + 404 => response_schema(404) + } + } + }. + +fields(backend_status) -> + emqx_dashboard_sso_schema:common_backend_schema(enum(emqx_dashboard_sso:types())). + +%% ------------------------------------------------------------------------------------------------- +%% API +running(get, _Request) -> + {200, emqx_dashboard_sso_manager:running()}. + +login(post, #{backend := Backend} = Request) -> + case emqx_dashboard_sso_manager:lookup_state(Backend) of + undefined -> + {404, ?BACKEND_NOT_FOUND}; + State -> + Provider = emqx_dashboard_sso:provider(Backend), + case Provider:login(Request, State) of + {ok, Token} -> + ?SLOG(info, #{msg => "Dashboard SSO login successfully", request => Request}), + Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), + {200, #{ + token => Token, + version => Version, + license => #{edition => emqx_release:edition()} + }}; + {error, Reason} -> + ?SLOG(info, #{ + msg => "Dashboard SSO login failed", request => Request, reason => Reason + }), + {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>} + end + end. + +sso(get, _Request) -> + SSO = emqx:get_config([dashboard_sso], #{}), + {200, + lists:map( + fun(Backend) -> + maps:with([backend, enabled], Backend) + end, + maps:values(SSO) + )}. + +backend(get, #{bindings := #{backend := Type}}) -> + case emqx:get_config([dashboard_sso, Type], undefined) of + undefined -> + {404, ?BACKEND_NOT_FOUND}; + Backend -> + {200, Backend} + end; +backend(create, #{bindings := #{backend := Backend}, body := Config}) -> + on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:create/2); +backend(put, #{bindings := #{backend := Backend}, body := Config}) -> + on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:update/2); +backend(delete, #{bindings := #{backend := Backend}, body := Config}) -> + on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:delete/2). + +%% ------------------------------------------------------------------------------------------------- +%% internal +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)). + +backend_union() -> + hoconsc:union([Mod:hocon_ref() || Mod <- emqx_dashboard_sso:modules()]). + +login_union() -> + hoconsc:union([Mod:login_ref() || Mod <- emqx_dashboard_sso:modules()]). + +backend_name_in_path() -> + [ + {name, + mk( + binary(), + #{ + in => path, + desc => ?DESC(backend_name_in_qs), + example => <<"ldap">> + } + )} + ]. + +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. + +handle_backend_update_result({ok, _}, Config) -> + {200, Config}; +handle_backend_update_result(ok, _) -> + 204; +handle_backend_update_result({error, not_exists}, _) -> + {404, ?BACKEND_NOT_FOUND}; +handle_backend_update_result({error, already_exists}, _) -> + {400, ?BAD_REQUEST, <<"Backend already exists.">>}; +handle_backend_update_result({error, Reason}, _) -> + {400, ?BAD_REQUEST, Reason}. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl new file mode 100644 index 000000000..2ad280b5e --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_app.erl @@ -0,0 +1,18 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso_app). + +-behaviour(application). + +-export([ + start/2, + stop/1 +]). + +start(_StartType, _StartArgs) -> + emqx_dashboard_sso_sup:start_link(). + +stop(_State) -> + ok. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl new file mode 100644 index 000000000..94762081c --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl @@ -0,0 +1,134 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso_ldap). + +-include_lib("emqx_dashboard/include/emqx_dashboard.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("eldap/include/eldap.hrl"). + +-behaviour(emqx_dashboard_sso). + +-export([ + fields/1 +]). + +-export([ + hocon_ref/0, + login_ref/0, + sign/2, + create/1, + update/2, + destroy/1 +]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +hocon_ref() -> + hoconsc:ref(?MODULE, ldap). + +login_ref() -> + hoconsc:ref(?MODULE, login). + +fields(ldap) -> + emqx_dashboard_sso_schema:common_backend_schema(ldap) ++ + [ + {query_timeout, fun query_timeout/1} + ] ++ + emqx_ldap:fields(config) ++ emqx_ldap:fields(bind_opts); +fields(login) -> + [ + emqx_dashboard_sso_schema:backend_schema(ldap) + | emqx_dashboard_sso_schema:username_password_schema() + ]. + +query_timeout(type) -> emqx_schema:timeout_duration_ms(); +query_timeout(desc) -> ?DESC(?FUNCTION_NAME); +query_timeout(default) -> <<"5s">>; +query_timeout(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +create(Config0) -> + ResourceId = emqx_dashboard_sso_manager:make_resource_id(ldap), + {Config, State} = parse_config(Config0), + case emqx_dashboard_sso_manager:create_resource(ResourceId, emqx_ldap, Config) of + {ok, _} -> + {ok, State#{resource_id => ResourceId}}; + Error -> + Error + end. + +update(Config0, #{resource_id := ResourceId} = _State) -> + {Config, NState} = parse_config(Config0), + case emqx_dashboard_sso_manager:update_resource(ResourceId, emqx_ldap, Config) of + {ok, _} -> + {ok, NState#{resource_id => ResourceId}}; + Error -> + Error + end. + +destroy(#{resource_id := ResourceId}) -> + _ = emqx_resource:remove_local(ResourceId), + ok. + +sign( + #{username := Username} = Req, + #{ + query_timeout := Timeout, + resource_id := ResourceId + } = _State +) -> + case + emqx_resource:simple_sync_query( + ResourceId, + {query, Req, [], Timeout} + ) + of + {ok, []} -> + {error, user_not_found}; + {ok, [_Entry | _]} -> + case + emqx_resource:simple_sync_query( + ResourceId, + {bind, Req} + ) + of + ok -> + User = ensure_user_exists(Username), + {ok, emqx_dashboard_token:sign(User, <<>>)}; + {error, _} = Error -> + Error + end; + {error, _} = Error -> + Error + end. + +parse_config(Config) -> + State = lists:foldl( + fun(Key, Acc) -> + case maps:find(Key, Config) of + {ok, Value} when is_binary(Value) -> + Acc#{Key := erlang:binary_to_list(Value)}; + _ -> + Acc + end + end, + Config, + [query_timeout] + ), + {Config, State}. + +ensure_user_exists(Username) -> + case emqx_dashboard_admin:lookup_user(ldap, Username) of + [User] -> + User; + [] -> + emqx_dashboard_admin:add_sso_user(ldap, Username, ?ROLE_VIEWER, <<>>) + end. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl new file mode 100644 index 000000000..63bb6536c --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl @@ -0,0 +1,269 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso_manager). + +-behaviour(gen_server). + +%% API +-export([start_link/0]). + +%% gen_server callbacks +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3, + format_status/2 +]). + +-export([ + running/0, + lookup_state/1, + make_resource_id/1, + create_resource/3, + update_resource/3, + call/1 +]). + +-export([ + create/2, + update/2, + delete/1, + pre_config_update/3, + post_config_update/5 +]). + +-import(emqx_dashboard_sso, [provider/1]). + +-define(MOD_KEY_PATH, [dashboard_sso]). +-define(RESOURCE_GROUP, <<"emqx_dashboard_sso">>). +-define(DEFAULT_RESOURCE_OPTS, #{ + start_after_created => false +}). + +-record(dashboard_sso, { + backend :: atom(), + state :: map() +}). + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +running() -> + maps:fold( + fun + (Type, #{enable := true}, Acc) -> + [Type | Acc]; + (_Type, _Cfg, Acc) -> + Acc + end, + [], + emqx:get_config([emqx_dashboard_sso]) + ). + +create(Backend, Config) -> + update_config(Backend, {?FUNCTION_NAME, Backend, Config}). +update(Backend, Config) -> + update_config(Backend, {?FUNCTION_NAME, Backend, Config}). +delete(Backend) -> + update_config(Backend, {?FUNCTION_NAME, Backend}). + +lookup_state(Backend) -> + case ets:lookup(dashboard_sso, Backend) of + [Data] -> + Data#dashboard_sso.state; + [] -> + undefined + end. + +make_resource_id(Backend) -> + BackendBin = bin(Backend), + emqx_resource:generate_id(BackendBin). + +create_resource(ResourceId, Module, Config) -> + Result = emqx_resource:create_local( + ResourceId, + ?RESOURCE_GROUP, + Module, + Config, + ?DEFAULT_RESOURCE_OPTS + ), + start_resource_if_enabled(Result, ResourceId, Config). + +update_resource(ResourceId, Module, Config) -> + Result = emqx_resource:recreate_local( + ResourceId, Module, Config, ?DEFAULT_RESOURCE_OPTS + ), + start_resource_if_enabled(ResourceId, Result, Config). + +call(Req) -> + gen_server:call(?MODULE, Req). + +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ +init([]) -> + process_flag(trap_exit, true), + emqx_conf:add_handler(?MOD_KEY_PATH, ?MODULE), + emqx_utils_ets:new( + dashboard_sso, + [ + set, + public, + named_table, + {keypos, #dashboard_sso.backend}, + {read_concurrency, true} + ] + ), + start_backend_services(), + {ok, #{}}. + +handle_call({update_config, Req, NewConf, OldConf}, _From, State) -> + Result = on_config_update(Req, NewConf, OldConf), + {reply, Result, State}; +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + emqx_conf:remove_handler(?MOD_KEY_PATH), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +format_status(_Opt, Status) -> + Status. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ +start_backend_services() -> + Backends = emqx_conf:get([dashboard_sso], #{}), + lists:foreach( + fun({Backend, Config}) -> + Provider = provider(Backend), + on_backend_updated( + Provider:create(Config), + fun(State) -> + ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State}) + end + ) + end, + maps:to_list(Backends) + ). + +update_config(Backend, UpdateReq) -> + case emqx_conf:update([dashboard_sso, Backend], UpdateReq, #{override_to => cluster}) of + {ok, UpdateResult} -> + #{post_config_update := #{?MODULE := Result}} = UpdateResult, + {ok, Result}; + Error -> + Error + end. + +pre_config_update(_Path, {create, Backend, Config}, OldConf) -> + case maps:find(Backend, OldConf) of + {ok, _} -> + throw(already_exists); + error -> + {ok, OldConf#{Backend => Config}} + end; +pre_config_update(_Path, {update, Backend, Config}, OldConf) -> + case maps:find(Backend, OldConf) of + error -> + throw(not_exists); + {ok, _} -> + {ok, OldConf#{Backend => Config}} + end; +pre_config_update(_Path, {delete, Backend}, OldConf) -> + case maps:find(Backend, OldConf) of + error -> + throw(not_exists); + {ok, _} -> + {ok, maps:remove(Backend, OldConf)} + end. + +post_config_update(_Path, UpdateReq, NewConf, OldConf, _AppEnvs) -> + Result = call({update_config, UpdateReq, NewConf, OldConf}), + {ok, Result}. + +on_config_update({create, Backend, Config}, _NewConf, _OldConf) -> + case lookup(Backend) of + undefined -> + Provider = provider(Backend), + on_backend_updated( + Provider:create(Config), + fun(State) -> + ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State}) + end + ); + _Data -> + {error, already_exists} + end; +on_config_update({update, Backend, Config}, _NewConf, _OldConf) -> + case lookup(Backend) of + undefined -> + {error, not_exists}; + Data -> + Provider = provider(Backend), + on_backend_updated( + Provider:update(Config, Data#dashboard_sso.state), + fun(State) -> + ets:insert(dashboard_sso, Data#dashboard_sso{state = State}) + end + ) + end; +on_config_update({delete, Backend}, _NewConf, _OldConf) -> + case lookup(Backend) of + undefined -> + {error, not_exists}; + Data -> + Provider = provider(Backend), + on_backend_updated( + Provider:destroy(Data#dashboard_sso.state), + fun() -> + ets:delete(dashboard_sso, Backend) + end + ) + end. + +lookup(Backend) -> + case ets:lookup(dashboard_sso, Backend) of + [Data] -> + Data; + [] -> + undefined + end. + +start_resource_if_enabled(ResourceId, {ok, _} = Result, #{enable := true}) -> + _ = emqx_resource:start(ResourceId), + Result; +start_resource_if_enabled(_ResourceId, Result, _Config) -> + Result. + +on_backend_updated({ok, State} = Ok, Fun) -> + Fun(State), + Ok; +on_backend_updated(ok, Fun) -> + Fun(), + ok; +on_backend_updated(Error, _) -> + Error. + +bin(A) when is_atom(A) -> atom_to_binary(A, utf8); +bin(L) when is_list(L) -> list_to_binary(L); +bin(X) -> X. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl new file mode 100644 index 000000000..ce9a53557 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl @@ -0,0 +1,82 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso_schema). + +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("typerefl/include/types.hrl"). + +%% Hocon +-export([namespace/0, roots/0, fields/1, tags/0, desc/1]). +-export([ + common_backend_schema/1, + backend_schema/1, + username_password_schema/0 +]). +-import(hoconsc, [ref/2, mk/2]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ +namespace() -> dashboard_sso. + +tags() -> + [<<"Dashboard Single Sign-On">>]. + +roots() -> [dashboard_sso]. + +fields(dashboard_sso) -> + lists:map( + fun({Type, Module}) -> + {Type, mk(Module:hocon_ref(), #{required => {false, recursively}})} + end, + maps:to_list(emqx_dashboard_sso:backends()) + ). + +desc(dashboard_sso) -> + "Dashboard Single Sign-On"; +desc(_) -> + undefined. + +common_backend_schema(Backend) -> + [ + {enable, + mk( + boolean(), #{ + desc => ?DESC(backend_enable), + required => false, + default => false + } + )}, + backend_schema(Backend) + ]. + +backend_schema(Backend) -> + {backend, + mk(Backend, #{ + required => true, + desc => ?DESC(backend) + })}. + +username_password_schema() -> + [ + {username, + mk( + binary(), + #{ + desc => ?DESC(username), + 'maxLength' => 100, + example => <<"admin">> + } + )}, + {password, + mk( + binary(), + #{ + desc => ?DESC(password), + 'maxLength' => 100, + example => <<"public">> + } + )} + ]. diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_sup.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_sup.erl new file mode 100644 index 000000000..fb3f5a68d --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_sup.erl @@ -0,0 +1,22 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +-define(CHILD(I, ShutDown), {I, {I, start_link, []}, permanent, ShutDown, worker, [I]}). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + {ok, + {{one_for_one, 5, 100}, [ + ?CHILD(emqx_dashboard_sso_manager, 5000) + ]}}. diff --git a/apps/emqx_enterprise/src/emqx_enterprise_schema.erl b/apps/emqx_enterprise/src/emqx_enterprise_schema.erl index a801825e0..c238dcea4 100644 --- a/apps/emqx_enterprise/src/emqx_enterprise_schema.erl +++ b/apps/emqx_enterprise/src/emqx_enterprise_schema.erl @@ -11,7 +11,8 @@ -define(EE_SCHEMA_MODULES, [ emqx_license_schema, emqx_schema_registry_schema, - emqx_ft_schema + emqx_ft_schema, + emqx_dashboard_sso_schema ]). namespace() -> diff --git a/apps/emqx_machine/priv/reboot_lists.eterm b/apps/emqx_machine/priv/reboot_lists.eterm index d794dabd9..b90c52d36 100644 --- a/apps/emqx_machine/priv/reboot_lists.eterm +++ b/apps/emqx_machine/priv/reboot_lists.eterm @@ -115,7 +115,8 @@ emqx_ft, emqx_ldap, emqx_gcp_device, - emqx_dashboard_rbac + emqx_dashboard_rbac, + emqx_dashboard_sso ], %% must always be of type `load' ce_business_apps => diff --git a/mix.exs b/mix.exs index 68365f44c..451eacaad 100644 --- a/mix.exs +++ b/mix.exs @@ -227,7 +227,8 @@ defmodule EMQXUmbrella.MixProject do :emqx_bridge_azure_event_hub, :emqx_ldap, :emqx_gcp_device, - :emqx_dashboard_rbac + :emqx_dashboard_rbac, + :emqx_dashboard_sso ]) end diff --git a/rebar.config.erl b/rebar.config.erl index 255a8e3cc..c81c40eb7 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -110,6 +110,7 @@ is_community_umbrella_app("apps/emqx_bridge_azure_event_hub") -> false; is_community_umbrella_app("apps/emqx_ldap") -> false; is_community_umbrella_app("apps/emqx_gcp_device") -> false; is_community_umbrella_app("apps/emqx_dashboard_rbac") -> false; +is_community_umbrella_app("apps/emqx_dashboard_sso") -> false; is_community_umbrella_app(_) -> true. is_jq_supported() -> diff --git a/rel/i18n/emqx_dashboard_sso_api.hocon b/rel/i18n/emqx_dashboard_sso_api.hocon new file mode 100644 index 000000000..122ef0085 --- /dev/null +++ b/rel/i18n/emqx_dashboard_sso_api.hocon @@ -0,0 +1,50 @@ +emqx_dashboard_api { + +get_sso.desc: +"""List all SSO backends""" +get_sso.label: +"""SSO Backends""" + +login.desc: +"""Get Dashboard Auth Token.""" +login.label: +"""Get Dashboard Auth Token.""" + +get_running.desc: +"""Get Running SSO backends""" +get_running.label: +"""Running SSO""" + +get_backend.desc: +"""Get details of a backend""" +get_backend.label: +"""Backend Details""" + +create_backend.desc: +"""Create a backend""" +create_backend.label: +"""Create Backend""" + +update_backend.desc: +"""Update a backend""" +update_backend.label: +"""Update Backend""" + +delete_backend.desc: +"""Delete a backend""" +delete_backend.label: +"""Delete Backend""" + +login_failed401.desc: +"""Login failed. Bad username or password""" + +backend_not_found.desc: +"""Operate failed. Backend not exists""" + +backend_name.desc: +"""Backend name""" + +backend_name.label: +"""Backend Name""" + +} diff --git a/rel/i18n/emqx_dashboard_sso_ldap.hocon b/rel/i18n/emqx_dashboard_sso_ldap.hocon new file mode 100644 index 000000000..f15975416 --- /dev/null +++ b/rel/i18n/emqx_dashboard_sso_ldap.hocon @@ -0,0 +1,11 @@ +emqx_dashboard_sso_ldap { + +ldap_bind.desc: +"""Configuration of authenticator using the LDAP bind operation as the authentication method.""" + +query_timeout.desc: +"""Timeout for the LDAP query.""" + +query_timeout.label: +"""Query Timeout""" +} diff --git a/rel/i18n/emqx_dashboard_sso_schema.hocon b/rel/i18n/emqx_dashboard_sso_schema.hocon new file mode 100644 index 000000000..72d35a760 --- /dev/null +++ b/rel/i18n/emqx_dashboard_sso_schema.hocon @@ -0,0 +1,11 @@ +emqx_dashboard_sso_schema { + +backend_enable.desc: +"""Whether to enable this backend.""" + +backend.desc: +"""Backend type.""" + +backend.label: +"""Backend Type""" +} From 285e5297667f88e1d4845c52fb6f572c24526c2d Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 19 Sep 2023 19:15:52 +0800 Subject: [PATCH 2/4] fix(sso): fix sso errors found when manual test --- .../src/emqx_dashboard_admin.erl | 43 ++++++--- .../emqx_dashboard/src/emqx_dashboard_api.erl | 64 +++++++++---- .../src/emqx_dashboard_token.erl | 10 ++- apps/emqx_dashboard_sso/README.md | 4 +- .../src/emqx_dashboard_sso.erl | 2 +- .../src/emqx_dashboard_sso_api.erl | 89 +++++++++---------- .../src/emqx_dashboard_sso_ldap.erl | 26 +++--- .../src/emqx_dashboard_sso_manager.erl | 49 ++++------ .../src/emqx_dashboard_sso_schema.erl | 5 +- .../src/emqx_enterprise.app.src | 2 +- apps/emqx_ldap/src/emqx_ldap.erl | 10 ++- rel/i18n/emqx_dashboard_api.hocon | 6 ++ rel/i18n/emqx_dashboard_sso_api.hocon | 10 --- 13 files changed, 180 insertions(+), 140 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index d5e83b293..3ae2a33e1 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -103,11 +103,9 @@ add_default_user() -> %% API %%-------------------------------------------------------------------- - - -spec add_user(dashboard_username(), binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, any()}. -add_user(Username, Password, Role, Desc) when is_binary(Password) -> +add_user(Username, Password, Role, Desc) when is_binary(Username), is_binary(Password) -> case {legal_username(Username), legal_password(Password), legal_role(Role)} of {ok, ok, ok} -> do_add_user(Username, Password, Role, Desc); {{error, Reason}, _, _} -> {error, Reason}; @@ -120,8 +118,6 @@ do_add_user(Username, Password, Role, Desc) -> return(Res). %% 0-9 or A-Z or a-z or $_ -legal_username(?SSO_USERNAME(_, _)) -> - ok; legal_username(<<>>) -> {error, <<"Username cannot be empty">>}; legal_username(UserName) -> @@ -211,7 +207,7 @@ add_user_(Username, Password, Role, Desc) -> description = Desc }, mnesia:write(Admin), - #{username => Username, role => Role, description => Desc}; + flatten_username(#{username => Username, role => Role, description => Desc}); [_] -> mnesia:abort(<<"username_already_exist">>) end. @@ -232,7 +228,8 @@ remove_user(Username) when is_binary(Username) -> {error, Reason} end. --spec update_user(binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, term()}. +-spec update_user(dashboard_username(), dashboard_user_role(), binary()) -> + {ok, map()} | {error, term()}. update_user(Username, Role, Desc) when is_binary(Username) -> case legal_role(Role) of ok -> @@ -279,7 +276,10 @@ update_user_(Username, Role, Desc) -> mnesia:abort(<<"username_not_found">>); [Admin] -> mnesia:write(Admin#?ADMIN{role = Role, description = Desc}), - {role(Admin) =:= Role, #{username => Username, role => Role, description => Desc}} + { + role(Admin) =:= Role, + flatten_username(#{username => Username, role => Role, description => Desc}) + } end. change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) -> @@ -335,11 +335,11 @@ all_users() -> role = Role } ) -> - #{ + flatten_username(#{ username => Username, description => Desc, role => ensure_role(Role) - } + }) end, ets:tab2list(?ADMIN) ). @@ -417,10 +417,24 @@ legal_role(Role) -> role(Data) -> emqx_dashboard_rbac:role(Data). --spec add_sso_user(atom(), binary(), dashboard_user_role(), binary()) -> +flatten_username(#{username := ?SSO_USERNAME(Backend, Name)} = Data) -> + Data#{ + username := Name, + backend => Backend + }; +flatten_username(#{username := Username} = Data) when is_binary(Username) -> + Data#{backend => local}. + +-spec add_sso_user(dashboard_sso_backend(), binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, any()}. -add_sso_user(Backend, Username, Role, Desc) -> - add_user(?SSO_USERNAME(Backend, Username), <<>>, Role, Desc). +add_sso_user(Backend, Username0, Role, Desc) when is_binary(Username0) -> + case legal_role(Role) of + ok -> + Username = ?SSO_USERNAME(Backend, Username0), + do_add_user(Username, <<>>, Role, Desc); + {error, _} = Error -> + Error + end. -spec lookup_user(dashboard_sso_backend(), binary()) -> [emqx_admin()]. lookup_user(Backend, Username) when is_atom(Backend) -> @@ -434,6 +448,9 @@ legal_role(_) -> role(_) -> ?ROLE_DEFAULT. + +flatten_username(Data) -> + Data. -endif. -ifdef(TEST). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 9ed6d1a77..3d68fd098 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -89,6 +89,7 @@ schema("/logout") -> post => #{ tags => [<<"dashboard">>], desc => ?DESC(logout_api), + parameters => sso_parameters(), 'requestBody' => fields([username]), responses => #{ 204 => <<"Dashboard logout successfully">>, @@ -114,7 +115,7 @@ schema("/users") -> desc => ?DESC(create_user_api), 'requestBody' => fields([username, password, role, description]), responses => #{ - 200 => fields([username, role, description]) + 200 => fields([username, role, description, backend]) } } }; @@ -124,17 +125,17 @@ schema("/users/:username") -> put => #{ tags => [<<"dashboard">>], desc => ?DESC(update_user_api), - parameters => fields([username_in_path]), + parameters => sso_parameters(fields([username_in_path])), 'requestBody' => fields([role, description]), responses => #{ - 200 => fields([username, role, description]), + 200 => fields([username, role, description, backend]), 404 => response_schema(404) } }, delete => #{ tags => [<<"dashboard">>], desc => ?DESC(delete_user_api), - parameters => fields([username_in_path]), + parameters => sso_parameters(fields([username_in_path])), responses => #{ 204 => <<"Delete User successfully">>, 400 => emqx_dashboard_swagger:error_codes( @@ -169,7 +170,7 @@ response_schema(404) -> emqx_dashboard_swagger:error_codes([?USER_NOT_FOUND], ?DESC(users_api404)). fields(user) -> - fields([username, description]); + fields([username, role, description, backend]); fields(List) -> [field(Key) || Key <- List, field_filter(Key)]. @@ -206,7 +207,10 @@ field(old_pwd) -> field(new_pwd) -> {new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})}; field(role) -> - {role, mk(binary(), #{desc => ?DESC(role), example => ?ROLE_DEFAULT})}. + {role, + mk(binary(), #{desc => ?DESC(role), default => ?ROLE_DEFAULT, example => ?ROLE_DEFAULT})}; +field(backend) -> + {backend, mk(binary(), #{desc => ?DESC(backend), example => <<"local">>})}. %% ------------------------------------------------------------------------------------------------- %% API @@ -229,15 +233,16 @@ login(post, #{body := Params}) -> end. logout(_, #{ - body := #{<<"username">> := Username}, + body := #{<<"username">> := Username0} = Req, headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>} }) -> + Username = username(Req, Username0), case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of ok -> - ?SLOG(info, #{msg => "Dashboard logout successfully", username => Username}), + ?SLOG(info, #{msg => "Dashboard logout successfully", username => Username0}), 204; _R -> - ?SLOG(info, #{msg => "Dashboard logout failed.", username => Username}), + ?SLOG(info, #{msg => "Dashboard logout failed.", username => Username0}), {401, ?WRONG_TOKEN_OR_USERNAME, <<"Ensure your token & username">>} end. @@ -266,9 +271,10 @@ users(post, #{body := Params}) -> end end. -user(put, #{bindings := #{username := Username}, body := Params}) -> +user(put, #{bindings := #{username := Username0}, body := Params} = Req) -> Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT), Desc = maps:get(<<"description">>, Params), + Username = username(Req, Username0), case emqx_dashboard_admin:update_user(Username, Role, Desc) of {ok, Result} -> {200, filter_result(Result)}; @@ -277,14 +283,15 @@ user(put, #{bindings := #{username := Username}, body := Params}) -> {error, Reason} -> {400, ?BAD_REQUEST, Reason} end; -user(delete, #{bindings := #{username := Username}, headers := Headers}) -> - case Username == emqx_dashboard_admin:default_username() of +user(delete, #{bindings := #{username := Username0}, headers := Headers} = Req) -> + case Username0 == emqx_dashboard_admin:default_username() of true -> - ?SLOG(info, #{msg => "Dashboard delete admin user failed", username => Username}), - Message = list_to_binary(io_lib:format("Cannot delete user ~p", [Username])), + ?SLOG(info, #{msg => "Dashboard delete admin user failed", username => Username0}), + Message = list_to_binary(io_lib:format("Cannot delete user ~p", [Username0])), {400, ?NOT_ALLOWED, Message}; false -> - case is_self_auth(Username, Headers) of + Username = username(Req, Username0), + case is_self_auth(Username0, Headers) of true -> {400, ?NOT_ALLOWED, <<"Cannot delete self">>}; false -> @@ -293,13 +300,15 @@ user(delete, #{bindings := #{username := Username}, headers := Headers}) -> {404, ?USER_NOT_FOUND, Reason}; {ok, _} -> ?SLOG(info, #{ - msg => "Dashboard delete admin user", username => Username + msg => "Dashboard delete admin user", username => Username0 }), {204} end end end. +is_self_auth(?SSO_USERNAME(_, _), _) -> + fasle; is_self_auth(Username, #{<<"authorization">> := Token}) -> is_self_auth(Username, Token); is_self_auth(Username, #{<<"Authorization">> := Token}) -> @@ -362,6 +371,19 @@ field_filter(_) -> filter_result(Result) -> Result. +sso_parameters() -> + sso_parameters([]). + +sso_parameters(Params) -> + emqx_dashboard_sso_api:sso_parameters(Params). + +username(#{bindings := #{backend := local}}, Username) -> + Username; +username(#{bindings := #{backend := Backend}}, Username) -> + ?SSO_USERNAME(Backend, Username); +username(_Req, Username) -> + Username. + -else. field_filter(role) -> @@ -372,6 +394,14 @@ field_filter(_) -> filter_result(Result) when is_list(Result) -> lists:map(fun filter_result/1, Result); filter_result(Result) -> - maps:without([role], Result). + maps:without([role, backend], Result). +sso_parameters() -> + sso_parameters([]). + +sso_parameters(Any) -> + Any. + +username(_Req, Username) -> + Username. -endif. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index f71df77bd..b45df1930 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -179,6 +179,9 @@ owner(Token) -> {atomic, []} -> {error, not_found} end. +jwk(?SSO_USERNAME(Backend, Name), Password, Salt) -> + BackendBin = erlang:atom_to_binary(Backend), + jwk(<>, Password, Salt); jwk(Username, Password, Salt) -> Key = crypto:hash(md5, <>), #{ @@ -192,12 +195,17 @@ jwt_expiration_time() -> token_ttl() -> emqx_conf:get([dashboard, token_expired_time], ?EXPTIME). +format(Token, ?SSO_USERNAME(Backend, Name), Role, ExpTime) -> + format(Token, Backend, Name, Role, ExpTime); format(Token, Username, Role, ExpTime) -> + format(Token, local, Username, Role, ExpTime). + +format(Token, Backend, Username, Role, ExpTime) -> #?ADMIN_JWT{ token = Token, username = Username, exptime = ExpTime, - extra = #{role => Role} + extra = #{role => Role, backend => Backend} }. %%-------------------------------------------------------------------- diff --git a/apps/emqx_dashboard_sso/README.md b/apps/emqx_dashboard_sso/README.md index da4ff7c9a..530117a27 100644 --- a/apps/emqx_dashboard_sso/README.md +++ b/apps/emqx_dashboard_sso/README.md @@ -1,4 +1,4 @@ -# Dashboard Single sign-on +# Dashboard Single sign-On Single Sign-On is a mechanism that allows a user to automatically sign in to multiple applications after signing in to one. This improves convenience and security. @@ -8,4 +8,4 @@ Please see our [contributing.md](../../CONTRIBUTING.md). ## License -See [APL](../../APL.txt). +See EMQ Business Source License 1.1, refer to [LICENSE](BSL.txt). diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl index 957437161..287082414 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -26,7 +26,7 @@ -callback update(Config :: config(), State :: state()) -> {ok, NewState :: state()} | {error, Reason :: term()}. -callback destroy(State :: state()) -> ok. --callback sign(request(), State :: state()) -> +-callback login(request(), State :: state()) -> {ok, Token :: binary()} | {error, Reason :: term()}. %%------------------------------------------------------------------------------ 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 5bd6a9da3..dd473931c 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -27,12 +27,13 @@ ]). -export([ - running/2, login/2, sso/2, backend/2 ]). +-export([sso_parameters/1]). + -define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). -define(BAD_REQUEST, 'BAD_REQUEST'). @@ -47,8 +48,7 @@ api_spec() -> paths() -> [ "/sso", - "/sso/login", - "/sso/running", + "/sso/login/:backend", "/sso/:backend" ]. @@ -59,16 +59,17 @@ schema("/sso") -> tags => [?TAGS], desc => ?DESC(get_sso), responses => #{ - 200 => array(ref(backend_status)) + 200 => array(enum(emqx_dashboard_sso:types())) } } }; -schema("/sso/login") -> +schema("/sso/login/:backend") -> #{ 'operationId' => login, post => #{ tags => [?TAGS], desc => ?DESC(login), + parameters => backend_name_in_path(), 'requestBody' => login_union(), responses => #{ 200 => emqx_dashboard_api:fields([token, version, license]), @@ -77,17 +78,6 @@ schema("/sso/login") -> } } }; -schema("/sso/running") -> - #{ - 'operationId' => running, - get => #{ - tags => [?TAGS], - desc => ?DESC(get_running), - responses => #{ - 200 => array(enum(emqx_dashboard_sso:types())) - } - } - }; schema("/sso/:backend") -> #{ 'operationId' => backend, @@ -100,15 +90,6 @@ schema("/sso/:backend") -> 404 => response_schema(404) } }, - post => #{ - tags => [?TAGS], - desc => ?DESC(create_backend), - parameters => backend_name_in_path(), - 'requestBody' => backend_union(), - responses => #{ - 200 => backend_union() - } - }, put => #{ tags => [?TAGS], desc => ?DESC(update_backend), @@ -131,22 +112,19 @@ schema("/sso/:backend") -> }. fields(backend_status) -> - emqx_dashboard_sso_schema:common_backend_schema(enum(emqx_dashboard_sso:types())). + emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()). %% ------------------------------------------------------------------------------------------------- %% API -running(get, _Request) -> - {200, emqx_dashboard_sso_manager:running()}. - -login(post, #{backend := Backend} = Request) -> +login(post, #{bindings := #{backend := Backend}, body := Sign}) -> case emqx_dashboard_sso_manager:lookup_state(Backend) of undefined -> - {404, ?BACKEND_NOT_FOUND}; + {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; State -> Provider = emqx_dashboard_sso:provider(Backend), - case Provider:login(Request, State) of + case Provider:login(Sign, State) of {ok, Token} -> - ?SLOG(info, #{msg => "Dashboard SSO login successfully", request => Request}), + ?SLOG(info, #{msg => "Dashboard SSO login successfully", request => Sign}), Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), {200, #{ token => Token, @@ -155,7 +133,9 @@ login(post, #{backend := Backend} = Request) -> }}; {error, Reason} -> ?SLOG(info, #{ - msg => "Dashboard SSO login failed", request => Request, reason => Reason + msg => "Dashboard SSO login failed", + request => Sign, + reason => Reason }), {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>} end @@ -166,7 +146,7 @@ sso(get, _Request) -> {200, lists:map( fun(Backend) -> - maps:with([backend, enabled], Backend) + maps:with([backend, enable], Backend) end, maps:values(SSO) )}. @@ -176,14 +156,15 @@ backend(get, #{bindings := #{backend := Type}}) -> undefined -> {404, ?BACKEND_NOT_FOUND}; Backend -> - {200, Backend} + {200, to_json(Backend)} end; -backend(create, #{bindings := #{backend := Backend}, body := Config}) -> - on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:create/2); backend(put, #{bindings := #{backend := Backend}, body := Config}) -> on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:update/2); -backend(delete, #{bindings := #{backend := Backend}, body := Config}) -> - on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:delete/2). +backend(delete, #{bindings := #{backend := Backend}}) -> + handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined). + +sso_parameters(Params) -> + backend_name_as_arg(query, [local], <<"local">>) ++ Params. %% ------------------------------------------------------------------------------------------------- %% internal @@ -199,14 +180,18 @@ login_union() -> hoconsc:union([Mod:login_ref() || Mod <- emqx_dashboard_sso:modules()]). backend_name_in_path() -> + backend_name_as_arg(path, [], <<"ldap">>). + +backend_name_as_arg(In, Extra, Default) -> [ - {name, + {backend, mk( - binary(), + enum(Extra ++ emqx_dashboard_sso:types()), #{ - in => path, + in => In, desc => ?DESC(backend_name_in_qs), - example => <<"ldap">> + required => true, + example => Default } )} ]. @@ -216,7 +201,7 @@ on_backend_update(Backend, Config, Fun) -> handle_backend_update_result(Result, Config). valid_config(Backend, Config, Fun) -> - case maps:get(backend, Config, undefined) of + case maps:get(<<"backend">>, Config, undefined) of Backend -> Fun(Backend, Config); _ -> @@ -224,12 +209,20 @@ valid_config(Backend, Config, Fun) -> end. handle_backend_update_result({ok, _}, Config) -> - {200, Config}; + {200, to_json(Config)}; handle_backend_update_result(ok, _) -> 204; handle_backend_update_result({error, not_exists}, _) -> - {404, ?BACKEND_NOT_FOUND}; + {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; handle_backend_update_result({error, already_exists}, _) -> - {400, ?BAD_REQUEST, <<"Backend already exists.">>}; + {400, ?BAD_REQUEST, <<"Backend already exists">>}; handle_backend_update_result({error, Reason}, _) -> {400, ?BAD_REQUEST, Reason}. + +to_json(Data) -> + emqx_utils_maps:jsonable_map( + Data, + fun(K, V) -> + {K, emqx_utils_maps:binary_string(V)} + end + ). 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 94762081c..dc7e6a74a 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl @@ -18,7 +18,7 @@ -export([ hocon_ref/0, login_ref/0, - sign/2, + login/2, create/1, update/2, destroy/1 @@ -35,14 +35,14 @@ login_ref() -> hoconsc:ref(?MODULE, login). fields(ldap) -> - emqx_dashboard_sso_schema:common_backend_schema(ldap) ++ + emqx_dashboard_sso_schema:common_backend_schema([ldap]) ++ [ {query_timeout, fun query_timeout/1} ] ++ emqx_ldap:fields(config) ++ emqx_ldap:fields(bind_opts); fields(login) -> [ - emqx_dashboard_sso_schema:backend_schema(ldap) + emqx_dashboard_sso_schema:backend_schema([ldap]) | emqx_dashboard_sso_schema:username_password_schema() ]. @@ -61,7 +61,7 @@ create(Config0) -> case emqx_dashboard_sso_manager:create_resource(ResourceId, emqx_ldap, Config) of {ok, _} -> {ok, State#{resource_id => ResourceId}}; - Error -> + {error, _} = Error -> Error end. @@ -70,7 +70,7 @@ update(Config0, #{resource_id := ResourceId} = _State) -> case emqx_dashboard_sso_manager:update_resource(ResourceId, emqx_ldap, Config) of {ok, _} -> {ok, NState#{resource_id => ResourceId}}; - Error -> + {error, _} = Error -> Error end. @@ -78,8 +78,8 @@ destroy(#{resource_id := ResourceId}) -> _ = emqx_resource:remove_local(ResourceId), ok. -sign( - #{username := Username} = Req, +login( + #{<<"username">> := Username} = Req, #{ query_timeout := Timeout, resource_id := ResourceId @@ -101,8 +101,7 @@ sign( ) of ok -> - User = ensure_user_exists(Username), - {ok, emqx_dashboard_token:sign(User, <<>>)}; + ensure_user_exists(Username); {error, _} = Error -> Error end; @@ -128,7 +127,12 @@ parse_config(Config) -> ensure_user_exists(Username) -> case emqx_dashboard_admin:lookup_user(ldap, Username) of [User] -> - User; + {ok, emqx_dashboard_token:sign(User, <<>>)}; [] -> - emqx_dashboard_admin:add_sso_user(ldap, Username, ?ROLE_VIEWER, <<>>) + case emqx_dashboard_admin:add_sso_user(ldap, Username, ?ROLE_VIEWER, <<>>) of + {ok, _} -> + ensure_user_exists(Username); + Error -> + Error + end end. 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 63bb6536c..a087af615 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl @@ -30,7 +30,6 @@ ]). -export([ - create/2, update/2, delete/1, pre_config_update/3, @@ -68,8 +67,6 @@ running() -> emqx:get_config([emqx_dashboard_sso]) ). -create(Backend, Config) -> - update_config(Backend, {?FUNCTION_NAME, Backend, Config}). update(Backend, Config) -> update_config(Backend, {?FUNCTION_NAME, Backend, Config}). delete(Backend) -> @@ -85,7 +82,7 @@ lookup_state(Backend) -> make_resource_id(Backend) -> BackendBin = bin(Backend), - emqx_resource:generate_id(BackendBin). + emqx_resource:generate_id(<<"sso:", BackendBin/binary>>). create_resource(ResourceId, Module, Config) -> Result = emqx_resource:create_local( @@ -95,7 +92,7 @@ create_resource(ResourceId, Module, Config) -> Config, ?DEFAULT_RESOURCE_OPTS ), - start_resource_if_enabled(Result, ResourceId, Config). + start_resource_if_enabled(ResourceId, Result, Config). update_resource(ResourceId, Module, Config) -> Result = emqx_resource:recreate_local( @@ -127,6 +124,9 @@ init([]) -> handle_call({update_config, Req, NewConf, OldConf}, _From, State) -> Result = on_config_update(Req, NewConf, OldConf), + io:format(">>> on_config_update:~p~n,Req:~p~n NewConf:~p~n OldConf:~p~n", [ + Result, Req, NewConf, OldConf + ]), {reply, Result, State}; handle_call(_Request, _From, State) -> Reply = ok, @@ -166,60 +166,43 @@ start_backend_services() -> maps:to_list(Backends) ). -update_config(Backend, UpdateReq) -> - case emqx_conf:update([dashboard_sso, Backend], UpdateReq, #{override_to => cluster}) of +update_config(_Backend, UpdateReq) -> + case emqx_conf:update([dashboard_sso], UpdateReq, #{override_to => cluster}) of {ok, UpdateResult} -> #{post_config_update := #{?MODULE := Result}} = UpdateResult, - {ok, Result}; + Result; Error -> Error end. -pre_config_update(_Path, {create, Backend, Config}, OldConf) -> - case maps:find(Backend, OldConf) of - {ok, _} -> - throw(already_exists); - error -> - {ok, OldConf#{Backend => Config}} - end; pre_config_update(_Path, {update, Backend, Config}, OldConf) -> - case maps:find(Backend, OldConf) of - error -> - throw(not_exists); - {ok, _} -> - {ok, OldConf#{Backend => Config}} - end; + BackendBin = bin(Backend), + {ok, OldConf#{BackendBin => Config}}; pre_config_update(_Path, {delete, Backend}, OldConf) -> - case maps:find(Backend, OldConf) of + BackendBin = bin(Backend), + case maps:find(BackendBin, OldConf) of error -> throw(not_exists); {ok, _} -> - {ok, maps:remove(Backend, OldConf)} + {ok, maps:remove(BackendBin, OldConf)} end. post_config_update(_Path, UpdateReq, NewConf, OldConf, _AppEnvs) -> Result = call({update_config, UpdateReq, NewConf, OldConf}), {ok, Result}. -on_config_update({create, Backend, Config}, _NewConf, _OldConf) -> +on_config_update({update, Backend, _Config}, NewConf, _OldConf) -> + Provider = provider(Backend), + Config = maps:get(Backend, NewConf), case lookup(Backend) of undefined -> - Provider = provider(Backend), on_backend_updated( Provider:create(Config), fun(State) -> ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State}) end ); - _Data -> - {error, already_exists} - end; -on_config_update({update, Backend, Config}, _NewConf, _OldConf) -> - case lookup(Backend) of - undefined -> - {error, not_exists}; Data -> - Provider = provider(Backend), on_backend_updated( Provider:update(Config, Data#dashboard_sso.state), fun(State) -> diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl index ce9a53557..19b0473fd 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl @@ -14,7 +14,7 @@ backend_schema/1, username_password_schema/0 ]). --import(hoconsc, [ref/2, mk/2]). +-import(hoconsc, [ref/2, mk/2, enum/1]). %%------------------------------------------------------------------------------ %% Hocon Schema @@ -39,6 +39,7 @@ desc(dashboard_sso) -> desc(_) -> undefined. +-spec common_backend_schema(list(atom())) -> proplists:proplist(). common_backend_schema(Backend) -> [ {enable, @@ -54,7 +55,7 @@ common_backend_schema(Backend) -> backend_schema(Backend) -> {backend, - mk(Backend, #{ + mk(enum(Backend), #{ required => true, desc => ?DESC(backend) })}. diff --git a/apps/emqx_enterprise/src/emqx_enterprise.app.src b/apps/emqx_enterprise/src/emqx_enterprise.app.src index ac35da5b9..37d31c5ec 100644 --- a/apps/emqx_enterprise/src/emqx_enterprise.app.src +++ b/apps/emqx_enterprise/src/emqx_enterprise.app.src @@ -1,6 +1,6 @@ {application, emqx_enterprise, [ {description, "EMQX Enterprise Edition"}, - {vsn, "0.1.2"}, + {vsn, "0.1.3"}, {registered, []}, {applications, [ kernel, diff --git a/apps/emqx_ldap/src/emqx_ldap.erl b/apps/emqx_ldap/src/emqx_ldap.erl index cdf6a9a4c..021916339 100644 --- a/apps/emqx_ldap/src/emqx_ldap.erl +++ b/apps/emqx_ldap/src/emqx_ldap.erl @@ -75,8 +75,16 @@ fields(config) -> ?HOCON(emqx_schema:timeout_duration_ms(), #{ desc => ?DESC(request_timeout), default => <<"5s">> + })}, + {ssl, + ?HOCON(?R_REF(?MODULE, ssl), #{ + default => #{<<"enable">> => false}, + desc => ?DESC(emqx_connector_schema_lib, "ssl") })} - ] ++ emqx_connector_schema_lib:ssl_fields(); + ]; +fields(ssl) -> + Schema = emqx_schema:client_ssl_opts_schema(#{}), + lists:keydelete("user_lookup_fun", 1, Schema); fields(bind_opts) -> [ {bind_password, diff --git a/rel/i18n/emqx_dashboard_api.hocon b/rel/i18n/emqx_dashboard_api.hocon index 01fc6eb16..5f6bd3cde 100644 --- a/rel/i18n/emqx_dashboard_api.hocon +++ b/rel/i18n/emqx_dashboard_api.hocon @@ -79,4 +79,10 @@ users_api404.desc: version.desc: """EMQX Version""" +role.desc: +"""User role""" + +backend.desc: +"""User account source""" + } diff --git a/rel/i18n/emqx_dashboard_sso_api.hocon b/rel/i18n/emqx_dashboard_sso_api.hocon index 122ef0085..85cb7d31b 100644 --- a/rel/i18n/emqx_dashboard_sso_api.hocon +++ b/rel/i18n/emqx_dashboard_sso_api.hocon @@ -10,21 +10,11 @@ login.desc: login.label: """Get Dashboard Auth Token.""" -get_running.desc: -"""Get Running SSO backends""" -get_running.label: -"""Running SSO""" - get_backend.desc: """Get details of a backend""" get_backend.label: """Backend Details""" -create_backend.desc: -"""Create a backend""" -create_backend.label: -"""Create Backend""" - update_backend.desc: """Update a backend""" update_backend.label: From 5b009980501c8753e6b2a36c0004a051128e2b51 Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 19 Sep 2023 22:47:48 +0800 Subject: [PATCH 3/4] fix(sso): fix invalid_dynamic_call --- .../src/emqx_dashboard_sso.erl | 38 ++++++++++++++++++- .../src/emqx_dashboard_sso_api.erl | 7 ++-- .../src/emqx_dashboard_sso_manager.erl | 8 ++-- .../src/emqx_dashboard_sso_schema.erl | 2 +- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl index 287082414..8fbf220f5 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -6,7 +6,17 @@ -include_lib("hocon/include/hoconsc.hrl"). +-export([ + hocon_ref/1, + login_ref/1, + create/2, + update/3, + destroy/2, + login/3 +]). + -export([types/0, modules/0, provider/1, backends/0]). + %%------------------------------------------------------------------------------ %% Callbacks %%------------------------------------------------------------------------------ @@ -18,9 +28,10 @@ -type state() :: #{atom() => term()}. -type raw_config() :: #{binary() => term()}. -type config() :: parsed_config() | raw_config(). +-type hocon_ref() :: ?R_REF(Module :: atom(), Name :: atom() | binary()). --callback hocon_ref() -> ?R_REF(Module :: atom(), Name :: atom() | binary()). --callback login_ref() -> ?R_REF(Module :: atom(), Name :: atom() | binary()). +-callback hocon_ref() -> hocon_ref(). +-callback login_ref() -> hocon_ref(). -callback create(Config :: config()) -> {ok, State :: state()} | {error, Reason :: term()}. -callback update(Config :: config(), State :: state()) -> @@ -29,6 +40,29 @@ -callback login(request(), State :: state()) -> {ok, Token :: binary()} | {error, Reason :: term()}. +%%------------------------------------------------------------------------------ +%% Callback Interface +%%------------------------------------------------------------------------------ +-spec hocon_ref(Mod :: module()) -> hocon_ref(). +hocon_ref(Mod) -> + Mod:hocon_ref(). + +-spec login_ref(Mod :: module()) -> hocon_ref(). +login_ref(Mod) -> + Mod:login_ref(). + +create(Mod, Config) -> + Mod:create(Config). + +update(Mod, Config, State) -> + Mod:update(Config, State). + +destroy(Mod, State) -> + Mod:destroy(State). + +login(Mod, Req, State) -> + Mod:login(Req, State). + %%------------------------------------------------------------------------------ %% API %%------------------------------------------------------------------------------ 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 dd473931c..2f1c19d29 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -36,7 +36,6 @@ -define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). -define(BAD_REQUEST, 'BAD_REQUEST'). - -define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND'). -define(TAGS, <<"Dashboard Single Sign-on">>). @@ -122,7 +121,7 @@ login(post, #{bindings := #{backend := Backend}, body := Sign}) -> {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; State -> Provider = emqx_dashboard_sso:provider(Backend), - case Provider:login(Sign, State) of + case emqx_dashboard_sso:login(Provider, Sign, State) of {ok, Token} -> ?SLOG(info, #{msg => "Dashboard SSO login successfully", request => Sign}), Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), @@ -174,10 +173,10 @@ response_schema(404) -> emqx_dashboard_swagger:error_codes([?BACKEND_NOT_FOUND], ?DESC(backend_not_found)). backend_union() -> - hoconsc:union([Mod:hocon_ref() || Mod <- emqx_dashboard_sso:modules()]). + hoconsc:union([emqx_dashboard_sso:hocon_ref(Mod) || Mod <- emqx_dashboard_sso:modules()]). login_union() -> - hoconsc:union([Mod:login_ref() || Mod <- emqx_dashboard_sso:modules()]). + hoconsc:union([emqx_dashboard_sso:login_ref(Mod) || Mod <- emqx_dashboard_sso:modules()]). backend_name_in_path() -> backend_name_as_arg(path, [], <<"ldap">>). 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 a087af615..6097a39ff 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl @@ -157,7 +157,7 @@ start_backend_services() -> fun({Backend, Config}) -> Provider = provider(Backend), on_backend_updated( - Provider:create(Config), + emqx_dashboard_sso:create(Provider, Config), fun(State) -> ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State}) end @@ -197,14 +197,14 @@ on_config_update({update, Backend, _Config}, NewConf, _OldConf) -> case lookup(Backend) of undefined -> on_backend_updated( - Provider:create(Config), + emqx_dashboard_sso:create(Provider, Config), fun(State) -> ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State}) end ); Data -> on_backend_updated( - Provider:update(Config, Data#dashboard_sso.state), + emqx_dashboard_sso:update(Provider, Config, Data#dashboard_sso.state), fun(State) -> ets:insert(dashboard_sso, Data#dashboard_sso{state = State}) end @@ -217,7 +217,7 @@ on_config_update({delete, Backend}, _NewConf, _OldConf) -> Data -> Provider = provider(Backend), on_backend_updated( - Provider:destroy(Data#dashboard_sso.state), + emqx_dashboard_sso:destroy(Provider, Data#dashboard_sso.state), fun() -> ets:delete(dashboard_sso, Backend) end diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl index 19b0473fd..b45bca8e5 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl @@ -29,7 +29,7 @@ roots() -> [dashboard_sso]. fields(dashboard_sso) -> lists:map( fun({Type, Module}) -> - {Type, mk(Module:hocon_ref(), #{required => {false, recursively}})} + {Type, mk(emqx_dashboard_sso:hocon_ref(Module), #{required => {false, recursively}})} end, maps:to_list(emqx_dashboard_sso:backends()) ). From 61311081d861b51098e943067a88eca6ffe518f6 Mon Sep 17 00:00:00 2001 From: firest Date: Wed, 20 Sep 2023 00:25:11 +0800 Subject: [PATCH 4/4] fix(sso): fix RBAC test errors && spellcheck --- .../test/emqx_dashboard_rbac_SUITE.erl | 16 ++++++++-------- .../src/emqx_dashboard_sso_api.erl | 10 ++++------ .../src/emqx_dashboard_sso_ldap.erl | 8 +++++++- apps/emqx_ldap/src/emqx_ldap.erl | 7 ++++++- 4 files changed, 25 insertions(+), 16 deletions(-) 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 607cb710d..73094f059 100644 --- a/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl +++ b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl @@ -61,11 +61,11 @@ t_permission(_) -> } ), - ?assertEqual( + ?assertMatch( #{ - <<"username">> => ViewerUser, - <<"role">> => ?ROLE_VIEWER, - <<"description">> => ?ADD_DESCRIPTION + <<"username">> := ViewerUser, + <<"role">> := ?ROLE_VIEWER, + <<"description">> := ?ADD_DESCRIPTION }, emqx_utils_json:decode(Payload, [return_maps]) ), @@ -104,11 +104,11 @@ t_update_role(_) -> } ), - ?assertEqual( + ?assertMatch( #{ - <<"username">> => ?DEFAULT_SUPERUSER, - <<"role">> => ?ROLE_VIEWER, - <<"description">> => ?ADD_DESCRIPTION + <<"username">> := ?DEFAULT_SUPERUSER, + <<"role">> := ?ROLE_VIEWER, + <<"description">> := ?ADD_DESCRIPTION }, emqx_utils_json:decode(Payload, [return_maps]) ), 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 2f1c19d29..93a5d7be7 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -8,14 +8,12 @@ -include_lib("hocon/include/hoconsc.hrl"). -include_lib("emqx/include/logger.hrl"). --include_lib("typerefl/include/types.hrl"). -import(hoconsc, [ mk/2, array/1, enum/1, - ref/1, - union/1 + ref/1 ]). -export([ @@ -37,7 +35,7 @@ -define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). -define(BAD_REQUEST, 'BAD_REQUEST'). -define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND'). --define(TAGS, <<"Dashboard Single Sign-on">>). +-define(TAGS, <<"Dashboard Single Sign-On">>). namespace() -> "dashboard_sso". @@ -58,7 +56,7 @@ schema("/sso") -> tags => [?TAGS], desc => ?DESC(get_sso), responses => #{ - 200 => array(enum(emqx_dashboard_sso:types())) + 200 => array(ref(backend_status)) } } }; @@ -189,7 +187,7 @@ backend_name_as_arg(In, Extra, Default) -> #{ in => In, desc => ?DESC(backend_name_in_qs), - required => true, + required => false, example => Default } )} 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 dc7e6a74a..adde6c274 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl @@ -12,7 +12,8 @@ -behaviour(emqx_dashboard_sso). -export([ - fields/1 + fields/1, + desc/1 ]). -export([ @@ -51,6 +52,11 @@ query_timeout(desc) -> ?DESC(?FUNCTION_NAME); query_timeout(default) -> <<"5s">>; query_timeout(_) -> undefined. +desc(ldap) -> + "LDAP"; +desc(_) -> + undefined. + %%------------------------------------------------------------------------------ %% APIs %%------------------------------------------------------------------------------ diff --git a/apps/emqx_ldap/src/emqx_ldap.erl b/apps/emqx_ldap/src/emqx_ldap.erl index 021916339..f16166793 100644 --- a/apps/emqx_ldap/src/emqx_ldap.erl +++ b/apps/emqx_ldap/src/emqx_ldap.erl @@ -25,7 +25,7 @@ %% ecpool connect & reconnect -export([connect/1]). --export([roots/0, fields/1]). +-export([roots/0, fields/1, desc/1]). -export([do_get_status/1]). @@ -100,6 +100,11 @@ fields(bind_opts) -> )} ]. +desc(ssl) -> + ?DESC(emqx_connector_schema_lib, "ssl"); +desc(_) -> + undefined. + server() -> Meta = #{desc => ?DESC("server")}, emqx_schema:servers_sc(Meta, ?LDAP_HOST_OPTIONS).