diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56cd64d..90eda79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Yvand.LDAPCPSE/Yvand.LDAPCPSE.csproj b/Yvand.LDAPCPSE/Yvand.LDAPCPSE.csproj
index c1a3aaa..ff01a1e 100644
--- a/Yvand.LDAPCPSE/Yvand.LDAPCPSE.csproj
+++ b/Yvand.LDAPCPSE/Yvand.LDAPCPSE.csproj
@@ -65,6 +65,7 @@
False
references\SPSE\Microsoft.SharePoint.dll
+ False
diff --git a/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/Configuration/ClaimsProviderConstants.cs b/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/Configuration/ClaimsProviderConstants.cs
index 98cf5a6..d585dce 100644
--- a/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/Configuration/ClaimsProviderConstants.cs
+++ b/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/Configuration/ClaimsProviderConstants.cs
@@ -312,7 +312,7 @@ public class OperationContext
///
public List CurrentClaimTypeConfigList { get; private set; }
- public List LdapConnections { get; private set; }
+ public List LdapConnections { get; set; }
public OperationContext(ClaimsProviderSettings settings, OperationType currentRequestType, string input, SPClaim incomingEntity, Uri context, string[] entityTypes, string hierarchyNodeID, int maxCount)
{
diff --git a/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/Configuration/DirectoryConnection.cs b/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/Configuration/DirectoryConnection.cs
index 695c9ec..367397f 100644
--- a/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/Configuration/DirectoryConnection.cs
+++ b/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/Configuration/DirectoryConnection.cs
@@ -105,6 +105,17 @@ public string[] GroupMembershipLdapAttributes
[Persisted]
private string[] _GroupMembershipLdapAttributes = new string[] { "memberOf", "uniquememberof" };
+ ///
+ /// Get or set a LDAP filter specific to this LDAP connection
+ ///
+ public string CustomFilter
+ {
+ get { return _CustomFilter; }
+ set { _CustomFilter = value; }
+ }
+ [Persisted]
+ private string _CustomFilter;
+
///
/// DirectoryEntry used to make LDAP queries
///
diff --git a/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LDAPCPSE.cs b/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LDAPCPSE.cs
index 4f9a338..4b3f40b 100644
--- a/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LDAPCPSE.cs
+++ b/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LDAPCPSE.cs
@@ -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 groups = this.EntityProvider.GetEntityGroups(currentContext);
@@ -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 entities = SearchOrValidate(currentContext);
if (entities == null || entities.Count == 0) { return; }
foreach (PickerEntity entity in entities)
@@ -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 entities = this.SearchOrValidate(currentContext);
if (entities == null || entities.Count == 0) { return; }
SPProviderHierarchyNode matchNode = null;
@@ -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 entities = this.SearchOrValidate(currentContext);
if (entities?.Count == 1)
{
@@ -948,6 +952,10 @@ protected virtual string FormatPermissionDisplayText(LdapEntityProviderResult di
}
return entityDisplayText;
}
+
+ public virtual void ValidateRuntimeSettings(OperationContext operationContext)
+ {
+ }
#endregion
///
diff --git a/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LdapEntityProvider.cs b/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LdapEntityProvider.cs
index 2cda0bf..91d935d 100644
--- a/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LdapEntityProvider.cs
+++ b/Yvand.LDAPCPSE/Yvand.LdapClaimsProvider/LdapEntityProvider.cs
@@ -411,30 +411,42 @@ public override List SearchOrValidateEntities(Operatio
return new List(0);
}
- string ldapFilter = this.BuildFilter(currentContext);
+ //string ldapFilter = this.BuildFilter(currentContext);
List 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 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)
@@ -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))
@@ -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;
}
@@ -484,17 +496,23 @@ protected string AddLdapAttributeToFilter(OperationContext currentContext, strin
return filter;
}
- protected List QueryLDAPServers(OperationContext currentContext, string ldapFilter)
+ protected List 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 results = new List();
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)
diff --git a/custom-claims-provider-samples/.gitignore b/custom-claims-provider-samples/.gitignore
new file mode 100644
index 0000000..ea23616
--- /dev/null
+++ b/custom-claims-provider-samples/.gitignore
@@ -0,0 +1 @@
+*.dll
\ No newline at end of file
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.EventReceiver.cs b/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.EventReceiver.cs
new file mode 100644
index 0000000..8df1ac5
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.EventReceiver.cs
@@ -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
+{
+ ///
+ /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
+ ///
+ ///
+ /// The GUID attached to this class may be used during packaging and should not be modified.
+ ///
+
+ [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);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.Template.xml b/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.Template.xml
new file mode 100644
index 0000000..c27273d
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.Template.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.feature b/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.feature
new file mode 100644
index 0000000..f4d85ff
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/Features/LDAPCPSE_basic.ClaimsProvider/LDAPCPSE_basic.ClaimsProvider.feature
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_Custom.cs b/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_Custom.cs
new file mode 100644
index 0000000..7bd85f8
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_Custom.cs
@@ -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
+ {
+ ///
+ /// Sets the name of the claims provider, also set in (Get-SPTrustedIdentityTokenIssuer).ClaimProviderName property
+ ///
+ public new const string ClaimsProviderName = "LDAPCPSE_Custom";
+
+ ///
+ /// Do not remove or change this property
+ ///
+ 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);
+ }
+ }
+}
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_basic.csproj b/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_basic.csproj
new file mode 100644
index 0000000..d5d2693
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_basic.csproj
@@ -0,0 +1,93 @@
+
+
+
+ Debug
+ AnyCPU
+ {CC278266-3F09-4908-BCE8-725D2AA9153E}
+ Library
+ Properties
+ LDAPCPSE_basic
+ LDAPCPSE_basic
+ v4.8
+ 19.0
+ 512
+ {C1CDDADD-2546-481F-9697-4EA41081F2FC};{14822709-B5A1-4724-98CA-57A101D1B079};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 15.0
+ 14.1
+ False
+
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ false
+ x64
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ false
+
+
+ true
+
+
+ key.snk
+
+
+
+
+
+ False
+
+
+ False
+ ..\Yvand.LDAPCPSE.dll
+
+
+
+
+ LDAPCPSE_basic.ClaimsProvider.feature
+
+
+
+
+
+
+ {9492009a-e84b-4bcc-a960-e0671966464d}
+
+
+
+ {034f2ce9-76ca-4b10-a136-85143ef303c9}
+
+
+ Package.package
+
+
+
+
+
+ LDAPCPSE_basic.ClaimsProvider.feature
+
+
+
+
+ 10.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\gacutil.exe" /f /i "$(TargetPath)"
+copy /Y "$(TargetDir)Yvand.LDAPCPSE.dll" $(ProjectDir)\bin
+
+
\ No newline at end of file
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_basic.sln b/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_basic.sln
new file mode 100644
index 0000000..38b9df5
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/LDAPCPSE_basic.sln
@@ -0,0 +1,27 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35013.160
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LDAPCPSE_basic", "LDAPCPSE_basic.csproj", "{CC278266-3F09-4908-BCE8-725D2AA9153E}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {CC278266-3F09-4908-BCE8-725D2AA9153E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC278266-3F09-4908-BCE8-725D2AA9153E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC278266-3F09-4908-BCE8-725D2AA9153E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {CC278266-3F09-4908-BCE8-725D2AA9153E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC278266-3F09-4908-BCE8-725D2AA9153E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC278266-3F09-4908-BCE8-725D2AA9153E}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {75E70229-6D16-4FD1-B776-1D3CF6994E65}
+ EndGlobalSection
+EndGlobal
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/Package/Package.Template.xml b/custom-claims-provider-samples/LDAPCPSE_basic/Package/Package.Template.xml
new file mode 100644
index 0000000..640ff0f
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/Package/Package.Template.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/Package/Package.package b/custom-claims-provider-samples/LDAPCPSE_basic/Package/Package.package
new file mode 100644
index 0000000..4928ed3
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/Package/Package.package
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/Properties/AssemblyInfo.cs b/custom-claims-provider-samples/LDAPCPSE_basic/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..bebe4c0
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/Properties/AssemblyInfo.cs
@@ -0,0 +1,38 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("EntraCP.Custom")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("EntraCP.Custom")]
+[assembly: AssemblyCopyright("Copyright © 2024")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("cc278266-3f09-4908-bce8-725d2aa9153e")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+
diff --git a/custom-claims-provider-samples/LDAPCPSE_basic/README.md b/custom-claims-provider-samples/LDAPCPSE_basic/README.md
new file mode 100644
index 0000000..faace8b
--- /dev/null
+++ b/custom-claims-provider-samples/LDAPCPSE_basic/README.md
@@ -0,0 +1,6 @@
+# Sample with a hard-coded configuration and a manual reference to LDAPCPSE
+
+This project shows how to create a claims provider that inherits LDAPCPSE. It uses a simple, hard-coded configuration to specify the tenant.
+
+> [!WARNING]
+> Do NOT deploy this solution in a SharePoint farm that already has LDAPCPSE deployed, unless both use **exactly** the same version of `Yvand.LDAPCPSE.dll`.
diff --git a/custom-claims-provider-samples/README.md b/custom-claims-provider-samples/README.md
new file mode 100644
index 0000000..9c44320
--- /dev/null
+++ b/custom-claims-provider-samples/README.md
@@ -0,0 +1,4 @@
+# Sample projects for developers
+
+This folder contains Visual Studio projects that developers can use to create their custom clainms providers based on LDAPCPSE.
+This is useful only for specific needs.