Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: System.DirectoryServices.Protocols.LdapFilter #59900

Open
RoadTrain opened this issue Oct 2, 2021 · 11 comments
Open

[API Proposal]: System.DirectoryServices.Protocols.LdapFilter #59900

RoadTrain opened this issue Oct 2, 2021 · 11 comments
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.DirectoryServices needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Milestone

Comments

@RoadTrain
Copy link

RoadTrain commented Oct 2, 2021

Background and motivation

As per discussion at #38609 (comment), it was suggested that runtime might benefit from LDAP query escape functionality.

As per RFC 4515, LDAP search filter arguments should be properly escaped.
S.DS.P currently lacks such a functionality, which prevents some corner cases to work without consumer implementing such functionality themselves.

Detailed context (copied from #38609 (comment)):

Another case with escaping is when filter argument (e.g. group CN) contains parentheses, e.g. Some (very) important group.
Since parentheses are a part of filter syntax itself, it might be problematic to correctly parse the filter.

(&(objectClass=group)(sAMAccountName=Some (very) important group))

The correct representation, as per RFC, is:

(&(objectClass=group)(sAMAccountName=Some \28very\29 important group))

What's interesting is that the first one works on Windows, but fails on Linux.
The second one works on both Windows and Linux, as per my tests.

If that is the case I propose to add the escaping functionality to System.DirectoryServices.Protocols.LdapConnection, so both libraries act the same on the windows as well as on linux.

I think this might benefit from some general escape method which we can call manually when constructing filters, like:

var filter = $"(&(objectClass=group)(sAMAccountName={LdapFilter.Escape("Some (very) important group")}))";

Something along the lines of what I did here: dotnet/aspnetcore@7f72499
Is it a good idea to provide such a method as part of core System.DirectoryServices.Protocols functionality?

Originally posted by @RoadTrain in #38609 (comment)

API Proposal

namespace System.DirectoryServices.Protocols
{
    public static class LdapFilter
    {
        public static string EscapeFilterArgument(string filterArgument);
    }
}

API Usage

// Construct the LDAP filter by correctly escaping filter arguments
var filter = $"(&(objectClass=group)(sAMAccountName={LdapFilter.EscapeFilterArgument("Some (very) important group")}))";
Console.WriteLine(filter);
// Output:
// (&(objectClass=group)(sAMAccountName=Some \28very\29 important group))

Risks

None that I'm aware of.

@RoadTrain RoadTrain added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Oct 2, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.DirectoryServices untriaged New issue has not been triaged by the area owner labels Oct 2, 2021
@ghost
Copy link

ghost commented Oct 2, 2021

Tagging subscribers to this area: @jay98014
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

As per discussion at #38609 (comment), it was suggested that runtime might benefit from LDAP query escape functionality.

As per RFC 4515, LDAP search filter arguments should be properly escaped.
S.DS.P currently lacks such a functionality, which prevents some corner cases to work without consumer implementing such functionality themselves.

Detailed context (copied from linked issue):

Another case with escaping is when filter argument (e.g. group CN) contains parentheses, e.g. Some (very) important group.
Since parentheses are a part of filter syntax itself, it might be problematic to correctly parse the filter.

(&(objectClass=group)(sAMAccountName=Some (very) important group))

The correct representation, as per RFC, is:

(&(objectClass=group)(sAMAccountName=Some \28very\29 important group))

What's interesting is that the first one works on Windows, but fails on Linux.
The second one works on both Windows and Linux, as per my tests.

If that is the case I propose to add the escaping functionality to System.DirectoryServices.Protocols.LdapConnection, so both libraries act the same on the windows as well as on linux.

I think this might benefit from some general escape method which we can call manually when constructing filters, like:

var filter = $"(&(objectClass=group)(sAMAccountName={LdapFilter.Escape("Some (very) important group")}))";

Something along the lines of what I did here: dotnet/aspnetcore@7f72499
Is it a good idea to provide such a method as part of core System.DirectoryServices.Protocols functionality?

Originally posted by @RoadTrain in #38609 (comment)

API Proposal

namespace System.DirectoryServices.Protocols
{
    public static class LdapFilter
    {
        public static string EscapeFilterArgument(string filterArgument);
    }
}

API Usage

// Construct the LDAP filter by correctly escaping filter arguments
var filter = $"(&(objectClass=group)(sAMAccountName={LdapFilter.EscapeFilterArgument("Some (very) important group")}))";
Console.WriteLine(filter);
// Output:
// (&(objectClass=group)(sAMAccountName=Some \28very\29 important group))

Risks

None that I'm aware of.

Author: RoadTrain
Assignees: -
Labels:

api-suggestion, area-System.DirectoryServices, untriaged

Milestone: -

@RoadTrain
Copy link
Author

RoadTrain commented Oct 2, 2021

One thing to consider: the escaping method should only do one thing -- replace reserved characters with their RFC-compliant form.
So the user of that method should be aware if the string is already escaped, because double-escaping would result in invalid argument value, i.e. the method cannot trivially be made idempotent.

@jeffhandley jeffhandley added this to the Future milestone Oct 4, 2021
@jeffhandley jeffhandley added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed untriaged New issue has not been triaged by the area owner labels Oct 4, 2021
@joperezr
Copy link
Member

joperezr commented Nov 3, 2021

This sounds like a great feature, thanks for the proposal. Can you confirm a list of all characters that would need to be escaped for this? Also, we would need to make sure that the RFC-compliant form works for both wldap32 and libldap (openldap) which are the native libraries that we use underneath.

@joperezr joperezr removed the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Nov 3, 2021
@joperezr joperezr moved this from Untriaged to Future in Triage POD for Reflection, META, etc. Nov 3, 2021
@RoadTrain
Copy link
Author

RoadTrain commented Nov 3, 2021

This sounds like a great feature, thanks for the proposal. Can you confirm a list of all characters that would need to be escaped for this?

The RFC lists 5 characters that must be escaped:

The rule ensures that the entire filter string is a
valid UTF-8 string and provides that the octets that represent the
ASCII characters "*" (ASCII 0x2a), "(" (ASCII 0x28), ")" (ASCII
0x29), "" (ASCII 0x5c), and NUL (ASCII 0x00) are represented as a
backslash "" (ASCII 0x5c) followed by the two hexadecimal digits
representing the value of the encoded octet.

From Richard L. Mueller:
image

Also, we would need to make sure that the RFC-compliant form works for both wldap32 and libldap (openldap) which are the native libraries that we use underneath.

That would probably require some custom AD setup. In our case I tested probably the most common occurrence -- escaping parentheses. It worked on both Linux (openldap) and Windows (wldap32).

There's a note on this at Technet. An interesting detail is:

Actually, the parentheses only need to be escaped if they are unmatched, as above. If instead the Common Name were "James (Jim) Smith", nothing would need to be escaped. However, any characters, including non-display characters, can be escaped in a similar manner in an LDAP filter.

I believe it's an implementation detail of wldap32, since openldap still needs matched parentheses escaped.

@ghost ghost added the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Nov 3, 2021
@joperezr
Copy link
Member

joperezr commented Nov 10, 2021

Great, I guess last thing to consider is whether we want to have this type public, or instead simply make it part of the LdapConnection request implementation detail, where we automatically escape all filters that get sent to us. what are your thoughts around that @RoadTrain? Main thinking behind that being that this way more folks would benefit from the escaping, as opposed to just folks who know about the new API.

@joperezr joperezr removed the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Nov 10, 2021
@RoadTrain
Copy link
Author

@joperezr

Great, I guess last thing to consider is whether we want to have this type public, or instead simply make it part of the LdapConnection request implementation detail, where we automatically escape all filters that get sent to us. what are your thoughts around that @RoadTrain? Main thinking behind that being that this way more folks would benefit from the escaping, as opposed to just folks who know about the new API.

That would be ideal, but the problem is that it would require parsing the entire query, which adds a whole bunch of issues and risks to the generalizability of implementation.
For example, for a query like

(&(objectClass=group)(sAMAccountName=John Smith (work)))

we would need to determine where the filter argument (John Smith (work)) starts and ends, then escape only that part.
If the argument is John Smith work) instead, we would need to account for unmatched parentheses.

