Table of Contents

Spring Security

The spring-security is a project, which solves Authentication/Authorisation, protection against common attacks and many other security concerns. It nicely integrates with any Spring application (grails, spring-boot). You can check https://projects.spring.io/spring-security/ for more information.

Use in Errigal projects

At the time of writing this, the most 'advanced' example of using spring-security was Content Distributor. It leverages spring-security to implement authentication (configurable either for CAS or basic-auth) backed by a DB table. Using this provides currently logged in user context to each Controller/Service. Annotations are used in order to authorise the controller operations to particular roles. Filtering of returned entities by user's relation to particular company is also implemented.

Setting up authentication

The authentication (and all the other security things) are set-up in the SecurityConfiguration.groovy file. Setting up the authentication means providing your AuthenticationProvider and configuring it in the SecurityConfiguration just like in following code. The code actually allows switching between CAS authentication and simple local DB based authentication provider based on the configuration (variable casEnabled). The passed authenticationUserDetailsService is implemented in RepositoryBasedUserDetailsService.groovy and uses DB access to actually look for user by username (or cas authentication token, which contains username). The key point that Spring uses this service in order to get user details (containing the list of his roles).

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    if (casEnabled) {
      auth.authenticationProvider(casAuthenticationProvider())
    } else {
      auth.authenticationProvider(localAuthenticationProvider())
    }
  }
 
  AuthenticationProvider localAuthenticationProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider()
    provider.setUserDetailsService(authenticationUserDetailsService)
    provider.setPasswordEncoder(passwordEncoder)
    return provider
  }
 
  AuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider provider = new CasAuthenticationProvider()
    provider.setAuthenticationUserDetailsService(authenticationUserDetailsService)
    provider.setServiceProperties(serviceProperties())
    provider.setTicketValidator(new Cas20ServiceTicketValidator(urlPrefix))
    provider.setKey('an_id_for_this_auth_provider_only')
    return provider
  }

Securing Resources

Once the authentication is set-up, we can start enforcing certain authorisations for our code. There are several ways of doing that.

Securing URLs

This can be done in the already mentioned SecurityConfiguration.groovy in a following way (the key part starts with http.authorizeRequests() and allows you to fine tune the access to URL in your web app). The provided

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    if (casEnabled) {
      http.exceptionHandling()
        .authenticationEntryPoint(casAuthenticationEntryPoint()).and().addFilter(casAuthenticationFilter())
        .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
        .addFilterBefore(requestCasGlobalLogoutFilter(), LogoutFilter.class)
    } else {
      http.httpBasic()
    }
 
    http.authorizeRequests().anyRequest().fullyAuthenticated().and().httpBasic()
 
    http.logout()
      .logoutUrl('/logout')
      .logoutSuccessUrl('/')
      .invalidateHttpSession(true)
      .deleteCookies('JSESSIONID')
 
    http.csrf().disable()
  }

This sample is pretty simple, but you can use more complex/combined filters as seen bellow (the rules are evaluated in the order they are defined).

http.authorizeRequests()                                                                
    .antMatchers("/resources/**", "/signup", "/about").permitAll()
    .antMatchers("/admin/**").hasRole("ADMIN")
    .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
    .anyRequest().authenticated()
    .and()

Programatically

The most straightforward way is to programatically get the context of the currently logged in user in a following way. The important part is the first 2 lines in the filter method. They get the current logged in user (UserPrincipal) from the Spring context. The good thing about this is that it can contain some application specific information (e.g. relation of the user to a company), which can then be used to make a decision about restricting access to a resource (in this case returning simply true/false). E.g. this particular code uses the hasUnlimitedAccess method, which itself encapsulates logic to determine whether this user is ADMIN.

@Transactional
abstract class CompanyBasedPermission<T> {
 
  boolean authorize(T domain) {
    filter(domain)
  }
 
  boolean filter(T domain) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication()
    UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal()
    if (userPrincipal.hasUnlimitedAccess()) {
      return true
    } else {
      Company company = userPrincipal.company
      if (company == null) {
        return false
      } else {
        return resolveCompanyPermission(domain, company)
      }
    }
  }
 
  abstract boolean resolveCompanyPermission(T domain, Company companyFromCurrentUser)
 
}

Using Annotations

This is a very convenient and declarative way of defining authorisation and is preferred to be used for simple rules. It has to be explicitly enabled in SecurityConfiguration.groovy:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
...

Then you can use various annotations as can be seen on following example:

@RepositoryRestResource(collectionResourceRel = 'distribution', path = 'distribution')
interface DistributionRepository extends PagingAndSortingRepository<Distribution, Long> {
 
  Distribution findByName(String name)
 
  // have to implement the filtering logic in query as the paging doesn't work with filtering and would be also not nice with large counts of Distributions
  @Override
  @Query("select d from Distribution d JOIN d.companies c WHERE 1 = ?#{principal.hasUnlimitedAccess() ? 1 : 0} OR c.code = ?#{principal.company?.code}")
  Page<Distribution> findAll(Pageable pageable)
 
  @Override
  @PostAuthorize("@distributionPermission.authorize(returnObject)")
  Distribution findOne(Long aLong)
 
  @PostFilter("@distributionPermission.filter(filterObject)")
  @Query("SELECT d FROM Distribution d JOIN d.companies c WHERE  c.code  in (:companyCodes)")
  List<Distribution> findByCompanyCodes(@Param("companyCodes") List<String> companyCodes)
 
  @Override
  @Secured(['ROLE_ADMIN', 'ROLE_DISTRIBUTION_MANAGER', 'ROLE_COMPANY_ADMIN'])
  def <S extends Distribution> S save(S entity)
 
  @Secured(['ROLE_ADMIN', 'ROLE_DISTRIBUTION_MANAGER', 'ROLE_COMPANY_ADMIN'])
  void delete(Long aLong)
 
}

@Secured

The last method (delete) is secured using the @Secured annotation. In this particular case we want to the delete method to be accessible only to user, who has one of the defined (ADMIN) roles.

@PostFilter

The findByCompanyCodes method is annotated with @PostFilter annotation, which does filtering on the resulting objects. The filtering is implemented by DistributionPermission, which is based on the CompanyBasedPermission mentioned above - it basically just checks every returned Distribution, whether it is related to a Company of current user (in case it is not, it is not returned from the method).

@PostAuthorize

The findOne method uses the @PostAuthorize annotation. It's behaviour is very similar to @PostFilter, but instead of filtering out the particular objects from list it simply returns back a value or throws security exception. This is intended as if you are asking for an item you are not authorized to get, it's much better to get the Exception than the empty value.

Using user context in @Query

All of the previous methods mean that the data has to be somehow present in the application to authorise it. This might not be very feasible for large data sets or in case you want to use some limit or paging of the data from DB. For these cases the current user's context can be also used in the query to DB. This is done in the findAll method, which actually uses the current UserPrincipal in order to do the filtering on the DB level. This is possible due to use of SpEL, which allows using Spring things in annotations. In the sample the first condition checks the if the user is admin (using the same method as in CompanyBasedPermission above) and then checks whether the Distribution.company is the same as in current UserPrincipal. As we can see we implement exactly the same logic as in DistributionPermission, which itself isn't good, but there is no way how to express that logic in a single way for both backend and DB. This is a trade-off for making this the best way for large data or paging.

@PreAuthorize / @PreFilter

There are also other annotation, which can be used in a very similar manner, but instead of filtering/authorising the return values, they can filter/authorise the input parameters of a method.