Security Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we'll look at how to implement fine-grained Permissions-Based Access Control with the Apache Shiro Java security framework.

2. Setup

We'll use the same setup as our introduction to Shiro — that is, we'll only add the shiro-core module to our dependencies:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.1</version>
</dependency>

Furthermore, for testing purposes, we'll use a simple INI Realm by placing the following shiro.ini file at the root of the classpath:

[users]
jane.admin = password, admin
john.editor = password2, editor
zoe.author = password3, author
 
[roles]
admin = *
editor = articles:*
author = articles:create, articles:edit

Then, we'll initialize Shiro with the above realm:

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);
SecurityUtils.setSecurityManager(securityManager);

3. Roles and Permissions

Usually, when we talk about authentication and authorization, we center on the concepts of users and roles.

In particular, roles are cross-cutting classes of users of an application or service. So, all the users that have a specific role will have access to some resources and operations and might have restricted access to other parts of the application or service.

The set of roles is usually designed up-front and rarely changes to accommodate new business requirements. However, roles could also be defined dynamically — for example, by an administrator.

With Shiro, we have several ways of testing if a user has a particular role. The most straightforward way is to use the hasRole method:

Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {       
    logger.info("Welcome Admin");              
}

3.1. Permissions

However, there's a problem if we check for authorization by testing if the user has a specific role. In fact, we're hardcoding the relationship between roles and permissions. In other words, when we want to grant or revoke access to a resource, we'll have to change the source code. Of course, this also means rebuilding and redeploying.

We can do better; that's why we'll now introduce the concept of permissions. Permissions represent what the software can do that we can authorize or deny, and not who can do it. For example, “edit the current user's profile”, “approve a document”, or “create a new article”.

Shiro makes very few assumptions about permissions. In the simplest case, permissions are plain strings:

Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("articles:create")) {
    //Create a new article
}

Note that the use of permissions is entirely optional in Shiro.

3.2. Associating Permissions to Users

Shiro has a flexible model of associating permissions with roles or individual users. However, typical realms, including the simple INI realm we're using in this tutorial, only associate permissions to roles.

So, a user, identified by a Principal, has several roles, and each role has several Permissions.

For example, we can see that in our INI file, user zoe.author has the author role, and that gives them the articles:create and articles:edit permissions:

[users]
zoe.author = password3, author
#Other users...

[roles]
author = articles:create, articles:edit
#Other roles...

Similarly, other realm types (such as the built-in JDBC realm) can be configured to associate permissions to roles.

4. Wildcard Permissions

The default implementation of permissions in Shiro is wildcard permissions, a flexible representation for a variety of permission schemes.

We represent wildcard permissions in Shiro with strings. A permission string is made of one or more components separated by a colon, such as:

articles:edit:1

The meaning of each part of the string depends on the application, as Shiro doesn't enforce any rule. However, in the above example, we can quite clearly interpret the string as a hierarchy:

  1. The class of resources we're exposing (articles)
  2. An action on such a resource (edit)
  3. The id of a specific resource on which we want to allow or deny the action

This three-tiered structure of resource:action:id is a common pattern in Shiro applications, as it's both simple and effective at representing many different scenarios.

So, we could revisit our previous example to follow this scheme:

Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("articles:edit:123")) {
    //Edit article with id 123
}

Note that the number of components in a wildcard permissions string does not have to be three, even though three components is the usual case.

4.1. Permission Implication and Instance-Level Granularity

Wildcard permissions shine when we combine them with another feature of Shiro permissions — implication.

When we test for roles, we test for exact membership: either a Subject has a particular role, or it doesn't. In other words, Shiro tests roles for equality.

On the other hand, when we test for permissions, we test for implication: do the Subject‘s permissions imply the one we're testing it against?

What implication means concretely depends on the implementation of the permission. In fact, for wildcard permissions, the implication is a partial string match, with the possibility of wild components, as the name suggests.

So, let's say we assign the following permissions to the author role:

[roles]
author = articles:*

Then, everyone with the author role will be allowed every possible operation on articles:

Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("articles:create")) {
    //Create a new article
}

That is, the string articles:* will match against any wildcard permission whose first component is articles.

With this scheme, we can both assign very specific permissions – a certain action on a certain resource with a given id – or broad permissions, such as edit any article or perform any operation on any article.

Of course, for performance reasons, since implication is not a simple equality comparison, we should always test against the most specific permission:

if (subject.isPermitted("articles:edit:1")) { //Better than "articles:*"
    //Edit article
}

5. Custom Permissions Implementations

Let's touch briefly on permissions customizations. Even though wildcard permissions cover an extensive range of scenarios, we might want to replace them with a solution custom-made for our application.

Suppose that we need to model permissions on paths so that permission on a path implies permissions on all subpaths. In reality, we could use wildcard permissions just fine for the task, but let's ignore that.

So, what do we need?

  1. a Permission implementation
  2. to tell Shiro about it

Let's see how to achieve both points.

5.1. Writing a Permission Implementation

A Permission implementation is a class with a single method — implies:

public class PathPermission implements Permission {

    private final Path path;

    public PathPermission(Path path) {
        this.path = path;
    }

    @Override
    public boolean implies(Permission p) {
        if(p instanceof PathPermission) {
            return ((PathPermission) p).path.startsWith(path);
        }
        return false;
    }
}

The method returns true if this implies the other permission object, and returns false otherwise.

5.2. Telling Shiro About our Implementation

Then, there are various ways of integrating a Permission implementation into Shiro, but the most straightforward way is to inject a custom PermissionResolver into our Realm:

IniRealm realm = new IniRealm();
Ini ini = Ini.fromResourcePath(Main.class.getResource("/com/.../shiro.ini").getPath());
realm.setIni(ini);
realm.setPermissionResolver(new PathPermissionResolver());
realm.init();

SecurityManager securityManager = new DefaultSecurityManager(realm);

The PermissionResolver is responsible for converting the string representation of our permissions to actual Permission objects:

public class PathPermissionResolver implements PermissionResolver {
    @Override
    public Permission resolvePermission(String permissionString) {
        return new PathPermission(Paths.get(permissionString));
    }
}

We'll have to modify our previous shiro.ini with path-based permissions:

[roles]
admin = /
editor = /articles
author = /articles/drafts

Then, we'll be able to check for permissions on paths:

if(currentUser.isPermitted("/articles/drafts/new-article")) {
    log.info("You can access articles");
}

Note that here we're configuring a simple realm programmatically. In a typical application, we'll use a shiro.ini file or other means such as Spring to configure Shiro and the realm. A real-world shiro.ini file might contain:

[main]
permissionResolver = com.baeldung.shiro.permissions.custom.PathPermissionResolver
dataSource = org.apache.shiro.jndi.JndiObjectFactory
dataSource.resourceName = java://app/jdbc/myDataSource

jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource = $dataSource 
jdbcRealm.permissionResolver = $permissionResolver

6. Conclusion

In this article, we've reviewed how Apache Shiro implements Permissions-Based Access Control. The implementations of all these examples and code snippets are available over on GitHub.

Security bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of