4

This is my first question on stackoverflow, so please be gentle. I am writing a customer portal to a warehouse application using MVC4, Entity Framework and SimpleMembership. The warehouse hosts contents for multiple companies. Each company has divisions and departments. The users will have varying access to the information for their company, divisions, and departments. I am looking for an elegant solution for access control. So far, my model looks like this:

public class UserProfile
{
    UserProfile()
    {
        this.AccessControl = new HashSet<AccessControl>();
    }

    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }
    public string UserName { get; set; }
    public Nullable<int> CompanyId { get; set; }
    public virtual ICollection<AccessControl> { get; set; }
    public virtual Company Company { get; set; }
}

public class AccessControl
{
    public int AccessControlId { get; set; }
    public int UserId { get; set; }
    public int CompanyId { get; set; }
    public Nullable<int> DivisionId { get; set; }
    public Nullable<int> DepartmentId { get; set; }
    public Boolean ReadAccess { get; set; }
    public Boolean WriteAccess { get; set; }

    // other properties for access control

    public virtual UserProfile UserProfile { get; set; }
    public virtual Company Company { get; set; }
    public virtual Division Division { get; set; }
    public virtual Department Department { get; set; }
}

public class Content
{
    public int ContentId { get; set; }
    public int CompanyId { get; set; }
    public int DivisionId { get; set; }
    public int DepartmentId { get; set; }

    // Various other properties

    public virtual Company Company { get; set; }
    public virtual Division Division { get; set; }
    public virtual Department { get; set; }
}

My thought was that a NULL Division means all divisions and a NULL Department means all departments. My questions are:

  1. What is an elegant way to write the repository method to retrieve a list of Content objects for a user based on their access control list as well as populating division and department select lists in CRUD views?
  2. Is there a better way to model this access control list?
Nathan Hughes
  • 94,330
  • 19
  • 181
  • 276
tomc
  • 83
  • 1
  • 1
  • 6
  • 1
    Do you already have an "inelegant" solution you could show for improvement? The possible `null` cases for company, division and department make a solution pretty hard, even more an "elegant" solution that would not have a lengthy sequence of case distictions. Without the `null`s a query could be: `var query = from c in context.Contents join a in context.AccessControls on new { c.CompanyId, c.DivisionId, c.DepartmentId } equals new { a.CompanyId, a.DivisionId, a.DepartmentId } where a.UserProfile.UserId == givenUserId select c;` But I failed to extend this somehow to cover the `null`s. – Slauma Mar 02 '13 at 14:47
  • Regarding your division/department select lists, can you elaborate on what you are envisioning for the CRUD views? (ie what do you want do be able to do with a view) – scott-pascoe Mar 02 '13 at 18:19
  • Thank you for your answers. If user A has two Access Control objects. One that gives Access to Divison1, Accounting and another that gives access to Division1, Treasury, then the select lists when creating, updating or deleting would provide for Division 1 only for the Division, and Accounting and Treasury on the select list for Department. If the user has one Access Control object for all Divisions (i.e. Division is NULL) and department Sales, then the select lists would contain all defined divisions, and only the Sales department. – tomc Mar 02 '13 at 23:27
  • Slauma, unfortunately, I have struggled very much with a solution. One thought was to query the Content Dbset filtering with each access control object criteria and unioning the result until a complete collection is built based on the user's access. But, I don't know how to do that with an iteration through all of the Access Control objects. – tomc Mar 02 '13 at 23:59

2 Answers2

0

I don't think this addresses all of your questions yet, but I think a repository that looks something like this:

public class accessRepository
{
    accessContext context = new accessContext();

    public IQueryable<Content> GetAccessibleContentFor(int userId)
    {
        var up = context.UserProfiles.Single(u => u.UserId == userId);
        var companyId = up.CompanyId;

        return from c in context.Content 
               where c.CompanyId == companyId 
               && (up.AccessControl.Any(
                    a=> 
                        a.CompanyId == c.CompanyId && 
                        a.DivisionId == c.DivisionId && 
                        a.DepartmentId == c.DepartmentId) 
               || up.AccessControl.Any(
                    a=>a.CompanyId == c.CompanyId && 
                        a.DivisionId == c.DivisionId && 
                        a.DepartmentId == null)
               || up.AccessControl.Any(
                    a=>
                        a.CompanyId == c.CompanyId && 
                        a.DivisionId == null)
               select c;
    }
}

would allow you get back the content that is accessible if:

  1. The content belongs to the user's Company.
  2. The user Can access content for the Company, Division, and Department
  3. Or the user can access content for the Company and Division (all Departments)
  4. Or the user can access content for the Company (all divisions) [ all departments, is assumed in this case.]
scott-pascoe
  • 1,463
  • 1
  • 13
  • 31
  • I have some doubt that this will work because you are mixing LINQ-to-Entities with LINQ-to-Objects in a single query (`a.XXX` are values in memory and `c.XXX` are database values) which will probably throw an exception at runtime. – Slauma Mar 02 '13 at 18:53
  • I'm not sure that your comment is valid. Everything here is an IQueryable, and therefore should be able to generate SQL all the way down to the actual request. I've been playing with building a test implementation and so far it looks like it would work. Of course, I like Davis Brossard's suggestion even better than my solution. – scott-pascoe Mar 03 '13 at 13:27
  • `up.AccessControl` isn't an `IQueryable`, it's just a local collection in memory populated by lazy loading. I would expect the infamous "*Unable to create constant value bla bla...*" exception (http://stackoverflow.com/questions/7220867/unable-to-create-a-constant-value-of-type-type-only-primitive-types-such-as). But maybe I am wrong... – Slauma Mar 03 '13 at 21:14
0

You should look into a policy- and attribute-based solution that's independent of your app where you can write authorization policies e.g.

a user can access content in the warehouse if the content.department==user.department && content.company==user.company.

XACML sounds like the perfect model. I wrote this demo where I do access control on purchase orders based on the purchaser, the amount, the location and the status of the PO. I don't need to change the app code because I use XACML externally.

David Brossard
  • 13,584
  • 6
  • 55
  • 88