In our last blog post, we have created a simple blog application using JHipster, which is a Yeoman based code generator for Angular and Spring Boot. This time, we will explore and improve the Spring Boot REST API that is generated by JHipster automatically.

In the last blog, we had created following model; a simple blog:

In this blog post, we will concentrate on the „Blog“ objects. We will try to create, read, update and delete such objects. We will find out that any user has all rights to perform those function, and we will make sure that only the blog owner can update and delete the objects.

Step 1: Consulting Swagger

A good thing about JHipster is, that it not only creates a Spring Boot application with the desired objects, but it also auto-generates a swagger documentation page about the API. This will help us find out how to handle the API.

Another good thing about JHipster is, that the REST API is secure by default in the sense that only logged in users will receive positive answers.

If we try to retrieve a blog (http://localhost:9000/api/blogs/4 in this case), we will get the answer, that we are not authorized:

{
    "timestamp": "2017-08-23T19:41:41.175+0000",
    "status": 401,
    "error": "Unauthorized",
    "message": "Access Denied",
    "path": "/api/blogs/4"
}

That is good in one sense, but we need to find out how to authenticate, before we can perform anything on the API. For that, we consult the swagger API documentation on http://localhost:/9000/#/docs. We find an entry for the user-jwt-controller and it tells us that we

According to the documentation, we need to POST a body of the format

{
  "password": "user",
  "rememberMe": true,
  "username": "user"
}

According to swagger, this has to be sent to the URL http://localhost:9060/api/authenticate. However, this did not work in my case. In my case, I had to send the POST to http://localhost:9000/api/authenticate instead. This might be a reason, why swagger’s Try it out! Button did not work.  I got an error telling me that there was no response from the server:

Also the curl command had to be reworked a little bit: the port had to be changed from 9060 to 9000 and the Backslashes had to be removed. But then it worked fine:

(anyhost)$ curl -X POST --header 'Content-Type: application/json' --header 'Accept: */*'  -d '{
   "password": "user",
   "rememberMe": true,
   "username": "user"
 }' 'http://localhost:9000/api/authenticate'
{
  "id_token" : "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwiYXV0aCI6IlJPTEVfVVNFUiIsImV4cCI6MTUwNTkxOTg3NH0.n4B1RrmokbjuSQqWjOxNcUWgUXoqZODLb-nzEN-Km-7zjx7sGElWL8xgSWhc3DMrTHQDauT81ZZKF4IrmNh71A"
}

The curl command can be issued from the container or the docker host. Or it can be issued on the Vagrant host (a Windows 10 machine in my case), that is hosting the docker host VM, if port 9000 is mapped from Vagrant host to the VM:

In the latter case, we can use the graphical POSTman instead of a curl command, if we wish to:

Step 2: Create/Get Tokens

In the previous step, swagger has helped us to retrieve a token. Let us now write the result into an environment variable like follows:

(anyhost)$ USER_TOKEN=$(curl -k -D - -X POST --header 'Content-Type: application/json' --header 'Accept: */*' -d '{ "password": "user", "rememberMe": true, "username": "user" }' 'http://localhost:9000/api/authenticate' | grep id_token | awk -F '["]' '{print $4;}')
 % Total % Received % Xferd Average Speed Time Time Time Current
 Dload Upload Total Spent Left Speed
100 258 0 196 100 62 164 51 0:00:01 0:00:01 --:--:-- 164
(anyhost)$ echo $USER_TOKEN
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwiYXV0aCI6IlJPTEVfVVNFUiIsImV4cCI6MTUwNjExMDU5NX0.MozFvp72L23PyAPsHg2tLfaxmqIRQXhT0DGrlRwAZDXISaceANqJIeOkaXbZXwDNPGW-3H_n3bzAwitvCeZE8g
Similarily, we can write the admin's token into a different environment variable:
(anyhost)$ ADMIN_TOKEN=$(curl -k -D - -X POST --header 'Content-Type: application/json' --header 'Accept: */*' -d '{ "password": "admin", "rememberMe": true, "username": "admin" }' 'http://localhost:9000/api/authenticate' | grep id_token | awk -F '["]' '{print $4;}')
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   276    0   212  100    64   1532    462 --:--:-- --:--:-- --:--:--  1536

Step 3: Create a Blog

Step 3.1: Explore createBlog on Swagger

Now let us use the token to create a blog. We consult swagger again. we find a createBlog function on

POST /api/blogs

with the example body

{
  "handle": "string",
  "id": 0,
  "name": "string",
  "user": {
    "activated": true,
    "email": "string",
    "firstName": "string",
    "id": 0,
    "imageUrl": "string",
    "langKey": "string",
    "lastName": "string",
    "login": "string",
    "resetDate": "2017-08-21T13:49:44.421Z"
  }
}

However, it does not make sense to specify all user parameters in the request. The user is not a blog private variable, it is only a pointer to an existing user. And the user is fully specified by the user ID (ID=4 for user named „user“). Therefore, let us try to use a minimized version as follows:

{
  "handle": "usersBlogCreatedByApi",
  "name": "user's blog created by API",
  "user": {
    "id": 4
  }
}

We also have omitted the blog id 0, since the ID will be assigned automatically.

Step 3.2: Find the user ID

How did I know that the user named „user“ has the ID 4? I did not. I just issued the following curl command

(anyhost)$ curl -X GET --header 'Accept: */*' --header "Authorization: Bearer $USER_TOKEN" 'http://localhost:9000/api/users'

[ {
  "id" : 1,
  "login" : "system",
  "firstName" : "System",
  "lastName" : "System",
  "email" : "system@localhost",
  "imageUrl" : "",
  "activated" : true,
  "langKey" : "en",
  "createdBy" : "system",
  "createdDate" : "2017-08-15T11:47:06.180Z",
  "lastModifiedBy" : "system",
  "lastModifiedDate" : null,
  "authorities" : [ "ROLE_USER", "ROLE_ADMIN" ]
}, {
  "id" : 3,
  "login" : "admin",
  "firstName" : "Administrator",
  "lastName" : "Administrator",
  "email" : "admin@localhost",
  "imageUrl" : "",
  "activated" : true,
  "langKey" : "en",
  "createdBy" : "system",
  "createdDate" : "2017-08-15T11:47:06.180Z",
  "lastModifiedBy" : "system",
  "lastModifiedDate" : null,
  "authorities" : [ "ROLE_USER", "ROLE_ADMIN" ]
}, {
  "id" : 4,
  "login" : "user",
  "firstName" : "User",
  "lastName" : "User",
  "email" : "user@localhost",
  "imageUrl" : "",
  "activated" : true,
  "langKey" : "en",
  "createdBy" : "system",
  "createdDate" : "2017-08-15T11:47:06.180Z",
  "lastModifiedBy" : "system",
  "lastModifiedDate" : null,
  "authorities" : [ "ROLE_USER" ]
} ]

And there it is: the user named „user“ has the ID=4.

Step 3.3: Create the Blog

Now let us create the blog:

(anyhost)$ curl -D - -X POST --header 'Content-Type: application/json' --header 'Accept: */*' --header "Authorization: Bearer $USER_TOKEN" -d '{
    "handle": "users_blog_via_api_using_user_token",
    "name": "user'\''s blog created by the API using the user'\''s token",
    "user": {
      "id": 4
    }
  }' 'http://localhost:9000/api/blogs'

HTTP/1.1 201 Created
x-powered-by: Express
x-blogapp-alert: blogApp.blog.created
expires: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-xss-protection: 1; mode=block
pragma: no-cache
location: /api/blogs/19
date: Wed, 23 Aug 2017 20:25:11 GMT
connection: close
x-content-type-options: nosniff
content-type: application/json;charset=UTF-8
x-application-context: blog:swagger,dev:8080
x-blogapp-params: 19
transfer-encoding: chunked

{
  "id" : 19,
  "name" : "user's blog created by the API using the user's token",
  "handle" : "users_blog_via_api_using_user_token",
  "user" : {
    "id" : 4,
    "login" : null,
    "firstName" : null,
    "lastName" : null,
    "email" : null,
    "activated" : false,
    "langKey" : null,
    "imageUrl" : null,
    "resetDate" : null
  }
}

Note that I have escaped the single quotes by ending the quote with a ', then write an escaped quote \' and start the quote again with a single '. Together a single quote within single quotes is escaped as '\''.

The blog can be seen on the JHipster UI on http://localhost:9000/#/blog:

Step 4: Update the Blog

Step 4.1: Consult Swagger

Before we delete the blog, let us update the blog. For that, let us consult swagger again:

The updateBlog function is similar to the createBlog function. However, this time we will not omit the blog ID, since this is the way to tell the API, which entity is to be updated. As with the createBlog function, we can omit all user’s variables apart from the user ID. This way, we can move the blog to the ownership of the admin, if we wish. Let us do that:

curl -D - -X PUT --header 'Content-Type: application/json' --header 'Accept: */*' --header "Authorization: Bearer $USER_TOKEN" -d '{ 
 "id": 19,
 "handle": "users_blog_via_api_using_user_token", 
 "name": "modified user'\''s blog created by the API using the user'\''s token and assigned to the admin now", 
 "user": { "id": 3 } 
}' 'http://localhost:9000/api/blogs'


HTTP/1.1 200 OK
x-powered-by: Express
x-blogapp-alert: blogApp.blog.updated
expires: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-xss-protection: 1; mode=block
pragma: no-cache
date: Wed, 23 Aug 2017 20:43:46 GMT
connection: close
x-content-type-options: nosniff
content-type: application/json;charset=UTF-8
x-application-context: blog:swagger,dev:8080
x-blogapp-params: 19
transfer-encoding: chunked

{
  "id" : 19,
  "name" : "modified user's blog created by the API using the user's token and assigned to the admin now",
  "handle" : "users_blog_via_api_using_user_token",
  "user" : {
    "id" : 3,
    "login" : "admin",
    "firstName" : "Administrator",
    "lastName" : "Administrator",
    "email" : "admin@localhost",
    "activated" : true,
    "langKey" : "en",
    "imageUrl" : "",
    "resetDate" : null
  }
}

With this request, we have assinged the blog to the admin and we have changed the name of the blog.

Note that you most probably need to change the blog ID of the request: use the ID you have received in the createBlog response.

Note that it is not possible to change only the name of the blog without specifying the handle or the user. The handle is mandatory also for PUT (Update) requests, and the user is cleared, if it is omitted.

Step 5: Delete the Blog

Now that we have created and updated a blog, let us delete the blog. We will see that the user can delete a blog that is owned by the admin. This is something we will improve later on.

Step 5.1: Consult Swagger

Again, let us have a look to Swagger on http://localhost:9000/#/docs:

Step 5.2: Delete Blog

For deletion of blog with ID=19, we just need to send a HTTP DELETE to /api/blogs/19:

(anyhost)$ curl -D - -X DELETE --header 'Content-Type: application/json' --header 'Accept: */*' --header "Authorization: Bearer $USER_TOKEN" 'http://localhost:9000/api/blogs/19'
HTTP/1.1 200 OK
x-powered-by: Express
x-blogapp-alert: blogApp.blog.deleted
expires: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-xss-protection: 1; mode=block
pragma: no-cache
date: Wed, 23 Aug 2017 20:53:21 GMT
connection: close
x-content-type-options: nosniff
content-length: 0
x-application-context: blog:swagger,dev:8080
x-blogapp-params: 19

We receive a 200 OK message. Yes, the blog was destroyed as expected, as can be verified on the UI again.

However, in a real-world scenario, the admin would be pissed, if a normal user was allowed to delete a blog owned by the admin, I guess. Let us improve the situation.

Step 6: Improving the API

Step 6.1: Improve the Answer when trying to delete an non-existing entity

Before improving the API, let us, see, the answer of the system, if we try to delete a blog that does not exist anymore:

(anyhost)$ curl -D - -X DELETE --header 'Content-Type: application/json' --header 'Accept: */*' --header "Authorization: Bearer $USER_TOKEN" 'http://localhost:9000/api/blogs/19'

HTTP/1.1 500 Internal Server Error
x-powered-by: Express
expires: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-xss-protection: 1; mode=block
pragma: no-cache
date: Mon, 21 Aug 2017 18:27:13 GMT
connection: close
x-content-type-options: nosniff
content-type: application/json;charset=UTF-8
x-application-context: blog:swagger,dev:8080
transfer-encoding: chunked

{
  "message" : "error.internalServerError",
  "description" : "Internal server error",
  "fieldErrors" : null
}

A HTTP 500 Server Error! Not good. It seems like the JHipster implementation is sub-optimal. We should receive a 404 Not Found Error instead.

A clue for the reason can be seen in the log of the server:

2017-08-21 18:27:13.426 DEBUG 4253 --- [  XNIO-2 task-8] org.jhipster.web.rest.BlogResource       : REST request to delete Blog : 9
Hibernate: select blog0_.id as id1_0_0_, blog0_.handle as handle2_0_0_, blog0_.name as name3_0_0_, blog0_.user_id as user_id4_0_0_, user1_.id as id1_6_1_, user1_.created_by as created_2_6_1_, user1_.created_date as created_3_6_1_, user1_.last_modified_by as last_mod4_6_1_, user1_.last_modified_date as last_mod5_6_1_, user1_.activated as activate6_6_1_, user1_.activation_key as activati7_6_1_, user1_.email as email8_6_1_, user1_.first_name as first_na9_6_1_, user1_.image_url as image_u10_6_1_, user1_.lang_key as lang_ke11_6_1_, user1_.last_name as last_na12_6_1_, user1_.login as login13_6_1_, user1_.password_hash as passwor14_6_1_, user1_.reset_date as reset_d15_6_1_, user1_.reset_key as reset_k16_6_1_ from blog blog0_ left outer join jhi_user user1_ on blog0_.user_id=user1_.id where blog0_.id=?
2017-08-21 18:27:13.431 ERROR 4253 --- [  XNIO-2 task-8] org.jhipster.aop.logging.LoggingAspect   : Exception in org.jhipster.web.rest.BlogResource.deleteBlog() with cause = 'NULL' and exception = 'No class org.jhipster.domain.Blog entity with id 9 exists!'

org.springframework.dao.EmptyResultDataAccessException: No class org.jhipster.domain.Blog entity with id 9 exists!

If we peek into org.jhipster.web.rest.BlogResource, the reason becomes clear:

@DeleteMapping("/blogs/{id}")
    @Timed
    public ResponseEntity deleteBlog(@PathVariable Long id) {
        log.debug("REST request to delete Blog : {}", id);
        blogRepository.delete(id); 
        return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();      
    }

The deleteBlog function calls blogRepository.delete(id) without checking, whether a blog with the corresponding id exists. Let us improve the situation:

    @DeleteMapping("/blogs/{id}")
    @Timed
    public ResponseEntity deleteBlog(@PathVariable Long id) {
        log.debug("REST request to delete Blog : {}", id);
        
        Blog blog = blogRepository.findOne(id);
      
        if(blog != null) { 
        	blogRepository.delete(id); 
        	return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();
        } else {
        	return ResponseEntity.notFound().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();
        }
        
        
    }

This has done the job:

(anyhost)$ curl -D - -X DELETE --header 'Content-Type: application/json' --header 'Accept: */*' --header "Authorization: Bearer $USER_TOKEN" 'http://localhost:90
00/api/blogs/9'

HTTP/1.1 404 Not Found
x-powered-by: Express
x-blogapp-alert: blogApp.blog.deleted
expires: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-xss-protection: 1; mode=block
pragma: no-cache
date: Mon, 21 Aug 2017 18:36:03 GMT
connection: close
x-content-type-options: nosniff
content-length: 0
x-application-context: blog:swagger,dev:8080
x-blogapp-params: 9

Let us update swagger correspondingly:

Step 6.2: Make sure only Owner can DELETE an blog object

In the moment, every user that is logged in can delete a blog. Let us make sure only the user of the blog can delete the object.

For that, let us try the following:

    @DeleteMapping("/blogs/{id}")
    @Timed
    public ResponseEntity deleteBlog(@PathVariable Long id) {
        log.debug("REST request to delete Blog : {}", id);
        
        Blog blog = blogRepository.findOne(id);
        
        
        if(blog != null) {
        	if (!blog.getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin())) { 
        		// The user is not allowed to delete this blog, if it is not owned by this user:
            	        log.debug("Found blog, but user is not allowed to delete it");
            	        return ResponseEntity.status(403).headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();
        	} else {
	        	blogRepository.delete(id); 
	        	return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();
        	}
        } else {
        	return ResponseEntity.notFound().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id.toString())).build();
        }
        
        
    }

If we now create a blog for user „admin“:

$ curl -D - -X POST --header 'Content-Type: application/json' --header 'Accept: */*' --header "Authorization: Bearer $ADMIN_TOKEN" -d '{
   "id": 9, "handle": "users_blog_via_api_using_admin_token",
   "name": "modified user'\''s blog created by the API using the admin'\''s token",
   "user": {
     "id": 3
   }
 }' 'http://localhost:9000/api/blogs'

HTTP/1.1 200 OK
x-powered-by: Express
x-blogapp-alert: blogApp.blog.updated
expires: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-xss-protection: 1; mode=block
pragma: no-cache
date: Mon, 21 Aug 2017 19:30:40 GMT
connection: close
x-content-type-options: nosniff
content-type: application/json;charset=UTF-8
x-application-context: blog:swagger,dev:8080
x-blogapp-params: 9
transfer-encoding: chunked

{
  "id" : 18,
  "name" : "modified user's blog created by the API using the admin's token",
  "handle" : "users_blog_via_api_using_admin_token",
  "user" : {
    "id" : 3,
    "login" : "admin",
    "firstName" : "Administrator",
    "lastName" : "Administrator",
    "email" : "admin@localhost",
    "activated" : true,
    "langKey" : "en",
    "imageUrl" : "",
    "resetDate" : null
  }
}

Now we try to delete it with as user „user“:

$ curl -D - -X DELETE --header 'Content-Type: application/json' --header 'Accept: */*' --header "Authorization: Bearer $USER_TOKEN" 'http://localhost:90
00/api/blogs/18'

HTTP/1.1 403 Forbidden
x-powered-by: Express
x-blogapp-alert: blogApp.blog.deleted
expires: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-xss-protection: 1; mode=block
pragma: no-cache
date: Mon, 21 Aug 2017 19:33:20 GMT
connection: close
x-content-type-options: nosniff
content-length: 0
x-application-context: blog:swagger,dev:8080
x-blogapp-params: 18

That works as expected.

Now let us delete it as user admin:

$ curl -D - -X DELETE --header 'Content-Type: application/json' --header 'Accept: */*' --header "Authorization: Bearer $ADMIN_TOKEN" 'http://localhost:9
000/api/blogs/18'

HTTP/1.1 200 OK
x-powered-by: Express
x-blogapp-alert: blogApp.blog.deleted
expires: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-xss-protection: 1; mode=block
pragma: no-cache
date: Mon, 21 Aug 2017 19:39:34 GMT
connection: close
x-content-type-options: nosniff
content-length: 0
x-application-context: blog:swagger,dev:8080
x-blogapp-params: 18

Perfect, this is doing the job.

Excellent!

 

Summary

We have explored the API of a Spring Boot application created by JHipster. We have seen how we can use Swagger to find out, how to use the API. We have created, updated and deleted an object. After seeing that the JHipster implementation of the API is sub-optimal, we have improved the API:

  • we have made sure the API does not throw a HTTP 500 Server Error, if someone tries to delete a non-existing entity. Instead, we will return a 404 Not Found Error.
  • We have show, how we can make sure that only the owner can delete an entity.

Next Steps

  • We have (not yet) applied the improvements on other functions like create (automatically assign the object to the logged in user as owner), update.
  • In the moment, only logged-in users can read the entities. We could show, how to make an entity visible to the world instead, i.e. how to allow anonymous access.

 

 

 

Comments

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.