dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.21k stars 4.72k forks source link

LDAP referral callback cannot be set in Linux #54411

Open iinuwa opened 3 years ago

iinuwa commented 3 years ago

The System.DirectoryServices.Protocols API for setting referral callbacks aligns with how the Windows LDAP library works, but . This prevents consumers to use LDAP referrals on Linux. See below for more analysis:

Originally posted by @makrattaur in https://github.com/dotnet/runtime/issues/44826#issuecomment-777963933

Hi, I am also running into this issue and I am able to reproduce with ldapsearch. I am using Microsoft Active Directory as the LDAP server (which has the behaviour described).

I have changed the query so it returns one match and one property only so the ldapsearch output is shorter.

On any other DN other than the base DN, the result looks like this:

$ ldapsearch -o ldif-wrap=no -H ldap://contoso.com -b 'CN=Users,DC=contoso,DC=com' '(sAMAccountName=Administrator)' 'name'
SASL/GSS-SPNEGO authentication started
SASL username: johndoe@contoso.com
SASL SSF: 256
SASL data security layer installed.
# extended LDIF
#
# LDAPv3
# base <CN=Users,DC=contoso,DC=com> with scope subtree
# filter: (sAMAccountName=Administrator)
# requesting: name
#

# Administrator, Users, contoso.com
dn: CN=Administrator,CN=Users,DC=contoso,DC=com
name: Administrator

# search result
search: 3
result: 0 Success

# numResponses: 2
# numEntries: 1

But on the root DN, the result looks like this:

$ ldapsearch -o ldif-wrap=no -H ldap://contoso.com -b 'DC=contoso,DC=com' '(sAMAccountName=Administrator)' 'name'
SASL/GSS-SPNEGO authentication started
SASL username: johndoe@contoso.com
SASL SSF: 256
SASL data security layer installed.
# extended LDIF
#
# LDAPv3
# base <DC=contoso,DC=com> with scope subtree
# filter: (sAMAccountName=Administrator)
# requesting: name
#

# Administrator, Users, contoso.com
dn: CN=Administrator,CN=Users,DC=contoso,DC=com
name: Administrator

# search reference
ref: ldap://ForestDnsZones.contoso.com/DC=ForestDnsZones,DC=contoso,DC=com

# search reference
ref: ldap://DomainDnsZones.contoso.com/DC=DomainDnsZones,DC=contoso,DC=com

# search reference
ref: ldap://contoso.com/CN=Configuration,DC=contoso,DC=com

# search result
search: 3
result: 0 Success

# numResponses: 5
# numEntries: 1
# numReferences: 3

The difference between the two is that referrals are returned. They can be also seen in the first comment in this issue after the line Response.Error Message: Referral: in the exception details, ForestDnsZones and DomainDnsZones are usually Active Directory specific.

By default, ldapsearch does not follow referrals, the option -C enables referral following:

$ ldapsearch -C -o ldif-wrap=no -H ldap://contoso.com -b 'DC=contoso,DC=com' '(sAMAccountName=Administrator)' 'name'
SASL/GSS-SPNEGO authentication started
SASL username: johndoe@contoso.com
SASL SSF: 256
SASL data security layer installed.
# extended LDIF
#
# LDAPv3
# base <DC=contoso,DC=com> with scope subtree
# filter: (sAMAccountName=Administrator)
# requesting: name
#

# Administrator, Users, contoso.com
dn: CN=Administrator,CN=Users,DC=contoso,DC=com
name: Administrator

# search reference
ref: ldap://ForestDnsZones.contoso.com/DC=ForestDnsZones,DC=contoso,DC=com

# search reference
ref: ldap://contoso.com/CN=Configuration,DC=contoso,DC=com

# search reference
ref: ldap://DomainDnsZones.contoso.com/DC=DomainDnsZones,DC=contoso,DC=com

# search reference
ref: ldap://contoso.com/CN=Schema,CN=Configuration,DC=contoso,DC=com

# search result
search: 3
result: 1 Operations error
text: 000004DC: LdapErr: DSID-0C090A7D, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v3839

# numResponses: 6
# numEntries: 1
# numReferences: 4

