Sunday, May 28, 2017

Create a Secured Restful API App with Grails 3 and PostgreSQL- Step by Step: Part 4 of 5

Posts on this topic

Part 4: Secure end-points fully and cleanly

I left my last post with basic domain objects (Artist, ArtWork, Specification) created, and one of the core domain objects (Artistexposed as a RESTful resource by leveraging Grails provided @Resource annotation. Without writing any further code, I got all CRUD operations for Artist resource working in RESTful way. That's pretty neat out-of-the-box default implementation provided by Grails framework. I also ended my last post with a note about not-so-readable UUID and date formats. This post is a continuation of previous and is all about securing Artist resource end-point fully.

Environment: Grails 3.1.6, Java 1.8, IntelliJ 15 on Mac OS X 10.9.5

Step 1 First, let's make id, dateCreated and lastUpdated formats more readable
There are multiple ways to customize data formatting.

i) The easiest way is to simply register JSON marshallers as shown below in Bootstrap.groovy. Grails runs grails-app/init/*Bootstrap classes' init closure(s) at the startup of the application.

grails-app/init/Bootstrap.groovy
class BootStrap { def init = { //register JSON marshaller for Date grails.converters.JSON.registerObjectMarshaller(Date){ return it?.format('MM/dd/yyyy') } //register JSON marshaller for UUID grails.converters.JSON.registerObjectMarshaller(UUID){ return it?.toString() } ... } ... }

ii) Another way to register marshallers is by defining a Spring bean that registers all marshallers as shown below:

src/main/groovy/com/giri/marshallers/CustomObjectMarshaller.groovy
package com.giri.marshallers /** * Custom object marshaller trait for all custom object marshallers to implement. */ trait CustomObjectMarshaller { abstract void register() }

src/main/groovy/com/giri/marshallers/UUIDMarshaller.groovy
package com.giri.marshallers import grails.converters.JSON /** * UUID marshaller, registers a {@link JSON} marshaller to output the string representation of {@link UUID} */ class UUIDMarshaller implements CustomObjectMarshaller { @Override void register(){ JSON.registerObjectMarshaller(UUID){ UUID uuid-> return uuid.toString() } } }

src/main/groovy/com/giri/marshallers/DateMarshaller.groovy
package com.giri.marshallers import grails.converters.JSON /** * Date marshaller, registers a {@link JSON} marshaller to output the string representation of {@link Date} */ class DateMarshaller implements CustomObjectMarshaller { @Override void register() { JSON.registerObjectMarshaller(Date) {Date date -> return date.format('MM/dd/yyyy') } } }

src/main/groovy/com/giri/marshallers/CustomMarshallerRegistrar.groovy
package com.giri.marshallers import javax.annotation.PostConstruct /** * Custom Marshaller Registrar, registers custom object marshallers with spring. * Configured as a spring managed bean in resources.groovy * * @see resources.groovy */ class CustomMarshallerRegistrar { /** List of custom marshallers to be registered, initialized with bean configuration in resources.groovy */ List marshallers @PostConstruct void registerCustomMarshallers() { marshallers.each{ it.register() } } }

grails-app/conf/spring/resources.groovy
import com.giri.marshallers.CustomMarshallerRegistrar import com.giri.marshallers.DateMarshaller import com.giri.marshallers.UUIDMarshaller beans = { //JSON Marshallers customMarshallerRegistrar(CustomMarshallerRegistrar) { marshallers = [ new UUIDMarshaller(), new DateMarshaller() ] } }

With this, UUID and date formats in the response look like:
$ curl -i -X GET 'http://localhost:8080/api/artists' HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ... [{"id":"8d6698a1-03db-4676-973b-bb374aa1381c","dateCreated":"05/27/2017","firstName":"Giri","lastName":"Pottepalem","lastUpdated":"05/27/2017"}]

iii) There is even a better way of customizing the response using Grails recent addition- JSON views, which is not covered in this post.

Step 2 Secure Resource end-point

Let's start securing Artist resource end-point.

When domain class is annotated with @Resource, Grails provides  RestfulController implementation for CRUD actions, maps them to appropriate HTTP method verbs and makes the resource accessible at the end-point specified through uri property of @Resource annotation in RESTful way.

In addition, Spring security core plugin's @Secured annotation can be applied on the domain object to secure the resource. In my previous post's Step 9, I allowed everyone access to /api/artists end-point by annotating Artist domain object with @Secured(['permitAll']). With this all CRUD operations are allowed without a login. We need to secure this resource now.

Let's say we want to allow only Admin user to access Artist resource. This can easily be done by changing the annotation to @Secured(['ROLE_ADMIN']) or simply to @Secured('ROLE_ADMIN')

