I have a custom RouteBase, MyRoute which I want to work for an area "MyArea" which contains code like:
public override GetRouteData(HttpContextBase httpContext)
{
var result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("area", "MyArea");
result.Values.Add("controller", "MyController");
result.Values.Add("action", "Index");
}
I register this in the MyAreaAreaRegistration.cs file:
public override string AreaName { get { return "MyArea"; } }
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.Add(new MyRoute());
// other routes
context.MapRoute(/* ... */);
}
and when making requests, it successfully calls the Index action on MyController:
public ActionResult Index()
{
return this.View();
}
However, MVC doesn't search in the correct folders for the view:
The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/MyController/Index.aspx
~/Views/MyController/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
~/Views/MyController/Index.cshtml
~/Views/MyController/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml
when the view is located in
~/Areas/MyArea/Views/MyController/Index.cshtml
How can I make MVC search in the correct area?
If you look at the source for AreaRegistrationContext.MapRoute you can see that it treats the area differently to the other route variables:
public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces)
{
if (namespaces == null && this.Namespaces != null)
{
namespaces = this.Namespaces.ToArray<string>();
}
Route route = this.Routes.MapRoute(name, url, defaults, constraints, namespaces);
route.DataTokens["area"] = this.AreaName; // *** HERE! ***
bool flag = namespaces == null || namespaces.Length == 0;
route.DataTokens["UseNamespaceFallback"] = flag;
return route;
}
where this.AreaName is populated from the AreaRegistration.
So a quick fix for the problem is to replace the call:
result.Values.Add("area", "MyArea");
with
result.DataTokens["area"] = "MyArea";
Related
While implementing a WebApplication we came across an dubious entry in the RouteValueDictionary with the key "RouteModels".
Even Google doesn't give me results for that entry.
Can someone explain where it comes from?
Does it come from the custom routes?
Or are this the values bound by modelbinding?
The declaration of the ActionMethode at the controller is:
[Route("User/{user}")]
public ActionResult UserForums(User user, int page = 1)
The RouteValueDictionary of the Request contains the following entries:
Does it come from the custom routes?
Unclear. However, route values are completely dependent on how a custom route class is designed.
public class SampleRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var path = httpContext.Request.Path.Substring(1);
if (path.Equals("the/virtual/path"))
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["user"] = "TheUser";
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "Custom";
routeData.Values["RouteModels"] = new List<RouteModel> { new RouteModel { Name = "Foo" }, new RouteModel { Name = "Bar" } };
routeData.DataTokens["MetaData"] = "SomeMetadata";
return routeData;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
string user = values["user"] as string;
string controller = values["controller"] as string;
string action = values["action"] as string;
IEnumerable<RouteModel> routeModels = values["RouteModels"] as IEnumerable<RouteModel>;
if ("TheUser".Equals(user) && "Home".Equals(controller) && "Custom".Equals(action))
{
// Use the route models to either complete the match or to do something else.
return new VirtualPathData(this, "the/virtual/path");
}
return null;
}
private class RouteModel
{
public string Name { get; set; }
}
}
My guess is that this is either some specialized route or filter that is storing extra data in the route collection. But why the author chose to store it as route values rather than DataTokens (for metadata) is a mystery.
I'm using a custom filter (defined as follows):
if (user == null || !user.Active)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary
{
{"controller", "Home"},
{"action", "NotAuthorized"}
});
}
base.OnActionExecuting(filterContext);
This is run site-wide (in RegisterGlobalFilters() within FilterConfig.cs. However, there is one page I'd like to allow access to - the NotAuthorized page. In the HomeController, I have created the following ActionResult method:
[AllowAnonymous]
public ActionResult NotAuthorized()
{
return View();
}
Being unauthorized does lead the user to this view, but it results in a redirect loop (likely because the filter is still being run on this page).
How can I allow anonymous users to access this page?
You need to check for the attribute in your custom filter.
Try:
if (!filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false)
&& !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false)
&& (user == null || !user.Active))
{
//....
}
Check for the AllowAnonymousAttribute in your custom filter. Here is one, resuable way to do it.
Add the following extension method.
public static class MyExtensionMethods
{
public static bool HasAttribute(this ActionExecutingContext context, Type attribute)
{
var actionDesc = context.ActionDescriptor;
var controllerDesc = actionDesc.ControllerDescriptor;
bool allowAnon =
actionDesc.IsDefined(attribute, true) ||
controllerDesc.IsDefined(attribute, true);
return allowAnon;
}
}
Then use it in your filter.
public class MyActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// use the extension method in your filter
if (filterContext.HasAttribute(typeof(AllowAnonymousAttribute)))
{
// exit early...
return;
}
// ...or do whatever else you need to do
if (user == null || !user.Active)
{
filterContext.Result =
new RedirectToRouteResult(new RouteValueDictionary
{
{ "controller", "Home" },
{ "action", "NotAuthorized" }
});
}
base.OnActionExecuting(filterContext);
}
}
Here is a fiddle that implements a solution.
I want to show/hide Edit/Delete links (including menu items) depending on user's authorization. I have implemented AuthorizeAttribute and have custom logic for Roles checking in overriden AuthorizeCore. I would like to use that logic when checking whether user has permissions to view edit/delete links inside the LinkExtensions method.
This is my setup:
public class AuthorizeActivity : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
}
protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
{
bool isAuthorized = base.AuthorizeCore(httpContext);
string actionType = httpContext.Request.HttpMethod;
string controller = httpContext.Request.RequestContext.RouteData.Values["controller"].ToString();
string action = httpContext.Request.RequestContext.RouteData.Values["action"].ToString();
//ADMINS
if (controller == "Admin")
{
if (httpContext.User.IsInRole(Constants.Admin))
return true;
}
else
{
//DATA READERS ONLY
if ((action == "Details") || (action == "Index"))
{
if (httpContext.User.IsInRole(Constants.DataReader))
return true;
}
//DATA WRITERS & IT
else
{
...
}
}
return false;
}
Also I used Vivien Chevallier's logic for creating authorized action link extension outlined here: http://vivien-chevallier.com/Articles/create-an-authorized-action-link-extension-for-aspnet-mvc-3
Now in my view I can use:
<li>#Html.ActionLinkAuthorized("Admin", "Index", "Admin",false) </li>
And the link will either show up or not depending on user's rights.
In my controller the action is decorated with:
[AuthorizeActivity]
public ActionResult Index()
{
return View(view);
}
The authorized link will not work unless I also specify 'Roles' in the attribute which I believe is redundant, like so:
[AuthorizeActivity(Roles = Constants.roleSalesContractAdmin)]
public ActionResult Index()
{
return View(view);
}
I cant seem to find a way to reuse the logic in AuthorizeAttribute. Ideally it would be called in the ActionLinkAuthorized like Vivien's have it:
public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool showActionLinkAsDisabled)
{
if (htmlHelper.ActionAuthorized(actionName, controllerName)) //The call to verify here -- or inside ActionAuthorized
{
return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
}
else
{
if (showActionLinkAsDisabled)
{
TagBuilder tagBuilder = new TagBuilder("span");
tagBuilder.InnerHtml = linkText;
return MvcHtmlString.Create(tagBuilder.ToString());
}
else
{
return MvcHtmlString.Empty;
}
}
}
This is ActionAuthorized method. The OnAuthorization call does not go to the customized one
public static bool ActionAuthorized(this HtmlHelper htmlHelper, string actionName, string controllerName)
{
ControllerBase controllerBase = string.IsNullOrEmpty(controllerName) ? htmlHelper.ViewContext.Controller : htmlHelper.GetControllerByName(controllerName);
ControllerContext controllerContext = new ControllerContext(htmlHelper.ViewContext.RequestContext, controllerBase);
ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(controllerContext.Controller.GetType());
ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
if (actionDescriptor == null)
return false;
FilterInfo filters = new FilterInfo(FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor));
AuthorizationContext authorizationContext = new AuthorizationContext(controllerContext, actionDescriptor);
foreach (IAuthorizationFilter authorizationFilter in filters.AuthorizationFilters)
{
authorizationFilter.OnAuthorization(authorizationContext); //This call
if (authorizationContext.Result != null)
return false;
}
return true;
}
In your view, you can write:
#if (User.IsInRole("role"))
{
<li>#Html.ActionLink("Words", "View", "Controller")</li>
<li>#Html.ActionLink("Words", "View", "Controller")</li>
}
... and assuming they're logged in, it will conditionally hide the links
When you decorate an action or a controller with an authorization attribute, the action is executed only when user is authorized. This means that if the user is not authorized the view(which is going to contain all your authorized link extensions) won't be render at all.
Because of this you need to separate between authorization logic in your attribute and your logic for html extensions.
I also noticed that in authorization core of your attribute you are doing the following:
if ((action == "Details") || (action == "Index"))
{
if (httpContext.User.IsInRole(Constants.DataReader))
return true;
}
It is really-really bad idea! You should not specify action names in your authorize core logic!
All you need to do is to decorate "Details" and "Index" methods with default authorize attribute that has an appropriate roles:
[Authorize(Roles=Constants.DataReader)]
public ActionResult Index()
{
}
Now regarding the role dependent helpers:
you could do something like that:
public static MvcHtmlString ActionLinkAuthorized(this HtmlHelper htmlHelper, string roles, other arguments)
{
//assuming that roles are passed as coma separated strings
var rolesList = roles.Split(",",roles);
bool shouldShow = false;
foreach(var role in rolesList )
{
if (HttpContext.User.IsInRole(role))
{
shouldShow = true;
break;
}
}
if(shouldShow)
{
//return your extension representation
}
else
{
//fallback
}
}
I had a similar problem
I solved this way:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class MyAuthorizedAttribute : AuthorizeAttribute
{
public bool CheckPermissions(HttpContextBase httpContext, string controller, string action)
{
bool authorized;
//Validate User permissions of the way you think is best
return authorized;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
var action = filterContext.ActionDescriptor.ActionName;
var controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
if (filterContext == null)
{
throw new ArgumentNullException(nameof(filterContext));
}
if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
{
// If a child action cache block is active, we need to fail immediately, even if authorization
// would have succeeded. The reason is that there's no way to hook a callback to rerun
// authorization before the fragment is served from the cache, so we can't guarantee that this
// filter will be re-run on subsequent requests.
throw new InvalidOperationException("AuthorizeAttribute Cannot Use Within Child Action Cache");
}
var skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true)
||
filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(
typeof (AllowAnonymousAttribute), true);
if (skipAuthorization)
{
return;
}
if (AuthorizeCore(filterContext.HttpContext) && CheckPermissions(filterContext.HttpContext, controller, action))
{
// ** IMPORTANT **
// Since we're performing authorization at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether a page should be served from the cache.
var cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
else
{
HandleUnauthorizedRequest(filterContext);
}
}
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
base.HandleUnauthorizedRequest(filterContext);
}
else
{
filterContext.Result =
new RedirectToRouteResult(
new RouteValueDictionary(new {controller = "Error", action = "Unauthorized"}));
}
}
}
This way the logic of Vivien Chevallier worked perfectly
I'm developing an application and I'm securing it using Fluent Security.
I have 2 different areas within the web application, namely an Admin area and a Members area.
I implemented my Policies Violation Handlers following the guidelines in the fluent security website.
Everything works fine except that when an exception is thrown I need to redirect to an error page. These are different depending on which area you are. For instance, the ForbiddenAccess error page from the Admin area is different than the one from the Members Area.
My Implementation of IPolicyViolationHandler is the following.
public class DenyAnonymousAccessPolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
return
new RedirectToRouteResult(
new RouteValueDictionary(new { action = "AnonymousError", controller = "Error", area = "Admin" }));
}
}
I need to get where the error occurs so that when I redirect my area is either "
Admin" or "Members" and I can't figure out how to get that information from the exception. If I can get that information, redirecting to the appropriate page is trivial.
Any help, suggestions, or if not possible, a workaround is appreciated.
Thanks,
It is currently not possible to extract that information from FluentSecurity but it should be really easy to do anyway.
Below are two extensionmethods that should help you get the current area name.
public static string GetAreaName(this RouteData routeData)
{
object value;
if (routeData.DataTokens.TryGetValue("area", out value))
{
return (value as string);
}
return GetAreaName(routeData.Route);
}
public static string GetAreaName(this RouteBase route)
{
var areRoute = route as IRouteWithArea;
if (areRoute != null)
{
return areRoute.Area;
}
var standardRoute = route as Route;
if ((standardRoute != null) && (standardRoute.DataTokens != null))
{
return (standardRoute.DataTokens["area"] as string) ?? string.Empty;
}
return string.Empty;
}
An here's how you would use it:
public class DenyAnonymousAccessPolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(HttpContext.Current));
var areaName = routeData.GetAreaName();
return
new RedirectToRouteResult(
new RouteValueDictionary(new { action = "AnonymousError", controller = "Error", area = areaName }));
}
}
Feel free to add a feature request for it.
How do you get the current area name in the view or controller?
Is there anything like ViewContext.RouteData.Values["controller"] for areas?
From MVC2 onwards you can use ViewContext.RouteData.DataTokens["area"]
HttpContext.Current.Request.RequestContext.RouteData.DataTokens["area"]
You can get it from the controller using:
ControllerContext.RouteData.DataTokens["area"]
In ASP.NET Core 1.0 the value is found in
ViewContext.RouteData.Values["area"];
I just wrote a blog entry about this, you can visit that for more details, but my answer was to create an Extension Method, shown below.
The key kicker was that you pull the MVC Area from the .DataTokens and the controller/action from the .Values of the RouteData.
public static MvcHtmlString TopMenuLink(this HtmlHelper htmlHelper, string linkText, string controller, string action, string area, string anchorTitle)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var url = urlHelper.Action(action, controller, new { #area = area });
var anchor = new TagBuilder("a");
anchor.InnerHtml = HttpUtility.HtmlEncode(linkText);
anchor.MergeAttribute("href", url);
anchor.Attributes.Add("title", anchorTitle);
var listItem = new TagBuilder("li");
listItem.InnerHtml = anchor.ToString(TagRenderMode.Normal);
if (CheckForActiveItem(htmlHelper, controller, action, area))
listItem.GenerateId("menu_active");
return MvcHtmlString.Create(listItem.ToString(TagRenderMode.Normal));
}
private static bool CheckForActiveItem(HtmlHelper htmlHelper, string controller, string action, string area)
{
if (!CheckIfTokenMatches(htmlHelper, area, "area"))
return false;
if (!CheckIfValueMatches(htmlHelper, controller, "controller"))
return false;
return CheckIfValueMatches(htmlHelper, action, "action");
}
private static bool CheckIfValueMatches(HtmlHelper htmlHelper, string item, string dataToken)
{
var routeData = (string)htmlHelper.ViewContext.RouteData.Values[dataToken];
if (routeData == null) return string.IsNullOrEmpty(item);
return routeData == item;
}
private static bool CheckIfTokenMatches(HtmlHelper htmlHelper, string item, string dataToken)
{
var routeData = (string)htmlHelper.ViewContext.RouteData.DataTokens[dataToken];
if (dataToken == "action" && item == "Index" && string.IsNullOrEmpty(routeData))
return true;
if (dataToken == "controller" && item == "Home" && string.IsNullOrEmpty(routeData))
return true;
if (routeData == null) return string.IsNullOrEmpty(item);
return routeData == item;
}
Then you can implement it as below :
<ul id="menu">
#Html.TopMenuLink("Dashboard", "Home", "Index", "", "Click here for the dashboard.")
#Html.TopMenuLink("Courses", "Home", "Index", "Courses", "List of our Courses.")
</ul>
I created an extension method for RouteData that returns the current area name.
public static string GetAreaName(this RouteData routeData)
{
object area;
if (routeData.DataTokens.TryGetValue("area", out area))
{
return area as string;
}
return null;
}
Since RouteData is available on both ControllerContext and ViewContext it can be accessed in your controller and views.
It is also very easy to test:
[TestFixture]
public class RouteDataExtensionsTests
{
[Test]
public void GetAreaName_should_return_area_name()
{
var routeData = new RouteData();
routeData.DataTokens.Add("area", "Admin");
routeData.GetAreaName().ShouldEqual("Admin");
}
[Test]
public void GetAreaName_should_return_null_when_not_set()
{
var routeData = new RouteData();
routeData.GetAreaName().ShouldBeNull();
}
}
There is no need to check if RouteData.DataTokens is null since this always initialized internally.
Get area name in View (.NET Core 2.2):
ViewContext?.ActionDescriptor?.RouteValues["area"]
MVC Futures has an AreaHelpers.GetAreaName() method. However, use caution if you're using this method. Using the current area to make runtime decisions about your application could lead to difficult-to-debug or insecure code.
I know this is old, but also, when in a filter like ActionFilter, the context does not easily provide you with the area information.
It can be found in the following code:
var routeData = filterContext.RequestContext.RouteData;
if (routeData.DataTokens["area"] != null)
area = routeData.DataTokens["area"].ToString();
So the filterContext is being passed in on the override and the correct RouteData is found under the RequestContext. There is a RoutData at the Base level, but the DataTokens DO NOT have the area in it's dictionary.
To get area name in the view, in ASP.NET Core MVC 2.1:
#Context.GetRouteData().Values["area"]
I dont know why but accepted answer is not working. It returns null with e.g ( maybe about mvc, i use .net core )
http://localhost:5000/Admin/CustomerGroup
I always debug the variable and fetch data from in it.
Try this. It works for me
var area = ViewContext.RouteData.Values["area"]
Detailed logical example
Layout = ViewContext.RouteData.Values["area"] == null ? "_LayoutUser" : "_LayoutAdmin";
Asp .Net Core 3.1
Scenario: I wanted to retrieve the current area name in a ViewCompnent Invoke method.
public IViewComponentResult Invoke()
{
string areaName = this.RouteData.Values["area"];
//Your code here...
return View(items);
}
I know this is a very very old post but we can use the Values Property exactly the same way as the DataTokens
Url.RequestContext.RouteData.Values["action"] worked for me.
In MVC 5, this seems to be needed now. When in a Controller, pass this.ControllerContext.RouteData to this routine:
/// <summary>
/// Get the name of the Area from the RouteData
/// </summary>
/// <param name="routeData"></param>
/// <returns></returns>
private static string GetArea(RouteData routeData)
{
var area = routeData.DataTokens["area"]?.ToString();
if (area != null)
{
// this used to work
return area;
}
// newer approach
var matchedList = routeData.Values["MS_DirectRouteMatches"] as List<RouteData>;
if (matchedList != null)
{
foreach (var matchedRouteData in matchedList)
{
if (matchedRouteData.DataTokens.TryGetValue("area", out var rawArea))
{
return rawArea.ToString();
}
}
}
return "";
}