Skip to content
Open
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sdk": {
"version": "7.0.100",
"rollForward": "latestFeature",
"rollForward": "latestMajor",
"allowPrerelease": false
}
}
25 changes: 23 additions & 2 deletions samples/TagHelperPack.Sample/Pages/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
<li>Use the <code>template-name</code> attribute to override the template used to render the model.</li>
<li>Use the <code>html-field-name</code> attribute to override the HTML field name used to render the model.</li>
<li>Use <code>view-data-*</code> and <code>view-data</code> attributes to provide additional <code>ViewData</code> to the template.</li>
<li>Use <code>view-data-htmlAttributes</code> attribute to add properties to the <code>htmlAttributes</code> property in <code>ViewData</code>. This property can then be used as an argument to <code>Html helpers</code> in the template to render it in the final html.</li>
<li>Use <code>class</code> and <code>style</code> attributes to add additional css classes or inline styles to the <code>htmlAttributes</code> property in <code>ViewData</code>.</li>
<li>Use <code>id</code> attribute to add the id property to the <code>htmlAttributes</code> property in <code>ViewData</code>.</li>
</ul>
</p>
<p>
Expand All @@ -51,7 +54,7 @@
<li>Use the <code>asp-template-name</code> attribute to override the template used to render the model.</li>
<li>Use the <code>asp-html-field-name</code> attribute to override the HTML field name used to render the model.</li>
<li>Use <code>asp-view-data-*</code> and <code>asp-view-data</code> attributes to provide additional <code>ViewData</code> to the template.</li>
</ul>
</ul>
</p>
<p>
Use <code>&lt;datalist asp-list="..."&gt;</code> to render a <code>&lt;datalist&gt;</code> element containing <code>&lt;option&gt;</code> elements
Expand Down Expand Up @@ -79,6 +82,11 @@
<editor for="Customer.LastName" template-name="String2" />
<span asp-validation-for="Customer.LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Customer.LastName"></label>
<editor for="Customer.LastName" template-name="String3" id="myId" class="myClass" style="color: green" view-data-htmlAttributes='new {data_info = "some info"}' />
<span asp-validation-for="Customer.LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-display-name-for="Customer.LastName" for="CustomerLastName"></label>
<editor for="Customer.LastName" html-field-name="CustomerLastName" />
Expand Down Expand Up @@ -143,6 +151,11 @@
<strong>&lt;editor for="LastName" template-name="String2" /&gt;</strong>
&lt;span asp-validation-for="LastName" class="text-danger"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="form-group"&gt;
&lt;label asp-for="LastName"&gt;&lt;/label&gt;
<strong>&lt;editor for="LastName" template-name="String3" id="myId" class="myClass" style="color: green" view-data-htmlAttributes='new {data_info = "some info"}' /&gt;</strong>
&lt;span asp-validation-for="LastName" class="text-danger"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="form-group"&gt;
&lt;label <strong>asp-display-name-for="LastName"</strong> for="CustomerLastName"&gt;&lt;/label&gt;
<strong>&lt;editor for="LastName" html-field-name="CustomerLastName" /&gt;</strong>
Expand Down Expand Up @@ -327,13 +340,21 @@ public Customer Customer { get; set; }
<div asp-authz>This will only render if the user is authenticated.</div>
<div asp-authz="false">This will only render when the user is <strong>*not*</strong> authenticated.</div>
<div asp-authz-policy="AdminPolicy">This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.</div>
<div asp-authz-policy="PermissionPolicy" asp-authz-resource='"ViewUsers"'>This will only render if the user has "ViewUsers" permission.</div>
<div asp-authz-policy="PermissionPolicy" asp-authz-resource='"ManageUsers"'>This will only render if the user has "ManageUsers" permission.</div>
<div asp-authz-role="standard">This will only render if the user belongs to the "standard" role.</div>
<div asp-authz-role="admin">This will only render if the user belongs to the "admin" role.</div>
</div>
</div>
<h4>Source</h4>
<figure>
<pre>&lt;div asp-authz&gt;This will only render if the user is authenticated.&lt;/div&gt;
&lt;div asp-authz="false"&gt;This will only render when the user is &lt;strong&gt;*not*&lt;/strong&gt; authenticated.&lt;/div&gt;
&lt;div asp-authz-policy="AdminPolicy"&gt;This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.&lt;/div&gt;</pre>
&lt;div asp-authz-policy="AdminPolicy"&gt;This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.&lt;/div&gt;
&lt;div asp-authz-policy=&quot;PermissionPolicy&quot; asp-authz-resource='&quot;ViewUsers&quot;'&gt;This will only render if the user has &quot;ViewUsers&quot; permission.&lt;/div&gt;
&lt;div asp-authz-policy=&quot;PermissionPolicy&quot; asp-authz-resource='&quot;ManageUsers&quot;'&gt;This will only render if the user has &quot;ManageUsers&quot; permission.&lt;/div&gt;
&lt;div asp-authz-role=&quot;standard&quot;&gt;This will only render if the user belongs to the &quot;standard&quot; role.&lt;/div&gt;
&lt;div asp-authz-role=&quot;admin&quot;&gt;This will only render if the user belongs to the &quot;admin&quot; role.&lt;/div&gt;</pre>
</figure>

