Create a custom accounts provider

1. Overview

This sample shows how to create a custom accounts provider. You will learn the following:

  • What is a custom accounts provider
  • How to create a Dundas BI extension
  • How to create a custom accounts provider

2. About custom accounts providers

A custom accounts provider manages user accounts, and authentication. Out of the box, Dundas BI lets you authenticate your users in three ways:

  • Using the Dundas BI local database for user accounts.
  • Using an Active Directory server (Windows authentication) to provide user account information.
  • Configuring federated authentication to use a third-party identity provider (OpenID, Google, Microsoft, etc.)

While the above options are enough for many networks, a custom accounts provider can allow you to authenticate against any other custom or legacy provider to integrate Dundas BI with your existing systems and networks.

Note
Authentication is handled by extensions, but authorization is handled internally by Dundas BI to enforce licensing restrictions.

The following image demonstrates the difference between using standard local or Windows authentication, and using a custom accounts provider extension:

Left: Built-in accounts provider, Right: Custom accounts provider
Left: Built-in accounts provider, Right: Custom accounts provider

3. 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

3.1. Downloading sample solution

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

(Sample solutions are also available for Dundas BI versions 2.5 through 5.0, version 2.0, and version 1.)

3.2. Extracting sample to SDK folder

This sample is designed to automatically publish the extension to the instance, but must first be extracted to the SDK folder within the instance:

  1. Find the SDK folder for your instance. It is located at [instance root]\sdk.
  2. Extract the CustomAccountsProviderSample.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
                • CustomAccountsProviderSample

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

3.3. Opening solution

To open the Visual Studio solution:

  1. Right-click on the Microsoft Visual Studio shortcut, and choose Run as administrator.
  2. Click the File menu, then click Open Solution.
  3. Choose the solution located at: [instance root]\sdk\Samples\CustomAccountsProviderSample\CustomAccountsProviderSample.sln

4. The project

The project is a class library.

  • AccountInformation.csv - Comma separated values containing user information. This is used as the back end of the accounts provider.
  • CustomAccountProvider.cs - This class contains the implementation of the custom account provider.
  • CustomAccountProviderSamplePackageInfo.cs - This class contains the package information about the extension package.
  • PublishExtension.targets - Used for auto publishing the extension after the build succeeds. This also adds the AccountInformation.csv file to the application data folder.

4.1. ExtensionPackageInfo class

In order for an accounts provider to be read by Dundas BI, it needs to contain a class that extends the ExtensionPackageInfo class. This class contains the extension package information.

/// <summary>
/// This class contains the package information about the extension package.
/// </summary>
public class CustomAccountProviderSamplePackageInfo : ExtensionPackageInfo
{
    /// <summary>
    /// Gets the name of the extension package author.
    /// </summary>
    public override string Author
    {
        get { return "Dundas Data Visualization Sample Author"; }
    }

    /// <summary>
    /// Gets the copyright text associated with the extension package.
    /// </summary>
    public override string Copyright
    {
        get { return "Dundas Data Visualization, Inc."; }
    }

    /// <summary>
    /// Gets the localized display name of the extension package.
    /// </summary>
    public override string DisplayName
    {
        get { return "Custom Account Provider Sample Package"; }
    }

    /// <summary>
    /// Gets the unique identifier of the extension package.
    /// </summary>
    public override Guid Id
    {
        get { return new Guid("c4889691-ab7f-452b-ba16-fb3bcda46724"); }
    }

    /// <summary>
    /// Gets the name of the extension package.
    /// </summary>
    public override string Name
    {
        get { return "Custom Account Provider Sample Package"; }
    }

    /// <summary>
    /// Gets the version of the extension package.
    /// </summary>
    public override Version Version
    {
        get { return new Version(0, 0, 1); }
    }
}
        

4.2. Publish extension targets

This sample has a mechanism to automatically publish the extension, and copy the AccountInformation.csv file to the application data folder. This mechanism is the PublishExtension.targets file, which overrides the AfterBuild target. This will create the following files after successfully compiling the solution:

    • [instance root]\www\BIWebsite\App_Data\Extensions\CustomAccountsProviderSample\bin\CustomAccountsProviderSample.dll

    • [instance root]\www\BIWebsite\App_Data\AccountInformation.csv

It will then touch the web.config to force the web application to reset.

4.3. Defining the custom account provider

In order to create a custom account provider, you will need to implement the IAccountsProvider2 Interface.

using Dundas.BI;
using Dundas.BI.AccountServices;
using Dundas.BI.AccountServices.Extensibility;
  ...
