Authorization Logic — Reports
- 6 minutes to read
Use strategies outlined in this topic to implement authorization logic for both the DevExpress ASP.NET MVC Document Viewer and Report Designer (and address CWE-285-related security risks).
#Apply Authorization Attributes
The Authorize attribute specifies authorization rules for application pages. To protect your application, apply the Authorize attribute to the controller class. Use the AllowAnonymous attribute to allow anonymous access to public actions:
namespace SecurityBestPractices.Mvc.Controllers {
[Authorize]
public partial class AuthorizationController : Controller {
[AllowAnonymous]
public ActionResult PublicReport() {
return View("Reports/PublicReport");
}
// ...
}
}
#Implement Authorization Logic
Create a custom report storage derived from the ReportStorageWebExtension class to implement authorization logic for DevExpress Document Viewer and Report Designer extensions. As a starting point, you can use the following ReportStorageWithAccessRules
class implementation and modify it based on your requirements:
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 here
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 here. 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 here. Refer to the following topic for
// more information and examples: https://docs.devexpress.com/XtraReports/17553
return "New name";
}
}
}
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 overriddenGetData
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 overriddenIsValidUrl
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 controller and redirect unauthorized users to a public view:
public ActionResult ReportDesigner(string name) {
var reportNames = ReportStorageWithAccessRules.GetEditableReportNamesForCurrentUser();
if(reportNames.Contains(name))
return View("Reports/ReportDesigner", new ReportNameModel() { ReportName = name });
else
return RedirectToAction("ReportViewer", "Authorization");
}
Once you implement your custom report storage, register it in the Global.asax.cs file:
DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension.RegisterExtensionGlobal(
new ReportStorageWithAccessRules()
);
#Implement Operation Logger for Document Viewer
The Document Viewer extension keeps 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:
Note
For demonstration purposes, the Operation
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);
}
}
}
Register the operation logger in the Global.asax.cs file:
DefaultWebDocumentViewerContainer.Register<WebDocumentViewerOperationLogger, OperationLogger>();
#Restrict Access to Data Connections and Tables
The Report Designer component allows users to browse available data connections/tables in its integrated Query Builder. Refer to the following topic to restrict access to these connections/tables: Authorization Logic — Query Builder.