How to selectively inactivate REST endpoints in a Java application?
Asked Answered
K

2

10

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.

Kraft answered 17/1, 2016 at 15:37 Comment(6)
Which JAX-RS implementation are you using? Jersey? RESTEasy? Apache CXF? Other?Celisse
If you are using Jersey, have a look here.Celisse
the application itself only has a dependency on Java EE (it does not have a dependency on a specific JAX-RS implementation) but it is being executed in Apache TomEE. Would this mean that the implementation is provided by the Apache?Kraft
Yes, it's very likely you are using a implementation provided by Apache TomEE, which I don't know what is. My solution works for JAX-RS 2.0 implementations. If you can, I do recommend using JAX-RS 2.x rather then JAX-RS 1.x.Celisse
In the end, it worked as expected following your suggested approach. But with one problem. My application is deployed as several war files, which I put in the webapps directory. However, if I include the filter class in a war in webapps, then it is not found. I get an error "SEVERE: Exception starting filter LoggingFilter". To get it to work, I have to include the filter class in the libs directory, which means I have to have more complex deployment process. Is this the normal behavior? or is it possible to include a filter in a war, in webapps?Kraft
Which approach have you chosen? If you will use your filter in multiple WARs, you can pack it in a JAR and use the JAR as a dependency of your WAR.Celisse
C
6

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.

Celisse answered 19/1, 2016 at 10:52 Comment(6)
Hi, thanks for that suggestion. I am not sure if I totally understand your approach, but it looks like an approach for disabling endpoints during the entire duration that an application is running. I need the ability to initially have some endpoints disabled (all except one endpoint), but later enable all the endpoints.If I follow your approach, then the filter method should contain some logic that checks if a condition is true, and if it is true, then it should enable (or forward the request to) the appropriate endpoint. Does the approach that you suggest allow that?Kraft
@Kraft Yes, having your DisabledEndpointRequestFilter bound to the endpoints you need to disable, you can refuse the request or forward it to some other endpoint. It depends on your requirements.Celisse
ok.... what i am asking is how do you forward the request? if the filter method has no code in it, are the requests forwarded by default? or does a method have to be called to explicitly forward them? (in the initial approach that I tried, the filter method has to call "filter.doFilter(...)" to actually call the endpoint, and this was the line that caused me problems)Kraft
thanks! I will play around with your suggestion.... although i am slightly confused now. I dont understand why you talked about "temporary redirects". The logic I need is to simply initially (when the application starts up) disable some endpoints, and later enable them, and its not clear to me how to enable them. Its not clear if (inside the filter method), if the block condition is not meet, then will the endpoint be called? (in other words, if the request is not aborted, is it forwarded by default? or do I have to call a method to explicitly forward the request).Kraft
@Kraft I understand your "forward" as a "temporary redirect", since you will perform it only when your main endpoint is "disabled". Please, note all the endpoints are enabled by default. You won't need to "enable" it. Anyways, you can perform whatever you want in your filter.Celisse
Let us continue this discussion in chat.Kraft
Z
3

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.

Zeringue answered 17/1, 2016 at 21:52 Comment(3)
Hi, thanks for that answer, its really useful. I have one more related question: if I want to share a single class that will perform the filtering, is there a way to make that class accessible to all the deployed services (which are wars) without having to include in it in those war files? and without out adding a dependency to those services? (I included a filter class into one of the wars, but i get errors from other deployed services "SEVERE: One or more Filters failed to start.")Kraft
@user3441604: You could have the filter class written somewhat decoupled from services implementations and place it inside a separate JAR that you deploy in all your WARs, but you do need to declare the <filter-mapping> and configure the order in which filters apply (if your services have any other filters) in web.xml. So I don't think you can get away by not including it into your WARs.Zeringue
Hi Bogdan. Thanks for that. I added an update if you have more time!Kraft

© 2022 - 2024 — McMap. All rights reserved.