Sunday, November 11, 2018

Micronaut and Redirect on Security Check Fail

Micronaut is a new framework for building micro-services.  Since it is focused on micro-services, it does not support a lot of the GUI features found in a framework like Grails.

One of the things missing is in the micronaut-security-session is the ability to forward onto the desired page after a successful login.  This post describes two replacement beans for the security module that works around this issue.

This example uses handlebars for the login view.  Someday, I will probably make a pull-request for the micronaut-security-session module.

The first is a redirect handler that sends the user to a login page, with the target page saved for the eventual success:

  package org.simplemes.eframe.security
  
  import groovy.util.logging.Slf4j
  import io.micronaut.context.annotation.Replaces
  import io.micronaut.core.async.publisher.Publishers
  import io.micronaut.http.HttpRequest
  import io.micronaut.http.HttpResponse
  import io.micronaut.http.HttpStatus
  import io.micronaut.http.MutableHttpResponse
  import io.micronaut.security.handlers.RejectionHandler
  import io.micronaut.security.session.SecuritySessionConfiguration
  import io.micronaut.security.session.SessionSecurityfilterRejectionHandler
  import org.reactivestreams.Publisher
  
  import javax.inject.Singleton
  
  /*
   * Copyright Michael Houston 2018. All rights reserved.
   * Original Author: mph
   *
  */
  /**
   * This handles any security rejection for any request.
   * This is different from the micronaut core implementation
   * that it replaces (SessionSecurityFilterRejectionHandler).
   * The main difference is that it saves the target path
   * for use after the login is successful.
   *
   * This class also use /login/auth and /login/denied as the
   * defaults for the unauthorized-target-url and
   * forbidden-target-url configuration properties.
   */
  @Slf4j
  @Singleton
  @Replaces(SessionSecurityfilterRejectionHandler)
  class RedirectToLoginRejectionHandler implements RejectionHandler {
  
    protected final SecuritySessionConfiguration securitySessionConfiguration
  
    /**
     * Constructor.
     * @param securitySessionConfiguration Security Session Configuration
     * @param sessionStore The session store
     * @param cookieHttpSessionStrategy The strategy for naming the cookies.
     */
    RedirectToLoginRejectionHandler(SecuritySessionConfiguration securitySessionConfiguration) {
      this.securitySessionConfiguration = securitySessionConfiguration
    }
  
    @Override
    Publisher<MutableHttpResponse<?>> reject(final HttpRequest<?> request,
                                             final boolean forbidden) {
      def accept = request.headers.get('Accept')
      if (accept?.contains('text/html') && !forbidden) {
        try {
          def location
          if (forbidden) {
            location = securitySessionConfiguration.getForbiddenTargetUrl() ?: '/login/denied'
          } else {
            location = securitySessionConfiguration.getUnauthorizedTargetUrl() ?: '/login/auth'
          }
          URI uri = new URI("${location}?target=${request.path}")
          log.debug('reject(): re-direct (303) to {} for request {}', uri, request)
          return Publishers.just(HttpResponse.seeOther(uri))
        } catch (URISyntaxException ignored) {
          return Publishers.just(HttpResponse.serverError())
        }
      }
  
      return Publishers.just(HttpResponse.status(forbidden ? HttpStatus.FORBIDDEN :
                                                   HttpStatus.UNAUTHORIZED))
     }
    
  }

