Posts on this topic
Part 2: Add Core Security and REST API Security
Part 3: Add PostgreSQL and start coding
Part 5: Assure REST & Publish your API
Part 3: Add PostgreSQL and start coding
Part 5: Assure REST & Publish your API
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 (Artist) exposed 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.
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.
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 formThe 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.
No comments:
Post a Comment