LDAP provider sample

Contents[Hide]

1. Overview

This sample shows how to create a simple LDAP accounts provider. This sample is intended as an extension of the Create a custom accounts provider article, and assumes you are familiar with it. You will learn the following:

  • How to create a simple LDAP accounts provider
  • How to create custom application configuration settings

2. Getting started

The following prerequisites must be installed on your computer:

  • Visual Studio 2015 or higher
  • Microsoft .NET Framework 4.6.1
  • A deployed Dundas BI instance

2.1. Downloading the sample solution

To download the LDAP custom accounts provider sample solution, click here.

(A sample solution is also available for Dundas BI version 5.)

2.2. Extracting the sample to the SDK folder

This sample is designed to automatically publish the extension to the instance. First, you must extract LdapProviderSample.zip to the SDK folder within the instance:

  1. Find the SDK folder for your instance. It is located at [instance root]\sdk.
  2. Extract LdapProviderSample.zip to the SDK folder.
  3. You should see the following result:

    Samples added to SDK folder
    Samples added to SDK folder

    With the following structure:

    [instance root]
        • sdk
            • Samples
                • LdapProviderSample

Important
If the sample is not in this specific folder, it will not work correctly.

2.3. Opening the solution

To open the Visual Studio solution:

  1. Right-click on the Microsoft Visual Studio shortcut, and click Run as administrator.
  2. From the File menu, click to open a solution.
  3. Choose the solution located at: [instance root]\sdk\Samples\LdapProviderSample\LdapProviderSample.sln

3. The project

The project is a class library.

  • LdapProvider.cs - This class contains the implementation of the LDAP custom account provider.
  • LdapProviderSamplePackageInfo.cs - This class contains information about the extension package, and contains the loaded event that allows for application configuration settings to be added to Dundas BI.
  • PublishExtension.targets - Used for auto publishing the extension after the build succeeds.

3.1. Register application configuration settings

The LDAP provider needs a few properties to be configured:

  • LDAP Distinguished Name
  • LDAP Server Path
  • LDAP Query Username
  • LDAP Query Password

To accomplish this, we need to override the OnLoaded method of the ExtensionPackageInfo class.

using Dundas.BI;
using Dundas.BI.Configuration;
using Dundas.BI.Extensibility;
   ...

/// <summary>
/// Called after the extension package is loaded during engine startup.
/// </summary>
public override void OnLoaded()
{
    LdapProvider.RegisterSettings();
}

On the provider itself is the code that registers the application configuration settings:

using Dundas.BI;
using Dundas.BI.AccountServices;
using Dundas.BI.AccountServices.Extensibility;
using Dundas.BI.Configuration;
using Dundas.BI.Licensing;
   ...

private static Guid ldapDistinguishedNameId = new Guid("e370e76d-8eb2-44b9-b59f-a3fb1eae99ef");
private static Guid ldapServerPathId = new Guid("630d1ae8-43fb-4d7c-acb2-d3558823e4f5");
private static Guid ldapQueryUsernameId = new Guid("3ff30651-fa6a-41d3-bcfb-54c1d5843025");
private static Guid ldapQueryPasswordId = new Guid("8bc5b031-da8b-4959-9c25-556b45d4df1c");

/// <summary>
/// Registers the settings for the LDAP provider.
/// </summary>
public static void RegisterSettings()
{
	IAppConfigService appConfigService = Engine.Current.GetService<IAppConfigService>();

	AppSettingProperties appSettingsProperties = new AppSettingProperties(
        ldapDistinguishedNameId, 
        "LdapDistinguishedName", 
        LdapProviderSamplePackageInfo.PackageId, 
        typeof(string)
    );
    appSettingsProperties.Description = string.Concat(
        "Enter the LDAP Distinguished Name.  ",
        "e.g.:  OU=Interactive Users,OU=Your Company,DC=yourcompany,DC=com"
    );
                
	appSettingsProperties.DefaultValue = string.Empty;
	appConfigService.RegisterSetting(appSettingsProperties);

	appSettingsProperties = new AppSettingProperties(
        ldapServerPathId, 
        "LdapServerPath", 
        LdapProviderSamplePackageInfo.PackageId, 
        typeof(string)
    );

	appSettingsProperties.Description = string.Concat(
		"Enter the LDAP server path.  ",
		"e.g.:   dc02.yourcompany.com"
	);
	appSettingsProperties.DefaultValue = string.Empty;
	appConfigService.RegisterSetting(appSettingsProperties);

	appSettingsProperties = new AppSettingProperties(
        ldapQueryUsernameId, 
        "LdapQueryUsername", 
        LdapProviderSamplePackageInfo.PackageId, 
        typeof(string)
	);
    appSettingsProperties.Description = string.Concat(
        "Enter the LDAP query username.  ",
        "e.g.: ",
        "CN=User A,OU=Interactive Users,OU=Your Company,DC=yourcompany,DC=com"
    );
	appSettingsProperties.DefaultValue = string.Empty;
	appConfigService.RegisterSetting(appSettingsProperties);

	appSettingsProperties = new AppSettingProperties(
        ldapQueryPasswordId, 
        "LdapQueryPassword", 
        LdapProviderSamplePackageInfo.PackageId, 
        typeof(string)
    );
	appSettingsProperties.Description = "Enter the LDAP query password.";
	appSettingsProperties.IsEncrypted = true;
	appSettingsProperties.ClientValueVisibility = AppSettingValueVisibility.SystemAdministrator;
	appSettingsProperties.IsPassword = true;
	appSettingsProperties.DefaultValue = string.Empty;
	appConfigService.RegisterSetting(appSettingsProperties);
}

