diff --git a/global.json b/global.json
index 8cce0fe..30e2afc 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
"version": "7.0.100",
- "rollForward": "latestFeature",
+ "rollForward": "latestMajor",
"allowPrerelease": false
}
}
\ No newline at end of file
diff --git a/samples/TagHelperPack.Sample/Pages/Index.cshtml b/samples/TagHelperPack.Sample/Pages/Index.cshtml
index fee5589..fa3933a 100644
--- a/samples/TagHelperPack.Sample/Pages/Index.cshtml
+++ b/samples/TagHelperPack.Sample/Pages/Index.cshtml
@@ -42,6 +42,9 @@
Use the template-name attribute to override the template used to render the model.
Use the html-field-name attribute to override the HTML field name used to render the model.
Use view-data-* and view-data attributes to provide additional ViewData to the template.
+
Use view-data-htmlAttributes attribute to add properties to the htmlAttributes property in ViewData. This property can then be used as an argument to Html helpers in the template to render it in the final html.
+
Use class and style attributes to add additional css classes or inline styles to the htmlAttributes property in ViewData.
+
Use id attribute to add the id property to the htmlAttributes property in ViewData.
@@ -51,7 +54,7 @@
Use the asp-template-name attribute to override the template used to render the model.
Use the asp-html-field-name attribute to override the HTML field name used to render the model.
Use asp-view-data-* and asp-view-data attributes to provide additional ViewData to the template.
-
+
Use <datalist asp-list="..."> to render a <datalist> element containing <option> elements
@@ -79,6 +82,11 @@
+
This will only render if the user is authenticated.
This will only render when the user is *not* authenticated.
This will only render if the user is authenticated and authorized via the "AdminPolicy" policy.
+
This will only render if the user has "ViewUsers" permission.
+
This will only render if the user has "ManageUsers" permission.
+
This will only render if the user belongs to the "standard" role.
+
This will only render if the user belongs to the "admin" role.
Source
<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="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>
If Tag Helper
diff --git a/samples/TagHelperPack.Sample/QueryAuthScheme.cs b/samples/TagHelperPack.Sample/QueryAuthScheme.cs
index cd77266..1d5f876 100644
--- a/samples/TagHelperPack.Sample/QueryAuthScheme.cs
+++ b/samples/TagHelperPack.Sample/QueryAuthScheme.cs
@@ -28,10 +28,12 @@ protected override Task 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))));
diff --git a/samples/TagHelperPack.Sample/Startup.cs b/samples/TagHelperPack.Sample/Startup.cs
index 407937a..0fac120 100644
--- a/samples/TagHelperPack.Sample/Startup.cs
+++ b/samples/TagHelperPack.Sample/Startup.cs
@@ -53,6 +53,25 @@ public void ConfigureServices(IServiceCollection services)
return httpContext is not null;
});
});
+ options.AddPolicy("PermissionPolicy", policy =>
+ {
+ var standardPermissions = new List { "ViewUsers" };
+ var adminPermissions = new List(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
diff --git a/samples/TagHelperPack.Sample/Views/Shared/EditorTemplates/String3.cshtml b/samples/TagHelperPack.Sample/Views/Shared/EditorTemplates/String3.cshtml
new file mode 100644
index 0000000..1b9fcb4
--- /dev/null
+++ b/samples/TagHelperPack.Sample/Views/Shared/EditorTemplates/String3.cshtml
@@ -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)
diff --git a/src/TagHelperPack/AuthzTagHelper.cs b/src/TagHelperPack/AuthzTagHelper.cs
index fd83588..68cbd25 100644
--- a/src/TagHelperPack/AuthzTagHelper.cs
+++ b/src/TagHelperPack/AuthzTagHelper.cs
@@ -12,6 +12,8 @@ namespace TagHelperPack;
///
[HtmlTargetElement("*", Attributes = AspAuthzAttributeName)]
[HtmlTargetElement("*", Attributes = AspAuthzPolicyAttributeName)]
+[HtmlTargetElement("*", Attributes = AspAuthzRoleAttributeName)]
+[HtmlTargetElement("*", Attributes = AspAuthzPolicyResourceAttributeName)]
public class AuthzTagHelper : TagHelper
{
internal static object SuppressedKey = new();
@@ -19,6 +21,8 @@ public class AuthzTagHelper : TagHelper
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;
@@ -49,6 +53,18 @@ public AuthzTagHelper(IAuthorizationService authz)
[HtmlAttributeName(AspAuthzPolicyAttributeName)]
public string RequiredPolicy { get; set; }
+ ///
+ /// A role that the User must belong to in order for the current element to be rendered.
+ ///
+ [HtmlAttributeName(AspAuthzRoleAttributeName)]
+ public string RequiredRole { get; set; }
+
+ ///
+ /// An optional resource used by the authorization policy handler. asp-authz-policy should be set as well.
+ ///
+ [HtmlAttributeName(AspAuthzPolicyResourceAttributeName)]
+ public object RequiredPolicyResource { get; set; }
+
///
/// Gets or sets the .
///
@@ -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;
@@ -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))
+ {
+ showOutput = user.IsInRole(RequiredRole);
+ }
else if (requiresAuth && user.Identity.IsAuthenticated)
{
// auth="true" & user is authenticated
diff --git a/src/TagHelperPack/DisplayTagHelper.cs b/src/TagHelperPack/DisplayTagHelper.cs
index 9265fec..7b8ee33 100644
--- a/src/TagHelperPack/DisplayTagHelper.cs
+++ b/src/TagHelperPack/DisplayTagHelper.cs
@@ -62,6 +62,24 @@ public IDictionary ViewData
[ViewContext]
public ViewContext ViewContext { get; set; }
+ ///
+ /// CSS class name.
+ ///
+ [HtmlAttributeName("class")]
+ public string Class { get; set; }
+
+ ///
+ /// CSS inline style.
+ ///
+ [HtmlAttributeName("style")]
+ public string Style { get; set; }
+
+ ///
+ /// HTML id.
+ ///
+ [HtmlAttributeName("id")]
+ public string Id { get; set; }
+
///
public override void Process(TagHelperContext context, TagHelperOutput output)
{
@@ -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 htmlAttributes = new Dictionary();
+ 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;
diff --git a/src/TagHelperPack/EditorTagHelper.cs b/src/TagHelperPack/EditorTagHelper.cs
index 5784f41..07c4878 100644
--- a/src/TagHelperPack/EditorTagHelper.cs
+++ b/src/TagHelperPack/EditorTagHelper.cs
@@ -55,13 +55,31 @@ public IDictionary ViewData
set => _viewData = value;
}
- ///
+ ///
/// Gets or sets the .
///
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
+ ///
+ /// CSS class name.
+ ///
+ [HtmlAttributeName("class")]
+ public string Class { get; set; }
+
+ ///
+ /// CSS inline style.
+ ///
+ [HtmlAttributeName("style")]
+ public string Style { get; set; }
+
+ ///
+ /// HTML id.
+ ///
+ [HtmlAttributeName("id")]
+ public string Id { get; set; }
+
///
public override void Process(TagHelperContext context, TagHelperOutput output)
{
@@ -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 htmlAttributes = new Dictionary();
+ 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;
}
-}
+}
\ No newline at end of file
diff --git a/src/TagHelperPack/HtmlHelperExtensions.cs b/src/TagHelperPack/HtmlHelperExtensions.cs
index 65d440d..2b076c2 100644
--- a/src/TagHelperPack/HtmlHelperExtensions.cs
+++ b/src/TagHelperPack/HtmlHelperExtensions.cs
@@ -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;
diff --git a/src/TagHelperPack/PublicHtmlHelperExtensions.cs b/src/TagHelperPack/PublicHtmlHelperExtensions.cs
new file mode 100644
index 0000000..babefcd
--- /dev/null
+++ b/src/TagHelperPack/PublicHtmlHelperExtensions.cs
@@ -0,0 +1,48 @@
+using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Routing;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using TagHelperPack;
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
+
+public static class PublicHtmlHelperExtensions
+{
+ private static IDictionary htmlAttributesWithSeparators = new Dictionary()
+ {
+ { "class", " "},
+ { "style", "; " },
+ };
+
+ ///
+ /// Merge values from 2 anonymous or IDictionary objects. Values of overlapping keys from the 'existing values' object are replaced by the values
+ /// from the 'new values' object except if the keys are 'class' or 'style', in which case the values are concatentated with a space or ; respectively.
+ ///
+ /// new values
+ /// existing values
+ ///
+ public static IDictionary MergeHtmlAttributesObjects(this IHtmlHelper helper, object newHtmlAttributesObject, object existingHtmlAttributesObject)
+ {
+ IDictionary newHtmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(newHtmlAttributesObject);
+ IDictionary existingHtmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(existingHtmlAttributesObject);
+
+ foreach (var item in newHtmlAttributes)
+ {
+ htmlAttributesWithSeparators.TryGetValue(item.Key, out string separator);
+
+ existingHtmlAttributes.TryGetValue(item.Key, out object? value);
+
+ if(item.Value != null)
+ {
+ existingHtmlAttributes[item.Key] = value != null && !string.IsNullOrEmpty(separator) ?
+ string.Format("{0}{1}{2}", existingHtmlAttributes[item.Key], separator, item.Value)
+ : item.Value;
+ }
+ }
+
+ return existingHtmlAttributes;
+ }
+}