So I can get the operation error using ldapsearch. (The option is in fact obsolete, see here, it's not documented)

Trying to understand what is happening, I turned on debug logging for ldapsearch (-d 1), indicated a SASL mechanism so it's easier to see in the debug log (-Y GSSAPI), redirected the debug log to stdout and filtered the output with grep -B 10 -A 5 ldap_pvt_connect. I get the following output:

$ ldapsearch -C -d 1 -o ldif-wrap=no -Y GSSAPI -H ldap://contoso.com -b 'DC=contoso,DC=com' -s subtree '(sAMAccountName=Administrator)' 'name' 2>&1 | grep -B 10 -A 5 ldap_pvt_connect
ldap_create
ldap_url_parse_ext(ldap://contoso.com:389/??base)
ldap_sasl_interactive_bind: user selected: GSSAPI
ldap_int_sasl_bind: GSSAPI
ldap_new_connection 1 1 0
ldap_int_open_connection
ldap_connect_to_host: TCP contoso.com:389
ldap_new_socket: 3
ldap_prepare_socket: 3
ldap_connect_to_host: Trying 10.10.10.42:389
ldap_pvt_connect: fd: 3 tm: -1 async: 0
attempting to connect:
connect success
ldap_int_sasl_open: host=dc1.contoso.com
SASL/GSSAPI authentication started
ldap_sasl_bind
--
ber_scanf fmt ({it) ber:
ber_scanf fmt ({me) ber:
ldap_chase_v3referral: msgid 4, url "ldap://ForestDnsZones.contoso.com/DC=ForestDnsZones,DC=contoso,DC=com"
ldap_send_server_request
ldap_new_connection 0 1 1
ldap_int_open_connection
ldap_connect_to_host: TCP ForestDnsZones.contoso.com:389
ldap_new_socket: 5
ldap_prepare_socket: 5
ldap_connect_to_host: Trying 10.10.10.42:389
ldap_pvt_connect: fd: 5 tm: -1 async: 0
attempting to connect:
connect success
anonymous rebind via ldap_sasl_bind("")
ldap_sasl_bind
ldap_send_initial_request
--
ber_scanf fmt ({it) ber:
ber_scanf fmt ({me) ber:
ldap_chase_v3referral: msgid 4, url "ldap://DomainDnsZones.contoso.com/DC=DomainDnsZones,DC=contoso,DC=com"
ldap_send_server_request
ldap_new_connection 0 1 1
ldap_int_open_connection
ldap_connect_to_host: TCP DomainDnsZones.contoso.com:389
ldap_new_socket: 6
ldap_prepare_socket: 6
ldap_connect_to_host: Trying 10.10.10.42:389
ldap_pvt_connect: fd: 6 tm: -1 async: 0
attempting to connect:
connect success
anonymous rebind via ldap_sasl_bind("")
ldap_sasl_bind
ldap_send_initial_request

OpenLDAP seems to follow referrals unauthenticated by default and Active Directory seems to accept the anonymous bind done in this case but gives an error on the first request done on the connection (the errors are further in the output but mixed together).

Callbacks are set with LdapConnection.SessionOptions.ReferralCallback and this property contains three delegates which match the ones in the structure LDAP_REFERRAL_CALLBACK (see here) and the structure is passed to ldap_set_option with LDAP_OPT_REFERRAL_CALLBACK to set them on a LDAP connection object. But OpenLDAP does not support this structure and option, it only has one callback which is set with ldap_set_rebind_proc and is for rebinds only, there is no multiple callbacks, so setting the property causes a LDAP exception when checking the error code of ldap_set_option.

prcdpr commented 2 years ago

Workaround:

Add the following line:

REFERRALS off

to one of OpenLDAP config files:

system file  /usr/local/etc/openldap/ldap.conf,
user files   $HOME/ldaprc,  $HOME/.ldaprc,  ./ldaprc,

Reference: https://www.openldap.org/software//man.cgi?query=ldap.conf&sektion=5&apropos=0&manpath=OpenLDAP+2.4-Release

joperezr commented 2 years ago

Thanks for logging this @iinuwa and thanks @prcdpr for providing a workaround. Ideally we would want the library itself to handle this as opposed to having to tamper with a setting of openldap, so it would be great to be able to fix this, specially since it is pretty common to perform searches at the root of the directory. On the bare minimum, I think we should at least throw a comprehensive error when you hit this issue so it is easier to understand from the consumer side when the issue is that you are searching at the root of the directory, since the debugging and getting to the root cause is very complex.

tpisciotta commented 3 months ago

Here is another option or possibility to updating the ldap.conf file.

Add this to your code: SearchOptionsControl soc = new SearchOptionsControl(System.DirectoryServices.Protocols.SearchOption.DomainScope); request.Controls.Add(soc);

I got it from here: https://stackoverflow.com/questions/10336553/system-directoryservices-protocols-paged-get-all-users-code-suddenly-stopped-get

iinuwa commented 3 months ago

Thanks for the reply. I think your workaround is addressing a different issue. If you don't need referrals, then you can disable it with LdapConnection.SessionOptions.ReferralChasing = ReferralChasingOptions.None.

However, if you do need to follow referrals, it currently can't work on Linux if your directory requires authentication due to this API mismatch between S.DS.P and OpenLDAP.

tpisciotta commented 3 months ago

@iinuwa A note to your last comment:

@prcdpr Just a note: I found that updating system file, /usr/local/etc/openldap/ldap.conf, with [REFERRALS off] did not work for me running .Net on an alpine Linux container == I had to put the [REFERRALS off] in the file /etc/ldap/ldap.conf to get it work...

iinuwa commented 3 months ago

I had the same error you communicated in the 1st post to this issue. I am running .Net on an alpine Linux container and from what I understand, communicated through this GitHub issue, this option, [ReferralChasing = ReferralChasingOptions.None], does not work on Linux.

Interesting, we use this in production, but I may have modified some code locally to make it work. I'll try to remember to check tomorrow.

I think in either case that is a separate problem from this one that deserves its own issue so it doesn't get lost in this one (which is about modifying referral callbacks, not requesting referrals not to be sent, nor disabling referral chasing).