Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Grails spring security, servlet filters, and respond

I need to extend spring security to hash the http response content and place the result inside of a header. My approach is to create a servlet filter that reads the response and places the appropriate header. The filter is registered with spring security via a separate plugin. The implementation is largely taken from here.

The entire setup works perfectly when the final application uses "render" in the controller to output JSON to the client. However, if the same data is formatted via "respond" a 404 is returned to the client. I am at a loss to explain the difference.

For reference everything is grails version 2.3.11 and spring security core version 2.0-RC4

Register the filter via my plugin's doWithSpring

responseHasher(ResponseHasher)

SpringSecurityUtils.registerFilter(
                'responseHasher', SecurityFilterPosition.LAST.order - 1)

My filter implementation

public class ResponseHasher implements Filter{

    @Override
    public void init(FilterConfig config) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response, FilterChain chain)
            throws IOException, ServletException {


        HttpServletResponseCopier wrapper = new HttpServletResponseCopier((HttpServletResponse)response);

        chain.doFilter(request, wrapper);
        wrapper.flushBuffer();

        /*
        Take the response, hash it, and set it in a header.  for brevity sake just prove we can read it for now
        and set a static header
        */
        byte[] copy = wrapper.getCopy();
        System.out.println(new String(copy, response.getCharacterEncoding()));
        wrapper.setHeader("foo","bar");

    }

    @Override
    public void destroy() {

    }

}

The HttpServletResponseCopier implementation. The only change from the source is to override all 3 method signatures of write instead of just the one.

class HttpServletResponseCopier extends HttpServletResponseWrapper{

    private ServletOutputStream outputStream;
    private PrintWriter writer;
    private ServletOutputStreamCopier copier;

    public HttpServletResponseCopier(HttpServletResponse response) throws IOException {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (writer != null) {
            throw new IllegalStateException("getWriter() has already been called on this response.");
        }

        if (outputStream == null) {
            outputStream = getResponse().getOutputStream();
            copier = new ServletOutputStreamCopier(outputStream);
        }

        return copier;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (outputStream != null) {
            throw new IllegalStateException("getOutputStream() has already been called on this response.");
        }

        if (writer == null) {
            copier = new ServletOutputStreamCopier(getResponse().getOutputStream());
            writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
        }

        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (writer != null) {
            writer.flush();
        } else if (outputStream != null) {
            copier.flush();
        }
    }

    public byte[] getCopy() {
        if (copier != null) {
            return copier.getCopy();
        } else {
            return new byte[0];
        }
    }

    private class ServletOutputStreamCopier extends ServletOutputStream {

        private OutputStream outputStream;
        private ByteArrayOutputStream copy;

        public ServletOutputStreamCopier(OutputStream outputStream) {
            this.outputStream = outputStream;
            this.copy = new ByteArrayOutputStream(1024);
        }

        @Override
        public void write(int b) throws IOException {
            outputStream.write(b);
            copy.write(b);
        }

        @Override
        public void write(byte[] b,int off, int len) throws IOException {
            outputStream.write(b,off,len);
            copy.write(b,off,len);
        }

        @Override
        public void write(byte[] b) throws IOException {
            outputStream.write(b);
            copy.write(b);
        }

        public byte[] getCopy() {
            return copy.toByteArray();
        }

    }
}

And finally my controller method in the actual application

