Multi-tenant application allow one user to access many tenant accounts? - c#

I'm writing a multi-tenant application using a one database per tenant model. I have allowed each user account to access multiple tenants (as long as that tenant has given them access)
Each page sent to the browser includes the current TenantId in Site.Master
<%= Html.Hidden("TenantId") %>
But when any request is made from the browser (submit button, AJAX GET or AJAX POST), this TenantId is NOT actually checked to see if it matches the user's current TenantId.
Now if the user opens one tab, with TenantId = 1, then connects to another tenant in another tab with TenantId = 2, then switches back to the first tab and it has access to data from Tenant 2.
What can I do to fix this? I have a large number of existing ActionResult and JsonResult methods and I do not want to go through every one of them and add
if (request.TenantId != user.CurrentTenantId) return false
Because that would be a large amount of duplicated effort
Can I change my base controller to always read the value of TenantId? That could work for submitted requests (ActionResult), but what about AJAX requests?
How can I check the TenantId of the page inside of JsonResult actions without changing every single existing AJAX method (there are a lot of them)?

You could check your in the Application_Request event in the Global.asax.cs file. If what you need is populated via MVC model binding, then maybe write a custom ActionFilter to check it and register it with all actions via a GlobalFilter.

If I understood correctly, users could have simultaneously open 2 different tabs, each with a different tenant. And each page should display data relevant to each tenant.
So that means a solution involving a cookie or the session needs to be discarded as the tenant is specific to each browser tab.
And reading your answer to Cyril Gupta´s suggestion, I understand the hidden tenantId on each page may not be submitted on every AJAX request.
Of course, one solution could be to modify your application and make sure this is always the case with every AJAX request.
Otherwise that would also discard a global filter based on the request parameters as the tenantId may not always be there.
I think the best option then is to add a segment in the URL holding the tenantId.
For example replacing the default route with something like the following route (If you have many different routes you would need to be very careful to avoid route collision):
routes.MapRoute(
name: "Default",
url: "{tenant}/{controller}/{action}/{id}",
defaults: new { tenant = "defaultTenant", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
This way you can make sure the tenant will always be submitted on each request, and you could also have 2 different tabs with different tenants displaying each the appropriate data.
There are different options on how to recover the value of the route segment.
The binding will automatically populate the value on any parameter named "tenant" on your action method, or any parameter named "tenant" in a model class that is a parameter of the action method:
public ActionResult Foo(FooModel model, string tenant)
{
//both tenant and model.tenant will contain the value of the URL segment
return View();
}
You could also write a filter that access the value of the route parameter (RouteData is a property of the ActionExecutingContext and ActionExecutedContext class received as parameters of the filter methods), and performs some logic. The filter would then be set as a global filter in your application or to your base controller:
public class FooFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var tenant = filterContext.RouteData.Values["tenant"]
//do whatever you need to do before executing the action, based on the tenant
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var tenant = filterContext.RouteData.Values["tenant"]
//do whatever you need to do after executing the action, based on the tenant
}
}
The final option is to directly access the RouteData parameter on your base controller class. (As RouteData is a property of the base MVC Controller class)
As long as you are using Html and Ajax helpers for URL generation, the tenant segment of the URL will be maintained in your links. However if you have jquery code directly sending AJAX calls with hardcoded URLs, you would then need to update that code so the new url segment is taken into account.
Finally, in case the tenantId values are something not very user friendly like an integer, you could have unique names for each tenant and use the names in the URL. You would then add some logic that maps it to the integer value your application need.

You can write you own filter:
How do I get certain code to execute before every single controller action in ASP.NET MVC 2?
Executing code before any action
Of course there is no ready answer for you question. You need to write you own logic, how to handle tenantId. For example on each action, check if it doesnt equal as current session tenant id make redirect. Or put it in cookie and check every time in filter whether id's are equal. It's up to you. From my point of view cookie is more preferrable. But it eat traffic.

You can apply a filter at the controller level and check the tenantid that is being sent. Controller level filters shouldn't be any more difficult than action filters. For my project I needed to check authorisation in a similar manner but I overrode the controller class and then inherited from my own controller class as I had some very special needs.
Where are you going to store the tenant Id on the client side? It seems to me that you should be using a session object to do this.

Related

Global redirect back to front page if claim is empty or does not exist (NOT identity verification)

