Campus.ru security system

    When developing almost any software product, sooner or later, developers face the problem of restricting access. For example, in a web application, some pages may be accessible only to administrators or only to registered users. In the Campus.ru project, such access control is provided by the Spring Security library.

    Spring Security works with static roles, which are defined through the URI for each request from the HTTP client. But what about dynamic roles that change depending on the relationship between the current user and the requested object (for example, the role of the author in relation to the article)?


    To solve the problem of access to individual entities, the Domain Object Security system from Spring can be used. However, this system is rigidly tied to the domain model, supports a limited number of functions over entities (up to 32) and is quite difficult to implement (7 key interfaces).

    We decided to develop a new CRMedia.Security framework that would be more flexible, support dynamic access lists, and would also be easier to use. The idea of ​​our framework is that it allows you to protect some user actions with a specific set of arguments, the meaning of which is known only to the web application, but not to the security system. Such a model of the system allows us to abstract from Campus.ru entities and use the framework in other projects.

    CRMedia.Security receives an action, after which its arguments from the application look for restrictions on these actions through the PermissionProvider and calculate the intersection of these restrictions with the access list of the current user provided by ACLProvider. If the intersection is an empty set, then the handler is called corresponding to the case of access denied. In the event of a non-empty intersection, the call to a specific Tapestry event continues.

    Here is a specific scenario that requires verification of rights: for example, viewing in a community with id = 10 an article with number 20. In this case, “viewing an article” is an action, and 10, 20 are its arguments. Suppose this article is available only to community members, and a non-community user is trying to view it. Thus, the PermissionProvider implemented in the application should return a “member of the community” constraint, and the ACLProvider - “not a member of the community”. The intersection of these sets is empty, and therefore access to the post page will be closed.

    Consider a software implementation of a security system using the CRMedia.Security framework. First you need to connect the CRMedia.Security module to the application you are developing. This module connects the annotation processor to the Tapestry component converter chain (ComponentClassTransformWorker).

    The interface of the restriction provider (PermissionProvider) is as follows:

    public interface PermissionProvider {

      /**
       * Наложить ограничение на выполнение действия
       * @param action      действие
       * @param permission    ACL
       */
      void restrict(Action action, List permission);

      /**
       * Получить ACL для выполнения указанного действия
       * @param action  действие
       * @return ACL
       */
      List get(Action action);

      /**
       * Снять все ограничения на выполнение указанного действия
       * @param action  действие
       */
      void revoke(Action action);
      
      /**
       * Удалить ограничения на все действия хотя бы с одним аргументом из указанного списка
       * @param params  список именованных аргументов
       */
      void revokeReferenced(Map params);

    }

    * This source code was highlighted with Source Code Highlighter.


    It can be implemented as a DAO to store constraints in the database. Here, Action is a combination of the action name and a list of named arguments ({view_article; community = 10; article = 20} for the above example), and PermissionEntry is one access list entry (for example, {status = member}).

    ACLProvider contains the only method by which CRMedia.Security obtains the access list of the current user:

    public interface ACLProvider {

      /**
       * Получить список проверяемых значений перед выполнением действия для указанного компонента
       * @param component     компонент
       * @param action      действие
       * @return список проверяемых значений; null, если доступ запрещен
       */
      List getACL(Component component, Action action);

    }

    * This source code was highlighted with Source Code Highlighter.


    For our example, the action {view_article; community = 10; article = 20} The getACL method should return a set of {status = nonmember} based on the relationship between the current user and the specified community.

    Now you need to tell the security kernel which events should be protected. To do this, you need to put the annotations CRMedia.Security.

    Let the post be viewed through the ViewArticle page. When accessing it according to Tapestry rules, the onActivate method is called. To restrict access to it, we need to arrange annotations in this way:

    public class ViewArticle {

      @Restricted(action = "view_article")
      Object onActivate(@SecuredParam("community") Community community,
              @SecuredParam("article") Article article) {

        ...

      }

    }

    * This source code was highlighted with Source Code Highlighter.


    The Restricted annotation is put before an event that has a constraint. Each event corresponds to one action (in this case, “view_article”) with a specific set of arguments (“community” and “article”). Arguments from this are followed by SecuredParam annotations.

    By default, when access is denied, the client receives a response from the server with code 403 (Forbidden). If necessary, you can use your own handler:

    public class ViewArticle {

      @Restricted(action = "view_article")
      void onActivate(@SecuredParam("community") Community community,
              @SecuredParam("article") Article article) {

        ...

      }

      Object onForbidForListArticles() {
          return new TextStreamResponse("text/plain", "access denied");
      }

    }

    * This source code was highlighted with Source Code Highlighter.


    CRMedia.Security supports nested properties in validated arguments. For example, in the following example, the “community” parameter is taken from the community property of the Article entity:

    public class Article {
      
      public Community getCommunity() {
        …
      }
      
    }

    public class ViewArticle {

      @Restricted(
        action = "view_article",
        params = {
              @SecuredProp(name = "community", paramProp = "article.community")
        }
      )
      Object onActivate(@SecuredParam("article") Article article) {

        ...

      }

    }

    * This source code was highlighted with Source Code Highlighter.


    If the page retains its state between requests, then the values ​​of the action arguments for the protected event (“view” in the example below) can be taken from the page itself:
    public class ViewArticle {

      @Property
      @Persist
      private Article article;

      @Restricted(
        action = "view_article",
        params = {
            @SecuredProp(name = "article", pageProp = "article"),
            @SecuredProp(name = "community", paramProp = "article.community")
        }
      )
      Object onView() {

        ...

      }

    }

    * This source code was highlighted with Source Code Highlighter.


    In addition to restrictions on events, you can set a restriction on viewing parts of a page. To do this, the IfCan component exists in the framework, which is completely similar to using the standard If component from Tapestry Core in the template:


      
        
          Открыть пост
          
            Просмотр запрещен
          

        

      


    * This source code was highlighted with Source Code Highlighter.


    The viewArticleContext property, similar to SecuredProp annotations, defines a list of protected parameters:

    public class ViewArticle {

      @Property
      @Persist
      private Article article;

      public Object[] getViewArticleContext() {
        return new Object[]{"article", article, "community", article.getCommunity()};
      }

      @Restricted(
        action = "view_article",
        params = {
            @SecuredProp(name = "article", pageProp = "article"),
            @SecuredProp(name = "community", paramProp = "article.community")
        }
      )
      Object onView() {

        ...

      }

    }

    * This source code was highlighted with Source Code Highlighter.


    Thus, the developed framework has a sufficient degree of abstraction, allows protecting various events on Tapestry pages from unauthorized execution and adjusting the display of the page depending on the user's rights.

    Also popular now: