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.
|
%% a predefined configuration would replace these macros.
|
||||||
-define(ROLE_VIEWER, <<"viewer">>).
|
-define(ROLE_VIEWER, <<"viewer">>).
|
||||||
-define(ROLE_SUPERUSER, <<"superuser">>).
|
-define(ROLE_SUPERUSER, <<"superuser">>).
|
||||||
|
|
||||||
-define(ROLE_DEFAULT, ?ROLE_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, {
|
-record(?ADMIN, {
|
||||||
username :: binary(),
|
username :: dashboard_username(),
|
||||||
pwdhash :: binary(),
|
pwdhash :: binary(),
|
||||||
description :: binary(),
|
description :: binary(),
|
||||||
role = ?ROLE_DEFAULT :: binary(),
|
role = ?ROLE_DEFAULT :: dashboard_user_role(),
|
||||||
extra = #{} :: map()
|
extra = #{} :: map()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-type dashboard_user_role() :: binary().
|
|
||||||
-type dashboard_user() :: #?ADMIN{}.
|
-type dashboard_user() :: #?ADMIN{}.
|
||||||
|
|
||||||
-define(ADMIN_JWT, emqx_admin_jwt).
|
-define(ADMIN_JWT, emqx_admin_jwt).
|
||||||
|
|
|
@ -60,6 +60,10 @@
|
||||||
|
|
||||||
-export([backup_tables/0]).
|
-export([backup_tables/0]).
|
||||||
|
|
||||||
|
-if(?EMQX_RELEASE_EDITION == ee).
|
||||||
|
-export([add_sso_user/4, lookup_user/2]).
|
||||||
|
-endif.
|
||||||
|
|
||||||
-type emqx_admin() :: #?ADMIN{}.
|
-type emqx_admin() :: #?ADMIN{}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -99,10 +103,9 @@ add_default_user() ->
|
||||||
%% API
|
%% API
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-spec add_user(binary(), binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, any()}.
|
-spec add_user(dashboard_username(), binary(), dashboard_user_role(), binary()) ->
|
||||||
add_user(Username, Password, Role, Desc) when
|
{ok, map()} | {error, any()}.
|
||||||
is_binary(Username), is_binary(Password)
|
add_user(Username, Password, Role, Desc) when is_binary(Username), is_binary(Password) ->
|
||||||
->
|
|
||||||
case {legal_username(Username), legal_password(Password), legal_role(Role)} of
|
case {legal_username(Username), legal_password(Password), legal_role(Role)} of
|
||||||
{ok, ok, ok} -> do_add_user(Username, Password, Role, Desc);
|
{ok, ok, ok} -> do_add_user(Username, Password, Role, Desc);
|
||||||
{{error, Reason}, _, _} -> {error, Reason};
|
{{error, Reason}, _, _} -> {error, Reason};
|
||||||
|
@ -204,7 +207,7 @@ add_user_(Username, Password, Role, Desc) ->
|
||||||
description = Desc
|
description = Desc
|
||||||
},
|
},
|
||||||
mnesia:write(Admin),
|
mnesia:write(Admin),
|
||||||
#{username => Username, role => Role, description => Desc};
|
flatten_username(#{username => Username, role => Role, description => Desc});
|
||||||
[_] ->
|
[_] ->
|
||||||
mnesia:abort(<<"username_already_exist">>)
|
mnesia:abort(<<"username_already_exist">>)
|
||||||
end.
|
end.
|
||||||
|
@ -225,7 +228,8 @@ remove_user(Username) when is_binary(Username) ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
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) ->
|
update_user(Username, Role, Desc) when is_binary(Username) ->
|
||||||
case legal_role(Role) of
|
case legal_role(Role) of
|
||||||
ok ->
|
ok ->
|
||||||
|
@ -272,7 +276,10 @@ update_user_(Username, Role, Desc) ->
|
||||||
mnesia:abort(<<"username_not_found">>);
|
mnesia:abort(<<"username_not_found">>);
|
||||||
[Admin] ->
|
[Admin] ->
|
||||||
mnesia:write(Admin#?ADMIN{role = Role, description = Desc}),
|
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.
|
end.
|
||||||
|
|
||||||
change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) ->
|
change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) ->
|
||||||
|
@ -312,8 +319,8 @@ update_pwd(Username, Fun) ->
|
||||||
end,
|
end,
|
||||||
return(mria:transaction(?DASHBOARD_SHARD, Trans)).
|
return(mria:transaction(?DASHBOARD_SHARD, Trans)).
|
||||||
|
|
||||||
-spec lookup_user(binary()) -> [emqx_admin()].
|
-spec lookup_user(dashboard_username()) -> [emqx_admin()].
|
||||||
lookup_user(Username) when is_binary(Username) ->
|
lookup_user(Username) ->
|
||||||
Fun = fun() -> mnesia:read(?ADMIN, Username) end,
|
Fun = fun() -> mnesia:read(?ADMIN, Username) end,
|
||||||
{atomic, User} = mria:ro_transaction(?DASHBOARD_SHARD, Fun),
|
{atomic, User} = mria:ro_transaction(?DASHBOARD_SHARD, Fun),
|
||||||
User.
|
User.
|
||||||
|
@ -328,11 +335,11 @@ all_users() ->
|
||||||
role = Role
|
role = Role
|
||||||
}
|
}
|
||||||
) ->
|
) ->
|
||||||
#{
|
flatten_username(#{
|
||||||
username => Username,
|
username => Username,
|
||||||
description => Desc,
|
description => Desc,
|
||||||
role => ensure_role(Role)
|
role => ensure_role(Role)
|
||||||
}
|
})
|
||||||
end,
|
end,
|
||||||
ets:tab2list(?ADMIN)
|
ets:tab2list(?ADMIN)
|
||||||
).
|
).
|
||||||
|
@ -410,6 +417,28 @@ legal_role(Role) ->
|
||||||
role(Data) ->
|
role(Data) ->
|
||||||
emqx_dashboard_rbac: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.
|
-else.
|
||||||
|
|
||||||
-dialyzer({no_match, [add_user/4, update_user/3]}).
|
-dialyzer({no_match, [add_user/4, update_user/3]}).
|
||||||
|
@ -419,6 +448,9 @@ legal_role(_) ->
|
||||||
|
|
||||||
role(_) ->
|
role(_) ->
|
||||||
?ROLE_DEFAULT.
|
?ROLE_DEFAULT.
|
||||||
|
|
||||||
|
flatten_username(Data) ->
|
||||||
|
Data.
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
|
|
|
@ -89,6 +89,7 @@ schema("/logout") ->
|
||||||
post => #{
|
post => #{
|
||||||
tags => [<<"dashboard">>],
|
tags => [<<"dashboard">>],
|
||||||
desc => ?DESC(logout_api),
|
desc => ?DESC(logout_api),
|
||||||
|
parameters => sso_parameters(),
|
||||||
'requestBody' => fields([username]),
|
'requestBody' => fields([username]),
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Dashboard logout successfully">>,
|
204 => <<"Dashboard logout successfully">>,
|
||||||
|
@ -114,7 +115,7 @@ schema("/users") ->
|
||||||
desc => ?DESC(create_user_api),
|
desc => ?DESC(create_user_api),
|
||||||
'requestBody' => fields([username, password, role, description]),
|
'requestBody' => fields([username, password, role, description]),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => fields([username, role, description])
|
200 => fields([username, role, description, backend])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -124,17 +125,17 @@ schema("/users/:username") ->
|
||||||
put => #{
|
put => #{
|
||||||
tags => [<<"dashboard">>],
|
tags => [<<"dashboard">>],
|
||||||
desc => ?DESC(update_user_api),
|
desc => ?DESC(update_user_api),
|
||||||
parameters => fields([username_in_path]),
|
parameters => sso_parameters(fields([username_in_path])),
|
||||||
'requestBody' => fields([role, description]),
|
'requestBody' => fields([role, description]),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => fields([username, role, description]),
|
200 => fields([username, role, description, backend]),
|
||||||
404 => response_schema(404)
|
404 => response_schema(404)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
tags => [<<"dashboard">>],
|
tags => [<<"dashboard">>],
|
||||||
desc => ?DESC(delete_user_api),
|
desc => ?DESC(delete_user_api),
|
||||||
parameters => fields([username_in_path]),
|
parameters => sso_parameters(fields([username_in_path])),
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Delete User successfully">>,
|
204 => <<"Delete User successfully">>,
|
||||||
400 => emqx_dashboard_swagger:error_codes(
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
|
@ -169,7 +170,7 @@ response_schema(404) ->
|
||||||
emqx_dashboard_swagger:error_codes([?USER_NOT_FOUND], ?DESC(users_api404)).
|
emqx_dashboard_swagger:error_codes([?USER_NOT_FOUND], ?DESC(users_api404)).
|
||||||
|
|
||||||
fields(user) ->
|
fields(user) ->
|
||||||
fields([username, description]);
|
fields([username, role, description, backend]);
|
||||||
fields(List) ->
|
fields(List) ->
|
||||||
[field(Key) || Key <- List, field_filter(Key)].
|
[field(Key) || Key <- List, field_filter(Key)].
|
||||||
|
|
||||||
|
@ -206,7 +207,10 @@ field(old_pwd) ->
|
||||||
field(new_pwd) ->
|
field(new_pwd) ->
|
||||||
{new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})};
|
{new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})};
|
||||||
field(role) ->
|
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
|
%% API
|
||||||
|
@ -229,15 +233,16 @@ login(post, #{body := Params}) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
logout(_, #{
|
logout(_, #{
|
||||||
body := #{<<"username">> := Username},
|
body := #{<<"username">> := Username0} = Req,
|
||||||
headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}
|
headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}
|
||||||
}) ->
|
}) ->
|
||||||
|
Username = username(Req, Username0),
|
||||||
case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of
|
case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of
|
||||||
ok ->
|
ok ->
|
||||||
?SLOG(info, #{msg => "Dashboard logout successfully", username => Username}),
|
?SLOG(info, #{msg => "Dashboard logout successfully", username => Username0}),
|
||||||
204;
|
204;
|
||||||
_R ->
|
_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">>}
|
{401, ?WRONG_TOKEN_OR_USERNAME, <<"Ensure your token & username">>}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -266,9 +271,10 @@ users(post, #{body := Params}) ->
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
user(put, #{bindings := #{username := Username}, body := Params}) ->
|
user(put, #{bindings := #{username := Username0}, body := Params} = Req) ->
|
||||||
Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT),
|
Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT),
|
||||||
Desc = maps:get(<<"description">>, Params),
|
Desc = maps:get(<<"description">>, Params),
|
||||||
|
Username = username(Req, Username0),
|
||||||
case emqx_dashboard_admin:update_user(Username, Role, Desc) of
|
case emqx_dashboard_admin:update_user(Username, Role, Desc) of
|
||||||
{ok, Result} ->
|
{ok, Result} ->
|
||||||
{200, filter_result(Result)};
|
{200, filter_result(Result)};
|
||||||
|
@ -277,14 +283,15 @@ user(put, #{bindings := #{username := Username}, body := Params}) ->
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, ?BAD_REQUEST, Reason}
|
{400, ?BAD_REQUEST, Reason}
|
||||||
end;
|
end;
|
||||||
user(delete, #{bindings := #{username := Username}, headers := Headers}) ->
|
user(delete, #{bindings := #{username := Username0}, headers := Headers} = Req) ->
|
||||||
case Username == emqx_dashboard_admin:default_username() of
|
case Username0 == emqx_dashboard_admin:default_username() of
|
||||||
true ->
|
true ->
|
||||||
?SLOG(info, #{msg => "Dashboard delete admin user failed", username => Username}),
|
?SLOG(info, #{msg => "Dashboard delete admin user failed", username => Username0}),
|
||||||
Message = list_to_binary(io_lib:format("Cannot delete user ~p", [Username])),
|
Message = list_to_binary(io_lib:format("Cannot delete user ~p", [Username0])),
|
||||||
{400, ?NOT_ALLOWED, Message};
|
{400, ?NOT_ALLOWED, Message};
|
||||||
false ->
|
false ->
|
||||||
case is_self_auth(Username, Headers) of
|
Username = username(Req, Username0),
|
||||||
|
case is_self_auth(Username0, Headers) of
|
||||||
true ->
|
true ->
|
||||||
{400, ?NOT_ALLOWED, <<"Cannot delete self">>};
|
{400, ?NOT_ALLOWED, <<"Cannot delete self">>};
|
||||||
false ->
|
false ->
|
||||||
|
@ -293,13 +300,15 @@ user(delete, #{bindings := #{username := Username}, headers := Headers}) ->
|
||||||
{404, ?USER_NOT_FOUND, Reason};
|
{404, ?USER_NOT_FOUND, Reason};
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
?SLOG(info, #{
|
?SLOG(info, #{
|
||||||
msg => "Dashboard delete admin user", username => Username
|
msg => "Dashboard delete admin user", username => Username0
|
||||||
}),
|
}),
|
||||||
{204}
|
{204}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
is_self_auth(?SSO_USERNAME(_, _), _) ->
|
||||||
|
fasle;
|
||||||
is_self_auth(Username, #{<<"authorization">> := Token}) ->
|
is_self_auth(Username, #{<<"authorization">> := Token}) ->
|
||||||
is_self_auth(Username, Token);
|
is_self_auth(Username, Token);
|
||||||
is_self_auth(Username, #{<<"Authorization">> := Token}) ->
|
is_self_auth(Username, #{<<"Authorization">> := Token}) ->
|
||||||
|
@ -362,6 +371,19 @@ field_filter(_) ->
|
||||||
filter_result(Result) ->
|
filter_result(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.
|
-else.
|
||||||
|
|
||||||
field_filter(role) ->
|
field_filter(role) ->
|
||||||
|
@ -372,6 +394,14 @@ field_filter(_) ->
|
||||||
filter_result(Result) when is_list(Result) ->
|
filter_result(Result) when is_list(Result) ->
|
||||||
lists:map(fun filter_result/1, Result);
|
lists:map(fun filter_result/1, Result);
|
||||||
filter_result(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.
|
-endif.
|
||||||
|
|
|
@ -179,6 +179,9 @@ owner(Token) ->
|
||||||
{atomic, []} -> {error, not_found}
|
{atomic, []} -> {error, not_found}
|
||||||
end.
|
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) ->
|
jwk(Username, Password, Salt) ->
|
||||||
Key = crypto:hash(md5, <<Salt/binary, Username/binary, Password/binary>>),
|
Key = crypto:hash(md5, <<Salt/binary, Username/binary, Password/binary>>),
|
||||||
#{
|
#{
|
||||||
|
@ -192,12 +195,17 @@ jwt_expiration_time() ->
|
||||||
token_ttl() ->
|
token_ttl() ->
|
||||||
emqx_conf:get([dashboard, token_expired_time], ?EXPTIME).
|
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, Username, Role, ExpTime) ->
|
||||||
|
format(Token, local, Username, Role, ExpTime).
|
||||||
|
|
||||||
|
format(Token, Backend, Username, Role, ExpTime) ->
|
||||||
#?ADMIN_JWT{
|
#?ADMIN_JWT{
|
||||||
token = Token,
|
token = Token,
|
||||||
username = Username,
|
username = Username,
|
||||||
exptime = ExpTime,
|
exptime = ExpTime,
|
||||||
extra = #{role => Role}
|
extra = #{role => Role, backend => Backend}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
|
@ -61,11 +61,11 @@ t_permission(_) ->
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertMatch(
|
||||||
#{
|
#{
|
||||||
<<"username">> => ViewerUser,
|
<<"username">> := ViewerUser,
|
||||||
<<"role">> => ?ROLE_VIEWER,
|
<<"role">> := ?ROLE_VIEWER,
|
||||||
<<"description">> => ?ADD_DESCRIPTION
|
<<"description">> := ?ADD_DESCRIPTION
|
||||||
},
|
},
|
||||||
emqx_utils_json:decode(Payload, [return_maps])
|
emqx_utils_json:decode(Payload, [return_maps])
|
||||||
),
|
),
|
||||||
|
@ -104,11 +104,11 @@ t_update_role(_) ->
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
?assertEqual(
|
?assertMatch(
|
||||||
#{
|
#{
|
||||||
<<"username">> => ?DEFAULT_SUPERUSER,
|
<<"username">> := ?DEFAULT_SUPERUSER,
|
||||||
<<"role">> => ?ROLE_VIEWER,
|
<<"role">> := ?ROLE_VIEWER,
|
||||||
<<"description">> => ?ADD_DESCRIPTION
|
<<"description">> := ?ADD_DESCRIPTION
|
||||||
},
|
},
|
||||||
emqx_utils_json:decode(Payload, [return_maps])
|
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, [
|
{application, emqx_enterprise, [
|
||||||
{description, "EMQX Enterprise Edition"},
|
{description, "EMQX Enterprise Edition"},
|
||||||
{vsn, "0.1.2"},
|
{vsn, "0.1.3"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{applications, [
|
{applications, [
|
||||||
kernel,
|
kernel,
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
-define(EE_SCHEMA_MODULES, [
|
-define(EE_SCHEMA_MODULES, [
|
||||||
emqx_license_schema,
|
emqx_license_schema,
|
||||||
emqx_schema_registry_schema,
|
emqx_schema_registry_schema,
|
||||||
emqx_ft_schema
|
emqx_ft_schema,
|
||||||
|
emqx_dashboard_sso_schema
|
||||||
]).
|
]).
|
||||||
|
|
||||||
namespace() ->
|
namespace() ->
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
%% ecpool connect & reconnect
|
%% ecpool connect & reconnect
|
||||||
-export([connect/1]).
|
-export([connect/1]).
|
||||||
|
|
||||||
-export([roots/0, fields/1]).
|
-export([roots/0, fields/1, desc/1]).
|
||||||
|
|
||||||
-export([do_get_status/1]).
|
-export([do_get_status/1]).
|
||||||
|
|
||||||
|
@ -75,8 +75,16 @@ fields(config) ->
|
||||||
?HOCON(emqx_schema:timeout_duration_ms(), #{
|
?HOCON(emqx_schema:timeout_duration_ms(), #{
|
||||||
desc => ?DESC(request_timeout),
|
desc => ?DESC(request_timeout),
|
||||||
default => <<"5s">>
|
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) ->
|
fields(bind_opts) ->
|
||||||
[
|
[
|
||||||
{bind_password,
|
{bind_password,
|
||||||
|
@ -92,6 +100,11 @@ fields(bind_opts) ->
|
||||||
)}
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
desc(ssl) ->
|
||||||
|
?DESC(emqx_connector_schema_lib, "ssl");
|
||||||
|
desc(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
server() ->
|
server() ->
|
||||||
Meta = #{desc => ?DESC("server")},
|
Meta = #{desc => ?DESC("server")},
|
||||||
emqx_schema:servers_sc(Meta, ?LDAP_HOST_OPTIONS).
|
emqx_schema:servers_sc(Meta, ?LDAP_HOST_OPTIONS).
|
||||||
|
|
|
@ -115,7 +115,8 @@
|
||||||
emqx_ft,
|
emqx_ft,
|
||||||
emqx_ldap,
|
emqx_ldap,
|
||||||
emqx_gcp_device,
|
emqx_gcp_device,
|
||||||
emqx_dashboard_rbac
|
emqx_dashboard_rbac,
|
||||||
|
emqx_dashboard_sso
|
||||||
],
|
],
|
||||||
%% must always be of type `load'
|
%% must always be of type `load'
|
||||||
ce_business_apps =>
|
ce_business_apps =>
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -227,7 +227,8 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
:emqx_bridge_azure_event_hub,
|
:emqx_bridge_azure_event_hub,
|
||||||
:emqx_ldap,
|
:emqx_ldap,
|
||||||
:emqx_gcp_device,
|
:emqx_gcp_device,
|
||||||
:emqx_dashboard_rbac
|
:emqx_dashboard_rbac,
|
||||||
|
:emqx_dashboard_sso
|
||||||
])
|
])
|
||||||
end
|
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_ldap") -> false;
|
||||||
is_community_umbrella_app("apps/emqx_gcp_device") -> 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_rbac") -> false;
|
||||||
|
is_community_umbrella_app("apps/emqx_dashboard_sso") -> false;
|
||||||
is_community_umbrella_app(_) -> true.
|
is_community_umbrella_app(_) -> true.
|
||||||
|
|
||||||
is_jq_supported() ->
|
is_jq_supported() ->
|
||||||
|
|
|
@ -79,4 +79,10 @@ users_api404.desc:
|
||||||
version.desc:
|
version.desc:
|
||||||
"""EMQX Version"""
|
"""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