With that, the end-point /api/artists is now secured and is accessible to only users Admin role. Let's test it.

Get Artists
$ curl -i -X GET 'http://localhost:8080/api/artists' HTTP/1.1 403 Forbidden Server: Apache-Coyote/1.1 Content-Type: application/json;charset=UTF-8 ...

Login as Admin
$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"admin"}' http://localhost:8080/api/login HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Cache-Control: no-store Pragma: no-cache Content-Type: application/json;charset=UTF-8 Content-Length: 93 Date: Wed, 24 May 2017 22:14:07 GMT {"username":"admin","roles":["ROLE_ADMIN"],"access_token":"ucsbqbd3f26fjpb5b6794ph7cbu3fqq2"}

Get Artists as logged in Admin
$ curl -i -H "X-Auth-Token: ucsbqbd3f26fjpb5b6794ph7cbu3fqq2" http://localhost:8080/api/artists HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application:development Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 24 May 2017 22:15:13 GMT []

Post an Artist
$ curl -i -X POST -H "Content-Type:application/json" -d '{ "firstName": "Giri", "lastName": "Pottepalem" }' 'http://localhost:8080/api/artists' HTTP/1.1 403 Forbidden Server: Apache-Coyote/1.1 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 24 May 2017 22:25:52 GMT {"timestamp":1495664752925,"status":403,"error":"Forbidden","message":"Access Denied","path":"/api/artists"}

Post an Artist as logged in Admin
$ curl -i -X POST -H "X-Auth-Token: ucsbqbd3f26fjpb5b6794ph7cbu3fqq2" -H "Content-Type: application/json" -d '{ "firstName": "Giri", "lastName": "Pottepalem" }' 'http://localhost:8080/api/artists' HTTP/1.1 201 Created Server: Apache-Coyote/1.1 X-Application-Context: application:development Location: http://localhost:8080/api/artists/a0480de2-d5df-43eb-a919-196e34c40ab5 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 24 May 2017 22:53:07 GMT {"id":"a0480de2-d5df-43eb-a919-196e34c40ab5","dateCreated":"05/24/2017","firstName":"Giri","lastName":"Pottepalem","lastUpdated":"05/24/2017"}

Step 3 Secure Resource end-point properly and fully

Though @Secured('ROLE_ADMIN') makes the resource secured easily to the role specified, this may not meet the actual security requirements. Let's say, the right level of security we want to apply to the end-point: /api/artists is as follows:
  • Allow everyone to see the list of Artists
  • Only allow admin to create/delete an Artist
  • A logged in Artist can only see/update his/her details
We have now specific security logic that we need to apply to different actions on the resource. This level of customization is not possible with @Secured annotation applied at the resource-level. It requires some customization at the action level and this is where we can implement our own REST controller for the resource to achieve this. Grails comes with grails.rest.RestfulController base implementation that can be extended. This is not required but gives you some common base logic that can be leveraged.

Let's generate a REST controller now for the resource/domain object. Grails 3 offers create-restful-controller command for creating a RESTful controller.

$ grails create-restful-controller com.giri.Artist | Created grails-app/controllers/com/giri/ArtistController.groovy

The generated class looks like:
package com.giri import grails.rest.* import grails.converters.* class ArtistController extends RestfulController { static responseFormats = ['json', 'xml'] ArtistController() { super(Artist) } }

The generated controller is minimal with default implementation for all actions derived from the base RestfulController class provided by Grails. With this controller in place for our customization we no longer need @Resource and @Secured annotations on the domain class. Let's remove those and add URL mappings in UrlMappings.groovy for the resource.

grails-app/contrllers/giri/api/UrlMappings.groovy
package giri.api class UrlMappings { static mappings = { ... "/api/artists"(resources: 'artist') }

With this we can run grails url-mappings-report command to check url mappings for the resource end-point. It should look same as it was with @Resource applied on the Artist domain class. Now we can provide necessary action-methods implementations and secure each action-method with @Secured annotation and achieve custom security that we wanted for the end-point's each HTTP verb mapped to a specific action-method.

The customized ArtistController class looks like(with custom security highlighted):
package com.giri import grails.plugin.springsecurity.annotation.Secured import grails.rest.RestfulController import grails.transaction.Transactional /** * Customized Artists RestfulController. * * @author Gpottepalem * Created on May 26, 2017 */ class ArtistController extends RestfulController { static responseFormats = ['json', 'xml'] ArtistController() { super(Artist) } @Secured('permitAll') @Override def index(Integer max) { super.index(max) } @Secured('ROLE_USER') @Override def show() { super.show() } @Secured('ROLE_ADMIN') @Override def save() { super.save() } @Secured('ROLE_USER') @Override def update() { super.update() } @Secured('ROLE_ADMIN') @Override def delete() { super.delete() } }
Not much customization is done to achieve this other than overwriting just needed action-methods and annotating them properly per security requirements. All overwritten methods simple delegate the implementation to the super class.
Gotcha: There is no need for @Transactional annotation for methods like save(), update() and delete() as they all simply call corresponding super methods and all super methods are annotated appropriately for transactionality. In fact, annotating these methods again in this kind of implementation results into exception ;)