The second replacement is for the successful login.  It will use the target to send the browser to the page the user originally asked for:

  package org.simplemes.eframe.security
  
  import groovy.util.logging.Slf4j
  import io.micronaut.context.annotation.Replaces
  import io.micronaut.core.convert.value.MutableConvertibleValues
  import io.micronaut.http.HttpRequest
  import io.micronaut.http.HttpResponse
  import io.micronaut.security.authentication.UserDetails
  import io.micronaut.security.filters.SecurityFilter
  import io.micronaut.security.session.SecuritySessionConfiguration
  import io.micronaut.security.session.SessionLoginHandler
  import io.micronaut.session.Session
  import io.micronaut.session.SessionStore
  import io.micronaut.session.http.HttpSessionFilter
  
  import javax.inject.Singleton
  
  /*
   * Copyright Michael Houston 2018. All rights reserved.
   * Original Author: mph
   *
  */
  
  /**
   * This class replaces the micronaut core SessionLoginHandler.  It does mostly 
   * what the core class does,
   * but it does support a target re-direct for the user.
   */
  @Slf4j
  @Singleton
  @Replaces(SessionLoginHandler)
  class RedirectSessionLoginHandler extends SessionLoginHandler {
  
    /**
     * Constructor.
     * @param securitySessionConfiguration Security Session Configuration
     * @param sessionStore The session store
     */
    RedirectSessionLoginHandler(SecuritySessionConfiguration securitySessionConfiguration,
                                SessionStore<Session> sessionStore) {
      super(securitySessionConfiguration, sessionStore)
    }
  
    @Override
    HttpResponse loginSuccess(UserDetails userDetails, HttpRequest<?> request) {
      def body = request.body?.get()
      def target = body?.target
      Session session = findSession(request)
      session.put(SecurityFilter.AUTHENTICATION, userDetails)
      try {
        if (target) {
          log.debug('loginSuccess() redirecting to {}', target)
        }
        URI location = new URI(target ?: securitySessionConfiguration.getLoginSuccessTargetUrl())
        return HttpResponse.seeOther(location)
      } catch (URISyntaxException ignored) {
        return HttpResponse.serverError()
      }
    }
  
    /**
     * Copied as-is from the core SessionLoginHandler.
     * @param request
     * @return
     */
    private Session findSession(HttpRequest<?> request) {
      MutableConvertibleValues<Object> attrs = request.getAttributes()
      Optional<Session> existing = attrs.get(HttpSessionFilter.SESSION_ATTRIBUTE, 
                                             Session.class)
      if (existing.isPresent()) {
        return existing.get()
      } else {
        // create a new session store it in the attribute
        Session newSession = sessionStore.newSession()
        attrs.put(HttpSessionFilter.SESSION_ATTRIBUTE, newSession)
        return newSession
      }
    }
  }