<h3>If Tag Helper</h3>
Expand Down
2 changes: 2 additions & 0 deletions samples/TagHelperPack.Sample/QueryAuthScheme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
identity.AddClaim(new Claim("Name", "AdminUser"));
identity.AddClaim(new Claim("IsAdmin", "true"));
identity.AddClaim(new Claim(ClaimTypes.Role, "admin"));
}
else
{
identity.AddClaim(new Claim("Name", "StandardUser"));
identity.AddClaim(new Claim(ClaimTypes.Role, "standard"));
}
var user = new ClaimsPrincipal(identity);
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, nameof(QueryAuthScheme))));
Expand Down
19 changes: 19 additions & 0 deletions samples/TagHelperPack.Sample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ public void ConfigureServices(IServiceCollection services)
return httpContext is not null;
});
});
options.AddPolicy("PermissionPolicy", policy =>
{
var standardPermissions = new List<string> { "ViewUsers" };
var adminPermissions = new List<string>(standardPermissions) { "ManageUsers" };

policy.RequireAuthenticatedUser();
policy.RequireAssertion(handler =>
{
var permission = handler.Resource as string;
if (handler.User.IsInRole("admin"))
{
return adminPermissions.Contains(permission);
}
else //standard role
{
return standardPermissions.Contains(permission);
}
});
});
});

// Optional optimizations to avoid Reflection
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@model string

@using Microsoft.AspNetCore.Mvc.ViewFeatures;

@{
//default html attributes added by the template
var defaultHtmlAttributesObject = new
{
Class = "form-control",
style= "font-weight: bold"
};

//merging the above html attributes with attributes received from the view
var viewHtmlAttributes = ViewData["htmlAttributes"] ?? new { };
var mergedHtmlAttributes = Html.MergeHtmlAttributesObjects(viewHtmlAttributes, defaultHtmlAttributesObject);
}

