From e179b00ecdfbcd3c0c01aacc9292149e3870bdcc Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 11 Mar 2019 10:53:49 +0100 Subject: [PATCH 01/21] adds namespace strategies for current year, repository type, username and custom --- .../sonia/scm/config/ScmConfiguration.java | 2 +- .../CurrentYearNamespaceStrategy.java | 29 +++++++++++++ .../repository/CustomNamespaceStrategy.java | 20 +++++++++ .../repository/DefaultNamespaceStrategy.java | 24 ----------- .../RepositoryTypeNamespaceStrategy.java | 11 +++++ .../repository/UsernameNamespaceStrategy.java | 13 ++++++ .../CurrentYearNamespaceStrategyTest.java | 39 +++++++++++++++++ .../CustomNamespaceStrategyTest.java | 28 +++++++++++++ .../DefaultNamespaceStrategyTest.java | 32 -------------- .../RepositoryTypeNamespaceStrategyTest.java | 20 +++++++++ .../UsernameNamespaceStrategyTest.java | 42 +++++++++++++++++++ 11 files changed, 203 insertions(+), 57 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/CurrentYearNamespaceStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/CustomNamespaceStrategy.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/repository/DefaultNamespaceStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/RepositoryTypeNamespaceStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/UsernameNamespaceStrategy.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/CurrentYearNamespaceStrategyTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/CustomNamespaceStrategyTest.java delete mode 100644 scm-webapp/src/test/java/sonia/scm/repository/DefaultNamespaceStrategyTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/RepositoryTypeNamespaceStrategyTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/UsernameNamespaceStrategyTest.java diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index e94fabfa60..3fcea05d23 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -185,7 +185,7 @@ public class ScmConfiguration implements Configuration { private boolean enabledXsrfProtection = true; @XmlElement(name = "default-namespace-strategy") - private String defaultNamespaceStrategy = "sonia.scm.repository.DefaultNamespaceStrategy"; + private String defaultNamespaceStrategy = "sonia.scm.repository.UsernameNamespaceStrategy"; /** diff --git a/scm-webapp/src/main/java/sonia/scm/repository/CurrentYearNamespaceStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/CurrentYearNamespaceStrategy.java new file mode 100644 index 0000000000..6c9be7c2ac --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/CurrentYearNamespaceStrategy.java @@ -0,0 +1,29 @@ +package sonia.scm.repository; + +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; +import java.time.Clock; +import java.time.Year; + +@Extension +public class CurrentYearNamespaceStrategy implements NamespaceStrategy { + + private final Clock clock; + + @Inject + public CurrentYearNamespaceStrategy() { + this(Clock.systemDefaultZone()); + } + + @VisibleForTesting + CurrentYearNamespaceStrategy(Clock clock) { + this.clock = clock; + } + + @Override + public String createNamespace(Repository repository) { + return String.valueOf(Year.now(clock).getValue()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/CustomNamespaceStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/CustomNamespaceStrategy.java new file mode 100644 index 0000000000..052465f9b7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/CustomNamespaceStrategy.java @@ -0,0 +1,20 @@ +package sonia.scm.repository; + +import sonia.scm.plugin.Extension; +import sonia.scm.util.ValidationUtil; + +import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; + +@Extension +public class CustomNamespaceStrategy implements NamespaceStrategy { + @Override + public String createNamespace(Repository repository) { + String namespace = repository.getNamespace(); + + doThrow() + .violation("invalid namespace", "namespace") + .when(!ValidationUtil.isRepositoryNameValid(namespace)); + + return namespace; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultNamespaceStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultNamespaceStrategy.java deleted file mode 100644 index 35a5abea24..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultNamespaceStrategy.java +++ /dev/null @@ -1,24 +0,0 @@ -package sonia.scm.repository; - -import com.google.common.base.Strings; -import org.apache.shiro.SecurityUtils; -import sonia.scm.plugin.Extension; - -/** - * The DefaultNamespaceStrategy returns the predefined namespace of the given repository, if the namespace was not set - * the username of the currently loggedin user is used. - * - * @since 2.0.0 - */ -@Extension -public class DefaultNamespaceStrategy implements NamespaceStrategy { - - @Override - public String createNamespace(Repository repository) { - String namespace = repository.getNamespace(); - if (Strings.isNullOrEmpty(namespace)) { - namespace = SecurityUtils.getSubject().getPrincipal().toString(); - } - return namespace; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryTypeNamespaceStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryTypeNamespaceStrategy.java new file mode 100644 index 0000000000..8643316230 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryTypeNamespaceStrategy.java @@ -0,0 +1,11 @@ +package sonia.scm.repository; + +import sonia.scm.plugin.Extension; + +@Extension +public class RepositoryTypeNamespaceStrategy implements NamespaceStrategy { + @Override + public String createNamespace(Repository repository) { + return repository.getType(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/UsernameNamespaceStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/UsernameNamespaceStrategy.java new file mode 100644 index 0000000000..9ddd9eab5d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/UsernameNamespaceStrategy.java @@ -0,0 +1,13 @@ +package sonia.scm.repository; + +import org.apache.shiro.SecurityUtils; +import sonia.scm.plugin.Extension; + +@Extension +public class UsernameNamespaceStrategy implements NamespaceStrategy { + + @Override + public String createNamespace(Repository repository) { + return SecurityUtils.getSubject().getPrincipal().toString(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/CurrentYearNamespaceStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/CurrentYearNamespaceStrategyTest.java new file mode 100644 index 0000000000..bc59940791 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/CurrentYearNamespaceStrategyTest.java @@ -0,0 +1,39 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CurrentYearNamespaceStrategyTest { + + @Mock + private Clock clock; + private NamespaceStrategy namespaceStrategy; + + @BeforeEach + void setupObjectUnderTest() { + namespaceStrategy = new CurrentYearNamespaceStrategy(clock); + } + + @Test + void shouldReturn1985() { + LocalDateTime dateTime = LocalDateTime.of(1985, 4, 9, 21, 42); + when(clock.instant()).thenReturn(dateTime.toInstant(ZoneOffset.UTC)); + when(clock.getZone()).thenReturn(ZoneId.systemDefault()); + + String namespace = namespaceStrategy.createNamespace(RepositoryTestData.createHeartOfGold()); + assertThat(namespace).isEqualTo("1985"); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/CustomNamespaceStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/CustomNamespaceStrategyTest.java new file mode 100644 index 0000000000..4c1cc1568c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/CustomNamespaceStrategyTest.java @@ -0,0 +1,28 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; +import sonia.scm.ScmConstraintViolationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CustomNamespaceStrategyTest { + + private final NamespaceStrategy namespaceStrategy = new CustomNamespaceStrategy(); + + @Test + void shouldReturnNamespaceFromRepository() { + Repository heartOfGold = RepositoryTestData.createHeartOfGold(); + assertThat(namespaceStrategy.createNamespace(heartOfGold)).isEqualTo(RepositoryTestData.NAMESPACE); + } + + @Test + void shouldThrowAnValidationExceptionForAnInvalidNamespace() { + Repository repository = new Repository(); + repository.setNamespace(".."); + repository.setName("."); + + assertThrows(ScmConstraintViolationException.class, () -> namespaceStrategy.createNamespace(repository)); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultNamespaceStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultNamespaceStrategyTest.java deleted file mode 100644 index 257ae5cff1..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultNamespaceStrategyTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package sonia.scm.repository; - -import com.github.sdorra.shiro.ShiroRule; -import com.github.sdorra.shiro.SubjectAware; -import org.junit.Rule; -import org.junit.Test; - -import static org.junit.Assert.*; - -@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") -public class DefaultNamespaceStrategyTest { - - @Rule - public ShiroRule shiroRule = new ShiroRule(); - - private DefaultNamespaceStrategy namespaceStrategy = new DefaultNamespaceStrategy(); - - @Test - @SubjectAware(username = "trillian", password = "secret") - public void testNamespaceStrategyWithoutPreset() { - assertEquals("trillian", namespaceStrategy.createNamespace(new Repository())); - } - - @Test - @SubjectAware(username = "trillian", password = "secret") - public void testNamespaceStrategyWithPreset() { - Repository repository = new Repository(); - repository.setNamespace("awesome"); - assertEquals("awesome", namespaceStrategy.createNamespace(repository)); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryTypeNamespaceStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryTypeNamespaceStrategyTest.java new file mode 100644 index 0000000000..b157a4369f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryTypeNamespaceStrategyTest.java @@ -0,0 +1,20 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class RepositoryTypeNamespaceStrategyTest { + + private final RepositoryTypeNamespaceStrategy namespaceStrategy = new RepositoryTypeNamespaceStrategy(); + + @Test + void shouldReturnTypeOfRepository() { + Repository git = RepositoryTestData.create42Puzzle("git"); + assertThat(namespaceStrategy.createNamespace(git)).isEqualTo("git"); + + Repository hg = RepositoryTestData.create42Puzzle("hg"); + assertThat(namespaceStrategy.createNamespace(hg)).isEqualTo("hg"); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/UsernameNamespaceStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/UsernameNamespaceStrategyTest.java new file mode 100644 index 0000000000..1f3b49aa68 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/UsernameNamespaceStrategyTest.java @@ -0,0 +1,42 @@ +package sonia.scm.repository; + + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UsernameNamespaceStrategyTest { + + @Mock + private Subject subject; + + private final NamespaceStrategy usernameNamespaceStrategy = new UsernameNamespaceStrategy(); + + @BeforeEach + void setupSubject() { + ThreadContext.bind(subject); + } + + @AfterEach + void clearThreadContext() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldReturnPrimaryPrincipal() { + when(subject.getPrincipal()).thenReturn("trillian"); + + String namespace = usernameNamespaceStrategy.createNamespace(RepositoryTestData.createHeartOfGold()); + assertThat(namespace).isEqualTo("trillian"); + } + +} From 3660ac9fd2e3ff637be007d0ae3f7491c10a302f Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 11 Mar 2019 13:26:04 +0100 Subject: [PATCH 02/21] adds rest endpoint for namespace strategies --- .../main/java/sonia/scm/web/VndMediaType.java | 2 + .../api/v2/resources/IndexDtoGenerator.java | 3 + .../v2/resources/NamespaceStrategiesDto.java | 20 +++++ .../resources/NamespaceStrategyResource.java | 68 +++++++++++++++++ .../scm/api/v2/resources/ResourceLinks.java | 17 +++++ .../NamespaceStrategyResourceTest.java | 74 +++++++++++++++++++ .../api/v2/resources/ResourceLinksMock.java | 2 + 7 files changed, 186 insertions(+) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/NamespaceStrategyResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 19859b876b..d0a64b17ec 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -46,6 +46,8 @@ public class VndMediaType { public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX; public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX; + public static final String NAMESPACE_STRATEGIES = PREFIX + "namespaceStrategies" + SUFFIX; + public static final String ME = PREFIX + "me" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX; public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 90445bcdc2..a3e7568957 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -59,6 +59,9 @@ public class IndexDtoGenerator extends HalAppenderMapper { builder.single(link("permissions", resourceLinks.permissions().self())); } builder.single(link("availableRepositoryPermissions", resourceLinks.availableRepositoryPermissions().self())); + + builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self())); + builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self())); } else { builder.single(link("login", resourceLinks.authentication().jsonLogin())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java new file mode 100644 index 0000000000..47d2a59837 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class NamespaceStrategiesDto extends HalRepresentation { + + private String current; + private List available; + + public NamespaceStrategiesDto(Links links) { + super(links); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java new file mode 100644 index 0000000000..145bc7100e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java @@ -0,0 +1,68 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import sonia.scm.repository.NamespaceStrategy; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.UriInfo; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * RESTFul WebService Endpoint for namespace strategies. + */ +@Path(NamespaceStrategyResource.PATH) +public class NamespaceStrategyResource { + + static final String PATH = "v2/namespaceStrategies"; + + private Set namespaceStrategies; + private Provider namespaceStrategyProvider; + + @Inject + public NamespaceStrategyResource(Set namespaceStrategies, Provider namespaceStrategyProvider) { + this.namespaceStrategies = namespaceStrategies; + this.namespaceStrategyProvider = namespaceStrategyProvider; + } + + /** + * Returns all available namespace strategies and the current selected. + * + * @param uriInfo uri info + * + * @return available and current namespace strategies + */ + @GET + @Path("") + @Produces(VndMediaType.NAMESPACE_STRATEGIES) + public NamespaceStrategiesDto get(@Context UriInfo uriInfo) { + NamespaceStrategiesDto dto = new NamespaceStrategiesDto(createLinks(uriInfo)); + + String currentStrategy = strategyAsString(namespaceStrategyProvider.get()); + dto.setCurrent(currentStrategy); + + List availableStrategies = collectStrategyNames(); + dto.setAvailable(availableStrategies); + + return dto; + } + + private Links createLinks(@Context UriInfo uriInfo) { + return Links.linkingTo().self(uriInfo.getAbsolutePath().toASCIIString()).build(); + } + + private String strategyAsString(NamespaceStrategy namespaceStrategy) { + return namespaceStrategy.getClass().getName(); + } + + private List collectStrategyNames() { + return namespaceStrategies.stream().map(this::strategyAsString).collect(Collectors.toList()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 644b3adc3f..1fc6b2a442 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -277,6 +277,23 @@ class ResourceLinks { } } + public NamespaceStrategiesLinks namespaceStrategies() { + return new NamespaceStrategiesLinks(scmPathInfoStore.get()); + } + + static class NamespaceStrategiesLinks { + + private final LinkBuilder namespaceStrategiesLinkBuilder; + + NamespaceStrategiesLinks(ScmPathInfo pathInfo) { + namespaceStrategiesLinkBuilder = new LinkBuilder(pathInfo, NamespaceStrategyResource.class); + } + + String self() { + return namespaceStrategiesLinkBuilder.method("get").parameters().href(); + } + } + public RepositoryTypeLinks repositoryType() { return new RepositoryTypeLinks(scmPathInfoStore.get()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NamespaceStrategyResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NamespaceStrategyResourceTest.java new file mode 100644 index 0000000000..a122fd1908 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NamespaceStrategyResourceTest.java @@ -0,0 +1,74 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.collect.Lists; +import com.google.inject.util.Providers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.NamespaceStrategy; +import sonia.scm.repository.Repository; + +import javax.inject.Provider; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class NamespaceStrategyResourceTest { + + @Mock + private UriInfo uriInfo; + + @Test + void shouldReturnNamespaceStrategies() { + when(uriInfo.getAbsolutePath()).thenReturn(URI.create("/namespace-strategies")); + + Set namespaceStrategies = allStrategies(); + Provider current = Providers.of(new MegaNamespaceStrategy()); + + NamespaceStrategyResource resource = new NamespaceStrategyResource(namespaceStrategies, current); + + NamespaceStrategiesDto dto = resource.get(uriInfo); + assertThat(dto.getCurrent()).isEqualTo(MegaNamespaceStrategy.class.getName()); + assertThat(dto.getAvailable()).contains( + AwesomeNamespaceStrategy.class.getName(), + SuperNamespaceStrategy.class.getName(), + MegaNamespaceStrategy.class.getName() + ); + assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/namespace-strategies"); + } + + private Set allStrategies() { + return strategies(new AwesomeNamespaceStrategy(), new SuperNamespaceStrategy(), new MegaNamespaceStrategy()); + } + + private Set strategies(NamespaceStrategy... strategies) { + return new LinkedHashSet<>(Lists.newArrayList(strategies)); + } + + private static class AwesomeNamespaceStrategy implements NamespaceStrategy { + @Override + public String createNamespace(Repository repository) { + return "awesome"; + } + } + + private static class SuperNamespaceStrategy implements NamespaceStrategy { + @Override + public String createNamespace(Repository repository) { + return "super"; + } + } + + private static class MegaNamespaceStrategy implements NamespaceStrategy { + @Override + public String createNamespace(Repository repository) { + return "mega"; + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 8714496e1a..073e41a65e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -43,6 +43,8 @@ public class ResourceLinksMock { when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo)); when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo)); + when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); + when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); return resourceLinks; } From d31ef73b886f28ace9f4b293cbb9b6fbdff14a78 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 11 Mar 2019 14:46:35 +0100 Subject: [PATCH 03/21] use simplename of namespace strategy, to simplify i18n --- .../sonia/scm/config/ScmConfiguration.java | 2 +- .../resources/NamespaceStrategyResource.java | 2 +- .../repository/NamespaceStrategyProvider.java | 10 ++- .../main/resources/locales/de/plugins.json | 6 ++ .../main/resources/locales/en/plugins.json | 6 ++ .../NamespaceStrategyResourceTest.java | 8 +-- .../NamespaceStrategyProviderTest.java | 69 +++++++++++++++++++ 7 files changed, 95 insertions(+), 8 deletions(-) rename {scm-core => scm-webapp}/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java (67%) create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 3fcea05d23..3cf7f7eddc 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -185,7 +185,7 @@ public class ScmConfiguration implements Configuration { private boolean enabledXsrfProtection = true; @XmlElement(name = "default-namespace-strategy") - private String defaultNamespaceStrategy = "sonia.scm.repository.UsernameNamespaceStrategy"; + private String defaultNamespaceStrategy = "UsernameNamespaceStrategy"; /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java index 145bc7100e..8368504546 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java @@ -59,7 +59,7 @@ public class NamespaceStrategyResource { } private String strategyAsString(NamespaceStrategy namespaceStrategy) { - return namespaceStrategy.getClass().getName(); + return namespaceStrategy.getClass().getSimpleName(); } private List collectStrategyNames() { diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java b/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java similarity index 67% rename from scm-core/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java rename to scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java index 49aefe71ac..95ae25e6d1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java @@ -1,5 +1,7 @@ package sonia.scm.repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.config.ScmConfiguration; import javax.inject.Inject; @@ -8,6 +10,8 @@ import java.util.Set; public class NamespaceStrategyProvider implements Provider { + private static final Logger LOG = LoggerFactory.getLogger(NamespaceStrategyProvider.class); + private final Set strategies; private final ScmConfiguration scmConfiguration; @@ -22,11 +26,13 @@ public class NamespaceStrategyProvider implements Provider { String namespaceStrategy = scmConfiguration.getDefaultNamespaceStrategy(); for (NamespaceStrategy s : this.strategies) { - if (s.getClass().getCanonicalName().equals(namespaceStrategy)) { + if (s.getClass().getSimpleName().equals(namespaceStrategy)) { return s; } } - return null; + + LOG.warn("could not find namespace strategy {}, using default strategy", namespaceStrategy); + return new UsernameNamespaceStrategy(); } } diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 0822be9cdd..adff12414a 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -143,5 +143,11 @@ "displayName": "Ungültige Eingabe", "description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut." } + }, + "namespaceStrategies": { + "sonia.scm.repository.UsernameNamespaceStrategy": "Benutzername", + "sonia.scm.repository.CustomNamespaceStrategy": "Benutzerdefiniert", + "sonia.scm.repository.CurrentYearNamespaceStrategy": "Aktuelles Jahr", + "sonia.scm.repository.RepositoryTypeNamespaceStrategy": "Repository Typ" } } diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index cc9902565b..ac7309a655 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -143,5 +143,11 @@ "displayName": "Illegal input", "description": "The values could not be validated. Please correct your input and try again." } + }, + "namespaceStrategies": { + "UsernameNamespaceStrategy": "Username", + "CustomNamespaceStrategy": "Custom", + "CurrentYearNamespaceStrategy": "Current year", + "RepositoryTypeNamespaceStrategy": "Repository type" } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NamespaceStrategyResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NamespaceStrategyResourceTest.java index a122fd1908..7ab98bb904 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NamespaceStrategyResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/NamespaceStrategyResourceTest.java @@ -34,11 +34,11 @@ class NamespaceStrategyResourceTest { NamespaceStrategyResource resource = new NamespaceStrategyResource(namespaceStrategies, current); NamespaceStrategiesDto dto = resource.get(uriInfo); - assertThat(dto.getCurrent()).isEqualTo(MegaNamespaceStrategy.class.getName()); + assertThat(dto.getCurrent()).isEqualTo(MegaNamespaceStrategy.class.getSimpleName()); assertThat(dto.getAvailable()).contains( - AwesomeNamespaceStrategy.class.getName(), - SuperNamespaceStrategy.class.getName(), - MegaNamespaceStrategy.class.getName() + AwesomeNamespaceStrategy.class.getSimpleName(), + SuperNamespaceStrategy.class.getSimpleName(), + MegaNamespaceStrategy.class.getSimpleName() ); assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/namespace-strategies"); } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java new file mode 100644 index 0000000000..607775994e --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java @@ -0,0 +1,69 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.Test; +import sonia.scm.config.ScmConfiguration; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class NamespaceStrategyProviderTest { + + @Test + void shouldReturnConfiguredStrategy() { + Set strategies = allStrategiesAsSet(); + + ScmConfiguration configuration = new ScmConfiguration(); + configuration.setDefaultNamespaceStrategy("Arthur"); + + NamespaceStrategyProvider provider = new NamespaceStrategyProvider(strategies, configuration); + NamespaceStrategy strategy = provider.get(); + + assertThat(strategy).isInstanceOf(Arthur.class); + } + + @Test + void shouldReturnUsernameStrategyForUnknown() { + Set strategies = Collections.emptySet(); + + ScmConfiguration configuration = new ScmConfiguration(); + configuration.setDefaultNamespaceStrategy("Arthur"); + + NamespaceStrategyProvider provider = new NamespaceStrategyProvider(strategies, configuration); + NamespaceStrategy strategy = provider.get(); + + assertThat(strategy).isInstanceOf(UsernameNamespaceStrategy.class); + } + + private LinkedHashSet allStrategiesAsSet() { + return new LinkedHashSet<>(Arrays.asList(new Trillian(), new Zaphod(), new Arthur())); + } + + private static class Trillian implements NamespaceStrategy{ + + @Override + public String createNamespace(Repository repository) { + return "trillian"; + } + } + + private static class Zaphod implements NamespaceStrategy { + + @Override + public String createNamespace(Repository repository) { + return "zaphod"; + } + } + + private static class Arthur implements NamespaceStrategy { + + @Override + public String createNamespace(Repository repository) { + return "arthur"; + } + } + +} From 3f6d1ed4fdf9c85d9cdbf6a64ff7152981ae2947 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 11 Mar 2019 14:47:42 +0100 Subject: [PATCH 04/21] use provider to reflect namespace strategy without restart --- .../sonia/scm/repository/DefaultRepositoryManager.java | 9 +++++---- .../scm/repository/DefaultRepositoryManagerPerfTest.java | 5 +++-- .../scm/repository/DefaultRepositoryManagerTest.java | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 4fd4682456..d5d71ce532 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -52,6 +52,7 @@ import sonia.scm.util.CollectionAppender; import sonia.scm.util.IOUtil; import sonia.scm.util.Util; +import javax.inject.Provider; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -84,7 +85,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { private final KeyGenerator keyGenerator; private final RepositoryDAO repositoryDAO; private final Set types; - private NamespaceStrategy namespaceStrategy; + private final Provider namespaceStrategyProvider; private final ManagerDaoAdapter managerDaoAdapter; @@ -92,11 +93,11 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { public DefaultRepositoryManager(ScmConfiguration configuration, SCMContextProvider contextProvider, KeyGenerator keyGenerator, RepositoryDAO repositoryDAO, Set handlerSet, - NamespaceStrategy namespaceStrategy) { + Provider namespaceStrategyProvider) { this.configuration = configuration; this.keyGenerator = keyGenerator; this.repositoryDAO = repositoryDAO; - this.namespaceStrategy = namespaceStrategy; + this.namespaceStrategyProvider = namespaceStrategyProvider; ThreadFactory factory = new ThreadFactoryBuilder() .setNameFormat(THREAD_NAME).build(); @@ -130,7 +131,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { public Repository create(Repository repository, boolean initRepository) { repository.setId(keyGenerator.createKey()); - repository.setNamespace(namespaceStrategy.createNamespace(repository)); + repository.setNamespace(namespaceStrategyProvider.get().createNamespace(repository)); logger.info("create repository {}/{} of type {} in namespace {}", repository.getNamespace(), repository.getName(), repository.getType(), repository.getNamespace()); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index f3ff9fd0f9..b94c483dd0 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -34,6 +34,7 @@ import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.inject.Provider; +import com.google.inject.util.Providers; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; @@ -117,8 +118,8 @@ public class DefaultRepositoryManagerPerfTest { contextProvider, keyGenerator, repositoryDAO, - handlerSet, - namespaceStrategy + handlerSet, + Providers.of(namespaceStrategy) ); setUpTestRepositories(); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index 416babfab0..0ad4ca0e9f 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -37,6 +37,7 @@ import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import com.google.inject.util.Providers; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.util.ThreadContext; import org.junit.Before; @@ -445,7 +446,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace); return new DefaultRepositoryManager(configuration, contextProvider, - keyGenerator, repositoryDAO, handlerSet, namespaceStrategy); + keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy)); } private void createRepository(RepositoryManager m, Repository repository) { From 158bb8bf8a16b65c794961c672efb9ca559fea89 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 11 Mar 2019 14:48:48 +0100 Subject: [PATCH 05/21] adds combobox to select namespace strategy --- .../ui-types/src/NamespaceStrategies.js | 9 + .../packages/ui-types/src/index.js | 2 + .../src/config/components/form/ConfigForm.js | 9 +- .../config/components/form/GeneralSettings.js | 29 +-- .../form/NamespaceStrategySelect.js | 55 ++++++ scm-ui/src/config/containers/GlobalConfig.js | 30 ++- .../src/config/modules/namespaceStrategies.js | 105 ++++++++++ .../modules/namespaceStrategies.test.js | 187 ++++++++++++++++++ scm-ui/src/createReduxStore.js | 4 +- 9 files changed, 411 insertions(+), 19 deletions(-) create mode 100644 scm-ui-components/packages/ui-types/src/NamespaceStrategies.js create mode 100644 scm-ui/src/config/components/form/NamespaceStrategySelect.js create mode 100644 scm-ui/src/config/modules/namespaceStrategies.js create mode 100644 scm-ui/src/config/modules/namespaceStrategies.test.js diff --git a/scm-ui-components/packages/ui-types/src/NamespaceStrategies.js b/scm-ui-components/packages/ui-types/src/NamespaceStrategies.js new file mode 100644 index 0000000000..ec53e6a7db --- /dev/null +++ b/scm-ui-components/packages/ui-types/src/NamespaceStrategies.js @@ -0,0 +1,9 @@ +// @flow + +import type { Links } from "./hal"; + +export type NamespaceStrategies = { + current: string, + available: string[], + _links: Links +}; diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index f7b375ac98..02e88f12e1 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -26,3 +26,5 @@ export type { SubRepository, File } from "./Sources"; export type { SelectValue, AutocompleteObject } from "./Autocomplete"; export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions"; + +export type { NamespaceStrategies } from "./NamespaceStrategies"; diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 7b650ccbfd..1c574a2912 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -2,6 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { SubmitButton, Notification } from "@scm-manager/ui-components"; +import type { NamespaceStrategies } from "@scm-manager/ui-types"; import type { Config } from "@scm-manager/ui-types"; import ProxySettings from "./ProxySettings"; import GeneralSettings from "./GeneralSettings"; @@ -13,9 +14,11 @@ type Props = { submitForm: Config => void, config?: Config, loading?: boolean, - t: string => string, configReadPermission: boolean, - configUpdatePermission: boolean + configUpdatePermission: boolean, + namespaceStrategies?: NamespaceStrategies, + // context props + t: string => string, }; type State = { @@ -88,6 +91,7 @@ class ConfigForm extends React.Component { const { loading, t, + namespaceStrategies, configReadPermission, configUpdatePermission } = this.props; @@ -118,6 +122,7 @@ class ConfigForm extends React.Component {
{noPermissionNotification} string, + namespaceStrategies?: NamespaceStrategies, onChange: (boolean, any, string) => void, - hasUpdatePermission: boolean + hasUpdatePermission: boolean, + // context props + t: string => string }; class GeneralSettings extends React.Component { + render() { const { t, @@ -31,7 +36,8 @@ class GeneralSettings extends React.Component { pluginUrl, enabledXsrfProtection, defaultNamespaceStrategy, - hasUpdatePermission + hasUpdatePermission, + namespaceStrategies } = this.props; return ( @@ -67,13 +73,14 @@ class GeneralSettings extends React.Component { />
- +
diff --git a/scm-ui/src/config/components/form/NamespaceStrategySelect.js b/scm-ui/src/config/components/form/NamespaceStrategySelect.js new file mode 100644 index 0000000000..a480dadc98 --- /dev/null +++ b/scm-ui/src/config/components/form/NamespaceStrategySelect.js @@ -0,0 +1,55 @@ +//@flow +import React from "react"; +import { translate, type TFunction } from "react-i18next"; +import { Select } from "@scm-manager/ui-components"; +import type { NamespaceStrategies } from "@scm-manager/ui-types"; + +type Props = { + namespaceStrategies: NamespaceStrategies, + label: string, + value?: string, + disabled?: boolean, + helpText?: string, + onChange: (value: string, name?: string) => void, + // context props + t: TFunction +}; + +class NamespaceStrategySelect extends React.Component { + createNamespaceOptions = () => { + const { namespaceStrategies, t } = this.props; + let available = []; + if (namespaceStrategies && namespaceStrategies.available) { + available = namespaceStrategies.available; + } + + return available.map(ns => { + const key = "namespaceStrategies." + ns; + let label = t("namespaceStrategies." + ns); + if (label === key) { + label = ns; + } + return { + value: ns, + label: label + }; + }); + }; + + render() { + const { label, value, helpText, disabled, onChange } = this.props; + const nsOptions = this.createNamespaceOptions(); + return ( + Date: Tue, 12 Mar 2019 15:10:05 +0100 Subject: [PATCH 11/21] align validation of repository name and namespace --- .../java/sonia/scm/repository/Repository.java | 10 ++- .../java/sonia/scm/util/ValidationUtil.java | 14 ++-- .../components/form/repositoryValidation.js | 4 +- .../form/repositoryValidation.test.js | 75 +++++++++++++++++++ .../scm/api/v2/resources/RepositoryDto.java | 4 +- 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 665f63487d..5bb50db06f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -248,7 +248,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per /** * Returns true if the {@link Repository} is valid. *
    - *
  • The name is not empty and contains only A-z, 0-9, _, -, /
  • + *
  • The namespace is valid
  • + *
  • The name is valid
  • *
  • The type is not empty
  • *
  • The contact is empty or contains a valid email address
  • *
@@ -257,9 +258,10 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per */ @Override public boolean isValid() { - return ValidationUtil.isRepositoryNameValid(name) && Util.isNotEmpty(type) - && ((Util.isEmpty(contact)) - || ValidationUtil.isMailAddressValid(contact)); + return ValidationUtil.isRepositoryNameValid(namespace) + && ValidationUtil.isRepositoryNameValid(name) + && Util.isNotEmpty(type) + && ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact)); } /** diff --git a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java index 2dbf55287d..e354b7efc6 100644 --- a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java @@ -35,14 +35,12 @@ package sonia.scm.util; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Splitter; - import sonia.scm.Validateable; -//~--- JDK imports ------------------------------------------------------------ - import java.util.regex.Pattern; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -58,10 +56,10 @@ public final class ValidationUtil private static final String REGEX_NAME = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$"; + public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_]*$"; + /** Field description */ - private static final Pattern REGEX_REPOSITORYNAME = Pattern.compile( - "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_]*$" - ); + private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME); //~--- constructors --------------------------------------------------------- @@ -151,7 +149,7 @@ public final class ValidationUtil * @return {@code true} if repository name is valid */ public static boolean isRepositoryNameValid(String name) { - return REGEX_REPOSITORYNAME.matcher(name).matches(); + return PATTERN_REPOSITORYNAME.matcher(name).matches(); } /** diff --git a/scm-ui/src/repos/components/form/repositoryValidation.js b/scm-ui/src/repos/components/form/repositoryValidation.js index c7f6fdb8b2..cdd180ad47 100644 --- a/scm-ui/src/repos/components/form/repositoryValidation.js +++ b/scm-ui/src/repos/components/form/repositoryValidation.js @@ -1,8 +1,10 @@ // @flow import { validation } from "@scm-manager/ui-components"; +const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[\\\[\]])^[A-z0-9\.][A-z0-9\.\-_]*$/; + export const isNameValid = (name: string) => { - return validation.isNameValid(name); + return nameRegex.test(name); }; export function isContactValid(mail: string) { diff --git a/scm-ui/src/repos/components/form/repositoryValidation.test.js b/scm-ui/src/repos/components/form/repositoryValidation.test.js index bcb29f3ef7..f0b524f2a9 100644 --- a/scm-ui/src/repos/components/form/repositoryValidation.test.js +++ b/scm-ui/src/repos/components/form/repositoryValidation.test.js @@ -11,6 +11,81 @@ describe("repository name validation", () => { expect(validator.isNameValid("scm/manager")).toBe(false); expect(validator.isNameValid("scm/ma/nager")).toBe(false); }); + + it("should allow same names as the backend", () => { + const validPaths = [ + "scm", + "s", + "sc", + ".hiddenrepo", + "b.", + "...", + "..c", + "d..", + "a..c" + ]; + + validPaths.forEach((path) => + expect(validator.isNameValid(path)).toBe(true) + ); + }); + + it("should deny same names as the backend", () => { + const invalidPaths = [ + ".", + "/", + "//", + "..", + "/.", + "/..", + "./", + "../", + "/../", + "/./", + "/...", + "/abc", + ".../", + "/sdf/", + "asdf/", + "./b", + "scm/plugins/.", + "scm/../plugins", + "scm/main/", + "/scm/main/", + "scm/./main", + "scm//main", + "scm\\main", + "scm/main-$HOME", + "scm/main-${HOME}-home", + "scm/main-%HOME-home", + "scm/main-%HOME%-home", + "abc$abc", + "abc%abc", + "abcabc", + "abc#abc", + "abc+abc", + "abc{abc", + "abc}abc", + "abc(abc", + "abc)abc", + "abc[abc", + "abc]abc", + "abc|abc", + "scm/main", + "scm/plugins/git-plugin", + ".scm/plugins", + "a/b..", + "a/..b", + "scm/main", + "scm/plugins/git-plugin", + "scm/plugins/git-plugin" + ]; + + invalidPaths.forEach((path) => + expect(validator.isNameValid(path)).toBe(false) + ); + }); }); describe("repository contact validation", () => { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index 8b48311bba..f3f28191ae 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -9,6 +9,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; +import sonia.scm.util.ValidationUtil; import javax.validation.constraints.Pattern; import java.time.Instant; @@ -25,8 +26,9 @@ public class RepositoryDto extends HalRepresentation { private List healthCheckFailures; @JsonInclude(JsonInclude.Include.NON_NULL) private Instant lastModified; + // we could not validate the namespace, this must be done by the namespace strategy private String namespace; - @Pattern(regexp = "^[A-z0-9\\-_]+$") + @Pattern(regexp = ValidationUtil.REGEX_REPOSITORYNAME) private String name; private boolean archived = false; @NotEmpty From cb554eb7bcc61026638e5fb1ef13bd7dd2b68f00 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 12 Mar 2019 15:54:34 +0100 Subject: [PATCH 12/21] rename defaultNamespaceStrategy to namespaceStrategy --- .../sonia/scm/config/ScmConfiguration.java | 14 ++++++------- .../packages/ui-types/src/Config.js | 2 +- scm-ui/public/locales/de/config.json | 4 ++-- scm-ui/public/locales/en/config.json | 4 ++-- .../src/config/components/form/ConfigForm.js | 7 +++---- .../config/components/form/GeneralSettings.js | 20 +++++++++---------- scm-ui/src/config/modules/config.test.js | 4 ++-- .../src/config/modules/namespaceStrategies.js | 2 +- .../modules/namespaceStrategies.test.js | 2 +- .../sonia/scm/api/v2/resources/ConfigDto.java | 2 +- .../repository/NamespaceStrategyProvider.java | 2 +- ...ConfigDtoToScmConfigurationMapperTest.java | 4 ++-- ...ScmConfigurationToConfigDtoMapperTest.java | 4 ++-- .../NamespaceStrategyProviderTest.java | 4 ++-- 14 files changed, 36 insertions(+), 39 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 3cf7f7eddc..5f4c7cf3d3 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -184,8 +184,8 @@ public class ScmConfiguration implements Configuration { @XmlElement(name = "xsrf-protection") private boolean enabledXsrfProtection = true; - @XmlElement(name = "default-namespace-strategy") - private String defaultNamespaceStrategy = "UsernameNamespaceStrategy"; + @XmlElement(name = "namespace-strategy") + private String namespaceStrategy = "UsernameNamespaceStrategy"; /** @@ -227,7 +227,7 @@ public class ScmConfiguration implements Configuration { this.loginAttemptLimit = other.loginAttemptLimit; this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; this.enabledXsrfProtection = other.enabledXsrfProtection; - this.defaultNamespaceStrategy = other.defaultNamespaceStrategy; + this.namespaceStrategy = other.namespaceStrategy; } public Set getAdminGroups() { @@ -366,8 +366,8 @@ public class ScmConfiguration implements Configuration { return loginAttemptLimit > 0; } - public String getDefaultNamespaceStrategy() { - return defaultNamespaceStrategy; + public String getNamespaceStrategy() { + return namespaceStrategy; } @@ -501,8 +501,8 @@ public class ScmConfiguration implements Configuration { this.enabledXsrfProtection = enabledXsrfProtection; } - public void setDefaultNamespaceStrategy(String defaultNamespaceStrategy) { - this.defaultNamespaceStrategy = defaultNamespaceStrategy; + public void setNamespaceStrategy(String namespaceStrategy) { + this.namespaceStrategy = namespaceStrategy; } @Override diff --git a/scm-ui-components/packages/ui-types/src/Config.js b/scm-ui-components/packages/ui-types/src/Config.js index 916cf9f509..4eee3e47d5 100644 --- a/scm-ui-components/packages/ui-types/src/Config.js +++ b/scm-ui-components/packages/ui-types/src/Config.js @@ -22,6 +22,6 @@ export type Config = { pluginUrl: string, loginAttemptLimitTimeout: number, enabledXsrfProtection: boolean, - defaultNamespaceStrategy: string, + namespaceStrategy: string, _links: Links }; diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index b67bf90262..fd19e83c65 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -57,7 +57,7 @@ "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "plugin-url": "Plugin URL", "enabled-xsrf-protection": "XSRF Protection aktivieren", - "default-namespace-strategy": "Default Namespace Strategie" + "namespace-strategy": "Namespace Strategie" }, "validation": { "date-format-invalid": "Das Datumsformat ist ungültig", @@ -87,6 +87,6 @@ "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", - "defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces." + "nameSpaceStrategyHelpText": "Strategie für Namespaces." } } diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 1b42878015..61670f0b2c 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -57,7 +57,7 @@ "skip-failed-authenticators": "Skip Failed Authenticators", "plugin-url": "Plugin URL", "enabled-xsrf-protection": "Enabled XSRF Protection", - "default-namespace-strategy": "Default Namespace Strategy" + "namespace-strategy": "Namespace Strategy" }, "validation": { "date-format-invalid": "The date format is not valid", @@ -87,6 +87,6 @@ "proxyUserHelpText": "The username for the proxy server authentication.", "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", - "defaultNameSpaceStrategyHelpText": "The default namespace strategy." + "nameSpaceStrategyHelpText": "The namespace strategy." } } diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 1c574a2912..1fda491c2e 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -2,8 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { SubmitButton, Notification } from "@scm-manager/ui-components"; -import type { NamespaceStrategies } from "@scm-manager/ui-types"; -import type { Config } from "@scm-manager/ui-types"; +import type { NamespaceStrategies, Config } from "@scm-manager/ui-types"; import ProxySettings from "./ProxySettings"; import GeneralSettings from "./GeneralSettings"; import BaseUrlSettings from "./BaseUrlSettings"; @@ -57,7 +56,7 @@ class ConfigForm extends React.Component { pluginUrl: "", loginAttemptLimitTimeout: 0, enabledXsrfProtection: true, - defaultNamespaceStrategy: "", + namespaceStrategy: "", _links: {} }, showNotification: false, @@ -131,7 +130,7 @@ class ConfigForm extends React.Component { skipFailedAuthenticators={config.skipFailedAuthenticators} pluginUrl={config.pluginUrl} enabledXsrfProtection={config.enabledXsrfProtection} - defaultNamespaceStrategy={config.defaultNamespaceStrategy} + namespaceStrategy={config.namespaceStrategy} onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js index 85c301286e..b221eb4d6a 100644 --- a/scm-ui/src/config/components/form/GeneralSettings.js +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -1,7 +1,7 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import { Checkbox, InputField, Select } from "@scm-manager/ui-components"; +import { Checkbox, InputField} from "@scm-manager/ui-components"; import type { NamespaceStrategies } from "@scm-manager/ui-types"; import NamespaceStrategySelect from "./NamespaceStrategySelect"; @@ -14,7 +14,7 @@ type Props = { skipFailedAuthenticators: boolean, pluginUrl: string, enabledXsrfProtection: boolean, - defaultNamespaceStrategy: string, + namespaceStrategy: string, namespaceStrategies?: NamespaceStrategies, onChange: (boolean, any, string) => void, hasUpdatePermission: boolean, @@ -35,7 +35,7 @@ class GeneralSettings extends React.Component { skipFailedAuthenticators, pluginUrl, enabledXsrfProtection, - defaultNamespaceStrategy, + namespaceStrategy, hasUpdatePermission, namespaceStrategies } = this.props; @@ -74,12 +74,12 @@ class GeneralSettings extends React.Component {
@@ -153,19 +153,17 @@ class GeneralSettings extends React.Component { handleAnonymousAccessEnabledChange = (value: string) => { this.props.onChange(true, value, "anonymousAccessEnabled"); }; - handleSkipFailedAuthenticatorsChange = (value: string) => { this.props.onChange(true, value, "skipFailedAuthenticators"); }; handlePluginUrlChange = (value: string) => { this.props.onChange(true, value, "pluginUrl"); }; - handleEnabledXsrfProtectionChange = (value: boolean) => { this.props.onChange(true, value, "enabledXsrfProtection"); }; - handleDefaultNamespaceStrategyChange = (value: string) => { - this.props.onChange(true, value, "defaultNamespaceStrategy"); + handleNamespaceStrategyChange = (value: string) => { + this.props.onChange(true, value, "namespaceStrategy"); }; } diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index b6c97826b0..23f58c61ac 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -50,7 +50,7 @@ const config = { "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false", loginAttemptLimitTimeout: 300, enabledXsrfProtection: true, - defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy", + namespaceStrategy: "UsernameNamespaceStrategy", _links: { self: { href: "http://localhost:8081/api/v2/config" }, update: { href: "http://localhost:8081/api/v2/config" } @@ -79,7 +79,7 @@ const configWithNullValues = { "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false", loginAttemptLimitTimeout: 300, enabledXsrfProtection: true, - defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy", + namespaceStrategy: "UsernameNamespaceStrategy", _links: { self: { href: "http://localhost:8081/api/v2/config" }, update: { href: "http://localhost:8081/api/v2/config" } diff --git a/scm-ui/src/config/modules/namespaceStrategies.js b/scm-ui/src/config/modules/namespaceStrategies.js index 8164bf8b32..a9d2ef0cf6 100644 --- a/scm-ui/src/config/modules/namespaceStrategies.js +++ b/scm-ui/src/config/modules/namespaceStrategies.js @@ -91,7 +91,7 @@ export default function reducer( const config = action.payload; return { ...state, - current: config.defaultNamespaceStrategy + current: config.namespaceStrategy }; } return state; diff --git a/scm-ui/src/config/modules/namespaceStrategies.test.js b/scm-ui/src/config/modules/namespaceStrategies.test.js index 8854a17795..91f511f81e 100644 --- a/scm-ui/src/config/modules/namespaceStrategies.test.js +++ b/scm-ui/src/config/modules/namespaceStrategies.test.js @@ -149,7 +149,7 @@ describe("namespace strategies reducer", () => { const modifyConfigAction = { type: MODIFY_CONFIG_SUCCESS, payload: { - defaultNamespaceStrategy: "CustomNamespaceStrategy" + namespaceStrategy: "CustomNamespaceStrategy" } }; const newState = reducer(strategies, modifyConfigAction); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index f77823eaac..410d64eeaf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -35,7 +35,7 @@ public class ConfigDto extends HalRepresentation { private String pluginUrl; private long loginAttemptLimitTimeout; private boolean enabledXsrfProtection; - private String defaultNamespaceStrategy; + private String namespaceStrategy; @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package diff --git a/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java b/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java index 95ae25e6d1..50b3cad7a5 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyProvider.java @@ -23,7 +23,7 @@ public class NamespaceStrategyProvider implements Provider { @Override public NamespaceStrategy get() { - String namespaceStrategy = scmConfiguration.getDefaultNamespaceStrategy(); + String namespaceStrategy = scmConfiguration.getNamespaceStrategy(); for (NamespaceStrategy s : this.strategies) { if (s.getClass().getSimpleName().equals(namespaceStrategy)) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java index 525d5814e1..e8d00a9363 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java @@ -51,7 +51,7 @@ public class ConfigDtoToScmConfigurationMapperTest { assertEquals("https://plug.ins" , config.getPluginUrl()); assertEquals(40 , config.getLoginAttemptLimitTimeout()); assertTrue(config.isEnabledXsrfProtection()); - assertEquals("username", config.getDefaultNamespaceStrategy()); + assertEquals("username", config.getNamespaceStrategy()); } private ConfigDto createDefaultDto() { @@ -76,7 +76,7 @@ public class ConfigDtoToScmConfigurationMapperTest { configDto.setPluginUrl("https://plug.ins"); configDto.setLoginAttemptLimitTimeout(40); configDto.setEnabledXsrfProtection(true); - configDto.setDefaultNamespaceStrategy("username"); + configDto.setNamespaceStrategy("username"); return configDto; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index cdbd9ae344..cd59ce4647 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -81,7 +81,7 @@ public class ScmConfigurationToConfigDtoMapperTest { assertEquals("pluginurl" , dto.getPluginUrl()); assertEquals(2 , dto.getLoginAttemptLimitTimeout()); assertTrue(dto.isEnabledXsrfProtection()); - assertEquals("username", dto.getDefaultNamespaceStrategy()); + assertEquals("username", dto.getNamespaceStrategy()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); @@ -121,7 +121,7 @@ public class ScmConfigurationToConfigDtoMapperTest { config.setPluginUrl("pluginurl"); config.setLoginAttemptLimitTimeout(2); config.setEnabledXsrfProtection(true); - config.setDefaultNamespaceStrategy("username"); + config.setNamespaceStrategy("username"); return config; } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java index 607775994e..63cff75411 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyProviderTest.java @@ -17,7 +17,7 @@ class NamespaceStrategyProviderTest { Set strategies = allStrategiesAsSet(); ScmConfiguration configuration = new ScmConfiguration(); - configuration.setDefaultNamespaceStrategy("Arthur"); + configuration.setNamespaceStrategy("Arthur"); NamespaceStrategyProvider provider = new NamespaceStrategyProvider(strategies, configuration); NamespaceStrategy strategy = provider.get(); @@ -30,7 +30,7 @@ class NamespaceStrategyProviderTest { Set strategies = Collections.emptySet(); ScmConfiguration configuration = new ScmConfiguration(); - configuration.setDefaultNamespaceStrategy("Arthur"); + configuration.setNamespaceStrategy("Arthur"); NamespaceStrategyProvider provider = new NamespaceStrategyProvider(strategies, configuration); NamespaceStrategy strategy = provider.get(); From 7d994c62a6612aba62b131ef78d93077286a2fe7 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 12 Mar 2019 16:40:08 +0100 Subject: [PATCH 13/21] validate namespace strategies on configuration update --- .../scm/api/v2/resources/ConfigResource.java | 10 +++++- .../NamespaceStrategyValidator.java | 26 +++++++++++++++ .../api/v2/resources/ConfigResourceTest.java | 24 +++++++++++++- .../NamespaceStrategyValidatorTest.java | 33 +++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyValidator.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyValidatorTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index c646dceab4..e5e754afea 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.NamespaceStrategyValidator; import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.web.VndMediaType; @@ -27,12 +28,16 @@ public class ConfigResource { private final ConfigDtoToScmConfigurationMapper dtoToConfigMapper; private final ScmConfigurationToConfigDtoMapper configToDtoMapper; private final ScmConfiguration configuration; + private final NamespaceStrategyValidator namespaceStrategyValidator; @Inject - public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper, ScmConfigurationToConfigDtoMapper configToDtoMapper, ScmConfiguration configuration) { + public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper, + ScmConfigurationToConfigDtoMapper configToDtoMapper, + ScmConfiguration configuration, NamespaceStrategyValidator namespaceStrategyValidator) { this.dtoToConfigMapper = dtoToConfigMapper; this.configToDtoMapper = configToDtoMapper; this.configuration = configuration; + this.namespaceStrategyValidator = namespaceStrategyValidator; } /** @@ -78,6 +83,9 @@ public class ConfigResource { // But to where to check? load() or store()? Leave it for now, SCMv1 legacy that can be cleaned up later. ConfigurationPermissions.write(configuration).check(); + // ensure the namespace strategy is valid + namespaceStrategyValidator.check(configDto.getNamespaceStrategy()); + ScmConfiguration config = dtoToConfigMapper.map(configDto); synchronized (ScmConfiguration.class) { configuration.load(config); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyValidator.java b/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyValidator.java new file mode 100644 index 0000000000..89ee0fa1bc --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/NamespaceStrategyValidator.java @@ -0,0 +1,26 @@ +package sonia.scm.repository; + +import javax.inject.Inject; +import java.util.Set; + +import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; + +public class NamespaceStrategyValidator { + + private final Set strategies; + + @Inject + public NamespaceStrategyValidator(Set strategies) { + this.strategies = strategies; + } + + public void check(String name) { + doThrow() + .violation("unknown NamespaceStrategy " + name, "namespaceStrategy") + .when(!isValid(name)); + } + + private boolean isValid(String name) { + return strategies.stream().anyMatch(ns -> ns.getClass().getSimpleName().equals(name)); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index 66689c6ac6..584ffaf4fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -13,7 +13,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.InjectMocks; +import org.mockito.Mock; import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.NamespaceStrategyValidator; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; @@ -22,10 +24,12 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; @SubjectAware( @@ -46,6 +50,9 @@ public class ConfigResourceTest { @SuppressWarnings("unused") // Is injected private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + @Mock + private NamespaceStrategyValidator namespaceStrategyValidator; + @InjectMocks private ConfigDtoToScmConfigurationMapperImpl dtoToConfigMapper; @InjectMocks @@ -62,7 +69,7 @@ public class ConfigResourceTest { public void prepareEnvironment() { initMocks(this); - ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration()); + ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator); dispatcher.getRegistry().addSingletonResource(configResource); } @@ -140,6 +147,21 @@ public class ConfigResourceTest { assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); } + + @Test + @SubjectAware(username = "readWrite") + public void shouldValidateNamespaceStrategy() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) + .contentType(VndMediaType.CONFIG) + .content("{ \"namespaceStrategy\": \"AwesomeStrategy\" }".getBytes(StandardCharsets.UTF_8)); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + verify(namespaceStrategyValidator).check("AwesomeStrategy"); + } + private MockHttpRequest post(String resourceName) throws IOException, URISyntaxException { URL url = Resources.getResource(resourceName); byte[] configJson = Resources.toByteArray(url); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyValidatorTest.java b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyValidatorTest.java new file mode 100644 index 0000000000..ec08710755 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/NamespaceStrategyValidatorTest.java @@ -0,0 +1,33 @@ +package sonia.scm.repository; + +import com.google.common.collect.Sets; +import org.junit.jupiter.api.Test; +import sonia.scm.ScmConstraintViolationException; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class NamespaceStrategyValidatorTest { + + @Test + void shouldThrowConstraintValidationException() { + NamespaceStrategyValidator validator = new NamespaceStrategyValidator(Collections.emptySet()); + assertThrows(ScmConstraintViolationException.class, () -> validator.check("AwesomeStrategy")); + } + + @Test + void shouldDoNotThrowAnException() { + NamespaceStrategyValidator validator = new NamespaceStrategyValidator(Sets.newHashSet(new AwesomeStrategy())); + validator.check("AwesomeStrategy"); + } + + public static class AwesomeStrategy implements NamespaceStrategy { + + @Override + public String createNamespace(Repository repository) { + return null; + } + } + +} From 38d480e2ea57aca7c0d5bfcd2c60c1784ca363d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 12 Mar 2019 17:07:14 +0100 Subject: [PATCH 14/21] Fix A-Za-z ranges --- scm-core/src/main/java/sonia/scm/util/ValidationUtil.java | 6 +++--- scm-ui/src/repos/components/form/repositoryValidation.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java index e354b7efc6..bc710d9a50 100644 --- a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java @@ -50,13 +50,13 @@ public final class ValidationUtil /** Field description */ private static final String REGEX_MAIL = - "^[A-z0-9][\\w.-]*@[A-z0-9][\\w\\-\\.]*\\.[A-z0-9][A-z0-9-]+$"; + "^[A-Za-z0-9][\\w.-]*@[A-Za-z0-9][\\w\\-\\.]*\\.[A-Za-z0-9][A-Za-z0-9-]+$"; /** Field description */ private static final String REGEX_NAME = - "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$"; + "^[A-Za-z0-9\\.\\-_@]|[^ ]([A-Za-z0-9\\.\\-_@ ]*[A-Za-z0-9\\.\\-_@]|[^ ])?$"; - public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_]*$"; + public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$"; /** Field description */ private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME); diff --git a/scm-ui/src/repos/components/form/repositoryValidation.js b/scm-ui/src/repos/components/form/repositoryValidation.js index cdd180ad47..4f216d1c6b 100644 --- a/scm-ui/src/repos/components/form/repositoryValidation.js +++ b/scm-ui/src/repos/components/form/repositoryValidation.js @@ -1,7 +1,7 @@ // @flow import { validation } from "@scm-manager/ui-components"; -const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[\\\[\]])^[A-z0-9\.][A-z0-9\.\-_]*$/; +const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[\\\[\]])^[A-Za-z0-9\.][A-Za-z0-9\.\-_]*$/; export const isNameValid = (name: string) => { return nameRegex.test(name); From 63e3a795b3b2a4d49e995b1d8d5c92c869001ca5 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 13 Mar 2019 09:36:42 +0100 Subject: [PATCH 15/21] fixes german locale for namespace strategies --- scm-webapp/src/main/resources/locales/de/plugins.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index d2a8dd93b4..a811a7c544 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -149,9 +149,9 @@ } }, "namespaceStrategies": { - "sonia.scm.repository.UsernameNamespaceStrategy": "Benutzername", - "sonia.scm.repository.CustomNamespaceStrategy": "Benutzerdefiniert", - "sonia.scm.repository.CurrentYearNamespaceStrategy": "Aktuelles Jahr", - "sonia.scm.repository.RepositoryTypeNamespaceStrategy": "Repository Typ" + "UsernameNamespaceStrategy": "Benutzername", + "CustomNamespaceStrategy": "Benutzerdefiniert", + "CurrentYearNamespaceStrategy": "Aktuelles Jahr", + "RepositoryTypeNamespaceStrategy": "Repository Typ" } } From 54170c04a7e5e53fb53239b33c5e039bc4974958 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 13 Mar 2019 09:38:27 +0100 Subject: [PATCH 16/21] removes unused setter from NamespaceStrategiesDto --- .../java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java index 47d2a59837..4c7806ce31 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java @@ -3,12 +3,10 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; -import lombok.Setter; import java.util.List; @Getter -@Setter public class NamespaceStrategiesDto extends HalRepresentation { private String current; From 3fa43f2775fe9e2da0ac0e8017d9208853ec849c Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 13 Mar 2019 09:38:56 +0100 Subject: [PATCH 17/21] use predefined key, instead of building it again --- scm-ui/src/config/components/form/NamespaceStrategySelect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/components/form/NamespaceStrategySelect.js b/scm-ui/src/config/components/form/NamespaceStrategySelect.js index f1197e15c0..71f54de247 100644 --- a/scm-ui/src/config/components/form/NamespaceStrategySelect.js +++ b/scm-ui/src/config/components/form/NamespaceStrategySelect.js @@ -25,7 +25,7 @@ class NamespaceStrategySelect extends React.Component { return available.map(ns => { const key = "namespaceStrategies." + ns; - let label = t("namespaceStrategies." + ns); + let label = t(key); if (label === key) { label = ns; } From 9d0e3e568aa5cef922ad8ea14aa1b2b39d68119d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 13 Mar 2019 09:55:43 +0100 Subject: [PATCH 18/21] Add missing translation --- scm-webapp/src/main/resources/locales/de/plugins.json | 1 + scm-webapp/src/main/resources/locales/en/plugins.json | 1 + 2 files changed, 2 insertions(+) diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 0a0719ec18..90f2e1f66b 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -103,6 +103,7 @@ "errorCode": "Fehlercode", "transactionId": "Transaktions-ID", "moreInfo": "Für mehr Informationen, siehe", + "violations": "Ungültige Werte:", "AGR7UzkhA1": { "displayName": "Nicht gefunden", "description": "Der gewünschte Datensatz konnte nicht gefunden werden. Möglicherweise wurde er in einer weiteren Session gelöscht." diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 86e8a40df1..fd2fb1eba5 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -103,6 +103,7 @@ "errorCode": "Error Code", "transactionId": "Transaction ID", "moreInfo": "For more information, see", + "violations": "Violations:", "AGR7UzkhA1": { "displayName": "Not found", "description": "The requested entity could not be found. It may have been deleted in another session." From be12167f0a3462edba5e45a0bd67d6d7915aff28 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 13 Mar 2019 10:00:47 +0100 Subject: [PATCH 19/21] validates namespace if the strategy is custom --- scm-ui/src/repos/components/form/RepositoryForm.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scm-ui/src/repos/components/form/RepositoryForm.js b/scm-ui/src/repos/components/form/RepositoryForm.js index de0bfe0c9a..a1cdf5a863 100644 --- a/scm-ui/src/repos/components/form/RepositoryForm.js +++ b/scm-ui/src/repos/components/form/RepositoryForm.js @@ -28,6 +28,8 @@ type State = { contactValidationError: boolean }; +const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy"; + class RepositoryForm extends React.Component { constructor(props: Props) { super(props); @@ -62,12 +64,14 @@ class RepositoryForm extends React.Component { } isValid = () => { - const repository = this.state.repository; + const { namespaceStrategy } = this.props; + const { repository } = this.state; return !( this.state.namespaceValidationError || this.state.nameValidationError || this.state.contactValidationError || - this.isFalsy(repository.name) + this.isFalsy(repository.name) || + (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY && this.isFalsy(repository.namespace)) ); }; @@ -143,7 +147,7 @@ class RepositoryForm extends React.Component { validationError: this.state.namespaceValidationError }; - if (namespaceStrategy === "CustomNamespaceStrategy") { + if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) { return ; } From dea57321416e1e737ed2b1679a679d7d640d0baf Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 13 Mar 2019 10:01:46 +0100 Subject: [PATCH 20/21] move dto parameters to constructor in order to create an immutable object --- .../sonia/scm/api/v2/resources/NamespaceStrategiesDto.java | 4 +++- .../scm/api/v2/resources/NamespaceStrategyResource.java | 7 +------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java index 4c7806ce31..f3c62fa0f9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategiesDto.java @@ -12,7 +12,9 @@ public class NamespaceStrategiesDto extends HalRepresentation { private String current; private List available; - public NamespaceStrategiesDto(Links links) { + public NamespaceStrategiesDto(String current, List available, Links links) { super(links); + this.current = current; + this.available = available; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java index 8368504546..7b87c612d7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceStrategyResource.java @@ -43,15 +43,10 @@ public class NamespaceStrategyResource { @Path("") @Produces(VndMediaType.NAMESPACE_STRATEGIES) public NamespaceStrategiesDto get(@Context UriInfo uriInfo) { - NamespaceStrategiesDto dto = new NamespaceStrategiesDto(createLinks(uriInfo)); - String currentStrategy = strategyAsString(namespaceStrategyProvider.get()); - dto.setCurrent(currentStrategy); - List availableStrategies = collectStrategyNames(); - dto.setAvailable(availableStrategies); - return dto; + return new NamespaceStrategiesDto(currentStrategy, availableStrategies, createLinks(uriInfo)); } private Links createLinks(@Context UriInfo uriInfo) { From 89fd1a5e4ae588a12078b2f068f118ef21eff617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 13 Mar 2019 11:20:36 +0000 Subject: [PATCH 21/21] Close branch feature/namespace_strategies