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

Make LDAPCP SE more customizable for developers and add a custom sample #229

Merged
merged 11 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading