• 《转》Don’t Do RoleBased Authorization Checks; Do ActivityBased Checks Posted by Derick Bailey on May 24, 2011


    I’ve built a few dozen security mechanisms in my career. Unfortunately, I kept getting it wrong, hence the need to keep building them. Over the years, though, I learned a number of different ways that a security system can be built. One of my favorite ways to build authorization systems is through the use of role-based security. The idea is fairly simple: you assign users to roles and roles have permissions. That way you have a nice abstraction that people can be assigned to, as a role, so that you don’t have to assign the same 5, 10, or 500 permissions to every user in your system. It’s a great way to handle authorization when your system has more than a handful of permissions and users. Unfortunately, most of the role-based code that I have ever seen, or written, is done wrong.

    Stop Using Roles For Authorization Checks

    When it comes time to check authorization rules, we often see the role being checked. This is especially true in the .NET arena where Microsoft has built several very specific authorization mechanisms into their frameworks that all expect to use the role as the thing being checked. Does this user belong to this role? If so, go ahead, otherwise throw a security exception.

    For example, look at the MSDN documentation for ASP.NET MVC’s AuthorizeAttribute. In the remarks, it states (emphasis mine):

    The Authorize attribute lets you indicate that authorization is restricted to predefined roles or to individual users.  This gives you a high degree of control over who is authorized to view any page on the site.

    Now .NET is not the only place where this is common, it’s just the place that I have the most experience so it’s where I have found it more often than not. It’s also unfortunate that Microsoft is directly pushing this form of authorization, as it leads a lot of developers (myself included, in the past) to believe that this is how authorization should be done.

    No matter what language and what platform / run time you are using, though, you need to stop doing role-based authorization checks. That is not how role-based security should be done.

    What’s Wrong With Role-Based Authorization Checks?

    Plenty of authorization systems have been created with role-based checks, so what’s wrong with them? A lot of things, including documentation and coupling, modeling and encapsulation issues, and requirements growth and change.

    Documentation

    It can be very difficult to know which roles have which permissions in a system. You either create documentation (manually or through code-sniffing tools), or you hope and pray that you can remember everything that a given role is able to do. As a system continues to grow, the problem becomes worse. The documentation has to be updated and you have to hope and pray that the documentation reflects the reality of the executing software.

    Having a document that is generated by a tool is a great idea, no doubt. However, it’s only working around the real problem and it doesn’t solve all of the problems, either. Even with a generated document, there is no way to know with 100% certainty that the document you are reading reflects the actual software because the document is not executable. Even if you provide an executable specification (a unit test, or functional test, or some other automated test suite) to cover roles and permissions, there’s no guarantee that a developer didn’t go and add a permission somewhere to fix a bug, with no automated test to back it up.

    Requirements Growth And Change

    How many times have you built a system that has a hard and fast number of roles, throughout the entire lifetime of the system? I can count that on my left hand with half of my fingers missing. Yes, it does happen, but not often in my experience. The typical scenario that I see is a client who says that there is a limited number of roles that they need. With that in mind, we start down the path of using roles as the authorization check and all is well. We know how this story ends, though. Eventually the client will realized that they forgot about a role, or see that what they originally needed is no longer valid and need to change things around to match the actual system.

    Look at a portion of the example provided in the AuthorizeAttribute’s documentation, for the use of the attribute:

    [HandleError]
     public class HomeController : Controller
     {
         [Authorize(Roles = "Admin, Super User")]
         public ActionResult AdministratorsOnly()
         {
             return View();
         }
     }
    

    In this example, we can see the roles that are required being passed into the constructor of the Authorize attribute. This is possibly the worst coupling you can do between roles and authorization. Since attributes in .NET are serialized into the assembly directly, any data passed into the attribute via a constructor must be assembly-serializable. With simple strings meeting this criteria and being serialized into the assembly, the knowledge of which roles are allowed to execute the “AdministratorOnly” action are compiled into the assembly. The only possible way to change which roles can execute this method is to change the code and recompile it.

    Not ever authentication system that checks against roles will use attributes, of course. That doesn’t mean the problem goes away, though. Look at this example code as one possible way of handling authorization checks without using attributes:

    public class SomeClass
    {
      public void DoSomething()
      {
        authorizationService.RequireRoles("Admin", "Super User");
        // some code here that only runs if the user is in a specific role
      }
    }
    

    There are a number of ways that this can be altered and executed, including the use of custom IPrincipal objects.

    It could also very easily be written in another language. For example, ruby:

    class SomeClass
      def do_something
        authorization.require_roles :admin, :super_user
      end
    end
    

    Either way, the problem of coupling the authorization to roles and compiling it down into the assembly has only moved at this point. Instead of being assembly-serialized, it’s now being converted to assembly resources or strings in the code, directly. Either way, the information is still stored directly in the assembly and cannot be updated without recompiling the code.

    What happens when your client wants you to change which roles can run the DoSomething method or the AdministratorsOnly action? That’s right – you get to change the code and any associated tests, recompile, re-test, and re-deploy… all because the authorization checks are based on roles.

    Modeling and Encapsulation

    If we were following good object oriented design principles, we would want to have proper modeling and encapsulation of our security needs. This includes the assignment of permissions to roles, whether you are hard coding the assignment or loading the permissions from a database.

    If we have our roles hard coded all throughout our system in any place that needs to do authorization, we have no encapsulation of the permissions or assignment of permissions. Encapsulation and cohesion tell us that we should keep all of related bits of information and process near each other, in a manner that allows us to stack the bits together in meaningful ways, without having to duplicate anything. By having our roles thrown all around our code, though, we have none of this. The knowledge of authorization is thrown around the app, in disparate areas, unable to be re-arranged and stacked together into anything more meaningful than just the individual authorization rule at the individual location.

     

    Baby Steps: Load The Roles At Run-Time?

    If the problem with the previous code is that the roles are still hard coded into the method call of the authorizationService, then we can fix that by passing the roles into the method through another means – namely by loading the roles that are allowed, at run-time. Here’s an example of what may look like, in a very naive implementation.

    public class SomeClass
    {
      public void DoSomething()
      {
        var roles = roleRepository.GetRoles();
        authorizationService.RequireRoles(roles);
      }
    }
    

    Or in ruby:

    class SomeClass
      def do_something
        roles = Roles.all
        authorization.require_roles roles
      end
    end
    

    But there’s a problem here. In this example, all roles are being loaded up at run time for the requirement check. While this may serve some purpose for some requirement, somewhere, it’s not a likely scenario (at least not one that I’ve seen). In the real world, we need to restrict which roles can execute the code in question. So, what we need here is a way to know which roles should be loaded.

    Authorize Against Activities

    Every example that I’ve shown has used authorization to ensure that users are or are not allowed to perform certain activities within the software. In the original example, the [Authorize] attribute is being used to authorize the execution of the AdminitratorsOnly action on a controller. In the other examples, we’re verifying authorization to allow the DoSomething method to be executed. Each of these can be expressed in terms of activities:

    • Authorize current user for AdministratorsOnly activity on Home controller
    • Authorize current user for DoSomething activity on SomeClass

    In the end, all software systems that need any type of authorization will be authorizing against activities, even if those activities are only implied in the security system’s mechanics. Even if we’re talking about data level security – to ensure only certain users can see certain data records – we can still express these security needs as activities. The basic activities surrounding data are typically expressed as CRUD: Create, Read, Update, Delete.

    • Authorize current user to Create a record of specified type (add a new record)
    • Authorize current user to Read a specific record or record of specified type (view the record on the screen)
    • Authorize current user to Update a specific record or record of specified type (edit / save the record on the screen)
    • Authorize current user to Delete a specific record or record of specified type

    Look at the previous sample code again, this time with the idea of an activity as your focus for the authorization needs:

    public class SomeClass
    {
      public void DoSomething()
      {
      }
    }
    

    And in ruby:

    class SomeClass
      def do_something
      end
    end
    

    What do you you see, in terms of activities, in this code? The DoSomething method should stand out. The method name indicates some action being performed – an activity in the system. If the code we are writing represents an activity that needs authorization, then let the code represent itself that way. Provide a way for this code to say that it is an action that needs authorization. For example, we can take the previous authorization code and modify it only a little, to provide an activity name that allows us to load all of the roles that are authorized for an activity.

    public class SomeClass
    {
      public void DoSomething()
      {
        var roles = roleRepository.GetRolesForActivity("Do Something");
        authorizationService.RequireRoles(roles);
      }
    }
    

    Or in ruby:

    class SomeClass
      def do_something
        roles = Roles.for_activity "Do Something"
        authorization.require_roles roles
      end
    end
    

    At this point, though, the code is going to get redundant when we start using it throughout our system. Since the roles are loaded by the activity and always passed to the authorization service in the same way, we can encapsulate that call with a higher level abstraction. This also gives us the advantage of removing roles from the direct authorization call, further reducing the mental reliance on the idea of a role being the thing we authorize against.

    public class SomeClass
    {
      public void DoSomething()
      {
        authorizationService.AuthorizeActivity("Do Something");
      }
    }
    

    Or in ruby:

    class SomeClass
      def do_something
        authorization.authorize_activity "Do Something"
      end
    end
    

    Behind the scenes, we are still loading the roles that are allowed to execute this activity. However, we have removed the notion of roles from the authorization call, entirely. We have effectively decoupled the activity from the roles that are allowed to execute it, letting the activity represent itself in a manner that is meaningful, to the authorization mechanism. The authorization mechanism becomes the middle man between the activity that needs authorization and the implementation of the authorization and permissions scheme.

    Solving The Role-Based Authorization Problems

    If we approach the problem of authorization not from the perspective of which roles are allowed to do something, but which activities we must be authorized to perform, we can solve the problems of role-based authorization checks.

    Modeling And Encapsulation

    Activities give us a more intuitive abstraction and the ability to grow and change our security system with relative ease. Instead of sprinkling roles all throughout our code, let the code speak for itself and represent itself as an activity that can be performed. Behind the authorization mechanism that I showed, we can then properly model the relationships between roles and permissions.

    We have created a natural boundary between the code that needs authorization and the code that implements the authorization scheme. This boundary gives us an easy way to achieve encapsulation through modeling. Whether we are hard coding the assignment of permissions to roles (as is often done with the ‘ability.rb’ file when using CanCan for authorization in Rails) or loading the list of permissions for a given role from a database, the assignment of roles and permissions can happen in a single location. Even if we need to provide different role -> permission assignments depending on the context of the system or code, we can encapsulate those contexts into their own classes and can use tools like IoC containers to provide the correct permissions set at run time.

    Self-Documenting Code

    Instead of having to search through potentially millions of lines of code to find everywhere that a role is checked, provide a single, consolidated location for the role’s permissions. We no longer have to parse an entire code base to learn and document what roles are assigned to what permissions. We can see the assignment easily because we have all of the relevant information in one place.

     

    Growing An Authorization System

    And once we have our authorization system based on activities instead of roles, it becomes fairly easy to grow with our systems requirements and change which roles are allowed to do what. We also start to see change happening in our system along better vectors; rather than having to change and redeploy every time a role needs to have a different set of permissions, we can begin to write code that knows what activities are in the system and change the assignment of activity permissions to roles through better mechanisms.

    We also get the benefit of not having to build our security system in an all-or-nothing manner. As the needs for security change, the implementation of the security mechanisms can change. The simple API of letting the code represent itself as an activity and having an authorization class that handles the details of executing authorization checks means we are not tightly coupled to a specific implementation. The internals and implementation of the authorization check can be changed as needed without affecting the use of the authorization API. We can change from hard coded role -> assignment permissions to data-driven assignment, and can change from local authorization storage to LDAP, ActiveDirectory or web services calls.

    The system can grow and change as needed, roles can be added and removed, permissions can be changed and we don’t have to touch a large number of files to make the change. If we have a data-driven role -> permission assignment, we can add and remove roles and permission assignment without redeploying the system. Even if we are changing the name of an activity and the code associated with it, we have to change very little to make that happen. Rename the method in question, change the activity name where the method is, and then change the assignment of permission for that activity name. The largest number of changes in this case will be the number of roles that are assigned to the permission. Since the permission assignment is all within the same area of the system, though – and probably within the same file if you’re doing hard coded assignments – then you shouldn’t have any issue finding the activity name and changing it.

    Simplifying Activity-Based Authorization In .NET, With Attributes

    Attributes are not the cause of the problem with ASP.NET MVC example that I showed, previously. In fact, attributes may make your life easier. Here is one example of what an attribute based authorization check in .NET might look like if we’re running off the premise of activity-based authorization.

    [HandleError]
     public class HomeController : Controller
     {
         [Authorize(Activity = "Administrators Only")]
         public ActionResult AdministratorsOnly()
         {
             return View();
         }
     }
    

    Does that look familiar? The only difference between this and the original example from the MSDN documentation is that I’m specifying the activity instead of the roles. Since our AdministratorsOnly method should represent some activity that the system can perform, though, there really isn’t a need to specify the action name at all. We can use run time reflection and conventions to know that the activity that needs authorization is “Administrators Only” and omit the declaration of the activity entirely:

    [HandleError]
     public class HomeController : Controller
     {
         [AuthorizeActivity]
         public ActionResult AdministratorsOnly()
         {
             return View();
         }
     }
    

    In this case, I provided a custom attribute named AuthorizeActivity, to prevent confusion or mixup with the built in Authorize attribute.

    Rails And Conventions For Activity-Based Authorization

    On the Ruby / Rails side of things, we don’t have anything that equates to .NET’s attributes. However, we don’t need any equivalent. The CanCan system (my personal preference for authorization in Rails) uses conventions and meta-programming to provide authorization of controller actions and resources. For example, a controller that edits user accounts can be secured with very little code:

    class AccountsController < ApplicationController
      authorize_resource
    end
    

    The authorize_resource method plugs in all of the necessary authorization code to ensure the user that is hitting any of the actions on the controller is authorized to execute that action for an Account.

    Note: “Action” Vs “Activity”

    I used to call this action-based security. However, the recent popularity in Model-View-Controller application architectures has overloaded the term “action”. Most people think of actions as controller actions because of this. While a controller action is certainly an action or activity that may have authorization needs, it’s not the only place that authorization may need to be checked. Using the term “activity” instead of action gives a little bit of differentiation and distinction, to hopefully indicate that authorization is not strictly limited to controller actions.

  • 相关阅读:
    错题集-index.html
    面向对象-原型
    jQuery案例
    jQuery报错
    关于《哈利波特》书的购买方案
    《大道至简》读后感
    网络助手之NABCD
    返回一个二维整数数组中最大联通子数组的和
    返回一个二维整数数组中最大子数组的和。
    返回一个整数数组中最大子数组的和(环)(已更正)
  • 原文地址:https://www.cnblogs.com/tianzibobo/p/2799937.html
Copyright © 2020-2023  润新知