views:

139

answers:

1

I'm building the 2nd iteration of a web-based CRM+CMS for a franchise service business in ASP.NET MVC 2. I need to control access to each franchise's services based on the roles a user is assigned for that franchise.

4 examples:

  • Receptionist should be able to book service jobs in for her "Atlantic Seaboard" franchise, but not do any reporting.
  • Technician should be able to alter service jobs, but not modify invoices.
  • Managers should be able to apply discount to invoices for jobs within their stores.
  • Owner should be able to pull reports for any franchises he owns.

Where should franchise-level access control fit in between the Data - Services - Web layer?
If it belongs in my Controllers, how should I best implement it?

Partial Schema

Roles class

int ID { get; set; } // primary key for Role
string Name { get; set; }

Partial Franchises class

short ID { get; set; } // primary key for Franchise
string Slug { get; set; } // unique key for URL access, eg /{franchise}/{job}
string Name { get; set; }

UserRoles mapping

short FranchiseID; // related to franchises table
Guid UserID; // related to Users table
int RoleID; // related to Roles table
DateTime ValidFrom;
DateTime ValidUntil;

Controller Implementation

Access Control with [Authorize] attribute

If there was just one franchise involved, I could simply limit access to a controller action like so:

[Authorize(Roles="Receptionist, Technician, Manager, Owner")]
public ActionResult CreateJob(Job job)
{
    ...
}

And since franchises don't just pop up over night, perhaps this is a strong case to use the new Areas feature in ASP.NET MVC 2? Or would this lead to duplicate Views?

Controllers, URL Routing & Areas

Assuming Areas aren't used, what would be the best way to determine which franchise's data is being accessed? I thought of this:

{franchise}/{controller}/{action}/{id}

or is it better to determine a job's franchise in a Details(...) action and limit a user's action with [Authorize]:

{job}/{id}/{action}/{subaction}
{invoice}/{id}/{action}/{subaction}

which makes more sense if any user could potentially have access to more than one franchise without cluttering the URL with a {franchise} parameter.

Any input is appreciated.

Edit:

Background

I built the previous CRM in classic ASP and it runs the business well, but it's time for an upgrade to speed up workflow and leave less room for error. For the sake of proper testing and better separation between data and presentation, I decided to implement the repository pattern as seen in Rob Conery's MVC Storefront series.

How to arrange services and repositories?

It makes sense to have a JobService that retrieves any service jobs based on available filters, eg. IQueryable<Job> GetJobs();. But since a job can only belong to one franchise, a function like IQueryable<Job> GetJobs(int franchiseID); could belong in either FranchiseService or in JobService. Should FranchiseService act as a CatalogService (like in MVC Storefront)?

A: 

Let me take a stab at answering this. I am in the process of playing with a sample app that touches some of the aspects mentioned. This is not an authoritative answer, merely experience.

Where should franchise-level access control fit in between the Data - Services - Web layer?

This access restrictions should permeated through your application at two levels 1) the database 2) the application layer. In an MVC context I would suggest having creating a custom Authorization attribute - this handles the security between the Web-Services layer. I would have this attribute do two things

  1. Get the current roles allowed for the user (either from the DB of it may be stored in the user session)
  2. Do the checking to see if the user is part of the allowed list of roles. With regards to the database, this depends on how you are storing the data, one database for all franchises or database per franchise. In the first case there are several ways to limit and setup access restrictions for data to a particular franchise.

Since franchises don't just pop up over night, perhaps this is a strong case to use the new Areas feature in ASP.NET MVC 2? Or would this lead to duplicate Views?

I think that Areas should be used to split and group functionality. If you were to use Areas to split franchises, this is where I see a duplication of views, controllers etc. occurring. Duplicate views can be overcome by using a custom view engine to specifically overriding the way MVC locates your views. Plug: See my answer to ASP.NET MVC: customized design per domain

Assuming Areas aren't used, what would be the best way to determine which franchise's data is being accessed?

As mentioned above, you could the users session to store basic information such as the franchise the user belongs to and the roles etc assigned. I think the rule I read somewhere goes along the lines of "Secure your actions, not your controllers"

Create you routes etc for the norm and not for the exception. eg. Is there currently a business case that says a user can have access to more than one franchise?

How to arrange services and repositories?

Have a set of base services or base classes that will contain all the information required for a particular franchise such as the franchiseId. Th main issue that it does resolve is that your service methods are cleaner not having the franchiseId argument. The repository however may need this value since as some point you need to disambiguate the data you are requesting or storing (assuming one db for all franchises). However, you could overcome some of this using IoC.

The downside I see is that they there will always be calls to the database every time your objects are creating (i.e. if the franchise route were to be used, you would need to go the database to obtain the corresponding franchiseId every time you create a service object. ( I might be mistaken on this one, since the IoC containers do have some LifeStyle options that may be able to assist and prevent this) You could have a list of Franchises that are created on you Application start that you could use to map your route values to obtain the correct information. This part of the answer is scattered, but the main thing is that IoC will help you decouple a lot of dependencies.

Hope this helps..

Ahmad
Thanks for taking the time to answer my long question, Ahmad. I'm pretty sure I want to keep everything in one database because it makes comparing reports easier and because franchises share stock and products. The only case where a user has access to more than one franchise is the administrator and the overall business owner. For me this indicates that a `/{franchise}/{controller}` route makes sense. Having a route like that will enable my [Authorize] attribute to look-up/cache a user's roles based on the {franchise} being accessed. I am checking out using StructureMap for IoC.
FreshCode
I think I understand how are wanting to use this routing scheme - I still dont see why you need `franchise` as part of the route since your `UserRoles` tables has the mapping or user to franchise. Can you further clarify how the [Authorize] will be used since to me all the info needed for access is the user's id and a lookup to the `UserRoles` table. [I get the feeling that having franchise in the URL is more for aesthetic purposes -:)]
Ahmad