@Html.TextBox("", Model, mergedHtmlAttributes)
68 changes: 58 additions & 10 deletions src/TagHelperPack/AuthzTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ namespace TagHelperPack;
/// </summary>
[HtmlTargetElement("*", Attributes = AspAuthzAttributeName)]
[HtmlTargetElement("*", Attributes = AspAuthzPolicyAttributeName)]
[HtmlTargetElement("*", Attributes = AspAuthzRoleAttributeName)]
[HtmlTargetElement("*", Attributes = AspAuthzPolicyResourceAttributeName)]
public class AuthzTagHelper : TagHelper
{
internal static object SuppressedKey = new();
internal static object SuppressedValue = new();

private const string AspAuthzAttributeName = "asp-authz";
private const string AspAuthzPolicyAttributeName = "asp-authz-policy";
private const string AspAuthzRoleAttributeName = "asp-authz-role";
private const string AspAuthzPolicyResourceAttributeName = "asp-authz-resource";

private readonly IAuthorizationService _authz;

Expand Down Expand Up @@ -49,6 +53,18 @@ public AuthzTagHelper(IAuthorizationService authz)
[HtmlAttributeName(AspAuthzPolicyAttributeName)]
public string RequiredPolicy { get; set; }

/// <summary>
/// A role that the User must belong to in order for the current element to be rendered.
/// </summary>
[HtmlAttributeName(AspAuthzRoleAttributeName)]
public string RequiredRole { get; set; }

/// <summary>
/// An optional resource used by the authorization policy handler. <c>asp-authz-policy</c> should be set as well.
/// </summary>
[HtmlAttributeName(AspAuthzPolicyResourceAttributeName)]
public object RequiredPolicyResource { get; set; }

/// <summary>
/// Gets or sets the <see cref="ViewContext"/>.
/// </summary>
Expand All @@ -69,12 +85,22 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
throw new ArgumentNullException(nameof(output));
}

if (RequiredPolicyResource != null && string.IsNullOrEmpty(RequiredPolicy))
{
throw new ArgumentNullException(AspAuthzPolicyAttributeName);
}

if (!string.IsNullOrEmpty(RequiredRole) && !string.IsNullOrEmpty(RequiredPolicy))
{
throw new InvalidOperationException($"{AspAuthzRoleAttributeName} and {AspAuthzPolicyAttributeName} cannot be set at the same time.");
}

if (context.SuppressedByAspIf() || context.SuppressedByAspAuthz())
{
return;
}

var requiresAuth = RequiresAuthentication || !string.IsNullOrEmpty(RequiredPolicy);
var requiresAuth = RequiresAuthentication || !string.IsNullOrEmpty(RequiredPolicy) || !string.IsNullOrEmpty(RequiredRole);
var showOutput = false;

var user = ViewContext.HttpContext.User;
Expand All @@ -85,23 +111,45 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
}
else if (!string.IsNullOrEmpty(RequiredPolicy))
{
// auth-policy="foo" & user is authorized for policy "foo"
var cacheKey = AspAuthzPolicyAttributeName + "." + RequiredPolicy;
bool authorized;
var cachedResult = ViewContext.ViewData[cacheKey];
if (cachedResult != null)
if (RequiredPolicyResource != null)
{
authorized = (bool)cachedResult;
// auth-policy="foo" & user is authorized for policy "foo" based on permission
var cacheKey = AspAuthzPolicyResourceAttributeName + "." + RequiredPolicyResource;
var cachedResult = ViewContext.ViewData[cacheKey];
if (cachedResult != null)
{
authorized = (bool)cachedResult;
}
else
{
var authResult = await _authz.AuthorizeAsync(user, RequiredPolicyResource, RequiredPolicy);
authorized = authResult.Succeeded;
ViewContext.ViewData[cacheKey] = authorized;
}
}
else
{
var authResult = await _authz.AuthorizeAsync(user, ViewContext, RequiredPolicy);
authorized = authResult.Succeeded;
ViewContext.ViewData[cacheKey] = authorized;
// auth-policy="foo" & user is authorized for policy "foo"
var cacheKey = AspAuthzPolicyAttributeName + "." + RequiredPolicy;
var cachedResult = ViewContext.ViewData[cacheKey];
if (cachedResult != null)
{
authorized = (bool)cachedResult;
}
else
{
var authResult = await _authz.AuthorizeAsync(user, ViewContext, RequiredPolicy);
authorized = authResult.Succeeded;
ViewContext.ViewData[cacheKey] = authorized;
}
}

showOutput = authorized;
}
else if (!string.IsNullOrEmpty(RequiredRole))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a guard to ensure invalid combinations of attributes aren't being set and throw an InvalidOperationException in that case, e.g. if both a required role and policy are set.

Copy link
Author

@MSingh-13 MSingh-13 Mar 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As suggested, I have added a condition to guard against RequiredRoles and RequiredPolicy being set at the same time.
See commit fbdab70

