Skip to content

test(ldap.controls): test control options against what get_option promises - #639

Open
droideck wants to merge 2 commits into
python-ldap:mainfrom
droideck:noop-control-decode
Open

droideck wants to merge 2 commits into
python-ldap:mainfrom
droideck:noop-control-decode

Conversation

@droideck

@droideck droideck commented Sep 7, 2026

Copy link
Copy Markdown
Member

OPT_SERVER_CONTROLS and OPT_CLIENT_CONTROLS hold request controls, but LDAPObject.get_option() decodes them with the response classes. Only controls with identical request and response schemas round-trip; SearchNoOpControl cannot, so drop the expectedFailure markers and test the actual behaviour instead.

Fixes: #102

@droideck droideck added this to the 3.4.8 milestone Sep 7, 2026
@mistotebe

Copy link
Copy Markdown
Contributor

You are confusing a request control 1.3.6.1.4.1.4203.666.5.18 and response control 1.3.6.1.4.1.4203.666.5.18. They have a different schema, our API is a client API, so we expect to encode request controls and decode response controls only. What the tests do is wrong, they're enconing a request control and trying to decode it as a response control, that's always going to fail.

@droideck droideck removed this from the 3.4.8 milestone Sep 7, 2026
@droideck
droideck force-pushed the noop-control-decode branch from f224588 to 27e2fdc Compare September 8, 2026 05:12
@droideck droideck changed the title fix(ldap.controls): decode value-less response controls test(ldap.controls): test control options against what get_option promises Sep 8, 2026
@droideck

droideck commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

You are confusing a request control 1.3.6.1.4.1.4203.666.5.18 and response control 1.3.6.1.4.1.4203.666.5.18. They have a different schema, our API is a client API, so we expect to encode request controls and decode response controls only. What the tests do is wrong, they're enconing a request control and trying to decode it as a response control, that's always going to fail.

Yeah, agreed. What I chased was the SubstrateUnderrunError out of decodeControlValue() that the expectedFailure comment points at. That was short sighted...
Reworked into a test-only change.

FWIW this isn't new. I found an interesting 2011 question about PasswordPolicyControl, same get_option -> DecodeControlTuples -> decodeControlValue path: https://mail.python.org/pipermail/python-ldap/2011q4/003037.html
With Michael answering.

About the issue, IIUC, libldap keeps these options as request controls and hands back a duplicate (options.c does ldap_controls_dup(lo->ldo_sctrls)), and that slot is only read by ldap_int_put_controls() when encoding an outgoing request, so a response control never lands there. Module-level ldap.get_option() already returns the raw tuples.

Should the object-level one just do the same, so nothing ever decodes a request control? Is it something relevant for the #464 work you do?

@droideck
droideck requested a review from mistotebe September 8, 2026 05:21
@mistotebe

Copy link
Copy Markdown
Contributor

Personally I have never found much use for the ldap_set_option(SERVER_CONTROLS) API which attaches it to every request. The only use so far has been SessionTrackingControl which is probably the only one you do want to keep around forever. Or possibly ProxyAuthzControl but that needs a lot of care so never bothered.

@mistotebe

Copy link
Copy Markdown
Contributor

FWIW this isn't new. I found an interesting 2011 question about PasswordPolicyControl, same get_option -> DecodeControlTuples -> decodeControlValue path: https://mail.python.org/pipermail/python-ldap/2011q4/003037.html
With Michael answering.

Yes, ldap_set_option(SERVER_CONTROLS) was confusing mostly for the reason that there used to be no way to extract a control from a failed operation where we would raise a naked LDAPError with that the only thing left to explore. Any leftover confusion should probably be sorted at the documentation side of things.

Should the object-level one just do the same, so nothing ever decodes a request control?

Makes sense.

Is it something relevant for the #464 work you do?

Yes and no, now that you get message/response objects from the result() endpoint, you don't have to fish for the controls anymore and they are right where you'd want them (entry, result, ...) all the time. Raising is the job of requests-like result.raise_for_result() getting rid of what I feel was a design mistake.

Comment thread Tests/t_ldap_options.py
…mises

OPT_SERVER_CONTROLS and OPT_CLIENT_CONTROLS hold request controls, but
LDAPObject.get_option() decodes them with the response classes. Only
controls with identical request and response schemas round-trip;
SearchNoOpControl cannot, so drop the expectedFailure markers and test
the actual behaviour instead.

Fixes: python-ldap#102
Related: python-ldap#643
@droideck
droideck force-pushed the noop-control-decode branch from 27e2fdc to 8f00f75 Compare September 9, 2026 02:33
Comment thread Doc/reference/ldap.rst Outdated
Both entries were empty. Say that they hold request controls attached to
every operation, how the two set_option() variants take them, and that
response controls come back from result3()/result4() instead. Point to
the 4.0 issue for get_option().

Related: python-ldap#643
Co-Authored-By: Ondřej Kuzník <ondra@mistotebe.net>
@mistotebe

Copy link
Copy Markdown
Contributor

LGTM, did you want this for 3.4.8?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SearchNoOpControlValue: SubstrateUnderrunError: Short octet stream on tag decoding

2 participants