Now the application configuration settings are available to our provider:

private string ldapDistinguishedName
{
	get
	{
		return Engine.Current.GetService<IAppConfigService>().GetString(ldapDistinguishedNameId);
	}
}

private string ldapQueryPassword
{
	get
	{
		return Engine.Current.GetService<IAppConfigService>().GetString(ldapQueryPasswordId);
	}
}

private string ldapQueryUsername
{
	get
	{
		return Engine.Current.GetService<IAppConfigService>().GetString(ldapQueryUsernameId);
	}
}

private string ldapServerPath
{
	get
	{
		return Engine.Current.GetService<IAppConfigService>().GetString(ldapServerPathId);
	}
}

3.2. Define interface members that are handled by LDAP

In the case of this simple LDAP accounts provider, many of the interface members are handled by LDAP. We do not want to handle them in this case. The following code demonstrates this:

public bool IsModifySupported
{
    get { return false; }
}

public void DeleteRecord(Guid recordId)
{
    throw new NotSupportedException(string.Concat(
        "Accounts should be deleted through the LDAP server",
        " and not through Dundas BI."
    ));
}

public AccountData ExportRecord(Guid accountId)
{
	throw new NotSupportedException(string.Concat(
        "Accounts should be exported through the LDAP server", 
        " and not through Dundas BI."
    ));
}

public bool ImportRecord(AccountData record)
{
    throw new NotSupportedException(string.Concat(
        "Accounts should be created through the LDAP server",
        " and not through Dundas BI."
    ));
}

public void SetLocalUserAccountNewPassword(Guid accountId, string newPassword)
{
	throw new NotSupportedException(string.Concat(
        "Passwords should be managed through the LDAP server",
        "and not through Dundas BI."
    ));
}

public void UpdateDynamicAccountProperties(Guid accountId, DynamicAccountProperties properties)
{
    // Accounts should be modified through the LDAP server 
    // and not through Dundas BI.
}

public void UpdateLogOnFailureInfo(Guid accountId, int failedLogOnCount, DateTime? lockedUntil)
{
    // This method is not called in IAccountsProvider2, 
    // is replaced by UpdateDynamicAccountProperties
}


public void UpdateLogOnTimestampAndCount(Guid accountId, DateTime logOnTimestamp)
{
    // This method is not called in IAccountsProvider2, 
    // is replaced by UpdateDynamicAccountProperties
}

public void ResetFailedLogOnInfo(Guid accountId)
{
    // Failed logons should be modified through the LDAP server
    // and not through Dundas BI.
}

public Guid SaveRecord(AccountData record)
{
    throw new NotSupportedException(string.Concat(
        "Account Data should be managed through the LDAP server",
        " and not through Dundas BI."
    ));
}
	

3.3. Connecting to LDAP and retrieving accounts

The following code connects to the LDAP server and retrieves all the account data:


private AccountData GetAccountData(string ldapFilter)
{
	ICollection<AccountData> accountDataCollection = GetAccountDataCollection(ldapFilter);

	if (accountDataCollection.Count.Equals(0))
	{
		return null;
	}
	else
	{
		return accountDataCollection.First();
	}
}

private ICollection<AccountData> GetAccountDataCollection(string ldapFilter)
{
	ICollection<AccountData> accountDataCollection = new Collection<AccountData>();

	using (LdapConnection ldapConnection = new LdapConnection(this.ldapServerPath))
	{
		NetworkCredential queryCredential = new NetworkCredential(
			this.ldapQueryUsername,
			this.ldapQueryPassword
		);

		ldapConnection.AuthType = AuthType.Basic;

		try
		{
			ldapConnection.Bind(queryCredential);
			SearchRequest request = new SearchRequest(
				this.ldapDistinguishedName,
				ldapFilter,
				SearchScope.Subtree,
				accountDataAttributesToReturn
			);

			SearchResponse searchResponse = (SearchResponse)ldapConnection.SendRequest(request);

			foreach (SearchResultEntry entry in searchResponse.Entries)
			{
				accountDataCollection.Add(GetAccountDataFromSearchResultEntry(entry));
			}

		}
		catch (DirectoryOperationException ex)
		{
			throw new ArgumentException(string.Concat(
				"Query authentication failed for reasons ",
				"other than invalid credentials: ",
				ex.Message,
				"  Please ensure your LDAP server is available and",
				" your query account is a member of at least one user group."
			));
		}
		catch (LdapException ex)
		{
			throw new ArgumentException(string.Concat(
				"The LDAP server did not accept the query connection: ",
				ex.Message
			));
		}

		return accountDataCollection;
	}
}

/// <summary>
/// Gets the account data from search result entry.
/// </summary>
/// <param name="searchResultEntry">The search result entry.</param>
/// <returns></returns>
private AccountData GetAccountDataFromSearchResultEntry(SearchResultEntry entry)
{
	string stringTime;
	DateTime timeCreated = DateTime.MinValue;
	if (entry.Attributes["createTimeStamp"] != null)
	{
		stringTime = entry.Attributes["createTimeStamp"].GetValues(typeof(String))[0].ToString();
		timeCreated = new DateTime(
			Int32.Parse(stringTime.Substring(0, 4)),
			Int32.Parse(stringTime.Substring(4, 2)),
			Int32.Parse(stringTime.Substring(6, 2)),
			Int32.Parse(stringTime.Substring(8, 2)),
			Int32.Parse(stringTime.Substring(10, 2)),
			Int32.Parse(stringTime.Substring(12, 2))
		);
	}
	DateTime lastLogon = DateTime.MinValue;
	if (entry.Attributes["lastLogonTimestamp"] != null)
	{
		stringTime = entry.Attributes["lastLogonTimestamp"].GetValues(typeof(String))[0].ToString();
		lastLogon = DateTime.FromFileTimeUtc(long.Parse(stringTime));
	}
	DateTime? accountExpires = null;
	if (entry.Attributes["accountExpires"] != null)
	{
		stringTime = entry.Attributes["accountExpires"].GetValues(typeof(String))[0].ToString();
		long expiresAsLong = long.Parse(stringTime);

		if (expiresAsLong.Equals(Int64.MaxValue))
		{
			accountExpires = null;
		}
		else
		{
			accountExpires = DateTime.FromFileTimeUtc(expiresAsLong);
		}
	}
	string mail = String.Empty;
	if (entry.Attributes["mail"] != null)
	{
		mail = entry.Attributes["mail"].GetValues(typeof(String))[0].ToString();
	}
	string givenName = String.Empty;
	if (entry.Attributes["givenName"] != null)
	{
		givenName = entry.Attributes["givenName"].GetValues(typeof(String))[0].ToString();
	}
	String surname = String.Empty;
	if (entry.Attributes["userPrincipalName"] != null)
	{
		surname = entry.Attributes["userPrincipalName"].GetValues(typeof(String))[0].ToString();
	}
	bool accountIsEnabled = true;

	string returnedUsername = String.Empty;
	if (entry.Attributes["name"] != null)
	{
		returnedUsername = entry.Attributes["name"].GetValues(typeof(String))[0].ToString();
	}

	byte[] id = new byte[0];
	if (entry.Attributes["objectGUID"] != null)
	{
		id = (byte[])entry.Attributes["objectGUID"].GetValues(typeof(byte[]))[0];
	}

	LicenseSeatKind licenseSeatKind = LicenseSeatKind.StandardUser;
	bool isSeatReserved = false;

	if (entry.Attributes["department"] != null)
	{
		if (entry.Attributes["department"].GetValues(typeof(String))[0]
			.ToString().Equals("Product Development"))
		{
			licenseSeatKind = LicenseSeatKind.Developer;
			isSeatReserved = true;
		}
	}

	string returnedDisplayName = String.Empty;
	if (entry.Attributes["displayName"] != null)
	{
		returnedDisplayName = entry.Attributes["displayName"].GetValues(typeof(String))[0].ToString();
	}


	LocalUserAccountData accountData = new LocalUserAccountData();
	accountData.CreatedTime = timeCreated;
	accountData.LastLogOnTime = lastLogon;
	accountData.LogOnCount = 0;
	accountData.AccountExpiryDate = accountExpires;
	accountData.CanChangePassword = false;
	accountData.EmailAddress = mail;
	accountData.IsEnabled = accountIsEnabled;
	accountData.DisplayName = returnedUsername;
	accountData.SeatKind = licenseSeatKind;
	accountData.IsSeatReserved = isSeatReserved;
	accountData.DisplayName = returnedDisplayName;
	accountData.Name = returnedUsername;
	accountData.Id = new Guid(id);

	return accountData;
}

/// <summary>
/// Gets all the user records.
/// </summary>
/// <returns>
/// An IEnumerable of 
/// <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// </returns>
public IEnumerable<AccountData> GetAllRecords()
{
	return GetAccountDataCollection("(objectClass=user)").ToList();
}

/// <summary>
/// Retrieves a list of 
/// <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// having the specified IDs.
/// </summary>
/// <param name="recordIds">
/// The IDs of the records to retrieve.
/// </param>
/// <returns>
/// The requested records as 
/// <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// </returns>
public ICollection<AccountData> GetById(ICollection<Guid> recordIds)
{
	ICollection<AccountData> accountDataList = new Collection<AccountData>();

	foreach (Guid id in recordIds)
	{
		string ldapFilter = string.Format(
			CultureInfo.InvariantCulture,
			"(objectGUID={0})",
			id.ToLdapObjectGuidFormattedString()
		);

		AccountData accountData = GetAccountData(ldapFilter);

		if (accountData == null)
		{
			throw new NotFoundException(string.Format(
				CultureInfo.CurrentCulture,
				"Unable to find user with ID = {0}",
				id.ToString()
			));
		}
		else
		{
			accountDataList.Add(accountData);
		}
	}

	return accountDataList;
}

/// <summary>
/// Retrieves an account specified by its name.
/// </summary>
/// <param name="accountName">The account's name.</param>
/// <returns>
/// The 
/// <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />
/// or <see langword="null" /> if it does not exist.
/// </returns>
public AccountData GetByName(string accountName)
{
	string filter = string.Format(
		CultureInfo.InvariantCulture,
		"(&(objectClass=user)(name={0}))",
		accountName
	);

	return GetAccountData(filter);
}

3.4. Logging into LDAP

The following code validates the passed logon credentials against the specified LDAP server.

/// <summary>
/// Validates local user account logon credentials.
/// </summary>
/// <param name="context">The logon context.</param>
/// <returns>
///   <see langword="true" /> 
///   if the credentials are valid; otherwise, 
///   <see langword="false" />.
/// </returns>
/// <exception cref="System.ArgumentNullException">context</exception>
public bool ValidateLocalUserLogOnCredentials(LocalLogOnContext context)
{
	// Param validation.
	if (context == null)
	{
		throw new ArgumentNullException("context");
	}

	LogOnCredential usernameCredential = 
        context.Credentials.First(c => c.Id == LogOnCredentialIds.AccountName);
	LogOnCredential passwordCredential = 
        context.Credentials.First(c => c.Id == LogOnCredentialIds.Password);

	// Treats the username as the common name 
    // and creates a distinguished name for the user
	String userNameAndDistinguishedName =
		string.Concat("cn=", usernameCredential.Value, ",", this.ldapDistinguishedName);

	NetworkCredential ldapCredentials = 
        new NetworkCredential(userNameAndDistinguishedName, passwordCredential.Value);

	using (LdapConnection ldapConnection = new LdapConnection(this.ldapServerPath))
	{
		ldapConnection.AuthType = AuthType.Basic;

		try
		{
			ldapConnection.Bind(ldapCredentials);
		}
		catch
		{
			// If the bind threw an exception 
            // then something went wrong and the account wasn't authenticated
			return false;
		}

		return true;
	}
}

4. See also

Dundas Data Visualization, Inc.
500-250 Ferrand Drive
Toronto, ON, Canada
M3C 3G8

North America: 1.800.463.1492
International: 1.416.467.5100

Dundas Support Hours: 7am-6pm, ET, Mon-Fri