diff --git a/apps/emqx_dashboard/include/emqx_dashboard.hrl b/apps/emqx_dashboard/include/emqx_dashboard.hrl index 8cb1853c9..3e089ccbf 100644 --- a/apps/emqx_dashboard/include/emqx_dashboard.hrl +++ b/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}). diff --git a/apps/emqx_dashboard_sso/docker-ct b/apps/emqx_dashboard_sso/docker-ct index 8b1378917..c1142c3c5 100644 --- a/apps/emqx_dashboard_sso/docker-ct +++ b/apps/emqx_dashboard_sso/docker-ct @@ -1 +1 @@ - +ldap diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl index c6b0f8673..c163c3e8a 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/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) -> diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl index adde6c274..395deea7d 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl +++ b/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, _} -> diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl index 6097a39ff..afa27cb47 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_manager.erl +++ b/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}; diff --git a/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl b/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl new file mode 100644 index 000000000..5166aa4c5 --- /dev/null +++ b/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).