Skip to content

Commit

Permalink
Rework search flow to minimize LDAP results
Browse files Browse the repository at this point in the history
The previous implementation would search for all users that potentially
matched the (mandatory) configured search filter, then loop through the
results to find a user matching one of the attributes.

This method worked fine for small instances, but became unwieldy for
larger LDAP instances, and could even result in a Size Limit Exceeded
error for extremely large databases.

This commit introduces a better method by integrating the user search
component into the initial LDAP search by way of the search filter. This
will ideally result in only a single LDAP search result for a given user
rather than potentially dozens or hundreds, reducing both the runtime
complexity of the plugin as well as the load on the LDAP server.

This new method has two potential ways of generating the limited
search filter:

First, the existing search filter option may now accept a
"{username}" variable within it, which will be interpolated at runtime
based on what the user entered. This method allows for very complex
queries to be crafted at will, providing better administrator
flexibility and options.

Second, if no "{username}" variable is found within the search filter,
the filter will be modified at runtime to include all of the search
attributes combined with the username to generate a search query which
will return any matches between the username and those queries. For
example, given attributes "uid" and "mail", a (base) search filter of
"(objectclass=mailUser)", and the entered username "joshua", the
following "real" search query would be produced:

  (&(objectclass=mailUser)(|(uid=joshua)(mail=joshua)))

These two methods are, functionally, mutually exclusive: if the
search filter contains the "{username}" variable, the search attributes
will be ignored and may be blank/empty; on the other side, with valid
search attributes, it is no longer necessary to specify a search filter
at all, since there will still be a filter on the attributes and
username. Thus, the plugin now accepts blank input for both fields in
the configuration, though leaving both blank would not work properly.

In both cases with the new method, searches are case-insensitive, so the
option for case-insensitive username searches has been removed.

The new configuration is also backwards-compatible: if no changes are
made to the search filter, the second method above will be used and this
should continue to function exactly as expected.

Some debug messages have also been updated to provide a clearer picture
of what the plugin is doing at various steps and aid in troubleshooting.

Closes jellyfin#139 jellyfin#34
Obsoletes PR jellyfin#71
  • Loading branch information
