Solutions to IT problems

Solutions I found when learning new IT stuff

Creating a Framework for Chemical Structure Search – Part 8

leave a comment »


Series Overview

This is Part 8 – Spring Security Integration of the “Creating a Framework for Chemical Structure Search“-Series.

Previous posts:

Follow-ups:

Introduction

MoleculeDatabaseFramework is integrated with Spring-Security. It offers optional method level security in the service layer. This article will explain how it works and how to configure your application to use Spring-Security.

Annotation-Based

MoleculeDatabaseFramework has been integrated with Spring-Security using annotations. This means as long as you do not enable security in you Application Context, everything will work just fine without any security. Security is applied to methods in the Service interfaces.

The security integrations allows you to limit a user to certain types of entities, eg. one user can only read SimpleCompound while his supervisor also has access to SecretCompound. You can also assign roles that allows a user to update and/or delete compounds he created himself. And of course roles that allow to update or delete any compound of a given implementation.

The security integration uses the @PreAuthorize annotation. The value of this annotation is written in SpEL – Spring Expression Language. MoleculeDatabaseFramework uses the expressions hasRole or hasPermission. hasRole directly checks if the current user has the according role (called authority in Spring Security) and if yes grants access to the method or else throws an AccessDeniedException. hasPermission requires an implementation of org.springframework.security.access.PermissionEvaluator which has 2 overloaded methods public boolean hasPermission();. This is used on the save(entity) methods to determine if the given entity can be created or updated by the current user.

See the Spring Security Method Expression Documentation for more information on this topic.

Conventions

To make use of security you need to follow certain conventions. There are 6 basic “role-types”: create, read, update, delete, update_created and delete_created. A complete role is a “role-type” followed by and underscore and the entity class implementations SimpleClassName. So if you have an entity RegistrationCompound and you want a user to be able to read RegistrationCompounds he needs the role read_RegistrationCompound. And so forth.

  • A role starts with a “role-type” followed by “_” and the simple class name: read_RegistrationCompound
  • A user can either read none or all entries for a given entity implementation.
  • A service interface method that requires read-role must be annotated with @PreAuthorize("hasRole(#root.this.getReadRole())")

Above applies to ChemicalCompound, Containable and ChemicalCompoundContainer. For ChemicalStructure there is always only the supplied entity ChemicalStructure which a user of the framework should not extend. ChemicalStructure only has a save-Role (instead of save-Permission) and any user that can create or update any type of ChemicalCompound must have this role save_ChemicalStructure. Or in code:


public interface ChemicalCompoundService<T extends ChemicalCompound>
		extends Service<T> {

	//...snipped...
		
	@Transactional(readOnly = false)
	@PreAuthorize("hasPermission(#compound, 'save_' + #root.this.getCompoundClassSimpleName())")
	@Override
	T save(T compound);
	
	//...snipped...
}

public interface ChemicalStructureService<T extends ChemicalStructure>
		extends Service<T> {	
		
	//...snipped...
	
	@Transactional(readOnly = false)
	@PreAuthorize("hasRole('save_ChemicalStructure')")
	@Override
	T save(T structure);
	
	//...snipped...
}
  • For managing Users and Roles you need to use the supplied entities User and Role and their services.

User implements UserDetails from Spring Security and UserService extends UserDetailsService. To create, update or delete a User or a Role you need the role manage_User or manage_Role respectively.

Security Behaviour

MoleculeDatabaseFramework ships with a PermissionEvaluator implementation. This PermissionEvaluator checks if a given user can create, update or delete a given domain object. (Note: For read-methods just having the read-role is enough; they use hasRole instead of hasPermission in @PreAuthorize). The supplied DefaultPermissionEvaluator internally uses Permission objects to determine a users permissions.