{
showOutput = user.IsInRole(RequiredRole);
}
else if (requiresAuth && user.Identity.IsAuthenticated)
{
// auth="true" & user is authenticated
Expand Down
42 changes: 42 additions & 0 deletions src/TagHelperPack/DisplayTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ public IDictionary<string, object> ViewData
[ViewContext]
public ViewContext ViewContext { get; set; }

/// <summary>
/// CSS class name.
/// </summary>
[HtmlAttributeName("class")]
public string Class { get; set; }

/// <summary>
/// CSS inline style.
/// </summary>
[HtmlAttributeName("style")]
public string Style { get; set; }

/// <summary>
/// HTML id.
/// </summary>
[HtmlAttributeName("id")]
public string Id { get; set; }

/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
Expand All @@ -82,6 +100,30 @@ public override void Process(TagHelperContext context, TagHelperOutput output)

((IViewContextAware)_htmlHelper).Contextualize(ViewContext);

//create a local htmlAttributes dictionary for html attributes exposed by the tag helper
IDictionary<string, object> htmlAttributes = new Dictionary<string, object>();
if (!string.IsNullOrWhiteSpace(Id))
{
htmlAttributes["id"] = Id;
}
if (!string.IsNullOrWhiteSpace(Class))
{
htmlAttributes["class"] = Class;
}
if (!string.IsNullOrWhiteSpace(Style))
{
htmlAttributes["style"] = Style;
}

//get the htmlAttributes property from tag ViewData
ViewData.TryGetValue("htmlAttributes", out var viewDataHtmlAttributes);

//merging local and ViewData htmlAttributes properties
htmlAttributes = _htmlHelper.MergeHtmlAttributesObjects(htmlAttributes, viewDataHtmlAttributes);

//setting the merged htmlAttributes on the ViewData of the tag.
ViewData["htmlAttributes"] = htmlAttributes;

output.Content.SetHtmlContent(_htmlHelper.Display(For, HtmlFieldName, TemplateName, ViewData));

output.TagName = null;
Expand Down
46 changes: 44 additions & 2 deletions src/TagHelperPack/EditorTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,31 @@ public IDictionary<string, object> ViewData
set => _viewData = value;
}

/// <summary>
/// <summary>
/// Gets or sets the <see cref="ViewContext"/>.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }

/// <summary>
/// CSS class name.
/// </summary>
[HtmlAttributeName("class")]
public string Class { get; set; }

/// <summary>
/// CSS inline style.
/// </summary>
[HtmlAttributeName("style")]
public string Style { get; set; }

/// <summary>
/// HTML id.
/// </summary>
[HtmlAttributeName("id")]
public string Id { get; set; }

/// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output)
{
Expand All @@ -82,8 +100,32 @@ public override void Process(TagHelperContext context, TagHelperOutput output)

((IViewContextAware)_htmlHelper).Contextualize(ViewContext);

//create a local htmlAttributes dictionary for html attributes exposed by the tag helper
IDictionary<string, object> htmlAttributes = new Dictionary<string, object>();
if (!string.IsNullOrWhiteSpace(Id))
{
htmlAttributes["id"] = Id;
}
if (!string.IsNullOrWhiteSpace(Class))
{
htmlAttributes["class"] = Class;
}
if (!string.IsNullOrWhiteSpace(Style))
{
htmlAttributes["style"] = Style;
}

//get the htmlAttributes property from tag ViewData
ViewData.TryGetValue("htmlAttributes", out var viewDataHtmlAttributes);

//merging local and ViewData htmlAttributes properties
htmlAttributes = _htmlHelper.MergeHtmlAttributesObjects(htmlAttributes, viewDataHtmlAttributes);

//setting the merged htmlAttributes on the ViewData of the tag.
ViewData["htmlAttributes"] = htmlAttributes;

output.Content.SetHtmlContent(_htmlHelper.Editor(For, HtmlFieldName, TemplateName, ViewData));

output.TagName = null;
}
}
}
9 changes: 6 additions & 3 deletions src/TagHelperPack/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Routing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using TagHelperPack;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
Expand Down
Loading