Skip to main content
Skip table of contents

Creating Swagger-enabled XNAT REST APIs in a Plugin

The updated XNAT API method known as XAPI allows you to use annotations to specify how your methods should work. Perhaps most useful is the new restrictTo field, which makes it easy to set which users should be able to use your method. This means you won't have to add if statements to every method you write merely to do basic permissions checking. Once you know how restrictTo works, it will be quicker to see at a glance whether the permissions are correctly set on your methods. Another major advantage of using XAPI is that all XAPI methods are added to your site's Swagger page, meaning that you can invoke them from a user interface (rather than only from the command line). This makes it a lot easier to quickly invoke these methods and see whether they are working properly.

Getting Started

We encourage all open source developers to place custom code in plugins, rather than modifying the XNAT code itself. If you are starting fresh, you should consult our page on developing XNAT plugins. The main files you will need in your plugin are the build.gradle and settings.gradle files, a class with your XAPI REST calls, and a plugin class annotated with @XnatPlugin and with @ComponentScan (with the package of your XAPI REST call class included in the ComponentScan). The focus of this article is on the Java class containing your XAPi REST calls, so if you need more general plugin help, please consult our plugin documentation.

Your XAPI Class

You should create a Java class in your plugin and annotate it with @XapiRestController and @Api("The name you want for your API"). You should also give your class a @RequestMapping annotation, which all REST calls in the class will include in their URL (this helps keep your REST calls organized and provides information in the REST URL about what the thing is that is being operated upon). You may also wish to add a logger to log any errors or other information you might want logged. You should also add a constructor for your class. You will want to have userManagementService and roleHolder as arguments to your constructor. You may also want to add additional arguments, for example StuffService service (if you have a service for managing the storage and access to your stuff). At this point you should have a class that looks something like this:

StuffPlugin
JAVA
package org.nrg.stuff.rest;
import org.nrg.stuff.StuffService;
import org.nrg.framework.annotations.XapiRestController;
import org.nrg.xapi.rest.AbstractXapiRestController;
import org.nrg.xapi.rest.XapiRequestMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.nrg.xdat.security.services.RoleHolder;
import org.nrg.xdat.security.services.UserManagementServiceI;

@Api("API for stuff")
@XapiRestController
@RequestMapping(value = "/stuff")
public class StuffRestApi extends AbstractXapiRestController {
	
	@Autowired
	public StuffRestApi(final UserManagementServiceI userManagementService, final RoleHolder roleHolder, final StuffService service) {
   		super(userManagementService, roleHolder);
   		__service = service;
	}

	private static final Logger _log = LoggerFactory.getLogger(StuffRestApi.class);
	private final StuffService _service;
}

Adding methods

Now that you have created a class for your new REST calls it's time to create them! Let's say we want to add a REST call that will take a JSON representation of a piece of stuff that we want to create and create the stuff in XNAT. The method to add this REST call might look something like this:

createStuff method
JAVA
@ApiOperation(value = "Creates a new stuff object from the submitted attributes.", notes = "Returns the newly stuff object with the submitted attributes.", response = Stuff.class)
@ApiResponses({@ApiResponse(code = 200, message = "Returns the newly created stuff object."),
               @ApiResponse(code = 400, message = "Bad request, likely a missing stuff ID."),
               @ApiResponse(code = 403, message = "Insufficient privileges to create the submitted stuff object."),
               @ApiResponse(code = 500, message = "An unexpected or unknown error occurred.")})
@XapiRequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST)
@ResponseBody
public ResponseEntity<Stuff> createStuff(@RequestBody final Stuff stuff) throws Exception {
	if(StringUtils.isBlank(stuff.getIdentifier())){
      	_log.error("User {} tried to create stuff without an identifier.", getSessionUser().getUsername());
		return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
    final UserI userI = XDAT.getUserDetails();
	if(_service.canCreateStuff(userI)){
	    stuff.setOwner(userI.getUsername());
    	final Stuff newlyCreatedStuff = _service.create(stuff);
    	return new ResponseEntity<>(newlyCreatedStuff, HttpStatus.CREATED);
	}
	else{
		return new ResponseEntity<>(HttpStatus.FORBIDDEN);
	}
}

The method consumes JSON, maps it to a Stuff object (which we named stuff), checks whether stuff has an identifier. If it does not, the method returns a BAD_REQUEST (400). If it does, the method gets the UserI object for the user who made the request and checks whether the user can create stuff. If the user cannot, the method returns a FORBIDDEN (403). If the user can create stuff, the method then has the stuff service create the requested stuff. This service could be implemented a variety of ways, such as making direct database queries, using Hibernate, or using XNAT's XFTItem. Finally, the method returns a JSON representation of the created stuff object.

Adding RestrictTo

However, we might want to separate the code that checks whether the user can make the REST call from the code that actually performs the REST call. Let's say that only admins are supposed to be able to add stuff objects to the site. Then we can remove the user check from the method itself and replace it with a restrictTo (in this case: restrictTo = Admin). After that change, the method looks like this:

createStuff method
JAVA
@ApiOperation(value = "Creates a new stuff object from the submitted attributes.", notes = "Returns the newly stuff object with the submitted attributes.", response = Stuff.class)
@ApiResponses({@ApiResponse(code = 200, message = "Returns the newly created stuff object."),
               @ApiResponse(code = 400, message = "Bad request, likely a missing stuff ID."),
               @ApiResponse(code = 403, message = "Insufficient privileges to create the submitted stuff object."),
               @ApiResponse(code = 500, message = "An unexpected or unknown error occurred.")})
@XapiRequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = Admin))
@ResponseBody
public ResponseEntity<Stuff> createStuff(@RequestBody final Stuff stuff) throws Exception {
	if(StringUtils.isBlank(stuff.getIdentifier())){
      	_log.error("User {} tried to create stuff without an identifier.", getSessionUser().getUsername());
		return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
	stuff.setOwner(userI.getUsername());
    final Stuff newlyCreatedStuff = _service.create(stuff);
    return new ResponseEntity<>(newlyCreatedStuff, HttpStatus.CREATED);
}

If we wanted a method that would get the stuff objects associated with one of the user's projects the user has read access to, we could do a request mapping like this:

getStuffForProject
JAVA
@XapiRequestMapping(value = {"/projects/{projectId}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = RequestMethod.GET, restrictTo = Read)
public ResponseEntity<List<Stuff>> getStuffForProject(@PathVariable("projectId") @ProjectId final String projectId) {

The @ProjectId annotation is important so that the code processing "restrictTo = Read" knows which project to check permissions of.

Only Project Permissions Checked

The Read, Edit, and Delete restrictTo options ONLY check project permissions. If your REST calls operate on non-project XNAT objects like subjects and sessions, the permissions for these should be checked separately. This is particularly important if data is shared into other projects (where users don't have equal access to both projects) or if some of your users are in custom user groups and should be restricted from operating on some of the types of data within a project. To properly handle these cases, you can add explicit checks to your code like 'if(Permissions.canEdit(user,session))' or 'if(Permissions.canRead(user,subject))'. You can also define your own more complex permissions checks using the restrictTo = Authorizer option discussed below.

Read can be true even if a user doesn't have full Read access

In XNAT 1.7.5, the meaning of restrictTo = Read was changed. It will now let users that can view the project use the method that has the restrictTo. If a project is set to protected in XNAT, a user will be able to view the project page, but limited data will be displayed and there will be a button to request access. All users (including guest if login is not required on the XNAT) will be able to use REST calls that have restrictTo = Read on protected projects. Depending on what the method does, this may result in users being shown data that they should not have access to. If you want to check that a user has explicitly been granted access to the project, you can do this check: StringUtils.isNotBlank(Permissions.getUserProjectAccess(user, projectId)). You may also want to permit users that are admins or have all data access to perform your REST call: Roles.isSiteAdmin(user) || Groups.isDataAdmin(user) || Groups.isDataAccess(user) .

 

We could also have a REST call which would return all of a user's stuff. We would only want users to be able to get their own stuff (though admins should be able to see all stuff). This can be accomplished by setting restrictTo = User like so:

getStuffForUser
JAVA
@XapiRequestMapping(value = {"/user/{username}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = RequestMethod.GET, restrictTo = User)
public ResponseEntity<List<Stuff>> getStuffForUser(@ApiParam(value = "Username of the user to fetch stuff for", required = true) @PathVariable("username") @Username final String username) {

 

Similar to the @ProjectId annotation, we need the @Username annotation to tell the processing code which user's stuff is trying to be obtained. If the current user is trying to get stuff for herself or is the site admin, the request will be allowed.

All Data Access

Prior to XNAT 1.7.5, XNAT had a bug where XAPI REST calls did not check whether a user had All Data Access. If a user was not a Site Manager (a.k.a. an admin), but had been given All Data Access, they would not have been able to access data using XAPI REST calls (and would also not have been able to access data when the new Permissions class was used to check whether they had access). If you wanted these users to be able to use XAPI REST calls, you could have either made them an admin, or you could have used the Authorizer annotation discussed below to write your own permissions checks.

 

The values restrictTo can have are Authenticated (true if user is currently signed in), Role (if the user has one of the roles in the @AuthorizedRoles roles), Edit (if the user can edit the @ProjectId project), Delete (if the user can delete the @ProjectId project), Owner (if the user is an owner of the @ProjectId project), Member (if the user is a member or owner of the @ProjectId project), Collaborator (if the user is a collaborator, member, or owner of the @ProjectId project).

restrictTo Owner/Member/Collaborator

Doing a Owner/Member/Collaborator restrictTo may not actually be doing the check you expect. It only checks whether the user making the request has that specific role. If you have restrictTo = Owner, the site administrator will not be able to perform the REST call unless they have been explicitly added as a project owner. In some cases that may be what you want, but often it would be better to use restrictTo = Delete so that both project owners and admins can perform the REST call.

 

RestrictTo Authorizer

But let's say we want to have more complicated permissions on who can create stuff. Let's say that we want admins to create stuff, but also want to be able to give other users the StuffCreator role and let them create stuff too. Let's also say that after a trial run period with a more limited set of stuff creators, we want to be able to allow everyone to create stuff (though we also want to be able to turn this option off if needed). We probably want to keep all this logic out of the createStuff method, especially since we may also want to add other REST calls which can create stuff from a set of arguments rather than from a single JSON argument, or add REST calls to create multiple stuff objects. To eliminate this code duplication, we can set restrictTo = Authorizer and then add the @AuthDelegate(CreateStuffXapiAuthorization.class) annotation to the stuff creation methods. So the method would become:

createStuff method
JAVA
@ApiOperation(value = "Creates a new stuff object from the submitted attributes.", notes = "Returns the newly stuff object with the submitted attributes.", response = Stuff.class)
@ApiResponses({@ApiResponse(code = 200, message = "Returns the newly created stuff object."),
               @ApiResponse(code = 400, message = "Bad request, likely a missing stuff ID."),
               @ApiResponse(code = 403, message = "Insufficient privileges to create the submitted stuff object."),
               @ApiResponse(code = 500, message = "An unexpected or unknown error occurred.")})
@XapiRequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST, restrictTo = Authorizer))
@ResponseBody
@AuthDelegate(CreateStuffXapiAuthorization.class)
 public ResponseEntity<Stuff> createStuff(@RequestBody final Stuff stuff) throws Exception {
	if(StringUtils.isBlank(stuff.getIdentifier())){
      	_log.error("User {} tried to create stuff without an identifier.", getSessionUser().getUsername());
		return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
	stuff.setOwner(userI.getUsername());
    final Stuff newlyCreatedStuff = _service.create(stuff);
    return new ResponseEntity<>(newlyCreatedStuff, HttpStatus.CREATED);
}

 

And we would need to create a CreateStuffXapiAuthorization class as well. This should be an @Component and this class' package should be added to the @ComponentScan in your plugin class. It should also extend AbstractXapiAuthorization. We will have the considerGuests method return false because we do not ever want guests to create stuff (if considerGuests is false, guests will immediately get a not authenticated exception). 

Finally, we need a checkImpl method, which should return true in situations where we want users to be granted access to the REST call. So we end up with something like this:

CreateStuffXapiAuthorization
JAVA
package org.nrg.stuff.authorization;

import ...

@Component
public class CreateStuffXapiAuthorization extends AbstractXapiAuthorization {

    @Override
    protected boolean checkImpl() {
        final UserI user = getUser();
		//Return true if user is an admin, user has the StuffCreator role, or the site configuration property we will create is true (the property will be named: "allUsersCanCreateStuff")
        if(TurbineUtils.isSiteAdmin(user) || TurbineUtils.checkRole(user, "StuffCreator") || XDAT.getSiteConfigurationProperty("allUsersCanCreateStuff") ){
            return true;
        }
        return false;
    }

    @Override
    protected boolean considerGuests() {
        return false;
    }
	
    private static final Logger _log = LoggerFactory.getLogger(CreateStuffXapiAuthorization.class);
}

Using the XAPI REST Calls

Once we have added the class defining our XAPI REST calls to our plugin and added the plugin to our site, we're ready to start using them! The URLs for our REST calls starts with the base URL of our site (e.g. localhost:8080/xnat for a local XNAT where it is not the ROOT webapp), followed by "/xapi", followed by the value of the RequestMapping annotation in our REST calls class, followed by the value of the XapiRequestMapping annotation for the REST call. For example, the getStuffForProject REST call would be accessible at localhost:8080/xnat/xapi/stuff/projects/{projectId}.

We can also access these REST calls using Swagger. If I was trying to access the Swagger page for the local XNAT described above, I would go to localhost:8080/xnat/xapi/swagger-ui.html. This should look something like this:

If we then use one of the REST calls, we see something like this:

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.