Just to be clear: I am NOT talking about claims-based identity validation.
I am building an app in which I make fine use of Identity 2.2 to provide validation. It is sufficient for my needs.
My problem is that once a user logs in, only the first page is widely accessible without storing additional information in the user’s “session”. In particular, when the user clicks on a major internal item (for sake of convenience, let’s call this a “customer module”, the Guid for that customer is stored in a claim held by the user. That way, the user can move from page to page and still have that same customer’s data brought up on every page regardless of what chunk of data the page was meant to display. This claim is only refreshed with something different when they return to the main page and click on another customer.
For security’s sake I would like to ensure that if a claim gets accidentally dropped or set to empty, the user gets shunted back to the main page regardless of where they are in the system, and preferably without having to put code in each and every page action of every controller.
Suggestions? Or am I completely wrong by making use of claims? Because it’s still early enough in the project to make a u-turn if the advantages of a different method are compelling enough.
EDIT:
Just to let people know my solution: Because only one group of people will be accessing this application (the users that interact with companies, this app is to record the interactions and “company information”), I decided to use a base controller. The users would be able to log on and view lists of companies without coming across any page that derived from BaseController, but once they chose a Company to work with, they needed to have Claims set to be able to maintain page-by-page contact with this company’s information. This information would be reset only when they chose a different company, but since there was always a chance that a claim could be disabled, I needed something to automagically redirect them back to the list of companies to re-set the claims. A BaseController that was employed by only those pages where information specific to one company would be displayed was the perfect solution.
A base controller is simple. Just create a controller called BaseController and you’re off to the races. Change any controller that needs to work with this base controller such that they are public class YourOtherController : BaseController.
I initially tried to make an Initialize method to handle everything, but ran into a rather big problem: I was unable to successfully both access and write to my Claims. As in, I was able to either read my claims but not make use of my ClaimWriter extension, or I was able to make use of my ClaimWriter extension but be unable to read claims in the first place. Since Initialize is wayyyy too low in the stack to actually do both these things, I abandoned it and went for an OnActionExecuted method, which ended up being successful. My code ended up being this:
public class BaseController : Controller {
private ApplicationDbContext db = new ApplicationDbContext();
protected override void OnActionExecuted(ActionExecutedContext filterContext) {
base.OnActionExecuted(filterContext);
var principal = ClaimsPrincipal.Current.Identities.First();
var company = User.GetClaimValue("CWD-Company");
var prospect = User.GetClaimValue("CWD-Prospect");
if(string.IsNullOrEmpty(company)) {
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.Redirect("/");
filterContext.HttpContext.Response.End();
}
if(!string.IsNullOrEmpty(company) && string.IsNullOrEmpty(prospect)) {
var id = new Guid(company);
var prospecting = db.Prospecting
.Where(x => x.CompanyId.Equals(id))
.Select(x => x.ProspectingId)
.ToList().SingleOrDefault();
if(prospecting.Equals(Guid.Empty)) { // null prospecting
User.AddUpdateClaim("CWD-Prospecting", "");
} else { // fill prospecting
User.AddUpdateClaim("CWD-Prospecting", Convert.ToString(prospecting));
}
}
}
}
I am probably going to change the if(prospecting.Equals(Guid.Empty) part of the Prospecting section to automagically create the first entry in the db (with all null values except for the ProspectingId and the CompanyId, of course), but this is what works for now.
That's a fine use of claims you describe, no need a u-turn. What you need is a MVC filter, authorisation filter. Something like this:
public class MyAuthorisationFilter : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var principal = HttpContext.Current.User as ClaimsPrincipal;
if(!principal.Claims.Any(c => c.Type == "My Claim Name"))
{
// user has no claim - do redirection
// you need to create 'AuthenticateAgain' route to your table of routes
// or you can do other means of redirection
filterContext.Result = new RedirectToRouteResult("AuthenticateAgain", new RouteValueDictionary());
}
}
}
Then you can add it globally in your filters configuration, but you'll have to exclude your authorisation page from this filter. Or apply on per controller basis - whenever this needs to happen.
This is very basic form of filter - a lot of checks are stripped out, but it gives a general direction how to proceed.
Update
This is a good article about Authorise attribute.
Here use of AllowAnonymous attribute is explained
The way you use it - depends on your scenario. In most cases when you only expose a login page to the world - it is sufficient to add this attribute as a global filter (see second link, part about RegisterGlobalFilters) and then sprinkle [AllowAnonymous] on top of controllers/actions which should be exposed without authentication.
Another approach is to have a base controller that has your attribute applied. And then all your controllers inherit from this base controller. This is more sufficient when global filter does not cut it: cases when you expose different pages to different users - think companies and customers. Your controllers for companies will inherit CompaniesBaseController that has [CompaniesAuthFilter] and customers will be inheriting from CustomersBaseController with [CustomersAuthFilter].

