summaryrefslogtreecommitdiff
path: root/lib/eldap
diff options
context:
space:
mode:
authorHans Nilsson <hans@erlang.org>2022-02-11 10:55:09 +0100
committerHans Nilsson <hans@erlang.org>2022-02-11 10:55:09 +0100
commite4dd1b91baf9ecbf72b57ad9497f4ab4b26781e8 (patch)
tree794bab3947996c6cbed1bea4ccddc83421ba9770 /lib/eldap
parent7e885a9b4d64f0f620b7f673872e2e0350ce48f8 (diff)
parent64e333d16b73b49d0d59a1454cdd652d8f7e0b29 (diff)
downloaderlang-e4dd1b91baf9ecbf72b57ad9497f4ab4b26781e8.tar.gz
Merge branch 'maint'
* maint: Add support for paged search results to LDAP
Diffstat (limited to 'lib/eldap')
-rw-r--r--lib/eldap/asn1/ELDAPv3.asn111
-rw-r--r--lib/eldap/doc/src/eldap.xml66
-rw-r--r--lib/eldap/include/eldap.hrl3
-rw-r--r--lib/eldap/src/eldap.erl54
-rw-r--r--lib/eldap/test/README13
-rw-r--r--lib/eldap/test/eldap_basic_SUITE.erl57
-rw-r--r--lib/eldap/test/make_certs.erl4
-rwxr-xr-xlib/eldap/test/run_server.sh26
8 files changed, 224 insertions, 10 deletions
diff --git a/lib/eldap/asn1/ELDAPv3.asn1 b/lib/eldap/asn1/ELDAPv3.asn1
index 3fe7e815cc..ed1647f11e 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 8bb4323117..e562110d8e 100644
--- a/lib/eldap/doc/src/eldap.xml
+++ b/lib/eldap/doc/src/eldap.xml
@@ -482,6 +482,72 @@
</type>
<desc> <p>Negate a filter.</p> </desc>
</func>
+ <func>
+ <name since="OTP 24.3">paged_result_control(PageSize) -&gt;
+ {control, "1.2.840.113556.1.4.319", true, binary()}</name>
+ <fsummary>Create a paged result control tuple</fsummary>
+ <type>
+ <v>PageSize = positive_integer()</v>
+ </type>
+ <desc>
+ <p>Paged results is an extension to the LDAP protocol
+ specified by RFC2696</p>
+ <p>This function creates a control with the specified page
+ size for use in
+ <c>search/3</c>, for example:</p>
+ <code>
+Control = eldap:paged_result_control(50),
+{ok, SearchResults} = search(Handle, [{base, "dc=example, dc=com"}], [Control]),
+ </code>
+ </desc>
+ </func>
+ <func>
+ <name since="OTP 24.3">paged_result_control(PageSize, Cookie)
+ -&gt; {control, "1.2.840.113556.1.4.319", true,
+ binary()}</name>
+ <fsummary>Create a paged result control tuple with the given
+ Cookie</fsummary>
+ <type>
+ <v>PageSize = positive_integer()</v>
+ <v>Cookie = binary()</v>
+ </type>
+ <desc>
+ <p>Paged results is an extension to the LDAP protocol
+ specified by RFC2696</p>
+ <p>This function creates a control with the specified page
+ size and cookie for use in
+ <c>search/3</c> to retrieve the next results page.</p>
+ <p>For example:</p>
+ <code>
+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
+ </code>
+ </desc>
+ </func>
+ <func>
+ <name since="OTP 24.3">paged_result_cookie(SearchResult)
+ -&gt; binary()</name>
+ <fsummary>Extract a cookie from search results for use in the
+ subsequent search.</fsummary>
+ <type>
+ <v>SearchResult = #eldap_search_result{}</v>
+ </type>
+ <desc>
+ <p>Paged results is an extension to the LDAP protocol
+ specified by RFC2696.</p>
+ <p>This function extracts the cookie returned from the
+ server as a result of a paged search result.</p>
+ <p>If the returned cookie is the empty string
+ <c>""</c>, then these search results represent the last in
+ the series.</p>
+ </desc>
+ </func>
</funcs>
diff --git a/lib/eldap/include/eldap.hrl b/lib/eldap/include/eldap.hrl
index b670de871f..3185c08930 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 dfdcfccf55..22d816c8c8 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 af1bf6a082..62dc8ae1e2 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 1abc6f7c0c..5fa6d4ca69 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,
@@ -823,6 +825,61 @@ search_referral(Config) ->
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),
diff --git a/lib/eldap/test/make_certs.erl b/lib/eldap/test/make_certs.erl
index 03bdc32c11..10a6bfcc54 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 0000000000..81a6162331
--- /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