Suresh Payankannur

Tuesday, October 22, 2013

Hibernate Row Level Security using Apache Shiro

Apache Shiro is a feature rich and simpler security framework that can be easily integrated with any enterprise level systems. It comes with a default implementation of permissions and it is much easier to implement any custom security framework on top of it. The features offered by Shiro are good enough for most of the security requirements.

This posting describes a simple way to implement row level security using Hibernate ORM and Apache Shiro in a multi-tenant environment.

Design Approach


  1. Each entity in the model that requires row level security should have a tenant id as part of the entity.
  2. Define the user level permissions using the Shiro permission syntax: entity-type:operation:instance-id
  3. Use hibernate filters to add additional predicates to the queries.
  4. Set the values of hibernate filters using the currently authenticated users permissions.

Notes


  1. Shiro do not have direct way of obtaining the current roles or permissions for the current subject. So this information is obtained using the Realm interface.
  2. Use caching to improve performance. Shiro supports out of the box support caching. Simplest way is to use EHCache.

The Model


The model contains two entities. A Company and a list of Employees. An authenticated user can only see Employees for the Company he/she has granted permission to.
@MappedSuperclass
@FilterDef(name = "rlsFilter",
           defaultCondition = "fk_company_id in (:companies)",
           parameters = {@ParamDef(name="companies", type="long")})
@Filter(name = "rlsFilter")
public abstract class SecuredEntity {
    @ManyToOne
    @JoinColumn(name = "fk_company_id", nullable=false)
    public Company getCompany() {
        return this.company;
    }
    public void setCompany(Company c) {
        this.company = c;
    }
    private Company company;
}

@Entity
@Table(name = "employee")
public class Employee extends SecuredEntity {
    @Id
    @GeneratedValue
    public long getId() {
        return this.id;
    }
    public void setId(long newId) {
        this.id = newId;
    }
    @Column(name = "email")
    public String getEmail() {
        return this.email;
    }
    public void setEmail(String neEmail) {
        this.email = newEmail;
    }
    @Column(name = "full_name")
    public String getFullname() {
        return this.fullname;
    }
    public void setFullname(String name) {
        this.fullname = name;
    }

    private long id;
    private String email;
    private String fullname;
}

@Entity
@Table(name = "company")
public class Company {
    @Id
    @GeneratedValue
    public long getId() {
        return this.id;
    }
    public void setId(long newId) {
        this.id = newId;
    }
    public String getName() {
        return this.name;
    }
    public void setName(String nm) {
        this.name = nm;
    }
    private long id;
    private String name;
} 

Permission Model


The permission model consists of a User representing a user in the system and a set of Permissions. The Permissions will follow the Shiro StringPermission syntax: resource-type:operation:instance-id. For example, company:*:1234 means permission for company with id 1234 for all operations.
@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue
    public long getId() {
        return this.id;
    }
    public void setId(long newId) {
        this.id = newId;
    }
    @Column(name = "user_name")
    public String getUsername() {
        return this.username;
    }
    public void setUsername(String nm) {
        this.username = nm;
    }
    @Column(name = "user_pass")
    public String getPassword() {
        return this.password;
    }
    public void setPassword(String pwd) {
        this.password = pwd;
    }
    @ManyToMany
    @JoinTable(name = "user_permission",
               joinColumns = @JoinColumn(name = "fk_user_id"),
               inverseJoinColumn(name = "fk_permission_id"))
    public Set<Permission> getPermissions() {
        return this.permissions;
    }
    public void setPermission(Set<Permission> perms) {
        this.permissions = perms;
    }
    private long id;
    private String username;
    private String password;
    private Set<Permission> permissions = new HashSet<String>();
}

@Entity
@Table(name = "permission")
public class Permission {
    @Id
    @GeneratedValue
    public long getId() {
        return this.id;
    }
    public void setId(long newId) {
        this.id = newId;
    }
    public String getValue() {
        return this.value;
    }
    public void setValue(String val) {
        this.value = val;
    }
    private long id;
    private String value;
}

Shiro Plumbing


  1. Create a custom Realm class. Implement the authentication and authorization methods based on the permission model.
  2. Get the permissions for the currently logged in user
