Decoupling You Security Model From The Application Model With SimpleMembership

One of the peeves I have with ASP.NET MVC and  how security is handled is that you have to design your security model up front while you are designing your web application.  You have to determine which roles you need to have while implementing your application so that you can add them to the Authorize attribute on the controllers actions.  Here is an example of how you control access to controller actions.


   [Authorize(Roles = "admin,user")]
    public ActionResult DoSomething()
    {
        ViewBag.Message = "Do something really spectacular.";
        return View();
    }

Now when we want to change our security model, such as adding new roles, we have to go through the whole application changing these Authorize attributes, recompile the application, and redeploy. This coupling causes a lot of inefficiencies.

But you can customize SimpleMembership to handle this in a better way that decouples the security model form your application domain. I will demonstrate how to do this based on the work that has already been started on customizing SimpleMembership in the SimpleSecurity project. This is an open source project you can find on GitHub which demonstrates methods to enhance SimpleMembership. The design of this enhancement is to use a customized Authorize attribute that does not take roles as parameters; instead it uses a resource parameter and an operation parameter. A resource is an abstract representation of an item in your application domain. An operation is an action you are going to perform on that resource, such as read, write or modify. An example might be that you have a web application that lets a user modify their profile. In this case you could label the resource as "UserProfile" and the operation would be "modify". Here is an example of what our new Authorize attribute will look like.

    [SimpleAuthorize(Resource = "UserProfile", Operation = "modify")]
    public ActionResult ModifyUserProfile()
    {
        ViewBag.Message = "Modify Your Profile";
        return View();
    }

It is important to point out that MVC 4 has two types of Authroize attributes; one used for controller's that manage views (System.Web.Mvc.AuthorizeAttribute) and one used for Web API controllers (System.Web.Http.AuthorizeAttribute). Each has very different behavior because of what they are controlling. For this article we discussing how to customize the Authorize attribute for controllers that manage views. We will look at customizing the one for Web API's in a later article.

So how is resource and operation used to determine what controller actions a user has access to.  Users are still assigned to roles, but now we map the roles to resource/operations.  For a user to have access the resource/operation must have a role assigned that the user has.  Now when we change our security model we do not have to modify any code, instead we configure it in our database.  To implement this design we need to customize the SimpleMembership database.  Here is what the new database looks like.



We have added a new table called Resources which contains all of the resources for the application and Operations which contains all of the operations for a resource.  The relationship between operations and resources is one resource to many operations.  Operations are then mapped to roles in the OperationsToRoles table.  This is a many-to-many relationship. To see how these tables were added to the SimpleMembership database look at the source code in the SimpleSecurity project.  The new entities are defined in SimpleSecurity\Entities.  This project uses a Repository and Unit of Work design pattern and you can see the implementation for this in SimpleSecurity\Repositories.  The details for the entities configuration and relationships are done using the Fluent API and can be found in SimpleSecurity\Repositories\ModelConfiguration.

So how did we customize the Authorize attribute to work with resource and operations instead of roles. Lets take a look at the code.

    [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Method, Inherited = true,
    AllowMultiple = true)]
    public class SimpleAuthorize : AuthorizeAttribute
    {
        public string Resource { get; set; }
        public string Operation { get; set; }

        public override void OnAuthorization(AuthorizationContext actionContext)
        {
            base.Roles = ResourceService.GetRolesAsCsv(Resource, Operation);
            base.OnAuthorization(actionContext);
        }

    }

We inherit from the standard AuthorizeAttribute and override the OnAuthorization method.  The implementation is pretty straight forward. Now we just get a list of the roles assigned to the resource/operation by calling ResourceService.GetRolesAsCsv (Csv stands for comma separated values) and pass these roles to the base AuthorizeAttribute class.  ResourceService is an application domain layer on top of the repositories for handling resources and operations.

You can test this out in the example project in SimpleSecurity called SeedSimple.  Download the source code for this project and run it to see how it works.  I have added two new actions to the HomeController.

    [SimpleAuthorize(Resource = "Foo", Operation = "Read")]
    public ActionResult ReadFoo()
    {
        ViewBag.Message = "Read Foo";
        return View();
    }

    [SimpleAuthorize(Resource = "Foo", Operation = "Write")]
    public ActionResult WriteFoo()
    {
        ViewBag.Message = "Write Foo";
        return View();
    }

I also modified how the database is seeded to add the resources/operations and their role mapping.

        protected override void Seed(SecurityContext context)
        {

            WebMatrix.WebData.WebSecurity.InitializeDatabaseConnection("SimpleSecurityConnection",
               "UserProfile", "UserId", "UserName", autoCreateTables: true);
            var roles = (WebMatrix.WebData.SimpleRoleProvider)Roles.Provider;
            var membership = (WebMatrix.WebData.SimpleMembershipProvider)Membership.Provider;

            if (!roles.RoleExists("Admin"))
            {
                roles.CreateRole("Admin");
            }
            if (!roles.RoleExists("User"))
            {
                roles.CreateRole("User");
            }
            if (!roles.RoleExists("SuperUser"))
            {
                roles.CreateRole("SuperUser");
            }
            if (!WebSecurity.FoundUser("test"))
            {
                WebSecurity.CreateUserAndAccount("test", "password", "test@gmail.com");
            }
            if (!roles.GetRolesForUser("test").Contains("Admin"))
            {
                roles.AddUsersToRoles(new[] { "test" }, new[] { "admin" });
            }
            if (!WebSecurity.FoundUser("joe"))
            {
                WebSecurity.CreateUserAndAccount("joe", "password", "joe@gmail.com");
            }
            if (!roles.GetRolesForUser("joe").Contains("User"))
            {
                roles.AddUsersToRoles(new[] { "joe" }, new[] { "User" });
            }
            List<operation> operations = new List<operation>();
            operations.Add(new Operation() {Name = "Read"});
            operations.Add(new Operation() {Name = "Write"});
            ResourceService.AddResource("Foo", operations);
            ResourceService.MapOperationToRole("Foo", "Read", "User");
            ResourceService.MapOperationToRole("Foo", "Write", "Admin");
 
        }

As you can see from this code the resource/operation Foo/Read is assigned to the role of User and Foo/Write is assigned to the role of Admin.   The user joe is assigned the role User and the user test is assigned to the role Admin.

Run the application and select the tab Read Foo.  If you are not logged in you will be taken to the logon page. Log in as joe with the password of password. You will be redirected to the Read Foo page because joe has authorization for this page. Now select the Write Foo tab and again you will be redirected to the logon page because joe does not have authorization for this page. Log in as the user test and you will be redirected to the Write Foo page because user test does have authorization.

So now we can use SimpleMembership without worrying about our security model changing. We can change our security model by just changing our database configuration instead of making code changes, recompiling them and redeploying.  Another benefit is we can defer designing our security model until we are ready to deploy. We no longer have to design our security model while we are implementing the application. What do you think about this design?  Please let me know your thoughts in the comments.




Comments

Popular posts from this blog

Using Claims in ASP.NET Identity

Seeding & Customizing ASP.NET MVC SimpleMembership

Customizing Claims for Authorization in ASP.NET Core 2.0