ASP.NET MVC5 - append querystring to all URLs

My team is building a simple MVC site for very low end "feature" phones. One problem we have is that certain phone aggressively cache HTML, so what the user actually gets to see is not what we intend. An extreme example is, a user registers and gets a "thank you page". Another user on the same device then tries to register. The phone simply serves the cached page without creating a new account.
In the past I've dealt with this by adding a "cache buster" querystring to things I don't want cached, eg all pages will be served in the format
http://domain.com/controller/route?cb=somerandomstringofnumbers
In this case we'd need to do this for all URLs in the site - this includes the URLs auto-generated by Controller actions such as RedirectToAction or Redirect and also the Razor Url.Action, Html.BeginForm, Html.ActionLink etc.
Now obviously I could decorate the Razor HTML helpers (or extend them) and add the argument to an controller action, but it seems to me that because the actual URLs generated by these built in methods are auto-generated from the Controller/Action params passed in, there should be a way to hijack that process.
Unfortunately the MS classes are protected - I'm mostly looking in System.Web.Routing.Routes.
I've tried a few things I've found online but they are not MVC5 (dating back to 2008) and it seems the framework has changed significantly.
eg, from
http://forums.asp.net/t/1216840.aspx?Append+value+to+all+urls+built+by+RouteCollection+GetUrl
public class SessionAppendingRouteHandler : IRouteHandler
{
public IHttpHandler GetHandler(RequestContext context)
{
SessionAppendingHttpHandler handler = new SessionAppendingHttpHandler();
handler.RequestContext = context;
return handler;
}
}
public class SessionAppendingHttpHandler : MvcHandler
{
public override ProcessRequest(RequestContext context)
{
//append your sid here
}
}
// and in the route setup
RouteTable.Routes.Add( new Route
{
Url = "/[controller].mvc/[action]/",
Defaults = new { action = "index" },
RouteHandler = typeof(SessionAppendingRouteHandler)
});
This I cant get to work as the framework has changed too much, but it looks very close to what I would like to achieve.
I feel like I'm in the right area, but I've hit a brick wall.
Any suggestions?
This is quite old, but let me answer based on how I solved a similar problem:
Instead of having it as query string, have the cb as a route value just as action and controller are route values. You can do this by registering a route; for instance:
routes.MapRoute(
name: "CB",
url: "{cb}/{controller}/{action}/{id}",
defaults: new { cb = "3600", area = "", id = UrlParameter.Optional }
);
If the value for cb is not a constant, then you can find a convenient point to set the cb for each user session. A good place will be after a successful login. With this, you'll now need to provide just two custom methods for RedirectToAction and ActionLink. Your implementation will simply package a RouteValueDictionary and then pass it in to MVC's own implementation using the appropriate overloads.

ASP.NET, MVC4, authorization: users should see only own content