Finally, I have a handlebars view that is used to log the user in.  It is pretty basic, but it passes the target to the server for the RedirectSessionLoginHandler above.

  <!DOCTYPE html>
  <html>
  <head>
    {{#if errors}}
      <title>Login Failed</title>
    {{else}}
      <title>Login</title>
    {{/if}}
  </head>
  <body>
  <form action="/login" method="POST">
    <ol>
      <li>
        <label for="username">Username</label>
        <input type="text" name="username" id="username"/>
      </li>
      <li>
        <label for="password">Password</label>
        <input type="password" name="password" id="password"/>
      </li>
      <li>
        <input type="submit" value="Login"/>
      </li>
      {{#if errors}}
        <li id="errors">
          <span style="color: red;">Login Failed</span>
        </li>
      {{/if}}
    </ol>
    <input id="target" name="target" type="hidden" value="{{target}}"/>
  </form>
  </body>
  </html>









Friday, February 2, 2018

Running JUnit/Spock Grails Tests From Intellij Triggers Application Restart

This problem has cropped up several times over the years.  We never tracked down the problem until we
spent some hours debugging the spring startup logic when used with an IDE test runner (Intellij).
This also affects tests run from Gradle.

The problem is that the Grails application would restart between integration tests under some conditions.  
This slowed things down a lot and caused errors.

Symptoms:
  • The first test class starts up fine.
  • This first class initializes the Grails context correctly.
  • All tests in the first test class runs correctly.
  • The second test class then starts up.
  • This attempts to start a new Grails context, which conflicts with the original.
  • This normally fails with a session not found exception, but sometimes it is a listener port already in use error.
The example output is shown below:

Configuring Spring Security Core ...
... finished configuring Spring Security Core

Grails application running at http://localhost:64901 in environment: test     



Configuring Spring Security Core ...
... finished configuring Spring Security Core

Grails application running at http://localhost:64901 in environment: test     


org.hibernate.HibernateException: No Session found for current thread
 at org.grails.orm.hibernate.GrailsSessionContext.currentSession(GrailsSessionContext.java:117)
 at org.hibernate.internal.SessionFactoryImpl.getCurrentSession(SessionFactoryImpl.java:688)
 at org.grails.orm.hibernate.HibernateSession.createQuery(HibernateSession.java:177)
 at org.grails.orm.hibernate.HibernateSession.createQuery(HibernateSession.java:170)

. . .

Root Cause:

The root cause is that the tests had different super-class hierarchies.  One test had an @Integration
annotation and its super-class also had an @Integration annotation.  In the internals of Spring
(AbstractTestContextBootstrapper.buildMergedContextConfiguration()), the MergedContextConfiguration
internally refers to two Application classes.  The other test class on referred to one Application class,
so Spring thinks they are two different application contexts.   Spring then tries to start the application again.

Fix:

Make sure your test classes have the @Integration annotation just once in their hierarchies (at the test
class itself).

Also, it turns out there is no need for the @Integration annotation in the super-class.

Friday, May 6, 2016

Mocking DSL From a Plugin

Sometimes, you want to test some Grails logic that uses one of the nice DSLs (Domain Specific Language) from a plugin.  For example, you are using the Mail Service Plugin and want to test the code that calls it:

mailService.sendMail {
  to email
  subject title
  text body
}

This is not simple with normal mocking tools.  What you need is a simple Mock mechanism that records the method calls made via the DSL.  The DSLMock class (shown at the end) can handle this.  The test will look something like this:

@Testvoid testAlertLogging() {
  def mailDSLMock = DSLMock.mock(MailService, 'sendMail', [])
  service.mailService = new MailService()

  service.triggerAlert('dummy@abc.xyz')
  // check email was sent to the right user with the right subject/body
  assert mailDSLMock.result.contains([name: 'to', args: ['dummy@abc.xyz']])
  def subjectMap = mailDSLMock.result.find() { it.name == 'subject' }
  assert subjectMap.args[0] == 'Default Subject'

  def bodyMap = mailDSLMock.result.find() { it.name == 'text' }
  assert body.bodyMap.args[0].contains('alert')
}


The DSLMock will simply record the calls to the DSL and your test code must verify the correct calls were made.

The DSLMock class is:

class DSLMock {
  /**   
  * Creates a mock for the given clazz and method.   
  * @param clazz The class to add the mock to.   
  * @param method The method that starts the DSL.   
  * @param result The expected result of the DSL.   
  * @return The mock.   */
  static DSLMock mock(clazz, method, result) {
    def tester = new DSLMock()
    clazz.metaClass.static."${method}" = { Closure dsl ->
      dsl.delegate = tester
      dsl.resolveStrategy = Closure.DELEGATE_FIRST      dsl()
      return result
    }
    return tester
  }

  /**   * The list of method calls made to the mocked DSL and their arguments.   */

  def result = []

  /**   
  * Used to record the method calls made to the mock.   
  * @param name The method name.    
  * @param args The arguments.   
  * @return   */

  def methodMissing(String name, args) {
    result << [name: name, args: args.toList()]
  }
}

(Sorry for the formatting of the code.  I am still learning how to use the blog editor).

Grails 3 Testing Hints/Tips

Grails 3.x changed a lot of things. Most for the good, but this introduced a number of changes in our test code.  Some of the misc hints/tips for testing are:
  • Don't ever put @Integration in a test super-class.  Only use this in the concrete test classes.
  • IDEA needs the tests to be marked with @Test to run in the IDEA/JUnit world.  Gradle can still run them, but the IDEA JUnit system misses them.
  • Don't use @Rollback together with .withTransaction{} in most cases.  The .withTransaction{} gets rolled back too.
  • Unit Tests need to use the save(flush:true)syntax now.
  • For GEB testing of GUIs, never use @Transaction.  Instead use .withTransaction{} to create records.  The test code will not commit until it exits the method, so the server will never see the data while the test method is running.
  • To test a controller in an integration test, you will need to send an HTTP request to the running server. I use HTTPClient for this.  A login is needed to do this (similar to GEB, but at the pure HTTP level, no GUI).
  • application.groovy is needed for custom GORM data types.  The application.yml does not support the grails.gorm.default.mapping.user-type syntax needed for a custom user type.
(The last one is not test related, but I did not want to create a new blog post for it :).

Some of these could be bugs that might need fixes, but I am just not sure they are worth the time to fix them.  The work-arounds seem acceptable.


Saturday, April 16, 2016

Big Gotcha with Adding New Artefact Type to Grails

I just recently lost a day fighting some issues with Grails and a custom artefact in my plugin development.  The cause was simple: some classes must be Java classes to work correctly.

I followed the format of the quartz plugin and created my own versions of the artefact handlers and such.  I missed that some of the files were .java files:

  • DefaultGrailsJobClass.java
  • GrailsJobClass.java

My clones of those two files were .groovy files.

Of course, I started getting weird results like stack over flow exceptions and methods not found (e.g. getName()).  Even getClass() would fail with a stack over flow.

It turns out Grails is using some metaClass magic with the artefact class DefaultGrailsJobClass, and you just can't use Groovy for them.  I causes the Grails magic to get stuck.

Also, you can store these .java files under the src/main/groovy without problems.

As usual, when you deviate from the Grails approach, odd things jump up and bite you...

Sunday, February 7, 2016

Running Grails 3 JUnit Tests 3-4 Times Faster with Intellij

Grails 3 uses Gradle for the build and most development tasks such as running unit tests.  This is great, but during the edit/compile/test cycle, it adds a good bit of overhead.  When you make a change and want to test it with a unit test, the Gradle process takes much longer than the Intellij/IDEA JUnit test runner (30sec vs. 7sec).

Why is this?  When you make a small change to your code under test, Gradle will recompile the entire groovy codebase.  This takes time.  Also, the Gradle process is much less efficient at detecting code changes than IDEA.  Gradle can take 25sec to compile the code while IDEA takes 3sec.

To make this work, you need to run the unit test using IDEA's JUnit test runner and use the built-in make logic:



Note: This uses the really nice action 'Run context configuration from editor' (Ctrl+Shift+F10 on Windows).  I have also set the JUnit default in the Run Configuration to set the VM Options to '-ea -Dserver.port=8091 -Dgrails.env=test'.  This is not needed for JUnit tests, but I use it for Integration and GUI tests (a future blog).

Also, the IDEA and Gradle compilers tend to step on each other's output class files.  This causes a lot of trouble when the classes are used together.  You need to set the test output path on the project settings to a directory that is different from the Gradle build directory ('.../testi' below):



This avoids the problem, but the output path is overwritten when IDEA updates the project settings from the build.gradle file.  So, if you get weird errors when testing, do a Gradle clean and then check the output settings above.

Update for IDEA 2016.1 (March 2016).  When upgrading, you will need to refresh the gradle project.  This changes the project structure in IDEA significantly, but it makes sure the JUnit tests are run correctly:


If you don't do this, then you get a lot of Spring DEBUG/INFO trace messages like this:

DEBUG o.s.w.c.s.StandardServletEnvironment - Initializing new StandardServletEnvironment

Welcome

Welcome.

This blog is meant to document some of my experiences in producing complex applications/plugins using Grails, Groovy and various tools.  This includes how to use Jetbrains Intellij/IDEA with Grails.

Grails is a very opinionated framework.  If you follow its conventions, then it is one of the most productive environments I know of for developing complex database-oriented web applications.  While Grails is very powerful and concise, if you stray outside of the lines, it can be difficult to diagnose problems.

This blog is meant to help others with these problems.  I considered just logging these on stackoverflow.com, but they don't like it when you post the question and answer together.

I hope others will benefit from my mistakes :).