Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to selectively inactivate REST endpoints in a Java application?

I am working on an application that consists of several backend services and a frontend client. The entire application is written in Java, and we use the Apache TomEE webserver to run it.

The backend services expose several APIs, and contain several controllers. Some of these APIs are accessible to the frontend client, and some are for internal communication between backend services.

Logging is very important for this application. There is a requirement that the logging system is always initialized before commencing normal operations (to ensure full traceability). The application uses a secure logging system that requires a key to initialize the logging (the logs are signed using this key to prevent tampering of the logs). There is also a requirement that the logging key should be uploaded to each service. Each backend service has an endpoint for receiving a logging key.

There is a "chicken or egg" type problem. The application needs to be running to receive the key, but also the application should not be fully operational until the key has been received.

To meet the requirements, we are considering the following startup procedure:

  1. Startup the backend services in a reduced mode of operation, in which the only accessible endpoint in each service is the one for receiving an incoming key.
  2. Once a key has been received, and the logging system initialized, then active the other endpoints, and commence normal operations.

Is there a standard way of activating endpoints to facilitate this startup process? or anyway of controlling access to endpoints.

Some extra information: the controllers classes within the application do not extend any other class, and are only decorated with the @Path and @Stateless annotations.


Update 1

I followed the approach of using a filter (as suggested by Bogdan below). I have created a filter that captures all requests. The application starts up correctly. The init() method in the filter class is called. But when I access the /installkey endpoint an error occurs.

What seems to happen is that the doFilter(ServletRequest, ServletResponse, FilterChain) method is being called, and my code detects that the request is for the /installkey endpoint. But an error comes from the call: filterChain.doFilter(request, response);.

I have checked, and I know that the variable filterChain is not null, however within the method doFilter(ServletRequest, ServletResponse, FilterChain) something goes wrong and I cant debug it.

Possibly, I didn't initialized something that needs to be initialized.

I have added the output that I get below.

Now I have the following in my web.xml:

<filter>
    <filter-name>myFilter</filter-name>
    <filter-class>com.company.filter.LoggingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>myFilter</filter-name>
    <url-pattern>*</url-pattern>
</filter-mapping>

And the following class:

public class LoggingFilter implements Filter {

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(final ServletRequest request, 
                         final ServletResponse response, 
                         final FilterChain filterChain) throws IOException, 
                                                               ServletException {

        String url = "";
        if (request instanceof HttpServletRequest) {
            url = ((HttpServletRequest) request).getRequestURL().toString();
        }

        if (url.endsWith("/installkey/")) {
            filterChain.doFilter(request, response);
            return;
        } else if (loggerConfig.isInitialized()) {
            filterChain.doFilter(request, response);
            return;
        }
    }

    public void destroy() {
        System.out.println("XXXXXXXXXXX Running destroy");
    }
}

But I get the following error:

Jan 19, 2016 10:42:25 AM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [default] in context with path [/vw-ws-rest] threw exception [Error processing webservice request] with root cause
java.lang.NullPointerException
    at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:240)
    at org.apache.openejb.server.cxf.rs.CxfRsHttpListener.doInvoke(CxfRsHttpListener.java:227)
    at org.apache.tomee.webservices.CXFJAXRSFilter.doFilter(CXFJAXRSFilter.java:94)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at com.company.filter.LoggingFilter.doFilter(LoggingFilter.java:63)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
    at org.apache.tomee.catalina.OpenEJBValve.invoke(OpenEJBValve.java:44)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)

Update 2

As an alternative, I tried the approach of using JAX-RS name binding, as suggested by Cássio Mazzochi Molin.

I created the interface:

import javax.ws.rs.NameBinding;

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD })
public @interface TemporarilyDisabled {
}

And I created a filter class as follows:

@Provider
@TemporarilyDisabled
public class LoggingFilter implements ContainerRequestFilter {

    @Override
    public void filter(final ContainerRequestContext requestContext) throws IOException {

        System.out.println("in filter method!");
    }
}

And updated my resource controller class as follows:

@Path("installkey")
@Stateless(name = "vw-installKeyResource")
public class VwInstallKeyResource {

    @Inject
    private Logger LOG;

    @EJB
    //... some required classes

    @POST
    @TemporarilyDisabled
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response savePlatformData(final InstallKeyData installKeyData)
        throws CryptographicOperationException, DuplicateEntryException {

        ....
    }
}

This application is using Java EE 6, which I cant update. To test this approach, I had to add the following dependency to the application:

<!-- JAX-RS -->
<dependency>
    <groupId>javax.ws.rs</groupId>
    <artifactId>javax.ws.rs-api</artifactId>
    <version>2.0</version>
</dependency>

The code all compiles ok, and the application starts up ok.