@Secured()
    def myAction() {
        def thing = Thing.get(1)  //thing can be any domain object really.  in this case we created thing 1 in bootstap

         //throws a 404
         respond(thing)
         /*
         works as expected, output is both rendered 
         and sent to system out, header "foo" is in response
         /*
         //render thing as JSON
}

Any insight would be appreciated as I do not understand why render would work and respond would not. Additionally, I'm open to other approaches to solving this need if what I am attempting just will not work in grails. Thanks in advance.

like image 625
Lance Staples Avatar asked Aug 18 '15 18:08

Lance Staples


People also ask

Do I need to know every security filter in Spring Security?

OOTB Spring security provides several securities filters. It is typically not necessary to know every filter but keep in mind that they work in certain order or sequence. Let’s look at some important filters.

How many dependencies does the Grails Spring-Security-Core plugin have?

Grails spring-security-core plugin Owner:grails| 4.0.3| Oct 16, 2020 | Package| Issues| Source| Documentation| License:Apache-2.0 241 dependencies { compile 'org.grails.plugins:spring-security-core:4.0.3' }

What is the default configuration for GSP in Grails?

GSP features the ability to automatically HTML encode GSP expressions, and as of Grails 2.3 this is the default configuration. The default configuration (found in application.yml) for a newly created Grails application can be seen below: GSP features several codecs that it uses when writing the page to the response.

What is the default authentication mechanism for Grails?

Grails has no default mechanism for authentication as it is possible to implement authentication in many different ways. It is however, easy to implement a simple authentication mechanism using interceptors.


1 Answers

I put all the project in grails, and got a similar problem. I had to make some changes.

For register, I used SpringSecurityUtils.clientRegisterFilter method, as I was doing from a Bootstrap.groovy application. Also, I declared the filter in resources.groovy

It worked with render, but 404 with respond. So changed the respond to:

respond user, [formats:['json']]

And it worked after I removed your filter. I got 404 whenever I put your filter and it was trying to find action.gsp.

I made a change in ServletOutputStreamCopier, implementing the delegates for close and flush methods, and it worked fine for render and respond:

class HttpServletResponseCopier extends HttpServletResponseWrapper {

    private ServletOutputStream outputStream;
    private PrintWriter writer;
    private ServletOutputStreamCopier copier;

    public HttpServletResponseCopier(HttpServletResponse response)
            throws IOException {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (writer != null) {
            throw new IllegalStateException(
                    "getWriter() has already been called on this response.");
        }

        if (outputStream == null) {
            outputStream = getResponse().getOutputStream();
            copier = new ServletOutputStreamCopier(outputStream);
        }

        return copier;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (outputStream != null) {
            throw new IllegalStateException(
                    "getOutputStream() has already been called on this response.");
        }

        if (writer == null) {
            copier = new ServletOutputStreamCopier(getResponse()
                    .getOutputStream());
            writer = new PrintWriter(new OutputStreamWriter(copier,
                    getResponse().getCharacterEncoding()), true);
        }

        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (writer != null) {
            writer.flush();
        } else if (outputStream != null) {
            copier.flush();
        }
    }

    public byte[] getCopy() {
        if (copier != null) {
            return copier.getCopy();
        } else {
            return new byte[0];
        }
    }

    private class ServletOutputStreamCopier extends ServletOutputStream {

        private OutputStream outputStream;
        private ByteArrayOutputStream copy;

        public ServletOutputStreamCopier(OutputStream outputStream) {
            this.outputStream = outputStream;
            this.copy = new ByteArrayOutputStream(1024);
        }

        @Override
        public void write(int b) throws IOException {
            outputStream.write(b);
            copy.write(b);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            outputStream.write(b, off, len);
            copy.write(b, off, len);
        }

        @Override
        public void write(byte[] b) throws IOException {
            outputStream.write(b);
            copy.write(b);
        }

        @Override
        public void flush() throws IOException {
            outputStream.flush();
            copy.flush();
        }

        @Override
        public void close() throws IOException {
            outputStream.close();
            copy.close();
        }

        public byte[] getCopy() {
            return copy.toByteArray();
        }

    }
}

I didn't go through respond implementation details, but I think it get some confusion as it don't have a way to flush or close, and has a fallback to call the view instead of render json.

I know it is a little bit late, but now it is working.

resources.groovy

beans = {

    responseHasher(ResponseHasher){

    }

}

Boostrap.groovy

def init = { servletContext ->

    .

    SpringSecurityUtils.clientRegisterFilter('responseHasher', SecurityFilterPosition.LAST.order - 1)

}

Best, Eder

like image 50
Eder Luis Jorge Avatar answered Nov 16 '22 03:11

Eder Luis Jorge