%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2012-2023. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% -module(eldap_basic_SUITE). -export([ add_already_exists/1, add_referral/1, add_when_bound/1, add_when_not_bound/1, app/1, appup/1, bind/1, client_side_add_timeout/1, client_side_bind_timeout/1, client_side_search_timeout/1, client_side_start_tls_timeout/1, close_after_tcp_error/1, close_ret_val/1, decode/1, delete/1, delete_referral/1, elementary_search/1, encode/1, modify/1, modify_dn_delete_old/1, modify_dn_keep_old/1, modify_referral/1, more_add/1, open_ret_val_error/1, open_ret_val_success/1, plain_ldap_socket_info/1, search_filter_and/1, search_filter_and_not/1, search_filter_equalityMatch/1, search_filter_equalityMatch_objectClass_exists/1, search_filter_final/1, search_filter_initial/1, search_filter_or/1, search_filter_or_sizelimit_ok/1, search_filter_or_sizelimit_exceeded/1, search_filter_substring_any/1, search_non_existant/1, search_referral/1, search_two_hits/1, search_extensible_match_with_dn/1, search_extensible_match_without_dn/1, search_paged_results/1, ssl_connection/1, ssl_conn_socket_info/1, start_tls_on_ssl_should_fail/1, start_tls_twice_should_fail/1, tcp_connection/1, tcp_connection_option/1 ]). -export([ all/0, end_per_group/2, end_per_suite/1, end_per_testcase/2, groups/0, init_per_group/2, init_per_suite/1, init_per_testcase/2, suite/0 ]). -include_lib("common_test/include/ct.hrl"). -include_lib("stdlib/include/assert.hrl"). -include_lib("eldap/include/eldap.hrl"). -include_lib("eldap/ebin/ELDAPv3.hrl"). %% Control to delete a referral object: -define(manageDsaIT, {control,"2.16.840.1.113730.3.4.2",false,asn1_NOVALUE}). suite() -> [{timetrap,{seconds,360}}]. all() -> [app, appup, {group, encode_decode}, {group, return_values}, {group, v4_connections}, {group, v6_connections}, {group, plain_api}, {group, ssl_api}, {group, start_tls_api} ]. groups() -> [{encode_decode, [], [encode, decode ]}, {plain_api, [], [{group,api}]}, {ssl_api, [], [{group,api}, start_tls_on_ssl_should_fail]}, {start_tls_api, [], [{group,api}, start_tls_twice_should_fail]}, {api, [], [{group,api_not_bound}, {group,api_bound}]}, {api_not_bound, [], [elementary_search, search_non_existant, add_when_not_bound, bind]}, {api_bound, [], [add_when_bound, add_already_exists, more_add, add_referral, search_filter_equalityMatch, search_filter_equalityMatch_objectClass_exists, search_filter_substring_any, search_filter_initial, search_filter_final, search_filter_and, search_filter_or, search_filter_and_not, search_two_hits, search_extensible_match_with_dn, search_extensible_match_without_dn, search_referral, search_filter_or_sizelimit_ok, search_filter_or_sizelimit_exceeded, search_paged_results, modify, modify_referral, delete, delete_referral, modify_dn_delete_old, modify_dn_keep_old]}, {v4_connections, [], connection_tests()}, {v6_connections, [], connection_tests()}, {return_values, [], [open_ret_val_success, open_ret_val_error, close_ret_val]} ]. connection_tests() -> [tcp_connection, tcp_connection_option, ssl_connection, client_side_start_tls_timeout, client_side_bind_timeout, client_side_add_timeout, client_side_search_timeout, close_after_tcp_error, ssl_conn_socket_info, plain_ldap_socket_info ]. ldap_servers(_Config) -> ["ulrad", "orome", "ecthelion", "bifur" ]. init_per_suite(Config) -> SSL_available = init_ssl_certs_et_al(Config), ServerHosts = ["localhost" | random_sorted(ldap_servers(Config))], LDAP_server = find_first_server(false, [{H,9876} || H <- ServerHosts]), LDAPS_server = case SSL_available of true -> find_first_server(true, [{H,9877} || H <- ServerHosts]); false -> undefined end, [{ssl_available, SSL_available}, {ldap_server, LDAP_server}, {ldaps_server, LDAPS_server} | Config]. end_per_suite(_Config) -> try ssl:stop() catch _:_ -> ok end. init_per_group(return_values, Config) -> case proplists:get_value(ldap_server,Config) of undefined -> {skip, "LDAP server not available"}; {Host,Port} -> ct:comment("ldap://~s:~p",[Host,Port]), Config end; init_per_group(plain_api, Config0) -> case proplists:get_value(ldap_server,Config0) of undefined -> {skip, "LDAP server not available"}; Server = {Host,Port} -> ct:comment("ldap://~s:~p",[Host,Port]), initialize_db([{server,Server}, {ssl_flag,false}, {start_tls,false} | Config0]) end; init_per_group(ssl_api, Config0) -> case proplists:get_value(ldaps_server,Config0) of undefined -> {skip, "LDAPS server not available"}; Server = {Host,Port} -> ct:comment("ldaps://~s:~p",[Host,Port]), initialize_db([{server,Server}, {ssl_flag,true}, {start_tls,false} | Config0]) end; init_per_group(start_tls_api, Config0) -> case {proplists:get_value(ldap_server,Config0), proplists:get_value(ssl_available,Config0)} of {undefined,true} -> {skip, "LDAP server not available"}; {_,false} -> {skip, "TLS not available"}; {Server={Host,Port}, true} -> ct:comment("ldap://~s:~p + start_tls",[Host,Port]), Config = [{server,Server}, {ssl_flag,false} | Config0], case supported_extension("1.3.6.1.4.1.1466.20037", Config) of true -> initialize_db([{start_tls,true} | Config]); false -> {skip, "start_tls not supported according to the server"} end end; init_per_group(v4_connections, Config) -> [{tcp_listen_opts, [{reuseaddr, true}]}, {listen_host, "localhost"}, {tcp_connect_opts, []} | Config]; init_per_group(v6_connections, Config) -> {ok, Hostname} = inet:gethostname(), case lists:member(list_to_atom(Hostname), ct:get_config(ipv6_hosts,[])) of true -> [{tcp_listen_opts, [inet6,{reuseaddr, true}]}, {listen_host, "::"}, {tcp_connect_opts, [{tcpopts,[inet6]}]} | Config]; false -> {skip, io_lib:format("~p is not an ipv6_host",[Hostname])} end; init_per_group(_, Config) -> Config. end_per_group(plain_api, Config) -> clear_db(Config); end_per_group(ssl_api, Config) -> clear_db(Config); end_per_group(start_tls_api, Config) -> clear_db(Config); end_per_group(_Group, Config) -> Config. init_per_testcase(TC, Config) when TC == ssl_connection; TC == ssl_conn_socket_info -> case proplists:get_value(ssl_available,Config) of true -> SSL_Port = 9999, CertFile = filename:join(proplists:get_value(data_dir,Config), "certs/server/cert.pem"), KeyFile = filename:join(proplists:get_value(data_dir,Config), "certs/server/key.pem"), Parent = self(), Listener = spawn_link( fun() -> case ssl:listen(SSL_Port, [{certfile, CertFile}, {keyfile, KeyFile} | proplists:get_value(tcp_listen_opts,Config) ]) of {ok,SSL_LSock} -> Parent ! {ok,self()}, (fun L() -> ct:log("ssl server waiting for connections...",[]), {ok, S} = ssl:transport_accept(SSL_LSock), ct:log("ssl:transport_accept/1 ok",[]), {ok,_} = ssl:handshake(S), ct:log("ssl:handshake/1 ok",[]), L() end)(); Other -> Parent ! {not_ok,Other,self()} end end), receive {ok,Listener} -> ct:log("SSL listening to port ~p (process ~p)",[SSL_Port, Listener]), [{ssl_listener,Listener}, {ssl_listen_port,SSL_Port}, {ssl_connect_opts,[]} | Config]; {no_ok,SSL_Other,Listener} -> ct:log("ssl:listen on port ~p failed: ~p",[SSL_Port,SSL_Other]), {fail, "ssl:listen/2 failed"} after 5000 -> {fail, "Waiting for ssl:listen timeout"} end; false -> {skip, "ssl not available"} end; init_per_testcase(TC, Config) -> case lists:member(TC,connection_tests()) of true -> case gen_tcp:listen(0, proplists:get_value(tcp_listen_opts,Config)) of {ok,LSock} -> {ok,{_,Port}} = inet:sockname(LSock), [{listen_socket,LSock}, {listen_port,Port} | Config]; Other -> {fail, Other} end; false -> case proplists:get_value(name,proplists:get_value(tc_group_properties, Config)) of api_not_bound -> {ok,H} = open(Config), [{handle,H} | Config]; api_bound -> {ok,H} = open(Config), ok = eldap:simple_bind(H, "cn=Manager,dc=ericsson,dc=se", "hejsan"), [{handle,H} | Config]; _Name -> Config end end. end_per_testcase(_, Config) -> catch gen_tcp:close( proplists:get_value(listen_socket, Config) ), catch eldap:close( proplists:get_value(handle,Config) ). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% Test cases %%% %%%---------------------------------------------------------------- %%% Test that the eldap app file is ok app(Config) when is_list(Config) -> ok = test_server:app_test(eldap). %%%---------------------------------------------------------------- %%% Test that the eldap appup file is ok appup(Config) when is_list(Config) -> ok = test_server:appup_test(eldap). %%%---------------------------------------------------------------- open_ret_val_success(Config) -> {Host,Port} = proplists:get_value(ldap_server,Config), {ok,H} = eldap:open([Host], [{port,Port}]), catch eldap:close(H). %%%---------------------------------------------------------------- open_ret_val_error(_Config) -> {error,_} = eldap:open(["nohost.example.com"], [{port,65535}]). %%%---------------------------------------------------------------- close_ret_val(Config) -> {Host,Port} = proplists:get_value(ldap_server,Config), {ok,H} = eldap:open([Host], [{port,Port}]), ok = eldap:close(H). %%%---------------------------------------------------------------- tcp_connection(Config) -> Host = proplists:get_value(listen_host, Config), Port = proplists:get_value(listen_port, Config), Opts = proplists:get_value(tcp_connect_opts, Config), case eldap:open([Host], [{port,Port}|Opts]) of {ok,_H} -> Sl = proplists:get_value(listen_socket, Config), case gen_tcp:accept(Sl,1000) of {ok,_S} -> ok; {error,timeout} -> ct:fail("server side accept timeout",[]); Other -> ct:fail("gen_tdp:accept failed: ~p",[Other]) end; Other -> ct:fail("eldap:open failed: ~p",[Other]) end. %%%---------------------------------------------------------------- close_after_tcp_error(Config) -> Host = proplists:get_value(listen_host, Config), Port = proplists:get_value(listen_port, Config), Opts = proplists:get_value(tcp_connect_opts, Config), T = 1000, case eldap:open([Host], [{timeout,T},{port,Port}|Opts]) of {ok,H} -> Sl = proplists:get_value(listen_socket, Config), gen_tcp:close(Sl), {error,{gen_tcp_error,closed}} = eldap:simple_bind(H, anon, anon), ok = eldap:close(H), wait_for_close(H); Other -> ct:fail("eldap:open failed: ~p",[Other]) end. wait_for_close(H) -> case erlang:is_process_alive(H) of true -> timer:sleep(100), wait_for_close(H); false -> ok end. %%%---------------------------------------------------------------- ssl_connection(Config) -> Host = proplists:get_value(listen_host, Config), Port = proplists:get_value(ssl_listen_port, Config), Opts = proplists:get_value(tcp_connect_opts, Config), SSLOpts = proplists:get_value(ssl_connect_opts, Config), case eldap:open([Host], [{port,Port}, {ssl,true}, {timeout,5000}, {sslopts,SSLOpts}|Opts]) of {ok,_H} -> ok; Other -> ct:fail("eldap:open failed: ~p",[Other]) end. %%%---------------------------------------------------------------- ssl_conn_socket_info(Config) -> Host = proplists:get_value(listen_host, Config), Port = proplists:get_value(ssl_listen_port, Config), Opts = proplists:get_value(tcp_connect_opts, Config), SSLOpts = proplists:get_value(ssl_connect_opts, Config), case eldap:open([Host], [{port,Port}, {ssl,true}, {timeout,5000}, {sslopts,SSLOpts}|Opts]) of {ok,H} -> #{socket := Socket, socket_type := ssl} = eldap:info(H), ?assertMatch({ok, _Data}, ssl:connection_information(Socket)); Other -> ct:fail("eldap:open failed: ~p",[Other]) end. %%%---------------------------------------------------------------- plain_ldap_socket_info(Config) -> Host = proplists:get_value(listen_host, Config), Port = proplists:get_value(listen_port, Config), Opts = proplists:get_value(tcp_connect_opts, Config), T = 1000, case eldap:open([Host], [{timeout,T},{port,Port}|Opts]) of {ok,H} -> ?assertMatch(#{socket := _, socket_type := tcp}, eldap:info(H)); Other -> ct:fail("eldap:open failed: ~p",[Other]) end. %%%---------------------------------------------------------------- client_side_add_timeout(Config) -> client_timeout( fun(H) -> eldap:add(H, "cn=Foo Bar,dc=host,dc=ericsson,dc=se", [{"objectclass", ["person"]}, {"cn", ["Foo Bar"]}, {"sn", ["Bar"]}, {"telephoneNumber", ["555-1232", "555-5432"]}]) end, Config). %%%---------------------------------------------------------------- client_side_bind_timeout(Config) -> client_timeout( fun(H) -> eldap:simple_bind(H, anon, anon) end, Config). %%%---------------------------------------------------------------- client_side_search_timeout(Config) -> client_timeout( fun(H) -> eldap:search(H, [{base,"dc=host,dc=ericsson,dc=se"}, {filter, eldap:present("objectclass")}, {scope, eldap:wholeSubtree()}]) end, Config). %%%---------------------------------------------------------------- client_side_start_tls_timeout(Config) -> client_timeout( fun(H) -> eldap:start_tls(H, []) end, Config). %%%---------------------------------------------------------------- tcp_connection_option(Config) -> Host = proplists:get_value(listen_host, Config), Port = proplists:get_value(listen_port, Config), Opts = proplists:get_value(tcp_connect_opts, Config), Sl = proplists:get_value(listen_socket, Config), %% Make an option value to test. The option must be implemented on all %% platforms that we test on. Must check what the default value is %% so we don't happen to choose that particular value. {ok,[{linger,DefaultLinger}]} = inet:getopts(Sl, [linger]), TestLinger = case DefaultLinger of {false,_} -> {true,5}; {true,_} -> {false,0} end, case catch eldap:open([Host], [{port,Port},{tcpopts,[{linger,TestLinger}]}|Opts]) of {ok,H} -> case gen_tcp:accept(Sl,1000) of {ok,_} -> case eldap:getopts(H, [{tcpopts,[linger]}]) of {ok,[{tcpopts,[{linger,ActualLinger}]}]} -> case ActualLinger of TestLinger -> ok; DefaultLinger -> ct:fail("eldap:getopts: 'linger' didn't change," " got ~p (=default) expected ~p", [ActualLinger,TestLinger]); _ -> ct:fail("eldap:getopts: bad 'linger', got ~p expected ~p", [ActualLinger,TestLinger]) end; Other -> ct:fail("eldap:getopts: bad result ~p",[Other]) end; {error,timeout} -> ct:fail("server side accept timeout",[]) end; Other -> ct:fail("eldap:open failed: ~p",[Other]) end. %%%---------------------------------------------------------------- %%% Basic test that all api functions works as expected %%%---------------------------------------------------------------- elementary_search(Config) -> {ok, #eldap_search_result{entries=[_]}} = eldap:search(proplists:get_value(handle,Config), #eldap_search{base = proplists:get_value(eldap_path, Config), filter= eldap:present("objectclass"), scope = eldap:wholeSubtree()}). %%%---------------------------------------------------------------- search_non_existant(Config) -> {error, noSuchObject} = eldap:search(proplists:get_value(handle,Config), #eldap_search{base = "cn=Bar," ++ proplists:get_value(eldap_path, Config), filter= eldap:present("objectclass"), scope = eldap:wholeSubtree()}). %%%---------------------------------------------------------------- add_when_not_bound(Config) -> {error, _} = eldap:add(proplists:get_value(handle,Config), "cn=Jonas Jonsson," ++ proplists:get_value(eldap_path, Config), [{"objectclass", ["person"]}, {"cn", ["Jonas Jonsson"]}, {"sn", ["Jonsson"]}]). %%%---------------------------------------------------------------- bind(Config) -> ok = eldap:simple_bind(proplists:get_value(handle,Config), "cn=Manager,dc=ericsson,dc=se", "hejsan"). %%%---------------------------------------------------------------- add_when_bound(Config) -> ok = eldap:add(proplists:get_value(handle, Config), "cn=Jonas Jonsson," ++ proplists:get_value(eldap_path, Config), [{"objectclass", ["person"]}, {"cn", ["Jonas Jonsson"]}, {"sn", ["Jonsson"]}]). %%%---------------------------------------------------------------- add_already_exists(Config) -> {error, entryAlreadyExists} = eldap:add(proplists:get_value(handle, Config), "cn=Jonas Jonsson," ++ proplists:get_value(eldap_path, Config), [{"objectclass", ["person"]}, {"cn", ["Jonas Jonsson"]}, {"sn", ["Jonsson"]}]). %%%---------------------------------------------------------------- more_add(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), ok = eldap:add(H, "cn=Foo Bar," ++ BasePath, [{"objectclass", ["person"]}, {"cn", ["Foo Bar"]}, {"sn", ["Bar"]}, {"telephoneNumber", ["555-1232", "555-5432"]}]), ok = eldap:add(H, "ou=Team," ++ BasePath, [{"objectclass", ["organizationalUnit"]}, {"ou", ["Team"]}]). %%%---------------------------------------------------------------- add_referral(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), {ok,{referral,["ldap://nowhere.example.com"++_]}} = eldap:add(H, "cn=Foo Bar,dc=notHere," ++ BasePath, [{"objectclass", ["person"]}, {"cn", ["Foo Bar"]}, {"sn", ["Bar"]}, {"telephoneNumber", ["555-1232", "555-5432"]}]). %%%---------------------------------------------------------------- search_filter_equalityMatch(Config) -> BasePath = proplists:get_value(eldap_path, Config), ExpectedDN = "cn=Jonas Jonsson," ++ BasePath, {ok, #eldap_search_result{entries=[#eldap_entry{object_name=ExpectedDN}]}} = eldap:search(proplists:get_value(handle, Config), #eldap_search{base = BasePath, filter = eldap:equalityMatch("sn", "Jonsson"), scope=eldap:singleLevel()}). %%%---------------------------------------------------------------- search_filter_equalityMatch_objectClass_exists(Config) -> BasePath = proplists:get_value(eldap_path, Config), ExpectedDN = "cn=Jonas Jonsson," ++ BasePath, {ok, #eldap_search_result{entries=[#eldap_entry{object_name=ExpectedDN}]}} = eldap:search(proplists:get_value(handle, Config), #eldap_search{base = BasePath, filter = eldap:'and'([eldap:equalityMatch("sn", "Jonsson"), eldap:present("objectclass")]), scope=eldap:singleLevel()}). %%%---------------------------------------------------------------- search_filter_substring_any(Config) -> BasePath = proplists:get_value(eldap_path, Config), ExpectedDN = "cn=Jonas Jonsson," ++ BasePath, {ok, #eldap_search_result{entries=[#eldap_entry{object_name=ExpectedDN}]}} = eldap:search(proplists:get_value(handle, Config), #eldap_search{base = BasePath, filter = eldap:substrings("sn", [{any, "ss"}]), scope=eldap:singleLevel()}). %%%---------------------------------------------------------------- search_filter_initial(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), ExpectedDN = "cn=Foo Bar," ++ BasePath, {ok, #eldap_search_result{entries=[#eldap_entry{object_name=ExpectedDN}]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:substrings("sn", [{initial, "B"}]), scope=eldap:singleLevel()}). %%%---------------------------------------------------------------- search_filter_final(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), ExpectedDN = "cn=Foo Bar," ++ BasePath, {ok, #eldap_search_result{entries=[#eldap_entry{object_name=ExpectedDN}]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:substrings("sn", [{final, "r"}]), scope=eldap:singleLevel()}). %%%---------------------------------------------------------------- search_filter_and(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), ExpectedDN = "cn=Foo Bar," ++ BasePath, {ok, #eldap_search_result{entries=[#eldap_entry{object_name=ExpectedDN}]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:'and'([eldap:substrings("sn", [{any, "a"}]), eldap:equalityMatch("cn","Foo Bar")]), scope=eldap:singleLevel()}). %%%---------------------------------------------------------------- search_filter_or(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), ExpectedDNs = lists:sort(["cn=Foo Bar," ++ BasePath, "ou=Team," ++ BasePath]), {ok, #eldap_search_result{entries=Es}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:'or'([eldap:substrings("sn", [{any, "a"}]), eldap:equalityMatch("ou","Team")]), scope=eldap:singleLevel()}), ExpectedDNs = lists:sort([DN || #eldap_entry{object_name=DN} <- Es]). %%%---------------------------------------------------------------- search_filter_or_sizelimit_ok(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), ExpectedDNs = lists:sort(["cn=Foo Bar," ++ BasePath, "ou=Team," ++ BasePath]), {ok, #eldap_search_result{entries=Es}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:'or'([eldap:substrings("sn", [{any, "a"}]), eldap:equalityMatch("ou","Team")]), size_limit = 2, scope=eldap:singleLevel()}), ExpectedDNs = lists:sort([DN || #eldap_entry{object_name=DN} <- Es]). %%%---------------------------------------------------------------- search_filter_or_sizelimit_exceeded(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), %% The quesry without the {size_limit,1} option would return two answers: ExpectedDNs = ["cn=Foo Bar," ++ BasePath, "ou=Team," ++ BasePath], %% Expect exact one of the two answers, but we don't know which: {ok, #eldap_search_result{entries=[E]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:'or'([eldap:substrings("sn", [{any, "a"}]), eldap:equalityMatch("ou","Team")]), size_limit = 1, scope=eldap:singleLevel()}), #eldap_entry{object_name=DN} = E, true = lists:member(DN, ExpectedDNs). %%%---------------------------------------------------------------- search_filter_and_not(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), {ok, #eldap_search_result{entries=[]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:'and'([eldap:substrings("sn", [{any, "a"}]), eldap:'not'( eldap:equalityMatch("cn","Foo Bar") )]), scope=eldap:singleLevel()}). %%%---------------------------------------------------------------- search_two_hits(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), DN1 = "cn=Santa Claus," ++ BasePath, DN2 = "cn=Jultomten," ++ BasePath, %% Add two objects: ok = eldap:add(H, DN1, [{"objectclass", ["person"]}, {"cn", ["Santa Claus"]}, {"sn", ["Santa"]}, {"description", ["USA"]}]), ok = eldap:add(H, DN2, [{"objectclass", ["person"]}, {"cn", ["Jultomten"]}, {"sn", ["Tomten"]}, {"description", ["Sweden"]}]), %% Search for them: {ok, #eldap_search_result{entries=Es}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:present("description"), scope=eldap:singleLevel()}), %% And check that they are the expected ones: ExpectedDNs = lists:sort([DN1, DN2]), ExpectedDNs = lists:sort([D || #eldap_entry{object_name=D} <- Es]), %% Restore the database: [ok=eldap:delete(H,DN) || DN <- ExpectedDNs]. %%%---------------------------------------------------------------- search_extensible_match_with_dn(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), %% Create intermediate tree OU1 = "o=Designers," ++ BasePath, ok = eldap:add(H, OU1, [{"objectclass", ["top", "organization"]}, {"o", ["Designers"]}]), OU2 = "o=Graphics," ++ BasePath, ok = eldap:add(H, OU2, [{"objectclass", ["top", "organization"]}, {"o", ["Graphics"]}]), %% Add objects, they belongs to different trees DN1 = "cn=Bob Noorda,o=Designers," ++ BasePath, DN2 = "cn=Bob Noorda,o=Graphics," ++ BasePath, ok = eldap:add(H, DN1, [{"objectclass", ["person"]}, {"cn", ["Bob Noorda"]}, {"sn", ["Noorda"]}, {"description", ["Amsterdam"]}]), ok = eldap:add(H, DN2, [{"objectclass", ["person"]}, {"cn", ["Bob Noorda"]}, {"sn", ["Noorda"]}, {"description", ["Milan"]}]), %% Search using extensible filter only in Designers tree Filter = eldap:'and'([ eldap:extensibleMatch("Designers", [{type, "o"}, {dnAttributes, true}]), eldap:equalityMatch("sn", "Noorda") ]), {ok, #eldap_search_result{entries=Es}} = eldap:search(H, #eldap_search{base = BasePath, filter = Filter, scope=eldap:wholeSubtree()}), %% Check [DN1] = [D || #eldap_entry{object_name=D} <- Es], %% Restore the database [ok=eldap:delete(H,DN) || DN <- [DN1, DN2, OU1, OU2]]. %%%---------------------------------------------------------------- search_extensible_match_without_dn(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), %% Create intermediate tree OU1 = "o=Teachers," ++ BasePath, ok = eldap:add(H, OU1, [{"objectclass", ["top", "organization"]}, {"o", ["Teachers"]}]), OU2 = "o=Designers," ++ BasePath, ok = eldap:add(H, OU2, [{"objectclass", ["top", "organization"]}, {"o", ["Designers"]}]), %% Add objects, they belongs to different trees DN1 = "cn=Max Huber,o=Teachers," ++ BasePath, DN2 = "cn=Max Huber,o=Designers," ++ BasePath, ok = eldap:add(H, DN1, [{"objectclass", ["person"]}, {"cn", ["Max Huber"]}, {"sn", ["Huber"]}, {"description", ["Baar"]}]), ok = eldap:add(H, DN2, [{"objectclass", ["person"]}, {"cn", ["Max Huber"]}, {"sn", ["Huber"]}, {"description", ["Milan"]}]), %% Search using extensible filter without dn attribute Filter = eldap:extensibleMatch("Huber", [{type, "sn"}]), {ok, #eldap_search_result{entries=Es}} = eldap:search(H, #eldap_search{base = BasePath, filter = Filter, scope = eldap:wholeSubtree() }), %% And check that they are the expected ones: ExpectedDNs = lists:sort([DN1, DN2]), ExpectedDNs = lists:sort([D || #eldap_entry{object_name=D} <- Es]), %% Restore the database: [ok=eldap:delete(H,DN) || DN <- [DN1, DN2, OU1, OU2]]. %%%---------------------------------------------------------------- search_referral(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), DN = "cn=Santa Claus,dc=notHere," ++ BasePath, {ok,{referral,["ldap://nowhere.example.com"++_]}} = eldap:search(H, #eldap_search{base = DN, filter = eldap:present("description"), scope=eldap:singleLevel()}). %%%---------------------------------------------------------------- search_paged_results(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), %% Add a lot of objects: Desc = "Frogs", Names = ["Frog" ++ integer_to_list(N) || N <- lists:seq(1, 20)], DNs = [{"cn=Jeremy " ++ N ++ "," ++ BasePath, [{"objectclass", ["person"]}, {"cn", ["Jeremy " ++ N]}, {"sn", [N]}, {"description", [Desc]}]} || N <- Names], [ok = eldap:add(H, Entry, Attrs) || {Entry, Attrs} <- DNs], PageSize = 10, Control1 = eldap:paged_result_control(PageSize), {ok, SearchResult1} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:equalityMatch("description", Desc), scope=eldap:singleLevel()}, [Control1]), #eldap_search_result{entries=Es1} = Res = SearchResult1, PageSize = length(Es1), {ok, Cookie1} = eldap:paged_result_cookie(SearchResult1), Control2 = eldap:paged_result_control(PageSize, Cookie1), {ok, SearchResult2} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:equalityMatch("description", Desc), scope=eldap:singleLevel()}, [Control2]), #eldap_search_result{entries=Es2} = SearchResult2, PageSize = length(Es2), %% all results have been returned so cookie should be empty {ok, []} = eldap:paged_result_cookie(SearchResult2), ExpectedDNs = lists:sort([DN || {DN, _} <- DNs]), ResultDNs = lists:sort([DN || #eldap_entry{object_name=DN} <- Es1 ++ Es2]), ExpectedDNs = ResultDNs, %% Restore the database: [ok=eldap:delete(H,DN) || {DN, _} <- DNs]. %%%---------------------------------------------------------------- modify(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), %% The object to modify DN = "cn=Foo Bar," ++ BasePath, %% Save a copy to restore later: {ok,OriginalAttrs} = attributes(H, DN), %% Do a change Mod = [eldap:mod_replace("telephoneNumber", ["555-12345"]), eldap:mod_add("description", ["Nice guy"])], ok = eldap:modify(H, DN, Mod), %% Check that the object was changed {ok, #eldap_search_result{entries=[#eldap_entry{object_name=DN}]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:equalityMatch("telephoneNumber", "555-12345"), scope=eldap:singleLevel()}), %% Do another type of change ok = eldap:modify(H, DN, [eldap:mod_delete("telephoneNumber", [])]), %% and check that it worked by repeating the test above {ok, #eldap_search_result{entries=[]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:equalityMatch("telephoneNumber", "555-12345"), scope=eldap:singleLevel()}), %% restore the original version: restore_original_object(H, DN, OriginalAttrs). %%%---------------------------------------------------------------- modify_referral(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), %% The object to modify DN = "cn=Foo Bar,dc=notHere," ++ BasePath, %% Do a change Mod = [eldap:mod_replace("telephoneNumber", ["555-12345"]), eldap:mod_add("description", ["Nice guy"])], {ok,{referral,["ldap://nowhere.example.com"++_]}} = eldap:modify(H, DN, Mod). %%%---------------------------------------------------------------- delete(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), %% The element to play with: DN = "cn=Jonas Jonsson," ++ BasePath, %% Prove that the element is present before deletion {ok,OriginalAttrs} = attributes(H, DN), %% Do what the test has to do: ok = eldap:delete(H, DN), %% check that it really was deleted: {error, noSuchObject} = eldap:delete(H, DN), %% And restore the object for subsequent tests restore_original_object(H, DN, OriginalAttrs). %%%---------------------------------------------------------------- delete_referral(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), %% The element to play with: DN = "cn=Jonas Jonsson,dc=notHere," ++ BasePath, {ok,{referral,["ldap://nowhere.example.com"++_]}} = eldap:delete(H, DN). %%%---------------------------------------------------------------- modify_dn_delete_old(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), OrigCN = "Foo Bar", OriginalRDN = "cn="++OrigCN, DN = OriginalRDN ++ "," ++ BasePath, NewCN = "Niclas Andre", NewRDN = "cn="++NewCN, NewDN = NewRDN ++ "," ++BasePath, %% Check that the object to modify_dn of exists: {ok,OriginalAttrs} = attributes(H, DN), CN_orig = lists:sort(proplists:get_value("cn",OriginalAttrs)), {ok, #eldap_search_result{entries=[#eldap_entry{object_name=DN}]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:substrings("sn", [{any, "a"}]), scope = eldap:singleLevel()}), %% Modify and delete the old one: ok = eldap:modify_dn(H, DN, NewRDN, true, ""), %% Check that DN was modified and the old one was deleted: {ok,NewAttrs} = attributes(H, NewDN), CN_new = lists:sort(proplists:get_value("cn",NewAttrs)), {ok, #eldap_search_result{entries=[#eldap_entry{object_name=NewDN}]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:substrings("sn", [{any, "a"}]), scope = eldap:singleLevel()}), %% What we expect: CN_new = lists:sort([NewCN | CN_orig -- [OrigCN]]), %% Change back: ok = eldap:modify_dn(H, NewDN, OriginalRDN, true, ""), %% Check that DN was modified and the new one was deleted: {ok,SameAsOriginalAttrs} = attributes(H, DN), CN_orig = lists:sort(proplists:get_value("cn",SameAsOriginalAttrs)), {ok, #eldap_search_result{entries=[#eldap_entry{object_name=DN}]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:substrings("sn", [{any, "a"}]), scope = eldap:singleLevel()}). %%%---------------------------------------------------------------- modify_dn_keep_old(Config) -> H = proplists:get_value(handle, Config), BasePath = proplists:get_value(eldap_path, Config), OriginalRDN = "cn=Foo Bar", DN = OriginalRDN ++ "," ++ BasePath, NewCN = "Niclas Andre", NewRDN = "cn="++NewCN, NewDN = NewRDN ++ "," ++BasePath, %% Check that the object to modify_dn of exists but the new one does not: {ok,OriginalAttrs} = attributes(H, DN), {ok, #eldap_search_result{entries=[#eldap_entry{object_name=DN}]}} = eldap:search(H, #eldap_search{base = BasePath, filter = eldap:substrings("sn", [{any, "a"}]), scope = eldap:singleLevel()}), %% Modify but keep the old "cn" attr: ok = eldap:modify_dn(H, DN, NewRDN, false, ""), %% Check that DN was modified and the old CN entry is not deleted: {ok,NewAttrs} = attributes(H, NewDN), CN_orig = proplists:get_value("cn",OriginalAttrs), CN_new = proplists:get_value("cn",NewAttrs), Expected = lists:sort([NewCN|CN_orig]), Expected = lists:sort(CN_new), %% Restore db: ok = eldap:delete(H, NewDN), restore_original_object(H, DN, OriginalAttrs). %%%---------------------------------------------------------------- %%% Test that start_tls on an already upgraded connection makes no noise start_tls_twice_should_fail(Config) -> {ok,H} = open_bind(Config), {error,tls_already_started} = eldap:start_tls(H, []), eldap:close(H). %%%---------------------------------------------------------------- %%% Test that start_tls on an ldaps connection fails start_tls_on_ssl_should_fail(Config) -> {ok,H} = open_bind(Config), {error,tls_already_started} = eldap:start_tls(H, []), eldap:close(H). %%%---------------------------------------------------------------- encode(_Config) -> {ok,Bin} = 'ELDAPv3':encode('AddRequest', #'AddRequest'{entry="hejHopp" ,attributes=[]} ), Expected = <<104,11,4,7,104,101,106,72,111,112,112,48,0>>, case Bin of Expected -> ok; _ -> ct:log("Encoded erroneously to:~n~p~nExpected:~n~p",[Bin,Expected]), {fail, "Bad encode"} end. %%%---------------------------------------------------------------- decode(_Config) -> {ok,Res} = 'ELDAPv3':decode('AddRequest', <<104,11,4,7,104,101,106,72,111,112,112,48,0>>), ct:log("Res = ~p", [Res]), Expected = #'AddRequest'{entry = "hejHopp",attributes = []}, case Res of Expected -> ok; #'AddRequest'{entry= <<"hejHopp">>, attributes=[]} -> {fail, "decoded to (correct) binary!!"}; _ -> {fail, "Bad decode"} end. %%%**************************************************************** %%% Private attributes(H, DN) -> case eldap:search(H, #eldap_search{base = DN, filter= eldap:present("objectclass"), scope = eldap:wholeSubtree()}) of {ok, #eldap_search_result{entries=[#eldap_entry{object_name=DN, attributes=OriginalAttrs}]}} -> {ok, OriginalAttrs}; Other -> Other end. restore_original_object(H, DN, Attrs) -> eldap:delete(H, DN), ok = eldap:add(H, DN, Attrs). find_first_server(UseSSL, [{config,Key}|Ss]) -> case ct:get_config(Key) of {Host,Port} -> ct:log("find_first_server config ~p -> ~p",[Key,{Host,Port}]), find_first_server(UseSSL, [{Host,Port}|Ss]); undefined -> ct:log("find_first_server config ~p is undefined",[Key]), find_first_server(UseSSL, Ss) end; find_first_server(UseSSL, [{Host,Port}|Ss]) -> case eldap:open([Host],[{port,Port},{ssl,UseSSL},{timeout,10000}]) of {ok,H} when UseSSL==false, Ss=/=[] -> case eldap:start_tls(H, [], 10000) of ok -> ct:log("find_first_server ~p UseSSL=~p -> ok",[{Host,Port},UseSSL]), eldap:close(H), {Host,Port}; Res -> ct:log("find_first_server ~p UseSSL=~p failed with~n~p~nSave as spare host.",[{Host,Port},UseSSL,Res]), eldap:close(H), find_first_server(UseSSL, Ss++[{spare_host,Host,Port}]) end; {ok,H} -> ct:log("find_first_server ~p UseSSL=~p -> ok",[{Host,Port},UseSSL]), eldap:close(H), {Host,Port}; Res -> ct:log("find_first_server ~p UseSSL=~p failed with~n~p",[{Host,Port},UseSSL,Res]), find_first_server(UseSSL, Ss) end; find_first_server(false, [{spare_host,Host,Port}|_]) -> ct:log("find_first_server can't find start_tls host, use the spare non-start_tls host for plain ldap: ~p",[{Host,Port}]), {Host,Port}; find_first_server(_, []) -> ct:log("find_first_server, nothing left to try",[]), undefined. initialize_db(Config) -> case {open_bind(Config), inet:gethostname()} of {{ok,H}, {ok,MyHost}} -> Path = "dc="++MyHost++",dc=ericsson,dc=se", delete_old_contents(H, Path), add_new_contents(H, Path, MyHost), eldap:close(H), [{eldap_path,Path}|Config]; Other -> ct:fail("initialize_db failed: ~p",[Other]) end. clear_db(Config) -> {ok,H} = open_bind(Config), Path = proplists:get_value(eldap_path, Config), delete_old_contents(H, Path), eldap:close(H), Config. delete_old_contents(H, Path) -> case eldap:search(H, [{base, Path}, {filter, eldap:present("objectclass")}, {scope, eldap:wholeSubtree()}]) of {ok, _R=#eldap_search_result{entries=Entries}} -> case eldap:delete(H, "dc=notHere,"++Path, [?manageDsaIT]) of ok -> ok; {error,noSuchObject} -> ok; Other -> ct:fail("eldap:delete notHere ret ~p",[Other]) end, [ok = eldap:delete(H,DN) || #eldap_entry{object_name=DN} <- Entries]; _Res -> ignore end. -define(ok(X), ok(?MODULE,?LINE,X)). add_new_contents(H, Path, MyHost) -> ?ok(eldap:add(H,"dc=ericsson,dc=se", [{"objectclass", ["dcObject", "organization"]}, {"dc", ["ericsson"]}, {"o", ["Testing"]}])), ?ok(eldap:add(H,Path, [{"objectclass", ["dcObject", "organization"]}, {"dc", [MyHost]}, {"o", ["Test machine"]}])), ?ok(eldap:add(H, "dc=notHere,"++Path, [{"objectclass", ["referral", "dcObject" ]}, {"ref", ["ldap://nowhere.example.com/notHere,"++Path]}, {"dc", ["notHere"]} ])). ok(_, _, {error,entryAlreadyExists}) -> ok; ok(_, _, ok) -> ok; ok(MODULE, LINE, X) -> ct:pal("~p:~p add_new_contents: ret from eldap:add = ~p",[MODULE,LINE,X]), X. cond_start_tls(H, Config) -> case proplists:get_value(start_tls,Config) of true -> start_tls(H,Config); _ -> Config end. start_tls(H, Config) -> KeyFile = filename:join([proplists:get_value(data_dir,Config), "certs/client/key.pem" ]), case eldap:start_tls(H, [{keyfile, KeyFile}]) of ok -> [{start_tls_success,true} | Config]; Error -> ct:log("Start_tls on ~p failed: ~p",[proplists:get_value(url,Config) ,Error]), ct:fail("start_tls failed") end. %%%---------------------------------------------------------------- open_bind(Config) -> {ok,H} = open(Config), ok = eldap:simple_bind(H, "cn=Manager,dc=ericsson,dc=se", "hejsan"), {ok,H}. open(Config) -> {Host,Port} = proplists:get_value(server,Config), SSLflag = proplists:get_value(ssl_flag,Config), {ok,H} = eldap:open([Host], [{port,Port},{ssl,SSLflag}]), cond_start_tls(H, Config), {ok,H}. %%%---------------------------------------------------------------- supported_extension(OID, Config) -> {ok,H} = open_bind(Config), case eldap:search(H, [{scope, eldap:baseObject()}, {filter, eldap:present("objectclass")}, {deref, eldap:neverDerefAliases()}, {attributes, ["+"]}]) of {ok,R=#eldap_search_result{}} -> eldap:close(H), lists:member(OID, [SE || EE <- R#eldap_search_result.entries, {"supportedExtension",SEs} <- EE#eldap_entry.attributes, SE<-SEs]); _ -> eldap:close(H), false end. %%%---------------------------------------------------------------- client_timeout(Fun, Config) -> Host = proplists:get_value(listen_host, Config), Port = proplists:get_value(listen_port, Config), Opts = proplists:get_value(tcp_connect_opts, Config), T = 1000, case eldap:open([Host], [{timeout,T},{port,Port}|Opts]) of {ok,H} -> T0 = erlang:monotonic_time(), {error,{gen_tcp_error,timeout}} = Fun(H), T_op = ms_passed(T0), ct:log("Time = ~p, Timeout spec = ~p",[T_op,T]), if T_op < T -> {fail, "Timeout too early"}; true -> ok end; Other -> ct:fail("eldap:open failed: ~p",[Other]) end. %% Help function, elapsed milliseconds since T0 ms_passed(T0) -> %% OTP 18 erlang:convert_time_unit(erlang:monotonic_time() - T0, native, micro_seconds) / 1000. %%%---------------------------------------------------------------- init_ssl_certs_et_al(Config) -> try ssl:start() of R when R==ok ; R=={error,{already_started,ssl}} -> try make_certs:all("/dev/null", filename:join(proplists:get_value(data_dir,Config), "certs")) of {ok,_} -> true; Other -> ct:comment("make_certs failed"), ct:log("make_certs failed ~p", [Other]), false catch C:E -> ct:comment("make_certs crashed"), ct:log("make_certs failed ~p:~p", [C,E]), false end; _ -> false catch Error:Reason -> ct:comment("ssl failed to start"), ct:log("init_per_suite failed to start ssl Error=~p Reason=~p", [Error, Reason]), false end. %%%---------------------------------------------------------------- random_sorted(L) when is_list(L) -> random_sorted(L, length(L), []). random_sorted([], 0, Acc) -> Acc; random_sorted(L, N, Acc) -> R = rand:uniform(N), E = lists:nth(R, L), random_sorted(L -- [E], N-1, [E|Acc]).