Right now I have the following website structure:
Front end: website\user1, website\user2
Back end: website\account\user1, website\account\user2
Where website\account controller has generic [Authorize] attribute applied. Which is not enough since any authorized user can access other user's backed functionality simply by going to website\account\ url (if he knows his name).
What is the best way to resolve this issue?
I have two approaches so far:
Create custom Authorize attribute, inspect controller context, extract user information from there and compare it against current authorized user in ASP.NET:
var currerntUserId = (long)System.Web.Security.Membership.GetUser().ProviderUserKey;
return ExtractCurrentUserId(filterContext) == currerntUserId;
Remove part from back-end URLs and have all users access \account. Current user information will be provided by ASP.NET framework.
How about this?
Route /website/my-account
[Authorize]
public class MyAccountController : Controller
{
public ActionResult Index()
{
var userData = System.Web.Security.Membership.GetUser();
// note you could also get this from db using this.User.Identity.Name
return View(userData);
}
}
It is much easier to control the authorization this way because we are not passing the userid to the action method via a route parameter. The only way someone can get to the backend for a particular user account is by being logged in as that user.
Reply to comments:
To answer your questions in comments about what is easier / harder / better / what my preference is, I am going to go ahead and make my final answer "It depends."
It depends on the sensitivity of the data, what things admins can do that users aren't allowed to (or vice versa), how many controller actions needed to be secured, how similar the views are for public / account / admin perspectives on the data, etc. Pretty much everything stated in your question and all the answers here are valid approaches. You can certainly do it with an ActionFilter and keep the user URL's, or you could do it directly in the action method (if there aren't a lot of them), change your URL schema, implement impersonation (or not), etc.
When you are retrieving a user's data from your datastore (most likely a database), you should only retrieve data for the username of the authenticated user. In your controller, this will give you the username of the currently authenticated user:
User.Identity.Name
So you could do something like:
return ExtractCurrentUserId(filterContext) == User.Identity.Name;
If you use Role based authentication with SimpleMembership you can do something like this and give users roles that should be able to access certain controller actions:
public class MyAccountController : Controller
{
[Authorize(Roles = "Admin")]
public ActionResult User1()
{
// do user1 work
}
[Authorize]
public ActionResult User2()
{
// do user2 work
}
}

MVC 3 How can I make a user view a warning/disclaimer screen

I thought this would be very simple but I'm struggling a little. I'm working on a project for a client using MVC 3 that requires users to agree to certain conditions before using the site. I have created a standard agree/disagree screen which is loaded when first coming into the site, but if a user types a address to a different part of the site they can bypass the conditions for example www.test.com loads the conditions but if the user types www.test.com/home they bypass the conditions.
How can I make sure they have agreed to the conditions before they can get anywhere else on the site? I have been trying a session variable, which I think is the way to go, but is there a way to check this variable on every page request without having to write a check into every Controller Action on the site?
You could make a custom attribute and add it to the top of the Controller.
For example:
[AgreedToDisclaimer]
public ActionResult LoadPage()
{
return View();
}
Which would only load the view if the AgreedToDisclaimer returns true.
public class AgreedToDisclaimerAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
throw new ArgumentNullException("httpContext");
// logic to check if they have agreed to disclaimer (cookie, session, database)
return true;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Returns HTTP 401 by default - see HttpUnauthorizedResult.cs.
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "action", "ActionName" },
{ "controller", "ControllerName" },
{ "parameterName", "parameterValue" }
});
}
}
http://msdn.microsoft.com/en-us/library/dd410209(v=vs.90).aspx
http://msdn.microsoft.com/en-us/library/system.web.mvc.authorizeattribute.handleunauthorizedrequest.aspx
There are two approaches for the issue:
If this condition alter the site (like StackOverflow's notifications, that show at the top), but do not prevent you from using it, then I think it should be solved in presentation logic ( thus in the view, if you have real view an not just glorified template ).
Session variable in this case is just another part of model layer's state. When view instance requests data from the model layer, it is informed, that there is new notification for the user. This would mean that each view would have an ability to add representation of this notification to the current response. Preferably, by choosing to use one additional template, when assembling the output. This functionality would be shared between all views and thus could be implemented in base class, providing one single point of change.
If this acceptance of disclaimer is mandatory for everyone (like "I am 18 or older" in adult-themed sites), then the best option, in this case, would be to check, if user has agreed to condition in the routing mechanism. I am not sure how much control you have over request routing in ASP.NET MVC, but that again would provide you with as single point of change.
If decision is made in routing mechanism, it would also mean, that you put the condition outside the standard MVC triad entirely.

Is it possible to pass route value to show the name of the user without the controller name in MVC

I want to get the url for my site as "www.ABCD.com/username" without the controller name in MVC architecture, such that when i click the name of the particular user i want to show details of the user with just the name of the user showing in url.
This should get you going in the right direction:
routes.MapRoute(
"ViewProfile",
"{username}",
new { controller = "User", action = "ViewProfile" },
// new { username = "\w+" } // consider using a username regex here
);
Note that you will need to update the controller and action values to match your application. The ViewProfile action should look something like this:
public ActionResult ViewProfile(string username) { }
Keep in mind that the above route is very greedy and you will have to make sure any other actions which need to be accessed within your application are placed higher up in the route definitions list. Also keep in mind that if a user should pick a username that conflicts with a route you've defined above the ViewProfile route, the user's profile will never be accessible via the rooted path because the other route will override it.

Categories