Back to Devexpress

Authorization Logic — Reports

aspnet-405157-security-considerations-authorization-reporting.md

latest27.0 KB
Original Source

Authorization Logic — Reports

  • Mar 06, 2025
  • 11 minutes to read

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

Implement Authorization Logic

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

cs
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";
        }
    }
}
vb
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:

aspx
<dx:ASPxReportDesigner ID="ASPxReportDesigner1" runat="server">
</dx:ASPxReportDesigner>
cs
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");
}
vb
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:

cs
DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension.RegisterExtensionGlobal(
    new ReportStorageWithAccessRules()
);
vb
DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension.RegisterExtensionGlobal(
    New ReportStorageWithAccessRules()
)

Implement Operation Logger for Document Viewer

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.

cs
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);
        }
    }
}
vb
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:

cs
DefaultWebDocumentViewerContainer.Register<WebDocumentViewerOperationLogger, OperationLogger>();
vb
DefaultWebDocumentViewerContainer.Register(Of WebDocumentViewerOperationLogger, OperationLogger)()

Restrict Access to Data Connections and Tables

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.

See Also