Merge pull request #11631 from lafirest/feat/dashboard_ldap
feat(dashboard): add SSO feature and integrate with LDAP
This commit is contained in:
commit
bc6edac45f
|
@ -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).
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(<<BackendBin/binary, "-", Name/binary>>, Password, Salt);
|
||||
jwk(Username, Password, Salt) ->
|
||||
Key = crypto:hash(md5, <<Salt/binary, Username/binary, Password/binary>>),
|
||||
#{
|
||||
|
@ -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}
|
||||
}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -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])
|
||||
),
|
||||
|
|
|
@ -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.
|
|
@ -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).
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
%% -*- mode: erlang; -*-
|
||||
|
||||
{erl_opts, [debug_info]}.
|
||||
{deps, [
|
||||
{emqx_ldap, {path, "../../apps/emqx_ldap"}},
|
||||
{emqx_dashboard, {path, "../../apps/emqx_dashboard"}}
|
||||
]}.
|
|
@ -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 <contact@emqx.io>"]},
|
||||
{links, [
|
||||
{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://github.com/emqx/emqx-dashboard5"}
|
||||
]}
|
||||
]}.
|
|
@ -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}.
|
|
@ -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
|
||||
).
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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">>
|
||||
}
|
||||
)}
|
||||
].
|
|
@ -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)
|
||||
]}}.
|
|
@ -1,6 +1,6 @@
|
|||
{application, emqx_enterprise, [
|
||||
{description, "EMQX Enterprise Edition"},
|
||||
{vsn, "0.1.2"},
|
||||
{vsn, "0.1.3"},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
kernel,
|
||||
|
|
|
@ -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() ->
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 =>
|
||||
|
|
3
mix.exs
3
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
|
||||
|
||||
|
|
|
@ -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() ->
|
||||
|
|
|
@ -79,4 +79,10 @@ users_api404.desc:
|
|||
version.desc:
|
||||
"""EMQX Version"""
|
||||
|
||||
role.desc:
|
||||
"""User role"""
|
||||
|
||||
backend.desc:
|
||||
"""User account source"""
|
||||
|
||||
}
|
||||
|
|
|
@ -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"""
|
||||
|
||||
}
|
|
@ -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"""
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
emqx_dashboard_sso_schema {
|
||||
|
||||
backend_enable.desc:
|
||||
"""Whether to enable this backend."""
|
||||
|
||||
backend.desc:
|
||||
"""Backend type."""
|
||||
|
||||
backend.label:
|
||||
"""Backend Type"""
|
||||
}
|
Loading…
Reference in New Issue