Skip to content
This repository has been archived by the owner on Oct 29, 2024. It is now read-only.

Commit

Permalink
Add impersonation / transient grant support during report callback ex…
Browse files Browse the repository at this point in the history
…ecution based on current user / transient granting status
  • Loading branch information
volkanceylan committed May 4, 2024
1 parent 003e607 commit b90860b
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using System.Net;

namespace Serenity.Reporting;
Expand All @@ -10,12 +12,15 @@ namespace Serenity.Reporting;
public class HtmlReportCallbackUrlBuilder(
ISiteAbsoluteUrl siteAbsoluteUrl,
IOptionsMonitor<CookieAuthenticationOptions> cookieOptions = null,
IHttpContextAccessor httpContextAccessor = null) : IHtmlReportCallbackUrlBuilder
IPermissionService permissionService = null,
IUserAccessor userAccessor = null,
IHttpContextAccessor httpContextAccessor = null,
IDataProtectionProvider dataProtectionProvider = null) : IHtmlReportCallbackUrlBuilder
{
protected readonly IOptionsMonitor<CookieAuthenticationOptions> cookieOptions = cookieOptions;
protected readonly IHttpContextAccessor httpContextAccessor = httpContextAccessor;
protected readonly ISiteAbsoluteUrl siteAbsoluteUrl = siteAbsoluteUrl ?? throw new ArgumentNullException(nameof(siteAbsoluteUrl));

internal const string ReportAuthCookieName = ".ReportAuth";

protected virtual string GetRenderAction(IReport report)
{
return "Serenity.Extensions/Report/Render";
Expand Down Expand Up @@ -52,7 +57,7 @@ protected virtual string GetLanguageCookieName()

private static int ParseChunksCount(string value)
{
if (value != null &&
if (value != null &&
value.StartsWith(ChunkCountPrefix, StringComparison.Ordinal) &&
int.TryParse(value.AsSpan(ChunkCountPrefix.Length), NumberStyles.None, CultureInfo.InvariantCulture, out var chunksCount))
return chunksCount;
Expand All @@ -62,6 +67,37 @@ private static int ParseChunksCount(string value)

protected virtual IEnumerable<Cookie> GetCookiesToForward()
{
if (dataProtectionProvider != null)
{
var transientGrantor = permissionService as ITransientGrantor;
var username = userAccessor?.User?.Identity?.Name;
var isAllGranted = transientGrantor?.IsAllGranted() ?? false;
var granted = transientGrantor?.GetGranted() ?? [];

if (!string.IsNullOrEmpty(username) ||
isAllGranted ||
granted.Any())
{
byte[] bytes;
using var ms = new System.IO.MemoryStream();
using var bw = new System.IO.BinaryWriter(ms);
bw.Write(DateTime.UtcNow.AddMinutes(5).ToBinary());
bw.Write(username ?? "");
bw.Write(isAllGranted ? -1 : granted.Count());
if (!isAllGranted)
{
foreach (var p in granted)
bw.Write(p);
}
bw.Flush();
bytes = ms.ToArray();

var protector = dataProtectionProvider.CreateProtector(ReportAuthCookieName);
var token = WebEncoders.Base64UrlEncode(protector.Protect(bytes));
yield return new Cookie(ReportAuthCookieName, token);
}
}

var request = httpContextAccessor?.HttpContext?.Request;
if (request is null)
yield break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;

namespace Serenity.Reporting;

/// <summary>
/// Implementation for <see cref="IReportExecutor" /> that uses callback report cookie
/// to impersonate / transient grant permissions
/// </summary>
public class HtmlReportCallbackUrlInterceptor(
ILogger<HtmlReportCallbackUrlBuilder> logger,
IPermissionService permissionService = null,
IUserAccessor userAccessor = null,
IUserClaimCreator userClaimCreator = null,
IHttpContextAccessor httpContextAccessor = null,
IDataProtectionProvider dataProtectionProvider = null) : IReportCallbackInterceptor
{
public ReportRenderResult InterceptCallback(ReportRenderOptions options, Func<ReportRenderOptions, ReportRenderResult> action)
{
IImpersonator impersonator = userAccessor as IImpersonator;
ITransientGrantor transientGrantor = permissionService as ITransientGrantor;
bool undoImpersonate = false;
bool undoGrant = false;
try
{
try
{
if (dataProtectionProvider != null &&
(impersonator != null || transientGrantor != null) &&
httpContextAccessor?.HttpContext?.Request?.Cookies?.TryGetValue(
HtmlReportCallbackUrlBuilder.ReportAuthCookieName, out var token) == true &&
!string.IsNullOrEmpty(token))
{
var protector = dataProtectionProvider.CreateProtector(HtmlReportCallbackUrlBuilder.ReportAuthCookieName);
var tokenBytes = WebEncoders.Base64UrlDecode(token);
var ticket = protector.Unprotect(tokenBytes);

using var ms = new System.IO.MemoryStream(ticket);
using var br = new System.IO.BinaryReader(ms);
var dt = DateTime.FromBinary(br.ReadInt64());
if (dt > DateTime.UtcNow)
{
var username = br.ReadString();
if (impersonator != null &&
!string.IsNullOrEmpty(username) &&
userClaimCreator != null &&
userAccessor?.User?.Identity?.Name != username)
{
var principal = userClaimCreator.CreatePrincipal(username, "ReportImpersonation");
impersonator.Impersonate(principal);
undoImpersonate = true;
}

if (transientGrantor != null)
{
var count = br.ReadInt32();
if (count == -1)
{
transientGrantor.GrantAll();
undoGrant = true;
}
else if (count > 0 && count < 10000)
{
var perms = new string[count];
for (var i = 0; i < count; i++)
perms[i] = br.ReadString();
transientGrantor.Grant(perms);
undoGrant = true;
}
}
}
}
}
catch (Exception ex)
{
// ignore errors while decrypting / deserializing / applying ticket
logger.LogError(ex, "Error decrypting/applying report auth ticket");
}

return action(options);
}
finally
{
if (undoImpersonate)
impersonator.UndoImpersonate();
if (undoGrant)
transientGrantor.UndoGrant();
}
}
}
41 changes: 19 additions & 22 deletions src/Serenity.Extensions/Modules/Reporting/ReportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ namespace Serenity.Extensions.Pages;

[Route("Serenity.Extensions/Report/[action]")]
public class ReportController(IReportFactory reportFactory,
IReportRenderer reportRenderer) : Controller
IReportRenderer reportRenderer,
IReportCallbackInterceptor callbackInterceptor = null) : Controller
{
protected readonly IReportFactory reportFactory = reportFactory ?? throw new ArgumentNullException(nameof(reportFactory));
private readonly IReportRenderer reportRenderer = reportRenderer ?? throw new ArgumentNullException(nameof(reportRenderer));
protected readonly IReportRenderer reportRenderer = reportRenderer ?? throw new ArgumentNullException(nameof(reportRenderer));

public ActionResult Render(string key, string opt, string ext, int? print = 0)
{
Expand All @@ -24,14 +25,22 @@ public ActionResult Download(string key, string opt, string ext)

private ActionResult Execute(string key, string opt, string ext, bool download, bool printing)
{
var report = reportFactory.Create(key, opt, validatePermission: true);
var result = reportRenderer.Render(report, new ReportRenderOptions
var options = new ReportRenderOptions
{
ExportFormat = ext,
PreviewMode = !download && !printing,
ReportKey = key,
ReportParams = opt,
});
ReportParams = opt
};

ReportRenderResult callback(ReportRenderOptions options)
{
var report = reportFactory.Create(options.ReportKey, options.ReportParams, validatePermission: true);
return reportRenderer.Render(report, options);
}

var result = callbackInterceptor != null ?
callbackInterceptor.InterceptCallback(options, callback) : callback(options);

if (!string.IsNullOrEmpty(result.RedirectUri))
return Redirect(result.RedirectUri);
Expand All @@ -43,29 +52,17 @@ private ActionResult Execute(string key, string opt, string ext, bool download,
return View(viewName: result.ViewName, model: result.Model);
}

var downloadName = GetDownloadNameFor(report, result.FileExtension);
var downloadName = (string.IsNullOrEmpty(result.FileName) ?
("Report_" + DateTime.Now.ToString("yyyyMMdd_HHss", CultureInfo.InvariantCulture))
: result.FileName) + result.FileExtension;

Response.Headers[HeaderNames.ContentDisposition] = $"{(download ? "attachment" : "inline")};filename=" +
WebUtility.UrlEncode(downloadName);

return File(result.ContentBytes, result.MimeType ??
KnownMimeTypes.Get("_" + result.FileExtension));
}

public static string GetDownloadNameFor(IReport report, string extension)
{
if (report is ICustomFileName customFileName)
return customFileName.GetFileName() + extension;
else
{
var filePrefix = report.GetType().GetAttribute<DisplayNameAttribute>()?.DisplayName ??
report.GetType().GetAttribute<ReportAttribute>()?.ReportKey ??
report.GetType().Name;

return filePrefix + "_" +
DateTime.Now.ToString("yyyyMMdd_HHss", CultureInfo.InvariantCulture) + extension;
}
}

[HttpPost, JsonRequest]
public ActionResult Retrieve(ReportRetrieveRequest request,
[FromServices] IReportRetrieveHandler handler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static void AddHtmlToPdf(this IServiceCollection services)
services.TryAddSingleton<ISiteAbsoluteUrl, SiteAbsoluteUrl>();
services.TryAddSingleton<IHtmlReportCallbackUrlBuilder, HtmlReportCallbackUrlBuilder>();
services.TryAddSingleton<IHtmlReportRenderUrlBuilder, HtmlReportCallbackUrlBuilder>();
services.TryAddSingleton<IReportCallbackInterceptor, HtmlReportCallbackUrlInterceptor>();
services.TryAddSingleton<IWKHtmlToPdfConverter, WKHtmlToPdfConverter>();
services.TryAddSingleton<IHtmlToPdfConverter, WKHtmlToPdfConverter>();
services.TryAddSingleton<IHtmlReportPdfRenderer, HtmlReportPdfRenderer>();
Expand Down

0 comments on commit b90860b

Please sign in to comment.