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 get started
  • How to create a Dundas BI extension
  • How to create a custom accounts provider

2. What is a custom accounts provider

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

  • Using the Dundas BI local database for user accounts.
  • Using an Active Directory server to provide user account information.

While this is enough for many networks, being able to authenticate against a custom or legacy provider makes it much easier 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 in using standard authentication, and 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 2012, or higher.
  • Microsoft .NET Framework 4.5
  • Dundas BI Server

3.1. Downloading file

To download the Custom Accounts Provider Sample solution click here.

3.2. Extracting sample to SDK folder

This sample is designed to automatically publish the extension to the instance. First you must extract the CustomAccountsProviderSample.zip to the SDK folder within the instance. To extract the CustomAccountsProviderSample.zip do the following:

  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 the exact folder, it will not work correctly.

3.3. Opening Solution

To open the Visual Studio solution simply do the following:

  1. Right click on the Microsoft Visual Studio shortcut, and click run as administrator.
  2. Click the File Menu, then Open Solution.
  3. Double click 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 first contain a class that extends the ExtensionPackageInfo class. This class contains the package information about the extension package.

/// <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 Dundas.BI.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 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

In order for the custom account provider to lock out malicious brute force attacks the provider must implement the ResetFailedLogOnInfo, and UpdateLogOnFailureInfo methods.

This example demonstrates how to implement the ResetFailedLogOnInfo, and UpdateLogOnFailureInfo methods.

/// <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, is replaced by UpdateDynamicAccountProperties.
}

4.4.5. The UpdateDynamicAccountProperties method

This method updates the dynamic properties of an account. This example demonstrates how to implement the UpdateDynamicAccountProperties methods.

/// <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. The UpdateLogOnTimestampAndCount and UpdateLogOnFailureInfo 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(
		accountDataItem => accountDataItem.Name.Equals(
			accountName, StringComparison.CurrentCultureIgnoreCase
			)
		);
}

4.4.7. Importing and Exporting Users

In order for the users to be imported and exported 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

In order to enable the custom accounts provider you need to do the following:

1. Log on to Dundas BI using the administrator account.

2. Click the Admin toolbar icon:

Admin toolbar icon
Admin toolbar icon

3. Click the Setup section, and then the config button:

Setup config button
Setup config button

4. Set the setting scope to Global:

Scope global
Scope global

5. In the Authentication category with the name Custom Accounts Provider enter the following text:

    c4889691-ab7f-452b-ba16-fb3bcda46724:CustomAccountsProviderSample:Dundas.BI.Sample.CustomAccountsProviderSample.CustomAccountProvider

{articlenote title="Note"|text=

The format of this value is "[pkgId]:[assemblyName]:[typeName]", where:

  • [pkgId] is the ID of the extension package containing the provider (e.g. "37676181-23df-4bdf-b618-79d5f2aa5f52");
  • [assemblyName] is the name of the assembly containing the provider (e.g."MyCompany.DundasExtensions");
  • [typeName] is the full type name of the provider (e.g. "MyCompany.DundasExtensions.MyAccountsProvider").

}

6. Click the checkmark to save the changes:

Checkmark
Checkmark

7. Reset the Dundas BI application pool

5. Debugging

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

System.Diagnostics.Debugger.Launch();

This pop 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