diff --git a/strongbox-web-core/src/main/java/org/carlspring/strongbox/controllers/configuration/StoragesConfigurationController.java b/strongbox-web-core/src/main/java/org/carlspring/strongbox/controllers/configuration/StoragesConfigurationController.java index 3d566db302..96714a7b77 100644 --- a/strongbox-web-core/src/main/java/org/carlspring/strongbox/controllers/configuration/StoragesConfigurationController.java +++ b/strongbox-web-core/src/main/java/org/carlspring/strongbox/controllers/configuration/StoragesConfigurationController.java @@ -305,7 +305,7 @@ public ResponseEntity getRepositoryResponseEntity(@RepositoryMapping Repository @PreAuthorize("hasAuthority('CONFIGURATION_DELETE_REPOSITORY')") @DeleteMapping(value = "/{storageId}/{repositoryId}", produces = { MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE }) - public ResponseEntity removeRepository(@RepositoryMapping Repository repository, + public ResponseEntity removeRepository(@RepositoryMapping(allowOutOfServiceRepository = true) Repository repository, @ApiParam(value = "Whether to force delete the repository from the file system") @RequestParam(name = "force", defaultValue = "false") final boolean force, @RequestHeader(HttpHeaders.ACCEPT) String accept) diff --git a/strongbox-web-core/src/main/java/org/carlspring/strongbox/web/RepositoryMapping.java b/strongbox-web-core/src/main/java/org/carlspring/strongbox/web/RepositoryMapping.java index cacaf26032..365644a6bf 100644 --- a/strongbox-web-core/src/main/java/org/carlspring/strongbox/web/RepositoryMapping.java +++ b/strongbox-web-core/src/main/java/org/carlspring/strongbox/web/RepositoryMapping.java @@ -23,5 +23,5 @@ String repositoryVariableName() default "repositoryId"; - boolean inServiceRepository() default true; + boolean allowOutOfServiceRepository() default false; } diff --git a/strongbox-web-core/src/main/java/org/carlspring/strongbox/web/RepositoryMethodArgumentResolver.java b/strongbox-web-core/src/main/java/org/carlspring/strongbox/web/RepositoryMethodArgumentResolver.java index a66a058a5b..574a437419 100644 --- a/strongbox-web-core/src/main/java/org/carlspring/strongbox/web/RepositoryMethodArgumentResolver.java +++ b/strongbox-web-core/src/main/java/org/carlspring/strongbox/web/RepositoryMethodArgumentResolver.java @@ -32,9 +32,7 @@ public class RepositoryMethodArgumentResolver { public static final String NOT_FOUND_STORAGE_MESSAGE = "Could not find requested storage %s."; - public static final String NOT_FOUND_REPOSITORY_MESSAGE = "Could not find requested repository %s:%s."; - public static final String NOT_IN_SERVICE_REPOSITORY_MESSAGE = "Requested repository %s:%s is out of service."; @Inject @@ -58,7 +56,7 @@ public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer modelAndViewContainer, final NativeWebRequest nativeWebRequest, final WebDataBinderFactory webDataBinderFactory) - throws MissingPathVariableException + throws MissingPathVariableException { final RepositoryMapping repositoryMapping = parameter.getParameterAnnotation(RepositoryMapping.class); final String storageVariableName = repositoryMapping.storageVariableName(); @@ -71,7 +69,7 @@ public Object resolveArgument(final MethodParameter parameter, RequestAttributes.SCOPE_REQUEST); if (repository != null && Objects.equals(repository.getId(), repositoryId) && - Objects.equals(repository.getStorage().getId(), storageId)) + Objects.equals(repository.getStorage().getId(), storageId)) { return repository; } @@ -90,8 +88,10 @@ public Object resolveArgument(final MethodParameter parameter, throw new RepositoryNotFoundException(message); } - final boolean inService = repositoryMapping.inServiceRepository(); - if (!inService) + // This annotation is used in a lot of controllers - some of which are related to the configuration management. + // It is necessary to allow requests to pass when the repository status is `out of service` (i.e. `/api/configuration/**`), + // but still return `ServiceUnavailableException` when people are accessing `/storages/**`. + if (!repository.isInService() && !repositoryMapping.allowOutOfServiceRepository()) { final String message = String.format(NOT_IN_SERVICE_REPOSITORY_MESSAGE, storageId, repositoryId); throw new ServiceUnavailableException(message); @@ -103,12 +103,12 @@ public Object resolveArgument(final MethodParameter parameter, private String getRequiredPathVariable(final MethodParameter parameter, final NativeWebRequest nativeWebRequest, final String variableName) - throws MissingPathVariableException + throws MissingPathVariableException { // Check @PathVariable parameter. @SuppressWarnings("unchecked") - final Map uriTemplateVars = (Map) nativeWebRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, - RequestAttributes.SCOPE_REQUEST); + final Map uriTemplateVars = (Map) nativeWebRequest.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (MapUtils.isNotEmpty(uriTemplateVars)) { final String pathVariable = uriTemplateVars.get(variableName); @@ -149,4 +149,4 @@ private Repository getRepository(final String storageId, return storage.getRepository(repositoryId); } -} +} \ No newline at end of file diff --git a/strongbox-web-core/src/test/java/org/carlspring/strongbox/web/RepositoryMethodArgumentResolverTest.java b/strongbox-web-core/src/test/java/org/carlspring/strongbox/web/RepositoryMethodArgumentResolverTest.java index 0eeb73fac8..318bb39282 100644 --- a/strongbox-web-core/src/test/java/org/carlspring/strongbox/web/RepositoryMethodArgumentResolverTest.java +++ b/strongbox-web-core/src/test/java/org/carlspring/strongbox/web/RepositoryMethodArgumentResolverTest.java @@ -15,6 +15,7 @@ import org.carlspring.strongbox.storage.repository.Repository; import org.carlspring.strongbox.storage.repository.RepositoryData; import org.carlspring.strongbox.storage.repository.RepositoryDto; +import org.carlspring.strongbox.storage.repository.RepositoryStatusEnum; import javax.inject.Inject; @@ -75,9 +76,12 @@ public void testSupportsParameter() boolean supportsParameterTrueCase1 = repositoryMethodArgumentResolver.supportsParameter(getMethodParameter("uploadArtifactWithRepositoryMapping", Repository.class)); assertTrue(supportsParameterTrueCase1); - boolean supportsParameterTrueCase2 = repositoryMethodArgumentResolver.supportsParameter(getMethodParameter("uploadArtifactForRepositoryNotInService", + boolean supportsParameterTrueCase2 = repositoryMethodArgumentResolver.supportsParameter(getMethodParameter("uploadArtifactWithOutOfServiceRepositoryAllowed", Repository.class)); assertTrue(supportsParameterTrueCase2); + boolean supportsParameterTrueCase3 = repositoryMethodArgumentResolver.supportsParameter(getMethodParameter("uploadArtifactWithOutOfServiceRepositoryNotAllowed", + Repository.class)); + assertTrue(supportsParameterTrueCase3); boolean supportsParameterFalseCase1 = repositoryMethodArgumentResolver.supportsParameter(getMethodParameter("uploadArtifactWithoutRepositoryMapping", Repository.class)); assertFalse(supportsParameterFalseCase1); @@ -91,27 +95,57 @@ public void testResolveArgument() throws MissingPathVariableException { - mockStoragesAndRepositories(); - Map uriTemplateVars = new HashMap<>(); uriTemplateVars.put("storageId", "storage-pypi"); uriTemplateVars.put("repositoryId", "pypi-releases"); nativeWebRequest.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars, RequestAttributes.SCOPE_REQUEST); - MethodParameter uploadArtifactWithRepositoryMappingMethod = getMethodParameter("uploadArtifactWithRepositoryMapping", - Repository.class); - Repository repository = (Repository) repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithRepositoryMappingMethod, + mockStoragesAndRepositories(RepositoryStatusEnum.OUT_OF_SERVICE); + + MethodParameter uploadArtifactWithOutOfServiceRepositoryAllowedMethod = getMethodParameter("uploadArtifactWithOutOfServiceRepositoryAllowed", + Repository.class); + Repository repository = (Repository) repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithOutOfServiceRepositoryAllowedMethod, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); assertNotNull(repository); + mockStoragesAndRepositories(RepositoryStatusEnum.IN_SERVICE); + repository = (Repository) repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithOutOfServiceRepositoryAllowedMethod, + modelAndViewContainer, + nativeWebRequest, + webDataBinderFactory); + assertNotNull(repository); + + mockStoragesAndRepositories(RepositoryStatusEnum.IN_SERVICE); + MethodParameter uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod = getMethodParameter("uploadArtifactWithOutOfServiceRepositoryNotAllowed", + Repository.class); + repository = (Repository) repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod, + modelAndViewContainer, + nativeWebRequest, + webDataBinderFactory); + assertNotNull(repository); + + } + + @Test + public void testResolveArgumentThrowingServiceUnavailableException() + throws MissingPathVariableException + { + Map uriTemplateVars = new HashMap<>(); + uriTemplateVars.put("storageId", "storage-pypi"); + uriTemplateVars.put("repositoryId", "pypi-releases"); + nativeWebRequest.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars, + RequestAttributes.SCOPE_REQUEST); + + mockStoragesAndRepositories(RepositoryStatusEnum.OUT_OF_SERVICE); + // ServiceUnavailableException Test Case - MethodParameter uploadArtifactForRepositoryNotInServiceMethod = getMethodParameter("uploadArtifactForRepositoryNotInService", - Repository.class); + MethodParameter uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod = getMethodParameter("uploadArtifactWithOutOfServiceRepositoryNotAllowed", + Repository.class); ServiceUnavailableException serviceUnavailableException = Assertions.assertThrows(ServiceUnavailableException.class, - () -> repositoryMethodArgumentResolver.resolveArgument(uploadArtifactForRepositoryNotInServiceMethod, + () -> repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod, modelAndViewContainer, nativeWebRequest, webDataBinderFactory)); @@ -119,13 +153,25 @@ public void testResolveArgument() uriTemplateVars.get("storageId"), uriTemplateVars.get("repositoryId")), serviceUnavailableException.getMessage()); - // StorageNotFoundException Test Case + } + + @Test + public void testResolveArgumentThrowingStorageNotFoundException() + throws MissingPathVariableException + { + Map uriTemplateVars = new HashMap<>(); uriTemplateVars.put("storageId", "storage-nuget"); uriTemplateVars.put("repositoryId", "pypi-releases"); nativeWebRequest.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars, RequestAttributes.SCOPE_REQUEST); + + mockStoragesAndRepositories(RepositoryStatusEnum.IN_SERVICE); + + // StorageNotFoundException Test Case + MethodParameter uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod = getMethodParameter("uploadArtifactWithOutOfServiceRepositoryNotAllowed", + Repository.class); StorageNotFoundException storageNotFoundException = Assertions.assertThrows(StorageNotFoundException.class, - () -> repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithRepositoryMappingMethod, + () -> repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod, modelAndViewContainer, nativeWebRequest, webDataBinderFactory)); @@ -133,13 +179,34 @@ public void testResolveArgument() uriTemplateVars.get("storageId")), storageNotFoundException.getMessage()); - // RepositoryNotFoundException Test Case + mockStoragesAndRepositories(RepositoryStatusEnum.OUT_OF_SERVICE); + storageNotFoundException = Assertions.assertThrows(StorageNotFoundException.class, + () -> repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod, + modelAndViewContainer, + nativeWebRequest, + webDataBinderFactory)); + assertEquals(String.format(RepositoryMethodArgumentResolver.NOT_FOUND_STORAGE_MESSAGE, + uriTemplateVars.get("storageId")), + storageNotFoundException.getMessage()); + } + + @Test + public void testResolveArgumentThrowingRepositoryNotFoundException() + throws MissingPathVariableException + { + Map uriTemplateVars = new HashMap<>(); uriTemplateVars.put("storageId", "storage-pypi"); uriTemplateVars.put("repositoryId", "maven-releases"); nativeWebRequest.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars, RequestAttributes.SCOPE_REQUEST); + + mockStoragesAndRepositories(RepositoryStatusEnum.IN_SERVICE); + + // RepositoryNotFoundException Test Case + MethodParameter uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod = getMethodParameter("uploadArtifactWithOutOfServiceRepositoryNotAllowed", + Repository.class); RepositoryNotFoundException repositoryNotFoundException = Assertions.assertThrows(RepositoryNotFoundException.class, - () -> repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithRepositoryMappingMethod, + () -> repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod, modelAndViewContainer, nativeWebRequest, webDataBinderFactory)); @@ -147,15 +214,25 @@ public void testResolveArgument() uriTemplateVars.get("storageId"), uriTemplateVars.get("repositoryId")), repositoryNotFoundException.getMessage()); + mockStoragesAndRepositories(RepositoryStatusEnum.OUT_OF_SERVICE); + repositoryNotFoundException = Assertions.assertThrows(RepositoryNotFoundException.class, + () -> repositoryMethodArgumentResolver.resolveArgument(uploadArtifactWithOutOfServiceRepositoryNotAllowedMethod, + modelAndViewContainer, + nativeWebRequest, + webDataBinderFactory)); + assertEquals(String.format(RepositoryMethodArgumentResolver.NOT_FOUND_REPOSITORY_MESSAGE, + uriTemplateVars.get("storageId"), uriTemplateVars.get("repositoryId")), + repositoryNotFoundException.getMessage()); } - private void mockStoragesAndRepositories() + private void mockStoragesAndRepositories(RepositoryStatusEnum repositoryStatus) { Map repositories = new HashMap<>(); Map storages = new HashMap<>(); RepositoryDto repositoryDto = new RepositoryDto(); repositoryDto.setId("pypi-releases"); + repositoryDto.setStatus(repositoryStatus.getStatus()); repositories.put(repositoryDto.getId(), repositoryDto); StorageDto storageDto = new StorageDto(); @@ -198,8 +275,13 @@ void uploadArtifactWithRepositoryMappingAndInvalidRepository(@RepositoryMapping { } - @PutMapping(path = "/{storageId}/{repositoryId}/uploadArtifactForRepositoryNotInService") - void uploadArtifactForRepositoryNotInService(@RepositoryMapping(inServiceRepository = false) Repository repository) + @PutMapping(path = "/{storageId}/{repositoryId}/uploadArtifactWithOutOfServiceRepositoryAllowed") + void uploadArtifactWithOutOfServiceRepositoryAllowed(@RepositoryMapping(allowOutOfServiceRepository = true) Repository repository) + { + } + + @PutMapping(path = "/{storageId}/{repositoryId}/uploadArtifactWithOutOfServiceRepositoryNotAllowed") + void uploadArtifactWithOutOfServiceRepositoryNotAllowed(@RepositoryMapping(allowOutOfServiceRepository = false) Repository repository) { }