Is it possible to bypass the authorization role check on a controller, but enforce the role check on an action? I've spent a bit of time researching this and everything I find shows how to implement an AllowAnonymousAttribute. I'm currently using the AllowAnonymousAttribute and it works great for completely bypassing authorization for an action. That isn't what I want. I have a controller that requires certain roles. When a particular action is requested I want to skip the roles at the controller level and just verify user has the roles designated on the action.
Here's some code:
[Authorize(Roles="Administrator")]
public class MembersController : ViewApiController<MemberView>
{
// a list of actions....
[Authorize(Roles="ApiUser")]
[HttpPost]
public void AutoPayPost([FromBody] List<AutoPayModel> autoPayList)
{
//....
}
}
The problem is I want users with just the 'ApiUser' role to have access to the 'AutoPayPost' action. I realize I can remove the class level authorize attribute, then add it to every action method on my controller, minus the 'AutoPayPost' action. I would like to avoid this because several of my controllers inherit from a base class that provides a long list of actions that require the 'Administrative' role. Because of that I would have to override every base action, add the Authorize attribute to the overridden method, then delegate the call back to the base class. This WILL work but if I later decide to add functionality to the base class I'll have to remember to go back to the MembersController and override the new methods, add the attribute etc...
It would be great if the end result looked like this:
[Authorize(Roles="Administrator")]
public class MembersController : ViewApiController<MemberView>
{
// a list of actions....
[Authorize(Roles="ApiUser", IgnoreControllerRoles=true)]
[HttpPost]
public void AutoPayPost([FromBody] List<AutoPayModel> autoPayList)
{
//....
}
}
Do something like this, where you will check if the roles/users are in the roles and then deny any of them.
public class ByPassAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
string[] roles = this.Roles.Split(',');
string[] users = this.Users.Split(',');
foreach (var r in roles)
{
if (httpContext.User.IsInRole(r.Trim()))
return false;
}
foreach (var u in users)
{
if (httpContext.User.Identity.Name.Equals(u))
return false;
}
return base.AuthorizeCore(httpContext);
}
}
And then decore your controller/action like this:
[ByPassAuthorize(Roles = "Admin,test,testint", Users = "Tester")]
public ActionResult Edit(int id = 0)
{
FooModel foomodel = db.FooModels.Find(id);
if (foomodel == null)
{
return HttpNotFound();
}
return View(foomodel);
}
Hope its help you!
If I understand you correctly, you could implement a custom ByPassControllerChecksAttribute (it is for decorating methods that you want to allow "passthrough" access to), then in your LogonAuthorizeAttribute retrieve the action method being called by this request and check if its custom attribute collection has an instance of ByPassControllerChecksAttribute. If it does, run the code that checks if the user is allowed access to the method, otherwise run the code that checks if the user is allowed access to the controller. Of course if you have just one method and the name is known not to change, you can bypass the extra attribute and just check for the name, but of course the first method is much better.
EDIT
If your LogonAuthorizeAttribute inherits from AuthorizeAttribute then you can override the AuthorizeCore method which returns a boolean (true meaning the user is authorized, false otherwise). In this method you can have something along the following pseudocode:
if(CheckIfMethodHasByPassAttribute()){
return CheckIfUserIsAllowedToRunThisMethod();
}
return CheckIfUserIsAllowedToRunThisController();
The method CheckIfUserIsAllowedToRunThisMethod would have whatever checks you need to do to determine if a user is allowed to run this method, while the CheckIfUserIsAllowedToRunThisController would have the code to check if a user is allowed access to the controller in general (which I assume is already in you LogonAuthorizeAttribute)
Related
So I've created a custom authorize attribute I use in a few places that is derived from an abstract base class which is derived from AuthorizeAttribute:
CustomAuthorizeAttributeBase.cs
public abstract class CustomAuthorizeAttributeBase : AuthorizeAttribute
{
public abstract string GetUsers();
public abstract string GetRoles();
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.IsChildAction)
{
return;
}
filterContext.Result =
new RedirectToRouteResult(new RouteValueDictionary
{
{"controller", "NotAuthorized"},
{"action", "Index"},
});
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (GetUsers().IndexOf(httpContext.User.Identity.Name, StringComparison.CurrentCultureIgnoreCase) >= 0 ||
GetRoles().Split(',').Any(s => httpContext.User.IsInRole(s)))
{
return true;
}
return false;
}
}
AreaLevelReadonly.cs
public class AreaLevelReadOnly : CustomAuthorizeAttributeBase
{
public override string GetUsers()
{
return ConfigurationManager.AppSettings["AreaReadonlyUsers"];
}
public override string GetRoles()
{
return ConfigurationManager.AppSettings["AreaReadonlyRoles"];
}
}
I also have some fairly simple code that gets me the currently logged in user:
UserIdentity.cs
public class UserIdentity : IUserIdentity
{
public string GetUserName()
{
return HttpContext.Current.User.Identity.Name.Split('\\')[1];
}
}
However, when I add my AreaLevelReadonly attribute to my controllers, getUserName fails and returns an exception that Name is null. I agonized over it for about an hour before putting authorize attribute on there as well, at which point it magically started working again. So, what is so different on the implementation level that my attribute deriving from authorizeattribute doesn't cause the Name to be populated.
Note: Windows authentication is on for the area, and the code works, but I don't understand why the Readonly attribute isn't enough to trigger authorization and population of the HttpContext.Current.User.Identity.Name.
Edit: Working:
[AreaLevelReadonly]
[Authorize]
public class DeleteAreaDataController : Controller {
//etc
var username = _userIdentity.GetUserName(HttpContext);
//etc
}
Exception on name:
[AreaLevelReadonly]
public class DeleteAreaDataController : Controller {
//etc
var username = _userIdentity.GetUserName(HttpContext);
//etc
}
More likely than not, you're accessing User.Identity.Name before it's populated. By including the standard Authorize attribute, as well, your code is then only running after the user has been authorized already and User.Identity.Name has been populated.
EDIT
Sorry, I misunderstood where the code attempting to call User.Identity.Name was running. Based on the belief that it was happening in your custom attribute, I was suggesting that you're trying to access it too early. However, I now see that you're calling it in your controller (although an explanation of what happens in GetUserAccount(HttpContext) would have helped.)
Anyways, your custom attribute obviously adds extra conditions on whether a user is authorized or not. When you return false, there is no user. It's not a situation where the user is "logged in" but not allowed to see the page. It's either there or it isn't. So the user is failing authorization based on your custom attribute (User.Identity.Name is null) but is authorized when you include Authorize (User.Identity.Name has a value).
Long and short, your GetUserName or GetUserAccount or whatever code needs to account for when the user has failed authorization. Or, if the user shouldn't be failing authorization, you'll need to look into why your custom attribute isn't working. Though, either way, you should still account for User.Identity.Name being null.
Your custom attribute is probably reading User.Identity.Name before you check that the user is authenticated.
In other words, in IsAuthorized(), before you read User.Identity.Name, you should be doing something like this:
if (!user.Identity.IsAuthenticated)
{
// Your custom code...
return false;
}
The reason you need this is because Windows Authentication (at least for NTLM) is a 2-step negotiation process between the client and server (see https://support.citrix.com/article/CTX221693). There will be 2 requests - the first with no name, and the second with a name. You can test this yourself - the source code for AuthorizeAttribute is provided here. Copy/paste that into your code and put a breakpoint in IsAuthorized - you will see that the breakpoint is hit twice. First time, the name is null, second time, it's set to your username.
So I think the solution is to either check user.Identity.IsAuthenticated at the start of your method, if you need to run custom code (as shown above), or alternatively if you only need to return false, simply replace the above code with base.IsAuthorized() which should do it for you.
I'm looking for recommendations on how to have multiple authorize attributes on an action.
eg:
[AuthorizePermission(PermissionName.SectionOne, PermissionLevel.Two)]
[AuthorizePermission(PermissionName.SectionTwo, PermissionLevel.Three)]
public ActionResult Index(int userId = 0){
}
If the user has access to SectionOne OR SectionTwo with the required PermissionLevel then they should be allowed in.
The problem i'm facing is how do I check both attributes before deciding they aren't allowed in (as they are separate attributes)? If the first one fails then it will never get to the second one.
I can not pass both permission sets to one attribute as they need to be paired together.
Does anyone have any suggestions?
I can not pass both permission sets to one attribute as they need to be paired together.
Yes, you can.
There is no reason why you can't include all the permissions in a single attribute. Something like this:
[AuthorizePermission(new Permission[]{
new Permission(PermissionName.SectionOne, PermissionLevel.Two),
new Permission(PermissionName.SectionTwo, PermissionLevel.Three)}]
This would pass an array of Permission objects, which you can then evaluate in your method with OR logic.
public class AuthorizePermissionAttribute : AuthorizeAttribute
{
private Permission[] _permissions = null;
public AuthorizePermissionAttribute(Permission[] permissions)
{
_permissions = permissions;
}
}
You could even get fancy and add a parameter that tells whether to AND or OR them...
The only way that I know is something like this
public class CustomRolesAttribute : AuthorizeAttribute
{
public CustomRolesAttribute(params string[] roles)
{
Roles = String.Join(",", roles);
}
}
Usage:
[CustomRoles("members", "admin")]
I'm not sure if this is the correct way to go about the problem I need to solve... however in an OnActionExecuting action filter that I have created, I set a cookie with various values. One of these values is used to determine whether the user is visiting the website for the very first time. If they are a new visitor then I set the ViewBag with some data so that I can display this within my view.
The problem I have is that in some of my controller actions I perform a RedirectToAction. The result is OnActionExecuting is fired twice, once for the original action and then a second time when it fires the new action.
<HttpGet()>
Function Index(ByVal PageID As String) As ActionResult
Dim wo As WebPage = Nothing
Try
wp = WebPages.GetWebPage(PageID)
Catch sqlex As SqlException
Throw
Catch ex As Exception
Return RedirectToAction("Index", New With {.PageID = "Home"})
End If
End Try
Return View("WebPage", wp)
End Function
This is a typical example. I have a data driven website that gets a webpage from the database based on the PageID specified. If the page cannot be found in the database I redirect the user to the home page.
Is it possible to prevent the double firing in anyway or is there a better way to set a cookie? The action filter is used on multiple controllers.
Had the same issue. Resolved by overriding property AllowMultiple:
public override bool AllowMultiple { get { return false; } }
public override void OnActionExecuting(HttpActionContext actionContext)
{
//your logic here
base.OnActionExecuting(actionContext);
}
You can save some flag value into TempData collection of controller on first executing and if this value presented, skip filter logic:
if (filterContext.Controller.TempData["MyActionFilterAttribute_OnActionExecuting"] == null)
{
filterContext.Controller.TempData["MyActionFilterAttribute_OnActionExecuting"] = true;
}
You could return the actual action instead of redirecting to the new action. That way, you dont cause an http-request, thereby not triggering the onactionexecuting (i believe)
Old question, but I just dealt with this so I thought I'd throw in my answer. After some investigating I disovered this was only happening on endpoints that returned a view (i.e. return View()). The only endpoints that had multiple OnActionExecuting fired were HTML views that were composed of partial views (i.e. return PartialView(...)), so a single request was "executing" multiple times.
I was applying my ActionFilterAttribute globally to all endpoints, which was working correctly on all other endpoints except for the view endpoints I just described. The solution was to create an additional attribute applied conditionally to the partial view endpoints.
// Used specifically to ignore the GlobalFilterAttribute filter on an endpoint
public class IgnoreGlobalFilterAttribute : Attribute { }
public class GlobalFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Does not apply to endpoints decorated with Ignore attribute
if (!filterContext.ActionDescriptor.GetCustomAttributes(typeof(IgnoreGlobalFilterAttribute), false).Any())
{
// ... attribute logic here
}
}
}
And then on my partial view endpoints
[HttpGet]
[AllowAnonymous]
[IgnoreGlobalFilter] //HERE this keeps the attribute from firing again
public ActionResult GetPartialView()
{
// partial view logic
return PartialView();
}
What is the correct way to restrict access to a controller?
For instance, I might have "ProductReviewController", I want to be able to check that this controller is accessible in the current store and is enabled. I'm not after the code to do that but am interested in the approach to stopping the user getting to the controller should this criteria not be met. I would like the request to just carry on as if the controller was never there (so perhaps throwing a 404).
My thoughts so far:
A data annotation i.e [IsValidController]. Which Attribute class would I derive from - Authorize doesn't really seem to fit and I would associate this with user authentication. Also, I'm not sure what the correct response would be if the criteria wasn't met (but I guess this would depend on the Attribute it's deriving from). I could put this data annotation against my base controller.
Find somewhere lower down in the page life cycle and stop the user hitting the controller at all if the controller doesn't meet my criteria. i.e Create my own controller factory as depicted in point 7 here: http://blogs.msdn.com/b/varunm/archive/2013/10/03/understanding-of-mvc-page-life-cycle.aspx
What is the best approach for this?
Note: At the moment, I am leaning towards option 1 and using AuthorizeAttribute with something like the code below. I feel like I am misusing the AuthorizeAttribute though.
public class IsControllerAccessible : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (!CriteriaMet())
return false;
return true;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(
new
{
controller = "Generic",
action = "404"
})
);
}
}
I think you are confused about AuthorizeAttribute. It is an Action Filter, not a Data Annotation. Data Annotations decorate model properties for validatioj, Action Filter's decorate controller actions to examine the controller's context and doing something before the action executes.
So, restricting access to a controller action is the raison d'etre of the AuthorizeAttribute, so let's use it!
With the help of the good folks of SO, I created a customer Action Filter that restricted access to actions (and even controllers) based on being part of an Access Directory group:
public class AuthorizeADAttribute : AuthorizeAttribute
{
public string Groups { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (base.AuthorizeCore(httpContext))
{
/* Return true immediately if the authorization is not
locked down to any particular AD group */
if (String.IsNullOrEmpty(Groups))
return true;
// Get the AD groups
var groups = Groups.Split(',').ToList<string>();
// Verify that the user is in the given AD group (if any)
var context = new PrincipalContext(ContextType.Domain, "YOURADCONTROLLER");
var userPrincipal = UserPrincipal.FindByIdentity(context,
IdentityType.SamAccountName,
httpContext.User.Identity.Name);
foreach (var group in groups)
{
try
{
if (userPrincipal.IsMemberOf(context, IdentityType.Name, group))
return true;
}
catch (NoMatchingPrincipalException exc)
{
var msg = String.Format("While authenticating a user, the operation failed due to the group {0} could not be found in Active Directory.", group);
System.ApplicationException e = new System.ApplicationException(msg, exc);
ErrorSignal.FromCurrentContext().Raise(e);
return false;
}
catch (Exception exc)
{
var msg = "While authenticating a user, the operation failed.";
System.ApplicationException e = new System.ApplicationException(msg, exc);
ErrorSignal.FromCurrentContext().Raise(e);
return false;
}
}
}
return false;
}
}
Note this will return a 401 Unauthorized, which makes sense, and not the 404 Not Found you indicated above.
Now, the magic in this is you can restrict access by applying it at the action level:
[AuthorizeAD(Groups = "Editor,Contributer")]
public ActionResult Create()
Or at the controller level:
[AuthorizeAD(Groups = "Admin")]
public class AdminController : Controller
Or even globally by editing FilterConfig.cs in `/App_Start':
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new Code.Filters.MVC.AuthorizeADAttribute() { Groups = "User, Editor, Contributor, Admin" });
}
Complete awesome sauce!
P.S. You mention page lifecycle in your second point. There is no such thing in MVC, at least not in the Web Forms sense you might be thinking. That's a good thing to my mind, as things are greatly simplified, and I don't have to remember a dozen or so different lifecycle events and what the heck each one of them is raised for!
On my controller I have it inherit a MainController and there I override the Initialize and the OnActionExecuting.
Here I see what is the URL and by that I can check what Client is it, but I learned that for every Method called, this is fired up again and again, even a simple redirectToAction will fire the Initialization of the same controller.
Is there a better technique to avoid this repetition of database call? I'm using Entity Framework, so it will take no time to call the DB as it has the result in cache already, but ... just to know if there is a better technique now in MVC3 rather that host the variables in a Session Variable
sample code
public class MyController : MainController
{
public ActionResult Index()
{
return View();
}
}
public class MainController : Controller
{
public OS_Clients currentClient { get; set; }
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
// get URL Info
string url = requestContext.HttpContext.Request.Url.AbsoluteUri;
string action = requestContext.RouteData.GetRequiredString("action");
string controller = requestContext.RouteData.GetRequiredString("controller");
object _clientUrl = requestContext.RouteData.Values["cliurl"];
if (_clientUrl != null && _clientUrl.ToString() != "none")
{
// Fill up variables
this.currrentClient = db.FindClientById(_clientUrl.ToString());
}
base.Initialize(requestContext);
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
// based on client and other variables, redirect to Disable or Login Actions
// ... more code here like:
// filterContext.Result = RedirectToAction("Login", "My");
base.OnActionExecuting(filterContext);
}
}
is it still best to do as:
public OS_Clients currentClient {
get {
OS_Clients _currentClient = null;
if (Session["CurrentClient"] != null)
_currentClient = (OS_Clients)Session["CurrentClient"];
return _currentClient;
}
set {
Session["CurrentClient"] = value;
}
}
It seems that you dealing with application security in that case I would suggest to create Authorization filter, which comes much early into the action. You can put your permission checking code over there and the framework will automatically redirect the user to login page if the permission does not meet AuthorizeCore.
Next, if the user has permission you can use the HttpContext.Items as a request level cache. And then you can create another ActionFilter and in action executing or you can use the base controller to get the user from the Httpcontext.items and assign it to controller property.
If you are using asp.net mvc 3 then you can use the GlobalFilters to register the above mentioned filters instead of decorating each controller.
Hope that helps.
In your base controller, you need to cache the result of the first call in a Session variable.
This makes sure the back-end (DB) is not called unnecessarily, and that the data is bound to the user's Session instead of shared across users, as would be the case with the Application Cache.