feat(dashboard): add SSO feature and integrate with LDAP
This commit is contained in:
parent
6fe846bf0e
commit
2cddce5479
|
@ -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,11 @@ add_default_user() ->
|
|||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec add_user(binary(), binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, any()}.
|
||||
add_user(Username, Password, Role, Desc) when
|
||||
is_binary(Username), is_binary(Password)
|
||||
->
|
||||
|
||||
|
||||
-spec add_user(dashboard_username(), binary(), dashboard_user_role(), binary()) ->
|
||||
{ok, map()} | {error, any()}.
|
||||
add_user(Username, Password, Role, Desc) when is_binary(Password) ->
|
||||
case {legal_username(Username), legal_password(Password), legal_role(Role)} of
|
||||
{ok, ok, ok} -> do_add_user(Username, Password, Role, Desc);
|
||||
{{error, Reason}, _, _} -> {error, Reason};
|
||||
|
@ -115,6 +120,8 @@ do_add_user(Username, Password, Role, Desc) ->
|
|||
return(Res).
|
||||
|
||||
%% 0-9 or A-Z or a-z or $_
|
||||
legal_username(?SSO_USERNAME(_, _)) ->
|
||||
ok;
|
||||
legal_username(<<>>) ->
|
||||
{error, <<"Username cannot be empty">>};
|
||||
legal_username(UserName) ->
|
||||
|
@ -312,8 +319,8 @@ update_pwd(Username, Fun) ->
|
|||
end,
|
||||
return(mria:transaction(?DASHBOARD_SHARD, Trans)).
|
||||
|
||||
-spec lookup_user(binary()) -> [emqx_admin()].
|
||||
lookup_user(Username) when is_binary(Username) ->
|
||||
-spec lookup_user(dashboard_username()) -> [emqx_admin()].
|
||||
lookup_user(Username) ->
|
||||
Fun = fun() -> mnesia:read(?ADMIN, Username) end,
|
||||
{atomic, User} = mria:ro_transaction(?DASHBOARD_SHARD, Fun),
|
||||
User.
|
||||
|
@ -410,6 +417,14 @@ legal_role(Role) ->
|
|||
role(Data) ->
|
||||
emqx_dashboard_rbac:role(Data).
|
||||
|
||||
-spec add_sso_user(atom(), binary(), dashboard_user_role(), binary()) ->
|
||||
{ok, map()} | {error, any()}.
|
||||
add_sso_user(Backend, Username, Role, Desc) ->
|
||||
add_user(?SSO_USERNAME(Backend, Username), <<>>, Role, Desc).
|
||||
|
||||
-spec lookup_user(dashboard_sso_backend(), binary()) -> [emqx_admin()].
|
||||
lookup_user(Backend, Username) when is_atom(Backend) ->
|
||||
lookup_user(?SSO_USERNAME(Backend, Username)).
|
||||
-else.
|
||||
|
||||
-dialyzer({no_match, [add_user/4, update_user/3]}).
|
||||
|
|
|
@ -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 [APL](../../APL.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,45 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_dashboard_sso).
|
||||
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
|
||||
-export([types/0, modules/0, provider/1, backends/0]).
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Callbacks
|
||||
%%------------------------------------------------------------------------------
|
||||
-type request() :: map().
|
||||
-type parsed_config() :: #{
|
||||
backend => atom(),
|
||||
atom() => term()
|
||||
}.
|
||||
-type state() :: #{atom() => term()}.
|
||||
-type raw_config() :: #{binary() => term()}.
|
||||
-type config() :: parsed_config() | raw_config().
|
||||
|
||||
-callback hocon_ref() -> ?R_REF(Module :: atom(), Name :: atom() | binary()).
|
||||
-callback login_ref() -> ?R_REF(Module :: atom(), Name :: atom() | binary()).
|
||||
-callback create(Config :: config()) ->
|
||||
{ok, State :: state()} | {error, Reason :: term()}.
|
||||
-callback update(Config :: config(), State :: state()) ->
|
||||
{ok, NewState :: state()} | {error, Reason :: term()}.
|
||||
-callback destroy(State :: state()) -> ok.
|
||||
-callback sign(request(), State :: state()) ->
|
||||
{ok, Token :: binary()} | {error, Reason :: term()}.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% API
|
||||
%%------------------------------------------------------------------------------
|
||||
types() ->
|
||||
maps:keys(backends()).
|
||||
|
||||
modules() ->
|
||||
maps:values(backends()).
|
||||
|
||||
provider(Backend) ->
|
||||
maps:get(Backend, backends()).
|
||||
|
||||
backends() ->
|
||||
#{ldap => emqx_dashboard_sso_ldap}.
|
|
@ -0,0 +1,235 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_dashboard_sso_api).
|
||||
|
||||
-behaviour(minirest_api).
|
||||
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-import(hoconsc, [
|
||||
mk/2,
|
||||
array/1,
|
||||
enum/1,
|
||||
ref/1,
|
||||
union/1
|
||||
]).
|
||||
|
||||
-export([
|
||||
api_spec/0,
|
||||
fields/1,
|
||||
paths/0,
|
||||
schema/1,
|
||||
namespace/0
|
||||
]).
|
||||
|
||||
-export([
|
||||
running/2,
|
||||
login/2,
|
||||
sso/2,
|
||||
backend/2
|
||||
]).
|
||||
|
||||
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
||||
-define(BAD_REQUEST, 'BAD_REQUEST').
|
||||
|
||||
-define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND').
|
||||
-define(TAGS, <<"Dashboard Single Sign-on">>).
|
||||
|
||||
namespace() -> "dashboard_sso".
|
||||
|
||||
api_spec() ->
|
||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||
|
||||
paths() ->
|
||||
[
|
||||
"/sso",
|
||||
"/sso/login",
|
||||
"/sso/running",
|
||||
"/sso/:backend"
|
||||
].
|
||||
|
||||
schema("/sso") ->
|
||||
#{
|
||||
'operationId' => sso,
|
||||
get => #{
|
||||
tags => [?TAGS],
|
||||
desc => ?DESC(get_sso),
|
||||
responses => #{
|
||||
200 => array(ref(backend_status))
|
||||
}
|
||||
}
|
||||
};
|
||||
schema("/sso/login") ->
|
||||
#{
|
||||
'operationId' => login,
|
||||
post => #{
|
||||
tags => [?TAGS],
|
||||
desc => ?DESC(login),
|
||||
'requestBody' => login_union(),
|
||||
responses => #{
|
||||
200 => emqx_dashboard_api:fields([token, version, license]),
|
||||
401 => response_schema(401),
|
||||
404 => response_schema(404)
|
||||
}
|
||||
}
|
||||
};
|
||||
schema("/sso/running") ->
|
||||
#{
|
||||
'operationId' => running,
|
||||
get => #{
|
||||
tags => [?TAGS],
|
||||
desc => ?DESC(get_running),
|
||||
responses => #{
|
||||
200 => array(enum(emqx_dashboard_sso:types()))
|
||||
}
|
||||
}
|
||||
};
|
||||
schema("/sso/:backend") ->
|
||||
#{
|
||||
'operationId' => backend,
|
||||
get => #{
|
||||
tags => [?TAGS],
|
||||
desc => ?DESC(get_backend),
|
||||
parameters => backend_name_in_path(),
|
||||
responses => #{
|
||||
200 => backend_union(),
|
||||
404 => response_schema(404)
|
||||
}
|
||||
},
|
||||
post => #{
|
||||
tags => [?TAGS],
|
||||
desc => ?DESC(create_backend),
|
||||
parameters => backend_name_in_path(),
|
||||
'requestBody' => backend_union(),
|
||||
responses => #{
|
||||
200 => backend_union()
|
||||
}
|
||||
},
|
||||
put => #{
|
||||
tags => [?TAGS],
|
||||
desc => ?DESC(update_backend),
|
||||
parameters => backend_name_in_path(),
|
||||
'requestBody' => backend_union(),
|
||||
responses => #{
|
||||
200 => backend_union(),
|
||||
404 => response_schema(404)
|
||||
}
|
||||
},
|
||||
delete => #{
|
||||
tags => [?TAGS],
|
||||
desc => ?DESC(delete_backend),
|
||||
parameters => backend_name_in_path(),
|
||||
responses => #{
|
||||
204 => <<"Delete successfully">>,
|
||||
404 => response_schema(404)
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
fields(backend_status) ->
|
||||
emqx_dashboard_sso_schema:common_backend_schema(enum(emqx_dashboard_sso:types())).
|
||||
|
||||
%% -------------------------------------------------------------------------------------------------
|
||||
%% API
|
||||
running(get, _Request) ->
|
||||
{200, emqx_dashboard_sso_manager:running()}.
|
||||
|
||||
login(post, #{backend := Backend} = Request) ->
|
||||
case emqx_dashboard_sso_manager:lookup_state(Backend) of
|
||||
undefined ->
|
||||
{404, ?BACKEND_NOT_FOUND};
|
||||
State ->
|
||||
Provider = emqx_dashboard_sso:provider(Backend),
|
||||
case Provider:login(Request, State) of
|
||||
{ok, Token} ->
|
||||
?SLOG(info, #{msg => "Dashboard SSO login successfully", request => Request}),
|
||||
Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
||||
{200, #{
|
||||
token => Token,
|
||||
version => Version,
|
||||
license => #{edition => emqx_release:edition()}
|
||||
}};
|
||||
{error, Reason} ->
|
||||
?SLOG(info, #{
|
||||
msg => "Dashboard SSO login failed", request => Request, reason => Reason
|
||||
}),
|
||||
{401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>}
|
||||
end
|
||||
end.
|
||||
|
||||
sso(get, _Request) ->
|
||||
SSO = emqx:get_config([dashboard_sso], #{}),
|
||||
{200,
|
||||
lists:map(
|
||||
fun(Backend) ->
|
||||
maps:with([backend, enabled], Backend)
|
||||
end,
|
||||
maps:values(SSO)
|
||||
)}.
|
||||
|
||||
backend(get, #{bindings := #{backend := Type}}) ->
|
||||
case emqx:get_config([dashboard_sso, Type], undefined) of
|
||||
undefined ->
|
||||
{404, ?BACKEND_NOT_FOUND};
|
||||
Backend ->
|
||||
{200, Backend}
|
||||
end;
|
||||
backend(create, #{bindings := #{backend := Backend}, body := Config}) ->
|
||||
on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:create/2);
|
||||
backend(put, #{bindings := #{backend := Backend}, body := Config}) ->
|
||||
on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:update/2);
|
||||
backend(delete, #{bindings := #{backend := Backend}, body := Config}) ->
|
||||
on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:delete/2).
|
||||
|
||||
%% -------------------------------------------------------------------------------------------------
|
||||
%% internal
|
||||
response_schema(401) ->
|
||||
emqx_dashboard_swagger:error_codes([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401));
|
||||
response_schema(404) ->
|
||||
emqx_dashboard_swagger:error_codes([?BACKEND_NOT_FOUND], ?DESC(backend_not_found)).
|
||||
|
||||
backend_union() ->
|
||||
hoconsc:union([Mod:hocon_ref() || Mod <- emqx_dashboard_sso:modules()]).
|
||||
|
||||
login_union() ->
|
||||
hoconsc:union([Mod:login_ref() || Mod <- emqx_dashboard_sso:modules()]).
|
||||
|
||||
backend_name_in_path() ->
|
||||
[
|
||||
{name,
|
||||
mk(
|
||||
binary(),
|
||||
#{
|
||||
in => path,
|
||||
desc => ?DESC(backend_name_in_qs),
|
||||
example => <<"ldap">>
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
on_backend_update(Backend, Config, Fun) ->
|
||||
Result = valid_config(Backend, Config, Fun),
|
||||
handle_backend_update_result(Result, Config).
|
||||
|
||||
valid_config(Backend, Config, Fun) ->
|
||||
case maps:get(backend, Config, undefined) of
|
||||
Backend ->
|
||||
Fun(Backend, Config);
|
||||
_ ->
|
||||
{error, invalid_config}
|
||||
end.
|
||||
|
||||
handle_backend_update_result({ok, _}, Config) ->
|
||||
{200, Config};
|
||||
handle_backend_update_result(ok, _) ->
|
||||
204;
|
||||
handle_backend_update_result({error, not_exists}, _) ->
|
||||
{404, ?BACKEND_NOT_FOUND};
|
||||
handle_backend_update_result({error, already_exists}, _) ->
|
||||
{400, ?BAD_REQUEST, <<"Backend already exists.">>};
|
||||
handle_backend_update_result({error, Reason}, _) ->
|
||||
{400, ?BAD_REQUEST, Reason}.
|
|
@ -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,134 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_dashboard_sso_ldap).
|
||||
|
||||
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-include_lib("eldap/include/eldap.hrl").
|
||||
|
||||
-behaviour(emqx_dashboard_sso).
|
||||
|
||||
-export([
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([
|
||||
hocon_ref/0,
|
||||
login_ref/0,
|
||||
sign/2,
|
||||
create/1,
|
||||
update/2,
|
||||
destroy/1
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
hocon_ref() ->
|
||||
hoconsc:ref(?MODULE, ldap).
|
||||
|
||||
login_ref() ->
|
||||
hoconsc:ref(?MODULE, login).
|
||||
|
||||
fields(ldap) ->
|
||||
emqx_dashboard_sso_schema:common_backend_schema(ldap) ++
|
||||
[
|
||||
{query_timeout, fun query_timeout/1}
|
||||
] ++
|
||||
emqx_ldap:fields(config) ++ emqx_ldap:fields(bind_opts);
|
||||
fields(login) ->
|
||||
[
|
||||
emqx_dashboard_sso_schema:backend_schema(ldap)
|
||||
| emqx_dashboard_sso_schema:username_password_schema()
|
||||
].
|
||||
|
||||
query_timeout(type) -> emqx_schema:timeout_duration_ms();
|
||||
query_timeout(desc) -> ?DESC(?FUNCTION_NAME);
|
||||
query_timeout(default) -> <<"5s">>;
|
||||
query_timeout(_) -> undefined.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
create(Config0) ->
|
||||
ResourceId = emqx_dashboard_sso_manager:make_resource_id(ldap),
|
||||
{Config, State} = parse_config(Config0),
|
||||
case emqx_dashboard_sso_manager:create_resource(ResourceId, emqx_ldap, Config) of
|
||||
{ok, _} ->
|
||||
{ok, State#{resource_id => ResourceId}};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
update(Config0, #{resource_id := ResourceId} = _State) ->
|
||||
{Config, NState} = parse_config(Config0),
|
||||
case emqx_dashboard_sso_manager:update_resource(ResourceId, emqx_ldap, Config) of
|
||||
{ok, _} ->
|
||||
{ok, NState#{resource_id => ResourceId}};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
destroy(#{resource_id := ResourceId}) ->
|
||||
_ = emqx_resource:remove_local(ResourceId),
|
||||
ok.
|
||||
|
||||
sign(
|
||||
#{username := Username} = Req,
|
||||
#{
|
||||
query_timeout := Timeout,
|
||||
resource_id := ResourceId
|
||||
} = _State
|
||||
) ->
|
||||
case
|
||||
emqx_resource:simple_sync_query(
|
||||
ResourceId,
|
||||
{query, Req, [], Timeout}
|
||||
)
|
||||
of
|
||||
{ok, []} ->
|
||||
{error, user_not_found};
|
||||
{ok, [_Entry | _]} ->
|
||||
case
|
||||
emqx_resource:simple_sync_query(
|
||||
ResourceId,
|
||||
{bind, Req}
|
||||
)
|
||||
of
|
||||
ok ->
|
||||
User = ensure_user_exists(Username),
|
||||
{ok, emqx_dashboard_token:sign(User, <<>>)};
|
||||
{error, _} = Error ->
|
||||
Error
|
||||
end;
|
||||
{error, _} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
parse_config(Config) ->
|
||||
State = lists:foldl(
|
||||
fun(Key, Acc) ->
|
||||
case maps:find(Key, Config) of
|
||||
{ok, Value} when is_binary(Value) ->
|
||||
Acc#{Key := erlang:binary_to_list(Value)};
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end,
|
||||
Config,
|
||||
[query_timeout]
|
||||
),
|
||||
{Config, State}.
|
||||
|
||||
ensure_user_exists(Username) ->
|
||||
case emqx_dashboard_admin:lookup_user(ldap, Username) of
|
||||
[User] ->
|
||||
User;
|
||||
[] ->
|
||||
emqx_dashboard_admin:add_sso_user(ldap, Username, ?ROLE_VIEWER, <<>>)
|
||||
end.
|
|
@ -0,0 +1,269 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_dashboard_sso_manager).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3,
|
||||
format_status/2
|
||||
]).
|
||||
|
||||
-export([
|
||||
running/0,
|
||||
lookup_state/1,
|
||||
make_resource_id/1,
|
||||
create_resource/3,
|
||||
update_resource/3,
|
||||
call/1
|
||||
]).
|
||||
|
||||
-export([
|
||||
create/2,
|
||||
update/2,
|
||||
delete/1,
|
||||
pre_config_update/3,
|
||||
post_config_update/5
|
||||
]).
|
||||
|
||||
-import(emqx_dashboard_sso, [provider/1]).
|
||||
|
||||
-define(MOD_KEY_PATH, [dashboard_sso]).
|
||||
-define(RESOURCE_GROUP, <<"emqx_dashboard_sso">>).
|
||||
-define(DEFAULT_RESOURCE_OPTS, #{
|
||||
start_after_created => false
|
||||
}).
|
||||
|
||||
-record(dashboard_sso, {
|
||||
backend :: atom(),
|
||||
state :: map()
|
||||
}).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% API
|
||||
%%------------------------------------------------------------------------------
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
running() ->
|
||||
maps:fold(
|
||||
fun
|
||||
(Type, #{enable := true}, Acc) ->
|
||||
[Type | Acc];
|
||||
(_Type, _Cfg, Acc) ->
|
||||
Acc
|
||||
end,
|
||||
[],
|
||||
emqx:get_config([emqx_dashboard_sso])
|
||||
).
|
||||
|
||||
create(Backend, Config) ->
|
||||
update_config(Backend, {?FUNCTION_NAME, Backend, Config}).
|
||||
update(Backend, Config) ->
|
||||
update_config(Backend, {?FUNCTION_NAME, Backend, Config}).
|
||||
delete(Backend) ->
|
||||
update_config(Backend, {?FUNCTION_NAME, Backend}).
|
||||
|
||||
lookup_state(Backend) ->
|
||||
case ets:lookup(dashboard_sso, Backend) of
|
||||
[Data] ->
|
||||
Data#dashboard_sso.state;
|
||||
[] ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
make_resource_id(Backend) ->
|
||||
BackendBin = bin(Backend),
|
||||
emqx_resource:generate_id(BackendBin).
|
||||
|
||||
create_resource(ResourceId, Module, Config) ->
|
||||
Result = emqx_resource:create_local(
|
||||
ResourceId,
|
||||
?RESOURCE_GROUP,
|
||||
Module,
|
||||
Config,
|
||||
?DEFAULT_RESOURCE_OPTS
|
||||
),
|
||||
start_resource_if_enabled(Result, ResourceId, Config).
|
||||
|
||||
update_resource(ResourceId, Module, Config) ->
|
||||
Result = emqx_resource:recreate_local(
|
||||
ResourceId, Module, Config, ?DEFAULT_RESOURCE_OPTS
|
||||
),
|
||||
start_resource_if_enabled(ResourceId, Result, Config).
|
||||
|
||||
call(Req) ->
|
||||
gen_server:call(?MODULE, Req).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% gen_server callbacks
|
||||
%%------------------------------------------------------------------------------
|
||||
init([]) ->
|
||||
process_flag(trap_exit, true),
|
||||
emqx_conf:add_handler(?MOD_KEY_PATH, ?MODULE),
|
||||
emqx_utils_ets:new(
|
||||
dashboard_sso,
|
||||
[
|
||||
set,
|
||||
public,
|
||||
named_table,
|
||||
{keypos, #dashboard_sso.backend},
|
||||
{read_concurrency, true}
|
||||
]
|
||||
),
|
||||
start_backend_services(),
|
||||
{ok, #{}}.
|
||||
|
||||
handle_call({update_config, Req, NewConf, OldConf}, _From, State) ->
|
||||
Result = on_config_update(Req, NewConf, OldConf),
|
||||
{reply, Result, State};
|
||||
handle_call(_Request, _From, State) ->
|
||||
Reply = ok,
|
||||
{reply, Reply, State}.
|
||||
|
||||
handle_cast(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
emqx_conf:remove_handler(?MOD_KEY_PATH),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
format_status(_Opt, Status) ->
|
||||
Status.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
start_backend_services() ->
|
||||
Backends = emqx_conf:get([dashboard_sso], #{}),
|
||||
lists:foreach(
|
||||
fun({Backend, Config}) ->
|
||||
Provider = provider(Backend),
|
||||
on_backend_updated(
|
||||
Provider:create(Config),
|
||||
fun(State) ->
|
||||
ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State})
|
||||
end
|
||||
)
|
||||
end,
|
||||
maps:to_list(Backends)
|
||||
).
|
||||
|
||||
update_config(Backend, UpdateReq) ->
|
||||
case emqx_conf:update([dashboard_sso, Backend], UpdateReq, #{override_to => cluster}) of
|
||||
{ok, UpdateResult} ->
|
||||
#{post_config_update := #{?MODULE := Result}} = UpdateResult,
|
||||
{ok, Result};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
pre_config_update(_Path, {create, Backend, Config}, OldConf) ->
|
||||
case maps:find(Backend, OldConf) of
|
||||
{ok, _} ->
|
||||
throw(already_exists);
|
||||
error ->
|
||||
{ok, OldConf#{Backend => Config}}
|
||||
end;
|
||||
pre_config_update(_Path, {update, Backend, Config}, OldConf) ->
|
||||
case maps:find(Backend, OldConf) of
|
||||
error ->
|
||||
throw(not_exists);
|
||||
{ok, _} ->
|
||||
{ok, OldConf#{Backend => Config}}
|
||||
end;
|
||||
pre_config_update(_Path, {delete, Backend}, OldConf) ->
|
||||
case maps:find(Backend, OldConf) of
|
||||
error ->
|
||||
throw(not_exists);
|
||||
{ok, _} ->
|
||||
{ok, maps:remove(Backend, OldConf)}
|
||||
end.
|
||||
|
||||
post_config_update(_Path, UpdateReq, NewConf, OldConf, _AppEnvs) ->
|
||||
Result = call({update_config, UpdateReq, NewConf, OldConf}),
|
||||
{ok, Result}.
|
||||
|
||||
on_config_update({create, Backend, Config}, _NewConf, _OldConf) ->
|
||||
case lookup(Backend) of
|
||||
undefined ->
|
||||
Provider = provider(Backend),
|
||||
on_backend_updated(
|
||||
Provider:create(Config),
|
||||
fun(State) ->
|
||||
ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State})
|
||||
end
|
||||
);
|
||||
_Data ->
|
||||
{error, already_exists}
|
||||
end;
|
||||
on_config_update({update, Backend, Config}, _NewConf, _OldConf) ->
|
||||
case lookup(Backend) of
|
||||
undefined ->
|
||||
{error, not_exists};
|
||||
Data ->
|
||||
Provider = provider(Backend),
|
||||
on_backend_updated(
|
||||
Provider:update(Config, Data#dashboard_sso.state),
|
||||
fun(State) ->
|
||||
ets:insert(dashboard_sso, Data#dashboard_sso{state = State})
|
||||
end
|
||||
)
|
||||
end;
|
||||
on_config_update({delete, Backend}, _NewConf, _OldConf) ->
|
||||
case lookup(Backend) of
|
||||
undefined ->
|
||||
{error, not_exists};
|
||||
Data ->
|
||||
Provider = provider(Backend),
|
||||
on_backend_updated(
|
||||
Provider:destroy(Data#dashboard_sso.state),
|
||||
fun() ->
|
||||
ets:delete(dashboard_sso, Backend)
|
||||
end
|
||||
)
|
||||
end.
|
||||
|
||||
lookup(Backend) ->
|
||||
case ets:lookup(dashboard_sso, Backend) of
|
||||
[Data] ->
|
||||
Data;
|
||||
[] ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
start_resource_if_enabled(ResourceId, {ok, _} = Result, #{enable := true}) ->
|
||||
_ = emqx_resource:start(ResourceId),
|
||||
Result;
|
||||
start_resource_if_enabled(_ResourceId, Result, _Config) ->
|
||||
Result.
|
||||
|
||||
on_backend_updated({ok, State} = Ok, Fun) ->
|
||||
Fun(State),
|
||||
Ok;
|
||||
on_backend_updated(ok, Fun) ->
|
||||
Fun(),
|
||||
ok;
|
||||
on_backend_updated(Error, _) ->
|
||||
Error.
|
||||
|
||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||
bin(L) when is_list(L) -> list_to_binary(L);
|
||||
bin(X) -> X.
|
|
@ -0,0 +1,82 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_dashboard_sso_schema).
|
||||
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
%% Hocon
|
||||
-export([namespace/0, roots/0, fields/1, tags/0, desc/1]).
|
||||
-export([
|
||||
common_backend_schema/1,
|
||||
backend_schema/1,
|
||||
username_password_schema/0
|
||||
]).
|
||||
-import(hoconsc, [ref/2, mk/2]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
%%------------------------------------------------------------------------------
|
||||
namespace() -> dashboard_sso.
|
||||
|
||||
tags() ->
|
||||
[<<"Dashboard Single Sign-On">>].
|
||||
|
||||
roots() -> [dashboard_sso].
|
||||
|
||||
fields(dashboard_sso) ->
|
||||
lists:map(
|
||||
fun({Type, Module}) ->
|
||||
{Type, mk(Module:hocon_ref(), #{required => {false, recursively}})}
|
||||
end,
|
||||
maps:to_list(emqx_dashboard_sso:backends())
|
||||
).
|
||||
|
||||
desc(dashboard_sso) ->
|
||||
"Dashboard Single Sign-On";
|
||||
desc(_) ->
|
||||
undefined.
|
||||
|
||||
common_backend_schema(Backend) ->
|
||||
[
|
||||
{enable,
|
||||
mk(
|
||||
boolean(), #{
|
||||
desc => ?DESC(backend_enable),
|
||||
required => false,
|
||||
default => false
|
||||
}
|
||||
)},
|
||||
backend_schema(Backend)
|
||||
].
|
||||
|
||||
backend_schema(Backend) ->
|
||||
{backend,
|
||||
mk(Backend, #{
|
||||
required => true,
|
||||
desc => ?DESC(backend)
|
||||
})}.
|
||||
|
||||
username_password_schema() ->
|
||||
[
|
||||
{username,
|
||||
mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => ?DESC(username),
|
||||
'maxLength' => 100,
|
||||
example => <<"admin">>
|
||||
}
|
||||
)},
|
||||
{password,
|
||||
mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => ?DESC(password),
|
||||
'maxLength' => 100,
|
||||
example => <<"public">>
|
||||
}
|
||||
)}
|
||||
].
|
|
@ -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)
|
||||
]}}.
|
|
@ -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() ->
|
||||
|
|
|
@ -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() ->
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
emqx_dashboard_api {
|
||||
|
||||
get_sso.desc:
|
||||
"""List all SSO backends"""
|
||||
get_sso.label:
|
||||
"""SSO Backends"""
|
||||
|
||||
login.desc:
|
||||
"""Get Dashboard Auth Token."""
|
||||
login.label:
|
||||
"""Get Dashboard Auth Token."""
|
||||
|
||||
get_running.desc:
|
||||
"""Get Running SSO backends"""
|
||||
get_running.label:
|
||||
"""Running SSO"""
|
||||
|
||||
get_backend.desc:
|
||||
"""Get details of a backend"""
|
||||
get_backend.label:
|
||||
"""Backend Details"""
|
||||
|
||||
create_backend.desc:
|
||||
"""Create a backend"""
|
||||
create_backend.label:
|
||||
"""Create Backend"""
|
||||
|
||||
update_backend.desc:
|
||||
"""Update a backend"""
|
||||
update_backend.label:
|
||||
"""Update Backend"""
|
||||
|
||||
delete_backend.desc:
|
||||
"""Delete a backend"""
|
||||
delete_backend.label:
|
||||
"""Delete Backend"""
|
||||
|
||||
login_failed401.desc:
|
||||
"""Login failed. Bad username or password"""
|
||||
|
||||
backend_not_found.desc:
|
||||
"""Operate failed. Backend not exists"""
|
||||
|
||||
backend_name.desc:
|
||||
"""Backend name"""
|
||||
|
||||
backend_name.label:
|
||||
"""Backend Name"""
|
||||
|
||||
}
|
|
@ -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