FetchMetadataInterceptorTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.struts2.interceptor;

import org.apache.struts2.ActionContext;
import org.apache.struts2.XWorkTestCase;
import org.apache.struts2.config.RuntimeConfiguration;
import org.apache.struts2.config.entities.ActionConfig;
import org.apache.struts2.config.entities.InterceptorMapping;
import org.apache.struts2.config.entities.InterceptorStackConfig;
import org.apache.struts2.config.entities.PackageConfig;
import org.apache.struts2.config.providers.XmlConfigurationProvider;
import org.apache.struts2.mock.MockActionInvocation;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;

import static org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_EMBED;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_OBJECT;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_SCRIPT;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.MODE_NAVIGATE;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_DEST_HEADER;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_MODE_HEADER;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_SITE_HEADER;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_USER_HEADER;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SITE_NONE;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SITE_SAME_ORIGIN;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SITE_SAME_SITE;
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.VARY_HEADER;
import static org.junit.Assert.assertNotEquals;

public class FetchMetadataInterceptorTest extends XWorkTestCase {

    private final FetchMetadataInterceptor interceptor = new FetchMetadataInterceptor();
    private final MockActionInvocation mai = new MockActionInvocation();
    private final MockHttpServletRequest request = new MockHttpServletRequest();
    private final MockHttpServletResponse response = new MockHttpServletResponse();
    private static final String ACCEPT_ENCODING_VALUE = "Accept-Encoding";
    private static final String VARY_HEADER_VALUE = String.format("%s,%s,%s,%s", SEC_FETCH_DEST_HEADER, SEC_FETCH_MODE_HEADER, SEC_FETCH_SITE_HEADER, SEC_FETCH_USER_HEADER);
    private static final String SC_FORBIDDEN = String.valueOf(HttpServletResponse.SC_FORBIDDEN);

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        container.inject(interceptor);
        interceptor.setExemptedPaths("/foo,/bar");
        ServletActionContext.setRequest(request);
        ServletActionContext.setResponse(response);
        ActionContext context = ServletActionContext.getActionContext();
        mai.setInvocationContext(context);
    }

    public void testNoSite() throws Exception {
        request.removeHeader(SEC_FETCH_SITE_HEADER);

        assertNotEquals("Expected interceptor to accept this request", SC_FORBIDDEN, interceptor.intercept(mai));
    }

    public void testValidSite() throws Exception {
        for (String header : Arrays.asList(SITE_SAME_ORIGIN, SITE_SAME_SITE, SITE_NONE)) {
            request.addHeader(SEC_FETCH_SITE_HEADER, header);

            assertNotEquals("Expected interceptor to accept this request", SC_FORBIDDEN, interceptor.intercept(mai));
        }

    }

    public void testValidTopLevelNavigation() throws Exception {
        request.addHeader(SEC_FETCH_MODE_HEADER, MODE_NAVIGATE);
        request.addHeader(SEC_FETCH_DEST_HEADER, DEST_SCRIPT);
        request.setMethod("GET");

        assertNotEquals("Expected interceptor to accept this request", SC_FORBIDDEN, interceptor.intercept(mai));
    }

    public void testInvalidTopLevelNavigation() throws Exception {
        for (String header : Arrays.asList(DEST_OBJECT, DEST_EMBED)) {
            request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
            request.addHeader(SEC_FETCH_MODE_HEADER, MODE_NAVIGATE);
            request.addHeader(SEC_FETCH_DEST_HEADER, header);
            request.setMethod("GET");

            assertEquals("Expected interceptor to NOT accept this request", SC_FORBIDDEN, interceptor.intercept(mai));
        }
    }

    public void testPathInExemptedPaths() throws Exception {
        request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
        request.setContextPath("/foo");

        assertNotEquals("Expected interceptor to accept this request", SC_FORBIDDEN, interceptor.intercept(mai));
    }

    public void testPathNotInExemptedPaths() throws Exception {
        request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
        request.setContextPath("/foobar");

        assertEquals("Expected interceptor to NOT accept this request", SC_FORBIDDEN, interceptor.intercept(mai));
    }

    public void testVaryHeaderAcceptedReq() throws Exception {
        request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
        request.setContextPath("/foo");

        interceptor.intercept(mai);

        assertTrue("Expected vary header to be included", response.containsHeader(VARY_HEADER));
        assertEquals("Expected different vary header value", response.getHeader(VARY_HEADER), VARY_HEADER_VALUE);
    }

    public void testVaryHeaderRejectedReq() throws Exception {
        request.addHeader(SEC_FETCH_SITE_HEADER, "foo");

        interceptor.intercept(mai);

        assertTrue("Expected vary header to be included", response.containsHeader(VARY_HEADER));
        assertEquals("Expected different vary header value", response.getHeader(VARY_HEADER), VARY_HEADER_VALUE);
    }

    public void testVaryHeaderReplaced() throws Exception {
        request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
        response.addHeader(VARY_HEADER, ACCEPT_ENCODING_VALUE);  // Simulate Vary header present due to processing before this interceptor.
        assertEquals("Initial vary response header addition failed ?", response.getHeader(VARY_HEADER), ACCEPT_ENCODING_VALUE);

        interceptor.intercept(mai);

        assertTrue("Expected vary header to be included", response.containsHeader(VARY_HEADER));
        assertFalse("Expected original vary header content to be replaced", Objects.requireNonNull(response.getHeader(VARY_HEADER)).contains(ACCEPT_ENCODING_VALUE));
        assertTrue("Expected added vary header content to be present", Objects.requireNonNull(response.getHeader(VARY_HEADER)).contains(VARY_HEADER_VALUE));
    }

    public void testSetExemptedPathsInjectionIndirectly() throws Exception {
        // Perform a multistep test to confirm (indirectly) that the method parameter injection of setExemptedPaths() for
        // the FetchMetadataInterceptor is functioning as expected, when configured appropriately.
        // Ensure we're using the specific test configuration, not the default simple configuration.
        XmlConfigurationProvider configurationProvider = new StrutsXmlConfigurationProvider("struts-testing.xml");
        container.inject(configurationProvider);
        loadConfigurationProviders(configurationProvider);

        // The test configuration in "struts-testing.xml" should define a "default" package.  That "default" package should contain a "defaultInterceptorStack" containing
        // a "fetchMetadata" interceptor parameter "fetchMetadata.setExemptedPaths".  If the parameter method injection is working correctly for the FetchMetadataInterceptor,
        // the exempted paths should be set appropriately for the interceptor instances, once the configuration is loaded into the container.
        final PackageConfig defaultPackageConfig = configuration.getPackageConfig("default");
        final InterceptorStackConfig defaultInterceptorStackConfig = (InterceptorStackConfig) defaultPackageConfig.getInterceptorConfig("defaultInterceptorStack");
        final Collection<InterceptorMapping> defaultInterceptorStackInterceptors = defaultInterceptorStackConfig.getInterceptors();
        assertFalse("'defaultInterceptorStack' interceptors in struts-testing.xml is empty ?", defaultInterceptorStackInterceptors.isEmpty());
        InterceptorMapping configuredFetchMetadataInterceptorMapping = null;
        Iterator<InterceptorMapping> interceptorIterator = defaultInterceptorStackInterceptors.iterator();
        while (interceptorIterator.hasNext()) {
            InterceptorMapping currentMapping = interceptorIterator.next();
            if (currentMapping != null && "fetchMetadata".equals(currentMapping.getName())) {
                configuredFetchMetadataInterceptorMapping = currentMapping;
                break;
            }
        }
        assertNotNull("'fetchMetadata' interceptor mapping not present after loading 'struts-testing.xml' ?", configuredFetchMetadataInterceptorMapping);
        assertTrue("'fetchMetadata' interceptor mapping loaded from 'struts-testing.xml' produced a non-FetchMetadataInterceptor type ?", configuredFetchMetadataInterceptorMapping.getInterceptor() instanceof FetchMetadataInterceptor);
        FetchMetadataInterceptor configuredFetchMetadataInterceptor = (FetchMetadataInterceptor) configuredFetchMetadataInterceptorMapping.getInterceptor();
        request.removeHeader(SEC_FETCH_SITE_HEADER);
        request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
        request.setContextPath("/foo");
        assertEquals("Expected interceptor to NOT accept this request [/foo]", SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));
        request.setContextPath("/fetchMetadataExemptedGlobal");
        assertNotEquals("Expected interceptor to accept this request [/fetchMetadataExemptedGlobal]", SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));
        request.setContextPath("/someOtherPath");
        assertNotEquals("Expected interceptor to accept this request [/someOtherPath]", SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));

        // The test configuration in "struts-testing.xml" should also contain three actions configured differently for the "fetchMetadata" interceptor.
        // "fetchMetadataExempted" has an override exemption matching its action name, "fetchMetadataNotExempted" has an override exemption NOT matching its action name,
        // and "fetchMetadataExemptedGlobal" has an action name matching an exemption defined in "defaultInterceptorStack".
        final RuntimeConfiguration runtimeConfiguration = configuration.getRuntimeConfiguration();
        final ActionConfig fetchMetadataExemptedActionConfig = runtimeConfiguration.getActionConfig("/", "fetchMetadataExempted");
        final ActionConfig fetchMetadataNotExemptedActionConfig = runtimeConfiguration.getActionConfig("/", "fetchMetadataNotExempted");
        final ActionConfig fetchMetadataExemptedGlobalActionConfig = runtimeConfiguration.getActionConfig("/", "fetchMetadataExemptedGlobal");
        assertNotNull("'fetchMetadataExempted' action config not present in 'struts-testing.xml' ?", fetchMetadataExemptedActionConfig);
        assertNotNull("'fetchMetadataNotExempted' action config not present in 'struts-testing.xml' ?", fetchMetadataExemptedActionConfig);
        assertNotNull("'fetchMetadataExemptedGlobal' action config not present in 'struts-testing.xml' ?", fetchMetadataExemptedActionConfig);

        // Test fetchMetadata interceptor for the "fetchMetadataExempted" action.
        Collection<InterceptorMapping> currentActionInterceptors = fetchMetadataExemptedActionConfig.getInterceptors();
        assertFalse("'fetchMetadataExempted' interceptors in struts-testing.xml is empty ?", currentActionInterceptors.isEmpty());
        configuredFetchMetadataInterceptorMapping = null;
        interceptorIterator = currentActionInterceptors.iterator();
        while (interceptorIterator.hasNext()) {
            InterceptorMapping currentMapping = interceptorIterator.next();
            if (currentMapping != null && "fetchMetadata".equals(currentMapping.getName())) {
                configuredFetchMetadataInterceptorMapping = currentMapping;
                break;
            }
        }
        assertNotNull("'fetchMetadata' interceptor mapping for action 'fetchMetadataExempted' not present in 'struts-testing.xml' ?", configuredFetchMetadataInterceptorMapping);
        assertTrue("'fetchMetadata' interceptor mapping for action 'fetchMetadataExempted' in 'struts-testing.xml' produced a non-FetchMetadataInterceptor type ?", configuredFetchMetadataInterceptorMapping.getInterceptor() instanceof FetchMetadataInterceptor);
        configuredFetchMetadataInterceptor = (FetchMetadataInterceptor) configuredFetchMetadataInterceptorMapping.getInterceptor();
        request.removeHeader(SEC_FETCH_SITE_HEADER);
        request.addHeader(SEC_FETCH_SITE_HEADER, fetchMetadataExemptedActionConfig.getName());
        request.setContextPath("/" + fetchMetadataExemptedActionConfig.getName());
        assertNotEquals("Expected interceptor to accept this request [" + "/" + fetchMetadataExemptedActionConfig.getName() + "]", SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));

        // Test fetchMetadata interceptor for the "fetchMetadataNotExempted" action.
        currentActionInterceptors = fetchMetadataNotExemptedActionConfig.getInterceptors();
        assertFalse("'fetchMetadataNotExempted' interceptors in struts-testing.xml is empty ?", currentActionInterceptors.isEmpty());
        configuredFetchMetadataInterceptorMapping = null;
        interceptorIterator = currentActionInterceptors.iterator();
        while (interceptorIterator.hasNext()) {
            InterceptorMapping currentMapping = interceptorIterator.next();
            if (currentMapping != null && "fetchMetadata".equals(currentMapping.getName())) {
                configuredFetchMetadataInterceptorMapping = currentMapping;
                break;
            }
        }
        assertNotNull("'fetchMetadata' interceptor mapping for action 'fetchMetadataNotExempted' not present in 'struts-testing.xml' ?", configuredFetchMetadataInterceptorMapping);
        assertTrue("'fetchMetadata' interceptor mapping 'fetchMetadataExempted' in 'struts-testing.xml' produced a non-FetchMetadataInterceptor type ?", configuredFetchMetadataInterceptorMapping.getInterceptor() instanceof FetchMetadataInterceptor);
        configuredFetchMetadataInterceptor = (FetchMetadataInterceptor) configuredFetchMetadataInterceptorMapping.getInterceptor();
        request.removeHeader(SEC_FETCH_SITE_HEADER);
        request.addHeader(SEC_FETCH_SITE_HEADER, fetchMetadataNotExemptedActionConfig.getName());
        request.setContextPath("/" + fetchMetadataNotExemptedActionConfig.getName());
        assertEquals("Expected interceptor to NOT accept this request [" + "/" + fetchMetadataNotExemptedActionConfig.getName() + "]", SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));

        // Test fetchMetadata interceptor for the "fetchMetadataExemptedGlobal" action.
        currentActionInterceptors = fetchMetadataExemptedGlobalActionConfig.getInterceptors();
        assertFalse("'fetchMetadataExemptedGlobal' interceptors in struts-testing.xml is empty ?", currentActionInterceptors.isEmpty());
        configuredFetchMetadataInterceptorMapping = null;
        interceptorIterator = currentActionInterceptors.iterator();
        while (interceptorIterator.hasNext()) {
            InterceptorMapping currentMapping = interceptorIterator.next();
            if (currentMapping != null && "fetchMetadata".equals(currentMapping.getName())) {
                configuredFetchMetadataInterceptorMapping = currentMapping;
                break;
            }
        }
        assertNotNull("'fetchMetadata' interceptor mapping for action 'fetchMetadataExemptedGlobal' not present in 'struts-testing.xml' ?", configuredFetchMetadataInterceptorMapping);
        assertTrue("'fetchMetadata' interceptor mapping 'fetchMetadataExemptedGlobal' in 'struts-testing.xml' produced a non-FetchMetadataInterceptor type ?", configuredFetchMetadataInterceptorMapping.getInterceptor() instanceof FetchMetadataInterceptor);
        configuredFetchMetadataInterceptor = (FetchMetadataInterceptor) configuredFetchMetadataInterceptorMapping.getInterceptor();
        request.removeHeader(SEC_FETCH_SITE_HEADER);
        request.addHeader(SEC_FETCH_SITE_HEADER, fetchMetadataExemptedGlobalActionConfig.getName());
        request.setContextPath("/" + fetchMetadataExemptedGlobalActionConfig.getName());
        assertNotEquals("Expected interceptor to accept this request [" + "/" + fetchMetadataExemptedGlobalActionConfig.getName() + "]", SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));
    }

}