namespace MyCompany.Sample.CustomAccountsProviderSample
{
    /// <summary>
    /// This class represents a simple accounts provider that uses a csv file. 
    /// </summary>
    public class CustomAccountProvider : IAccountsProvider2
    {
      ...
    }
}

4.4. Implementing the IAccountsProvider2 interface

4.4.1. Defining the supported query operations

The supported query operation specifies one or more operations available in the accounts provider. In the following example, we return the Count operation. This means we should implement a System.NotSupportedException for the query method on the interface, and we should implement the QueryCount method.

/// <summary>
/// Gets the supported query operations.
/// </summary>
/// <value>
/// The supported query operations.
/// </value>
public Dundas.BI.Utility.QueryApiOperations SupportedQueryOperations
{
	get { return Dundas.BI.Utility.QueryApiOperations.Count; }
}

/// <summary>
/// This operation is not supported by this account provider.
/// </summary>
/// <exception cref="System.NotSupportedException"></exception>
public IList<Guid> Query(
    int pageNumber, 
    int pageSize, 
    IList<Tuple<Dundas.BI.AccountServices.AccountQueryField, 
    Dundas.BI.SortDirection>> orderBy, 
    ICollection<Dundas.BI.AccountServices.AccountQueryFilterRule> filter
)
{
	throw new NotSupportedException();
}

/// <summary>
/// Queries the amount of users.
/// </summary>
/// <param name="filter">The filter.</param>
/// <returns>The number of accounts.</returns>
public int QueryCount(ICollection<Dundas.BI.AccountServices.AccountQueryFilterRule> filter)
{
	return GetAllRecords().Count();
}

4.4.2. Defining if modify is supported

The IsModifySupported property indicates whether the provider supports saving changes. If true, all the methods that change the account should be implemented. Otherwise, they should throw a System.NotSupportedException.

This example demonstrates how to set the custom account provider to modify the user accounts:

/// <summary>
/// Gets a value indicating whether modify is supported.
/// </summary>
/// <value>
/// <c>true</c> if modify is supported; otherwise, <c>false</c>.
/// </value>
public bool IsModifySupported
{
	get { return true; }
}

/// <summary>
/// Queries the amount of users.
/// </summary>
/// <param name="filter">The filter.</param>
/// <returns>The number of accounts.</returns>
public int QueryCount(ICollection<Dundas.BI.AccountServices.AccountQueryFilterRule> filter)
{
	return GetAllRecords().Count();
}

/// <summary>
/// Saves the record.
/// </summary>
/// <param name="record">The record.</param>
/// <returns>A Guid representing the id of the item it just saved.</returns>
public Guid SaveRecord(AccountData record)
{
    
    AccountData[] listToSave;

    if (record.Id.Equals(Guid.Empty))
    {
        // Record is new
        record.Id = Guid.NewGuid();
        listToSave = GetAllRecords().Concat(new AccountData[] { record }).ToArray();
    }
    else
    {
        // Existing record
        listToSave = GetAllRecords().ToArray();

        for (int index = 0; index < listToSave.Count(); index++)
        {
            if (listToSave[index].Id.Equals(record.Id))
            {
                listToSave[index] = record;

                break;
            }
        }   
    }

    SaveAllAccountDataToFile(listToSave);

	return record.Id;

	        
}

/// <summary>
/// Deletes the record.
/// </summary>
/// <param name="recordId">The record identifier.</param>
public void DeleteRecord(Guid recordId)
{
    SaveAllAccountDataToFile(GetAllRecords().Where(record => !record.Id.Equals(recordId)));
}

/// <summary>
/// Updates a local user account's password.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="newPassword">The new password.</param>
public void SetLocalUserAccountNewPassword(Guid accountId, string newPassword)
{
    IEnumerable<AccountData> accountDataList = GetAllRecords();
    LocalUserAccountData accountData = (LocalUserAccountData)accountDataList.First(
        accountDataItem => accountDataItem.Id.Equals(accountId)
    );
    accountData.Password = newPassword;
    SaveAllAccountDataToFile(accountDataList);
}

/// <summary>
/// Updates the logon timestamp and logon count of a 
/// <see cref="T:Dundas.BI.AccountServices.LocalUserAccount" />
/// or a <see cref="T:Dundas.BI.AccountServices.WindowsUserAccount" />.
/// </summary>
/// <param name="accountId">The ID of the account to update.</param>
/// <param name="logOnTimestamp">The logon timestamp (UTC).</param>
public void UpdateLogOnTimestampAndCount(Guid accountId, DateTime logOnTimestamp)
{
   // This method is not called in IAccountsProvider2, is replaced by UpdateDynamicAccountProperties.
}

