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..3ae2a33e1 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,9 @@ 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(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}; @@ -204,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. @@ -225,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 -> @@ -272,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) -> @@ -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. @@ -328,11 +335,11 @@ all_users() -> role = Role } ) -> - #{ + flatten_username(#{ username => Username, description => Desc, role => ensure_role(Role) - } + }) end, ets:tab2list(?ADMIN) ). @@ -410,6 +417,28 @@ legal_role(Role) -> role(Data) -> emqx_dashboard_rbac:role(Data). +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, 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) -> + lookup_user(?SSO_USERNAME(Backend, Username)). -else. -dialyzer({no_match, [add_user/4, update_user/3]}). @@ -419,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_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/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..530117a27 --- /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 EMQ Business Source License 1.1, refer to [LICENSE](BSL.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..8fbf220f5 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -0,0 +1,79 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_sso). + +-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 +%%------------------------------------------------------------------------------ +-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(). +-type hocon_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()) -> + {ok, NewState :: state()} | {error, Reason :: term()}. +-callback destroy(State :: state()) -> ok. +-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 +%%------------------------------------------------------------------------------ +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..93a5d7be7 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -0,0 +1,225 @@ +%%-------------------------------------------------------------------- +%% 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"). + +-import(hoconsc, [ + mk/2, + array/1, + enum/1, + ref/1 +]). + +-export([ + api_spec/0, + fields/1, + paths/0, + schema/1, + namespace/0 +]). + +-export([ + login/2, + sso/2, + backend/2 +]). + +-export([sso_parameters/1]). + +-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/:backend", + "/sso/:backend" + ]. + +schema("/sso") -> + #{ + 'operationId' => sso, + get => #{ + tags => [?TAGS], + desc => ?DESC(get_sso), + responses => #{ + 200 => array(ref(backend_status)) + } + } + }; +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]), + 401 => response_schema(401), + 404 => response_schema(404) + } + } + }; +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) + } + }, + 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(emqx_dashboard_sso:types()). + +%% ------------------------------------------------------------------------------------------------- +%% API +login(post, #{bindings := #{backend := Backend}, body := Sign}) -> + case emqx_dashboard_sso_manager:lookup_state(Backend) of + undefined -> + {404, ?BACKEND_NOT_FOUND, <<"Backend not found">>}; + State -> + Provider = emqx_dashboard_sso:provider(Backend), + case emqx_dashboard_sso:login(Provider, Sign, State) of + {ok, Token} -> + ?SLOG(info, #{msg => "Dashboard SSO login successfully", request => Sign}), + 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 => Sign, + 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, enable], 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, to_json(Backend)} + end; +backend(put, #{bindings := #{backend := Backend}, body := Config}) -> + on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:update/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 +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([emqx_dashboard_sso:hocon_ref(Mod) || Mod <- emqx_dashboard_sso:modules()]). + +login_union() -> + hoconsc:union([emqx_dashboard_sso:login_ref(Mod) || Mod <- emqx_dashboard_sso:modules()]). + +backend_name_in_path() -> + backend_name_as_arg(path, [], <<"ldap">>). + +backend_name_as_arg(In, Extra, Default) -> + [ + {backend, + mk( + enum(Extra ++ emqx_dashboard_sso:types()), + #{ + in => In, + desc => ?DESC(backend_name_in_qs), + required => false, + example => Default + } + )} + ]. + +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, to_json(Config)}; +handle_backend_update_result(ok, _) -> + 204; +handle_backend_update_result({error, not_exists}, _) -> + {404, ?BACKEND_NOT_FOUND, <<"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}. + +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_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..adde6c274 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl @@ -0,0 +1,144 @@ +%%-------------------------------------------------------------------- +%% 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, + desc/1 +]). + +-export([ + hocon_ref/0, + login_ref/0, + login/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. + +desc(ldap) -> + "LDAP"; +desc(_) -> + 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 -> + 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 -> + Error + end. + +destroy(#{resource_id := ResourceId}) -> + _ = emqx_resource:remove_local(ResourceId), + ok. + +login( + #{<<"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 -> + ensure_user_exists(Username); + {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] -> + {ok, emqx_dashboard_token:sign(User, <<>>)}; + [] -> + 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 new file mode 100644 index 000000000..6097a39ff --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl @@ -0,0 +1,252 @@ +%%-------------------------------------------------------------------- +%% 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([ + 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]) + ). + +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(<<"sso:", BackendBin/binary>>). + +create_resource(ResourceId, Module, Config) -> + Result = emqx_resource:create_local( + ResourceId, + ?RESOURCE_GROUP, + Module, + Config, + ?DEFAULT_RESOURCE_OPTS + ), + start_resource_if_enabled(ResourceId, Result, 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), + 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, + {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( + emqx_dashboard_sso:create(Provider, 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], UpdateReq, #{override_to => cluster}) of + {ok, UpdateResult} -> + #{post_config_update := #{?MODULE := Result}} = UpdateResult, + Result; + Error -> + Error + end. + +pre_config_update(_Path, {update, Backend, Config}, OldConf) -> + BackendBin = bin(Backend), + {ok, OldConf#{BackendBin => Config}}; +pre_config_update(_Path, {delete, Backend}, OldConf) -> + BackendBin = bin(Backend), + case maps:find(BackendBin, OldConf) of + error -> + throw(not_exists); + {ok, _} -> + {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({update, Backend, _Config}, NewConf, _OldConf) -> + Provider = provider(Backend), + Config = maps:get(Backend, NewConf), + case lookup(Backend) of + undefined -> + on_backend_updated( + emqx_dashboard_sso:create(Provider, Config), + fun(State) -> + ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State}) + end + ); + Data -> + on_backend_updated( + emqx_dashboard_sso:update(Provider, 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( + emqx_dashboard_sso:destroy(Provider, 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..b45bca8e5 --- /dev/null +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_schema.erl @@ -0,0 +1,83 @@ +%%-------------------------------------------------------------------- +%% 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, enum/1]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ +namespace() -> dashboard_sso. + +tags() -> + [<<"Dashboard Single Sign-On">>]. + +roots() -> [dashboard_sso]. + +fields(dashboard_sso) -> + lists:map( + fun({Type, Module}) -> + {Type, mk(emqx_dashboard_sso:hocon_ref(Module), #{required => {false, recursively}})} + end, + maps:to_list(emqx_dashboard_sso:backends()) + ). + +desc(dashboard_sso) -> + "Dashboard Single Sign-On"; +desc(_) -> + undefined. + +-spec common_backend_schema(list(atom())) -> proplists:proplist(). +common_backend_schema(Backend) -> + [ + {enable, + mk( + boolean(), #{ + desc => ?DESC(backend_enable), + required => false, + default => false + } + )}, + backend_schema(Backend) + ]. + +backend_schema(Backend) -> + {backend, + mk(enum(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.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_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_ldap/src/emqx_ldap.erl b/apps/emqx_ldap/src/emqx_ldap.erl index cdf6a9a4c..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]). @@ -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, @@ -92,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). 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 f38ff7ed4..26bbf10f8 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_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 new file mode 100644 index 000000000..85cb7d31b --- /dev/null +++ b/rel/i18n/emqx_dashboard_sso_api.hocon @@ -0,0 +1,40 @@ +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_backend.desc: +"""Get details of a backend""" +get_backend.label: +"""Backend Details""" + +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""" +}