MVC 3 - Limiting sections and/or controls of a View by Roles/Functions - asp.net-mvc-3

I will have many Roles, and each Role has many functions, so the RequireRoles Attribute I don't think will suffice in my case. I need some way to dynamically let the Controller action define to the View what sections and/or controls in the View (without adding if/else logic inside the View).
My thought is that the Controller should be telling the View how to present itself and not the View with the if/else logic.
Any ideas on how to design this ?

You need to first of all create a filter which you can use an attribute to control what roles see what actions. See http://www.asp.net/mvc/tutorials/older-versions/controllers-and-routing/understanding-action-filters-cs.
public class RequiresRoleAttribute : ActionFilterAttribute {
private List<string> requiredRoles = null;
/// <summary>
/// Initializes a new instance of the <see cref="RequiresRoleAttribute"/> class.
/// </summary>
/// <param name="roleNames">The role names.</param>
public RequiresRoleAttribute(params string[] roleNames) {
this.requiredRoles = new List<string>(roleNames);
}
/// <summary>
/// Called by the MVC framework before the action method executes.
/// </summary>
/// <param name="filterContext">The filter context.</param>
public override void OnActionExecuting(ActionExecutingContext filterContext) {
bool hasRole = false;
// check to see if the user has the proper role here
// if the do not have the role, they are not allowed to execute the action
if (!hasRole)
throw new UserAccessException("You do not have access to this action (" + filterContext.ActionDescriptor.ActionName + ", " + filterContext.ActionDescriptor.ControllerDescriptor.ControllerName + ")");
base.OnActionExecuting(filterContext);
}
}
Second to solve your problem of no logic in the views you could use child actions for each section which requires a role. Again you could apply your filter to the child actions. For more on child actions see: http://msdn.microsoft.com/en-us/library/ie/ee839451.aspx.
What you would need to change is the section that throws the exception. You'd need to check to see if the action being executed is a child action. If so, you'd want to return an empty content result.

Related

How to declaritively specify authorization policy based on HTTP verb and other attributes in ASP.Net Core (WebAPI)

I've got a couple of authorization polcies registered:
ConfigureServices()
{
services.AddAuthorization(authorisationOptions =>
{
authorisationOptions.AddPolicy(StandardAuthorizationPolicy.Name, StandardAuthorizationPolicy.Value);
authorisationOptions.AddPolicy(MutatingActionAuthorizationPolicy.Name, MutatingActionAuthorizationPolicy.Value);
});
}
& then I set a default authorization policy across all endpoints:
Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoints =>
{
endpoints
.MapControllers()
.RequireAuthorization(StandardAuthorizationPolicy.Name); // Declaratively require the standard authorization policy on all controller endpoints
});
}
On the endpoints where I want to specify the mutating policy, I currently do the following:
[HttpPut]
[Authorize(MutatingActionAuthorizationPolicy.Name)] // Because 'PUT'. NOT DECLARATIVE! :-(
public async Task<IActionResult> AddOrUpdateOverride(SourceOverride sourceOverride, CancellationToken cancellationToken)
{
// ..
}
What I really want is a bit more control to declaritively apply the mutating policy based on the HttpVerb (i.e. POST, PUT, PATCH, DELETE).
Any idea on how to achieve that? Bonus points for allowing me to use other attributes on the controller method/class, not just [HttpPost] etc.
NB: I've seen solutions floating around that involve casting the content (and seem to revolve around a single access policy). I'd really rather stick with multiple access policies.
If I get stuck, I might end up writing a convention test for it.
You can implement a custom RequireAuthorization extension that takes HTTP verb filtering function as an argument and checks each endpoint metadata for HttpMethodAttribute
public static class AuthorizationEndpointConventionBuilderExtensions
{
/// <summary>
/// Adds authorization policies with the specified <see cref="IAuthorizeData"/> to the endpoint(s) filtered by supplied filter function
/// </summary>
/// <param name="builder">The endpoint convention builder.</param>
/// <param name="filterOnHttpMethods">Filters http methods that we applying specific policies to</param>
/// <param name="authorizeData">
/// A collection of <paramref name="authorizeData"/>. If empty, the default authorization policy will be used.
/// </param>
/// <returns>The original convention builder parameter.</returns>
public static TBuilder RequireAuthorizationForHttpMethods<TBuilder>(this TBuilder builder, Func<IEnumerable<HttpMethod>, bool> filterOnHttpMethods, params IAuthorizeData[] authorizeData)
where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (authorizeData == null)
{
throw new ArgumentNullException(nameof(authorizeData));
}
if (authorizeData.Length == 0)
{
authorizeData = new IAuthorizeData[] { new AuthorizeAttribute(), };
}
builder.Add(endpointBuilder =>
{
var appliedHttpMethodAttributes = endpointBuilder.Metadata
.Where(x => x is HttpMethodAttribute)
.Cast<HttpMethodAttribute>();
if (appliedHttpMethodAttributes.Any(x => filterOnHttpMethods(x.HttpMethods
.Select(method => new HttpMethod(method)))))
{
foreach (var data in authorizeData)
{
endpointBuilder.Metadata.Add(data);
}
}
});
return builder;
}
/// <summary>
/// Adds authorization policies with the specified names to the endpoint(s) for filtered endpoints that return for filterOnHttpMethod
/// </summary>
/// <param name="builder">The endpoint convention builder.</param>
/// <param name="filterOnHttpMethods">Filters http methods that we applying specific policies to</param>
/// <param name="policyNames">A collection of policy names. If empty, the default authorization policy will be used.</param>
/// <returns>The original convention builder parameter.</returns>
public static TBuilder RequireAuthorizationForHttpMethods<TBuilder>(this TBuilder builder, Func<IEnumerable<HttpMethod>, bool> filterOnHttpMethods, params string[] policyNames)
where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (policyNames == null)
{
throw new ArgumentNullException(nameof(policyNames));
}
return builder.RequireAuthorizationForHttpMethods(filterOnHttpMethods, policyNames.Select(n => new AuthorizeAttribute(n)).ToArray());
}
}
And then use this extension next to the original one:
app.UseEndpoints(endpoints =>
{
var mutatingHttpMethods = new HashSet<HttpMethod>()
{
HttpMethod.Post,
HttpMethod.Put,
HttpMethod.Delete
};
endpoints
.MapControllers()
.RequireAuthorization(StandardAuthorizationPolicy.Name)
.RequireAuthorizationForHttpMethods(httpMethods =>
httpMethods.Any(httpMethod => mutatingHttpMethods.Contains(httpMethod)),
MutatingActionAuthorizationPolicy.Name);
});
}

One controller's actions not appearing in help docs

I have an MVC Web API project which is all working fine, but for some reason the entries in the help docs for just one of the controllers does not appear.
This was all fine until just recently, unfortunately though I don't know at what point it disappeared but it definitely used to be there.
The XML comments all look ok.
The XmlDocument.xml file looks correct.
Is there a way to specify which controllers and methods feature in the help docs?
How can I make sure the actions for this controller appear?
In case it helps, this is a snip with the first action:
public class UserController : ApiController
{
/// <summary>
/// Get details of a user or all users, including accounts and group memberships
/// </summary>
/// <param name="username">The name of the user, if a single result is required</param>
/// <param name="account">The account id, if multiple results are required</param>
/// <param name="offset">The first row to return</param>
/// <param name="limit">The maximum number of rows to return</param>
/// <param name="sortby">The column to sort by, if required</param>
/// <param name="order">The sort order; asc[ending] (default) or desc[ending]</param>
/// <returns>Response structure including status and error message (if appropriate), as well as User structure including account and group details</returns>
[Route("user")]
[CombinedAuthentication(AuthLevel = "2")]
[HttpGet]
[AcceptVerbs("GET")]
public UserGetResponse Get(string username = "", int account = 0, int offset = 0, int limit = 0, string sortby = "", string order = "")
{
if (!string.IsNullOrEmpty(username))
{
return new UserGetResponse(username);
}
else
{
return new UserGetResponse(account, offset, limit, sortby, order);
}
}
}
This bit stops the help docs entry being generated:
[AcceptVerbs("GET")]
I removed that and all was fine!

List event receiver to record date, when the workflow status on the item has been updated??

Here is the code I have to record the date when the workflow status on an item has been updated/changed. I created accustom column named Completed Date as a Date type in the list to display the date.
The workflow deploys fine but does not render any data under the Completed Date Column. Am I missing something?
namespace WorkflowDateRecorder.EventReceiver1
{
/// <summary>
/// List Item Events
/// </summary>
public class EventReceiver1 : SPItemEventReceiver
{
/// <summary>
/// An item is being updated.
/// </summary>
public override void ItemUpdating(SPItemEventProperties properties)
{
base.ItemUpdating(properties);
if (properties.BeforeProperties["Wokflowstatus"] != properties.AfterProperties["Wokflowstatus"])
{
properties.ListItem["Completed Date"] = DateTime.Now;
properties.ListItem.Update();
properties.Web.Update();
}
}
}
}
In ItemUpdating method, you should be setting your new field value using
properties.AfterProperties[Completed_x0020_Date] = newFieldValue;
//AfterProperties and BeforeProperties are using internal names of columns.
Also question is then, if your if statement is ever visited? Cause calling ListItem.Update() most probably would result in recursive call on this event receiver.

Custom MVC routing based on URL stored in database

I'm trying to add some custom routing logic based on url's stored in a database for mvc. (CMS Like), I think its fairly basic, but I feel like i'm not really getting anywhere.
Basically a user may type url's such as:
www.somesite.com/categorya/categoryb/categoryf/someitem
www.somesite.com/about/someinfo
In the database these items are stored, along with the type they are, i.e. a normal page, or a product page.
Depending on this I then want to actually hit a different 'action' method, i.e. I would like the above to hit the methods:
PageController/Product
PageController/Normal
These actions then load the content for this page and display the same view (product view, or a normal view).
Using the normal way of routing won't work, since I could potentially have things like;
cata/producta
cata/catb/catc/catd/cate/catf/producta
Now i've been looking here : ASP.NET MVC custom routing for search
And trying to use this as a basis, but how do I actually 'change' my action method I want to hit within the InvokeActionMethod call?
Using MVC 3.0 btw.
Thanks for any help/suggestions
Final Solution:
Global.asax
routes.MapRoute(
"Default",
"{*path}",
new { controller = "Page", action = "NotFound", path= "Home" }
).RouteHandler = new ApplicationRouteHandler();
Route Handlers
public class ApplicationRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new ApplicationHandler(requestContext);
}
}
public class ApplicationHandler : MvcHandler, IRequiresSessionState
{
public ApplicationHandler(RequestContext requestContext)
: base(requestContext)
{
}
protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
{
var url = RequestContext.RouteData.Values["path"].ToString();
var page = SomePageService.GetPageByUrl(url);
if (page == null)
{
RequestContext.RouteData.Values["Action"] = "NotFound";
}
else
{
RequestContext.RouteData.Values["Action"] = page.Action;
RequestContext.RouteData.Values["page"] = page;
}
return base.BeginProcessRequest(httpContext, callback, state);
}
}
Maybe not an exact solution for your situation, but I've recently had to handle something similar so this might point you in the right direction.
What I did was setup a simple route in Global.asax with a catch-all parameter which calls a custom RouteHandler class.
// Custom MVC route
routes.MapRoute(
"Custom",
"{lang}/{*path}",
new { controller = "Default", action = "Index" },
new { lang = #"fr|en" }
).RouteHandler = new ApplicationRouteHandler();
ApplicationRouteHandler.cs :
public class ApplicationRouteHandler : IRouteHandler
{
/// <summary>
/// Provides the object that processes the request.
/// </summary>
/// <param name="requestContext">An object that encapsulates information about the request.</param>
/// <returns>
/// An object that processes the request.
/// </returns>
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
string path = requestContext.RouteData.Values["path"] as string;
// attempt to retrieve controller and action for current path
Page page = GetPageData(path);
// Method that returns a 404 error
if (page == null)
return SetupErrorHandler(requestContext, "ApplicationRouteHandler");
// Assign route values to current requestContext
requestContext.RouteData.Values["controller"] = page.Controller;
requestContext.RouteData.Values["action"] = page.Action;
return new MvcHandler(requestContext);
}
}
Obviously the way you retrieve the action and controller names from your database will probably be much different than mine, but this should give you an idea.

RequiredAttribute with AllowEmptyString=true in ASP.NET MVC 3 unobtrusive validation

If i have [Required(AllowEmptyStrings = true)] declaration in my view model the validation is always triggered on empty inputs. I found the article which explains why it happens. Do you know if there is a fix available? If not, how do you handle it?
Note: I'm assuming you have AllowEmptyStrings = true because you're also using your view model outside of a web scenario; otherwise it doesn't seem like there's much of a point to having a Required attribute that allows empty strings in a web scenario.
There are three steps to handle this:
Create a custom attribute adapter which adds that validation parameter
Register your adapter as an adapter factory
Override the jQuery Validation function to allow empty strings when that attribute is present
Step 1: The custom attribute adapter
I modified the RequiredAttributeAdapter to add in that logic:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace CustomAttributes
{
/// <summary>Provides an adapter for the <see cref="T:System.Runtime.CompilerServices.RequiredAttributeAttribute" /> attribute.</summary>
public class RequiredAttributeAdapter : DataAnnotationsModelValidator<RequiredAttribute>
{
/// <summary>Initializes a new instance of the <see cref="T:System.Runtime.CompilerServices.RequiredAttributeAttribute" /> class.</summary>
/// <param name="metadata">The model metadata.</param>
/// <param name="context">The controller context.</param>
/// <param name="attribute">The required attribute.</param>
public RequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute)
: base(metadata, context, attribute)
{
}
/// <summary>Gets a list of required-value client validation rules.</summary>
/// <returns>A list of required-value client validation rules.</returns>
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRequiredRule(base.ErrorMessage);
if (base.Attribute.AllowEmptyStrings)
{
//setting "true" rather than bool true which is serialized as "True"
rule.ValidationParameters["allowempty"] = "true";
}
return new ModelClientValidationRequiredRule[] { rule };
}
}
}
Step 2. Register this in your global.asax / Application_Start()
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(typeof(RequiredAttribute),
(metadata, controllerContext, attribute) => new CustomAttributes.RequiredAttributeAdapter(metadata,
controllerContext, (RequiredAttribute)attribute));
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Step 3. Override the jQuery "required" validation function
This is done using the jQuery.validator.addMethod() call, adding our custom logic and then calling the original function - you can read more about this approach here. If you are using this throughout your site, perhaps in a script file referenced from your _Layout.cshtml. Here's a sample script block you can drop in a page to test:
<script>
jQuery.validator.methods.oldRequired = jQuery.validator.methods.required;
jQuery.validator.addMethod("required", function (value, element, param) {
if ($(element).attr('data-val-required-allowempty') == 'true') {
return true;
}
return jQuery.validator.methods.oldRequired.call(this, value, element, param);
},
jQuery.validator.messages.required // use default message
);
</script>
Rather than decorating the value with the 'Required' attribute, I use the following. I find it to be the simplest solution to this issue.
[DisplayFormat(ConvertEmptyStringToNull=false)]

Resources