But when i access the endpoint (the endpoint that should be caught by the filter), then the filter code is not executed (I never see the print statement in the filter method), and the endpoint is simply executed as normal.

For some reason, the filter is not capturing the request.

I don't know if the problem is related to the fact that the endpoint is a POST. Alternatively, possibly JAX-RS does not find the filter class, it is decorated with @provider, but I dont know if I need to register the filter in any other way.

like image 478
user3441604 Avatar asked Jan 17 '16 15:01

user3441604


People also ask

How do you define a REST endpoint?

A REST Service Endpoint is an endpoint which services a set of REST resources.

What is endpoint in REST API with example?

When an API interacts with another system, the touchpoints of this communication are considered endpoints. For APIs, an endpoint can include a URL of a server or service. Each endpoint is the location from which APIs can access the resources they need to carry out their function.


2 Answers

I don't think you will find any out of the box solution for it.

When using JAX-RS 2.0 and its implementations, you will find some great resources: you could use a name binding filter to abort the request to a certain endpoint, based on your conditions.

What is cool in this approach is that you can keep your endpoints lean and focused on their business logic. The logic responsible for aborting the request will be in a filter. To temporarily disable one or more endpoints, you'll just have to place an annotation on them. It will activate the filter that prevents the request from reaching the endpoint. All endpoints are enabled by default and you will selectively disable the ones you don't want to receive requests.

Defining a name binding annotation

To bind filters to your REST endpoints, JAX-RS 2.0 provides the meta-annotation @NameBinding. It can be used to create other annotations that will be used to bind filters to your JAX-RS endpoints.

Consider the @TemporarilyDisabled annotation defined below. It's annotated with @NameBinding:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface TemporarilyDisabled { }

Preventing the HTTP request to reach your endpoints

The @TemporarilyDisabled annotation created above will be used to decorate a filter class, which implements ContainerRequestFilter, allowing you to abort the request:

@Provider
@TemporarilyDisabled
public class TemporarilyDisableEndpointRequestFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        
        if (isEndpointTemporarilyDisabled) {
            requestContext.abortWith(Response.status(Response.Status.SERVICE_UNAVAILABLE)
                                        .entity("Service temporarily unavailable.")
                                        .build());
        }
    }
}

The @Provider annotation marks an implementation of an extension interface that should be discoverable by JAX-RS runtime during a provider scanning phase.

You can write any condition to test if your endpoint should be temporarily disabled or not.

In the example above:

  • If the isEndpointTemporarilyDisabled condition is evaluated to true, the request will be aborted with a HTTP 503 Service Unavailable response.

  • If the isEndpointTemporarilyDisabled is evaluated to false, the request won't be aborted and will reach the endpoint the user requested.

Why aborting the request with a 503 Service Unavailable response?

According to the RFC 2616, the HTTP 503 Service Unavailable should be used in the following situations:

10.5.4 503 Service Unavailable

The server is currently unable to handle the request due to a temporary overloading or maintenance of the server. The implication is that this is a temporary condition which will be alleviated after some delay. If known, the length of the delay MAY be indicated in a Retry-After header. If no Retry-After is given, the client SHOULD handle the response as it would for a 500 response.

Note: The existence of the 503 status code does not imply that a server must use it when becoming overloaded. Some servers may wish to simply refuse the connection.

Binding the filter to your endpoints

To bind the filter to your endpoints methods or classes, annotate them with the @TemporarilyDisabled annotation defined above. For the methods and/or classes which are annotated, the filter will be executed:

@Path("/")
public class MyEndpoint {

    @GET
    @Path("{id}")
    @Produces("application/json")
    public Response myMethod(@PathParam("id") Long id) {
        // This method is not annotated with @TemporarilyDisabled
        // The request filter won't be executed when invoking this method
        ...
    }

    @DELETE
    @Path("{id}")
    @TemporarilyDisabled
    @Produces("application/json")
    public Response myTemporarilyDisabledMethod(@PathParam("id") Long id) {
        // This method is annotated with @TemporarilyDisabled
        // The request filter will be executed when invoking this method
        ...
    }
}

In the example above, the request filter will be executed only for the myTemporarilyDisabledMethod(Long) method because it's annotated with @TemporarilyDisabled.

like image 75
cassiomolin Avatar answered Nov 10 '22 20:11

cassiomolin


You could activate all the endpoints but disallow access until the key is received. You could do that by placing a Servlet Filter in front of all your endpoints that look for some flag that you set up after you activate the key.

If the flag is set (meaning the incoming key was set up successfully) then you allow access to the endpoints, otherwise from the filter you return a status of some sort (401 or 403). If you set that flag in memory or somewhere fast to read the performance overhead of the filter should be small enough to be ignored.

like image 45
Bogdan Avatar answered Nov 10 '22 21:11

Bogdan