4.4.3. Authenticating users

To authenticate users, the ValidateLocalUserLogOnCredentials method is used. This method returns true if the credentials are valid; otherwise, false.

This example demonstrates how to implement the ValidateLocalUserLogOnCredentials and PopulateLocalLogOnContext methods.

/// <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
    );

    IEnumerable<AccountData> accountDataList = GetAllRecords();

    AccountData accountData = accountDataList.FirstOrDefault(
        accountDataItem => accountDataItem.Name.Equals(usernameCredential.Value)
    );

    LocalUserAccountData userAccount = (LocalUserAccountData)accountData;

    if (passwordCredential.Value.Equals(userAccount.Password))
    {
        // Nothing went wrong - return success.
        return true;
    }
    else
    {               
        // Passwords did not match.
        return false;
    }
}

/// <summary>
/// Populates the local log on context.
/// </summary>
/// <param name="context">The context.</param>
public void PopulateLocalLogOnContext(LocalLogOnContext context)
{
    // Param validation.
    if (context == null)
    {
        throw new ArgumentNullException("context");
    }

    if (context.Credentials.Any(c => c.Id == LogOnCredentialIds.AccountName) == false
        || context.Credentials.Any(c => c.Id == LogOnCredentialIds.Password) == false)
    {
        // No username or password provided.
        return;
    }

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

    IEnumerable<AccountData> accountDataList = GetAllRecords();

    AccountData accountData = accountDataList.FirstOrDefault(
        accountDataItem => accountDataItem.Name.Equals(usernameCredential.Value)
    );

    if (accountData == null)
    {
        // Account does not exist.
        return;
    }

    LocalUserAccountData localUserAccount = (LocalUserAccountData)accountData;

    // Populate log on context.
    context.AccountId = localUserAccount.Id;
    context.LockedUntil = localUserAccount.LockedUntil;
    context.FailedLogOnCount = localUserAccount.FailedLogOnCount;
}

4.4.4. Logon security

To lock out malicious brute force attacks, the provider must implement the ResetFailedLogOnInfo, and UpdateLogOnFailureInfo methods.

This example demonstrates how to implement these:

/// <summary>
/// Resets any information about failed logon attempts for the specified account.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
public void ResetFailedLogOnInfo(Guid accountId)
{
    IEnumerable<AccountData> accountDataList = GetAllRecords();

    LocalUserAccountData accountData = (LocalUserAccountData)accountDataList.First(
        accountDataItem => accountDataItem.Id.Equals(accountId)
    );

    accountData.FailedLogOnCount = 0;

    SaveAllAccountDataToFile(accountDataList);
}

/// <summary>
/// Updates the log on failure information.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="failedLogOnCount">The new number of consecutive failed logon attempts.</param>
/// <param name="lockedUntil">The time until which the account is locked, or <see langword="null"/>
/// if the account is not yet locked.</param>
public void UpdateLogOnFailureInfo(Guid accountId, int failedLogOnCount, DateTime? lockedUntil)
{
    // This method is not called in IAccountsProvider2, replaced by UpdateDynamicAccountProperties.
}

4.4.5. Updating dynamic account properties

This method updates the dynamic properties of an account. This example demonstrates how to implement this:

/// <summary>
/// Update the dynamic properties of an account.
/// </summary>
/// <param name="accountId">The ID of the account to update.</param>
/// <param name="properties">Dynamic properties of the account.</param>
public void UpdateDynamicAccountProperties(Guid accountId, DynamicAccountProperties properties)
{

    AccountData[] accountDataList = GetAllRecords().ToArray();

    for (int index = 0; index < accountDataList.Count(); index++)
    {
        if (accountDataList[index].Id.Equals(accountId))
        {
            LocalUserAccountData accountData = (LocalUserAccountData)accountDataList[index];

            if (properties.FailedLogOnCount != null)
            {
                accountData.FailedLogOnCount = properties.FailedLogOnCount.Item1;
            }

            if (properties.LockedUntil != null)
            {
                accountData.LockedUntil = properties.LockedUntil.Item1;
            }

            break;

        }
    }

    SaveAllAccountDataToFile(accountDataList);
}

