diff --git a/compose/assets/etc/storm/webdav/config/application-policies.yml b/compose/assets/etc/storm/webdav/config/application-policies.yml index 27f9bf15..768b36ef 100644 --- a/compose/assets/etc/storm/webdav/config/application-policies.yml +++ b/compose/assets/etc/storm/webdav/config/application-policies.yml @@ -5,6 +5,13 @@ storm: authz: policies: + - sa: noauth + actions: + - all + effect: permit + description: Grant read/write access to all users + principals: + - type: anyone - sa: fga actions: - all diff --git a/compose/assets/etc/storm/webdav/sa.d/noauth.properties b/compose/assets/etc/storm/webdav/sa.d/noauth.properties index c705f0df..1b397961 100644 --- a/compose/assets/etc/storm/webdav/sa.d/noauth.properties +++ b/compose/assets/etc/storm/webdav/sa.d/noauth.properties @@ -9,3 +9,4 @@ accessPoints=/noauth authenticatedReadEnabled=true anonymousReadEnabled=true voMapGrantsWritePermission=false +fineGrainedAuthzEnabled=true diff --git a/robot/common/setup_and_teardown.robot b/robot/common/setup_and_teardown.robot index ccd54c96..87f848ff 100644 --- a/robot/common/setup_and_teardown.robot +++ b/robot/common/setup_and_teardown.robot @@ -10,9 +10,9 @@ Default Setup Default Teardown Unset VOMS credential -Setup file [Arguments] ${file_name} ${content}=Hello World! +Setup file [Arguments] ${file_name} ${content}=Hello World! ${sa}=${sa.default} Default Setup - Create Test File ${file_name} ${content} + Create Test File ${file_name} ${content} ${sa} Setup directory [Arguments] ${dir_name} Default Setup @@ -23,12 +23,12 @@ Teardown file [Arguments] ${file_name} Remove Test File ${file_name} Remove Test File ${file_name}.dest -Teardown file cross sa [Arguments] ${file_name} +Teardown file cross sa [Arguments] ${file_name} ${sa_source}=${sa.default} ${sa_dest}=${sa.oauth} Default Teardown - Remove Test File ${file_name} - Remove Test File ${file_name}.dest sa=${sa.oauth} + Remove Test File ${file_name} sa=${sa_source} + Remove Test File ${file_name}.dest sa=${sa_dest} Teardown directory [Arguments] ${dir_name} Default Teardown Remove Test Directory ${dir_name} - Remove Test Directory ${dir_name}.dest \ No newline at end of file + Remove Test Directory ${dir_name}.dest diff --git a/robot/test/copy.robot b/robot/test/copy.robot index 055535c5..de6710f6 100644 --- a/robot/test/copy.robot +++ b/robot/test/copy.robot @@ -86,12 +86,21 @@ Copy with destination equal to source Should Contain ${out} 403 [Teardown] Teardown file copy_works -Copy across storage areas fails +Copy across storage areas with Source header works + [Tags] voms + [Setup] Setup file copy_x_sa_works sa=${sa.noauth} + ${dest} DAVS URL copy_x_sa_works.dest + ${source} DAVS URL copy_x_sa_works sa=${sa.noauth} + ${rc} ${out} Curl Voms Pull COPY Success ${dest} ${source} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown file cross sa copy_x_sa_works sa_source=${sa.noauth} + +Copy across storage areas with Destination header fails [Tags] voms [Setup] Setup file copy_x_sa_works ${dest} DAVS URL copy_x_sa_works.dest sa=${sa.oauth} ${source} DAVS URL copy_x_sa_works ${rc} ${out} Curl Voms Push COPY ${dest} ${source} Should Contain ${out} 400 - Should Contain ${out} Local copy across storage areas is not supported - [Teardown] Teardown file cross sa copy_x_sa_works \ No newline at end of file + Should Contain ${out} Local copy across storage areas with Destination header is not supported + [Teardown] Teardown file cross sa copy_x_sa_works diff --git a/src/main/java/org/italiangrid/storm/webdav/error/Forbidden.java b/src/main/java/org/italiangrid/storm/webdav/error/Forbidden.java new file mode 100644 index 00000000..283e83d9 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/error/Forbidden.java @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2014 Istituto Nazionale di Fisica Nucleare +// +// SPDX-License-Identifier: Apache-2.0 + +package org.italiangrid.storm.webdav.error; + +public class Forbidden extends StoRMWebDAVError { + + /** */ + private static final long serialVersionUID = 1L; + + public Forbidden(String message) { + super(message); + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/fs/DefaultFSStrategy.java b/src/main/java/org/italiangrid/storm/webdav/fs/DefaultFSStrategy.java index b4098be2..a6a4c27e 100644 --- a/src/main/java/org/italiangrid/storm/webdav/fs/DefaultFSStrategy.java +++ b/src/main/java/org/italiangrid/storm/webdav/fs/DefaultFSStrategy.java @@ -15,6 +15,7 @@ import org.apache.commons.io.IOUtils; import org.italiangrid.storm.webdav.checksum.Adler32ChecksumInputStream; import org.italiangrid.storm.webdav.error.SameFileError; +import org.italiangrid.storm.webdav.error.StoRMWebDAVError; import org.italiangrid.storm.webdav.fs.attrs.ExtendedAttributesHelper; import org.italiangrid.storm.webdav.utils.IOExceptionHelper; import org.slf4j.Logger; @@ -97,12 +98,18 @@ public void cp(File source, File dest) { FileUtils.copyDirectory(source, dest); } else { - - Files.copy(source.toPath(), dest.toPath()); + Process process = + Runtime.getRuntime().exec(new String[] {"cp", "-a", source.getPath(), dest.getPath()}); + int returnCode = process.waitFor(); + if (returnCode != 0) { + throw new IOException("cp -a error"); + } } } catch (IOException e) { throw IOExceptionHelper.getStoRMWebDAVError(e); + } catch (InterruptedException e) { + throw new StoRMWebDAVError(e.getMessage(), e); } } diff --git a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonBehaviour.java b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonBehaviour.java index 4171f8d1..19f9a340 100644 --- a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonBehaviour.java +++ b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonBehaviour.java @@ -16,10 +16,12 @@ import io.milton.http.exceptions.NotAuthorizedException; import io.milton.http.http11.Http11ResponseHandler; import java.io.IOException; +import java.time.Clock; import org.italiangrid.storm.webdav.error.DirectoryNotEmpty; import org.italiangrid.storm.webdav.error.DiskQuotaExceeded; import org.italiangrid.storm.webdav.error.ResourceNotFound; import org.italiangrid.storm.webdav.error.SameFileError; +import org.italiangrid.storm.webdav.tpc.transfer.TransferStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.server.MethodNotAllowedException; @@ -49,6 +51,17 @@ public void process(FilterChain chain, Request request, Response response) { if (response.getEntity() != null) { manager.sendResponseEntity(response); } + // If it was a TPC send a success PerfMarker, so davix knows that the COPY was successful + if (request instanceof StoRMMiltonRequest stoRMMiltonRequest + && stoRMMiltonRequest.sendSuccessPerfMarker()) { + TransferStatus.Builder statusBuilder = TransferStatus.builder(Clock.systemDefaultZone()); + try { + response.getOutputStream().write(statusBuilder.done(0).asPerfMarker().getBytes()); + response.getOutputStream().close(); + } catch (IOException e) { + LOG.error("Error sending success PerfMarker: {}", e.getMessage(), e); + } + } } catch (DiskQuotaExceeded e) { // responseHandler does not support sending insufficient storage response.sendError(Status.SC_INSUFFICIENT_STORAGE, e.getMessage()); diff --git a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonRequest.java b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonRequest.java index f86ff1d2..4f72b0b6 100644 --- a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonRequest.java +++ b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonRequest.java @@ -11,15 +11,20 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jetty.util.URIUtil; +import org.italiangrid.storm.webdav.tpc.SwappedServletRequest; public class StoRMMiltonRequest extends ServletRequest { private static final String REGEX = "(http.*:\\d*)/webdav/(.*)$"; private static final Pattern PATTERN = Pattern.compile(REGEX); + private boolean sendSuccessPerfMarker = false; public StoRMMiltonRequest(HttpServletRequest r, ServletContext servletContext) { super(r, servletContext); + if (r instanceof SwappedServletRequest) { + this.sendSuccessPerfMarker = true; + } } @Override @@ -47,4 +52,8 @@ public Auth getAuthorization() { // Always return null as milton is confused by the OAuth2 Bearer scheme return null; } + + public boolean sendSuccessPerfMarker() { + return sendSuccessPerfMarker; + } } diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/SwappedServletRequest.java b/src/main/java/org/italiangrid/storm/webdav/tpc/SwappedServletRequest.java new file mode 100644 index 00000000..92b1ef48 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/SwappedServletRequest.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2014 Istituto Nazionale di Fisica Nucleare +// +// SPDX-License-Identifier: Apache-2.0 + +package org.italiangrid.storm.webdav.tpc; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import java.net.URI; +import java.net.URISyntaxException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; + +// Wraps a request so that the Source header becomes the request URL and the request URL becomes the +// Destination header. This is necessary because the WebDAV protocol defines COPY only with the +// Destination header. +public class SwappedServletRequest extends HttpServletRequestWrapper { + public static final Logger LOG = LoggerFactory.getLogger(SwappedServletRequest.class); + + final HttpServletRequest req; + + public SwappedServletRequest(HttpServletRequest req) { + super(req); + this.req = req; + } + + // The getHeader method is used by milton to obtain information about the COPY request + // https://github.com/miltonio/milton2/blob/master/milton-server-ce/src/main/java/io/milton/servlet/ServletRequest.java#L122 + @Override + public String getHeader(String name) { + if (TransferConstants.DESTINATION_HEADER.equals(name)) { + // The Destination header is the requested URL of the original request + return req.getRequestURL().toString(); + } else if (HttpHeaders.HOST.equals(name)) { + // The Host header is the Host specified in the Source header of the + // original request if it is present + try { + URI sourceUri = new URI(req.getHeader(TransferConstants.SOURCE_HEADER)); + String sourceHost = sourceUri.getHost(); + if (sourceHost != null) { + return sourceHost + ':' + sourceUri.getPort(); + } + } catch (URISyntaxException e) { + LOG.warn("Error parsing Source header: {}", e.getMessage(), e); + } + } + return req.getHeader(name); + } + + // The getRequestURL method is used by milton to obtain information about the COPY request + // https://github.com/miltonio/milton2/blob/master/milton-server-ce/src/main/java/io/milton/http/UrlAdapterImpl.java#L32 + @Override + public StringBuffer getRequestURL() { + String source = req.getHeader(TransferConstants.SOURCE_HEADER); + try { + // If the Source header of the original request includes the host, just + // use it as the request URL + String sourceHost = new URI(source).getHost(); + if (sourceHost != null) { + return new StringBuffer(source); + } + } catch (URISyntaxException e) { + LOG.warn("Error parsing Source header: {}", e.getMessage(), e); + } + // Otherwise get the scheme, host and port from the original request and use the Source header + // as the path + return new StringBuffer( + req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + source); + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/TpcUtils.java b/src/main/java/org/italiangrid/storm/webdav/tpc/TpcUtils.java index b1d56cd0..9093ae13 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/TpcUtils.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/TpcUtils.java @@ -40,6 +40,22 @@ default boolean requestHasSourceHeader(HttpServletRequest request) { return Optional.ofNullable(request.getHeader(TransferConstants.SOURCE_HEADER)).isPresent(); } + default boolean requestHasLocalSourceHeader( + HttpServletRequest request, LocalURLService localURLService) { + Optional source = + Optional.ofNullable(request.getHeader(TransferConstants.SOURCE_HEADER)); + + return source.isPresent() && localURLService.isLocalURL(source.get()); + } + + default boolean requestHasRemoteSourceHeader( + HttpServletRequest request, LocalURLService localURLService) { + Optional source = + Optional.ofNullable(request.getHeader(TransferConstants.SOURCE_HEADER)); + + return source.isPresent() && !localURLService.isLocalURL(source.get()); + } + default boolean requestHasDestinationHeader(HttpServletRequest request) { return Optional.ofNullable(request.getHeader(TransferConstants.DESTINATION_HEADER)).isPresent(); } @@ -111,11 +127,27 @@ default boolean requestHasTranferHeader(HttpServletRequest request) { return false; } + default boolean requestHasTranferHeaderOtherThanAuthorization(HttpServletRequest request) { + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + if (headerName.toLowerCase().startsWith(TransferConstants.TRANSFER_HEADER_LC) + && !headerName + .toLowerCase() + .equals( + TransferConstants.TRANSFER_HEADER_LC + + TransferConstants.AUTHORIZATION_HEADER.toLowerCase())) { + return true; + } + } + return false; + } + default boolean isTpc(HttpServletRequest request, LocalURLService localUrlService) { return "COPY".equals(request.getMethod()) - && (requestHasSourceHeader(request) + && (requestHasRemoteSourceHeader(request, localUrlService) || requestHasRemoteDestinationHeader(request, localUrlService) - || requestHasTranferHeader(request)); + || requestHasTranferHeaderOtherThanAuthorization(request)); } default boolean isCopyOrMoveRequest(HttpServletRequest request) { diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilter.java b/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilter.java index 0612025e..6b68d6a2 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilter.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilter.java @@ -20,6 +20,7 @@ import org.apache.hc.client5.http.ClientProtocolException; import org.apache.hc.client5.http.HttpResponseException; import org.italiangrid.storm.webdav.error.BadRequest; +import org.italiangrid.storm.webdav.error.Forbidden; import org.italiangrid.storm.webdav.error.ResourceNotFound; import org.italiangrid.storm.webdav.scitag.SciTag; import org.italiangrid.storm.webdav.server.PathResolver; @@ -37,6 +38,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; +import org.springframework.http.HttpHeaders; public class TransferFilter extends TransferFilterSupport implements Filter { @@ -60,10 +62,32 @@ public TransferFilter( private void localCopySanityChecks(HttpServletRequest req) throws URISyntaxException { if (!requestPathAndDestinationHeaderAreInSameStorageArea(req, resolver)) { - throw new BadRequest("Local copy across storage areas is not supported"); + throw new BadRequest( + "Local copy across storage areas with Destination header is not supported"); } } + private void checkAccessPermission(SwappedServletRequest wrappedRequest) + throws ClientProtocolException { + URI uri = URI.create(wrappedRequest.getRequestURL().toString()); + String path = getScopedPathInfo(wrappedRequest); + GetTransferRequest xferRequest = + GetTransferRequestBuilder.create() + .uuid(RequestIdHolder.getRequestId()) + .uri(uri) + .path(path) + .headers(getTransferHeaders(wrappedRequest)) + .addHeader("Range", "bytes=0-0") + .build(); + client.handleCheckAccessPermission( + xferRequest, + (r, s) -> { + if (s.getStatus() == TransferStatus.Status.ERROR) { + throw new Forbidden(s.asPerfMarker()); + } + }); + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -73,13 +97,32 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha if (isTpc(req, localURLService)) { handleTpc(req, res); - } else if (isCopy(req) && requestHasLocalDestinationHeader(req, localURLService)) { + } else if (isCopy(req)) { try { - localCopySanityChecks(req); + if (requestHasLocalDestinationHeader(req, localURLService)) { + localCopySanityChecks(req); + } else if (requestHasLocalSourceHeader(req, localURLService)) { + SwappedServletRequest wrappedRequest = new SwappedServletRequest(req); + if (LOG.isInfoEnabled()) { + LOG.info( + "Wrapped the COPY request to swap Source/Destination, URL: {} -> {}, Host: {} -> {}, Source header {} -> Destination header {}", + req.getRequestURL(), + wrappedRequest.getRequestURL(), + req.getHeader(HttpHeaders.HOST), + wrappedRequest.getHeader(HttpHeaders.HOST), + req.getHeader(TransferConstants.SOURCE_HEADER), + wrappedRequest.getHeader(TransferConstants.DESTINATION_HEADER)); + } + res.setStatus(HttpServletResponse.SC_ACCEPTED); + checkAccessPermission(wrappedRequest); + request = wrappedRequest; + } // Let milton handle the local copy chain.doFilter(request, response); - } catch (URISyntaxException | BadRequest | ResourceNotFound e) { - res.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } catch (URISyntaxException | BadRequest | Forbidden | ResourceNotFound e) { + if (!(e instanceof Forbidden)) { + res.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } res.setContentType("text/plain"); res.getWriter().print(e.getMessage()); res.flushBuffer(); diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClient.java b/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClient.java index 95b40bb6..d09cf234 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClient.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClient.java @@ -24,6 +24,7 @@ import org.apache.hc.client5.http.ClientProtocolException; import org.apache.hc.client5.http.HttpResponseException; import org.apache.hc.client5.http.classic.methods.HttpHead; +import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.EndpointDetails; @@ -216,8 +217,7 @@ public void handle(GetTransferRequest request, TransferStatusCallback cb) { String.format( "Error fetching %s: %s", request.remoteURI().toString(), e.getMessage()))); observation.error(e); - - } catch (Throwable e) { + } catch (Exception e) { LOG.error(e.getMessage(), e); // we explicitly always log a generic error reportStatus( cb, @@ -244,6 +244,44 @@ public void handle(GetTransferRequest request, TransferStatusCallback cb) { } } + @Override + public void handleCheckAccessPermission(GetTransferRequest request, TransferStatusCallback cb) { + TransferStatus.Builder statusBuilder = TransferStatus.builder(clock).withIsPushMode(false); + BasicClassicHttpRequest get = prepareRequest(request); + HttpClientContext context = HttpClientContext.create(); + + try { + httpClient.execute(get, context, new BasicHttpClientResponseHandler()); + reportStatus(cb, request, statusBuilder.done(1)); + } catch (HttpResponseException e) { + logException(e); + reportStatus( + cb, + request, + statusBuilder.error( + String.format( + "Error fetching %s: %d %s", + request.remoteURI().toString(), e.getStatusCode(), e.getMessage()))); + } catch (ClientProtocolException e) { + logException(e); + reportStatus( + cb, + request, + statusBuilder.error( + String.format( + "Error fetching %s: %s", request.remoteURI().toString(), e.getMessage()))); + } catch (Exception e) { + LOG.error(e.getMessage(), e); // we explicitly always log a generic error + reportStatus( + cb, + request, + statusBuilder.error( + String.format( + "%s while fetching %s: %s", + e.getClass().getSimpleName(), request.remoteURI().toString(), e.getMessage()))); + } + } + private void checkOverwrite(PutTransferRequest request) throws IOException { if (!request.overwrite()) { HttpHead head = new HttpHead(request.remoteURI()); @@ -322,7 +360,7 @@ public void handle(PutTransferRequest request, TransferStatusCallback cb) { String.format( "Error pushing %s: %s", request.remoteURI().toString(), e.getMessage()))); observation.error(e); - } catch (Throwable e) { + } catch (Exception e) { LOG.error(e.getMessage(), e); // we explicitly always log a generic error reportStatus( cb, diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClientMetricsWrapper.java b/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClientMetricsWrapper.java index dce37797..9400e91d 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClientMetricsWrapper.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClientMetricsWrapper.java @@ -112,6 +112,12 @@ public void handle(GetTransferRequest request, TransferStatusCallback status) } } + @Override + public void handleCheckAccessPermission(GetTransferRequest request, TransferStatusCallback status) + throws ClientProtocolException { + delegate.handleCheckAccessPermission(request, status); + } + @Override public void handle(PutTransferRequest request, TransferStatusCallback status) throws ClientProtocolException { diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/TransferClient.java b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/TransferClient.java index 78cfdcb9..a54b2d12 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/TransferClient.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/TransferClient.java @@ -11,6 +11,9 @@ public interface TransferClient { void handle(GetTransferRequest request, TransferStatusCallback status) throws ClientProtocolException; + void handleCheckAccessPermission(GetTransferRequest request, TransferStatusCallback status) + throws ClientProtocolException; + void handle(PutTransferRequest request, TransferStatusCallback status) throws ClientProtocolException; } diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/AuthorizationIntegrationTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/AuthorizationIntegrationTests.java index 77961caa..f9021cc3 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/AuthorizationIntegrationTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/AuthorizationIntegrationTests.java @@ -349,6 +349,12 @@ void tpcJwtPullCopyRequiresStorageModifyScope() throws Exception { .header("Source", "http://localhost/wlcg/destination") .with(jwt().jwt(token))) .andExpect(status().isAccepted()); + + mvc.perform( + request(COPY_HTTP_METHOD, URI.create("http://localhost/wlcg/source")) + .header("Source", "/wlcg/destination") + .with(jwt().jwt(token))) + .andExpect(status().isAccepted()); } @Test diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/SwappedServletRequestTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/SwappedServletRequestTest.java new file mode 100644 index 00000000..81e1f2dc --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/SwappedServletRequestTest.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2014 Istituto Nazionale di Fisica Nucleare +// +// SPDX-License-Identifier: Apache-2.0 + +package org.italiangrid.storm.webdav.test.tpc; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.lenient; + +import jakarta.servlet.http.HttpServletRequest; +import org.italiangrid.storm.webdav.server.servlet.WebDAVMethod; +import org.italiangrid.storm.webdav.tpc.SwappedServletRequest; +import org.italiangrid.storm.webdav.tpc.TransferConstants; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; + +@ExtendWith(MockitoExtension.class) +class SwappedServletRequestTest { + + @Mock HttpServletRequest request; + + final String scheme = "https"; + final String serverName = "storm.example"; + final int port = 8443; + final String destinationPath = "/sa2/destination"; + final String sourcePath = "/sa1/source"; + + @Test + void localCopyAcrossStorageAreasWithRelativeSourcePath() { + lenient() + .when(request.getRequestURL()) + .thenReturn(new StringBuffer(scheme + "://" + serverName + ":" + port + destinationPath)); + lenient().when(request.getMethod()).thenReturn(WebDAVMethod.COPY.name()); + lenient().when(request.getScheme()).thenReturn(scheme); + lenient().when(request.getServerName()).thenReturn(serverName); + lenient().when(request.getServerPort()).thenReturn(port); + lenient().when(request.getHeader(HttpHeaders.HOST)).thenReturn(serverName + ":" + port); + lenient().when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(sourcePath); + SwappedServletRequest swappedRequest = new SwappedServletRequest(request); + assertThat( + swappedRequest.getRequestURL().toString(), + is(scheme + "://" + serverName + ":" + port + sourcePath)); + assertThat(swappedRequest.getHeader(HttpHeaders.HOST), is(serverName + ":" + port)); + assertThat( + swappedRequest.getHeader(TransferConstants.DESTINATION_HEADER), + is(scheme + "://" + serverName + ":" + port + destinationPath)); + } + + @Test + void localCopyAcrossStorageAreasWithAbsoluteSourcePath() { + lenient() + .when(request.getRequestURL()) + .thenReturn(new StringBuffer(scheme + "://" + serverName + ":" + port + destinationPath)); + lenient().when(request.getMethod()).thenReturn(WebDAVMethod.COPY.name()); + lenient().when(request.getScheme()).thenReturn(scheme); + lenient().when(request.getServerName()).thenReturn(serverName); + lenient().when(request.getServerPort()).thenReturn(port); + lenient().when(request.getHeader(HttpHeaders.HOST)).thenReturn(serverName + ":" + port); + lenient() + .when(request.getHeader(TransferConstants.SOURCE_HEADER)) + .thenReturn(scheme + "://" + serverName + ":" + port + sourcePath); + SwappedServletRequest swappedRequest = new SwappedServletRequest(request); + assertThat( + swappedRequest.getRequestURL().toString(), + is(scheme + "://" + serverName + ":" + port + sourcePath)); + assertThat(swappedRequest.getHeader(HttpHeaders.HOST), is(serverName + ":" + port)); + assertThat( + swappedRequest.getHeader(TransferConstants.DESTINATION_HEADER), + is(scheme + "://" + serverName + ":" + port + destinationPath)); + } + + @Test + void localCopyAcrossStorageAreasWithAbsoluteSourcePathAndDifferentHost() { + String differentServerName = "storm2.example"; + lenient() + .when(request.getRequestURL()) + .thenReturn(new StringBuffer(scheme + "://" + serverName + ":" + port + destinationPath)); + lenient().when(request.getMethod()).thenReturn(WebDAVMethod.COPY.name()); + lenient().when(request.getScheme()).thenReturn(scheme); + lenient().when(request.getServerName()).thenReturn(serverName); + lenient().when(request.getServerPort()).thenReturn(port); + lenient().when(request.getHeader(HttpHeaders.HOST)).thenReturn(serverName + ":" + port); + lenient() + .when(request.getHeader(TransferConstants.SOURCE_HEADER)) + .thenReturn(scheme + "://" + differentServerName + ":" + port + sourcePath); + SwappedServletRequest swappedRequest = new SwappedServletRequest(request); + assertThat( + swappedRequest.getRequestURL().toString(), + is(scheme + "://" + differentServerName + ":" + port + sourcePath)); + assertThat(swappedRequest.getHeader(HttpHeaders.HOST), is(differentServerName + ":" + port)); + assertThat( + swappedRequest.getHeader(TransferConstants.DESTINATION_HEADER), + is(scheme + "://" + serverName + ":" + port + destinationPath)); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterActivationTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterActivationTest.java index 4889c771..ef275aea 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterActivationTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterActivationTest.java @@ -85,7 +85,9 @@ void filterBlocksLocalCopyAcrossStorageAreas() throws IOException, ServletExcept .thenReturn("https://localhost/other/file"); filter.doFilter(request, response, chain); verify(responseWriter).print(error.capture()); - assertThat(error.getValue(), is("Local copy across storage areas is not supported")); + assertThat( + error.getValue(), + is("Local copy across storage areas with Destination header is not supported")); verifyNoInteractions(chain); } @@ -99,7 +101,8 @@ void filterIgnoresLocalCopyInSameStorageArea() throws IOException, ServletExcept } @Test - void filterHandlesLocalCopyWithTransferHeader() throws IOException, ServletException { + void filterIgnoresLocalCopyWithOnlyTransferHeaderAuthorization() + throws IOException, ServletException { when(request.getMethod()).thenReturn(WebDAVMethod.COPY.toString()); when(request.getHeader(TransferConstants.DESTINATION_HEADER)) .thenReturn("https://localhost/test/otherfile"); @@ -107,6 +110,23 @@ void filterHandlesLocalCopyWithTransferHeader() throws IOException, ServletExcep when(requestHeaderNames.nextElement()) .thenReturn(TransferConstants.DESTINATION_HEADER, TRANSFER_HEADER_AUTHORIZATION_KEY); + filter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + } + + @Test + void filterHandlesLocalCopyWithTransferHeadersOtherThanAuthorization() + throws IOException, ServletException { + when(request.getMethod()).thenReturn(WebDAVMethod.COPY.toString()); + when(request.getHeader(TransferConstants.DESTINATION_HEADER)) + .thenReturn("https://localhost/test/otherfile"); + when(requestHeaderNames.hasMoreElements()).thenReturn(true, true, true, false); + when(requestHeaderNames.nextElement()) + .thenReturn( + TransferConstants.DESTINATION_HEADER, + TRANSFER_HEADER_AUTHORIZATION_KEY, + TRANSFER_HEADER_WHATEVER_KEY); + filter.doFilter(request, response, chain); verifyNoInteractions(chain); }