Skip to content

Commit

Permalink
Make LDAPCP SE more customizable for developers and add a custom samp…
Browse files Browse the repository at this point in the history
…le (#229)

* scaffold sample

* work

* work

* Update .gitignore

* update sample

* update readme files

* Add property CustomFilter to class DirectoryConnection

* work

* Update LDAPCPSE_Custom.cs

* Update CHANGELOG.md
  • Loading branch information
Yvand authored Oct 25, 2024
1 parent f464770 commit 4cdd290
Show file tree
Hide file tree
Showing 18 changed files with 360 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## LDAPCP Second Edition v20.0 - Unreleased

* Add property CustomFilter to class DirectoryConnection, to allow setting a custom LDAP filter per LDAP connection
* Add sample custom claims provider LDAPCPSE_basic
* Fix validation issue when multiple LDAP connections return an identical entity

## LDAPCP Second Edition v19.0.20240823.4 - Published in August 23, 2024
Expand Down
1 change: 1 addition & 0 deletions Yvand.LDAPCPSE/Yvand.LDAPCPSE.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
<Reference Include="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>references\SPSE\Microsoft.SharePoint.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ public class OperationContext
/// </summary>
public List<ClaimTypeConfig> CurrentClaimTypeConfigList { get; private set; }

public List<DirectoryConnection> LdapConnections { get; private set; }
public List<DirectoryConnection> LdapConnections { get; set; }

public OperationContext(ClaimsProviderSettings settings, OperationType currentRequestType, string input, SPClaim incomingEntity, Uri context, string[] entityTypes, string hierarchyNodeID, int maxCount)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ public string[] GroupMembershipLdapAttributes
[Persisted]
private string[] _GroupMembershipLdapAttributes = new string[] { "memberOf", "uniquememberof" };

/// <summary>
/// Get or set a LDAP filter specific to this LDAP connection
/// </summary>
public string CustomFilter
{
get { return _CustomFilter; }
set { _CustomFilter = value; }
}
[Persisted]
private string _CustomFilter;

/// <summary>
/// DirectoryEntry used to make LDAP queries
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LDAPCPSE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ protected void AugmentEntity(Uri context, SPClaim entity, SPClaimProviderContext

Logger.Log($"[{Name}] Starting augmentation for user '{decodedEntity.Value}'.", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation);
currentContext = new OperationContext(this.Settings as ClaimsProviderSettings, OperationType.Augmentation, String.Empty, decodedEntity, context, null, null, Int32.MaxValue);
ValidateRuntimeSettings(currentContext);
Stopwatch timer = new Stopwatch();
timer.Start();
List<string> groups = this.EntityProvider.GetEntityGroups(currentContext);
Expand Down Expand Up @@ -439,6 +440,7 @@ protected override void FillResolve(Uri context, string[] entityTypes, string re
try
{
OperationContext currentContext = new OperationContext(this.Settings as ClaimsProviderSettings, OperationType.Search, resolveInput, null, context, entityTypes, null, 30);
ValidateRuntimeSettings(currentContext);
List<PickerEntity> entities = SearchOrValidate(currentContext);
if (entities == null || entities.Count == 0) { return; }
foreach (PickerEntity entity in entities)
Expand Down Expand Up @@ -470,6 +472,7 @@ protected override void FillSearch(Uri context, string[] entityTypes, string sea
try
{
OperationContext currentContext = new OperationContext(this.Settings as ClaimsProviderSettings, OperationType.Search, searchPattern, null, context, entityTypes, hierarchyNodeID, maxCount);
ValidateRuntimeSettings(currentContext);
List<PickerEntity> entities = this.SearchOrValidate(currentContext);
if (entities == null || entities.Count == 0) { return; }
SPProviderHierarchyNode matchNode = null;
Expand Down Expand Up @@ -523,6 +526,7 @@ protected override void FillResolve(Uri context, string[] entityTypes, SPClaim r
if (!String.Equals(resolveInput.OriginalIssuer, this.OriginalIssuerName, StringComparison.InvariantCultureIgnoreCase)) { return; }

OperationContext currentContext = new OperationContext(this.Settings as ClaimsProviderSettings, OperationType.Validation, resolveInput.Value, resolveInput, context, entityTypes, null, 1);
ValidateRuntimeSettings(currentContext);
List<PickerEntity> entities = this.SearchOrValidate(currentContext);
if (entities?.Count == 1)
{
Expand Down Expand Up @@ -948,6 +952,10 @@ protected virtual string FormatPermissionDisplayText(LdapEntityProviderResult di
}
return entityDisplayText;
}

public virtual void ValidateRuntimeSettings(OperationContext operationContext)
{
}
#endregion

/// <summary>
Expand Down
42 changes: 30 additions & 12 deletions Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LdapEntityProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -411,30 +411,42 @@ public override List<LdapEntityProviderResult> SearchOrValidateEntities(Operatio
return new List<LdapEntityProviderResult>(0);
}

string ldapFilter = this.BuildFilter(currentContext);
//string ldapFilter = this.BuildFilter(currentContext);
List<LdapEntityProviderResult> LdapSearchResult = null;
SPSecurity.RunWithElevatedPrivileges(delegate ()
{
LdapSearchResult = this.QueryLDAPServers(currentContext, ldapFilter);
LdapSearchResult = this.QueryLDAPServers(currentContext);
});
return LdapSearchResult;
}

protected string BuildFilter(OperationContext currentContext)
protected string BuildFilter(List<ClaimTypeConfig> claimTypeConfigList, string inputText, bool exactSearch, DirectoryConnection ldapConnection)
{
if (ldapConnection != null && String.IsNullOrWhiteSpace(ldapConnection.CustomFilter))
{ // In this case, the generic LDAP filter can be used
return String.Empty;
}

StringBuilder filter = new StringBuilder();
if (this.Settings.FilterEnabledUsersOnly)
{
filter.Append(ClaimsProviderConstants.LDAPFilterEnabledUsersOnly);
}

// A LDAP connection may have a custom filter
if (!String.IsNullOrWhiteSpace(ldapConnection?.CustomFilter))
{
filter.Append(ldapConnection.CustomFilter);
}

filter.Append("(| "); // START OR

// Fix bug https://github.com/Yvand/LDAPCP/issues/53 by escaping special characters with their hex representation as documented in https://ldap.com/ldap-filters/
string input = Utils.EscapeSpecialCharacters(currentContext.Input);
string input = Utils.EscapeSpecialCharacters(inputText);

foreach (var ctConfig in currentContext.CurrentClaimTypeConfigList)
foreach (var ctConfig in claimTypeConfigList)
{
filter.Append(AddLdapAttributeToFilter(currentContext, input, ctConfig));
filter.Append(AddLdapAttributeToFilter(exactSearch, input, ctConfig));
}

if (this.Settings.FilterEnabledUsersOnly)
Expand All @@ -446,7 +458,7 @@ protected string BuildFilter(OperationContext currentContext)
return filter.ToString();
}

protected string AddLdapAttributeToFilter(OperationContext currentContext, string input, ClaimTypeConfig attributeConfig)
protected string AddLdapAttributeToFilter(bool exactSearch, string input, ClaimTypeConfig attributeConfig)
{
// Prevent use of wildcard for LDAP attributes which do not support it
if (String.Equals(attributeConfig.DirectoryObjectAttribute, "objectSid", StringComparison.InvariantCultureIgnoreCase))
Expand All @@ -460,7 +472,7 @@ protected string AddLdapAttributeToFilter(OperationContext currentContext, strin

// Test if wildcard(s) should be added to the input
string inputFormatted;
if (currentContext.ExactSearch || !attributeConfig.DirectoryObjectAttributeSupportsWildcard)
if (exactSearch || !attributeConfig.DirectoryObjectAttributeSupportsWildcard)
{
inputFormatted = input;
}
Expand All @@ -484,17 +496,23 @@ protected string AddLdapAttributeToFilter(OperationContext currentContext, strin
return filter;
}

protected List<LdapEntityProviderResult> QueryLDAPServers(OperationContext currentContext, string ldapFilter)
protected List<LdapEntityProviderResult> QueryLDAPServers(OperationContext currentContext)
{
if (this.Settings.LdapConnections == null || this.Settings.LdapConnections.Count == 0) { return null; }
if (currentContext.LdapConnections == null || currentContext.LdapConnections.Count == 0) { return null; }
object lockResults = new object();
List<LdapEntityProviderResult> results = new List<LdapEntityProviderResult>();
Stopwatch globalStopWatch = new Stopwatch();
globalStopWatch.Start();

//foreach (var ldapConnection in this.Settings.LdapConnections.Where(x => x.LdapEntry != null))
Parallel.ForEach(this.Settings.LdapConnections.Where(x => x.LdapEntry != null), ldapConnection =>
string ldapFilter = this.BuildFilter(currentContext.CurrentClaimTypeConfigList, currentContext.Input, currentContext.ExactSearch, null);
//foreach (var ldapConnection in currentContext.LdapConnections.Where(x => x.LdapEntry != null))
Parallel.ForEach(currentContext.LdapConnections.Where(x => x.LdapEntry != null), ldapConnection =>
{
if (!String.IsNullOrWhiteSpace(ldapConnection.CustomFilter))
{
// The LDAP filter needs to be entirely rewritten to include the filter specified in current connection
ldapFilter = this.BuildFilter(currentContext.CurrentClaimTypeConfigList, currentContext.Input, currentContext.ExactSearch, ldapConnection);
}
Debug.WriteLine($"ldapConnection: Path: {ldapConnection.LdapEntry.Path}, UseDefaultADConnection: {ldapConnection.UseDefaultADConnection}");
Logger.LogDebug($"ldapConnection: Path: {ldapConnection.LdapEntry.Path}, UseDefaultADConnection: {ldapConnection.UseDefaultADConnection}");
using (DirectoryEntry directory = ldapConnection.LdapEntry)
Expand Down
1 change: 1 addition & 0 deletions custom-claims-provider-samples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.dll
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Administration.Claims;
using System;
using System.Runtime.InteropServices;
using Yvand.LdapClaimsProvider.Logging;

namespace LDAPCPSE_basic.Features
{
/// <summary>
/// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
/// </summary>
/// <remarks>
/// The GUID attached to this class may be used during packaging and should not be modified.
/// </remarks>

[Guid("8a0189bf-2e90-48b0-85f7-f639b42e3ef8")]
public class LDAPCPSECustomEventReceiver : SPClaimProviderFeatureReceiver
{
public override string ClaimProviderAssembly => typeof(LDAPCPSE_Custom).Assembly.FullName;

public override string ClaimProviderDescription => LDAPCPSE_Custom.ClaimsProviderName;

public override string ClaimProviderDisplayName => LDAPCPSE_Custom.ClaimsProviderName;

public override string ClaimProviderType => typeof(LDAPCPSE_Custom).FullName;

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
ExecBaseFeatureActivated(properties);
}

private void ExecBaseFeatureActivated(Microsoft.SharePoint.SPFeatureReceiverProperties properties)
{
// Wrapper function for base FeatureActivated.
// Used because base keywork can lead to unverifiable code inside lambda expression
base.FeatureActivated(properties);
SPSecurity.RunWithElevatedPrivileges((SPSecurity.CodeToRunElevated)delegate ()
{
try
{
Logger svc = Logger.Local;
Logger.Log($"[{LDAPCPSE_Custom.ClaimsProviderName}] Activating farm-scoped feature for claims provider \"{LDAPCPSE_Custom.ClaimsProviderName}\"", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration);
}
catch (Exception ex)
{
Logger.LogException((string)LDAPCPSE_Custom.ClaimsProviderName, $"activating farm-scoped feature for claims provider \"{LDAPCPSE_Custom.ClaimsProviderName}\"", TraceCategory.Configuration, ex);
}
});
}

public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
SPSecurity.RunWithElevatedPrivileges((SPSecurity.CodeToRunElevated)delegate ()
{
try
{
Logger.Log($"[{LDAPCPSE_Custom.ClaimsProviderName}] Deactivating farm-scoped feature for claims provider \"{LDAPCPSE_Custom.ClaimsProviderName}\": Removing claims provider from the farm (but not its configuration)", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration);
base.RemoveClaimProvider((string)LDAPCPSE_Custom.ClaimsProviderName);
}
catch (Exception ex)
{
Logger.LogException((string)LDAPCPSE_Custom.ClaimsProviderName, $"deactivating farm-scoped feature for claims provider \"{LDAPCPSE_Custom.ClaimsProviderName}\"", TraceCategory.Configuration, ex);
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/">
</Feature>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<feature xmlns:dm0="http://schemas.microsoft.com/VisualStudio/2008/DslTools/Core" dslVersion="1.0.0.0" Id="23e0908f-e7c6-4359-a4f6-294e0a9568dd" creator="Yvan Duhamel" featureId="4d6c1089-9e73-458d-9b77-10566ff6ec63" imageUrl="" receiverAssembly="$SharePoint.Project.AssemblyFullName$" receiverClass="$SharePoint.Type.8a0189bf-2e90-48b0-85f7-f639b42e3ef8.FullName$" scope="Farm" solutionId="184186f1-9e4b-4624-9782-21e275751d43" title="LDAPCPSE_basic.ClaimsProvider" version="" deploymentPath="$SharePoint.Feature.FileNameWithoutExtension$" xmlns="http://schemas.microsoft.com/VisualStudio/2008/SharePointTools/FeatureModel" />
49 changes: 49 additions & 0 deletions custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_Custom.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.SharePoint.Administration;
using System;
using Yvand.LdapClaimsProvider;
using Yvand.LdapClaimsProvider.Configuration;
using Yvand.LdapClaimsProvider.Logging;

namespace LDAPCPSE_basic
{
public class LDAPCPSE_Custom : LDAPCPSE
{
/// <summary>
/// Sets the name of the claims provider, also set in (Get-SPTrustedIdentityTokenIssuer).ClaimProviderName property
/// </summary>
public new const string ClaimsProviderName = "LDAPCPSE_Custom";

/// <summary>
/// Do not remove or change this property
/// </summary>
public override string Name => ClaimsProviderName;

public LDAPCPSE_Custom(string displayName) : base(displayName)
{
}

public override ILdapProviderSettings GetSettings()
{
ClaimsProviderSettings settings = ClaimsProviderSettings.GetDefaultSettings(ClaimsProviderName);
settings.EntityDisplayTextPrefix = "(custom) ";
//settings.UserIdentifierClaimTypeConfig.DirectoryObjectAttributeForDisplayText = "displayName";
return settings;
}

public override void ValidateRuntimeSettings(OperationContext operationContext)
{
Uri currentSite = operationContext.UriContext;
string currentUser = operationContext.UserInHttpContext?.Value;
Logger.Log($"New request with input {operationContext.Input} from URL {currentSite} and user {currentUser}", TraceSeverity.High, EventSeverity.Information, TraceCategory.Custom);
// Returns all groups, or only users members of group testLdapcpGroup_002
string customFilter = "(|(objectClass=group)(memberOf=CN=testLdapcpGroup_002,OU=ldapcp,DC=contoso,DC=local))";
customFilter = "(|(&(objectClass=group)(|(sAMAccountName=testLdapcpGroup_002)(sAMAccountName=testLdapcpGroup_003)))(memberOf=CN=testLdapcpGroup_002,OU=ldapcp,DC=contoso,DC=local))";
operationContext.LdapConnections[0].CustomFilter = customFilter;
//if (currentSite.Port == 6000)
//{
// operationContext.LdapConnections[0].CustomFilter = "(telephoneNumber=00110011)";
//}
Logger.Log($"Apply custom LDAP filter \"{customFilter}\"", TraceSeverity.High, EventSeverity.Information, TraceCategory.Custom);
}
}
}
Loading

0 comments on commit 4cdd290

Please sign in to comment.