Просмотр исходного кода

Merge pull request #11639 from lafirest/test/sso

test(sso): add test case for the integration of SSO with LDAP
JianBo He 2 лет назад
Родитель
Сommit
b924fb618a

+ 1 - 1
apps/emqx_dashboard/include/emqx_dashboard.hrl

@@ -21,7 +21,7 @@
 %% In full RBAC feature, the role may be customised created and deleted,
 %% a predefined configuration would replace these macros.
 -define(ROLE_VIEWER, <<"viewer">>).
--define(ROLE_SUPERUSER, <<"superuser">>).
+-define(ROLE_SUPERUSER, <<"administrator">>).
 -define(ROLE_DEFAULT, ?ROLE_SUPERUSER).
 
 -define(SSO_USERNAME(Backend, Name), {Backend, Name}).

+ 1 - 1
apps/emqx_dashboard_sso/docker-ct

@@ -1 +1 @@
-
+ldap

+ 2 - 0
apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl

@@ -156,8 +156,10 @@ backend(get, #{bindings := #{backend := Type}}) ->
             {200, to_json(Backend)}
     end;
 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);
 backend(delete, #{bindings := #{backend := Backend}}) ->
+    ?SLOG(info, #{msg => "Delete SSO backend", backend => Backend}),
     handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined).
 
 sso_parameters(Params) ->

+ 42 - 17
apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl

@@ -40,7 +40,7 @@ fields(ldap) ->
         [
             {query_timeout, fun query_timeout/1}
         ] ++
-        emqx_ldap:fields(config) ++ emqx_ldap:fields(bind_opts);
+        adjust_ldap_fields(emqx_ldap:fields(config));
 fields(login) ->
     [
         emqx_dashboard_sso_schema:backend_schema([ldap])
@@ -84,6 +84,46 @@ destroy(#{resource_id := ResourceId}) ->
     _ = emqx_resource:remove_local(ResourceId),
     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(
     #{<<"username">> := Username} = Req,
     #{
@@ -115,25 +155,10 @@ login(
             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, <<>>)};
+            emqx_dashboard_token:sign(User, <<>>);
         [] ->
             case emqx_dashboard_admin:add_sso_user(ldap, Username, ?ROLE_VIEWER, <<>>) of
                 {ok, _} ->

+ 34 - 17
apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl

@@ -6,6 +6,8 @@
 
 -behaviour(gen_server).
 
+-include_lib("emqx/include/logger.hrl").
+
 %% API
 -export([start_link/0]).
 
@@ -122,11 +124,8 @@ init([]) ->
     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
-    ]),
+handle_call({update_config, Req, NewConf}, _From, State) ->
+    Result = on_config_update(Req, NewConf),
     {reply, Result, State};
 handle_call(_Request, _From, State) ->
     Reply = ok,
@@ -156,22 +155,40 @@ start_backend_services() ->
     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
-            )
+            case emqx_dashboard_sso:create(Provider, Config) of
+                {ok, State} ->
+                    ?SLOG(info, #{
+                        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,
         maps:to_list(Backends)
     ).
 
-update_config(_Backend, UpdateReq) ->
+update_config(Backend, UpdateReq) ->
     case emqx_conf:update([dashboard_sso], UpdateReq, #{override_to => cluster}) of
         {ok, UpdateResult} ->
             #{post_config_update := #{?MODULE := Result}} = UpdateResult,
+            ?SLOG(info, #{
+                msg => "Update SSO configuration successfully",
+                backend => Backend,
+                result => Result
+            }),
             Result;
-        Error ->
+        {error, Reason} = Error ->
+            ?SLOG(error, #{
+                msg => "Update SSO configuration failed",
+                backend => Backend,
+                reason => Reason
+            }),
             Error
     end.
 
@@ -187,11 +204,11 @@ pre_config_update(_Path, {delete, Backend}, OldConf) ->
             {ok, maps:remove(BackendBin, OldConf)}
     end.
 
-post_config_update(_Path, UpdateReq, NewConf, OldConf, _AppEnvs) ->
-    Result = call({update_config, UpdateReq, NewConf, OldConf}),
+post_config_update(_Path, UpdateReq, NewConf, _OldConf, _AppEnvs) ->
+    Result = call({update_config, UpdateReq, NewConf}),
     {ok, Result}.
 
-on_config_update({update, Backend, _Config}, NewConf, _OldConf) ->
+on_config_update({update, Backend, _Config}, NewConf) ->
     Provider = provider(Backend),
     Config = maps:get(Backend, NewConf),
     case lookup(Backend) of
@@ -210,7 +227,7 @@ on_config_update({update, Backend, _Config}, NewConf, _OldConf) ->
                 end
             )
     end;
-on_config_update({delete, Backend}, _NewConf, _OldConf) ->
+on_config_update({delete, Backend}, _NewConf) ->
     case lookup(Backend) of
         undefined ->
             {error, not_exists};

+ 152 - 0
apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl

@@ -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).