joshuaboniface committed Feb 28, 2023
1 parent a9bf63c commit 04d2387
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 37 deletions.
1 change: 0 additions & 1 deletion LDAP-Auth/Api/LdapController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ public IActionResult LdapUserSearch([FromBody] UserSearchAttributes body)
{
var configuration = LdapPlugin.Instance.Configuration;
configuration.LdapSearchAttributes = body.LdapSearchAttributes;
configuration.EnableCaseInsensitiveUsername = body.EnableCaseInsensitiveUsername;
LdapPlugin.Instance.UpdateConfiguration(configuration);

var response = new UserSearchResponse();
Expand Down
1 change: 0 additions & 1 deletion LDAP-Auth/Api/Models/UserFilterInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ public class UserFilterInfo
/// <summary>
/// Gets or sets the ldap user search filter.
/// </summary>
[Required]
public string LdapSearchFilter { get; set; }

/// <summary>
Expand Down
6 changes: 0 additions & 6 deletions LDAP-Auth/Api/Models/UserSearchAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,8 @@ public class UserSearchAttributes
/// <summary>
/// Gets or sets the ldap search attributes.
/// </summary>
[Required]
public string LdapSearchAttributes { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to use case insensitive username comparison.
/// </summary>
public bool EnableCaseInsensitiveUsername { get; set; }

/// <summary>
/// Gets or sets the username to search for as a test.
/// </summary>
Expand Down
6 changes: 0 additions & 6 deletions LDAP-Auth/Config/PluginConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public PluginConfiguration()
LdapClientCertPath = string.Empty;
LdapClientKeyPath = string.Empty;
LdapRootCaPath = string.Empty;
EnableCaseInsensitiveUsername = false;
CreateUsersFromLdap = true;
LdapUsernameAttribute = "uid";
LdapPasswordAttribute = "userPassword";
Expand Down Expand Up @@ -102,11 +101,6 @@ public PluginConfiguration()
/// </summary>
public string LdapSearchAttributes { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to use case insensitive username comparison.
/// </summary>
public bool EnableCaseInsensitiveUsername { get; set; }

/// <summary>
/// Gets or sets the ldap client cert path.
/// </summary>
Expand Down
44 changes: 26 additions & 18 deletions LDAP-Auth/Config/configPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,30 @@ <h2 class="sectionTitle">LDAP Settings:</h2>
</div>
<div class="verticalSection" is="emby-collapse" title="LDAP User Settings">
<div class="collapseContent">
<h4 style="margin-top:-0.3em">Users</h4>
<p>There are two methods possible to search for users.</p>
<ul>
<li>The {username} variable can be placed directly into the LDAP Search Filter. This variable will be replaced during the search with the entered username. If you use this method, the LDAP Search Attributes value will be ignored, and you must implement the attribute comparison(s) manually in your filter.</li>
<li>The LDAP Search Filter can be left as a subcomponent of a larger search filter constructed at runtime. For each LDAP Search Attributes entry, the entered username will be used with the attribute as an 'or' condition search filter. If you use this method, the LDAP Search Filter is optional and may be empty. This will be the default if you do not adjust your LDAP Search Filter to include the {username} variable at least once.</li>
</ul>
<p>At least one method must be chosen and configured below. If upgrading from plugin version 16 or newer, the second option will be used by default.</p>
<p>Note: Usernames are treated case-insensitive regardless of the method, as an LDAP search is not case-sensitive.</p>
<hr>
<div class="inputContainer fldExternalAddressFilter">
<input is="emby-input" type="text" id="txtLdapSearchFilter" required placeholder="(memberOf=CN=JellyfinUsers,DC=contoso,DC=com)" label="LDAP Extra Search Filters:" />
<div class="fieldDescription">Any extra LDAP search filter(s) to limit users by. OPTIONAL.<br/><i>For example, '(objectClass=inetOrgPerson)' will only search for LDAP users with that particular attribute value. Do not forget the surrounding brackets!</i></div>
<input is="emby-input" type="text" id="txtLdapSearchFilter" placeholder="(memberOf=CN=JellyfinUsers,DC=contoso,DC=com)" label="LDAP Search Filter:" />
<div class="fieldDescription">
LDAP search filter to limit user searches.<br/>
</div>
</div>
<div class="inputContainer fldExternalAddressFilter">
<input is="emby-input" type="text" id="txtLdapSearchAttributes" placeholder="uid, cn, mail, displayName" label="LDAP Search Attributes:" />
<div class="fieldDescription">
A comma-separated list of attributes to filter the username by.<br/>
</div>
</div>
<hr>

<h4>Administrators</h4>
<div class="inputContainer fldExternalAddressFilter">
<input is="emby-input" type="text" id="txtLdapAdminBaseDn" label="LDAP Admin Base DN:" />
<div class="fieldDescription">The LDAP search base dn to find administrative users for Jellyfin, e.g. (cn=admins,dc=contoso,dc=com). Defaults to user dn if empty</div>
Expand All @@ -115,22 +135,14 @@ <h2 class="sectionTitle">LDAP Settings:</h2>
<span>Enable Admin Filter 'memberUid' mode</span>
</label>
</div>
<hr>

<h4>Testing</h4>
<button id="btnTestFilters" is="emby-button" type="button" class="raised button block">
<span>Save and Test LDAP Filter Settings</span>
</button>
<div id="divFilterTestResults"></div>
<hr>
<div class="inputContainer fldExternalAddressFilter">
<input is="emby-input" type="text" id="txtLdapSearchAttributes" required placeholder="uid, cn, mail, displayName" label="LDAP Attributes:" />
<div class="fieldDescription">A comma-separated list of attributes to filter the username by.<br/><i>For example, 'uid' means we will filter the LDAP search by '(uid=[entered_username])'. If more than one attribute is specified, the entered username will be checked against all of them using an 'or' pattern.<br/>NOTE: If more than one LDAP object ever matches the search filter, the first result will be used. Ensure you use unique-per-user attributes here.</i></div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableCaseInsensitiveUsername" />
<span>Enable Case Insensitive Username</span>
</label>
<div class="fieldDescription checkboxFieldDescription">Enable case insensitive username comparison</div>
</div>
<br/>
<div class="inputContainer fldExternalAddressFilter">
<input is="emby-input" type="text" id="txtLdapSearchTest" label="Test Login Name:" />
<div class="fieldDescription">A user login to search the LDAP for to test attribute configuration.</div>
Expand Down Expand Up @@ -206,7 +218,6 @@ <h2>${HeaderLibraryAccess}</h2>
chkEnableLdapAdminFilterMemberUid: document.querySelector("#chkEnableLdapAdminFilterMemberUid"),
divFilterTestResults: document.querySelector("#divFilterTestResults"),
txtLdapSearchAttributes: document.querySelector("#txtLdapSearchAttributes"),
chkEnableCaseInsensitiveUsername: document.querySelector("#chkEnableCaseInsensitiveUsername"),
txtLdapSearchTest: document.querySelector("#txtLdapSearchTest"),
divSearchTestResults: document.querySelector("#divSearchTestResults"),
txtLdapClientCertPath: document.querySelector("#txtLdapClientCertPath"),
Expand Down Expand Up @@ -245,7 +256,6 @@ <h2>${HeaderLibraryAccess}</h2>
LdapConfigurationPage.txtLdapClientCertPath.value = config.LdapClientCertPath || "";
LdapConfigurationPage.txtLdapClientKeyPath.value = config.LdapClientKeyPath || "";
LdapConfigurationPage.txtLdapRootCaPath.value = config.LdapRootCaPath || "";
LdapConfigurationPage.chkEnableCaseInsensitiveUsername.checked = config.EnableCaseInsensitiveUsername;
LdapConfigurationPage.chkEnableUserCreation.checked = config.CreateUsersFromLdap;
LdapConfigurationPage.txtLdapUsernameAttribute.value = config.LdapUsernameAttribute;
LdapConfigurationPage.txtLdapPasswordAttribute.value = config.LdapPasswordAttribute;
Expand Down Expand Up @@ -332,7 +342,6 @@ <h2>${HeaderLibraryAccess}</h2>
config.LdapClientCertPath = LdapConfigurationPage.txtLdapClientCertPath.value;
config.LdapClientKeyPath = LdapConfigurationPage.txtLdapClientKeyPath.value;
config.LdapRootCaPath = LdapConfigurationPage.txtLdapRootCaPath.value;
config.EnableCaseInsensitiveUsername = LdapConfigurationPage.chkEnableCaseInsensitiveUsername.checked;
config.CreateUsersFromLdap = LdapConfigurationPage.chkEnableUserCreation.checked;
config.LdapUsernameAttribute = LdapConfigurationPage.txtLdapUsernameAttribute.value;
config.LdapPasswordAttribute = LdapConfigurationPage.txtLdapPasswordAttribute.value;
Expand Down Expand Up @@ -473,7 +482,6 @@ <h2>${HeaderLibraryAccess}</h2>

const configUpdate = {
LdapSearchAttributes: LdapConfigurationPage.txtLdapSearchAttributes.value,
EnableCaseInsensitiveUsername: LdapConfigurationPage.chkEnableCaseInsensitiveUsername.checked,
TestSearchUsername: LdapConfigurationPage.txtLdapSearchTest.value,
}

Expand Down
13 changes: 8 additions & 5 deletions LDAP-Auth/LDAPAuthenticationProviderPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,18 +356,21 @@ public LdapEntry LocateLdapUser(string username)
LdapPlugin.Instance.Configuration.LdapBindUser,
LdapPlugin.Instance.Configuration.LdapBindPassword);

var usernameComparison = LdapPlugin.Instance.Configuration.EnableCaseInsensitiveUsername
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;

string realSearchFilter;

if (SearchFilter.Contains("{username}"))
{
realSearchFilter = SearchFilter.Replace("{username}", username, usernameComparison),
realSearchFilter = SearchFilter.Replace("{username}", username);
}
else
{
realSearchFilter = "(&" + SearchFilter + "(|";
foreach (var attr in LdapUsernameAttributes)
{
realSearchFilter += "(" + attr + "=" + username + ")";
}

realSearchFilter += "))";
}

_logger.LogDebug("LDAP Search: {BaseDn} {realSearchFilter} @ {LdapServer}", LdapPlugin.Instance.Configuration.LdapBaseDn, realSearchFilter, LdapPlugin.Instance.Configuration.LdapServer);
Expand Down

0 comments on commit 04d2387

Please sign in to comment.