04 October 2016

Written by Christo Mastoroudes

Introduction

This blog will show how to check the permissions of users making REST calls by using annotation. These changes were made to our Forms for Confluence plugin.

Problem

In the past we were only concerned with checking that the user making the call was indeed a global admin. Previously only the global admin had access to the Forms for Confluence admin section. Recently we implemented a new feature which required us to change the way permissions are checked. This is because we now also have a space level admin section which is relevant only to forms created in that space. This means that we have to first establish if the call is being made from the global admin level or space level, and then checking the permissions of the user.

Solution

This is part of the original rest endpoint:

@GET
@Path("/emails/{id}")
public Response getEmail(@PathParam("id") Long id,
                         @QueryParam("fullyLoaded") @DefaultValue("true") boolean fullyLoaded) {
    return safeCallAsAdmin(id, fullyLoaded, new RestCallerTwo<Long, Boolean>() {
        @Override
        public Response call(Long id, Boolean fullyLoaded) {
            return entityResponse(inboxManager.findById(id, fullyLoaded));
        }
    });
}

We took the opportunity to delegate the responsibility of authentication away from the receiver, because this would improve code legibility, reuse and maintenance.

What we ended up with is the following:

@GET
@Path("/emails/{id}")
@ResourceFilters(SpaceOrConfluenceAdminResourceFilter.class)
public Response getEmail(@PathParam("id") Long id,
                         @QueryParam("fullyLoaded") @DefaultValue("true") boolean fullyLoaded,
                         @QueryParam("spaceKey") String spaceKey) {
    return entityResponse(inboxManager.findById(id, fullyLoaded));
}

Notice the annotation @ResourceFilters. The argument is the class we created which will be responsible for checking permissions.

Here is a snippet of the SpaceOrConfluenceAdminResourceFilter class:

@Override
public ContainerRequest filter(ContainerRequest containerRequest) {
    String spaceKey = "";
    if (containerRequest.getQueryParameters().get(SPACE_KEY) != null) {
        spaceKey = containerRequest.getQueryParameters().get(SPACE_KEY).get(0);
    }

    if (isAdminOrSpaceAdmin(spaceKey)) {
        return containerRequest;
    } else {
        throw new AuthenticationRequiredException();
    }
}

protected boolean isAdminOrSpaceAdmin(final String spaceKey) {
    if (spaceKey.isEmpty()) {
        return permissionManager.hasPermission(AuthenticatedUserThreadLocal.get(),
               Permission.ADMINISTER, PermissionManager.TARGET_SYSTEM);
    } else {
        return spacePermissionManager.hasPermission(SpacePermission.ADMINISTER_SPACE_PERMISSION, 
               spaceManager.getSpace(spaceKey), AuthenticatedUserThreadLocal.get());
    }
}

We can determine if the user making the originating call is in a space by using the following javascript, and then adding it to the rest call as a QueryParam.

spaceKey = AJS.params.spaceKey;

$http.post(AJS.contextPath() + path + '?' + "spaceKey=" + spaceKey, postData);

If the query parameter passed to the rest endpoint is empty, that means that the call is being made from the global admin section of our pluging. This is because the global administration section of Confluence is not contained within any space. This means that we know we have to check if the user has global permissions.

If the query parameter passed is a space, then we check the space permission privileges for a space administrator. Since a global admin will have space admin privileges for all spaces we do not distinguish global admin from space admin in this instance. However we do need to consider when it is a space admin, if this specific space admin does have space admin privileges for the space in question.

Worth mentioning here is whether to use PathParam or QueryParam to send the space key. I learned it is better to use QueryParam if the parameter is only sent in certain situations. PathParam is useful in situations where the parameter is always sent. In this case the parameter is only sent if the call originates from a space, so a QueryParam is appropriate.

Conclusion

By using the ResourceFilters annotation we have simplified the REST endpoint and made the code easier to understand and modify, by abstracting away the permission checking of the caller. Any new REST endpoint added simply needs to add one line for the user permissions to be checked.



blog comments powered by Disqus