Custom Access Manager
Introduction
This section will provide detailed information on how to implement a custom Access Manager.
For further help, please refer to the RESTHeart support channels https://restheart.org/support.html
The Access Manager is responsible of authorizing the users against a security policy.
Following the dependency injection approach, the actual AM implementation to use is specified in the configuration file.
There is a ready-to-use AM implementation and custom ones can be developed.
This section explains how to develop a custom AM.
If you develop a general purpose AM please consider contributing to RESTHeart project using the github pull request feature.
The steps required to develop and configure an AM are:
- develop the AM in Java
- configure RESTHeart to use the new AM via its configuration file
- add the implementation class(es) to the java classpath
Develop
The AM class
The AM implementation class must implement the interface org.restheart.security.AccessManager
The AM is a singleton instance of this class.
Constructor
The constructor must have the following signature:
public MyAccessManager(Map<String, Object> args)
The argument args maps the access-manager properties as specified in the configuration file.
Methods to implement
The interface org.restheart.security.AccessManager mandates to implement 2 methods:
Modifier and Type | Method and Description |
---|---|
boolean |
isAllowed(HttpServerExchange exchange, RequestContext context);
returns true if request is allowed
|
boolean | isAuthenticationRequired(final HttpServerExchange exchange);
return true if not authenticated user won't be allowed
|
The second method isAuthenticationRequired() checks if authentication is required. If it returns true, then the request is actually checked against the first method. If it return false, then any user, including unauthenticated ones, will be allowed.
The actual security policy check occurs in the method isAllowed().
The RequestContext context argument easily allows to get information about the request. More information is available in the HttpServerExchange exchange arguments. In fact, context is derived from exchange.
context vs exchange
The context object is easier to use.
For instance:
- it incapsulates the request json payload as an object resulting form the parsing of the json string data, where the exchange gives access to it as a an StreamSourceChannel
- In case of a file resource, it incapsulates the file type (e.g. image/png for images)
- it provides some convenient properties, such as type (resource type, such as DOCUMENT or COLLECTION) and method (GET, PUT, POST, etc)
Configuration
The AM is configured in the access-manager section of the yaml configuration file.
Here you specify the actual AM implementation to use and any parameters it needs (for instance, the path of the file where the security policy is defined or some parameters that control caching).
For example, if the access-manager configuration section is:
access-manager:
implementation-class: org.restheart.examples.security.MyAccessManager
arg1: 5
arg2: hey man!
arg3:
arg31: 1
arg32: 2
Then:
- the AM singleton will be of class MyAccessManager
- its constructor will be invoked passing a Map argument with 4 keys
- implementation-class of class String
- arg1 of class Integer
- arg2 of class String
- arg3 of class Map<String, Object>, having in turn 2 keys (arg31 and arg32)
How to add the custom classes to the classpath
Using the java classpath option
The custom classes must be added to the java classpath.
In order to achieve this, start RESTHeart with the following command:
java -server -classpath restheart.jar:custom-classes.jar org.restheart.Bootstrapper restheart.yml
Using the Maven shade plugin
The maven share plugin provides the capability to package the artifact in an uber-jar, including its dependencies and to shade - i.e. rename - the packages of some of the dependencies.
It allows to create a single jar including any RESTHeart class and your custom ones. In this case you can start RESTHeart with
java -server -jar restheart_plus_custom.jar restheart.yml
Example
A project with RESTHeart customization examples is available on github; find it at restheart-customization-examples.
It includes the ExampleAccessManager; this is a simple AM that hardcodes the security policy:
- allow any authenticated user to GET /_logic/aggregate
- allow any authenticated user to GET,POST /test/bands
- allow any authenticated user to GET /test/bands/<bandid>
- allow users with ROLE admin to GET,PUT,PATCH,DELETE /test/bands/<bandid>
Any other requests are not allowed.
The implementation class follows:
package org.restheart.examples.security;
import io.undertow.attribute.ExchangeAttributes;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import java.util.Map;
import java.util.Set;
import org.restheart.examples.applogic.ExampleAggregateHandler;
import org.restheart.handlers.RequestContext;
import org.restheart.handlers.RequestContext.METHOD;
import static org.restheart.handlers.RequestContext.PATCH;
import org.restheart.handlers.RequestContext.TYPE;
import org.restheart.security.AccessManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Andrea Di Cesare <andrea@softinstigate.com>
*/
public class ExampleAccessManager implements AccessManager {
private static final Logger LOGGER = LoggerFactory.getLogger("org.restheart.examples.security.ExampleAccessManager");
public static final String AGGREGATE_URI = "/_logic/aggregate";
public ExampleAccessManager(Map<String, Object> arguments) {
// args are ignored
}
@Override
public boolean isAllowed(HttpServerExchange exchange, RequestContext context) {
// *** request info
String requestURI = exchange.getRequestURI();
String db = null;
String collection = null;
TYPE requestType;
METHOD requestMethod;
// context is null for secured non-mongodb resources, e.g. /_logic
if (context != null) {
db = context.getDBName();
collection = context.getCollectionName();
requestType = context.getType();
requestMethod = context.getMethod();
} else {
db = null;
collection = null;
requestType = null;
requestMethod = selectRequestMethod(exchange.getRequestMethod());
}
// *** user info
String username = ExchangeAttributes.remoteUser().readAttribute(exchange);
Set<String> roles = null;
if (exchange.getSecurityContext() != null && exchange.getSecurityContext().getAuthenticatedAccount() != null) {
roles = exchange.getSecurityContext().getAuthenticatedAccount().getRoles();
}
if (username == null || roles == null) {
LOGGER.warn("DENIED, user is not authenticated.");
return false;
}
// log request
LOGGER.debug("checking request {} {} from user {} with roles {}", requestMethod, requestURI, username, roles);
// allow any authenticated user to GET /_logic/aggregate
if (AGGREGATE_URI.equals(requestURI)) {
if (METHOD.GET.equals(requestMethod)) {
LOGGER.debug("ALLOWED (GET request to {} are allowed)", requestURI);
return true;
}
}
// allow any authenticated user to GET,POST /test/bands
if (TYPE.COLLECTION.equals(requestType)) {
if (ExampleAggregateHandler.DB.equals(db)
&& ExampleAggregateHandler.COLL.equals(collection)
&& (METHOD.GET.equals(requestMethod))
|| METHOD.POST.equals(requestMethod)) {
LOGGER.debug("ALLOWED (anyone can GET and POST /test/bands)");
return true;
}
}
// allow any authenticated user to GET /test/bands/<bandid>
if (TYPE.DOCUMENT.equals(requestType)
&& ExampleAggregateHandler.DB.equals(db)
&& ExampleAggregateHandler.COLL.equals(collection)
&& METHOD.GET.equals(requestMethod)) {
LOGGER.debug("ALLOWED (anyone can GET /test/bands/<docid>)");
return true;
}
// allow users with ROLE admin to GET,PUT,PATCH,DELETE /test/bands/<bandid>
if (roles.contains(ExampleIdentityManager.ROLE.ADMIN.name())
&& TYPE.DOCUMENT.equals(requestType)
&& ExampleAggregateHandler.DB.equals(db)
&& ExampleAggregateHandler.COLL.equals(collection)
&& (METHOD.PUT.equals(requestMethod)
|| METHOD.PATCH.equals(requestMethod)
|| METHOD.DELETE.equals(requestMethod))) {
LOGGER.debug("ALLOWED (admins can PUT, PATCH and DELETE /test/bands/<docid>)");
return true;
}
LOGGER.warn("DENIED, no permission found.");
return false;
}
@Override
public boolean isAuthenticationRequired(HttpServerExchange exchange) {
// always require authentication
return true;
}
private static METHOD selectRequestMethod(HttpString _method) {
METHOD method;
if (Methods.GET.equals(_method)) {
method = METHOD.GET;
} else if (Methods.POST.equals(_method)) {
method = METHOD.POST;
} else if (Methods.PUT.equals(_method)) {
method = METHOD.PUT;
} else if (Methods.DELETE.equals(_method)) {
method = METHOD.DELETE;
} else if (PATCH.equals(_method.toString())) {
method = METHOD.PATCH;
} else if (Methods.OPTIONS.equals(_method)) {
method = METHOD.OPTIONS;
} else {
method = METHOD.OTHER;
}
return method;
}
}