I have a very simple scenario. I want to decorate my controllers/actions with a custom authorization attribute. Authorization should be granted if any of the attributes is valid. For example,
[MyAuth(1)]
[MyAuth(2)]
public class MyController : Controller
{
...
}
I cannot combine the parameters into a single authorization attribute. The above example is a simplified example, only.
If either attribute authorizes the user, I want the user to be authorized. I assumed that ActionFilterAttribute or AuthorizeAttribute would have the means to see what other filters have been executed and are waiting to be executed, but no such luck.
How can I accomplish this? Since the attributes don't seem to have any awareness, maybe an HttpModule? A custom ControllerActionInvoker?
I managed to get this to work last night. My solution is below. The attribute is pretty standard and I've trimmed the actual authorization parts. The interesting stuff happens in HasAssignedAcccessActionInvoker.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequiresAssignedAccess : AuthorizeAttribute
{
public int AccessType { get; private set; }
public int IdType { get; private set; }
public int IdValue { get; private set; }
public int Level { get; private set; }
public RequiresAssignedAccess(int accessType, int idType, int idValue, int level)
{
...
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (!base.AuthorizeCore(httpContext))
return false;
bool retval = ...
return retval;
}
}
HasAssignedAcccessActionInvoker inherits from the standard action invoker, but I overrode the InvokeAuthorizationFilters method to add the authorization logic we need. The standard invoker just spins through the authorization filters and if any of them returns a result, it breaks the loop.
public class HasAssignedAcccessActionInvoker : ControllerActionInvoker
{
protected override AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
{
AuthorizationContext authCtx = new AuthorizationContext(controllerContext, actionDescriptor);
/*
* If any of the filters are RequiresAssignedAccess, default this to false. One of them must authorize the user.
*/
bool hasAccess = !filters.Any(f => f is RequiresAssignedAccess);
foreach (IAuthorizationFilter current in filters)
{
/*
* This sets authorizationContext.Result, usually to an instance of HttpUnauthorizedResult
*/
current.OnAuthorization(authCtx);
if (current is RequiresAssignedAccess)
{
if (authCtx.Result == null)
{
hasAccess = true;
}
else if (authCtx.Result is HttpUnauthorizedResult)
{
authCtx.Result = null;
}
continue;
}
if (authCtx.Result != null)
break;
}
if (!hasAccess && authCtx.Result == null)
authCtx.Result = new HttpUnauthorizedResult();
return authCtx;
}
}
I had to look at MVC's internals with ILSpy to figure this out. For reference, this is the overridden version of that method:
protected virtual AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
{
AuthorizationContext authorizationContext = new AuthorizationContext(controllerContext, actionDescriptor);
foreach (IAuthorizationFilter current in filters)
{
current.OnAuthorization(authorizationContext);
if (authorizationContext.Result != null)
{
break;
}
}
return authorizationContext;
}
Lastly, to wire this up and make everything possible, our controllers inherit from BaseController, which now returns the new invoker.
public class BaseController : Controller
{
protected override IActionInvoker CreateActionInvoker()
{
return new HasAssignedAcccessActionInvoker();
}
}
As far as I know, you cannot chain [Authorize] attributes in the manner that you want because they all have to pass (AND) and not (OR) behavior. However, the combining of the items into one does not cause you to have to do some magic string manipulation, regardless of the number of parameters that you need to pass to it. You can define your own set of parameters that are available to the Authorize attribute.
public class SuperCoolAuthorize : AuthorizationAttribute
{
public string Parameter1{get;set;}
public string Parameter2{get;set;}
public int Parameter3{get;set;}
public string Parameter4{get;set;}
public override void OnAuthorization(AuthorizationContext filterContext)
{
// your custom behaviour
}
}
And on your controller/action method
[Authorize(Parameter1 = "Foo", Parameter2 = "Bar", Parameter3 = 47, Parameter4 = string.Empty)
public ActionResult MyControllerAction(){
...
}
A great post on some other considerations on custom Authorizing attributes I came across in helping to formulate this answer.
public class AuthUserAttribute : AuthorizeAttribute {
public string[] SecurityGroups;
public string Groups { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext) {
bool valid = false;
var user = UserInformation.Current;
if (user.SecurityGroups.Select(x => x).Intersect(this.SecurityGroups).Any()) {
valid = true;
}
if (user.SecurityGroups.Select(x => x).Intersect(new string[] { "IT Administrators" }).Any()) {
valid = true;
}
return valid;
}
public override void OnAuthorization(AuthorizationContext filterContext) {
if (!this.AuthorizeCore(filterContext.HttpContext)) {
if (UserInformation.Current.SecurityGroups.Count == 0) {
filterContext.Result = new RedirectResult(string.Format("/oa?ReturnUrl={0}", filterContext.HttpContext.Request.RawUrl));
}
else {
filterContext.Result = new RedirectResult(string.Format("/oa/user/permissions?ReturnUrl={0}", filterContext.HttpContext.Request.RawUrl));
}
}
else {
base.OnAuthorization(filterContext);
}
}
}
then I decorate with
[AuthUser(SecurityGroups = new string[] { "Data1", "Data2" })]
public ActionResult ForYourEyesOnly() {
}
We'll see if anyone catches the Bond reference. LOL
Related
in my app I'd like to add functionality for admins to go to the specific screen and make certain controllers/methods available for certain roles.
Right now I'm using a build-in role check like
[Authorize(Roles = "APUL_Admin")]
So I changed that to be [AuthorizeExtended()] and I'm implementing it like that:
public class AuthorizeExtended : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var isAuthorized = base.AuthorizeCore(httpContext);
if (!isAuthorized)
{
return false;
}
// Point of interest **HERE**
return true;
}
}
which is all pretty standard.
At this moment (HERE see above) from HttpContextBase I know user's roles, and controller and method. And I can go to the DB and make sure those roles has access to this controller/action.
Here is my problem:
I don't want to go to the database for every request since it is slow and it is a lot of overhead for DB. What's the best way to deal with that? Cache it? I'm looking for implementation details.
Any help is appreciated. Thank you!
Yes, the cache is what you need to avoid duplicated requests to the DB. Here is the basic implementation:
internal class CacheKey
{
public string Role { get; set; }
public string Controller { get; set; }
public string Method { get; set; }
public override bool Equals(Object obj)
{
CacheKey cmp = obj as CacheKey;
if (cmp == null)
{
return false;
}
return Role == cmp.Role && Controller == cmp.Controller && Method == cmp.Method;
}
public override int GetHashCode()
{
// Overflow is fine, just wrap
unchecked
{
int hash = 17;
hash = hash * 23 + Role.GetHashCode();
hash = hash * 23 + Controller.GetHashCode();
hash = hash * 23 + Method.GetHashCode();
return hash;
}
}
}
public class AuthorizeExtended : AuthorizeAttribute
{
private static ConcurrentDictionary<CacheKey, bool> cache = new ConcurrentDictionary<CacheKey, bool>();
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var isAuthorized = base.AuthorizeCore(httpContext);
if (!isAuthorized)
{
return false;
}
// Point of interest **HERE**
// Looking up in the cache
var cacheKey = new CacheKey
{
Role = role,
Controller = controller,
Method = method,
};
bool authorized;
if (cache.TryGetValue(cacheKey, out authorized))
{
return authorized;
}
// Make DB call and get value for authorized
// ...
// Store 'authorized' value in the cache
cache.TryAdd(cacheKey, authorized);
return authorized;
}
}
I've been looking around to find 'script' to help me to access properties set at runtime from an attribute on a method. I implemented something, but properties set at runtime have only their default value. Is this even possible? I show you the code I have:
First if all, i have an interface:
public interface IRootController{}
My Controller inherits from a base controller who implements this interface.
In this controller i have a method with an attribute:
[LowerAndUpperDateTime]
public HttpResponseMessage GetAllLastResponding(string cultureinfo, Guid campaignUniqueID, int pageIndex, int pageSize, string lowerDateTime, string upperDateTime, BreakDown breakDown)
{
var test = this.GetAttribute<LowerAndUpperDateTimeAttribute>(GetCurrentMethod());
return null;
}
The code of the attribute is like this:
public class LowerAndUpperDateTimeAttribute : ActionFilterAttribute
{
public DateTime UpperDateTime;
public DateTime LowerDateTime;
public string test = "sfsdfdsf";
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.Count < 1)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest,
new[]{Errors.ObjectRequired});
}
else
{
try
{
var lowerDateTime = string.Empty;
var upperDateTime = string.Empty;
//controleren of de arguments bestaan
if (actionContext.ActionArguments.ContainsKey("lowerDateTime"))
lowerDateTime = actionContext.ActionArguments["lowerDateTime"].ToString();
if (actionContext.ActionArguments.ContainsKey("upperDateTime"))
upperDateTime = actionContext.ActionArguments["upperDateTime"].ToString();
if (string.IsNullOrEmpty(lowerDateTime) || string.IsNullOrEmpty(upperDateTime))
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest,
new[]{Errors.EmptyDateTimes});
}
else{
try
{
LowerDateTime = DateTime.ParseExact(lowerDateTime, "yyyyMMddHHmmss", CultureInfo.InvariantCulture);
UpperDateTime = DateTime.ParseExact(upperDateTime, "yyyyMMddHHmmss", CultureInfo.InvariantCulture);
if (LowerDateTime > UpperDateTime)
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest,
new[]{Errors.LowerDateBiggerTheUpperDate});
}
catch (Exception e)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest,
new[]{e.Message});
}
}
}
catch
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, new[]{Errors.NoAccess});
}
}
}
}
The extensionmethod i created that is used in the method of my controller to access the attribute looks like this:
public static TAttribute GetAttribute<TAttribute>(this IRootController currentclass, string methodname)where TAttribute : Attribute
{
return (TAttribute)currentclass.GetType().GetMethod(methodname).GetCustomAttributes(typeof(TAttribute), false).FirstOrDefault();
}
So this way i can access the attribute, but both datetimes have the defaultvalues, even when they got set.
Is it even possible what i'm trying here and if so, where am i wrong?
Thanks!
I have interception working currently (very simplistically) with the following code:
(see question at bottom)
My Interceptor:
public interface IAuthorizationInterceptor : IInterceptor { }
public class AuthorizationInterceptor : IAuthorizationInterceptor
{
public IParameter[] AttributeParameters { get; private set; }
// This doesnt work currently... paramters has no values
public AuthorizationInterceptor(IParameter[] parameters) {
AttributeParameters = parameters;
}
public void Intercept(IInvocation invocation) {
// I have also tried to get the attributes like this
// which also returns nothing.
var attr = invocation.Request.Method.GetCustomAttributes(true);
try {
BeforeInvoke(invocation);
} catch (AccessViolationException ex) {
} catch (Exception ex) {
throw;
}
// Continue method and/or processing additional attributes
invocation.Proceed();
AfterInvoke(invocation);
}
protected void BeforeInvoke(IInvocation invocation) {
// Enumerate parameters of method call
foreach (var arg in invocation.Request.Arguments) {
// Just a test to see if I can get arguments
}
//TODO: Replace with call to auth system code.
bool isAuthorized = true;
if (isAuthorized == true) {
// Do stuff
}
else {
throw new AccessViolationException("Failed");
}
}
protected void AfterInvoke(IInvocation invocation) {
}
}
My Attribute:
public class AuthorizeAttribute : InterceptAttribute
{
public string[] AttributeParameters { get; private set; }
public AuthorizeAttribute(params string[] parameters) {
AttributeParameters = parameters;
}
public override IInterceptor CreateInterceptor(IProxyRequest request) {
var param = new List<Parameter>();
foreach(string p in AttributeParameters) {
param.Add( new Parameter(p, p, false));
}
// Here I have tried passing ConstructorArgument(s) but the result
// in the inteceptor constructor is the same.
return request.Context.Kernel.Get<IAuthorizationInterceptor>(param.ToArray());
}
}
Applied to a method:
[Authorize("test")]
public virtual Result<Vault> Vault(DateTime date, bool LiveMode = true, int? SnapshotId = null)
{
...
}
This works, and I am able to pass additional parameters through the attribute like this:
[Authorize("test")]
If you'll noticed in my attribute I am grabbing some parameters from the attribute, which I am able to access in the attribute class, but I am unable to pass those to the Interceptor. I have tried using the ConstructorArgument in the Kernel.Get<>() call, which doesnt throw an error, but the AuthorizationInterceptor constructor doesnt get any values from ninject. I have also tried GetCustomAttributes() as you can see in the code sample but this also returns nothing. From looking at other similar posts like this (Ninject Interception 3.0 Interface proxy by method attributes) that seems to be the correct way, but it doesn't work. Any ideas?
I was able to get something working by creating an initialization method on the interceptor. I don't really like it, because it ties me to the specific implementation of AuthorizationInterceptor, but it gets the job done (damn deadlines lol). I would still like to know if there is a better way to do this, so I am not going to mark my own answer in hopes that somebody comes along with a better way of doing this.
I modified the attribute as follows:
public override IInterceptor CreateInterceptor(IProxyRequest request) {
AuthorizationInterceptor attr = (AuthorizationInterceptor)request.Context.Kernel.Get<IAuthorizationInterceptor>();
attr.Init(AttributeParameters);
return attr;
}
And created an Init method on the the interceptor:
public void Init(params string[] parameters) {
AttributeParameters = parameters;
}
I have API where I need to validate my user model. I choose an approach where I create different classes for Create/Edit actions to avoid mass-assignment and divide validation and actual model apart.
I don't know why but ModelState.IsValid returns true even when it should not. Am I doing something wrong?
Controller
public HttpResponseMessage Post(UserCreate user)
{
if (ModelState.IsValid) // It's valid even when user = null
{
var newUser = new User
{
Username = user.Username,
Password = user.Password,
Name = user.Name
};
_db.Users.Add(newUser);
_db.SaveChanges();
return Request.CreateResponse(HttpStatusCode.Created, new { newUser.Id, newUser.Username, newUser.Name });
}
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
Model
public class UserCreate
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string Name { get; set; }
}
Debug proof
The ModelState.IsValid internally checks the Values.All(modelState => modelState.Errors.Count == 0) expression.
Because there was no input the Values collection will be empty so ModelState.IsValid will be true.
So you need to explicitly handle this case with:
if (user != null && ModelState.IsValid)
{
}
Whether this is a good or bad design decision that if you validate nothing it will true is a different question...
Here is an action filter to check for null models or invalid models. (so you dont have to write the check on every action)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace Studio.Lms.TrackingServices.Filters
{
public class ValidateViewModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.Any(kv => kv.Value == null)) {
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Arguments cannot be null");
}
if (actionContext.ModelState.IsValid == false) {
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
}
You can register it globally:
config.Filters.Add(new ValidateViewModelAttribute());
Or use it on demand on classes/actions
[ValidateViewModel]
public class UsersController : ApiController
{ ...
I wrote a custom filter which not only ensures that all non optional object properties are passed, but also checks if model state is valid:
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public sealed class ValidateModelAttribute : ActionFilterAttribute
{
private static readonly ConcurrentDictionary<HttpActionDescriptor, IList<string>> NotNullParameterNames =
new ConcurrentDictionary<HttpActionDescriptor, IList<string>> ();
/// <summary>
/// Occurs before the action method is invoked.
/// </summary>
/// <param name="actionContext">The action context.</param>
public override void OnActionExecuting (HttpActionContext actionContext)
{
var not_null_parameter_names = GetNotNullParameterNames (actionContext);
foreach (var not_null_parameter_name in not_null_parameter_names)
{
object value;
if (!actionContext.ActionArguments.TryGetValue (not_null_parameter_name, out value) || value == null)
actionContext.ModelState.AddModelError (not_null_parameter_name, "Parameter \"" + not_null_parameter_name + "\" was not specified.");
}
if (actionContext.ModelState.IsValid == false)
actionContext.Response = actionContext.Request.CreateErrorResponse (HttpStatusCode.BadRequest, actionContext.ModelState);
}
private static IList<string> GetNotNullParameterNames (HttpActionContext actionContext)
{
var result = NotNullParameterNames.GetOrAdd (actionContext.ActionDescriptor,
descriptor => descriptor.GetParameters ()
.Where (p => !p.IsOptional && p.DefaultValue == null &&
!p.ParameterType.IsValueType &&
p.ParameterType != typeof (string))
.Select (p => p.ParameterName)
.ToList ());
return result;
}
}
And I put it in global filter for all Web API actions:
config.Filters.Add (new ValidateModelAttribute ());
Updated slightly for asp.net core...
[AttributeUsage(AttributeTargets.Method)]
public sealed class CheckRequiredModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var requiredParameters = context.ActionDescriptor.Parameters.Where(
p => ((ControllerParameterDescriptor)p).ParameterInfo.GetCustomAttribute<RequiredModelAttribute>() != null).Select(p => p.Name);
foreach (var argument in context.ActionArguments.Where(a => requiredParameters.Contains(a.Key, StringComparer.Ordinal)))
{
if (argument.Value == null)
{
context.ModelState.AddModelError(argument.Key, $"The argument '{argument.Key}' cannot be null.");
}
}
if (!context.ModelState.IsValid)
{
var errors = context.ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage);
context.Result = new BadRequestObjectResult(errors);
return;
}
base.OnActionExecuting(context);
}
}
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class RequiredModelAttribute : Attribute
{
}
services.AddMvc(options =>
{
options.Filters.Add(typeof(CheckRequiredModelAttribute));
});
public async Task<IActionResult> CreateAsync([FromBody][RequiredModel]RequestModel request, CancellationToken cancellationToken)
{
//...
}
This happened to me, and in my case, I had to change using Microsoft.Build.Framework; to using System.ComponentModel.DataAnnotations; (and add the reference).
I was looking for a solution to this problem and came out here first. After some further research I have realized the following solution:
How do you use my solution?
You can register it globally:
config.Filters.Add(new ValidateModelStateAttribute());
Or use it on demand for a class
[ValidateModelState]
public class UsersController : ApiController
{...
or for a methode
[ValidateModelState]
public IHttpActionResult Create([Required] UserModel data)
{...
As you can see, a [System.ComponentModel.DataAnnotations.Required] atribute has been placed in the method parameter.
This indicates that the model is required and can not be null.
You can also use with a custom message:
[ValidateModelState]
public IHttpActionResult Create([Required(ErrorMessage = "Custom message")] UserModel data)
{...
Here is my code:
using System;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace your_base_namespace.Web.Http.Filters
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)]
public class ValidateModelStateAttribute : ActionFilterAttribute
{
private delegate void ValidateHandler(HttpActionContext actionContext);
private static readonly ConcurrentDictionary<HttpActionBinding, ValidateHandler> _validateActionByActionBinding;
static ValidateModelStateAttribute()
{
_validateActionByActionBinding = new ConcurrentDictionary<HttpActionBinding, ValidateHandler>();
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
GetValidateHandler(actionContext.ActionDescriptor.ActionBinding)(actionContext);
if (actionContext.ModelState.IsValid)
return;
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
private ValidateHandler GetValidateHandler(HttpActionBinding actionBinding)
{
ValidateHandler validateAction;
if (!_validateActionByActionBinding.TryGetValue(actionBinding, out validateAction))
_validateActionByActionBinding.TryAdd(actionBinding, validateAction = CreateValidateHandler(actionBinding));
return validateAction;
}
private ValidateHandler CreateValidateHandler(HttpActionBinding actionBinding)
{
ValidateHandler handler = new ValidateHandler(c => { });
var parameters = actionBinding.ParameterBindings;
for (int i = 0; i < parameters.Length; i++)
{
var parameterDescriptor = (ReflectedHttpParameterDescriptor)parameters[i].Descriptor;
var attribute = parameterDescriptor.ParameterInfo.GetCustomAttribute<RequiredAttribute>(true);
if (attribute != null)
handler += CreateValidateHandler(attribute, parameterDescriptor.ParameterName);
}
return handler;
}
private static ValidateHandler CreateValidateHandler(ValidationAttribute attribute, string name)
{
return CreateValidateHandler(attribute, new ValidationContext(new object()) { MemberName = name });
}
private static ValidateHandler CreateValidateHandler(ValidationAttribute attribute, ValidationContext context)
{
return new ValidateHandler(actionContext =>
{
object value;
actionContext.ActionArguments.TryGetValue(context.MemberName, out value);
var validationResult = attribute.GetValidationResult(value, context);
if (validationResult != null)
actionContext.ModelState.AddModelError(context.MemberName, validationResult.ErrorMessage);
});
}
}
}
There is a simple Solution for your problem
public class UserCreate
{
[Required(AllowEmptyStrings = false)]
public string Username { get; set; }
}
Here AllowEmptyStrings = false can be used for your validation
Try
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
in the startup.cs file's ConfigureServices()
What I did was to create an Attribute along with an ActionFilter and a Extension Method to avoid null models.
The extension method looks for parameters with the NotNull attribute and check if they are null, if true, they are instantiated and set in the ActionArguments property.
This solution can be found here: https://gist.github.com/arielmoraes/63a39a758026b47483c405b77c3e96b9
This "ModelState.IsValid returns true even when it should not" problem can also appear if you forgot to add getters and setters on your model (OP didn't forget, but I did which led me to this question). I hope it's ok to provide solutions that have the same symptoms but are slightly different than OP's code:
Wrong:
public class UserRegisterModel
{
[Required]
public string Login; // WRONG
[Required]
public string Password; // WRONG
}
Good:
public class UserRegisterModel
{
[Required]
public string Login { get; set; }
[Required]
public string Password { get; set; }
}
this issue happened to me .i do not know why but take it easy just change your action Object name(UserCreate User) by some other like (UserCreate User_create)
I'm trying to secure my MVC routes from a set of users that meet a set of criteria. Since MVC seems to use attributes quite a bit and Steven Sanderson uses one for security extensibility in his pro MVC book I started heading down this route, but I'd like to define the rule contextually based on the action I am applying it to.
Some actions are for employees only, some aren't.
Some actions are for company1 only, some aren't.
So I was thinking this type of usage...
[DisableAccess(BlockUsersWhere = u => u.Company != "Acme")]
public ActionResult AcmeOnlyAction()
{
...
}
[DisableAccess(BlockUsersWhere = u => u.IsEmployee == false)]
public ActionResult EmployeeOnlyAction()
{
...
}
Looks pretty clean to me and is really pretty easy to implement, but I get the following compiler error:
'BlockUsersWhere' is not a valid named attribute argument because it is not a valid attribute parameter type
Apparently you can not use a Func as an attribute argument. Any other suggestions to get around this issue or something else that provides the simple usage we've come to love in our MVC projects?
Necros' suggestion would work, however you would have to invoke his SecurityGuard helper in the body of every action method.
If you would still like to go with the declarative attribute-based approach (which has the advantage that you can apply the attribute to the whole Controller) you could write your own AuthorizeAttribute
public class CustomAuthorizeAttribute : AuthorizeAttribute {
public bool EmployeeOnly { get; set; }
private string _company;
public string Company {
get { return _company; }
set { _company = value; }
}
protected override bool AuthorizeCore(HttpContextBase httpContext) {
return base.AuthorizeCore(httpContext) && MyAuthorizationCheck(httpContext);
}
private bool MyAuthorizationCheck(HttpContextBase httpContext) {
IPrincipal user = httpContext.User;
if (EmployeeOnly && !VerifyUserIsEmployee(user)) {
return false;
}
if (!String.IsNullOrEmpty(Company) && !VerifyUserIsInCompany(user)) {
return false;
}
return true;
}
private bool VerifyUserIsInCompany(IPrincipal user) {
// your check here
}
private bool VerifyUserIsEmployee(IPrincipal user) {
// your check here
}
}
Then you would use it as follows
[CustomAuthorize(Company = "Acme")]
public ActionResult AcmeOnlyAction()
{
...
}
[CustomAuthorize(EmployeeOnly = true)]
public ActionResult EmployeeOnlyAction()
{
...
}
Since you can only use constants, types or array initializers in attribute parameters, they probably won't do, or at least the won't be as flexible.
Alternatively, you could use something similar I came up with when solving this problem.
This is the API:
public static class SecurityGuard
{
private const string ExceptionText = "Permission denied.";
public static bool Require(Action<ISecurityExpression> action)
{
var expression = new SecurityExpressionBuilder();
action.Invoke(expression);
return expression.Eval();
}
public static bool RequireOne(Action<ISecurityExpression> action)
{
var expression = new SecurityExpressionBuilder();
action.Invoke(expression);
return expression.EvalAny();
}
public static void ExcpetionIf(Action<ISecurityExpression> action)
{
var expression = new SecurityExpressionBuilder();
action.Invoke(expression);
if(expression.Eval())
{
throw new SecurityException(ExceptionText);
}
}
}
public interface ISecurityExpression
{
ISecurityExpression UserWorksForCompany(string company);
ISecurityExpression IsTrue(bool expression);
}
Then create an expression builder:
public class SecurityExpressionBuilder : ISecurityExpression
{
private readonly List<SecurityExpression> _expressions;
public SecurityExpressionBuilder()
{
_expressions = new List<SecurityExpression>();
}
public ISecurityExpression UserWorksForCompany(string company)
{
var expression = new CompanySecurityExpression(company);
_expressions.Add(expression);
return this;
}
public ISecurityExpression IsTrue(bool expr)
{
var expression = new BooleanSecurityExpression(expr);
_expressions.Add(expression);
return this;
}
public bool Eval()
{
return _expressions.All(e => e.Eval());
}
public bool EvalAny()
{
return _expressions.Any(e => e.Eval());
}
}
Implement the security expressions:
internal abstract class SecurityExpression
{
public abstract bool Eval();
}
internal class BooleanSecurityExpression : SecurityExpression
{
private readonly bool _result;
public BooleanSecurityExpression(bool expression)
{
_result = expression;
}
public override bool Eval()
{
return _result;
}
}
internal class CompanySecurityExpression : SecurityExpression
{
private readonly string _company;
public CompanySecurityExpression(string company)
{
_company = company;
}
public override bool Eval()
{
return (WhereverYouGetUser).Company == company;
}
}
You can add as many custom expressions as you need. The infrastructure is a bit complicated, but then usage is really simple:
public ActionResult AcmeOnlyAction()
{
SecurityGuard.ExceptionIf(s => s.UserWorksForCompany("Acme"));
}
You can also chain the expression, and use it as a condition in view fro example (using SecurityGuard.Require()).
Sry for long post, hope this helps.