diff --git a/src/posts/RazorPagesAccessCheck.md b/src/posts/RazorPagesAccessCheck.md new file mode 100644 index 0000000..01c8b3d --- /dev/null +++ b/src/posts/RazorPagesAccessCheck.md @@ -0,0 +1,171 @@ +--- +title: Check If User Has Access To Razor Page +description: An extension method and tag helper for checking if the user has access to a Razor Page +tags: [ Programming, C#, ASP.NET Core, Razor Pages ] +--- + +These extension methods and tag helpers will check if the user has access to a Razor Page. This can be useful to only display links if the user has access to that page. For example only display a link to edit an item if the user has access to the edit page. + +## How To Use + +Call the method directly: + +```csharp +this.Url.HasPageAccess("/Things/View"); +``` + +Or use the tag helper: + +```cshtml + + @thingId + +``` + +`remove-if-unauthorized` will remove the link if the user doesn't have access to the page, and `preserve-content` will leave the text (the thing ID) when the link is removed. + +## Code + +The extension method: + +```csharp +public static class AspNetExtensions +{ + /// + /// Checks if the current user has the permissions to access the razor page at + /// + /// + /// + /// The view engine path to the razor page + /// + /// true if the user can access the page, false if they can't or if the page is + /// invalid + /// + public static async Task HasPageAccess(this IUrlHelper urlHelper, string pagePath) + { + // Get all of the necessary services + HttpContext httpContext = urlHelper.ActionContext.HttpContext; + EndpointDataSource endpointDataSource = httpContext.RequestServices + .GetRequiredService(); + IAuthorizationService authorizationService = httpContext.RequestServices + .GetRequiredService(); + IAuthorizationPolicyProvider policyProvider = httpContext.RequestServices + .GetRequiredService(); + + // The endpoint that is: + // 1) a razor page + // 2) the page path matches + Endpoint pageEndpoint = endpointDataSource.Endpoints + .Where(e => e.Metadata + .Where(m => m is PageActionDescriptor pad + && pad.ViewEnginePath.ToUpper() == pagePath.ToUpper()) + .Any()) + .FirstOrDefault(); + + if (pageEndpoint is null) + return false; + + // .AuthorizeFolder policies and AuthorizeAttributes get included in this + IList pageAuthorization = pageEndpoint.Metadata + .Select(m => m as IAuthorizeData) + .Where(m => m is not null) + .ToList(); + + AuthorizationPolicy pagePolicy = await AuthorizationPolicy + .CombineAsync(policyProvider, pageAuthorization); + + if (pagePolicy is null) + return true; + + return (await authorizationService.AuthorizeAsync(httpContext.User, pageEndpoint, pagePolicy)) + .Succeeded; + } +} +``` + +The tag helper: + +```csharp +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; + +[HtmlTargetElement("a", Attributes = "asp-page,remove-if-unauthorized")] +public class AuthorizedLinkTagHelper : AnchorTagHelper +{ + protected IUrlHelperFactory UrlHelperFactory { get; set; } + protected IUrlHelper Url { get => this.UrlHelperFactory.GetUrlHelper(this.ViewContext); } + + /// + /// If the current user doesn't have access to the linked page, then remove the link + /// + public bool RemoveIfUnauthorized { get; set; } = false; + /// + /// When true only remove the link itself (the opening and closing tags) and keep the + /// contents of the link + /// + public bool PreserveContent { get; set; } = false; + + public AuthorizedLinkTagHelper(IHtmlGenerator generator, + IUrlHelperFactory urlHelperFactory) : base(generator) + { + this.UrlHelperFactory = urlHelperFactory; + } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (!string.IsNullOrEmpty(this.Page)) + { + // This is a link to a razor page + + string pagePath = NormalizePagePath( + this.ViewContext.HttpContext.GetRouteData().Values["page"].ToString(), + this.Page); + + bool hasAccessToPage = await this.Url.HasPageAccess(pagePath); + + if (!hasAccessToPage && this.PreserveContent) + { + output.TagName = null; + return; + } + + if (!hasAccessToPage) + { + output.TagName = null; + output.Content.Clear(); + return; + } + } + else if (!string.IsNullOrEmpty(this.Controller) && !string.IsNullOrEmpty(this.Action)) + { + // TODO: This is a link to an MVC action + } + } + + /// Converts a relative Razor Page path to an absolute path + /// + /// The path to the current page, used to get the path prefix + /// + /// The page to get the path to + /// as an absolute path + private static string NormalizePagePath(string currentPagePath, string linkPath) + { + if (linkPath[0] == '/') + return linkPath; + + int index = currentPagePath.LastIndexOf('/'); + + if (index == currentPagePath.Length - 1) + { + // If the first ends in a trailing slash e.g. "/Home/", assume it's a directory. + return currentPagePath + linkPath; + } + + return string.Concat(currentPagePath.AsSpan(0, index + 1), linkPath); + + } +} +```