Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:Ledest:erlang:23
erlang
5141-Add-support-for-paged-search-results-to-LD...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 5141-Add-support-for-paged-search-results-to-LDAP.patch of Package erlang
From 3b7f35016c3ded3347712574771e5be097e4b513 Mon Sep 17 00:00:00 2001 From: Garry Hill <garry@magnetised.net> Date: Tue, 21 Dec 2021 12:14:24 +0000 Subject: [PATCH] Add support for paged search results to LDAP Returns the controls from an LDAP search and includes the PagedResultsControl ASN so that user code can perform paged searches according to https://www.rfc-editor.org/rfc/rfc2696.txt --- lib/eldap/asn1/ELDAPv3.asn1 | 11 +++++ lib/eldap/doc/src/eldap.xml | 66 ++++++++++++++++++++++++++++ lib/eldap/include/eldap.hrl | 3 +- lib/eldap/src/eldap.erl | 54 ++++++++++++++++++++--- lib/eldap/test/README | 13 +++++- lib/eldap/test/eldap_basic_SUITE.erl | 57 ++++++++++++++++++++++++ lib/eldap/test/make_certs.erl | 4 +- lib/eldap/test/run_server.sh | 26 +++++++++++ 8 files changed, 224 insertions(+), 10 deletions(-) create mode 100755 lib/eldap/test/run_server.sh 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) -> + {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) + -> {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) + -> 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, @@ -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 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 -- 2.34.1
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor