I have two entity sets like below:
public class Serial
{
[HiddenInput(DisplayValue=false)]
public int SerialID { get; set; }
[HiddenInput(DisplayValue=false)]
public string Id { get; set; }
[Required(ErrorMessage="Please provide your membership serial")]
[StringLength(16,ErrorMessage="This field can't be longer as of 16 characters.")]
public string UserSerial { get; set; }
}
AND
public class Subscription
{
[HiddenInput(DisplayValue=false)]
public int SubscriptionID { get; set; }
[Required(ErrorMessage="Please provide a subscription code.")]
public string AdminSerial { get; set; }
}
I would like to create a custom authorization attribute to design my action methods within my controllers with following scenario:
I would like to check if the any value of UserSerial in Serial
Entity not equal to any value ofAdminSerial in Subscription Entity.
If the above condition become true so the ActionResult method itself should be executed else the Custom AuthorizeAttribute should redirect it to another action method, here is what i tried but it isn't working am i missing something?
public class RequireSerial : AuthorizeAttribute
{
EFDbContext db = new EFDbContext();
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!db.Subscriptions.Any(s => s.AdminSerial.Equals(db.Serials.Any())))
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Serials", action = "Create" }));
}
else
{
// Execute the Action method itself
}
}
}
I tried to put this RequireSerial custom authorize attribute on the top of action methods but nothing really happens.
[RequireSerial]
public ViewResult Checkout()
{
return View();
}
Any help would be appreciated.
You need to override OnAuthorization a HandleUnauthorizedRequest method. HandleUnauthorizedRequest is called by default implementation of OnAuthorization method if user is not authorized.
Default implementation of HandleUnauthorizedRequestredirects the user to login page.
EFDbContext db = new EFDbContext();
public override void OnAuthorization(AuthorizationContext filterContext)
{
//handle base authorization logic
base.OnAuthorization(filterContext);
//if user is not authorized (by base rules) simply return because redirect was set in 'base.OnAuthorization' call.
if (this.AuthorizeCore(filterContext.HttpContext) == false)
{
return;
}
//Here comes your custom redirect logic:
if (!db.Subscriptions.Any(s => s.AdminSerial.Equals(db.Serials.Any())))
{
filterContext.Result = your redirect url goes here;
}
}
Authorization is basically a "boolean" value (not exactly a true boolean but it returns either an authorization or a failure of it)
to full get it this, MSDN's article about is very clear.
Custom Authorization
Related
I am having some ActionFilters in my asp.net core mvc application, which validate some input data. For example, the client sends a userId inside the header, the filter loads that user from a repository, and validates, if the user exists, is active, has a license, and so on.
This filter is attached to a Controller method. The Controller method also needs to collect the same user object. Because of performance, I want to pass that user object, collected inside the filter, to the controller, so the controller does not need to load the same user object again. I know there are ways to do so, like mentioned here.
Because of clean code, I wonder if this would be possible, coding an attribute which defines what to retrieve, like the [FromBody] attribute does, for instance.
I could imagine this attribute named [FromFilter("User")], which takes a parameter to specify the key inside the HttpContext.Items
A basic implementation could be something like this:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromFilterAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
/// <inheritdoc />
public BindingSource BindingSource => BindingSource.Custom;
/// <inheritdoc />
public string Name { get; set; }
}
Neither do I know if this would a be a good idea, nor how to implement such a feature. Hopefully someone can me point into the right direction
As far as I know, we couldn't directly pass the object from filter to action.
In my opinion, the best solution is creating a custom model binding and then find the user from the repository and pass the user to the action.
Since the model binding is triggered before the filter, you could get the custom model binding result from the ActionExecutingContext.
Order of execution:
UserModelBinder --> OnActionExecuting --> Index action
More details about to do it, you could refer to below codes:
Custom model binding:
public class UserModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var model = new UserModel()
{
id = 1,
name = "test"
};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Controller action and OnActionExecuting method:
OnActionExecuting:
public override void OnActionExecuting(ActionExecutingContext context)
{
//ActionArguments["user"] is the parameter name of the action parameter
var user = context.ActionArguments["user"] as UserModel;
// Do something before the action executes.
base.OnActionExecuting(context);
}
Action method:
public async Task<IActionResult> Index([ModelBinder(BinderType = typeof(UserModelBinder))] UserModel user)
{
int i =0;
return View();
}
Result:
Filter onexecuting:
Action parameter:
You can use HttpContext.Items for this and create HttpContextItemsModelBinder which will bind model from HttpContext.Items
public class HttpContextItemsModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var items = bindingContext.HttpContext.Items;
string name = bindingContext.BinderModelName ?? bindingContext.FieldName;
bindingContext.Result = items.TryGetValue(name, out object item)
? ModelBindingResult.Success(item)
: ModelBindingResult.Failed();
return Task.CompletedTask;
}
}
Create and register model binder provider
public static class CustomBindingSources
{
public static BindingSource HttpContextItems { get; } = new BindingSource("HttpContextItems", "HttpContext Items", true, true);
}
public class HttpContextItemsModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.BindingInfo.BindingSource == CustomBindingSources.HttpContextItems)
{
return new HttpContextItemsModelBinder();
}
return null;
}
}
In Startup.cs
services
.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new HttpContextItemsModelBinderProvider());
//...
})
Create an attribute which will set correct BindingSource to use HttpContextItemsModelBinder
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromHttpContextItemsAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
public string Name { get; set; }
public BindingSource BindingSource => CustomBindingSources.HttpContextItems;
public FromHttpContextItemsAttribute(string name)
{
Name = name;
}
public FromHttpContextItemsAttribute() { }
}
Usage:
//in controller
[HttpGet]
[ValidateUserFilter]
public IActionResult TestHttpContextItems([FromHttpContextItems("UserItem")]UserItemModel model)
{
return Ok(model);
}
//your action filter
public class ValidateUserFilterAttribute : ActionFilterAttribute, IAuthorizationFilter
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//...
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var model = new UserItemModel
{
Id = 45,
Name = "Some user name"
};
context.HttpContext.Items["UserItem"] = model;
}
}
Important note
Pay attention that I save user model to HttpContext.Items during OnAuthorization and not OnActionExecuting because model binding happens before any action filters run, so HttpContext.Items won't contain user and model binding will fail. You might need to adjust filter code to your needs and to make the solution work as expected.
Usage without specifying item name. Parameter name in action method should match key ("userModel") used to store value in HttpContext.Items:
//in controller
[HttpGet]
[ValidateUserFilter]
public IActionResult TestHttpContextItems([FromHttpContextItems]UserItemModel userModel)
{
return Ok(userModel);
}
//action filter
public class ValidateUserFilterAttribute : ActionFilterAttribute, IAuthorizationFilter
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//...
}
public void OnAuthorization(AuthorizationFilterContext context)
{
//...
context.HttpContext.Items["userModel"] = model;
}
}
I have the follow Custom AuthorizeAttribute:
public class SystemAuthorizeAttribute : AuthorizeAttribute
{
public Form PermissionForm { get; set; } //Enum
public PermissionValue Permissions { get; set; }//Enum
public override void OnAuthorization(AuthorizationContext filterContext)
{
//Request is an Authenticated Method?
if (filterContext.HttpContext.Request.IsAuthenticated)
{
Debug.WriteLine("Test 1 " + PermissionForm);
if (!CurrentUserHasPermissionForm(PermissionForm))
{
//Deny access code
}
}
}
//...
}
After Login method it redirects to Index page from HomeController. The problem is when use SystemAuthorize Attribute in my HomeController the Form value always come as 0 when it should be 4 (Content).
HomeController method:
[SystemAuthorize(PermissionForm = Form.CONTENT, Permissions = PermissionValue.VIEW)]
public ActionResult Index()
{
return this.View();
}
Login method:
[AllowAnonymous]
[Route("Account/Login")]
public ActionResult Login(LoginViewModel model, string url = "")
{
var user= GetUserAccount(model);
if (user == null)
{
ModelState.AddModelError("", "User not found!");
return View(model);
}
else
{
FormsAuthentication.SetAuthCookie(user.Sign, false);
var authTicket = new FormsAuthenticationTicket(1, user.Sign, DateTime.Now, DateTime.Now.AddMinutes(20), false, JsonConvert.SerializeObject(user));
var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket));
HttpContext.Response.Cookies.Add(authCookie);
return RedirectToAction("Index", "Home");
}
}
Form enum:
public enum Form : short
{
PATIENT = 1,
USERS = 2,
MEDICE = 3,
CONTENT = 4,
}
What I'm doing wrong or missing?
Unfortunately Microsoft made this a bit confusing by combining IAuthorizationFilter with Attribute in the same class. The fact of the matter is that attributes cannot do anything except store meta-data.
The part of MVC that reads the attribute is the IAuthorizationFilter which through some MVC magic is registered with MVC automatically when you place AuthorizeAttribute (or a subclass) on a controller or action.
But the only way to actually read the meta-data from the attribute is to use Reflection. The meta-data is in the same class, but not the same instance of the class. The meta-data is in the Attribute, but the code that executes when the filter runs is in the IAuthorizationFilter, which is a separate instance of the same class.
public class SystemAuthorizeAttribute : AuthorizeAttribute
{
public Form PermissionForm { get; set; } //Enum
public PermissionValue Permissions { get; set; }//Enum
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var actionDescriptor = httpContext.Items["ActionDescriptor"] as ActionDescriptor;
if (actionDescriptor != null)
{
var authorizeAttribute = this.GetSystemAuthorizeAttribute(actionDescriptor);
// If the authorization attribute exists
if (authorizeAttribute != null)
{
// Run the authorization based on the attribute
var form = authorizeAttribute.PermissionForm;
var permissions = authorizeAttribute.Permissions;
// Return true if access is allowed, false if not...
if (!CurrentUserHasPermissionForm(form))
{
//Deny access code
}
}
}
return true;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
// Pass the current action descriptor to the AuthorizeCore
// method on the same thread by using HttpContext.Items
filterContext.HttpContext.Items["ActionDescriptor"] = filterContext.ActionDescriptor;
base.OnAuthorization(filterContext);
}
private SystemAuthorizeAttribute GetSystemAuthorizeAttribute(ActionDescriptor actionDescriptor)
{
SystemAuthorizeAttribute result = null;
// Check if the attribute exists on the action method
result = (SystemAuthorizeAttribute)actionDescriptor
.GetCustomAttributes(attributeType: typeof(SystemAuthorizeAttribute), inherit: true)
.SingleOrDefault();
if (result != null)
{
return result;
}
// Check if the attribute exists on the controller
result = (SystemAuthorizeAttribute)actionDescriptor
.ControllerDescriptor
.GetCustomAttributes(attributeType: typeof(SystemAuthorizeAttribute), inherit: true)
.SingleOrDefault();
return result;
}
}
Note that OnAuthorization has some logic in it that you will need to support output caching and the part of the code that checks for [AllowAnonymous], so you should not put your authorization check there, but in AuthorizeCore. But unfortunately, AuthorizeCore isn't passed the ActionDescriptor you need to check whether the attribute exists, so you need the above httpContext.Items hack to ensure it is passed into that method on the same thread.
The Reflection part becomes much more clear if you separate your Attribute into a different class from the IAuthorizationFilter, as in this example.
Is there a way in MVC C# to allow users that don't have a role assigned yet.
For example
[Authorize(Roles="a, b, c, d")]
public class SomeController : Controller
{
[Authorize(Roles="")
public ActionResult ChooseYourRole()
//I want only authenticated users with no roles assigned to access this action
{
}
....other actions....
}
You can create a custom Authorization Attribute to achieve this
public class MyAuthorizationAttribute : AuthorizeAttribute
{
private bool allowUserWithNoRole = false;
public MyAuthorizationAttribute (bool AllowUserWithNoRole) : base()
{
this.allowUserWithNoRole = allowUserWithNoRole;
}
}
In your AuthorizeCore perform your required check which is roles == 0
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (!httpContext.Request.IsAuthenticated)
return false;
if (allowUserWithNoRole && Roles.Count() == 0)
{
return true;
}
else {
//Perform additional checks for authorization
}
}
I have also added a field called allowUserWithNoRole. This will take care of instances where you want a different behaviour for your Authorization when Roles is left empty.
Finally, you set this custom attribute above your Action
[MyAuthorization(AllowUserWithNoRole=true, Roles="")
public ActionResult ChooseYourRole()
//I want only authenticated users with no roles assigned to access this action
{
}
I have try to create a url rewriter according with current language. For example if I have controller Article, for each language I have a record like:
public class ControllerRewriter
{
public LanguageName { get; set; }
public NiceName { get; set; }
public Controller { get; set; }
}
And also a list of ControllerRewriter where I made search.
public class ControllerRewriterList: List<ControllerRewriter>
{
public string GetController(string niceName)
{
var cr = this.FirstOrDefault( c => c.NiceName == niceName);
if( cr == null )
return niceName;
else
return cr.Controller;
}
}
In a base Controller class I override the method OnActionExecuting in order to intercept the niceName of controller and then to rewrite it with the real name of controller, but I fail...
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
_actionName = filterContext.ActionDescriptor.ActionName.ToLower();
//here I get a nice name of controller according with the current language
_controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower();
//rewrite the controller with the real name of controller
//this line doesn't work AND I NEED A SOLUTION ( thanks! )
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName = crList.GetController(_controllerName);
base.OnActionExecuting(filterContext);
}
Do you have any idea/sugestion about how I can rewrite the controller name, here in OnActionExecuting or maybe in other method... ?
Update: RedirectToRouteResult is not an option because it will change the current url. :-)
Thanks,
Ovidiu
You can do something like
filterContext.Result = new RedirectToRouteResult(routeName, routeValues);
I am exactly at the situation where this person (Controller ModelState with ModelStateWrappper) is except that I am using Castle Windsor DI. I am trying to validate my model's business logic side in my service before I save the model pass it to the data store. if the validation fails I want to get the model state errors and display in my view.
(I am following this article as a guide to implement validation from service: http://www.asp.net/mvc/tutorials/older-versions/models-(data)/validating-with-a-service-layer-cs)
Below is my sample code,
//My controller code
public class TimeEntryController : TempoBaseController
{
public TimeEntryController(TimeService service, UserService userService)
{
_service = service;
_userService = userService;
}
[Authorize(Roles = "Worker")]
public ActionResult Index()
{
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(EntryLogDto entry)
{
if(_service.AddEntryLog(entry))
{
return ViewSuccess();
}else
{
return ViewFailue();
}
}
}
//My Service class
public class TimeService : GenericService
{
public TimeService(IRepository repository, IValidationDictionary validationDictionary, UserManager<ApplicationUser> userManager)
: base(repository, validationDictionary, userManager)
{
}
public bool AddEntryLog(EntryLogDto log)
{
if (!ValidateEntryLog(log))
{
//return false;
}
else
{
//insert entry into database and return true
}
}
protected bool ValidateEntryLog(EntryLogDto log)
{
//Check if the entry overlaps with any other entries
bool res = _repository.Find<EntryLogDto>(//my linq logic);
if (res)
{
_validationDictionary.IsValid = true;
}else
{
_validatonDictionary.AddError("Entry", "Entry Overlaps.");
_validationDictionary.IsValid = false;
}
return _validationDictionary.IsValid;
}
}
//Validation class
public class TempoValidation : IValidationDictionary
{
private ModelStateDictionary _modelState;
public TempoValidation(ModelStateDictionary modelState) // Issue is how am I gona give it this as the ModelStateDictiona ry is controller specific
{
_modelState = modelState;
}
public void AddError(string key, string error)
{
_modelState.AddModelError(key, error);
}
public bool IsValid
{
get { return _modelState.IsValid; }
}
}
//Global.asax castle compnonent registration method
container
.Register(Component
.For<Tempo.Model.Configuration.TempoDbContext>()
.LifestylePerWebRequest()
.DependsOn(new { connectionString }))
.Register(Component
.For<DbRepositories.ITempoDataContextFactory>()
.AsFactory())
.Register(Component
.For<IRepository>()
.ImplementedBy<Tempo.Repositories.EntityFrameworkRepository.Repository>())
.Register(Component
.For<TimeService>().LifestyleTransient())
I am injecting IValidationDictionary in my service class where I set the model state depending on the validation result. Is there a way I can pass in the model state of the controller when I use it? I don't know how to approach this, I have many controllers and I don't know how I will/when will I pass the respective controller's model state (I would like to do that by DI if its possible )... I don't know if castle can create a separate instance of TempoValidation class for each controller??
I know that this is impossible to do this by default, but you can use Fluent Validation to achieve this.
Example:
ViewModel
[Validator(typeof(VmSysTestValidator))]
public class VmSysTestModel
{
public int Id { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
Fluent validation implementation :
public class VmSysTestValidator : AbstractValidator<VmSysTestModel>
{
public VmSysTestValidator()
{
RuleFor(x => x.FirstName).NotNull().WithMessage("First name is required");
RuleFor(x => x.LastName).NotNull().WithMessage("Last Name is required");
}
}
Controller or business logic side :
[HttpPost]
public ActionResult TestPost(VmSysTestModel obj)
{
//Start Validation From start to end you can call this code everywhere you want , this will work like server side valdiatin
//Instead of ModelState.IsValid you will call your fluent validator
var testValidator = new VmSysTestValidator();
var validationResult = testValidator.Validate(obj);
if (validationResult.IsValid)
{
}
else
{
}
//End valdiation
}