Apache Shiro: Implementing complex permissions autonomously
Although Apache Shiro includes a powerful and flexible tool for fine-grained allocation of authorizations with its WildcardPermissions, one is faced with its limitations when it comes to complex scenarios. This article shows how any authorization checks can be implemented autonomously using the Permission interface.
Authorization with Apache Shiro
With Apache Shiro, Java developers have a comprehensive and perfected security framework with the core features authentication, authorization, session management and cryptography at their disposal. This article focuses on the topic of authorization and demonstrates how Shiro’s permission concept can be taken advantage of to implement complex authorizations. A general introduction to Shiro is available at http://shiro.apache.org/. A complete example of the code for the solution presented here can be viewed at https://github.com/triologygmbh/complex-shiro-permissions.
Shiro permissions
Shiro offers two concepts for authorization: Roles and permissions. Role-based authorization checks are suited to simple, straightforward scenarios. In contrast, permissions provide the option of defining and assigning authorizations with flexibility and as finely grained as is needed. Here, everything which is not explicitly allowed, is prohibited.
With WildcardPermissions, Shiro already provides a powerful permission implementation which is sufficient for most applications. WildcardPermissions can be defined as strings. It would, for example, be possible to inquire whether the current user is authorized to modify the product with the ID 123 using the string “product:update:123“. Inversely, we can assign the authorization “product:update:*” to a user. This allows him to modify any product. “product:*” would allow the user to perform any action for any product, “product:*:123” would allow any action for product 123, and so on. The number of elements a WildcardPermission has is limitless. To restrict the authorization to a certain product line, the permission ”productline:hardware:product:update:*”, for example, would be conceivable.
Shiro fundamentals
Let us now look at the basic operation of Shiro before we consider the limitations of string based WildcardPermissions. The interface for client code is quite straightforward:
In Shiro, the so-called Realm forms the bridge to the domain of the developer’s own application. Application-specific issues such as integrating a user administration or assigning authorizations are delegated to a previously registered implementation of this interface by Shiro. If a client code attempts to log in the Subject, then the defined AuthenticationToken (a UsernamePasswordToken in this example) is passed on to the Realm to be tested. In a similar way, the Realm is asked for the roles and permissions of the current user when the user’s authorizations are being tested.
So that developers only need concentrate on these two aspects, Shiro provides the class AuthorizingRealm, which defines two template methods for this purpose. We are extending it as follows:
If the method doGetAuthenticationInfo returns an AuthenticationInfo instance after the test of the transmitted AuthenticationToken without throwing an exception, Shiro recognises the user as authenticated.
When authorizations are checked, doGetAuthorizationInfo is called. The method returns the authorizations and the roles of a user bundled in an AuthorizationInfo instance.
In the example, we define a series of WildcardPermissions. This class implements the Permission interface, which only defines one method:
When user.checkPermission(“product:update:123”) is called, Shiro first converts the requested Permission string into a WildcardPermission. With this instance, the implies methods of the Permissions assigned to the user are called until one of them returns true. In our example this would be
new WildcardPermission(“user:update:*”). If none of the Permissions of the user implies the requested Permission, then the user is not authorized.
Requirements of complex Permissions
With the string-based WildcardPermissions, developers have a simple tool at their disposal which can be used to implement rather complex scenarios. This tool should be used whenever possible.
However, this type of authorization definition is not appropriate in every case. Let us assume, for example, that we want to model authorizations on a file tree as follows:
> File access can be authorized for reading or writing.
> Files that have writing access also have reading access.
> When access to a file is permitted reading access to all superordinate directories must also be permitted in order to be able to navigate to the file.
> If a directory is authorized for reading or writing, this authorization is inherited by all subordinate directories and files.
With this definition, there are three conditions that result in a user having access to a file:
1. The user has authorization for the requested file assigned directly.
2. The user requests reading access to the file and the user has access to a subordinate file in the tree.
3. The user has access to a file which is above the requested file in the tree hierarchy.
We will implement the tests for each of these three constellations in their own custom implementations of the Permission interface.
Implementation of complex Permissions
Because the implies method to be implemented also expects a Permission, we need yet another implementation which depicts the requested access to the file:
All three authorization checks to be implemented have mutual basic functions: They only imply RequestedFileAccess PermissionS. They also have to ensure that the requested FileOperation (READ or WRITE) matches the configured authorization. For this purpose, we define the base class BaseFilePermission which tests the type of the requested Permission and the requested operation. Subsequently, it calls a template method so that specific tests can be implemented in its subclasses.
In the following, we will implement the defined tests 1., 2. and 3. in custom Permissions. Here, we are using the following file tree as an example:
Let us now implement the first test, the direct authorization for a file. As BaseFilePermission does most of the work for us, we only have to ensure that the requested file is the same file there is a direct authorization for. We delegate the testing of two files for equality and for their relative position to one another in the file tree to the FileRelations helping class.
Using a static import of the is method, we can expressively formulate the test of whether the requested file is the same as that which is configured in the Permission. This takes place in the PermissionForTheFileItself class:
This way, the user has reading access to the file departments/development/employee_987. Writing access and access to other files is not possible. For a demonstration, please refer to the test class Demo.java which can be found in the GitHub repository mentioned at the beginning.
The reading authorization for all superordinate files in the tree is also implemented in a similarly simply manner:
then reading access is possible to directories above the file employee_987.
In the same manner, we can implement the third and last Permission – the one which allows access to all the subordinate files in a directory:
Conclusion
This article shows how any authorizations can be implemented with Apache Shiro using the simple Permission interface. Instead of implementing one complex Permission which covers every case, practice has shown that it is preferable to distribute the tests among multiple Permissions. This is how fine-grained authorizations can be assigned in the Realm.
Note, however, that Shiro always calls every Permission a user has until one is found which allows the requested access. Expensive operations such as data base accesses should therefore be avoided when possible or be reduced with the use of caching. The file access demonstrated here could also have a negative effect on performance if large file hierarchies are searched through or if the issue is a remote file system. Such cases lend themselves to executing the extensive operations only once before testing the authorizations. In this example, the structure of the file tree could, e.g., be loaded in advance and kept in the memory instead of navigating within the file system for every request.
The question of how a user has access to their authorizations is not handled in this article. In general, what a user is allowed to do is stored in a data base with a role and authorization concept. When Permissions are being assigned in the Realm, this information is used to generate the appropriate Permissions. Caution is advised here as well: If nothing else is done, Shiro asks for the Permissions a user has every time a test regarding rights is executed. This could mean an unnecessary burden. In such cases, the use of Caching API provided by Shiro is advisable.
Comments
No Comments