public class MyShiroRealm extends AuthorizingRealm {
    public boolean supports(AuthenticationToken token) {
        return token ! null
            &&  UsernamePasswordToken.class.isAssignableFrom(token.getClass());
    }
    public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.getPrimaryPrincipal();

        SimpleAuthorizationInfo auth = new SimpleAuthorizationInfo();

        for (Permisison p : user.getPermissions()) {
            auth.addStringPermission(p.getPermission());
        }
    }
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToke token) {
        UsernamePasswordToken authToken = (UsernamePasswordToken) token;

        User user = userDao.getUser(authToken.getUsername());
        if (user != null) {
            auth = new SimpleAuthenticationInfo(user, user.getPassword(),
                                                realmName);
        } else {
            throw new AccountException("Account not found for user: "
                                       + authToken.getUsername())
        }
        return auth;
    }
    public Collection<String> getPermissions() {
        Subject subject = SecurityUtils.getSubject();
        AuthorizationInfo auth = getAuthorizationInfo(subject.getPrincipals());

        return auth != null
            ?  auth.getStringPermissions()
            :  new ArrayList<String>();
    }

    private String realmName = "MyShiroRealm";
}

Hibernate Integration


Hibernate layer integrates the Shiro permissions using filters as explained above. This can be done when a new session is obtained. If you are using OpenSessionInViewFilter, then subclass it and enable the filter soon after the session is opened.

  1. To provide individual permissions, follow the syntax: company:*:123
  2. To provide permissions to all companies, define permission string as: company:*
  3. If the user has permission to ALL companies, then the filter will be disabled
  4. If user has no company permission, then the filter will be executed with a bogus company id of -2.
public class EmployeeDao {
    protected Session getSession() {
        Session session = sessionFactory.getCurrentSession();

        Collecton<Long> companies = getAuhorizedCompanies();
        if (!companies.isEmpty()) {
            if (companies.size() == 1
                &&  companies.iterator().next() == ALL_COMPANIES) {

                session.disableFilter("rlsFilter");
            } else {
                Filter filter = sesson.enableFilter("rlsFilter");
                filter.setParameterList("companies", companies);
            }
        } else {
            companies.add(NO_COMPANIES);
            Filter filter = sesson.enableFilter("rlsFilter");
            filter.setParameterList("companies", companies);
        }
        return session;
    }
    protected List<Long> getAuthorizedCompanies() {
        List<Long> companies = new ArrayList<Long>();

        List<String> permissions = getPermissions();
        for (String p : permissions) {
            String[] parts = s.split(":");

            if (parts != null  &&  parts.length > 0) {
                if (parts[0].equalsIgnoreCase("company")) {
                    if (parts.length == 3) {
                        if (parts[2].equals("*")) {
                            companies.add(new Long(ALL_COMPANIES));
                            break;

                        } else {
                            companies.add(Long.parseLong(parts[2]));
                        }

                    } else if (parts.length < 3) {
                        companies.add(new Long(ALL_COMPANIES));
                        break;
                    }
                }
            }
        }
        return companies;
    }
    protected List<String> getPermissions() {
        List<String> permissions = new ArrayList<String>();

        RealmSecurityManager mgr =
            (RealmSecurityManager) SecurityUtils.getSecurityManager();

        if (mgr != null) {
            for (Realm r : mgr.getRealms()) {
                Collection<String> list = ((MyShiroRealm) r).getPermissions();

                if (list != null) {
                    permisisons.addAll(list);
                }
            }
        }
        return permissions;
    }

    public List<Employee> getAllEmployees() {
        Session session = getSession();
        // do the hibernate query
    }
    private SessionFactory sessionFactory;

    private static final long ALL_COMPANIES = -1;
    private static final long NO_COMPANIES  = -2;
}

Testing


  1. Create some users
  2. Assign some permissions
  3. Execute a hibernate based query (eg: getAllEmployees)
  4. Watch the SQL query executed and see the in clause predicate

1 comment:

  1. i tried,but doesnt work.some class have problems so im mad.please Can u share project (maven) files.? thanks

    ReplyDelete

Blog Archive

Scroll To Top