diff options
author | Hans Nilsson <hans@erlang.org> | 2022-02-11 10:55:09 +0100 |
---|---|---|
committer | Hans Nilsson <hans@erlang.org> | 2022-02-11 10:55:09 +0100 |
commit | e4dd1b91baf9ecbf72b57ad9497f4ab4b26781e8 (patch) | |
tree | 794bab3947996c6cbed1bea4ccddc83421ba9770 /lib/eldap | |
parent | 7e885a9b4d64f0f620b7f673872e2e0350ce48f8 (diff) | |
parent | 64e333d16b73b49d0d59a1454cdd652d8f7e0b29 (diff) | |
download | erlang-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.asn1 | 11 | ||||
-rw-r--r-- | lib/eldap/doc/src/eldap.xml | 66 | ||||
-rw-r--r-- | lib/eldap/include/eldap.hrl | 3 | ||||
-rw-r--r-- | lib/eldap/src/eldap.erl | 54 | ||||
-rw-r--r-- | lib/eldap/test/README | 13 | ||||
-rw-r--r-- | lib/eldap/test/eldap_basic_SUITE.erl | 57 | ||||
-rw-r--r-- | lib/eldap/test/make_certs.erl | 4 | ||||
-rwxr-xr-x | lib/eldap/test/run_server.sh | 26 |
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) -> + {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, @@ -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 |