Note
The UpdateDynamicAccountProperties method replaces the UpdateLogOnTimestampAndCount and UpdateLogOnFailureInfo methods in version 2.5 and above. These replaced methods will not be called if the account provider implements IAccountsProvider2.

4.4.6. Getting users

/// <summary>
/// Gets all user records.
/// </summary>
/// <returns>
/// A IEnumerable of <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// </returns>
public IEnumerable<AccountData> GetAllRecords()
{
	List<AccountData> accountDataList = new List<AccountData>();

	foreach (string line in File.ReadAllLines(this.AccountInformationFilePath))
	{
		string[] splitLine = line.Split(',');

        DateTime? lockedUntil = null;

        if (!string.IsNullOrEmpty(splitLine[5]))
        {
            lockedUntil = DateTime.Parse(splitLine[5]);
        }

        accountDataList.Add(new LocalUserAccountData()
        {
            Id = new Guid(splitLine[0]),
            Name = splitLine[1],
            AccountType = AccountType.LocalUser,
            Password = splitLine[2],
            IsEnabled = bool.Parse(splitLine[3]),
            CanChangePassword = true,
            FailedLogOnCount = int.Parse(splitLine[4]),
            LockedUntil = lockedUntil
        });
	}

	return accountDataList;
}
    
/// <summary>
/// Gets a collection of <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// </summary>
/// <param name="recordIds">The record ids.</param>
/// <returns>
/// A collection of <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// </returns>
public ICollection<AccountData> GetById(ICollection<Guid> recordIds)
{
	Collection<accountdata> accountDataCollection = new Collection<accountdata>();
	var allRecords = GetAllRecords();

	foreach (Guid id in recordIds)
	{
        AccountData foundItem = allRecords.FirstOrDefault(
            accountDataItem => accountDataItem.Id.Equals(id)
        );
        if (foundItem != null)
        {
            accountDataCollection.Add(foundItem);
        }
	}
	return accountDataCollection;
}

/// <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)
{
	return GetAllRecords().FirstOrDefault(
		accountData => accountData.Name.Equals(accountName, StringComparison.CurrentCultureIgnoreCase)
	);
}

4.4.7. Importing and exporting users

To import and export users in the project manager, the ImportRecord and ExportRecord methods are used. This is demonstrated below:

/// <summary>
/// Export an account from the underlying storage mechanism.
/// </summary>
/// <param name="accountId">The ID of the account to be exported.</param>
/// <returns>
/// The account data to be exported.
/// </returns>
/// <exception cref="System.NotImplementedException"></exception>
public AccountData ExportRecord(Guid accountId)
{
	return GetAllRecords().FirstOrDefault(
		accountDataItem => accountDataItem.Id.Equals(accountId)
	);
}
/// <summary>
/// Imports a record to the underlying storage mechanism.
/// </summary>
/// <param name="record">The record to be imported.</param>
/// <returns><c>true</c> if a new record was imported; otherwise, <c>false</c>.</returns>
public bool ImportRecord(AccountData record)
{
    IEnumerable<AccountData> allAccounts = GetAllRecords();
    AccountData accountData = allAccounts.FirstOrDefault(
        accountItem => accountItem.Id.Equals(record.Id)
    );
    if (accountData != null)
    {
        // Overwrite existin record.
        accountData = record;
        SaveAllAccountDataToFile(allAccounts);
        return false;
    }
    else
    {
        // New record.
        SaveAllAccountDataToFile(allAccounts.Concat(new AccountData[] { record }));
        return true;
    }
}

4.5. Enabling the custom accounts provider

The custom accounts provider needs to be enabled in Dundas BI's configuration settings.

Log on to Dundas BI using an account that's a member of the System Administrators group, and go to Admin in the main menu on the left.

Click the Setup section, and then the Config button.

Config button
Config button

Select Show Advanced Settings and edit the Custom Accounts Provider configuration setting.

Custom Accounts Provider configuration setting
Custom Accounts Provider configuration setting

Select Edit value and choose your accounts provider from the available values in the drop-down menu.

Custom Accounts Provider sample value
Custom Accounts Provider sample value

5. Debugging

In order to debug the accounts provider, you can use the following:

System.Diagnostics.Debugger.Launch();

This pops up a window that will allow you to attach the debugger.

Debugging popup
Debugging popup

The following example demonstrates how you would debug the login:

public bool ValidateLocalUserLogOnCredentials(LocalLogOnContext context)
{
    System.Diagnostics.Debugger.Launch();
       ...
    // Rest of the ValidateLocalUserLogOnCredentials method.
}

6. 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