Manage Multi-Tenancy
- 14 minutes to read
The Web Dashboard control shares data sources, dashboards, and connection strings between all users. If you want to configure different access rights for different users, you can create custom storages or custom providers to implement your logic and limit access.
- Dashboards
- Custom dashboard storage allows you to specify which dashboards the user can access, edit, and save.
- Data sources
- Custom data source storage allows you to specify which data sources are available to the user.
- Data source schema
- A custom data source schema provider allows you to filter the data source for different users to show only a part of the data source.
- Connection strings
- A custom connection string provider allows you to specify connection strings depending on the user’s access rights.
- Working mode
- The Web Dashboard control can operate in
ViewerOnly
mode for unauthorized users.
When a user logs in, you can get information about this user in the current session. You can specify what permissions the current user has and what information to show the user based on their role.
Register Custom Storage and Provider Objects
To register custom storage and provider classes, call the following DashboardConfigurator methods in the application startup code:
- DashboardConfigurator.SetDashboardStorage(IDashboardStorage)
- DashboardConfigurator.SetDataSourceStorage(IDataSourceStorage)
- DashboardConfigurator.SetDBSchemaProvider(IDBSchemaProviderEx)
- DashboardConfigurator.SetConnectionStringsProvider
ASP.NET Core
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDevExpressControls();
builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
DashboardConfigurator configurator = new DashboardConfigurator();
configurator.SetDashboardStorage(new CustomDashboardStorage(FileProvider.GetFileInfo("App_Data/Dashboards/").PhysicalPath));
configurator.SetDataSourceStorage(new CustomDataSourceStorage());
configurator.SetDBSchemaProvider(new CustomDBSchemaProvider());
configurator.SetConnectionStringsProvider(new CustomConnectionStringProvider());
return configurator;
});
var app = builder.Build();
ASP.NET MVC
public static void RegisterService(RouteCollection routes) {
// ...
DashboardConfigurator.Default.SetDashboardStorage(new CustomDashboardStorage("~/App_Data/Dashboards/"));
DashboardConfigurator.Default.SetDataSourceStorage(new CustomDataSourceStorage());
DashboardConfigurator.Default.SetDBSchemaProvider(new CustomDBSchemaProvider());
DashboardConfigurator.Default.SetConnectionStringsProvider(new CustomConnectionStringProvider());
}
Below you can find how to implement custom storages and providers.
Manage Dashboard Access
Dashboards are stored in dashboard storage. You can create custom dashboard storage and implement your own logic to specify which users can access, edit, and save dashboards. Implement the IEditableDashboardStorage interface that extends IDashboardStorage and allows you to add new dashboards to the custom storage. Define an implementation of the following interface methods:
GetAvailableDashboardsInfo
You can use the GetAvailableDashboardsInfo() method to return a list of dashboards. Create a new list with DashboardInfo objects and use Name and ID methods to limit this list for the current session.
The following code builds a storage list based on files from the specified folder. It also specifies that users can only access storage files that end with their username:
using DevExpress.DashboardWeb;
using System.Collections.Generic;
using System.IO;
using System.Web;
public class CustomDashboardStorage : IEditableDashboardStorage {
private string dashboardStorageFolder;
public CustomDashboardStorage(string dashboardStorageFolder) {
this.dashboardStorageFolder = dashboardStorageFolder;
}
public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo() {
var dashboardInfos = new List<DashboardInfo>();
var files = Directory.GetFiles(HttpContext.Current.Server.MapPath(dashboardStorageFolder), "*.xml");
foreach (var item in files) {
var name = Path.GetFileNameWithoutExtension(item);
var userName = (string)HttpContext.Current.Session["CurrentUser"];
if (userName != null && name.EndsWith(userName, System.StringComparison.InvariantCultureIgnoreCase))
dashboardInfos.Add(new DashboardInfo() { ID = name, Name = name });
}
return dashboardInfos;
}
}
LoadDashboard
Implement the LoadDashboard(String) method to load a dashboard with the specified identifier from the storage. If the dashboard is not available for the current user, you can throw an exception.
The code below parses a dashboard from an XML file depending on the current user name and throws the following exception if you do not have access to the dashboard:
You are not authorized to view this dashboard.
using DevExpress.DashboardWeb;
using System;
using System.IO;
using System.Linq;
using System.Web;
using System.Xml.Linq;
public class CustomDashboardStorage : IEditableDashboardStorage {
private string dashboardStorageFolder;
public CustomDashboardStorage(string dashboardStorageFolder) {
this.dashboardStorageFolder = dashboardStorageFolder;
}
public XDocument LoadDashboard(string dashboardID) {
if (GetAvailableDashboardsInfo().Any(di => di.Name == dashboardID)) {
var path = HttpContext.Current.Server.MapPath(dashboardStorageFolder + dashboardID + ".xml");
var content = File.ReadAllText(path);
return XDocument.Parse(content);
}
else {
throw new ApplicationException("You are not authorized to view this dashboard.");
}
}
}
AddDashboard
The AddDashboard(XDocument, String) method adds a new dashboard to the current IEditableDashboardStorage instance. When you implement this method, you can add a condition that specifies if users can add dashboards.
The following code allows to add new dashboards to dashboard storage only if the current user is Admin
. Otherwise, the app throws the exception:
You are not authorized to add dashboards.
using DevExpress.DashboardWeb;
using System;
using System.IO;
using System.Web;
using System.Xml.Linq;
public class CustomDashboardStorage : IEditableDashboardStorage {
private string dashboardStorageFolder;
public CustomDashboardStorage(string dashboardStorageFolder) {
this.dashboardStorageFolder = dashboardStorageFolder;
}
public string AddDashboard(XDocument dashboard, string dashboardName) {
var userName = (string)HttpContext.Current.Session["CurrentUser"];
if (userName == null || userName != "Admin")
throw new ApplicationException("You are not authorized to add dashboards.");
var path = HttpContext.Current.Server.MapPath(dashboardStorageFolder + dashboardName + "_" + userName + ".xml");
File.WriteAllText(path, dashboard.ToString());
return Path.GetFileNameWithoutExtension(path);
}
}
SaveDashboard
The SaveDashboard(String, XDocument) method saves the specified dashboard to the current storage. Implement this method and specify which users can save newly created dashboards.
The following code only allows users with Admin
to save modified dashboards:
using DevExpress.DashboardWeb;
using System;
using System.IO;
using System.Web;
using System.Xml.Linq;
public class CustomDashboardStorage : IEditableDashboardStorage {
private string dashboardStorageFolder;
public CustomDashboardStorage(string dashboardStorageFolder) {
this.dashboardStorageFolder = dashboardStorageFolder;
}
public void SaveDashboard(string dashboardID, XDocument dashboard) {
var userName = (string)HttpContext.Current.Session["CurrentUser"];
if (userName == null || userName != "Admin")
throw new ApplicationException("You are not authorized to save dashboards.");
var path = HttpContext.Current.Server.MapPath(dashboardStorageFolder + dashboardID + ".xml");
File.WriteAllText(path, dashboard.ToString());
}
}
Manage Data Source Access
To make data sources available for users, add these data sources to data source storage. You can create custom data source storage and specify which users can access the predefined data sources. To do this, implement the IDataSourceStorage interface with the following methods:
Configure Data Sources
You can create a new dictionary where a key is the data source name and a value is the data source in the XDocument XML format. Configure data sources and add them to the dictionary to manage user permissions.
using System;
using System.Xml.Linq;
using System.Collections.Generic;
using DevExpress.DashboardCommon;
using DevExpress.DashboardWeb;
using DevExpress.DataAccess.Json;
using DevExpress.DataAccess.Sql;
public class CustomDataSourceStorage : IDataSourceStorage {
private Dictionary<string, XDocument> documents = new Dictionary<string, XDocument>();
private const string sqlDataSourceId1 = "SQL Data Source (Northwind)";
private const string sqlDataSourceId2 = "SQL Data Source (CarsXtraScheduling)";
private const string jsonDataSourceId = "JSON Data Source";
public CustomDataSourceStorage() {
DashboardSqlDataSource sqlDataSource1 = new DashboardSqlDataSource(sqlDataSourceId1, "NorthwindConnectionString");
SelectQuery query1 = SelectQueryFluentBuilder
.AddTable("Categories")
.SelectAllColumnsFromTable()
.Build("Categories");
sqlDataSource1.Queries.Add(query1);
SelectQuery query2 = SelectQueryFluentBuilder
.AddTable("Products")
.SelectAllColumnsFromTable()
.Build("Products");
sqlDataSource1.Queries.Add(query2);
DashboardSqlDataSource sqlDataSource2 = new DashboardSqlDataSource(sqlDataSourceId2, "CarsXtraSchedulingConnectionString");
SelectQuery query = SelectQueryFluentBuilder
.AddTable("Cars")
.SelectAllColumnsFromTable()
.Build("Cars");
sqlDataSource2.Queries.Add(query);
DashboardJsonDataSource jsonDataSource = new DashboardJsonDataSource(jsonDataSourceId);
jsonDataSource.JsonSource = new UriJsonSource(new Uri("https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"));
jsonDataSource.RootElement = "Customers";
documents[sqlDataSourceId1] = new XDocument(sqlDataSource1.SaveToXml());
documents[sqlDataSourceId2] = new XDocument(sqlDataSource2.SaveToXml());
documents[jsonDataSourceId] = new XDocument(jsonDataSource.SaveToXml());
}
}
GetDataSourcesID
The GetDataSourcesID() method gets identifiers of the data sources stored in the current IDataSourceStorage. Specify which identifiers to add to the dictionary depending on the user role.
The following code returns all data sources for Admin
, sqlDataSourceId2
for User
, and no data sources for other users:
using System.Collections.Generic;
using DevExpress.DashboardWeb;
using System.Web;
public class CustomDataSourceStorage : IDataSourceStorage {
// ...
public IEnumerable<string> GetDataSourcesID() {
var userName = (string)HttpContext.Current.Session["CurrentUser"];
if (userName == "Admin") {
return documents.Keys;
}
else if (userName == "User") {
return new string[] { sqlDataSourceId2 };
}
else {
return new string[0];
}
}
}
GetDataSource
The GetDataSource(String) method loads the data source’s XML definition from the current IDataSourceStorage.
The following code checks whether the dictionary contains the specified key and returns the corresponding data source’s XML definition if the condition is true:
using System;
using System.Linq;
using System.Xml.Linq;
using DevExpress.DashboardWeb;
public class CustomDataSourceStorage : IDataSourceStorage {
// ...
public XDocument GetDataSource(string dataSourceID) {
if (GetDataSourcesID().Contains(dataSourceID)) {
return documents[dataSourceID];
}
else {
throw new ApplicationException("You are not authorized to use this datasource.");
}
}
}
Manage Database Schema
You can limit tables and views depending on the user role. Implement the DBSchemaProviderEx interface and the GetTables method to manage user permissions.
For example, the following code filters tables to show only the Cars
table if the username is User
:
using DevExpress.DataAccess.Sql;
using DevExpress.Xpo.DB;
using System.Linq;
using System.Web;
public class CustomDBSchemaProvider : DBSchemaProviderEx {
public override DBTable[] GetTables(SqlDataConnection connection, params string[] tableList) {
var result = base.GetTables(connection, tableList);
var userName = (string)HttpContext.Current.Session["CurrentUser"];
if (userName == "Admin") {
return result;
}
else if (userName == "User") {
return result.Where(t => t.Name == "Cars").ToArray();
}
else {
return new DBTable[0];
}
}
}
Manage Connection String Access
A custom connection string provider allows you to manage which data connections are available in the Data Source Wizard. This wizard allows users to select one of the predefined data connections, connect to a database, and select data. To create the custom connection string provider, implement the IDataSourceWizardConnectionStringsProvider interface with the following methods:
You can create a new dictionary where a key is the data connection name and a value is the connection string. Configure connection strings and add them to the dictionary:
public class CustomConnectionStringProvider : IDataSourceWizardConnectionStringsProvider {
private Dictionary<string, string> connectionStrings = new Dictionary<string, string>();
public CustomConnectionStringProvider() {
connectionStrings.Add("NorthwindConnectionString", @"XpoProvider=MSAccess; Provider=Microsoft.Jet.OLEDB.4.0; Data Source=|DataDirectory|\nwind.mdb;");
connectionStrings.Add("CarsXtraSchedulingConnectionString", @"XpoProvider=MSAccess; Provider=Microsoft.Jet.OLEDB.4.0; Data Source=|DataDirectory|\CarsDB.mdb;");
}
}
GetConnectionDescriptions
The GetConnectionDescriptions() method returns the dictionary that lists the data connections by name. When you implement this method, create a dictionary and specify which connections are available in this dictionary for users depending on their role.
The following code provides access to the NorthwindConnectionString
and CarsXtraSchedulingConnectionString
connection strings for Admin
and only CarsXtraSchedulingConnectionString
for User
. The other users do not have access to connection strings.
using DevExpress.DataAccess.Web;
using System.Collections.Generic;
using System.Web;
public class CustomConnectionStringProvider : IDataSourceWizardConnectionStringsProvider {
// ...
private Dictionary<string, string> connectionStrings = new Dictionary<string, string>();
public CustomConnectionStringProvider() {
connectionStrings.Add("NorthwindConnectionString", @"XpoProvider=MSAccess; Provider=Microsoft.Jet.OLEDB.4.0; Data Source=|DataDirectory|\nwind.mdb;");
connectionStrings.Add("CarsXtraSchedulingConnectionString", @"XpoProvider=MSAccess; Provider=Microsoft.Jet.OLEDB.4.0; Data Source=|DataDirectory|\CarsDB.mdb;");
}
public Dictionary<string, string> GetConnectionDescriptions() {
var connections = new Dictionary<string, string>();
var userName = (string)HttpContext.Current.Session["CurrentUser"];
if (userName == "Admin") {
connections.Add("NorthwindConnectionString", "Northwind Connection");
connections.Add("CarsXtraSchedulingConnectionString", "CarsXtraScheduling Connection");
}
else if (userName == "User") {
connections.Add("CarsXtraSchedulingConnectionString", "CarsXtraScheduling Connection");
}
return connections;
}
}
GetDataConnectionParameters
The GetDataConnectionParameters(String) method returns the parameters of a specific data connection. Return the CustomStringConnectionParameters objects for users that have the corresponding permissions.
using DevExpress.DataAccess.ConnectionParameters;
using DevExpress.DataAccess.Web;
using System;
using System.Collections.Generic;
public class CustomConnectionStringProvider : IDataSourceWizardConnectionStringsProvider {
// ...
private Dictionary<string, string> connectionStrings = new Dictionary<string, string>();
public CustomConnectionStringProvider() {
connectionStrings.Add("NorthwindConnectionString", @"XpoProvider=MSAccess; Provider=Microsoft.Jet.OLEDB.4.0; Data Source=|DataDirectory|\nwind.mdb;");
connectionStrings.Add("CarsXtraSchedulingConnectionString", @"XpoProvider=MSAccess; Provider=Microsoft.Jet.OLEDB.4.0; Data Source=|DataDirectory|\CarsDB.mdb;");
}
public DataConnectionParametersBase GetDataConnectionParameters(string name) {
if (GetConnectionDescriptions().ContainsKey(name)) {
return new CustomStringConnectionParameters(connectionStrings[name]);
}
else {
throw new ApplicationException("You are not authorized to use this connection.");
}
}
}
Prevent Unauthorized Access
For unauthorized users and guests, you can display dashboards in ViewerOnly
mode to prevent inadvertent or unauthorized modifications to dashboards stored on a server. To do this, handle the DashboardConfigurator.VerifyClientTrustLevel event and set the ClientTrustLevel property to Restricted
. In this case, the unauthorized users can only view the dashboards.
More information about security:
- Security Considerations in ASP.NET Core
- Security Considerations in ASP.NET MVC
- Security Considerations in ASP.NET Web Forms
Example - How to Implement Multi-Tenant Dashboard Architecture
The examples below show how to configure the Web Dashboard so that it works in the multi-user environment. You can identify a user in the current session and return the user-specific content.
Example - How to Load Different Data Based on the Current User
The following examples configure the Dashboard control so that it loads data based on the current user. You can identify a user in the current session and handle the events to select the underlying data source.
Example - How to Use Separate Server-Side Settings for Different Views
The following examples illustrate how to use separate DashboardConfigurator instances within an application to provide different server-side settings. In this example, the Sales and Marketing views use different dashboard storages.