The supplied PermissionEvaluator allows users with create, update or delete role to perform that action on any domain object (of the given implementation). Users with update_created or delete_created role can perform that action only on domain objects they created (getCreatedBy().equals(loggedInUserName).

Services only offer a save(entity) method. If a domain object is being created or being updated is determined whether its id is set or not (id == null -> create). This is exactly the same what hibernate does.

In your application you should only create exactly 1 implementation of ChemicalCompoundContainer. However this Container can hold any type of Containable and hence any type of ChemicalCompound. To ensure that a user only sees Containers that contain a ChemicalCompound he has the read-role for, all other Containers are filtered out. This includes the count() service method. So users with different privileges on ChemicalCompounds see different numbers of Containers.

The supplied PermissionEvaluator requires a RoleHierarchy bean to be configured in the security context. RoleHierarchy is very useful as it automatically assigns a “lower” privilege to someone with a “higher” one. So you say

create_RegistrationCompound > read_RegistrationCompound

then anyone with create_RegistrationCompound role automatically also has role read_RegistrationCompound.

Cascading of Persist and Merge

ChemicalCompound, Containable and ChemicalCompoundContainer in their JPA relationships between each other all use CascadeType.PERSIST and CascadeType.REFRESH. This means changes (updates) to existing entities must always be done using that entities service.save(entity) method because CascadeType.MERGE (update) is not set and hence updates are not cascaded.

In case of creating a new entity, the Permission implementations check if the current user has the privilege to also create the associated, new entities. If you create a new ChemicalCompoundContainer that contains a new Containable which is made of a new ChemicalCompound, the ContainerPermission will verify if the current user has the privilege to not only create the new ChemicalCompoundContainer but also to create the new ChemicalCompound and new Containable. If this is not the case, an AccessDeniedException is thrown.

Below example will create a new RegistrationCompound, a new Batch and a new CompoundContainer if the current user has the privileges create_RegistrationCompound, create_Batch and create_CompoundContainer.

RegistrationCompound regCompound = new RegistrationCompound();
regCompound.setCompoundName("Registration Compound");
regCompound.setCas(cas);
regCompound.setRegNumber(regNumber);

ChemicalStructure structure =
        chemicalStructureFactory.createChemicalStructure(structureData);

ChemicalCompoundComposition composition = new ChemicalCompoundComposition();
composition.setCompound(regCompound);
composition.setChemicalStructure(structure);
composition.setPercentage(100.0);

regCompound.getCompositions().add(composition);

Batch batch = new Batch(regCompound, batchNumber);

regCompound.getBatches().add(batch);

CompoundContainer container = new CompoundContainer("C00001", batch);
container = compoundContainerService.save(container);

In case an associated entity already exists, create-privilege is not required, if the entity that is being persisted is the “parent” in the hierarchy. Or said otherwise you can associate a new ChemicalCompoundContainer with an existing Containable but you can not associate a new Containable with an existing ChemicalCompoundContainer because that Container could not exist before the Containable was created. Same logic for relationship between Containable and ChemicalCompound.

Customize Security

To do so, you need to implement your own PermissionEvaluator and / or Permission implementations.

Configuration Example

Below the SecurityContext.xml used for testing security in MoleculeDatabaseFramework.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
             http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
             http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">

    <!-- User Service - In a real application this should use database
   and the according UserService and RoleService.  -->
    <security:user-service id="userService">
        <security:user name="user" password="password" authorities="read_TestCompound,
            read_TestContainable, create_TestCompoundContainer, create_RegistrationCompound, create_Batch"/>
        <security:user name="creator" password="password" authorities="create_TestCompound, create_TestContainable"/>
        <security:user name="owner" password="password" authorities="update_created_TestCompound, update_created_TestContainable, delete_created_TestCompound, delete_created_TestContainable, delete_created_TestCompoundContainer"/>
        <security:user name="editor" password="password" authorities="update_TestCompound, update_TestCompoundContainer, read_TestContainable"/>
        <security:user name="admin" password="admin" authorities="admin_TestCompound, admin_TestContainable, admin_TestCompoundContainer"/>
    </security:user-service>

    <!--    <bean id="userService"
        class="org.bitbucket.kienerj.moleculedatabaseframework.service.UserServiceImpl">
    </bean>-->

    <!-- Use a RoleHierarchy and a PermissionEvaluator in SpEL expression in
    @PreAuthorize -->
    <bean id = "methodSecurityExpressionHandler"
          class = "org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
        <property name="roleHierarchy" ref="roleHierarchy"/>
        <property name="permissionEvaluator" ref="permissionEvaluator"/>
    </bean>

    <!-- Role Hierachy - Probably should use database for this too in real app -->
    <bean id="roleHierarchy"
          class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
        <property name="hierarchy">
            <value>
                admin_TestCompound > update_TestCompound
                update_TestCompound > update_created_TestCompound
                update_created_TestCompound > create_TestCompound
                admin_TestCompound > create_TestCompound
                create_TestCompound > read_TestCompound
                create_TestCompound > save_ChemicalStructure
                admin_TestCompound > delete_TestCompound
                delete_TestCompound > delete_created_TestCompound
                update_RegistrationCompound > create_RegistrationCompound
                create_RegistrationCompound > read_RegistrationCompound
                create_RegistrationCompound > save_ChemicalStructure
                admin_TestContainable > update_TestContainable
                admin_TestContainable > delete_TestContainable
                update_TestContainable > create_TestContainable
                create_TestContainable > read_TestContainable
                update_created_TestContainable > create_TestContainable
                admin_TestCompoundContainer > update_TestCompoundContainer
                admin_TestCompoundContainer > delete_TestCompoundContainer
                delete_created_TestCompoundContainer > create_TestCompoundContainer
                update_TestCompoundContainer > create_TestCompoundContainer
                create_TestCompoundContainer > read_TestCompoundContainer
            </value>
        </property>
    </bean>

    <!-- Permission Evaluator supplied by framework. The constructor takes a Map
    of SimpleClassName -> PermissionImplementation associations-->
    <bean id="permissionEvaluator"
         class="org.bitbucket.kienerj.moleculedatabaseframework.security.DefaultPermissionEvaluator">
        <constructor-arg index="0">
            <map key-type="java.lang.String"
                 value-type="org.bitbucket.kienerj.moleculedatabaseframework.security.Permission">
                <entry key="TestCompound" value-ref="chemicalCompoundPermission"/>
                <entry key="RegistrationCompound" value-ref="chemicalCompoundPermission"/>
                <entry key="TestContainable" value-ref="containablePermission"/>
                <entry key="TestCompoundContainer" value-ref="chemicalCompoundContainerPermission"/>
                <entry key="Batch" value-ref="containablePermission"/>
            </map>
        </constructor-arg>
    </bean>

    <!-- Permission implementations uses -->
    <bean id="chemicalCompoundPermission"
          class="org.bitbucket.kienerj.moleculedatabaseframework.security.ChemicalCompoundPermission">
    </bean>
    <bean id="containablePermission"
          class="org.bitbucket.kienerj.moleculedatabaseframework.security.ContainablePermission">
    </bean>
    <bean id="chemicalCompoundContainerPermission"
          class="org.bitbucket.kienerj.moleculedatabaseframework.security.ChemicalCompoundContainerPermission">
    </bean>


    <security:authentication-manager alias="testAuthenticationManager">
        <security:authentication-provider user-service-ref="userService"/>
    </security:authentication-manager>

    <!-- enable annotations and set expression handler to use-->
    <security:global-method-security pre-post-annotations="enabled">
        <security:expression-handler ref = "methodSecurityExpressionHandler"/>
    </security:global-method-security>
</beans>
Advertisements

Written by kienerj

May 22, 2013 at 08:37

Posted in Chemistry, Java, Programming

Tagged with ,

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: