aspnet-405157-security-considerations-authorization-reporting.md
Use strategies outlined in this topic to implement authorization logic for both the DevExpress ASP.NET Web Forms Document Viewer and Report Designer (and address CWE-285-related security risks).
Create a custom report storage derived from the ReportStorageWebExtension class to implement authorization logic for DevExpress Document Viewer and Report Designer components. As a starting point, you can use the following ReportStorageWithAccessRules class implementation and modify it based on your requirements:
Show the ReportStorageWithAccessRules class
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using DevExpress.XtraReports.UI;
using DevExpress.XtraReports.Web.Extensions;
namespace SecurityBestPractices.Authorization.Reports {
public class ReportStorageWithAccessRules : ReportStorageWebExtension {
private static readonly Dictionary<Type, string> reports = new Dictionary<Type, string> {
{typeof(PublicReport), "Public Report"},
{typeof(AdminReport), "Admin Report"},
{typeof(JohnReport), "John Report"}
};
static string GetIdentityName() {
return HttpContext.Current.User?.Identity?.Name;
}
static XtraReport CreateReportByDisplayName(string displayName) {
Type type = reports.First(v => v.Value == displayName).Key;
return (XtraReport)Activator.CreateInstance(type);
}
// Returns reports that the current user can view
public static IEnumerable<string> GetViewableReportDisplayNamesForCurrentUser() {
var identityName = GetIdentityName();
var result = new List<string>();
if (identityName == "Admin") {
result.AddRange(new[] { reports[typeof(AdminReport)], reports[typeof(JohnReport)] });
} else if (identityName == "John") {
result.Add(reports[typeof(JohnReport)]);
}
result.Add(reports[typeof(PublicReport)]); // For unauthenticated users (i.e., public)
return result;
}
// Returns reports that the current user can edit
public static IEnumerable<string> GetEditableReportNamesForCurrentUser() {
var identityName = GetIdentityName();
if (identityName == "Admin") {
return new[] { reports[typeof(AdminReport)], reports[typeof(JohnReport)] };
}
if (identityName == "John") {
return new[] { reports[typeof(JohnReport)] };
}
return Array.Empty<string>();
}
// Overrides ReportStorageWebExtension
public override bool CanSetData(string url) {
var reportNames = GetEditableReportNamesForCurrentUser();
return reportNames.Contains(url);
}
public override byte[] GetData(string url) {
var reportNames = GetViewableReportDisplayNamesForCurrentUser();
if(!reportNames.Contains(url))
throw new UnauthorizedAccessException();
// Implement your logic to get bytes from DB
XtraReport publicReport = CreateReportByDisplayName(url);
using(MemoryStream ms = new MemoryStream()) {
publicReport.SaveLayoutToXml(ms);
return ms.GetBuffer();
}
}
// Returns URLs and display names for all reports available for editing in the storage
public override Dictionary<string, string> GetUrls() {
var result = new Dictionary<string, string>();
var reportNames = GetEditableReportNamesForCurrentUser();
foreach(var reportName in reportNames) {
result.Add(reportName, reportName);
}
return result;
}
public override bool IsValidUrl(string url) {
var reportNames = GetEditableReportNamesForCurrentUser();
return reportNames.Contains(url);
}
public override void SetData(XtraReport report, string url) {
// Implement your logic to save bytes to the database. Refer to the following topic for
// more information and examples: https://docs.devexpress.com/XtraReports/17553
}
public override string SetNewData(XtraReport report, string defaultUrl) {
// Implement your logic to save bytes to the database. Refer to the following topic for
// more information and examples: https://docs.devexpress.com/XtraReports/17553
return "New name";
}
}
}
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Linq
Imports System.Web
Imports DevExpress.XtraReports.UI
Imports DevExpress.XtraReports.Web.Extensions
Namespace SecurityBestPractices.Authorization.Reports
Public Class ReportStorageWithAccessRules
Inherits ReportStorageWebExtension
Private Shared ReadOnly reports As Dictionary(Of Type, String) = New Dictionary(Of Type, String) From {
{GetType(PublicReport), "Public Report"},
{GetType(AdminReport), "Admin Report"},
{GetType(JohnReport), "John Report"}
}
Private Shared Function GetIdentityName() As String
Return HttpContext.Current.User?.Identity?.Name
End Function
Private Shared Function CreateReportByDisplayName(ByVal displayName As String) As XtraReport
Dim type As Type = reports.First(Function(v) v.Value = displayName).Key
Return CType(Activator.CreateInstance(type), XtraReport)
End Function
' Returns reports that the current user can view
Public Shared Function GetViewableReportDisplayNamesForCurrentUser() As IEnumerable(Of String)
Dim identityName = GetIdentityName()
Dim result = New List(Of String)()
If Equals(identityName, "Admin") Then
result.AddRange({reports(GetType(AdminReport)), reports(GetType(JohnReport))})
ElseIf Equals(identityName, "John") Then
result.Add(reports(GetType(JohnReport)))
End If
result.Add(reports(GetType(PublicReport))) ' For unauthenticated users (i.e., public)
Return result
End Function
' Returns reports that the current user can edit
Public Shared Function GetEditableReportNamesForCurrentUser() As IEnumerable(Of String)
Dim identityName = GetIdentityName()
If Equals(identityName, "Admin") Then
Return {reports(GetType(AdminReport)), reports(GetType(JohnReport))}
End If
If Equals(identityName, "John") Then
Return {reports(GetType(JohnReport))}
End If
Return Array.Empty(Of String)()
End Function
' Overrides ReportStorageWebExtension
Public Overrides Function CanSetData(ByVal url As String) As Boolean
Dim reportNames = GetEditableReportNamesForCurrentUser()
Return reportNames.Contains(url)
End Function
Public Overrides Function GetData(ByVal url As String) As Byte()
Dim reportNames = GetViewableReportDisplayNamesForCurrentUser()
If Not reportNames.Contains(url)
Then Throw New UnauthorizedAccessException()
' Implement your logic to get bytes from DB
Dim publicReport As XtraReport = CreateReportByDisplayName(url)
Using ms As MemoryStream = New MemoryStream()
publicReport.SaveLayoutToXml(ms)
Return ms.GetBuffer()
End Using
End Function
' Returns URLs and display names for all reports available for editing in the storage
Public Overrides Function GetUrls() As Dictionary(Of String, String)
Dim result = New Dictionary(Of String, String)()
Dim reportNames = GetEditableReportNamesForCurrentUser()
For Each reportName In reportNames
result.Add(reportName, reportName)
Next
Return result
End Function
Public Overrides Function IsValidUrl(ByVal url As String) As Boolean
Dim reportNames = GetEditableReportNamesForCurrentUser()
Return reportNames.Contains(url)
End Function
Public Overrides Sub SetData(ByVal report As XtraReport, ByVal url As String)
' Implement your logic to save bytes to the database. Refer to the following topic for
' more information and examples: https://docs.devexpress.com/XtraReports/17553
End Sub
Public Overrides Function SetNewData(ByVal report As XtraReport, ByVal defaultUrl As String) As String
' Implement your logic to save bytes to the database. Refer to the following topic for
' more information and examples: https://docs.devexpress.com/XtraReports/17553
Return "New name"
End Function
End Class
End Namespace
Note the following implementation details:
The GetViewableReportDisplayNamesForCurrentUser method returns a list of reports available to the current user (view mode). Call this method from the overridden GetData method and other methods that interact with the report storage.
The GetEditableReportNamesForCurrentUser method returns a list of reports available to the current user (edit mode). Call this method from the overridden IsValidUrl method and other methods that write report data.
Our ReportStorageWithAccessRules class implementation throws an UnauthorizedAccessException when users try to open reports they cannot access. To prevent errors, you can verify access rights in the PageLoad event and redirect unauthorized users to a public page:
<dx:ASPxReportDesigner ID="ASPxReportDesigner1" runat="server">
</dx:ASPxReportDesigner>
protected void Page_Load(object sender, EventArgs e) {
var name = Request.QueryString["name"];
var reportNames = ReportStorageWithAccessRules.GetEditableReportNamesForCurrentUser();
if(reportNames.Contains(name))
ASPxReportDesigner1.OpenReport(name);
else
Response.Redirect("~/Authorization/Reports/ReportViewerPage.aspx");
}
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
Dim name = Request.QueryString("name")
Dim reportNames = ReportStorageWithAccessRules.GetEditableReportNamesForCurrentUser()
If reportNames.Contains(name) Then
ASPxReportDesigner1.OpenReport(name)
Else
Response.Redirect("~/Authorization/Reports/ReportViewerPage.aspx")
End If
End Sub
Once you implement your custom report storage, register it in the Global.asax.cs or Global.asax.vb file:
DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension.RegisterExtensionGlobal(
new ReportStorageWithAccessRules()
);
DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension.RegisterExtensionGlobal(
New ReportStorageWithAccessRules()
)
The Document Viewer control maintains an open connection with the server to obtain additional document data when required (for instance, when a user switches pages or exports the document). This connection allows users to navigate through report pages even after they log out.
Implement a custom operation logger to limit operations available to the current user. To implement access control rules, extend the WebDocumentViewerOperationLogger class and override its methods. Use the following OperationLogger class implementation as a starting point:
Show the OperationLogger class
Note
For demonstration purposes, the OperationLogger class obtains user account data from a static property. In a real project, you should store authentication information in a data storage.
using System;
using System.Collections.Generic;
using System.Web;
using DevExpress.XtraPrinting;
using DevExpress.XtraReports.UI;
using DevExpress.XtraReports.Web.ClientControls;
using DevExpress.XtraReports.Web.WebDocumentViewer;
namespace SecurityBestPractices.Authorization.Reports {
public class OperationLogger : WebDocumentViewerOperationLogger, IWebDocumentViewerAuthorizationService,
IExportingAuthorizationService {
const string ReportDictionaryName = "reports";
const string DocumentDictionaryName = "documents";
const string ExportedDocumentDictionaryName = "exportedDocuments";
static readonly Dictionary<string, Dictionary<string, HashSet<string>>> authDictionary =
new Dictionary<string, Dictionary<string, HashSet<string>>>();
static OperationLogger() {
authDictionary.Add("Public", new Dictionary<string, HashSet<string>> {
{ReportDictionaryName, new HashSet<string>()},
{DocumentDictionaryName, new HashSet<string>()},
{ExportedDocumentDictionaryName, new HashSet<string>()}
});
authDictionary.Add("Admin", new Dictionary<string, HashSet<string>> {
{ReportDictionaryName, new HashSet<string>()},
{DocumentDictionaryName, new HashSet<string>()},
{ExportedDocumentDictionaryName, new HashSet<string>()}
});
authDictionary.Add("John", new Dictionary<string, HashSet<string>> {
{ReportDictionaryName, new HashSet<string>()},
{DocumentDictionaryName, new HashSet<string>()},
{ExportedDocumentDictionaryName, new HashSet<string>()}
});
}
public override void ReportOpening(string reportId, string documentId, XtraReport report) {
if(report == null) {
var identityName = GetIdentityName();
if(string.IsNullOrEmpty(identityName))
identityName = "Public";
SaveUsedEntityId(ReportDictionaryName, identityName, reportId);
SaveUsedEntityId(DocumentDictionaryName, identityName, documentId);
} else
if(report is PublicReport) {
SaveUsedEntityId(ReportDictionaryName, "Public", reportId);
SaveUsedEntityId(DocumentDictionaryName, "Public", documentId);
}
else if(report is AdminReport) {
SaveUsedEntityId(ReportDictionaryName, "Admin", reportId);
SaveUsedEntityId(DocumentDictionaryName, "Admin", documentId);
}
else if(report is JohnReport) {
SaveUsedEntityId(ReportDictionaryName, "John", reportId);
SaveUsedEntityId(DocumentDictionaryName, "John", documentId);
SaveUsedEntityId(ReportDictionaryName, "Admin", reportId);
SaveUsedEntityId(DocumentDictionaryName, "Admin", documentId);
}
}
public override void BuildStarted(string reportId, string documentId, ReportBuildProperties buildProperties) {
if(IsEntityAuthorized("Public", ReportDictionaryName, reportId)) {
SaveUsedEntityId(DocumentDictionaryName, "Public", documentId);
}
if(IsEntityAuthorized("Admin", ReportDictionaryName, reportId)) {
SaveUsedEntityId(DocumentDictionaryName, "Admin", documentId);
}
if(IsEntityAuthorized("John", ReportDictionaryName, reportId)) {
SaveUsedEntityId(DocumentDictionaryName, "John", documentId);
}
}
public override ExportedDocument ExportDocumentStarting(string documentId, string asyncExportOperationId,
string format, ExportOptions options, PrintingSystemBase printingSystem,
Func<ExportedDocument> doExportSynchronously) {
if(!IsEntityAuthorizedForCurrentUser(DocumentDictionaryName, documentId))
throw new UnauthorizedAccessException();
return base.ExportDocumentStarting(documentId, asyncExportOperationId, format, options, printingSystem,
doExportSynchronously);
}
public override void ReleaseDocument(string documentId) {
}
bool IWebDocumentViewerAuthorizationService.CanCreateDocument() {
return true;
}
bool IWebDocumentViewerAuthorizationService.CanCreateReport() {
return true;
}
bool IWebDocumentViewerAuthorizationService.CanReadDocument(string documentId) {
return IsEntityAuthorizedForCurrentUser(DocumentDictionaryName, documentId);
}
bool IWebDocumentViewerAuthorizationService.CanReadReport(string reportId) {
return IsEntityAuthorizedForCurrentUser(ReportDictionaryName, reportId);
}
bool IWebDocumentViewerAuthorizationService.CanReleaseDocument(string documentId) {
return IsEntityAuthorizedForCurrentUser(DocumentDictionaryName, documentId);
}
bool IWebDocumentViewerAuthorizationService.CanReleaseReport(string reportId) {
return IsEntityAuthorizedForCurrentUser(ReportDictionaryName, reportId);
}
static string GetIdentityName() {
return HttpContext.Current.User?.Identity?.Name;
}
void SaveUsedEntityId(string dictionaryName, string user, string id) {
if(string.IsNullOrEmpty(id))
return;
lock(authDictionary)
authDictionary[user][dictionaryName].Add(id);
}
bool IsEntityAuthorizedForCurrentUser(string dictionaryName, string id) {
return IsEntityAuthorized(GetIdentityName(), dictionaryName, id);
}
bool IsEntityAuthorized(string user, string dictionaryName, string id) {
if(string.IsNullOrEmpty(id))
return false;
lock(authDictionary)
return authDictionary["Public"][dictionaryName].Contains(id) || !string.IsNullOrEmpty(user) && authDictionary[user][dictionaryName].Contains(id);
}
public bool CanReadExportedDocument(string id) {
// For DevExpress.Report.Preview.AsyncExportApproach = true;
return IsEntityAuthorizedForCurrentUser(ExportedDocumentDictionaryName, id);
}
}
}
Imports System
Imports System.Collections.Generic
Imports System.Web
Imports DevExpress.XtraPrinting
Imports DevExpress.XtraReports.UI
Imports DevExpress.XtraReports.Web.ClientControls
Imports DevExpress.XtraReports.Web.WebDocumentViewer
Namespace SecurityBestPractices.Authorization.Reports
Public Class OperationLogger
Inherits WebDocumentViewerOperationLogger
Implements IWebDocumentViewerAuthorizationService, IExportingAuthorizationService
Const ReportDictionaryName As String = "reports"
Const DocumentDictionaryName As String = "documents"
Const ExportedDocumentDictionaryName As String = "exportedDocuments"
Shared ReadOnly authDictionary As Dictionary(Of String, Dictionary(Of String, HashSet(Of String))) = New Dictionary(Of String, Dictionary(Of String, HashSet(Of String)))()
Private Shared Sub New()
authDictionary.Add("Public", New Dictionary(Of String, HashSet(Of String)) From {
{ReportDictionaryName, New HashSet(Of String)()},
{DocumentDictionaryName, New HashSet(Of String)()},
{ExportedDocumentDictionaryName, New HashSet(Of String)()}
})
authDictionary.Add("Admin", New Dictionary(Of String, HashSet(Of String)) From {
{ReportDictionaryName, New HashSet(Of String)()},
{DocumentDictionaryName, New HashSet(Of String)()},
{ExportedDocumentDictionaryName, New HashSet(Of String)()}
})
authDictionary.Add("John", New Dictionary(Of String, HashSet(Of String)) From {
{ReportDictionaryName, New HashSet(Of String)()},
{DocumentDictionaryName, New HashSet(Of String)()},
{ExportedDocumentDictionaryName, New HashSet(Of String)()}
})
End Sub
Public Overrides Sub ReportOpening(ByVal reportId As String, ByVal documentId As String, ByVal report As XtraReport)
If report Is Nothing Then
Dim identityName = GetIdentityName()
If String.IsNullOrEmpty(identityName) Then identityName = "Public"
SaveUsedEntityId(ReportDictionaryName, identityName, reportId)
SaveUsedEntityId(DocumentDictionaryName, identityName, documentId)
ElseIf TypeOf report Is PublicReport Then
SaveUsedEntityId(ReportDictionaryName, "Public", reportId)
SaveUsedEntityId(DocumentDictionaryName, "Public", documentId)
ElseIf TypeOf report Is AdminReport Then
SaveUsedEntityId(ReportDictionaryName, "Admin", reportId)
SaveUsedEntityId(DocumentDictionaryName, "Admin", documentId)
ElseIf TypeOf report Is JohnReport Then
SaveUsedEntityId(ReportDictionaryName, "John", reportId)
SaveUsedEntityId(DocumentDictionaryName, "John", documentId)
SaveUsedEntityId(ReportDictionaryName, "Admin", reportId)
SaveUsedEntityId(DocumentDictionaryName, "Admin", documentId)
End If
End Sub
Public Overrides Sub BuildStarted(ByVal reportId As String, ByVal documentId As String, ByVal buildProperties As ReportBuildProperties)
If IsEntityAuthorized("Public", ReportDictionaryName, reportId) Then
SaveUsedEntityId(DocumentDictionaryName, "Public", documentId)
End If
If IsEntityAuthorized("Admin", ReportDictionaryName, reportId) Then
SaveUsedEntityId(DocumentDictionaryName, "Admin", documentId)
End If
If IsEntityAuthorized("John", ReportDictionaryName, reportId) Then
SaveUsedEntityId(DocumentDictionaryName, "John", documentId)
End If
End Sub
Public Overrides Function ExportDocumentStarting(ByVal documentId As String, ByVal asyncExportOperationId As String, ByVal format As String, ByVal options As ExportOptions, ByVal printingSystem As PrintingSystemBase, ByVal doExportSynchronously As Func(Of ExportedDocument)) As ExportedDocument
If Not IsEntityAuthorizedForCurrentUser(DocumentDictionaryName, documentId) Then Throw New UnauthorizedAccessException()
Return MyBase.ExportDocumentStarting(documentId, asyncExportOperationId, format, options, printingSystem, doExportSynchronously)
End Function
Public Overrides Sub ReleaseDocument(ByVal documentId As String)
End Sub
Private Function CanCreateDocument() As Boolean
Return True
End Function
Private Function CanCreateReport() As Boolean
Return True
End Function
Private Function CanReadDocument(ByVal documentId As String) As Boolean
Return IsEntityAuthorizedForCurrentUser(DocumentDictionaryName, documentId)
End Function
Private Function CanReadReport(ByVal reportId As String) As Boolean
Return IsEntityAuthorizedForCurrentUser(ReportDictionaryName, reportId)
End Function
Private Function CanReleaseDocument(ByVal documentId As String) As Boolean
Return IsEntityAuthorizedForCurrentUser(DocumentDictionaryName, documentId)
End Function
Private Function CanReleaseReport(ByVal reportId As String) As Boolean
Return IsEntityAuthorizedForCurrentUser(ReportDictionaryName, reportId)
End Function
Private Shared Function GetIdentityName() As String
Return HttpContext.Current.User?.Identity?.Name
End Function
Private Sub SaveUsedEntityId(ByVal dictionaryName As String, ByVal user As String, ByVal id As String)
If String.IsNullOrEmpty(id) Then Return
SyncLock authDictionary
authDictionary(user)(dictionaryName).Add(id)
End SyncLock
End Sub
Private Function IsEntityAuthorizedForCurrentUser(ByVal dictionaryName As String, ByVal id As String) As Boolean
Return IsEntityAuthorized(GetIdentityName(), dictionaryName, id)
End Function
Private Function IsEntityAuthorized(ByVal user As String, ByVal dictionaryName As String, ByVal id As String) As Boolean
If String.IsNullOrEmpty(id) Then Return False
SyncLock authDictionary
Return authDictionary("Public")(dictionaryName).Contains(id) OrElse Not String.IsNullOrEmpty(user) AndAlso authDictionary(user)(dictionaryName).Contains(id)
End SyncLock
End Function
Public Function CanReadExportedDocument(ByVal id As String) As Boolean
' For DevExpress.Report.Preview.AsyncExportApproach = true
Return IsEntityAuthorizedForCurrentUser(ExportedDocumentDictionaryName, id)
End Function
End Class
End Namespace
Register the operation logger in the Global.asax.cs or Global.asax.vb file:
DefaultWebDocumentViewerContainer.Register<WebDocumentViewerOperationLogger, OperationLogger>();
DefaultWebDocumentViewerContainer.Register(Of WebDocumentViewerOperationLogger, OperationLogger)()
The Report Designer component allows users to browse available data connections/tables via its integrated Query Builder. Refer to the following topic to restrict access to these connections/tables: Authorization Logic — Query Builder.