Step 4 Test fully customized Resource end-point

With the required customization done, let's take a spin and test it. Note that I have bootstrapped an admin user and a me user as specified in Step 4 of my earlier post.

# GET Artists (index) $ curl -i -X GET 'http://localhost:8080/api/artists' HTTP/1.1 200 OK ... []

# Login as Admin $ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"admin"}' http://localhost:8080/api/login HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ... {"username":"admin","roles":["ROLE_ADMIN"],"access_token":"h1tdbs1cc8e7qt1bt7ohpsar57nt8car"}

# Login as me user $ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"username":"me","password":"password"}' http://localhost:8080/api/login HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ... {"username":"me","roles":["ROLE_USER"],"access_token":"ci9ct5hocreljl5pbqga60npsi8ol03f"}

# POST Artist as user (save) $ curl -i -X POST -H "X-Auth-Token: ci9ct5hocreljl5pbqga60npsi8ol03f" -H "Content-Type: application/json" -d '{ "firstName": "Giri", "lastName": "Potte" }' 'http://localhost:8080/api/artists' HTTP/1.1 403 Forbidden Server: Apache-Coyote/1.1 ... {"timestamp":1495891839637,"status":403,"error":"Forbidden","message":"Access is denied","path":"/api/artists"}

# POST Artist as admin (save) $ curl -i -X POST -H "X-Auth-Token: h1tdbs1cc8e7qt1bt7ohpsar57nt8car" -H "Content-Type: application/json" -d '{ "firstName": "Giri", "lastName": "Potte" }' 'http://localhost:8080/api/artists' HTTP/1.1 201 Created Server: Apache-Coyote/1.1 ... {"id":"8d6698a1-03db-4676-973b-bb374aa1381c","dateCreated":"05/27/2017","firstName":"Giri","lastName":"Potte","lastUpdated":"05/27/2017"}

# GET Artists (index) $ curl -i -X GET 'http://localhost:8080/api/artists' HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ... [{"id":"8d6698a1-03db-4676-973b-bb374aa1381c","dateCreated":"05/27/2017","firstName":"Giri","lastName":"Potte","lastUpdated":"05/27/2017"}]

# GET an Artist (show) $ curl -i -X GET 'http://localhost:8080/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c' HTTP/1.1 403 Forbidden Server: Apache-Coyote/1.1 ... {"timestamp":1495893259443,"status":403,"error":"Forbidden","message":"Access Denied","path":"/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c"}

# GET an Artist (show) as admin - secured for ROLE_USER $ curl -i -X GET -H "X-Auth-Token: h1tdbs1cc8e7qt1bt7ohpsar57nt8car" 'http://localhost:8080/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c' HTTP/1.1 403 Forbidden Server: Apache-Coyote/1.1 ... {"timestamp":1495893471587,"status":403,"error":"Forbidden","message":"Access is denied","path":"/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c"}

# GET an Artist (show) as me user - secured for ROLE_USER $ curl -i -X GET -H "X-Auth-Token: ci9ct5hocreljl5pbqga60npsi8ol03f" 'http://localhost:8080/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c' HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ... {"id":"8d6698a1-03db-4676-973b-bb374aa1381c","dateCreated":"05/27/2017","firstName":"Giri","lastName":"Potte","lastUpdated":"05/27/2017"}

# PUT Artist as admin (update) $ curl -i -X PUT -H "X-Auth-Token: h1tdbs1cc8e7qt1bt7ohpsar57nt8car" -H "Content-Type: application/json" -d '{ "lastName": "Pottepalem" }' 'http://localhost:8080/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c' HTTP/1.1 403 Forbidden ... {"timestamp":1495892176757,"status":403,"error":"Forbidden","message":"Access is denied","path":"/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c"}

# PUT Artist as me user (update) $ curl -i -X PUT -H "X-Auth-Token: ci9ct5hocreljl5pbqga60npsi8ol03f" -H "Content-Type: application/json" -d '{ "lastName": "Pottepalem" }' 'http://localhost:8080/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c' HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ... {"id":"8d6698a1-03db-4676-973b-bb374aa1381c","dateCreated":"05/27/2017","firstName":"Giri","lastName":"Pottepalem","lastUpdated":"05/27/2017"}

# GET Artists (index) $ curl -i -X GET 'http://localhost:8080/api/artists' HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ... [{"id":"8d6698a1-03db-4676-973b-bb374aa1381c","dateCreated":"05/27/2017","firstName":"Giri","lastName":"Pottepalem","lastUpdated":"05/27/2017"}]

# DELETE Artist as user (delete) $ curl -i -X DELETE -H "X-Auth-Token: ci9ct5hocreljl5pbqga60npsi8ol03f" 'http://localhost:8080/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c' HTTP/1.1 403 Forbidden Server: Apache-Coyote/1.1 ... {"timestamp":1495892582172,"status":403,"error":"Forbidden","message":"Access is denied","path":"/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c"}

# DELETE Artist as admin (delete) $ curl -i -X DELETE -H "X-Auth-Token: h1tdbs1cc8e7qt1bt7ohpsar57nt8car" 'http://localhost:8080/api/artists/8d6698a1-03db-4676-973b-bb374aa1381c' HTTP/1.1 204 No Content Server: Apache-Coyote/1.1 ...

# GET Artists (index) $ curl -i -X GET 'http://localhost:8080/api/artists' HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ... []

# Logout admin $ curl -i -X POST -H "X-Auth-Token: h1tdbs1cc8e7qt1bt7ohpsar57nt8car" http://localhost:8080/api/logout HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ...

# Logout user $ curl -i -X POST -H "X-Auth-Token: ci9ct5hocreljl5pbqga60npsi8ol03f" http://localhost:8080/api/logout HTTP/1.1 200 OK Server: Apache-Coyote/1.1 ...

Everything looks good except that any logged-in user can see/update any other user as we only secured update method to ROLE_USER. We can easily add some custom logic to show() and update() action-methods to lock-down these actions further so that a logged-in user can only see/update his/her own user. Grails Spring Security core plugin provides SpringSecurityService class that can be leveraged to achieve this. Since the basic domain model currently I have has not evolved enough for making this check, I am only showing pseudo-coding-steps here:
class ArtistController extends RestfulController { SpringSecurityService springSecurityService ... def update() { AppUser currentUser = springSecurityService.currentUser as AppUser AppUser updateArtist = //Find user account of the Artist's id (params.id) being updated if(currentUser != updateArtist) { respond([message: 'Access Denied'], status: HttpStatus.FORBIDDEN) return } else { ... } }

Gotchas

Grails Spring Security Core plugin's login form
The default Grails Spring Security Core plugin provided login action url: /login/auth when accessed runs into an exception upon not finding an associated view resulting into Internal Server Error response. This is available due to "/$controller/$action?/$id?(.$format)?" mapping in UrlMappings.groovy and /login/auth is mapped to LoginController's auth() action-method provided by Grails Spring Security Core plugin. There is no point in having this wide-open anymore as it provides a form-based login for web application which is not used with Grails Spring Security REST plugin. So, let's lock it down.

When /login/auth is accessed, it runs into the following exception:
javax.servlet.ServletException: Could not resolve view with name '/login/auth' in servlet with name 'grailsDispatcherServlet'

And the response looks like:
$ curl -i -X GET 'http://localhost:8080/login/auth' HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 ... Connection: close {"message":"Internal server error","error":500}

Lock it down by adding the following pattern to staticRules in application.groovy
grails.plugin.springsecurity.controllerAnnotations.staticRules = [ [pattern: '/login/auth', access: ['denyAll']] //lock down spring security login form url ... ] //Spring Security REST API plugin config String statelessFilters = 'JOINED_FILTERS, -exceptionTranslationFilter, -authenticationProcessingFilter, -securityContextPersistenceFilter, -rememberMeAuthenticationFilter' //common def filterChainChainMaps = [ //Stateless chain [pattern: '/api/**', filters: statelessFilters], [pattern: '/**', filters: statelessFilters] //Traditional stateful chain - We are stateless, no stateful chain is required ] grails.plugin.springsecurity.filterChain.chainMap = filterChainChainMaps

With this, when we access /login/auth, we get the following response:
$ curl -i -X GET 'http://localhost:8080/login/auth' HTTP/1.1 403 Forbidden Server: Apache-Coyote/1.1 ... {"timestamp":1495926839720,"status":403,"error":"Forbidden","message":"Access Denied","path":"/login/auth"}

Using Custom subclass of RestfulController with @Resource annotation
If you prefer to annotate your custom RestfulController with @Resource instead of mapping the resource in UrlMappings.groovy, there is a small section in Grails docs that describes how to get this done. However, it has some limitations at the time of my exploration as I had to place the controller under src/main/groovy instead of under grails-app/controllers.

References

No comments:

Post a Comment