diff --git a/lib/eldap/asn1/ELDAPv3.asn1 b/lib/eldap/asn1/ELDAPv3.asn1 index 3fe7e815cc3d..ed1647f11e8a 100644 --- a/lib/eldap/asn1/ELDAPv3.asn1 +++ b/lib/eldap/asn1/ELDAPv3.asn1 @@ -286,5 +286,16 @@ PasswdModifyRequestValue ::= SEQUENCE { PasswdModifyResponseValue ::= SEQUENCE { genPasswd [0] OCTET STRING OPTIONAL } +-- LDAP Control Extension for Simple Paged Results Manipulation +-- https://www.rfc-editor.org/rfc/rfc2696.txt +-- controlType 1.2.840.113556.1.4.319 + +RealSearchControlValue ::= SEQUENCE { + size INTEGER (0..maxInt), + -- requested page size from client + -- result set size estimate from server + cookie OCTET STRING +} + END diff --git a/lib/eldap/doc/src/eldap.xml b/lib/eldap/doc/src/eldap.xml index 8bb4323117a7..e562110d8e48 100644 --- a/lib/eldap/doc/src/eldap.xml +++ b/lib/eldap/doc/src/eldap.xml @@ -482,6 +482,72 @@

Negate a filter.

+ + paged_result_control(PageSize) -> + {control, "1.2.840.113556.1.4.319", true, binary()} + Create a paged result control tuple + + PageSize = positive_integer() + + +

Paged results is an extension to the LDAP protocol + specified by RFC2696

+

This function creates a control with the specified page + size for use in + search/3, for example:

+ +Control = eldap:paged_result_control(50), +{ok, SearchResults} = search(Handle, [{base, "dc=example, dc=com"}], [Control]), + +
+
+ + paged_result_control(PageSize, Cookie) + -> {control, "1.2.840.113556.1.4.319", true, + binary()} + Create a paged result control tuple with the given + Cookie + + PageSize = positive_integer() + Cookie = binary() + + +

Paged results is an extension to the LDAP protocol + specified by RFC2696

+

This function creates a control with the specified page + size and cookie for use in + search/3 to retrieve the next results page.

+

For example:

+ +PageSize = 50, +Control1 = eldap:paged_result_control(PageSize), +{ok, SearchResults1} = search(Handle, [{base, "dc=example, dc=com"}], [Control1]), +%% retrieve the returned cookie from the search results +{ok, Cookie1} = eldap:paged_result_cookie(SearchResults1), +Control2 = eldap:paged_result_control(PageSize, Cookie1), +{ok, SearchResults2} = eldap:search(Handle, [{base, "dc=example,dc=com"}], [Control2]), +%% etc + +
+
+ + paged_result_cookie(SearchResult) + -> binary() + Extract a cookie from search results for use in the + subsequent search. + + SearchResult = #eldap_search_result{} + + +

Paged results is an extension to the LDAP protocol + specified by RFC2696.

+

This function extracts the cookie returned from the + server as a result of a paged search result.

+

If the returned cookie is the empty string + "", then these search results represent the last in + the series.

+
+
diff --git a/lib/eldap/include/eldap.hrl b/lib/eldap/include/eldap.hrl index b670de871f3d..3185c08930bd 100644 --- a/lib/eldap/include/eldap.hrl +++ b/lib/eldap/include/eldap.hrl @@ -20,7 +20,8 @@ %%% -record(eldap_search_result, { entries = [], % List of #eldap_entry{} records - referrals = [] % List of referrals + referrals = [], % List of referrals + controls = [] % List of controls }). %%% diff --git a/lib/eldap/src/eldap.erl b/lib/eldap/src/eldap.erl index dfdcfccf5590..22d816c8c881 100644 --- a/lib/eldap/src/eldap.erl +++ b/lib/eldap/src/eldap.erl @@ -27,7 +27,10 @@ add/3, add/4, delete/2, delete/3, modify_dn/5,parse_dn/1, - parse_ldap_url/1]). + parse_ldap_url/1, + paged_result_control/1, + paged_result_control/2, + paged_result_cookie/1]). -export([neverDerefAliases/0, derefInSearching/0, derefFindingBaseObj/0, derefAlways/0]). @@ -722,7 +725,7 @@ do_search(Data, A, Controls) -> {error,Emsg} -> {ldap_closed_p(Data, Emsg),Data}; {'EXIT',Error} -> {ldap_closed_p(Data, Error),Data}; {{ok,Val},NewData} -> {{ok,Val},NewData}; - {ok,Res,Ref,NewData} -> {{ok,polish(Res, Ref)},NewData}; + {ok,Res,Ref,ResultControls,NewData} -> {{ok,polish(Res, Ref, ResultControls)},NewData}; {{error,Reason},NewData} -> {{error,Reason},NewData}; Else -> {ldap_closed_p(Data, Else),Data} end. @@ -731,11 +734,11 @@ do_search(Data, A, Controls) -> %%% Polish the returned search result %%% -polish(Res, Ref) -> +polish(Res, Ref, Controls) -> R = polish_result(Res), %%% No special treatment of referrals at the moment. #eldap_search_result{entries = R, - referrals = Ref}. + referrals = Ref, controls = Controls}. polish_result([H|T]) when is_record(H, 'SearchResultEntry') -> ObjectName = H#'SearchResultEntry'.objectName, @@ -778,10 +781,10 @@ collect_search_responses(Data, S, ID, {ok,Msg}, Acc, Ref) case R#'LDAPResult'.resultCode of success -> log2(Data, "search reply = searchResDone ~n", []), - {ok,Acc,Ref,Data}; + {ok,Acc,Ref,Msg#'LDAPMessage'.controls,Data}; sizeLimitExceeded -> log2(Data, "[TRUNCATED] search reply = searchResDone ~n", []), - {ok,Acc,Ref,Data}; + {ok,Acc,Ref,Msg#'LDAPMessage'.controls,Data}; referral -> {{ok, {referral,R#'LDAPResult'.referral}}, Data}; Reason -> @@ -1432,3 +1435,42 @@ get_head(Str,Tail) -> %%% Should always succeed ! get_head([H|Tail],Tail,Rhead) -> lists:reverse([H|Rhead]); get_head([H|Rest],Tail,Rhead) -> get_head(Rest,Tail,[H|Rhead]). + +%%% -------------------------------------------------------------------- +%%% Return a paged result control as described by RFC2696 +%%% https://www.rfc-editor.org/rfc/rfc2696.txt +%%% -------------------------------------------------------------------- + +paged_result_control(PageSize) when is_integer(PageSize) -> + paged_result_control(PageSize, ""). + +paged_result_control(PageSize, Cookie) when is_integer(PageSize) -> + RSCV = #'RealSearchControlValue'{size=PageSize, cookie=Cookie}, + {ok, ControlValue} = 'ELDAPv3':encode('RealSearchControlValue', RSCV), + + {control, "1.2.840.113556.1.4.319", true, ControlValue}. + + +%%% -------------------------------------------------------------------- +%%% Extract the returned cookie from search results in order to +%%% retrieve the next set of results from the server according to +%%% RFC2696 +%%% +%%% https://www.rfc-editor.org/rfc/rfc2696.txt +%%% -------------------------------------------------------------------- + +paged_result_cookie(#eldap_search_result{controls=Controls}) -> + find_paged_result_cookie(Controls). + +find_paged_result_cookie([]) -> + {error, no_cookie}; + +find_paged_result_cookie([C|Controls]) -> + case C of + #'Control'{controlType="1.2.840.113556.1.4.319",controlValue=ControlValue} -> + {ok, #'RealSearchControlValue'{cookie=Cookie}} = + 'ELDAPv3':decode('RealSearchControlValue', ControlValue), + {ok, Cookie}; + _ -> + find_paged_result_cookie(Controls) + end. diff --git a/lib/eldap/test/README b/lib/eldap/test/README index af1bf6a08214..62dc8ae1e24f 100644 --- a/lib/eldap/test/README +++ b/lib/eldap/test/README @@ -11,7 +11,18 @@ erl > make_certs:all("/dev/null", "eldap_basic_SUITE_data/certs"). 2)------- -To start slapd: +To start slapd you have two options: + +- Via Docker and provided `run_server.sh` script. + +This uses the [bitnami/openldap:2.5](https://hub.docker.com/r/bitnami/openldap) +image to run an openldap/slapd server using docker. + +It will also take care of generating the server TLS certificates if they're not +present. + +- Using system installed slapd: + sudo slapd -f $ERL_TOP/lib/eldap/test/ldap_server/slapd.conf -F /tmp/slapd/slapd.d -h "ldap://localhost:9876 ldaps://localhost:9877" This will however not work, since slapd is guarded by apparmor that checks that slapd does not access other than allowed files... diff --git a/lib/eldap/test/eldap_basic_SUITE.erl b/lib/eldap/test/eldap_basic_SUITE.erl index 1abc6f7c0c5d..5fa6d4ca699f 100644 --- a/lib/eldap/test/eldap_basic_SUITE.erl +++ b/lib/eldap/test/eldap_basic_SUITE.erl @@ -61,6 +61,7 @@ search_two_hits/1, search_extensible_match_with_dn/1, search_extensible_match_without_dn/1, + search_paged_results/1, ssl_connection/1, start_tls_on_ssl_should_fail/1, start_tls_twice_should_fail/1, @@ -136,6 +137,7 @@ groups() -> search_referral, search_filter_or_sizelimit_ok, search_filter_or_sizelimit_exceeded, + search_paged_results, modify, modify_referral, delete, @@ -822,6 +824,61 @@ search_referral(Config) -> 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), diff --git a/lib/eldap/test/make_certs.erl b/lib/eldap/test/make_certs.erl index 03bdc32c111e..10a6bfcc5401 100644 --- a/lib/eldap/test/make_certs.erl +++ b/lib/eldap/test/make_certs.erl @@ -347,7 +347,7 @@ req_cnf(C) -> "default_bits = ", integer_to_list(C#config.default_bits), "\n" "RANDFILE = $ROOTDIR/RAND\n" "encrypt_key = no\n" - "default_md = sha1\n" + "default_md = sha256\n" "#string_mask = pkix\n" "x509_extensions = ca_ext\n" "prompt = no\n" @@ -393,7 +393,7 @@ ca_cnf(C) -> ["crl_extensions = crl_ext\n" || C#config.v2_crls], "unique_subject = no\n" "default_days = 3600\n" - "default_md = sha1\n" + "default_md = sha256\n" "preserve = no\n" "policy = policy_match\n" "\n" diff --git a/lib/eldap/test/run_server.sh b/lib/eldap/test/run_server.sh new file mode 100755 index 000000000000..81a616233184 --- /dev/null +++ b/lib/eldap/test/run_server.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +__dir__="$(cd "$(dirname "$0")"; pwd)" + +if [ ! -d "$__dir__/eldap_basic_SUITE_data/certs" ]; then + echo "Creating certs..." + ( + cd $__dir__ \ + && erlc make_certs.erl \ + && erl -noinput -eval 'make_certs:all("/dev/null", "eldap_basic_SUITE_data/certs").' -s init stop + ) +fi + +docker run \ + --rm \ + -v "${__dir__}/eldap_basic_SUITE_data/certs:/opt/otp/openldap/certs" \ + -e LDAP_ENABLE_TLS=yes \ + -e LDAP_TLS_CERT_FILE=/opt/otp/openldap/certs/server/cert.pem \ + -e LDAP_TLS_KEY_FILE=/opt/otp/openldap/certs/server/keycert.pem \ + -e LDAP_TLS_CA_FILE=/opt/otp/openldap/certs/server/cacerts.pem \ + -e LDAP_ROOT="dc=ericsson,dc=se" \ + -e LDAP_ADMIN_USERNAME="Manager" \ + -e LDAP_ADMIN_PASSWORD="hejsan" \ + -p 9877:1636 \ + -p 9876:1389 \ + bitnami/openldap:2.5