I think this problem is better illustrated by this example: https://stackoverflow.com/a/62193991
Another reply there actually shows an approach that is equivalent to my proposal (except it's in Java :D ): https://stackoverflow.com/a/67284683 . The API is similar: https://nightlies.apache.org/directory/api/1.0.2/apidocs/org/apache/directory/api/ldap/model/filter/FilterEncoder.html#encodeFilterValue-java.lang.String-

I personally don't know how many people use LdapConnection directly, my own use case is actually hidden behind ASP.NET Core APIs (i.e. RBAC authorization on Linux). So presumably, if such a functionality is implemented and used inside ASP.NET Core, at least some customers would benefit indirectly.

@ghost ghost added the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Nov 11, 2021
@RoadTrain
Copy link
Author

RoadTrain commented Nov 11, 2021

Another approach might be to implement some form of an LDAP query builder.
Yet another one is parametrized queries.
E.g.

var filter = "(&(objectClass=group)(sAMAccountName={0}))";
var searchRequest = new SearchRequest(distinguishedName, filter, new [] {accountName}, SearchScope.Subtree, null);

That would require an additional parameter in the constructor of SearchRequest.

Maybe the best way is to do both via statics:

namespace System.DirectoryServices.Protocols
{
    public static class LdapFilter
    {
        public static string EscapeFilterArgument(string filterArgument);
        public static string Format(string filter, string[] args);
    }
}

@joperezr
Copy link
Member

You have a good point regarding it being better to scope it down to a helper method for now, specially since that way it can be used for something else and not just SearchRequest. I think this API is ready for review so I’ll mark it as such.

@joperezr joperezr added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration labels Nov 11, 2021
@joperezr
Copy link
Member

One last question here, I was looking for precedence in the framework and found a similar pattern in Regex, which has the static API Regex.Escape for escaping special characters on a regex pattern directly onto the regex class. The question being, would it make sense to add these APIs directly into LdapConnection? That could help a lot with the visibility and discoverability of the API.

@RoadTrain
Copy link
Author

RoadTrain commented Nov 21, 2021

One last question here, I was looking for precedence in the framework and found a similar pattern in Regex, which has the static API Regex.Escape for escaping special characters on a regex pattern directly onto the regex class. The question being, would it make sense to add these APIs directly into LdapConnection? That could help a lot with the visibility and discoverability of the API.

Yeah, I think it's a valid approach. One thing to consider is possibly breaking SRP in case of LdapConnection. It feels like Regex is a bit more suited for escape functionality than LdapConnection. So they question really is whether LdapConnection is a good place for this method.
I also can probably envision a situation where we might need other escape methods, e.g. for escaping Distinguished Names.
I think it's something for the runtime team to decide :)
I personally have no strong preferences, but I'm leaning towards a separate static class.
Either way will be good, so it's up to you.

@ghost ghost added the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Nov 21, 2021
@terrajobst
Copy link
Member

terrajobst commented Nov 23, 2021

Video

  • Do we need a new type? It seems more sensible to put it on an existing type.
    • SearchRequest
    • LdapConnection / DirectoryConnection
  • Is this specific to filters? If not, should we remove the term "Filter" from the method?
  • Do we need more a complex concept, such as builder-based approach?
namespace System.DirectoryServices.Protocols
{
    public static class LdapFilter
    {
        public static string EscapeFilterArgument(string filterArgument);
    }
}

@terrajobst terrajobst added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Nov 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.DirectoryServices needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Projects
No open projects
Development

No branches or pull requests

4 participants