In the introduction to multi-tenant reporting, we discussed how ActiveReports Server uses a security provider and security filters to implement custom authentication when you deploy self-service reporting in multi-tenant environments. These are environments in which multiple clients, such as multiple companies, each with its own group of employees, share a single application with access to only their information. This is very common in SaaS applications that need to add reporting. In this article, we will use the framework to deploy custom authentication for a sample multi-tenant reporting scenario.

Getting started

The ActiveReports Server installation includes a sample Visual Studio project that demonstrates the use of the security provider. It allows customers and employees to use their email addresses and IDs to log into the system and access only those reports to which they have been assigned permissions. The ActiveTunesEmployee role is used for the employees and the ActiveTunesCustomer role is used for the customers.

Initializing the security provider

The ActiveTunesSecurityProviderFactory class initializes the security provider and specifies settings that you can configure in the Administrator Dashboard.

namespace ActiveReports.Server.Security  
{  
    public interface ISecurityProviderFactory  
    {  
        ISecurityProvider Create(IDictionary<string, string> settings);  
        IEnumerable<string> GetSupportedSettings();  
    }  
}

Provide a connection string to get user authentication data

The GetSupportedSettings function tells ActiveReports Server which settings the administrator can configure using the Administrator Dashboard.

public IEnumerable<string> GetSupportedSettings()  
        {  
            return new string[] { "ConnectionString"};  
        }

Create custom object for custom authentication

The Create function initializes the custom security provider with the settings configured in the Administrator Dashboard. In this case it is a database connection string.

public ISecurityProvider Create(IDictionary<string, string> settings)  
        {  
            return newActiveTunesSecurityProvider(settings);  
        }

Authenticating the user

namespace ActiveReports.Server.Security  
{  
    public interface ISecurityProvider  
    {  
        string CreateToken(string username, string password, string custom);  
        void DisposeToken(string token);  
        IEnumerable<IRole> FilterRoles(string token, IEnumerable<IRole> roles);  
        UserContext GetAdminContext();  
        string GetCacheKeySalt(string token);  
        UserContext GetUserContext(string token);  
        IEnumerable<string> GetUserContextKeys();  
        UserDescription GetUserDescription(string token);  
        bool ValidateToken(string token);  
    }  
}

The ActiveTunesSecurityProvider constructor uses the database connection string to initialize a Database object to retrieve user information from the database.

public ActiveTunesSecurityProvider(IDictionary<string, string> settings)  
        {  
            if (settings == null)  
                throw new ArgumentNullException("settings");  

            string connectionString;  
            if (settings.TryGetValue("ConnectionString", outconnectionString))  
                Database.SetConnectionString(connectionString);  
        }

Creating the user security token

When the user logs in for the first time, the CreateToken function authenticates the user with information from the database. In this code sample, we are using the EmployeeID and CustomerID as passwords, and validating them from the database. The security token also identifies the user throughout the user session.

public string CreateToken(string username, string password, string custom)  
        {  
            /* For example purposes we are expecting username to be the email address.  
 * If it is a customer the CustomerID is the password.   
 * If it is an Employee his EmployeeID is his password.  
 */  

var customer = Database.GetCustomer(username);  
            if (customer != null && string.Equals(customer.CustomerID.ToString(), password))  
                return CreateTokenForUser(username, Constants.RoleNames.Customers);  
            // no customer apparently...  

var employee = Database.GetEmployee(username);  
            if (employee != null && string.Equals(employee.EmployeeID.ToString(), password))  
                return CreateTokenForUser(username, Constants.RoleNames.Employees);  
            // no employee and no customer  

return null;  
        }

This is the function that you can customize to contain your proprietary authentication algorithms to validate the user who is attempting to log in.

Getting user permissions at run time

In addition to authenticating the user, ActiveReports Server also needs to know what permissions the user needs at run time. The FilterRoles function provides the information by mapping the user permissions from your proprietary authentication layer to the roles defined in ActiveReports Server.

public IEnumerable<IRole> FilterRoles(string token, IEnumerable<IRole> roles)  
        {  
            if (token == null)  
                throw new ArgumentNullException("token");  
            var p = GetPersonFromToken(token);  
            if (p != null)  
            {  
                var targetRole = "";  
                if (p is Database.Customer)  
                    targetRole = Constants.RoleNames.Customers;  
                else if (p is Database.Employee)  
                    targetRole = Constants.RoleNames.Employees;  
                var q = from role in roles where role.Name == targetRole select role;  
                return q;  
            }  
            throw new ArgumentException(SR.InvalidToken, "token");  
        }

In the sample, the roles are determined by user type (whether Employee or Customer) and the role names are stored as constants. However, you can modify this algorithm according to how the roles are stored in your authentication layer.

Getting user-specific information

Using the UserDescription object The UserDescription object stores a friendly name of the logged-in user and her email address. ActiveReports Server uses this object to personalize the user experience in these ways:

  • The name identifies the user in tracking information for Reports and Schedules.
  • The email is used to submit feedback while using ActiveReports Server.

Using the UserContext object The UserContext values enable the security provider to pass additional information about the user to ActiveReports Server. You can use these values in security filters and database connection strings in your data models. In the ActiveTunesSecurityProvider, the GetUserContextKeys function defines the user context values that ActiveReports Server expects when a user logs into the system. The GetUserContext function specifies the actual values for these keys. For Customers, the CustomerID key is populated with valid data, and for Employees, the EmployeeID key contains valid data. It is important to pass a value for each key to ensure consistent behavior.

public IEnumerable<string> GetUserContextKeys()  
        {  
            return new [] { "CustomerID", "EmployeeID" };  
        }  

public UserContext GetUserContext(string token)  
        {  
            // The values from this UserContext are used in our SecurityFilters in the logical model.  
            var p = GetPersonFromToken(token);  
            var context = new Dictionary<string, string>();  

            var customer = p as Database.Customer;  
            if (customer != null)  
            {  
                context["CustomerID"] = customer.CustomerID.ToString();  
                context["EmployeeID"] = "-1";  
            }  
            else  
            {  
                var employee = p as Database.Employee;  
                if (employee != null)  
                {  
                    context["EmployeeID"] = employee.EmployeeID.ToString();  
                    context["CustomerID"] = "-1";  
                }  
            }  
            return new MyUserContext(context);  
        }

In your own custom security provider, you might want to provide information about the department, designation, tenant identification, database server, etc.

Getting administrator information with AdminContext

While using the Administrator Dashboard, administrators get information from the database as they create entities and attributes and preview the resulting data. To ensure that they see only the authorized data, ActiveReports Server uses a set of default AdminContext values.

public UserContext GetAdminContext()  
        {  
            var context = new Dictionary<string, string>();  
            context["CustomerID"] = "-1";  
            context["EmployeeID"] = "-1";  
            return new MyUserContext(context);  
        }

In your own custom security provider, you might want to expose specific configuration information to ensure that your customer data is protected from internal administration users.

Implementing row-level security

To implement Row-level security, use UserContext values for the logged-in user in security filters when you edit entities in the data model. This enforces row-level security by allowing the user to see only the data from specific rows in the database that they have access to. In multi-tenant environments with a shared database, this maps to the specific tenant that the user belongs to. Row level security

Use dynamic connection strings to simplify multi-tenant administration

For multi-tenant environments with isolated databases, these user context values can act as dynamic place holders in the connection string of the data model. For example, in the image below, the “DBName” in the connection string is replaced by the user context value for the DBName key based on the logged-in user. This allows you to maintain one data model for multiple tenants, thus reducing ongoing maintenance and making it easy to add new tenants. Multi-tenant administration