Integration between custom DecimalModelBinder and IValidatableObject.Validate - asp.net-mvc

I'm working with MVC custom validation server side and as i have to use several custom attribute.
I'd like to implement the interface ValidatableObject because I think it is easier way then writing several custom attributes.
To force the ValidationContext I've to use a custom model binder and I've following the instructions by David Haney in his article
Trigger IValidatableObject.Validate When ModelState.IsValid is false
so I've put in global.asax
ModelBinderProviders.BinderProviders.Clear();
ModelBinderProviders.BinderProviders.Add(new ForceValidationModelBinderProvider());
and then in a class
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
/// <summary>
/// A custom model binder to force an IValidatableObject to execute the Validate method, even when the ModelState is not valid.
/// </summary>
public class ForceValidationModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
ForceModelValidation(bindingContext);
}
private static void ForceModelValidation(ModelBindingContext bindingContext)
{
// Only run this code for an IValidatableObject model
IValidatableObject model = bindingContext.Model as IValidatableObject;
if (model == null)
{
// Nothing to do
return;
}
// Get the model state
ModelStateDictionary modelState = bindingContext.ModelState;
// Get the errors
IEnumerable<ValidationResult> errors = model.Validate(new ValidationContext(model, null, null));
// Define the keys and values of the model state
List<string> modelStateKeys = modelState.Keys.ToList();
List<ModelState> modelStateValues = modelState.Values.ToList();
foreach (ValidationResult error in errors)
{
// Account for errors that are not specific to a member name
List<string> errorMemberNames = error.MemberNames.ToList();
if (errorMemberNames.Count == 0)
{
// Add empty string for errors that are not specific to a member name
errorMemberNames.Add(string.Empty);
}
foreach (string memberName in errorMemberNames)
{
// Only add errors that haven't already been added.
// (This can happen if the model's Validate(...) method is called more than once, which will happen when there are no property-level validation failures)
int index = modelStateKeys.IndexOf(memberName);
// Try and find an already existing error in the model state
if (index == -1 || !modelStateValues[index].Errors.Any(i => i.ErrorMessage == error.ErrorMessage))
{
// Add error
modelState.AddModelError(memberName, error.ErrorMessage);
}
}
}
}
}
/// <summary>
/// A custom model binder provider to provide a binder that forces an IValidatableObject to execute the Validate method, even when the ModelState is not valid.
/// </summary>
public class ForceValidationModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
return new ForceValidationModelBinder();
}
}
It works great...
But here come the question..
I've also to add to this binder a specific behaviour in case of double and double? type to validate number in this format 1.000.000,000 so I was looking at these resources by Reilly and Haack
https://gist.github.com/johnnyreilly/5135647
using System;
using System.Globalization;
using System.Web.Mvc;
public class CustomDecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
//Check if this is a nullable decimal and a null or empty string has been passed
var isNullableAndNull = (bindingContext.ModelMetadata.IsNullableValueType &&
string.IsNullOrEmpty(valueResult.AttemptedValue));
//If not nullable and null then we should try and parse the decimal
if (!isNullableAndNull)
{
actualValue = double.Parse(valueResult.AttemptedValue, NumberStyles.Any, CultureInfo.CurrentCulture);
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Then as suggested in the comment by Haney I've substituted the default DecimalModelBinder with the CustomModelBinder in the global.asax this way
ModelBinders.Binders.Remove(typeof(double));
ModelBinders.Binders.Remove(typeof(double?));
ModelBinders.Binders.Add(typeof(double?), new CustomDecimalModelBinder());
ModelBinders.Binders.Add(typeof(double), new CustomDecimalModelBinder());
But I can't understand why.. the CustomDecimalModelBinder doesn't fire...
So at the moment my workarounf has been to comment the 4 row above in the global.asax
And to add in the custom ModelBinder class the override of BindModel in a way to accept the double and double? in it-It culture
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//if (bindingContext.ModelName == "commercialQty")
if (bindingContext.ModelType == typeof(double?) || bindingContext.ModelType == typeof(double))
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
//Check if this is a nullable decimal and a null or empty string has been passed
var isNullableAndNull = (bindingContext.ModelMetadata.IsNullableValueType &&
string.IsNullOrEmpty(valueResult.AttemptedValue));
//If not nullable and null then we should try and parse the decimal
if (!isNullableAndNull)
{
actualValue = double.Parse(valueResult.AttemptedValue, NumberStyles.Any, CultureInfo.CurrentCulture);
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
In this way the ValidationContext with my customValidation works and I also manage to validate double types in a custom way
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
var fieldPreliminaryCostNum = new[] { "preliminaryCostNum" };
var fieldPreliminaryCostAmount = new[] { "preliminaryCostAmount" };
var fieldPreliminaryVoucherNum = new[] { "preliminaryVoucherNum" };
var fieldCodiceIva = new[] { "codiceIva" };
var fieldContoRicavi = new[] { "contoRicavi" };
var fieldContoAnticipi = new[] { "contoAnticipi" };
//per la obbligatorietà di preliminary cost num, preliminary voucher num e preliminary cost amount è sufficiente
//il flag additional oppure occorre anche verificare che il voucher type code sia final?
if (flagAdditional == BLCostanti.fAdditional && preliminaryCostNum == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryCostNum ", fieldPreliminaryCostNum));
}
if (flagAdditional == BLCostanti.fAdditional && preliminaryCostAmount == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryCostAmount ", fieldPreliminaryCostAmount));
}
if (flagAdditional == BLCostanti.fAdditional && preliminaryVoucherNum == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryVoucherNum ", fieldPreliminaryVoucherNum));
//inoltre il preliminary deve essere approvato!
if (! BLUpdateQueries.CheckPreliminaryVoucherApproved(preliminaryVoucherNum) )
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryVoucherNum non approvato", fieldPreliminaryVoucherNum));
}
}
if (costPayReceiveInd == BLCostanti.attivo && String.IsNullOrWhiteSpace(codiceIva))
{
//yield return new ValidationResult("codiceIva obbligatorio", fieldCodiceIva);
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "codiceIva ", fieldCodiceIva));
}
if ((sapFlowType == BLCostanti.girocontoAcquisto || sapFlowType == BLCostanti.girocontiVendita)
&& String.IsNullOrWhiteSpace(contoRicavi))
{
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "conto Ricavi ", fieldContoRicavi));
}
if ((sapFlowType == BLCostanti.girocontoAcquisto || sapFlowType == BLCostanti.girocontiVendita)
&& String.IsNullOrWhiteSpace(contoAnticipi))
{
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "conto Anticipi ", fieldContoAnticipi));
}
return results;
}
Any better idea is welcome!

Related

Manipulate model value before passing it to DefaultModelBinder.BindModel

Some decimal and decimal? properties in my view model are marked as "Percent" data type, along with other data annotations, for example:
[DataType("Percent")]
[Display(Name = "Percent of foo completed")]
[Range(0, 1)]
public decimal? FooPercent { get; set; }
I'd like to permit the user some flexibility in how they enter the data, i.e. with or without the percent sign, intermediate spaces, etc. But I still want to use the DefaultModelBinder behavior to get all of its functionality such as checking the RangeAttribute and adding the appropriate validation messages.
Is there a way to parse and change the model value, then pass it along? Here is what I am trying, but am getting a runtime exception. (Ignore the actual parsing logic; this is not its final form. I'm just interested in the model replacement question at this point.)
public class PercentModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.DataTypeName == "Percent")
{
ValueProviderResult result =
bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (result != null)
{
string stringValue =
(string)result.ConvertTo(typeof(string));
decimal decimalValue;
if (!string.IsNullOrWhiteSpace(stringValue) &&
decimal.TryParse(
stringValue.TrimEnd(new char[] { '%', ' ' }),
out decimalValue))
{
decimalValue /= 100.0m;
// EXCEPTION : This property setter is obsolete,
// because its value is derived from
// ModelMetadata.Model now.
bindingContext.Model = decimalValue;
}
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
Never mind, this was a fundamental misunderstanding of where validation happens in the MVC cycle. After spending some time in the MVC source code, I see how this works.
In case it is helpful to others, here is what is working for me:
[DataType("Percent")]
[Display(Name = "Percent of foo completed")]
[Range(0.0d, 1.0d, ErrorMessage="The field {0} must be between {1:P0} and {2:P0}.")]
public decimal? FooPercent { get; set; }
And in the binder, you just return the value:
public class PercentModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.DataTypeName == "Percent")
{
ValueProviderResult result =
bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (result != null)
{
string stringValue =
(string)result.ConvertTo(typeof(string));
decimal decimalValue;
if (!string.IsNullOrWhiteSpace(stringValue) &&
decimal.TryParse(
stringValue.TrimEnd(new char[] { '%', ' ' }),
out decimalValue))
{
return decimalValue / 100.0m;
}
}
}
return base.BindModel(controllerContext, bindingContext);
}
}

MVC pass ids separated by "+" to action

I want to have possibility to access action by the following URL type:
http://localhost/MyControllerName/MyActionName/Id1+Id2+Id3+Id4 etc.
and handle it in code in the following way:
public ActionResult MyActionName(string[] ids)
{
return View(ids);
}
+ is a reserved symbol in an url. It means white space. So to achieve what you are looking for you could write a custom model binder:
public class StringModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value != null && !string.IsNullOrEmpty(value.AttemptedValue))
{
return value.AttemptedValue.Split(' ');
}
return base.BindModel(controllerContext, bindingContext);
}
}
and then either register it globally for the string[] type or use the ModelBinder attribute:
public ActionResult MyActionName(
[ModelBinder(typeof(StringModelBinder))] string[] ids
)
{
return View(ids);
}
Obviously if you want to use an url of the form /MyControllerName/MyActionName/Id1+Id2+Id3+Id4 that will bind the last part as an action parameter called ids you will have to modify the default route definition which uses {id}.
After all chose the following solution:
public ActionResult Action(string id = "")
{
var ids = ParseIds(id);
return View(ids);
}
private static int[] ParseIds(string idsString)
{
idsString = idsString ?? string.Empty;
var idsStrings = idsString.Split(new[] { ' ', '+' });
var ids = new List<int>();
foreach (var idString in idsStrings)
{
int id;
if (!int.TryParse(idString, out id))
continue;
if (!ids.Contains(id))
ids.Add(id);
}
return ids.ToArray();
}

How to call IValidatableObject Validate(ValdationContext) in MVC3 or include in ModelUpdate? [duplicate]

It seems that when MVC validates a Model that it runs through the DataAnnotation attributes (like required, or range) first and if any of those fail it skips running the Validate method on my IValidatableObject model.
Is there a way to have MVC go ahead and run that method even if the other validation fails?
You can manually call Validate() by passing in a new instance of ValidationContext, like so:
[HttpPost]
public ActionResult Create(Model model) {
if (!ModelState.IsValid) {
var errors = model.Validate(new ValidationContext(model, null, null));
foreach (var error in errors)
foreach (var memberName in error.MemberNames)
ModelState.AddModelError(memberName, error.ErrorMessage);
return View(post);
}
}
A caveat of this approach is that in instances where there are no property-level (DataAnnotation) errors, the validation will be run twice. To avoid that, you could add a property to your model, say a boolean Validated, which you set to true in your Validate() method once it runs and then check before manually calling the method in your controller.
So in your controller:
if (!ModelState.IsValid) {
if (!model.Validated) {
var validationResults = model.Validate(new ValidationContext(model, null, null));
foreach (var error in validationResults)
foreach (var memberName in error.MemberNames)
ModelState.AddModelError(memberName, error.ErrorMessage);
}
return View(post);
}
And in your model:
public bool Validated { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
// perform validation
Validated = true;
}
There's a way to do it without requiring boilerplate code at the top of each controller action.
You'll need to replace the default model binder with one of your own:
protected void Application_Start()
{
// ...
ModelBinderProviders.BinderProviders.Clear();
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
// ...
}
Your model binder provider looks like this:
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
return new CustomModelBinder();
}
}
Now create a custom model binder that actually forces the validation. This is where the heavy lifting's done:
public class CustomModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
ForceModelValidation(bindingContext);
}
private static void ForceModelValidation(ModelBindingContext bindingContext)
{
var model = bindingContext.Model as IValidatableObject;
if (model == null) return;
var modelState = bindingContext.ModelState;
var errors = model.Validate(new ValidationContext(model, null, null));
foreach (var error in errors)
{
foreach (var memberName in error.MemberNames)
{
// Only add errors that haven't already been added.
// (This can happen if the model's Validate(...) method is called more than once, which will happen when
// there are no property-level validation failures.)
var memberNameClone = memberName;
var idx = modelState.Keys.IndexOf(k => k == memberNameClone);
if (idx < 0) continue;
if (modelState.Values.ToArray()[idx].Errors.Any()) continue;
modelState.AddModelError(memberName, error.ErrorMessage);
}
}
}
}
You'll need an IndexOf extension method, too. This is a cheap implementation but it'll work:
public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
if (source == null) throw new ArgumentNullException("source");
if (predicate == null) throw new ArgumentNullException("predicate");
var i = 0;
foreach (var item in source)
{
if (predicate(item)) return i;
i++;
}
return -1;
}

asp.net MVC 1.0 and 2.0 currency model binding

I would like to create model binding functionality so a user can enter ',' '.' etc for currency values which bind to a double value of my ViewModel.
I was able to do this in MVC 1.0 by creating a custom model binder, however since upgrading to MVC 2.0 this functionality no longer works.
Does anyone have any ideas or better solutions for performing this functionality? A better solution would be to use some data annotation or custom attribute.
public class MyViewModel
{
public double MyCurrencyValue { get; set; }
}
A preferred solution would be something like this...
public class MyViewModel
{
[CurrencyAttribute]
public double MyCurrencyValue { get; set; }
}
Below is my solution for model binding in MVC 1.0.
public class MyCustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = null;
ValueProviderResult valueResult;
bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out valueResult);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueResult);
if (bindingContext.ModelType == typeof(double))
{
string modelName = bindingContext.ModelName;
string attemptedValue = bindingContext.ValueProvider[modelName].AttemptedValue;
string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
string alternateSeperator = (wantedSeperator == "," ? "." : ",");
try
{
result = double.Parse(attemptedValue, NumberStyles.Any);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
}
else
{
result = base.BindModel(controllerContext, bindingContext);
}
return result;
}
}
You might try something among the lines:
// Just a marker attribute
public class CurrencyAttribute : Attribute
{
}
public class MyViewModel
{
[Currency]
public double MyCurrencyValue { get; set; }
}
public class CurrencyBinder : DefaultModelBinder
{
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
var currencyAttribute = propertyDescriptor.Attributes[typeof(CurrencyAttribute)];
// Check if the property has the marker attribute
if (currencyAttribute != null)
{
// TODO: improve this to handle prefixes:
var attemptedValue = bindingContext.ValueProvider
.GetValue(propertyDescriptor.Name).AttemptedValue;
return SomeMagicMethodThatParsesTheAttemptedValue(attemtedValue);
}
return base.GetPropertyValue(
controllerContext,
bindingContext, propertyDescriptor,
propertyBinder
);
}
}
public class HomeController: Controller
{
[HttpPost]
public ActionResult Index([ModelBinder(typeof(CurrencyBinder))] MyViewModel model)
{
return View();
}
}
UPDATE:
Here's an improvement of the binder (see TODO section in previous code):
if (!string.IsNullOrEmpty(bindingContext.ModelName))
{
var attemptedValue = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName).AttemptedValue;
return SomeMagicMethodThatParsesTheAttemptedValue(attemtedValue);
}
In order to handle collections you will need to register the binder in Application_Start as you will no longer be able to decorate the list with the ModelBinderAttribute:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(MyViewModel), new CurrencyBinder());
}
And then your action could look like this:
[HttpPost]
public ActionResult Index(IList<MyViewModel> model)
{
return View();
}
Summarizing the important part:
bindingContext.ValueProvider.GetValue(bindingContext.ModelName)
A further improvement step of this binder would be to handle validation (AddModelError/SetModelValue)

Stuck creating a "security trimmed" html.ActionLink extension method

I'm trying to create an Extension Method for MVC's htmlHelper.
The purpose is to enable or disable an ActionLink based on the AuthorizeAttribute set on the controller/action.
Borrowing from the MVCSitemap
code that Maarten Balliauw created, I wanted to validate the user's permissions against the controller/action before deciding how to render the actionlink.
When I try to get the MvcHandler, I get a null value.
Is there a better way to the the attributes for the controller/action?
Here is the code for the extension method:
public static class HtmlHelperExtensions
{
public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller)
{
//simplified for brevity
if (IsAccessibleToUser(action, controller))
{
return htmlHelper.ActionLink(linkText, action,controller);
}
else
{
return String.Format("<span>{0}</span>",linkText);
}
}
public static bool IsAccessibleToUser(string action, string controller)
{
HttpContext context = HttpContext.Current;
MvcHandler handler = context.Handler as MvcHandler;
IController verifyController =
ControllerBuilder
.Current
.GetControllerFactory()
.CreateController(handler.RequestContext, controller);
object[] controllerAttributes = verifyController.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true);
object[] actionAttributes = verifyController.GetType().GetMethod(action).GetCustomAttributes(typeof(AuthorizeAttribute), true);
if (controllerAttributes.Length == 0 && actionAttributes.Length == 0)
return true;
IPrincipal principal = handler.RequestContext.HttpContext.User;
string roles = "";
string users = "";
if (controllerAttributes.Length > 0)
{
AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
roles += attribute.Roles;
users += attribute.Users;
}
if (actionAttributes.Length > 0)
{
AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
roles += attribute.Roles;
users += attribute.Users;
}
if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
return true;
string[] roleArray = roles.Split(',');
string[] usersArray = users.Split(',');
foreach (string role in roleArray)
{
if (role != "*" && !principal.IsInRole(role)) return false;
}
foreach (string user in usersArray)
{
if (user != "*" && (principal.Identity.Name == "" || principal.Identity.Name != user)) return false;
}
return true;
}
}
Here is the working code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Principal;
using System.Web.Routing;
using System.Web.Mvc;
using System.Collections;
using System.Reflection;
namespace System.Web.Mvc.Html
{
public static class HtmlHelperExtensions
{
public static string SecurityTrimmedActionLink(
this HtmlHelper htmlHelper,
string linkText,
string action,
string controller)
{
return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, false);
}
public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, bool showDisabled)
{
if (IsAccessibleToUser(action, controller))
{
return htmlHelper.ActionLink(linkText, action, controller);
}
else
{
return showDisabled ? String.Format("<span>{0}</span>", linkText) : "";
}
}
public static bool IsAccessibleToUser(string actionAuthorize, string controllerAuthorize)
{
Assembly assembly = Assembly.GetExecutingAssembly();
GetControllerType(controllerAuthorize);
Type controllerType = GetControllerType(controllerAuthorize);
var controller = (IController)Activator.CreateInstance(controllerType);
ArrayList controllerAttributes = new ArrayList(controller.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true));
ArrayList actionAttributes = new ArrayList();
MethodInfo[] methods = controller.GetType().GetMethods();
foreach (MethodInfo method in methods)
{
object[] attributes = method.GetCustomAttributes(typeof(ActionNameAttribute), true);
if ((attributes.Length == 0 && method.Name == actionAuthorize) || (attributes.Length > 0 && ((ActionNameAttribute)attributes[0]).Name == actionAuthorize))
{
actionAttributes.AddRange(method.GetCustomAttributes(typeof(AuthorizeAttribute), true));
}
}
if (controllerAttributes.Count == 0 && actionAttributes.Count == 0)
return true;
IPrincipal principal = HttpContext.Current.User;
string roles = "";
string users = "";
if (controllerAttributes.Count > 0)
{
AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
roles += attribute.Roles;
users += attribute.Users;
}
if (actionAttributes.Count > 0)
{
AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
roles += attribute.Roles;
users += attribute.Users;
}
if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
return true;
string[] roleArray = roles.Split(',');
string[] usersArray = users.Split(',');
foreach (string role in roleArray)
{
if (role == "*" || principal.IsInRole(role))
return true;
}
foreach (string user in usersArray)
{
if (user == "*" && (principal.Identity.Name == user))
return true;
}
return false;
}
public static Type GetControllerType(string controllerName)
{
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (Type type in assembly.GetTypes())
{
if (type.BaseType.Name == "Controller" && (type.Name.ToUpper() == (controllerName.ToUpper() + "Controller".ToUpper())))
{
return type;
}
}
return null;
}
}
}
I don't like using reflection, but I can't get to the ControllerTypeCache.
Your ViewPage has a reference to the view context, so you could make it an extension method on that instead.
Then you can just say if Request.IsAuthenticated or Request.User.IsInRole(...)
usage would be like <%= this.SecurityLink(text, demandRole, controller, action, values) %>
I really liked the code from #Robert's post, but there were a few bugs and I wanted to cache the gathering of the roles and users because reflection can be a little time costly.
Bugs fixed: if there is both a Controller attribute and an Action attribute, then when the roles get concatenated, an extra comma doesn't get inserted between the controller's roles and the action's roles which will not get analyzed correctly.
[Authorize(Roles = "SuperAdmin,Executives")]
public class SomeController() {
[Authorize(Roles = "Accounting")]
public ActionResult Stuff() {
}
}
then the roles string ends up being SuperAdmin,ExecutivesAccounting, my version ensures that Executives and Accounting is separate.
My new code also ignores Auth on HttpPost actions because that could throw things off, albeit unlikely.
Lastly, it returns MvcHtmlString instead of string for newer versions of MVC
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Reflection;
using System.Collections;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Security.Principal;
public static class HtmlHelperExtensions
{
/// <summary>
/// only show links the user has access to
/// </summary>
/// <returns></returns>
public static MvcHtmlString SecurityLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, bool showDisabled = false)
{
if (IsAccessibleToUser(action, controller))
{
return htmlHelper.ActionLink(linkText, action, controller);
}
else
{
return new MvcHtmlString(showDisabled ? String.Format("<span>{0}</span>", linkText) : "");
}
}
/// <summary>
/// reflection can be kinda slow, lets cache auth info
/// </summary>
private static Dictionary<string, Tuple<string[], string[]>> _controllerAndActionToRolesAndUsers = new Dictionary<string, Tuple<string[], string[]>>();
private static Tuple<string[], string[]> GetAuthRolesAndUsers(string actionName, string controllerName)
{
var controllerAndAction = controllerName + "~~" + actionName;
if (_controllerAndActionToRolesAndUsers.ContainsKey(controllerAndAction))
return _controllerAndActionToRolesAndUsers[controllerAndAction];
Type controllerType = GetControllerType(controllerName);
MethodInfo matchingMethodInfo = null;
foreach (MethodInfo method in controllerType.GetMethods())
{
if (method.GetCustomAttributes(typeof(HttpPostAttribute), true).Any())
continue;
if (method.GetCustomAttributes(typeof(HttpPutAttribute), true).Any())
continue;
if (method.GetCustomAttributes(typeof(HttpDeleteAttribute), true).Any())
continue;
var actionNameAttr = method.GetCustomAttributes(typeof(ActionNameAttribute), true).Cast<ActionNameAttribute>().FirstOrDefault();
if ((actionNameAttr == null && method.Name == actionName) || (actionNameAttr != null && actionNameAttr.Name == actionName))
{
matchingMethodInfo = method;
}
}
if (matchingMethodInfo == null)
return new Tuple<string[], string[]>(new string[0], new string[0]);
var authAttrs = new List<AuthorizeAttribute>();
authAttrs.AddRange(controllerType.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast<AuthorizeAttribute>());
var roles = new List<string>();
var users = new List<string>();
foreach(var authAttr in authAttrs)
{
roles.AddRange(authAttr.Roles.Split(','));
users.AddRange(authAttr.Roles.Split(','));
}
var rolesAndUsers = new Tuple<string[], string[]>(roles.ToArray(), users.ToArray());
try
{
_controllerAndActionToRolesAndUsers.Add(controllerAndAction, rolesAndUsers);
}
catch (System.ArgumentException ex)
{
//possible but unlikely that two threads hit this code at the exact same time and enter a race condition
//instead of using a mutex, we'll just swallow the exception when the method gets added to dictionary
//for the second time. mutex only allow single worker regardless of which action method they're getting
//auth for. doing it this way eliminates permanent bottleneck in favor of a once in a bluemoon time hit
}
return rolesAndUsers;
}
public static bool IsAccessibleToUser(string actionName, string controllerName)
{
var rolesAndUsers = GetAuthRolesAndUsers(actionName, controllerName);
var roles = rolesAndUsers.Item1;
var users = rolesAndUsers.Item2;
IPrincipal principal = HttpContext.Current.User;
if (!roles.Any() && !users.Any() && principal.Identity.IsAuthenticated)
return true;
foreach (string role in roles)
{
if (role == "*" || principal.IsInRole(role))
return true;
}
foreach (string user in users)
{
if (user == "*" && (principal.Identity.Name == user))
return true;
}
return false;
}
public static Type GetControllerType(string controllerName)
{
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (Type type in assembly.GetTypes())
{
if (type.BaseType.Name == "Controller" && (type.Name.ToUpper() == (controllerName.ToUpper() + "Controller".ToUpper())))
{
return type;
}
}
return null;
}
}

Resources