Merge pull request #11639 from lafirest/test/sso
test(sso): add test case for the integration of SSO with LDAP
This commit is contained in:
commit
b924fb618a
|
@ -21,7 +21,7 @@
|
||||||
%% In full RBAC feature, the role may be customised created and deleted,
|
%% In full RBAC feature, the role may be customised created and deleted,
|
||||||
%% 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, <<"administrator">>).
|
||||||
-define(ROLE_DEFAULT, ?ROLE_SUPERUSER).
|
-define(ROLE_DEFAULT, ?ROLE_SUPERUSER).
|
||||||
|
|
||||||
-define(SSO_USERNAME(Backend, Name), {Backend, Name}).
|
-define(SSO_USERNAME(Backend, Name), {Backend, Name}).
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
|
ldap
|
||||||
|
|
|
@ -156,8 +156,10 @@ backend(get, #{bindings := #{backend := Type}}) ->
|
||||||
{200, to_json(Backend)}
|
{200, to_json(Backend)}
|
||||||
end;
|
end;
|
||||||
backend(put, #{bindings := #{backend := Backend}, body := Config}) ->
|
backend(put, #{bindings := #{backend := Backend}, body := Config}) ->
|
||||||
|
?SLOG(info, #{msg => "Update SSO backend", backend => Backend, config => Config}),
|
||||||
on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:update/2);
|
on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:update/2);
|
||||||
backend(delete, #{bindings := #{backend := Backend}}) ->
|
backend(delete, #{bindings := #{backend := Backend}}) ->
|
||||||
|
?SLOG(info, #{msg => "Delete SSO backend", backend => Backend}),
|
||||||
handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined).
|
handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined).
|
||||||
|
|
||||||
sso_parameters(Params) ->
|
sso_parameters(Params) ->
|
||||||
|
|
|
@ -40,7 +40,7 @@ fields(ldap) ->
|
||||||
[
|
[
|
||||||
{query_timeout, fun query_timeout/1}
|
{query_timeout, fun query_timeout/1}
|
||||||
] ++
|
] ++
|
||||||
emqx_ldap:fields(config) ++ emqx_ldap:fields(bind_opts);
|
adjust_ldap_fields(emqx_ldap:fields(config));
|
||||||
fields(login) ->
|
fields(login) ->
|
||||||
[
|
[
|
||||||
emqx_dashboard_sso_schema:backend_schema([ldap])
|
emqx_dashboard_sso_schema:backend_schema([ldap])
|
||||||
|
@ -84,6 +84,46 @@ destroy(#{resource_id := ResourceId}) ->
|
||||||
_ = emqx_resource:remove_local(ResourceId),
|
_ = emqx_resource:remove_local(ResourceId),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
parse_config(Config0) ->
|
||||||
|
Config = ensure_bind_password(Config0),
|
||||||
|
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}.
|
||||||
|
|
||||||
|
%% In this feature, the `bind_password` is fixed, so it should conceal from the swagger,
|
||||||
|
%% but the connector still needs it, hence we should add it back here
|
||||||
|
ensure_bind_password(Config) ->
|
||||||
|
Config#{bind_password => <<"${password}">>}.
|
||||||
|
|
||||||
|
adjust_ldap_fields(Fields) ->
|
||||||
|
adjust_ldap_fields(Fields, []).
|
||||||
|
|
||||||
|
adjust_ldap_fields([{filter, Meta} | T], Acc) ->
|
||||||
|
adjust_ldap_fields(
|
||||||
|
T,
|
||||||
|
[
|
||||||
|
{filter, Meta#{
|
||||||
|
default => <<"(objectClass=user)">>,
|
||||||
|
example => <<"(objectClass=user)">>
|
||||||
|
}}
|
||||||
|
| Acc
|
||||||
|
]
|
||||||
|
);
|
||||||
|
adjust_ldap_fields([Any | T], Acc) ->
|
||||||
|
adjust_ldap_fields(T, [Any | Acc]);
|
||||||
|
adjust_ldap_fields([], Acc) ->
|
||||||
|
lists:reverse(Acc).
|
||||||
|
|
||||||
login(
|
login(
|
||||||
#{<<"username">> := Username} = Req,
|
#{<<"username">> := Username} = Req,
|
||||||
#{
|
#{
|
||||||
|
@ -115,25 +155,10 @@ login(
|
||||||
Error
|
Error
|
||||||
end.
|
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) ->
|
ensure_user_exists(Username) ->
|
||||||
case emqx_dashboard_admin:lookup_user(ldap, Username) of
|
case emqx_dashboard_admin:lookup_user(ldap, Username) of
|
||||||
[User] ->
|
[User] ->
|
||||||
{ok, emqx_dashboard_token:sign(User, <<>>)};
|
emqx_dashboard_token:sign(User, <<>>);
|
||||||
[] ->
|
[] ->
|
||||||
case emqx_dashboard_admin:add_sso_user(ldap, Username, ?ROLE_VIEWER, <<>>) of
|
case emqx_dashboard_admin:add_sso_user(ldap, Username, ?ROLE_VIEWER, <<>>) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([start_link/0]).
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
@ -122,11 +124,8 @@ init([]) ->
|
||||||
start_backend_services(),
|
start_backend_services(),
|
||||||
{ok, #{}}.
|
{ok, #{}}.
|
||||||
|
|
||||||
handle_call({update_config, Req, NewConf, OldConf}, _From, State) ->
|
handle_call({update_config, Req, NewConf}, _From, State) ->
|
||||||
Result = on_config_update(Req, NewConf, OldConf),
|
Result = on_config_update(Req, NewConf),
|
||||||
io:format(">>> on_config_update:~p~n,Req:~p~n NewConf:~p~n OldConf:~p~n", [
|
|
||||||
Result, Req, NewConf, OldConf
|
|
||||||
]),
|
|
||||||
{reply, Result, State};
|
{reply, Result, State};
|
||||||
handle_call(_Request, _From, State) ->
|
handle_call(_Request, _From, State) ->
|
||||||
Reply = ok,
|
Reply = ok,
|
||||||
|
@ -156,22 +155,40 @@ start_backend_services() ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun({Backend, Config}) ->
|
fun({Backend, Config}) ->
|
||||||
Provider = provider(Backend),
|
Provider = provider(Backend),
|
||||||
on_backend_updated(
|
case emqx_dashboard_sso:create(Provider, Config) of
|
||||||
emqx_dashboard_sso:create(Provider, Config),
|
{ok, State} ->
|
||||||
fun(State) ->
|
?SLOG(info, #{
|
||||||
ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State})
|
msg => "Start SSO backend successfully",
|
||||||
|
backend => Backend
|
||||||
|
}),
|
||||||
|
ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State});
|
||||||
|
{error, Reason} ->
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => "Start SSO backend failed",
|
||||||
|
backend => Backend,
|
||||||
|
reason => Reason
|
||||||
|
})
|
||||||
end
|
end
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
maps:to_list(Backends)
|
maps:to_list(Backends)
|
||||||
).
|
).
|
||||||
|
|
||||||
update_config(_Backend, UpdateReq) ->
|
update_config(Backend, UpdateReq) ->
|
||||||
case emqx_conf:update([dashboard_sso], UpdateReq, #{override_to => cluster}) of
|
case emqx_conf:update([dashboard_sso], UpdateReq, #{override_to => cluster}) of
|
||||||
{ok, UpdateResult} ->
|
{ok, UpdateResult} ->
|
||||||
#{post_config_update := #{?MODULE := Result}} = UpdateResult,
|
#{post_config_update := #{?MODULE := Result}} = UpdateResult,
|
||||||
|
?SLOG(info, #{
|
||||||
|
msg => "Update SSO configuration successfully",
|
||||||
|
backend => Backend,
|
||||||
|
result => Result
|
||||||
|
}),
|
||||||
Result;
|
Result;
|
||||||
Error ->
|
{error, Reason} = Error ->
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => "Update SSO configuration failed",
|
||||||
|
backend => Backend,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -187,11 +204,11 @@ pre_config_update(_Path, {delete, Backend}, OldConf) ->
|
||||||
{ok, maps:remove(BackendBin, OldConf)}
|
{ok, maps:remove(BackendBin, OldConf)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
post_config_update(_Path, UpdateReq, NewConf, OldConf, _AppEnvs) ->
|
post_config_update(_Path, UpdateReq, NewConf, _OldConf, _AppEnvs) ->
|
||||||
Result = call({update_config, UpdateReq, NewConf, OldConf}),
|
Result = call({update_config, UpdateReq, NewConf}),
|
||||||
{ok, Result}.
|
{ok, Result}.
|
||||||
|
|
||||||
on_config_update({update, Backend, _Config}, NewConf, _OldConf) ->
|
on_config_update({update, Backend, _Config}, NewConf) ->
|
||||||
Provider = provider(Backend),
|
Provider = provider(Backend),
|
||||||
Config = maps:get(Backend, NewConf),
|
Config = maps:get(Backend, NewConf),
|
||||||
case lookup(Backend) of
|
case lookup(Backend) of
|
||||||
|
@ -210,7 +227,7 @@ on_config_update({update, Backend, _Config}, NewConf, _OldConf) ->
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
end;
|
end;
|
||||||
on_config_update({delete, Backend}, _NewConf, _OldConf) ->
|
on_config_update({delete, Backend}, _NewConf) ->
|
||||||
case lookup(Backend) of
|
case lookup(Backend) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{error, not_exists};
|
{error, not_exists};
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_dashboard_sso_ldap_SUITE).
|
||||||
|
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(LDAP_HOST, "ldap").
|
||||||
|
-define(LDAP_DEFAULT_PORT, 389).
|
||||||
|
-define(LDAP_USER, <<"mqttuser0001">>).
|
||||||
|
-define(LDAP_USER_PASSWORD, <<"mqttuser0001">>).
|
||||||
|
-import(emqx_mgmt_api_test_util, [request/2, request/3, uri/1]).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[
|
||||||
|
t_create,
|
||||||
|
t_update,
|
||||||
|
t_get,
|
||||||
|
t_login_with_bad,
|
||||||
|
t_first_login,
|
||||||
|
t_next_login,
|
||||||
|
t_delete
|
||||||
|
].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
_ = application:load(emqx_conf),
|
||||||
|
emqx_config:save_schema_mod_and_names(emqx_dashboard_sso_schema),
|
||||||
|
emqx_mgmt_api_test_util:init_suite([emqx_dashboard, emqx_dashboard_sso]),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
All = emqx_dashboard_admin:all_users(),
|
||||||
|
[emqx_dashboard_admin:remove_user(Name) || #{username := Name} <- All],
|
||||||
|
emqx_mgmt_api_test_util:end_suite([emqx_conf, emqx_dashboard_sso]).
|
||||||
|
|
||||||
|
init_per_testcase(_, Config) ->
|
||||||
|
{ok, _} = emqx_cluster_rpc:start_link(),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_testcase(Case, _) ->
|
||||||
|
Case =:= t_delete_backend andalso emqx_dashboard_sso_manager:delete(ldap),
|
||||||
|
case erlang:whereis(node()) of
|
||||||
|
undefined ->
|
||||||
|
ok;
|
||||||
|
P ->
|
||||||
|
erlang:unlink(P),
|
||||||
|
erlang:exit(P, kill)
|
||||||
|
end,
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_create(_) ->
|
||||||
|
Path = uri(["sso", "ldap"]),
|
||||||
|
{ok, 200, Result} = request(put, Path, ldap_config()),
|
||||||
|
?assertMatch(#{backend := <<"ldap">>, enable := false}, decode_json(Result)),
|
||||||
|
?assertMatch([#{backend := <<"ldap">>, enable := false}], get_sso()),
|
||||||
|
?assertNotEqual(undefined, emqx_dashboard_sso_manager:lookup_state(ldap)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_update(_) ->
|
||||||
|
Path = uri(["sso", "ldap"]),
|
||||||
|
{ok, 200, Result} = request(put, Path, ldap_config(#{<<"enable">> => <<"true">>})),
|
||||||
|
?assertMatch(#{backend := <<"ldap">>, enable := true}, decode_json(Result)),
|
||||||
|
?assertMatch([#{backend := <<"ldap">>, enable := true}], get_sso()),
|
||||||
|
?assertNotEqual(undefined, emqx_dashboard_sso_manager:lookup_state(ldap)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_get(_) ->
|
||||||
|
Path = uri(["sso", "ldap"]),
|
||||||
|
{ok, 200, Result} = request(get, Path),
|
||||||
|
?assertMatch(#{backend := <<"ldap">>, enable := true}, decode_json(Result)),
|
||||||
|
|
||||||
|
NotExists = uri(["sso", "not"]),
|
||||||
|
{ok, 400, _} = request(get, NotExists),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_login_with_bad(_) ->
|
||||||
|
Path = uri(["sso", "login", "ldap"]),
|
||||||
|
Req = #{
|
||||||
|
<<"backend">> => <<"ldap">>,
|
||||||
|
<<"username">> => <<"bad">>,
|
||||||
|
<<"password">> => <<"password">>
|
||||||
|
},
|
||||||
|
{ok, 401, Result} = request(post, Path, Req),
|
||||||
|
?assertMatch(#{code := <<"BAD_USERNAME_OR_PWD">>}, decode_json(Result)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_first_login(_) ->
|
||||||
|
Path = uri(["sso", "login", "ldap"]),
|
||||||
|
Req = #{
|
||||||
|
<<"backend">> => <<"ldap">>,
|
||||||
|
<<"username">> => ?LDAP_USER,
|
||||||
|
<<"password">> => ?LDAP_USER_PASSWORD
|
||||||
|
},
|
||||||
|
{ok, 200, Result} = request(post, Path, Req),
|
||||||
|
?assertMatch(#{license := _, token := _}, decode_json(Result)),
|
||||||
|
?assertMatch(
|
||||||
|
[#?ADMIN{username = ?SSO_USERNAME(ldap, ?LDAP_USER)}],
|
||||||
|
emqx_dashboard_admin:lookup_user(ldap, ?LDAP_USER)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_next_login(_) ->
|
||||||
|
Path = uri(["sso", "login", "ldap"]),
|
||||||
|
Req = #{
|
||||||
|
<<"backend">> => <<"ldap">>,
|
||||||
|
<<"username">> => ?LDAP_USER,
|
||||||
|
<<"password">> => ?LDAP_USER_PASSWORD
|
||||||
|
},
|
||||||
|
{ok, 200, Result} = request(post, Path, Req),
|
||||||
|
?assertMatch(#{license := _, token := _}, decode_json(Result)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_delete(_) ->
|
||||||
|
Path = uri(["sso", "ldap"]),
|
||||||
|
?assertMatch({ok, 204, _}, request(delete, Path)),
|
||||||
|
?assertMatch({ok, 404, _}, request(delete, Path)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
get_sso() ->
|
||||||
|
Path = uri(["sso"]),
|
||||||
|
{ok, 200, Result} = request(get, Path),
|
||||||
|
decode_json(Result).
|
||||||
|
|
||||||
|
ldap_config() ->
|
||||||
|
ldap_config(#{}).
|
||||||
|
|
||||||
|
ldap_config(Override) ->
|
||||||
|
maps:merge(
|
||||||
|
#{
|
||||||
|
<<"backend">> => <<"ldap">>,
|
||||||
|
<<"enable">> => <<"false">>,
|
||||||
|
<<"server">> => ldap_server(),
|
||||||
|
<<"base_dn">> => <<"uid=${username},ou=testdevice,dc=emqx,dc=io">>,
|
||||||
|
<<"filter">> => <<"(objectClass=mqttUser)">>,
|
||||||
|
<<"username">> => <<"cn=root,dc=emqx,dc=io">>,
|
||||||
|
<<"password">> => <<"public">>,
|
||||||
|
<<"pool_size">> => 8
|
||||||
|
},
|
||||||
|
Override
|
||||||
|
).
|
||||||
|
|
||||||
|
ldap_server() ->
|
||||||
|
iolist_to_binary(io_lib:format("~s:~B", [?LDAP_HOST, ?LDAP_DEFAULT_PORT])).
|
||||||
|
|
||||||
|
decode_json(Data) ->
|
||||||
|
BinJson = emqx_utils_json:decode(Data, [return_maps]),
|
||||||
|
emqx_utils_maps:unsafe_atom_key_map(BinJson).
|
Loading…
Reference in New Issue